# -*- coding: utf-8 -*- from odoo import models, fields, api, _ from odoo.exceptions import UserError class GarmentPackingPlan(models.Model): _name = 'garment.packing.plan' _inherit = ['mail.thread', 'mail.activity.mixin'] _description = 'Garment Export & Retail Packing Management' _order = 'name desc' name = fields.Char(string='Packing Plan No', required=True, default=lambda self: _('New'), copy=False, tracking=True) production_plan_id = fields.Many2one('garment.production.plan', string='Production Order', required=True, 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) company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company) packing_type = fields.Selection([ ('solid_color_solid_size', 'Solid Color / Solid Size (Single Variant per Box)'), ('solid_color_assorted_size', 'Solid Color / Assorted Size (Ratio Pack)'), ('assorted_color_assorted_size', 'Assorted Color / Assorted Size (Pre-Pack)'), ], string='Carton Packing Method', default='solid_color_assorted_size', required=True, tracking=True) target_pack_qty = fields.Integer(string='Target Pack Pieces', default=1000, required=True) total_packed_qty = fields.Integer(string='Total Packed Pieces', compute='_compute_carton_totals', store=True) total_cartons = fields.Integer(string='Total Cartons', compute='_compute_carton_totals', store=True) total_cbm = fields.Float(string='Total Shipment CBM (m³)', compute='_compute_carton_totals', store=True) total_gross_weight = fields.Float(string='Total Gross Weight (Kg)', compute='_compute_carton_totals', store=True) carton_ids = fields.One2many('garment.carton', 'packing_plan_id', string='Master Cartons') state = fields.Selection([ ('draft', 'Draft'), ('in_packing', 'Packing In Progress'), ('completed', 'Packed & Transferred to FG'), ], string='Status', default='draft', tracking=True, required=True) @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.packing.plan') or _('PACK/%s') % fields.Date.today().strftime('%Y%m%d') return super(GarmentPackingPlan, self).create(vals_list) @api.depends('carton_ids.total_pieces', 'carton_ids.cbm', 'carton_ids.gross_weight_kg') def _compute_carton_totals(self): for plan in self: plan.total_packed_qty = sum(plan.carton_ids.mapped('total_pieces')) plan.total_cartons = len(plan.carton_ids) plan.total_cbm = round(sum(plan.carton_ids.mapped('cbm')), 3) plan.total_gross_weight = round(sum(plan.carton_ids.mapped('gross_weight_kg')), 2) def action_start_packing(self): self.write({'state': 'in_packing'}) def action_transfer_to_finished_goods(self): """Creates or updates finished goods stock records from packed cartons""" self.ensure_one() if not self.carton_ids: raise UserError(_("No cartons found in this packing plan.")) FG = self.env['garment.finished.goods'] for carton in self.carton_ids: for line in carton.line_ids: existing_fg = FG.search([ ('style_id', '=', self.style_id.id), ('color_id', '=', line.color_id.id), ('size_id', '=', line.size_id.id), ], limit=1) if existing_fg: existing_fg.write({'on_hand_qty': existing_fg.on_hand_qty + line.quantity}) else: FG.create({ 'style_id': self.style_id.id, ('color_id'): line.color_id.id, ('size_id'): line.size_id.id, 'on_hand_qty': line.quantity, }) self.write({'state': 'completed'}) return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': _('Finished Goods Stock Updated'), 'message': _('Successfully transferred %d packed garments into Finished Goods Inventory.') % self.total_packed_qty, 'sticky': False, 'type': 'success', } } class GarmentCarton(models.Model): _name = 'garment.carton' _inherit = ['mail.thread', 'mail.activity.mixin'] _description = 'Master Export Carton' _order = 'carton_no, name' name = fields.Char(string='Carton ID', required=True, default=lambda self: _('New'), copy=False) carton_no = fields.Integer(string='Carton #', default=1, required=True) packing_plan_id = fields.Many2one('garment.packing.plan', string='Packing Plan', required=True, ondelete='cascade') barcode = fields.Char(string='Carton Barcode / Shipping QR', index=True, copy=False) style_id = fields.Many2one('garment.style', string='Garment Style', related='packing_plan_id.style_id', readonly=True) # Dimensions & Weights length_cm = fields.Float(string='Length (cm)', default=60.0) width_cm = fields.Float(string='Width (cm)', default=40.0) height_cm = fields.Float(string='Height (cm)', default=30.0) cbm = fields.Float(string='Carton CBM (m³)', compute='_compute_cbm', store=True) net_weight_kg = fields.Float(string='Net Weight (Kg)', default=12.5) gross_weight_kg = fields.Float(string='Gross Weight (Kg)', default=14.0) line_ids = fields.One2many('garment.carton.line', 'carton_id', string='Carton Garment Breakdown') total_pieces = fields.Integer(string='Total Pieces in Box', compute='_compute_pieces', store=True) dispatch_id = fields.Many2one('garment.dispatch', string='Dispatch Order') @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.carton') or _('CTN-%s') % fields.Date.today().strftime('%Y%m%d') if not vals.get('barcode'): vals['barcode'] = vals['name'] return super(GarmentCarton, self).create(vals_list) @api.depends('length_cm', 'width_cm', 'height_cm') def _compute_cbm(self): for ctn in self: ctn.cbm = round((ctn.length_cm * ctn.width_cm * ctn.height_cm) / 1000000.0, 4) @api.depends('line_ids.quantity') def _compute_pieces(self): for ctn in self: ctn.total_pieces = sum(ctn.line_ids.mapped('quantity')) class GarmentCartonLine(models.Model): _name = 'garment.carton.line' _description = 'Carton Contents Breakdown Line' carton_id = fields.Many2one('garment.carton', string='Carton', required=True, ondelete='cascade') color_id = fields.Many2one('garment.color', string='Color', required=True) size_id = fields.Many2one('garment.size', string='Size', required=True) quantity = fields.Integer(string='Pieces in Carton', default=10, required=True)