# -*- coding: utf-8 -*- from odoo import models, fields, api, _ from odoo.exceptions import UserError class SaleOrder(models.Model): _inherit = 'sale.order' is_garment_order = fields.Boolean(string='Garment Manufacturing Order', default=True) garment_style_id = fields.Many2one('garment.style', string='Garment Style', tracking=True) buyer_id = fields.Many2one('res.partner', string='Buyer / Brand', tracking=True) buyer_po_ref = fields.Char(string='Buyer PO Reference', tracking=True) season_id = fields.Many2one('garment.season', string='Season') customer_style_ref = fields.Char(string='Buyer Style Code') # Shipment Details ex_factory_date = fields.Date(string='Ex-Factory Date', tracking=True) shipment_date = fields.Date(string='Target Shipment Date', tracking=True) shipment_mode = fields.Selection([ ('sea', 'Sea Freight (FCL/LCL)'), ('air', 'Air Freight'), ('courier', 'Express Courier'), ('road', 'Road Transport'), ], string='Shipment Mode', default='sea') destination_port = fields.Char(string='Destination Port / City') # Order Matrix order_matrix_line_ids = fields.One2many('garment.order.matrix.line', 'order_id', string='Size-Color Order Matrix') total_garment_qty = fields.Integer(string='Total Garment Pieces', compute='_compute_total_garment_qty', store=True) matrix_html_summary = fields.Html(string='Order Matrix Summary', compute='_compute_matrix_html_summary') # Production Link production_plan_ids = fields.One2many('garment.production.plan', 'sale_order_id', string='Production Plans') production_plan_count = fields.Integer(string='Production Plans Count', compute='_compute_production_plan_count') @api.onchange('garment_style_id') def _onchange_garment_style_id(self): if self.garment_style_id: self.season_id = self.garment_style_id.season_id self.customer_style_ref = self.garment_style_id.customer_style_ref if self.garment_style_id.buyer_id and not self.buyer_id: self.buyer_id = self.garment_style_id.buyer_id @api.depends('order_matrix_line_ids.quantity') def _compute_total_garment_qty(self): for order in self: order.total_garment_qty = sum(order.order_matrix_line_ids.mapped('quantity')) @api.depends('order_matrix_line_ids.quantity', 'order_matrix_line_ids.color_id', 'order_matrix_line_ids.size_id') def _compute_matrix_html_summary(self): for order in self: lines = order.order_matrix_line_ids if not lines: order.matrix_html_summary = "

No matrix lines configured yet.

" continue # Distinct sorted colors and sizes colors = sorted(lines.mapped('color_id'), key=lambda c: c.name) sizes = sorted(lines.mapped('size_id'), key=lambda s: (s.sequence, s.code)) # Map (color_id, size_id) -> quantity grid = {} for l in lines: grid[(l.color_id.id, l.size_id.id)] = grid.get((l.color_id.id, l.size_id.id), 0) + l.quantity # Build HTML table html = ["""
"""] for s in sizes: html.append(f"") html.append("") total_col = {s.id: 0 for s in sizes} grand_total = 0 for idx, c in enumerate(colors): c_hex = c.color_hex or '#eee' row_bg = '#ffffff' if idx % 2 == 0 else '#f8fafc' html.append(f"") html.append(f"") row_sum = 0 for s in sizes: qty = grid.get((c.id, s.id), 0) row_sum += qty total_col[s.id] += qty cell_val = f"{qty:,}" if qty else "-" html.append(f"") grand_total += row_sum html.append(f"") # Footer total row html.append("") html.append("") for s in sizes: html.append(f"") html.append(f"") html.append("
Color \\ Size{s.code}Total Pcs
{c.name}{cell_val}{row_sum:,}
Total Pcs{total_col[s.id]:,}{grand_total:,}
") order.matrix_html_summary = "".join(html) def action_sync_matrix_to_order_lines(self): """Generates or updates standard sale.order.line items from the Garment Matrix""" self.ensure_one() if not self.garment_style_id: raise UserError(_("Please select a Garment Style before synchronizing order lines.")) if not self.order_matrix_line_ids: raise UserError(_("Order Matrix is empty. Please enter size-color quantities first.")) # First ensure variants exist self.garment_style_id.action_generate_product_variants() ProductProduct = self.env['product.product'] # Clear existing lines linked to this style or create lines created = 0 for m_line in self.order_matrix_line_ids: if m_line.quantity <= 0: continue sku = f"{self.garment_style_id.name}-{m_line.color_id.code}-{m_line.size_id.code}" product = ProductProduct.search([('default_code', '=', sku)], limit=1) if not product: product = ProductProduct.create({ 'name': f"{self.garment_style_id.style_name} ({m_line.color_id.name}/{m_line.size_id.code})", 'default_code': sku, 'type': 'consu', }) # Find or create sale.order.line existing_line = self.order_line.filtered(lambda l: l.product_id.id == product.id) if existing_line: existing_line.write({ 'product_uom_qty': m_line.quantity, 'price_unit': m_line.unit_price or existing_line.price_unit or 10.0, }) else: uom = self.garment_style_id.uom_id.id if self.garment_style_id.uom_id else self.env.ref('uom.product_uom_unit').id sol_vals = { 'order_id': self.id, 'product_id': product.id, 'name': f"[{sku}] {self.garment_style_id.style_name} - {m_line.color_id.name} / {m_line.size_id.name}", 'product_uom_qty': m_line.quantity, 'price_unit': m_line.unit_price or 10.0, } if 'product_uom_id' in self.env['sale.order.line']._fields: sol_vals['product_uom_id'] = uom elif 'product_uom' in self.env['sale.order.line']._fields: sol_vals['product_uom'] = uom self.env['sale.order.line'].create(sol_vals) created += 1 return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': _('Sale Lines Synchronized'), 'message': _('Successfully synchronized %d product variant lines.') % created, 'sticky': False, 'type': 'success', } } def action_create_production_plan(self): """Creates a Garment Production Plan from this Sales Order""" self.ensure_one() if not self.garment_style_id: raise UserError(_("Please assign a Garment Style.")) if self.total_garment_qty <= 0: raise UserError(_("Total order quantity must be greater than 0.")) plan = self.env['garment.production.plan'].create({ 'sale_order_id': self.id, 'style_id': self.garment_style_id.id, 'planned_qty': self.total_garment_qty, 'date_start': fields.Date.today(), 'date_delivery': self.shipment_date or self.ex_factory_date or fields.Date.today(), 'company_id': self.company_id.id, }) return { 'name': _('Garment Production Plan'), 'type': 'ir.actions.act_window', 'res_model': 'garment.production.plan', 'res_id': plan.id, 'view_mode': 'form', 'target': 'current', } @api.depends('production_plan_ids') def _compute_production_plan_count(self): for order in self: order.production_plan_count = len(order.production_plan_ids) def action_view_production_plans(self): self.ensure_one() return { 'name': _('Production Plans for SO %s') % self.name, 'type': 'ir.actions.act_window', 'res_model': 'garment.production.plan', 'view_mode': 'list,form', 'domain': [('sale_order_id', '=', self.id)], 'context': {'default_sale_order_id': self.id, 'default_style_id': self.garment_style_id.id}, } class GarmentOrderMatrixLine(models.Model): _name = 'garment.order.matrix.line' _description = 'Garment Order Matrix Line (Color x Size x Quantity)' _order = 'color_id, size_id' order_id = fields.Many2one('sale.order', string='Sales Order', 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='Quantity (Pcs)', required=True, default=0) unit_price = fields.Float(string='Unit Price', default=0.0) subtotal = fields.Float(string='Subtotal', compute='_compute_subtotal', store=True) @api.depends('quantity', 'unit_price') def _compute_subtotal(self): for rec in self: rec.subtotal = rec.quantity * rec.unit_price