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 # 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.
## Features The journal entry uses the expense account from the default advance payment product
and the outstanding payment account from the selected cash/bank journal.
- Link payments to purchase orders as advance payments
- Automatically subtract advance payments from the total when the PO is fully billed ## Features
- Create deposit products on the PO so the final invoice includes the deposit product
- Track advance payments linked to purchase orders - Create advance payments directly from Purchase Order form
- Automatic deposit line creation in PO
## Usage - Proper accounting entries using:
- Expense account from default advance payment product
1. Create a purchase order - Outstanding payment account from selected journal
2. Create a payment and link it to the purchase order as an advance payment - Payment remains in draft state for review before posting
3. When the payment is posted, it will automatically be applied as a deposit to the purchase order - View all advance payments linked to a PO
4. The deposit will appear as a negative line item on the purchase order - Deposit line automatically updates when payment is posted
5. When creating the vendor bill, the deposit will be included
## Configuration
## Configuration
1. **Set Default Advance Payment Product**
No additional configuration is required. - Go to **Purchase > Configuration > Settings**
- Scroll to **Advance Payment** section
## Known Issues - Set the **Default Deposit Product** - this product's expense account will be used for advance payment journal entries
- None 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 from . import wizard

View File

@ -1,25 +1,31 @@
{ {
'name': 'Purchase Advance Payment', 'name': 'Purchase Advance Payment',
'version': '17.0.1.0.0', 'version': '17.0.2.0.0',
'category': 'Purchase', 'category': 'Purchase',
'summary': 'Link payments to purchase orders as advance payments', 'summary': 'Create advance payments directly from purchase orders',
'description': """ 'description': """
This module allows linking payments to purchase orders as advance payments. This module allows creating advance payments directly from purchase orders.
When a PO is fully billed, the total is subtracted by the advance payment made. When an advance payment is created, a deposit line is automatically added to the PO.
After payment is linked to the PO, a deposit product is created so the final The journal entry uses the expense account from the default advance payment product
invoice/vendor bills will include the deposit product. and the outstanding payment account from the selected cash/bank journal.
""",
'author': 'Suherdy Yacob', Features:
'depends': ['purchase', 'account'], - Create advance payments from PO form
'data': [ - Automatic deposit line creation
'security/ir.model.access.csv', - Proper accounting with configurable accounts
'data/product_data.xml', - Payment remains in draft for review
'wizard/link_advance_payment_wizard_views.xml', """,
'views/purchase_advance_payment_views.xml', 'author': 'Suherdy Yacob',
'views/purchase_order_views.xml', 'depends': ['purchase', 'account'],
'views/res_config_settings_views.xml', 'data': [
], 'security/ir.model.access.csv',
'installable': True, 'data/product_data.xml',
'auto_install': False, 'wizard/create_advance_payment_wizard_views.xml',
'license': 'LGPL-3', '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"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<data noupdate="1"> <data noupdate="1">
<!-- Product Category for Deposit Products --> <!-- Product Category for Deposit Products -->
<record id="product_category_deposit" model="product.category"> <record id="product_category_deposit" model="product.category">
<field name="name">Deposit</field> <field name="name">Deposit</field>
<field name="parent_id" ref="product.product_category_all"/> <field name="parent_id" ref="product.product_category_all"/>
</record> </record>
</data> </data>
</odoo> </odoo>

View File

@ -1,3 +1,3 @@
from . import purchase_order from . import purchase_order
from . import account_payment from . import account_payment
from . import res_config_settings 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 from odoo import models, fields, api
class AccountPayment(models.Model): class AccountPayment(models.Model):
_inherit = 'account.payment' _inherit = 'account.payment'
purchase_order_id = fields.Many2one( purchase_order_id = fields.Many2one(
'purchase.order', 'purchase.order',
string='Purchase Order', string='Purchase Order',
domain="[('partner_id', '=', partner_id), ('state', 'in', ('purchase', 'done'))]" domain="[('partner_id', '=', partner_id), ('state', 'in', ('purchase', 'done'))]"
) )
is_advance_payment = fields.Boolean( is_advance_payment = fields.Boolean(
string='Is Advance Payment', string='Is Advance Payment',
default=False, default=False,
help='Identifies if this payment is an advance payment for a purchase order' help='Identifies if this payment is an advance payment for a purchase order'
) )
@api.onchange('purchase_order_id') @api.onchange('purchase_order_id')
def _onchange_purchase_order_id(self): def _onchange_purchase_order_id(self):
if self.purchase_order_id: if self.purchase_order_id:
self.amount = self.purchase_order_id.amount_residual self.amount = self.purchase_order_id.amount_residual
def action_post(self): def _prepare_move_line_default_vals(self, write_off_line_vals=None, force_balance=None):
res = super().action_post() """Override to use correct accounts for advance payments"""
# When an advance payment is posted, link it to the purchase order line_vals_list = super()._prepare_move_line_default_vals(write_off_line_vals=write_off_line_vals, force_balance=force_balance)
for payment in self:
if payment.is_advance_payment and payment.purchase_order_id: if self.is_advance_payment and self.purchase_order_id:
# Apply the deposit to the purchase order # Get expense account from default deposit product
payment.purchase_order_id.action_apply_deposit() 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 return res

View File

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

View File

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

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

View File

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

View File

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