diff --git a/account_invoice_section_sale_order/models/sale_order.py b/account_invoice_section_sale_order/models/sale_order.py index b6013f0771d..ea215455284 100644 --- a/account_invoice_section_sale_order/models/sale_order.py +++ b/account_invoice_section_sale_order/models/sale_order.py @@ -16,14 +16,16 @@ def _create_invoices(self, grouped=False, final=False, date=None): the group name. Only do this for invoices targetting multiple groups """ - invoice_ids = super()._create_invoices(grouped=grouped, final=final, date=date) - for invoice in invoice_ids: - if ( + invoices = super()._create_invoices(grouped=grouped, final=final, date=date) + for invoice in invoices.sudo(): + if invoice.line_ids and ( len(invoice.line_ids.mapped(invoice.line_ids._get_section_grouping())) == 1 ): continue sequence = 10 + # Because invoices are already created, this would require + # an extra read access in order to read order fields. move_lines = invoice._get_ordered_invoice_lines() # Group move lines according to their sale order section_grouping_matrix = OrderedDict() @@ -55,19 +57,17 @@ def _create_invoices(self, grouped=False, final=False, date=None): ) ) sequence += 10 - for move_line in self.env["account.move.line"].browse(move_line_ids): - if move_line.display_type == "line_section": - # add extra indent for existing SO Sections - move_line.name = f"- {move_line.name}" + for move_line in ( + self.env["account.move.line"].sudo().browse(move_line_ids) + ): + # Because invoices are already created, this would require + # an extra write access in order to read order fields. move_line.sequence = sequence sequence += 10 + # Because invoices are already created, this would require + # an extra write access in order to read order fields. invoice.line_ids = section_lines - return invoice_ids - - def _get_ordered_invoice_lines(self, invoice): - return invoice.invoice_line_ids.sorted( - key=lambda r: r.sale_line_ids.order_id.id - ) + return invoices def _get_invoice_section_name(self): """Returns the text for the section name.""" diff --git a/account_invoice_section_sale_order/tests/__init__.py b/account_invoice_section_sale_order/tests/__init__.py index 20dcf91002a..ca02ec06eca 100644 --- a/account_invoice_section_sale_order/tests/__init__.py +++ b/account_invoice_section_sale_order/tests/__init__.py @@ -1 +1,2 @@ from . import test_invoice_group_by_sale_order +from . import test_access_rights diff --git a/account_invoice_section_sale_order/tests/common.py b/account_invoice_section_sale_order/tests/common.py new file mode 100644 index 00000000000..a8d3a372275 --- /dev/null +++ b/account_invoice_section_sale_order/tests/common.py @@ -0,0 +1,84 @@ +from odoo.tests import tagged +from odoo.tests.common import TransactionCase + + +@tagged("-at_install", "post_install") +class Common(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.setUpClassOrder() + + @classmethod + def setUpClassOrder(cls): + cls.partner_1 = cls.env.ref("base.res_partner_1") + cls.product_1 = cls.env.ref("product.product_product_1") + cls.product_2 = cls.env.ref("product.product_product_2") + cls.product_1.invoice_policy = "order" + cls.product_2.invoice_policy = "order" + cls.order1_p1 = cls.env["sale.order"].create( + { + "partner_id": cls.partner_1.id, + "partner_shipping_id": cls.partner_1.id, + "partner_invoice_id": cls.partner_1.id, + "client_order_ref": "ref123", + "order_line": [ + ( + 0, + 0, + { + "name": "order 1 line 1", + "product_id": cls.product_1.id, + "price_unit": 20, + "product_uom_qty": 1, + "product_uom": cls.product_1.uom_id.id, + }, + ), + ( + 0, + 0, + { + "name": "order 1 line 2", + "product_id": cls.product_2.id, + "price_unit": 20, + "product_uom_qty": 1, + "product_uom": cls.product_1.uom_id.id, + }, + ), + ], + } + ) + cls.order1_p1.action_confirm() + cls.order2_p1 = cls.env["sale.order"].create( + { + "partner_id": cls.partner_1.id, + "partner_shipping_id": cls.partner_1.id, + "partner_invoice_id": cls.partner_1.id, + "order_line": [ + ( + 0, + 0, + { + "name": "order 2 line 1", + "product_id": cls.product_1.id, + "price_unit": 20, + "product_uom_qty": 1, + "product_uom": cls.product_1.uom_id.id, + }, + ), + ( + 0, + 0, + { + "name": "order 2 line 2", + "product_id": cls.product_2.id, + "price_unit": 20, + "product_uom_qty": 1, + "product_uom": cls.product_1.uom_id.id, + }, + ), + ], + } + ) + cls.order2_p1.action_confirm() diff --git a/account_invoice_section_sale_order/tests/test_access_rights.py b/account_invoice_section_sale_order/tests/test_access_rights.py new file mode 100644 index 00000000000..f1afd39d1f1 --- /dev/null +++ b/account_invoice_section_sale_order/tests/test_access_rights.py @@ -0,0 +1,60 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.tests import tagged + +from .common import Common + + +@tagged("-at_install", "post_install") +class TestAccessRights(Common): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.setUpClassUser() + + @classmethod + def setUpClassUser(cls): + cls.create_only_group = cls.env["res.groups"].create( + {"name": "Create Only Group"} + ) + cls.sale_manager_group = cls.env.ref("sales_team.group_sale_manager") + cls.env["ir.model.access"].create( + [ + { + "name": "invoice_create_only", + "model_id": cls.env.ref("account.model_account_move").id, + "group_id": cls.create_only_group.id, + "perm_read": 0, + "perm_write": 0, + "perm_create": 1, + "perm_unlink": 0, + }, + { + "name": "invoice_line_create_only", + "model_id": cls.env.ref("account.model_account_move_line").id, + "group_id": cls.create_only_group.id, + "perm_read": 0, + "perm_write": 0, + "perm_create": 1, + "perm_unlink": 0, + }, + ] + ) + cls.create_only_user = cls.env["res.users"].create( + { + "name": "Create Only User", + "login": "createonlyuser@example.com", + "groups_id": [ + (6, 0, (cls.create_only_group | cls.sale_manager_group).ids), + ], + } + ) + + def test_access_rights(self): + orders = self.order1_p1 + self.order2_p1 + # We're testing that no exception is raised while creating invoices + # with a user having only create access on the invoices models + invoice_ids = orders.with_user(self.create_only_user)._create_invoices() + self.assertTrue(bool(invoice_ids)) diff --git a/account_invoice_section_sale_order/tests/test_invoice_group_by_sale_order.py b/account_invoice_section_sale_order/tests/test_invoice_group_by_sale_order.py index 872d5a22337..2a3369cdb60 100644 --- a/account_invoice_section_sale_order/tests/test_invoice_group_by_sale_order.py +++ b/account_invoice_section_sale_order/tests/test_invoice_group_by_sale_order.py @@ -3,7 +3,8 @@ from unittest import mock from odoo.exceptions import UserError -from odoo.tests.common import TransactionCase + +from .common import Common SECTION_GROUPING_FUNCTION = "odoo.addons.account_invoice_section_sale_order.models.account_move.AccountMoveLine._get_section_grouping" # noqa SECTION_NAME_FUNCTION = ( @@ -11,103 +12,7 @@ ) -class TestInvoiceGroupBySaleOrder(TransactionCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.partner_1 = cls.env.ref("base.res_partner_1") - cls.product_1 = cls.env.ref("product.product_product_1") - cls.product_2 = cls.env.ref("product.product_product_2") - cls.product_1.invoice_policy = "order" - cls.product_2.invoice_policy = "order" - eur = cls.env.ref("base.EUR") - cls.pricelist = cls.env["product.pricelist"].create( - {"name": "Europe pricelist", "currency_id": eur.id} - ) - cls.order1_p1 = cls.env["sale.order"].create( - { - "partner_id": cls.partner_1.id, - "partner_shipping_id": cls.partner_1.id, - "partner_invoice_id": cls.partner_1.id, - "pricelist_id": cls.pricelist.id, - "client_order_ref": "ref123", - "order_line": [ - ( - 0, - 0, - { - "name": "order 1 line 1", - "product_id": cls.product_1.id, - "price_unit": 20, - "product_uom_qty": 1, - "product_uom": cls.product_1.uom_id.id, - }, - ), - ( - 0, - 0, - { - "name": "order 1 line 2", - "product_id": cls.product_2.id, - "price_unit": 20, - "product_uom_qty": 1, - "product_uom": cls.product_1.uom_id.id, - }, - ), - ], - } - ) - cls.order1_p1.action_confirm() - cls.order2_p1 = cls.env["sale.order"].create( - { - "partner_id": cls.partner_1.id, - "partner_shipping_id": cls.partner_1.id, - "partner_invoice_id": cls.partner_1.id, - "pricelist_id": cls.pricelist.id, - "order_line": [ - ( - 0, - 0, - { - "name": "order 2 section 1", - "display_type": "line_section", - }, - ), - ( - 0, - 0, - { - "name": "order 2 line 1", - "product_id": cls.product_1.id, - "price_unit": 20, - "product_uom_qty": 1, - "product_uom": cls.product_1.uom_id.id, - }, - ), - ( - 0, - 0, - { - "name": "order 2 section 2", - "display_type": "line_section", - }, - ), - ( - 0, - 0, - { - "name": "order 2 line 2", - "product_id": cls.product_2.id, - "price_unit": 20, - "product_uom_qty": 1, - "product_uom": cls.product_1.uom_id.id, - }, - ), - ], - } - ) - cls.order2_p1.action_confirm() - +class TestInvoiceGroupBySaleOrder(Common): def test_create_invoice(self): """Check invoice is generated with sale order sections.""" result = { @@ -118,10 +23,8 @@ def test_create_invoice(self): 20: ("order 1 line 1", "product"), 30: ("order 1 line 2", "product"), 40: (self.order2_p1.name, "line_section"), - 50: ("- order 2 section 1", "line_section"), - 60: ("order 2 line 1", "product"), - 70: ("- order 2 section 2", "line_section"), - 80: ("order 2 line 2", "product"), + 50: ("order 2 line 1", "product"), + 60: ("order 2 line 2", "product"), } invoice_ids = (self.order1_p1 + self.order2_p1)._create_invoices() lines = invoice_ids[0].invoice_line_ids.sorted("sequence") @@ -135,7 +38,7 @@ def test_create_invoice_with_currency(self): """Check invoice is generated with a correct total amount""" orders = self.order1_p1 | self.order2_p1 invoices = orders._create_invoices() - self.assertEqual(invoices.amount_total, 80) + self.assertEqual(invoices.amount_untaxed, 80) def test_create_invoice_with_default_journal(self): """Using a specific journal for the invoice should not be broken""" @@ -197,13 +100,11 @@ def test_custom_grouping_by_sale_order_user(self): 10: ("Mocked value from ResUsers", "line_section"), 20: ("order 1 line 1", "product"), 30: ("order 1 line 2", "product"), - 40: ("- order 2 section 1", "line_section"), - 50: ("order 2 line 1", "product"), - 60: ("- order 2 section 2", "line_section"), - 70: ("order 2 line 2", "product"), - 80: ("Mocked value from ResUsers", "line_section"), - 90: ("order 3 line 1", "product"), - 100: ("order 3 line 2", "product"), + 40: ("order 2 line 1", "product"), + 50: ("order 2 line 2", "product"), + 60: ("Mocked value from ResUsers", "line_section"), + 70: ("order 3 line 1", "product"), + 80: ("order 3 line 2", "product"), } for line in invoice.invoice_line_ids.sorted("sequence"): if line.sequence not in result: diff --git a/partner_invoicing_mode_at_shipping/README.rst b/partner_invoicing_mode_at_shipping/README.rst new file mode 100644 index 00000000000..119dcd6f845 --- /dev/null +++ b/partner_invoicing_mode_at_shipping/README.rst @@ -0,0 +1,99 @@ +================================== +Partner Invoicing Mode At Shipping +================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:6f96589039ddad82b6acdcd148f04264085935d83c2fc147507bf3ab3ae41c1b + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Faccount--invoicing-lightgray.png?logo=github + :target: https://github.com/OCA/account-invoicing/tree/17.0/partner_invoicing_mode_at_shipping + :alt: OCA/account-invoicing +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/account-invoicing-17-0/account-invoicing-17-0-partner_invoicing_mode_at_shipping + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/account-invoicing&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to select a At shipping invoicing mode for a +customer. It is based on partner_invoicing_mode. When this mode is +selected the customer will be invoiced automatically on delivery of the +goods. + +Another option is the 'One Invoice Per Shipping'. That one is not +compatible with the 'At shipping' invoicing mode. In that case, the +invoicing validation will occur at a different moment (monthly, ...). + +**Table of contents** + +.. contents:: + :local: + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Camptocamp + +Contributors +------------ + +- `Camptocamp `__: + + - Thierry Ducrest + +- Phuc (Tran Thanh) + +- Nils Coenen + +- Chau Le + +Other credits +------------- + +The development and migration of this module has been financially +supported by: + +- Camptocamp + +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/account-invoicing `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/partner_invoicing_mode_at_shipping/__init__.py b/partner_invoicing_mode_at_shipping/__init__.py new file mode 100644 index 00000000000..6d58305f5dd --- /dev/null +++ b/partner_invoicing_mode_at_shipping/__init__.py @@ -0,0 +1,2 @@ +from . import models +from .hooks import pre_init_hook diff --git a/partner_invoicing_mode_at_shipping/__manifest__.py b/partner_invoicing_mode_at_shipping/__manifest__.py new file mode 100644 index 00000000000..cfb64ef27fd --- /dev/null +++ b/partner_invoicing_mode_at_shipping/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2020 Camptocamp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Partner Invoicing Mode At Shipping", + "version": "17.0.1.0.0", + "summary": "Create invoices automatically when goods are shipped.", + "author": "Camptocamp, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/account-invoicing", + "license": "AGPL-3", + "category": "Accounting & Finance", + "data": [ + "data/queue_job_data.xml", + "views/res_partner.xml", + ], + "depends": ["account", "partner_invoicing_mode", "queue_job", "stock"], + "external_dependencies": { + "python": ["openupgradelib"], + }, + "pre_init_hook": "pre_init_hook", +} diff --git a/partner_invoicing_mode_at_shipping/data/queue_job_data.xml b/partner_invoicing_mode_at_shipping/data/queue_job_data.xml new file mode 100644 index 00000000000..b5f70b53f5c --- /dev/null +++ b/partner_invoicing_mode_at_shipping/data/queue_job_data.xml @@ -0,0 +1,15 @@ + + + + + invoice_at_shipping + + + + + + + _invoicing_at_shipping + + + diff --git a/partner_invoicing_mode_at_shipping/hooks.py b/partner_invoicing_mode_at_shipping/hooks.py new file mode 100644 index 00000000000..e6a69d915ed --- /dev/null +++ b/partner_invoicing_mode_at_shipping/hooks.py @@ -0,0 +1,23 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from openupgradelib import openupgrade + + +def _add_one_invoice_per_shipping(env): + if not openupgrade.column_exists(env.cr, "sale_order", "one_invoice_per_shipping"): + field_spec = [ + ( + "one_invoice_per_shipping", + "sale.order", + "sale_order", + "boolean", + "boolean", + "partner_invoicing_mode_at_shipping", + False, + ) + ] + openupgrade.add_fields(env, field_spec) + + +def pre_init_hook(env): + _add_one_invoice_per_shipping(env) diff --git a/partner_invoicing_mode_at_shipping/i18n/es.po b/partner_invoicing_mode_at_shipping/i18n/es.po new file mode 100644 index 00000000000..61af664fb58 --- /dev/null +++ b/partner_invoicing_mode_at_shipping/i18n/es.po @@ -0,0 +1,97 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * partner_invoicing_mode_at_shipping +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-06-29 10:47+0000\n" +"Last-Translator: Anna Martínez \n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model.fields.selection,name:partner_invoicing_mode_at_shipping.selection__res_partner__invoicing_mode__at_shipping +msgid "At Shipping" +msgstr "En envío" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model.fields,help:partner_invoicing_mode_at_shipping.field_res_partner__one_invoice_per_shipping +#: model:ir.model.fields,help:partner_invoicing_mode_at_shipping.field_res_users__one_invoice_per_shipping +msgid "" +"Check this if you want to create one invoice per shipping using the partner " +"invoicing mode that should be different than 'At Shipping'." +msgstr "" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model,name:partner_invoicing_mode_at_shipping.model_res_partner +msgid "Contact" +msgstr "Contacto" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model.fields,field_description:partner_invoicing_mode_at_shipping.field_res_partner__invoicing_mode +#: model:ir.model.fields,field_description:partner_invoicing_mode_at_shipping.field_res_users__invoicing_mode +msgid "Invoicing Mode" +msgstr "Modo de facturación" + +#. module: partner_invoicing_mode_at_shipping +#. odoo-python +#: code:addons/partner_invoicing_mode_at_shipping/models/stock_picking.py:0 +#, python-format +msgid "Nothing to invoice." +msgstr "Nada para Facturar." + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model.fields,field_description:partner_invoicing_mode_at_shipping.field_res_partner__one_invoice_per_shipping +#: model:ir.model.fields,field_description:partner_invoicing_mode_at_shipping.field_res_users__one_invoice_per_shipping +#: model:ir.model.fields,field_description:partner_invoicing_mode_at_shipping.field_sale_order__one_invoice_per_shipping +msgid "One Invoice Per Shipping" +msgstr "" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model,name:partner_invoicing_mode_at_shipping.model_sale_order +msgid "Sales Order" +msgstr "Pedido de venta" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model,name:partner_invoicing_mode_at_shipping.model_stock_move +msgid "Stock Move" +msgstr "Movimiento de existencias" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model,name:partner_invoicing_mode_at_shipping.model_stock_picking +msgid "Transfer" +msgstr "Transferencia" + +#. module: partner_invoicing_mode_at_shipping +#. odoo-python +#: code:addons/partner_invoicing_mode_at_shipping/models/sale_order.py:0 +#, python-format +msgid "" +"Validate the invoices generated by shipping for the invoicing mode " +"%(invoicing_mode_name)s" +msgstr "" + +#. module: partner_invoicing_mode_at_shipping +#. odoo-python +#: code:addons/partner_invoicing_mode_at_shipping/models/res_partner.py:0 +#, python-format +msgid "" +"You cannot configure the partner %(partner)s with 'One Invoice Per Order' " +"and 'One Invoice Per Shipping'!" +msgstr "" + +#. module: partner_invoicing_mode_at_shipping +#. odoo-python +#: code:addons/partner_invoicing_mode_at_shipping/models/res_partner.py:0 +#, python-format +msgid "" +"You cannot configure the partner %(partner)s with Invoicing Mode 'At " +"Shipping' and 'One Invoice Per Shipping'!" +msgstr "" diff --git a/partner_invoicing_mode_at_shipping/i18n/fr.po b/partner_invoicing_mode_at_shipping/i18n/fr.po new file mode 100644 index 00000000000..a0cca2a2d9c --- /dev/null +++ b/partner_invoicing_mode_at_shipping/i18n/fr.po @@ -0,0 +1,106 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * partner_invoicing_mode_at_shipping +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-11-06 14:06+0000\n" +"Last-Translator: samibc2c \n" +"Language-Team: none\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 5.6.2\n" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model.fields.selection,name:partner_invoicing_mode_at_shipping.selection__res_partner__invoicing_mode__at_shipping +msgid "At Shipping" +msgstr "A l'expédition" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model.fields,help:partner_invoicing_mode_at_shipping.field_res_partner__one_invoice_per_shipping +#: model:ir.model.fields,help:partner_invoicing_mode_at_shipping.field_res_users__one_invoice_per_shipping +msgid "" +"Check this if you want to create one invoice per shipping using the partner " +"invoicing mode that should be different than 'At Shipping'." +msgstr "" +"Cochez si vous souhaitez créer une facture par expédition en utilisant le " +"mode de facturation du partenaire, qui doit être différent de 'A " +"l'expédition'." + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model,name:partner_invoicing_mode_at_shipping.model_res_partner +msgid "Contact" +msgstr "Contact" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model.fields,field_description:partner_invoicing_mode_at_shipping.field_res_partner__invoicing_mode +#: model:ir.model.fields,field_description:partner_invoicing_mode_at_shipping.field_res_users__invoicing_mode +msgid "Invoicing Mode" +msgstr "Mode de facturation" + +#. module: partner_invoicing_mode_at_shipping +#. odoo-python +#: code:addons/partner_invoicing_mode_at_shipping/models/stock_picking.py:0 +#, python-format +msgid "Nothing to invoice." +msgstr "Rien à facturer." + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model.fields,field_description:partner_invoicing_mode_at_shipping.field_res_partner__one_invoice_per_shipping +#: model:ir.model.fields,field_description:partner_invoicing_mode_at_shipping.field_res_users__one_invoice_per_shipping +#: model:ir.model.fields,field_description:partner_invoicing_mode_at_shipping.field_sale_order__one_invoice_per_shipping +msgid "One Invoice Per Shipping" +msgstr "Une facture par expédition" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model,name:partner_invoicing_mode_at_shipping.model_sale_order +msgid "Sales Order" +msgstr "Bon de commande" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model,name:partner_invoicing_mode_at_shipping.model_stock_move +msgid "Stock Move" +msgstr "Mouvement de stock" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model,name:partner_invoicing_mode_at_shipping.model_stock_picking +msgid "Transfer" +msgstr "Transfert" + +#. module: partner_invoicing_mode_at_shipping +#. odoo-python +#: code:addons/partner_invoicing_mode_at_shipping/models/sale_order.py:0 +#, python-format +msgid "" +"Validate the invoices generated by shipping for the invoicing mode " +"%(invoicing_mode_name)s" +msgstr "" +"Valider les factures générées par l'expédition pour le mode de facturation " +"%(invoicing_mode_name)s" + +#. module: partner_invoicing_mode_at_shipping +#. odoo-python +#: code:addons/partner_invoicing_mode_at_shipping/models/res_partner.py:0 +#, python-format +msgid "" +"You cannot configure the partner %(partner)s with 'One Invoice Per Order' " +"and 'One Invoice Per Shipping'!" +msgstr "" +"Vous ne pouvez pas configurer le partenaire %(partner)s with 'Une facture " +"par commande' et 'Une facture par expédition' !" + +#. module: partner_invoicing_mode_at_shipping +#. odoo-python +#: code:addons/partner_invoicing_mode_at_shipping/models/res_partner.py:0 +#, python-format +msgid "" +"You cannot configure the partner %(partner)s with Invoicing Mode 'At " +"Shipping' and 'One Invoice Per Shipping'!" +msgstr "" +"Vous ne pouvez pas configurer le partenaire %(partner)s avec le mode de " +"facturation 'à l'expédition' et 'Une facture par expédition' !" diff --git a/partner_invoicing_mode_at_shipping/i18n/it.po b/partner_invoicing_mode_at_shipping/i18n/it.po new file mode 100644 index 00000000000..5082725eec0 --- /dev/null +++ b/partner_invoicing_mode_at_shipping/i18n/it.po @@ -0,0 +1,106 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * partner_invoicing_mode_at_shipping +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-07-15 07:37+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.6.2\n" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model.fields.selection,name:partner_invoicing_mode_at_shipping.selection__res_partner__invoicing_mode__at_shipping +msgid "At Shipping" +msgstr "Alla spedizione" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model.fields,help:partner_invoicing_mode_at_shipping.field_res_partner__one_invoice_per_shipping +#: model:ir.model.fields,help:partner_invoicing_mode_at_shipping.field_res_users__one_invoice_per_shipping +msgid "" +"Check this if you want to create one invoice per shipping using the partner " +"invoicing mode that should be different than 'At Shipping'." +msgstr "" +"Selezionare questa opzione se si vuole creare una fattura per spedizione " +"utilizzando il modo di fatturazione del partner che deve essere diverso da " +"'Alla spedizione'." + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model,name:partner_invoicing_mode_at_shipping.model_res_partner +msgid "Contact" +msgstr "Contatto" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model.fields,field_description:partner_invoicing_mode_at_shipping.field_res_partner__invoicing_mode +#: model:ir.model.fields,field_description:partner_invoicing_mode_at_shipping.field_res_users__invoicing_mode +msgid "Invoicing Mode" +msgstr "Modo fatturazione" + +#. module: partner_invoicing_mode_at_shipping +#. odoo-python +#: code:addons/partner_invoicing_mode_at_shipping/models/stock_picking.py:0 +#, python-format +msgid "Nothing to invoice." +msgstr "Nulla da fatturare." + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model.fields,field_description:partner_invoicing_mode_at_shipping.field_res_partner__one_invoice_per_shipping +#: model:ir.model.fields,field_description:partner_invoicing_mode_at_shipping.field_res_users__one_invoice_per_shipping +#: model:ir.model.fields,field_description:partner_invoicing_mode_at_shipping.field_sale_order__one_invoice_per_shipping +msgid "One Invoice Per Shipping" +msgstr "Una fattura per spedizione" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model,name:partner_invoicing_mode_at_shipping.model_sale_order +msgid "Sales Order" +msgstr "Ordine di vendita" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model,name:partner_invoicing_mode_at_shipping.model_stock_move +msgid "Stock Move" +msgstr "Movimento di magazzino" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model,name:partner_invoicing_mode_at_shipping.model_stock_picking +msgid "Transfer" +msgstr "Trasferimento" + +#. module: partner_invoicing_mode_at_shipping +#. odoo-python +#: code:addons/partner_invoicing_mode_at_shipping/models/sale_order.py:0 +#, python-format +msgid "" +"Validate the invoices generated by shipping for the invoicing mode " +"%(invoicing_mode_name)s" +msgstr "" +"Validare la fattura generata dalla spedizione per il metodo di fatturazione " +"%(invoicing_mode_name)s" + +#. module: partner_invoicing_mode_at_shipping +#. odoo-python +#: code:addons/partner_invoicing_mode_at_shipping/models/res_partner.py:0 +#, python-format +msgid "" +"You cannot configure the partner %(partner)s with 'One Invoice Per Order' " +"and 'One Invoice Per Shipping'!" +msgstr "" +"Non si può configurare il partner %(partner)s con 'Una fattura per ordine' e " +"'Una fattura per spedizione'!" + +#. module: partner_invoicing_mode_at_shipping +#. odoo-python +#: code:addons/partner_invoicing_mode_at_shipping/models/res_partner.py:0 +#, python-format +msgid "" +"You cannot configure the partner %(partner)s with Invoicing Mode 'At " +"Shipping' and 'One Invoice Per Shipping'!" +msgstr "" +"Non si può configurare il partner %(partner)s con il metodo di fatturazione " +"'Alla spedizione' e 'Una fattura per spedizione'!" diff --git a/partner_invoicing_mode_at_shipping/i18n/partner_invoicing_mode_at_shipping.pot b/partner_invoicing_mode_at_shipping/i18n/partner_invoicing_mode_at_shipping.pot new file mode 100644 index 00000000000..801cb53f05f --- /dev/null +++ b/partner_invoicing_mode_at_shipping/i18n/partner_invoicing_mode_at_shipping.pot @@ -0,0 +1,94 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * partner_invoicing_mode_at_shipping +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \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: partner_invoicing_mode_at_shipping +#: model:ir.model.fields.selection,name:partner_invoicing_mode_at_shipping.selection__res_partner__invoicing_mode__at_shipping +msgid "At Shipping" +msgstr "" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model.fields,help:partner_invoicing_mode_at_shipping.field_res_partner__one_invoice_per_shipping +#: model:ir.model.fields,help:partner_invoicing_mode_at_shipping.field_res_users__one_invoice_per_shipping +msgid "" +"Check this if you want to create one invoice per shipping using the partner " +"invoicing mode that should be different than 'At Shipping'." +msgstr "" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model,name:partner_invoicing_mode_at_shipping.model_res_partner +msgid "Contact" +msgstr "" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model.fields,field_description:partner_invoicing_mode_at_shipping.field_res_partner__invoicing_mode +#: model:ir.model.fields,field_description:partner_invoicing_mode_at_shipping.field_res_users__invoicing_mode +msgid "Invoicing Mode" +msgstr "" + +#. module: partner_invoicing_mode_at_shipping +#. odoo-python +#: code:addons/partner_invoicing_mode_at_shipping/models/stock_picking.py:0 +#, python-format +msgid "Nothing to invoice." +msgstr "" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model.fields,field_description:partner_invoicing_mode_at_shipping.field_res_partner__one_invoice_per_shipping +#: model:ir.model.fields,field_description:partner_invoicing_mode_at_shipping.field_res_users__one_invoice_per_shipping +#: model:ir.model.fields,field_description:partner_invoicing_mode_at_shipping.field_sale_order__one_invoice_per_shipping +msgid "One Invoice Per Shipping" +msgstr "" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model,name:partner_invoicing_mode_at_shipping.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model,name:partner_invoicing_mode_at_shipping.model_stock_move +msgid "Stock Move" +msgstr "" + +#. module: partner_invoicing_mode_at_shipping +#: model:ir.model,name:partner_invoicing_mode_at_shipping.model_stock_picking +msgid "Transfer" +msgstr "" + +#. module: partner_invoicing_mode_at_shipping +#. odoo-python +#: code:addons/partner_invoicing_mode_at_shipping/models/sale_order.py:0 +#, python-format +msgid "" +"Validate the invoices generated by shipping for the invoicing mode " +"%(invoicing_mode_name)s" +msgstr "" + +#. module: partner_invoicing_mode_at_shipping +#. odoo-python +#: code:addons/partner_invoicing_mode_at_shipping/models/res_partner.py:0 +#, python-format +msgid "" +"You cannot configure the partner %(partner)s with 'One Invoice Per Order' " +"and 'One Invoice Per Shipping'!" +msgstr "" + +#. module: partner_invoicing_mode_at_shipping +#. odoo-python +#: code:addons/partner_invoicing_mode_at_shipping/models/res_partner.py:0 +#, python-format +msgid "" +"You cannot configure the partner %(partner)s with Invoicing Mode 'At " +"Shipping' and 'One Invoice Per Shipping'!" +msgstr "" diff --git a/partner_invoicing_mode_at_shipping/models/__init__.py b/partner_invoicing_mode_at_shipping/models/__init__.py new file mode 100644 index 00000000000..ddbf4e06b0d --- /dev/null +++ b/partner_invoicing_mode_at_shipping/models/__init__.py @@ -0,0 +1,3 @@ +from . import res_partner +from . import stock_picking +from . import sale_order diff --git a/partner_invoicing_mode_at_shipping/models/res_partner.py b/partner_invoicing_mode_at_shipping/models/res_partner.py new file mode 100644 index 00000000000..7c3fa457544 --- /dev/null +++ b/partner_invoicing_mode_at_shipping/models/res_partner.py @@ -0,0 +1,51 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class ResPartner(models.Model): + _inherit = "res.partner" + + invoicing_mode = fields.Selection( + selection_add=[("at_shipping", "At Shipping")], + ondelete={"at_shipping": "set default"}, + ) + + one_invoice_per_shipping = fields.Boolean( + index=True, + help="Check this if you want to create one invoice per shipping using the" + " partner invoicing mode that should be different than 'At Shipping'.", + ) + + @api.model + def _commercial_fields(self): + return super()._commercial_fields() + [ + "one_invoice_per_shipping", + ] + + @api.constrains( + "invoicing_mode", "one_invoice_per_shipping", "one_invoice_per_order" + ) + def _check_invoicing_mode_one_invoice_per_shipping(self): + for partner in self: + if ( + partner.invoicing_mode == "at_shipping" + and partner.one_invoice_per_shipping + ): + raise ValidationError( + _( + "You cannot configure the partner %(partner)s with " + "Invoicing Mode 'At Shipping' and 'One Invoice Per Shipping'!", + partner=partner.name, + ), + ) + if partner.one_invoice_per_shipping and partner.one_invoice_per_order: + raise ValidationError( + _( + "You cannot configure the partner %(partner)s with " + "'One Invoice Per Order' and 'One Invoice Per Shipping'!", + partner=partner.name, + ), + ) diff --git a/partner_invoicing_mode_at_shipping/models/sale_order.py b/partner_invoicing_mode_at_shipping/models/sale_order.py new file mode 100644 index 00000000000..4b1093cf310 --- /dev/null +++ b/partner_invoicing_mode_at_shipping/models/sale_order.py @@ -0,0 +1,92 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import _, api, fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + one_invoice_per_shipping = fields.Boolean( + compute="_compute_one_invoice_per_shipping", + store=True, + index=True, + ) + + @api.depends("partner_invoice_id") + def _compute_one_invoice_per_shipping(self): + """ + Compute this field (instead a related) to avoid computing all + related sale orders if option changed on partner level. + """ + for order in self: + order.one_invoice_per_shipping = ( + order.partner_invoice_id.one_invoice_per_shipping + ) + + def generate_invoices( + self, + companies=None, + invoicing_mode="standard", + last_execution_field="invoicing_mode_standard_last_execution", + ): + saleorders = super().generate_invoices( + companies=companies, + invoicing_mode=invoicing_mode, + last_execution_field=last_execution_field, + ) + # Validate the preceding generated invoices in draft mode. + description = _( + "Validate the invoices generated by shipping for the " + "invoicing mode %(invoicing_mode_name)s" + ) + self.with_delay( + description=description + )._validate_per_shipping_generated_invoices( + companies=companies, invoicing_mode=invoicing_mode + ) + + return saleorders + + @api.model + def _validate_per_shipping_generated_invoices( + self, companies=None, invoicing_mode="standard" + ) -> str: + """ + This will validate all draft invoices that have been generated. + + :param companies: _description_, defaults to None + :type companies: _type_, optional + :param invoicing_mode: _description_, defaults to "standard" + :type invoicing_mode: str, optional + :return: String result for queue job + :rtype: AccountMove + """ + if companies is None: + companies = self.env.company + invoices = self.env["account.move"].search( + self._get_per_shipping_to_validate_invoices_domain( + companies=companies, invoicing_mode=invoicing_mode + ) + ) + for invoice in invoices: + invoice.with_delay()._validate_invoice() + for partner, __invoices in invoices.partition("partner_id").items(): + partner._update_next_invoice_date() + return ",".join(invoices.mapped("display_name")) + + def _get_per_shipping_to_validate_invoices_domain( + self, companies, invoicing_mode="standard" + ) -> list: + """ + This will return the domain for invoices that should be posted. + + :return: Domain + :rtype: list + """ + return [ + ("company_id", "in", companies.ids), + ("move_type", "in", ("out_invoice", "out_refund")), + ("state", "=", "draft"), + ("partner_id.one_invoice_per_shipping", "=", True), + ("partner_id.invoicing_mode", "=", invoicing_mode), + ] diff --git a/partner_invoicing_mode_at_shipping/models/stock_picking.py b/partner_invoicing_mode_at_shipping/models/stock_picking.py new file mode 100644 index 00000000000..ae4d5208967 --- /dev/null +++ b/partner_invoicing_mode_at_shipping/models/stock_picking.py @@ -0,0 +1,52 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from odoo import _, api, models + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + def _action_done(self): + res = super()._action_done() + for picking in self: + if picking._invoice_at_shipping(): + picking.with_delay()._invoicing_at_shipping() + return res + + def _invoice_at_shipping(self): + """Check if picking must be invoiced at shipping.""" + self.ensure_one() + return self.picking_type_code == "outgoing" and ( + self.sale_id.partner_invoice_id.invoicing_mode == "at_shipping" + or self.sale_id.partner_invoice_id.one_invoice_per_shipping + ) + + def _invoicing_at_shipping_validation(self, invoices): + return invoices.filtered( + lambda invoice: invoice.partner_id.invoicing_mode == "at_shipping" + ) + + @api.model + def _invoicing_at_shipping(self): + self.ensure_one() + sales = self._get_sales_order_to_invoice() + # Split invoice creation on partner sales grouping on invoice settings + sales_one_invoice_per_order = sales.filtered( + "partner_invoice_id.one_invoice_per_order" + ) + invoices = self.env["account.move"] + if sales_one_invoice_per_order: + invoices |= sales_one_invoice_per_order._create_invoices(grouped=True) + sales_many_invoice_per_order = sales - sales_one_invoice_per_order + if sales_many_invoice_per_order: + invoices |= sales_many_invoice_per_order._create_invoices(grouped=False) + # The invoices per picking will use the invoicing_mode + for invoice in self._invoicing_at_shipping_validation(invoices): + invoice.with_delay()._validate_invoice() + return invoices or _("Nothing to invoice.") + + def _get_sales_order_to_invoice(self): + return self.move_ids.sale_line_id.order_id.filtered( + lambda r: r._get_invoiceable_lines() + ) diff --git a/partner_invoicing_mode_at_shipping/pyproject.toml b/partner_invoicing_mode_at_shipping/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/partner_invoicing_mode_at_shipping/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/partner_invoicing_mode_at_shipping/readme/CONTRIBUTORS.md b/partner_invoicing_mode_at_shipping/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..fd3082e3509 --- /dev/null +++ b/partner_invoicing_mode_at_shipping/readme/CONTRIBUTORS.md @@ -0,0 +1,7 @@ +- [Camptocamp](https://www.camptocamp.com): + + > - Thierry Ducrest \<\> + +- Phuc (Tran Thanh) \<\> +- Nils Coenen \<\> +- Chau Le \<\> diff --git a/partner_invoicing_mode_at_shipping/readme/CREDITS.md b/partner_invoicing_mode_at_shipping/readme/CREDITS.md new file mode 100644 index 00000000000..c2d2a1e6b83 --- /dev/null +++ b/partner_invoicing_mode_at_shipping/readme/CREDITS.md @@ -0,0 +1,3 @@ +The development and migration of this module has been financially supported by: + +- Camptocamp diff --git a/partner_invoicing_mode_at_shipping/readme/DESCRIPTION.md b/partner_invoicing_mode_at_shipping/readme/DESCRIPTION.md new file mode 100644 index 00000000000..a59d643c323 --- /dev/null +++ b/partner_invoicing_mode_at_shipping/readme/DESCRIPTION.md @@ -0,0 +1,8 @@ +This module allows to select a At shipping invoicing mode for a +customer. It is based on partner_invoicing_mode. When this mode is +selected the customer will be invoiced automatically on delivery of the +goods. + +Another option is the 'One Invoice Per Shipping'. That one is not +compatible with the 'At shipping' invoicing mode. In that case, the +invoicing validation will occur at a different moment (monthly, ...). diff --git a/partner_invoicing_mode_at_shipping/static/description/icon.png b/partner_invoicing_mode_at_shipping/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/partner_invoicing_mode_at_shipping/static/description/icon.png differ diff --git a/partner_invoicing_mode_at_shipping/static/description/index.html b/partner_invoicing_mode_at_shipping/static/description/index.html new file mode 100644 index 00000000000..d84384fd192 --- /dev/null +++ b/partner_invoicing_mode_at_shipping/static/description/index.html @@ -0,0 +1,450 @@ + + + + + +Partner Invoicing Mode At Shipping + + + +
+

Partner Invoicing Mode At Shipping

+ + +

Beta License: AGPL-3 OCA/account-invoicing Translate me on Weblate Try me on Runboat

+

This module allows to select a At shipping invoicing mode for a +customer. It is based on partner_invoicing_mode. When this mode is +selected the customer will be invoiced automatically on delivery of the +goods.

+

Another option is the ‘One Invoice Per Shipping’. That one is not +compatible with the ‘At shipping’ invoicing mode. In that case, the +invoicing validation will occur at a different moment (monthly, …).

+

Table of contents

+ +
+

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 to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

The development and migration of this module has been financially +supported by:

+
    +
  • Camptocamp
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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/account-invoicing project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/partner_invoicing_mode_at_shipping/tests/__init__.py b/partner_invoicing_mode_at_shipping/tests/__init__.py new file mode 100644 index 00000000000..5fa78f34aa5 --- /dev/null +++ b/partner_invoicing_mode_at_shipping/tests/__init__.py @@ -0,0 +1 @@ +from . import test_invoice_mode_at_shipping, test_invoice_mode_group_delivery diff --git a/partner_invoicing_mode_at_shipping/tests/common.py b/partner_invoicing_mode_at_shipping/tests/common.py new file mode 100644 index 00000000000..a2690f3cc9d --- /dev/null +++ b/partner_invoicing_mode_at_shipping/tests/common.py @@ -0,0 +1,54 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +class InvoiceModeAtShippingCommon: + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.partner = cls.env.ref("base.res_partner_1") + cls.product = cls.env.ref("product.product_delivery_01") + cls.so1 = cls.env["sale.order"].create( + { + "partner_id": cls.partner.id, + "partner_invoice_id": cls.partner.id, + "partner_shipping_id": cls.partner.id, + "order_line": [ + ( + 0, + 0, + { + "name": "Line one", + "product_id": cls.product.id, + "product_uom_qty": 4, + "product_uom": cls.product.uom_id.id, + "price_unit": 123, + }, + ) + ], + } + ) + + @classmethod + def _create_order(cls): + cls.so1 = cls.env["sale.order"].create( + { + "partner_id": cls.partner.id, + "partner_invoice_id": cls.partner.id, + "partner_shipping_id": cls.partner.id, + "order_line": [ + ( + 0, + 0, + { + "name": "Line one", + "product_id": cls.product.id, + "product_uom_qty": 4, + "product_uom": cls.product.uom_id.id, + "price_unit": 123, + }, + ) + ], + } + ) diff --git a/partner_invoicing_mode_at_shipping/tests/test_invoice_mode_at_shipping.py b/partner_invoicing_mode_at_shipping/tests/test_invoice_mode_at_shipping.py new file mode 100644 index 00000000000..254929c81d1 --- /dev/null +++ b/partner_invoicing_mode_at_shipping/tests/test_invoice_mode_at_shipping.py @@ -0,0 +1,105 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from odoo.tests.common import TransactionCase +from odoo.tools import mute_logger + +from .common import InvoiceModeAtShippingCommon + + +class TestInvoiceModeAtShipping(InvoiceModeAtShippingCommon, TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._create_order() + + def test_invoice_created_at_shipping(self): + """Check that an invoice is created when goods are shipped.""" + self.partner.invoicing_mode = "at_shipping" + self.so1.action_confirm() + for picking in self.so1.picking_ids: + for move in picking.move_ids: + move.quantity = move.product_uom_qty + picking.action_assign() + with mute_logger("odoo.addons.queue_job.delay"): + picking.with_context(queue_job__no_delay=True).button_validate() + self.assertEqual(len(self.so1.invoice_ids), 1) + self.assertEqual(self.so1.invoice_ids.state, "posted") + + def test_invoice_not_created_at_shipping(self): + """Check that an invoice is not created when goods are shipped.""" + self.partner.invoicing_mode = "standard" + self.so1.action_confirm() + for picking in self.so1.picking_ids: + for move in picking.move_ids: + move.quantity = move.product_uom_qty + picking.action_assign() + with mute_logger("odoo.addons.queue_job.delay"): + picking.with_context(queue_job__no_delay=True).button_validate() + self.assertEqual(len(self.so1.invoice_ids), 0) + + def test_picking_multi_order_single_invoice(self): + """A picking for more than one sale order creating a single invoice""" + self.partner.invoicing_mode = "at_shipping" + self.partner.one_invoice_per_order = False + so2 = self.so1.copy() + for order in self.so1, so2: + order.action_confirm() + # Effectively merge both pickings + picking = self.so1.picking_ids + so2.picking_ids.move_ids.picking_id = picking + # Transfer the remaining picking with moves + for move in picking.move_ids: + move.quantity = move.product_uom_qty + picking.action_assign() + with mute_logger("odoo.addons.queue_job.delay"): + picking.with_context(queue_job__no_delay=True).button_validate() + self.assertEqual(len(self.so1.invoice_ids), 1) + self.assertEqual(self.so1.invoice_ids.state, "posted") + self.assertEqual(self.so1.invoice_ids, so2.invoice_ids) + + def test_picking_multi_order_multi_invoice(self): + """A picking for more than one sale order creates more than one invoice""" + self.partner.invoicing_mode = "at_shipping" + self.partner.one_invoice_per_order = True + so2 = self.so1.copy() + for order in self.so1, so2: + order.action_confirm() + # Effectively merge both pickings + picking = self.so1.picking_ids + so2.picking_ids.move_ids.picking_id = picking + # Transfer the remaining picking with moves + for move in picking.move_ids: + move.quantity = move.product_uom_qty + picking.action_assign() + with mute_logger("odoo.addons.queue_job.delay"): + picking.with_context(queue_job__no_delay=True).button_validate() + self.assertEqual(len(self.so1.invoice_ids), 1) + self.assertEqual(self.so1.invoice_ids.state, "posted") + self.assertEqual(len(so2.invoice_ids), 1) + self.assertEqual(so2.invoice_ids.state, "posted") + self.assertNotEqual(self.so1.invoice_ids, so2.invoice_ids) + + def test_picking_backorder(self): + """In case of a backorder, another invoice is created""" + self.partner.invoicing_mode = "at_shipping" + self.so1.action_confirm() + picking = self.so1.picking_ids + picking.move_ids.quantity = 2 + picking.action_assign() + with mute_logger("odoo.addons.queue_job.delay"): + picking.with_context( + skip_backorder=True, queue_job__no_delay=True + ).button_validate() + self.assertEqual(len(self.so1.invoice_ids), 1) + self.assertEqual(self.so1.invoice_ids.state, "posted") + # Now process the backorder + backorder = self.so1.picking_ids - picking + backorder.move_ids.quantity = 2 + backorder.action_assign() + with mute_logger("odoo.addons.queue_job.delay"): + backorder.with_context(queue_job__no_delay=True).button_validate() + self.assertEqual(len(self.so1.invoice_ids), 2) + self.assertTrue( + all(invoice.state == "posted") for invoice in self.so1.invoice_ids + ) diff --git a/partner_invoicing_mode_at_shipping/tests/test_invoice_mode_group_delivery.py b/partner_invoicing_mode_at_shipping/tests/test_invoice_mode_group_delivery.py new file mode 100644 index 00000000000..7a69359eb9b --- /dev/null +++ b/partner_invoicing_mode_at_shipping/tests/test_invoice_mode_group_delivery.py @@ -0,0 +1,88 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase + +from odoo.addons.queue_job.tests.common import trap_jobs + +from .common import InvoiceModeAtShippingCommon + + +class TestInvoiceModeAtShippingGrouped(InvoiceModeAtShippingCommon, TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.companies = cls.env["res.company"].search([]) + + def test_invoice_created_at_shipping_per_delivery(self): + """Check that an invoice is created when goods are shipped.""" + self.partner.invoicing_mode = "standard" + self.partner.one_invoice_per_shipping = True + self._create_order() + self.so1.action_confirm() + picking = self.so1.picking_ids + + # Deliver partially + picking.move_ids.write({"quantity": 2.0, "picked": True}) + with trap_jobs() as trap: + picking._action_done() + trap.assert_enqueued_job( + picking._invoicing_at_shipping, + ) + trap.perform_enqueued_jobs() + + self.assertEqual(picking.state, "done") + invoice = self.so1.invoice_ids + # Invoice is generated but is still draft + self.assertEqual( + "draft", + invoice.state, + ) + + backorder = self.so1.picking_ids - picking + self.assertTrue(backorder) + + backorder.move_ids.write({"quantity": 2.0, "picked": True}) + with trap_jobs() as trap: + backorder._action_done() + trap.assert_enqueued_job( + backorder._invoicing_at_shipping, + ) + with trap_jobs() as trap_invoice: + trap.perform_enqueued_jobs() + self.assertFalse(trap_invoice.enqueued_jobs) + invoice_2 = self.so1.invoice_ids - invoice + self.assertEqual( + "draft", + invoice_2.state, + ) + # Launch the invoicing + with trap_jobs() as trap: + self.env["sale.order"].cron_generate_standard_invoices() + trap.assert_enqueued_job( + self.env["sale.order"]._validate_per_shipping_generated_invoices, + args=(), + kwargs={"companies": self.companies, "invoicing_mode": "standard"}, + ) + with trap_jobs() as trap_invoice: + trap.perform_enqueued_jobs() + trap_invoice.assert_enqueued_job( + self.so1.invoice_ids[0]._validate_invoice + ) + trap_invoice.assert_enqueued_job( + self.so1.invoice_ids[1]._validate_invoice + ) + trap_invoice.perform_enqueued_jobs() + self.assertEqual("posted", invoice.state) + self.assertEqual("posted", invoice_2.state) + + def test_invoice_created_at_shipping_per_delivery_constrains(self): + with self.assertRaises(ValidationError): + self.partner.write( + {"one_invoice_per_shipping": True, "invoicing_mode": "at_shipping"} + ) + with self.assertRaises(ValidationError): + self.partner.write( + {"one_invoice_per_order": True, "one_invoice_per_shipping": True} + ) diff --git a/partner_invoicing_mode_at_shipping/views/res_partner.xml b/partner_invoicing_mode_at_shipping/views/res_partner.xml new file mode 100644 index 00000000000..00ebcef0dfb --- /dev/null +++ b/partner_invoicing_mode_at_shipping/views/res_partner.xml @@ -0,0 +1,18 @@ + + + + + view_partner_property_form + res.partner + + + + + + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000000..180fc49789b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# generated from manifests external_dependencies +openupgradelib