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

266 lines
12 KiB
Python

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