Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions sale_order_picking_hold_paid/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
============================
Sale Order Picking Hold Paid
============================

This module adds a dropdown field "Delivery Block Reason" to payment terms. If a sale order is validated with a payment term where a delivery block reason is selected, the creation of the picking will be held until the invoice has been fully paid.

The creation of manufacturing orders is not blocked, allowing products with routes Make-to-order and Manufacturing to proceed with production even if delivery is on hold.

Features
--------

* Add "Delivery Block Reason" dropdown to payment terms
* Add "Remove Block on Payment" checkbox to delivery block reasons for automatic removal upon payment
* Automatically hold delivery orders for sale orders with payment terms that have a delivery block reason selected
* Automatically create delivery orders when invoices are fully paid (if the block reason has "Remove Block on Payment" enabled)
* Allow manufacturing orders to be created even when delivery is on hold

Configuration
-------------

1. Go to: "Accounting" -> "Configuration" -> "Invoicing" -> "Payment Terms"
2. Edit or create a payment term
3. In the "Delivery Block Reason" field choose or create an option and
4. Expand the option that you choose
5. In the expanded view you will see the "Remove Block on Payment" option; enable it

Usage
-----

1. Go to: "Sales" -> "Orders" -> "Orders" -> "+New"
2. In the "Payment Terms" field make sure that the option you choose has "Remove Block on Payment" enabled
3. Confirm the sale order
4. Create the invoice and then confirm it
5. The delivery order will not be created until the invoice is fully paid
6. Once the invoice is paid go back into the sale order section
7. The delivery order will be automatically created if the "Delivery Block Reason" field that has the option selected with the "Remove Block on Payment" enabled

For products with Make-to-order (MTO) and Manufacturing routes enabled, manufacturing and delivery orders will be created upon clicking "Release Delivery Block" on Sale Order, regardless of payment status.

Bug Tracker
-----------

Bugs are tracked on `GitHub Issues <https://github.com/Nitrokey/odoo-modules/issues>`_.
In case of trouble, please check there if your issue has already been reported.

Credits
-------

Authors
~~~~~~~

* Nitrokey GmbH

Contributors
~~~~~~~~~~~~

* Nitrokey GmbH <[email protected]>
* Dmytro Kashuba <[email protected]>
1 change: 1 addition & 0 deletions sale_order_picking_hold_paid/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
22 changes: 22 additions & 0 deletions sale_order_picking_hold_paid/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2025 Nitrokey GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Sale Order Picking Hold Paid",
"version": "18.0.1.0.0",
"category": "Sales/Sales",
"summary": "Hold pickings until invoice is paid based on payment terms",
"author": "initOS GmbH, Nitrokey GmbH",
"website": "https://github.com/nitrokey/odoo-modules",
"license": "AGPL-3",
"depends": [
"sale_stock",
"account",
"mrp",
"sale_stock_picking_blocking",
"web_notify",
],
"data": [
"views/sale_stock_picking_blocking_reason_view.xml",
"views/sale_order_views.xml",
],
}
3 changes: 3 additions & 0 deletions sale_order_picking_hold_paid/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import account_move
from . import sale_delivery_block_reason
from . import sale_order
17 changes: 17 additions & 0 deletions sale_order_picking_hold_paid/models/account_move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright 2025 Nitrokey GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import models


class AccountMove(models.Model):
_inherit = "account.move"

def _invoice_paid_hook(self):
"""Check if delivery blocks should be removed when invoice is paid."""
res = super()._invoice_paid_hook()
orders = self.filtered(lambda move: move.is_invoice()).mapped(
"invoice_line_ids.sale_line_ids.order_id"
)
orders.with_context(auto_removal_on_payment=True).action_remove_delivery_block()
return res
14 changes: 14 additions & 0 deletions sale_order_picking_hold_paid/models/sale_delivery_block_reason.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright 2025 Nitrokey GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import fields, models


class SaleDeliveryBlockReason(models.Model):
_inherit = "sale.delivery.block.reason"

remove_on_payment = fields.Boolean(
"Remove Block on Payment",
help="If checked, this delivery block will be automatically removed "
"when the invoice is paid",
)
73 changes: 73 additions & 0 deletions sale_order_picking_hold_paid/models/sale_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Copyright 2025 Nitrokey GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import api, models


class SaleOrder(models.Model):
_inherit = "sale.order"

@api.model_create_multi
def create(self, vals):
"""Set delivery block when creating quotation with payment term that has
delivery block reason."""
order = super().create(vals)
if payment_term := order.payment_term_id:
order.delivery_block_id = payment_term.default_delivery_block_reason_id
return order

def action_confirm(self):
"""Ensure delivery block is set if payment term requires it."""
result = super().action_confirm()
for order in self:
payment_term = order.payment_term_id
block_reason = payment_term.default_delivery_block_reason_id
if payment_term and block_reason and not order.delivery_block_id:
order.delivery_block_id = block_reason
return result

def action_remove_delivery_block(self):
"""Remove the delivery block and create procurements as usual."""
if self.env.context.get("auto_removal_on_payment"):
self = self.filtered("delivery_block_id.remove_on_payment")
return super().action_remove_delivery_block()


class SaleOrderLine(models.Model):
_inherit = "sale.order.line"

def _action_launch_stock_rule(self, previous_product_uom_qty=False):
"""Allow manufacturing orders even when delivery is blocked."""
# Check if this line has manufacturing route
mto_route = self.env.ref("stock.route_warehouse0_mto", raise_if_not_found=False)
manufacture_route = self.env.ref(
"mrp.route_warehouse0_manufacture", raise_if_not_found=False
)

lines_to_process = self.env["sale.order.line"]

for line in self:
# Allow if no delivery block
if not line.order_id.delivery_block_id:
lines_to_process |= line
# Allow manufacturing lines even with delivery block
elif mto_route and manufacture_route:
# Get all routes for the product (including category routes)
product_routes = (
line.product_id.route_ids | line.product_id.categ_id.route_ids
)
# Also check warehouse routes
warehouse = line.order_id.warehouse_id
if warehouse:
product_routes |= warehouse.route_ids

# Check if product has both MTO and Manufacturing routes
has_mto_manufacture = (
mto_route in product_routes and manufacture_route in product_routes
)
if has_mto_manufacture:
lines_to_process |= line

return super(SaleOrderLine, lines_to_process)._action_launch_stock_rule(
previous_product_uom_qty=previous_product_uom_qty
)
1 change: 1 addition & 0 deletions sale_order_picking_hold_paid/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_sale_order_picking_hold_paid
Loading