diff --git a/sale_down_payment/__manifest__.py b/sale_down_payment/__manifest__.py index b078878..f97d4cc 100644 --- a/sale_down_payment/__manifest__.py +++ b/sale_down_payment/__manifest__.py @@ -1,10 +1,10 @@ -# Copyright 2019 Akretion France (http://www.akretion.com) +# Copyright 2019-2024 Akretion France (http://www.akretion.com) # @author Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { 'name': 'Sale Down Payment', - 'version': '12.0.1.0.0', + 'version': '14.0.1.0.0', 'category': 'Sales', 'license': 'AGPL-3', 'summary': 'Link payment to sale orders', @@ -14,7 +14,7 @@ Sale Down Payment This module adds a link between payments and sale orders. It allows to see down payments directly on the sale order form view. -After processing a bank statement, you can start a wizard to link unreconciled incoming payments to a sale order. There is also a button *Register Payment* on the sale order. +After processing a bank statement, you can start a wizard to link unreconciled incoming payments to a sale order (NOT ported to v14 so far). There is also a button *Register Payment* on the sale order. This module targets B2B companies that don't want to generate a down payment invoice for an advanced payment. @@ -25,11 +25,13 @@ This module has been written by Alexis de Lattre from Akretion 'website': 'http://www.akretion.com', 'depends': ['sale'], 'data': [ - 'wizard/account_bank_statement_sale_view.xml', - 'views/account_bank_statement.xml', + 'security/ir.model.access.csv', + 'wizard/account_payment_register_sale_view.xml', +# 'wizard/account_bank_statement_sale_view.xml', +# 'views/account_bank_statement.xml', 'views/sale.xml', 'views/account_move_line.xml', 'views/account_payment.xml', ], - 'installable': False, + 'installable': True, } diff --git a/sale_down_payment/models/__init__.py b/sale_down_payment/models/__init__.py index a967445..de169dc 100644 --- a/sale_down_payment/models/__init__.py +++ b/sale_down_payment/models/__init__.py @@ -1,3 +1,4 @@ from . import sale +from . import account_move from . import account_move_line from . import account_payment diff --git a/sale_down_payment/models/account_move.py b/sale_down_payment/models/account_move.py new file mode 100644 index 0000000..da105fa --- /dev/null +++ b/sale_down_payment/models/account_move.py @@ -0,0 +1,26 @@ +# Copyright 2023-2024 Akretion France (http://www.akretion.com) +# @author Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class AccountMove(models.Model): + _inherit = 'account.move' + + def _post(self, soft=True): + res = super()._post(soft=soft) + amlo = self.env['account.move.line'] + for move in self: + if move.state == 'posted' and move.move_type == 'out_invoice': + sales = move.invoice_line_ids.sale_line_ids.order_id + if sales: + mlines = amlo.search([('sale_id', 'in', sales.ids)]) + if mlines: + mlines_to_reconcile = move.line_ids.filtered( + lambda line: line.account_id == + move.commercial_partner_id.property_account_receivable_id) + mlines_to_reconcile |= mlines + mlines_to_reconcile.remove_move_reconcile() + mlines_to_reconcile.reconcile() + return res diff --git a/sale_down_payment/models/account_move_line.py b/sale_down_payment/models/account_move_line.py index 601f3ef..a426570 100644 --- a/sale_down_payment/models/account_move_line.py +++ b/sale_down_payment/models/account_move_line.py @@ -1,4 +1,4 @@ -# Copyright 2019 Akretion France (http://www.akretion.com) +# Copyright 2019-2024 Akretion France (http://www.akretion.com) # @author Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -9,24 +9,23 @@ from odoo.exceptions import ValidationError class AccountMoveLine(models.Model): _inherit = 'account.move.line' - sale_id = fields.Many2one('sale.order', string='Sale Order') - account_internal_type = fields.Selection( - related='account_id.user_type_id.type', store=True, - string='Account Internal Type') + sale_id = fields.Many2one( + 'sale.order', string='Sale Order', check_company=True, + domain="[('partner_invoice_id', 'child_of', partner_id), ('state', '!=', 'cancel'), ('invoice_status', '!=', 'invoiced'), ('company_id', '=', company_id)]") @api.constrains('sale_id', 'account_id') - def sale_id_check(self): + def _sale_id_check(self): for line in self: - if line.sale_id and line.account_id.internal_type != 'receivable': + if line.sale_id and line.account_internal_type != 'receivable': raise ValidationError(_( "The account move line '%s' is linked to sale order '%s' " "but it uses account '%s' which is not a receivable " "account.") - % (line.name, - line.sale_id.name, + % (line.display_name, + line.sale_id.display_name, line.account_id.display_name)) @api.onchange('account_id') def sale_advance_payement_account_id_change(self): - if self.sale_id and self.account_id.user_type_id.type != 'receivable': + if self.sale_id and self.account_internal_type != 'receivable': self.sale_id = False diff --git a/sale_down_payment/models/account_payment.py b/sale_down_payment/models/account_payment.py index a10af15..defeb4d 100644 --- a/sale_down_payment/models/account_payment.py +++ b/sale_down_payment/models/account_payment.py @@ -1,9 +1,8 @@ -# Copyright 2019 Akretion France (http://www.akretion.com) +# Copyright 2019-2024 Akretion France (http://www.akretion.com) # @author Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import fields, models -from odoo.tools import float_round class AccountPayment(models.Model): @@ -11,49 +10,24 @@ class AccountPayment(models.Model): sale_id = fields.Many2one('sale.order', string='Sale Order') - def action_validate_invoice_payment(self): - if self.sale_id: - self.post() - else: - return super(AccountPayment, self).\ - action_validate_invoice_payment() - - def _get_counterpart_move_line_vals(self, invoice=False): - res = super(AccountPayment, self)._get_counterpart_move_line_vals( - invoice=invoice) - if self.sale_id: - res['sale_id'] = self.sale_id.id - return res - - -class AccountAbstractPayment(models.AbstractModel): - _inherit = "account.abstract.payment" - - def default_get(self, fields_list): - res = super(AccountAbstractPayment, self).default_get(fields_list) + def _prepare_move_line_default_vals(self, write_off_line_vals=None): + line_vals_list = super()._prepare_move_line_default_vals( + write_off_line_vals=write_off_line_vals) + # Add to the receivable/payable line if ( - self._context.get('active_model') == 'sale.order' and - self._context.get('active_id')): - so = self.env['sale.order'].browse(self._context['active_id']) - res.update({ - 'amount': so.amount_total, - 'currency_id': so.currency_id.id, - 'payment_type': 'inbound', - 'partner_id': so.partner_invoice_id.commercial_partner_id.id, - 'partner_type': 'customer', - 'communication': so.name, - 'sale_id': so.id, - }) - return res + self.sale_id and + len(line_vals_list) >= 2 and + line_vals_list[1].get('account_id') == self.destination_account_id.id): + line_vals_list[1]['sale_id'] = self.sale_id.id + return line_vals_list - def _compute_payment_amount(self, invoices=None, currency=None): - amount = super(AccountAbstractPayment, self)._compute_payment_amount( - invoices=invoices, currency=currency) - if self.sale_id: - payment_currency = currency - if not payment_currency: - payment_currency = self.sale_id.currency_id - amount = float_round( - self.sale_id.amount_total - self.sale_id.amount_down_payment, - precision_rounding=payment_currency.rounding) - return amount + def action_post(self): + super().action_post() + for pay in self: + if pay.sale_id and pay.payment_type == 'inbound': + pay._sale_down_payment_hook() + + def _sale_down_payment_hook(self): + # can be used for notifications + # WAS on account.move.line on v12 ; is on account.payment on v14 + self.ensure_one() diff --git a/sale_down_payment/models/sale.py b/sale_down_payment/models/sale.py index 687cc5d..0246b55 100644 --- a/sale_down_payment/models/sale.py +++ b/sale_down_payment/models/sale.py @@ -1,4 +1,4 @@ -# Copyright 2019 Akretion France (http://www.akretion.com) +# Copyright 2019-2024 Akretion France (http://www.akretion.com) # @author Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -9,8 +9,8 @@ from odoo.tools import float_round class SaleOrder(models.Model): _inherit = 'sale.order' - payment_line_ids = fields.One2many( - 'account.move.line', 'sale_id', string='Advance Payments', + payment_ids = fields.One2many( + 'account.payment', 'sale_id', string='Advance Payments', readonly=True) amount_down_payment = fields.Monetary( compute='_compute_amount_down_payment', string='Down Payment Amount') @@ -19,30 +19,20 @@ class SaleOrder(models.Model): compute='_compute_amount_down_payment', string='Residual') @api.depends( - 'payment_line_ids.credit', 'payment_line_ids.debit', - 'payment_line_ids.amount_currency', 'payment_line_ids.currency_id', - 'payment_line_ids.date', 'currency_id') + 'payment_ids.amount', 'payment_ids.currency_id', 'payment_ids.date', + 'payment_ids.state', 'currency_id') def _compute_amount_down_payment(self): for sale in self: down_payment = 0.0 - sale_currency = sale.pricelist_id.currency_id - if sale_currency == sale.company_id.currency_id: - for pl in sale.payment_line_ids: - down_payment -= pl.balance - else: - for pl in sale.payment_line_ids: - if ( - pl.currency_id and - pl.currency_id == sale_currency and - pl.amount_currency): - down_payment -= pl.amount_currency - else: - down_payment -= sale.company_id.currency_id._convert( - pl.balance, sale_currency, sale.company_id, - pl.date) + sale_currency = sale.currency_id + prec_rounding = sale_currency.rounding or 0.01 + for payment in sale.payment_ids: + if payment.payment_type == 'inbound' and payment.state == 'posted': + down_payment += payment.currency_id._convert( + payment.amount, sale_currency, sale.company_id, + payment.date) down_payment = float_round( - down_payment, precision_rounding=sale.currency_id.rounding) + down_payment, precision_rounding=prec_rounding) sale.amount_down_payment = down_payment sale.amount_residual = float_round( - sale.amount_total - down_payment, - precision_rounding=sale.currency_id.rounding) + sale.amount_total - down_payment, precision_rounding=prec_rounding) diff --git a/sale_down_payment/security/ir.model.access.csv b/sale_down_payment/security/ir.model.access.csv new file mode 100644 index 0000000..be1281a --- /dev/null +++ b/sale_down_payment/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_account_payment_register_sale,Full access on account.payment.register.sale,model_account_payment_register_sale,account.group_account_invoice,1,1,1,1 diff --git a/sale_down_payment/views/account_move_line.xml b/sale_down_payment/views/account_move_line.xml index e59d7d0..b827032 100644 --- a/sale_down_payment/views/account_move_line.xml +++ b/sale_down_payment/views/account_move_line.xml @@ -1,6 +1,6 @@ @@ -13,10 +13,12 @@ account.move.line - - - - + + + + + + diff --git a/sale_down_payment/views/account_payment.xml b/sale_down_payment/views/account_payment.xml index e833311..27ab858 100644 --- a/sale_down_payment/views/account_payment.xml +++ b/sale_down_payment/views/account_payment.xml @@ -1,6 +1,6 @@ @@ -8,13 +8,13 @@ - + account.payment.sale.form account.payment - + - - + + diff --git a/sale_down_payment/views/sale.xml b/sale_down_payment/views/sale.xml index bbe7448..923dd78 100644 --- a/sale_down_payment/views/sale.xml +++ b/sale_down_payment/views/sale.xml @@ -1,6 +1,6 @@ @@ -8,14 +8,6 @@ - - Register Payment - account.payment - form - - new - - advance_payment.sale.order.form sale.order @@ -23,7 +15,7 @@ - + @@ -34,7 +26,7 @@ diff --git a/sale_down_payment/wizard/__init__.py b/sale_down_payment/wizard/__init__.py index cb77527..baa027d 100644 --- a/sale_down_payment/wizard/__init__.py +++ b/sale_down_payment/wizard/__init__.py @@ -1 +1,2 @@ -from . import account_bank_statement_sale +from . import account_payment_register_sale +# from . import account_bank_statement_sale diff --git a/sale_down_payment/wizard/account_bank_statement_sale.py b/sale_down_payment/wizard/account_bank_statement_sale.py index 8d86a85..5288dd7 100644 --- a/sale_down_payment/wizard/account_bank_statement_sale.py +++ b/sale_down_payment/wizard/account_bank_statement_sale.py @@ -64,7 +64,9 @@ class AccountBankStatementSale(models.TransientModel): self.ensure_one() for line in self.line_ids: if line.move_line_id.sale_id != line.sale_id: - line.move_line_id.sale_id = line.sale_id.id + line.move_line_id.write({'sale_id': line.sale_id.id or False}) + if line.sale_id: + line.move_line_id._sale_down_payment_hook() class AccountBankStatementSaleLine(models.TransientModel): diff --git a/sale_down_payment/wizard/account_payment_register_sale.py b/sale_down_payment/wizard/account_payment_register_sale.py new file mode 100644 index 0000000..14eabdd --- /dev/null +++ b/sale_down_payment/wizard/account_payment_register_sale.py @@ -0,0 +1,62 @@ +# Copyright 2024 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class AccountPaymentRegisterSale(models.TransientModel): + _name = 'account.payment.register.sale' + _description = "Register a payment from a sale.order" + _check_company_auto = True + + sale_id = fields.Many2one( + "sale.order", string="Sale Order", + check_company=True, readonly=True, required=True) + company_id = fields.Many2one('res.company', required=True) + journal_id = fields.Many2one( + 'account.journal', string="Journal", check_company=True, required=True, + domain="[('company_id', '=', company_id), ('type', 'in', ('bank', 'cash'))]") + amount = fields.Monetary(required=True) + currency_id = fields.Many2one('res.currency', required=True) + date = fields.Date(default=fields.Date.context_today, required=True) + ref = fields.Char() + + @api.model + def default_get(self, fields_list): + res = super().default_get(fields_list) + if self._context.get('active_model') == 'sale.order' and self._context.get('active_id'): + sale = self.env['sale.order'].browse(self._context['active_id']) + res.update({ + 'sale_id': sale.id, + 'company_id': sale.company_id.id, + 'amount': sale.amount_total, + 'currency_id': sale.currency_id.id, + }) + return res + + def run(self): + self.ensure_one() + pay_vals = { + 'company_id': self.company_id.id, + 'sale_id': self.sale_id.id, + 'date': self.date, + 'amount': self.amount, + 'payment_type': 'inbound', + 'partner_type': 'customer', + 'ref': self.ref, + 'journal_id': self.journal_id.id, + 'currency_id': self.currency_id.id, + 'partner_id': self.sale_id.partner_invoice_id.commercial_partner_id.id, + 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id, + } + payment = self.env['account.payment'].create(pay_vals) + payment.action_post() + + @api.onchange("journal_id") + def journal_id_change(self): + if ( + self.journal_id and + self.journal_id.currency_id and + self.journal_id.currency_id != self.currency_id): + self.currency_id = self.journal_id.currency_id.id diff --git a/sale_down_payment/wizard/account_payment_register_sale_view.xml b/sale_down_payment/wizard/account_payment_register_sale_view.xml new file mode 100644 index 0000000..3906fd1 --- /dev/null +++ b/sale_down_payment/wizard/account_payment_register_sale_view.xml @@ -0,0 +1,43 @@ + + + + + + + + account.payment.register.sale + +
+ + + + + + +
+
+
+
+
+ + + Register Payment from Sale Order + account.payment.register.sale + form + new + + + +