Product Borrow Line Documentation¶
Overview¶
The Product Borrow Line module (product.borrow.line) represents individual items within a borrow request. Each line tracks a specific product, requested quantity, and automatically calculates issued and returned quantities based on stock movements.
Model Information¶
Model Name: product.borrow.line
Display Name: Borrow Line
Name Field: number (inherited)
Key Fields: N/A
Features¶
- ✅ Automatic quantity tracking (issued/returned)
- ✅ Lot/Serial number support
- ❌ Audit logging
- ❌ Cascade deletion with parent
Key Fields Reference¶
Line Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
request_id |
Many2One | ✅ | Parent borrow request (product.borrow) |
product_id |
Many2One | ✅ | Product being borrowed (product) |
qty |
Decimal | ✅ | Requested quantity |
uom_id |
Many2One | ✅ | Unit of measure (uom) |
lot_id |
Many2One | ❌ | Lot/Serial number (stock.lot) |
Computed Fields¶
| Field | Description |
|---|---|
issued_qty |
Total quantity issued to employee (from outbound moves) |
returned_qty |
Total quantity returned from employee (from inbound moves) |
API Methods¶
1. Create Borrow Line¶
Method: create(vals, context)
Creates a new borrow line item.
Parameters:
vals = {
"request_id": 123, # Required: Parent borrow request
"product_id": 456, # Required: Product ID
"qty": 2, # Required: Quantity
"uom_id": 1, # Required: UoM ID
"lot_id": 789 # Optional: Lot/serial number
}
Returns: int - New line ID
Example:
# Add line to existing borrow request
line_id = get_model("product.borrow.line").create({
"request_id": borrow_id,
"product_id": product_id,
"qty": 1,
"uom_id": unit_id,
"lot_id": serial_id
})
2. Update Line¶
Method: write(ids, vals, context)
Updates borrow line data.
Example:
line = get_model("product.borrow.line").browse(line_id)
# Update quantity
line.write({"qty": 3})
# Update lot
line.write({"lot_id": new_lot_id})
3. Delete Line¶
Method: delete(ids, context)
Deletes a borrow line.
Example:
Computed Fields Functions¶
get_qty_issued(ids, context)¶
Calculates total issued quantity from stock movements.
Behavior:
- Sums quantities from outbound stock moves (type="out")
- Only counts moves in "done" state
- Links via related_id to parent borrow request
Example:
line = get_model("product.borrow.line").browse(line_id)
print(f"Requested: {line.qty}")
print(f"Issued: {line.issued_qty}")
print(f"Pending: {line.qty - line.issued_qty}")
get_qty_returned(ids, context)¶
Calculates total returned quantity from stock movements.
Behavior:
- Sums quantities from inbound stock moves (type="in")
- Only counts moves in "done" state
- Links via related_id to parent borrow request
Example:
line = get_model("product.borrow.line").browse(line_id)
print(f"Issued: {line.issued_qty}")
print(f"Returned: {line.returned_qty}")
print(f"Outstanding: {line.issued_qty - line.returned_qty}")
Related Models¶
| Model | Relationship | Description |
|---|---|---|
product.borrow |
Many2One | Parent borrow request |
product |
Many2One | Product being borrowed |
uom |
Many2One | Unit of measure |
stock.lot |
Many2One | Lot/serial tracking |
stock.move |
Related | Stock movements (via parent) |
Common Use Cases¶
Use Case 1: Add Item to Borrow Request¶
# Add product to existing borrow request
line_id = get_model("product.borrow.line").create({
"request_id": borrow_id,
"product_id": laptop_id,
"qty": 1,
"uom_id": unit_id,
"lot_id": serial_number_id
})
print(f"Line created: {line_id}")
Use Case 2: Track Issue and Return Status¶
# Check status of borrowed item
line = get_model("product.borrow.line").browse(line_id)
print(f"Product: {line.product_id.name}")
print(f"Requested: {line.qty}")
print(f"Issued: {line.issued_qty}")
print(f"Returned: {line.returned_qty}")
outstanding = line.issued_qty - line.returned_qty
if outstanding > 0:
print(f"⚠️ Outstanding: {outstanding} units")
else:
print("✅ Fully returned")
Use Case 3: Find Outstanding Items by Employee¶
# Find all items not returned by employee
employee_id = 123
borrows = get_model("product.borrow").search_browse([
["employee_id", "=", employee_id],
["state", "=", "approved"]
])
for borrow in borrows:
for line in borrow.lines:
outstanding = line.issued_qty - line.returned_qty
if outstanding > 0:
print(f"Borrow: {borrow.number}")
print(f" Product: {line.product_id.name}")
print(f" Outstanding: {outstanding}")
print(f" Due: {borrow.due_date}")
Use Case 4: Partial Return Tracking¶
# Monitor partial returns
line = get_model("product.borrow.line").browse(line_id)
total_issued = line.issued_qty
total_returned = line.returned_qty
percent_returned = (total_returned / total_issued * 100) if total_issued > 0 else 0
print(f"Return Progress: {percent_returned:.0f}%")
print(f"Issued: {total_issued}, Returned: {total_returned}")
if percent_returned < 100:
print(f"Still waiting for: {total_issued - total_returned} units")
Best Practices¶
1. Always Specify Lot for Serialized Items¶
# Good: Track high-value items by serial
line_vals = {
"request_id": borrow_id,
"product_id": laptop_id,
"qty": 1,
"uom_id": unit_id,
"lot_id": serial_number_id # ✅ Can identify exact unit
}
# Bad: No serial tracking for expensive item
line_vals = {
"request_id": borrow_id,
"product_id": laptop_id,
"qty": 1,
"uom_id": unit_id
# ❌ Cannot identify which laptop
}
2. Check Outstanding Before Closing¶
# Good: Verify all items returned before completing
borrow = get_model("product.borrow").browse(borrow_id)
all_returned = True
for line in borrow.lines:
if line.issued_qty - line.returned_qty > 0:
all_returned = False
print(f"⚠️ {line.product_id.name} not fully returned")
if all_returned:
borrow.set_done() # ✅ Safe to complete
else:
print("❌ Cannot complete - items still outstanding")
# Bad: Complete without checking
borrow.set_done() # ❌ May have outstanding items
3. Use Computed Fields, Don't Store¶
# Good: Use computed fields
line = get_model("product.borrow.line").browse(line_id)
outstanding = line.issued_qty - line.returned_qty # ✅ Always accurate
# Bad: Store calculated values
line.write({"outstanding_qty": outstanding}) # ❌ Can become stale
Search Functions¶
Search by Product¶
# Find all borrow lines for a specific product
condition = [["product_id", "=", product_id]]
lines = get_model("product.borrow.line").search(condition)
Search by Borrow Request¶
# Find lines for a specific borrow request
condition = [["request_id", "=", borrow_id]]
lines = get_model("product.borrow.line").search(condition)
Search by Lot¶
# Find lines for a specific serial number
condition = [["lot_id", "=", lot_id]]
lines = get_model("product.borrow.line").search(condition)
Troubleshooting¶
Issued/Returned quantities not updating¶
Cause: Stock moves not in "done" state or missing related_id link
Solution: Ensure pickings are validated and properly linked to borrow request
Computed fields showing None¶
Cause: No stock moves exist yet
Solution: This is normal before issuing items; values populate after validation
Outstanding quantity incorrect¶
Cause: UoM mismatch in stock moves
Solution: Ensure consistent UoM across borrow lines and stock moves
Performance Tips¶
1. Access Lines Through Parent¶
# Efficient: Load borrow with lines
borrow = get_model("product.borrow").browse(borrow_id)
for line in borrow.lines: # ✅ Single query
print(line.product_id.name)
# Less efficient: Query each line
line_ids = get_model("product.borrow.line").search([
["request_id", "=", borrow_id]
])
for line_id in line_ids:
line = get_model("product.borrow.line").browse(line_id) # ❌ Multiple queries
2. Computed Fields Recalculate¶
# Computed fields recalculate on each access
line = get_model("product.borrow.line").browse(line_id)
# Cache if using multiple times
issued = line.issued_qty
returned = line.returned_qty
outstanding = issued - returned # ✅ Use cached values
# Bad: Multiple accesses
if line.issued_qty > line.returned_qty: # Computes twice
print(f"{line.issued_qty - line.returned_qty}") # Computes twice more
Testing Examples¶
Unit Test: Create Line¶
def test_create_borrow_line():
# Create parent borrow
borrow_id = get_model("product.borrow").create({
"date": "2025-10-27",
"employee_id": emp_id,
"borrow_for": "Test",
"due_date": "2025-11-10"
})
# Create line
line_id = get_model("product.borrow.line").create({
"request_id": borrow_id,
"product_id": prod_id,
"qty": 5,
"uom_id": uom_id
})
# Verify
line = get_model("product.borrow.line").browse(line_id)
assert line.request_id.id == borrow_id
assert line.qty == 5
assert line.issued_qty == 0 # Not issued yet
assert line.returned_qty == 0
Unit Test: Track Issued Quantity¶
def test_issued_quantity():
# Create borrow with line
borrow_id = get_model("product.borrow").create({
"date": "2025-10-27",
"employee_id": emp_id,
"borrow_for": "Test",
"due_date": "2025-11-10",
"lines": [("create", {
"product_id": prod_id,
"qty": 10,
"uom_id": uom_id
})]
})
borrow = get_model("product.borrow").browse(borrow_id)
borrow.approve()
# Issue items
result = borrow.copy_to_picking()
pick = get_model("stock.picking").browse(result["picking_id"])
pick.set_done()
# Verify issued quantity updated
line = borrow.lines[0]
assert line.issued_qty == 10
assert line.returned_qty == 0
Unit Test: Track Returned Quantity¶
def test_returned_quantity():
# Setup: Create and issue borrow
borrow_id = setup_borrow_and_issue(prod_id, 10)
# Return 6 items
borrow = get_model("product.borrow").browse(borrow_id)
result = borrow.copy_to_pick_in()
return_pick = get_model("stock.picking").browse(result["picking_id"])
return_pick.lines[0].write({"qty": 6})
return_pick.set_done()
# Verify
line = borrow.lines[0]
assert line.issued_qty == 10
assert line.returned_qty == 6
assert line.issued_qty - line.returned_qty == 4 # Outstanding
Version History¶
Last Updated: 2025-10-27
Model Version: product_borrow_line.py
Framework: Netforce
Additional Resources¶
- Product Borrow Documentation:
product.borrow - Product Documentation:
product - Stock Move Documentation:
stock.move - Stock Lot Documentation:
stock.lot
This documentation is generated for developer onboarding and reference purposes.