565 lines
24 KiB
Python
565 lines
24 KiB
Python
# -*- 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))
|
|
|