change the flow of advance payment, now creating advance payment only from purchase order form

This commit is contained in:
admin.suherdy 2025-12-03 13:53:46 +07:00
parent 137a64686c
commit ec53246e23
30 changed files with 789 additions and 561 deletions

View File

@ -1,25 +1,77 @@
# Purchase Advance Payment
This module allows linking payments to purchase orders as advance payments.
This module allows creating advance payments directly from purchase orders.
When an advance payment is created, a deposit line is automatically added to the PO.
The journal entry uses the expense account from the default advance payment product
and the outstanding payment account from the selected cash/bank journal.
## Features
- Link payments to purchase orders as advance payments
- Automatically subtract advance payments from the total when the PO is fully billed
- Create deposit products on the PO so the final invoice includes the deposit product
- Track advance payments linked to purchase orders
## Usage
1. Create a purchase order
2. Create a payment and link it to the purchase order as an advance payment
3. When the payment is posted, it will automatically be applied as a deposit to the purchase order
4. The deposit will appear as a negative line item on the purchase order
5. When creating the vendor bill, the deposit will be included
- Create advance payments directly from Purchase Order form
- Automatic deposit line creation in PO
- Proper accounting entries using:
- Expense account from default advance payment product
- Outstanding payment account from selected journal
- Payment remains in draft state for review before posting
- View all advance payments linked to a PO
- Deposit line automatically updates when payment is posted
## Configuration
No additional configuration is required.
1. **Set Default Advance Payment Product**
- Go to **Purchase > Configuration > Settings**
- Scroll to **Advance Payment** section
- Set the **Default Deposit Product** - this product's expense account will be used for advance payment journal entries
2. **Configure Outstanding Payment Accounts** (Choose one option)
**Option A: Journal-Specific** (for multiple banks)
- Go to **Accounting > Configuration > Journals**
- Open each journal > **Outgoing Payments** tab
- Set **Outstanding Payments Account** in payment methods
**Option B: Company Default** (for single bank)
- Go to **Accounting > Configuration > Settings**
- Set **Outstanding Payments Account** in Default Accounts section
Note: Journal-specific accounts take priority over company default.
## Usage
### Creating an Advance Payment
1. Open a Purchase Order (must be in confirmed state)
2. Go to **Advance Payments** tab
3. Click **Create Advance Payment** button
4. Fill in the wizard:
- **Journal**: Select cash or bank journal
- **Amount**: Enter advance payment amount
- **Date**: Payment date
- **Memo**: Optional description
5. Click **Confirm**
6. The payment is created in draft state for review
7. A deposit line is automatically added to the PO with negative amount
8. Review the payment and click **Confirm** to post it
### Journal Entry Accounts
When the payment is posted, the journal entry will use:
- **Debit**: Expense account from the default advance payment product
- **Credit**: Outstanding payment account from the selected journal
### Vendor Bill Creation
When creating a vendor bill from the PO:
1. The deposit line will be included in the bill
2. The deposit reduces the total amount payable
3. The advance payment is properly accounted for
## Technical Details
- Module creates a deposit product line in the PO with negative price
- Payment model is extended to handle advance payment accounting
- Journal entries are customized to use correct accounts
- Deposit line updates automatically when payment is posted
## Known Issues

View File

@ -1,20 +1,26 @@
{
'name': 'Purchase Advance Payment',
'version': '17.0.1.0.0',
'version': '17.0.2.0.0',
'category': 'Purchase',
'summary': 'Link payments to purchase orders as advance payments',
'summary': 'Create advance payments directly from purchase orders',
'description': """
This module allows linking payments to purchase orders as advance payments.
When a PO is fully billed, the total is subtracted by the advance payment made.
After payment is linked to the PO, a deposit product is created so the final
invoice/vendor bills will include the deposit product.
This module allows creating advance payments directly from purchase orders.
When an advance payment is created, a deposit line is automatically added to the PO.
The journal entry uses the expense account from the default advance payment product
and the outstanding payment account from the selected cash/bank journal.
Features:
- Create advance payments from PO form
- Automatic deposit line creation
- Proper accounting with configurable accounts
- Payment remains in draft for review
""",
'author': 'Suherdy Yacob',
'depends': ['purchase', 'account'],
'data': [
'security/ir.model.access.csv',
'data/product_data.xml',
'wizard/link_advance_payment_wizard_views.xml',
'wizard/create_advance_payment_wizard_views.xml',
'views/purchase_advance_payment_views.xml',
'views/purchase_order_views.xml',
'views/res_config_settings_views.xml',

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -21,11 +21,49 @@ class AccountPayment(models.Model):
if self.purchase_order_id:
self.amount = self.purchase_order_id.amount_residual
def _prepare_move_line_default_vals(self, write_off_line_vals=None, force_balance=None):
"""Override to use correct accounts for advance payments"""
line_vals_list = super()._prepare_move_line_default_vals(write_off_line_vals=write_off_line_vals, force_balance=force_balance)
if self.is_advance_payment and self.purchase_order_id:
# Get expense account from default deposit product
deposit_product_id = self.env['ir.config_parameter'].sudo().get_param(
'purchase_advance_payment.deposit_product_id')
if deposit_product_id:
product = self.env['product.product'].browse(int(deposit_product_id))
expense_account = product.property_account_expense_id or product.categ_id.property_account_expense_categ_id
if expense_account:
# Get outstanding payment account from journal
# For outbound payments, first check journal-specific account, then company default
outstanding_account = None
# Try to get from journal's outbound payment method
if self.journal_id.outbound_payment_method_line_ids:
outstanding_account = self.journal_id.outbound_payment_method_line_ids[0].payment_account_id
# Fall back to company default if not set on journal
if not outstanding_account:
outstanding_account = self.journal_id.company_id.account_journal_payment_credit_account_id
if outstanding_account:
# Modify the line vals to use correct accounts
for line_vals in line_vals_list:
# The debit line (expense) - partner line
if line_vals.get('debit', 0) > 0 and line_vals.get('partner_id'):
line_vals['account_id'] = expense_account.id
# The credit line (outstanding payment)
elif line_vals.get('credit', 0) > 0:
line_vals['account_id'] = outstanding_account.id
return line_vals_list
def action_post(self):
res = super().action_post()
# When an advance payment is posted, link it to the purchase order
# When an advance payment is posted, update deposit line in purchase order
for payment in self:
if payment.is_advance_payment and payment.purchase_order_id:
# Apply the deposit to the purchase order
payment.purchase_order_id.action_apply_deposit()
# Update the deposit line with the actual posted amount
payment.purchase_order_id._update_deposit_line()
return res

View File

@ -87,6 +87,57 @@ class PurchaseOrder(models.Model):
deposit_lines.unlink()
return res
def _update_deposit_line(self):
"""Update deposit line based on posted advance payments"""
self.ensure_one()
# Calculate total posted advance payments
posted_advance_total = sum(
payment.amount for payment in self.advance_payment_ids.filtered(lambda p: p.state == 'posted')
)
if posted_advance_total <= 0:
# Remove deposit line if no posted payments
deposit_lines = self.order_line.filtered(lambda l: l.is_deposit)
deposit_lines.unlink()
return
# Get or create deposit product
deposit_product_id = self.env['ir.config_parameter'].sudo().get_param(
'purchase_advance_payment.deposit_product_id')
if not deposit_product_id:
raise UserError(
"Please configure a default advance payment product in Purchase settings."
)
deposit_product = self.env['product.product'].browse(int(deposit_product_id))
# Check if deposit line already exists
existing_deposit_line = self.order_line.filtered(lambda l: l.is_deposit)
if existing_deposit_line:
# Update existing deposit line
existing_deposit_line.write({
'product_qty': 1,
'price_unit': -posted_advance_total, # Negative value for deposit
'taxes_id': [(6, 0, [])], # No taxes
})
else:
# Create new deposit line
deposit_vals = {
'order_id': self.id,
'product_id': deposit_product.id,
'name': f'Advance payment for {self.name}',
'product_qty': 1,
'product_uom': deposit_product.uom_id.id,
'price_unit': -posted_advance_total, # Negative value for deposit
'is_deposit': True,
'date_planned': fields.Datetime.now(),
'taxes_id': [(6, 0, [])], # No taxes
}
self.env['purchase.order.line'].create(deposit_vals)
def action_apply_deposit(self):
"""Apply advance payment as deposit line in the purchase order"""
self.ensure_one()

View File

@ -1,2 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_link_advance_payment_wizard,access.link.advance.payment.wizard,model_link_advance_payment_wizard,base.group_user,1,1,1,1
access_create_advance_payment_wizard,access.create.advance.payment.wizard,model_create_advance_payment_wizard,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_link_advance_payment_wizard access_create_advance_payment_wizard access.link.advance.payment.wizard access.create.advance.payment.wizard model_link_advance_payment_wizard model_create_advance_payment_wizard base.group_user 1 1 1 1

View File

@ -15,13 +15,7 @@
required="is_advance_payment"/>
</group>
</xpath>
<xpath expr="//header" position="inside">
<button name="%(purchase_advance_payment.action_link_advance_payment_wizard_payment)d"
string="Link to PO"
type="action"
class="btn-primary"
invisible="state != 'draft' or is_advance_payment"/>
</xpath>
<!-- Removed Link to PO button as per requirement -->
</field>
</record>

View File

@ -16,23 +16,20 @@
<xpath expr="//sheet/notebook" position="inside">
<page string="Advance Payments" name="advance_payments">
<group>
<group>
<field name="advance_payment_total" widget="monetary"/>
<field name="amount_residual" widget="monetary"/>
</group>
<group>
<button name="action_apply_deposit"
string="Apply Deposit"
type="object"
class="btn-primary"
invisible="advance_payment_total &lt;= 0"/>
<button name="%(purchase_advance_payment.action_link_advance_payment_wizard)d"
string="Link Advance Payment"
<button name="%(purchase_advance_payment.action_create_advance_payment_wizard)d"
string="Create Advance Payment"
type="action"
class="btn-secondary"
class="btn-primary"
context="{'default_purchase_order_id': active_id}"/>
</group>
<field name="advance_payment_ids">
<tree>
</group>
<field name="advance_payment_ids" nolabel="1">
<tree create="false" delete="false">
<field name="name"/>
<field name="date"/>
<field name="amount" widget="monetary"/>

View File

@ -1 +1 @@
from . import link_advance_payment_wizard
from . import create_advance_payment_wizard

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,189 @@
from odoo import models, fields, api
from odoo.exceptions import UserError
class CreateAdvancePaymentWizard(models.TransientModel):
_name = 'create.advance.payment.wizard'
_description = 'Create Advance Payment Wizard'
purchase_order_id = fields.Many2one(
'purchase.order',
string='Purchase Order',
required=True,
readonly=True
)
partner_id = fields.Many2one(
'res.partner',
string='Vendor',
related='purchase_order_id.partner_id',
readonly=True
)
journal_id = fields.Many2one(
'account.journal',
string='Journal',
required=True,
domain=[('type', 'in', ['bank', 'cash'])]
)
amount = fields.Monetary(
string='Amount',
required=True,
currency_field='currency_id'
)
currency_id = fields.Many2one(
'res.currency',
string='Currency',
related='purchase_order_id.currency_id',
readonly=True
)
date = fields.Date(
string='Date',
required=True,
default=fields.Date.context_today
)
memo = fields.Char(
string='Memo'
)
expense_account_id = fields.Many2one(
'account.account',
string='Expense Account',
compute='_compute_expense_account',
store=True,
readonly=True
)
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
if self._context.get('default_purchase_order_id'):
po = self.env['purchase.order'].browse(self._context['default_purchase_order_id'])
res['amount'] = po.amount_residual
res['memo'] = f'Advance payment for {po.name}'
return res
@api.depends('purchase_order_id')
def _compute_expense_account(self):
"""Get expense account from default advance payment product"""
for wizard in self:
deposit_product_id = self.env['ir.config_parameter'].sudo().get_param(
'purchase_advance_payment.deposit_product_id')
if deposit_product_id:
product = self.env['product.product'].browse(int(deposit_product_id))
# Get expense account from product
account = product.property_account_expense_id or product.categ_id.property_account_expense_categ_id
wizard.expense_account_id = account.id
else:
wizard.expense_account_id = False
def action_create_payment(self):
"""Create advance payment with proper journal entries"""
self.ensure_one()
if self.amount <= 0:
raise UserError("Amount must be greater than zero.")
# Get expense account from default deposit product
if not self.expense_account_id:
raise UserError(
"Please configure a default advance payment product in Purchase settings "
"with a valid expense account."
)
# Get outstanding payment account from journal
# For outbound payments, first check journal-specific account, then company default
outstanding_account = None
# Try to get from journal's outbound payment method
if self.journal_id.outbound_payment_method_line_ids:
outstanding_account = self.journal_id.outbound_payment_method_line_ids[0].payment_account_id
# Fall back to company default if not set on journal
if not outstanding_account:
outstanding_account = self.journal_id.company_id.account_journal_payment_credit_account_id
if not outstanding_account:
raise UserError(
f"Please configure an outstanding payment account for journal '{self.journal_id.name}'.\n"
f"You can set it in:\n"
f"1. Journal level: Accounting > Configuration > Journals > {self.journal_id.name} > "
f"Outgoing Payments tab > Payment Method > Outstanding Payments Account\n"
f"OR\n"
f"2. Company level: Accounting > Configuration > Settings > Default Accounts > "
f"Outstanding Payments Account"
)
# Create payment
payment_vals = {
'payment_type': 'outbound',
'partner_type': 'supplier',
'partner_id': self.partner_id.id,
'amount': self.amount,
'currency_id': self.currency_id.id,
'date': self.date,
'journal_id': self.journal_id.id,
'ref': self.memo or f'Advance payment for {self.purchase_order_id.name}',
'purchase_order_id': self.purchase_order_id.id,
'is_advance_payment': True,
}
payment = self.env['account.payment'].create(payment_vals)
# Create deposit line in purchase order immediately
self._create_deposit_line()
# Return action to open the created payment
return {
'type': 'ir.actions.act_window',
'name': 'Advance Payment',
'res_model': 'account.payment',
'res_id': payment.id,
'view_mode': 'form',
'target': 'current',
}
def _create_deposit_line(self):
"""Create deposit line in purchase order"""
# Get or create deposit product
deposit_product_id = self.env['ir.config_parameter'].sudo().get_param(
'purchase_advance_payment.deposit_product_id')
if not deposit_product_id:
raise UserError(
"Please configure a default advance payment product in Purchase settings."
)
deposit_product = self.env['product.product'].browse(int(deposit_product_id))
# Check if deposit line already exists
existing_deposit_line = self.purchase_order_id.order_line.filtered(lambda l: l.is_deposit)
# Calculate new total advance payment
new_advance_total = self.purchase_order_id.advance_payment_total + self.amount
if existing_deposit_line:
# Update existing deposit line
existing_deposit_line.write({
'product_qty': 1,
'price_unit': -new_advance_total,
'taxes_id': [(6, 0, [])],
})
else:
# Create new deposit line
deposit_vals = {
'order_id': self.purchase_order_id.id,
'product_id': deposit_product.id,
'name': f'Advance payment for {self.purchase_order_id.name}',
'product_qty': 1,
'product_uom': deposit_product.uom_id.id,
'price_unit': -self.amount,
'is_deposit': True,
'date_planned': fields.Datetime.now(),
'taxes_id': [(6, 0, [])],
}
self.env['purchase.order.line'].create(deposit_vals)

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Create Advance Payment Wizard Form View -->
<record id="view_create_advance_payment_wizard_form" model="ir.ui.view">
<field name="name">create.advance.payment.wizard.form</field>
<field name="model">create.advance.payment.wizard</field>
<field name="arch" type="xml">
<form string="Create Advance Payment">
<group>
<group>
<field name="purchase_order_id" readonly="1"/>
<field name="partner_id" readonly="1"/>
<field name="journal_id" required="1"/>
</group>
<group>
<field name="amount" required="1"/>
<field name="currency_id" invisible="1"/>
<field name="date" required="1"/>
<field name="memo"/>
</group>
</group>
<group>
<field name="expense_account_id" readonly="1"/>
</group>
<footer>
<button string="Confirm" name="action_create_payment" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- Create Advance Payment Wizard Action -->
<record id="action_create_advance_payment_wizard" model="ir.actions.act_window">
<field name="name">Create Advance Payment</field>
<field name="res_model">create.advance.payment.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</data>
</odoo>

View File

@ -1,100 +0,0 @@
from odoo import models, fields, api
from odoo.exceptions import UserError
class LinkAdvancePaymentWizard(models.TransientModel):
_name = 'link.advance.payment.wizard'
_description = 'Link Advance Payment Wizard'
purchase_order_id = fields.Many2one(
'purchase.order',
string='Purchase Order'
)
payment_id = fields.Many2one(
'account.payment',
string='Payment',
domain=[('state', '=', 'draft')]
)
amount = fields.Monetary(
string='Amount'
)
currency_id = fields.Many2one(
'res.currency',
string='Currency'
)
@api.model
def default_get(self, fields):
res = super().default_get(fields)
# Handle context from purchase order
if self._context.get('active_model') == 'purchase.order' and self._context.get('active_id'):
po = self.env['purchase.order'].browse(self._context['active_id'])
res['purchase_order_id'] = po.id
res['currency_id'] = po.currency_id.id
if 'amount' in fields:
res['amount'] = po.amount_residual
# Handle context from payment
elif self._context.get('active_model') == 'account.payment' and self._context.get('active_id'):
payment = self.env['account.payment'].browse(self._context['active_id'])
res['payment_id'] = payment.id
res['currency_id'] = payment.currency_id.id
if 'amount' in fields:
res['amount'] = payment.amount
return res
@api.onchange('payment_id')
def _onchange_payment_id(self):
if self.payment_id:
self.currency_id = self.payment_id.currency_id.id
self.amount = self.payment_id.amount
# Filter purchase orders by partner when payment is selected
if self.payment_id.partner_id:
purchase_orders = self.env['purchase.order'].search([
('partner_id', '=', self.payment_id.partner_id.id),
('state', 'in', ['purchase', 'done'])
])
return {'domain': {'purchase_order_id': [('id', 'in', purchase_orders.ids)]}}
return {'domain': {'purchase_order_id': []}}
@api.onchange('purchase_order_id')
def _onchange_purchase_order_id(self):
if self.purchase_order_id:
self.currency_id = self.purchase_order_id.currency_id.id
self.amount = self.purchase_order_id.amount_residual
# Filter payments by partner when purchase order is selected
if self.purchase_order_id.partner_id:
payments = self.env['account.payment'].search([
('partner_id', '=', self.purchase_order_id.partner_id.id),
('state', '=', 'draft'),
('payment_type', '=', 'outbound')
])
return {'domain': {'payment_id': [('id', 'in', payments.ids)]}}
return {'domain': {'payment_id': []}}
def action_link_payment(self):
self.ensure_one()
if self.amount <= 0:
raise UserError("Amount must be greater than zero.")
# Check if we have both payment and purchase order
if not self.payment_id:
raise UserError("Payment must be specified.")
if not self.purchase_order_id:
raise UserError("Purchase order must be specified.")
# Link the payment to the purchase order
self.payment_id.write({
'purchase_order_id': self.purchase_order_id.id,
'is_advance_payment': True,
'amount': self.amount
})
return {
'type': 'ir.actions.act_window_close'
}

View File

@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Link Advance Payment Wizard Form View -->
<record id="view_link_advance_payment_wizard_form" model="ir.ui.view">
<field name="name">link.advance.payment.wizard.form</field>
<field name="model">link.advance.payment.wizard</field>
<field name="arch" type="xml">
<form string="Link Advance Payment">
<group>
<field name="purchase_order_id"/>
<field name="payment_id" domain="[('state', '=', 'draft'), ('payment_type', '=', 'outbound')]"/>
<field name="amount"/>
<field name="currency_id" invisible="1"/>
</group>
<footer>
<button string="Link Payment" name="action_link_payment" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- Link Advance Payment Wizard Action -->
<record id="action_link_advance_payment_wizard" model="ir.actions.act_window">
<field name="name">Link Advance Payment</field>
<field name="res_model">link.advance.payment.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<!-- Link Advance Payment Wizard Action from Payment -->
<record id="action_link_advance_payment_wizard_payment" model="ir.actions.act_window">
<field name="name">Link to Purchase Order</field>
<field name="res_model">link.advance.payment.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{'active_model': 'account.payment', 'active_id': active_id}</field>
</record>
</data>
</odoo>