200 lines
9.5 KiB
Python
200 lines
9.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
from odoo import models, fields, api, _
|
|
|
|
class GarmentBom(models.Model):
|
|
_name = 'garment.bom'
|
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
|
_description = 'Garment Bill of Materials (BOM)'
|
|
_order = 'name desc'
|
|
|
|
name = fields.Char(string='BOM Reference', required=True, default=lambda self: _('New'), copy=False)
|
|
style_id = fields.Many2one('garment.style', string='Garment Style', required=True, tracking=True)
|
|
base_qty = fields.Float(string='Base Quantity', default=1.0, required=True, help='Number of garment pieces this BOM defines')
|
|
uom_id = fields.Many2one('uom.uom', string='UoM', related='style_id.uom_id', readonly=True)
|
|
active = fields.Boolean(default=True)
|
|
company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company)
|
|
notes = fields.Text(string='Notes / Engineering Specs')
|
|
|
|
# BOM Lines
|
|
fabric_line_ids = fields.One2many('garment.bom.fabric', 'bom_id', string='Fabric Components')
|
|
trim_line_ids = fields.One2many('garment.bom.trim', 'bom_id', string='Trims & Accessories')
|
|
operation_line_ids = fields.One2many('garment.bom.operation', 'bom_id', string='Operations & Routing')
|
|
|
|
# Cost Summary Computes
|
|
total_fabric_cost = fields.Float(string='Total Fabric Cost', compute='_compute_totals', store=True)
|
|
total_trim_cost = fields.Float(string='Total Trims Cost', compute='_compute_totals', store=True)
|
|
total_operation_cost = fields.Float(string='Total Labor / CM Cost', compute='_compute_totals', store=True)
|
|
total_unit_cost = fields.Float(string='Estimated Garment Cost (per piece)', compute='_compute_totals', store=True)
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
for vals in vals_list:
|
|
if vals.get('name', _('New')) == _('New'):
|
|
vals['name'] = self.env['ir.sequence'].next_by_code('garment.bom') or _('BOM-%s') % fields.Date.today().strftime('%Y%m%d')
|
|
return super().create(vals_list)
|
|
|
|
@api.depends('fabric_line_ids.subtotal_cost', 'trim_line_ids.subtotal_cost', 'operation_line_ids.subtotal_cost', 'base_qty')
|
|
def _compute_totals(self):
|
|
for bom in self:
|
|
fab_sum = sum(bom.fabric_line_ids.mapped('subtotal_cost'))
|
|
trim_sum = sum(bom.trim_line_ids.mapped('subtotal_cost'))
|
|
op_sum = sum(bom.operation_line_ids.mapped('subtotal_cost'))
|
|
bom.total_fabric_cost = fab_sum
|
|
bom.total_trim_cost = trim_sum
|
|
bom.total_operation_cost = op_sum
|
|
qty = bom.base_qty or 1.0
|
|
bom.total_unit_cost = (fab_sum + trim_sum + op_sum) / qty
|
|
|
|
def calculate_requirement_for_qty(self, order_qty):
|
|
"""Calculates material requirements for an arbitrary production quantity"""
|
|
self.ensure_one()
|
|
factor = order_qty / (self.base_qty or 1.0)
|
|
fabric_req = []
|
|
for fab in self.fabric_line_ids:
|
|
net_kg = fab.consumption_kg * factor
|
|
gross_kg = net_kg * (1.0 + (fab.wastage_pct / 100.0))
|
|
fabric_req.append({
|
|
'component': fab.component_type,
|
|
'fabric_type': fab.fabric_type_id.name if fab.fabric_type_id else 'Fabric',
|
|
'net_kg': round(net_kg, 2),
|
|
'wastage_pct': fab.wastage_pct,
|
|
'gross_kg': round(gross_kg, 2),
|
|
'cost': round(gross_kg * fab.cost_per_kg, 2),
|
|
})
|
|
|
|
trims_req = []
|
|
for trim in self.trim_line_ids:
|
|
net_units = trim.consumption_qty * factor
|
|
gross_units = net_units * (1.0 + (trim.wastage_pct / 100.0))
|
|
trims_req.append({
|
|
'item': trim.item_name or trim.product_id.name,
|
|
'item_type': trim.trim_type,
|
|
'net_units': round(net_units, 2),
|
|
'gross_units': round(gross_units, 2),
|
|
'cost': round(gross_units * trim.unit_cost, 2),
|
|
})
|
|
return {
|
|
'order_qty': order_qty,
|
|
'fabrics': fabric_req,
|
|
'trims': trims_req,
|
|
}
|
|
|
|
|
|
class GarmentBomFabric(models.Model):
|
|
_name = 'garment.bom.fabric'
|
|
_description = 'Garment BOM Fabric Component'
|
|
|
|
bom_id = fields.Many2one('garment.bom', string='BOM', required=True, ondelete='cascade')
|
|
component_type = fields.Selection([
|
|
('body', 'Body Panels (Front & Back)'),
|
|
('neck_rib', 'Neck Rib / Collar Band'),
|
|
('collar', 'Knit / Flat Collar'),
|
|
('cuff', 'Sleeve Cuffs'),
|
|
('pocket', 'Pocket Panels'),
|
|
('placket', 'Placket Facing'),
|
|
('hood', 'Hood Panels'),
|
|
('lining', 'Lining Fabric'),
|
|
('other', 'Other Fabric Part'),
|
|
], string='Component Part', default='body', required=True)
|
|
fabric_type_id = fields.Many2one('garment.fabric.type', string='Fabric Type')
|
|
product_id = fields.Many2one('product.product', string='Raw Fabric Product')
|
|
gsm = fields.Integer(string='GSM', default=180)
|
|
width = fields.Float(string='Width (Inches)', default=30.0)
|
|
|
|
# Consumption per garment (Kg)
|
|
consumption_kg = fields.Float(string='Net Consumption (Kg/Pc)', required=True, default=0.180, digits=(16, 4))
|
|
wastage_pct = fields.Float(string='Cutting Wastage %', default=5.0)
|
|
gross_consumption_kg = fields.Float(string='Gross Consumption (Kg/Pc)', compute='_compute_gross_kg', store=True, digits=(16, 4))
|
|
cost_per_kg = fields.Float(string='Fabric Cost / Kg', default=320.0)
|
|
subtotal_cost = fields.Float(string='Fabric Cost / Pc', compute='_compute_subtotal_cost', store=True)
|
|
|
|
@api.depends('consumption_kg', 'wastage_pct')
|
|
def _compute_gross_kg(self):
|
|
for rec in self:
|
|
rec.gross_consumption_kg = rec.consumption_kg * (1.0 + (rec.wastage_pct / 100.0))
|
|
|
|
@api.depends('gross_consumption_kg', 'cost_per_kg')
|
|
def _compute_subtotal_cost(self):
|
|
for rec in self:
|
|
rec.subtotal_cost = rec.gross_consumption_kg * rec.cost_per_kg
|
|
|
|
|
|
class GarmentBomTrim(models.Model):
|
|
_name = 'garment.bom.trim'
|
|
_description = 'Garment BOM Trims & Accessories'
|
|
|
|
bom_id = fields.Many2one('garment.bom', string='BOM', required=True, ondelete='cascade')
|
|
trim_type = fields.Selection([
|
|
('main_label', 'Main Woven / Heat-Transfer Label'),
|
|
('size_label', 'Size Label'),
|
|
('care_label', 'Wash Care Satin Label'),
|
|
('hang_tag', 'Brand Hang Tag'),
|
|
('price_tag', 'Retail Price Ticket'),
|
|
('polybag', 'Individual Polybag'),
|
|
('carton', 'Master Export Carton Box'),
|
|
('thread', 'Sewing Thread (Spun Polyester)'),
|
|
('button', 'Buttons'),
|
|
('zipper', 'Zipper'),
|
|
('elastic', 'Elastic Band'),
|
|
('drawstring', 'Drawstring / Cord'),
|
|
('tissue_paper', 'Tissue Paper / Board'),
|
|
('silica_gel', 'Silica Gel Desiccant'),
|
|
('other', 'Other Accessory'),
|
|
], string='Trim Category', required=True, default='main_label')
|
|
item_name = fields.Char(string='Trim Item Name', required=True)
|
|
product_id = fields.Many2one('product.product', string='Trims Product')
|
|
uom_id = fields.Many2one('uom.uom', string='UoM', default=lambda self: self.env.ref('uom.product_uom_unit', raise_if_not_found=False))
|
|
|
|
consumption_qty = fields.Float(string='Net Qty / Pc', required=True, default=1.0, digits=(16, 4))
|
|
wastage_pct = fields.Float(string='Allowance %', default=3.0)
|
|
gross_qty = fields.Float(string='Gross Qty / Pc', compute='_compute_gross_qty', store=True, digits=(16, 4))
|
|
unit_cost = fields.Float(string='Unit Cost', default=1.5)
|
|
subtotal_cost = fields.Float(string='Cost / Pc', compute='_compute_subtotal_cost', store=True)
|
|
|
|
@api.depends('consumption_qty', 'wastage_pct')
|
|
def _compute_gross_qty(self):
|
|
for rec in self:
|
|
rec.gross_qty = rec.consumption_qty * (1.0 + (rec.wastage_pct / 100.0))
|
|
|
|
@api.depends('gross_qty', 'unit_cost')
|
|
def _compute_subtotal_cost(self):
|
|
for rec in self:
|
|
rec.subtotal_cost = rec.gross_qty * rec.unit_cost
|
|
|
|
|
|
class GarmentBomOperation(models.Model):
|
|
_name = 'garment.bom.operation'
|
|
_description = 'Garment BOM Operations & Routing'
|
|
_order = 'sequence, id'
|
|
|
|
bom_id = fields.Many2one('garment.bom', string='BOM', required=True, ondelete='cascade')
|
|
sequence = fields.Integer(string='Sequence', default=10)
|
|
operation_name = fields.Char(string='Operation Name', required=True)
|
|
department = fields.Selection([
|
|
('cutting', 'Cutting Section'),
|
|
('sewing', 'Sewing Assembly Line'),
|
|
('jobwork', 'External Job Work'),
|
|
('washing', 'Washing & Bio-wash'),
|
|
('finishing', 'Ironing & Finishing'),
|
|
('packing', 'Packing Section'),
|
|
], string='Department', default='sewing', required=True)
|
|
machine_type = fields.Selection([
|
|
('manual', 'Manual / Hand Work'),
|
|
('snls', 'Single Needle Lockstitch (SNLS)'),
|
|
('overlock_4t', '4-Thread Overlock (4T)'),
|
|
('overlock_5t', '5-Thread Overlock (5T)'),
|
|
('flatlock', 'Flatlock / Coverstitch'),
|
|
('button_attach', 'Button Stitching Machine'),
|
|
('button_hole', 'Button Hole Machine'),
|
|
('kansai', 'Multi-Needle Kansai'),
|
|
('steam_iron', 'Vacuum Steam Press'),
|
|
], string='Machine Required', default='overlock_4t')
|
|
sam = fields.Float(string='SAM (Minutes)', default=1.5, help='Standard Allowed Minutes')
|
|
minute_rate = fields.Float(string='Labor Cost / Minute', default=2.5)
|
|
subtotal_cost = fields.Float(string='Cost / Pc', compute='_compute_subtotal_cost', store=True)
|
|
|
|
@api.depends('sam', 'minute_rate')
|
|
def _compute_subtotal_cost(self):
|
|
for rec in self:
|
|
rec.subtotal_cost = rec.sam * rec.minute_rate
|