From d9ee1adc28fee36189b61da6c6edf4249da8b4bc Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Mon, 1 Sep 2025 01:08:00 +0000 Subject: [PATCH 1/5] [ADD] account_billing_from_cutoff --- account_billing_from_cutoff/README.rst | 95 ++++ account_billing_from_cutoff/__init__.py | 1 + account_billing_from_cutoff/__manifest__.py | 22 + .../readme/CONTRIBUTORS.rst | 2 + .../readme/DESCRIPTION.rst | 2 + account_billing_from_cutoff/readme/USAGE.rst | 4 + .../security/ir.model.access.csv | 2 + .../static/description/index.html | 446 ++++++++++++++++++ account_billing_from_cutoff/tests/__init__.py | 4 + .../tests/test_account_billing_from_cutoff.py | 140 ++++++ .../wizards/__init__.py | 1 + .../wizards/account_billing_cutoff.py | 78 +++ .../wizards/account_billing_cutoff.xml | 54 +++ 13 files changed, 851 insertions(+) create mode 100644 account_billing_from_cutoff/README.rst create mode 100644 account_billing_from_cutoff/__init__.py create mode 100644 account_billing_from_cutoff/__manifest__.py create mode 100644 account_billing_from_cutoff/readme/CONTRIBUTORS.rst create mode 100644 account_billing_from_cutoff/readme/DESCRIPTION.rst create mode 100644 account_billing_from_cutoff/readme/USAGE.rst create mode 100644 account_billing_from_cutoff/security/ir.model.access.csv create mode 100644 account_billing_from_cutoff/static/description/index.html create mode 100644 account_billing_from_cutoff/tests/__init__.py create mode 100644 account_billing_from_cutoff/tests/test_account_billing_from_cutoff.py create mode 100644 account_billing_from_cutoff/wizards/__init__.py create mode 100644 account_billing_from_cutoff/wizards/account_billing_cutoff.py create mode 100644 account_billing_from_cutoff/wizards/account_billing_cutoff.xml diff --git a/account_billing_from_cutoff/README.rst b/account_billing_from_cutoff/README.rst new file mode 100644 index 00000000..de86116b --- /dev/null +++ b/account_billing_from_cutoff/README.rst @@ -0,0 +1,95 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +=========================== +Account Billing From Cutoff +=========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:3bd2ac035fb32366e6528bd429c87079ed4698590167692e1f9a8ed091e726f6 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/license-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%2Fl10n--japan-lightgray.png?logo=github + :target: https://github.com/OCA/l10n-japan/tree/16.0/account_billing_from_cutoff + :alt: OCA/l10n-japan +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/l10n-japan-16-0/l10n-japan-16-0-account_billing_from_cutoff + :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/l10n-japan&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module creates billings for partners with Summary Invoicing enabled, based on the +cutoff date of their invoices. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To use this module, + +- Go to *Invoicing -> Customers or Vendors -> Create Billing From Cutoff* +- Set the cutoff date and click on *Create Billings* + +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 +~~~~~~~ + +* Quartile + +Contributors +~~~~~~~~~~~~ + +- Quartile \<\> + - Aung Ko Ko Lin + +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/l10n-japan `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_billing_from_cutoff/__init__.py b/account_billing_from_cutoff/__init__.py new file mode 100644 index 00000000..5cb1c491 --- /dev/null +++ b/account_billing_from_cutoff/__init__.py @@ -0,0 +1 @@ +from . import wizards diff --git a/account_billing_from_cutoff/__manifest__.py b/account_billing_from_cutoff/__manifest__.py new file mode 100644 index 00000000..9d16daf3 --- /dev/null +++ b/account_billing_from_cutoff/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2025 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Account Billing From Cutoff", + "version": "16.0.1.0.0", + "depends": [ + "account_billing", + "account_payment_term_cutoff_day", + "l10n_jp_summary_invoice", + ], + "author": "Quartile, Odoo Community Association (OCA)", + "license": "AGPL-3", + "website": "https://github.com/OCA/l10n-japan", + "category": "Accounting", + "data": [ + "security/ir.model.access.csv", + "wizards/account_billing_cutoff.xml", + ], + "development_status": "Alpha", + "installable": True, +} diff --git a/account_billing_from_cutoff/readme/CONTRIBUTORS.rst b/account_billing_from_cutoff/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..a8a2dfb2 --- /dev/null +++ b/account_billing_from_cutoff/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +- Quartile \<\> + - Aung Ko Ko Lin diff --git a/account_billing_from_cutoff/readme/DESCRIPTION.rst b/account_billing_from_cutoff/readme/DESCRIPTION.rst new file mode 100644 index 00000000..6ee8bda0 --- /dev/null +++ b/account_billing_from_cutoff/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module creates billings for partners with Summary Invoicing enabled, based on the +cutoff date of their invoices. diff --git a/account_billing_from_cutoff/readme/USAGE.rst b/account_billing_from_cutoff/readme/USAGE.rst new file mode 100644 index 00000000..f24a11f1 --- /dev/null +++ b/account_billing_from_cutoff/readme/USAGE.rst @@ -0,0 +1,4 @@ +To use this module, + +- Go to *Invoicing -> Customers or Vendors -> Create Billing From Cutoff* +- Set the cutoff date and click on *Create Billings* diff --git a/account_billing_from_cutoff/security/ir.model.access.csv b/account_billing_from_cutoff/security/ir.model.access.csv new file mode 100644 index 00000000..b7e55b41 --- /dev/null +++ b/account_billing_from_cutoff/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_wiz_account_billing_cutoff,access_wiz_account_billing_cutoff,model_wiz_account_billing_cutoff,account.group_account_invoice,1,1,1,1 diff --git a/account_billing_from_cutoff/static/description/index.html b/account_billing_from_cutoff/static/description/index.html new file mode 100644 index 00000000..318fbe96 --- /dev/null +++ b/account_billing_from_cutoff/static/description/index.html @@ -0,0 +1,446 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Account Billing From Cutoff

+ +

Alpha License: AGPL-3 OCA/l10n-japan Translate me on Weblate Try me on Runboat

+

This module creates billings for partners with Summary Invoicing enabled, based on the +cutoff date of their invoices.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Usage

+

To use this module,

+
    +
  • Go to Invoicing -> Customers or Vendors -> Create Billing From Cutoff
  • +
  • Set the cutoff date and click on Create Billings
  • +
+
+
+

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

+
    +
  • Quartile
  • +
+
+
+

Contributors

+ +
+
+

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/l10n-japan project on GitHub.

+

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

+
+
+
+
+ + diff --git a/account_billing_from_cutoff/tests/__init__.py b/account_billing_from_cutoff/tests/__init__.py new file mode 100644 index 00000000..cee3ad4d --- /dev/null +++ b/account_billing_from_cutoff/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2025 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_account_billing_from_cutoff diff --git a/account_billing_from_cutoff/tests/test_account_billing_from_cutoff.py b/account_billing_from_cutoff/tests/test_account_billing_from_cutoff.py new file mode 100644 index 00000000..5ace0684 --- /dev/null +++ b/account_billing_from_cutoff/tests/test_account_billing_from_cutoff.py @@ -0,0 +1,140 @@ +# Copyright 2025 Quartile +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import date + +from odoo import Command +from odoo.tests.common import TransactionCase + + +class TestBillingFromCutoff(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.invoice_model = cls.env["account.move"] + cls.partner_1 = cls.env["res.partner"].create({"name": "Test Partner 1"}) + cls.partner_2 = cls.env["res.partner"].create( + {"name": "Test Partner 2", "is_not_for_billing": True} + ) + cls.payment_term = cls.env.ref( + "account.account_payment_term_end_following_month" + ) + cls.payment_term.line_ids.write({"months": 1, "cutoff_day": 20}) + cls.product = cls.env.ref("product.product_product_4") + cls.currency_eur = cls.env.ref("base.EUR") + cls.currency_eur.active = True + cls.currency_usd = cls.env.ref("base.USD") + cls.currency_usd.active = True + cls.account_revenue = cls.env["account.account"].search( + [ + ("account_type", "=", "income"), + ("company_id", "=", cls.env.company.id), + ], + limit=1, + ) + + def create_invoice(self, partner, currency, invoice_date): + """Returns an open invoice""" + invoice = self.invoice_model.create( + { + "partner_id": partner.id, + "currency_id": currency.id, + "move_type": "out_invoice", + "invoice_date": invoice_date, + "invoice_payment_term_id": self.payment_term.id, + "invoice_line_ids": [ + Command.create( + { + "product_id": self.product.id, + "quantity": 1, + "price_unit": 100.00, + "name": "Test", + "account_id": self.account_revenue.id, + } + ) + ], + } + ) + invoice.action_post() + return invoice + + def _run_create_billing_wizard(self, cutoff): + wiz = ( + self.env["wiz.account.billing.cutoff"] + .with_context(default_bill_type="out_invoice") + .create({"cutoff_date": cutoff}) + ) + return wiz.action_create_billings() + + def _created_billing_ids(self, action): + dom = action.get("domain") or [] + for field, op, value in dom: + if field == "id" and op == "in": + return list(value) + return [] + + def test_billing_created_for_summary_partner(self): + inv_1 = self.create_invoice( + partner=self.partner_1, + currency=self.currency_usd, + invoice_date=date(2025, 9, 15), + ) + inv_2 = self.create_invoice( + partner=self.partner_2, + currency=self.currency_usd, + invoice_date=date(2025, 9, 15), + ) + inv_3 = self.create_invoice( + partner=self.partner_1, + currency=self.currency_usd, + invoice_date=date(2025, 9, 21), + ) + inv_4 = self.create_invoice( + partner=self.partner_2, + currency=self.currency_usd, + invoice_date=date(2025, 9, 21), + ) + inv_5 = self.create_invoice( + partner=self.partner_1, + currency=self.currency_eur, + invoice_date=date(2025, 9, 21), + ) + inv_6 = self.create_invoice( + partner=self.partner_2, + currency=self.currency_eur, + invoice_date=date(2025, 9, 21), + ) + # 1) cutoff = 2025-09-30 → only partner_1’s 2025-09-15 (inv_1) qualifies + action = self._run_create_billing_wizard(date(2025, 9, 30)) + billing_ids = self._created_billing_ids(action) + billings = self.env["account.billing"].browse(billing_ids) + self.assertEqual(len(billings), 1) + self.assertEqual(billings.billing_line_ids.mapped("move_id"), inv_1) + # 2) cutoff = 2025-10-31 → partner_1 USD inv_3 added to existing draft billing, + # partner_1 EUR inv_5 creates a new billing. 2 billings returned (1 updated + 1 new). + billing_usd = billings + action = self._run_create_billing_wizard(date(2025, 10, 31)) + billing_ids = self._created_billing_ids(action) + billings = self.env["account.billing"].browse(billing_ids) + self.assertEqual(len(billings), 2) + self.assertIn(billing_usd.id, billings.ids) + self.assertEqual(billing_usd.billing_line_ids.mapped("move_id"), inv_1 | inv_3) + billing_eur = billings - billing_usd + self.assertEqual(billing_eur.billing_line_ids.mapped("move_id"), inv_5) + # 3) Re-run same cutoff → nothing new (already billed and still in billed/draft states) + action = self._run_create_billing_wizard(date(2025, 10, 31)) + billing_ids = self._created_billing_ids(action) + billings = self.env["account.billing"].browse(billing_ids) + self.assertFalse(billings) + # 4) Cancel existing billings, include partner_2 in billing, rerun → 4 billings + # (p1 USD/EUR again because previous are cancelled; plus p2 USD/EUR) + self.env["account.billing"].search([]).write({"state": "cancel"}) + self.partner_2.is_not_for_billing = False + action = self._run_create_billing_wizard(date(2025, 10, 31)) + billing_ids = self._created_billing_ids(action) + billings = self.env["account.billing"].browse(billing_ids) + self.assertEqual(len(billings), 4) + self.assertEqual( + billings.billing_line_ids.mapped("move_id"), + inv_1 | inv_2 | inv_3 | inv_4 | inv_5 | inv_6, + ) diff --git a/account_billing_from_cutoff/wizards/__init__.py b/account_billing_from_cutoff/wizards/__init__.py new file mode 100644 index 00000000..3787a862 --- /dev/null +++ b/account_billing_from_cutoff/wizards/__init__.py @@ -0,0 +1 @@ +from . import account_billing_cutoff diff --git a/account_billing_from_cutoff/wizards/account_billing_cutoff.py b/account_billing_from_cutoff/wizards/account_billing_cutoff.py new file mode 100644 index 00000000..dc4a9906 --- /dev/null +++ b/account_billing_from_cutoff/wizards/account_billing_cutoff.py @@ -0,0 +1,78 @@ +# Copyright 2025 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from collections import defaultdict + +from odoo import fields, models + + +class AccountBillingCutoff(models.TransientModel): + _name = "wiz.account.billing.cutoff" + _description = "Account Billing From Cutoff" + + cutoff_date = fields.Date(required=True, default=fields.Date.context_today) + bill_type = fields.Selection( + selection=[("out_invoice", "Customer Invoice"), ("in_invoice", "Vendor Bill")], + readonly=True, + ) + + def _allowed_move_types(self): + if self.bill_type == "out_invoice": + return ["out_invoice", "out_refund", "out_receipt"] + return ["in_invoice", "in_refund", "in_receipt"] + + def _search_moves_domain(self): + billed_move_ids = ( + self.env["account.billing.line"] + .search( + [ + ("billing_id.state", "in", ["draft", "billed"]), + ] + ) + .mapped("move_id") + .ids + ) + return [ + ("id", "not in", billed_move_ids), + ("is_not_for_billing", "=", False), + ("company_id", "=", self.env.company.id), + ("state", "=", "posted"), + ("payment_state", "not in", ["paid", "reversed", "invoicing_legacy"]), + ("move_type", "in", self._allowed_move_types()), + ("cutoff_date", "<=", self.cutoff_date), + ] + + def action_create_billings(self): + moves = self.env["account.move"].search(self._search_moves_domain()) + if not moves: + return {"type": "ir.actions.act_window_close"} + groups = defaultdict(lambda: self.env["account.move"]) + for m in moves: + key = (m.partner_id.id, m.currency_id.id) + groups[key] |= m + billings = self.env["account.billing"] + for (partner_id, currency_id), recs in groups.items(): + partner = self.env["res.partner"].browse(partner_id) + existing_billing = self.env["account.billing"].search( + [ + ("partner_id", "=", partner_id), + ("currency_id", "=", currency_id), + ("bill_type", "=", recs._get_billing_type()), + ("state", "=", "draft"), + ], + limit=1, + ) + if existing_billing: + billing_line_dict = existing_billing._get_billing_line_dict(recs) + existing_billing.billing_line_ids.create(billing_line_dict) + billings |= existing_billing + else: + billings |= recs._create_billing(partner) + return { + "type": "ir.actions.act_window", + "name": "Billings", + "res_model": "account.billing", + "view_mode": "tree,form", + "domain": [("id", "in", billings.ids)], + "target": "current", + } diff --git a/account_billing_from_cutoff/wizards/account_billing_cutoff.xml b/account_billing_from_cutoff/wizards/account_billing_cutoff.xml new file mode 100644 index 00000000..79545066 --- /dev/null +++ b/account_billing_from_cutoff/wizards/account_billing_cutoff.xml @@ -0,0 +1,54 @@ + + + + wiz.account.billing.cutoff + wiz.account.billing.cutoff + +
+ + + + + + + + +
+
+
+
+
+ + Create Billing From Cutoff + wiz.account.billing.cutoff + form + {'default_bill_type':'out_invoice'} + new + + + Create Billing From Cutoff + wiz.account.billing.cutoff + form + {'default_bill_type':'in_invoice'} + new + + + +
From 623cde4065652d5098f978fd4c0f240c67bce683 Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Mon, 23 Feb 2026 13:43:20 +0000 Subject: [PATCH 2/5] [IMP] account_billing_from_cutoff: pre-commit auto fixes --- account_billing_from_cutoff/README.rst | 36 ++++++++-------- account_billing_from_cutoff/pyproject.toml | 3 ++ .../readme/CONTRIBUTORS.md | 2 + .../readme/CONTRIBUTORS.rst | 2 - .../{DESCRIPTION.rst => DESCRIPTION.md} | 4 +- account_billing_from_cutoff/readme/USAGE.md | 5 +++ account_billing_from_cutoff/readme/USAGE.rst | 4 -- .../static/description/index.html | 43 +++++++++---------- 8 files changed, 49 insertions(+), 50 deletions(-) create mode 100644 account_billing_from_cutoff/pyproject.toml create mode 100644 account_billing_from_cutoff/readme/CONTRIBUTORS.md delete mode 100644 account_billing_from_cutoff/readme/CONTRIBUTORS.rst rename account_billing_from_cutoff/readme/{DESCRIPTION.rst => DESCRIPTION.md} (54%) create mode 100644 account_billing_from_cutoff/readme/USAGE.md delete mode 100644 account_billing_from_cutoff/readme/USAGE.rst diff --git a/account_billing_from_cutoff/README.rst b/account_billing_from_cutoff/README.rst index de86116b..2e06214e 100644 --- a/account_billing_from_cutoff/README.rst +++ b/account_billing_from_cutoff/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - =========================== Account Billing From Cutoff =========================== @@ -17,23 +13,23 @@ Account Billing From Cutoff .. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png :target: https://odoo-community.org/page/development-status :alt: Alpha -.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png +.. |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%2Fl10n--japan-lightgray.png?logo=github - :target: https://github.com/OCA/l10n-japan/tree/16.0/account_billing_from_cutoff + :target: https://github.com/OCA/l10n-japan/tree/18.0/account_billing_from_cutoff :alt: OCA/l10n-japan .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/l10n-japan-16-0/l10n-japan-16-0-account_billing_from_cutoff + :target: https://translation.odoo-community.org/projects/l10n-japan-18-0/l10n-japan-18-0-account_billing_from_cutoff :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/l10n-japan&target_branch=16.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/l10n-japan&target_branch=18.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| -This module creates billings for partners with Summary Invoicing enabled, based on the -cutoff date of their invoices. +This module creates billings for partners with Summary Invoicing +enabled, based on the cutoff date of their invoices. .. IMPORTANT:: This is an alpha version, the data model and design can change at any time without warning. @@ -50,8 +46,9 @@ Usage To use this module, -- Go to *Invoicing -> Customers or Vendors -> Create Billing From Cutoff* -- Set the cutoff date and click on *Create Billings* +- Go to *Invoicing -> Customers or Vendors -> Create Billing From + Cutoff* +- Set the cutoff date and click on *Create Billings* Bug Tracker =========== @@ -59,7 +56,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -67,18 +64,19 @@ Credits ======= Authors -~~~~~~~ +------- * Quartile Contributors -~~~~~~~~~~~~ +------------ + +- Quartile <> -- Quartile \<\> - - Aung Ko Ko Lin + - Aung Ko Ko Lin Maintainers -~~~~~~~~~~~ +----------- This module is maintained by the OCA. @@ -90,6 +88,6 @@ 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/l10n-japan `_ project on GitHub. +This module is part of the `OCA/l10n-japan `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_billing_from_cutoff/pyproject.toml b/account_billing_from_cutoff/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/account_billing_from_cutoff/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/account_billing_from_cutoff/readme/CONTRIBUTORS.md b/account_billing_from_cutoff/readme/CONTRIBUTORS.md new file mode 100644 index 00000000..791c4fd3 --- /dev/null +++ b/account_billing_from_cutoff/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Quartile \<\<\>\> + - Aung Ko Ko Lin diff --git a/account_billing_from_cutoff/readme/CONTRIBUTORS.rst b/account_billing_from_cutoff/readme/CONTRIBUTORS.rst deleted file mode 100644 index a8a2dfb2..00000000 --- a/account_billing_from_cutoff/readme/CONTRIBUTORS.rst +++ /dev/null @@ -1,2 +0,0 @@ -- Quartile \<\> - - Aung Ko Ko Lin diff --git a/account_billing_from_cutoff/readme/DESCRIPTION.rst b/account_billing_from_cutoff/readme/DESCRIPTION.md similarity index 54% rename from account_billing_from_cutoff/readme/DESCRIPTION.rst rename to account_billing_from_cutoff/readme/DESCRIPTION.md index 6ee8bda0..c10555e1 100644 --- a/account_billing_from_cutoff/readme/DESCRIPTION.rst +++ b/account_billing_from_cutoff/readme/DESCRIPTION.md @@ -1,2 +1,2 @@ -This module creates billings for partners with Summary Invoicing enabled, based on the -cutoff date of their invoices. +This module creates billings for partners with Summary Invoicing +enabled, based on the cutoff date of their invoices. diff --git a/account_billing_from_cutoff/readme/USAGE.md b/account_billing_from_cutoff/readme/USAGE.md new file mode 100644 index 00000000..7f706bf3 --- /dev/null +++ b/account_billing_from_cutoff/readme/USAGE.md @@ -0,0 +1,5 @@ +To use this module, + +- Go to *Invoicing -\> Customers or Vendors -\> Create Billing From + Cutoff* +- Set the cutoff date and click on *Create Billings* diff --git a/account_billing_from_cutoff/readme/USAGE.rst b/account_billing_from_cutoff/readme/USAGE.rst deleted file mode 100644 index f24a11f1..00000000 --- a/account_billing_from_cutoff/readme/USAGE.rst +++ /dev/null @@ -1,4 +0,0 @@ -To use this module, - -- Go to *Invoicing -> Customers or Vendors -> Create Billing From Cutoff* -- Set the cutoff date and click on *Create Billings* diff --git a/account_billing_from_cutoff/static/description/index.html b/account_billing_from_cutoff/static/description/index.html index 318fbe96..fca7712b 100644 --- a/account_billing_from_cutoff/static/description/index.html +++ b/account_billing_from_cutoff/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Account Billing From Cutoff -
+
+

Account Billing From Cutoff

- - -Odoo Community Association - -
-

Account Billing From Cutoff

-

Alpha License: AGPL-3 OCA/l10n-japan Translate me on Weblate Try me on Runboat

-

This module creates billings for partners with Summary Invoicing enabled, based on the -cutoff date of their invoices.

+

Alpha License: AGPL-3 OCA/l10n-japan Translate me on Weblate Try me on Runboat

+

This module creates billings for partners with Summary Invoicing +enabled, based on the cutoff date of their invoices.

Important

This is an alpha version, the data model and design can change at any time without warning. @@ -397,38 +392,41 @@

Account Billing From Cutoff

-

Usage

+

Usage

To use this module,

    -
  • Go to Invoicing -> Customers or Vendors -> Create Billing From Cutoff
  • +
  • Go to Invoicing -> Customers or Vendors -> Create Billing From +Cutoff
  • Set the cutoff date and click on Create Billings
-

Bug Tracker

+

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.

+feedback.

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Quartile
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -436,11 +434,10 @@

Maintainers

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/l10n-japan project on GitHub.

+

This module is part of the OCA/l10n-japan project on GitHub.

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

-
From 14353a908f9ff4e118e286b6ed5458033eb3a113 Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Mon, 23 Feb 2026 14:11:29 +0000 Subject: [PATCH 3/5] [MIG] account_billing_from_cutoff: migration to 18.0 --- account_billing_from_cutoff/README.rst | 14 +++++++++++++- account_billing_from_cutoff/__manifest__.py | 4 +++- .../readme/CONTRIBUTORS.md | 3 ++- .../static/description/index.html | 5 ++++- .../tests/test_account_billing_from_cutoff.py | 14 +++++++++----- .../wizards/account_billing_cutoff.py | 16 ++++++++-------- 6 files changed, 39 insertions(+), 17 deletions(-) diff --git a/account_billing_from_cutoff/README.rst b/account_billing_from_cutoff/README.rst index 2e06214e..72bbc5ae 100644 --- a/account_billing_from_cutoff/README.rst +++ b/account_billing_from_cutoff/README.rst @@ -71,9 +71,10 @@ Authors Contributors ------------ -- Quartile <> +- Quartile - Aung Ko Ko Lin + - Yoshi Tashiro Maintainers ----------- @@ -88,6 +89,17 @@ 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. +.. |maintainer-yostashiro| image:: https://github.com/yostashiro.png?size=40px + :target: https://github.com/yostashiro + :alt: yostashiro +.. |maintainer-aungkokolin1997| image:: https://github.com/aungkokolin1997.png?size=40px + :target: https://github.com/aungkokolin1997 + :alt: aungkokolin1997 + +Current `maintainers `__: + +|maintainer-yostashiro| |maintainer-aungkokolin1997| + This module is part of the `OCA/l10n-japan `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_billing_from_cutoff/__manifest__.py b/account_billing_from_cutoff/__manifest__.py index 9d16daf3..80392618 100644 --- a/account_billing_from_cutoff/__manifest__.py +++ b/account_billing_from_cutoff/__manifest__.py @@ -3,7 +3,8 @@ { "name": "Account Billing From Cutoff", - "version": "16.0.1.0.0", + "summary": "Create billings based on invoice cutoff dates", + "version": "18.0.1.0.0", "depends": [ "account_billing", "account_payment_term_cutoff_day", @@ -18,5 +19,6 @@ "wizards/account_billing_cutoff.xml", ], "development_status": "Alpha", + "maintainers": ["yostashiro", "aungkokolin1997"], "installable": True, } diff --git a/account_billing_from_cutoff/readme/CONTRIBUTORS.md b/account_billing_from_cutoff/readme/CONTRIBUTORS.md index 791c4fd3..f6444ad9 100644 --- a/account_billing_from_cutoff/readme/CONTRIBUTORS.md +++ b/account_billing_from_cutoff/readme/CONTRIBUTORS.md @@ -1,2 +1,3 @@ -- Quartile \<\<\>\> +- Quartile \<\> - Aung Ko Ko Lin + - Yoshi Tashiro diff --git a/account_billing_from_cutoff/static/description/index.html b/account_billing_from_cutoff/static/description/index.html index fca7712b..dcf53d83 100644 --- a/account_billing_from_cutoff/static/description/index.html +++ b/account_billing_from_cutoff/static/description/index.html @@ -419,8 +419,9 @@

Authors

Contributors

diff --git a/account_billing_from_cutoff/tests/test_account_billing_from_cutoff.py b/account_billing_from_cutoff/tests/test_account_billing_from_cutoff.py index 5ace0684..5b9dc5f2 100644 --- a/account_billing_from_cutoff/tests/test_account_billing_from_cutoff.py +++ b/account_billing_from_cutoff/tests/test_account_billing_from_cutoff.py @@ -19,7 +19,9 @@ def setUpClass(cls): cls.payment_term = cls.env.ref( "account.account_payment_term_end_following_month" ) - cls.payment_term.line_ids.write({"months": 1, "cutoff_day": 20}) + cls.payment_term.line_ids.write( + {"has_cutoff_day": True, "months": 1, "cutoff_day": 20} + ) cls.product = cls.env.ref("product.product_product_4") cls.currency_eur = cls.env.ref("base.EUR") cls.currency_eur.active = True @@ -28,7 +30,7 @@ def setUpClass(cls): cls.account_revenue = cls.env["account.account"].search( [ ("account_type", "=", "income"), - ("company_id", "=", cls.env.company.id), + ("company_ids", "in", cls.env.company.id), ], limit=1, ) @@ -111,7 +113,8 @@ def test_billing_created_for_summary_partner(self): self.assertEqual(len(billings), 1) self.assertEqual(billings.billing_line_ids.mapped("move_id"), inv_1) # 2) cutoff = 2025-10-31 → partner_1 USD inv_3 added to existing draft billing, - # partner_1 EUR inv_5 creates a new billing. 2 billings returned (1 updated + 1 new). + # partner_1 EUR inv_5 creates a new billing. 2 billings returned + # (1 updated + 1 new). billing_usd = billings action = self._run_create_billing_wizard(date(2025, 10, 31)) billing_ids = self._created_billing_ids(action) @@ -121,7 +124,8 @@ def test_billing_created_for_summary_partner(self): self.assertEqual(billing_usd.billing_line_ids.mapped("move_id"), inv_1 | inv_3) billing_eur = billings - billing_usd self.assertEqual(billing_eur.billing_line_ids.mapped("move_id"), inv_5) - # 3) Re-run same cutoff → nothing new (already billed and still in billed/draft states) + # 3) Re-run same cutoff → nothing new + # (already billed and still in billed/draft states) action = self._run_create_billing_wizard(date(2025, 10, 31)) billing_ids = self._created_billing_ids(action) billings = self.env["account.billing"].browse(billing_ids) @@ -129,7 +133,7 @@ def test_billing_created_for_summary_partner(self): # 4) Cancel existing billings, include partner_2 in billing, rerun → 4 billings # (p1 USD/EUR again because previous are cancelled; plus p2 USD/EUR) self.env["account.billing"].search([]).write({"state": "cancel"}) - self.partner_2.is_not_for_billing = False + (inv_2 | inv_4 | inv_6).is_not_for_billing = False action = self._run_create_billing_wizard(date(2025, 10, 31)) billing_ids = self._created_billing_ids(action) billings = self.env["account.billing"].browse(billing_ids) diff --git a/account_billing_from_cutoff/wizards/account_billing_cutoff.py b/account_billing_from_cutoff/wizards/account_billing_cutoff.py index dc4a9906..c18857d6 100644 --- a/account_billing_from_cutoff/wizards/account_billing_cutoff.py +++ b/account_billing_from_cutoff/wizards/account_billing_cutoff.py @@ -68,11 +68,11 @@ def action_create_billings(self): billings |= existing_billing else: billings |= recs._create_billing(partner) - return { - "type": "ir.actions.act_window", - "name": "Billings", - "res_model": "account.billing", - "view_mode": "tree,form", - "domain": [("id", "in", billings.ids)], - "target": "current", - } + xml_id = ( + "account_billing.action_customer_billing" + if self.bill_type == "out_invoice" + else "account_billing.action_supplier_billing" + ) + action = self.env["ir.actions.act_window"]._for_xml_id(xml_id) + action["domain"] = [("id", "in", billings.ids)] + return action From 90ba8bf021d5dc849de65d30a2d3cc7f9a328bf1 Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Thu, 13 Aug 2026 02:46:13 +0000 Subject: [PATCH 4/5] [FIX] account_billing_from_cutoff: set threshold date from cutoff and match billing by bank - Group moves by remit-to bank and restrict the existing-billing lookup to the current company, so invoices with different partner banks are not merged. - Set threshold_date from the wizard's cutoff date on new billings, and raise it on an updated billing, so validation is not blocked by lines dated after it. - Only reuse a billing that is based on invoice dates, as the cutoff date is derived from the invoice date. The threshold date handling is documented in the usage section. --- account_billing_from_cutoff/README.rst | 12 ++++ account_billing_from_cutoff/readme/USAGE.md | 12 ++++ .../static/description/index.html | 12 ++++ .../tests/test_account_billing_from_cutoff.py | 56 +++++++++++++++++++ .../wizards/account_billing_cutoff.py | 18 +++++- 5 files changed, 107 insertions(+), 3 deletions(-) diff --git a/account_billing_from_cutoff/README.rst b/account_billing_from_cutoff/README.rst index 72bbc5ae..fe5d6a5a 100644 --- a/account_billing_from_cutoff/README.rst +++ b/account_billing_from_cutoff/README.rst @@ -50,6 +50,18 @@ To use this module, Cutoff* - Set the cutoff date and click on *Create Billings* +The cutoff date you enter becomes the threshold date of the billing: + +- A new billing is created with *Invoice Date* as its threshold date + type and the cutoff date as its threshold date. +- When the invoices are added to an existing draft billing, its + threshold date is moved forward to the cutoff date. A threshold date + that is already later is kept, so that it is never lowered. +- A draft billing whose threshold date type is *Due Date* is not + reused. As the cutoff date is derived from the invoice date, applying + it to such a billing would leave its threshold date earlier than the + due dates of its lines and prevent the billing from being validated. + Bug Tracker =========== diff --git a/account_billing_from_cutoff/readme/USAGE.md b/account_billing_from_cutoff/readme/USAGE.md index 7f706bf3..4e1e9dd1 100644 --- a/account_billing_from_cutoff/readme/USAGE.md +++ b/account_billing_from_cutoff/readme/USAGE.md @@ -3,3 +3,15 @@ To use this module, - Go to *Invoicing -\> Customers or Vendors -\> Create Billing From Cutoff* - Set the cutoff date and click on *Create Billings* + +The cutoff date you enter becomes the threshold date of the billing: + +- A new billing is created with *Invoice Date* as its threshold date + type and the cutoff date as its threshold date. +- When the invoices are added to an existing draft billing, its + threshold date is moved forward to the cutoff date. A threshold date + that is already later is kept, so that it is never lowered. +- A draft billing whose threshold date type is *Due Date* is not reused. + As the cutoff date is derived from the invoice date, applying it to + such a billing would leave its threshold date earlier than the due + dates of its lines and prevent the billing from being validated. diff --git a/account_billing_from_cutoff/static/description/index.html b/account_billing_from_cutoff/static/description/index.html index dcf53d83..c92b1568 100644 --- a/account_billing_from_cutoff/static/description/index.html +++ b/account_billing_from_cutoff/static/description/index.html @@ -399,6 +399,18 @@

Usage

Cutoff
  • Set the cutoff date and click on Create Billings
  • +

    The cutoff date you enter becomes the threshold date of the billing:

    +
      +
    • A new billing is created with Invoice Date as its threshold date +type and the cutoff date as its threshold date.
    • +
    • When the invoices are added to an existing draft billing, its +threshold date is moved forward to the cutoff date. A threshold date +that is already later is kept, so that it is never lowered.
    • +
    • A draft billing whose threshold date type is Due Date is not +reused. As the cutoff date is derived from the invoice date, applying +it to such a billing would leave its threshold date earlier than the +due dates of its lines and prevent the billing from being validated.
    • +

    Bug Tracker

    diff --git a/account_billing_from_cutoff/tests/test_account_billing_from_cutoff.py b/account_billing_from_cutoff/tests/test_account_billing_from_cutoff.py index 5b9dc5f2..a38a30aa 100644 --- a/account_billing_from_cutoff/tests/test_account_billing_from_cutoff.py +++ b/account_billing_from_cutoff/tests/test_account_billing_from_cutoff.py @@ -112,6 +112,7 @@ def test_billing_created_for_summary_partner(self): billings = self.env["account.billing"].browse(billing_ids) self.assertEqual(len(billings), 1) self.assertEqual(billings.billing_line_ids.mapped("move_id"), inv_1) + self.assertEqual(billings.threshold_date, date(2025, 9, 30)) # 2) cutoff = 2025-10-31 → partner_1 USD inv_3 added to existing draft billing, # partner_1 EUR inv_5 creates a new billing. 2 billings returned # (1 updated + 1 new). @@ -122,8 +123,10 @@ def test_billing_created_for_summary_partner(self): self.assertEqual(len(billings), 2) self.assertIn(billing_usd.id, billings.ids) self.assertEqual(billing_usd.billing_line_ids.mapped("move_id"), inv_1 | inv_3) + self.assertEqual(billing_usd.threshold_date, date(2025, 10, 31)) billing_eur = billings - billing_usd self.assertEqual(billing_eur.billing_line_ids.mapped("move_id"), inv_5) + self.assertEqual(billing_eur.threshold_date, date(2025, 10, 31)) # 3) Re-run same cutoff → nothing new # (already billed and still in billed/draft states) action = self._run_create_billing_wizard(date(2025, 10, 31)) @@ -142,3 +145,56 @@ def test_billing_created_for_summary_partner(self): billings.billing_line_ids.mapped("move_id"), inv_1 | inv_2 | inv_3 | inv_4 | inv_5 | inv_6, ) + for billing in billings: + billing.validate_billing() + + def test_billing_on_due_date_is_not_reused(self): + inv_1 = self.create_invoice( + partner=self.partner_1, + currency=self.currency_usd, + invoice_date=date(2025, 9, 15), + ) + action = self._run_create_billing_wizard(date(2025, 9, 30)) + billing_1 = self.env["account.billing"].browse( + self._created_billing_ids(action) + ) + self.assertEqual(billing_1.threshold_date_type, "invoice_date") + billing_1.threshold_date_type = "invoice_date_due" + + inv_2 = self.create_invoice( + partner=self.partner_1, + currency=self.currency_usd, + invoice_date=date(2025, 9, 21), + ) + action = self._run_create_billing_wizard(date(2025, 10, 31)) + billing_2 = self.env["account.billing"].browse( + self._created_billing_ids(action) + ) + # The draft billing based on due dates is left untouched, and a new billing + # is created for the moves selected from the cutoff date. + self.assertNotIn(billing_1.id, billing_2.ids) + self.assertEqual(billing_1.billing_line_ids.mapped("move_id"), inv_1) + self.assertEqual(billing_1.threshold_date, date(2025, 9, 30)) + self.assertEqual(billing_2.billing_line_ids.mapped("move_id"), inv_2) + self.assertEqual(billing_2.threshold_date, date(2025, 10, 31)) + + def test_append_does_not_lower_threshold_date(self): + inv_1 = self.create_invoice( + partner=self.partner_1, + currency=self.currency_usd, + invoice_date=date(2025, 9, 15), + ) + action = self._run_create_billing_wizard(date(2025, 9, 30)) + billing = self.env["account.billing"].browse(self._created_billing_ids(action)) + billing.threshold_date = date(2025, 11, 30) + + inv_2 = self.create_invoice( + partner=self.partner_1, + currency=self.currency_usd, + invoice_date=date(2025, 9, 21), + ) + self._run_create_billing_wizard(date(2025, 10, 31)) + + self.assertEqual(billing.billing_line_ids.mapped("move_id"), inv_1 | inv_2) + self.assertEqual(billing.threshold_date, date(2025, 11, 30)) + billing.validate_billing() diff --git a/account_billing_from_cutoff/wizards/account_billing_cutoff.py b/account_billing_from_cutoff/wizards/account_billing_cutoff.py index c18857d6..5d526734 100644 --- a/account_billing_from_cutoff/wizards/account_billing_cutoff.py +++ b/account_billing_from_cutoff/wizards/account_billing_cutoff.py @@ -48,26 +48,38 @@ def action_create_billings(self): return {"type": "ir.actions.act_window_close"} groups = defaultdict(lambda: self.env["account.move"]) for m in moves: - key = (m.partner_id.id, m.currency_id.id) + key = (m.partner_id.id, m.currency_id.id, m.partner_bank_id.id) groups[key] |= m billings = self.env["account.billing"] - for (partner_id, currency_id), recs in groups.items(): + for (partner_id, currency_id, partner_bank_id), recs in groups.items(): partner = self.env["res.partner"].browse(partner_id) existing_billing = self.env["account.billing"].search( [ ("partner_id", "=", partner_id), ("currency_id", "=", currency_id), + ("remit_to_bank_id", "=", partner_bank_id), + ("company_id", "=", self.env.company.id), ("bill_type", "=", recs._get_billing_type()), + # The cutoff date is derived from the invoice date, so a billing + # based on due dates cannot be extended with the moves selected + # here without making its threshold date inconsistent. + ("threshold_date_type", "=", "invoice_date"), ("state", "=", "draft"), ], limit=1, ) if existing_billing: + existing_billing.threshold_date = max( + existing_billing.threshold_date, self.cutoff_date + ) billing_line_dict = existing_billing._get_billing_line_dict(recs) existing_billing.billing_line_ids.create(billing_line_dict) billings |= existing_billing else: - billings |= recs._create_billing(partner) + billings |= recs.with_context( + default_threshold_date=self.cutoff_date, + default_threshold_date_type="invoice_date", + )._create_billing(partner) xml_id = ( "account_billing.action_customer_billing" if self.bill_type == "out_invoice" From ac9473193244daa9c5f560442f858f1c055ddb3b Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Thu, 13 Aug 2026 02:46:13 +0000 Subject: [PATCH 5/5] [IMP] account_billing_from_cutoff: sort billing lines and require bill_type Keep the line order of an updated billing consistent with manually created billings, and make bill_type required as it is always set from the action context. --- account_billing_from_cutoff/wizards/account_billing_cutoff.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/account_billing_from_cutoff/wizards/account_billing_cutoff.py b/account_billing_from_cutoff/wizards/account_billing_cutoff.py index 5d526734..a1c7f70c 100644 --- a/account_billing_from_cutoff/wizards/account_billing_cutoff.py +++ b/account_billing_from_cutoff/wizards/account_billing_cutoff.py @@ -13,6 +13,7 @@ class AccountBillingCutoff(models.TransientModel): cutoff_date = fields.Date(required=True, default=fields.Date.context_today) bill_type = fields.Selection( selection=[("out_invoice", "Customer Invoice"), ("in_invoice", "Vendor Bill")], + required=True, readonly=True, ) @@ -74,6 +75,7 @@ def action_create_billings(self): ) billing_line_dict = existing_billing._get_billing_line_dict(recs) existing_billing.billing_line_ids.create(billing_line_dict) + existing_billing._sort_billing_lines() billings |= existing_billing else: billings |= recs.with_context(