213 lines
9.5 KiB
Python
213 lines
9.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
from odoo import models, fields, api, _
|
|
from odoo.exceptions import UserError
|
|
|
|
class GarmentCuttingPlan(models.Model):
|
|
_name = 'garment.cutting.plan'
|
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
|
_description = 'Garment Cutting Plan & Lay Management'
|
|
_order = 'name desc'
|
|
|
|
name = fields.Char(string='Cutting Order No', required=True, default=lambda self: _('New'), copy=False, tracking=True)
|
|
production_plan_id = fields.Many2one('garment.production.plan', string='Production Plan', required=True, tracking=True)
|
|
style_id = fields.Many2one('garment.style', string='Garment Style', related='production_plan_id.style_id', readonly=True)
|
|
color_id = fields.Many2one('garment.color', string='Body Fabric Color', required=True)
|
|
company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company)
|
|
|
|
# Lay Planning & Marker
|
|
marker_no = fields.Char(string='Marker Code / Reference', default='MKR-01')
|
|
marker_length = fields.Float(string='Marker Length (Meters)', default=6.2)
|
|
marker_width = fields.Float(string='Marker Width (Inches)', default=30.0)
|
|
plies_count = fields.Integer(string='Number of Plies / Layers', default=100, tracking=True)
|
|
bundle_size = fields.Integer(string='Bundle Size (Pieces/Bundle)', default=25, help='Standard number of garments per bundle ticket')
|
|
|
|
# Quantitative Results
|
|
ratio_line_ids = fields.One2many('garment.cutting.ratio.line', 'cutting_plan_id', string='Size Cut Ratio')
|
|
roll_line_ids = fields.One2many('garment.cutting.roll.line', 'cutting_plan_id', string='Fabric Rolls Issued')
|
|
|
|
planned_cut_qty = fields.Integer(string='Planned Cut Pieces', compute='_compute_planned_cut_qty', store=True)
|
|
actual_cut_qty = fields.Integer(string='Actual Cut Pieces', default=0, tracking=True)
|
|
|
|
# Wastage Calculations
|
|
total_fabric_issued_kg = fields.Float(string='Total Fabric Issued (Kg)', compute='_compute_fabric_usage', store=True)
|
|
total_fabric_used_kg = fields.Float(string='Actual Fabric Consumed (Kg)', compute='_compute_fabric_usage', store=True)
|
|
end_bit_scrap_kg = fields.Float(string='End-Bits / Remnant Scrap (Kg)', compute='_compute_fabric_usage', store=True)
|
|
cutting_wastage_pct = fields.Float(string='Cutting Wastage %', compute='_compute_fabric_usage', store=True)
|
|
|
|
# State Workflow
|
|
state = fields.Selection([
|
|
('draft', 'Draft'),
|
|
('planned', 'Lay Planned'),
|
|
('cutting', 'In Cutting'),
|
|
('completed', 'Cut & Bundled'),
|
|
('cancelled', 'Cancelled'),
|
|
], string='Status', default='draft', tracking=True, required=True)
|
|
|
|
bundle_ids = fields.One2many('garment.bundle', 'cutting_plan_id', string='Generated Bundles')
|
|
bundle_count = fields.Integer(string='Bundles Count', compute='_compute_bundle_count')
|
|
|
|
@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.cutting.plan') or _('CUT/%s') % fields.Date.today().strftime('%Y%m%d')
|
|
plans = super().create(vals_list)
|
|
for plan in plans:
|
|
if not plan.ratio_line_ids and plan.production_plan_id:
|
|
plan._populate_ratios_from_plan()
|
|
return plans
|
|
|
|
def _populate_ratios_from_plan(self):
|
|
self.ensure_one()
|
|
matching_lines = self.production_plan_id.line_ids.filtered(lambda l: l.color_id.id == self.color_id.id)
|
|
if not matching_lines:
|
|
matching_lines = self.production_plan_id.line_ids
|
|
lines = []
|
|
for pl in matching_lines:
|
|
lines.append((0, 0, {
|
|
'size_id': pl.size_id.id,
|
|
'ratio': 1,
|
|
}))
|
|
if lines:
|
|
self.write({'ratio_line_ids': lines})
|
|
|
|
@api.depends('plies_count', 'ratio_line_ids.ratio')
|
|
def _compute_planned_cut_qty(self):
|
|
for plan in self:
|
|
total_ratio = sum(plan.ratio_line_ids.mapped('ratio'))
|
|
plan.planned_cut_qty = plan.plies_count * total_ratio
|
|
|
|
@api.depends('roll_line_ids.weight_issued', 'roll_line_ids.weight_used', 'roll_line_ids.end_bit_scrap')
|
|
def _compute_fabric_usage(self):
|
|
for plan in self:
|
|
issued = sum(plan.roll_line_ids.mapped('weight_issued'))
|
|
used = sum(plan.roll_line_ids.mapped('weight_used'))
|
|
scrap = sum(plan.roll_line_ids.mapped('end_bit_scrap'))
|
|
plan.total_fabric_issued_kg = issued
|
|
plan.total_fabric_used_kg = used
|
|
plan.end_bit_scrap_kg = scrap
|
|
if used > 0:
|
|
plan.cutting_wastage_pct = round((scrap / (used + scrap)) * 100.0, 2)
|
|
else:
|
|
plan.cutting_wastage_pct = 0.0
|
|
|
|
@api.depends('bundle_ids')
|
|
def _compute_bundle_count(self):
|
|
for plan in self:
|
|
plan.bundle_count = len(plan.bundle_ids)
|
|
|
|
def action_start_cutting(self):
|
|
self.write({'state': 'cutting'})
|
|
|
|
def action_generate_bundles(self):
|
|
"""Generates production bundles based on lay plies and size ratios"""
|
|
self.ensure_one()
|
|
if not self.ratio_line_ids:
|
|
raise UserError(_("Please define Size Cut Ratios before generating bundles."))
|
|
if self.plies_count <= 0:
|
|
raise UserError(_("Plies count must be greater than 0."))
|
|
|
|
Bundle = self.env['garment.bundle']
|
|
bundle_size = self.bundle_size or 25
|
|
created_bundles = []
|
|
total_actual = 0
|
|
|
|
for r_line in self.ratio_line_ids:
|
|
if r_line.ratio <= 0:
|
|
continue
|
|
total_pieces_for_size = self.plies_count * r_line.ratio
|
|
total_actual += total_pieces_for_size
|
|
|
|
# Divide into bundles of bundle_size
|
|
remaining = total_pieces_for_size
|
|
ply_cursor = 1
|
|
bundle_idx = 1
|
|
while remaining > 0:
|
|
cur_bundle_qty = min(bundle_size, remaining)
|
|
bundle_vals = {
|
|
'cutting_plan_id': self.id,
|
|
'production_plan_id': self.production_plan_id.id,
|
|
'style_id': self.style_id.id,
|
|
'color_id': self.color_id.id,
|
|
'size_id': r_line.size_id.id,
|
|
'quantity': cur_bundle_qty,
|
|
'ply_start': ply_cursor,
|
|
'ply_end': ply_cursor + cur_bundle_qty - 1,
|
|
'stage': 'sewing',
|
|
'status': 'ready',
|
|
}
|
|
b = Bundle.create(bundle_vals)
|
|
created_bundles.append(b.id)
|
|
remaining -= cur_bundle_qty
|
|
ply_cursor += cur_bundle_qty
|
|
bundle_idx += 1
|
|
|
|
self.write({
|
|
'actual_cut_qty': total_actual,
|
|
'state': 'completed',
|
|
})
|
|
return {
|
|
'type': 'ir.actions.client',
|
|
'tag': 'display_notification',
|
|
'params': {
|
|
'title': _('Bundles Generated'),
|
|
'message': _('Successfully generated %d production bundles for %d cut pieces.') % (len(created_bundles), total_actual),
|
|
'sticky': False,
|
|
'type': 'success',
|
|
}
|
|
}
|
|
|
|
def action_view_bundles(self):
|
|
self.ensure_one()
|
|
return {
|
|
'name': _('Bundles for %s') % self.name,
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'garment.bundle',
|
|
'view_mode': 'list,form',
|
|
'domain': [('cutting_plan_id', '=', self.id)],
|
|
}
|
|
|
|
|
|
class GarmentCuttingRatioLine(models.Model):
|
|
_name = 'garment.cutting.ratio.line'
|
|
_description = 'Cutting Size Ratio Line'
|
|
_order = 'size_id'
|
|
|
|
cutting_plan_id = fields.Many2one('garment.cutting.plan', string='Cutting Plan', required=True, ondelete='cascade')
|
|
size_id = fields.Many2one('garment.size', string='Size', required=True)
|
|
ratio = fields.Integer(string='Ratio per Ply', default=1, required=True)
|
|
expected_cut_qty = fields.Integer(string='Expected Cut Pcs', compute='_compute_expected', store=True)
|
|
|
|
@api.depends('ratio', 'cutting_plan_id.plies_count')
|
|
def _compute_expected(self):
|
|
for line in self:
|
|
line.expected_cut_qty = line.ratio * (line.cutting_plan_id.plies_count or 0)
|
|
|
|
|
|
class GarmentCuttingRollLine(models.Model):
|
|
_name = 'garment.cutting.roll.line'
|
|
_description = 'Cutting Fabric Roll Allocation'
|
|
|
|
cutting_plan_id = fields.Many2one('garment.cutting.plan', string='Cutting Plan', required=True, ondelete='cascade')
|
|
roll_id = fields.Many2one('garment.fabric.roll', string='Fabric Roll', required=True, domain=[('status', 'in', ['approved', 'issued'])])
|
|
shade_lot = fields.Char(string='Shade Lot', related='roll_id.shade_lot', readonly=True)
|
|
weight_issued = fields.Float(string='Weight Issued (Kg)', required=True, default=24.0)
|
|
weight_used = fields.Float(string='Weight Consumed (Kg)', required=True, default=23.0)
|
|
end_bit_scrap = fields.Float(string='End-Bit Scrap (Kg)', default=1.0)
|
|
|
|
@api.onchange('roll_id')
|
|
def _onchange_roll_id(self):
|
|
if self.roll_id:
|
|
self.weight_issued = self.roll_id.remaining_weight
|
|
self.weight_used = self.roll_id.remaining_weight
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
records = super(GarmentCuttingRollLine, self).create(vals_list)
|
|
for rec in records:
|
|
if rec.roll_id:
|
|
rec.roll_id.issued_weight += rec.weight_used
|
|
rec.roll_id.scrap_weight += rec.end_bit_scrap
|
|
rec.roll_id.status = 'issued'
|
|
return records
|