# -*- coding: utf-8 -*- from odoo import models, fields, api, _ from odoo.exceptions import UserError class GarmentProductionPlan(models.Model): _name = 'garment.production.plan' _inherit = ['mail.thread', 'mail.activity.mixin'] _description = 'Garment Production Master Plan' _order = 'name desc' name = fields.Char(string='Plan Number', required=True, default=lambda self: _('New'), copy=False, tracking=True) sale_order_id = fields.Many2one('sale.order', string='Sales Order', tracking=True) style_id = fields.Many2one('garment.style', string='Garment Style', required=True, tracking=True) bom_id = fields.Many2one('garment.bom', string='Garment BOM', tracking=True) company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company) # Dates & Scheduling date_start = fields.Date(string='Production Start Date', default=fields.Date.today, tracking=True) date_delivery = fields.Date(string='Target Delivery Date', tracking=True) priority = fields.Selection([ ('0', 'Low'), ('1', 'Normal'), ('2', 'High'), ('3', 'Urgent'), ], string='Priority', default='1', tracking=True) # State Workflow state = fields.Selection([ ('draft', 'Draft'), ('planned', 'Planned / Scheduled'), ('in_progress', 'In Production'), ('completed', 'Completed'), ('cancelled', 'Cancelled'), ], string='Status', default='draft', tracking=True, required=True) # Quantitative Progress Tracking planned_qty = fields.Integer(string='Planned Quantity (Pcs)', required=True, default=1000, tracking=True) cut_qty = fields.Integer(string='Cut Quantity', compute='_compute_stage_quantities', store=True) sewn_qty = fields.Integer(string='Sewn Quantity', compute='_compute_stage_quantities', store=True) packed_qty = fields.Integer(string='Packed Quantity', compute='_compute_stage_quantities', store=True) dispatched_qty = fields.Integer(string='Dispatched Quantity', compute='_compute_stage_quantities', store=True) progress_pct = fields.Float(string='Production Progress %', compute='_compute_progress_pct', store=True) # Line Breakdown (Color x Size breakdown for this production order) line_ids = fields.One2many('garment.production.plan.line', 'plan_id', string='Color/Size Production Targets') # Child Records cutting_plan_ids = fields.One2many('garment.cutting.plan', 'production_plan_id', string='Cutting Orders') bundle_ids = fields.One2many('garment.bundle', 'production_plan_id', string='Bundles') jobwork_order_ids = fields.One2many('garment.jobwork.order', 'production_plan_id', string='Job Work Orders') qc_inspection_ids = fields.One2many('garment.quality.inspection', 'production_plan_id', string='QC Inspections') packing_plan_ids = fields.One2many('garment.packing.plan', 'production_plan_id', string='Packing Plans') mrp_requirement_ids = fields.One2many('garment.mrp.requirement', 'production_plan_id', string='MRP Requirements') # Smart Button Counts cutting_count = fields.Integer(string='Cutting Count', compute='_compute_counts') bundle_count = fields.Integer(string='Bundle Count', compute='_compute_counts') jobwork_count = fields.Integer(string='Job Work Count', compute='_compute_counts') qc_count = fields.Integer(string='QC Count', compute='_compute_counts') packing_count = fields.Integer(string='Packing Count', compute='_compute_counts') notes = fields.Html(string='Production Notes') @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.production.plan') or _('PP/%s') % fields.Date.today().strftime('%Y%m%d') plans = super().create(vals_list) for plan in plans: if plan.sale_order_id and not plan.line_ids: plan._populate_lines_from_sale_order() return plans def _populate_lines_from_sale_order(self): self.ensure_one() if not self.sale_order_id: return lines = [] for m_line in self.sale_order_id.order_matrix_line_ids: lines.append((0, 0, { 'color_id': m_line.color_id.id, 'size_id': m_line.size_id.id, 'planned_qty': m_line.quantity, })) if lines: self.write({'line_ids': lines}) @api.onchange('style_id') def _onchange_style_id(self): if self.style_id: bom = self.env['garment.bom'].search([('style_id', '=', self.style_id.id)], limit=1) if bom: self.bom_id = bom.id @api.depends('cutting_plan_ids.actual_cut_qty', 'packing_plan_ids.total_packed_qty') def _compute_stage_quantities(self): for plan in self: plan.cut_qty = sum(plan.cutting_plan_ids.mapped('actual_cut_qty')) plan.packed_qty = sum(plan.packing_plan_ids.mapped('total_packed_qty')) # Sewn qty from bundles completed in sewing sewn_bundles = plan.bundle_ids.filtered(lambda b: b.stage in ['quality', 'packing', 'completed']) plan.sewn_qty = sum(sewn_bundles.mapped('quantity')) # Dispatched qty from dispatch orders dispatches = self.env['garment.dispatch'].search([('production_plan_id', '=', plan.id), ('state', '=', 'dispatched')]) plan.dispatched_qty = sum(dispatches.mapped('total_pieces')) @api.depends('planned_qty', 'packed_qty') def _compute_progress_pct(self): for plan in self: if plan.planned_qty > 0: plan.progress_pct = min(100.0, round((plan.packed_qty / plan.planned_qty) * 100.0, 1)) else: plan.progress_pct = 0.0 @api.depends('cutting_plan_ids', 'bundle_ids', 'jobwork_order_ids', 'qc_inspection_ids', 'packing_plan_ids') def _compute_counts(self): for plan in self: plan.cutting_count = len(plan.cutting_plan_ids) plan.bundle_count = len(plan.bundle_ids) plan.jobwork_count = len(plan.jobwork_order_ids) plan.qc_count = len(plan.qc_inspection_ids) plan.packing_count = len(plan.packing_plan_ids) # Workflow Actions def action_plan(self): self.write({'state': 'planned'}) def action_start(self): self.write({'state': 'in_progress'}) def action_complete(self): self.write({'state': 'completed'}) def action_cancel(self): self.write({'state': 'cancelled'}) def action_reset_to_draft(self): self.write({'state': 'draft'}) def action_create_cutting_plan(self): self.ensure_one() cut_plan = self.env['garment.cutting.plan'].create({ 'production_plan_id': self.id, 'style_id': self.style_id.id, 'planned_cut_qty': self.planned_qty, 'color_id': self.line_ids[0].color_id.id if self.line_ids else False, }) return { 'name': _('Cutting Order'), 'type': 'ir.actions.act_window', 'res_model': 'garment.cutting.plan', 'res_id': cut_plan.id, 'view_mode': 'form', } def action_create_mrp_requirement(self): self.ensure_one() if not self.bom_id: raise UserError(_("Please assign a BOM to calculate Material Requirements.")) req = self.env['garment.mrp.requirement'].create({ 'production_plan_id': self.id, 'style_id': self.style_id.id, 'bom_id': self.bom_id.id, 'order_qty': self.planned_qty, }) req.action_compute_requirements() return { 'name': _('Material Requirements Calculation'), 'type': 'ir.actions.act_window', 'res_model': 'garment.mrp.requirement', 'res_id': req.id, 'view_mode': 'form', } def action_create_packing_plan(self): self.ensure_one() pack = self.env['garment.packing.plan'].create({ 'production_plan_id': self.id, 'sale_order_id': self.sale_order_id.id if self.sale_order_id else False, 'style_id': self.style_id.id, 'target_pack_qty': self.planned_qty, }) return { 'name': _('Packing Plan'), 'type': 'ir.actions.act_window', 'res_model': 'garment.packing.plan', 'res_id': pack.id, 'view_mode': 'form', } # Smart Button navigation actions def action_view_cutting_plans(self): self.ensure_one() return { 'name': _('Cutting Orders (%s)') % self.name, 'type': 'ir.actions.act_window', 'res_model': 'garment.cutting.plan', 'view_mode': 'list,form', 'domain': [('production_plan_id', '=', self.id)], 'context': {'default_production_plan_id': self.id, 'default_style_id': self.style_id.id}, } def action_view_bundles(self): self.ensure_one() return { 'name': _('Production Bundles (%s)') % self.name, 'type': 'ir.actions.act_window', 'res_model': 'garment.bundle', 'view_mode': 'list,form', 'domain': [('production_plan_id', '=', self.id)], 'context': {'default_production_plan_id': self.id}, } def action_view_jobwork(self): self.ensure_one() return { 'name': _('Job Work Orders (%s)') % self.name, 'type': 'ir.actions.act_window', 'res_model': 'garment.jobwork.order', 'view_mode': 'list,form', 'domain': [('production_plan_id', '=', self.id)], 'context': {'default_production_plan_id': self.id}, } def action_view_qc(self): self.ensure_one() return { 'name': _('Quality Inspections (%s)') % self.name, 'type': 'ir.actions.act_window', 'res_model': 'garment.quality.inspection', 'view_mode': 'list,form', 'domain': [('production_plan_id', '=', self.id)], 'context': {'default_production_plan_id': self.id}, } def action_view_packing(self): self.ensure_one() return { 'name': _('Packing Plans (%s)') % self.name, 'type': 'ir.actions.act_window', 'res_model': 'garment.packing.plan', 'view_mode': 'list,form', 'domain': [('production_plan_id', '=', self.id)], 'context': {'default_production_plan_id': self.id}, } class GarmentProductionPlanLine(models.Model): _name = 'garment.production.plan.line' _description = 'Production Target Line by Color & Size' _order = 'color_id, size_id' plan_id = fields.Many2one('garment.production.plan', string='Production Plan', required=True, ondelete='cascade') color_id = fields.Many2one('garment.color', string='Color', required=True) size_id = fields.Many2one('garment.size', string='Size', required=True) planned_qty = fields.Integer(string='Planned Pcs', required=True, default=0) cut_qty = fields.Integer(string='Cut Pcs', default=0) sewn_qty = fields.Integer(string='Sewn Pcs', default=0) packed_qty = fields.Integer(string='Packed Pcs', default=0) variance = fields.Integer(string='Balance to Pack', compute='_compute_variance', store=True) @api.depends('planned_qty', 'packed_qty') def _compute_variance(self): for line in self: line.variance = line.planned_qty - line.packed_qty