2026-09-21 16:43:23 +05:30

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