# -*- 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 = ["""
| Color \\ Size |
"""]
for s in sizes:
html.append(f"{s.code} | ")
html.append("Total Pcs |
")
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"| {c.name} | ")
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"{cell_val} | ")
grand_total += row_sum
html.append(f"{row_sum:,} |
")
# Footer total row
html.append("")
html.append("| Total Pcs | ")
for s in sizes:
html.append(f"{total_col[s.id]:,} | ")
html.append(f"{grand_total:,} |
")
html.append("
")
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