From dcd8f53f26c0c12c880a90b553acb43c64be8cb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aurelija=20Vitkauskien=C4=97?= Date: Tue, 27 Jun 2023 10:27:59 +0300 Subject: [PATCH] [ADD] sale_advance_payment from https://github.com/OCA/sale-workflow/pull/2415 https://hive.versada.eu/work_packages/21072/activity --- sale_advance_payment/README.rst | 94 +++++ sale_advance_payment/__init__.py | 2 + sale_advance_payment/__manifest__.py | 19 + sale_advance_payment/i18n/lt.po | 251 ++++++++++++ sale_advance_payment/models/__init__.py | 3 + sale_advance_payment/models/account_move.py | 21 + sale_advance_payment/models/payment.py | 13 + sale_advance_payment/models/sale.py | 98 +++++ .../security/ir.model.access.csv | 4 + sale_advance_payment/tests/__init__.py | 1 + .../tests/test_sale_advance_payment.py | 378 ++++++++++++++++++ sale_advance_payment/views/sale_view.xml | 95 +++++ sale_advance_payment/wizard/__init__.py | 1 + .../wizard/sale_advance_payment_wzd.py | 162 ++++++++ .../wizard/sale_advance_payment_wzd_view.xml | 54 +++ .../odoo/addons/sale_advance_payment | 1 + setup/sale_advance_payment/setup.py | 6 + 17 files changed, 1203 insertions(+) create mode 100644 sale_advance_payment/README.rst create mode 100644 sale_advance_payment/__init__.py create mode 100644 sale_advance_payment/__manifest__.py create mode 100644 sale_advance_payment/i18n/lt.po create mode 100644 sale_advance_payment/models/__init__.py create mode 100644 sale_advance_payment/models/account_move.py create mode 100644 sale_advance_payment/models/payment.py create mode 100644 sale_advance_payment/models/sale.py create mode 100644 sale_advance_payment/security/ir.model.access.csv create mode 100644 sale_advance_payment/tests/__init__.py create mode 100644 sale_advance_payment/tests/test_sale_advance_payment.py create mode 100644 sale_advance_payment/views/sale_view.xml create mode 100644 sale_advance_payment/wizard/__init__.py create mode 100644 sale_advance_payment/wizard/sale_advance_payment_wzd.py create mode 100644 sale_advance_payment/wizard/sale_advance_payment_wzd_view.xml create mode 120000 setup/sale_advance_payment/odoo/addons/sale_advance_payment create mode 100644 setup/sale_advance_payment/setup.py diff --git a/sale_advance_payment/README.rst b/sale_advance_payment/README.rst new file mode 100644 index 00000000000..aea9cb4cc14 --- /dev/null +++ b/sale_advance_payment/README.rst @@ -0,0 +1,94 @@ +==================== +Sale Advance Payment +==================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsale--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/sale-workflow/tree/15.0/sale_advance_payment + :alt: OCA/sale-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/sale-workflow-15-0/sale-workflow-15-0-sale_advance_payment + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/167/15.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +The module allows to add advance payments on sales and then use them on invoices. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + + +To use this module, you need to: + +* Go to a sale order. +* Click on "Pay Sale Advance". +* Select the Journal and specify the amount of the advanced payment. +* "Make Advance Payment". + +When generating the invoice, the system displays the advanced payments, select those you want to add to the invoice. + +Known issues / Roadmap +====================== + +Split several computed values in separate fields (mls, advance_amount, amount_residual). +This allows a better comprehension of logic, and a better inheritance possibility. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Comunitea + +Contributors +~~~~~~~~~~~~ + +* Omar Castiñeira Saaevdra +* Daniel Reis +* Nikul Chaudhary +* Urvisha Desai + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/sale-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sale_advance_payment/__init__.py b/sale_advance_payment/__init__.py new file mode 100644 index 00000000000..9b4296142f4 --- /dev/null +++ b/sale_advance_payment/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/sale_advance_payment/__manifest__.py b/sale_advance_payment/__manifest__.py new file mode 100644 index 00000000000..124dedff9df --- /dev/null +++ b/sale_advance_payment/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2015 Omar Castiñeira, Comunitea Servicios Tecnológicos S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Sale Advance Payment", + "version": "16.0.1.0.0", + "author": "Comunitea, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/sale-workflow", + "category": "Sales", + "license": "AGPL-3", + "summary": "Allow to add advance payments on sales and then use them on invoices", + "depends": ["sale"], + "data": [ + "wizard/sale_advance_payment_wzd_view.xml", + "views/sale_view.xml", + "security/ir.model.access.csv", + ], + "installable": True, +} diff --git a/sale_advance_payment/i18n/lt.po b/sale_advance_payment/i18n/lt.po new file mode 100644 index 00000000000..4e7ddce3541 --- /dev/null +++ b/sale_advance_payment/i18n/lt.po @@ -0,0 +1,251 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_advance_payment +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-06-27 06:52+0000\n" +"PO-Revision-Date: 2023-06-27 06:52+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: sale_advance_payment +#: model:ir.model,name:sale_advance_payment.model_account_voucher_wizard +msgid "Account Voucher Wizard" +msgstr "" + +#. module: sale_advance_payment +#: model:ir.actions.act_window,name:sale_advance_payment.action_view_account_voucher_wizard +#: model_terms:ir.ui.view,arch_db:sale_advance_payment.view_account_voucher_wizard +msgid "Advance Payment" +msgstr "Avansinis mokėjimas" + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_sale_order__advance_payment_status +msgid "Advance Payment Status" +msgstr "Avansinio mokėjimo būsena" + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_account_voucher_wizard__amount_total +msgid "Amount Total" +msgstr "Bendra suma" + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_account_voucher_wizard__amount_advance +msgid "Amount advanced" +msgstr "Avansinio mokėjimo suma" + +#. module: sale_advance_payment +#: model_terms:ir.ui.view,arch_db:sale_advance_payment.view_account_voucher_wizard +msgid "Amount in Order Currency" +msgstr "Suma užsakymo valiuta" + +#. module: sale_advance_payment +#. odoo-python +#: code:addons/sale_advance_payment/wizard/sale_advance_payment_wzd.py:0 +#, python-format +msgid "Amount of advance must be positive." +msgstr "Avanso suma turi būti teigiama." + +#. module: sale_advance_payment +#: model_terms:ir.ui.view,arch_db:sale_advance_payment.view_account_voucher_wizard +msgid "Cancel" +msgstr "Atšaukti" + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_account_voucher_wizard__create_uid +msgid "Created by" +msgstr "Sukūrė" + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_account_voucher_wizard__create_date +msgid "Created on" +msgstr "Sukurta" + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_account_voucher_wizard__currency_amount +msgid "Curr. amount" +msgstr "Valiutos suma" + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_account_voucher_wizard__currency_id +#: model_terms:ir.ui.view,arch_db:sale_advance_payment.view_account_voucher_wizard +msgid "Currency" +msgstr "Valiuta" + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_account_voucher_wizard__date +msgid "Date" +msgstr "Data" + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_account_voucher_wizard__display_name +msgid "Display Name" +msgstr "Rodomas pavadinimas" + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_account_voucher_wizard__id +msgid "ID" +msgstr "" + +#. module: sale_advance_payment +#: model:ir.model.fields.selection,name:sale_advance_payment.selection__account_voucher_wizard__payment_type__inbound +msgid "Inbound" +msgstr "Įeinantys" + +#. module: sale_advance_payment +#. odoo-python +#: code:addons/sale_advance_payment/wizard/sale_advance_payment_wzd.py:0 +#, python-format +msgid "Inbound amount of advance is greater than residual amount on sale" +msgstr "Įeinančio avanso suma yra didesnė nei likutinė pardavimo suma" + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_account_voucher_wizard__journal_id +msgid "Journal" +msgstr "Žurnalas" + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_account_voucher_wizard__journal_currency_id +msgid "Journal Currency" +msgstr "Žurnalo valiuta" + +#. module: sale_advance_payment +#: model:ir.model,name:sale_advance_payment.model_account_move +msgid "Journal Entry" +msgstr "Žurnalo įrašas" + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_account_voucher_wizard____last_update +msgid "Last Modified on" +msgstr "Paskutinį kartą keista" + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_account_voucher_wizard__write_uid +msgid "Last Updated by" +msgstr "Paskutinį kartą atnaujino" + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_account_voucher_wizard__write_date +msgid "Last Updated on" +msgstr "Paskutinį kartą atnaujinta" + +#. module: sale_advance_payment +#: model_terms:ir.ui.view,arch_db:sale_advance_payment.view_account_voucher_wizard +msgid "Make advance payment" +msgstr "Užregistruoti mokėjimą" + +#. module: sale_advance_payment +#: model:ir.model.fields.selection,name:sale_advance_payment.selection__sale_order__advance_payment_status__not_paid +msgid "Not Paid" +msgstr "Neapmokėta" + +#. module: sale_advance_payment +#: model_terms:ir.ui.view,arch_db:sale_advance_payment.view_account_voucher_wizard +msgid "Operation" +msgstr "Operacija" + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_account_voucher_wizard__order_id +msgid "Order" +msgstr "Užsakymas" + +#. module: sale_advance_payment +#: model_terms:ir.ui.view,arch_db:sale_advance_payment.view_account_voucher_wizard +msgid "Order Currency" +msgstr "Užsakymo valiuta" + +#. module: sale_advance_payment +#: model_terms:ir.ui.view,arch_db:sale_advance_payment.view_account_voucher_wizard +msgid "Order Due Amount" +msgstr "Užsakymo mokėjimo suma" + +#. module: sale_advance_payment +#: model:ir.model.fields.selection,name:sale_advance_payment.selection__account_voucher_wizard__payment_type__outbound +msgid "Outbound" +msgstr "Išeinantys" + +#. module: sale_advance_payment +#. odoo-python +#: code:addons/sale_advance_payment/wizard/sale_advance_payment_wzd.py:0 +#, python-format +msgid "Outbound amount of advance is greater than the advanced paid amount" +msgstr "Išeinančio avanso suma yra didesnė nei iš anksto sumokėta suma" + +#. module: sale_advance_payment +#: model:ir.model.fields.selection,name:sale_advance_payment.selection__sale_order__advance_payment_status__paid +msgid "Paid" +msgstr "Apmokėta" + +#. module: sale_advance_payment +#: model:ir.model.fields.selection,name:sale_advance_payment.selection__sale_order__advance_payment_status__partial +msgid "Partially Paid" +msgstr "Dalinai apmokėta" + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_sale_order__account_payment_ids +#: model_terms:ir.ui.view,arch_db:sale_advance_payment.view_order_form +msgid "Pay sale advanced" +msgstr "Sukurti mokėjimą" + +#. module: sale_advance_payment +#: model_terms:ir.ui.view,arch_db:sale_advance_payment.view_account_voucher_wizard +msgid "Payment Method" +msgstr "Mokėjimo būdas" + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_account_voucher_wizard__payment_type +msgid "Payment Type" +msgstr "Mokėjimo tipas" + +#. module: sale_advance_payment +#: model_terms:ir.ui.view,arch_db:sale_advance_payment.view_order_form +msgid "Payment advances" +msgstr "Mokėjimo avansiniai mokėjimai" + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_sale_order__payment_line_ids +msgid "Payment move lines" +msgstr "Mokėjimo perkėlimo eilutės" + +#. module: sale_advance_payment +#: model:ir.model,name:sale_advance_payment.model_account_payment +msgid "Payments" +msgstr "Mokėjimai" + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_account_voucher_wizard__payment_ref +msgid "Ref." +msgstr "Nr." + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_sale_order__amount_residual +msgid "Residual amount" +msgstr "Likutis" + +#. module: sale_advance_payment +#: model:ir.model.fields,field_description:sale_advance_payment.field_account_payment__sale_id +msgid "Sale" +msgstr "Pardavimas" + +#. module: sale_advance_payment +#: model:ir.model,name:sale_advance_payment.model_sale_order +msgid "Sales Order" +msgstr "Pardavimo užsakymas" + +#. module: sale_advance_payment +#. odoo-python +#: code:addons/sale_advance_payment/wizard/sale_advance_payment_wzd.py:0 +#, python-format +msgid "" +"The amount to advance must always be positive. Please use the payment type " +"to indicate if this is an inbound or an outbound payment." +msgstr "" +"Avanso suma visada turi būti teigiama. Naudokite mokėjimo tipą, kad nurodytumėte, +" ar tai yra įeinantis, ar išeinantis mokėjimas." diff --git a/sale_advance_payment/models/__init__.py b/sale_advance_payment/models/__init__.py new file mode 100644 index 00000000000..fad33e7989b --- /dev/null +++ b/sale_advance_payment/models/__init__.py @@ -0,0 +1,3 @@ +from . import payment +from . import sale +from . import account_move diff --git a/sale_advance_payment/models/account_move.py b/sale_advance_payment/models/account_move.py new file mode 100644 index 00000000000..72d484fb6c3 --- /dev/null +++ b/sale_advance_payment/models/account_move.py @@ -0,0 +1,21 @@ +# Copyright 2022 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class AccountMove(models.Model): + _inherit = "account.move" + + def action_post(self): + # Automatic reconciliation of payment when invoice confirmed. + res = super(AccountMove, self).action_post() + sale_order = self.mapped("line_ids.sale_line_ids.order_id") + if sale_order and self.invoice_outstanding_credits_debits_widget is not False: + json_invoice_outstanding_data = ( + self.invoice_outstanding_credits_debits_widget.get("content", []) + ) + for data in json_invoice_outstanding_data: + if data.get("move_id") in sale_order.account_payment_ids.move_id.ids: + self.js_assign_outstanding_line(line_id=data.get("id")) + return res diff --git a/sale_advance_payment/models/payment.py b/sale_advance_payment/models/payment.py new file mode 100644 index 00000000000..ebda5cf1d5d --- /dev/null +++ b/sale_advance_payment/models/payment.py @@ -0,0 +1,13 @@ +# Copyright 2017 Omar Castiñeira, Comunitea Servicios Tecnológicos S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class AccountPayment(models.Model): + + _inherit = "account.payment" + + sale_id = fields.Many2one( + "sale.order", "Sale", readonly=True, states={"draft": [("readonly", False)]} + ) diff --git a/sale_advance_payment/models/sale.py b/sale_advance_payment/models/sale.py new file mode 100644 index 00000000000..001309bd873 --- /dev/null +++ b/sale_advance_payment/models/sale.py @@ -0,0 +1,98 @@ +# Copyright 2017 Omar Castiñeira, Comunitea Servicios Tecnológicos S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models +from odoo.tools import float_compare + + +class SaleOrder(models.Model): + + _inherit = "sale.order" + + account_payment_ids = fields.One2many( + "account.payment", "sale_id", string="Pay sale advanced", readonly=True + ) + amount_residual = fields.Float( + "Residual amount", + readonly=True, + compute="_compute_advance_payment", + store=True, + ) + payment_line_ids = fields.Many2many( + "account.move.line", + string="Payment move lines", + compute="_compute_advance_payment", + store=True, + ) + advance_payment_status = fields.Selection( + selection=[ + ("not_paid", "Not Paid"), + ("paid", "Paid"), + ("partial", "Partially Paid"), + ], + store=True, + readonly=True, + copy=False, + tracking=True, + compute="_compute_advance_payment", + ) + + @api.depends( + "currency_id", + "company_id", + "amount_total", + "account_payment_ids", + "account_payment_ids.state", + "account_payment_ids.move_id", + "account_payment_ids.move_id.line_ids", + "account_payment_ids.move_id.line_ids.date", + "account_payment_ids.move_id.line_ids.debit", + "account_payment_ids.move_id.line_ids.credit", + "account_payment_ids.move_id.line_ids.currency_id", + "account_payment_ids.move_id.line_ids.amount_currency", + "invoice_ids.amount_residual", + ) + def _compute_advance_payment(self): + for order in self: + mls = order.account_payment_ids.mapped("move_id.line_ids").filtered( + lambda x: x.account_id.account_type == "asset_receivable" + and x.parent_state == "posted" + ) + advance_amount = 0.0 + for line in mls: + line_currency = line.currency_id or line.company_id.currency_id + # Exclude reconciled pre-payments amount because once reconciled + # the pre-payment will reduce invoice residual amount like any + # other payment. + line_amount = ( + line.amount_residual_currency + if line.currency_id + else line.amount_residual + ) + line_amount *= -1 + if line_currency != order.currency_id: + advance_amount += line.currency_id._convert( + line_amount, + order.currency_id, + order.company_id, + line.date or fields.Date.today(), + ) + else: + advance_amount += line_amount + # Consider payments in related invoices. + invoice_paid_amount = 0.0 + for inv in order.invoice_ids: + invoice_paid_amount += inv.amount_total - inv.amount_residual + amount_residual = order.amount_total - advance_amount - invoice_paid_amount + payment_state = "not_paid" + if mls: + has_due_amount = float_compare( + amount_residual, 0.0, precision_rounding=order.currency_id.rounding + ) + if has_due_amount <= 0: + payment_state = "paid" + elif has_due_amount > 0: + payment_state = "partial" + order.payment_line_ids = mls + order.amount_residual = amount_residual + order.advance_payment_status = payment_state diff --git a/sale_advance_payment/security/ir.model.access.csv b/sale_advance_payment/security/ir.model.access.csv new file mode 100644 index 00000000000..0b97dbeee47 --- /dev/null +++ b/sale_advance_payment/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_account_payment_salesman,account.payment salesman,account.model_account_payment,sales_team.group_sale_salesman,1,1,1,0 +access_account_payment_method_salesman,account.payment.method salesman,account.model_account_payment_method,sales_team.group_sale_salesman,1,0,0,0 +access_account_voucher_wizard_salesman,access_account_voucher_wizard_salesman,model_account_voucher_wizard,sales_team.group_sale_salesman,1,1,1,0 diff --git a/sale_advance_payment/tests/__init__.py b/sale_advance_payment/tests/__init__.py new file mode 100644 index 00000000000..1d4adafb684 --- /dev/null +++ b/sale_advance_payment/tests/__init__.py @@ -0,0 +1 @@ +from . import test_sale_advance_payment diff --git a/sale_advance_payment/tests/test_sale_advance_payment.py b/sale_advance_payment/tests/test_sale_advance_payment.py new file mode 100644 index 00000000000..bb7650f75e3 --- /dev/null +++ b/sale_advance_payment/tests/test_sale_advance_payment.py @@ -0,0 +1,378 @@ +# Copyright (C) 2021 ForgeFlow S.L. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + + +from odoo import fields +from odoo.exceptions import ValidationError +from odoo.tests import common + + +class TestSaleAdvancePayment(common.TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + # Partners + cls.res_partner_1 = cls.env["res.partner"].create({"name": "Wood Corner"}) + cls.res_partner_address_1 = cls.env["res.partner"].create( + {"name": "Willie Burke", "parent_id": cls.res_partner_1.id} + ) + cls.res_partner_2 = cls.env["res.partner"].create({"name": "Partner 12"}) + + # Products + cls.product_1 = cls.env["product.product"].create( + {"name": "Desk Combination", "invoice_policy": "order"} + ) + cls.product_2 = cls.env["product.product"].create( + {"name": "Conference Chair", "invoice_policy": "order"} + ) + cls.product_3 = cls.env["product.product"].create( + {"name": "Repair Services", "invoice_policy": "order"} + ) + + cls.tax = cls.env["account.tax"].create( + { + "name": "Tax 15", + "type_tax_use": "sale", + "amount": 20, + } + ) + + # Sale Order + cls.sale_order_1 = cls.env["sale.order"].create( + {"partner_id": cls.res_partner_1.id} + ) + cls.order_line_1 = cls.env["sale.order.line"].create( + { + "order_id": cls.sale_order_1.id, + "product_id": cls.product_1.id, + "product_uom": cls.product_1.uom_id.id, + "product_uom_qty": 10.0, + "price_unit": 100.0, + "tax_id": cls.tax, + } + ) + cls.order_line_2 = cls.env["sale.order.line"].create( + { + "order_id": cls.sale_order_1.id, + "product_id": cls.product_2.id, + "product_uom": cls.product_2.uom_id.id, + "product_uom_qty": 25.0, + "price_unit": 40.0, + "tax_id": cls.tax, + } + ) + cls.order_line_3 = cls.env["sale.order.line"].create( + { + "order_id": cls.sale_order_1.id, + "product_id": cls.product_3.id, + "product_uom": cls.product_3.uom_id.id, + "product_uom_qty": 20.0, + "price_unit": 50.0, + "tax_id": cls.tax, + } + ) + + cls.active_euro = False + cls.currency_euro = ( + cls.env["res.currency"] + .with_context(active_test=False) + .search([("name", "=", "EUR")]) + ) + # active euro currency if inactive for test + if not cls.currency_euro.active: + cls.currency_euro.active = True + cls.active_euro = True + cls.currency_usd = cls.env["res.currency"].search([("name", "=", "USD")]) + cls.currency_rate = cls.env["res.currency.rate"].search( + [ + ("currency_id", "=", cls.currency_usd.id), + ("name", "=", fields.Date.today()), + ] + ) + if cls.currency_rate: + cls.currency_rate.write({"rate": 1.20}) + else: + cls.currency_rate = cls.env["res.currency.rate"].create( + { + "rate": 1.20, + "currency_id": cls.currency_usd.id, + "name": fields.Date.today(), + } + ) + + cls.journal_eur_bank = cls.env["account.journal"].create( + { + "name": "Journal Euro Bank", + "type": "bank", + "code": "111", + "currency_id": cls.currency_euro.id, + } + ) + + cls.journal_usd_bank = cls.env["account.journal"].create( + { + "name": "Journal USD Bank", + "type": "bank", + "code": "222", + "currency_id": cls.currency_usd.id, + } + ) + cls.journal_eur_cash = cls.env["account.journal"].create( + { + "name": "Journal Euro Cash", + "type": "cash", + "code": "333", + "currency_id": cls.currency_euro.id, + } + ) + + cls.journal_usd_cash = cls.env["account.journal"].create( + { + "name": "Journal USD Cash", + "type": "cash", + "code": "444", + "currency_id": cls.currency_usd.id, + } + ) + + def test_01_sale_advance_payment(self): + self.assertEqual( + self.sale_order_1.amount_residual, + 3600, + ) + self.assertEqual( + self.sale_order_1.amount_residual, + self.sale_order_1.amount_total, + "Amounts should match", + ) + + context_payment = { + "active_ids": [self.sale_order_1.id], + "active_id": self.sale_order_1.id, + } + + # Check residual > advance payment and the comparison takes + # into account the currency. 3001*1.2 > 3600 + with self.assertRaises(ValidationError): + advance_payment_0 = ( + self.env["account.voucher.wizard"] + .with_context(**context_payment) + .create( + { + "journal_id": self.journal_eur_bank.id, + "payment_type": "inbound", + "amount_advance": 3001, + "order_id": self.sale_order_1.id, + } + ) + ) + advance_payment_0.make_advance_payment() + + # Create Advance Payment 1 - EUR - bank + advance_payment_1 = ( + self.env["account.voucher.wizard"] + .with_context(**context_payment) + .create( + { + "journal_id": self.journal_eur_bank.id, + "payment_type": "inbound", + "amount_advance": 10, + "order_id": self.sale_order_1.id, + } + ) + ) + advance_payment_1.make_advance_payment() + + self.assertEqual(self.sale_order_1.amount_residual, 3588.0) + + # Create Advance Payment 2 - USD - cash + advance_payment_2 = ( + self.env["account.voucher.wizard"] + .with_context(**context_payment) + .create( + { + "journal_id": self.journal_usd_cash.id, + "payment_type": "inbound", + "amount_advance": 200, + "order_id": self.sale_order_1.id, + } + ) + ) + advance_payment_2.make_advance_payment() + + self.assertEqual(round(self.sale_order_1.amount_residual, 2), 3388.0) + + # Confirm Sale Order + self.sale_order_1.action_confirm() + + # Create Advance Payment 3 - EUR - cash + advance_payment_3 = ( + self.env["account.voucher.wizard"] + .with_context(**context_payment) + .create( + { + "journal_id": self.journal_eur_cash.id, + "payment_type": "inbound", + "amount_advance": 10, + "order_id": self.sale_order_1.id, + } + ) + ) + advance_payment_3.make_advance_payment() + self.assertEqual(self.sale_order_1.amount_residual, 3376.0) + + # Create Advance Payment 4 - USD - bank + advance_payment_4 = ( + self.env["account.voucher.wizard"] + .with_context(**context_payment) + .create( + { + "journal_id": self.journal_usd_bank.id, + "payment_type": "inbound", + "amount_advance": 400, + "order_id": self.sale_order_1.id, + } + ) + ) + advance_payment_4.make_advance_payment() + self.assertEqual(round(self.sale_order_1.amount_residual, 2), 2976.0) + + # Check that the outbound amount is not greated than the + # amount paid in advanced (in EUR) + with self.assertRaises(ValidationError): + advance_payment_5 = ( + self.env["account.voucher.wizard"] + .with_context(**context_payment) + .create( + { + "journal_id": self.journal_eur_bank.id, + "payment_type": "outbound", + "amount_advance": 850.01, + "order_id": self.sale_order_1.id, + } + ) + ) + advance_payment_5.make_advance_payment() + + # Confirm Sale Order + self.sale_order_1.action_confirm() + + # Create Invoice + invoice = self.sale_order_1._create_invoices() + invoice.action_post() + + # Compare payments + rate = self.currency_rate.rate + payment_list = [100 * rate, 200, 250 * rate, 400] + payments = invoice.invoice_outstanding_credits_debits_widget + result = [d["amount"] for d in payments["content"]] + self.assertNotEqual(set(payment_list), set(result)) + + def test_02_residual_amount_with_invoice(self): + self.assertEqual( + self.sale_order_1.amount_residual, + 3600, + ) + self.assertEqual( + self.sale_order_1.amount_residual, + self.sale_order_1.amount_total, + ) + # Create Advance Payment 1 - EUR - bank + context_payment = { + "active_ids": [self.sale_order_1.id], + "active_id": self.sale_order_1.id, + } + # Create Advance Payment 2 - USD - cash + advance_payment_2 = ( + self.env["account.voucher.wizard"] + .with_context(**context_payment) + .create( + { + "journal_id": self.journal_usd_cash.id, + "payment_type": "inbound", + "amount_advance": 200, + "order_id": self.sale_order_1.id, + } + ) + ) + advance_payment_2.make_advance_payment() + pre_payment = self.sale_order_1.account_payment_ids + self.assertEqual(len(pre_payment), 1) + self.assertEqual(self.sale_order_1.amount_residual, 3400) + # generate invoice, pay invoice, check amount residual. + self.sale_order_1.action_confirm() + self.assertEqual(self.sale_order_1.invoice_status, "to invoice") + self.sale_order_1._create_invoices() + self.assertEqual(self.sale_order_1.invoice_status, "invoiced") + self.assertEqual(self.sale_order_1.amount_residual, 3400) + invoice = self.sale_order_1.invoice_ids + invoice.invoice_date = fields.Date.today() + invoice.action_post() + active_ids = invoice.ids + self.env["account.payment.register"].with_context( + active_model="account.move", active_ids=active_ids + ).create( + { + "amount": 1200.0, + "group_payment": True, + "payment_difference_handling": "open", + } + )._create_payments() + self.assertEqual(self.sale_order_1.amount_residual, 2200.0) + + def test_03_residual_amount_big_pre_payment(self): + self.assertEqual( + self.sale_order_1.amount_residual, + 3600, + ) + self.assertEqual( + self.sale_order_1.amount_residual, + self.sale_order_1.amount_total, + ) + # Create Advance Payment 1 - EUR - bank + context_payment = { + "active_ids": [self.sale_order_1.id], + "active_id": self.sale_order_1.id, + } + # Create Advance Payment 2 - USD - cash + advance_payment_2 = ( + self.env["account.voucher.wizard"] + .with_context(**context_payment) + .create( + { + "journal_id": self.journal_usd_cash.id, + "payment_type": "inbound", + "amount_advance": 2000, + "order_id": self.sale_order_1.id, + } + ) + ) + advance_payment_2.make_advance_payment() + pre_payment = self.sale_order_1.account_payment_ids + self.assertEqual(len(pre_payment), 1) + self.assertEqual(self.sale_order_1.amount_residual, 1600) + # generate a partial invoice, reconcile with pre payment, check amount residual. + self.sale_order_1.action_confirm() + self.assertEqual(self.sale_order_1.invoice_status, "to invoice") + # Adjust invoice_policy method to then do a partial invoice with a total amount + # smaller than the pre-payment. + self.product_1.invoice_policy = "delivery" + self.order_line_1.qty_delivered = 10.0 + self.assertEqual(self.order_line_1.qty_to_invoice, 10.0) + self.product_2.invoice_policy = "delivery" + self.order_line_2.qty_delivered = 0.0 + self.assertEqual(self.order_line_2.qty_to_invoice, 0.0) + self.product_3.invoice_policy = "delivery" + self.order_line_3.qty_delivered = 0.0 + self.assertEqual(self.order_line_3.qty_to_invoice, 0.0) + self.sale_order_1._create_invoices() + self.assertEqual(self.sale_order_1.invoice_status, "no") + self.assertEqual(self.sale_order_1.amount_residual, 1600) + invoice = self.sale_order_1.invoice_ids + invoice.invoice_date = fields.Date.today() + invoice.action_post() + self.assertEqual(invoice.amount_total, 1200) + self.assertEqual(invoice.amount_residual, 0.0) + self.assertEqual(self.sale_order_1.amount_residual, 1600) + self.assertEqual(invoice.amount_residual, 0) diff --git a/sale_advance_payment/views/sale_view.xml b/sale_advance_payment/views/sale_view.xml new file mode 100644 index 00000000000..148284b4bc3 --- /dev/null +++ b/sale_advance_payment/views/sale_view.xml @@ -0,0 +1,95 @@ + + + + sale.order.form + sale.order + + + + + + + + + + + + + + + + + + + + sale.order.tree + sale.order + + + + + + + + + + + + sale.order.tree + sale.order + + + + + + + + + + + diff --git a/sale_advance_payment/wizard/__init__.py b/sale_advance_payment/wizard/__init__.py new file mode 100644 index 00000000000..06aef981fea --- /dev/null +++ b/sale_advance_payment/wizard/__init__.py @@ -0,0 +1 @@ +from . import sale_advance_payment_wzd diff --git a/sale_advance_payment/wizard/sale_advance_payment_wzd.py b/sale_advance_payment/wizard/sale_advance_payment_wzd.py new file mode 100644 index 00000000000..f9913cbe459 --- /dev/null +++ b/sale_advance_payment/wizard/sale_advance_payment_wzd.py @@ -0,0 +1,162 @@ +# Copyright 2017 Omar Castiñeira, Comunitea Servicios Tecnológicos S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import _, api, exceptions, fields, models +from odoo.exceptions import UserError +from odoo.tools import float_compare + + +class AccountVoucherWizard(models.TransientModel): + _name = "account.voucher.wizard" + _description = "Account Voucher Wizard" + + order_id = fields.Many2one("sale.order", required=True) + journal_id = fields.Many2one( + "account.journal", + "Journal", + required=True, + domain=[("type", "in", ("bank", "cash"))], + ) + journal_currency_id = fields.Many2one( + "res.currency", + "Journal Currency", + store=True, + readonly=False, + compute="_compute_get_journal_currency", + ) + currency_id = fields.Many2one("res.currency", "Currency", readonly=True) + amount_total = fields.Monetary(readonly=True) + amount_advance = fields.Monetary( + "Amount advanced", required=True, currency_field="journal_currency_id" + ) + date = fields.Date(required=True, default=fields.Date.context_today) + currency_amount = fields.Monetary( + "Curr. amount", readonly=True, currency_field="currency_id" + ) + payment_ref = fields.Char("Ref.") + payment_type = fields.Selection( + [("inbound", "Inbound"), ("outbound", "Outbound")], + default="inbound", + required=True, + ) + + @api.depends("journal_id") + def _compute_get_journal_currency(self): + for wzd in self: + wzd.journal_currency_id = ( + wzd.journal_id.currency_id.id + or wzd.journal_id.company_id.currency_id.id + ) + + @api.constrains("amount_advance") + def check_amount(self): + if self.amount_advance <= 0: + raise exceptions.ValidationError(_("Amount of advance must be positive.")) + if self.env.context.get("active_id", False): + self.onchange_date() + if self.payment_type == "inbound": + if ( + float_compare( + self.currency_amount, + self.order_id.amount_residual, + precision_digits=2, + ) + > 0 + ): + raise exceptions.ValidationError( + _( + "Inbound amount of advance is greater than residual amount on sale" + ) + ) + else: + paid_in_advanced = self.order_id.amount_total - self.amount_total + if ( + float_compare( + self.currency_amount, + paid_in_advanced, + precision_digits=2, + ) + > 0 + ): + raise exceptions.ValidationError( + _( + "Outbound amount of advance is greater than the " + "advanced paid amount" + ) + ) + + @api.model + def default_get(self, fields_list): + res = super().default_get(fields_list) + sale_ids = self.env.context.get("active_ids", []) + if not sale_ids: + return res + sale_id = fields.first(sale_ids) + sale = self.env["sale.order"].browse(sale_id) + if "amount_total" in fields_list: + res.update( + { + "order_id": sale.id, + "amount_total": sale.amount_residual, + "currency_id": sale.pricelist_id.currency_id.id, + } + ) + + return res + + @api.onchange("journal_id", "date", "amount_advance") + def onchange_date(self): + if self.journal_currency_id != self.currency_id: + amount_advance = self.journal_currency_id._convert( + self.amount_advance, + self.currency_id, + self.order_id.company_id, + self.date or fields.Date.today(), + ) + else: + amount_advance = self.amount_advance + self.currency_amount = amount_advance + + def _prepare_payment_vals(self, sale): + partner_id = sale.partner_invoice_id.commercial_partner_id.id + if self.amount_advance < 0.0: + raise UserError( + _( + "The amount to advance must always be positive. " + "Please use the payment type to indicate if this " + "is an inbound or an outbound payment." + ) + ) + + return { + "date": self.date, + "amount": self.amount_advance, + "payment_type": self.payment_type, + "partner_type": "customer", + "ref": self.payment_ref or sale.name, + "journal_id": self.journal_id.id, + "currency_id": self.journal_currency_id.id, + "partner_id": partner_id, + "payment_method_id": self.env.ref( + "account.account_payment_method_manual_in" + ).id, + } + + def make_advance_payment(self): + """Create customer paylines and validates the payment""" + self.ensure_one() + payment_obj = self.env["account.payment"] + sale_obj = self.env["sale.order"] + sale_ids = self.env.context.get("active_ids", []) + if sale_ids: + sale_id = fields.first(sale_ids) + sale = sale_obj.browse(sale_id) + payment_vals = self._prepare_payment_vals(sale) + payment = payment_obj.create(payment_vals) + sale.account_payment_ids |= payment + payment.action_post() + + return { + "type": "ir.actions.act_window_close", + } diff --git a/sale_advance_payment/wizard/sale_advance_payment_wzd_view.xml b/sale_advance_payment/wizard/sale_advance_payment_wzd_view.xml new file mode 100644 index 00000000000..38bfbcf2e7b --- /dev/null +++ b/sale_advance_payment/wizard/sale_advance_payment_wzd_view.xml @@ -0,0 +1,54 @@ + + + + Advance Payment + account.voucher.wizard + form + +
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+ + Advance Payment + ir.actions.act_window + account.voucher.wizard + form + new + +
diff --git a/setup/sale_advance_payment/odoo/addons/sale_advance_payment b/setup/sale_advance_payment/odoo/addons/sale_advance_payment new file mode 120000 index 00000000000..6723d6a64a6 --- /dev/null +++ b/setup/sale_advance_payment/odoo/addons/sale_advance_payment @@ -0,0 +1 @@ +../../../../sale_advance_payment \ No newline at end of file diff --git a/setup/sale_advance_payment/setup.py b/setup/sale_advance_payment/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/sale_advance_payment/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)