Created
July 15, 2025 00:56
-
-
Save codeagencybe/0f68cbbd450c190d02182cdf878aa4b6 to your computer and use it in GitHub Desktop.
Convert purchase order to vendor bill -- Odoo 18.3+ workaround
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Convert Purchase Order to Vendor Bill | |
# Works for single record or multiple records | |
log(f"Processing {len(records)} records") | |
for po in records: | |
log(f"Processing PO: {po.name}, Model: {po._name}, State: {po.state}") | |
# Skip if not a purchase order | |
if po._name != 'purchase.order': | |
log(f"Skipping {po.name} - not a purchase order", level='warning') | |
continue | |
# More lenient state check - include 'to approve' state | |
if po.state not in ['purchase', 'done', 'to approve']: | |
log(f"PO {po.name} is not in billable state (state: {po.state})", level='warning') | |
continue | |
# Check order lines | |
log(f"PO {po.name} has {len(po.order_line)} order lines") | |
for line in po.order_line: | |
log(f"Line: {line.name}, Qty ordered: {line.product_qty}, Qty received: {line.qty_received}, Qty invoiced: {line.qty_invoiced}") | |
# Less strict - allow billing even without receipts (useful for services) | |
billable_lines = po.order_line.filtered(lambda l: l.product_qty > 0 and l.qty_invoiced < l.product_qty) | |
if not billable_lines: | |
log(f"PO {po.name} has no billable lines", level='warning') | |
continue | |
# Create vendor bill | |
bill_vals = { | |
'move_type': 'in_invoice', | |
'partner_id': po.partner_id.id, | |
'purchase_vendor_bill_id': po.id, # Link to PO | |
'ref': po.partner_ref or po.name, | |
'invoice_date': datetime.date.today(), | |
'currency_id': po.currency_id.id, | |
'invoice_line_ids': [], | |
} | |
# Add invoice lines for all billable products | |
for line in billable_lines: | |
# Use received qty if available, otherwise use ordered qty | |
qty_to_bill = max(line.qty_received, line.product_qty) - line.qty_invoiced | |
if qty_to_bill <= 0: | |
continue | |
invoice_line_vals = { | |
'product_id': line.product_id.id, | |
'name': line.name, | |
'quantity': qty_to_bill, | |
'price_unit': line.price_unit, | |
'tax_ids': [(6, 0, line.taxes_id.ids)], | |
'purchase_line_id': line.id, | |
'account_id': line.product_id.property_account_expense_id.id or | |
line.product_id.categ_id.property_account_expense_categ_id.id, | |
} | |
bill_vals['invoice_line_ids'].append((0, 0, invoice_line_vals)) | |
# Create the bill | |
if bill_vals['invoice_line_ids']: | |
bill = env['account.move'].create(bill_vals) | |
log(f"Created vendor bill {bill.name} for PO {po.name}") | |
# Optional: Auto-confirm the bill | |
# bill.action_post() | |
else: | |
log(f"No billable lines found for PO {po.name}", level='warning') | |
# Optional: Return action to view created bills | |
if len(records) == 1: | |
# Single record - open the created bill | |
bills = env['account.move'].search([ | |
('purchase_vendor_bill_id', '=', records.id), | |
('state', '=', 'draft') | |
], limit=1) | |
if bills: | |
action = { | |
'type': 'ir.actions.act_window', | |
'name': 'Vendor Bill', | |
'res_model': 'account.move', | |
'res_id': bills.id, | |
'view_mode': 'form', | |
'target': 'current', | |
} | |
else: | |
# Multiple records - show list view of all bills | |
action = { | |
'type': 'ir.actions.act_window', | |
'name': 'Generated Vendor Bills', | |
'res_model': 'account.move', | |
'view_mode': 'tree,form', | |
'domain': [('purchase_vendor_bill_id', 'in', records.ids)], | |
'target': 'current', | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
For SaaS 18.3:
`import datetime
created_bills = env['account.move']
for po in records:
if po._name != 'purchase.order':
continue
if len(records) == 1:
if created_bills:
action = {
'type': 'ir.actions.act_window',
'name': 'Vendor Bill',
'res_model': 'account.move',
'res_id': created_bills.id,
'view_mode': 'form',
'target': 'current',
}
else:
action = {'type': 'ir.actions.act_window_close'}
else:
if created_bills:
action = {
'type': 'ir.actions.act_window',
'name': 'Generated Vendor Bills',
'res_model': 'account.move',
'view_mode': 'tree,form',
'domain': [('id', 'in', created_bills.ids)],
'target': 'current',
}
else:
action = {'type': 'ir.actions.act_window_close'}
`