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

223 lines
12 KiB
Python

# -*- 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 = "<p class='text-muted'>No matrix lines configured yet.</p>"
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 = ["""
<div class="o_garment_matrix_container w-100 mb-3" style="width: 100% !important; max-width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch;">
<table class="table table-sm table-bordered text-center align-middle" style="width: 100% !important; min-width: 650px; margin-bottom: 0; font-size: 13.5px; border: 1px solid #dee2e6; border-collapse: collapse;">
<thead>
<tr style="background-color: #1e293b; color: #ffffff;">
<th class="text-start" style="min-width: 170px; padding: 10px 14px; background-color: #1e293b; color: #ffffff; border: 1px solid #334155; font-weight: 600;">Color \\ Size</th>
"""]
for s in sizes:
html.append(f"<th style='min-width: 80px; padding: 10px 8px; background-color: #1e293b; color: #ffffff; border: 1px solid #334155; font-weight: 600;'>{s.code}</th>")
html.append("<th style='min-width: 110px; padding: 10px 12px; background-color: #0f172a; color: #38bdf8; border: 1px solid #334155; font-weight: 700;'>Total Pcs</th></tr></thead><tbody>")
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"<tr style='background-color: {row_bg}; border-bottom: 1px solid #e2e8f0;'>")
html.append(f"<td class='text-start fw-semibold' style='padding: 8px 14px; white-space: nowrap; color: #1e293b;'><span style='display:inline-block;width:14px;height:14px;background-color:{c_hex};border:1px solid #94a3b8;margin-right:8px;border-radius:3px;vertical-align:middle;box-shadow:0 1px 2px rgba(0,0,0,0.15);'></span>{c.name}</td>")
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"<span class='fw-semibold text-dark'>{qty:,}</span>" if qty else "<span style='color: #94a3b8;'>-</span>"
html.append(f"<td style='padding: 8px 6px; border: 1px solid #e2e8f0;'>{cell_val}</td>")
grand_total += row_sum
html.append(f"<td class='fw-bold' style='padding: 8px 12px; background-color: #f1f5f9; color: #0f172a; border: 1px solid #cbd5e1;'>{row_sum:,}</td></tr>")
# Footer total row
html.append("<tr style='background-color: #e2e8f0; font-weight: bold; border-top: 2px solid #94a3b8;'>")
html.append("<td class='text-start' style='padding: 10px 14px; color: #0f172a; font-weight: 700;'>Total Pcs</td>")
for s in sizes:
html.append(f"<td style='padding: 10px 6px; color: #0f172a; font-weight: 700; border: 1px solid #cbd5e1;'>{total_col[s.id]:,}</td>")
html.append(f"<td style='padding: 10px 12px; background-color: #dbeafe; color: #1d4ed8; font-size: 14px; font-weight: 800; border: 1px solid #93c5fd;'>{grand_total:,}</td></tr>")
html.append("</tbody></table></div>")
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