# -*- 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