commit 2da52a1858cd17cea8c27095a8b63aa5a4d4f278 Author: Suherdy SYC. Yacob Date: Tue Sep 23 14:15:11 2025 +0700 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..6330268 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# 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 \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..c536983 --- /dev/null +++ b/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard \ No newline at end of file diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..89e5bcb --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,25 @@ +{ + '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', +} \ No newline at end of file diff --git a/data/__init__.py b/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/data/product_data.xml b/data/product_data.xml new file mode 100644 index 0000000..624df5c --- /dev/null +++ b/data/product_data.xml @@ -0,0 +1,10 @@ + + + + + + Deposit + + + + \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..52cd9e9 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,3 @@ +from . import purchase_order +from . import account_payment +from . import res_config_settings \ No newline at end of file diff --git a/models/account_payment.py b/models/account_payment.py new file mode 100644 index 0000000..be311f5 --- /dev/null +++ b/models/account_payment.py @@ -0,0 +1,31 @@ +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() + return res \ No newline at end of file diff --git a/models/purchase_order.py b/models/purchase_order.py new file mode 100644 index 0000000..cbec487 --- /dev/null +++ b/models/purchase_order.py @@ -0,0 +1,187 @@ +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 + return super()._get_invoice_qty() \ No newline at end of file diff --git a/models/res_config_settings.py b/models/res_config_settings.py new file mode 100644 index 0000000..5d0a8e8 --- /dev/null +++ b/models/res_config_settings.py @@ -0,0 +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' + ) \ No newline at end of file diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv new file mode 100644 index 0000000..2f7f514 --- /dev/null +++ b/security/ir.model.access.csv @@ -0,0 +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 \ No newline at end of file diff --git a/views/__init__.py b/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/views/purchase_advance_payment_views.xml b/views/purchase_advance_payment_views.xml new file mode 100644 index 0000000..073c861 --- /dev/null +++ b/views/purchase_advance_payment_views.xml @@ -0,0 +1,40 @@ + + + + + + account.payment.form.inherit.advance.payment + account.payment + + + + + + + + + +