# -*- coding: utf-8 -*- """ Tiruppur Garment Manufacturing ERP - Standalone End-to-End Functional Test Suite Validates the complete lifecycle on Odoo 19 without requiring any demo or static XML data: 1. Dynamic Master Data (Brands, Types, Fabrics, Sizes, Colors, Lines, Defect Types) 2. Garment Style & Product Variant Generation 3. Sales Order with 2D Size x Color Matrix Grid & Sync to order lines 4. Garment BOM & Fabric Consumption (with Cutting Wastage) 5. Fabric Roll Inventory & ASTM D5430 4-Point Inspection Scoring 6. Cutting Order, Lay Planning & Automated Bundle Generation 7. Sewing Line Floor Logging & Hourly Efficiency % Formula 8. Subcontracting / Job Work GST Delivery Challan & Process Loss Audit 9. Quality Control Inspection & Automatic Rework Order Flow 10. Export Carton Packing (Ratio & Solid) with CBM Calculation & Finished Goods Receipt 11. Dispatch Order & Finished Goods Inventory Deduction 12. Pre-Costing Sheet & Commercial Margin Analysis """ import logging from odoo.tests.common import TransactionCase, tagged _logger = logging.getLogger(__name__) @tagged('post_install', '-at_install', 'garment_erp') class TestGarmentERPLifecycle(TransactionCase): @classmethod def setUpClass(cls): super(TestGarmentERPLifecycle, cls).setUpClass() # 1. Dynamic Master Data cls.brand = cls.env['garment.brand'].create({'name': 'Test Garment Brand', 'code': 'T-BRD'}) cls.gtype = cls.env['garment.type'].create({'name': 'T-Shirt', 'code': 'T-TSHIRT', 'category': 'top'}) cls.ftype = cls.env['garment.fabric.type'].create({'name': 'Single Jersey', 'code': 'T-SJ', 'structure': 'single_jersey'}) cls.comp = cls.env['garment.composition'].create({'name': '100% Cotton', 'code': 'T-100COT', 'cotton_pct': 100.0}) cls.color_blk = cls.env['garment.color'].create({'name': 'Test Black', 'code': 'T-BLK', 'color_hex': '#000000'}) cls.color_wht = cls.env['garment.color'].create({'name': 'Test White', 'code': 'T-WHT', 'color_hex': '#FFFFFF'}) cls.size_m = cls.env['garment.size'].create({'name': 'M', 'code': 'M', 'sequence': 2}) cls.size_l = cls.env['garment.size'].create({'name': 'L', 'code': 'L', 'sequence': 3}) cls.line = cls.env['garment.sewing.line'].create({ 'name': 'Test Sewing Line 01', 'code': 'T-LINE-01', 'operator_count': 20, 'helper_count': 4, 'daily_capacity_pcs': 800, }) cls.defect = cls.env['garment.defect.type'].create({ 'name': 'Skip Stitch', 'code': 'DEF-SS-01', 'category': 'stitching', 'severity': 'major', }) # Dynamic Partners cls.buyer = cls.env['res.partner'].create({ 'name': 'Test Buyer International Ltd', 'customer_rank': 1, }) cls.supplier = cls.env['res.partner'].create({ 'name': 'Test Subcontractor Dyeing & Washing', 'supplier_rank': 1, }) # Raw Material Product cls.raw_fabric = cls.env['product.product'].create({ 'name': 'Test Single Jersey 180 GSM Fabric', 'type': 'consu', }) # 2. Dynamic Garment Style cls.style = cls.env['garment.style'].create({ 'name': 'TEST-STYLE-001', 'style_name': 'Test Crew Neck T-Shirt', 'garment_type_id': cls.gtype.id, 'fabric_type_id': cls.ftype.id, 'composition_id': cls.comp.id, 'brand_id': cls.brand.id, 'sam': 12.0, 'gsm': 180, 'color_ids': [(6, 0, [cls.color_blk.id, cls.color_wht.id])], 'size_ids': [(6, 0, [cls.size_m.id, cls.size_l.id])], }) def test_01_dynamic_master_data(self): """Verify dynamic master data creation and attributes""" self.assertEqual(self.color_blk.code, 'T-BLK') self.assertEqual(self.size_m.code, 'M') self.assertEqual(self.line.operator_count, 20) self.assertEqual(self.defect.category, 'stitching') _logger.info("PASS: Test 01 - Dynamic Master Data verified successfully.") def test_02_garment_style_and_variants(self): """Test garment style and automatic variant generation""" self.assertEqual(self.style.sam, 12.0, "Sewing SAM should be 12.0 mins") self.style.action_generate_product_variants() self.assertTrue(self.style.variant_count >= 4, "4 product variants (2 colors x 2 sizes) should be generated") _logger.info("PASS: Test 02 - Garment Style & Variants (%s variants) verified.", self.style.variant_count) def test_03_sales_matrix_sync(self): """Test sales order matrix lines synchronization with standard sale.order.line""" so = self.env['sale.order'].create({ 'partner_id': self.buyer.id, 'buyer_po_ref': 'PO-TEST-1001', 'garment_style_id': self.style.id, 'order_matrix_line_ids': [ (0, 0, { 'color_id': self.color_blk.id, 'size_id': self.size_m.id, 'quantity': 500, 'unit_price': 150.0, }), (0, 0, { 'color_id': self.color_blk.id, 'size_id': self.size_l.id, 'quantity': 500, 'unit_price': 150.0, }), ] }) self.assertEqual(so.total_garment_qty, 1000, "Total quantity in matrix should be 1,000 pcs") # Verify sync action so.action_sync_matrix_to_order_lines() self.assertEqual(len(so.order_line), 2, "Order lines should be synchronized from matrix") self.assertEqual(sum(so.order_line.mapped('product_uom_qty')), 1000.0) _logger.info("PASS: Test 03 - Sales Order 2D Matrix & Sync verified.") def test_04_garment_bom(self): """Test Garment BOM fabric consumption & labor costs""" bom = self.env['garment.bom'].create({ 'style_id': self.style.id, 'fabric_line_ids': [ (0, 0, { 'component_type': 'body', 'product_id': self.raw_fabric.id, 'consumption_kg': 0.22, 'wastage_pct': 5.0, 'cost_per_kg': 320.0, }) ], 'operation_line_ids': [ (0, 0, { 'operation_name': 'Sewing Assembly', 'department': 'sewing', 'machine_type': 'snls', 'sam': 12.0, 'minute_rate': 2.5, }) ] }) self.assertTrue(bom.total_fabric_cost > 0, "Total fabric cost must be calculated") self.assertTrue(bom.total_unit_cost > 0, "Total unit cost must include fabric and operations") _logger.info("PASS: Test 04 - Garment BOM consumption (Total Cost: %.2f INR) verified.", bom.total_unit_cost) def test_05_fabric_roll_4point_inspection(self): """Test Fabric Roll 4-Point System score calculation and pass/fail verdict""" new_roll = self.env['garment.fabric.roll'].create({ 'product_id': self.raw_fabric.id, 'shade_lot': 'LOT-TEST-01', 'gross_weight': 25.0, 'tare_weight': 0.8, 'length_meters': 80.0, 'width': 30.0, 'gsm': 180, }) self.assertEqual(new_roll.net_weight, 24.2) # Inspection: 4-Point formula inspection = self.env['garment.fabric.inspection'].create({ 'roll_id': new_roll.id, 'inspected_meters': 30.0, 'actual_width': 30.0, 'defects_1pt_count': 2, 'defects_2pt_count': 1, 'defects_3pt_count': 0, 'defects_4pt_count': 0, }) self.assertEqual(inspection.total_defect_points, 4) # 2*1 + 1*2 = 4 self.assertTrue(inspection.score_per_100_sqm > 0) inspection.action_pass() self.assertEqual(inspection.state, 'passed') self.assertEqual(new_roll.status, 'approved') _logger.info("PASS: Test 05 - 4-Point Inspection (Score: %.2f pts/100m², Verdict: %s) verified.", inspection.score_per_100_sqm, inspection.state) def test_06_cutting_and_bundle_generation(self): """Test Cutting Plan lay plies and automatic bundle generation""" prod_plan = self.env['garment.production.plan'].create({ 'style_id': self.style.id, 'planned_qty': 400, 'date_start': '2026-09-20', }) cut = self.env['garment.cutting.plan'].create({ 'production_plan_id': prod_plan.id, 'color_id': self.color_blk.id, 'marker_no': 'MKR-TEST-01', 'marker_length': 6.2, 'plies_count': 100, 'bundle_size': 25, 'ratio_line_ids': [ (0, 0, {'size_id': self.size_m.id, 'ratio': 2}), (0, 0, {'size_id': self.size_l.id, 'ratio': 2}), ] }) # 100 plies * (2 + 2) ratio = 400 pieces planned self.assertEqual(cut.planned_cut_qty, 400) # Generate bundles cut.action_generate_bundles() self.assertEqual(len(cut.bundle_ids), 16) bundle = cut.bundle_ids[0] self.assertTrue(bundle.barcode, "Bundle must have a barcode") self.assertEqual(bundle.stage, 'sewing') _logger.info("PASS: Test 06 - Cutting Plan & %s Serialized Bundles generated.", len(cut.bundle_ids)) def test_07_sewing_line_hourly_efficiency(self): """Test Sewing Line 8-Hour Floor Production logging and efficiency % formula""" log = self.env['garment.sewing.hourly'].create({ 'line_id': self.line.id, 'style_id': self.style.id, 'sam': 12.0, 'operators_present': 20, 'total_worked_hours': 8.0, 'h1_actual': 100, 'h2_actual': 105, 'h3_actual': 110, 'h4_actual': 115, 'h5_actual': 105, 'h6_actual': 100, 'h7_actual': 95, 'h8_actual': 90, }) self.assertEqual(log.total_actual, 820) # Efficiency % formula: (Actual Output * SAM) / (Operators * Hours * 60) * 100 expected_eff = round((820 * 12.0) / (20 * 8.0 * 60.0) * 100.0, 2) self.assertEqual(log.efficiency_pct, expected_eff) _logger.info("PASS: Test 07 - Sewing Line Output: %s pcs, Efficiency: %.2f%% verified.", log.total_actual, log.efficiency_pct) def test_08_jobwork_delivery_challan_process_loss(self): """Test Subcontracting / Job Work GST DC and Process Loss % Audit""" jw = self.env['garment.jobwork.order'].create({ 'vendor_id': self.supplier.id, 'process_type': 'dyeing', 'rate_per_unit': 45.0, 'allowed_process_loss_pct': 4.0, 'sent_line_ids': [(0, 0, { 'description': 'Greige Single Jersey Fabric for Bio-Polishing & Dyeing', 'quantity': 1000.0, 'uom': 'Kg', })], }) self.assertEqual(jw.total_sent_qty, 1000.0) jw.action_dispatch_material() self.assertEqual(jw.state, 'sent') # Record inward receipt (990 kg received, 10 kg rejected) jw.write({ 'inward_line_ids': [(0, 0, { 'vendor_dc_no': 'DC-VENDOR-99', 'received_qty': 990.0, 'rejected_qty': 10.0, })], 'state': 'completed', }) self.assertEqual(jw.total_received_qty, 990.0) self.assertEqual(jw.total_accepted_qty, 980.0) # Process loss: (1000 - 990) / 1000 * 100 = 1.0% self.assertEqual(jw.actual_process_loss_pct, 1.0) _logger.info("PASS: Test 08 - Job Work Subcontracting & Process Loss (%.1f%%) verified.", jw.actual_process_loss_pct) def test_09_quality_control_automatic_rework(self): """Test Quality Inspection failure and automatic Rework Order creation""" qc = self.env['garment.quality.inspection'].create({ 'inspection_stage': 'sewing_endline', 'sample_size': 50, 'passed_qty': 46, 'failed_qty': 4, 'rework_qty': 4, 'verdict': 'rework_needed', 'defect_line_ids': [(0, 0, { 'defect_type_id': self.defect.id, 'defect_count': 4, })], }) self.assertEqual(qc.failed_qty, 4) # Trigger automatic rework order qc.action_trigger_rework() self.assertTrue(qc.rework_count > 0, "Rework order must be automatically created") rework = qc.rework_order_ids[0] self.assertEqual(rework.rework_qty, 4) self.assertEqual(rework.state, 'assigned') # Run rework workflow rework.action_start_rework() self.assertEqual(rework.state, 'in_rework') rework.action_ready_recheck() self.assertEqual(rework.state, 'ready_recheck') rework.action_pass_recheck() self.assertEqual(rework.state, 'passed') self.assertEqual(rework.passed_rework_qty, 4) _logger.info("PASS: Test 09 - Quality Inspection & Auto Rework (Passed 4 pcs) verified.") def test_10_carton_packing_finished_goods_dispatch(self): """Test Ratio Carton packing, CBM, Finished Goods receipt, and Dispatch deduction""" prod_plan = self.env['garment.production.plan'].create({ 'style_id': self.style.id, 'planned_qty': 100, }) # Create packing plan plan = self.env['garment.packing.plan'].create({ 'production_plan_id': prod_plan.id, 'style_id': self.style.id, 'packing_type': 'solid_color_solid_size', 'target_pack_qty': 100, }) carton = self.env['garment.carton'].create({ 'packing_plan_id': plan.id, 'carton_no': 1, 'length_cm': 60.0, 'width_cm': 40.0, 'height_cm': 30.0, 'line_ids': [(0, 0, { 'color_id': self.color_blk.id, 'size_id': self.size_m.id, 'quantity': 100, })] }) self.assertEqual(carton.total_pieces, 100) # CBM: (60 * 40 * 30) / 1,000,000 = 0.072 m³ self.assertEqual(carton.cbm, 0.072) self.assertEqual(plan.total_packed_qty, 100) # Transfer to Finished Goods plan.action_transfer_to_finished_goods() self.assertEqual(plan.state, 'completed') fg = self.env['garment.finished.goods'].search([ ('style_id', '=', self.style.id), ('color_id', '=', self.color_blk.id), ('size_id', '=', self.size_m.id), ], limit=1) self.assertTrue(fg, "Finished Goods stock record must exist") self.assertEqual(fg.on_hand_qty, 100) # Create Dispatch dispatch = self.env['garment.dispatch'].create({ 'customer_id': self.buyer.id, 'carrier_name': 'Maersk Line', 'container_no': 'MSKU-889900-1', 'seal_no': 'SL-77661', }) carton.dispatch_id = dispatch.id self.assertEqual(dispatch.total_pieces, 100) self.assertEqual(dispatch.total_cartons, 1) # Dispatch shipment dispatch.action_dispatch() self.assertEqual(dispatch.state, 'dispatched') # Check stock deduction fg.invalidate_recordset() self.assertEqual(fg.shipped_qty, 100) self.assertEqual(fg.on_hand_qty, 0) _logger.info("PASS: Test 10 - Export Packing (CBM: %.3f m³), FG & Dispatch verified.", carton.cbm) def test_11_garment_costing_margins(self): """Test Garment Costing Sheet and Profit Margin % calculation""" costing = self.env['garment.costing'].create({ 'style_id': self.style.id, 'target_selling_price': 180.0, 'fabric_cost_per_pc': 65.0, 'trims_accessories_cost_per_pc': 18.0, 'sewing_cm_cost_per_pc': 28.0, }) self.assertEqual(costing.target_selling_price, 180.0) self.assertTrue(costing.total_cost_per_pc > 0, "Total cost must be calculated") self.assertTrue(costing.gross_margin_amount > 0, "Gross margin amount must be > 0") self.assertTrue(costing.gross_margin_pct > 0, "Gross margin % must be > 0") _logger.info("PASS: Test 11 - Garment Costing (Cost: %.2f INR, Margin: %.2f%%) verified.", costing.total_cost_per_pc, costing.gross_margin_pct) def test_12_garment_mrp_requirements(self): """Test MRP Material Explosion, Shortage Calculation and Purchase RFQ Generation""" prod_plan = self.env['garment.production.plan'].create({ 'style_id': self.style.id, 'planned_qty': 1000, 'date_start': '2026-09-20', 'date_delivery': '2026-10-05', }) bom = self.env['garment.bom'].create({ 'style_id': self.style.id, 'base_qty': 1.0, 'fabric_line_ids': [ (0, 0, { 'component_type': 'body', 'product_id': self.raw_fabric.id, 'consumption_kg': 0.22, 'wastage_pct': 5.0, 'cost_per_kg': 320.0, }) ], 'trim_line_ids': [ (0, 0, { 'item_name': 'Woven Main Label', 'unit_cost': 1.5, 'consumption_qty': 1.0, 'wastage_pct': 2.0, }) ] }) mrp = self.env['garment.mrp.requirement'].create({ 'production_plan_id': prod_plan.id, 'bom_id': bom.id, 'order_qty': 1000, }) mrp.action_compute_requirements() self.assertEqual(mrp.state, 'calculated') self.assertTrue(len(mrp.line_ids) >= 2, "MRP must have lines for fabric and trim") self.assertTrue(mrp.total_shortage_cost > 0, "Shortage cost must be computed") # Generate Purchase RFQs mrp.action_generate_purchase_rfqs() self.assertEqual(mrp.state, 'rfq_created') self.assertTrue(mrp.purchase_count > 0, "At least 1 Purchase RFQ must be generated") po = mrp.purchase_order_ids[0] self.assertEqual(po.state, 'draft') _logger.info("PASS: Test 12 - MRP Requirements (%s lines, RFQ %s) verified.", len(mrp.line_ids), po.name) def test_13_live_factory_dashboard_kpis(self): """Test Live Factory Dashboard KPI Aggregations""" dashboard = self.env['garment.dashboard'].create({'name': 'Test Control Room'}) dashboard._compute_kpis() self.assertIsNotNone(dashboard.total_sales_orders) self.assertIsNotNone(dashboard.avg_line_efficiency) self.assertIsNotNone(dashboard.total_fabric_stock_kg) _logger.info("PASS: Test 13 - Factory Dashboard KPIs verified.") def test_14_all_9_qweb_reports_rendering(self): """Test rendering of all 9 industrial QWeb PDF report templates""" # 1. Costing costing = self.env['garment.costing'].create({ 'style_id': self.style.id, 'target_selling_price': 220.0, 'fabric_cost_per_pc': 75.0, 'trims_accessories_cost_per_pc': 22.0, 'sewing_cm_cost_per_pc': 35.0, 'washing_cost_per_pc': 12.0, 'factory_overhead_pct': 8.0, }) # 2. Sales Order with Matrix so = self.env['sale.order'].create({ 'partner_id': self.buyer.id, 'garment_style_id': self.style.id, 'buyer_po_ref': 'PO-TEST-REP-01', 'order_matrix_line_ids': [ (0, 0, {'color_id': self.color_blk.id, 'size_id': self.size_m.id, 'quantity': 200, 'unit_price': 200.0}), ] }) so.action_sync_matrix_to_order_lines() # 3. Production Plan prod_plan = self.env['garment.production.plan'].create({ 'style_id': self.style.id, 'planned_qty': 400, 'date_start': '2026-09-20', 'date_delivery': '2026-10-05', }) # 4. Cutting Plan & Bundles cut = self.env['garment.cutting.plan'].create({ 'production_plan_id': prod_plan.id, 'color_id': self.color_blk.id, 'marker_no': 'MKR-REP-01', 'marker_length': 6.0, 'plies_count': 50, 'bundle_size': 25, 'ratio_line_ids': [ (0, 0, {'size_id': self.size_m.id, 'ratio': 2}), ] }) cut.action_generate_bundles() bundle = cut.bundle_ids[0] # 5. Fabric Roll & Inspection roll = self.env['garment.fabric.roll'].create({ 'product_id': self.raw_fabric.id, 'shade_lot': 'LOT-REP-01', 'gross_weight': 25.0, 'tare_weight': 0.8, 'length_meters': 80.0, 'width': 30.0, 'gsm': 180, }) insp = self.env['garment.fabric.inspection'].create({ 'roll_id': roll.id, 'inspected_meters': 25.0, 'actual_width': 30.0, 'defects_1pt_count': 1, }) insp.action_pass() # 6. Job Work Order jw = self.env['garment.jobwork.order'].create({ 'vendor_id': self.supplier.id, 'process_type': 'washing', 'rate_per_unit': 15.0, 'sent_line_ids': [(0, 0, {'description': 'Washing batch', 'quantity': 500.0, 'uom': 'Pcs'})], }) # 7. Quality Inspection qc = self.env['garment.quality.inspection'].create({ 'inspection_stage': 'sewing_endline', 'sample_size': 50, 'passed_qty': 48, 'failed_qty': 2, 'defect_line_ids': [(0, 0, {'defect_type_id': self.defect.id, 'defect_count': 2})], }) # 8. Packing Plan pack = self.env['garment.packing.plan'].create({ 'production_plan_id': prod_plan.id, 'style_id': self.style.id, 'packing_type': 'solid_color_assorted_size', 'target_pack_qty': 200, }) carton = self.env['garment.carton'].create({ 'packing_plan_id': pack.id, 'carton_no': 1, 'length_cm': 60.0, 'width_cm': 40.0, 'height_cm': 30.0, 'line_ids': [(0, 0, {'color_id': self.color_blk.id, 'size_id': self.size_m.id, 'quantity': 50})] }) # Verify all 9 reports reports = [ ('garment_erp.report_sales_order_matrix_template', so), ('garment_erp.report_production_plan_template', prod_plan), ('garment_erp.report_bundle_barcode_template', bundle), ('garment_erp.report_fabric_inspection_template', insp), ('garment_erp.report_cutting_plan_template', cut), ('garment_erp.report_job_work_challan_template', jw), ('garment_erp.report_quality_inspection_template', qc), ('garment_erp.report_packing_list_template', pack), ('garment_erp.report_garment_costing_template', costing), ] for rep_xmlid, record in reports: report_action = self.env['ir.actions.report'].search([('report_name', '=', rep_xmlid)], limit=1) self.assertTrue(report_action, f"Report action {rep_xmlid} must exist") html = report_action._render_qweb_html(report_action.id, [record.id])[0] self.assertTrue(len(html) > 500, f"Rendered HTML for {rep_xmlid} must be non-empty") _logger.info("PASS: Report %s successfully rendered (%d bytes).", rep_xmlid, len(html))