diff --git a/pos_next/api/test_wallet.py b/pos_next/api/test_wallet.py new file mode 100644 index 00000000..954ce5d5 --- /dev/null +++ b/pos_next/api/test_wallet.py @@ -0,0 +1,89 @@ +# Copyright (c) 2025, BrainWise and contributors +# For license information, please see license.txt + +import unittest +from types import SimpleNamespace +from unittest.mock import Mock, patch + +from pos_next.api import wallet as api_wallet +from pos_next.pos_next.doctype.wallet import wallet as wallet_doctype + + +class TestWalletAPI(unittest.TestCase): + @patch("pos_next.api.wallet.frappe.db") + @patch("pos_next.api.wallet.frappe.get_all") + def test_get_pending_wallet_payments_only_counts_draft_wallet_payments_in_same_company( + self, + mock_get_all, + mock_db, + ): + mock_get_all.side_effect = [ + [ + SimpleNamespace(name="DRAFT-SAME"), + SimpleNamespace(name="EXCLUDED-DRAFT"), + ], + [ + SimpleNamespace(mode_of_payment="Wallet", amount=40), + SimpleNamespace(mode_of_payment="Cash", amount=10), + ], + ] + mock_db.get_value.side_effect = lambda doctype, name, field: 1 if name == "Wallet" else 0 + + result = api_wallet.get_pending_wallet_payments( + "Guest", + exclude_invoice="EXCLUDED-DRAFT", + company="Sonex", + ) + + self.assertEqual(result, 40) + self.assertEqual(mock_get_all.call_count, 2) + self.assertEqual( + mock_get_all.call_args_list[0].kwargs["filters"], + { + "customer": "Guest", + "docstatus": 0, + "outstanding_amount": [">", 0], + "is_pos": 1, + "company": "Sonex", + }, + ) + + @patch("pos_next.pos_next.doctype.wallet.wallet.get_pending_wallet_payments") + def test_wallet_get_available_balance_passes_company_to_pending_reservations(self, mock_pending): + mock_pending.return_value = 25 + wallet = Mock() + wallet.customer = "Guest" + wallet.company = "Sonex" + wallet.get_balance.return_value = 100 + + result = wallet_doctype.Wallet.get_available_balance(wallet) + + self.assertEqual(result, 75) + mock_pending.assert_called_once_with("Guest", company="Sonex") + + @patch("pos_next.pos_next.doctype.wallet.wallet.frappe.db") + @patch("pos_next.pos_next.doctype.wallet.wallet.frappe.get_all") + def test_doctype_pending_wallet_payments_uses_draft_only_and_company_scope( + self, + mock_get_all, + mock_db, + ): + mock_get_all.side_effect = [ + [SimpleNamespace(name="DRAFT-SAME")], + [SimpleNamespace(mode_of_payment="Wallet", amount=55)], + ] + mock_db.get_value.return_value = 1 + + result = wallet_doctype.get_pending_wallet_payments("Guest", company="Sonex") + + self.assertEqual(result, 55) + self.assertEqual( + mock_get_all.call_args_list[0].kwargs["filters"], + { + "customer": "Guest", + "docstatus": 0, + "outstanding_amount": [">", 0], + "is_pos": 1, + "company": "Sonex", + }, + ) diff --git a/pos_next/api/wallet.py b/pos_next/api/wallet.py index 6e3e9e5d..4b09756f 100644 --- a/pos_next/api/wallet.py +++ b/pos_next/api/wallet.py @@ -201,8 +201,12 @@ def get_customer_wallet_balance(customer, company=None, exclude_invoice=None): # Negate because negative receivable balance = positive wallet credit wallet_balance = -flt(gl_balance) - # Subtract pending wallet payments from open POS invoices - pending_wallet_amount = get_pending_wallet_payments(customer, exclude_invoice) + # Subtract wallet payments only from draft POS invoices in the same company. + pending_wallet_amount = get_pending_wallet_payments( + customer, + exclude_invoice=exclude_invoice, + company=company, + ) available_balance = flt(wallet_balance) - flt(pending_wallet_amount) @@ -213,16 +217,18 @@ def get_customer_wallet_balance(customer, company=None, exclude_invoice=None): return 0.0 -def get_pending_wallet_payments(customer, exclude_invoice=None): +def get_pending_wallet_payments(customer, exclude_invoice=None, company=None): """ - Get total wallet payments from unconsolidated/pending POS invoices. + Get total wallet payments from draft POS invoices that still reserve wallet balance. """ filters = { "customer": customer, - "docstatus": ["in", [0, 1]], + "docstatus": 0, "outstanding_amount": [">", 0], - "is_pos": 1 + "is_pos": 1, } + if company: + filters["company"] = company invoices = frappe.get_all( "Sales Invoice", diff --git a/pos_next/pos_next/doctype/wallet/wallet.py b/pos_next/pos_next/doctype/wallet/wallet.py index 77bc3379..dc9a769b 100644 --- a/pos_next/pos_next/doctype/wallet/wallet.py +++ b/pos_next/pos_next/doctype/wallet/wallet.py @@ -54,7 +54,7 @@ def get_balance(self): def get_available_balance(self): """Get available balance (current balance minus pending wallet payments)""" current = self.get_balance() - pending = get_pending_wallet_payments(self.customer) + pending = get_pending_wallet_payments(self.customer, company=self.company) available = flt(current) - flt(pending) return available if available > 0 else 0.0 @@ -120,8 +120,12 @@ def get_customer_wallet_balance(customer, company=None, exclude_invoice=None): # Negate because negative receivable balance = positive wallet credit wallet_balance = -flt(gl_balance) - # Subtract pending wallet payments from open POS invoices - pending_wallet_amount = get_pending_wallet_payments(customer, exclude_invoice) + # Subtract wallet payments only from draft POS invoices in the same company. + pending_wallet_amount = get_pending_wallet_payments( + customer, + exclude_invoice=exclude_invoice, + company=company, + ) available_balance = flt(wallet_balance) - flt(pending_wallet_amount) @@ -132,18 +136,20 @@ def get_customer_wallet_balance(customer, company=None, exclude_invoice=None): return 0.0 -def get_pending_wallet_payments(customer, exclude_invoice=None): +def get_pending_wallet_payments(customer, exclude_invoice=None, company=None): """ - Get total wallet payments from unconsolidated/pending POS invoices. - This prevents double-spending of wallet balance. + Get total wallet payments from draft POS invoices. + This prevents double-spending of wallet balance without double-counting + submitted invoices that are already reflected in GL. """ - # Get open Sales Invoices (draft or unconsolidated POS invoices) filters = { "customer": customer, - "docstatus": ["in", [0, 1]], # Draft or Submitted + "docstatus": 0, "outstanding_amount": [">", 0], - "is_pos": 1 + "is_pos": 1, } + if company: + filters["company"] = company invoices = frappe.get_all( "Sales Invoice", @@ -157,7 +163,6 @@ def get_pending_wallet_payments(customer, exclude_invoice=None): if exclude_invoice and invoice.name == exclude_invoice: continue - # Get wallet payments from this invoice payments = frappe.get_all( "Sales Invoice Payment", filters={"parent": invoice.name},