commit c365322e3d77b7a2c1ab441d744de8ca448eb8cc Author: Alaguraj0361 Date: Mon Sep 21 16:43:23 2026 +0530 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de710c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# OS / Editor artifacts +.DS_Store +Thumbs.db +.vscode/ +.idea/ +*.swp +*.swo + +# Logs +*.log diff --git a/WORKFLOW_GUIDELINE.md b/WORKFLOW_GUIDELINE.md new file mode 100644 index 0000000..134c2b2 --- /dev/null +++ b/WORKFLOW_GUIDELINE.md @@ -0,0 +1,427 @@ +# Tiruppur Garment Manufacturing ERP — Complete Step-by-Step Workflow Guide +**Odoo 19 Community / Enterprise Edition** +*Industrial Apparel & Knitwear ERP Suite for Tiruppur Garment Manufacturers* + +--- + +## Table of Contents +1. [End-to-End Workflow Architecture](#1-end-to-end-workflow-architecture) +2. [Phase 1: Base Masters & Factory Foundation](#phase-1-base-masters--factory-foundation) +3. [Phase 2: Style Master, POM Grading & Tech Pack](#phase-2-style-master-pom-grading--tech-pack) +4. [Phase 3: Garment Pre-Costing & Target Margin](#phase-3-garment-pre-costing--target-margin) +5. [Phase 4: Sales Orders & 2D Color × Size Order Matrix](#phase-4-sales-orders--2d-color--size-order-matrix) +6. [Phase 5: Garment Bill of Materials (BOM) & Routing](#phase-5-garment-bill-of-materials-bom--routing) +7. [Phase 6: Master Production Planning (PP)](#phase-6-master-production-planning-pp) +8. [Phase 7: Material Requirement Planning (MRP) & Purchase RFQs](#phase-7-material-requirement-planning-mrp--purchase-rfqs) +9. [Phase 8: Fabric Roll Inventory & 4-Point Quality Inspection](#phase-8-fabric-roll-inventory--4-point-quality-inspection) +10. [Phase 9: Cutting Room Lay Planning & Marker Ratio](#phase-9-cutting-room-lay-planning--marker-ratio) +11. [Phase 10: Barcode Bundle Generation & Traveler Tracking](#phase-10-barcode-bundle-generation--traveler-tracking) +12. [Phase 11: Sewing Floor & Real-Time Hourly Efficiency Logging](#phase-11-sewing-floor--real-time-hourly-efficiency-logging) +13. [Phase 12: Subcontracting & Job Work (Delivery Challans)](#phase-12-subcontracting--job-work-delivery-challans) +14. [Phase 13: Multi-Stage Quality Control (QC) & Rework Routing](#phase-13-multi-stage-quality-control-qc--rework-routing) +15. [Phase 14: Master Carton Packing, Finished Goods (FG) & Dispatch](#phase-14-master-carton-packing-finished-goods-fg--dispatch) +16. [Control Room: Factory Dashboard & Traceability Explorer](#control-room-factory-dashboard--traceability-explorer) +17. [Index of Standard Print Reports & Barcode Tickets](#index-of-standard-print-reports--barcode-tickets) + +--- + +## 1. End-to-End Workflow Architecture + +```mermaid +flowchart TD + M1[1. Base Masters: Colors, Sizes, Fabrics, Lines] --> S1[2. Garment Style & Tech Pack] + S1 --> C1[3. Pre-Costing Sheet] + S1 --> SO[4. Sales Order & 2D Matrix Grid] + SO --> PP[5. Production Master Plan] + PP --> BOM[6. Garment BOM & SAM Routing] + PP --> MRP[7. MRP & Shortage Purchase RFQ] + MRP --> FR[8. Fabric Roll Inward & 4-Point QC] + FR --> CP[9. Cutting Plan & Lay Ratio] + CP --> BND[10. Barcoded Bundle Tickets] + BND --> SEW[11. Sewing Line & Hourly Efficiency] + BND -.-> JW[12. Job Work Subcontracting: Dyeing/Print/Embroidery] + SEW --> QC[13. Endline QC & Rework Routing] + QC --> RW[13b. Rework Order & Repair] + RW --> QC + QC --> PCK[14. Export Carton Packing] + PCK --> FG[15. Finished Goods Inventory] + FG --> DSP[16. Dispatch & Shipping Logistics] +``` + +--- + +## Phase 1: Base Masters & Factory Foundation + +Configure these fundamental building blocks before entering transactions: + +* **Menu Path**: `Garment ERP > Configuration` + +| Master Name | Technical Model | Purpose & Required Settings | +| :--- | :--- | :--- | +| **Garment Colors** | `garment.color` | Name, unique color code, color family group, Pantone reference (TPX/TCX), and Hex swatch code (e.g., `#000000` for Black, `#1E3A8A` for Navy). | +| **Garment Sizes** | `garment.size` | Size code (e.g., `XS`, `S`, `M`, `L`, `XL`, `2XL`), size category (Adult, Youth, Infant), and display sequence (controls column ordering in the 2D matrix). | +| **Fabric Knit Types** | `garment.fabric.type` | Knit structures (Single Jersey, 1x1 Rib, 2x2 Rib, Interlock, Pique Polo, Fleece, French Terry, Waffle) with target GSM ranges. | +| **Compositions** | `garment.composition` | Yarn composition percentages (e.g., 100% Cotton, 60/40 CVC, 95/5 Cotton Lycra). | +| **Brands & Seasons** | `garment.brand`, `garment.season` | Brand identities, logos, and seasonal milestones (e.g., `SS-2026`, `AW-2026`). | +| **Garment Types** | `garment.type` | Top wear, Bottom wear, Outerwear, Activewear, Innerwear. | +| **Defect Types** | `garment.defect.type` | Under `Quality & Rework > Defect Categories Master`. Stitching, Fabric, Measurement, Print/Embroidery, and Finishing defects with severity levels (Minor, Major, Critical). | +| **Sewing Lines** | `garment.sewing.line` | Under `Sewing Floor > Sewing Lines Configuration`. Line code, supervisor, operator count, helper count, daily target capacity. | + +--- + +## Phase 2: Style Master, POM Grading & Tech Pack + +The Style Master represents the garment's design, Point of Measurement (POM) grading chart, and SKU generator. + +* **Menu Path**: `Garment ERP > Merchandising > Garment Styles` + +### Step-by-Step Procedure: +1. Click **New** and enter: + * **Style Code**: Internal style code (e.g., `TSH-1001`). + * **Style Description**: Full commercial title (e.g., *Men's Crew Neck Bio-Washed T-Shirt*). + * **Classification**: Select Brand, Season, Gender, Garment Type, and Buyer. + * **Tiruppur Fabric Specs**: + * **Knit Structure**: e.g., Single Jersey. + * **Fabric Composition**: e.g., 100% Combed Cotton. + * **Fabric GSM**: e.g., `180` g/m². + * **Fabric Width**: e.g., `30.0` inches. + * **Width Type**: Select `Tubular / Dia` (Circular Knit) or `Open Width` (Slit & Stentered). + * **Shrinkage %**: Enter Length & Width expected shrinkage (default 4.0%). + * **Yarn Count**: e.g., `30s Combed`. + * **Industrial Engineering (IE)**: + * **Sewing SAM**: Standard Allowed Minutes (e.g., `12.0` min). + * **Total SMV**: Total Standard Minute Value including cutting and finishing (e.g., `18.0` min). + * **Colors & Sizes**: Pick all active colors and sizes for this style. + * **Media & Tech Pack**: Upload front/back sketch photos and attach the buyer's Tech Pack PDF. + * **Point of Measurement (POM) Specifications**: + * Add measurement lines: Chest Width, Body Length, Across Shoulder, Sleeve Length, Neck Drop, Bottom Hem, etc. + * Define `Tol (+)` and `Tol (-)` tolerances. + * Enter graded measurements across XS, S, M, L, XL, XXL, 3XL in cm or inches. +2. Click **Generate Product Variants** (Header button): + * Automatically generates standard Odoo `product.product` records with the SKU structure `[STYLE]-[COLOR]-[SIZE]` (e.g., `TSH-1001-BLK-M`). + * Product variants are immediately available for inventory tracking and BOM linking. + +--- + +## Phase 3: Garment Pre-Costing & Target Margin + +Calculate cost per piece, cost per dozen, overheads, and target profit margins. + +* **Menu Path**: `Garment ERP > Merchandising > Garment Pre & Post Costing` + +### Step-by-Step Procedure: +1. Click **New**, select the **Garment Style**, **Buyer**, and enter **Target Order Quantity** (e.g., `10,000` Pcs). +2. Set **Costing Stage**: Select `Pre-Costing (Quotation / Estimate)`. +3. If an approved BOM exists, click **Load from BOM** to pull fabric, trim, and labor costs automatically; otherwise enter: + * **Raw Materials**: Fabric cost / pc + Dyeing & Finishing cost / pc. + * **Trims & Accessories**: Labels, tags, polybags, cartons, threads. + * **Value Addition**: Chest print / Rotary print / Embroidery / Bio-washing. + * **Manufacturing CM (Cut & Make)**: Cutting labor / pc + Sewing CM labor / pc. + * **Packaging & Freight**: Polybags & Cartons / pc + Local freight & Port handling / pc. + * **Factory Overheads %**: e.g., `8.0%`. +4. Review the auto-computed metrics: + * **Total Garment Cost / Pc** + * **Total Garment Cost / Dozen** + * **Total Order Cost** +5. Enter **Target Selling Price / Pc** (e.g., `₹180.00`). +6. Inspect the auto-computed **Gross Margin / Pc**, **Gross Margin %**, and **Total Expected Profit**. + +--- + +## Phase 4: Sales Orders & 2D Color × Size Order Matrix + +Handle customer orders with multi-dimensional Color × Size distribution. + +* **Menu Path**: `Garment ERP > Merchandising > Garment Sales Orders` + +### Step-by-Step Procedure: +1. Click **New**, pick the **Buyer / Customer**, and ensure **Garment Manufacturing Order** is checked. +2. Select the **Garment Style** (this auto-fills Season, Customer Style Ref, and Buyer). +3. Set **Buyer PO Reference**, **Target Shipment Date**, **Ex-Factory Date**, and **Shipment Mode** (Sea, Air, Road). +4. Navigate to the **Size-Color Order Matrix** tab: + * Add lines selecting Color, Size, Quantity, and Unit Price. + * The live **HTML Order Matrix Summary** renders the full 2D Color vs Size table with color swatches, row subtotals, column subtotals, and total piece count. +5. Click **Sync Matrix to Order Lines** (Header button): + * Creates or updates native Odoo `sale.order.line` records linked to the specific variant SKUs (`product.product`). +6. Click **Confirm** to lock the Sales Order. + +--- + +## Phase 5: Garment Bill of Materials (BOM) & Routing + +Define raw material consumption and process routing per garment unit. + +* **Menu Path**: `Garment ERP > Planning & MRP > Garment BOMs` + +### Step-by-Step Procedure: +1. Click **New**, choose the **Garment Style**, and keep **Base Quantity** as `1.0 Pc`. +2. **Fabric Components Tab**: + * Add components: Body Panels (Front & Back), Neck Rib, Flat Collar, Pocket Panels, Sleeve Cuffs, Plackets, Hoods. + * Specify **Fabric Product**, **Net Consumption (Kg/Pc)** (e.g., `0.1800 Kg`), **Cutting Wastage %** (e.g., `5.0%`), and **Fabric Cost / Kg**. + * The system computes **Gross Consumption (Kg/Pc)** and **Fabric Cost / Pc**. +3. **Trims & Accessories Tab**: + * Add trims: Main Label, Size Label, Wash Care Label, Hang Tag, Price Tag, Polybag, Master Carton Box, Sewing Thread, Buttons, Zippers. + * Specify **Net Qty / Pc**, **Allowance %** (e.g., `3%`), and **Unit Cost**. +4. **Operations & Routing Tab**: + * Add manufacturing operations in sequential order across Cutting, Sewing, Job Work, Washing, Finishing, and Packing. + * Assign machine types (SNLS, 4T Overlock, 5T Overlock, Flatlock, Button Stitch, Steam Iron) with operation SAM and labor minute rate. +5. Save the BOM. The system calculates estimated garment production cost per piece. + +--- + +## Phase 6: Master Production Planning (PP) + +Master Production Planning links customer demand, floor scheduling, and shop floor execution. + +* **Menu Path**: `Garment ERP > Planning & MRP > Production Plans` +*(Or click "Create Production Plan" inside the confirmed Sales Order)* + +### Step-by-Step Procedure: +1. In the Production Plan, verify: + * **Sales Order Ref**, **Garment Style**, and **Garment BOM**. + * **Planned Quantity**, **Production Start Date**, **Target Delivery Date**, and **Priority**. + * The **Color/Size Production Targets** table auto-populates all matrix lines from the sales order. +2. Click **Confirm Plan** $\rightarrow$ Status changes to `Planned / Scheduled`. +3. Click **Start Production** $\rightarrow$ Status changes to `In Production`. +4. Smart buttons track progress in real time: + * **Cutting Orders** + * **Bundles** + * **Job Work Orders** + * **QC Inspections** + * **Packing Plans** +5. Progress bar and stage quantities (**Cut Qty**, **Sewn Qty**, **Packed Qty**, **Dispatched Qty**) update automatically from shop floor execution. + +--- + +## Phase 7: Material Requirement Planning (MRP) & Purchase RFQs + +Calculate gross material requirements, compare against on-hand stock, and generate purchase orders for shortages. + +* **Menu Path**: Inside the Production Plan, click **Calculate Material Requirements (MRP)** +*(Or go to `Garment ERP > Planning & MRP > Material Requirements (MRP)`)* + +### Step-by-Step Procedure: +1. Open the MRP record and click **Compute Material Requirements**: + * Multiplies planned garment pieces by BOM gross fabric consumption (Kg) and trim quantities. + * Compares required quantities against warehouse stock on hand (`qty_available`). + * Determines net **Shortage Quantity** and total procurement cost. +2. Click **Generate Purchase RFQs**: + * Automatically creates draft Purchase Orders (`purchase.order`) for all shortage materials grouped by vendor. + * Links generated RFQs directly to the MRP record for traceability. + +--- + +## Phase 8: Fabric Roll Inventory & 4-Point Quality Inspection + +Track knitted fabric roll inventory and enforce the international 4-point defect grading system before cutting. + +* **Menu Path**: `Garment ERP > Fabric & Stores > Fabric Rolls` + +### Step-by-Step Procedure: +1. **Inward Fabric Rolls**: + * Click **New** and record Roll Number, Fabric Product, Shade/Dyeing Lot #, Actual GSM, Width, Width Type (Tubular/Open Width), Gross Weight (Kg), Tare Weight (Kg), and Fabric Mill Supplier. + * Initial status is `Received / Uninspected`. +2. **Conduct 4-Point Inspection**: + * Click **Create 4-Point Inspection** (or go to `Fabric 4-Point Inspections`). + * Inspector verifies measured GSM, measured Width, and evaluates **Shade Grade** (`Grade A` to `Grade D`). + * Enter defect counts found across the inspected length based on standard 4-point defect rules: + * **1 Point**: Defect $\le$ 3 inches. + * **2 Points**: Defect > 3 up to 6 inches. + * **3 Points**: Defect > 6 up to 9 inches. + * **4 Points**: Defect > 9 inches or any hole. + * The system computes the defect score: + $$\text{Score per 100 } \text{m}^2 = \frac{\text{Total Defect Points} \times 100}{\text{Inspected Length (Meters)} \times \text{Fabric Width (Meters)}}$$ + * **Pass/Fail Decision**: + * If $\text{Score} \le \text{Threshold (default 28 points)}$: Click **Quality Passed** $\rightarrow$ Roll status becomes **Quality Approved (Ready to Cut)**. + * If score exceeds threshold: Click **Quality Failed** $\rightarrow$ Roll status becomes **Quality Rejected (Quarantine)**. + * *Only Quality Approved rolls can be issued to cutting orders.* + +--- + +## Phase 9: Cutting Room Lay Planning & Marker Ratio + +Plan marker dimensions, fabric roll allocation, and cutting ratios. + +* **Menu Path**: Inside the Production Plan, click **Create Cutting Order** +*(Or go to `Garment ERP > Cutting & Bundling > Cutting Orders`)* + +### Step-by-Step Procedure: +1. On the Cutting Plan form: + * Select **Body Fabric Color**. + * Enter **Marker Length (Meters)**, **Marker Width (Inches)**, **Number of Plies / Layers** (e.g., `100`), and **Bundle Size** (default `25` pieces/bundle). + * Review the **Size Cut Ratio** lines (auto-populated from the production targets, e.g., S:1, M:2, L:2, XL:1). + * **Planned Cut Pieces** is computed: $\text{Plies Count} \times \sum(\text{Ratios})$. +2. **Fabric Roll Allocation Tab**: + * Select approved fabric rolls (`garment.fabric.roll`). + * Enter **Weight Issued (Kg)**, **Weight Consumed (Kg)**, and **End-Bit Scrap (Kg)**. + * The system computes **Fabric Wastage %** and deducts consumed weight from the fabric rolls. +3. Click **Start Cutting** $\rightarrow$ Status changes to `In Cutting`. + +--- + +## Phase 10: Barcode Bundle Generation & Traveler Tracking + +Split cut plies into serialized bundle travelers with barcode tickets. + +* **Menu Path**: Inside the Cutting Plan, click **Generate Production Bundles** + +### Step-by-Step Procedure: +1. Clicking **Generate Production Bundles**: + * Divides cut pieces into bundles conforming to the configured **Bundle Size** (e.g., 25 pcs). + * Creates serialized bundle records (`BND-XXXXX`) with ply ranges (e.g., Ply 1–25, Ply 26–50) and unique barcodes. + * Sets Cutting Plan status to `Cut & Bundled`. +2. **Print Bundle Barcode Tickets**: + * Open the **Generated Bundles** smart button, select all bundles, and click **Print > Production Bundle Barcode Tickets**. + * Attach printed tickets to the cut bundles as physical travelers. +3. **Shop Floor Traveler Workflow**: + * As bundles move through manufacturing cells, operators scan or click: + * **Send to Sewing** $\rightarrow$ Stage: `Sewing Assembly` + * **Send to Job Work** $\rightarrow$ Stage: `External Processing` + * **Move to Quality** $\rightarrow$ Stage: `Quality Inspection` + * **Move to Packing** $\rightarrow$ Stage: `Packing Section` + * **Complete Packing** $\rightarrow$ Stage: `Finished Goods / Packed` + * Every movement creates an immutable entry in the bundle's **Operation Tracking History** table. + +--- + +## Phase 11: Sewing Floor & Real-Time Hourly Efficiency Logging + +Track hourly line outputs, defect counts, and operator efficiency. + +* **Menu Path**: `Garment ERP > Sewing Floor > Hourly Production Logs` + +### Step-by-Step Procedure: +1. Click **New** at the start of each production shift: + * Select **Sewing Line**, **Date**, **Shift**, **Garment Style Running**, and **Production Order**. + * Operators present and Style SAM auto-populate from the line and style masters. +2. Record output and defects at the end of each hour: + * Enter **Target**, **Actual Output**, and **Defect / Alteration Count** for hours H1 through H8 (+ OT). +3. Review Live Line Performance: + * The system computes day totals and line efficiency: + $$\text{Line Efficiency \%} = \frac{\text{Total Day Actual Output} \times \text{SAM}}{\text{Operators Present} \times \text{Hours Worked} \times 60} \times 100$$ + * Awards an automated visual badge: + * **Excellent**: $\ge 90\%$ + * **Good**: $75\% - 89\%$ + * **Below Target**: $< 75\%$ + +--- + +## Phase 12: Subcontracting & Job Work (Delivery Challans) + +Manage external operations for knitting, dyeing, chest print, embroidery, or bio-washing. + +* **Menu Path**: `Garment ERP > Subcontracting / Job Work > Job Work Challans (DC)` + +### Step-by-Step Procedure: +1. Click **New** and enter: + * **Job Worker / Subcontractor** partner. + * **Processing Operation**: Knitting, Dyeing, All-Over Print, Chest Print, Embroidery, Washing, Finishing. + * **Rate Basis** (Per Kg / Per Piece) and **Job Work Rate (INR)**. + * **Standard Process Loss %**: Allowed shrinkage/wastage during process (e.g., `4.0%`). + * Transport details: Vehicle Registration No, GST E-Way Bill Number, Expected Inward Date. +2. **Materials Sent Outward Tab**: + * Add fabric rolls or garment bundles with dispatched quantities. + * Click **Dispatch Material (Outward DC)** $\rightarrow$ Status changes to `Material Sent`. + * Print the official GST **Job Work Delivery Challan (DC)** report. +3. **Materials Inward Received Tab**: + * As processed goods return, record Date, Vendor DC No, Received Qty, and Rejected Qty. + * The system calculates **Accepted Good Qty**, **Pending Balance Qty**, **Actual Process Loss %**, and **Total Job Work Cost**. +4. Click **Complete Job Work** once all materials are returned. + +--- + +## Phase 13: Multi-Stage Quality Control (QC) & Rework Routing + +Inspect garments at key control gates and route defects for alteration. + +* **Menu Path**: `Garment ERP > Quality & Rework > Quality Inspections (QC)` + +### Step-by-Step Procedure: +1. Click **New** and choose the **Inspection Stage**: + * `Fabric Inspection` + * `Cutting Section QC` + * `Sewing Inline Roaming QC` + * `Sewing Endline 100% Checking` + * `Job Work Inward QC` + * `Final AQL 2.5 Pre-Shipment Audit` +2. Select Production Plan, Garment Style, Color, Size, Bundle Ref, and Sewing Line. +3. Enter **Lot Quantity**, **Sample Quantity Checked**, **Passed Quantity**, and **Failed Quantity**. +4. Under **Defect Details**, log identified flaws from the Defect Master (e.g., Broken Stitch, Skipped Stitch, Oil Spot, Needle Hole, Measurement Variance). +5. **Inspection Verdict**: + * **Pass**: Click **Pass Inspection** $\rightarrow$ Status updates to `Quality Approved`. + * **Rework**: Click **Trigger Rework Order**: + * Creates a linked `garment.rework.order` record. + * Verdict sets to `Rework Required`. +6. **Rework Order Execution** (`Quality & Rework > Rework Orders`): + * Responsible section assigned: Sewing alteration, Cutting panel replacement, Spot cleaning, or Job worker re-processing. + * Workflow stages: `Assigned` $\rightarrow$ `In Rework` $\rightarrow$ `Ready for QA Re-check` $\rightarrow$ `Passed QA Re-check` (restores pieces to production) or `Irreparable / Scrapped`. + +--- + +## Phase 14: Master Carton Packing, Finished Goods (FG) & Dispatch + +Pack garments into shipping cartons, update finished goods inventory, and dispatch. + +### Part A: Carton Packing Plan +* **Menu Path**: `Garment ERP > Packing & Dispatch > Packing Plans` +1. Click **New**, select **Production Plan**, **Garment Style**, and **Carton Packing Method**: + * `Solid Color / Solid Size` (Single SKU per carton). + * `Solid Color / Assorted Size` (Ratio pack, e.g., 1S:2M:2L:1XL). + * `Assorted Color / Assorted Size` (Pre-pack mix). +2. Under **Master Cartons**, add cartons: + * Enter box dimensions: Length (cm), Width (cm), Height (cm). The system computes **CBM ($m^3$)**: + $$\text{CBM} = \frac{\text{Length} \times \text{Width} \times \text{Height}}{1,000,000}$$ + * Enter Net Weight & Gross Weight. + * Under **Carton Breakdown**, enter the color and size pieces packed in each box. +3. Click **Transfer to Finished Goods**: + * Updates **Finished Goods Inventory** (`garment.finished.goods`) by variant SKU with on-hand quantities. Status updates to `Packed & Transferred to FG`. + +### Part B: Dispatch & Shipping Logistics +* **Menu Path**: `Garment ERP > Packing & Dispatch > Dispatch & Shipments` +1. Click **New**, select **Buyer / Consignee**, **Sales Order**, and **Production Plan**. +2. Enter logistics info: Carrier / Shipping Line, B/L or Air Waybill No, Container No, Customs Seal No, Truck No, Port of Loading, and Port of Discharge. +3. In **Dispatched Cartons**, select all cartons loaded into the container or truck. + * Total cartons, total garments, total gross weight, and total shipment CBM are computed automatically. +4. Click **Dispatch Shipment**: + * Deducts shipped quantities from Finished Goods inventory and marks the shipment as `In Transit`. +5. Click **Mark Delivered** upon destination arrival. + +--- + +## Control Room: Factory Dashboard & Traceability Explorer + +### Factory Live Dashboard +* **Menu Path**: `Garment ERP > Dashboards > Factory Live Dashboard` + +Monitors real-time shop floor performance across 4 operational domains: +* **Executive KPIs**: Active Garment Orders, Total Booked Pieces, Active Production Plans, Average Production Progress %. +* **Sewing Floor Efficiency**: Active Sewing Lines, Today's Output Target vs Actual, Average Line Efficiency %, Today's Alteration Count. +* **Inventory & Materials**: Fabric Stock (Kg), QC Approved Rolls, Finished Goods Stock (Pcs). +* **Operations & Subcontracting**: Pending Job Work Challans, Open Rework Orders, Cartons Ready for Dispatch. + +### End-to-End Traceability Explorer +* **Menu Path**: `Garment ERP > Dashboards > Traceability Explorer` + +Enter any tracking number (Buyer PO, Production Plan #, Roll Barcode, Bundle QR, or Carton #) to view complete forward and backward traceability: +$$\text{Buyer PO} \longrightarrow \text{Production Plan} \longrightarrow \text{Fabric Rolls} \longrightarrow \text{Cutting Orders} \longrightarrow \text{Bundles} \longrightarrow \text{Sewing Logs} \longrightarrow \text{QC Inspections} \longrightarrow \text{Cartons} \longrightarrow \text{Dispatch}$$ + +--- + +## Index of Standard Print Reports & Barcode Tickets + +All reports are available via the standard **Print** menu on their respective documents: + +| Document / Report | Technical Report ID | Primary Model | Contents | +| :--- | :--- | :--- | :--- | +| **Sales Order Matrix Report** | `report_garment_sales_matrix` | `sale.order` | Buyer PO, Style specs, 2D Color × Size grid, delivery terms. | +| **Production Master Plan** | `report_garment_production_plan` | `garment.production.plan` | Target quantities, style BOM reference, scheduled dates. | +| **Bundle Barcode Tickets** | `report_garment_bundle_barcode` | `garment.bundle` | Printable QR/Barcoded ply bundle stickers for floor tracking. | +| **4-Point Fabric Inspection** | `report_garment_fabric_inspection` | `garment.fabric.inspection` | Defect point scoring sheet and roll approval certificate. | +| **Cutting Lay Plan & Marker** | `report_garment_cutting_plan` | `garment.cutting.plan` | Marker length, ply count, size cut ratio, and roll allocation. | +| **Job Work Delivery Challan** | `report_garment_jobwork_challan` | `garment.jobwork.order` | GST-compliant outward delivery challan (DC) for subcontractors. | +| **Quality Inspection Report** | `report_garment_quality_inspection` | `garment.quality.inspection` | Stage defect breakdown, sample size, and rework instructions. | +| **Export Packing List** | `report_garment_packing_list` | `garment.packing.plan` | Box-by-box breakdown, CBM, weights, and shipping marks. | +| **Garment Costing Sheet** | `report_garment_costing` | `garment.costing` | Pre/Post costing analysis with component-wise margins. | + +--- +*© 2026 Chennora Technologies / Tiruppur Garment Manufacturing ERP Suite for Odoo 19.* diff --git a/addons/garment_erp/__init__.py b/addons/garment_erp/__init__.py new file mode 100644 index 0000000..a0fdc10 --- /dev/null +++ b/addons/garment_erp/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models diff --git a/addons/garment_erp/__manifest__.py b/addons/garment_erp/__manifest__.py new file mode 100644 index 0000000..73156cc --- /dev/null +++ b/addons/garment_erp/__manifest__.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'Tiruppur Garment Manufacturing ERP', + 'version': '19.0.1.0.0', + 'category': 'Manufacturing/Apparel', + 'summary': 'End-to-End Garment Manufacturing ERP for Tiruppur Apparel Industry', + 'sequence': 1, + 'description': """ +Tiruppur Garment Manufacturing ERP for Odoo 19 +=============================================== +A complete, industrial ERP suite specifically built for Tiruppur knitwear & apparel manufacturers. + +Features & Lifecycle Management: +--------------------------------- +1. **Garment Master Data & Style Variants**: + - Garment Style, Brand, Season, Gender, Garment Type + - Knitwear Fabric Specifications (GSM, Tube/Open Width, Shrinkage %, Yarn Count) + - Size & Color Master with Swatches + - Point of Measurement (POM) Specs & Tech Pack management + - Auto-generation of Product Variants (Style + Color + Size) + +2. **Customer & Sales Order Management with Order Matrix**: + - Dynamic 2D Size × Color × Quantity Order Matrix grid + - Real-time HTML matrix calculation and synchronization with standard sale.order.line + - Buyer PO & Customer Style tracking with delivery milestones + +3. **Garment Bill of Materials (BOM) & Material Requisition**: + - Knitwear fabric consumption breakdown (Body, Neck Rib, Collar, Pocket) with cutting wastage % + - Trims & accessories (Labels, Tags, Polybags, Master Cartons, Threads) + - Industrial Engineering SAM routing per operation + - Material requirement calculator per order quantity + +4. **Roll-Wise Fabric Inventory & 4-Point Inspection**: + - Roll number, weight (gross/net/tare), length, GSM, width, shade/dyeing lot + - 4-Point System quality defect scoring (1 to 4 points) with score per 100 sq. meters + - Automated quality approval / quarantine status + +5. **Garment Material Requirement Planning (MRP)**: + - Order quantity vs on-hand fabric and trims comparison + - Shortage calculation and one-click purchase RFQ generation + +6. **Cutting & Lay Planning**: + - Marker length, width, plies/layers, size ratio per ply + - Fabric roll allocation and scrap/end-bit wastage tracking + - One-click automatic Production Bundle generation + +7. **Production Bundle Tracking with Barcode / QR**: + - Serialized bundles with ply range and barcode/QR tickets + - Stage-by-stage traveler through Cutting → Sewing → Job Work → Quality → Packing → Completed + - Full movement history and operator audit trail + +8. **Sewing Line & Hourly Production Efficiency**: + - Line configuration with operator & helper counts + - 8-hour floor logging (Target, Actual, Defects) + - Real-time efficiency % formula: (Output × SAM) / (Operators × Minutes) × 100 + +9. **Tiruppur Subcontracting / Job Work (Delivery Challan)**: + - External processing for Knitting, Dyeing, All-Over/Chest Printing, Embroidery, Bio-washing + - GST-compliant Delivery Challan (DC) with Outward & Inward tracking + - Automated process loss / shrinkage % audit + +10. **Quality Control & Automatic Rework Routing**: + - Stages: Fabric, Cutting, Sewing Inline, Sewing Endline, Job Work Inward, Final AQL + - Defect master classification (Stitching, Fabric, Measurement, Finishing) + - Automatic Rework Order creation on failed audits with repair & QA re-check + +11. **Export Carton Packing & Finished Goods**: + - Solid & Ratio (Assorted) carton packing + - Breakdown by Size × Color per box with CBM and weights + - Automatic stock transfer to Finished Goods inventory by variant + +12. **Dispatch & Delivery**: + - Shipping lines, Carrier, Container #, Seal #, B/L or AWB tracking + - Deduction of finished goods inventory and sales order fulfillment + +13. **Garment Pre-Costing & Post-Costing**: + - Fabric + Trims + CM Labor + Value Addition + Packaging + Overheads = Total Garment Cost + - Target selling price, gross margin amount and percentage + +14. **Live Factory Dashboard & Traceability Explorer**: + - Real-time KPI control room for orders, line efficiency, roll stock, and QC + - Complete forward & backward traceability across all manufacturing stages + - 9+ Professional QWeb PDF Reports and Bundle Barcode Tickets + """, + 'author': 'Chennora Technologies / Tiruppur Apparel ERP', + 'website': 'https://www.chennora.com', + 'license': 'LGPL-3', + 'depends': [ + 'base', + 'mail', + 'sale_management', + 'stock', + 'mrp', + 'purchase', + 'account', + 'hr', + ], + 'data': [ + # Security + 'security/garment_security.xml', + 'security/ir.model.access.csv', + # Sequences + 'data/garment_sequence_data.xml', + # Menus + 'views/garment_menus.xml', + # Views + 'views/garment_base_views.xml', + 'views/garment_style_views.xml', + 'views/sale_order_views.xml', + 'views/garment_bom_views.xml', + 'views/production_plan_views.xml', + 'views/fabric_roll_views.xml', + 'views/fabric_inspection_views.xml', + 'views/mrp_requirement_views.xml', + 'views/cutting_plan_views.xml', + 'views/bundle_views.xml', + 'views/sewing_line_views.xml', + 'views/job_work_views.xml', + 'views/quality_control_views.xml', + 'views/rework_views.xml', + 'views/packing_views.xml', + 'views/finished_goods_views.xml', + 'views/dispatch_views.xml', + 'views/garment_costing_views.xml', + 'views/garment_dashboard_views.xml', + 'views/garment_traceability_views.xml', + # Reports + 'report/garment_report_actions.xml', + 'report/report_sales_matrix_template.xml', + 'report/report_production_plan_template.xml', + 'report/report_bundle_barcode_template.xml', + 'report/report_fabric_inspection_template.xml', + 'report/report_cutting_plan_template.xml', + 'report/report_job_work_challan_template.xml', + 'report/report_quality_inspection_template.xml', + 'report/report_packing_list_template.xml', + 'report/report_garment_costing_template.xml', + ], + 'installable': True, + 'application': True, + 'auto_install': False, +} diff --git a/addons/garment_erp/data/garment_sequence_data.xml b/addons/garment_erp/data/garment_sequence_data.xml new file mode 100644 index 0000000..2a42c89 --- /dev/null +++ b/addons/garment_erp/data/garment_sequence_data.xml @@ -0,0 +1,132 @@ + + + + + + + Garment BOM + garment.bom + BOM/%(year)s/ + 5 + + + + + + Garment Production Plan + garment.production.plan + PP/%(year)s/ + 5 + + + + + + Fabric Roll + garment.fabric.roll + ROLL- + 6 + + + + + + Fabric 4-Point Inspection + garment.fabric.inspection + FINSP/%(year)s/ + 5 + + + + + + Garment MRP Requirement + garment.mrp.requirement + MRP/%(year)s/ + 5 + + + + + + Garment Cutting Order + garment.cutting.plan + CUT/%(year)s/ + 5 + + + + + + Production Bundle + garment.bundle + BND/%(year)s/ + 6 + + + + + + Job Work Delivery Challan + garment.jobwork.order + JW/%(year)s/ + 5 + + + + + + Garment Quality Inspection + garment.quality.inspection + QC/%(year)s/ + 5 + + + + + + Garment Rework Order + garment.rework.order + RWK/%(year)s/ + 5 + + + + + + Garment Packing Plan + garment.packing.plan + PACK/%(year)s/ + 5 + + + + + + Export Master Carton + garment.carton + CTN- + 6 + + + + + + Garment Dispatch Order + garment.dispatch + DISP/%(year)s/ + 5 + + + + + + Garment Cost Sheet + garment.costing + COST/%(year)s/ + 5 + + + + + diff --git a/addons/garment_erp/models/__init__.py b/addons/garment_erp/models/__init__.py new file mode 100644 index 0000000..a74861a --- /dev/null +++ b/addons/garment_erp/models/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from . import garment_base +from . import garment_style +from . import sale_order +from . import garment_bom +from . import production_plan +from . import fabric_roll +from . import fabric_inspection +from . import mrp_requirement +from . import cutting_plan +from . import bundle +from . import sewing_line +from . import job_work +from . import quality_control +from . import rework +from . import packing +from . import finished_goods +from . import dispatch +from . import garment_costing +from . import garment_dashboard diff --git a/addons/garment_erp/models/bundle.py b/addons/garment_erp/models/bundle.py new file mode 100644 index 0000000..b8b7bdf --- /dev/null +++ b/addons/garment_erp/models/bundle.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ + +class GarmentBundle(models.Model): + _name = 'garment.bundle' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _description = 'Production Bundle Ticket' + _order = 'name desc' + + name = fields.Char(string='Bundle Number', required=True, default=lambda self: _('New'), copy=False, tracking=True) + barcode = fields.Char(string='Barcode / QR Code', index=True, copy=False) + + # Parent References + cutting_plan_id = fields.Many2one('garment.cutting.plan', string='Cutting Order', ondelete='cascade', tracking=True) + production_plan_id = fields.Many2one('garment.production.plan', string='Production Plan', required=True, tracking=True) + style_id = fields.Many2one('garment.style', string='Garment Style', required=True) + color_id = fields.Many2one('garment.color', string='Color', required=True) + size_id = fields.Many2one('garment.size', string='Size', required=True) + + # Bundle Contents + quantity = fields.Integer(string='Bundle Quantity (Pcs)', required=True, default=25, tracking=True) + ply_start = fields.Integer(string='Ply Start #', default=1) + ply_end = fields.Integer(string='Ply End #', default=25) + + # Production Routing Stage + stage = fields.Selection([ + ('cutting', '1. Cutting Room'), + ('sewing', '2. Sewing Assembly'), + ('jobwork', '3. External Processing / Job Work'), + ('quality', '4. Quality Inspection'), + ('packing', '5. Packing Section'), + ('completed', '6. Finished Goods / Packed'), + ], string='Production Stage', default='cutting', tracking=True, required=True) + + status = fields.Selection([ + ('ready', 'Ready for Next Operation'), + ('in_process', 'In Process'), + ('on_hold', 'On Hold (Issue / Defect)'), + ('completed', 'Stage Completed'), + ], string='Status', default='ready', tracking=True) + + current_line_id = fields.Many2one('garment.sewing.line', string='Assigned Sewing Line') + current_operator_id = fields.Many2one('res.users', string='Current Operator / Supervisor') + + history_ids = fields.One2many('garment.bundle.history', 'bundle_id', string='Operation Tracking History') + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', _('New')) == _('New'): + vals['name'] = self.env['ir.sequence'].next_by_code('garment.bundle') or _('BND-%s') % fields.Date.today().strftime('%Y%m%d') + if not vals.get('barcode'): + vals['barcode'] = vals['name'] + records = super(GarmentBundle, self).create(vals_list) + for rec in records: + rec._log_history(rec.stage or 'cutting', 'Bundle Created from Cutting Plies') + return records + + def _log_history(self, stage, remarks): + self.ensure_one() + self.env['garment.bundle.history'].create({ + 'bundle_id': self.id, + 'stage': stage, + 'user_id': self.env.user.id, + 'sewing_line_id': self.current_line_id.id if self.current_line_id else False, + 'quantity': self.quantity, + 'remarks': remarks, + }) + + def action_move_to_sewing(self): + self.write({'stage': 'sewing', 'status': 'in_process'}) + self._log_history('sewing', 'Transferred to Sewing Floor') + + def action_move_to_jobwork(self): + self.write({'stage': 'jobwork', 'status': 'in_process'}) + self._log_history('jobwork', 'Sent to External Subcontractor') + + def action_move_to_quality(self): + self.write({'stage': 'quality', 'status': 'ready'}) + self._log_history('quality', 'Delivered to Quality Inspection Table') + + def action_move_to_packing(self): + self.write({'stage': 'packing', 'status': 'ready'}) + self._log_history('packing', 'Quality Passed - Sent to Packing') + + def action_complete_packing(self): + self.write({'stage': 'completed', 'status': 'completed'}) + self._log_history('completed', 'Packed into Master Carton') + + +class GarmentBundleHistory(models.Model): + _name = 'garment.bundle.history' + _description = 'Bundle Movement History' + _order = 'timestamp desc' + + bundle_id = fields.Many2one('garment.bundle', string='Bundle', required=True, ondelete='cascade') + timestamp = fields.Datetime(string='Timestamp', default=fields.Datetime.now, required=True) + stage = fields.Selection([ + ('cutting', 'Cutting'), + ('sewing', 'Sewing'), + ('jobwork', 'Job Work'), + ('quality', 'Quality'), + ('packing', 'Packing'), + ('completed', 'Completed'), + ], string='Stage', required=True) + user_id = fields.Many2one('res.users', string='Scanned / Updated By', default=lambda self: self.env.user) + sewing_line_id = fields.Many2one('garment.sewing.line', string='Sewing Line') + quantity = fields.Integer(string='Quantity Passed') + remarks = fields.Char(string='Remarks / Operation') diff --git a/addons/garment_erp/models/cutting_plan.py b/addons/garment_erp/models/cutting_plan.py new file mode 100644 index 0000000..5ffffd6 --- /dev/null +++ b/addons/garment_erp/models/cutting_plan.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + +class GarmentCuttingPlan(models.Model): + _name = 'garment.cutting.plan' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _description = 'Garment Cutting Plan & Lay Management' + _order = 'name desc' + + name = fields.Char(string='Cutting Order No', required=True, default=lambda self: _('New'), copy=False, tracking=True) + production_plan_id = fields.Many2one('garment.production.plan', string='Production Plan', required=True, tracking=True) + style_id = fields.Many2one('garment.style', string='Garment Style', related='production_plan_id.style_id', readonly=True) + color_id = fields.Many2one('garment.color', string='Body Fabric Color', required=True) + company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company) + + # Lay Planning & Marker + marker_no = fields.Char(string='Marker Code / Reference', default='MKR-01') + marker_length = fields.Float(string='Marker Length (Meters)', default=6.2) + marker_width = fields.Float(string='Marker Width (Inches)', default=30.0) + plies_count = fields.Integer(string='Number of Plies / Layers', default=100, tracking=True) + bundle_size = fields.Integer(string='Bundle Size (Pieces/Bundle)', default=25, help='Standard number of garments per bundle ticket') + + # Quantitative Results + ratio_line_ids = fields.One2many('garment.cutting.ratio.line', 'cutting_plan_id', string='Size Cut Ratio') + roll_line_ids = fields.One2many('garment.cutting.roll.line', 'cutting_plan_id', string='Fabric Rolls Issued') + + planned_cut_qty = fields.Integer(string='Planned Cut Pieces', compute='_compute_planned_cut_qty', store=True) + actual_cut_qty = fields.Integer(string='Actual Cut Pieces', default=0, tracking=True) + + # Wastage Calculations + total_fabric_issued_kg = fields.Float(string='Total Fabric Issued (Kg)', compute='_compute_fabric_usage', store=True) + total_fabric_used_kg = fields.Float(string='Actual Fabric Consumed (Kg)', compute='_compute_fabric_usage', store=True) + end_bit_scrap_kg = fields.Float(string='End-Bits / Remnant Scrap (Kg)', compute='_compute_fabric_usage', store=True) + cutting_wastage_pct = fields.Float(string='Cutting Wastage %', compute='_compute_fabric_usage', store=True) + + # State Workflow + state = fields.Selection([ + ('draft', 'Draft'), + ('planned', 'Lay Planned'), + ('cutting', 'In Cutting'), + ('completed', 'Cut & Bundled'), + ('cancelled', 'Cancelled'), + ], string='Status', default='draft', tracking=True, required=True) + + bundle_ids = fields.One2many('garment.bundle', 'cutting_plan_id', string='Generated Bundles') + bundle_count = fields.Integer(string='Bundles Count', compute='_compute_bundle_count') + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', _('New')) == _('New'): + vals['name'] = self.env['ir.sequence'].next_by_code('garment.cutting.plan') or _('CUT/%s') % fields.Date.today().strftime('%Y%m%d') + plans = super().create(vals_list) + for plan in plans: + if not plan.ratio_line_ids and plan.production_plan_id: + plan._populate_ratios_from_plan() + return plans + + def _populate_ratios_from_plan(self): + self.ensure_one() + matching_lines = self.production_plan_id.line_ids.filtered(lambda l: l.color_id.id == self.color_id.id) + if not matching_lines: + matching_lines = self.production_plan_id.line_ids + lines = [] + for pl in matching_lines: + lines.append((0, 0, { + 'size_id': pl.size_id.id, + 'ratio': 1, + })) + if lines: + self.write({'ratio_line_ids': lines}) + + @api.depends('plies_count', 'ratio_line_ids.ratio') + def _compute_planned_cut_qty(self): + for plan in self: + total_ratio = sum(plan.ratio_line_ids.mapped('ratio')) + plan.planned_cut_qty = plan.plies_count * total_ratio + + @api.depends('roll_line_ids.weight_issued', 'roll_line_ids.weight_used', 'roll_line_ids.end_bit_scrap') + def _compute_fabric_usage(self): + for plan in self: + issued = sum(plan.roll_line_ids.mapped('weight_issued')) + used = sum(plan.roll_line_ids.mapped('weight_used')) + scrap = sum(plan.roll_line_ids.mapped('end_bit_scrap')) + plan.total_fabric_issued_kg = issued + plan.total_fabric_used_kg = used + plan.end_bit_scrap_kg = scrap + if used > 0: + plan.cutting_wastage_pct = round((scrap / (used + scrap)) * 100.0, 2) + else: + plan.cutting_wastage_pct = 0.0 + + @api.depends('bundle_ids') + def _compute_bundle_count(self): + for plan in self: + plan.bundle_count = len(plan.bundle_ids) + + def action_start_cutting(self): + self.write({'state': 'cutting'}) + + def action_generate_bundles(self): + """Generates production bundles based on lay plies and size ratios""" + self.ensure_one() + if not self.ratio_line_ids: + raise UserError(_("Please define Size Cut Ratios before generating bundles.")) + if self.plies_count <= 0: + raise UserError(_("Plies count must be greater than 0.")) + + Bundle = self.env['garment.bundle'] + bundle_size = self.bundle_size or 25 + created_bundles = [] + total_actual = 0 + + for r_line in self.ratio_line_ids: + if r_line.ratio <= 0: + continue + total_pieces_for_size = self.plies_count * r_line.ratio + total_actual += total_pieces_for_size + + # Divide into bundles of bundle_size + remaining = total_pieces_for_size + ply_cursor = 1 + bundle_idx = 1 + while remaining > 0: + cur_bundle_qty = min(bundle_size, remaining) + bundle_vals = { + 'cutting_plan_id': self.id, + 'production_plan_id': self.production_plan_id.id, + 'style_id': self.style_id.id, + 'color_id': self.color_id.id, + 'size_id': r_line.size_id.id, + 'quantity': cur_bundle_qty, + 'ply_start': ply_cursor, + 'ply_end': ply_cursor + cur_bundle_qty - 1, + 'stage': 'sewing', + 'status': 'ready', + } + b = Bundle.create(bundle_vals) + created_bundles.append(b.id) + remaining -= cur_bundle_qty + ply_cursor += cur_bundle_qty + bundle_idx += 1 + + self.write({ + 'actual_cut_qty': total_actual, + 'state': 'completed', + }) + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Bundles Generated'), + 'message': _('Successfully generated %d production bundles for %d cut pieces.') % (len(created_bundles), total_actual), + 'sticky': False, + 'type': 'success', + } + } + + def action_view_bundles(self): + self.ensure_one() + return { + 'name': _('Bundles for %s') % self.name, + 'type': 'ir.actions.act_window', + 'res_model': 'garment.bundle', + 'view_mode': 'list,form', + 'domain': [('cutting_plan_id', '=', self.id)], + } + + +class GarmentCuttingRatioLine(models.Model): + _name = 'garment.cutting.ratio.line' + _description = 'Cutting Size Ratio Line' + _order = 'size_id' + + cutting_plan_id = fields.Many2one('garment.cutting.plan', string='Cutting Plan', required=True, ondelete='cascade') + size_id = fields.Many2one('garment.size', string='Size', required=True) + ratio = fields.Integer(string='Ratio per Ply', default=1, required=True) + expected_cut_qty = fields.Integer(string='Expected Cut Pcs', compute='_compute_expected', store=True) + + @api.depends('ratio', 'cutting_plan_id.plies_count') + def _compute_expected(self): + for line in self: + line.expected_cut_qty = line.ratio * (line.cutting_plan_id.plies_count or 0) + + +class GarmentCuttingRollLine(models.Model): + _name = 'garment.cutting.roll.line' + _description = 'Cutting Fabric Roll Allocation' + + cutting_plan_id = fields.Many2one('garment.cutting.plan', string='Cutting Plan', required=True, ondelete='cascade') + roll_id = fields.Many2one('garment.fabric.roll', string='Fabric Roll', required=True, domain=[('status', 'in', ['approved', 'issued'])]) + shade_lot = fields.Char(string='Shade Lot', related='roll_id.shade_lot', readonly=True) + weight_issued = fields.Float(string='Weight Issued (Kg)', required=True, default=24.0) + weight_used = fields.Float(string='Weight Consumed (Kg)', required=True, default=23.0) + end_bit_scrap = fields.Float(string='End-Bit Scrap (Kg)', default=1.0) + + @api.onchange('roll_id') + def _onchange_roll_id(self): + if self.roll_id: + self.weight_issued = self.roll_id.remaining_weight + self.weight_used = self.roll_id.remaining_weight + + @api.model_create_multi + def create(self, vals_list): + records = super(GarmentCuttingRollLine, self).create(vals_list) + for rec in records: + if rec.roll_id: + rec.roll_id.issued_weight += rec.weight_used + rec.roll_id.scrap_weight += rec.end_bit_scrap + rec.roll_id.status = 'issued' + return records diff --git a/addons/garment_erp/models/dispatch.py b/addons/garment_erp/models/dispatch.py new file mode 100644 index 0000000..bdc4afe --- /dev/null +++ b/addons/garment_erp/models/dispatch.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + +class GarmentDispatch(models.Model): + _name = 'garment.dispatch' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _description = 'Garment Dispatch & Delivery Management' + _order = 'name desc' + + name = fields.Char(string='Dispatch Order No', required=True, default=lambda self: _('New'), copy=False, tracking=True) + customer_id = fields.Many2one('res.partner', string='Buyer / Consignee', required=True, tracking=True) + sale_order_id = fields.Many2one('sale.order', string='Sales Order Ref', tracking=True) + production_plan_id = fields.Many2one('garment.production.plan', string='Production Plan Ref', tracking=True) + company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company) + + # Dates & Logistics + date_dispatch = fields.Date(string='Dispatch Date', default=fields.Date.today, tracking=True) + carrier_name = fields.Char(string='Carrier / Shipping Line') + bl_awb_no = fields.Char(string='B/L or Air Waybill No', tracking=True) + container_no = fields.Char(string='Container No (FCL)') + seal_no = fields.Char(string='Customs Seal No') + vehicle_no = fields.Char(string='Truck / Vehicle Registration No') + port_of_loading = fields.Char(string='Port of Loading', default='Tuticorin / Chennai Port') + port_of_discharge = fields.Char(string='Port of Discharge') + + # Cartons & Metrics + carton_ids = fields.One2many('garment.carton', 'dispatch_id', string='Dispatched Cartons') + total_cartons = fields.Integer(string='Total Cartons', compute='_compute_totals', store=True) + total_pieces = fields.Integer(string='Total Garments Dispatched', compute='_compute_totals', store=True) + total_gross_weight = fields.Float(string='Total Gross Weight (Kg)', compute='_compute_totals', store=True) + total_cbm = fields.Float(string='Total CBM (m³)', compute='_compute_totals', store=True) + + state = fields.Selection([ + ('draft', 'Draft Shipment'), + ('packed', 'Loaded & Ready to Depart'), + ('dispatched', 'Dispatched / In Transit'), + ('delivered', 'Delivered to Customer'), + ('cancelled', 'Cancelled'), + ], string='Status', default='draft', tracking=True, required=True) + notes = fields.Text(string='Shipping Instructions / Marks & Numbers') + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', _('New')) == _('New'): + vals['name'] = self.env['ir.sequence'].next_by_code('garment.dispatch') or _('DISP/%s') % fields.Date.today().strftime('%Y%m%d') + return super(GarmentDispatch, self).create(vals_list) + + @api.depends('carton_ids.total_pieces', 'carton_ids.gross_weight_kg', 'carton_ids.cbm') + def _compute_totals(self): + for rec in self: + rec.total_cartons = len(rec.carton_ids) + rec.total_pieces = sum(rec.carton_ids.mapped('total_pieces')) + rec.total_gross_weight = round(sum(rec.carton_ids.mapped('gross_weight_kg')), 2) + rec.total_cbm = round(sum(rec.carton_ids.mapped('cbm')), 3) + + def action_dispatch(self): + """Dispatches the shipment, deducts Finished Goods inventory""" + self.ensure_one() + if not self.carton_ids: + raise UserError(_("Please attach cartons to dispatch.")) + + FG = self.env['garment.finished.goods'] + for ctn in self.carton_ids: + for line in ctn.line_ids: + fg = FG.search([ + ('style_id', '=', ctn.style_id.id), + ('color_id', '=', line.color_id.id), + ('size_id', '=', line.size_id.id), + ], limit=1) + if fg: + fg.write({ + 'on_hand_qty': max(0, fg.on_hand_qty - line.quantity), + 'shipped_qty': fg.shipped_qty + line.quantity, + }) + + self.write({'state': 'dispatched'}) + + def action_mark_delivered(self): + self.write({'state': 'delivered'}) diff --git a/addons/garment_erp/models/fabric_inspection.py b/addons/garment_erp/models/fabric_inspection.py new file mode 100644 index 0000000..04d4a2c --- /dev/null +++ b/addons/garment_erp/models/fabric_inspection.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ + +class GarmentFabricInspection(models.Model): + _name = 'garment.fabric.inspection' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _description = 'Fabric 4-Point System Quality Inspection' + _order = 'name desc' + + name = fields.Char(string='Inspection Number', required=True, default=lambda self: _('New'), copy=False) + roll_id = fields.Many2one('garment.fabric.roll', string='Fabric Roll', required=True, tracking=True) + supplier_id = fields.Many2one('res.partner', string='Supplier', related='roll_id.supplier_id', readonly=True) + fabric_product_id = fields.Many2one('product.product', string='Fabric Product', related='roll_id.product_id', readonly=True) + shade_lot = fields.Char(string='Shade / Dyeing Lot', related='roll_id.shade_lot', readonly=True) + + # Inspector & Date + inspector_id = fields.Many2one('res.users', string='Quality Inspector', default=lambda self: self.env.user, tracking=True) + date_inspected = fields.Date(string='Inspection Date', default=fields.Date.today, tracking=True) + + # GSM & Width Verification + target_gsm = fields.Integer(string='Target GSM', default=180) + actual_gsm = fields.Integer(string='Actual GSM Measured', default=180) + target_width = fields.Float(string='Target Width (Inches)', default=30.0) + actual_width = fields.Float(string='Actual Width (Inches)', default=30.0) + + # Shade Evaluation + shade_grade = fields.Selection([ + ('grade_a', 'Grade A - Exact Match with Master Swatch'), + ('grade_b', 'Grade B - Slight Variation (Approved)'), + ('grade_c', 'Grade C - Noticeable Variation (Group Separately)'), + ('grade_d', 'Grade D - Severely Off-Shade (Rejected)'), + ], string='Shade / Color Evaluation', default='grade_a', required=True, tracking=True) + + # 4-Point Defect Scoring System + # Points 1 to 4 based on defect length: + # 1 pt: Defect up to 3 inches + # 2 pts: Defect over 3 up to 6 inches + # 3 pts: Defect over 6 up to 9 inches + # 4 pts: Defect over 9 inches or any hole + defects_1pt_count = fields.Integer(string='1-Point Defects (<=3")', default=0) + defects_2pt_count = fields.Integer(string='2-Point Defects (3"-6")', default=0) + defects_3pt_count = fields.Integer(string='3-Point Defects (6"-9")', default=0) + defects_4pt_count = fields.Integer(string='4-Point Defects (>9" or Holes)', default=0) + + total_defect_points = fields.Integer(string='Total Defect Points', compute='_compute_points', store=True) + inspected_meters = fields.Float(string='Inspected Length (Meters)', default=20.0, required=True) + + # 4-Point Formula: (Total Points * 100) / (Inspected Meters * Fabric Width in Meters) + score_per_100_sqm = fields.Float(string='Score / 100 Sq. Meters', compute='_compute_points', store=True) + max_allowed_score = fields.Float(string='AQL Max Points Threshold', default=28.0, help='Standard industry threshold: 28 points per 100 sq meters') + + # Quantitative Results + passed_meters = fields.Float(string='Passed Quantity (Meters)') + rejected_meters = fields.Float(string='Rejected Quantity (Meters)') + + # Status + state = fields.Selection([ + ('draft', 'Draft'), + ('inspected', 'Inspected'), + ('passed', 'Quality Passed'), + ('failed', 'Quality Failed / Rejected'), + ], string='Inspection Verdict', default='draft', tracking=True, required=True) + remarks = fields.Text(string='Inspector Remarks / Observations') + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', _('New')) == _('New'): + vals['name'] = self.env['ir.sequence'].next_by_code('garment.fabric.inspection') or _('FINSP/%s') % fields.Date.today().strftime('%Y%m%d') + return super().create(vals_list) + + @api.depends('defects_1pt_count', 'defects_2pt_count', 'defects_3pt_count', 'defects_4pt_count', 'inspected_meters', 'actual_width') + def _compute_points(self): + for insp in self: + total_pts = (insp.defects_1pt_count * 1) + (insp.defects_2pt_count * 2) + (insp.defects_3pt_count * 3) + (insp.defects_4pt_count * 4) + insp.total_defect_points = total_pts + # Width in meters: width in inches * 0.0254 + width_meters = (insp.actual_width or 30.0) * 0.0254 + area_sqm = (insp.inspected_meters or 1.0) * width_meters + if area_sqm > 0: + insp.score_per_100_sqm = round((total_pts * 100.0) / area_sqm, 1) + else: + insp.score_per_100_sqm = 0.0 + + def action_pass(self): + self.ensure_one() + self.write({ + 'state': 'passed', + 'passed_meters': self.roll_id.length_meters, + 'rejected_meters': 0.0, + }) + self.roll_id.write({'status': 'approved'}) + + def action_fail(self): + self.ensure_one() + self.write({ + 'state': 'failed', + 'passed_meters': 0.0, + 'rejected_meters': self.roll_id.length_meters, + }) + self.roll_id.write({'status': 'rejected'}) diff --git a/addons/garment_erp/models/fabric_roll.py b/addons/garment_erp/models/fabric_roll.py new file mode 100644 index 0000000..f489bb8 --- /dev/null +++ b/addons/garment_erp/models/fabric_roll.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ + +class GarmentFabricRoll(models.Model): + _name = 'garment.fabric.roll' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _description = 'Fabric Roll Inventory Tracking' + _order = 'name desc' + + name = fields.Char(string='Roll Number', required=True, default=lambda self: _('New'), copy=False, tracking=True) + product_id = fields.Many2one('product.product', string='Fabric Product', required=True) + fabric_type_id = fields.Many2one('garment.fabric.type', string='Fabric Structure') + color_id = fields.Many2one('garment.color', string='Color / Shade') + shade_lot = fields.Char(string='Shade / Dyeing Lot Number', required=True, index=True, tracking=True) + barcode = fields.Char(string='Barcode / QR Code', index=True, copy=False) + + # Technical Specifications + gsm = fields.Integer(string='GSM (Actual)', required=True, default=180) + width = fields.Float(string='Width (Inches)', required=True, default=30.0) + width_type = fields.Selection([('tube', 'Tubular'), ('open', 'Open Width')], string='Width Type', default='tube') + yarn_count = fields.Char(string='Yarn Count', default='30s Combed') + + # Quantitative Weights (Kg) + gross_weight = fields.Float(string='Gross Weight (Kg)', required=True, default=25.0) + tare_weight = fields.Float(string='Tare / Core Weight (Kg)', default=0.8) + net_weight = fields.Float(string='Initial Net Weight (Kg)', compute='_compute_net_weight', store=True) + length_meters = fields.Float(string='Fabric Length (Meters)', default=75.0) + + # Consumption & Balance + issued_weight = fields.Float(string='Issued Weight (Kg)', default=0.0) + scrap_weight = fields.Float(string='End-Bit Scrap (Kg)', default=0.0) + remaining_weight = fields.Float(string='Remaining Weight (Kg)', compute='_compute_remaining_weight', store=True) + + # Origin & Storage + supplier_id = fields.Many2one('res.partner', string='Fabric Mill / Supplier', domain=[('supplier_rank', '>', 0)]) + purchase_order_id = fields.Many2one('purchase.order', string='Purchase Order Ref') + date_received = fields.Date(string='Date Received', default=fields.Date.today) + location_id = fields.Many2one('stock.location', string='Storage Warehouse Location') + + # Workflow State + status = fields.Selection([ + ('received', 'Received / Uninspected'), + ('inspected', 'Inspected'), + ('approved', 'Quality Approved (Ready to Cut)'), + ('issued', 'Partially Issued'), + ('exhausted', 'Fully Exhausted'), + ('rejected', 'Quality Rejected (Quarantine)'), + ], string='Roll Status', default='received', tracking=True, required=True) + + # Inspections + inspection_ids = fields.One2many('garment.fabric.inspection', 'roll_id', string='Fabric Inspections') + inspection_count = fields.Integer(string='Inspections', compute='_compute_inspection_count') + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', _('New')) == _('New'): + vals['name'] = self.env['ir.sequence'].next_by_code('garment.fabric.roll') or _('ROLL-%s') % fields.Date.today().strftime('%Y%m%d') + if not vals.get('barcode'): + vals['barcode'] = vals['name'] + return super().create(vals_list) + + @api.depends('gross_weight', 'tare_weight') + def _compute_net_weight(self): + for roll in self: + roll.net_weight = max(0.0, roll.gross_weight - roll.tare_weight) + + @api.depends('net_weight', 'issued_weight', 'scrap_weight') + def _compute_remaining_weight(self): + for roll in self: + rem = roll.net_weight - roll.issued_weight - roll.scrap_weight + roll.remaining_weight = max(0.0, round(rem, 2)) + if roll.remaining_weight <= 0.2 and roll.status in ['approved', 'issued']: + roll.status = 'exhausted' + + @api.depends('inspection_ids') + def _compute_inspection_count(self): + for roll in self: + roll.inspection_count = len(roll.inspection_ids) + + def action_create_inspection(self): + self.ensure_one() + insp = self.env['garment.fabric.inspection'].create({ + 'roll_id': self.id, + 'target_gsm': self.gsm, + 'target_width': self.width, + 'inspected_meters': min(20.0, self.length_meters), + }) + return { + 'name': _('Fabric 4-Point Inspection'), + 'type': 'ir.actions.act_window', + 'res_model': 'garment.fabric.inspection', + 'res_id': insp.id, + 'view_mode': 'form', + } diff --git a/addons/garment_erp/models/finished_goods.py b/addons/garment_erp/models/finished_goods.py new file mode 100644 index 0000000..1422a8d --- /dev/null +++ b/addons/garment_erp/models/finished_goods.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ + +class GarmentFinishedGoods(models.Model): + _name = 'garment.finished.goods' + _description = 'Finished Goods Inventory by Variant (Style + Color + Size)' + _order = 'style_id, color_id, size_id' + + style_id = fields.Many2one('garment.style', string='Garment Style', required=True, index=True) + color_id = fields.Many2one('garment.color', string='Color', required=True, index=True) + size_id = fields.Many2one('garment.size', string='Size', required=True, index=True) + sku = fields.Char(string='Variant SKU', compute='_compute_sku', store=True, index=True) + + product_id = fields.Many2one('product.product', string='Odoo Variant Product') + location_id = fields.Many2one('stock.location', string='Warehouse Stock Location') + + on_hand_qty = fields.Integer(string='On Hand Pieces', default=0) + reserved_qty = fields.Integer(string='Reserved Pieces', default=0) + available_qty = fields.Integer(string='Available to Ship', compute='_compute_available_qty', store=True) + shipped_qty = fields.Integer(string='Total Shipped Pieces', default=0) + + _variant_uniq = models.Constraint('UNIQUE(style_id, color_id, size_id)', 'Finished Goods variant record already exists!') + + @api.depends('style_id', 'color_id', 'size_id') + def _compute_sku(self): + for rec in self: + s_name = rec.style_id.name if rec.style_id else 'STYLE' + c_code = rec.color_id.code if rec.color_id else 'CLR' + sz_code = rec.size_id.code if rec.size_id else 'SZ' + rec.sku = f"{s_name}-{c_code}-{sz_code}" + + @api.depends('on_hand_qty', 'reserved_qty') + def _compute_available_qty(self): + for rec in self: + rec.available_qty = max(0, rec.on_hand_qty - rec.reserved_qty) diff --git a/addons/garment_erp/models/garment_base.py b/addons/garment_erp/models/garment_base.py new file mode 100644 index 0000000..d7a5f2e --- /dev/null +++ b/addons/garment_erp/models/garment_base.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ + +class GarmentBrand(models.Model): + _name = 'garment.brand' + _description = 'Garment Brand' + _order = 'name' + + name = fields.Char(string='Brand Name', required=True, index=True) + code = fields.Char(string='Brand Code', required=True) + logo = fields.Binary(string='Brand Logo') + active = fields.Boolean(default=True) + notes = fields.Text(string='Notes') + + _code_uniq = models.Constraint('UNIQUE(code)', 'Brand Code must be unique!') + + +class GarmentSeason(models.Model): + _name = 'garment.season' + _description = 'Garment Season' + _order = 'year desc, name' + + name = fields.Char(string='Season Name', required=True) + code = fields.Char(string='Season Code', required=True) + year = fields.Char(string='Year', default=lambda self: fields.Date.today().strftime('%Y')) + active = fields.Boolean(default=True) + + _code_uniq = models.Constraint('UNIQUE(code)', 'Season Code must be unique!') + + +class GarmentGender(models.Model): + _name = 'garment.gender' + _description = 'Garment Gender Category' + _order = 'sequence, name' + + name = fields.Char(string='Gender / Age Group', required=True) + code = fields.Char(string='Code', required=True) + sequence = fields.Integer(string='Sequence', default=10) + + _code_uniq = models.Constraint('UNIQUE(code)', 'Gender Code must be unique!') + + +class GarmentType(models.Model): + _name = 'garment.type' + _description = 'Garment Product Type' + _order = 'name' + + name = fields.Char(string='Garment Type', required=True) + code = fields.Char(string='Code', required=True) + category = fields.Selection([ + ('top', 'Top Wear'), + ('bottom', 'Bottom Wear'), + ('outerwear', 'Outerwear / Jackets'), + ('innerwear', 'Innerwear / Undergarments'), + ('activewear', 'Activewear / Sportswear'), + ('accessories', 'Accessories'), + ], string='Category', default='top', required=True) + description = fields.Text(string='Description') + + _code_uniq = models.Constraint('UNIQUE(code)', 'Garment Type Code must be unique!') + + +class GarmentFabricType(models.Model): + _name = 'garment.fabric.type' + _description = 'Fabric Type / Structure' + _order = 'name' + + name = fields.Char(string='Fabric Type Name', required=True) + code = fields.Char(string='Code', required=True) + structure = fields.Selection([ + ('single_jersey', 'Single Jersey'), + ('rib_1x1', '1x1 Rib'), + ('rib_2x2', '2x2 Rib'), + ('interlock', 'Interlock'), + ('pique', 'Pique / Polo Knit'), + ('fleece', 'Fleece'), + ('french_terry', 'French Terry'), + ('waffle', 'Waffle Knit'), + ('woven', 'Woven Fabric'), + ('other', 'Other'), + ], string='Knit Structure', default='single_jersey', required=True) + gsm_min = fields.Integer(string='Min GSM', default=140) + gsm_max = fields.Integer(string='Max GSM', default=220) + description = fields.Text(string='Description') + + _code_uniq = models.Constraint('UNIQUE(code)', 'Fabric Type Code must be unique!') + + +class GarmentComposition(models.Model): + _name = 'garment.composition' + _description = 'Fabric Composition' + _order = 'name' + + name = fields.Char(string='Composition', required=True) + code = fields.Char(string='Code', required=True) + cotton_pct = fields.Float(string='Cotton %', default=100.0) + polyester_pct = fields.Float(string='Polyester %', default=0.0) + spandex_pct = fields.Float(string='Spandex / Lycra %', default=0.0) + viscose_pct = fields.Float(string='Viscose %', default=0.0) + other_pct = fields.Float(string='Other Fiber %', default=0.0) + + _code_uniq = models.Constraint('UNIQUE(code)', 'Composition Code must be unique!') + + +class GarmentColor(models.Model): + _name = 'garment.color' + _description = 'Garment Color Master' + _order = 'name' + + name = fields.Char(string='Color Name', required=True, index=True) + code = fields.Char(string='Color Code', required=True, index=True) + color_group = fields.Selection([ + ('black', 'Blacks'), + ('white', 'Whites & Off-Whites'), + ('grey', 'Greys & Melanges'), + ('navy', 'Navies & Blues'), + ('red', 'Reds & Maroons'), + ('green', 'Greens & Olives'), + ('yellow', 'Yellows & Ochers'), + ('pastel', 'Pastels'), + ('multicolor', 'Printed / Multi'), + ], string='Color Group', default='black') + pantone_code = fields.Char(string='Pantone Reference (TPX/TCX)') + color_hex = fields.Char(string='Hex Swatch Code', default='#000000') + active = fields.Boolean(default=True) + + _code_uniq = models.Constraint('UNIQUE(code)', 'Color Code must be unique!') + + def name_get(self): + result = [] + for rec in self: + result.append((rec.id, f"[{rec.code}] {rec.name}")) + return result + + +class GarmentSize(models.Model): + _name = 'garment.size' + _description = 'Garment Size Master' + _order = 'sequence, code' + + name = fields.Char(string='Size Name', required=True) + code = fields.Char(string='Size Code', required=True, index=True) + sequence = fields.Integer(string='Display Sequence', default=10, help='Used for sorting columns in Size Matrix') + size_group = fields.Selection([ + ('adult', 'Adult / Standard Alpha (XS-3XL)'), + ('numeric_adult', 'Adult Numeric (28-44)'), + ('youth', 'Kids / Youth (4Y-16Y)'), + ('infant', 'Infant / Baby (0M-24M)'), + ], string='Size Group', default='adult', required=True) + active = fields.Boolean(default=True) + + _code_uniq = models.Constraint('UNIQUE(code)', 'Size Code must be unique!') diff --git a/addons/garment_erp/models/garment_bom.py b/addons/garment_erp/models/garment_bom.py new file mode 100644 index 0000000..427de8b --- /dev/null +++ b/addons/garment_erp/models/garment_bom.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ + +class GarmentBom(models.Model): + _name = 'garment.bom' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _description = 'Garment Bill of Materials (BOM)' + _order = 'name desc' + + name = fields.Char(string='BOM Reference', required=True, default=lambda self: _('New'), copy=False) + style_id = fields.Many2one('garment.style', string='Garment Style', required=True, tracking=True) + base_qty = fields.Float(string='Base Quantity', default=1.0, required=True, help='Number of garment pieces this BOM defines') + uom_id = fields.Many2one('uom.uom', string='UoM', related='style_id.uom_id', readonly=True) + active = fields.Boolean(default=True) + company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company) + notes = fields.Text(string='Notes / Engineering Specs') + + # BOM Lines + fabric_line_ids = fields.One2many('garment.bom.fabric', 'bom_id', string='Fabric Components') + trim_line_ids = fields.One2many('garment.bom.trim', 'bom_id', string='Trims & Accessories') + operation_line_ids = fields.One2many('garment.bom.operation', 'bom_id', string='Operations & Routing') + + # Cost Summary Computes + total_fabric_cost = fields.Float(string='Total Fabric Cost', compute='_compute_totals', store=True) + total_trim_cost = fields.Float(string='Total Trims Cost', compute='_compute_totals', store=True) + total_operation_cost = fields.Float(string='Total Labor / CM Cost', compute='_compute_totals', store=True) + total_unit_cost = fields.Float(string='Estimated Garment Cost (per piece)', compute='_compute_totals', store=True) + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', _('New')) == _('New'): + vals['name'] = self.env['ir.sequence'].next_by_code('garment.bom') or _('BOM-%s') % fields.Date.today().strftime('%Y%m%d') + return super().create(vals_list) + + @api.depends('fabric_line_ids.subtotal_cost', 'trim_line_ids.subtotal_cost', 'operation_line_ids.subtotal_cost', 'base_qty') + def _compute_totals(self): + for bom in self: + fab_sum = sum(bom.fabric_line_ids.mapped('subtotal_cost')) + trim_sum = sum(bom.trim_line_ids.mapped('subtotal_cost')) + op_sum = sum(bom.operation_line_ids.mapped('subtotal_cost')) + bom.total_fabric_cost = fab_sum + bom.total_trim_cost = trim_sum + bom.total_operation_cost = op_sum + qty = bom.base_qty or 1.0 + bom.total_unit_cost = (fab_sum + trim_sum + op_sum) / qty + + def calculate_requirement_for_qty(self, order_qty): + """Calculates material requirements for an arbitrary production quantity""" + self.ensure_one() + factor = order_qty / (self.base_qty or 1.0) + fabric_req = [] + for fab in self.fabric_line_ids: + net_kg = fab.consumption_kg * factor + gross_kg = net_kg * (1.0 + (fab.wastage_pct / 100.0)) + fabric_req.append({ + 'component': fab.component_type, + 'fabric_type': fab.fabric_type_id.name if fab.fabric_type_id else 'Fabric', + 'net_kg': round(net_kg, 2), + 'wastage_pct': fab.wastage_pct, + 'gross_kg': round(gross_kg, 2), + 'cost': round(gross_kg * fab.cost_per_kg, 2), + }) + + trims_req = [] + for trim in self.trim_line_ids: + net_units = trim.consumption_qty * factor + gross_units = net_units * (1.0 + (trim.wastage_pct / 100.0)) + trims_req.append({ + 'item': trim.item_name or trim.product_id.name, + 'item_type': trim.trim_type, + 'net_units': round(net_units, 2), + 'gross_units': round(gross_units, 2), + 'cost': round(gross_units * trim.unit_cost, 2), + }) + return { + 'order_qty': order_qty, + 'fabrics': fabric_req, + 'trims': trims_req, + } + + +class GarmentBomFabric(models.Model): + _name = 'garment.bom.fabric' + _description = 'Garment BOM Fabric Component' + + bom_id = fields.Many2one('garment.bom', string='BOM', required=True, ondelete='cascade') + component_type = fields.Selection([ + ('body', 'Body Panels (Front & Back)'), + ('neck_rib', 'Neck Rib / Collar Band'), + ('collar', 'Knit / Flat Collar'), + ('cuff', 'Sleeve Cuffs'), + ('pocket', 'Pocket Panels'), + ('placket', 'Placket Facing'), + ('hood', 'Hood Panels'), + ('lining', 'Lining Fabric'), + ('other', 'Other Fabric Part'), + ], string='Component Part', default='body', required=True) + fabric_type_id = fields.Many2one('garment.fabric.type', string='Fabric Type') + product_id = fields.Many2one('product.product', string='Raw Fabric Product') + gsm = fields.Integer(string='GSM', default=180) + width = fields.Float(string='Width (Inches)', default=30.0) + + # Consumption per garment (Kg) + consumption_kg = fields.Float(string='Net Consumption (Kg/Pc)', required=True, default=0.180, digits=(16, 4)) + wastage_pct = fields.Float(string='Cutting Wastage %', default=5.0) + gross_consumption_kg = fields.Float(string='Gross Consumption (Kg/Pc)', compute='_compute_gross_kg', store=True, digits=(16, 4)) + cost_per_kg = fields.Float(string='Fabric Cost / Kg', default=320.0) + subtotal_cost = fields.Float(string='Fabric Cost / Pc', compute='_compute_subtotal_cost', store=True) + + @api.depends('consumption_kg', 'wastage_pct') + def _compute_gross_kg(self): + for rec in self: + rec.gross_consumption_kg = rec.consumption_kg * (1.0 + (rec.wastage_pct / 100.0)) + + @api.depends('gross_consumption_kg', 'cost_per_kg') + def _compute_subtotal_cost(self): + for rec in self: + rec.subtotal_cost = rec.gross_consumption_kg * rec.cost_per_kg + + +class GarmentBomTrim(models.Model): + _name = 'garment.bom.trim' + _description = 'Garment BOM Trims & Accessories' + + bom_id = fields.Many2one('garment.bom', string='BOM', required=True, ondelete='cascade') + trim_type = fields.Selection([ + ('main_label', 'Main Woven / Heat-Transfer Label'), + ('size_label', 'Size Label'), + ('care_label', 'Wash Care Satin Label'), + ('hang_tag', 'Brand Hang Tag'), + ('price_tag', 'Retail Price Ticket'), + ('polybag', 'Individual Polybag'), + ('carton', 'Master Export Carton Box'), + ('thread', 'Sewing Thread (Spun Polyester)'), + ('button', 'Buttons'), + ('zipper', 'Zipper'), + ('elastic', 'Elastic Band'), + ('drawstring', 'Drawstring / Cord'), + ('tissue_paper', 'Tissue Paper / Board'), + ('silica_gel', 'Silica Gel Desiccant'), + ('other', 'Other Accessory'), + ], string='Trim Category', required=True, default='main_label') + item_name = fields.Char(string='Trim Item Name', required=True) + product_id = fields.Many2one('product.product', string='Trims Product') + uom_id = fields.Many2one('uom.uom', string='UoM', default=lambda self: self.env.ref('uom.product_uom_unit', raise_if_not_found=False)) + + consumption_qty = fields.Float(string='Net Qty / Pc', required=True, default=1.0, digits=(16, 4)) + wastage_pct = fields.Float(string='Allowance %', default=3.0) + gross_qty = fields.Float(string='Gross Qty / Pc', compute='_compute_gross_qty', store=True, digits=(16, 4)) + unit_cost = fields.Float(string='Unit Cost', default=1.5) + subtotal_cost = fields.Float(string='Cost / Pc', compute='_compute_subtotal_cost', store=True) + + @api.depends('consumption_qty', 'wastage_pct') + def _compute_gross_qty(self): + for rec in self: + rec.gross_qty = rec.consumption_qty * (1.0 + (rec.wastage_pct / 100.0)) + + @api.depends('gross_qty', 'unit_cost') + def _compute_subtotal_cost(self): + for rec in self: + rec.subtotal_cost = rec.gross_qty * rec.unit_cost + + +class GarmentBomOperation(models.Model): + _name = 'garment.bom.operation' + _description = 'Garment BOM Operations & Routing' + _order = 'sequence, id' + + bom_id = fields.Many2one('garment.bom', string='BOM', required=True, ondelete='cascade') + sequence = fields.Integer(string='Sequence', default=10) + operation_name = fields.Char(string='Operation Name', required=True) + department = fields.Selection([ + ('cutting', 'Cutting Section'), + ('sewing', 'Sewing Assembly Line'), + ('jobwork', 'External Job Work'), + ('washing', 'Washing & Bio-wash'), + ('finishing', 'Ironing & Finishing'), + ('packing', 'Packing Section'), + ], string='Department', default='sewing', required=True) + machine_type = fields.Selection([ + ('manual', 'Manual / Hand Work'), + ('snls', 'Single Needle Lockstitch (SNLS)'), + ('overlock_4t', '4-Thread Overlock (4T)'), + ('overlock_5t', '5-Thread Overlock (5T)'), + ('flatlock', 'Flatlock / Coverstitch'), + ('button_attach', 'Button Stitching Machine'), + ('button_hole', 'Button Hole Machine'), + ('kansai', 'Multi-Needle Kansai'), + ('steam_iron', 'Vacuum Steam Press'), + ], string='Machine Required', default='overlock_4t') + sam = fields.Float(string='SAM (Minutes)', default=1.5, help='Standard Allowed Minutes') + minute_rate = fields.Float(string='Labor Cost / Minute', default=2.5) + subtotal_cost = fields.Float(string='Cost / Pc', compute='_compute_subtotal_cost', store=True) + + @api.depends('sam', 'minute_rate') + def _compute_subtotal_cost(self): + for rec in self: + rec.subtotal_cost = rec.sam * rec.minute_rate diff --git a/addons/garment_erp/models/garment_costing.py b/addons/garment_erp/models/garment_costing.py new file mode 100644 index 0000000..224f65d --- /dev/null +++ b/addons/garment_erp/models/garment_costing.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ + +class GarmentCosting(models.Model): + _name = 'garment.costing' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _description = 'Garment Pre-Costing & Post-Costing Sheet' + _order = 'name desc' + + name = fields.Char(string='Cost Sheet No', required=True, default=lambda self: _('New'), copy=False, tracking=True) + style_id = fields.Many2one('garment.style', string='Garment Style', required=True, tracking=True) + buyer_id = fields.Many2one('res.partner', string='Buyer / Customer', tracking=True) + order_qty = fields.Integer(string='Target Order Quantity (Pcs)', required=True, default=10000) + company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company) + currency_id = fields.Many2one('res.currency', string='Currency', default=lambda self: self.env.company.currency_id) + + costing_type = fields.Selection([ + ('pre_costing', 'Pre-Costing (Quotation / Estimate)'), + ('post_costing', 'Post-Costing (Actual Production Variance)'), + ], string='Costing Stage', default='pre_costing', required=True, tracking=True) + + # 1. Raw Materials (Fabric + Dyeing) + fabric_cost_per_pc = fields.Float(string='Yarn / Raw Fabric Cost / Pc', default=65.0, digits=(16, 2)) + dyeing_finishing_cost_per_pc = fields.Float(string='Dyeing & Bleaching / Pc', default=12.0, digits=(16, 2)) + + # 2. Trims & Accessories + trims_accessories_cost_per_pc = fields.Float(string='Trims & Labels / Pc', default=8.5, digits=(16, 2)) + + # 3. Value Addition + printing_embroidery_cost_per_pc = fields.Float(string='Chest Print / Embroidery / Pc', default=14.0, digits=(16, 2)) + washing_cost_per_pc = fields.Float(string='Garment Bio-Washing / Pc', default=5.0, digits=(16, 2)) + + # 4. Manufacturing Labor (CM - Cut & Make) + cutting_cost_per_pc = fields.Float(string='Cutting Labor / Pc', default=4.0, digits=(16, 2)) + sewing_cm_cost_per_pc = fields.Float(string='Sewing Assembly CM / Pc', default=28.0, digits=(16, 2), help='Standard sewing labor cost') + + # 5. Packaging & Forwarding + packing_carton_cost_per_pc = fields.Float(string='Polybags & Master Cartons / Pc', default=6.5, digits=(16, 2)) + commercial_freight_cost_per_pc = fields.Float(string='Local Freight & Port Handling / Pc', default=3.0, digits=(16, 2)) + + # 6. Factory Overheads + factory_overhead_pct = fields.Float(string='Factory Overheads %', default=8.0) + factory_overhead_cost = fields.Float(string='Overheads Cost / Pc', compute='_compute_totals', store=True) + + # Total Costs + total_cost_per_pc = fields.Float(string='Total Garment Cost / Pc', compute='_compute_totals', store=True) + total_cost_per_dozen = fields.Float(string='Total Cost / Dozen', compute='_compute_totals', store=True) + total_order_cost = fields.Float(string='Total Order Cost', compute='_compute_totals', store=True) + + # Commercial Pricing & Margins + target_selling_price = fields.Float(string='Target Selling Price / Pc', default=180.0, required=True, tracking=True) + gross_margin_amount = fields.Float(string='Gross Margin / Pc', compute='_compute_margins', store=True) + gross_margin_pct = fields.Float(string='Gross Margin %', compute='_compute_margins', store=True) + total_expected_profit = fields.Float(string='Total Expected Profit', compute='_compute_margins', store=True) + + notes = fields.Text(string='Costing Notes & Assumptions') + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', _('New')) == _('New'): + vals['name'] = self.env['ir.sequence'].next_by_code('garment.costing') or _('COST/%s') % fields.Date.today().strftime('%Y%m%d') + return super(GarmentCosting, self).create(vals_list) + + @api.depends('fabric_cost_per_pc', 'dyeing_finishing_cost_per_pc', 'trims_accessories_cost_per_pc', + 'printing_embroidery_cost_per_pc', 'washing_cost_per_pc', 'cutting_cost_per_pc', + 'sewing_cm_cost_per_pc', 'packing_carton_cost_per_pc', 'commercial_freight_cost_per_pc', + 'factory_overhead_pct', 'order_qty') + def _compute_totals(self): + for rec in self: + direct_cost = (rec.fabric_cost_per_pc + rec.dyeing_finishing_cost_per_pc + + rec.trims_accessories_cost_per_pc + rec.printing_embroidery_cost_per_pc + + rec.washing_cost_per_pc + rec.cutting_cost_per_pc + + rec.sewing_cm_cost_per_pc + rec.packing_carton_cost_per_pc + + rec.commercial_freight_cost_per_pc) + rec.factory_overhead_cost = round(direct_cost * (rec.factory_overhead_pct / 100.0), 2) + tot_pc = round(direct_cost + rec.factory_overhead_cost, 2) + rec.total_cost_per_pc = tot_pc + rec.total_cost_per_dozen = round(tot_pc * 12.0, 2) + rec.total_order_cost = round(tot_pc * (rec.order_qty or 1), 2) + + @api.depends('total_cost_per_pc', 'target_selling_price', 'order_qty') + def _compute_margins(self): + for rec in self: + margin = rec.target_selling_price - rec.total_cost_per_pc + rec.gross_margin_amount = round(margin, 2) + if rec.target_selling_price > 0: + rec.gross_margin_pct = round((margin / rec.target_selling_price) * 100.0, 2) + else: + rec.gross_margin_pct = 0.0 + rec.total_expected_profit = round(margin * (rec.order_qty or 1), 2) + + def action_load_from_bom(self): + """Pre-populates raw material and labor costs from the active Garment BOM""" + self.ensure_one() + bom = self.env['garment.bom'].search([('style_id', '=', self.style_id.id)], limit=1) + if bom: + self.fabric_cost_per_pc = bom.total_fabric_cost + self.trims_accessories_cost_per_pc = bom.total_trim_cost + self.sewing_cm_cost_per_pc = bom.total_operation_cost diff --git a/addons/garment_erp/models/garment_dashboard.py b/addons/garment_erp/models/garment_dashboard.py new file mode 100644 index 0000000..b3cc68b --- /dev/null +++ b/addons/garment_erp/models/garment_dashboard.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ + +class GarmentDashboard(models.Model): + _name = 'garment.dashboard' + _description = 'Garment Manufacturing Real-Time KPI Dashboard' + + name = fields.Char(string='Dashboard', default='Tiruppur Factory Control Room') + + # Executive KPIs + total_sales_orders = fields.Integer(string='Active Garment Orders', compute='_compute_kpis') + total_ordered_pieces = fields.Integer(string='Total Booked Pieces', compute='_compute_kpis') + active_production_plans = fields.Integer(string='Production Orders in Progress', compute='_compute_kpis') + avg_production_progress = fields.Float(string='Average Production Progress %', compute='_compute_kpis') + + # Shop Floor & Sewing KPIs + total_sewing_lines = fields.Integer(string='Active Sewing Lines', compute='_compute_kpis') + today_target_pieces = fields.Integer(string="Today's Sewing Target", compute='_compute_kpis') + today_actual_pieces = fields.Integer(string="Today's Actual Output", compute='_compute_kpis') + avg_line_efficiency = fields.Float(string='Factory Efficiency %', compute='_compute_kpis') + today_defects = fields.Integer(string="Today's Alteration Count", compute='_compute_kpis') + + # Material & Inventory KPIs + total_fabric_stock_kg = fields.Float(string='Fabric Rolls in Stock (Kg)', compute='_compute_kpis') + total_approved_rolls = fields.Integer(string='QC Approved Rolls', compute='_compute_kpis') + total_finished_goods_pcs = fields.Integer(string='Finished Goods Stock (Pcs)', compute='_compute_kpis') + + # Operations & Subcontracting + pending_jobwork_dc = fields.Integer(string='Pending Job Work Challans', compute='_compute_kpis') + pending_quality_inspections = fields.Integer(string='Pending Quality Inspections', compute='_compute_kpis') + active_rework_orders = fields.Integer(string='Open Rework Orders', compute='_compute_kpis') + pending_dispatch_cartons = fields.Integer(string='Cartons Ready for Dispatch', compute='_compute_kpis') + + def _compute_kpis(self): + for rec in self: + # Sales + orders = self.env['sale.order'].search([('is_garment_order', '=', True), ('state', 'in', ['sale', 'done'])]) + rec.total_sales_orders = len(orders) + rec.total_ordered_pieces = sum(orders.mapped('total_garment_qty')) + + # Production + plans = self.env['garment.production.plan'].search([('state', 'in', ['planned', 'in_progress'])]) + rec.active_production_plans = len(plans) + rec.avg_production_progress = round(sum(plans.mapped('progress_pct')) / (len(plans) or 1), 1) + + # Sewing Lines & Today's Hourly + lines = self.env['garment.sewing.line'].search([('active', '=', True)]) + rec.total_sewing_lines = len(lines) + today_logs = self.env['garment.sewing.hourly'].search([('date', '=', fields.Date.today())]) + rec.today_target_pieces = sum(today_logs.mapped('total_target')) + rec.today_actual_pieces = sum(today_logs.mapped('total_actual')) + rec.today_defects = sum(today_logs.mapped('total_defects')) + if today_logs: + rec.avg_line_efficiency = round(sum(today_logs.mapped('efficiency_pct')) / len(today_logs), 1) + else: + rec.avg_line_efficiency = 0.0 + + # Fabric & FG + rolls = self.env['garment.fabric.roll'].search([('status', 'in', ['received', 'inspected', 'approved', 'issued'])]) + rec.total_fabric_stock_kg = round(sum(rolls.mapped('remaining_weight')), 1) + rec.total_approved_rolls = self.env['garment.fabric.roll'].search_count([('status', '=', 'approved')]) + + fg = self.env['garment.finished.goods'].search([]) + rec.total_finished_goods_pcs = sum(fg.mapped('on_hand_qty')) + + # Job Work & QC + rec.pending_jobwork_dc = self.env['garment.jobwork.order'].search_count([('state', 'in', ['sent', 'partial'])]) + rec.pending_quality_inspections = self.env['garment.quality.inspection'].search_count([('verdict', '=', 'rework_needed')]) + rec.active_rework_orders = self.env['garment.rework.order'].search_count([('state', 'in', ['assigned', 'in_rework', 'ready_recheck'])]) + + # Packing + rec.pending_dispatch_cartons = self.env['garment.carton'].search_count([('dispatch_id', '=', False)]) diff --git a/addons/garment_erp/models/garment_style.py b/addons/garment_erp/models/garment_style.py new file mode 100644 index 0000000..7cc9ef5 --- /dev/null +++ b/addons/garment_erp/models/garment_style.py @@ -0,0 +1,172 @@ +# -*- 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') diff --git a/addons/garment_erp/models/job_work.py b/addons/garment_erp/models/job_work.py new file mode 100644 index 0000000..c6e1475 --- /dev/null +++ b/addons/garment_erp/models/job_work.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + +class GarmentJobworkOrder(models.Model): + _name = 'garment.jobwork.order' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _description = 'Garment Subcontracting & Job Work Order' + _order = 'name desc' + + name = fields.Char(string='Job Work DC No', required=True, default=lambda self: _('New'), copy=False, tracking=True) + vendor_id = fields.Many2one('res.partner', string='Job Worker / Subcontractor', required=True, domain=[('supplier_rank', '>', 0)], tracking=True) + production_plan_id = fields.Many2one('garment.production.plan', string='Production Plan', tracking=True) + style_id = fields.Many2one('garment.style', string='Garment Style', tracking=True) + company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company) + + process_type = fields.Selection([ + ('knitting', 'Knitting / Greige Fabric Formation'), + ('dyeing', 'Dyeing & Bleaching / Bio-Polishing'), + ('all_over_print', 'All-Over Rotary / Mill Print'), + ('chest_print', 'Chest Print (Screen / DTF / Sublimation)'), + ('embroidery', 'Computerized Multi-Head Embroidery'), + ('washing', 'Garment Washing / Enzyme / Silicone Wash'), + ('finishing', 'External Ironing & Thread Trimming'), + ('other', 'Other Job Work'), + ], string='Processing Operation', default='dyeing', required=True, tracking=True) + + # Dates & Challan + date_sent = fields.Date(string='DC Issue Date', default=fields.Date.today, tracking=True) + date_expected = fields.Date(string='Expected Inward Date', tracking=True) + date_received = fields.Date(string='Final Completion Date') + vehicle_no = fields.Char(string='Vehicle / Transport No') + e_way_bill_no = fields.Char(string='GST E-Way Bill Number') + + # Financials & Process Loss + pricing_uom = fields.Selection([('per_kg', 'Per Kilogram (Kg)'), ('per_pc', 'Per Piece (Pc)')], string='Rate Basis', default='per_kg') + rate_per_unit = fields.Float(string='Job Work Rate (INR)', default=45.0, required=True) + allowed_process_loss_pct = fields.Float(string='Standard Process Loss %', default=4.0, help='Allowed shrinkage/wastage during process') + + # Lines + sent_line_ids = fields.One2many('garment.jobwork.sent.line', 'jobwork_id', string='Materials Sent Outward') + inward_line_ids = fields.One2many('garment.jobwork.inward.line', 'jobwork_id', string='Materials Inward Received') + + # Computes + total_sent_qty = fields.Float(string='Total Sent Qty', compute='_compute_quantities', store=True) + total_received_qty = fields.Float(string='Total Received Qty', compute='_compute_quantities', store=True) + total_rejected_qty = fields.Float(string='Rejected Qty', compute='_compute_quantities', store=True) + total_accepted_qty = fields.Float(string='Accepted Inward Qty', compute='_compute_quantities', store=True) + pending_qty = fields.Float(string='Pending Balance Qty', compute='_compute_quantities', store=True) + + actual_process_loss_pct = fields.Float(string='Actual Process Loss %', compute='_compute_process_loss', store=True) + total_cost = fields.Float(string='Total Job Work Cost', compute='_compute_cost', store=True) + + state = fields.Selection([ + ('draft', 'Draft DC'), + ('sent', 'Material Sent (Outward)'), + ('partial', 'Partially Received'), + ('completed', 'Completed & Inwarded'), + ('cancelled', 'Cancelled'), + ], string='Status', default='draft', tracking=True, required=True) + notes = fields.Text(string='Processing Instructions / Shade Formula') + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', _('New')) == _('New'): + vals['name'] = self.env['ir.sequence'].next_by_code('garment.jobwork.order') or _('JW/%s') % fields.Date.today().strftime('%Y%m%d') + return super(GarmentJobworkOrder, self).create(vals_list) + + @api.depends('sent_line_ids.quantity', 'inward_line_ids.received_qty', 'inward_line_ids.rejected_qty', 'inward_line_ids.accepted_qty') + def _compute_quantities(self): + for jw in self: + sent = sum(jw.sent_line_ids.mapped('quantity')) + rec = sum(jw.inward_line_ids.mapped('received_qty')) + rej = sum(jw.inward_line_ids.mapped('rejected_qty')) + acc = sum(jw.inward_line_ids.mapped('accepted_qty')) + jw.total_sent_qty = sent + jw.total_received_qty = rec + jw.total_rejected_qty = rej + jw.total_accepted_qty = acc + jw.pending_qty = max(0.0, round(sent - rec, 2)) + + @api.depends('total_sent_qty', 'total_received_qty') + def _compute_process_loss(self): + for jw in self: + if jw.total_sent_qty > 0 and jw.total_received_qty > 0: + diff = jw.total_sent_qty - jw.total_received_qty + jw.actual_process_loss_pct = max(0.0, round((diff / jw.total_sent_qty) * 100.0, 2)) + else: + jw.actual_process_loss_pct = 0.0 + + @api.depends('total_accepted_qty', 'rate_per_unit') + def _compute_cost(self): + for jw in self: + jw.total_cost = round(jw.total_accepted_qty * jw.rate_per_unit, 2) + + def action_dispatch_material(self): + self.ensure_one() + if not self.sent_line_ids: + raise UserError(_("Please add materials sent outward before dispatching.")) + self.write({'state': 'sent'}) + + def action_complete_jobwork(self): + self.ensure_one() + self.write({'state': 'completed', 'date_received': fields.Date.today()}) + + +class GarmentJobworkSentLine(models.Model): + _name = 'garment.jobwork.sent.line' + _description = 'Job Work Sent Line' + + jobwork_id = fields.Many2one('garment.jobwork.order', string='Job Work Order', required=True, ondelete='cascade') + description = fields.Char(string='Item / Material Description', required=True) + fabric_roll_id = fields.Many2one('garment.fabric.roll', string='Fabric Roll #') + bundle_id = fields.Many2one('garment.bundle', string='Garment Bundle #') + color_id = fields.Many2one('garment.color', string='Color') + quantity = fields.Float(string='Sent Quantity', required=True, default=0.0) + uom = fields.Char(string='UoM', default='Kg') + + +class GarmentJobworkInwardLine(models.Model): + _name = 'garment.jobwork.inward.line' + _description = 'Job Work Inward Received Line' + + jobwork_id = fields.Many2one('garment.jobwork.order', string='Job Work Order', required=True, ondelete='cascade') + date_inward = fields.Date(string='Inward Date', default=fields.Date.today, required=True) + vendor_dc_no = fields.Char(string='Vendor Delivery Challan #', required=True) + received_qty = fields.Float(string='Received Qty', required=True, default=0.0) + rejected_qty = fields.Float(string='Rejected Qty', default=0.0) + accepted_qty = fields.Float(string='Accepted Good Qty', compute='_compute_accepted', store=True) + remarks = fields.Char(string='Shade / Quality Remarks') + + @api.depends('received_qty', 'rejected_qty') + def _compute_accepted(self): + for line in self: + line.accepted_qty = max(0.0, line.received_qty - line.rejected_qty) diff --git a/addons/garment_erp/models/mrp_requirement.py b/addons/garment_erp/models/mrp_requirement.py new file mode 100644 index 0000000..4fc87ee --- /dev/null +++ b/addons/garment_erp/models/mrp_requirement.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + +class GarmentMrpRequirement(models.Model): + _name = 'garment.mrp.requirement' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _description = 'Garment Material Requirement Planning (MRP)' + _order = 'name desc' + + name = fields.Char(string='MRP Requisition No', required=True, default=lambda self: _('New'), copy=False) + production_plan_id = fields.Many2one('garment.production.plan', string='Production Plan', required=True, tracking=True) + style_id = fields.Many2one('garment.style', string='Garment Style', related='production_plan_id.style_id', readonly=True) + bom_id = fields.Many2one('garment.bom', string='Garment BOM', required=True) + order_qty = fields.Integer(string='Production Order Qty (Pcs)', required=True, default=1000) + company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company) + + line_ids = fields.One2many('garment.mrp.requirement.line', 'mrp_id', string='Material Shortage & Requirement Lines') + total_shortage_cost = fields.Float(string='Total Shortage Purchase Cost', compute='_compute_totals', store=True) + state = fields.Selection([ + ('draft', 'Draft'), + ('calculated', 'Requirements Calculated'), + ('rfq_created', 'Purchase RFQs Generated'), + ('closed', 'Completed / Closed'), + ], string='Status', default='draft', tracking=True) + + purchase_order_ids = fields.Many2many('purchase.order', string='Generated Purchase Orders') + purchase_count = fields.Integer(string='Purchase Orders', compute='_compute_purchase_count') + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', _('New')) == _('New'): + vals['name'] = self.env['ir.sequence'].next_by_code('garment.mrp.requirement') or _('MRP/%s') % fields.Date.today().strftime('%Y%m%d') + return super().create(vals_list) + + @api.depends('line_ids.shortage_cost') + def _compute_totals(self): + for mrp in self: + mrp.total_shortage_cost = sum(mrp.line_ids.mapped('shortage_cost')) + + @api.depends('purchase_order_ids') + def _compute_purchase_count(self): + for mrp in self: + mrp.purchase_count = len(mrp.purchase_order_ids) + + def action_compute_requirements(self): + self.ensure_one() + if not self.bom_id: + raise UserError(_("Please assign a BOM.")) + + # Clear existing + self.line_ids.unlink() + calc = self.bom_id.calculate_requirement_for_qty(self.order_qty) + lines = [] + + # Fabric Requirements + for fab in self.bom_id.fabric_line_ids: + factor = self.order_qty / (self.bom_id.base_qty or 1.0) + req_kg = round(fab.gross_consumption_kg * factor, 2) + product = fab.product_id + avail_stock = product.qty_available if product else 0.0 + shortage = max(0.0, req_kg - avail_stock) + lines.append((0, 0, { + 'material_type': 'fabric', + 'name': f"{fab.fabric_type_id.name or 'Fabric'} ({fab.component_type.upper()})", + 'product_id': product.id if product else False, + 'required_qty': req_kg, + 'uom_name': 'Kg', + 'available_qty': avail_stock, + 'shortage_qty': shortage, + 'to_purchase_qty': shortage, + 'unit_price': fab.cost_per_kg or 320.0, + })) + + # Trims Requirements + for trim in self.bom_id.trim_line_ids: + factor = self.order_qty / (self.bom_id.base_qty or 1.0) + req_qty = round(trim.gross_qty * factor, 2) + product = trim.product_id + avail_stock = product.qty_available if product else 0.0 + shortage = max(0.0, req_qty - avail_stock) + lines.append((0, 0, { + 'material_type': 'trim', + 'name': trim.item_name, + 'product_id': product.id if product else False, + 'required_qty': req_qty, + 'uom_name': trim.uom_id.name if trim.uom_id else 'Units', + 'available_qty': avail_stock, + 'shortage_qty': shortage, + 'to_purchase_qty': shortage, + 'unit_price': trim.unit_cost or 1.5, + })) + + self.write({ + 'line_ids': lines, + 'state': 'calculated', + }) + + def action_generate_purchase_rfqs(self): + """Generates draft Purchase Orders for all shortage materials""" + self.ensure_one() + shortage_lines = self.line_ids.filtered(lambda l: l.to_purchase_qty > 0) + if not shortage_lines: + raise UserError(_("No material shortages found to purchase.")) + + # Find or create a default vendor if product doesn't have one + default_vendor = self.env['res.partner'].search([('supplier_rank', '>', 0)], limit=1) + if not default_vendor: + default_vendor = self.env['res.partner'].create({ + 'name': 'Garment Raw Materials Supplier', + 'supplier_rank': 1, + }) + + po_lines = [] + for line in shortage_lines: + product = line.product_id + if not product: + product = self.env['product.product'].create({ + 'name': line.name, + 'type': 'consu', + }) + line.product_id = product.id + + po_lines.append((0, 0, { + 'product_id': product.id, + 'name': f"Requirement for {self.production_plan_id.name}: {line.name}", + 'product_qty': line.to_purchase_qty, + 'price_unit': line.unit_price, + 'date_planned': fields.Datetime.now(), + })) + + po = self.env['purchase.order'].create({ + 'partner_id': default_vendor.id, + 'origin': f"MRP: {self.name} / {self.production_plan_id.name}", + 'order_line': po_lines, + }) + self.write({ + 'purchase_order_ids': [(4, po.id)], + 'state': 'rfq_created', + }) + return { + 'name': _('Generated Purchase Order'), + 'type': 'ir.actions.act_window', + 'res_model': 'purchase.order', + 'res_id': po.id, + 'view_mode': 'form', + } + + +class GarmentMrpRequirementLine(models.Model): + _name = 'garment.mrp.requirement.line' + _description = 'Garment MRP Material Shortage Line' + + mrp_id = fields.Many2one('garment.mrp.requirement', string='MRP', required=True, ondelete='cascade') + material_type = fields.Selection([ + ('fabric', 'Fabric (Kg)'), + ('trim', 'Trims & Accessories'), + ('packaging', 'Packaging Material'), + ], string='Type', default='fabric') + name = fields.Char(string='Material Description', required=True) + product_id = fields.Many2one('product.product', string='Inventory Product') + uom_name = fields.Char(string='UoM', default='Kg') + + required_qty = fields.Float(string='Required Quantity', default=0.0) + available_qty = fields.Float(string='Available Stock', default=0.0) + shortage_qty = fields.Float(string='Stock Shortage', default=0.0) + to_purchase_qty = fields.Float(string='To Purchase Qty', default=0.0) + unit_price = fields.Float(string='Est. Unit Price', default=0.0) + shortage_cost = fields.Float(string='Est. Shortage Cost', compute='_compute_shortage_cost', store=True) + + @api.depends('to_purchase_qty', 'unit_price') + def _compute_shortage_cost(self): + for line in self: + line.shortage_cost = line.to_purchase_qty * line.unit_price diff --git a/addons/garment_erp/models/packing.py b/addons/garment_erp/models/packing.py new file mode 100644 index 0000000..d2be972 --- /dev/null +++ b/addons/garment_erp/models/packing.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + +class GarmentPackingPlan(models.Model): + _name = 'garment.packing.plan' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _description = 'Garment Export & Retail Packing Management' + _order = 'name desc' + + name = fields.Char(string='Packing Plan No', required=True, default=lambda self: _('New'), copy=False, tracking=True) + production_plan_id = fields.Many2one('garment.production.plan', string='Production Order', required=True, tracking=True) + sale_order_id = fields.Many2one('sale.order', string='Sales Order', tracking=True) + style_id = fields.Many2one('garment.style', string='Garment Style', required=True, tracking=True) + company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company) + + packing_type = fields.Selection([ + ('solid_color_solid_size', 'Solid Color / Solid Size (Single Variant per Box)'), + ('solid_color_assorted_size', 'Solid Color / Assorted Size (Ratio Pack)'), + ('assorted_color_assorted_size', 'Assorted Color / Assorted Size (Pre-Pack)'), + ], string='Carton Packing Method', default='solid_color_assorted_size', required=True, tracking=True) + + target_pack_qty = fields.Integer(string='Target Pack Pieces', default=1000, required=True) + total_packed_qty = fields.Integer(string='Total Packed Pieces', compute='_compute_carton_totals', store=True) + total_cartons = fields.Integer(string='Total Cartons', compute='_compute_carton_totals', store=True) + total_cbm = fields.Float(string='Total Shipment CBM (m³)', compute='_compute_carton_totals', store=True) + total_gross_weight = fields.Float(string='Total Gross Weight (Kg)', compute='_compute_carton_totals', store=True) + + carton_ids = fields.One2many('garment.carton', 'packing_plan_id', string='Master Cartons') + + state = fields.Selection([ + ('draft', 'Draft'), + ('in_packing', 'Packing In Progress'), + ('completed', 'Packed & Transferred to FG'), + ], string='Status', default='draft', tracking=True, required=True) + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', _('New')) == _('New'): + vals['name'] = self.env['ir.sequence'].next_by_code('garment.packing.plan') or _('PACK/%s') % fields.Date.today().strftime('%Y%m%d') + return super(GarmentPackingPlan, self).create(vals_list) + + @api.depends('carton_ids.total_pieces', 'carton_ids.cbm', 'carton_ids.gross_weight_kg') + def _compute_carton_totals(self): + for plan in self: + plan.total_packed_qty = sum(plan.carton_ids.mapped('total_pieces')) + plan.total_cartons = len(plan.carton_ids) + plan.total_cbm = round(sum(plan.carton_ids.mapped('cbm')), 3) + plan.total_gross_weight = round(sum(plan.carton_ids.mapped('gross_weight_kg')), 2) + + def action_start_packing(self): + self.write({'state': 'in_packing'}) + + def action_transfer_to_finished_goods(self): + """Creates or updates finished goods stock records from packed cartons""" + self.ensure_one() + if not self.carton_ids: + raise UserError(_("No cartons found in this packing plan.")) + + FG = self.env['garment.finished.goods'] + for carton in self.carton_ids: + for line in carton.line_ids: + existing_fg = FG.search([ + ('style_id', '=', self.style_id.id), + ('color_id', '=', line.color_id.id), + ('size_id', '=', line.size_id.id), + ], limit=1) + if existing_fg: + existing_fg.write({'on_hand_qty': existing_fg.on_hand_qty + line.quantity}) + else: + FG.create({ + 'style_id': self.style_id.id, + ('color_id'): line.color_id.id, + ('size_id'): line.size_id.id, + 'on_hand_qty': line.quantity, + }) + self.write({'state': 'completed'}) + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Finished Goods Stock Updated'), + 'message': _('Successfully transferred %d packed garments into Finished Goods Inventory.') % self.total_packed_qty, + 'sticky': False, + 'type': 'success', + } + } + + +class GarmentCarton(models.Model): + _name = 'garment.carton' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _description = 'Master Export Carton' + _order = 'carton_no, name' + + name = fields.Char(string='Carton ID', required=True, default=lambda self: _('New'), copy=False) + carton_no = fields.Integer(string='Carton #', default=1, required=True) + packing_plan_id = fields.Many2one('garment.packing.plan', string='Packing Plan', required=True, ondelete='cascade') + barcode = fields.Char(string='Carton Barcode / Shipping QR', index=True, copy=False) + style_id = fields.Many2one('garment.style', string='Garment Style', related='packing_plan_id.style_id', readonly=True) + + # Dimensions & Weights + length_cm = fields.Float(string='Length (cm)', default=60.0) + width_cm = fields.Float(string='Width (cm)', default=40.0) + height_cm = fields.Float(string='Height (cm)', default=30.0) + cbm = fields.Float(string='Carton CBM (m³)', compute='_compute_cbm', store=True) + + net_weight_kg = fields.Float(string='Net Weight (Kg)', default=12.5) + gross_weight_kg = fields.Float(string='Gross Weight (Kg)', default=14.0) + + line_ids = fields.One2many('garment.carton.line', 'carton_id', string='Carton Garment Breakdown') + total_pieces = fields.Integer(string='Total Pieces in Box', compute='_compute_pieces', store=True) + dispatch_id = fields.Many2one('garment.dispatch', string='Dispatch Order') + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', _('New')) == _('New'): + vals['name'] = self.env['ir.sequence'].next_by_code('garment.carton') or _('CTN-%s') % fields.Date.today().strftime('%Y%m%d') + if not vals.get('barcode'): + vals['barcode'] = vals['name'] + return super(GarmentCarton, self).create(vals_list) + + @api.depends('length_cm', 'width_cm', 'height_cm') + def _compute_cbm(self): + for ctn in self: + ctn.cbm = round((ctn.length_cm * ctn.width_cm * ctn.height_cm) / 1000000.0, 4) + + @api.depends('line_ids.quantity') + def _compute_pieces(self): + for ctn in self: + ctn.total_pieces = sum(ctn.line_ids.mapped('quantity')) + + +class GarmentCartonLine(models.Model): + _name = 'garment.carton.line' + _description = 'Carton Contents Breakdown Line' + + carton_id = fields.Many2one('garment.carton', string='Carton', 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='Pieces in Carton', default=10, required=True) diff --git a/addons/garment_erp/models/production_plan.py b/addons/garment_erp/models/production_plan.py new file mode 100644 index 0000000..22c541d --- /dev/null +++ b/addons/garment_erp/models/production_plan.py @@ -0,0 +1,265 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + +class GarmentProductionPlan(models.Model): + _name = 'garment.production.plan' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _description = 'Garment Production Master Plan' + _order = 'name desc' + + name = fields.Char(string='Plan Number', required=True, default=lambda self: _('New'), copy=False, tracking=True) + sale_order_id = fields.Many2one('sale.order', string='Sales Order', tracking=True) + style_id = fields.Many2one('garment.style', string='Garment Style', required=True, tracking=True) + bom_id = fields.Many2one('garment.bom', string='Garment BOM', tracking=True) + company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company) + + # Dates & Scheduling + date_start = fields.Date(string='Production Start Date', default=fields.Date.today, tracking=True) + date_delivery = fields.Date(string='Target Delivery Date', tracking=True) + priority = fields.Selection([ + ('0', 'Low'), + ('1', 'Normal'), + ('2', 'High'), + ('3', 'Urgent'), + ], string='Priority', default='1', tracking=True) + + # State Workflow + state = fields.Selection([ + ('draft', 'Draft'), + ('planned', 'Planned / Scheduled'), + ('in_progress', 'In Production'), + ('completed', 'Completed'), + ('cancelled', 'Cancelled'), + ], string='Status', default='draft', tracking=True, required=True) + + # Quantitative Progress Tracking + planned_qty = fields.Integer(string='Planned Quantity (Pcs)', required=True, default=1000, tracking=True) + cut_qty = fields.Integer(string='Cut Quantity', compute='_compute_stage_quantities', store=True) + sewn_qty = fields.Integer(string='Sewn Quantity', compute='_compute_stage_quantities', store=True) + packed_qty = fields.Integer(string='Packed Quantity', compute='_compute_stage_quantities', store=True) + dispatched_qty = fields.Integer(string='Dispatched Quantity', compute='_compute_stage_quantities', store=True) + progress_pct = fields.Float(string='Production Progress %', compute='_compute_progress_pct', store=True) + + # Line Breakdown (Color x Size breakdown for this production order) + line_ids = fields.One2many('garment.production.plan.line', 'plan_id', string='Color/Size Production Targets') + + # Child Records + cutting_plan_ids = fields.One2many('garment.cutting.plan', 'production_plan_id', string='Cutting Orders') + bundle_ids = fields.One2many('garment.bundle', 'production_plan_id', string='Bundles') + jobwork_order_ids = fields.One2many('garment.jobwork.order', 'production_plan_id', string='Job Work Orders') + qc_inspection_ids = fields.One2many('garment.quality.inspection', 'production_plan_id', string='QC Inspections') + packing_plan_ids = fields.One2many('garment.packing.plan', 'production_plan_id', string='Packing Plans') + mrp_requirement_ids = fields.One2many('garment.mrp.requirement', 'production_plan_id', string='MRP Requirements') + + # Smart Button Counts + cutting_count = fields.Integer(string='Cutting Count', compute='_compute_counts') + bundle_count = fields.Integer(string='Bundle Count', compute='_compute_counts') + jobwork_count = fields.Integer(string='Job Work Count', compute='_compute_counts') + qc_count = fields.Integer(string='QC Count', compute='_compute_counts') + packing_count = fields.Integer(string='Packing Count', compute='_compute_counts') + notes = fields.Html(string='Production Notes') + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', _('New')) == _('New'): + vals['name'] = self.env['ir.sequence'].next_by_code('garment.production.plan') or _('PP/%s') % fields.Date.today().strftime('%Y%m%d') + plans = super().create(vals_list) + for plan in plans: + if plan.sale_order_id and not plan.line_ids: + plan._populate_lines_from_sale_order() + return plans + + def _populate_lines_from_sale_order(self): + self.ensure_one() + if not self.sale_order_id: + return + lines = [] + for m_line in self.sale_order_id.order_matrix_line_ids: + lines.append((0, 0, { + 'color_id': m_line.color_id.id, + 'size_id': m_line.size_id.id, + 'planned_qty': m_line.quantity, + })) + if lines: + self.write({'line_ids': lines}) + + @api.onchange('style_id') + def _onchange_style_id(self): + if self.style_id: + bom = self.env['garment.bom'].search([('style_id', '=', self.style_id.id)], limit=1) + if bom: + self.bom_id = bom.id + + @api.depends('cutting_plan_ids.actual_cut_qty', 'packing_plan_ids.total_packed_qty') + def _compute_stage_quantities(self): + for plan in self: + plan.cut_qty = sum(plan.cutting_plan_ids.mapped('actual_cut_qty')) + plan.packed_qty = sum(plan.packing_plan_ids.mapped('total_packed_qty')) + # Sewn qty from bundles completed in sewing + sewn_bundles = plan.bundle_ids.filtered(lambda b: b.stage in ['quality', 'packing', 'completed']) + plan.sewn_qty = sum(sewn_bundles.mapped('quantity')) + # Dispatched qty from dispatch orders + dispatches = self.env['garment.dispatch'].search([('production_plan_id', '=', plan.id), ('state', '=', 'dispatched')]) + plan.dispatched_qty = sum(dispatches.mapped('total_pieces')) + + @api.depends('planned_qty', 'packed_qty') + def _compute_progress_pct(self): + for plan in self: + if plan.planned_qty > 0: + plan.progress_pct = min(100.0, round((plan.packed_qty / plan.planned_qty) * 100.0, 1)) + else: + plan.progress_pct = 0.0 + + @api.depends('cutting_plan_ids', 'bundle_ids', 'jobwork_order_ids', 'qc_inspection_ids', 'packing_plan_ids') + def _compute_counts(self): + for plan in self: + plan.cutting_count = len(plan.cutting_plan_ids) + plan.bundle_count = len(plan.bundle_ids) + plan.jobwork_count = len(plan.jobwork_order_ids) + plan.qc_count = len(plan.qc_inspection_ids) + plan.packing_count = len(plan.packing_plan_ids) + + # Workflow Actions + def action_plan(self): + self.write({'state': 'planned'}) + + def action_start(self): + self.write({'state': 'in_progress'}) + + def action_complete(self): + self.write({'state': 'completed'}) + + def action_cancel(self): + self.write({'state': 'cancelled'}) + + def action_reset_to_draft(self): + self.write({'state': 'draft'}) + + def action_create_cutting_plan(self): + self.ensure_one() + cut_plan = self.env['garment.cutting.plan'].create({ + 'production_plan_id': self.id, + 'style_id': self.style_id.id, + 'planned_cut_qty': self.planned_qty, + 'color_id': self.line_ids[0].color_id.id if self.line_ids else False, + }) + return { + 'name': _('Cutting Order'), + 'type': 'ir.actions.act_window', + 'res_model': 'garment.cutting.plan', + 'res_id': cut_plan.id, + 'view_mode': 'form', + } + + def action_create_mrp_requirement(self): + self.ensure_one() + if not self.bom_id: + raise UserError(_("Please assign a BOM to calculate Material Requirements.")) + req = self.env['garment.mrp.requirement'].create({ + 'production_plan_id': self.id, + 'style_id': self.style_id.id, + 'bom_id': self.bom_id.id, + 'order_qty': self.planned_qty, + }) + req.action_compute_requirements() + return { + 'name': _('Material Requirements Calculation'), + 'type': 'ir.actions.act_window', + 'res_model': 'garment.mrp.requirement', + 'res_id': req.id, + 'view_mode': 'form', + } + + def action_create_packing_plan(self): + self.ensure_one() + pack = self.env['garment.packing.plan'].create({ + 'production_plan_id': self.id, + 'sale_order_id': self.sale_order_id.id if self.sale_order_id else False, + 'style_id': self.style_id.id, + 'target_pack_qty': self.planned_qty, + }) + return { + 'name': _('Packing Plan'), + 'type': 'ir.actions.act_window', + 'res_model': 'garment.packing.plan', + 'res_id': pack.id, + 'view_mode': 'form', + } + + # Smart Button navigation actions + def action_view_cutting_plans(self): + self.ensure_one() + return { + 'name': _('Cutting Orders (%s)') % self.name, + 'type': 'ir.actions.act_window', + 'res_model': 'garment.cutting.plan', + 'view_mode': 'list,form', + 'domain': [('production_plan_id', '=', self.id)], + 'context': {'default_production_plan_id': self.id, 'default_style_id': self.style_id.id}, + } + + def action_view_bundles(self): + self.ensure_one() + return { + 'name': _('Production Bundles (%s)') % self.name, + 'type': 'ir.actions.act_window', + 'res_model': 'garment.bundle', + 'view_mode': 'list,form', + 'domain': [('production_plan_id', '=', self.id)], + 'context': {'default_production_plan_id': self.id}, + } + + def action_view_jobwork(self): + self.ensure_one() + return { + 'name': _('Job Work Orders (%s)') % self.name, + 'type': 'ir.actions.act_window', + 'res_model': 'garment.jobwork.order', + 'view_mode': 'list,form', + 'domain': [('production_plan_id', '=', self.id)], + 'context': {'default_production_plan_id': self.id}, + } + + def action_view_qc(self): + self.ensure_one() + return { + 'name': _('Quality Inspections (%s)') % self.name, + 'type': 'ir.actions.act_window', + 'res_model': 'garment.quality.inspection', + 'view_mode': 'list,form', + 'domain': [('production_plan_id', '=', self.id)], + 'context': {'default_production_plan_id': self.id}, + } + + def action_view_packing(self): + self.ensure_one() + return { + 'name': _('Packing Plans (%s)') % self.name, + 'type': 'ir.actions.act_window', + 'res_model': 'garment.packing.plan', + 'view_mode': 'list,form', + 'domain': [('production_plan_id', '=', self.id)], + 'context': {'default_production_plan_id': self.id}, + } + + +class GarmentProductionPlanLine(models.Model): + _name = 'garment.production.plan.line' + _description = 'Production Target Line by Color & Size' + _order = 'color_id, size_id' + + plan_id = fields.Many2one('garment.production.plan', string='Production Plan', required=True, ondelete='cascade') + color_id = fields.Many2one('garment.color', string='Color', required=True) + size_id = fields.Many2one('garment.size', string='Size', required=True) + planned_qty = fields.Integer(string='Planned Pcs', required=True, default=0) + cut_qty = fields.Integer(string='Cut Pcs', default=0) + sewn_qty = fields.Integer(string='Sewn Pcs', default=0) + packed_qty = fields.Integer(string='Packed Pcs', default=0) + variance = fields.Integer(string='Balance to Pack', compute='_compute_variance', store=True) + + @api.depends('planned_qty', 'packed_qty') + def _compute_variance(self): + for line in self: + line.variance = line.planned_qty - line.packed_qty diff --git a/addons/garment_erp/models/quality_control.py b/addons/garment_erp/models/quality_control.py new file mode 100644 index 0000000..834890b --- /dev/null +++ b/addons/garment_erp/models/quality_control.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ + +class GarmentDefectType(models.Model): + _name = 'garment.defect.type' + _description = 'Garment Quality Defect Category' + _order = 'category, name' + + name = fields.Char(string='Defect Name', required=True) + code = fields.Char(string='Defect Code', required=True) + category = fields.Selection([ + ('stitching', 'Stitching / Sewing Defect'), + ('fabric', 'Fabric / Yarn Flaw'), + ('measurement', 'Measurement / Spec Variance'), + ('print_embroidery', 'Print / Embroidery Defect'), + ('finishing_packing', 'Finishing / Staining / Packing'), + ], string='Category', default='stitching', required=True) + severity = fields.Selection([ + ('minor', 'Minor Defect'), + ('major', 'Major Defect'), + ('critical', 'Critical (Must Reject)'), + ], string='Defect Severity', default='major') + active = fields.Boolean(default=True) + + +class GarmentQualityInspection(models.Model): + _name = 'garment.quality.inspection' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _description = 'Garment Quality Control Inspection' + _order = 'name desc' + + name = fields.Char(string='Inspection Number', required=True, default=lambda self: _('New'), copy=False, tracking=True) + inspection_stage = fields.Selection([ + ('fabric', '1. Fabric Inspection'), + ('cutting', '2. Cutting Section QC'), + ('sewing_inline', '3. Sewing Inline Roaming QC'), + ('sewing_endline', '4. Sewing Endline 100% Checking'), + ('jobwork', '5. Job Work Inward QC'), + ('final_aql', '6. Final AQL 2.5 Pre-Shipment Audit'), + ], string='Inspection Stage', default='sewing_endline', required=True, tracking=True) + + production_plan_id = fields.Many2one('garment.production.plan', string='Production Order', tracking=True) + style_id = fields.Many2one('garment.style', string='Garment Style', tracking=True) + color_id = fields.Many2one('garment.color', string='Color') + size_id = fields.Many2one('garment.size', string='Size') + bundle_id = fields.Many2one('garment.bundle', string='Bundle Ref') + sewing_line_id = fields.Many2one('garment.sewing.line', string='Sewing Line') + + inspector_id = fields.Many2one('res.users', string='QA Inspector', default=lambda self: self.env.user, tracking=True) + date_inspected = fields.Date(string='Inspection Date', default=fields.Date.today, tracking=True) + + # Quantities & AQL + total_lot_qty = fields.Integer(string='Lot / Batch Quantity', default=100) + sample_size = fields.Integer(string='Sample Quantity Checked', default=20, required=True) + passed_qty = fields.Integer(string='Passed Quantity', default=0, tracking=True) + failed_qty = fields.Integer(string='Defective Quantity', default=0, tracking=True) + rework_qty = fields.Integer(string='Sent for Rework', default=0) + scrapped_qty = fields.Integer(string='Permanently Scrapped', default=0) + + defect_line_ids = fields.One2many('garment.quality.defect.line', 'inspection_id', string='Defect Details') + + verdict = fields.Selection([ + ('passed', 'Quality Approved (Pass)'), + ('rework_needed', 'Rework Required'), + ('rejected', 'Completely Rejected / Scrapped'), + ], string='Final Verdict', default='passed', tracking=True, required=True) + + rework_order_ids = fields.One2many('garment.rework.order', 'inspection_id', string='Linked Rework Orders') + rework_count = fields.Integer(string='Rework Orders', compute='_compute_rework_count') + remarks = fields.Text(string='Inspector Notes') + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', _('New')) == _('New'): + vals['name'] = self.env['ir.sequence'].next_by_code('garment.quality.inspection') or _('QC/%s') % fields.Date.today().strftime('%Y%m%d') + return super(GarmentQualityInspection, self).create(vals_list) + + @api.depends('rework_order_ids') + def _compute_rework_count(self): + for rec in self: + rec.rework_count = len(rec.rework_order_ids) + + def action_pass(self): + self.write({'verdict': 'passed'}) + + def action_trigger_rework(self): + """Creates an automatic Rework Order for failed pieces""" + self.ensure_one() + rework_qty = self.rework_qty or self.failed_qty or 1 + rework = self.env['garment.rework.order'].create({ + 'inspection_id': self.id, + 'production_plan_id': self.production_plan_id.id if self.production_plan_id else False, + 'style_id': self.style_id.id if self.style_id else False, + 'color_id': self.color_id.id if self.color_id else False, + 'size_id': self.size_id.id if self.size_id else False, + 'bundle_id': self.bundle_id.id if self.bundle_id else False, + 'rework_qty': rework_qty, + 'defect_description': self.remarks or 'Defects identified during quality inspection', + 'assigned_dept': 'sewing', + }) + self.write({'verdict': 'rework_needed', 'rework_qty': rework_qty}) + return { + 'name': _('Rework Order'), + 'type': 'ir.actions.act_window', + 'res_model': 'garment.rework.order', + 'res_id': rework.id, + 'view_mode': 'form', + } + + +class GarmentQualityDefectLine(models.Model): + _name = 'garment.quality.defect.line' + _description = 'Quality Defect Occurrence Line' + + inspection_id = fields.Many2one('garment.quality.inspection', string='Inspection', required=True, ondelete='cascade') + defect_type_id = fields.Many2one('garment.defect.type', string='Defect Type', required=True) + category = fields.Selection(related='defect_type_id.category', readonly=True) + defect_count = fields.Integer(string='Defect Count', default=1, required=True) + action_type = fields.Selection([ + ('rework', 'Alteration / Rework'), + ('scrap', 'Permanent Rejection / Scrap'), + ], string='Action Required', default='rework') + notes = fields.Char(string='Defect Location / Remarks') diff --git a/addons/garment_erp/models/rework.py b/addons/garment_erp/models/rework.py new file mode 100644 index 0000000..2bb7dde --- /dev/null +++ b/addons/garment_erp/models/rework.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ + +class GarmentReworkOrder(models.Model): + _name = 'garment.rework.order' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _description = 'Garment Quality Rework Order' + _order = 'name desc' + + name = fields.Char(string='Rework Order No', required=True, default=lambda self: _('New'), copy=False, tracking=True) + inspection_id = fields.Many2one('garment.quality.inspection', string='Originating QC Inspection', required=True, tracking=True) + production_plan_id = fields.Many2one('garment.production.plan', string='Production Plan', tracking=True) + style_id = fields.Many2one('garment.style', string='Style', tracking=True) + color_id = fields.Many2one('garment.color', string='Color') + size_id = fields.Many2one('garment.size', string='Size') + bundle_id = fields.Many2one('garment.bundle', string='Bundle Ref') + + # Quantities + rework_qty = fields.Integer(string='Rework Quantity (Pcs)', required=True, default=1, tracking=True) + passed_rework_qty = fields.Integer(string='Passed After Rework', default=0) + scrapped_rework_qty = fields.Integer(string='Unrepairable / Scrapped', default=0) + + # Assignment + assigned_dept = fields.Selection([ + ('sewing', 'Sewing Floor Alteration'), + ('cutting', 'Cutting Panel Replacement'), + ('finishing', 'Spot Cleaning / Ironing'), + ('jobwork', 'Job Worker Re-processing'), + ], string='Responsible Section', default='sewing', required=True, tracking=True) + assigned_operator_id = fields.Many2one('res.users', string='Assigned Operator / Tailor', tracking=True) + date_assigned = fields.Date(string='Date Assigned', default=fields.Date.today) + date_completed = fields.Date(string='Date Completed') + + rework_cost = fields.Float(string='Estimated Rework Cost (INR)', default=15.0) + defect_description = fields.Text(string='Defect Description & Alteration Instructions', required=True) + + state = fields.Selection([ + ('assigned', 'Assigned for Rework'), + ('in_rework', 'In Rework / Alteration'), + ('ready_recheck', 'Ready for QA Re-check'), + ('passed', 'Passed QA Re-check (Restored)'), + ('scrapped', 'Irreparable / Scrapped'), + ], string='Status', default='assigned', tracking=True, required=True) + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', _('New')) == _('New'): + vals['name'] = self.env['ir.sequence'].next_by_code('garment.rework.order') or _('RWK/%s') % fields.Date.today().strftime('%Y%m%d') + return super(GarmentReworkOrder, self).create(vals_list) + + def action_start_rework(self): + self.write({'state': 'in_rework'}) + + def action_ready_recheck(self): + self.write({'state': 'ready_recheck'}) + + def action_pass_recheck(self): + self.write({ + 'state': 'passed', + 'passed_rework_qty': self.rework_qty, + 'date_completed': fields.Date.today(), + }) + + def action_scrap_rework(self): + self.write({ + 'state': 'scrapped', + 'scrapped_rework_qty': self.rework_qty, + 'date_completed': fields.Date.today(), + }) diff --git a/addons/garment_erp/models/sale_order.py b/addons/garment_erp/models/sale_order.py new file mode 100644 index 0000000..a171978 --- /dev/null +++ b/addons/garment_erp/models/sale_order.py @@ -0,0 +1,222 @@ +# -*- 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 = "

No matrix lines configured yet.

" + 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 = [""" +
+ + + + + """] + for s in sizes: + html.append(f"") + html.append("") + + 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"") + html.append(f"") + 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"{qty:,}" if qty else "-" + html.append(f"") + grand_total += row_sum + html.append(f"") + + # Footer total row + html.append("") + html.append("") + for s in sizes: + html.append(f"") + html.append(f"") + html.append("
Color \\ Size{s.code}Total Pcs
{c.name}{cell_val}{row_sum:,}
Total Pcs{total_col[s.id]:,}{grand_total:,}
") + + 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 diff --git a/addons/garment_erp/models/sewing_line.py b/addons/garment_erp/models/sewing_line.py new file mode 100644 index 0000000..e5b06f9 --- /dev/null +++ b/addons/garment_erp/models/sewing_line.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ + +class GarmentSewingLine(models.Model): + _name = 'garment.sewing.line' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _description = 'Garment Sewing Line' + _order = 'name' + + name = fields.Char(string='Line Name', required=True) + code = fields.Char(string='Line Code', required=True) + supervisor_id = fields.Many2one('res.users', string='Line Supervisor', tracking=True) + operator_count = fields.Integer(string='Standard Operators', default=20) + helper_count = fields.Integer(string='Helpers / Feeders', default=4) + daily_capacity_pcs = fields.Integer(string='Standard Daily Target (Pcs)', default=1000) + active = fields.Boolean(default=True) + notes = fields.Text(string='Machine Layout / Setup Notes') + + _code_uniq = models.Constraint('UNIQUE(code)', 'Line Code must be unique!') + + +class GarmentSewingHourly(models.Model): + _name = 'garment.sewing.hourly' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _description = 'Sewing Floor Hourly Production Log' + _order = 'date desc, line_id' + + name = fields.Char(string='Reference', compute='_compute_name', store=True) + line_id = fields.Many2one('garment.sewing.line', string='Sewing Line', required=True, tracking=True) + date = fields.Date(string='Production Date', default=fields.Date.today, required=True, tracking=True) + shift = fields.Selection([('general', 'General Shift (08:30-17:30)'), ('shift_a', 'Shift A'), ('shift_b', 'Shift B')], string='Shift', default='general') + + style_id = fields.Many2one('garment.style', string='Garment Style Running', required=True, tracking=True) + production_plan_id = fields.Many2one('garment.production.plan', string='Production Order', tracking=True) + + # Operators & SAM + operators_present = fields.Integer(string='Operators Present', default=20, required=True) + sam = fields.Float(string='Garment SAM (Minutes)', default=12.0, required=True) + working_minutes_per_hour = fields.Integer(string='Minutes / Hour', default=60) + + # Hourly Outputs (Target, Actual, Rejections) + h1_target = fields.Integer(string='H1 Target (08:30-09:30)', default=120) + h1_actual = fields.Integer(string='H1 Actual', default=0) + h1_defect = fields.Integer(string='H1 Defect', default=0) + + h2_target = fields.Integer(string='H2 Target (09:30-10:30)', default=120) + h2_actual = fields.Integer(string='H2 Actual', default=0) + h2_defect = fields.Integer(string='H2 Defect', default=0) + + h3_target = fields.Integer(string='H3 Target (10:30-11:30)', default=120) + h3_actual = fields.Integer(string='H3 Actual', default=0) + h3_defect = fields.Integer(string='H3 Defect', default=0) + + h4_target = fields.Integer(string='H4 Target (11:30-12:30)', default=120) + h4_actual = fields.Integer(string='H4 Actual', default=0) + h4_defect = fields.Integer(string='H4 Defect', default=0) + + h5_target = fields.Integer(string='H5 Target (13:15-14:15)', default=120) + h5_actual = fields.Integer(string='H5 Actual', default=0) + h5_defect = fields.Integer(string='H5 Defect', default=0) + + h6_target = fields.Integer(string='H6 Target (14:15-15:15)', default=120) + h6_actual = fields.Integer(string='H6 Actual', default=0) + h6_defect = fields.Integer(string='H6 Defect', default=0) + + h7_target = fields.Integer(string='H7 Target (15:15-16:15)', default=120) + h7_actual = fields.Integer(string='H7 Actual', default=0) + h7_defect = fields.Integer(string='H7 Defect', default=0) + + h8_target = fields.Integer(string='H8 Target (16:15-17:15)', default=120) + h8_actual = fields.Integer(string='H8 Actual', default=0) + h8_defect = fields.Integer(string='H8 Defect', default=0) + + ot_target = fields.Integer(string='OT Target (Overtime)', default=0) + ot_actual = fields.Integer(string='OT Actual', default=0) + ot_defect = fields.Integer(string='OT Defect', default=0) + + # Cumulative Day Totals + total_target = fields.Integer(string='Total Day Target', compute='_compute_totals', store=True) + total_actual = fields.Integer(string='Total Day Output', compute='_compute_totals', store=True) + total_defects = fields.Integer(string='Total Alterations / Defects', compute='_compute_totals', store=True) + + total_worked_hours = fields.Float(string='Total Hours Worked', default=8.0) + efficiency_pct = fields.Float(string='Line Efficiency %', compute='_compute_efficiency', store=True) + efficiency_badge = fields.Selection([ + ('excellent', 'Excellent (>=90%)'), + ('good', 'Good (75%-89%)'), + ('needs_attention', 'Below Target (<75%)'), + ], string='Performance Rating', compute='_compute_efficiency', store=True) + + @api.depends('line_id', 'date') + def _compute_name(self): + for rec in self: + line_name = rec.line_id.name if rec.line_id else 'Line' + rec.name = f"{line_name} - {rec.date}" + + @api.onchange('line_id') + def _onchange_line_id(self): + if self.line_id: + self.operators_present = self.line_id.operator_count + + @api.onchange('style_id') + def _onchange_style_id(self): + if self.style_id: + self.sam = self.style_id.sam or 12.0 + + @api.depends('h1_target', 'h2_target', 'h3_target', 'h4_target', 'h5_target', 'h6_target', 'h7_target', 'h8_target', 'ot_target', + 'h1_actual', 'h2_actual', 'h3_actual', 'h4_actual', 'h5_actual', 'h6_actual', 'h7_actual', 'h8_actual', 'ot_actual', + 'h1_defect', 'h2_defect', 'h3_defect', 'h4_defect', 'h5_defect', 'h6_defect', 'h7_defect', 'h8_defect', 'ot_defect') + def _compute_totals(self): + for rec in self: + rec.total_target = (rec.h1_target + rec.h2_target + rec.h3_target + rec.h4_target + + rec.h5_target + rec.h6_target + rec.h7_target + rec.h8_target + rec.ot_target) + rec.total_actual = (rec.h1_actual + rec.h2_actual + rec.h3_actual + rec.h4_actual + + rec.h5_actual + rec.h6_actual + rec.h7_actual + rec.h8_actual + rec.ot_actual) + rec.total_defects = (rec.h1_defect + rec.h2_defect + rec.h3_defect + rec.h4_defect + + rec.h5_defect + rec.h6_defect + rec.h7_defect + rec.h8_defect + rec.ot_defect) + + @api.depends('total_actual', 'sam', 'operators_present', 'total_worked_hours', 'working_minutes_per_hour') + def _compute_efficiency(self): + for rec in self: + total_minutes = (rec.operators_present or 1) * (rec.total_worked_hours or 8.0) * (rec.working_minutes_per_hour or 60) + produced_minutes = (rec.total_actual or 0) * (rec.sam or 1.0) + if total_minutes > 0: + eff = round((produced_minutes / total_minutes) * 100.0, 1) + else: + eff = 0.0 + rec.efficiency_pct = eff + if eff >= 90.0: + rec.efficiency_badge = 'excellent' + elif eff >= 75.0: + rec.efficiency_badge = 'good' + else: + rec.efficiency_badge = 'needs_attention' diff --git a/addons/garment_erp/report/garment_report_actions.xml b/addons/garment_erp/report/garment_report_actions.xml new file mode 100644 index 0000000..04c2357 --- /dev/null +++ b/addons/garment_erp/report/garment_report_actions.xml @@ -0,0 +1,105 @@ + + + + + + + Garment Order Confirmation (Matrix) + sale.order + qweb-pdf + garment_erp.report_sales_order_matrix_template + garment_erp.report_sales_order_matrix_template + + report + + + + + Production Plan & Floor Traveler + garment.production.plan + qweb-pdf + garment_erp.report_production_plan_template + garment_erp.report_production_plan_template + + report + + + + + Bundle Barcode Tickets + garment.bundle + qweb-pdf + garment_erp.report_bundle_barcode_template + garment_erp.report_bundle_barcode_template + + report + + + + + Fabric 4-Point Inspection Certificate + garment.fabric.inspection + qweb-pdf + garment_erp.report_fabric_inspection_template + garment_erp.report_fabric_inspection_template + + report + + + + + Cutting Lay & Marker Sheet + garment.cutting.plan + qweb-pdf + garment_erp.report_cutting_plan_template + garment_erp.report_cutting_plan_template + + report + + + + + Job Work Delivery Challan (GST) + garment.jobwork.order + qweb-pdf + garment_erp.report_job_work_challan_template + garment_erp.report_job_work_challan_template + + report + + + + + Quality Inspection Audit Certificate + garment.quality.inspection + qweb-pdf + garment_erp.report_quality_inspection_template + garment_erp.report_quality_inspection_template + + report + + + + + Master Export Packing List + garment.packing.plan + qweb-pdf + garment_erp.report_packing_list_template + garment_erp.report_packing_list_template + + report + + + + + Garment Pre-Costing Analysis + garment.costing + qweb-pdf + garment_erp.report_garment_costing_template + garment_erp.report_garment_costing_template + + report + + + + diff --git a/addons/garment_erp/report/report_bundle_barcode_template.xml b/addons/garment_erp/report/report_bundle_barcode_template.xml new file mode 100644 index 0000000..050c8f9 --- /dev/null +++ b/addons/garment_erp/report/report_bundle_barcode_template.xml @@ -0,0 +1,54 @@ + + + + diff --git a/addons/garment_erp/report/report_cutting_plan_template.xml b/addons/garment_erp/report/report_cutting_plan_template.xml new file mode 100644 index 0000000..c69dbb8 --- /dev/null +++ b/addons/garment_erp/report/report_cutting_plan_template.xml @@ -0,0 +1,93 @@ + + + + diff --git a/addons/garment_erp/report/report_fabric_inspection_template.xml b/addons/garment_erp/report/report_fabric_inspection_template.xml new file mode 100644 index 0000000..1919457 --- /dev/null +++ b/addons/garment_erp/report/report_fabric_inspection_template.xml @@ -0,0 +1,144 @@ + + + + diff --git a/addons/garment_erp/report/report_garment_costing_template.xml b/addons/garment_erp/report/report_garment_costing_template.xml new file mode 100644 index 0000000..c181f34 --- /dev/null +++ b/addons/garment_erp/report/report_garment_costing_template.xml @@ -0,0 +1,126 @@ + + + + diff --git a/addons/garment_erp/report/report_job_work_challan_template.xml b/addons/garment_erp/report/report_job_work_challan_template.xml new file mode 100644 index 0000000..875a1a9 --- /dev/null +++ b/addons/garment_erp/report/report_job_work_challan_template.xml @@ -0,0 +1,83 @@ + + + + diff --git a/addons/garment_erp/report/report_packing_list_template.xml b/addons/garment_erp/report/report_packing_list_template.xml new file mode 100644 index 0000000..8be760b --- /dev/null +++ b/addons/garment_erp/report/report_packing_list_template.xml @@ -0,0 +1,81 @@ + + + + diff --git a/addons/garment_erp/report/report_production_plan_template.xml b/addons/garment_erp/report/report_production_plan_template.xml new file mode 100644 index 0000000..5215116 --- /dev/null +++ b/addons/garment_erp/report/report_production_plan_template.xml @@ -0,0 +1,88 @@ + + + + diff --git a/addons/garment_erp/report/report_quality_inspection_template.xml b/addons/garment_erp/report/report_quality_inspection_template.xml new file mode 100644 index 0000000..9062345 --- /dev/null +++ b/addons/garment_erp/report/report_quality_inspection_template.xml @@ -0,0 +1,90 @@ + + + + diff --git a/addons/garment_erp/report/report_sales_matrix_template.xml b/addons/garment_erp/report/report_sales_matrix_template.xml new file mode 100644 index 0000000..863c10a --- /dev/null +++ b/addons/garment_erp/report/report_sales_matrix_template.xml @@ -0,0 +1,77 @@ + + + + diff --git a/addons/garment_erp/security/garment_security.xml b/addons/garment_erp/security/garment_security.xml new file mode 100644 index 0000000..e5a29b1 --- /dev/null +++ b/addons/garment_erp/security/garment_security.xml @@ -0,0 +1,106 @@ + + + + + + + Garment Manufacturing + Comprehensive Garment Manufacturing ERP for Tiruppur Apparel Industry + 20 + + + + + Garment Manufacturing + 10 + + + + + + Garment User (Viewer) + 10 + + + + + + + Garment Merchandiser + 20 + + + + + + + Garment Production Manager + 30 + + + + + + + Fabric & Stores Manager + 40 + + + + + + + Garment Quality Manager + 50 + + + + + + + Subcontracting & Job Work Manager + 60 + + + + + + + Packing & Dispatch Manager + 70 + + + + + + + Garment ERP Administrator + 80 + + + + + + + + + + + Garment Production Plan Multi-Company + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + + + Garment Jobwork Multi-Company + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + + + Garment Packing Multi-Company + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + + diff --git a/addons/garment_erp/security/ir.model.access.csv b/addons/garment_erp/security/ir.model.access.csv new file mode 100644 index 0000000..8ed7223 --- /dev/null +++ b/addons/garment_erp/security/ir.model.access.csv @@ -0,0 +1,104 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_garment_brand_user,garment.brand.user,model_garment_brand,group_garment_user,1,0,0,0 +access_garment_brand_admin,garment.brand.admin,model_garment_brand,group_garment_admin,1,1,1,1 +access_garment_season_user,garment.season.user,model_garment_season,group_garment_user,1,0,0,0 +access_garment_season_admin,garment.season.admin,model_garment_season,group_garment_admin,1,1,1,1 +access_garment_gender_user,garment.gender.user,model_garment_gender,group_garment_user,1,0,0,0 +access_garment_gender_admin,garment.gender.admin,model_garment_gender,group_garment_admin,1,1,1,1 +access_garment_type_user,garment.type.user,model_garment_type,group_garment_user,1,0,0,0 +access_garment_type_admin,garment.type.admin,model_garment_type,group_garment_admin,1,1,1,1 +access_garment_fabric_type_user,garment.fabric.type.user,model_garment_fabric_type,group_garment_user,1,0,0,0 +access_garment_fabric_type_admin,garment.fabric.type.admin,model_garment_fabric_type,group_garment_admin,1,1,1,1 +access_garment_composition_user,garment.composition.user,model_garment_composition,group_garment_user,1,0,0,0 +access_garment_composition_admin,garment.composition.admin,model_garment_composition,group_garment_admin,1,1,1,1 +access_garment_color_user,garment.color.user,model_garment_color,group_garment_user,1,0,0,0 +access_garment_color_admin,garment.color.admin,model_garment_color,group_garment_admin,1,1,1,1 +access_garment_size_user,garment.size.user,model_garment_size,group_garment_user,1,0,0,0 +access_garment_size_admin,garment.size.admin,model_garment_size,group_garment_admin,1,1,1,1 +access_garment_style_user,garment.style.user,model_garment_style,group_garment_user,1,0,0,0 +access_garment_style_merch,garment.style.merch,model_garment_style,group_garment_merchandiser,1,1,1,1 +access_garment_style_admin,garment.style.admin,model_garment_style,group_garment_admin,1,1,1,1 +access_garment_style_measurement_user,garment.style.measurement.user,model_garment_style_measurement,group_garment_user,1,0,0,0 +access_garment_style_measurement_admin,garment.style.measurement.admin,model_garment_style_measurement,group_garment_admin,1,1,1,1 +access_garment_order_matrix_line_user,garment.order.matrix.line.user,model_garment_order_matrix_line,group_garment_user,1,0,0,0 +access_garment_order_matrix_line_merch,garment.order.matrix.line.merch,model_garment_order_matrix_line,group_garment_merchandiser,1,1,1,1 +access_garment_order_matrix_line_admin,garment.order.matrix.line.admin,model_garment_order_matrix_line,group_garment_admin,1,1,1,1 +access_garment_bom_user,garment.bom.user,model_garment_bom,group_garment_user,1,0,0,0 +access_garment_bom_prod,garment.bom.prod,model_garment_bom,group_garment_production,1,1,1,1 +access_garment_bom_admin,garment.bom.admin,model_garment_bom,group_garment_admin,1,1,1,1 +access_garment_bom_fabric_user,garment.bom.fabric.user,model_garment_bom_fabric,group_garment_user,1,0,0,0 +access_garment_bom_fabric_admin,garment.bom.fabric.admin,model_garment_bom_fabric,group_garment_admin,1,1,1,1 +access_garment_bom_trim_user,garment.bom.trim.user,model_garment_bom_trim,group_garment_user,1,0,0,0 +access_garment_bom_trim_admin,garment.bom.trim.admin,model_garment_bom_trim,group_garment_admin,1,1,1,1 +access_garment_bom_operation_user,garment.bom.operation.user,model_garment_bom_operation,group_garment_user,1,0,0,0 +access_garment_bom_operation_admin,garment.bom.operation.admin,model_garment_bom_operation,group_garment_admin,1,1,1,1 +access_garment_production_plan_user,garment.production.plan.user,model_garment_production_plan,group_garment_user,1,0,0,0 +access_garment_production_plan_prod,garment.production.plan.prod,model_garment_production_plan,group_garment_production,1,1,1,1 +access_garment_production_plan_admin,garment.production.plan.admin,model_garment_production_plan,group_garment_admin,1,1,1,1 +access_garment_production_plan_line_user,garment.production.plan.line.user,model_garment_production_plan_line,group_garment_user,1,0,0,0 +access_garment_production_plan_line_admin,garment.production.plan.line.admin,model_garment_production_plan_line,group_garment_admin,1,1,1,1 +access_garment_fabric_roll_user,garment.fabric.roll.user,model_garment_fabric_roll,group_garment_user,1,0,0,0 +access_garment_fabric_roll_inv,garment.fabric.roll.inv,model_garment_fabric_roll,group_garment_inventory,1,1,1,1 +access_garment_fabric_roll_admin,garment.fabric.roll.admin,model_garment_fabric_roll,group_garment_admin,1,1,1,1 +access_garment_fabric_inspection_user,garment.fabric.inspection.user,model_garment_fabric_inspection,group_garment_user,1,0,0,0 +access_garment_fabric_inspection_qc,garment.fabric.inspection.qc,model_garment_fabric_inspection,group_garment_quality,1,1,1,1 +access_garment_fabric_inspection_admin,garment.fabric.inspection.admin,model_garment_fabric_inspection,group_garment_admin,1,1,1,1 +access_garment_mrp_requirement_user,garment.mrp.requirement.user,model_garment_mrp_requirement,group_garment_user,1,0,0,0 +access_garment_mrp_requirement_prod,garment.mrp.requirement.prod,model_garment_mrp_requirement,group_garment_production,1,1,1,1 +access_garment_mrp_requirement_admin,garment.mrp.requirement.admin,model_garment_mrp_requirement,group_garment_admin,1,1,1,1 +access_garment_mrp_requirement_line_user,garment.mrp.requirement.line.user,model_garment_mrp_requirement_line,group_garment_user,1,0,0,0 +access_garment_mrp_requirement_line_admin,garment.mrp.requirement.line.admin,model_garment_mrp_requirement_line,group_garment_admin,1,1,1,1 +access_garment_cutting_plan_user,garment.cutting.plan.user,model_garment_cutting_plan,group_garment_user,1,0,0,0 +access_garment_cutting_plan_prod,garment.cutting.plan.prod,model_garment_cutting_plan,group_garment_production,1,1,1,1 +access_garment_cutting_plan_admin,garment.cutting.plan.admin,model_garment_cutting_plan,group_garment_admin,1,1,1,1 +access_garment_cutting_ratio_line_user,garment.cutting.ratio.line.user,model_garment_cutting_ratio_line,group_garment_user,1,0,0,0 +access_garment_cutting_ratio_line_admin,garment.cutting.ratio.line.admin,model_garment_cutting_ratio_line,group_garment_admin,1,1,1,1 +access_garment_cutting_roll_line_user,garment.cutting.roll.line.user,model_garment_cutting_roll_line,group_garment_user,1,0,0,0 +access_garment_cutting_roll_line_admin,garment.cutting.roll.line.admin,model_garment_cutting_roll_line,group_garment_admin,1,1,1,1 +access_garment_bundle_user,garment.bundle.user,model_garment_bundle,group_garment_user,1,0,0,0 +access_garment_bundle_prod,garment.bundle.prod,model_garment_bundle,group_garment_production,1,1,1,1 +access_garment_bundle_admin,garment.bundle.admin,model_garment_bundle,group_garment_admin,1,1,1,1 +access_garment_bundle_history_user,garment.bundle.history.user,model_garment_bundle_history,group_garment_user,1,0,0,0 +access_garment_bundle_history_admin,garment.bundle.history.admin,model_garment_bundle_history,group_garment_admin,1,1,1,1 +access_garment_sewing_line_user,garment.sewing.line.user,model_garment_sewing_line,group_garment_user,1,0,0,0 +access_garment_sewing_line_prod,garment.sewing.line.prod,model_garment_sewing_line,group_garment_production,1,1,1,1 +access_garment_sewing_line_admin,garment.sewing.line.admin,model_garment_sewing_line,group_garment_admin,1,1,1,1 +access_garment_sewing_hourly_user,garment.sewing.hourly.user,model_garment_sewing_hourly,group_garment_user,1,0,0,0 +access_garment_sewing_hourly_prod,garment.sewing.hourly.prod,model_garment_sewing_hourly,group_garment_production,1,1,1,1 +access_garment_sewing_hourly_admin,garment.sewing.hourly.admin,model_garment_sewing_hourly,group_garment_admin,1,1,1,1 +access_garment_jobwork_order_user,garment.jobwork.order.user,model_garment_jobwork_order,group_garment_user,1,0,0,0 +access_garment_jobwork_order_jw,garment.jobwork.order.jw,model_garment_jobwork_order,group_garment_jobwork,1,1,1,1 +access_garment_jobwork_order_admin,garment.jobwork.order.admin,model_garment_jobwork_order,group_garment_admin,1,1,1,1 +access_garment_jobwork_sent_line_user,garment.jobwork.sent.line.user,model_garment_jobwork_sent_line,group_garment_user,1,0,0,0 +access_garment_jobwork_sent_line_admin,garment.jobwork.sent.line.admin,model_garment_jobwork_sent_line,group_garment_admin,1,1,1,1 +access_garment_jobwork_inward_line_user,garment.jobwork.inward.line.user,model_garment_jobwork_inward_line,group_garment_user,1,0,0,0 +access_garment_jobwork_inward_line_admin,garment.jobwork.inward.line.admin,model_garment_jobwork_inward_line,group_garment_admin,1,1,1,1 +access_garment_defect_type_user,garment.defect.type.user,model_garment_defect_type,group_garment_user,1,0,0,0 +access_garment_defect_type_admin,garment.defect.type.admin,model_garment_defect_type,group_garment_admin,1,1,1,1 +access_garment_quality_inspection_user,garment.quality.inspection.user,model_garment_quality_inspection,group_garment_user,1,0,0,0 +access_garment_quality_inspection_qc,garment.quality.inspection.qc,model_garment_quality_inspection,group_garment_quality,1,1,1,1 +access_garment_quality_inspection_admin,garment.quality.inspection.admin,model_garment_quality_inspection,group_garment_admin,1,1,1,1 +access_garment_quality_defect_line_user,garment.quality.defect.line.user,model_garment_quality_defect_line,group_garment_user,1,0,0,0 +access_garment_quality_defect_line_admin,garment.quality.defect.line.admin,model_garment_quality_defect_line,group_garment_admin,1,1,1,1 +access_garment_rework_order_user,garment.rework.order.user,model_garment_rework_order,group_garment_user,1,0,0,0 +access_garment_rework_order_qc,garment.rework.order.qc,model_garment_rework_order,group_garment_quality,1,1,1,1 +access_garment_rework_order_admin,garment.rework.order.admin,model_garment_rework_order,group_garment_admin,1,1,1,1 +access_garment_packing_plan_user,garment.packing.plan.user,model_garment_packing_plan,group_garment_user,1,0,0,0 +access_garment_packing_plan_pck,garment.packing.plan.pck,model_garment_packing_plan,group_garment_packing,1,1,1,1 +access_garment_packing_plan_admin,garment.packing.plan.admin,model_garment_packing_plan,group_garment_admin,1,1,1,1 +access_garment_carton_user,garment.carton.user,model_garment_carton,group_garment_user,1,0,0,0 +access_garment_carton_pck,garment.carton.pck,model_garment_carton,group_garment_packing,1,1,1,1 +access_garment_carton_admin,garment.carton.admin,model_garment_carton,group_garment_admin,1,1,1,1 +access_garment_carton_line_user,garment.carton.line.user,model_garment_carton_line,group_garment_user,1,0,0,0 +access_garment_carton_line_admin,garment.carton.line.admin,model_garment_carton_line,group_garment_admin,1,1,1,1 +access_garment_finished_goods_user,garment.finished.goods.user,model_garment_finished_goods,group_garment_user,1,0,0,0 +access_garment_finished_goods_inv,garment.finished.goods.inv,model_garment_finished_goods,group_garment_inventory,1,1,1,1 +access_garment_finished_goods_admin,garment.finished.goods.admin,model_garment_finished_goods,group_garment_admin,1,1,1,1 +access_garment_dispatch_user,garment.dispatch.user,model_garment_dispatch,group_garment_user,1,0,0,0 +access_garment_dispatch_pck,garment.dispatch.pck,model_garment_dispatch,group_garment_packing,1,1,1,1 +access_garment_dispatch_admin,garment.dispatch.admin,model_garment_dispatch,group_garment_admin,1,1,1,1 +access_garment_costing_user,garment.costing.user,model_garment_costing,group_garment_user,1,0,0,0 +access_garment_costing_merch,garment.costing.merch,model_garment_costing,group_garment_merchandiser,1,1,1,1 +access_garment_costing_admin,garment.costing.admin,model_garment_costing,group_garment_admin,1,1,1,1 +access_garment_dashboard_user,garment.dashboard.user,model_garment_dashboard,group_garment_user,1,0,0,0 +access_garment_dashboard_admin,garment.dashboard.admin,model_garment_dashboard,group_garment_admin,1,1,1,1 diff --git a/addons/garment_erp/static/description/icon.png b/addons/garment_erp/static/description/icon.png new file mode 100644 index 0000000..38aaea5 Binary files /dev/null and b/addons/garment_erp/static/description/icon.png differ diff --git a/addons/garment_erp/tests/__init__.py b/addons/garment_erp/tests/__init__.py new file mode 100644 index 0000000..a1d06dc --- /dev/null +++ b/addons/garment_erp/tests/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import test_e2e_lifecycle diff --git a/addons/garment_erp/tests/test_e2e_lifecycle.py b/addons/garment_erp/tests/test_e2e_lifecycle.py new file mode 100644 index 0000000..4726e8b --- /dev/null +++ b/addons/garment_erp/tests/test_e2e_lifecycle.py @@ -0,0 +1,564 @@ +# -*- 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)) + diff --git a/addons/garment_erp/views/bundle_views.xml b/addons/garment_erp/views/bundle_views.xml new file mode 100644 index 0000000..cbfe54b --- /dev/null +++ b/addons/garment_erp/views/bundle_views.xml @@ -0,0 +1,165 @@ + + + + + + garment.bundle.kanban + garment.bundle + + + + + + + + + + + + + +
+
+ + Pcs +
+
+
+ · Size: +
+
+ +
+
+
+
+
+
+
+ + + + garment.bundle.list + garment.bundle + + + + + + + + + + + + + + + + + + + + + + garment.bundle.form + garment.bundle + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + garment.bundle.search + garment.bundle + + + + + + + + + + + + + + + + + + + + + + + + + + + + Production Bundles + garment.bundle + kanban,list,form + + + + +
diff --git a/addons/garment_erp/views/cutting_plan_views.xml b/addons/garment_erp/views/cutting_plan_views.xml new file mode 100644 index 0000000..aa6a0c4 --- /dev/null +++ b/addons/garment_erp/views/cutting_plan_views.xml @@ -0,0 +1,128 @@ + + + + + + garment.cutting.plan.list + garment.cutting.plan + + + + + + + + + + + + + + + + + + + + garment.cutting.plan.form + garment.cutting.plan + +
+
+
+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + Cutting Orders + garment.cutting.plan + list,form + + + + +
diff --git a/addons/garment_erp/views/dispatch_views.xml b/addons/garment_erp/views/dispatch_views.xml new file mode 100644 index 0000000..089fadd --- /dev/null +++ b/addons/garment_erp/views/dispatch_views.xml @@ -0,0 +1,106 @@ + + + + + + garment.dispatch.list + garment.dispatch + + + + + + + + + + + + + + + + + + + garment.dispatch.form + garment.dispatch + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + Dispatch & Delivery Orders + garment.dispatch + list,form + + + + +
diff --git a/addons/garment_erp/views/fabric_inspection_views.xml b/addons/garment_erp/views/fabric_inspection_views.xml new file mode 100644 index 0000000..edc765e --- /dev/null +++ b/addons/garment_erp/views/fabric_inspection_views.xml @@ -0,0 +1,106 @@ + + + + + + garment.fabric.inspection.list + garment.fabric.inspection + + + + + + + + + + + + + + + + + + + + garment.fabric.inspection.form + garment.fabric.inspection + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + Fabric 4-Point Inspections + garment.fabric.inspection + list,form + + + + +
diff --git a/addons/garment_erp/views/fabric_roll_views.xml b/addons/garment_erp/views/fabric_roll_views.xml new file mode 100644 index 0000000..bf59009 --- /dev/null +++ b/addons/garment_erp/views/fabric_roll_views.xml @@ -0,0 +1,150 @@ + + + + + + garment.fabric.roll.list + garment.fabric.roll + + + + + + + + + + + + + + + + + + + + + garment.fabric.roll.form + garment.fabric.roll + +
+
+
+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + garment.fabric.roll.search + garment.fabric.roll + + + + + + + + + + + + + + + + + + + + + + + + Fabric Rolls Inventory + garment.fabric.roll + list,form + {'search_default_in_stock': 1} + + + + +
diff --git a/addons/garment_erp/views/finished_goods_views.xml b/addons/garment_erp/views/finished_goods_views.xml new file mode 100644 index 0000000..c062432 --- /dev/null +++ b/addons/garment_erp/views/finished_goods_views.xml @@ -0,0 +1,84 @@ + + + + + + garment.finished.goods.list + garment.finished.goods + + + + + + + + + + + + + + + + + + garment.finished.goods.form + garment.finished.goods + +
+ +
+
+ + + + + + + + + + + + + + + +
+
+
+
+ + + + garment.finished.goods.search + garment.finished.goods + + + + + + + + + + + + + + + + + + + Finished Goods Inventory + garment.finished.goods + list,form + {'search_default_in_stock': 1} + + + + +
diff --git a/addons/garment_erp/views/garment_base_views.xml b/addons/garment_erp/views/garment_base_views.xml new file mode 100644 index 0000000..e66f8c2 --- /dev/null +++ b/addons/garment_erp/views/garment_base_views.xml @@ -0,0 +1,228 @@ + + + + + + garment.color.list + garment.color + + + + + + + + + + + + + + garment.color.form + garment.color + +
+ + + + + + + + + + + + + + +
+
+
+ + + Colors Master + garment.color + list,form + + + + + garment.size.list + garment.size + + + + + + + + + + + + + garment.size.form + garment.size + +
+ + + + + + + + + + + + + +
+
+
+ + + Sizes Master + garment.size + list,form + + + + + garment.fabric.type.list + garment.fabric.type + + + + + + + + + + + + + garment.fabric.type.form + garment.fabric.type + +
+ + + + + + + + + + + + + + + + + + +
+
+
+ + + Fabric Knit Structures + garment.fabric.type + list,form + + + + + garment.composition.list + garment.composition + + + + + + + + + + + + + Fabric Compositions + garment.composition + list,form + + + + + garment.brand.list + garment.brand + + + + + + + + + + + Brands Master + garment.brand + list,form + + + + + garment.season.list + garment.season + + + + + + + + + + + + Seasons Master + garment.season + list,form + + + + + garment.type.list + garment.type + + + + + + + + + + + Garment Categories + garment.type + list,form + + + + + + + + + + + +
diff --git a/addons/garment_erp/views/garment_bom_views.xml b/addons/garment_erp/views/garment_bom_views.xml new file mode 100644 index 0000000..e3248c9 --- /dev/null +++ b/addons/garment_erp/views/garment_bom_views.xml @@ -0,0 +1,114 @@ + + + + + + garment.bom.list + garment.bom + + + + + + + + + + + + + + + + + garment.bom.form + garment.bom + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + Garment BOMs + garment.bom + list,form + + + + +
diff --git a/addons/garment_erp/views/garment_costing_views.xml b/addons/garment_erp/views/garment_costing_views.xml new file mode 100644 index 0000000..c77ee2b --- /dev/null +++ b/addons/garment_erp/views/garment_costing_views.xml @@ -0,0 +1,132 @@ + + + + + + garment.costing.list + garment.costing + + + + + + + + + + + + + + + + + + + garment.costing.form + garment.costing + +
+
+
+ +
+
+ + + + + + + + + + + + + + +
+
+ Total Cost / Pc +
+ ( / Dozen) +
+
+ Selling Price / Pc +
+
+
+ Gross Margin % +
%
+ ( / Pc) +
+
+ Total Expected Profit +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + Garment Costing Sheets + garment.costing + list,form + + + + +
diff --git a/addons/garment_erp/views/garment_dashboard_views.xml b/addons/garment_erp/views/garment_dashboard_views.xml new file mode 100644 index 0000000..2ea777c --- /dev/null +++ b/addons/garment_erp/views/garment_dashboard_views.xml @@ -0,0 +1,163 @@ + + + + + + garment.dashboard.form + garment.dashboard + +
+ +
+

Tiruppur Garment Manufacturing - Live Control Center

+
+ + +
+
+
+
Active Orders
+
Orders
+
Total Pieces
+
+
+
+
+
Production in Progress
+
Plans
+
Avg Progress: %
+
+
+
+
+
Today's Sewing Output
+
Pcs
+
Target: Pcs
+
+
+
+
+
Sewing Floor Efficiency
+
%
+
Alterations Logged
+
+
+
+ + +
+
+
+
Fabric Roll Stock
+
Kg
+
QC Approved Rolls
+
+
+
+
+
Finished Goods Stock
+
Pcs
+
Ready in Warehouse
+
+
+
+
+
Subcontracting (Job Work)
+
DCs
+
Outward in Processing
+
+
+
+
+
Ready for Dispatch
+
Cartons
+
Pending Rework
+
+
+
+ + + + + + + +
+ +
+
+ + + + Factory Control Room Dashboard + garment.dashboard + form + current + + + + + +
diff --git a/addons/garment_erp/views/garment_menus.xml b/addons/garment_erp/views/garment_menus.xml new file mode 100644 index 0000000..228be9f --- /dev/null +++ b/addons/garment_erp/views/garment_menus.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/garment_erp/views/garment_style_views.xml b/addons/garment_erp/views/garment_style_views.xml new file mode 100644 index 0000000..972319a --- /dev/null +++ b/addons/garment_erp/views/garment_style_views.xml @@ -0,0 +1,178 @@ + + + + + + garment.style.kanban + garment.style + + + + + + + + + + + +
+
+ +
+
+
+
+
· GSM
+
+ Variants +
+
+
+
+
+
+
+
+ + + + garment.style.list + garment.style + + + + + + + + + + + + + + + + + + + garment.style.form + garment.style + +
+
+
+ +
+ + + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + Garment Styles + garment.style + kanban,list,form + +

Create your first Garment Style!

+

Define style code, fabric specifications (GSM, width, shrinkage), colors, and size measurements.

+
+
+ + + +
diff --git a/addons/garment_erp/views/garment_traceability_views.xml b/addons/garment_erp/views/garment_traceability_views.xml new file mode 100644 index 0000000..eb6d936 --- /dev/null +++ b/addons/garment_erp/views/garment_traceability_views.xml @@ -0,0 +1,21 @@ + + + + + + Full Lifecycle Traceability Explorer + garment.bundle + list,form + +

Scan or Search any Barcode / QR Code

+

Search any Bundle, Fabric Roll, or Carton barcode to instantly trace from Buyer Sales Order down to Sewing Line and Operators.

+
+
+ + + +
diff --git a/addons/garment_erp/views/job_work_views.xml b/addons/garment_erp/views/job_work_views.xml new file mode 100644 index 0000000..e6db092 --- /dev/null +++ b/addons/garment_erp/views/job_work_views.xml @@ -0,0 +1,138 @@ + + + + + + garment.jobwork.order.list + garment.jobwork.order + + + + + + + + + + + + + + + + + + + + + garment.jobwork.order.form + garment.jobwork.order + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ Total Sent Outward +
+
+
+ Total Received Inward +
+
+
+ Accepted Good Qty +
+
+
+ Pending Balance +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + Job Work Orders (Subcontracting) + garment.jobwork.order + list,form + + + + +
diff --git a/addons/garment_erp/views/mrp_requirement_views.xml b/addons/garment_erp/views/mrp_requirement_views.xml new file mode 100644 index 0000000..ba2db3f --- /dev/null +++ b/addons/garment_erp/views/mrp_requirement_views.xml @@ -0,0 +1,91 @@ + + + + + + garment.mrp.requirement.list + garment.mrp.requirement + + + + + + + + + + + + + + + + garment.mrp.requirement.form + garment.mrp.requirement + +
+
+
+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + Material Requirements (MRP) + garment.mrp.requirement + list,form + + + + +
diff --git a/addons/garment_erp/views/packing_views.xml b/addons/garment_erp/views/packing_views.xml new file mode 100644 index 0000000..1a25d30 --- /dev/null +++ b/addons/garment_erp/views/packing_views.xml @@ -0,0 +1,158 @@ + + + + + + garment.carton.list + garment.carton + + + + + + + + + + + + + + + + + garment.carton.form + garment.carton + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + Master Cartons + garment.carton + list,form + + + + + garment.packing.plan.list + garment.packing.plan + + + + + + + + + + + + + + + + + + + garment.packing.plan.form + garment.packing.plan + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + Packing Plans + garment.packing.plan + list,form + + + + + +
diff --git a/addons/garment_erp/views/production_plan_views.xml b/addons/garment_erp/views/production_plan_views.xml new file mode 100644 index 0000000..6a06d80 --- /dev/null +++ b/addons/garment_erp/views/production_plan_views.xml @@ -0,0 +1,196 @@ + + + + + + garment.production.plan.kanban + garment.production.plan + + + + + + + + + + + + + +
+
+ + +
+
+
+ Planned: Pcs · Cut: Pcs +
+ +
+
+
+
+ Packed: + % +
+
+
+
+
+
+
+ + + + garment.production.plan.list + garment.production.plan + + + + + + + + + + + + + + + + + + + + + garment.production.plan.form + garment.production.plan + +
+
+
+ +
+ + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + Production Plans + garment.production.plan + kanban,list,form + + + + +
diff --git a/addons/garment_erp/views/quality_control_views.xml b/addons/garment_erp/views/quality_control_views.xml new file mode 100644 index 0000000..f8c9163 --- /dev/null +++ b/addons/garment_erp/views/quality_control_views.xml @@ -0,0 +1,127 @@ + + + + + + garment.defect.type.list + garment.defect.type + + + + + + + + + + + + + Quality Defect Master + garment.defect.type + list,form + + + + + garment.quality.inspection.list + garment.quality.inspection + + + + + + + + + + + + + + + + + + + + + garment.quality.inspection.form + garment.quality.inspection + +
+
+
+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + Quality Inspections + garment.quality.inspection + list,form + + + + + +
diff --git a/addons/garment_erp/views/rework_views.xml b/addons/garment_erp/views/rework_views.xml new file mode 100644 index 0000000..8dd89d1 --- /dev/null +++ b/addons/garment_erp/views/rework_views.xml @@ -0,0 +1,99 @@ + + + + + + garment.rework.order.list + garment.rework.order + + + + + + + + + + + + + + + + + + + + garment.rework.order.form + garment.rework.order + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + Rework Orders + garment.rework.order + list,form + + + + +
diff --git a/addons/garment_erp/views/sale_order_views.xml b/addons/garment_erp/views/sale_order_views.xml new file mode 100644 index 0000000..b90125a --- /dev/null +++ b/addons/garment_erp/views/sale_order_views.xml @@ -0,0 +1,75 @@ + + + + + + garment.sale.order.form + sale.order + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + + + + + + + + + + +
+
+ +
+
+ + + + Garment Sales Orders + sale.order + list,form + [('is_garment_order', '=', True)] + {'default_is_garment_order': True} + + + + +
diff --git a/addons/garment_erp/views/sewing_line_views.xml b/addons/garment_erp/views/sewing_line_views.xml new file mode 100644 index 0000000..76437ea --- /dev/null +++ b/addons/garment_erp/views/sewing_line_views.xml @@ -0,0 +1,204 @@ + + + + + + garment.sewing.line.list + garment.sewing.line + + + + + + + + + + + + + + + garment.sewing.line.form + garment.sewing.line + +
+ +
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+ + + Sewing Lines + garment.sewing.line + list,form + + + + + garment.sewing.hourly.list + garment.sewing.hourly + + + + + + + + + + + + + + + + + + garment.sewing.hourly.form + garment.sewing.hourly + +
+ +
+
+ + + + + + + + + + + + + + + + + +
+
+ Day Target +
Pcs
+
+
+ Actual Produced +
Pcs
+
+
+ Alterations / Defects +
Pcs
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MetricH1 (08:30-09:30)H2 (09:30-10:30)H3 (10:30-11:30)H4 (11:30-12:30)H5 (13:15-14:15)H6 (14:15-15:15)H7 (15:15-16:15)H8 (16:15-17:15)OT (Overtime)
Target Pcs
Actual Output
Defects / Alter
+
+
+
+ + +
+
+ + + + Hourly Production Tracking + garment.sewing.hourly + list,form + + + + + +
diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..cc6e24f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +name: chennora_odoo + +services: + db: + image: postgres:15 + environment: + POSTGRES_DB: postgres + POSTGRES_USER: odoo + POSTGRES_PASSWORD: odoo + volumes: + - client51_pgdata:/var/lib/postgresql/data + restart: always + + odoo: + image: odoo:19.0 + depends_on: + - db + ports: + - "10051:8069" + environment: + HOST: db + USER: odoo + PASSWORD: odoo + LIST_DB: "True" + volumes: + - client51_odoo_data:/var/lib/odoo + - ./addons:/mnt/extra-addons + restart: always + +volumes: + client51_pgdata: + client51_odoo_data: \ No newline at end of file