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
32 changes: 32 additions & 0 deletions pos_next/api/sales_invoice_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,35 @@ def before_cancel(doc, method=None):
alert=True,
indicator="orange"
)


def on_cancel(doc, method=None):
"""
On Cancel hook for Sales Invoice.
Roll back coupon usage only after the invoice has cancelled successfully.

Args:
doc: Sales Invoice document
method: Hook method name (unused)
"""
rollback_coupon_usage(doc)


def rollback_coupon_usage(doc):
"""Restore coupon usage for cancelled invoices that consumed a POS coupon."""
if doc.get("is_return"):
return

coupon_code = doc.get("coupon_code")
if not coupon_code or not frappe.db.table_exists("POS Coupon"):
return

try:
from pos_next.pos_next.doctype.pos_coupon.pos_coupon import decrement_coupon_usage

decrement_coupon_usage(coupon_code)
except Exception as e:
frappe.log_error(
title="Coupon Usage Rollback Failed",
message=f"Invoice: {doc.name}, Coupon: {coupon_code}, Error: {str(e)}\n{frappe.get_traceback()}",
)
74 changes: 74 additions & 0 deletions pos_next/api/test_sales_invoice_hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copyright (c) 2025, BrainWise and contributors
# For license information, please see license.txt

import unittest
from unittest.mock import Mock, patch

from pos_next.api.sales_invoice_hooks import on_cancel, rollback_coupon_usage


class TestSalesInvoiceHooks(unittest.TestCase):
@patch("pos_next.api.sales_invoice_hooks.frappe.db")
@patch("pos_next.pos_next.doctype.pos_coupon.pos_coupon.decrement_coupon_usage")
def test_rollback_coupon_usage_for_cancelled_invoice(self, mock_decrement, mock_db):
doc = Mock()
doc.get.side_effect = lambda key, default=None: {
"is_return": 0,
"coupon_code": "SAVE10",
}.get(key, default)
doc.name = "ACC-SINV-0001"
mock_db.table_exists.return_value = True

rollback_coupon_usage(doc)

mock_decrement.assert_called_once_with("SAVE10")

@patch("pos_next.api.sales_invoice_hooks.frappe.db")
@patch("pos_next.pos_next.doctype.pos_coupon.pos_coupon.decrement_coupon_usage")
def test_rollback_coupon_usage_skips_invoices_without_coupon(self, mock_decrement, mock_db):
doc = Mock()
doc.get.side_effect = lambda key, default=None: {
"is_return": 0,
"coupon_code": None,
}.get(key, default)

rollback_coupon_usage(doc)

mock_db.table_exists.assert_not_called()
mock_decrement.assert_not_called()

@patch("pos_next.api.sales_invoice_hooks.frappe.db")
@patch("pos_next.pos_next.doctype.pos_coupon.pos_coupon.decrement_coupon_usage")
def test_rollback_coupon_usage_skips_return_invoices(self, mock_decrement, mock_db):
doc = Mock()
doc.get.side_effect = lambda key, default=None: {
"is_return": 1,
"coupon_code": "SAVE10",
}.get(key, default)

rollback_coupon_usage(doc)

mock_db.table_exists.assert_not_called()
mock_decrement.assert_not_called()

@patch("pos_next.api.sales_invoice_hooks.frappe.db")
@patch("pos_next.pos_next.doctype.pos_coupon.pos_coupon.decrement_coupon_usage")
def test_rollback_coupon_usage_skips_when_coupon_table_missing(self, mock_decrement, mock_db):
doc = Mock()
doc.get.side_effect = lambda key, default=None: {
"is_return": 0,
"coupon_code": "SAVE10",
}.get(key, default)
mock_db.table_exists.return_value = False

rollback_coupon_usage(doc)

mock_decrement.assert_not_called()

@patch("pos_next.api.sales_invoice_hooks.rollback_coupon_usage")
def test_on_cancel_delegates_to_coupon_rollback(self, mock_rollback):
doc = Mock()

on_cancel(doc)

mock_rollback.assert_called_once_with(doc)
7 changes: 5 additions & 2 deletions pos_next/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,10 @@
"pos_next.realtime_events.emit_stock_update_event",
"pos_next.api.wallet.process_loyalty_to_wallet"
],
"on_cancel": "pos_next.realtime_events.emit_stock_update_event",
"on_cancel": [
"pos_next.api.sales_invoice_hooks.on_cancel",
"pos_next.realtime_events.emit_stock_update_event",
],
"after_insert": "pos_next.realtime_events.emit_invoice_created_event"
},
"POS Profile": {
Expand Down Expand Up @@ -294,4 +297,4 @@
# }


website_route_rules = [{'from_route': '/pos/<path:app_path>', 'to_route': 'pos'},]
website_route_rules = [{'from_route': '/pos/<path:app_path>', 'to_route': 'pos'},]
Loading