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