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

173 lines
8.5 KiB
Python

# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError
class GarmentStyle(models.Model):
_name = 'garment.style'
_inherit = ['mail.thread', 'mail.activity.mixin']
_description = 'Garment Style Master'
_order = 'name'
name = fields.Char(string='Style Number / Code', required=True, index=True, tracking=True)
style_name = fields.Char(string='Style Description / Name', required=True, tracking=True)
product_tmpl_id = fields.Many2one('product.template', string='Odoo Product Template', ondelete='set null')
active = fields.Boolean(default=True)
# Classification
brand_id = fields.Many2one('garment.brand', string='Brand', tracking=True)
season_id = fields.Many2one('garment.season', string='Season', tracking=True)
gender_id = fields.Many2one('garment.gender', string='Gender / Category')
garment_type_id = fields.Many2one('garment.type', string='Garment Type', required=True)
# Fabric Specifications (Tiruppur Specifics)
fabric_type_id = fields.Many2one('garment.fabric.type', string='Fabric Knit Type', required=True)
composition_id = fields.Many2one('garment.composition', string='Fabric Composition', required=True)
gsm = fields.Integer(string='Fabric GSM (g/m²)', required=True, default=180, help='Fabric Grams per Square Meter')
fabric_width = fields.Float(string='Fabric Width (Inches)', default=30.0, help='Tube Dia or Open Width')
width_type = fields.Selection([
('tube', 'Tubular / Dia (Circular Knit)'),
('open', 'Open Width (Slit & Heat Set)'),
], string='Width Type', default='tube', required=True)
shrinkage_length_pct = fields.Float(string='Shrinkage Length %', default=4.0)
shrinkage_width_pct = fields.Float(string='Shrinkage Width %', default=4.0)
yarn_count = fields.Char(string='Yarn Count')
# Dimensions & Variants
color_ids = fields.Many2many('garment.color', 'garment_style_color_rel', 'style_id', 'color_id', string='Available Colors', required=True)
size_ids = fields.Many2many('garment.size', 'garment_style_size_rel', 'style_id', 'size_id', string='Available Sizes', required=True)
uom_id = fields.Many2one('uom.uom', string='Unit of Measure', default=lambda self: self.env.ref('uom.product_uom_unit', raise_if_not_found=False))
# Industrial Engineering (IE)
sam = fields.Float(string='Sewing SAM (Minutes)', default=12.0, help='Standard Allowed Minutes / Standard Minute Value for sewing this style')
smv = fields.Float(string='Total Garment SMV', default=18.0, help='Total Standard Minute Value (Cutting + Sewing + Finishing)')
# Commercial & References
buyer_id = fields.Many2one('res.partner', string='Buyer / Customer', domain=[('customer_rank', '>', 0)])
customer_style_ref = fields.Char(string='Buyer Style Reference', help="Buyer's internal style code")
sample_ref = fields.Char(string='Sample / Proto Reference')
hsn_code = fields.Char(string='GST HSN Code', default='61091000', help='HSN 6109 for Cotton T-Shirts')
# Media & Tech Pack
image_front = fields.Binary(string='Front View Sketch / Photo')
image_back = fields.Binary(string='Back View Sketch / Photo')
techpack_attachment = fields.Binary(string='Tech Pack Document (PDF)')
techpack_filename = fields.Char(string='Tech Pack Filename')
notes = fields.Html(string='Style Notes & Construction Specs')
# Relational Lines & Computes
measurement_line_ids = fields.One2many('garment.style.measurement', 'style_id', string='Measurement Specifications')
variant_count = fields.Integer(string='Variant Count', compute='_compute_counts')
bom_count = fields.Integer(string='BOM Count', compute='_compute_counts')
order_count = fields.Integer(string='Orders Count', compute='_compute_counts')
production_count = fields.Integer(string='Production Count', compute='_compute_counts')
_name_uniq = models.Constraint('UNIQUE(name)', 'Style Code must be unique!')
@api.depends('color_ids', 'size_ids')
def _compute_counts(self):
for rec in self:
rec.variant_count = len(rec.color_ids) * len(rec.size_ids)
rec.bom_count = self.env['garment.bom'].search_count([('style_id', '=', rec.id)])
rec.order_count = self.env['sale.order'].search_count([('garment_style_id', '=', rec.id)])
rec.production_count = self.env['garment.production.plan'].search_count([('style_id', '=', rec.id)])
def action_generate_product_variants(self):
"""Creates or updates the Odoo Product Template and variants for this Style"""
self.ensure_one()
ProductTmpl = self.env['product.template']
ProductProduct = self.env['product.product']
if not self.product_tmpl_id:
tmpl_vals = {
'name': f"[{self.name}] {self.style_name}",
'type': 'consu',
'uom_id': self.uom_id.id if self.uom_id else self.env.ref('uom.product_uom_unit').id,
'image_1920': self.image_front,
}
if 'uom_po_id' in ProductTmpl._fields and self.uom_id:
tmpl_vals['uom_po_id'] = self.uom_id.id
if 'detailed_type' in ProductTmpl._fields:
tmpl_vals['detailed_type'] = 'product'
tmpl = ProductTmpl.create(tmpl_vals)
self.product_tmpl_id = tmpl.id
created_count = 0
for color in self.color_ids:
for size in self.size_ids:
sku = f"{self.name}-{color.code}-{size.code}"
variant_name = f"{self.style_name} ({color.name} / {size.name})"
existing = ProductProduct.search([('default_code', '=', sku)], limit=1)
if not existing:
ProductProduct.create({
'name': variant_name,
'default_code': sku,
'type': 'consu',
})
created_count += 1
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Product Variants Synchronized'),
'message': _('Successfully generated/verified %d garment product variants for Style %s.') % (created_count, self.name),
'sticky': False,
'type': 'success',
}
}
def action_view_variants(self):
self.ensure_one()
products = self.env['product.product'].search([('default_code', '=like', f"{self.name}-%")])
return {
'name': _('Style Variants (%s)') % self.name,
'type': 'ir.actions.act_window',
'res_model': 'product.product',
'view_mode': 'list,form',
'domain': [('id', 'in', products.ids)],
}
def action_view_boms(self):
self.ensure_one()
return {
'name': _('Garment BOMs for %s') % self.name,
'type': 'ir.actions.act_window',
'res_model': 'garment.bom',
'view_mode': 'list,form',
'domain': [('style_id', '=', self.id)],
'context': {'default_style_id': self.id},
}
def action_view_production(self):
self.ensure_one()
return {
'name': _('Production Plans for %s') % self.name,
'type': 'ir.actions.act_window',
'res_model': 'garment.production.plan',
'view_mode': 'list,form',
'domain': [('style_id', '=', self.id)],
'context': {'default_style_id': self.id},
}
class GarmentStyleMeasurement(models.Model):
_name = 'garment.style.measurement'
_description = 'Garment Point of Measurement (POM) Specification'
_order = 'sequence, pom_name'
style_id = fields.Many2one('garment.style', string='Style', required=True, ondelete='cascade')
sequence = fields.Integer(string='Seq', default=10)
pom_code = fields.Char(string='POM Code')
pom_name = fields.Char(string='Point of Measurement (POM)', required=True)
tolerance_plus = fields.Float(string='Tol (+)', default=0.5)
tolerance_minus = fields.Float(string='Tol (-)', default=0.5)
# Standard Grading Columns (in cm or inches)
val_xs = fields.Float(string='XS')
val_s = fields.Float(string='S')
val_m = fields.Float(string='M')
val_l = fields.Float(string='L')
val_xl = fields.Float(string='XL')
val_xxl = fields.Float(string='XXL')
val_3xl = fields.Float(string='3XL')
unit = fields.Selection([('inch', 'Inches'), ('cm', 'Centimeters')], string='Unit', default='cm')