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

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