diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 0720d9b2e9..ddca68a57b 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -84,12 +84,20 @@ class POSInvoiceMergeLog(Document): sales_invoice.set_posting_time = 1 sales_invoice.posting_date = getdate(self.posting_date) sales_invoice.save() + self.write_off_fractional_amount(sales_invoice, data) sales_invoice.submit() self.consolidated_invoice = sales_invoice.name return sales_invoice.name + def write_off_fractional_amount(self, invoice, data): + pos_invoice_grand_total = sum(d.grand_total for d in data) + + if abs(pos_invoice_grand_total - invoice.grand_total) < 1: + invoice.write_off_amount += -1 * (pos_invoice_grand_total - invoice.grand_total) + invoice.save() + def process_merging_into_credit_note(self, data): credit_note = self.get_new_sales_invoice() credit_note.is_return = 1 @@ -102,6 +110,7 @@ class POSInvoiceMergeLog(Document): # TODO: return could be against multiple sales invoice which could also have been consolidated? # credit_note.return_against = self.consolidated_invoice credit_note.save() + self.write_off_fractional_amount(credit_note, data) credit_note.submit() self.consolidated_credit_note = credit_note.name @@ -135,9 +144,15 @@ class POSInvoiceMergeLog(Document): i.uom == item.uom and i.net_rate == item.net_rate and i.warehouse == item.warehouse): found = True i.qty = i.qty + item.qty + i.amount = i.amount + item.net_amount + i.net_amount = i.amount + i.base_amount = i.base_amount + item.base_net_amount + i.base_net_amount = i.base_amount if not found: item.rate = item.net_rate + item.amount = item.net_amount + item.base_amount = item.base_net_amount item.price_list_rate = 0 si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"}) items.append(si_item) @@ -169,6 +184,7 @@ class POSInvoiceMergeLog(Document): found = True if not found: payments.append(payment) + rounding_adjustment += doc.rounding_adjustment rounded_total += doc.rounded_total base_rounding_adjustment += doc.base_rounding_adjustment 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 3555da83a4..5930aa097f 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 @@ -12,6 +12,7 @@ from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_inv from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import ( consolidate_pos_invoices, ) +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry class TestPOSInvoiceMergeLog(unittest.TestCase): @@ -150,3 +151,132 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") + + + def test_consolidation_round_off_error_1(self): + ''' + Test round off error in consolidated invoice creation if POS Invoice has inclusive tax + ''' + + frappe.db.sql("delete from `tabPOS Invoice`") + + try: + make_stock_entry( + to_warehouse="_Test Warehouse - _TC", + item_code="_Test Item", + rate=8000, + qty=10, + ) + + init_user_and_profile() + + inv = create_pos_invoice(qty=3, rate=10000, do_not_save=True) + inv.append("taxes", { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 7.5, + "included_in_print_rate": 1 + }) + inv.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000 + }) + inv.insert() + inv.submit() + + inv2 = create_pos_invoice(qty=3, rate=10000, do_not_save=True) + inv2.append("taxes", { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 7.5, + "included_in_print_rate": 1 + }) + inv2.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000 + }) + inv2.insert() + inv2.submit() + + consolidate_pos_invoices() + + inv.load_from_db() + consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice) + self.assertEqual(consolidated_invoice.outstanding_amount, 0) + self.assertEqual(consolidated_invoice.status, 'Paid') + + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") + + def test_consolidation_round_off_error_2(self): + ''' + Test the same case as above but with an Unpaid POS Invoice + ''' + frappe.db.sql("delete from `tabPOS Invoice`") + + try: + make_stock_entry( + to_warehouse="_Test Warehouse - _TC", + item_code="_Test Item", + rate=8000, + qty=10, + ) + + init_user_and_profile() + + inv = create_pos_invoice(qty=6, rate=10000, do_not_save=True) + inv.append("taxes", { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 7.5, + "included_in_print_rate": 1 + }) + inv.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000 + }) + inv.insert() + inv.submit() + + inv2 = create_pos_invoice(qty=6, rate=10000, do_not_save=True) + inv2.append("taxes", { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 7.5, + "included_in_print_rate": 1 + }) + inv2.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000 + }) + inv2.insert() + 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.insert() + inv3.submit() + + consolidate_pos_invoices() + + 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') + + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 754ca81424..b894f90c7e 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -285,7 +285,7 @@ class SalesInvoice(SellingController): filters={ invoice_or_credit_note: self.name }, pluck="pos_closing_entry" ) - if pos_closing_entry: + if pos_closing_entry and pos_closing_entry[0]: msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format( frappe.bold("Consolidated Sales Invoice"), get_link_to_form("POS Closing Entry", pos_closing_entry[0]) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 075e3e38fa..2776628227 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -106,6 +106,9 @@ class calculate_taxes_and_totals(object): self.doc.conversion_rate = flt(self.doc.conversion_rate) def calculate_item_values(self): + if self.doc.get('is_consolidated'): + return + if not self.discount_amount_applied: for item in self.doc.get("items"): self.doc.round_floats_in(item) @@ -647,12 +650,12 @@ class calculate_taxes_and_totals(object): def calculate_change_amount(self): self.doc.change_amount = 0.0 self.doc.base_change_amount = 0.0 + grand_total = self.doc.rounded_total or self.doc.grand_total + base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total if self.doc.doctype == "Sales Invoice" \ - and self.doc.paid_amount > self.doc.grand_total and not self.doc.is_return \ + and self.doc.paid_amount > grand_total and not self.doc.is_return \ and any(d.type == "Cash" for d in self.doc.payments): - grand_total = self.doc.rounded_total or self.doc.grand_total - base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total self.doc.change_amount = flt(self.doc.paid_amount - grand_total + self.doc.write_off_amount, self.doc.precision("change_amount")) diff --git a/erpnext/tests/test_point_of_sale.py b/erpnext/tests/test_point_of_sale.py index df2dc8b99a..3299c8885f 100644 --- a/erpnext/tests/test_point_of_sale.py +++ b/erpnext/tests/test_point_of_sale.py @@ -1,15 +1,25 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt +import unittest + +import frappe from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.selling.page.point_of_sale.point_of_sale import get_items from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -from erpnext.tests.utils import ERPNextTestCase -class TestPointOfSale(ERPNextTestCase): +class TestPointOfSale(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + frappe.db.savepoint('before_test_point_of_sale') + + @classmethod + def tearDownClass(cls) -> None: + frappe.db.rollback(save_point='before_test_point_of_sale') + def test_item_search(self): """ Test Stock and Service Item Search.