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
89 changes: 89 additions & 0 deletions pos_next/api/test_wallet.py
Original file line number Diff line number Diff line change
@@ -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",
},
)
18 changes: 12 additions & 6 deletions pos_next/api/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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",
Expand Down
25 changes: 15 additions & 10 deletions pos_next/pos_next/doctype/wallet/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand All @@ -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",
Expand All @@ -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},
Expand Down
Loading