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

104
README.md
View File

@ -1,26 +1,78 @@
# Purchase Advance Payment
This module allows linking payments to purchase orders as advance payments.
## 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
## Configuration
No additional configuration is required.
## Known Issues
- None
# Purchase Advance Payment
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 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
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
- None

View File

@ -1,2 +1,2 @@
from . import models
from . import models
from . import wizard

View File

@ -1,25 +1,31 @@
{
'name': 'Purchase Advance Payment',
'version': '17.0.1.0.0',
'category': 'Purchase',
'summary': 'Link payments to purchase orders as advance payments',
'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.
""",
'author': 'Suherdy Yacob',
'depends': ['purchase', 'account'],
'data': [
'security/ir.model.access.csv',
'data/product_data.xml',
'wizard/link_advance_payment_wizard_views.xml',
'views/purchase_advance_payment_views.xml',
'views/purchase_order_views.xml',
'views/res_config_settings_views.xml',
],
'installable': True,
'auto_install': False,
'license': 'LGPL-3',
{
'name': 'Purchase Advance Payment',
'version': '17.0.2.0.0',
'category': 'Purchase',
'summary': 'Create advance payments directly from purchase orders',
'description': """
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/create_advance_payment_wizard_views.xml',
'views/purchase_advance_payment_views.xml',
'views/purchase_order_views.xml',
'views/res_config_settings_views.xml',
],
'installable': True,
'auto_install': False,
'license': 'LGPL-3',
}

Binary file not shown.

Binary file not shown.

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Product Category for Deposit Products -->
<record id="product_category_deposit" model="product.category">
<field name="name">Deposit</field>
<field name="parent_id" ref="product.product_category_all"/>
</record>
</data>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Product Category for Deposit Products -->
<record id="product_category_deposit" model="product.category">
<field name="name">Deposit</field>
<field name="parent_id" ref="product.product_category_all"/>
</record>
</data>
</odoo>

View File

@ -1,3 +1,3 @@
from . import purchase_order
from . import account_payment
from . import purchase_order
from . import account_payment
from . import res_config_settings

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

@ -1,31 +1,69 @@
from odoo import models, fields, api
class AccountPayment(models.Model):
_inherit = 'account.payment'
purchase_order_id = fields.Many2one(
'purchase.order',
string='Purchase Order',
domain="[('partner_id', '=', partner_id), ('state', 'in', ('purchase', 'done'))]"
)
is_advance_payment = fields.Boolean(
string='Is Advance Payment',
default=False,
help='Identifies if this payment is an advance payment for a purchase order'
)
@api.onchange('purchase_order_id')
def _onchange_purchase_order_id(self):
if self.purchase_order_id:
self.amount = self.purchase_order_id.amount_residual
def action_post(self):
res = super().action_post()
# When an advance payment is posted, link it to the 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()
from odoo import models, fields, api
class AccountPayment(models.Model):
_inherit = 'account.payment'
purchase_order_id = fields.Many2one(
'purchase.order',
string='Purchase Order',
domain="[('partner_id', '=', partner_id), ('state', 'in', ('purchase', 'done'))]"
)
is_advance_payment = fields.Boolean(
string='Is Advance Payment',
default=False,
help='Identifies if this payment is an advance payment for a purchase order'
)
@api.onchange('purchase_order_id')
def _onchange_purchase_order_id(self):
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, update deposit line in purchase order
for payment in self:
if payment.is_advance_payment and payment.purchase_order_id:
# Update the deposit line with the actual posted amount
payment.purchase_order_id._update_deposit_line()
return res

View File

@ -1,187 +1,238 @@
from odoo import models, fields, api
from odoo.exceptions import UserError
from odoo.tools import float_compare
class PurchaseOrder(models.Model):
_inherit = 'purchase.order'
advance_payment_ids = fields.One2many(
'account.payment',
'purchase_order_id',
string='Advance Payments',
domain=[('state', '=', 'posted')]
)
advance_payment_total = fields.Monetary(
string='Advance Payment Total',
compute='_compute_advance_payment_total',
store=True
)
amount_residual = fields.Monetary(
string='Amount Residual',
compute='_compute_amount_residual',
store=True
)
deposit_product_id = fields.Many2one(
'product.product',
string='Deposit Product',
help='Product used for advance payment deposit'
)
@api.depends('advance_payment_ids', 'advance_payment_ids.state', 'advance_payment_ids.amount')
def _compute_advance_payment_total(self):
for order in self:
order.advance_payment_total = sum(
payment.amount for payment in order.advance_payment_ids.filtered(lambda p: p.state == 'posted')
)
@api.depends('amount_total', 'advance_payment_total')
def _compute_amount_residual(self):
for order in self:
order.amount_residual = order.amount_total - order.advance_payment_total
def action_view_advance_payments(self):
self.ensure_one()
action = self.env.ref('account.action_account_payments').sudo().read()[0]
action['domain'] = [('id', 'in', self.advance_payment_ids.ids)]
action['context'] = {
'default_purchase_order_id': self.id,
'default_partner_id': self.partner_id.id,
'default_payment_type': 'outbound',
'default_partner_type': 'supplier',
}
return action
def action_create_deposit_product(self):
"""Create a deposit product for this purchase order"""
self.ensure_one()
# Check if there's a default deposit product in settings
default_deposit_product = self.env['ir.config_parameter'].sudo().get_param(
'purchase_advance_payment.deposit_product_id')
if default_deposit_product:
self.deposit_product_id = int(default_deposit_product)
return self.deposit_product_id
# If no default product, create one
if not self.deposit_product_id:
product_vals = {
'name': f'Deposit for PO {self.name}',
'type': 'service',
'purchase_ok': True,
'sale_ok': False,
'invoice_policy': 'order', # Ordered quantities for deposit
'supplier_taxes_id': [(6, 0, [])], # No supplier taxes
}
deposit_product = self.env['product.product'].create(product_vals)
self.deposit_product_id = deposit_product.id
return self.deposit_product_id
def button_draft(self):
res = super().button_draft()
# Remove deposit lines when resetting to draft
for order in self:
deposit_lines = order.order_line.filtered(lambda l: l.is_deposit)
deposit_lines.unlink()
return res
def action_apply_deposit(self):
"""Apply advance payment as deposit line in the purchase order"""
self.ensure_one()
if self.advance_payment_total <= 0:
raise UserError("No advance payment found for this purchase order.")
# Create or update deposit product
if not self.deposit_product_id:
self.action_create_deposit_product()
# 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': -self.advance_payment_total, # Negative value for deposit
'taxes_id': [(6, 0, [])], # No taxes
})
else:
# Create new deposit line
deposit_vals = {
'order_id': self.id,
'product_id': self.deposit_product_id.id,
'name': f'Deposit payment for PO {self.name}',
'product_qty': 1,
'product_uom': self.deposit_product_id.uom_id.id,
'price_unit': -self.advance_payment_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)
return True
def action_create_invoice(self):
"""Override to ensure deposit line is included in vendor bill"""
# Apply deposit before creating invoice
for order in self:
if order.advance_payment_total > 0:
order.action_apply_deposit()
# Call super to create the invoice
invoices = super().action_create_invoice()
# Ensure deposit lines have quantity 1 in the created invoices
if 'res_id' in invoices and invoices['res_id']:
# Single invoice
invoice = self.env['account.move'].browse(invoices['res_id'])
self._fix_deposit_line_quantities(invoice)
elif 'domain' in invoices and invoices['domain']:
# Multiple invoices
invoice_ids = self.env['account.move'].search(invoices['domain'])
for invoice in invoice_ids:
self._fix_deposit_line_quantities(invoice)
return invoices
def _fix_deposit_line_quantities(self, invoice):
"""Fix deposit line quantities in the invoice"""
for line in invoice.invoice_line_ids:
if line.purchase_line_id and line.purchase_line_id.is_deposit:
line.write({
'quantity': 1.0,
'tax_ids': [(6, 0, [])] # No taxes
})
class PurchaseOrderLine(models.Model):
_inherit = 'purchase.order.line'
is_deposit = fields.Boolean(
string='Is Deposit',
default=False,
help='Identifies if this line is a deposit payment'
)
def _prepare_account_move_line(self, move=False):
"""Override to ensure deposit lines have correct quantity in vendor bill"""
self.ensure_one()
res = super()._prepare_account_move_line(move)
# If this is a deposit line, ensure quantity is 1 and no taxes
if self.is_deposit:
res['quantity'] = 1
res['tax_ids'] = [(6, 0, [])] # No taxes
return res
def _get_invoice_qty(self):
"""Override to ensure deposit lines have correct quantity for invoicing"""
self.ensure_one()
if self.is_deposit:
# For deposit lines, always invoice quantity 1 regardless of received qty
return 1.0
from odoo import models, fields, api
from odoo.exceptions import UserError
from odoo.tools import float_compare
class PurchaseOrder(models.Model):
_inherit = 'purchase.order'
advance_payment_ids = fields.One2many(
'account.payment',
'purchase_order_id',
string='Advance Payments',
domain=[('state', '=', 'posted')]
)
advance_payment_total = fields.Monetary(
string='Advance Payment Total',
compute='_compute_advance_payment_total',
store=True
)
amount_residual = fields.Monetary(
string='Amount Residual',
compute='_compute_amount_residual',
store=True
)
deposit_product_id = fields.Many2one(
'product.product',
string='Deposit Product',
help='Product used for advance payment deposit'
)
@api.depends('advance_payment_ids', 'advance_payment_ids.state', 'advance_payment_ids.amount')
def _compute_advance_payment_total(self):
for order in self:
order.advance_payment_total = sum(
payment.amount for payment in order.advance_payment_ids.filtered(lambda p: p.state == 'posted')
)
@api.depends('amount_total', 'advance_payment_total')
def _compute_amount_residual(self):
for order in self:
order.amount_residual = order.amount_total - order.advance_payment_total
def action_view_advance_payments(self):
self.ensure_one()
action = self.env.ref('account.action_account_payments').sudo().read()[0]
action['domain'] = [('id', 'in', self.advance_payment_ids.ids)]
action['context'] = {
'default_purchase_order_id': self.id,
'default_partner_id': self.partner_id.id,
'default_payment_type': 'outbound',
'default_partner_type': 'supplier',
}
return action
def action_create_deposit_product(self):
"""Create a deposit product for this purchase order"""
self.ensure_one()
# Check if there's a default deposit product in settings
default_deposit_product = self.env['ir.config_parameter'].sudo().get_param(
'purchase_advance_payment.deposit_product_id')
if default_deposit_product:
self.deposit_product_id = int(default_deposit_product)
return self.deposit_product_id
# If no default product, create one
if not self.deposit_product_id:
product_vals = {
'name': f'Deposit for PO {self.name}',
'type': 'service',
'purchase_ok': True,
'sale_ok': False,
'invoice_policy': 'order', # Ordered quantities for deposit
'supplier_taxes_id': [(6, 0, [])], # No supplier taxes
}
deposit_product = self.env['product.product'].create(product_vals)
self.deposit_product_id = deposit_product.id
return self.deposit_product_id
def button_draft(self):
res = super().button_draft()
# Remove deposit lines when resetting to draft
for order in self:
deposit_lines = order.order_line.filtered(lambda l: l.is_deposit)
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()
if self.advance_payment_total <= 0:
raise UserError("No advance payment found for this purchase order.")
# Create or update deposit product
if not self.deposit_product_id:
self.action_create_deposit_product()
# 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': -self.advance_payment_total, # Negative value for deposit
'taxes_id': [(6, 0, [])], # No taxes
})
else:
# Create new deposit line
deposit_vals = {
'order_id': self.id,
'product_id': self.deposit_product_id.id,
'name': f'Deposit payment for PO {self.name}',
'product_qty': 1,
'product_uom': self.deposit_product_id.uom_id.id,
'price_unit': -self.advance_payment_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)
return True
def action_create_invoice(self):
"""Override to ensure deposit line is included in vendor bill"""
# Apply deposit before creating invoice
for order in self:
if order.advance_payment_total > 0:
order.action_apply_deposit()
# Call super to create the invoice
invoices = super().action_create_invoice()
# Ensure deposit lines have quantity 1 in the created invoices
if 'res_id' in invoices and invoices['res_id']:
# Single invoice
invoice = self.env['account.move'].browse(invoices['res_id'])
self._fix_deposit_line_quantities(invoice)
elif 'domain' in invoices and invoices['domain']:
# Multiple invoices
invoice_ids = self.env['account.move'].search(invoices['domain'])
for invoice in invoice_ids:
self._fix_deposit_line_quantities(invoice)
return invoices
def _fix_deposit_line_quantities(self, invoice):
"""Fix deposit line quantities in the invoice"""
for line in invoice.invoice_line_ids:
if line.purchase_line_id and line.purchase_line_id.is_deposit:
line.write({
'quantity': 1.0,
'tax_ids': [(6, 0, [])] # No taxes
})
class PurchaseOrderLine(models.Model):
_inherit = 'purchase.order.line'
is_deposit = fields.Boolean(
string='Is Deposit',
default=False,
help='Identifies if this line is a deposit payment'
)
def _prepare_account_move_line(self, move=False):
"""Override to ensure deposit lines have correct quantity in vendor bill"""
self.ensure_one()
res = super()._prepare_account_move_line(move)
# If this is a deposit line, ensure quantity is 1 and no taxes
if self.is_deposit:
res['quantity'] = 1
res['tax_ids'] = [(6, 0, [])] # No taxes
return res
def _get_invoice_qty(self):
"""Override to ensure deposit lines have correct quantity for invoicing"""
self.ensure_one()
if self.is_deposit:
# For deposit lines, always invoice quantity 1 regardless of received qty
return 1.0
return super()._get_invoice_qty()

View File

@ -1,13 +1,13 @@
from odoo import models, fields, api
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
deposit_product_id = fields.Many2one(
'product.product',
string='Default Deposit Product',
domain=[('type', '=', 'service')],
config_parameter='purchase_advance_payment.deposit_product_id',
help='Default product used for advance payment deposits'
from odoo import models, fields, api
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
deposit_product_id = fields.Many2one(
'product.product',
string='Default Deposit Product',
domain=[('type', '=', 'service')],
config_parameter='purchase_advance_payment.deposit_product_id',
help='Default product used for advance payment deposits'
)

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
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
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

@ -1,40 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Account Payment Form View -->
<record id="view_account_payment_form_inherit_advance_payment" model="ir.ui.view">
<field name="name">account.payment.form.inherit.advance.payment</field>
<field name="model">account.payment</field>
<field name="inherit_id" ref="account.view_account_payment_form"/>
<field name="arch" type="xml">
<xpath expr="//sheet/group[2]" position="after">
<group>
<field name="is_advance_payment" invisible="1"/>
<field name="purchase_order_id"
invisible="not is_advance_payment"
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>
</field>
</record>
<!-- Account Payment Tree View -->
<record id="view_account_payment_tree_inherit_advance_payment" model="ir.ui.view">
<field name="name">account.payment.tree.inherit.advance.payment</field>
<field name="model">account.payment</field>
<field name="inherit_id" ref="account.view_account_payment_tree"/>
<field name="arch" type="xml">
<xpath expr="//tree/field[@name='partner_id']" position="after">
<field name="purchase_order_id"/>
</xpath>
</field>
</record>
</data>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Account Payment Form View -->
<record id="view_account_payment_form_inherit_advance_payment" model="ir.ui.view">
<field name="name">account.payment.form.inherit.advance.payment</field>
<field name="model">account.payment</field>
<field name="inherit_id" ref="account.view_account_payment_form"/>
<field name="arch" type="xml">
<xpath expr="//sheet/group[2]" position="after">
<group>
<field name="is_advance_payment" invisible="1"/>
<field name="purchase_order_id"
invisible="not is_advance_payment"
required="is_advance_payment"/>
</group>
</xpath>
<!-- Removed Link to PO button as per requirement -->
</field>
</record>
<!-- Account Payment Tree View -->
<record id="view_account_payment_tree_inherit_advance_payment" model="ir.ui.view">
<field name="name">account.payment.tree.inherit.advance.payment</field>
<field name="model">account.payment</field>
<field name="inherit_id" ref="account.view_account_payment_tree"/>
<field name="arch" type="xml">
<xpath expr="//tree/field[@name='partner_id']" position="after">
<field name="purchase_order_id"/>
</xpath>
</field>
</record>
</data>
</odoo>

View File

@ -1,56 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Purchase Order Form View -->
<record id="view_purchase_order_form_inherit_advance_payment" model="ir.ui.view">
<field name="name">purchase.order.form.inherit.advance.payment</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_form"/>
<field name="arch" type="xml">
<xpath expr="//sheet/group/group[2]" position="after">
<group>
<field name="deposit_product_id" invisible="1"/>
</group>
</xpath>
<xpath expr="//sheet/notebook" position="inside">
<page string="Advance Payments" name="advance_payments">
<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"
type="action"
class="btn-secondary"
context="{'default_purchase_order_id': active_id}"/>
</group>
<field name="advance_payment_ids">
<tree>
<field name="name"/>
<field name="date"/>
<field name="amount" widget="monetary"/>
<field name="state"/>
<field name="journal_id"/>
</tree>
</field>
</page>
</xpath>
<xpath expr="//field[@name='order_line']/tree/field[@name='price_unit']" position="after">
<field name="is_deposit" invisible="1"/>
</xpath>
<xpath expr="//field[@name='order_line']/form//field[@name='price_unit']" position="after">
<field name="is_deposit" invisible="1"/>
</xpath>
</field>
</record>
</data>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Purchase Order Form View -->
<record id="view_purchase_order_form_inherit_advance_payment" model="ir.ui.view">
<field name="name">purchase.order.form.inherit.advance.payment</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_form"/>
<field name="arch" type="xml">
<xpath expr="//sheet/group/group[2]" position="after">
<group>
<field name="deposit_product_id" invisible="1"/>
</group>
</xpath>
<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="%(purchase_advance_payment.action_create_advance_payment_wizard)d"
string="Create Advance Payment"
type="action"
class="btn-primary"
context="{'default_purchase_order_id': active_id}"/>
</group>
</group>
<field name="advance_payment_ids" nolabel="1">
<tree create="false" delete="false">
<field name="name"/>
<field name="date"/>
<field name="amount" widget="monetary"/>
<field name="state"/>
<field name="journal_id"/>
</tree>
</field>
</page>
</xpath>
<xpath expr="//field[@name='order_line']/tree/field[@name='price_unit']" position="after">
<field name="is_deposit" invisible="1"/>
</xpath>
<xpath expr="//field[@name='order_line']/form//field[@name='price_unit']" position="after">
<field name="is_deposit" invisible="1"/>
</xpath>
</field>
</record>
</data>
</odoo>

View File

@ -1,34 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="res_config_settings_view_form_purchase_advance_payment" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.purchase.advance.payment</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//form" position="inside">
<div class="app_settings_block" data-string="Purchase" string="Purchase" data-key="purchase">
<h2>Advance Payment</h2>
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane" />
<div class="o_setting_right_pane">
<label
for="deposit_product_id"
string="Advance Payments"
/>
<div class="text-muted">
Default product used for advance payment deposits
</div>
<div class="text-muted">
<field name="deposit_product_id" />
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</data>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="res_config_settings_view_form_purchase_advance_payment" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.purchase.advance.payment</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//form" position="inside">
<div class="app_settings_block" data-string="Purchase" string="Purchase" data-key="purchase">
<h2>Advance Payment</h2>
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane" />
<div class="o_setting_right_pane">
<label
for="deposit_product_id"
string="Advance Payments"
/>
<div class="text-muted">
Default product used for advance payment deposits
</div>
<div class="text-muted">
<field name="deposit_product_id" />
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</data>
</odoo>

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>