266 lines
12 KiB
Python
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
|