# -*- 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')