From 69b83240d31cc6a6e59fe097a0b9e8d37721d770 Mon Sep 17 00:00:00 2001
From: kanda999
Date: Thu, 4 Jun 2026 03:34:50 +0000
Subject: [PATCH 1/3] [ADD] purchase_auto_bill_on_receipt
---
purchase_auto_bill_on_receipt/README.rst | 111 +++++
purchase_auto_bill_on_receipt/__init__.py | 1 +
purchase_auto_bill_on_receipt/__manifest__.py | 20 +
.../models/__init__.py | 5 +
.../models/product_category.py | 13 +
.../models/product_template.py | 17 +
.../models/purchase_order.py | 105 ++++
.../models/purchase_order_line.py | 17 +
.../models/stock_picking.py | 23 +
purchase_auto_bill_on_receipt/pyproject.toml | 3 +
.../readme/CONFIGURE.md | 12 +
.../readme/CONTRIBUTORS.md | 3 +
.../readme/DESCRIPTION.md | 2 +
.../static/description/index.html | 453 ++++++++++++++++++
.../tests/__init__.py | 1 +
.../tests/test_auto_bill_on_receipt.py | 221 +++++++++
.../views/product_category_views.xml | 14 +
.../views/product_template_views.xml | 12 +
.../views/purchase_order_views.xml | 12 +
19 files changed, 1045 insertions(+)
create mode 100644 purchase_auto_bill_on_receipt/README.rst
create mode 100644 purchase_auto_bill_on_receipt/__init__.py
create mode 100644 purchase_auto_bill_on_receipt/__manifest__.py
create mode 100644 purchase_auto_bill_on_receipt/models/__init__.py
create mode 100644 purchase_auto_bill_on_receipt/models/product_category.py
create mode 100644 purchase_auto_bill_on_receipt/models/product_template.py
create mode 100644 purchase_auto_bill_on_receipt/models/purchase_order.py
create mode 100644 purchase_auto_bill_on_receipt/models/purchase_order_line.py
create mode 100644 purchase_auto_bill_on_receipt/models/stock_picking.py
create mode 100644 purchase_auto_bill_on_receipt/pyproject.toml
create mode 100644 purchase_auto_bill_on_receipt/readme/CONFIGURE.md
create mode 100644 purchase_auto_bill_on_receipt/readme/CONTRIBUTORS.md
create mode 100644 purchase_auto_bill_on_receipt/readme/DESCRIPTION.md
create mode 100644 purchase_auto_bill_on_receipt/static/description/index.html
create mode 100644 purchase_auto_bill_on_receipt/tests/__init__.py
create mode 100644 purchase_auto_bill_on_receipt/tests/test_auto_bill_on_receipt.py
create mode 100644 purchase_auto_bill_on_receipt/views/product_category_views.xml
create mode 100644 purchase_auto_bill_on_receipt/views/product_template_views.xml
create mode 100644 purchase_auto_bill_on_receipt/views/purchase_order_views.xml
diff --git a/purchase_auto_bill_on_receipt/README.rst b/purchase_auto_bill_on_receipt/README.rst
new file mode 100644
index 0000000..9dffd4e
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/README.rst
@@ -0,0 +1,111 @@
+.. image:: https://odoo-community.org/readme-banner-image
+ :target: https://odoo-community.org/get-involved?utm_source=readme
+ :alt: Odoo Community Association
+
+=============================
+Purchase Auto Bill on Receipt
+=============================
+
+..
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! source digest: sha256:137338ab34ea871f820d22f70aaf33c322560ab59cfd7f7425b56ee6b4aacdf8
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |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/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%2Fpurchase--workflow-lightgray.png?logo=github
+ :target: https://github.com/OCA/purchase-workflow/tree/19.0/purchase_auto_bill_on_receipt
+ :alt: OCA/purchase-workflow
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+ :target: https://translation.odoo-community.org/projects/purchase-workflow-19-0/purchase-workflow-19-0-purchase_auto_bill_on_receipt
+ :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/purchase-workflow&target_branch=19.0
+ :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+This module automatically creates and posts a Vendor Bill when a
+purchase receipt is validated.
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Configuration
+=============
+
+Three configuration levels control whether a Purchase Order line is
+auto-billed when the related receipt is validated. They are evaluated
+per line at receipt time:
+
+1. **Product Category** → *Auto Bill on Receipt* (boolean). Sets the
+ baseline for all products in the category.
+2. **Product** → *Auto Bill on Receipt* (selection: *Auto* / *No Auto* /
+ empty). Leave empty to inherit from the product category. *Auto* or
+ *No Auto* overrides the category setting for this product.
+3. **Purchase Order** → *Block Auto Bill* (boolean, on the *Other
+ Information* tab). When ticked, suppresses auto-billing for the whole
+ order regardless of product or category settings.
+
+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
+ - Yoshi Tashiro
+
+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.
+
+.. |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/purchase-workflow `_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/purchase_auto_bill_on_receipt/__init__.py b/purchase_auto_bill_on_receipt/__init__.py
new file mode 100644
index 0000000..0650744
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/purchase_auto_bill_on_receipt/__manifest__.py b/purchase_auto_bill_on_receipt/__manifest__.py
new file mode 100644
index 0000000..bcb0dca
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/__manifest__.py
@@ -0,0 +1,20 @@
+# Copyright 2026 Quartile
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
+
+{
+ "name": "Purchase Auto Bill on Receipt",
+ "summary": "Automatically create and post Vendor Bills when receipts are validated",
+ "version": "19.0.1.0.0",
+ "author": "Quartile, Odoo Community Association (OCA)",
+ "website": "https://github.com/OCA/purchase-workflow",
+ "license": "AGPL-3",
+ "category": "Purchase Management",
+ "depends": ["purchase_stock", "account"],
+ "data": [
+ "views/product_category_views.xml",
+ "views/product_template_views.xml",
+ "views/purchase_order_views.xml",
+ ],
+ "maintainers": ["yostashiro", "aungkokolin1997"],
+ "installable": True,
+}
diff --git a/purchase_auto_bill_on_receipt/models/__init__.py b/purchase_auto_bill_on_receipt/models/__init__.py
new file mode 100644
index 0000000..53160e1
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/models/__init__.py
@@ -0,0 +1,5 @@
+from . import product_category
+from . import product_template
+from . import purchase_order
+from . import purchase_order_line
+from . import stock_picking
diff --git a/purchase_auto_bill_on_receipt/models/product_category.py b/purchase_auto_bill_on_receipt/models/product_category.py
new file mode 100644
index 0000000..ea3b35f
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/models/product_category.py
@@ -0,0 +1,13 @@
+# Copyright 2026 Quartile (https://www.quartile.co)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
+
+from odoo import fields, models
+
+
+class ProductCategory(models.Model):
+ _inherit = "product.category"
+
+ auto_bill_on_receipt = fields.Boolean(
+ help="When enabled, products in this category will automatically "
+ "generate a Vendor Bill when goods are received.",
+ )
diff --git a/purchase_auto_bill_on_receipt/models/product_template.py b/purchase_auto_bill_on_receipt/models/product_template.py
new file mode 100644
index 0000000..e3aa432
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/models/product_template.py
@@ -0,0 +1,17 @@
+# Copyright 2026 Quartile (https://www.quartile.co)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
+
+from odoo import fields, models
+
+
+class ProductTemplate(models.Model):
+ _inherit = "product.template"
+
+ auto_bill_on_receipt = fields.Selection(
+ selection=[
+ ("auto", "Auto Bill on Receipt"),
+ ("no_auto", "No Auto Bill"),
+ ],
+ help="Override the product category setting. "
+ "Leave empty to inherit from the product category.",
+ )
diff --git a/purchase_auto_bill_on_receipt/models/purchase_order.py b/purchase_auto_bill_on_receipt/models/purchase_order.py
new file mode 100644
index 0000000..83dce7d
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/models/purchase_order.py
@@ -0,0 +1,105 @@
+# Copyright 2026 Quartile (https://www.quartile.co)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
+
+import logging
+
+from odoo import Command, fields, models
+
+_logger = logging.getLogger(__name__)
+
+
+class PurchaseOrder(models.Model):
+ _inherit = "purchase.order"
+
+ block_auto_bill = fields.Boolean(
+ help="When enabled, suppresses automatic bill creation on receipt "
+ "for this order, regardless of product or category settings.",
+ )
+
+ def _auto_bill_for_picking(self, picking):
+ self.ensure_one()
+ received_products = picking.move_ids.product_id
+ eligible_lines = self.order_line.filtered(
+ lambda line: line.product_id in received_products
+ and line._auto_bill_eligible()
+ and line.qty_to_invoice > 0
+ )
+ if not eligible_lines:
+ return self.env["account.move"]
+ try:
+ bill = self._auto_bill_create(picking, eligible_lines)
+ except Exception as e:
+ _logger.exception(
+ "Auto-bill creation failed for PO %s / picking %s",
+ self.name,
+ picking.name,
+ )
+ self._auto_bill_log_failure(picking, "create", e)
+ return self.env["account.move"]
+ try:
+ bill.action_post()
+ except Exception as e:
+ _logger.exception(
+ "Auto-bill posting failed for PO %s / picking %s / bill %s",
+ self.name,
+ picking.name,
+ bill.name,
+ )
+ self._auto_bill_log_failure(picking, "post", e, bill=bill)
+ return bill
+
+ def _auto_bill_create(self, picking, eligible_lines):
+ self.ensure_one()
+ invoice_vals = self._prepare_invoice()
+ invoice_vals["invoice_date"] = picking.date_done.date()
+ invoice_lines = []
+ sequence = 10
+ pending_section = None
+ for line in self.order_line:
+ if line.display_type in ("line_section", "line_subsection"):
+ pending_section = line
+ continue
+ if line not in eligible_lines:
+ continue
+ if pending_section:
+ section_vals = pending_section._prepare_account_move_line()
+ section_vals["sequence"] = sequence
+ invoice_lines.append(Command.create(section_vals))
+ sequence += 1
+ pending_section = None
+ line_vals = line._prepare_account_move_line()
+ line_vals["sequence"] = sequence
+ invoice_lines.append(Command.create(line_vals))
+ sequence += 1
+ invoice_vals["invoice_line_ids"] = invoice_lines
+ return (
+ self.env["account.move"]
+ .with_company(self.company_id)
+ .with_context(default_move_type="in_invoice")
+ .create(invoice_vals)
+ )
+
+ def _auto_bill_log_failure(self, picking, step, exception, bill=False):
+ self.ensure_one()
+ if step == "create":
+ summary = self.env._("Auto-bill creation failed — manual review needed")
+ body = self.env._(
+ "Auto-bill creation failed on receipt %(picking)s: %(error)s",
+ picking=picking.name,
+ error=exception,
+ )
+ else:
+ summary = self.env._("Auto-bill posting failed — manual review needed")
+ body = self.env._(
+ "Auto-bill posting failed on receipt %(picking)s "
+ "(bill %(bill)s remains in draft): %(error)s",
+ picking=picking.name,
+ bill=bill.display_name if bill else "",
+ error=exception,
+ )
+ self.message_post(body=body, message_type="notification")
+ self.activity_schedule(
+ "mail.mail_activity_data_todo",
+ summary=summary,
+ user_id=(self.user_id or self.env.user).id,
+ )
diff --git a/purchase_auto_bill_on_receipt/models/purchase_order_line.py b/purchase_auto_bill_on_receipt/models/purchase_order_line.py
new file mode 100644
index 0000000..1461ca6
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/models/purchase_order_line.py
@@ -0,0 +1,17 @@
+# Copyright 2026 Quartile (https://www.quartile.co)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
+
+from odoo import models
+
+
+class PurchaseOrderLine(models.Model):
+ _inherit = "purchase.order.line"
+
+ def _auto_bill_eligible(self):
+ self.ensure_one()
+ product_setting = self.product_id.auto_bill_on_receipt
+ if product_setting == "auto":
+ return True
+ if product_setting == "no_auto":
+ return False
+ return self.product_id.categ_id.auto_bill_on_receipt
diff --git a/purchase_auto_bill_on_receipt/models/stock_picking.py b/purchase_auto_bill_on_receipt/models/stock_picking.py
new file mode 100644
index 0000000..3b1076e
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/models/stock_picking.py
@@ -0,0 +1,23 @@
+# Copyright 2026 Quartile (https://www.quartile.co)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
+
+from odoo import models
+
+
+class StockPicking(models.Model):
+ _inherit = "stock.picking"
+
+ def button_validate(self):
+ res = super().button_validate()
+ for picking in self.filtered(
+ lambda p: p.state == "done" and p.picking_type_code == "incoming"
+ ):
+ picking._auto_create_vendor_bill()
+ return res
+
+ def _auto_create_vendor_bill(self):
+ self.ensure_one()
+ purchase = self.purchase_id
+ if not purchase or purchase.block_auto_bill:
+ return
+ purchase._auto_bill_for_picking(self)
diff --git a/purchase_auto_bill_on_receipt/pyproject.toml b/purchase_auto_bill_on_receipt/pyproject.toml
new file mode 100644
index 0000000..4231d0c
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["whool"]
+build-backend = "whool.buildapi"
diff --git a/purchase_auto_bill_on_receipt/readme/CONFIGURE.md b/purchase_auto_bill_on_receipt/readme/CONFIGURE.md
new file mode 100644
index 0000000..d200091
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/readme/CONFIGURE.md
@@ -0,0 +1,12 @@
+Three configuration levels control whether a Purchase Order line is
+auto-billed when the related receipt is validated. They are evaluated per line
+at receipt time:
+
+1. **Product Category** → *Auto Bill on Receipt* (boolean). Sets the baseline
+ for all products in the category.
+2. **Product** → *Auto Bill on Receipt* (selection: *Auto* / *No Auto* /
+ empty). Leave empty to inherit from the product category. *Auto* or
+ *No Auto* overrides the category setting for this product.
+3. **Purchase Order** → *Block Auto Bill* (boolean, on the *Other
+ Information* tab). When ticked, suppresses auto-billing for the whole
+ order regardless of product or category settings.
diff --git a/purchase_auto_bill_on_receipt/readme/CONTRIBUTORS.md b/purchase_auto_bill_on_receipt/readme/CONTRIBUTORS.md
new file mode 100644
index 0000000..2721fe7
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/readme/CONTRIBUTORS.md
@@ -0,0 +1,3 @@
+- [Quartile](https://www.quartile.co):
+ - Aung Ko Ko Lin
+ - Yoshi Tashiro
diff --git a/purchase_auto_bill_on_receipt/readme/DESCRIPTION.md b/purchase_auto_bill_on_receipt/readme/DESCRIPTION.md
new file mode 100644
index 0000000..c49ae3f
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/readme/DESCRIPTION.md
@@ -0,0 +1,2 @@
+This module automatically creates and posts a Vendor Bill when a purchase
+receipt is validated.
diff --git a/purchase_auto_bill_on_receipt/static/description/index.html b/purchase_auto_bill_on_receipt/static/description/index.html
new file mode 100644
index 0000000..833cfaf
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/static/description/index.html
@@ -0,0 +1,453 @@
+
+
+
+
+
+README.rst
+
+
+
+
+
+
+
+
+
+
+
Purchase Auto Bill on Receipt
+
+

+
This module automatically creates and posts a Vendor Bill when a
+purchase receipt is validated.
+
Table of contents
+
+
+
+
Three configuration levels control whether a Purchase Order line is
+auto-billed when the related receipt is validated. They are evaluated
+per line at receipt time:
+
+- Product Category → Auto Bill on Receipt (boolean). Sets the
+baseline for all products in the category.
+- Product → Auto Bill on Receipt (selection: Auto / No Auto /
+empty). Leave empty to inherit from the product category. Auto or
+No Auto overrides the category setting for this product.
+- Purchase Order → Block Auto Bill (boolean, on the Other
+Information tab). When ticked, suppresses auto-billing for the whole
+order regardless of product or category settings.
+
+
+
+
+
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.
+
+
+
+
+
+
+
+- Quartile:
+- Aung Ko Ko Lin
+- Yoshi Tashiro
+
+
+
+
+
+
+
This module is maintained by the OCA.
+
+
+
+
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.
+
Current maintainers:
+

+
This module is part of the OCA/purchase-workflow project on GitHub.
+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
+
+
+
+
+
+
diff --git a/purchase_auto_bill_on_receipt/tests/__init__.py b/purchase_auto_bill_on_receipt/tests/__init__.py
new file mode 100644
index 0000000..3e495b6
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_auto_bill_on_receipt
diff --git a/purchase_auto_bill_on_receipt/tests/test_auto_bill_on_receipt.py b/purchase_auto_bill_on_receipt/tests/test_auto_bill_on_receipt.py
new file mode 100644
index 0000000..1de2b5a
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/tests/test_auto_bill_on_receipt.py
@@ -0,0 +1,221 @@
+# Copyright 2026 Quartile (https://www.quartile.co)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
+
+
+from odoo import Command
+from odoo.tests import tagged
+
+from odoo.addons.base.tests.common import BaseCommon
+
+
+@tagged("post_install", "-at_install")
+class TestAutoBillOnReceipt(BaseCommon):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.warehouse = cls.env["stock.warehouse"].search(
+ [("company_id", "=", cls.company.id)], limit=1
+ )
+ cls.stock_location = cls.warehouse.lot_stock_id
+ cls.supplier_location = cls.env.ref("stock.stock_location_suppliers")
+ cls.picking_type_in = cls.warehouse.in_type_id
+ cls.uom_unit = cls.env.ref("uom.product_uom_unit")
+
+ cls.vendor = cls.env["res.partner"].create({"name": "Test Vendor"})
+
+ cls.category_auto = cls.env["product.category"].create(
+ {"name": "Auto-bill Category", "auto_bill_on_receipt": True}
+ )
+ cls.category_plain = cls.env["product.category"].create(
+ {"name": "Plain Category", "auto_bill_on_receipt": False}
+ )
+
+ cls.product_in_auto_categ = cls._create_product(
+ "Storable In Auto Category", cls.category_auto
+ )
+ cls.product_in_plain_categ = cls._create_product(
+ "Storable In Plain Category", cls.category_plain
+ )
+ cls.product_force_auto = cls._create_product(
+ "Force Auto Product", cls.category_plain, auto_bill_on_receipt="auto"
+ )
+ cls.product_force_no_auto = cls._create_product(
+ "Force No-Auto Product", cls.category_auto, auto_bill_on_receipt="no_auto"
+ )
+
+ @classmethod
+ def _create_product(cls, name, category, **vals):
+ return cls.env["product.product"].create(
+ {
+ "name": name,
+ "is_storable": True,
+ "standard_price": 10.0,
+ "list_price": 20.0,
+ "uom_id": cls.uom_unit.id,
+ "categ_id": category.id,
+ **vals,
+ }
+ )
+
+ def _line(self, product, qty=1.0, price=10.0):
+ return Command.create(
+ {
+ "product_id": product.id,
+ "product_qty": qty,
+ "product_uom_id": product.uom_id.id,
+ "price_unit": price,
+ "tax_ids": [Command.clear()],
+ }
+ )
+
+ def _section(self, name):
+ return Command.create(
+ {"display_type": "line_section", "name": name, "product_qty": 0.0}
+ )
+
+ def _create_po(self, lines, confirm=True):
+ po = self.env["purchase.order"].create(
+ {"partner_id": self.vendor.id, "order_line": lines}
+ )
+ if confirm:
+ po.button_confirm()
+ return po
+
+ def _receive(self, po, quantity=None):
+ pickings = po.picking_ids.filtered(lambda p: p.state != "done")
+ if quantity is not None:
+ pickings.move_ids.quantity = quantity
+ pickings.with_context(skip_backorder=True).button_validate()
+ return pickings
+
+ def _validate(self, picking, quantity):
+ picking.move_ids.quantity = quantity
+ picking.with_context(skip_backorder=True).button_validate()
+
+ def _bills_of(self, po):
+ return po.invoice_ids.sorted("id")
+
+ def test_01_category_auto_bill_inherited_by_product(self):
+ po = self._create_po([self._line(self.product_in_auto_categ, 5.0)])
+ picking = self._receive(po)
+ bills = self._bills_of(po)
+ self.assertEqual(len(bills), 1)
+ self.assertEqual(bills.state, "posted")
+ self.assertEqual(bills.invoice_line_ids.quantity, 5.0)
+ self.assertEqual(bills.invoice_origin, po.name)
+ self.assertEqual(bills.invoice_date, picking.date_done.date())
+
+ def test_02_product_override_auto_in_plain_category(self):
+ po = self._create_po([self._line(self.product_force_auto, 3.0)])
+ self._receive(po)
+ bills = self._bills_of(po)
+ self.assertEqual(len(bills), 1)
+ self.assertEqual(bills.state, "posted")
+
+ def test_03_product_override_no_auto_in_auto_category(self):
+ po = self._create_po([self._line(self.product_force_no_auto, 2.0)])
+ self._receive(po)
+ self.assertFalse(self._bills_of(po))
+
+ def test_04_block_auto_bill_on_po(self):
+ po = self._create_po([self._line(self.product_in_auto_categ, 4.0)])
+ po.block_auto_bill = True
+ self._receive(po)
+ self.assertFalse(self._bills_of(po))
+
+ def test_05_mixed_lines_only_eligible_billed(self):
+ po = self._create_po(
+ [
+ self._line(self.product_in_auto_categ, 2.0),
+ self._line(self.product_in_plain_categ, 3.0, price=20.0),
+ ]
+ )
+ self._receive(po)
+ bills = self._bills_of(po)
+ self.assertEqual(len(bills), 1)
+ self.assertEqual(bills.state, "posted")
+ self.assertEqual(bills.invoice_line_ids.product_id, self.product_in_auto_categ)
+ self.assertEqual(bills.invoice_line_ids.quantity, 2.0)
+
+ def test_06_partial_receipt_creates_one_bill_per_receipt(self):
+ po = self._create_po([self._line(self.product_in_auto_categ, 10.0)])
+ first_picking = po.picking_ids
+ self._validate(first_picking, 4.0)
+ bills = self._bills_of(po)
+ self.assertEqual(len(bills), 1)
+ self.assertEqual(bills.invoice_line_ids.quantity, 4.0)
+ self.assertEqual(bills.state, "posted")
+
+ backorder = po.picking_ids - first_picking
+ self.assertEqual(len(backorder), 1)
+ self._validate(backorder, 6.0)
+ bills = self._bills_of(po)
+ self.assertEqual(len(bills), 2)
+ second_bill = bills - bills[0]
+ self.assertEqual(second_bill.invoice_line_ids.quantity, 6.0)
+ self.assertEqual(second_bill.state, "posted")
+
+ def test_07_receipt_without_po_does_nothing(self):
+ picking = self.env["stock.picking"].create(
+ {
+ "picking_type_id": self.picking_type_in.id,
+ "location_id": self.supplier_location.id,
+ "location_dest_id": self.stock_location.id,
+ "move_ids": [
+ Command.create(
+ {
+ "product_id": self.product_in_auto_categ.id,
+ "product_uom_qty": 1.0,
+ "product_uom": self.product_in_auto_categ.uom_id.id,
+ "location_id": self.supplier_location.id,
+ "location_dest_id": self.stock_location.id,
+ }
+ )
+ ],
+ }
+ )
+ picking.action_confirm()
+ picking.move_ids.quantity = 1.0
+ picking.move_ids.picked = True
+ picking.button_validate()
+ self.assertFalse(
+ self.env["account.move"].search(
+ [
+ ("move_type", "=", "in_invoice"),
+ ("invoice_origin", "like", picking.name),
+ ]
+ )
+ )
+
+ def test_08_idempotent_on_second_call(self):
+ po = self._create_po([self._line(self.product_in_auto_categ, 5.0)])
+ self._receive(po)
+ self.assertEqual(len(self._bills_of(po)), 1)
+ po.picking_ids._auto_create_vendor_bill()
+ self.assertEqual(len(self._bills_of(po)), 1)
+
+ def test_09_sections_carried_over_only_when_needed(self):
+ po = self._create_po(
+ [
+ self._section("Billed Section"),
+ self._line(self.product_in_auto_categ, 2.0),
+ self._section("Skipped Section"),
+ self._line(self.product_in_plain_categ, 3.0, price=20.0),
+ ]
+ )
+ self._receive(po)
+ bills = self._bills_of(po)
+ self.assertEqual(len(bills), 1)
+
+ sections = bills.invoice_line_ids.filtered(
+ lambda line: line.display_type == "line_section"
+ )
+ # Only the section preceding an eligible line is carried over.
+ self.assertEqual(sections.name, "Billed Section")
+
+ product_lines = bills.invoice_line_ids.filtered(
+ lambda line: line.display_type == "product"
+ )
+ self.assertEqual(product_lines.product_id, self.product_in_auto_categ)
+ # Section header keeps its place above its product line.
+ self.assertLess(sections.sequence, product_lines.sequence)
diff --git a/purchase_auto_bill_on_receipt/views/product_category_views.xml b/purchase_auto_bill_on_receipt/views/product_category_views.xml
new file mode 100644
index 0000000..8f3247a
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/views/product_category_views.xml
@@ -0,0 +1,14 @@
+
+
+
+ product.category
+
+
+
+
+
+
+
+
+
+
diff --git a/purchase_auto_bill_on_receipt/views/product_template_views.xml b/purchase_auto_bill_on_receipt/views/product_template_views.xml
new file mode 100644
index 0000000..e54662c
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/views/product_template_views.xml
@@ -0,0 +1,12 @@
+
+
+
+ product.template
+
+
+
+
+
+
+
+
diff --git a/purchase_auto_bill_on_receipt/views/purchase_order_views.xml b/purchase_auto_bill_on_receipt/views/purchase_order_views.xml
new file mode 100644
index 0000000..d51ecff
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/views/purchase_order_views.xml
@@ -0,0 +1,12 @@
+
+
+
+ purchase.order
+
+
+
+
+
+
+
+
From d06c132be178eb55a1c47aba825de442a09a7e21 Mon Sep 17 00:00:00 2001
From: Aungkokolin1997
Date: Mon, 15 Jun 2026 08:31:39 +0000
Subject: [PATCH 2/3] reflect oca changes
---
purchase_auto_bill_on_receipt/README.rst | 60 +++-
purchase_auto_bill_on_receipt/__manifest__.py | 7 +-
.../data/ir_cron.xml | 12 +
.../models/__init__.py | 6 +-
.../models/product_template.py | 17 --
.../models/purchase_order.py | 173 ++++++++----
.../models/purchase_order_line.py | 17 --
.../models/res_company.py | 15 +
...uct_category.py => res_config_settings.py} | 8 +-
.../models/res_partner.py | 18 ++
.../models/stock_picking.py | 48 +++-
.../readme/CONFIGURE.md | 24 +-
.../readme/DESCRIPTION.md | 4 +-
purchase_auto_bill_on_receipt/readme/USAGE.md | 27 ++
.../static/description/index.html | 74 +++--
.../tests/test_auto_bill_on_receipt.py | 262 +++++++++++++++---
.../views/product_category_views.xml | 14 -
.../views/product_template_views.xml | 12 -
.../views/purchase_order_views.xml | 2 +-
.../views/res_config_settings_views.xml | 21 ++
.../views/res_partner_views.xml | 16 ++
21 files changed, 615 insertions(+), 222 deletions(-)
create mode 100644 purchase_auto_bill_on_receipt/data/ir_cron.xml
delete mode 100644 purchase_auto_bill_on_receipt/models/product_template.py
delete mode 100644 purchase_auto_bill_on_receipt/models/purchase_order_line.py
create mode 100644 purchase_auto_bill_on_receipt/models/res_company.py
rename purchase_auto_bill_on_receipt/models/{product_category.py => res_config_settings.py} (50%)
create mode 100644 purchase_auto_bill_on_receipt/models/res_partner.py
create mode 100644 purchase_auto_bill_on_receipt/readme/USAGE.md
delete mode 100644 purchase_auto_bill_on_receipt/views/product_category_views.xml
delete mode 100644 purchase_auto_bill_on_receipt/views/product_template_views.xml
create mode 100644 purchase_auto_bill_on_receipt/views/res_config_settings_views.xml
create mode 100644 purchase_auto_bill_on_receipt/views/res_partner_views.xml
diff --git a/purchase_auto_bill_on_receipt/README.rst b/purchase_auto_bill_on_receipt/README.rst
index 9dffd4e..66568e1 100644
--- a/purchase_auto_bill_on_receipt/README.rst
+++ b/purchase_auto_bill_on_receipt/README.rst
@@ -33,7 +33,9 @@ Purchase Auto Bill on Receipt
|badge1| |badge2| |badge3| |badge4| |badge5|
This module automatically creates and posts a Vendor Bill when a
-purchase receipt is validated.
+purchase receipt is validated. It also creates and posts a Credit Note
+when a purchase return is validated, provided the return lines are
+marked as *Update quantities on SO/PO* in the return wizard.
**Table of contents**
@@ -43,18 +45,54 @@ purchase receipt is validated.
Configuration
=============
-Three configuration levels control whether a Purchase Order line is
-auto-billed when the related receipt is validated. They are evaluated
-per line at receipt time:
+Whether a Purchase Order is auto-billed when its receipt is validated is
+resolved from two configuration levels, plus a per-order kill switch:
-1. **Product Category** → *Auto Bill on Receipt* (boolean). Sets the
- baseline for all products in the category.
-2. **Product** → *Auto Bill on Receipt* (selection: *Auto* / *No Auto* /
- empty). Leave empty to inherit from the product category. *Auto* or
- *No Auto* overrides the category setting for this product.
+1. **Company** → *Settings > Purchase > Invoicing > Auto Bill on
+ Receipt* (boolean). Sets the default for all vendors.
+2. **Vendor** → *Auto Bill on Receipt* (selection: *Auto* / *No Auto* /
+ empty), on the vendor form. Leave empty to inherit from the company.
+ *Auto* or *No Auto* overrides the company default for this vendor.
3. **Purchase Order** → *Block Auto Bill* (boolean, on the *Other
- Information* tab). When ticked, suppresses auto-billing for the whole
- order regardless of product or category settings.
+ Information* tab). When ticked, suppresses auto-billing for this
+ order regardless of the company or vendor settings.
+
+The bill date uses the timezone set on the company's contact. To set it,
+install the
+`partner_tz `__
+module and configure the timezone on the company's contact.
+
+Usage
+=====
+
+Once auto-billing is configured, validate a purchase receipt as usual. A
+Vendor Bill is automatically created and posted shortly after via the
+scheduled action. The bill only includes PO lines whose product bill
+control policy is *On received quantities* and that have a positive
+quantity to invoice.
+
+**Returns / Credit Notes:** When a return is validated and its return
+lines have *Update quantities on SO/PO* enabled in the return wizard, a
+Credit Note is automatically created and posted for the returned
+quantities. The credit note is only created when a Vendor Bill has
+already been posted (i.e. the PO line has a negative *Quantity to
+Invoice*).
+
+The bill/credit note date is the receipt/return validation date
+converted to the company's timezone (falls back to UTC when no timezone
+is set).
+
+If creation or posting fails, the error is logged in the Purchase Order
+chatter and a To-Do activity is scheduled for follow-up.
+
+**Case 1 — Cancelling an auto-bill:** An auto-posted bill can be
+cancelled like any other Vendor Bill. However, the module will not
+automatically create a new bill for the same receipt.
+
+**Case 2 — Reprocessing an auto-bill:** This is not supported. Once the
+scheduled action processes a receipt or return, it will not be picked up
+again. To bill or refund the same quantities, create the document
+manually from the Purchase Order.
Bug Tracker
===========
diff --git a/purchase_auto_bill_on_receipt/__manifest__.py b/purchase_auto_bill_on_receipt/__manifest__.py
index bcb0dca..1036ae4 100644
--- a/purchase_auto_bill_on_receipt/__manifest__.py
+++ b/purchase_auto_bill_on_receipt/__manifest__.py
@@ -9,10 +9,11 @@
"website": "https://github.com/OCA/purchase-workflow",
"license": "AGPL-3",
"category": "Purchase Management",
- "depends": ["purchase_stock", "account"],
+ "depends": ["purchase_stock"],
"data": [
- "views/product_category_views.xml",
- "views/product_template_views.xml",
+ "data/ir_cron.xml",
+ "views/res_config_settings_views.xml",
+ "views/res_partner_views.xml",
"views/purchase_order_views.xml",
],
"maintainers": ["yostashiro", "aungkokolin1997"],
diff --git a/purchase_auto_bill_on_receipt/data/ir_cron.xml b/purchase_auto_bill_on_receipt/data/ir_cron.xml
new file mode 100644
index 0000000..84d03f0
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/data/ir_cron.xml
@@ -0,0 +1,12 @@
+
+
+
+ Purchase: Auto Bill on Receipt
+
+ code
+ model._cron_auto_bill()
+ 1
+ days
+
+
+
diff --git a/purchase_auto_bill_on_receipt/models/__init__.py b/purchase_auto_bill_on_receipt/models/__init__.py
index 53160e1..e49b4f4 100644
--- a/purchase_auto_bill_on_receipt/models/__init__.py
+++ b/purchase_auto_bill_on_receipt/models/__init__.py
@@ -1,5 +1,5 @@
-from . import product_category
-from . import product_template
+from . import res_company
+from . import res_config_settings
+from . import res_partner
from . import purchase_order
-from . import purchase_order_line
from . import stock_picking
diff --git a/purchase_auto_bill_on_receipt/models/product_template.py b/purchase_auto_bill_on_receipt/models/product_template.py
deleted file mode 100644
index e3aa432..0000000
--- a/purchase_auto_bill_on_receipt/models/product_template.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# Copyright 2026 Quartile (https://www.quartile.co)
-# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
-
-from odoo import fields, models
-
-
-class ProductTemplate(models.Model):
- _inherit = "product.template"
-
- auto_bill_on_receipt = fields.Selection(
- selection=[
- ("auto", "Auto Bill on Receipt"),
- ("no_auto", "No Auto Bill"),
- ],
- help="Override the product category setting. "
- "Leave empty to inherit from the product category.",
- )
diff --git a/purchase_auto_bill_on_receipt/models/purchase_order.py b/purchase_auto_bill_on_receipt/models/purchase_order.py
index 83dce7d..28ad646 100644
--- a/purchase_auto_bill_on_receipt/models/purchase_order.py
+++ b/purchase_auto_bill_on_receipt/models/purchase_order.py
@@ -13,45 +13,81 @@ class PurchaseOrder(models.Model):
block_auto_bill = fields.Boolean(
help="When enabled, suppresses automatic bill creation on receipt "
- "for this order, regardless of product or category settings.",
+ "for this order, regardless of the company or vendor settings.",
)
- def _auto_bill_for_picking(self, picking):
+ def _auto_bill_enabled(self):
self.ensure_one()
- received_products = picking.move_ids.product_id
- eligible_lines = self.order_line.filtered(
- lambda line: line.product_id in received_products
- and line._auto_bill_eligible()
- and line.qty_to_invoice > 0
- )
- if not eligible_lines:
- return self.env["account.move"]
- try:
- bill = self._auto_bill_create(picking, eligible_lines)
- except Exception as e:
- _logger.exception(
- "Auto-bill creation failed for PO %s / picking %s",
- self.name,
- picking.name,
+ if self.block_auto_bill:
+ return False
+ partner_setting = self.partner_id.commercial_partner_id.auto_bill_on_receipt
+ if partner_setting == "auto":
+ return True
+ if partner_setting == "no_auto":
+ return False
+ return self.company_id.auto_bill_on_receipt
+
+ def _auto_bill_log_failure(
+ self, picking, step, exception, bill=False, refund=False
+ ):
+ self.ensure_one()
+ doc = self.env._("credit note") if refund else self.env._("bill")
+ src = self.env._("return") if refund else self.env._("receipt")
+ if step == "create":
+ summary = self.env._(
+ "Auto %(doc)s creation failed — manual review needed",
+ doc=doc,
)
- self._auto_bill_log_failure(picking, "create", e)
- return self.env["account.move"]
- try:
- bill.action_post()
- except Exception as e:
- _logger.exception(
- "Auto-bill posting failed for PO %s / picking %s / bill %s",
- self.name,
- picking.name,
- bill.name,
+ body = self.env._(
+ "Auto %(doc)s creation failed on %(src)s %(picking)s: %(error)s",
+ doc=doc,
+ src=src,
+ picking=picking.name,
+ error=exception,
)
- self._auto_bill_log_failure(picking, "post", e, bill=bill)
- return bill
+ else:
+ summary = self.env._(
+ "Auto %(doc)s posting failed — manual review needed",
+ doc=doc,
+ )
+ body = self.env._(
+ "Auto %(doc)s posting failed on %(src)s %(picking)s "
+ "(%(doc)s %(bill)s remains in draft): %(error)s",
+ doc=doc,
+ src=src,
+ picking=picking.name,
+ bill=bill.display_name if bill else "",
+ error=exception,
+ )
+ self.message_post(body=body, message_type="notification")
+ self.activity_schedule(
+ "mail.mail_activity_data_todo",
+ summary=summary,
+ user_id=(self.user_id or self.env.user).id,
+ )
- def _auto_bill_create(self, picking, eligible_lines):
+ def _get_picking_line_qty(self, picking, line, refund=False):
self.ensure_one()
- invoice_vals = self._prepare_invoice()
- invoice_vals["invoice_date"] = picking.date_done.date()
+ qty = 0.0
+ moves = picking.move_ids.filtered(lambda m: m.purchase_line_id == line)
+ if refund:
+ moves = moves.filtered("to_refund")
+ for move in moves:
+ qty += move.product_uom._compute_quantity(
+ move.quantity, line.product_uom_id
+ )
+ return qty
+
+ def _auto_bill_create(self, picking, eligible_lines, refund=False):
+ self.ensure_one()
+ move_type = "in_refund" if refund else "in_invoice"
+ invoice_vals = self.with_context(default_move_type=move_type)._prepare_invoice()
+ # Billing runs under the cron user, whose timezone is unreliable, so
+ # derive the bill date from the company partner's timezone instead.
+ tz = self.company_id.partner_id.tz or "UTC"
+ invoice_vals["invoice_date"] = fields.Date.context_today(
+ self.with_context(tz=tz), picking.date_done
+ )
invoice_lines = []
sequence = 10
pending_section = None
@@ -61,6 +97,13 @@ def _auto_bill_create(self, picking, eligible_lines):
continue
if line not in eligible_lines:
continue
+ picking_qty = self._get_picking_line_qty(picking, line, refund=refund)
+ if refund:
+ qty = min(picking_qty, abs(line.qty_to_invoice))
+ else:
+ qty = min(picking_qty, line.qty_to_invoice)
+ if qty <= 0:
+ continue
if pending_section:
section_vals = pending_section._prepare_account_move_line()
section_vals["sequence"] = sequence
@@ -68,6 +111,7 @@ def _auto_bill_create(self, picking, eligible_lines):
sequence += 1
pending_section = None
line_vals = line._prepare_account_move_line()
+ line_vals["quantity"] = qty
line_vals["sequence"] = sequence
invoice_lines.append(Command.create(line_vals))
sequence += 1
@@ -75,31 +119,52 @@ def _auto_bill_create(self, picking, eligible_lines):
return (
self.env["account.move"]
.with_company(self.company_id)
- .with_context(default_move_type="in_invoice")
+ .with_context(default_move_type=move_type)
.create(invoice_vals)
)
- def _auto_bill_log_failure(self, picking, step, exception, bill=False):
+ def _is_return_picking(self, picking):
+ return picking.picking_type_code != "incoming" and any(
+ m.to_refund for m in picking.move_ids if m.purchase_line_id
+ )
+
+ def _auto_bill_for_picking(self, picking):
self.ensure_one()
- if step == "create":
- summary = self.env._("Auto-bill creation failed — manual review needed")
- body = self.env._(
- "Auto-bill creation failed on receipt %(picking)s: %(error)s",
- picking=picking.name,
- error=exception,
+ refund = self._is_return_picking(picking)
+ moves = picking.move_ids
+ if refund:
+ moves = moves.filtered("to_refund")
+ eligible_lines = moves.purchase_line_id.filtered(
+ lambda line: line.product_id.purchase_method == "receive"
+ and (line.qty_to_invoice < 0 if refund else line.qty_to_invoice > 0)
+ )
+ if not eligible_lines:
+ return self.env["account.move"]
+ try:
+ # Isolate the create in a savepoint: a database-level error aborts
+ # the cursor, and we must roll back to a clean state before the
+ # failure logging below can write to it.
+ with self.env.cr.savepoint():
+ bill = self._auto_bill_create(picking, eligible_lines, refund=refund)
+ except Exception as e:
+ _logger.exception(
+ "Auto-%s creation failed for PO %s / picking %s",
+ "refund" if refund else "bill",
+ self.name,
+ picking.name,
)
- else:
- summary = self.env._("Auto-bill posting failed — manual review needed")
- body = self.env._(
- "Auto-bill posting failed on receipt %(picking)s "
- "(bill %(bill)s remains in draft): %(error)s",
- picking=picking.name,
- bill=bill.display_name if bill else "",
- error=exception,
+ self._auto_bill_log_failure(picking, "create", e, refund=refund)
+ return self.env["account.move"]
+ try:
+ with self.env.cr.savepoint():
+ bill.action_post()
+ except Exception as e:
+ _logger.exception(
+ "Auto-%s posting failed for PO %s / picking %s / %s",
+ "refund" if refund else "bill",
+ self.name,
+ picking.name,
+ bill.name,
)
- self.message_post(body=body, message_type="notification")
- self.activity_schedule(
- "mail.mail_activity_data_todo",
- summary=summary,
- user_id=(self.user_id or self.env.user).id,
- )
+ self._auto_bill_log_failure(picking, "post", e, bill=bill, refund=refund)
+ return bill
diff --git a/purchase_auto_bill_on_receipt/models/purchase_order_line.py b/purchase_auto_bill_on_receipt/models/purchase_order_line.py
deleted file mode 100644
index 1461ca6..0000000
--- a/purchase_auto_bill_on_receipt/models/purchase_order_line.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# Copyright 2026 Quartile (https://www.quartile.co)
-# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
-
-from odoo import models
-
-
-class PurchaseOrderLine(models.Model):
- _inherit = "purchase.order.line"
-
- def _auto_bill_eligible(self):
- self.ensure_one()
- product_setting = self.product_id.auto_bill_on_receipt
- if product_setting == "auto":
- return True
- if product_setting == "no_auto":
- return False
- return self.product_id.categ_id.auto_bill_on_receipt
diff --git a/purchase_auto_bill_on_receipt/models/res_company.py b/purchase_auto_bill_on_receipt/models/res_company.py
new file mode 100644
index 0000000..57fccc6
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/models/res_company.py
@@ -0,0 +1,15 @@
+# Copyright 2026 Quartile (https://www.quartile.co)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
+
+from odoo import fields, models
+
+
+class ResCompany(models.Model):
+ _inherit = "res.company"
+
+ auto_bill_on_receipt = fields.Boolean(
+ string="Auto Bill on Purchase Receipt",
+ help="When enabled, validating an incoming purchase receipt "
+ "automatically creates and posts a Vendor Bill. This is the default "
+ "for all vendors and can be overridden per vendor.",
+ )
diff --git a/purchase_auto_bill_on_receipt/models/product_category.py b/purchase_auto_bill_on_receipt/models/res_config_settings.py
similarity index 50%
rename from purchase_auto_bill_on_receipt/models/product_category.py
rename to purchase_auto_bill_on_receipt/models/res_config_settings.py
index ea3b35f..a6d260d 100644
--- a/purchase_auto_bill_on_receipt/models/product_category.py
+++ b/purchase_auto_bill_on_receipt/models/res_config_settings.py
@@ -4,10 +4,10 @@
from odoo import fields, models
-class ProductCategory(models.Model):
- _inherit = "product.category"
+class ResConfigSettings(models.TransientModel):
+ _inherit = "res.config.settings"
auto_bill_on_receipt = fields.Boolean(
- help="When enabled, products in this category will automatically "
- "generate a Vendor Bill when goods are received.",
+ related="company_id.auto_bill_on_receipt",
+ readonly=False,
)
diff --git a/purchase_auto_bill_on_receipt/models/res_partner.py b/purchase_auto_bill_on_receipt/models/res_partner.py
new file mode 100644
index 0000000..36eeb9a
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/models/res_partner.py
@@ -0,0 +1,18 @@
+# Copyright 2026 Quartile (https://www.quartile.co)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
+
+from odoo import fields, models
+
+
+class ResPartner(models.Model):
+ _inherit = "res.partner"
+
+ auto_bill_on_receipt = fields.Selection(
+ selection=[
+ ("auto", "Auto Bill"),
+ ("no_auto", "No Auto Bill"),
+ ],
+ string="Auto Bill on Purchase Receipt",
+ help="Override the company default for auto-billing on purchase "
+ "receipt. Leave empty to inherit the company setting.",
+ )
diff --git a/purchase_auto_bill_on_receipt/models/stock_picking.py b/purchase_auto_bill_on_receipt/models/stock_picking.py
index 3b1076e..b80d540 100644
--- a/purchase_auto_bill_on_receipt/models/stock_picking.py
+++ b/purchase_auto_bill_on_receipt/models/stock_picking.py
@@ -1,23 +1,53 @@
# Copyright 2026 Quartile (https://www.quartile.co)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
-from odoo import models
+import logging
+
+from odoo import fields, models
+
+_logger = logging.getLogger(__name__)
class StockPicking(models.Model):
_inherit = "stock.picking"
- def button_validate(self):
- res = super().button_validate()
- for picking in self.filtered(
- lambda p: p.state == "done" and p.picking_type_code == "incoming"
- ):
- picking._auto_create_vendor_bill()
- return res
+ auto_bill_pending = fields.Boolean(
+ copy=False,
+ help="Set when a validated receipt or return is queued for "
+ "auto-billing. The scheduled action picks it up to create and post "
+ "the Vendor Bill or Credit Note.",
+ )
def _auto_create_vendor_bill(self):
self.ensure_one()
+ self.auto_bill_pending = False
purchase = self.purchase_id
- if not purchase or purchase.block_auto_bill:
+ if not purchase or not purchase._auto_bill_enabled():
return
purchase._auto_bill_for_picking(self)
+
+ def _cron_auto_bill(self):
+ for picking in self.search([("auto_bill_pending", "=", True)]):
+ try:
+ with self.env.cr.savepoint():
+ picking._auto_create_vendor_bill()
+ except Exception:
+ _logger.exception("Failed to auto-bill picking %s", picking.name)
+
+ def button_validate(self):
+ res = super().button_validate()
+ to_process = self.filtered(
+ lambda p: p.state == "done"
+ and p.purchase_id
+ and p.purchase_id._auto_bill_enabled()
+ and (
+ p.picking_type_code == "incoming"
+ or any(m.to_refund for m in p.move_ids if m.purchase_line_id)
+ )
+ )
+ if to_process:
+ to_process.auto_bill_pending = True
+ self.env.ref(
+ "purchase_auto_bill_on_receipt.ir_cron_auto_bill"
+ ).sudo()._trigger()
+ return res
diff --git a/purchase_auto_bill_on_receipt/readme/CONFIGURE.md b/purchase_auto_bill_on_receipt/readme/CONFIGURE.md
index d200091..4cd9eb8 100644
--- a/purchase_auto_bill_on_receipt/readme/CONFIGURE.md
+++ b/purchase_auto_bill_on_receipt/readme/CONFIGURE.md
@@ -1,12 +1,16 @@
-Three configuration levels control whether a Purchase Order line is
-auto-billed when the related receipt is validated. They are evaluated per line
-at receipt time:
+Whether a Purchase Order is auto-billed when its receipt is validated is
+resolved from two configuration levels, plus a per-order kill switch:
-1. **Product Category** → *Auto Bill on Receipt* (boolean). Sets the baseline
- for all products in the category.
-2. **Product** → *Auto Bill on Receipt* (selection: *Auto* / *No Auto* /
- empty). Leave empty to inherit from the product category. *Auto* or
- *No Auto* overrides the category setting for this product.
+1. **Company** → *Settings > Purchase > Invoicing > Auto Bill on Receipt*
+ (boolean). Sets the default for all vendors.
+2. **Vendor** → *Auto Bill on Receipt* (selection: *Auto* / *No Auto* /
+ empty), on the vendor form. Leave empty to inherit from the company.
+ *Auto* or *No Auto* overrides the company default for this vendor.
3. **Purchase Order** → *Block Auto Bill* (boolean, on the *Other
- Information* tab). When ticked, suppresses auto-billing for the whole
- order regardless of product or category settings.
+ Information* tab). When ticked, suppresses auto-billing for this order
+ regardless of the company or vendor settings.
+
+The bill date uses the timezone set on the company's contact. To set it,
+install the
+[partner_tz](https://github.com/OCA/partner-contact/tree/19.0/partner_tz)
+module and configure the timezone on the company's contact.
diff --git a/purchase_auto_bill_on_receipt/readme/DESCRIPTION.md b/purchase_auto_bill_on_receipt/readme/DESCRIPTION.md
index c49ae3f..5dbb635 100644
--- a/purchase_auto_bill_on_receipt/readme/DESCRIPTION.md
+++ b/purchase_auto_bill_on_receipt/readme/DESCRIPTION.md
@@ -1,2 +1,4 @@
This module automatically creates and posts a Vendor Bill when a purchase
-receipt is validated.
+receipt is validated. It also creates and posts a Credit Note when a
+purchase return is validated, provided the return lines are marked as
+*Update quantities on SO/PO* in the return wizard.
diff --git a/purchase_auto_bill_on_receipt/readme/USAGE.md b/purchase_auto_bill_on_receipt/readme/USAGE.md
new file mode 100644
index 0000000..9051c75
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/readme/USAGE.md
@@ -0,0 +1,27 @@
+Once auto-billing is configured, validate a purchase receipt as usual.
+A Vendor Bill is automatically created and posted shortly after via the
+scheduled action. The bill only includes PO lines whose product bill
+control policy is *On received quantities* and that have a positive
+quantity to invoice.
+
+**Returns / Credit Notes:** When a return is validated and its return
+lines have *Update quantities on SO/PO* enabled in the return wizard, a
+Credit Note is automatically created and posted for the returned
+quantities. The credit note is only created when a Vendor Bill has
+already been posted (i.e. the PO line has a negative *Quantity to
+Invoice*).
+
+The bill/credit note date is the receipt/return validation date converted
+to the company's timezone (falls back to UTC when no timezone is set).
+
+If creation or posting fails, the error is logged in the Purchase
+Order chatter and a To-Do activity is scheduled for follow-up.
+
+**Case 1 — Cancelling an auto-bill:** An auto-posted bill can be
+cancelled like any other Vendor Bill. However, the module will not
+automatically create a new bill for the same receipt.
+
+**Case 2 — Reprocessing an auto-bill:** This is not supported. Once the
+scheduled action processes a receipt or return, it will not be picked up
+again. To bill or refund the same quantities, create the document
+manually from the Purchase Order.
diff --git a/purchase_auto_bill_on_receipt/static/description/index.html b/purchase_auto_bill_on_receipt/static/description/index.html
index 833cfaf..a61caa5 100644
--- a/purchase_auto_bill_on_receipt/static/description/index.html
+++ b/purchase_auto_bill_on_receipt/static/description/index.html
@@ -376,38 +376,70 @@ Purchase Auto Bill on Receipt
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

This module automatically creates and posts a Vendor Bill when a
-purchase receipt is validated.
+purchase receipt is validated. It also creates and posts a Credit Note
+when a purchase return is validated, provided the return lines are
+marked as Update quantities on SO/PO in the return wizard.
Table of contents
-
Three configuration levels control whether a Purchase Order line is
-auto-billed when the related receipt is validated. They are evaluated
-per line at receipt time:
+
Whether a Purchase Order is auto-billed when its receipt is validated is
+resolved from two configuration levels, plus a per-order kill switch:
-- Product Category → Auto Bill on Receipt (boolean). Sets the
-baseline for all products in the category.
-- Product → Auto Bill on Receipt (selection: Auto / No Auto /
-empty). Leave empty to inherit from the product category. Auto or
-No Auto overrides the category setting for this product.
+- Company → Settings > Purchase > Invoicing > Auto Bill on
+Receipt (boolean). Sets the default for all vendors.
+- Vendor → Auto Bill on Receipt (selection: Auto / No Auto /
+empty), on the vendor form. Leave empty to inherit from the company.
+Auto or No Auto overrides the company default for this vendor.
- Purchase Order → Block Auto Bill (boolean, on the Other
-Information tab). When ticked, suppresses auto-billing for the whole
-order regardless of product or category settings.
+Information tab). When ticked, suppresses auto-billing for this
+order regardless of the company or vendor settings.
+
The bill date uses the timezone set on the company’s contact. To set it,
+install the
+partner_tz
+module and configure the timezone on the company’s contact.
+
+
+
+
Once auto-billing is configured, validate a purchase receipt as usual. A
+Vendor Bill is automatically created and posted shortly after via the
+scheduled action. The bill only includes PO lines whose product bill
+control policy is On received quantities and that have a positive
+quantity to invoice.
+
Returns / Credit Notes: When a return is validated and its return
+lines have Update quantities on SO/PO enabled in the return wizard, a
+Credit Note is automatically created and posted for the returned
+quantities. The credit note is only created when a Vendor Bill has
+already been posted (i.e. the PO line has a negative Quantity to
+Invoice).
+
The bill/credit note date is the receipt/return validation date
+converted to the company’s timezone (falls back to UTC when no timezone
+is set).
+
If creation or posting fails, the error is logged in the Purchase Order
+chatter and a To-Do activity is scheduled for follow-up.
+
Case 1 — Cancelling an auto-bill: An auto-posted bill can be
+cancelled like any other Vendor Bill. However, the module will not
+automatically create a new bill for the same receipt.
+
Case 2 — Reprocessing an auto-bill: This is not supported. Once the
+scheduled action processes a receipt or return, it will not be picked up
+again. To bill or refund the same quantities, create the document
+manually from the Purchase Order.
-
+
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
@@ -415,15 +447,15 @@
Do not contact contributors directly about support or help with technical issues.
-
+
-
+
- Quartile:
- Aung Ko Ko Lin
@@ -433,7 +465,7 @@
-
+
This module is maintained by the OCA.
diff --git a/purchase_auto_bill_on_receipt/tests/test_auto_bill_on_receipt.py b/purchase_auto_bill_on_receipt/tests/test_auto_bill_on_receipt.py
index 1de2b5a..4fda24b 100644
--- a/purchase_auto_bill_on_receipt/tests/test_auto_bill_on_receipt.py
+++ b/purchase_auto_bill_on_receipt/tests/test_auto_bill_on_receipt.py
@@ -2,8 +2,11 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
-from odoo import Command
+from unittest.mock import patch
+
+from odoo import Command, fields
from odoo.tests import tagged
+from odoo.tools import mute_logger
from odoo.addons.base.tests.common import BaseCommon
@@ -22,29 +25,20 @@ def setUpClass(cls):
cls.uom_unit = cls.env.ref("uom.product_uom_unit")
cls.vendor = cls.env["res.partner"].create({"name": "Test Vendor"})
-
- cls.category_auto = cls.env["product.category"].create(
- {"name": "Auto-bill Category", "auto_bill_on_receipt": True}
+ cls.vendor_auto = cls.env["res.partner"].create(
+ {"name": "Auto Vendor", "auto_bill_on_receipt": "auto"}
)
- cls.category_plain = cls.env["product.category"].create(
- {"name": "Plain Category", "auto_bill_on_receipt": False}
+ cls.vendor_no_auto = cls.env["res.partner"].create(
+ {"name": "No-Auto Vendor", "auto_bill_on_receipt": "no_auto"}
)
- cls.product_in_auto_categ = cls._create_product(
- "Storable In Auto Category", cls.category_auto
- )
- cls.product_in_plain_categ = cls._create_product(
- "Storable In Plain Category", cls.category_plain
- )
- cls.product_force_auto = cls._create_product(
- "Force Auto Product", cls.category_plain, auto_bill_on_receipt="auto"
- )
- cls.product_force_no_auto = cls._create_product(
- "Force No-Auto Product", cls.category_auto, auto_bill_on_receipt="no_auto"
+ cls.product = cls._create_product("Storable Product")
+ cls.product_billed_on_order = cls._create_product(
+ "Billed On Order Product", purchase_method="purchase"
)
@classmethod
- def _create_product(cls, name, category, **vals):
+ def _create_product(cls, name, **vals):
return cls.env["product.product"].create(
{
"name": name,
@@ -52,7 +46,6 @@ def _create_product(cls, name, category, **vals):
"standard_price": 10.0,
"list_price": 20.0,
"uom_id": cls.uom_unit.id,
- "categ_id": category.id,
**vals,
}
)
@@ -73,30 +66,59 @@ def _section(self, name):
{"display_type": "line_section", "name": name, "product_qty": 0.0}
)
- def _create_po(self, lines, confirm=True):
+ def _create_po(self, lines, partner=None, confirm=True):
po = self.env["purchase.order"].create(
- {"partner_id": self.vendor.id, "order_line": lines}
+ {"partner_id": (partner or self.vendor).id, "order_line": lines}
)
if confirm:
po.button_confirm()
return po
- def _receive(self, po, quantity=None):
+ def _run_auto_bill_cron(self):
+ # button_validate only triggers the cron; run it explicitly so the
+ # bills are created within the test transaction.
+ self.env["stock.picking"]._cron_auto_bill()
+
+ def _receive(self, po, quantity=None, run_cron=True):
pickings = po.picking_ids.filtered(lambda p: p.state != "done")
if quantity is not None:
pickings.move_ids.quantity = quantity
pickings.with_context(skip_backorder=True).button_validate()
+ if run_cron:
+ self._run_auto_bill_cron()
return pickings
def _validate(self, picking, quantity):
picking.move_ids.quantity = quantity
picking.with_context(skip_backorder=True).button_validate()
+ self._run_auto_bill_cron()
+
+ def _return(self, picking, quantity=None, to_refund=True, run_cron=True):
+ """Create and validate a return for the given picking."""
+ return_wizard = (
+ self.env["stock.return.picking"]
+ .with_context(active_id=picking.id, active_model="stock.picking")
+ .create({})
+ )
+ for move_line in return_wizard.product_return_moves:
+ move_line.to_refund = to_refund
+ if quantity is not None:
+ move_line.quantity = quantity
+ action = return_wizard.action_create_returns()
+ return_picking = self.env["stock.picking"].browse(action["res_id"])
+ if quantity is not None:
+ return_picking.move_ids.quantity = quantity
+ return_picking.with_context(skip_backorder=True).button_validate()
+ if run_cron:
+ self._run_auto_bill_cron()
+ return return_picking
def _bills_of(self, po):
return po.invoice_ids.sorted("id")
- def test_01_category_auto_bill_inherited_by_product(self):
- po = self._create_po([self._line(self.product_in_auto_categ, 5.0)])
+ def test_company_default_creates_bill(self):
+ self.company.auto_bill_on_receipt = True
+ po = self._create_po([self._line(self.product, 5.0)])
picking = self._receive(po)
bills = self._bills_of(po)
self.assertEqual(len(bills), 1)
@@ -105,40 +127,54 @@ def test_01_category_auto_bill_inherited_by_product(self):
self.assertEqual(bills.invoice_origin, po.name)
self.assertEqual(bills.invoice_date, picking.date_done.date())
- def test_02_product_override_auto_in_plain_category(self):
- po = self._create_po([self._line(self.product_force_auto, 3.0)])
+ def test_company_default_off_no_bill(self):
+ self.company.auto_bill_on_receipt = False
+ po = self._create_po([self._line(self.product, 5.0)])
+ self._receive(po)
+ self.assertFalse(self._bills_of(po))
+
+ def test_vendor_override_auto_with_company_off(self):
+ self.company.auto_bill_on_receipt = False
+ po = self._create_po([self._line(self.product, 3.0)], partner=self.vendor_auto)
self._receive(po)
bills = self._bills_of(po)
self.assertEqual(len(bills), 1)
self.assertEqual(bills.state, "posted")
- def test_03_product_override_no_auto_in_auto_category(self):
- po = self._create_po([self._line(self.product_force_no_auto, 2.0)])
+ def test_vendor_override_no_auto_with_company_on(self):
+ self.company.auto_bill_on_receipt = True
+ po = self._create_po(
+ [self._line(self.product, 2.0)], partner=self.vendor_no_auto
+ )
self._receive(po)
self.assertFalse(self._bills_of(po))
- def test_04_block_auto_bill_on_po(self):
- po = self._create_po([self._line(self.product_in_auto_categ, 4.0)])
+ def test_block_auto_bill_on_po(self):
+ self.company.auto_bill_on_receipt = True
+ po = self._create_po([self._line(self.product, 4.0)])
po.block_auto_bill = True
self._receive(po)
self.assertFalse(self._bills_of(po))
- def test_05_mixed_lines_only_eligible_billed(self):
+ def test_billed_on_ordered_qty_not_auto_billed(self):
+ self.company.auto_bill_on_receipt = True
po = self._create_po(
[
- self._line(self.product_in_auto_categ, 2.0),
- self._line(self.product_in_plain_categ, 3.0, price=20.0),
+ self._line(self.product, 2.0),
+ self._line(self.product_billed_on_order, 3.0, price=20.0),
]
)
self._receive(po)
bills = self._bills_of(po)
self.assertEqual(len(bills), 1)
self.assertEqual(bills.state, "posted")
- self.assertEqual(bills.invoice_line_ids.product_id, self.product_in_auto_categ)
+ # Only the line billed on received quantities is invoiced.
+ self.assertEqual(bills.invoice_line_ids.product_id, self.product)
self.assertEqual(bills.invoice_line_ids.quantity, 2.0)
- def test_06_partial_receipt_creates_one_bill_per_receipt(self):
- po = self._create_po([self._line(self.product_in_auto_categ, 10.0)])
+ def test_partial_receipt_creates_one_bill_per_receipt(self):
+ self.company.auto_bill_on_receipt = True
+ po = self._create_po([self._line(self.product, 10.0)])
first_picking = po.picking_ids
self._validate(first_picking, 4.0)
bills = self._bills_of(po)
@@ -155,7 +191,8 @@ def test_06_partial_receipt_creates_one_bill_per_receipt(self):
self.assertEqual(second_bill.invoice_line_ids.quantity, 6.0)
self.assertEqual(second_bill.state, "posted")
- def test_07_receipt_without_po_does_nothing(self):
+ def test_receipt_without_po_does_nothing(self):
+ self.company.auto_bill_on_receipt = True
picking = self.env["stock.picking"].create(
{
"picking_type_id": self.picking_type_in.id,
@@ -164,9 +201,9 @@ def test_07_receipt_without_po_does_nothing(self):
"move_ids": [
Command.create(
{
- "product_id": self.product_in_auto_categ.id,
+ "product_id": self.product.id,
"product_uom_qty": 1.0,
- "product_uom": self.product_in_auto_categ.uom_id.id,
+ "product_uom": self.product.uom_id.id,
"location_id": self.supplier_location.id,
"location_dest_id": self.stock_location.id,
}
@@ -187,20 +224,22 @@ def test_07_receipt_without_po_does_nothing(self):
)
)
- def test_08_idempotent_on_second_call(self):
- po = self._create_po([self._line(self.product_in_auto_categ, 5.0)])
+ def test_idempotent_on_second_call(self):
+ self.company.auto_bill_on_receipt = True
+ po = self._create_po([self._line(self.product, 5.0)])
self._receive(po)
self.assertEqual(len(self._bills_of(po)), 1)
po.picking_ids._auto_create_vendor_bill()
self.assertEqual(len(self._bills_of(po)), 1)
- def test_09_sections_carried_over_only_when_needed(self):
+ def test_sections_carried_over_only_when_needed(self):
+ self.company.auto_bill_on_receipt = True
po = self._create_po(
[
self._section("Billed Section"),
- self._line(self.product_in_auto_categ, 2.0),
+ self._line(self.product, 2.0),
self._section("Skipped Section"),
- self._line(self.product_in_plain_categ, 3.0, price=20.0),
+ self._line(self.product_billed_on_order, 3.0, price=20.0),
]
)
self._receive(po)
@@ -216,6 +255,139 @@ def test_09_sections_carried_over_only_when_needed(self):
product_lines = bills.invoice_line_ids.filtered(
lambda line: line.display_type == "product"
)
- self.assertEqual(product_lines.product_id, self.product_in_auto_categ)
+ self.assertEqual(product_lines.product_id, self.product)
# Section header keeps its place above its product line.
self.assertLess(sections.sequence, product_lines.sequence)
+
+ def test_commercial_partner_setting_applies_to_contact(self):
+ # A PO placed on a child contact uses the vendor company's policy.
+ self.company.auto_bill_on_receipt = False
+ vendor = self.env["res.partner"].create(
+ {"name": "Vendor Co", "is_company": True, "auto_bill_on_receipt": "auto"}
+ )
+ contact = self.env["res.partner"].create(
+ {"name": "Vendor Contact", "parent_id": vendor.id}
+ )
+ po = self._create_po([self._line(self.product, 3.0)], partner=contact)
+ self._receive(po)
+ bills = self._bills_of(po)
+ self.assertEqual(len(bills), 1)
+ self.assertEqual(bills.state, "posted")
+
+ def test_does_not_over_bill_when_partially_invoiced(self):
+ self.company.auto_bill_on_receipt = True
+ po = self._create_po([self._line(self.product, 10.0)])
+ # Validate the full receipt but hold the cron so we can inject a
+ # manual bill before the auto-bill runs.
+ self._receive(po, run_cron=False)
+ # Manually bill 4 of the 10 received units.
+ po.action_create_invoice()
+ manual_bill = po.invoice_ids
+ manual_bill.invoice_line_ids.quantity = 4.0
+ manual_bill.invoice_date = fields.Date.today()
+ manual_bill.action_post()
+ self.assertEqual(po.order_line.qty_to_invoice, 6.0)
+ # Now let the auto-bill cron run: it must bill only the remaining 6.
+ self._run_auto_bill_cron()
+ auto_bill = po.invoice_ids - manual_bill
+ self.assertEqual(len(auto_bill), 1)
+ self.assertEqual(auto_bill.invoice_line_ids.quantity, 6.0)
+ # The line is fully invoiced, not over-invoiced.
+ self.assertEqual(po.order_line.qty_invoiced, 10.0)
+
+ @mute_logger(
+ "odoo.addons.purchase_auto_bill_on_receipt.models.purchase_order",
+ "odoo.addons.purchase_auto_bill_on_receipt.models.stock_picking",
+ )
+ def test_creation_failure_logs_to_chatter(self):
+ self.company.auto_bill_on_receipt = True
+ po = self._create_po([self._line(self.product, 5.0)])
+ self._receive(po, run_cron=False)
+ with patch.object(
+ type(po),
+ "_auto_bill_create",
+ side_effect=Exception("test creation error"),
+ ):
+ self._run_auto_bill_cron()
+ self.assertFalse(self._bills_of(po))
+ self.assertIn("Auto bill creation failed", po.message_ids[0].body)
+ activity = po.activity_ids.filtered(
+ lambda a: a.activity_type_id == self.env.ref("mail.mail_activity_data_todo")
+ )
+ self.assertTrue(activity)
+
+ @mute_logger(
+ "odoo.addons.purchase_auto_bill_on_receipt.models.purchase_order",
+ "odoo.addons.purchase_auto_bill_on_receipt.models.stock_picking",
+ )
+ def test_posting_failure_logs_to_chatter(self):
+ self.company.auto_bill_on_receipt = True
+ po = self._create_po([self._line(self.product, 5.0)])
+ self._receive(po, run_cron=False)
+ with patch.object(
+ type(self.env["account.move"]),
+ "action_post",
+ side_effect=Exception("test posting error"),
+ ):
+ self._run_auto_bill_cron()
+ bills = self._bills_of(po)
+ self.assertEqual(len(bills), 1)
+ self.assertEqual(bills.state, "draft")
+ self.assertIn("Auto bill posting failed", po.message_ids[0].body)
+ activity = po.activity_ids.filtered(
+ lambda a: a.activity_type_id == self.env.ref("mail.mail_activity_data_todo")
+ )
+ self.assertTrue(activity)
+
+ # ── Return / Credit Note tests ──
+
+ def test_return_with_to_refund_creates_credit_note(self):
+ self.company.auto_bill_on_receipt = True
+ po = self._create_po([self._line(self.product, 5.0)])
+ picking = self._receive(po)
+ self.assertEqual(len(self._bills_of(po)), 1)
+ # Return 2 units with to_refund=True
+ self._return(picking, quantity=2.0, to_refund=True)
+ invoices = self._bills_of(po)
+ self.assertEqual(len(invoices), 2)
+ credit_note = invoices.filtered(lambda m: m.move_type == "in_refund")
+ self.assertEqual(len(credit_note), 1)
+ self.assertEqual(credit_note.state, "posted")
+ self.assertEqual(credit_note.invoice_line_ids.quantity, 2.0)
+
+ def test_return_without_to_refund_no_credit_note(self):
+ self.company.auto_bill_on_receipt = True
+ po = self._create_po([self._line(self.product, 5.0)])
+ picking = self._receive(po)
+ self._return(picking, quantity=2.0, to_refund=False)
+ invoices = self._bills_of(po)
+ # Only the original bill, no credit note.
+ self.assertEqual(len(invoices), 1)
+ self.assertEqual(invoices.move_type, "in_invoice")
+
+ def test_return_auto_bill_disabled_no_credit_note(self):
+ self.company.auto_bill_on_receipt = False
+ po = self._create_po(
+ [self._line(self.product, 5.0)], partner=self.vendor_no_auto
+ )
+ picking = self._receive(po)
+ self._return(picking, quantity=2.0, to_refund=True)
+ self.assertFalse(po.invoice_ids.filtered(lambda m: m.move_type == "in_refund"))
+
+ @mute_logger(
+ "odoo.addons.purchase_auto_bill_on_receipt.models.purchase_order",
+ "odoo.addons.purchase_auto_bill_on_receipt.models.stock_picking",
+ )
+ def test_return_creation_failure_logs_to_chatter(self):
+ self.company.auto_bill_on_receipt = True
+ po = self._create_po([self._line(self.product, 5.0)])
+ picking = self._receive(po)
+ self._return(picking, quantity=2.0, run_cron=False)
+ with patch.object(
+ type(po),
+ "_auto_bill_create",
+ side_effect=Exception("test refund creation error"),
+ ):
+ self._run_auto_bill_cron()
+ self.assertFalse(po.invoice_ids.filtered(lambda m: m.move_type == "in_refund"))
+ self.assertIn("Auto credit note creation failed", po.message_ids[0].body)
diff --git a/purchase_auto_bill_on_receipt/views/product_category_views.xml b/purchase_auto_bill_on_receipt/views/product_category_views.xml
deleted file mode 100644
index 8f3247a..0000000
--- a/purchase_auto_bill_on_receipt/views/product_category_views.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
- product.category
-
-
-
-
-
-
-
-
-
-
diff --git a/purchase_auto_bill_on_receipt/views/product_template_views.xml b/purchase_auto_bill_on_receipt/views/product_template_views.xml
deleted file mode 100644
index e54662c..0000000
--- a/purchase_auto_bill_on_receipt/views/product_template_views.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
- product.template
-
-
-
-
-
-
-
-
diff --git a/purchase_auto_bill_on_receipt/views/purchase_order_views.xml b/purchase_auto_bill_on_receipt/views/purchase_order_views.xml
index d51ecff..7390622 100644
--- a/purchase_auto_bill_on_receipt/views/purchase_order_views.xml
+++ b/purchase_auto_bill_on_receipt/views/purchase_order_views.xml
@@ -5,7 +5,7 @@
-
+
diff --git a/purchase_auto_bill_on_receipt/views/res_config_settings_views.xml b/purchase_auto_bill_on_receipt/views/res_config_settings_views.xml
new file mode 100644
index 0000000..5375408
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/views/res_config_settings_views.xml
@@ -0,0 +1,21 @@
+
+
+
+ res.config.settings
+
+
+
+
+
+
+
+
+
+
diff --git a/purchase_auto_bill_on_receipt/views/res_partner_views.xml b/purchase_auto_bill_on_receipt/views/res_partner_views.xml
new file mode 100644
index 0000000..d021a45
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/views/res_partner_views.xml
@@ -0,0 +1,16 @@
+
+
+
+ res.partner
+
+
+
+
+
+
+
+
+
From 04c7af657104fbd8bed319e01d8460c7cb011ac9 Mon Sep 17 00:00:00 2001
From: sanami
Date: Tue, 16 Jun 2026 05:12:00 +0000
Subject: [PATCH 3/3] add translation
---
purchase_auto_bill_on_receipt/i18n/ja.po | 187 ++++++++++++++++++
.../i18n/purchase_auto_bill_on_receipt.pot | 180 +++++++++++++++++
2 files changed, 367 insertions(+)
create mode 100644 purchase_auto_bill_on_receipt/i18n/ja.po
create mode 100644 purchase_auto_bill_on_receipt/i18n/purchase_auto_bill_on_receipt.pot
diff --git a/purchase_auto_bill_on_receipt/i18n/ja.po b/purchase_auto_bill_on_receipt/i18n/ja.po
new file mode 100644
index 0000000..fb0f405
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/i18n/ja.po
@@ -0,0 +1,187 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * purchase_auto_bill_on_receipt
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 19.0\n"
+"Report-Msgid-Bugs-To: \n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"Language: ja_JP\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#. module: purchase_auto_bill_on_receipt
+#. odoo-python
+#: code:addons/purchase_auto_bill_on_receipt/models/purchase_order.py:0
+msgid "Auto %(doc)s creation failed on %(src)s %(picking)s: %(error)s"
+msgstr "%(src)s %(picking)s での %(doc)s の自動作成に失敗しました: %(error)s"
+
+#. module: purchase_auto_bill_on_receipt
+#. odoo-python
+#: code:addons/purchase_auto_bill_on_receipt/models/purchase_order.py:0
+msgid "Auto %(doc)s creation failed — manual review needed"
+msgstr "%(doc)s の自動作成に失敗しました — 手動確認が必要です"
+
+#. module: purchase_auto_bill_on_receipt
+#. odoo-python
+#: code:addons/purchase_auto_bill_on_receipt/models/purchase_order.py:0
+msgid ""
+"Auto %(doc)s posting failed on %(src)s %(picking)s (%(doc)s %(bill)s remains"
+" in draft): %(error)s"
+msgstr ""
+"%(src)s %(picking)s での %(doc)s の仕訳の自動作成に失敗しました (%(doc)s %(bill)s は下書きのままです): %(error)s"
+
+#. module: purchase_auto_bill_on_receipt
+#. odoo-python
+#: code:addons/purchase_auto_bill_on_receipt/models/purchase_order.py:0
+msgid "Auto %(doc)s posting failed — manual review needed"
+msgstr "%(doc)s の仕訳の自動作成に失敗しました — 手動確認が必要です"
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields.selection,name:purchase_auto_bill_on_receipt.selection__res_partner__auto_bill_on_receipt__auto
+msgid "Auto Bill"
+msgstr "自動請求書"
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_stock_picking__auto_bill_pending
+msgid "Auto Bill Pending"
+msgstr "自動請求書保留中"
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_res_company__auto_bill_on_receipt
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_res_config_settings__auto_bill_on_receipt
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_res_partner__auto_bill_on_receipt
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_res_users__auto_bill_on_receipt
+msgid "Auto Bill on Purchase Receipt"
+msgstr "購買入荷時の自動請求書"
+
+#. module: purchase_auto_bill_on_receipt
+#: model_terms:ir.ui.view,arch_db:purchase_auto_bill_on_receipt.res_config_settings_view_form
+msgid ""
+"Automatically create and post a Vendor Bill when a purchase receipt is "
+"validated, and a Credit Note when a purchase return is validated. Can be "
+"overridden per vendor."
+msgstr ""
+"購買入荷の確認時に仕入先請求書と仕訳を自動作成、購買返品の確認時にクレジットノートとその仕訳を自動作成します。仕入先ごとに上書き設定が可能です。"
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_purchase_order__block_auto_bill
+msgid "Block Auto Bill"
+msgstr "自動請求書をブロック"
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model,name:purchase_auto_bill_on_receipt.model_res_company
+msgid "Companies"
+msgstr "会社"
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model,name:purchase_auto_bill_on_receipt.model_res_partner
+msgid "Contact"
+msgstr "連絡先"
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_purchase_order__display_name
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_res_company__display_name
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_res_config_settings__display_name
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_res_partner__display_name
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_stock_picking__display_name
+msgid "Display Name"
+msgstr "表示名"
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_purchase_order__id
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_res_company__id
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_res_config_settings__id
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_res_partner__id
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_stock_picking__id
+msgid "ID"
+msgstr "ID"
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields.selection,name:purchase_auto_bill_on_receipt.selection__res_partner__auto_bill_on_receipt__no_auto
+msgid "No Auto Bill"
+msgstr "自動請求書なし"
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields,help:purchase_auto_bill_on_receipt.field_res_partner__auto_bill_on_receipt
+#: model:ir.model.fields,help:purchase_auto_bill_on_receipt.field_res_users__auto_bill_on_receipt
+msgid ""
+"Override the company default for auto-billing on purchase receipt. Leave "
+"empty to inherit the company setting."
+msgstr ""
+"購買入荷時の自動請求書作成に関する会社のデフォルト設定を上書きします。会社の設定を継承するには空のままにしてください。"
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model,name:purchase_auto_bill_on_receipt.model_res_config_settings
+msgid "Procurement purchase grouping settings"
+msgstr "調達購買グループ設定"
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model,name:purchase_auto_bill_on_receipt.model_purchase_order
+msgid "Purchase Order"
+msgstr "購買オーダ"
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.actions.server,name:purchase_auto_bill_on_receipt.ir_cron_auto_bill_ir_actions_server
+msgid "Purchase: Auto Bill on Receipt"
+msgstr "購買: 入荷時の自動請求書"
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields,help:purchase_auto_bill_on_receipt.field_stock_picking__auto_bill_pending
+msgid ""
+"Set when a validated receipt or return is queued for auto-billing. The "
+"scheduled action picks it up to create and post the Vendor Bill or Credit "
+"Note."
+msgstr ""
+"確認済みの入荷または返品が自動請求書作成のキューに追加されたときにセットされます。スケジュールアクションがこれを取得し、仕入先請求書またはクレジットノートとその仕訳を作成します。"
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model,name:purchase_auto_bill_on_receipt.model_stock_picking
+msgid "Transfer"
+msgstr "転送"
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields,help:purchase_auto_bill_on_receipt.field_purchase_order__block_auto_bill
+msgid ""
+"When enabled, suppresses automatic bill creation on receipt for this order, "
+"regardless of the company or vendor settings."
+msgstr ""
+"有効にすると、会社または仕入先の設定に関わらず、この注文の入荷時に自動請求書の作成を抑制します。"
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields,help:purchase_auto_bill_on_receipt.field_res_company__auto_bill_on_receipt
+#: model:ir.model.fields,help:purchase_auto_bill_on_receipt.field_res_config_settings__auto_bill_on_receipt
+msgid ""
+"When enabled, validating an incoming purchase receipt automatically creates "
+"and posts a Vendor Bill. This is the default for all vendors and can be "
+"overridden per vendor."
+msgstr ""
+"有効にすると、仕入入荷の確認時に自動的に仕入先請求書と仕訳を作成します。これはすべての仕入先のデフォルト設定であり、仕入先ごとに上書きできます。"
+
+#. module: purchase_auto_bill_on_receipt
+#. odoo-python
+#: code:addons/purchase_auto_bill_on_receipt/models/purchase_order.py:0
+msgid "bill"
+msgstr "請求書"
+
+#. module: purchase_auto_bill_on_receipt
+#. odoo-python
+#: code:addons/purchase_auto_bill_on_receipt/models/purchase_order.py:0
+msgid "credit note"
+msgstr "クレジットノート"
+
+#. module: purchase_auto_bill_on_receipt
+#. odoo-python
+#: code:addons/purchase_auto_bill_on_receipt/models/purchase_order.py:0
+msgid "receipt"
+msgstr "入荷"
+
+#. module: purchase_auto_bill_on_receipt
+#. odoo-python
+#: code:addons/purchase_auto_bill_on_receipt/models/purchase_order.py:0
+msgid "return"
+msgstr "返品"
diff --git a/purchase_auto_bill_on_receipt/i18n/purchase_auto_bill_on_receipt.pot b/purchase_auto_bill_on_receipt/i18n/purchase_auto_bill_on_receipt.pot
new file mode 100644
index 0000000..e2384bc
--- /dev/null
+++ b/purchase_auto_bill_on_receipt/i18n/purchase_auto_bill_on_receipt.pot
@@ -0,0 +1,180 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * purchase_auto_bill_on_receipt
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 19.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: purchase_auto_bill_on_receipt
+#. odoo-python
+#: code:addons/purchase_auto_bill_on_receipt/models/purchase_order.py:0
+msgid "Auto %(doc)s creation failed on %(src)s %(picking)s: %(error)s"
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#. odoo-python
+#: code:addons/purchase_auto_bill_on_receipt/models/purchase_order.py:0
+msgid "Auto %(doc)s creation failed — manual review needed"
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#. odoo-python
+#: code:addons/purchase_auto_bill_on_receipt/models/purchase_order.py:0
+msgid ""
+"Auto %(doc)s posting failed on %(src)s %(picking)s (%(doc)s %(bill)s remains"
+" in draft): %(error)s"
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#. odoo-python
+#: code:addons/purchase_auto_bill_on_receipt/models/purchase_order.py:0
+msgid "Auto %(doc)s posting failed — manual review needed"
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields.selection,name:purchase_auto_bill_on_receipt.selection__res_partner__auto_bill_on_receipt__auto
+msgid "Auto Bill"
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_stock_picking__auto_bill_pending
+msgid "Auto Bill Pending"
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_res_company__auto_bill_on_receipt
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_res_config_settings__auto_bill_on_receipt
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_res_partner__auto_bill_on_receipt
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_res_users__auto_bill_on_receipt
+msgid "Auto Bill on Purchase Receipt"
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#: model_terms:ir.ui.view,arch_db:purchase_auto_bill_on_receipt.res_config_settings_view_form
+msgid ""
+"Automatically create and post a Vendor Bill when a purchase receipt is "
+"validated, and a Credit Note when a purchase return is validated. Can be "
+"overridden per vendor."
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_purchase_order__block_auto_bill
+msgid "Block Auto Bill"
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model,name:purchase_auto_bill_on_receipt.model_res_company
+msgid "Companies"
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model,name:purchase_auto_bill_on_receipt.model_res_partner
+msgid "Contact"
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_purchase_order__display_name
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_res_company__display_name
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_res_config_settings__display_name
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_res_partner__display_name
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_stock_picking__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_purchase_order__id
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_res_company__id
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_res_config_settings__id
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_res_partner__id
+#: model:ir.model.fields,field_description:purchase_auto_bill_on_receipt.field_stock_picking__id
+msgid "ID"
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields.selection,name:purchase_auto_bill_on_receipt.selection__res_partner__auto_bill_on_receipt__no_auto
+msgid "No Auto Bill"
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields,help:purchase_auto_bill_on_receipt.field_res_partner__auto_bill_on_receipt
+#: model:ir.model.fields,help:purchase_auto_bill_on_receipt.field_res_users__auto_bill_on_receipt
+msgid ""
+"Override the company default for auto-billing on purchase receipt. Leave "
+"empty to inherit the company setting."
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model,name:purchase_auto_bill_on_receipt.model_res_config_settings
+msgid "Procurement purchase grouping settings"
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model,name:purchase_auto_bill_on_receipt.model_purchase_order
+msgid "Purchase Order"
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.actions.server,name:purchase_auto_bill_on_receipt.ir_cron_auto_bill_ir_actions_server
+msgid "Purchase: Auto Bill on Receipt"
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields,help:purchase_auto_bill_on_receipt.field_stock_picking__auto_bill_pending
+msgid ""
+"Set when a validated receipt or return is queued for auto-billing. The "
+"scheduled action picks it up to create and post the Vendor Bill or Credit "
+"Note."
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model,name:purchase_auto_bill_on_receipt.model_stock_picking
+msgid "Transfer"
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields,help:purchase_auto_bill_on_receipt.field_purchase_order__block_auto_bill
+msgid ""
+"When enabled, suppresses automatic bill creation on receipt for this order, "
+"regardless of the company or vendor settings."
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#: model:ir.model.fields,help:purchase_auto_bill_on_receipt.field_res_company__auto_bill_on_receipt
+#: model:ir.model.fields,help:purchase_auto_bill_on_receipt.field_res_config_settings__auto_bill_on_receipt
+msgid ""
+"When enabled, validating an incoming purchase receipt automatically creates "
+"and posts a Vendor Bill. This is the default for all vendors and can be "
+"overridden per vendor."
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#. odoo-python
+#: code:addons/purchase_auto_bill_on_receipt/models/purchase_order.py:0
+msgid "bill"
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#. odoo-python
+#: code:addons/purchase_auto_bill_on_receipt/models/purchase_order.py:0
+msgid "credit note"
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#. odoo-python
+#: code:addons/purchase_auto_bill_on_receipt/models/purchase_order.py:0
+msgid "receipt"
+msgstr ""
+
+#. module: purchase_auto_bill_on_receipt
+#. odoo-python
+#: code:addons/purchase_auto_bill_on_receipt/models/purchase_order.py:0
+msgid "return"
+msgstr ""