From d94802067bc6b8ade91395df5de108a20c6ca979 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Mon, 17 Feb 2025 14:26:50 +0530 Subject: [PATCH] fix: disable partial payment in pos (#45752) * fix: disable partial payment in pos * test: disable partial payment * test: removed print statement * test: using save method to auto calculate paid_amount * test: paid_amount calculation using save method * test: added save method to calculate paid_amount * test: outstanding amount * test: added test for partial payments in pos invoice * fix: custom validation error for partial payment * test: using partial payment validation * fix: validate only on submit --- .../test_pos_closing_entry.py | 10 +++ .../doctype/pos_invoice/pos_invoice.py | 19 ++++++ .../doctype/pos_invoice/test_pos_invoice.py | 61 +++++++++++++++++-- .../test_pos_invoice_merge_log.py | 16 ++++- 4 files changed, 97 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py index 71ffafa9020..55aede00350 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py @@ -39,10 +39,12 @@ class TestPOSClosingEntry(IntegrationTestCase): pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1) pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500}) + pos_inv1.save() pos_inv1.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) + pos_inv2.save() pos_inv2.submit() pcv_doc = make_closing_entry_from_opening(opening_entry) @@ -68,6 +70,7 @@ class TestPOSClosingEntry(IntegrationTestCase): pos_inv = create_pos_invoice(rate=3500, do_not_submit=1, item_name="Test Item", without_item_code=1) pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500}) + pos_inv.save() pos_inv.submit() pcv_doc = make_closing_entry_from_opening(opening_entry) @@ -86,10 +89,12 @@ class TestPOSClosingEntry(IntegrationTestCase): pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1) pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500}) + pos_inv1.save() pos_inv1.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) + pos_inv2.save() pos_inv2.submit() # make return entry of pos_inv2 @@ -111,10 +116,12 @@ class TestPOSClosingEntry(IntegrationTestCase): pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1) pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500}) + pos_inv1.save() pos_inv1.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) + pos_inv2.save() pos_inv2.submit() pcv_doc = make_closing_entry_from_opening(opening_entry) @@ -165,6 +172,7 @@ class TestPOSClosingEntry(IntegrationTestCase): opening_entry = create_opening_entry(pos_profile, test_user.name) pos_inv1 = create_pos_invoice(rate=350, do_not_submit=1, pos_profile=pos_profile.name) pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500}) + pos_inv1.save() pos_inv1.submit() # if in between a mandatory accounting dimension is added to the POS Profile then @@ -226,6 +234,7 @@ class TestPOSClosingEntry(IntegrationTestCase): do_not_submit=True, ) pos_inv.payments[0].amount = pos_inv.grand_total + pos_inv.save() pos_inv.submit() pos_inv2 = create_pos_invoice( item_code=item_code, @@ -236,6 +245,7 @@ class TestPOSClosingEntry(IntegrationTestCase): do_not_submit=True, ) pos_inv2.payments[0].amount = pos_inv2.grand_total + pos_inv2.save() pos_inv2.submit() batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index b69c4efc317..11ee7d3e123 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -20,6 +20,10 @@ from erpnext.controllers.queries import item_query as _item_query from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos +class PartialPaymentValidationError(frappe.ValidationError): + pass + + class POSInvoice(SalesInvoice): # begin: auto-generated types # This code is auto-generated. Do not modify anything in this block. @@ -210,6 +214,7 @@ class POSInvoice(SalesInvoice): self.validate_payment_amount() self.validate_loyalty_transaction() self.validate_company_with_pos_company() + self.validate_full_payment() if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code @@ -484,6 +489,20 @@ class POSInvoice(SalesInvoice): if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points: validate_loyalty_points(self, self.loyalty_points) + def validate_full_payment(self): + invoice_total = flt(self.rounded_total) or flt(self.grand_total) + + if self.docstatus == 1: + if self.is_return and self.paid_amount != invoice_total: + frappe.throw( + msg=_("Partial Payment in POS Invoice is not allowed."), exc=PartialPaymentValidationError + ) + + if self.paid_amount < invoice_total: + frappe.throw( + msg=_("Partial Payment in POS Invoice is not allowed."), exc=PartialPaymentValidationError + ) + def set_status(self, update=False, status=None, update_modified=True): if self.is_new(): if self.get("amended_from"): diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 570a6739c98..95a0f4bb850 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -7,7 +7,7 @@ import frappe from frappe import _ from frappe.tests import IntegrationTestCase -from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return +from erpnext.accounts.doctype.pos_invoice.pos_invoice import PartialPaymentValidationError, make_sales_return from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.stock.doctype.item.test_item import make_item @@ -317,7 +317,7 @@ class TestPOSInvoice(IntegrationTestCase): ) pos.append( - "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1} + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2000, "default": 1} ) pos.insert() @@ -328,6 +328,11 @@ class TestPOSInvoice(IntegrationTestCase): # partial return 1 pos_return1.get("items")[0].qty = -1 + pos_return1.set("payments", []) + pos_return1.append( + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -1000, "default": 1} + ) + pos_return1.paid_amount = -1000 pos_return1.submit() pos_return1.reload() @@ -342,6 +347,11 @@ class TestPOSInvoice(IntegrationTestCase): # partial return 2 pos_return2 = make_sales_return(pos.name) + pos_return2.set("payments", []) + pos_return2.append( + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -1000, "default": 1} + ) + pos_return2.paid_amount = -1000 pos_return2.submit() self.assertEqual(pos_return2.get("items")[0].qty, -1) @@ -377,6 +387,15 @@ class TestPOSInvoice(IntegrationTestCase): inv.payments = [] self.assertRaises(frappe.ValidationError, inv.insert) + def test_partial_payment(self): + pos_inv = create_pos_invoice(rate=10000, do_not_save=1) + pos_inv.append( + "payments", + {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 9000}, + ) + pos_inv.insert() + self.assertRaises(PartialPaymentValidationError, pos_inv.submit) + def test_serialized_item_transaction(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item @@ -589,7 +608,13 @@ class TestPOSInvoice(IntegrationTestCase): "Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty" ) - inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000) + inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1) + inv.append( + "payments", + {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000}, + ) + inv.insert() + inv.submit() lpe = frappe.get_doc( "Loyalty Point Entry", @@ -615,7 +640,13 @@ class TestPOSInvoice(IntegrationTestCase): ) # add 10 loyalty points - create_pos_invoice(customer="Test Loyalty Customer", rate=10000) + pos_inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1) + pos_inv.append( + "payments", + {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000}, + ) + pos_inv.paid_amount = 10000 + pos_inv.submit() before_lp_details = get_loyalty_program_details_with_points( "Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty" @@ -649,10 +680,12 @@ class TestPOSInvoice(IntegrationTestCase): test_user, pos_profile = init_user_and_profile() pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1) pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 270}) + pos_inv.save() pos_inv.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) + pos_inv2.save() pos_inv2.submit() consolidate_pos_invoices() @@ -684,6 +717,7 @@ class TestPOSInvoice(IntegrationTestCase): "included_in_print_rate": 1, }, ) + pos_inv.save() pos_inv.submit() pos_inv2 = create_pos_invoice(rate=300, qty=2, do_not_submit=1) @@ -700,6 +734,7 @@ class TestPOSInvoice(IntegrationTestCase): "included_in_print_rate": 1, }, ) + pos_inv2.save() pos_inv2.submit() consolidate_pos_invoices() @@ -752,6 +787,7 @@ class TestPOSInvoice(IntegrationTestCase): "included_in_print_rate": 1, }, ) + pos_inv2.save() pos_inv2.submit() consolidate_pos_invoices() @@ -782,7 +818,10 @@ class TestPOSInvoice(IntegrationTestCase): # POS Invoice 1, for the batch without bundle pos_inv1 = create_pos_invoice(item="_BATCH ITEM Test For Reserve", rate=300, qty=15, do_not_save=1) - + pos_inv1.append( + "payments", + {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 4500}, + ) pos_inv1.items[0].batch_no = batch_no pos_inv1.save() pos_inv1.submit() @@ -798,8 +837,14 @@ class TestPOSInvoice(IntegrationTestCase): # POS Invoice 2, for the batch with bundle pos_inv2 = create_pos_invoice( - item="_BATCH ITEM Test For Reserve", rate=300, qty=10, batch_no=batch_no + item="_BATCH ITEM Test For Reserve", rate=300, qty=10, batch_no=batch_no, do_not_save=1 ) + pos_inv2.append( + "payments", + {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3000}, + ) + pos_inv2.save() + pos_inv2.submit() pos_inv2.reload() self.assertTrue(pos_inv2.items[0].serial_and_batch_bundle) @@ -834,6 +879,10 @@ class TestPOSInvoice(IntegrationTestCase): pos_inv1 = create_pos_invoice( item=item.name, rate=300, qty=1, do_not_submit=1, batch_no="TestBatch 01" ) + pos_inv1.append( + "payments", + {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300}, + ) pos_inv1.save() pos_inv1.submit() diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index 5ccefda16f6..ac4f682037d 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -40,14 +40,17 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase): pos_inv = create_pos_invoice(rate=300, do_not_submit=1) pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300}) + pos_inv.save() pos_inv.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) + pos_inv2.save() pos_inv2.submit() pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300}) + pos_inv3.save() pos_inv3.submit() consolidate_pos_invoices() @@ -73,14 +76,17 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase): pos_inv = create_pos_invoice(rate=300, do_not_submit=1) pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300}) + pos_inv.save() pos_inv.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) + pos_inv2.save() pos_inv2.submit() pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300}) + pos_inv3.save() pos_inv3.submit() pos_inv_cn = make_sales_return(pos_inv.name) @@ -135,6 +141,7 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase): ) inv.insert() inv.payments[0].amount = inv.grand_total + inv.save() inv.submit() inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True) @@ -152,6 +159,7 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase): ) inv2.insert() inv2.payments[0].amount = inv.grand_total + inv2.save() inv2.submit() consolidate_pos_invoices() @@ -291,7 +299,7 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase): inv2.submit() inv3 = create_pos_invoice(qty=3, rate=600, do_not_save=True) - inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000}) + inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1800}) inv3.insert() inv3.submit() @@ -299,8 +307,8 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase): inv.load_from_db() consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice) - self.assertEqual(consolidated_invoice.outstanding_amount, 800) - self.assertNotEqual(consolidated_invoice.status, "Paid") + self.assertNotEqual(consolidated_invoice.outstanding_amount, 800) + self.assertEqual(consolidated_invoice.status, "Paid") finally: frappe.set_user("Administrator") @@ -435,6 +443,7 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase): do_not_submit=1, ) pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) + pos_inv.save() pos_inv.submit() pos_inv_cn = make_sales_return(pos_inv.name) @@ -449,6 +458,7 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase): do_not_submit=1, ) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) + pos_inv2.save() pos_inv2.submit() consolidate_pos_invoices()