Creating Prepayment Options in Odoo Purchase Without Bills

The Prepayment Challenge in Odoo

In standard Odoo, the Purchase Order workflow follows this sequence:

Purchase Order → Vendor Bill → Payment

But many companies, especially in manufacturing, imports, or wholesale trade, need to pay vendors in advance before they issue bills. Common scenarios include:

  • Paying a fixed percentage, such as 30% advance, when confirming a Purchase Order.
  • Sending full payment upfront for overseas suppliers.
  • Reserving scarce materials by partial payment before delivery.

Odoo doesn’t provide a clean out-of-the-box way to register prepayments directly from the Purchase Order without generating a Vendor Bill.

This article demonstrates how to extend Odoo with a “Register Prepayment” option that enables you to:

  • Record supplier advance payments directly from the Purchase Order.
  • Post real account.payment entries without relying on placeholder Vendor Bills.
  • Link prepayments back to the Purchase Order.
  • Reconcile prepayments later when the Vendor Bill arrives.

Prepayments in Practice

  • Vendors often require payment in advance.
  • The company may not want to generate a Vendor Bill yet.
  • Payment must still be linked to the Purchase Order for traceability.
  • Prepayments are later reconciled with the Vendor Bill at final invoicing.

Numla’s Prepayment Solution

We introduce a simple extension that adds:

  • A Prepayment Wizard (prepayment.register) that collects payment details (amount, journal, method, bank account, etc.).
  • A “Register Prepayment” button on Purchase Orders.
  • Automatic posting of a Vendor Payment (account.payment) linked to the Purchase Order.
  • Reconciliation of the Vendor Bill with the recorded prepayment once the invoice arrives.

Steps to Implement Prepayments in Odoo

1. Add Action on Purchase Order

We start by adding an action that triggers the Prepayment Wizard. This action ensures the wizard automatically pre-fills details such as partner, amount, currency, and company.

def action_prepayment(self):
    """
    Open the prepayment registration wizard for the current PO.
    Calculates remaining prepayment by subtracting already posted ones.
    """
    posted_payment = self.env['account.payment'].search([
        ('purchase_id', '=', self.id), 
        ('state', '=', 'posted')
    ])
    amount = self.amount_total - sum(posted_payment.mapped('amount')) if posted_payment else self.amount_total
    return {
        'name': "Register Prepayment",
        'type': 'ir.actions.act_window',
        'view_mode': 'form',
        'res_model': 'prepayment.register',
        'view_id': self.env.ref('purchase_extension_qp.view_prepayment_register_form').id,
        'target': 'new',
        'context': {
            'default_purchase_id': self.id,
            'default_communication': self.name,
            'default_amount': amount,
            'default_currency_id': self.currency_id.id,
            'default_company_id': self.company_id.id,
            'default_partner_id': self.partner_id.id,
        }
    }

2. Add Buttons on the Purchase Order Form

Next, we make the feature accessible by adding a “Register Pre-Payment” button on the Purchase Order form. Users can click this button to open the wizard and record prepayments directly from the PO.

<record model="ir.ui.view" id="purchase_order_prepayment">
    <field name="name">purchase.order.prepaymen</field>
    <field name="model">purchase.order</field>
    <field name="inherit_id" ref="purchase.purchase_order_form"/>
    <field name="arch" type="xml">
        <xpath expr="//header/button[@name='button_unlock']" position="after">
            <button name="action_prepayment" type="object" string="Register Pre-Payment" class="oe_highlight" data-hotkey="w"/>
        </xpath>
        <field name="priority" position="replace"/>
        <xpath expr="//div[@name='button_box']" position="inside">
            <button class="oe_stat_button" name="action_view_prepayment" type="object" icon="fa-dollar">
                <div class="o_field_widget o_stat_info">
                    <span class="o_stat_text">PrePayment</span>
                </div>
            </button>
        </xpath>
    </field>
</record>

3. Create the Prepayment Wizard

We define a transient model prepayment.register with fields for journal, amount, payment method, partner bank, etc.

class PrePaymentRegister(models.TransientModel):
    _name = "prepayment.register"
    _description = "Prepayment Register"
    payment_date = fields.Date(string="Payment Date", required=True,
        default=fields.Date.context_today)
    amount = fields.Monetary(currency_field='currency_id', store=True, readonly=False,
        compute='_compute_amount')
    communication = fields.Char(string="Memo")
    currency_id = fields.Many2one(
        comodel_name='res.currency',
        string='Currency',
        help="The payment's currency.")
    available_journal_ids = fields.Many2many(
        comodel_name='account.journal',
        compute='_compute_available_journal_ids',
        store=False
    )
    journal_id = fields.Many2one(
        comodel_name='account.journal',
        domain="[('id', 'in', available_journal_ids)]",
    )
    available_partner_bank_ids = fields.Many2many(
        comodel_name='res.partner.bank',
        compute='_compute_available_partner_bank_ids',
    )
    partner_bank_id = fields.Many2one(
        comodel_name='res.partner.bank',
        string="Recipient Bank Account",
        readonly=False, store=True, tracking=True,
        compute='_compute_partner_bank_id',
        domain="[('id', 'in', available_partner_bank_ids)]",
        check_company=True
    )
    available_payment_method_line_ids = fields.Many2many('account.payment.method.line',
        compute='_compute_payment_method_line_fields')
    payment_method_line_id = fields.Many2one('account.payment.method.line', string='Payment Method',
        readonly=False, store=True, copy=False,
        compute='_compute_payment_method_line_id',
        domain="[('id', 'in', available_payment_method_line_ids)]",
        help="Manual: Pay or Get paid by any method outside of Odoo.\n"
        "Payment Providers: Each payment provider has its own Payment Method. Request a transaction on/to a card thanks to a payment token saved by the partner when buying or subscribing online.\n"
        "Check: Pay bills by check and print it from Odoo.\n"
        "Batch Deposit: Collect several customer checks at once generating and submitting a batch deposit to your bank. Module account_batch_payment is necessary.\n"
        "SEPA Credit Transfer: Pay in the SEPA zone by submitting a SEPA Credit Transfer file to your bank. Module account_sepa is necessary.\n"
        "SEPA Direct Debit: Get paid in the SEPA zone thanks to a mandate your partner will have granted to you. Module account_sepa is necessary.\n")
    destination_account_id = fields.Many2one(
        comodel_name='account.account',
        string='Destination Account',
        store=True, readonly=False,
        compute='_compute_destination_account_id',
        domain="[('account_type', 'in', ('asset_receivable', 'liability_payable')), ('company_id', '=', company_id)]",
        check_company=True)
    payment_type = fields.Selection([
        ('outbound', 'Send'),
        ('inbound', 'Receive'),
    ], string='Payment Type', default='outbound')
    company_id = fields.Many2one('res.company')
    partner_id = fields.Many2one(
        comodel_name='res.partner',
        string="Customer/Vendor",
    )
    purchase_id = fields.Many2one('purchase.order')
Compute Methods
@api.depends('journal_id', 'partner_id')
def _compute_destination_account_id(self):
    self.destination_account_id = False
    for pay in self:
        # Send money to pay a bill or receive money to refund it.
        if pay.partner_id:
            pay.destination_account_id = pay.partner_id.with_company(pay.company_id).property_account_payable_id
        else:
            pay.destination_account_id = self.env['account.account'].search([
                ('company_id', '=', pay.company_id.id),
                ('account_type', '=', 'liability_payable'),
                ('deprecated', '=', False),
            ], limit=1)
@api.depends('partner_id', 'company_id', 'payment_type')
def _compute_available_partner_bank_ids(self):
    for pay in self:
        if pay.payment_type == 'inbound':
            pay.available_partner_bank_ids = pay.journal_id.bank_account_id
        else:
            pay.available_partner_bank_ids = pay.partner_id.bank_ids\
                .filtered(lambda x: x.company_id.id in (False, pay.company_id.id))._origin
@api.depends('available_partner_bank_ids', 'journal_id')
def _compute_partner_bank_id(self):
    ''' The default partner_bank_id will be the first available on the partner. '''
    for pay in self:
        # Avoid overwriting existing value
        if pay.partner_bank_id and pay.partner_bank_id in pay.available_partner_bank_ids:
            continue
        pay.partner_bank_id = pay.available_partner_bank_ids[:1]._origin
@api.depends('payment_type', 'company_id')
def _compute_available_journal_ids(self):
    """
    Get all journals having at least one payment method for inbound/outbound depending on the payment_type.
    """
    journals = self.env['account.journal'].search([
        ('company_id', 'in', self.company_id.ids), ('type', 'in', ('bank', 'cash'))
    ])
    for pay in self:
        if pay.payment_type == 'inbound':
            pay.available_journal_ids = journals.filtered(
                lambda j: j.company_id == pay.company_id and j.inbound_payment_method_line_ids.ids != []
            )
        else:
            pay.available_journal_ids = journals.filtered(
                lambda j: j.company_id == pay.company_id and j.outbound_payment_method_line_ids.ids != []
            )
@api.depends('payment_type', 'journal_id', 'currency_id')
def _compute_payment_method_line_fields(self):
    for pay in self:
        pay.available_payment_method_line_ids = pay.journal_id._get_available_payment_method_lines(pay.payment_type)
@api.depends('available_payment_method_line_ids')
def _compute_payment_method_line_id(self):
    ''' Compute the 'payment_method_line_id' field.
    This field is not computed in '_compute_payment_method_line_fields' because it's a stored editable one.
    '''
    for pay in self:
        available_payment_method_lines = pay.available_payment_method_line_ids
        # Select the first available one by default.
        if pay.payment_method_line_id in available_payment_method_lines:
            pay.payment_method_line_id = pay.payment_method_line_id
        elif available_payment_method_lines:
            pay.payment_method_line_id = available_payment_method_lines[0]._origin
        else:
            pay.payment_method_line_id = False

The compute methods handle defaults for accounts, journals, banks, and payment methods, ensuring a smooth user experience with minimal manual input.

4. Posting Prepayments

When the user confirms the wizard, it creates and posts a Vendor Payment linked to the Purchase Order:

def action_create_prepayments(self):
    """
    Create and post supplier prepayment linked to Purchase Order.
    """
    payment_id = self.env['account.payment'].create({
        'payment_type': 'outbound',
        'partner_type': 'supplier',
        'partner_id': self.purchase_id.partner_id.id,
        'amount': self.amount,
        'date': self.payment_date,
        'ref': self.communication,
        'journal_id': self.journal_id.id,
        'payment_method_line_id': self.payment_method_line_id.id,
        'partner_bank_id': self.partner_bank_id.id,
        'purchase_id': self.purchase_id.id,
        'destination_account_id': self.destination_account_id.id,
    })
    payment_id.action_post()
    return {'type': 'ir.actions.act_window_close'}

5. Prepayment Wizard Form View

<record id="view_prepayment_register_form" model="ir.ui.view">
    <field name="name">prepayment.register.form</field>
    <field name="model">prepayment.register</field>
    <field name="arch" type="xml">
        <form string="Register Prepayment">
            <field name="available_journal_ids" invisible="1"/>
            <field name="available_partner_bank_ids" invisible="1"/>
            <field name="available_payment_method_line_ids" invisible="1"/>
            <field name="destination_account_id" invisible="1"/>
            <field name="purchase_id" invisible="1"/>
            <field name="consumption_id" invisible="1"/>
            <field name="partner_id" invisible="1"/>
            <field name="company_id" invisible="1"/>
            <field name="payment_type" invisible="1"/>
            <group>
                <group name="group1">
                    <field name="journal_id" options="{'no_open': True, 'no_create': True}" required="1"/>
                    <field name="payment_method_line_id" required="1" options="{'no_create': True, 'no_open': True}"/>
                    <field name="partner_bank_id" 
                           context="{'default_allow_out_payment': True}"/>
                </group>
                <group name="group2">
                    <label for="amount"/>
                    <div name="amount_div" class="o_row">
                        <field name="amount"/>
                        <field name="currency_id" 
                               required="1" 
                               options="{'no_create': True, 'no_open': True}" 
                               groups="base.group_multi_currency"/>
                    </div>
                    <field name="payment_date"/>
                    <field name="communication"/>
                </group>
            </group>
            <footer>
                <button string="Create PrePayment" name="action_create_prepayments" 
                        type="object" class="oe_highlight" data-hotkey="q"/>
                <button string="Cancel" class="btn btn-secondary" 
                        special="cancel" data-hotkey="z"/>
            </footer>
        </form>
    </field>
</record>

User Workflow

1. Confirm the Purchase Order.

2. Click Register Pre-Payment.

3. Fill in:

  • Journal (Bank or Cash)
  • Payment Method
  • Amount and Date
  • Vendor Bank Account (if applicable)

4. Confirm. Odoo creates a posted Vendor Payment.

5. The payment is visible in the Purchase Order via the PrePayment smart button.

6. When the Vendor Bill arrives, the accountant reconciles it with the prepayment.

What Makes This Useful

  • No fake Vendor Bills required for advance payments.
  • Payments are fully traceable against the Purchase Order.
  • Simple workflow for users.
  • Fully compatible with Odoo accounting reconciliation.

Final Thoughts

This customisation gives businesses a clean way to handle supplier prepayments directly from Purchase Orders. It’s especially useful for industries with advance payment practices, and ensures accounting remains transparent without clutter from unnecessary bills.

By combining functional ease with solid accounting integrity, Odoo becomes more aligned with real-world purchase management.

Found this article useful?

Explore more development guides and solutions from our team.

Check out more posts
Sign in to leave a comment
Restoring Odoo Database from .sql / .dump File