From c0b017812e56bcc8e729a3bb10729f95d596d072 Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Tue, 7 Jul 2020 12:35:30 +0200 Subject: [PATCH 01/14] [13.0]]ADD] Add acconut_invoicing_mode Add three modules helping on automatically invoicing customers. The base module `account_invoice_base_invoicing_mode` does not actually do anything but adds a selection field on partner to allow assigning an invoicing mode to a customer. And a checkbox to choose regrouping invoices. The two other modules add specific invoicing mode. The module `account_invoice_mode_monthly` creates monthly invoices for customer on a specific day (configuration is in Accounting Settings) The module `account_invoice_mode_at_shipping` creates invoices on the shipping of the goods. Those modules use queue_job to generate and validate invoices. --- account_invoice_mode_monthly/__init__.py | 2 + account_invoice_mode_monthly/__manifest__.py | 13 ++ account_invoice_mode_monthly/data/ir_cron.xml | 18 +++ .../models/__init__.py | 4 + .../models/res_company.py | 21 +++ .../models/res_config_settings.py | 14 ++ .../models/res_partner.py | 10 ++ .../models/sale_order.py | 102 +++++++++++++ .../readme/CONTRIBUTORS.rst | 3 + .../readme/DESCRIPTION.rst | 4 + .../readme/INSTALL.rst | 0 .../tests/__init__.py | 2 + .../tests/test_invoice_mode_monthly.py | 138 ++++++++++++++++++ .../test_invoice_mode_monthly_is_it_today.py | 63 ++++++++ .../views/res_config_settings_views.xml | 43 ++++++ 15 files changed, 437 insertions(+) create mode 100644 account_invoice_mode_monthly/__init__.py create mode 100644 account_invoice_mode_monthly/__manifest__.py create mode 100644 account_invoice_mode_monthly/data/ir_cron.xml create mode 100644 account_invoice_mode_monthly/models/__init__.py create mode 100644 account_invoice_mode_monthly/models/res_company.py create mode 100644 account_invoice_mode_monthly/models/res_config_settings.py create mode 100644 account_invoice_mode_monthly/models/res_partner.py create mode 100644 account_invoice_mode_monthly/models/sale_order.py create mode 100644 account_invoice_mode_monthly/readme/CONTRIBUTORS.rst create mode 100644 account_invoice_mode_monthly/readme/DESCRIPTION.rst create mode 100644 account_invoice_mode_monthly/readme/INSTALL.rst create mode 100644 account_invoice_mode_monthly/tests/__init__.py create mode 100644 account_invoice_mode_monthly/tests/test_invoice_mode_monthly.py create mode 100644 account_invoice_mode_monthly/tests/test_invoice_mode_monthly_is_it_today.py create mode 100644 account_invoice_mode_monthly/views/res_config_settings_views.xml diff --git a/account_invoice_mode_monthly/__init__.py b/account_invoice_mode_monthly/__init__.py new file mode 100644 index 00000000000..0ee8b5073e2 --- /dev/null +++ b/account_invoice_mode_monthly/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import tests diff --git a/account_invoice_mode_monthly/__manifest__.py b/account_invoice_mode_monthly/__manifest__.py new file mode 100644 index 00000000000..43f24f6e71b --- /dev/null +++ b/account_invoice_mode_monthly/__manifest__.py @@ -0,0 +1,13 @@ +# Copyright 2020 Camptocamp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Account Invoice Mode Monthly", + "version": "13.0.1.0.0", + "summary": "Create invoices automatically on a monthly basis.", + "author": "Camptocamp, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/account-invoicing", + "license": "AGPL-3", + "category": "Accounting & Finance", + "depends": ["account", "account_invoice_base_invoicing_mode", "queue_job", "sale"], + "data": ["data/ir_cron.xml", "views/res_config_settings_views.xml"], +} diff --git a/account_invoice_mode_monthly/data/ir_cron.xml b/account_invoice_mode_monthly/data/ir_cron.xml new file mode 100644 index 00000000000..0410250612f --- /dev/null +++ b/account_invoice_mode_monthly/data/ir_cron.xml @@ -0,0 +1,18 @@ + + + + Generate Monthly Invoices + + + 1 + days + -1 + + + model.cron_generate_monthly_invoices() + + + diff --git a/account_invoice_mode_monthly/models/__init__.py b/account_invoice_mode_monthly/models/__init__.py new file mode 100644 index 00000000000..ea64a69d2e0 --- /dev/null +++ b/account_invoice_mode_monthly/models/__init__.py @@ -0,0 +1,4 @@ +from . import res_company +from . import res_config_settings +from . import res_partner +from . import sale_order diff --git a/account_invoice_mode_monthly/models/res_company.py b/account_invoice_mode_monthly/models/res_company.py new file mode 100644 index 00000000000..614a77c7cc3 --- /dev/null +++ b/account_invoice_mode_monthly/models/res_company.py @@ -0,0 +1,21 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + invoicing_mode_monthly_day_todo = fields.Integer( + "Invoicing Day", + default="31", + help="Day of the month to execute the invoicing. For a number higher" + "than the number of days in a month, the invoicing will be" + "executed on the last day of the month.", + ) + invoicing_mode_monthly_last_execution = fields.Datetime( + string="Last execution", + help="Last execution of monthly invoicing.", + readonly=True, + ) diff --git a/account_invoice_mode_monthly/models/res_config_settings.py b/account_invoice_mode_monthly/models/res_config_settings.py new file mode 100644 index 00000000000..a2c560c73c9 --- /dev/null +++ b/account_invoice_mode_monthly/models/res_config_settings.py @@ -0,0 +1,14 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + res_invoicing_mode_monthly_day_todo = fields.Integer( + related="company_id.invoicing_mode_monthly_day_todo", readonly=False + ) + invoicing_mode_monthly_last_execution = fields.Datetime( + related="company_id.invoicing_mode_monthly_last_execution", readonly=True + ) diff --git a/account_invoice_mode_monthly/models/res_partner.py b/account_invoice_mode_monthly/models/res_partner.py new file mode 100644 index 00000000000..99953595e59 --- /dev/null +++ b/account_invoice_mode_monthly/models/res_partner.py @@ -0,0 +1,10 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from odoo import fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + invoicing_mode = fields.Selection(selection_add=([("monthly", "Monthly")])) diff --git a/account_invoice_mode_monthly/models/sale_order.py b/account_invoice_mode_monthly/models/sale_order.py new file mode 100644 index 00000000000..70758847e4f --- /dev/null +++ b/account_invoice_mode_monthly/models/sale_order.py @@ -0,0 +1,102 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from datetime import datetime + +from dateutil.relativedelta import relativedelta + +from odoo import api, models +from odoo.osv.expression import OR + +from odoo.addons.queue_job.job import job + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + @api.model + def cron_generate_monthly_invoices(self): + """Cron called daily to check if monthly invoicing needs to be done.""" + company_ids = self._company_monthly_invoicing_today() + if company_ids: + self.generate_monthly_invoices(company_ids) + + @api.model + def generate_monthly_invoices(self, companies=None): + """Generate monthly invoices for customers who require that mode.""" + if not companies: + companies = self.company_id + partner_ids = self.read_group( + [ + ("invoicing_mode", "=", "monthly"), + ("invoice_status", "=", "to invoice"), + ("company_id", "in", companies.ids), + ], + ["partner_invoice_id"], + groupby=["partner_invoice_id"], + ) + for partner in partner_ids: + self.with_delay()._generate_invoices_by_partner( + partner["partner_invoice_id"][0] + ) + companies.write({"invoicing_mode_monthly_last_execution": datetime.now()}) + return partner_ids + + @job(default_channel="root.invoice_monthly") + def _generate_invoices_by_partner(self, partner_id, invoicing_mode="monthly"): + """Generate invoices for a customer sales order.""" + partner = self.env["res.partner"].browse(partner_id) + if partner.invoicing_mode != invoicing_mode: + return "Customer {} is not configured for monthly invoicing.".format( + partner.name + ) + sales = self.search( + [ + ("invoice_status", "=", "to invoice"), + ("partner_invoice_id", "=", partner.id), + ("order_line.qty_to_invoice", "!=", 0), + ] + ) + # By default grouped by partner/currency. Refund are not generated + invoices = sales._create_invoices( + grouped=partner.one_invoice_per_order, final=True + ) + for invoice in invoices: + invoice.with_delay()._validate_invoice() + return invoices + + @api.model + def _company_monthly_invoicing_today(self): + """Get company ids for which today is monthly invoicing day.""" + today = datetime.now() + first_day_this_month = today.replace( + day=1, hour=0, minute=0, second=0, microsecond=0 + ) + first_day_last_month = first_day_this_month - relativedelta(months=1) + last_day_of_this_month = (today + relativedelta(day=31)).day + # Last month still not executed, it needs to be done + domain_last_month = [ + ("invoicing_mode_monthly_last_execution", "<", first_day_last_month), + ] + # Invoicing day is today or in the past and invoicing not yet done + domain_this_month = [ + "|", + ("invoicing_mode_monthly_last_execution", "<", first_day_this_month), + ("invoicing_mode_monthly_last_execution", "=", False), + ("invoicing_mode_monthly_day_todo", "<=", today.day), + ] + # Make sure non exisiting days are done at the end of the month + domain_last_day_of_month = [ + "|", + ("invoicing_mode_monthly_last_execution", "<", first_day_this_month), + ("invoicing_mode_monthly_last_execution", "=", False), + ("invoicing_mode_monthly_day_todo", ">", today.day), + ] + if today.day == last_day_of_this_month: + domain = OR( + [domain_last_month, domain_this_month, domain_last_day_of_month] + ) + else: + domain = OR([domain_last_month, domain_this_month]) + + return self.env["res.company"].search(domain) diff --git a/account_invoice_mode_monthly/readme/CONTRIBUTORS.rst b/account_invoice_mode_monthly/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..b180cbde2b2 --- /dev/null +++ b/account_invoice_mode_monthly/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Camptocamp `_: + + * Thierry Ducrest diff --git a/account_invoice_mode_monthly/readme/DESCRIPTION.rst b/account_invoice_mode_monthly/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..4e45ab48a76 --- /dev/null +++ b/account_invoice_mode_monthly/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +This module allows to select a monthly invoicing mode for a customer. +It is based on `account_invoice_base_invoicing_mode`. +When this mode is selected for a customer, the customer will be automatically +invoiced diff --git a/account_invoice_mode_monthly/readme/INSTALL.rst b/account_invoice_mode_monthly/readme/INSTALL.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/account_invoice_mode_monthly/tests/__init__.py b/account_invoice_mode_monthly/tests/__init__.py new file mode 100644 index 00000000000..bae1b469a28 --- /dev/null +++ b/account_invoice_mode_monthly/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_invoice_mode_monthly +from . import test_invoice_mode_monthly_is_it_today diff --git a/account_invoice_mode_monthly/tests/test_invoice_mode_monthly.py b/account_invoice_mode_monthly/tests/test_invoice_mode_monthly.py new file mode 100644 index 00000000000..309b52a8f9a --- /dev/null +++ b/account_invoice_mode_monthly/tests/test_invoice_mode_monthly.py @@ -0,0 +1,138 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from odoo import tools +from odoo.tests.common import SavepointCase + + +class TestInvoiceModeMonthly(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.SaleOrder = cls.env["sale.order"] + cls.partner = cls.env.ref("base.res_partner_1") + cls.partner.invoicing_mode = "monthly" + cls.partner2 = cls.env.ref("base.res_partner_2") + cls.partner2.invoicing_mode = "monthly" + 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, + }, + ) + ], + "pricelist_id": cls.env.ref("product.list0").id, + } + ) + + cls.so2 = 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, + }, + ) + ], + "pricelist_id": cls.env.ref("product.list0").id, + } + ) + cls.company = cls.so1.company_id + + stock_location = cls.env.ref("stock.stock_location_stock") + inventory = cls.env["stock.inventory"].create( + { + "name": "Test Inventory", + "product_ids": [(6, 0, cls.product.ids)], + "state": "confirm", + "line_ids": [ + ( + 0, + 0, + { + "product_qty": 100, + "location_id": stock_location.id, + "product_id": cls.product.id, + "product_uom_id": cls.product.uom_id.id, + }, + ) + ], + } + ) + inventory.action_validate() + + def deliver_invoice(self, sale_order): + sale_order.action_confirm() + for picking in sale_order.picking_ids: + for line in picking.move_lines: + line.quantity_done = line.product_uom_qty + picking.action_assign() + picking.button_validate() + + def test_saleorder_grouped_in_invoice(self): + """Check multiple sale order grouped in one invoice""" + self.deliver_invoice(self.so1) + self.deliver_invoice(self.so2) + with tools.mute_logger("odoo.addons.queue_job.models.base"): + self.SaleOrder.with_context( + test_queue_job_no_delay=True + ).generate_monthly_invoices(self.company) + self.assertEqual(len(self.so1.invoice_ids), 1) + self.assertEqual(len(self.so2.invoice_ids), 1) + self.assertEqual(self.so1.invoice_ids, self.so2.invoice_ids) + self.assertEqual(self.so1.invoice_ids.state, "posted") + + def test_split_invoice_by_sale_order(self): + """For same customer invoice 2 sales order separately.""" + self.partner.invoicing_mode = "monthly" + self.partner.one_invoice_per_order = True + self.deliver_invoice(self.so1) + self.deliver_invoice(self.so2) + with tools.mute_logger("odoo.addons.queue_job.models.base"): + self.SaleOrder.with_context( + test_queue_job_no_delay=True + ).generate_monthly_invoices(self.company) + self.assertEqual(len(self.so1.invoice_ids), 1) + self.assertEqual(len(self.so2.invoice_ids), 1) + self.assertNotEqual(self.so1.invoice_ids, self.so2.invoice_ids) + self.assertEqual(self.so1.invoice_ids.state, "posted") + self.assertEqual(self.so2.invoice_ids.state, "posted") + + def test_invoice_for_multiple_customer(self): + """Check two sale order for different customers.""" + self.partner.invoicing_mode = "monthly" + self.so2.partner_id = self.partner2 + self.so2.partner_invoice_id = self.partner2 + self.so2.partner_shipping_id = self.partner2 + self.deliver_invoice(self.so1) + self.deliver_invoice(self.so2) + with tools.mute_logger("odoo.addons.queue_job.models.base"): + self.SaleOrder.with_context( + test_queue_job_no_delay=True + ).generate_monthly_invoices(self.company) + self.assertEqual(len(self.so1.invoice_ids), 1) + self.assertEqual(len(self.so2.invoice_ids), 1) + self.assertNotEqual(self.so1.invoice_ids, self.so2.invoice_ids) + self.assertEqual(self.so1.invoice_ids.state, "posted") + self.assertEqual(self.so2.invoice_ids.state, "posted") diff --git a/account_invoice_mode_monthly/tests/test_invoice_mode_monthly_is_it_today.py b/account_invoice_mode_monthly/tests/test_invoice_mode_monthly_is_it_today.py new file mode 100644 index 00000000000..d978d4336ef --- /dev/null +++ b/account_invoice_mode_monthly/tests/test_invoice_mode_monthly_is_it_today.py @@ -0,0 +1,63 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from freezegun import freeze_time + +from odoo.tests.common import SavepointCase + + +class TestInvoiceModeMonthly(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.company = cls.env.company + cls.SaleOrder = cls.env["sale.order"] + + def test_late_invoicing_for_last_month(self): + """Check that last month invoicing will be done if missed.""" + company = self.env.company + company.invoicing_mode_monthly_day_todo = 31 + company.invoicing_mode_monthly_last_execution = "2020-05-31" + self.assertTrue(self.env.company) + with freeze_time("2020-07-03"): + res = self.SaleOrder._company_monthly_invoicing_today() + self.assertTrue(res) + company.invoicing_mode_monthly_last_execution = "2020-06-30" + with freeze_time("2020-07-03"): + res = self.SaleOrder._company_monthly_invoicing_today() + self.assertFalse(res) + + def test_selected_day_not_exist_in_month(self): + """Check on last day of the month invoicing is done. + + The day of invoicing selected could not exist in the current + month, but invoicing should still be executed on the last + day of the month. + """ + company = self.env.company + company.invoicing_mode_monthly_day_todo = 31 + company.invoicing_mode_monthly_last_execution = "2020-05-29" + self.assertTrue(self.env.company) + with freeze_time("2020-06-29"): + res = self.SaleOrder._company_monthly_invoicing_today() + self.assertFalse(res) + with freeze_time("2020-06-30"): + res = self.SaleOrder._company_monthly_invoicing_today() + self.assertTrue(res) + + def test_no_invoicing_done_yet(self): + """Check when is the first monthly invoicing done. + + When monthly invoicing has never been done, it will not be run + for the previous month. + """ + company = self.env.company + company.invoicing_mode_monthly_day_todo = 15 + company.invoicing_mode_monthly_last_execution = None + self.assertTrue(self.env.company) + with freeze_time("2020-06-11"): + res = self.SaleOrder._company_monthly_invoicing_today() + self.assertFalse(res) + with freeze_time("2020-06-30"): + res = self.SaleOrder._company_monthly_invoicing_today() + self.assertTrue(res) diff --git a/account_invoice_mode_monthly/views/res_config_settings_views.xml b/account_invoice_mode_monthly/views/res_config_settings_views.xml new file mode 100644 index 00000000000..dbe09556648 --- /dev/null +++ b/account_invoice_mode_monthly/views/res_config_settings_views.xml @@ -0,0 +1,43 @@ + + + + + res.config.settings + +
+

Invoicing Mode

+
+
+
+
+ Monthly Invoicing Options +
+
+
+
+
+
+
+
+
+
+ + + From 291a81d19d9364eb7a244b30e5f85a9a93351353 Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Fri, 24 Jul 2020 07:57:15 +0200 Subject: [PATCH 02/14] [13.0][IMP] acconut_invoicing_mode group by terms Sales order with different payment term are not grouped into the same invoice anymore. This grouping can be easily changed by overriding the coresponding method. During this change the parameter of the job generating the invoices has changes and is now the sale_order_ids to process. --- .../models/sale_order.py | 50 ++++++++++--------- .../tests/test_invoice_mode_monthly.py | 24 ++++++++- 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/account_invoice_mode_monthly/models/sale_order.py b/account_invoice_mode_monthly/models/sale_order.py index 70758847e4f..dc104fa7868 100644 --- a/account_invoice_mode_monthly/models/sale_order.py +++ b/account_invoice_mode_monthly/models/sale_order.py @@ -23,43 +23,47 @@ def cron_generate_monthly_invoices(self): @api.model def generate_monthly_invoices(self, companies=None): - """Generate monthly invoices for customers who require that mode.""" + """Generate monthly invoices for customers who require that mode. + + Invoices will be generated by other jobs split for different customer + and different payment term. + """ if not companies: companies = self.company_id - partner_ids = self.read_group( + saleorder_groups = self.read_group( [ ("invoicing_mode", "=", "monthly"), ("invoice_status", "=", "to invoice"), ("company_id", "in", companies.ids), ], ["partner_invoice_id"], - groupby=["partner_invoice_id"], + groupby=self._get_groupby_fields_for_monthly_invoicing(), + lazy=False, ) - for partner in partner_ids: - self.with_delay()._generate_invoices_by_partner( - partner["partner_invoice_id"][0] - ) + for saleorder_group in saleorder_groups: + saleorder_ids = self.search(saleorder_group["__domain"]).ids + self.with_delay()._generate_invoices_by_partner(saleorder_ids) companies.write({"invoicing_mode_monthly_last_execution": datetime.now()}) - return partner_ids + return saleorder_groups + + @api.model + def _get_groupby_fields_for_monthly_invoicing(self): + """Returns the sale order fields used to group them into jobs.""" + return ["partner_invoice_id", "payment_term_id"] @job(default_channel="root.invoice_monthly") - def _generate_invoices_by_partner(self, partner_id, invoicing_mode="monthly"): - """Generate invoices for a customer sales order.""" - partner = self.env["res.partner"].browse(partner_id) - if partner.invoicing_mode != invoicing_mode: - return "Customer {} is not configured for monthly invoicing.".format( - partner.name - ) - sales = self.search( - [ - ("invoice_status", "=", "to invoice"), - ("partner_invoice_id", "=", partner.id), - ("order_line.qty_to_invoice", "!=", 0), - ] + def _generate_invoices_by_partner(self, saleorder_ids, invoicing_mode="monthly"): + """Generate invoices for a group of sale order belonging to a customer. + """ + sales = ( + self.browse(saleorder_ids) + .exists() + .filtered(lambda r: r.invoice_status == "to invoice") ) - # By default grouped by partner/currency. Refund are not generated + if not sales: + return "No sale order found to invoice ?" invoices = sales._create_invoices( - grouped=partner.one_invoice_per_order, final=True + grouped=sales[0].partner_invoice_id.one_invoice_per_order, final=True ) for invoice in invoices: invoice.with_delay()._validate_invoice() diff --git a/account_invoice_mode_monthly/tests/test_invoice_mode_monthly.py b/account_invoice_mode_monthly/tests/test_invoice_mode_monthly.py index 309b52a8f9a..949fb35bf11 100644 --- a/account_invoice_mode_monthly/tests/test_invoice_mode_monthly.py +++ b/account_invoice_mode_monthly/tests/test_invoice_mode_monthly.py @@ -15,11 +15,14 @@ def setUpClass(cls): cls.partner2 = cls.env.ref("base.res_partner_2") cls.partner2.invoicing_mode = "monthly" cls.product = cls.env.ref("product.product_delivery_01") + cls.pt1 = cls.env["account.payment.term"].create({"name": "Term Two"}) + cls.pt2 = cls.env["account.payment.term"].create({"name": "Term One"}) cls.so1 = cls.env["sale.order"].create( { "partner_id": cls.partner.id, "partner_invoice_id": cls.partner.id, "partner_shipping_id": cls.partner.id, + "payment_term_id": cls.pt1.id, "order_line": [ ( 0, @@ -36,12 +39,13 @@ def setUpClass(cls): "pricelist_id": cls.env.ref("product.list0").id, } ) - + # Lets give the saleorder the same partner and payment terms cls.so2 = cls.env["sale.order"].create( { "partner_id": cls.partner.id, "partner_invoice_id": cls.partner.id, "partner_shipping_id": cls.partner.id, + "payment_term_id": cls.pt1.id, "order_line": [ ( 0, @@ -90,6 +94,22 @@ def deliver_invoice(self, sale_order): picking.action_assign() picking.button_validate() + def test_saleorder_with_different_mode_term(self): + """Check multiple sale order one partner diverse terms.""" + self.so1.payment_term_id = self.pt1.id + self.deliver_invoice(self.so1) + self.so2.payment_term_id = self.pt2.id + self.deliver_invoice(self.so2) + with tools.mute_logger("odoo.addons.queue_job.models.base"): + self.SaleOrder.with_context( + test_queue_job_no_delay=True + ).generate_monthly_invoices(self.company) + self.assertEqual(len(self.so1.invoice_ids), 1) + self.assertEqual(len(self.so2.invoice_ids), 1) + # Two invoices because the term are different + self.assertNotEqual(self.so1.invoice_ids, self.so2.invoice_ids) + self.assertEqual(self.so1.invoice_ids.state, "posted") + def test_saleorder_grouped_in_invoice(self): """Check multiple sale order grouped in one invoice""" self.deliver_invoice(self.so1) @@ -100,6 +120,7 @@ def test_saleorder_grouped_in_invoice(self): ).generate_monthly_invoices(self.company) self.assertEqual(len(self.so1.invoice_ids), 1) self.assertEqual(len(self.so2.invoice_ids), 1) + # Same invoice for both order self.assertEqual(self.so1.invoice_ids, self.so2.invoice_ids) self.assertEqual(self.so1.invoice_ids.state, "posted") @@ -115,6 +136,7 @@ def test_split_invoice_by_sale_order(self): ).generate_monthly_invoices(self.company) self.assertEqual(len(self.so1.invoice_ids), 1) self.assertEqual(len(self.so2.invoice_ids), 1) + # Two invoices as they must be split self.assertNotEqual(self.so1.invoice_ids, self.so2.invoice_ids) self.assertEqual(self.so1.invoice_ids.state, "posted") self.assertEqual(self.so2.invoice_ids.state, "posted") From b49c1f0d13540cd215a535b97abf1a39fcf6d4de Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Tue, 22 Sep 2020 11:39:44 +0200 Subject: [PATCH 03/14] [13.0][FIX] Add acconut_invoicing_mode Add few changes from reviews. Use sudo to post the invoice. --- account_invoice_mode_monthly/tests/test_invoice_mode_monthly.py | 1 + 1 file changed, 1 insertion(+) diff --git a/account_invoice_mode_monthly/tests/test_invoice_mode_monthly.py b/account_invoice_mode_monthly/tests/test_invoice_mode_monthly.py index 949fb35bf11..1096cbd59c9 100644 --- a/account_invoice_mode_monthly/tests/test_invoice_mode_monthly.py +++ b/account_invoice_mode_monthly/tests/test_invoice_mode_monthly.py @@ -9,6 +9,7 @@ class TestInvoiceModeMonthly(SavepointCase): @classmethod def setUpClass(cls): super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) cls.SaleOrder = cls.env["sale.order"] cls.partner = cls.env.ref("base.res_partner_1") cls.partner.invoicing_mode = "monthly" From d97264ce32dc18e26f7a8c2ba0e6f99113b06aaa Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Wed, 28 Jul 2021 10:09:53 +0200 Subject: [PATCH 04/14] [13.0] account-invoicing-mode add roadmap --- account_invoice_mode_monthly/readme/ROADMAP.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 account_invoice_mode_monthly/readme/ROADMAP.rst diff --git a/account_invoice_mode_monthly/readme/ROADMAP.rst b/account_invoice_mode_monthly/readme/ROADMAP.rst new file mode 100644 index 00000000000..ec704cfa7ac --- /dev/null +++ b/account_invoice_mode_monthly/readme/ROADMAP.rst @@ -0,0 +1,5 @@ +On the version 13.0 PR there is some discussion about changing how the +`_company_monthly_invoicing_today` works by storing the next invoicing day +to simplify the computation. +Would it be an improvment or not ? +I answered with this `comment `_ From 4d92b3f5c4606ab99b3a1df13997d5574b131c23 Mon Sep 17 00:00:00 2001 From: Tran Thanh Phuc Date: Sun, 10 Jan 2021 19:23:09 +0700 Subject: [PATCH 05/14] [IMP] account_invoice_mode: black, isort, prettier --- account_invoice_mode_monthly/models/sale_order.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/account_invoice_mode_monthly/models/sale_order.py b/account_invoice_mode_monthly/models/sale_order.py index dc104fa7868..da3c9dea99c 100644 --- a/account_invoice_mode_monthly/models/sale_order.py +++ b/account_invoice_mode_monthly/models/sale_order.py @@ -53,8 +53,7 @@ def _get_groupby_fields_for_monthly_invoicing(self): @job(default_channel="root.invoice_monthly") def _generate_invoices_by_partner(self, saleorder_ids, invoicing_mode="monthly"): - """Generate invoices for a group of sale order belonging to a customer. - """ + """Generate invoices for a group of sale order belonging to a customer.""" sales = ( self.browse(saleorder_ids) .exists() From fe2904c6697c848c1f32d0a9057c9b23987808d6 Mon Sep 17 00:00:00 2001 From: Tran Thanh Phuc Date: Mon, 11 Jan 2021 21:24:24 +0700 Subject: [PATCH 06/14] [MIG] account_invoice_mode: Migration to 14.0 --- account_invoice_mode_monthly/README.rst | 87 ++++ account_invoice_mode_monthly/__manifest__.py | 8 +- .../data/queue_job_data.xml | 20 + .../models/sale_order.py | 3 - .../readme/CONTRIBUTORS.rst | 2 + .../readme/CREDITS.rst | 3 + .../static/description/index.html | 438 ++++++++++++++++++ 7 files changed, 556 insertions(+), 5 deletions(-) create mode 100644 account_invoice_mode_monthly/README.rst create mode 100644 account_invoice_mode_monthly/data/queue_job_data.xml create mode 100644 account_invoice_mode_monthly/readme/CREDITS.rst create mode 100644 account_invoice_mode_monthly/static/description/index.html diff --git a/account_invoice_mode_monthly/README.rst b/account_invoice_mode_monthly/README.rst new file mode 100644 index 00000000000..01613e99c6e --- /dev/null +++ b/account_invoice_mode_monthly/README.rst @@ -0,0 +1,87 @@ +============================ +Account Invoice Mode Monthly +============================ + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! 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%2Faccount--invoicing-lightgray.png?logo=github + :target: https://github.com/OCA/account-invoicing/tree/14.0/account_invoice_mode_monthly + :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-14-0/account-invoicing-14-0-account_invoice_mode_monthly + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/95/14.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to select a monthly invoicing mode for a customer. +It is based on `account_invoice_base_invoicing_mode`. +When this mode is selected for a customer, the customer will be automatically +invoiced + +**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 smashing 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) + +Other credits +~~~~~~~~~~~~~ + +The development 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/account_invoice_mode_monthly/__manifest__.py b/account_invoice_mode_monthly/__manifest__.py index 43f24f6e71b..ccd1928c8aa 100644 --- a/account_invoice_mode_monthly/__manifest__.py +++ b/account_invoice_mode_monthly/__manifest__.py @@ -2,12 +2,16 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { "name": "Account Invoice Mode Monthly", - "version": "13.0.1.0.0", + "version": "14.0.1.0.0", "summary": "Create invoices automatically on a monthly basis.", "author": "Camptocamp, Odoo Community Association (OCA)", "website": "https://github.com/OCA/account-invoicing", "license": "AGPL-3", "category": "Accounting & Finance", "depends": ["account", "account_invoice_base_invoicing_mode", "queue_job", "sale"], - "data": ["data/ir_cron.xml", "views/res_config_settings_views.xml"], + "data": [ + "data/ir_cron.xml", + "data/queue_job_data.xml", + "views/res_config_settings_views.xml", + ], } diff --git a/account_invoice_mode_monthly/data/queue_job_data.xml b/account_invoice_mode_monthly/data/queue_job_data.xml new file mode 100644 index 00000000000..4a7abf3e9ec --- /dev/null +++ b/account_invoice_mode_monthly/data/queue_job_data.xml @@ -0,0 +1,20 @@ + + + + + + invoice_monthly + + + + + + + _generate_invoices_by_partner + + + + diff --git a/account_invoice_mode_monthly/models/sale_order.py b/account_invoice_mode_monthly/models/sale_order.py index da3c9dea99c..84e064729e3 100644 --- a/account_invoice_mode_monthly/models/sale_order.py +++ b/account_invoice_mode_monthly/models/sale_order.py @@ -8,8 +8,6 @@ from odoo import api, models from odoo.osv.expression import OR -from odoo.addons.queue_job.job import job - class SaleOrder(models.Model): _inherit = "sale.order" @@ -51,7 +49,6 @@ def _get_groupby_fields_for_monthly_invoicing(self): """Returns the sale order fields used to group them into jobs.""" return ["partner_invoice_id", "payment_term_id"] - @job(default_channel="root.invoice_monthly") def _generate_invoices_by_partner(self, saleorder_ids, invoicing_mode="monthly"): """Generate invoices for a group of sale order belonging to a customer.""" sales = ( diff --git a/account_invoice_mode_monthly/readme/CONTRIBUTORS.rst b/account_invoice_mode_monthly/readme/CONTRIBUTORS.rst index b180cbde2b2..ecddfcde7f4 100644 --- a/account_invoice_mode_monthly/readme/CONTRIBUTORS.rst +++ b/account_invoice_mode_monthly/readme/CONTRIBUTORS.rst @@ -1,3 +1,5 @@ * `Camptocamp `_: * Thierry Ducrest + +* Phuc (Tran Thanh) diff --git a/account_invoice_mode_monthly/readme/CREDITS.rst b/account_invoice_mode_monthly/readme/CREDITS.rst new file mode 100644 index 00000000000..f5cc070c78e --- /dev/null +++ b/account_invoice_mode_monthly/readme/CREDITS.rst @@ -0,0 +1,3 @@ +The development of this module has been financially supported by: + +* Camptocamp diff --git a/account_invoice_mode_monthly/static/description/index.html b/account_invoice_mode_monthly/static/description/index.html new file mode 100644 index 00000000000..667179ce72e --- /dev/null +++ b/account_invoice_mode_monthly/static/description/index.html @@ -0,0 +1,438 @@ + + + + + + +Account Invoice Mode Monthly + + + +
+

Account Invoice Mode Monthly

+ + +

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

+

This module allows to select a monthly invoicing mode for a customer. +It is based on account_invoice_base_invoicing_mode. +When this mode is selected for a customer, the customer will be automatically +invoiced

+

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 smashing 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 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.

+
+
+
+ + From d2a10b0fb026510f1f559531c4a5ab310ab53729 Mon Sep 17 00:00:00 2001 From: oca-travis Date: Wed, 4 Aug 2021 05:14:33 +0000 Subject: [PATCH 07/14] [UPD] Update account_invoice_mode_monthly.pot --- .../i18n/account_invoice_mode_monthly.pot | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 account_invoice_mode_monthly/i18n/account_invoice_mode_monthly.pot diff --git a/account_invoice_mode_monthly/i18n/account_invoice_mode_monthly.pot b/account_invoice_mode_monthly/i18n/account_invoice_mode_monthly.pot new file mode 100644 index 00000000000..67336cd5268 --- /dev/null +++ b/account_invoice_mode_monthly/i18n/account_invoice_mode_monthly.pot @@ -0,0 +1,119 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_invoice_mode_monthly +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.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: account_invoice_mode_monthly +#: model_terms:ir.ui.view,arch_db:account_invoice_mode_monthly.res_config_settings_view_form +msgid "Monthly Invoicing Options" +msgstr "" + +#. module: account_invoice_mode_monthly +#: model:ir.model,name:account_invoice_mode_monthly.model_res_company +msgid "Companies" +msgstr "" + +#. module: account_invoice_mode_monthly +#: model:ir.model,name:account_invoice_mode_monthly.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: account_invoice_mode_monthly +#: model:ir.model,name:account_invoice_mode_monthly.model_res_partner +msgid "Contact" +msgstr "" + +#. module: account_invoice_mode_monthly +#: model:ir.model.fields,help:account_invoice_mode_monthly.field_res_company__invoicing_mode_monthly_day_todo +#: model:ir.model.fields,help:account_invoice_mode_monthly.field_res_config_settings__res_invoicing_mode_monthly_day_todo +msgid "" +"Day of the month to execute the invoicing. For a number higherthan the " +"number of days in a month, the invoicing will beexecuted on the last day of " +"the month." +msgstr "" + +#. module: account_invoice_mode_monthly +#: model:ir.model.fields,field_description:account_invoice_mode_monthly.field_res_company__display_name +#: model:ir.model.fields,field_description:account_invoice_mode_monthly.field_res_config_settings__display_name +#: model:ir.model.fields,field_description:account_invoice_mode_monthly.field_res_partner__display_name +#: model:ir.model.fields,field_description:account_invoice_mode_monthly.field_sale_order__display_name +msgid "Display Name" +msgstr "" + +#. module: account_invoice_mode_monthly +#: model:ir.actions.server,name:account_invoice_mode_monthly.ir_cron_generate_monthly_invoice_ir_actions_server +#: model:ir.cron,cron_name:account_invoice_mode_monthly.ir_cron_generate_monthly_invoice +#: model:ir.cron,name:account_invoice_mode_monthly.ir_cron_generate_monthly_invoice +msgid "Generate Monthly Invoices" +msgstr "" + +#. module: account_invoice_mode_monthly +#: model:ir.model.fields,field_description:account_invoice_mode_monthly.field_res_company__id +#: model:ir.model.fields,field_description:account_invoice_mode_monthly.field_res_config_settings__id +#: model:ir.model.fields,field_description:account_invoice_mode_monthly.field_res_partner__id +#: model:ir.model.fields,field_description:account_invoice_mode_monthly.field_sale_order__id +msgid "ID" +msgstr "" + +#. module: account_invoice_mode_monthly +#: model:ir.model.fields,field_description:account_invoice_mode_monthly.field_res_company__invoicing_mode_monthly_day_todo +#: model:ir.model.fields,field_description:account_invoice_mode_monthly.field_res_config_settings__res_invoicing_mode_monthly_day_todo +msgid "Invoicing Day" +msgstr "" + +#. module: account_invoice_mode_monthly +#: model:ir.model.fields,field_description:account_invoice_mode_monthly.field_res_partner__invoicing_mode +#: model:ir.model.fields,field_description:account_invoice_mode_monthly.field_res_users__invoicing_mode +#: model_terms:ir.ui.view,arch_db:account_invoice_mode_monthly.res_config_settings_view_form +msgid "Invoicing Mode" +msgstr "" + +#. module: account_invoice_mode_monthly +#: model_terms:ir.ui.view,arch_db:account_invoice_mode_monthly.res_config_settings_view_form +msgid "Invoicing day" +msgstr "" + +#. module: account_invoice_mode_monthly +#: model:ir.model.fields,field_description:account_invoice_mode_monthly.field_res_company____last_update +#: model:ir.model.fields,field_description:account_invoice_mode_monthly.field_res_config_settings____last_update +#: model:ir.model.fields,field_description:account_invoice_mode_monthly.field_res_partner____last_update +#: model:ir.model.fields,field_description:account_invoice_mode_monthly.field_sale_order____last_update +msgid "Last Modified on" +msgstr "" + +#. module: account_invoice_mode_monthly +#: model_terms:ir.ui.view,arch_db:account_invoice_mode_monthly.res_config_settings_view_form +msgid "Last executed on" +msgstr "" + +#. module: account_invoice_mode_monthly +#: model:ir.model.fields,field_description:account_invoice_mode_monthly.field_res_company__invoicing_mode_monthly_last_execution +#: model:ir.model.fields,field_description:account_invoice_mode_monthly.field_res_config_settings__invoicing_mode_monthly_last_execution +msgid "Last execution" +msgstr "" + +#. module: account_invoice_mode_monthly +#: model:ir.model.fields,help:account_invoice_mode_monthly.field_res_company__invoicing_mode_monthly_last_execution +#: model:ir.model.fields,help:account_invoice_mode_monthly.field_res_config_settings__invoicing_mode_monthly_last_execution +msgid "Last execution of monthly invoicing." +msgstr "" + +#. module: account_invoice_mode_monthly +#: model:ir.model.fields.selection,name:account_invoice_mode_monthly.selection__res_partner__invoicing_mode__monthly +msgid "Monthly" +msgstr "" + +#. module: account_invoice_mode_monthly +#: model:ir.model,name:account_invoice_mode_monthly.model_sale_order +msgid "Sales Order" +msgstr "" From 2ac2107f6fb3c5639a465287a388cd033373ff0e Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 4 Aug 2021 05:19:29 +0000 Subject: [PATCH 08/14] [UPD] README.rst --- account_invoice_mode_monthly/README.rst | 9 +++++ account_invoice_mode_monthly/__manifest__.py | 2 +- .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 35 +++++++++++------- 4 files changed, 32 insertions(+), 14 deletions(-) create mode 100644 account_invoice_mode_monthly/static/description/icon.png diff --git a/account_invoice_mode_monthly/README.rst b/account_invoice_mode_monthly/README.rst index 01613e99c6e..d636ccea9ec 100644 --- a/account_invoice_mode_monthly/README.rst +++ b/account_invoice_mode_monthly/README.rst @@ -35,6 +35,15 @@ invoiced .. contents:: :local: +Known issues / Roadmap +====================== + +On the version 13.0 PR there is some discussion about changing how the +`_company_monthly_invoicing_today` works by storing the next invoicing day +to simplify the computation. +Would it be an improvment or not ? +I answered with this `comment `_ + Bug Tracker =========== diff --git a/account_invoice_mode_monthly/__manifest__.py b/account_invoice_mode_monthly/__manifest__.py index ccd1928c8aa..8450fa36ef1 100644 --- a/account_invoice_mode_monthly/__manifest__.py +++ b/account_invoice_mode_monthly/__manifest__.py @@ -2,7 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { "name": "Account Invoice Mode Monthly", - "version": "14.0.1.0.0", + "version": "14.0.1.0.1", "summary": "Create invoices automatically on a monthly basis.", "author": "Camptocamp, Odoo Community Association (OCA)", "website": "https://github.com/OCA/account-invoicing", diff --git a/account_invoice_mode_monthly/static/description/icon.png b/account_invoice_mode_monthly/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/account_invoice_mode_monthly/static/description/index.html b/account_invoice_mode_monthly/static/description/index.html index 667179ce72e..58627afbb08 100644 --- a/account_invoice_mode_monthly/static/description/index.html +++ b/account_invoice_mode_monthly/static/description/index.html @@ -3,7 +3,7 @@ - + Account Invoice Mode Monthly