Skip to content

Product Transform Template Documentation

Overview

The Product Transform Template module provides standard recipes or bills of materials for stock transformation operations. Templates define repeatable input-output relationships for manufacturing processes, ensuring consistency and reducing manual data entry. This module works in conjunction with stock.transform to streamline production workflows.


Models Covered

This documentation covers two related models:

  1. product.transform.template - Template header with metadata
  2. product.transform.template.lines - Template line items (inputs/outputs)

Product Transform Template

Model Information

Model Name: product.transform.template
Display Name: Product Transform Template
Key Fields: ["name"]
Export Name Field: name

Features

  • ✅ Unique key constraint on name
  • ✅ Layer type categorization
  • ✅ State workflow
  • ✅ Reusable transform recipes
  • ❌ Audit logging

Understanding Key Fields

What are Key Fields?

For the product.transform.template model, the key field is:

_key = ["name"]

This means the name must be unique. You cannot create two templates with the same name.

Uniqueness Guarantee:

# Valid - unique names:
Template 1: "Widget Assembly - Type A"  
Template 2: "Widget Assembly - Type B"  

# This would fail - duplicate key:
Template 3: "Widget Assembly - Type A"   ERROR: Key already exists!

Database Implementation

The key field is enforced at the database level using a unique constraint.


State Workflow

draft → approve
State Description
draft Template being created/edited
approve Template approved for use in production

Key Fields Reference

Header Fields

Field Type Required Description
name Char Template name (unique key)
seq Char Sequence/order number
fg_product Many2One Finished goods product (product)
target_output_qty Char Target output quantity
state Selection Template state (draft/approve)
layer_type Selection Layer type (BL/ML/UL/LAM/PRT)

Layer Types

Type Code Description
Back Layer BL Back layer production
Middle Layer ML Middle layer production
Upper Layer UL Upper layer production
Laminating LAM Laminating process
Printing PRT Printing process

Relationship Fields

Field Type Description
lines_in One2Many Input product lines (type="in" or "balance")
lines_out One2Many Output product lines (type="out")

API Methods

1. Create Template

Method: create(vals, context)

Creates a new transform template.

Parameters:

vals = {
    "name": "Widget Assembly Standard",   # Required: Unique name
    "fg_product": 123,                    # Required: Final product
    "target_output_qty": "100",           # Optional: Target
    "layer_type": "BL",                   # Optional: Layer type
    "state": "draft",                     # Default: draft
    "lines_in": [                         # Input products
        ("create", {
            "type": "in",
            "product_id": 456,
            "qty": 2,
            "uom_id": 1,
            "location_id": 789
        })
    ],
    "lines_out": [                        # Output products
        ("create", {
            "type": "out",
            "product_id": 123,
            "qty": 1,
            "uom_id": 1,
            "location_id": 789
        })
    ]
}

Returns: int - New template ID

Example:

# Create standard assembly template
template_id = get_model("product.transform.template").create({
    "name": "Laptop Assembly - Model X",
    "fg_product": laptop_id,
    "target_output_qty": "50",
    "layer_type": "BL",
    "lines_in": [
        ("create", {
            "type": "in",
            "product_id": motherboard_id,
            "qty": 1,
            "uom_id": unit_id,
            "location_id": warehouse_id
        }),
        ("create", {
            "type": "in",
            "product_id": screen_id,
            "qty": 1,
            "uom_id": unit_id,
            "location_id": warehouse_id
        })
    ],
    "lines_out": [
        ("create", {
            "type": "out",
            "product_id": laptop_id,
            "qty": 1,
            "uom_id": unit_id,
            "location_id": warehouse_id
        })
    ]
})


2. Approve Template

Method: write(ids, vals, context)

Approves template for production use.

Example:

template = get_model("product.transform.template").browse(template_id)
template.write({"state": "approve"})


3. Search by Product

Method: search(condition)

Find templates by finished goods product.

Example:

# Find templates for specific product
templates = get_model("product.transform.template").search_browse([
    ["fg_product", "=", product_id]
])

for template in templates:
    print(f"Template: {template.name}")
    print(f"  Inputs: {len(template.lines_in)}")
    print(f"  Outputs: {len(template.lines_out)}")


Model Relationship Description
product.transform.template.lines One2Many Template line items
product Many2One Finished goods product
stock.transform Related Transforms using this template

Product Transform Template Lines

Model Information

Model Name: product.transform.template.lines
Display Name: Product Transform Template Lines
Key Fields: N/A

Features

  • ✅ Multiple line types (in/out/service/balance)
  • ✅ Dimensional specifications
  • ✅ Cost tracking
  • ❌ Audit logging

Line Types

Type Code Description
From in Input materials/products
To out Output/finished products
Service service Services (e.g., subcontracting)
Lot Balance balance Remaining balance from lot

Key Fields Reference

Basic Fields

Field Type Required Description
pt_template_id Many2One Parent template (product.transform.template)
type Selection Line type (in/out/service/balance)
product_id Many2One Product (product, stock type)
qty Decimal Quantity
uom_id Many2One Unit of measure (uom)
location_id Many2One Stock location (stock.location, internal)

Cost Fields

Field Type Description
cost_price Decimal Cost per unit (scale: 6)
cost_amount Decimal Total cost (computed: qty * cost_price)
cost_group Selection Cost grouping (balance/fixed_price/1-9)

Quantity & Measurement Fields

Field Type Description
uom2_id Many2One Secondary UoM
qty2 Decimal Secondary quantity
planned_qty Decimal Planned quantity
thickness Decimal Thickness in mm (scale: 2)
width Decimal Width in mm
area_m2 Decimal Area in square meters
gross_wt Decimal Gross weight
net_wt Decimal Net weight in kg
weight Decimal Gross weight (computed)

Tracking Fields

Field Type Description
lot_id Many2One Lot/Serial number (stock.lot)
picking_id Many2One Associated picking (stock.picking)
supplier_id Many2One Supplier (for service lines) (contact)
move_id Many2One Stock movement (stock.move)
confirmed Boolean Confirmation flag
notes Text Line notes

Material Specification Fields

Field Type Description
master_prod_code Char Master product code
prod_code Char Product code
material_categ Many2One Material category (material.categ)
defect_categ Many2One Defect category (defect.categ)
color_code Char Color code

Stock Information

Field Type Description
qty_stock Decimal Available stock (computed)
timestamp DateTime Timestamp

API Methods

1. Create Template Line

Method: create(vals, context)

Creates a new template line.

Parameters:

vals = {
    "pt_template_id": 123,                # Required: Parent template
    "type": "in",                         # Required: Line type
    "product_id": 456,                    # Required: Product ID
    "qty": 2,                             # Required: Quantity
    "uom_id": 1,                          # Required: UoM
    "location_id": 789,                   # Required: Location
    "planned_qty": 2,                     # Optional: Planned qty
    "thickness": 2.5,                     # Optional: Thickness (mm)
    "width": 1200,                        # Optional: Width (mm)
    "cost_price": 10.50                   # Optional: Cost
}

Returns: int - New line ID

Example:

# Add input line to template
line_id = get_model("product.transform.template.lines").create({
    "pt_template_id": template_id,
    "type": "in",
    "product_id": raw_material_id,
    "qty": 100,
    "uom_id": kg_uom_id,
    "location_id": warehouse_loc_id
})


Computed Fields Functions

get_cost_price(ids, context)

Returns cost price from associated stock move.

get_amount_price(ids, context)

Calculates total cost amount (cost_price * qty).

get_gross_weight(ids, context)

Returns weight from lot or product.

get_qty_stock(ids, context)

Returns available stock for product/lot/location.


Model Relationship Description
product.transform.template Many2One Parent template
product Many2One Product referenced
stock.lot Many2One Lot/serial tracking
stock.location Many2One Storage location
uom Many2One Unit of measure
stock.move Many2One Associated stock movement
contact Many2One Supplier (for services)

Common Use Cases

Use Case 1: Create Standard Recipe Template

# Define standard recipe for repeated use
template_id = get_model("product.transform.template").create({
    "name": "Pizza Dough - Standard Recipe",
    "fg_product": pizza_dough_id,
    "target_output_qty": "10 kg",
    "state": "draft",
    "lines_in": [
        ("create", {
            "type": "in",
            "product_id": flour_id,
            "qty": 6,
            "uom_id": kg_id,
            "location_id": warehouse_id
        }),
        ("create", {
            "type": "in",
            "product_id": water_id,
            "qty": 3.6,
            "uom_id": liter_id,
            "location_id": warehouse_id
        }),
        ("create", {
            "type": "in",
            "product_id": yeast_id,
            "qty": 0.2,
            "uom_id": kg_id,
            "location_id": warehouse_id
        }),
        ("create", {
            "type": "in",
            "product_id": salt_id,
            "qty": 0.2,
            "uom_id": kg_id,
            "location_id": warehouse_id
        })
    ],
    "lines_out": [
        ("create", {
            "type": "out",
            "product_id": pizza_dough_id,
            "qty": 10,
            "uom_id": kg_id,
            "location_id": warehouse_id
        })
    ]
})

# Approve template for use
template = get_model("product.transform.template").browse(template_id)
template.write({"state": "approve"})

Use Case 2: Use Template in Transform

# 1. Find template by product
templates = get_model("product.transform.template").search_browse([
    ["fg_product", "=", laptop_id],
    ["state", "=", "approve"]
])

if templates:
    template_id = templates[0].id

    # 2. Create transform using template
    transform_id = get_model("stock.transform").create({
        "date": "2025-10-27",
        "final_product_code": laptop_id,
        "pt_template": template_id
        # Lines automatically loaded via onchange
    })

    # 3. Adjust quantities if needed (scale up 2x)
    transform = get_model("stock.transform").browse(transform_id)
    for line in transform.lines_in:
        line.write({"qty": line.qty * 2})
    for line in transform.lines_out:
        line.write({"qty": line.qty * 2})

    # 4. Validate
    transform.validate()

Use Case 3: Create Template with Service Lines

# Template with outsourced services
template_id = get_model("product.transform.template").create({
    "name": "Fabric Dyeing - Standard Process",
    "fg_product": dyed_fabric_id,
    "target_output_qty": "100 meters",
    "lines_in": [
        ("create", {
            "type": "in",
            "product_id": raw_fabric_id,
            "qty": 100,
            "uom_id": meter_id,
            "location_id": warehouse_id
        })
    ],
    "lines_service": [
        ("create", {
            "type": "service",
            "product_id": dyeing_service_id,
            "qty": 100,
            "uom_id": meter_id,
            "supplier_id": dyeing_supplier_id,
            "cost_price": 5.0  # Cost per meter
        })
    ],
    "lines_out": [
        ("create", {
            "type": "out",
            "product_id": dyed_fabric_id,
            "qty": 98,  # 2% loss
            "uom_id": meter_id,
            "location_id": warehouse_id
        })
    ]
})

Use Case 4: Create Template with Dimensional Specs

# Template with material dimensions
template_id = get_model("product.transform.template").create({
    "name": "Sheet Metal Cutting - 1200x2400",
    "fg_product": cut_sheet_id,
    "lines_in": [
        ("create", {
            "type": "in",
            "product_id": sheet_metal_id,
            "qty": 1,
            "uom_id": sheet_id,
            "location_id": warehouse_id,
            "width": 1200,      # 1200mm
            "thickness": 2.0,   # 2.0mm
            "area_m2": 2.88     # 2.88 m²
        })
    ],
    "lines_out": [
        ("create", {
            "type": "out",
            "product_id": cut_sheet_id,
            "qty": 4,
            "uom_id": piece_id,
            "location_id": warehouse_id,
            "width": 600,       # 600mm each
            "thickness": 2.0,
            "area_m2": 0.72     # 0.72 m² each
        })
    ]
})

Use Case 5: Clone and Modify Template

# Clone existing template and modify
original_template = get_model("product.transform.template").browse(template_id)

# Create new template with similar structure
new_template_id = get_model("product.transform.template").create({
    "name": original_template.name + " - Modified",
    "fg_product": original_template.fg_product.id,
    "target_output_qty": original_template.target_output_qty,
    "layer_type": original_template.layer_type,
    "state": "draft"
})

# Copy and modify lines
for line in original_template.lines_in:
    get_model("product.transform.template.lines").create({
        "pt_template_id": new_template_id,
        "type": line.type,
        "product_id": line.product_id.id,
        "qty": line.qty * 1.1,  # 10% increase
        "uom_id": line.uom_id.id,
        "location_id": line.location_id.id
    })

for line in original_template.lines_out:
    get_model("product.transform.template.lines").create({
        "pt_template_id": new_template_id,
        "type": line.type,
        "product_id": line.product_id.id,
        "qty": line.qty * 1.1,  # 10% increase
        "uom_id": line.uom_id.id,
        "location_id": line.location_id.id
    })

Best Practices

1. Use Descriptive Template Names

# Good: Clear, descriptive names
"Laptop Assembly - Model X - Standard"  
"Pizza Dough - Commercial Recipe - 10kg Batch"  
"T-Shirt Printing - White - 50 units"  

# Bad: Vague names
"Template 1"  
"Assembly"  
"Recipe"  

2. Approve Templates After Testing

# Good workflow:
# 1. Create template in draft
template_id = get_model("product.transform.template").create({
    "name": "New Assembly Process",
    "state": "draft",  # ✅ Start as draft
    "fg_product": product_id,
    "lines_in": [...],
    "lines_out": [...]
})

# 2. Test with actual transform
test_transform_id = get_model("stock.transform").create({
    "pt_template": template_id,
    "date": "2025-10-27"
})
# Test, verify, adjust template if needed

# 3. Approve when ready
template = get_model("product.transform.template").browse(template_id)
template.write({"state": "approve"})  # ✅ Now ready for production

3. Organize by Layer Type

# Good: Use layer types for organization
templates = {
    "BL": get_model("product.transform.template").search_browse([
        ["layer_type", "=", "BL"],
        ["state", "=", "approve"]
    ]),
    "LAM": get_model("product.transform.template").search_browse([
        ["layer_type", "=", "LAM"],
        ["state", "=", "approve"]
    ])
}  # ✅ Easy to find relevant templates

4. Include Planned Quantities

# Good: Set planned quantities for visibility
line_vals = {
    "type": "in",
    "product_id": product_id,
    "qty": 100,           # Standard quantity
    "planned_qty": 100,   # ✅ Expected/target quantity
    "uom_id": uom_id,
    "location_id": location_id
}

5. Version Your Templates

# Good: Include version in name
"Widget Assembly - v1.0"
"Widget Assembly - v1.1 - Reduced waste"
"Widget Assembly - v2.0 - New process"  # ✅ Clear evolution

# Alternative: Use seq field
template.write({"seq": "v2.0"})

Search Functions

Search by Name

# Find template by name
templates = get_model("product.transform.template").search_browse([
    ["name", "=", "Laptop Assembly - Model X"]
])

Search by Product

# Find all templates for a product
templates = get_model("product.transform.template").search_browse([
    ["fg_product", "=", product_id]
])

Search by Layer Type

# Find all laminating templates
templates = get_model("product.transform.template").search_browse([
    ["layer_type", "=", "LAM"],
    ["state", "=", "approve"]
])

Search Approved Templates

# Find production-ready templates
templates = get_model("product.transform.template").search_browse([
    ["state", "=", "approve"]
])

Performance Tips

1. Cache Templates

# Efficient: Load and cache templates
template_cache = {}

def get_template_for_product(product_id):
    if product_id not in template_cache:
        templates = get_model("product.transform.template").search_browse([
            ["fg_product", "=", product_id],
            ["state", "=", "approve"]
        ])
        template_cache[product_id] = templates[0] if templates else None

    return template_cache[product_id]

# Use cached templates
template = get_template_for_product(laptop_id)  # ✅ Single query

2. Load Lines with Template

# Efficient: Access lines through parent
template = get_model("product.transform.template").browse(template_id)
for line in template.lines_in:  # ✅ Single query
    print(line.product_id.name)

# Less efficient: Query lines separately
line_ids = get_model("product.transform.template.lines").search([
    ["pt_template_id", "=", template_id],
    ["type", "=", "in"]
])
for line_id in line_ids:
    line = get_model("product.transform.template.lines").browse(line_id)  # ❌ Multiple queries

Troubleshooting

"Key already exists" error

Cause: Template name already exists (duplicate)
Solution: Use unique template names; consider versioning scheme

Template not appearing in selection

Cause: State is "draft" or fg_product doesn't match
Solution: Approve template and verify fg_product field

Lines not copying to transform

Cause: onchange not triggered or template_id not set
Solution: Ensure pt_template field is set when creating transform

Cost calculations incorrect

Cause: Missing cost_price on template lines
Solution: Set cost_price on template lines or let system calculate


Testing Examples

Unit Test: Create Template

def test_create_template():
    # Create template
    template_id = get_model("product.transform.template").create({
        "name": "Test Assembly",
        "fg_product": finished_prod_id,
        "lines_in": [
            ("create", {
                "type": "in",
                "product_id": part_a_id,
                "qty": 2,
                "uom_id": uom_id,
                "location_id": loc_id
            })
        ],
        "lines_out": [
            ("create", {
                "type": "out",
                "product_id": finished_prod_id,
                "qty": 1,
                "uom_id": uom_id,
                "location_id": loc_id
            })
        ]
    })

    # Verify
    template = get_model("product.transform.template").browse(template_id)
    assert template.name == "Test Assembly"
    assert len(template.lines_in) == 1
    assert len(template.lines_out) == 1
    assert template.state == "draft"

Unit Test: Template Usage in Transform

def test_template_usage():
    # Create template
    template_id = get_model("product.transform.template").create({
        "name": "Test Assembly",
        "fg_product": finished_prod_id,
        "lines_in": [
            ("create", {
                "type": "in",
                "product_id": part_a_id,
                "qty": 2,
                "uom_id": uom_id,
                "location_id": loc_id
            })
        ],
        "lines_out": [
            ("create", {
                "type": "out",
                "product_id": finished_prod_id,
                "qty": 1,
                "uom_id": uom_id,
                "location_id": loc_id
            })
        ]
    })

    # Create transform from template
    transform_id = get_model("stock.transform").create({
        "date": "2025-10-27",
        "pt_template": template_id
    })

    # Verify lines loaded from template
    transform = get_model("stock.transform").browse(transform_id)
    assert len(transform.lines_in) == 1
    assert len(transform.lines_out) == 1
    assert transform.lines_in[0].product_id.id == part_a_id
    assert transform.lines_in[0].qty == 2

Unit Test: Unique Name Constraint

def test_unique_name_constraint():
    # Create first template
    template1_id = get_model("product.transform.template").create({
        "name": "Unique Template Name",
        "fg_product": prod_id
    })

    # Try to create duplicate
    try:
        template2_id = get_model("product.transform.template").create({
            "name": "Unique Template Name",  # Same name
            "fg_product": prod_id
        })
        assert False, "Should have raised exception"
    except Exception as e:
        assert "Key already exists" in str(e) or "unique" in str(e).lower()

Version History

Last Updated: 2025-10-27
Model Version: product_transform_template.py, product_transform_template_lines.py
Framework: Netforce


Additional Resources

  • Stock Transform Documentation: stock.transform
  • Stock Transform Line Documentation: stock.transform.line
  • Product Documentation: product
  • Stock Location Documentation: stock.location

This documentation is generated for developer onboarding and reference purposes.