From 4e537cdb7451229ebc06695bd6003e46d49fd472 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Tue, 10 Jun 2025 17:51:11 +0530 Subject: [PATCH] refactor: sales invoice integration with pos (#47713) * fix: invoice doctype selection in accounts settings * test: change in accounts settings on sales invoice * test: refactored pos_invoice_merge_log tests * test: pos closing entry and pos invoice * fix: closing voucher details style * refactor: renamed fields and removed repeated methods * fix: patch to rename pos closing entry fields * refactor: replaced get_doc with sql query * fix: restrict cancelling sales invoice on cancellation of pos closing entry * fix: removed payment reconciliation summary field and rearranged total section fields * refactor: set_posting_date_and_time * test: create_sales_invoice added args for is_created_using_pos * test: added test for sales invoice creation during pos invoice mode * test: added test for pos invoice creation during sales invoice mode * fix: moved invoice type selection in pos settings * fix: pos additional fields label * refactor: pos closing entry rearranged fields, removed rate field from taxes field, fetching payments and taxes details * test: moved invoice creation in functions * refactor: using as_dict=1 * fix: wrong table chosen in query * fix: variable rename * test: fixed failing tests * test: fixed pos_closing_entry tests --- .../accounts_settings/accounts_settings.json | 10 +- .../accounts_settings/accounts_settings.py | 16 - .../closing_voucher_details.html | 8 +- .../pos_closing_entry/pos_closing_entry.js | 199 +---- .../pos_closing_entry/pos_closing_entry.json | 75 +- .../pos_closing_entry/pos_closing_entry.py | 320 ++++---- .../test_pos_closing_entry.py | 229 +++++- .../pos_closing_entry_taxes.json | 13 +- .../pos_closing_entry_taxes.py | 1 - .../doctype/pos_invoice/pos_invoice.py | 8 +- .../doctype/pos_invoice/test_pos_invoice.py | 9 +- .../pos_invoice_merge_log.py | 4 +- .../test_pos_invoice_merge_log.py | 747 +++++++++--------- .../doctype/pos_settings/pos_settings.json | 21 +- .../doctype/pos_settings/pos_settings.py | 18 + .../doctype/sales_invoice/sales_invoice.py | 6 +- .../sales_invoice/test_sales_invoice.py | 10 +- erpnext/patches.txt | 1 + .../v15_0/rename_pos_closing_entry_fields.py | 6 + .../page/point_of_sale/pos_controller.js | 7 +- 20 files changed, 891 insertions(+), 817 deletions(-) create mode 100644 erpnext/patches/v15_0/rename_pos_closing_entry_fields.py diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index fb9491b8ad1..e7a9fd690b9 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -65,7 +65,6 @@ "pos_setting_section", "post_change_gl_entries", "column_break_xrnd", - "use_sales_invoice_in_pos", "assets_tab", "asset_settings_section", "calculate_depr_using_total_days", @@ -550,13 +549,6 @@ "fieldname": "column_break_xrnd", "fieldtype": "Column Break" }, - { - "default": "0", - "description": "If enabled, Sales Invoice will be generated instead of POS Invoice in POS Transactions for real-time update of G/L and Stock Ledger.", - "fieldname": "use_sales_invoice_in_pos", - "fieldtype": "Check", - "label": "Use Sales Invoice" - }, { "default": "Buffered Cursor", "fieldname": "receivable_payable_fetch_method", @@ -630,7 +622,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-05-27 17:52:03.460522", + "modified": "2025-06-06 11:03:28.095723", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 4fdc3eef0b1..ca3efd0a358 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -72,7 +72,6 @@ class AccountsSettings(Document): unlink_advance_payment_on_cancelation_of_order: DF.Check unlink_payment_on_cancellation_of_invoice: DF.Check use_new_budget_controller: DF.Check - use_sales_invoice_in_pos: DF.Check # end: auto-generated types def validate(self): @@ -99,9 +98,6 @@ class AccountsSettings(Document): if old_doc.acc_frozen_upto != self.acc_frozen_upto: self.validate_pending_reposts() - if old_doc.use_sales_invoice_in_pos != self.use_sales_invoice_in_pos: - self.validate_invoice_mode_switch_in_pos() - if clear_cache: frappe.clear_cache() @@ -145,15 +141,3 @@ class AccountsSettings(Document): if self.has_value_changed("reconciliation_queue_size"): if cint(self.reconciliation_queue_size) < 5 or cint(self.reconciliation_queue_size) > 100: frappe.throw(_("Queue Size should be between 5 and 100")) - - def validate_invoice_mode_switch_in_pos(self): - pos_opening_entries_count = frappe.db.count( - "POS Opening Entry", filters={"docstatus": 1, "status": "Open"} - ) - if pos_opening_entries_count: - frappe.throw( - _("{0} can be enabled/disabled after all the POS Opening Entries are closed.").format( - frappe.bold(_("Use Sales Invoice")) - ), - title=_("Switch Invoice Mode Error"), - ) diff --git a/erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html b/erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html index 63e88cf44c2..301a583f7f9 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html +++ b/erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html @@ -1,11 +1,11 @@
-
+
-
{{ _("Sales Summary") }}
+
{{ _("Sales Summary") }}
@@ -32,7 +32,7 @@
-
{{ _("Mode of Payments") }}
+
{{ _("Mode of Payments") }}
@@ -57,7 +57,7 @@ {% if data.taxes %}
-
{{ _("Taxes") }}
+
{{ _("Taxes") }}
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js index 26c4591a854..65c717acffa 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js @@ -3,7 +3,7 @@ frappe.ui.form.on("POS Closing Entry", { onload: async function (frm) { - frm.ignore_doctypes_on_cancel_all = ["POS Invoice Merge Log"]; + frm.ignore_doctypes_on_cancel_all = ["POS Invoice Merge Log", "Sales Invoice"]; frm.set_query("pos_profile", function (doc) { return { filters: { user: doc.user }, @@ -36,17 +36,6 @@ frappe.ui.form.on("POS Closing Entry", { } }); - const is_pos_using_sales_invoice = await frappe.db.get_single_value( - "Accounts Settings", - "use_sales_invoice_in_pos" - ); - - if (is_pos_using_sales_invoice) { - frm.set_df_property("pos_transactions", "hidden", 1); - } - - set_html_data(frm); - if (frm.doc.docstatus == 1) { if (!frm.doc.posting_date) { frm.set_value("posting_date", frappe.datetime.nowdate()); @@ -91,8 +80,7 @@ frappe.ui.form.on("POS Closing Entry", { frappe.run_serially([ () => frappe.dom.freeze(__("Loading Invoices! Please Wait...")), () => frm.trigger("set_opening_amounts"), - () => frm.trigger("get_pos_invoices"), - () => frm.trigger("get_sales_invoices"), + () => frm.trigger("get_invoices"), () => frappe.dom.unfreeze(), ]); } @@ -112,9 +100,9 @@ frappe.ui.form.on("POS Closing Entry", { }); }, - get_pos_invoices(frm) { + get_invoices(frm) { return frappe.call({ - method: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices", + method: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_invoices", args: { start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date), end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date), @@ -122,101 +110,14 @@ frappe.ui.form.on("POS Closing Entry", { user: frm.doc.user, }, callback: (r) => { - let pos_docs = r.message; - set_pos_transaction_form_data(pos_docs, frm); + let inv_docs = r.message.invoices; + set_transaction_form_data(inv_docs, frm); + refresh_payments(r.message.payments, frm); + add_taxes(r.message.taxes, frm); refresh_fields(frm); - set_html_data(frm); }, }); }, - - get_sales_invoices(frm) { - return frappe.call({ - method: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_sales_invoices", - args: { - start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date), - end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date), - pos_profile: frm.doc.pos_profile, - user: frm.doc.user, - }, - callback: (r) => { - let sales_docs = r.message; - set_sales_invoice_transaction_form_data(sales_docs, frm); - refresh_fields(frm); - set_html_data(frm); - }, - }); - }, - - before_save: async function (frm) { - frappe.dom.freeze(__("Processing Sales! Please Wait...")); - - frm.set_value("grand_total", 0); - frm.set_value("net_total", 0); - frm.set_value("total_quantity", 0); - frm.set_value("taxes", []); - - for (let row of frm.doc.payment_reconciliation) { - row.expected_amount = row.opening_amount; - } - - const is_pos_using_sales_invoice = await frappe.db.get_single_value( - "Accounts Settings", - "use_sales_invoice_in_pos" - ); - - if (is_pos_using_sales_invoice) { - await Promise.all([ - frappe.call({ - method: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices", - args: { - start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date), - end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date), - pos_profile: frm.doc.pos_profile, - user: frm.doc.user, - }, - callback: (r) => { - let pos_invoices = r.message; - for (let doc of pos_invoices) { - frm.doc.grand_total += flt(doc.grand_total); - frm.doc.net_total += flt(doc.net_total); - frm.doc.total_quantity += flt(doc.total_qty); - refresh_payments(doc, frm, false); - refresh_taxes(doc, frm); - refresh_fields(frm); - set_html_data(frm); - } - }, - }), - ]); - } - - await Promise.all([ - frappe.call({ - method: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_sales_invoices", - args: { - start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date), - end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date), - pos_profile: frm.doc.pos_profile, - user: frm.doc.user, - }, - callback: (r) => { - let sales_invoices = r.message; - for (let doc of sales_invoices) { - frm.doc.grand_total += flt(doc.grand_total); - frm.doc.net_total += flt(doc.net_total); - frm.doc.total_quantity += flt(doc.total_qty); - refresh_payments(doc, frm, false); - refresh_taxes(doc, frm); - refresh_fields(frm); - set_html_data(frm); - } - }, - }), - ]); - - frappe.dom.unfreeze(); - }, }); frappe.ui.form.on("POS Closing Entry Detail", { @@ -226,57 +127,35 @@ frappe.ui.form.on("POS Closing Entry Detail", { }, }); -function set_pos_transaction_form_data(data, frm) { +function set_transaction_form_data(data, frm) { data.forEach((d) => { - add_to_pos_transaction(d, frm); + add_to_transaction(d, frm); frm.doc.grand_total += flt(d.grand_total); frm.doc.net_total += flt(d.net_total); frm.doc.total_quantity += flt(d.total_qty); - refresh_payments(d, frm, true); - refresh_taxes(d, frm); + frm.doc.total_taxes_and_charges += flt(d.total_taxes_and_charges); }); } -function set_sales_invoice_transaction_form_data(data, frm) { - data.forEach((d) => { - add_to_sales_invoice_transaction(d, frm); - frm.doc.grand_total += flt(d.grand_total); - frm.doc.net_total += flt(d.net_total); - frm.doc.total_quantity += flt(d.total_qty); - refresh_payments(d, frm, true); - refresh_taxes(d, frm); - }); -} - -function add_to_pos_transaction(d, frm) { - frm.add_child("pos_transactions", { - pos_invoice: d.name, +function add_to_transaction(d, frm) { + const field = d.doctype === "POS Invoice" ? "pos_invoices" : "sales_invoices"; + frm.add_child(field, { posting_date: d.posting_date, grand_total: d.grand_total, customer: d.customer, + ...(d.doctype === "POS Invoice" && { pos_invoice: d.name }), + ...(d.doctype === "Sales Invoice" && { sales_invoice: d.name }), }); } -function add_to_sales_invoice_transaction(d, frm) { - frm.add_child("sales_invoice_transactions", { - sales_invoice: d.name, - posting_date: d.posting_date, - grand_total: d.grand_total, - customer: d.customer, - }); -} - -function refresh_payments(d, frm, is_new) { - d.payments.forEach((p) => { +function refresh_payments(payments, frm) { + payments.forEach((p) => { const payment = frm.doc.payment_reconciliation.find( (pay) => pay.mode_of_payment === p.mode_of_payment ); - if (p.account == d.account_for_change_amount) { - p.amount -= flt(d.change_amount); - } if (payment) { payment.expected_amount += flt(p.amount); - if (is_new) payment.closing_amount = payment.expected_amount; + payment.closing_amount = payment.expected_amount; payment.difference = payment.closing_amount - payment.expected_amount; } else { frm.add_child("payment_reconciliation", { @@ -289,49 +168,33 @@ function refresh_payments(d, frm, is_new) { }); } -function refresh_taxes(d, frm) { - d.taxes.forEach((t) => { - const tax = frm.doc.taxes.find((tx) => tx.account_head === t.account_head && tx.rate === t.rate); - if (tax) { - tax.amount += flt(t.tax_amount); - } else { - frm.add_child("taxes", { - account_head: t.account_head, - rate: t.rate, - amount: t.tax_amount, - }); - } +function add_taxes(taxes, frm) { + taxes.forEach((t) => { + frm.add_child("taxes", { + account_head: t.account_head, + amount: t.tax_amount, + }); }); } function reset_values(frm) { - frm.set_value("pos_transactions", []); - frm.set_value("sales_invoice_transactions", []); + frm.set_value("pos_invoices", []); + frm.set_value("sales_invoices", []); frm.set_value("payment_reconciliation", []); frm.set_value("taxes", []); frm.set_value("grand_total", 0); frm.set_value("net_total", 0); + frm.set_value("total_taxes_and_charges", 0); frm.set_value("total_quantity", 0); } function refresh_fields(frm) { - frm.refresh_field("pos_transactions"); - frm.refresh_field("sales_invoice_transactions"); + frm.refresh_field("pos_invoices"); + frm.refresh_field("sales_invoices"); frm.refresh_field("payment_reconciliation"); frm.refresh_field("taxes"); frm.refresh_field("grand_total"); frm.refresh_field("net_total"); + frm.refresh_field("total_taxes_and_charges"); frm.refresh_field("total_quantity"); } - -function set_html_data(frm) { - if (frm.doc.docstatus === 1 && frm.doc.status == "Submitted") { - frappe.call({ - method: "get_payment_reconciliation_details", - doc: frm.doc, - callback: (r) => { - frm.get_field("payment_reconciliation_details").$wrapper.html(r.message); - }, - }); - } -} diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json index da925e36b41..09d2dc0c758 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json @@ -20,18 +20,19 @@ "pos_profile", "user", "section_break_12", - "pos_transactions", - "sales_invoice_transactions", - "section_break_9", - "payment_reconciliation_details", + "pos_invoices", + "sales_invoices", + "taxes_and_charges_section", + "taxes", + "section_break_13", + "column_break_16", + "total_quantity", + "column_break_ywgl", + "net_total", + "total_taxes_and_charges", + "grand_total", "section_break_11", "payment_reconciliation", - "section_break_13", - "grand_total", - "net_total", - "total_quantity", - "column_break_16", - "taxes", "failure_description_section", "error_message", "section_break_14", @@ -73,10 +74,12 @@ "label": "User Details" }, { + "fetch_if_empty": 1, "fieldname": "company", "fieldtype": "Link", "label": "Company", "options": "Company", + "read_only": 1, "reqd": 1 }, { @@ -85,11 +88,13 @@ }, { "fetch_from": "pos_opening_entry.pos_profile", + "fetch_if_empty": 1, "fieldname": "pos_profile", "fieldtype": "Link", "in_list_view": 1, "label": "POS Profile", "options": "POS Profile", + "read_only": 1, "reqd": 1 }, { @@ -100,16 +105,6 @@ "options": "User", "reqd": 1 }, - { - "fieldname": "section_break_9", - "fieldtype": "Section Break", - "read_only": 1 - }, - { - "depends_on": "eval:doc.docstatus==1", - "fieldname": "payment_reconciliation_details", - "fieldtype": "HTML" - }, { "fieldname": "section_break_11", "fieldtype": "Section Break", @@ -122,7 +117,6 @@ "options": "POS Closing Entry Detail" }, { - "collapsible": 1, "collapsible_depends_on": "eval:doc.docstatus==0", "fieldname": "section_break_13", "fieldtype": "Section Break", @@ -177,17 +171,12 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "pos_transactions", - "fieldtype": "Table", - "label": "POS Transactions", - "options": "POS Invoice Reference" - }, { "fieldname": "pos_opening_entry", "fieldtype": "Link", "label": "POS Opening Entry", "options": "POS Opening Entry", + "print_hide": 1, "reqd": 1 }, { @@ -230,10 +219,36 @@ "reqd": 1 }, { - "fieldname": "sales_invoice_transactions", + "fieldname": "pos_invoices", + "fieldtype": "Table", + "label": "POS Transactions", + "options": "POS Invoice Reference", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "sales_invoices", "fieldtype": "Table", "label": "Sales Invoice Transactions", - "options": "Sales Invoice Reference" + "options": "Sales Invoice Reference", + "print_hide": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "taxes_and_charges_section", + "fieldtype": "Section Break", + "label": "Taxes and Charges" + }, + { + "fieldname": "column_break_ywgl", + "fieldtype": "Column Break" + }, + { + "fieldname": "total_taxes_and_charges", + "fieldtype": "Currency", + "label": "Total Taxes and Charges", + "read_only": 1 } ], "grid_page_length": 50, @@ -244,7 +259,7 @@ "link_fieldname": "pos_closing_entry" } ], - "modified": "2025-03-19 19:49:58.845697", + "modified": "2025-06-06 12:00:31.955176", "modified_by": "Administrator", "module": "Accounts", "name": "POS Closing Entry", diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py index 0d3724a4bb0..fca87c613f6 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py @@ -4,7 +4,10 @@ import frappe from frappe import _ -from frappe.utils import flt, get_datetime +from frappe.query_builder import DocType +from frappe.query_builder import functions as fn +from frappe.query_builder.custom import ConstantColumn +from frappe.utils import flt from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import ( consolidate_pos_invoices, @@ -41,35 +44,45 @@ class POSClosingEntry(StatusUpdater): payment_reconciliation: DF.Table[POSClosingEntryDetail] period_end_date: DF.Datetime period_start_date: DF.Datetime + pos_invoices: DF.Table[POSInvoiceReference] pos_opening_entry: DF.Link pos_profile: DF.Link - pos_transactions: DF.Table[POSInvoiceReference] posting_date: DF.Date posting_time: DF.Time - sales_invoice_transactions: DF.Table[SalesInvoiceReference] + sales_invoices: DF.Table[SalesInvoiceReference] status: DF.Literal["Draft", "Submitted", "Queued", "Failed", "Cancelled"] taxes: DF.Table[POSClosingEntryTaxes] total_quantity: DF.Float + total_taxes_and_charges: DF.Currency user: DF.Link # end: auto-generated types def validate(self): - self.posting_date = self.posting_date or frappe.utils.nowdate() - self.posting_time = self.posting_time or frappe.utils.nowtime() + self.set_posting_date_and_time() + self.fetch_invoice_type() + self.validate_pos_opening_entry() + self.validate_invoice_mode() + def set_posting_date_and_time(self): + if self.posting_date: + self.posting_date = frappe.utils.nowdate() + if self.posting_time: + self.posting_time = frappe.utils.nowtime() + + def fetch_invoice_type(self): + self.invoice_type = frappe.db.get_single_value("POS Settings", "invoice_type") + + def validate_pos_opening_entry(self): if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open": frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry")) - self.is_pos_using_sales_invoice = frappe.get_single_value( - "Accounts Settings", "use_sales_invoice_in_pos" - ) - - if self.is_pos_using_sales_invoice == 0: + def validate_invoice_mode(self): + if self.invoice_type == "POS Invoice": self.validate_duplicate_pos_invoices() self.validate_pos_invoices() - if self.is_pos_using_sales_invoice == 1: - if len(self.pos_transactions) != 0: + if self.invoice_type == "Sales Invoice": + if len(self.pos_invoices) != 0: frappe.throw(_("POS Invoices can't be added when Sales Invoice is enabled")) self.validate_duplicate_sales_invoices() @@ -77,7 +90,7 @@ class POSClosingEntry(StatusUpdater): def validate_duplicate_pos_invoices(self): pos_occurences = {} - for idx, inv in enumerate(self.pos_transactions, 1): + for idx, inv in enumerate(self.pos_invoices, 1): pos_occurences.setdefault(inv.pos_invoice, []).append(idx) error_list = [] @@ -92,7 +105,7 @@ class POSClosingEntry(StatusUpdater): def validate_pos_invoices(self): invalid_rows = [] - for d in self.pos_transactions: + for d in self.pos_invoices: invalid_row = {"idx": d.idx} pos_invoice = frappe.db.get_values( "POS Invoice", @@ -130,7 +143,7 @@ class POSClosingEntry(StatusUpdater): def validate_duplicate_sales_invoices(self): sales_invoice_occurrences = {} - for idx, inv in enumerate(self.sales_invoice_transactions, 1): + for idx, inv in enumerate(self.sales_invoices, 1): sales_invoice_occurrences.setdefault(inv.sales_invoice, []).append(idx) error_list = [] @@ -145,7 +158,7 @@ class POSClosingEntry(StatusUpdater): def validate_sales_invoices(self): invalid_rows = [] - for d in self.sales_invoice_transactions: + for d in self.sales_invoices: invalid_row = {"idx": d.idx} sales_invoice = frappe.db.get_values( "Sales Invoice", @@ -193,14 +206,6 @@ class POSClosingEntry(StatusUpdater): frappe.throw(error_list, title=_("Invalid Sales Invoices"), as_list=True) - @frappe.whitelist() - def get_payment_reconciliation_details(self): - currency = frappe.get_cached_value("Company", self.company, "default_currency") - return frappe.render_template( - "erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html", - {"data": self, "currency": currency}, - ) - def on_submit(self): consolidate_pos_invoices(closing_entry=self) frappe.publish_realtime( @@ -227,7 +232,7 @@ class POSClosingEntry(StatusUpdater): opening_entry.save() def update_sales_invoices_closing_entry(self, cancel=False): - for d in self.sales_invoice_transactions: + for d in self.sales_invoices: frappe.db.set_value( "Sales Invoice", d.sales_invoice, "pos_closing_entry", self.name if not cancel else None ) @@ -241,50 +246,133 @@ def get_cashiers(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() -def get_pos_invoices(start, end, pos_profile, user): - data = frappe.db.sql( - """ - select - name, timestamp(posting_date, posting_time) as "timestamp" - from - `tabPOS Invoice` - where - owner = %s and docstatus = 1 and pos_profile = %s and ifnull(consolidated_invoice,'') = '' - """, - (user, pos_profile), - as_dict=1, +def get_invoices(start, end, pos_profile, user): + invoice_doctype = frappe.db.get_single_value("POS Settings", "invoice_type") + + SalesInvoice = DocType("Sales Invoice") + sales_inv_query = ( + frappe.qb.from_(SalesInvoice) + .select( + SalesInvoice.name, + SalesInvoice.customer, + SalesInvoice.posting_date, + SalesInvoice.grand_total, + SalesInvoice.net_total, + SalesInvoice.total_qty, + SalesInvoice.total_taxes_and_charges, + fn.Timestamp(SalesInvoice.posting_date, SalesInvoice.posting_time).as_("timestamp"), + ConstantColumn("Sales Invoice").as_("doctype"), + SalesInvoice.change_amount, + SalesInvoice.account_for_change_amount, + ) + .where( + (SalesInvoice.owner == user) + & (SalesInvoice.docstatus == 1) + & (SalesInvoice.is_pos == 1) + & (SalesInvoice.pos_profile == pos_profile) + & (SalesInvoice.is_created_using_pos == 1) + & fn.IfNull(SalesInvoice.pos_closing_entry, "").eq("") + & ( + (fn.Timestamp(SalesInvoice.posting_date, SalesInvoice.posting_time) >= start) + & (fn.Timestamp(SalesInvoice.posting_date, SalesInvoice.posting_time) <= end) + ) + ) ) - data = list(filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data)) - # need to get taxes and payments so can't avoid get_doc - data = [frappe.get_doc("POS Invoice", d.name).as_dict() for d in data] + query = sales_inv_query + + if invoice_doctype == "POS Invoice": + POSInvoice = DocType("POS Invoice") + pos_inv_query = ( + frappe.qb.from_(POSInvoice) + .select( + POSInvoice.name, + POSInvoice.customer, + POSInvoice.posting_date, + POSInvoice.grand_total, + POSInvoice.net_total, + POSInvoice.total_qty, + POSInvoice.total_taxes_and_charges, + fn.Timestamp(POSInvoice.posting_date, POSInvoice.posting_time).as_("timestamp"), + ConstantColumn("POS Invoice").as_("doctype"), + POSInvoice.change_amount, + POSInvoice.account_for_change_amount, + ) + .where( + (POSInvoice.owner == user) + & (POSInvoice.docstatus == 1) + & (POSInvoice.pos_profile == pos_profile) + & ( + (fn.Timestamp(POSInvoice.posting_date, POSInvoice.posting_time) >= start) + & (fn.Timestamp(POSInvoice.posting_date, POSInvoice.posting_time) <= end) + ) + & fn.IfNull(POSInvoice.consolidated_invoice, "").eq("") + ) + ) + query = query + pos_inv_query + + query = query.orderby(query.timestamp) + invoices = query.run(as_dict=1) + + data = {"invoices": invoices, "payments": get_payments(invoices), "taxes": get_taxes(invoices)} return data -@frappe.whitelist() -def get_sales_invoices(start, end, pos_profile, user): - data = frappe.db.sql( - """ - select - name, timestamp(posting_date, posting_time) as "timestamp" - from - `tabSales Invoice` - where - owner = %s - and docstatus = 1 - and is_pos = 1 - and pos_profile = %s - and is_created_using_pos = 1 - and ifnull(pos_closing_entry,'') = '' - """, - (user, pos_profile), - as_dict=1, - ) +def get_payments(invoices): + if not len(invoices): + return - data = [d for d in data if get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end)] - # need to get taxes and payments so can't avoid get_doc - data = [frappe.get_doc("Sales Invoice", d.name).as_dict() for d in data] + invoices_name = [d.name for d in invoices] + + SalesInvoicePayment = DocType("Sales Invoice Payment") + query = ( + frappe.qb.from_(SalesInvoicePayment) + .where( + (SalesInvoicePayment.parenttype.isin(["Sales Invoice", "POS Invoice"])) + & (SalesInvoicePayment.parent.isin(invoices_name)) + ) + .groupby(SalesInvoicePayment.mode_of_payment) + .select( + SalesInvoicePayment.mode_of_payment, + SalesInvoicePayment.account, + fn.Sum(SalesInvoicePayment.amount).as_("amount"), + ) + ) + data = query.run(as_dict=1) + + change_amount_by_account = {} + for d in invoices: + change_amount_by_account.setdefault(d.account_for_change_amount, 0) + change_amount_by_account[d.account_for_change_amount] += flt(d.change_amount) + + for d in data: + if change_amount_by_account.get(d.account): + d.amount -= flt(change_amount_by_account.get(d.account)) + + return data + + +def get_taxes(invoices): + if not len(invoices): + return + + invoices_name = [d.name for d in invoices] + + SalesInvoiceTaxesCharges = DocType("Sales Taxes and Charges") + query = ( + frappe.qb.from_(SalesInvoiceTaxesCharges) + .where( + (SalesInvoiceTaxesCharges.parenttype.isin(["Sales Invoice", "POS Invoice"])) + & (SalesInvoiceTaxesCharges.parent.isin(invoices_name)) + ) + .groupby(SalesInvoiceTaxesCharges.account_head) + .select( + SalesInvoiceTaxesCharges.account_head, + fn.Sum(SalesInvoiceTaxesCharges.tax_amount_after_discount_amount).as_("tax_amount"), + ) + ) + data = query.run(as_dict=1) return data @@ -300,97 +388,53 @@ def make_closing_entry_from_opening(opening_entry): closing_entry.grand_total = 0 closing_entry.net_total = 0 closing_entry.total_quantity = 0 + closing_entry.total_taxes_and_charges = 0 - is_pos_using_sales_invoice = frappe.get_single_value("Accounts Settings", "use_sales_invoice_in_pos") - - pos_invoices = ( - get_pos_invoices( - closing_entry.period_start_date, - closing_entry.period_end_date, - closing_entry.pos_profile, - closing_entry.user, - ) - if is_pos_using_sales_invoice == 0 - else [] - ) - - sales_invoices = get_sales_invoices( + data = get_invoices( closing_entry.period_start_date, closing_entry.period_end_date, closing_entry.pos_profile, closing_entry.user, ) - pos_transactions = [] - sales_invoice_transactions = [] - taxes = [] - payments = [] - for detail in opening_entry.balance_details: - payments.append( - frappe._dict( - { - "mode_of_payment": detail.mode_of_payment, - "opening_amount": detail.opening_amount, - "expected_amount": detail.opening_amount, - } - ) + pos_invoices = [] + sales_invoices = [] + taxes = [ + frappe._dict({"account_head": tx.account_head, "amount": tx.tax_amount}) for tx in data.get("taxes") + ] + payments = [ + frappe._dict( + { + "mode_of_payment": p.mode_of_payment, + "opening_amount": 0, + "expected_amount": p.amount, + } ) + for p in data.get("payments") + ] - for d in pos_invoices: - pos_transactions.append( - frappe._dict( - { - "pos_invoice": d.name, - "posting_date": d.posting_date, - "grand_total": d.grand_total, - "customer": d.customer, - } - ) + for d in data.get("invoices"): + invoice = "pos_invoice" if d.doctype == "POS Invoice" else "sales_invoice" + invoice_data = frappe._dict( + { + invoice: d.name, + "posting_date": d.posting_date, + "grand_total": d.grand_total, + "customer": d.customer, + } ) + if d.doctype == "POS Invoice": + pos_invoices.append(invoice_data) + else: + sales_invoices.append(invoice_data) - for d in sales_invoices: - sales_invoice_transactions.append( - frappe._dict( - { - "sales_invoice": d.name, - "posting_date": d.posting_date, - "grand_total": d.grand_total, - "customer": d.customer, - } - ) - ) - - for d in [*pos_invoices, *sales_invoices]: closing_entry.grand_total += flt(d.grand_total) closing_entry.net_total += flt(d.net_total) closing_entry.total_quantity += flt(d.total_qty) + closing_entry.total_taxes_and_charges += flt(d.total_taxes_and_charges) - for t in d.taxes: - existing_tax = [tx for tx in taxes if tx.account_head == t.account_head and tx.rate == t.rate] - if existing_tax: - existing_tax[0].amount += flt(t.tax_amount) - else: - taxes.append( - frappe._dict({"account_head": t.account_head, "rate": t.rate, "amount": t.tax_amount}) - ) - - for p in d.payments: - existing_pay = [pay for pay in payments if pay.mode_of_payment == p.mode_of_payment] - if existing_pay: - existing_pay[0].expected_amount += flt(p.amount) - else: - payments.append( - frappe._dict( - { - "mode_of_payment": p.mode_of_payment, - "opening_amount": 0, - "expected_amount": p.amount, - } - ) - ) - - closing_entry.set("pos_transactions", pos_transactions) - closing_entry.set("sales_invoice_transactions", sales_invoice_transactions) + closing_entry.set("pos_invoices", pos_invoices) + closing_entry.set("sales_invoices", sales_invoices) closing_entry.set("payment_reconciliation", payments) closing_entry.set("taxes", taxes) 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 fd620053728..148a5a83030 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 @@ -12,10 +12,10 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import ( make_closing_entry_from_opening, ) -from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry 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.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.serial_and_batch_bundle.test_serial_and_batch_bundle import ( @@ -25,8 +25,18 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry class TestPOSClosingEntry(IntegrationTestCase): + @classmethod + def setUpClass(cls): + frappe.db.sql("delete from `tabPOS Opening Entry`") + cls.enterClassContext(cls.change_settings("POS Settings", {"invoice_type": "POS Invoice"})) + + @classmethod + def tearDownClass(cls): + frappe.db.sql("delete from `tabPOS Opening Entry`") + def setUp(self): # Make stock available for POS Sales + frappe.db.sql("delete from `tabPOS Opening Entry`") make_stock_entry(target="_Test Warehouse - _TC", qty=2, basic_rate=100) def tearDown(self): @@ -82,6 +92,8 @@ class TestPOSClosingEntry(IntegrationTestCase): """ Test if quantity is calculated correctly for an item in POS Closing Entry """ + from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return + test_user, pos_profile = init_user_and_profile() opening_entry = create_opening_entry(pos_profile, test_user.name) @@ -200,9 +212,6 @@ class TestPOSClosingEntry(IntegrationTestCase): from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import ( init_user_and_profile, ) - from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import ( - consolidate_pos_invoices, - ) from erpnext.stock.doctype.batch.batch import get_batch_qty frappe.db.sql("delete from `tabPOS Invoice`") @@ -293,45 +302,171 @@ class TestPOSClosingEntry(IntegrationTestCase): batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code) self.assertEqual(batch_qty_with_pos, 10.0) + @IntegrationTestCase.change_settings("POS Settings", {"invoice_type": "Sales Invoice"}) def test_closing_entries_with_sales_invoice(self): - from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + test_user, pos_profile = init_user_and_profile() + opening_entry = create_opening_entry(pos_profile, test_user.name) + + pos_si = create_sales_invoice( + qty=10, is_created_using_pos=1, pos_profile=pos_profile.name, do_not_save=1 + ) + pos_si.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000}) + pos_si.save() + pos_si.submit() + + pos_si2 = create_sales_invoice( + qty=5, is_created_using_pos=1, pos_profile=pos_profile.name, do_not_save=11 + ) + pos_si2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000}) + pos_si2.save() + pos_si2.submit() + + pcv_doc = make_closing_entry_from_opening(opening_entry) + payment = pcv_doc.payment_reconciliation[0] + + self.assertEqual(payment.mode_of_payment, "Cash") + + for d in pcv_doc.payment_reconciliation: + if d.mode_of_payment == "Cash": + d.closing_amount = 1500 + + pcv_doc.submit() + + self.assertEqual(pcv_doc.total_quantity, 15) + self.assertEqual(pcv_doc.net_total, 1500) + + pos_si2.reload() + self.assertEqual(pos_si2.pos_closing_entry, pcv_doc.name) + + def test_sales_invoice_in_pos_invoice_mode(self): + """ + Test Sales Invoice and Return Sales Invoice creation during POS Invoice mode. + """ + from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return test_user, pos_profile = init_user_and_profile() - # Deleting all opening entry - frappe.db.sql("delete from `tabPOS Opening Entry`") - with self.change_settings("Accounts Settings", {"use_sales_invoice_in_pos": 1}): - opening_entry = create_opening_entry(pos_profile, test_user.name) + with self.change_settings("POS Settings", {"invoice_type": "Sales Invoice"}): + opening_entry1 = create_opening_entry(pos_profile, test_user.name) - pos_si = create_sales_invoice(qty=10, do_not_save=1) - pos_si.is_pos = 1 - pos_si.pos_profile = pos_profile.name - pos_si.is_created_using_pos = 1 - pos_si.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000}) - pos_si.save() - pos_si.submit() + pos_si1, pos_si2 = create_multiple_sales_invoices(pos_profile) - pos_si2 = create_sales_invoice(qty=5, do_not_save=1) - pos_si2.is_pos = 1 - pos_si2.pos_profile = pos_profile.name - pos_si2.is_created_using_pos = 1 - pos_si2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000}) - pos_si2.save() - pos_si2.submit() + pos_inv = create_pos_invoice(rate=100, do_not_save=1) + pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) + self.assertRaises(frappe.ValidationError, pos_inv.save) - pcv_doc = make_closing_entry_from_opening(opening_entry) - payment = pcv_doc.payment_reconciliation[0] - - self.assertEqual(payment.mode_of_payment, "Cash") - - for d in pcv_doc.payment_reconciliation: + pcv_doc1 = make_closing_entry_from_opening(opening_entry1) + for d in pcv_doc1.payment_reconciliation: if d.mode_of_payment == "Cash": - d.closing_amount = 1500 + d.closing_amount = 300 - pcv_doc.submit() + pcv_doc1.submit() + self.assertTrue(pcv_doc1.name) - self.assertEqual(pcv_doc.total_quantity, 15) - self.assertEqual(pcv_doc.net_total, 1500) + pos_si1.reload() + pos_si2.reload() + self.assertEqual(pos_si1.pos_closing_entry, pcv_doc1.name) + self.assertEqual(pos_si2.pos_closing_entry, pcv_doc1.name) + + with self.change_settings("POS Settings", {"invoice_type": "POS Invoice"}): + opening_entry2 = create_opening_entry(pos_profile, test_user.name) + + pos_inv1, pos_inv2 = create_multiple_pos_invoices(pos_profile) + + # Trying to create Sales Invoice when invoice_type is set to POS Invoice. + pos_si3 = create_sales_invoice( + qty=1, is_created_using_pos=1, pos_profile=pos_profile.name, do_not_save=1 + ) + pos_si3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) + self.assertRaises(frappe.ValidationError, pos_si3.save) + + # Trying to create Return Sales Invoice. + pos_rsi1 = make_sales_return(pos_si1.name) + pos_rsi1.save() + pos_rsi1.submit() + + self.assertEqual(pos_rsi1.paid_amount, -100) + + pcv_doc2 = make_closing_entry_from_opening(opening_entry2) + pcv_doc2.submit() + + self.assertTrue(pcv_doc2.name) + + pos_rsi1.reload() + self.assertEqual(pos_rsi1.pos_closing_entry, pcv_doc2.name) + + self.assertIn(pos_inv1.name, [d.pos_invoice for d in pcv_doc2.pos_invoices]) + self.assertNotIn(pos_inv2.name, [d.sales_invoice for d in pcv_doc2.sales_invoices]) + self.assertIn(pos_rsi1.name, [d.sales_invoice for d in pcv_doc2.sales_invoices]) + self.assertEqual(pcv_doc2.grand_total, 200) + + def test_pos_invoice_in_sales_invoice_mode(self): + """ + Test POS Invoice and Return POS Invoice creation during Sales Invoice mode. + """ + from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return + + test_user, pos_profile = init_user_and_profile() + + with self.change_settings("POS Settings", {"invoice_type": "POS Invoice"}): + opening_entry1 = create_opening_entry(pos_profile, test_user.name) + + pos_inv1, pos_inv2 = create_multiple_pos_invoices(pos_profile) + + # Trying to create Sales Invoice when invoice_type is set to POS Invoice. + pos_sinv = create_sales_invoice( + qty=1, is_created_using_pos=1, pos_profile=pos_profile.name, do_not_save=1 + ) + pos_sinv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) + self.assertRaises(frappe.ValidationError, pos_sinv.save) + + pcv_doc1 = make_closing_entry_from_opening(opening_entry1) + for d in pcv_doc1.payment_reconciliation: + if d.mode_of_payment == "Cash": + d.closing_amount = 300 + + pcv_doc1.submit() + + self.assertTrue(pcv_doc1.name) + + self.assertIn(pos_inv1.name, [d.pos_invoice for d in pcv_doc1.pos_invoices]) + self.assertEqual(pcv_doc1.grand_total, 300) + + with self.change_settings("POS Settings", {"invoice_type": "Sales Invoice"}): + opening_entry2 = create_opening_entry(pos_profile, test_user.name) + + pos_si1, pos_si2 = create_multiple_sales_invoices(pos_profile) + + pos_inv3 = create_pos_invoice(rate=100, do_not_save=1) + pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) + self.assertRaises(frappe.ValidationError, pos_inv3.save) + + # Creating Return POS Invoice + pos_rinv2 = make_sales_return(pos_inv2.name) + pos_rinv2.save() + pos_rinv2.submit() + + pos_rinv2.reload() + self.assertIsNotNone(pos_rinv2.consolidated_invoice) + + # Getting Sales Invoice created during POS Invoice submission. + pos_rinv2_si = frappe.get_doc("Sales Invoice", pos_rinv2.consolidated_invoice) + self.assertEqual(pos_rinv2_si.is_return, 1) + self.assertEqual(pos_rinv2_si.paid_amount, -200) + + pcv_doc2 = make_closing_entry_from_opening(opening_entry2) + for d in pcv_doc1.payment_reconciliation: + if d.mode_of_payment == "Cash": + d.closing_amount = 100 + + pcv_doc2.submit() + self.assertTrue(pcv_doc2.name) + + pos_si1.reload() + pos_si2.reload() + pos_rinv2_si.reload() + self.assertEqual(pos_si2.pos_closing_entry, pcv_doc2.name) + self.assertEqual(pos_rinv2_si.pos_closing_entry, pcv_doc2.name) def init_user_and_profile(**args): @@ -367,3 +502,31 @@ def get_test_item_qty(pos_profile): "actual_qty" ) return test_item_qty + + +def create_multiple_sales_invoices(pos_profile): + pos_si1 = create_sales_invoice(qty=1, is_created_using_pos=1, pos_profile=pos_profile.name, do_not_save=1) + pos_si1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) + pos_si1.save() + pos_si1.submit() + + pos_si2 = create_sales_invoice(qty=2, is_created_using_pos=1, pos_profile=pos_profile.name, do_not_save=1) + pos_si2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 200}) + pos_si2.save() + pos_si2.submit() + + return pos_si1, pos_si2 + + +def create_multiple_pos_invoices(pos_profile): + pos_inv1 = create_pos_invoice(pos_profile=pos_profile.name, rate=100, do_not_save=1) + pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) + pos_inv1.save() + pos_inv1.submit() + + pos_inv2 = create_pos_invoice(pos_profile=pos_profile.name, qty=2, do_not_save=1) + pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 200}) + pos_inv2.save() + pos_inv2.submit() + + return pos_inv1, pos_inv2 diff --git a/erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.json b/erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.json index e3c5fd513e2..b0c0a28a1f2 100644 --- a/erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.json +++ b/erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.json @@ -6,17 +6,9 @@ "engine": "InnoDB", "field_order": [ "account_head", - "rate", "amount" ], "fields": [ - { - "fieldname": "rate", - "fieldtype": "Percent", - "in_list_view": 1, - "label": "Tax Rate", - "read_only": 1 - }, { "fieldname": "amount", "fieldtype": "Currency", @@ -35,15 +27,16 @@ ], "istable": 1, "links": [], - "modified": "2024-03-27 13:10:14.420657", + "modified": "2025-06-06 11:54:02.414461", "modified_by": "Administrator", "module": "Accounts", "name": "POS Closing Entry Taxes", "owner": "Administrator", "permissions": [], "quick_entry": 1, + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.py b/erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.py index ce354a7552c..9eca5c033fa 100644 --- a/erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.py +++ b/erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.py @@ -19,7 +19,6 @@ class POSClosingEntryTaxes(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data - rate: DF.Percent # end: auto-generated types pass diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index dfb06bc6c9a..ce9e04c2869 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -243,7 +243,7 @@ class POSInvoice(SalesInvoice): update_coupon_code_count(self.coupon_code, "used") self.clear_unallocated_mode_of_payments() - if self.is_return and self.is_pos_using_sales_invoice: + if self.is_return and self.invoice_type_in_pos == "Sales Invoice": self.create_and_add_consolidated_sales_invoice() def before_cancel(self): @@ -424,10 +424,8 @@ class POSInvoice(SalesInvoice): ) def validate_is_pos_using_sales_invoice(self): - self.is_pos_using_sales_invoice = frappe.get_single_value( - "Accounts Settings", "use_sales_invoice_in_pos" - ) - if self.is_pos_using_sales_invoice and not self.is_return: + self.invoice_type_in_pos = frappe.db.get_single_value("POS Settings", "invoice_type") + if self.invoice_type_in_pos == "Sales Invoice" and not self.is_return: frappe.throw(_("Sales Invoice mode is activated in POS. Please create Sales Invoice instead.")) def validate_serialised_or_batched_item(self): diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 1a029980586..73cb6634b91 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -29,6 +29,7 @@ class TestPOSInvoice(IntegrationTestCase): def setUpClass(cls): super().setUpClass() cls.enterClassContext(cls.change_settings("Selling Settings", validate_selling_price=0)) + cls.enterClassContext(cls.change_settings("POS Settings", invoice_type="POS Invoice")) make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=800, basic_rate=100) frappe.db.sql("delete from `tabTax Rule`") @@ -36,10 +37,16 @@ class TestPOSInvoice(IntegrationTestCase): from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry cls.test_user, cls.pos_profile = init_user_and_profile() - create_opening_entry(cls.pos_profile, cls.test_user.name) + cls.opening_entry = create_opening_entry(cls.pos_profile, cls.test_user.name) mode_of_payment = frappe.get_doc("Mode of Payment", "Bank Draft") set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", "_Test Bank - _TC") + @classmethod + def tearDownClass(cls): + frappe.db.sql("delete from `tabPOS Invoice`") + opening_entry_doc = frappe.get_doc("POS Opening Entry", cls.opening_entry.name) + opening_entry_doc.cancel() + def tearDown(self): if frappe.session.user != "Administrator": frappe.set_user("Administrator") 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 5d4248377d1..90eb1279c5c 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 @@ -491,7 +491,7 @@ def split_invoices_by_accounting_dimension(pos_invoices): def consolidate_pos_invoices(pos_invoices=None, closing_entry=None): - invoices = pos_invoices or (closing_entry and closing_entry.get("pos_transactions")) + invoices = pos_invoices or (closing_entry and closing_entry.get("pos_invoices")) if frappe.flags.in_test and not invoices: invoices = get_all_unconsolidated_invoices() @@ -509,7 +509,7 @@ def unconsolidate_pos_invoices(closing_entry): "POS Invoice Merge Log", filters={"pos_closing_entry": closing_entry.name}, pluck="name" ) - if len(closing_entry.pos_transactions) >= 10: + if len(closing_entry.pos_invoices) >= 10: closing_entry.set_status(update=True, status="Queued") enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry) else: 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 9343ffb1f71..a7618377291 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 @@ -5,12 +5,16 @@ import json import frappe from frappe.tests import IntegrationTestCase +from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import ( + set_default_account_for_mode_of_payment, +) +from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import ( + make_closing_entry_from_opening, +) from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice -from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import ( - consolidate_pos_invoices, -) +from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import ( get_serial_nos_from_bundle, ) @@ -21,241 +25,310 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase): @classmethod def setUpClass(cls): super().setUpClass() + frappe.db.sql("delete from `tabPOS Opening Entry`") cls.enterClassContext(cls.change_settings("Selling Settings", validate_selling_price=0)) + cls.enterClassContext(cls.change_settings("POS Settings", invoice_type="POS Invoice")) + mode_of_payment = frappe.get_doc("Mode of Payment", "Bank Draft") + set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", "_Test Bank - _TC") + + def setUp(self): + frappe.db.sql("delete from `tabPOS Invoice`") + + def tearDown(self): + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") def test_consolidated_invoice_creation(self): - frappe.db.sql("delete from `tabPOS Invoice`") + test_user, pos_profile = init_user_and_profile() + opening_entry = create_opening_entry(pos_profile, test_user.name) - try: - test_user, pos_profile = init_user_and_profile() + 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_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_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_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() + closing_entry = make_closing_entry_from_opening(opening_entry) + closing_entry.insert() + closing_entry.submit() - consolidate_pos_invoices() + pos_inv.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) - pos_inv.load_from_db() - self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) + pos_inv3.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice)) - pos_inv3.load_from_db() - self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice)) - - self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice) - - finally: - frappe.set_user("Administrator") - frappe.db.sql("delete from `tabPOS Profile`") - frappe.db.sql("delete from `tabPOS Invoice`") + self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice) def test_consolidated_credit_note_creation(self): - frappe.db.sql("delete from `tabPOS Invoice`") + test_user, pos_profile = init_user_and_profile() + opening_entry = create_opening_entry(pos_profile, test_user.name) - try: - test_user, pos_profile = init_user_and_profile() + 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_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_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_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) + pos_inv_cn.set("payments", []) + pos_inv_cn.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -100}) + pos_inv_cn.append( + "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": -200} + ) + pos_inv_cn.paid_amount = -300 + pos_inv_cn.submit() - pos_inv_cn = make_sales_return(pos_inv.name) - pos_inv_cn.set("payments", []) - pos_inv_cn.append( - "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -100} - ) - pos_inv_cn.append( - "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": -200} - ) - pos_inv_cn.paid_amount = -300 - pos_inv_cn.submit() + closing_entry = make_closing_entry_from_opening(opening_entry) + closing_entry.insert() + closing_entry.submit() - consolidate_pos_invoices() + pos_inv.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) - pos_inv.load_from_db() - self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) + pos_inv3.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice)) - pos_inv3.load_from_db() - self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice)) - - pos_inv_cn.load_from_db() - self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice)) - consolidated_credit_note = frappe.get_doc("Sales Invoice", pos_inv_cn.consolidated_invoice) - self.assertEqual(consolidated_credit_note.is_return, 1) - self.assertEqual(consolidated_credit_note.payments[0].mode_of_payment, "Cash") - self.assertEqual(consolidated_credit_note.payments[0].amount, -100) - self.assertEqual(consolidated_credit_note.payments[1].mode_of_payment, "Bank Draft") - self.assertEqual(consolidated_credit_note.payments[1].amount, -200) - - finally: - frappe.set_user("Administrator") - frappe.db.sql("delete from `tabPOS Profile`") - frappe.db.sql("delete from `tabPOS Invoice`") + pos_inv_cn.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice)) + consolidated_credit_note = frappe.get_doc("Sales Invoice", pos_inv_cn.consolidated_invoice) + self.assertEqual(consolidated_credit_note.is_return, 1) + self.assertEqual(consolidated_credit_note.payments[0].mode_of_payment, "Cash") + self.assertEqual(consolidated_credit_note.payments[0].amount, -100) + self.assertEqual(consolidated_credit_note.payments[1].mode_of_payment, "Bank Draft") + self.assertEqual(consolidated_credit_note.payments[1].amount, -200) def test_consolidated_invoice_item_taxes(self): - frappe.db.sql("delete from `tabPOS Invoice`") + test_user, pos_profile = init_user_and_profile() + opening_entry = create_opening_entry(pos_profile, test_user.name) - try: - inv = create_pos_invoice(qty=1, rate=100, do_not_save=True) + inv = create_pos_invoice(qty=1, rate=100, 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": 9, - }, - ) - inv.insert() - inv.payments[0].amount = inv.grand_total - inv.save() - inv.submit() + 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": 9, + }, + ) + 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) - inv2.get("items")[0].item_code = "_Test Item 2" - 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": 5, - }, - ) - inv2.insert() - inv2.payments[0].amount = inv.grand_total - inv2.save() - inv2.submit() + inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True) + inv2.get("items")[0].item_code = "_Test Item 2" + 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": 5, + }, + ) + inv2.insert() + inv2.payments[0].amount = inv.grand_total + inv2.save() + inv2.submit() - consolidate_pos_invoices() - inv.load_from_db() + closing_entry = make_closing_entry_from_opening(opening_entry) + closing_entry.insert() + closing_entry.submit() - consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice) - item_wise_tax_detail = json.loads(consolidated_invoice.get("taxes")[0].item_wise_tax_detail) - expected_item_wise_tax_detail = { - "_Test Item": { - "tax_rate": 9, - "tax_amount": 9, - "net_amount": 100, - }, - "_Test Item 2": { - "tax_rate": 5, - "tax_amount": 5, - "net_amount": 100, - }, - } - self.assertEqual(item_wise_tax_detail, expected_item_wise_tax_detail) - finally: - frappe.set_user("Administrator") - frappe.db.sql("delete from `tabPOS Profile`") - frappe.db.sql("delete from `tabPOS Invoice`") + inv.load_from_db() + + consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice) + item_wise_tax_detail = json.loads(consolidated_invoice.get("taxes")[0].item_wise_tax_detail) + expected_item_wise_tax_detail = { + "_Test Item": { + "tax_rate": 9, + "tax_amount": 9, + "net_amount": 100, + }, + "_Test Item 2": { + "tax_rate": 5, + "tax_amount": 5, + "net_amount": 100, + }, + } + self.assertEqual(item_wise_tax_detail, expected_item_wise_tax_detail) 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`") + make_stock_entry( + to_warehouse="_Test Warehouse - _TC", + item_code="_Test Item", + rate=8000, + qty=10, + ) - try: - make_stock_entry( - to_warehouse="_Test Warehouse - _TC", - item_code="_Test Item", - rate=8000, - qty=10, - ) + test_user, pos_profile = init_user_and_profile() + opening_entry = create_opening_entry(pos_profile, test_user.name) - 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() - 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() - 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() + closing_entry = make_closing_entry_from_opening(opening_entry) + closing_entry.insert() + closing_entry.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`") + 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") 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`") + make_stock_entry( + to_warehouse="_Test Warehouse - _TC", + item_code="_Test Item", + rate=8000, + qty=10, + ) - try: - make_stock_entry( - to_warehouse="_Test Warehouse - _TC", - item_code="_Test Item", - rate=8000, - qty=10, - ) + test_user, pos_profile = init_user_and_profile() + opening_entry = create_opening_entry(pos_profile, test_user.name) - 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() - inv = create_pos_invoice(qty=6, rate=10000, do_not_save=True) + 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": 1800}) + inv3.insert() + inv3.submit() + + closing_entry = make_closing_entry_from_opening(opening_entry) + closing_entry.insert() + closing_entry.submit() + + inv.load_from_db() + consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice) + self.assertNotEqual(consolidated_invoice.outstanding_amount, 800) + self.assertEqual(consolidated_invoice.status, "Paid") + + @IntegrationTestCase.change_settings( + "System Settings", {"number_format": "#,###.###", "currency_precision": 3, "float_precision": 3} + ) + def test_consolidation_round_off_error_3(self): + make_stock_entry( + to_warehouse="_Test Warehouse - _TC", + item_code="_Test Item", + rate=8000, + qty=10, + ) + test_user, pos_profile = init_user_and_profile() + opening_entry = create_opening_entry(pos_profile, test_user.name) + + item_rates = [69, 59, 29] + for _i in [1, 2]: + inv = create_pos_invoice(is_return=1, do_not_save=1) + inv.items = [] + for rate in item_rates: + inv.append( + "items", + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "qty": -1, + "rate": rate, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ) inv.append( "taxes", { @@ -264,146 +337,56 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase): "cost_center": "_Test Cost Center - _TC", "description": "VAT", "doctype": "Sales Taxes and Charges", - "rate": 7.5, + "rate": 15, "included_in_print_rate": 1, }, ) - inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 60000}) - inv.insert() + inv.payments = [] + inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -157}) + inv.paid_amount = -157 + inv.save() 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() + closing_entry = make_closing_entry_from_opening(opening_entry) + closing_entry.insert() + closing_entry.submit() - inv3 = create_pos_invoice(qty=3, rate=600, do_not_save=True) - inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1800}) - inv3.insert() - inv3.submit() - - consolidate_pos_invoices() - - inv.load_from_db() - consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice) - self.assertNotEqual(consolidated_invoice.outstanding_amount, 800) - 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`") - - @IntegrationTestCase.change_settings( - "System Settings", {"number_format": "#,###.###", "currency_precision": 3, "float_precision": 3} - ) - def test_consolidation_round_off_error_3(self): - 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() - - item_rates = [69, 59, 29] - for _i in [1, 2]: - inv = create_pos_invoice(is_return=1, do_not_save=1) - inv.items = [] - for rate in item_rates: - inv.append( - "items", - { - "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC", - "qty": -1, - "rate": rate, - "income_account": "Sales - _TC", - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC", - }, - ) - 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": 15, - "included_in_print_rate": 1, - }, - ) - inv.payments = [] - inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -157}) - inv.paid_amount = -157 - inv.save() - inv.submit() - - consolidate_pos_invoices() - - inv.load_from_db() - consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice) - self.assertEqual(consolidated_invoice.status, "Return") - self.assertEqual(consolidated_invoice.rounding_adjustment, -0.002) - - finally: - frappe.set_user("Administrator") - frappe.db.sql("delete from `tabPOS Profile`") - frappe.db.sql("delete from `tabPOS Invoice`") + inv.load_from_db() + consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice) + self.assertEqual(consolidated_invoice.status, "Return") + self.assertEqual(consolidated_invoice.rounding_adjustment, -0.002) def test_consolidation_rounding_adjustment(self): """ Test if the rounding adjustment is calculated correctly """ - frappe.db.sql("delete from `tabPOS Invoice`") + make_stock_entry( + to_warehouse="_Test Warehouse - _TC", + item_code="_Test Item", + rate=8000, + qty=10, + ) - try: - make_stock_entry( - to_warehouse="_Test Warehouse - _TC", - item_code="_Test Item", - rate=8000, - qty=10, - ) + test_user, pos_profile = init_user_and_profile() + opening_entry = create_opening_entry(pos_profile, test_user.name) - init_user_and_profile() + inv = create_pos_invoice(qty=1, rate=69.5, do_not_save=True) + inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 70}) + inv.insert() + inv.submit() - inv = create_pos_invoice(qty=1, rate=69.5, do_not_save=True) - inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 70}) - inv.insert() - inv.submit() + inv2 = create_pos_invoice(qty=1, rate=59.5, do_not_save=True) + inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 60}) + inv2.insert() + inv2.submit() - inv2 = create_pos_invoice(qty=1, rate=59.5, do_not_save=True) - inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 60}) - inv2.insert() - inv2.submit() + closing_entry = make_closing_entry_from_opening(opening_entry) + closing_entry.insert() + closing_entry.submit() - consolidate_pos_invoices() - - inv.load_from_db() - consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice) - self.assertEqual(consolidated_invoice.rounding_adjustment, 1) - - finally: - frappe.set_user("Administrator") - frappe.db.sql("delete from `tabPOS Profile`") - frappe.db.sql("delete from `tabPOS Invoice`") + inv.load_from_db() + consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice) + self.assertEqual(consolidated_invoice.rounding_adjustment, 1) def test_serial_no_case_1(self): """ @@ -418,51 +401,46 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item - frappe.db.sql("delete from `tabPOS Invoice`") + se = make_serialized_item(self) + serial_no = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0] - try: - se = make_serialized_item(self) - serial_no = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0] + test_user, pos_profile = init_user_and_profile() + opening_entry = create_opening_entry(pos_profile, test_user.name) - init_user_and_profile() + pos_inv = create_pos_invoice( + item_code="_Test Serialized Item With Series", + serial_no=[serial_no], + qty=1, + rate=100, + 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 = create_pos_invoice( - item_code="_Test Serialized Item With Series", - serial_no=[serial_no], - qty=1, - rate=100, - 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) + pos_inv_cn.paid_amount = -100 + pos_inv_cn.submit() - pos_inv_cn = make_sales_return(pos_inv.name) - pos_inv_cn.paid_amount = -100 - pos_inv_cn.submit() + pos_inv2 = create_pos_invoice( + item_code="_Test Serialized Item With Series", + serial_no=[serial_no], + qty=1, + rate=100, + do_not_submit=1, + ) + pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) + pos_inv2.save() + pos_inv2.submit() - pos_inv2 = create_pos_invoice( - item_code="_Test Serialized Item With Series", - serial_no=[serial_no], - qty=1, - rate=100, - do_not_submit=1, - ) - pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) - pos_inv2.save() - pos_inv2.submit() + closing_entry = make_closing_entry_from_opening(opening_entry) + closing_entry.insert() + closing_entry.submit() - consolidate_pos_invoices() + pos_inv.load_from_db() + pos_inv2.load_from_db() - pos_inv.load_from_db() - pos_inv2.load_from_db() - - self.assertNotEqual(pos_inv.consolidated_invoice, pos_inv2.consolidated_invoice) - - finally: - frappe.set_user("Administrator") - frappe.db.sql("delete from `tabPOS Profile`") - frappe.db.sql("delete from `tabPOS Invoice`") + self.assertNotEqual(pos_inv.consolidated_invoice, pos_inv2.consolidated_invoice) def test_separate_consolidated_invoice_for_different_accounting_dimensions(self): """ @@ -473,48 +451,43 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase): """ from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center - frappe.db.sql("delete from `tabPOS Invoice`") - create_cost_center(cost_center_name="_Test POS Cost Center 1", is_group=0) create_cost_center(cost_center_name="_Test POS Cost Center 2", is_group=0) - try: - test_user, pos_profile = init_user_and_profile() + test_user, pos_profile = init_user_and_profile() + opening_entry = create_opening_entry(pos_profile, test_user.name) - 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.cost_center = "_Test POS Cost Center 1 - _TC" - pos_inv.save() - pos_inv.submit() + 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.cost_center = "_Test POS Cost Center 1 - _TC" + 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_inv.cost_center = "_Test POS Cost Center 2 - _TC" - pos_inv2.save() - pos_inv2.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_inv.cost_center = "_Test POS Cost Center 2 - _TC" + pos_inv2.save() + pos_inv2.submit() - pos_inv3 = create_pos_invoice(rate=2300, do_not_submit=1) - pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300}) - pos_inv.cost_center = "_Test POS Cost Center 2 - _TC" - pos_inv3.save() - pos_inv3.submit() + pos_inv3 = create_pos_invoice(rate=2300, do_not_submit=1) + pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300}) + pos_inv.cost_center = "_Test POS Cost Center 2 - _TC" + pos_inv3.save() + pos_inv3.submit() - consolidate_pos_invoices() + closing_entry = make_closing_entry_from_opening(opening_entry) + closing_entry.insert() + closing_entry.submit() - pos_inv.load_from_db() - self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) + pos_inv.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) - pos_inv2.load_from_db() - self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv2.consolidated_invoice)) + pos_inv2.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv2.consolidated_invoice)) - self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice) + self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice) - pos_inv3.load_from_db() - self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice)) + pos_inv3.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice)) - self.assertTrue(pos_inv2.consolidated_invoice == pos_inv3.consolidated_invoice) - - finally: - frappe.set_user("Administrator") - frappe.db.sql("delete from `tabPOS Profile`") - frappe.db.sql("delete from `tabPOS Invoice`") + self.assertTrue(pos_inv2.consolidated_invoice == pos_inv3.consolidated_invoice) diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.json b/erpnext/accounts/doctype/pos_settings/pos_settings.json index 03bf85b049a..a8413c599f3 100644 --- a/erpnext/accounts/doctype/pos_settings/pos_settings.json +++ b/erpnext/accounts/doctype/pos_settings/pos_settings.json @@ -5,6 +5,8 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "invoice_type", + "section_break_gyos", "invoice_fields", "pos_search_fields" ], @@ -12,7 +14,7 @@ { "fieldname": "invoice_fields", "fieldtype": "Table", - "label": "POS Field", + "label": "POS Additional Fields", "options": "POS Field" }, { @@ -20,11 +22,23 @@ "fieldtype": "Table", "label": "POS Search Fields", "options": "POS Search Fields" + }, + { + "default": "Sales Invoice", + "description": "The system will create a Sales Invoice or a POS Invoice from the POS interface based on this setting. For high-volume transactions, it is recommended to use POS Invoice.", + "fieldname": "invoice_type", + "fieldtype": "Select", + "label": "Invoice Type Created via POS Screen", + "options": "Sales Invoice\nPOS Invoice" + }, + { + "fieldname": "section_break_gyos", + "fieldtype": "Section Break" } ], "issingle": 1, "links": [], - "modified": "2024-03-27 13:10:17.083132", + "modified": "2025-06-06 11:36:44.885353", "modified_by": "Administrator", "module": "Accounts", "name": "POS Settings", @@ -56,8 +70,9 @@ } ], "quick_entry": 1, + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.py b/erpnext/accounts/doctype/pos_settings/pos_settings.py index 61fdf7bdd3e..4865262b83a 100644 --- a/erpnext/accounts/doctype/pos_settings/pos_settings.py +++ b/erpnext/accounts/doctype/pos_settings/pos_settings.py @@ -21,10 +21,16 @@ class POSSettings(Document): from erpnext.accounts.doctype.pos_search_fields.pos_search_fields import POSSearchFields invoice_fields: DF.Table[POSField] + invoice_type: DF.Literal["Sales Invoice", "POS Invoice"] pos_search_fields: DF.Table[POSSearchFields] # end: auto-generated types def validate(self): + old_doc = self.get_doc_before_save() + + if old_doc.invoice_type != self.invoice_type: + self.validate_invoice_type() + self.validate_invoice_fields() def validate_invoice_fields(self): @@ -36,3 +42,15 @@ class POSSettings(Document): frappe.throw( title=_("Duplicate POS Fields"), msg=_("'{0}' has been already added.").format(field) ) + + def validate_invoice_type(self): + pos_opening_entries_count = frappe.db.count( + "POS Opening Entry", filters={"docstatus": 1, "status": "Open"} + ) + if pos_opening_entries_count: + frappe.throw( + _("{0} cannot be changed with opened Opening Entries.").format( + frappe.bold(_("Invoice Type")) + ), + title=_("Invoice Document Type Selection Error"), + ) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index c4c8f8c7d4d..b3b259cf825 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1096,10 +1096,8 @@ class SalesInvoice(SellingController): if self.is_created_using_pos and not self.pos_profile: frappe.throw(_("POS Profile is mandatory to mark this invoice as POS Transaction.")) - self.is_pos_using_sales_invoice = frappe.get_single_value( - "Accounts Settings", "use_sales_invoice_in_pos" - ) - if not self.is_pos_using_sales_invoice and not self.is_return: + self.invoice_type_in_pos = frappe.db.get_single_value("POS Settings", "invoice_type") + if self.invoice_type_in_pos == "POS Invoice" and not self.is_return: frappe.throw(_("Transactions using Sales Invoice in POS are disabled.")) def validate_full_payment(self): diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 2f3c8b0f945..8124f1b95e5 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -4425,7 +4425,7 @@ class TestSalesInvoice(ERPNextTestSuite): # Deleting all opening entry frappe.db.sql("delete from `tabPOS Opening Entry`") - with self.change_settings("Accounts Settings", {"use_sales_invoice_in_pos": 0}): + with self.change_settings("POS Settings", {"invoice_type": "POS Invoice"}): pos_profile = make_pos_profile() pos_profile.payments = [] @@ -4495,6 +4495,14 @@ def create_sales_invoice(**args): si.naming_series = args.naming_series or "T-SINV-" si.cost_center = args.parent_cost_center si.is_internal_customer = args.is_internal_customer or 0 + if args.is_created_using_pos: + si.is_pos = 1 + si.is_created_using_pos = 1 + pos_profile = None + if not args.pos_profile: + pos_profile = make_pos_profile() + pos_profile.save() + si.pos_profile = args.pos_profile or pos_profile.name bundle_id = None if si.update_stock and (args.get("batch_no") or args.get("serial_no")): diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 2a86e8ee537..cf16cc37ee1 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -420,3 +420,4 @@ erpnext.patches.v15_0.remove_agriculture_roles erpnext.patches.v14_0.update_full_name_in_contract erpnext.patches.v15_0.drop_sle_indexes execute:frappe.db.set_single_value("Accounts Settings", "confirm_before_resetting_posting_date", 1) +erpnext.patches.v15_0.rename_pos_closing_entry_fields diff --git a/erpnext/patches/v15_0/rename_pos_closing_entry_fields.py b/erpnext/patches/v15_0/rename_pos_closing_entry_fields.py new file mode 100644 index 00000000000..782f219c564 --- /dev/null +++ b/erpnext/patches/v15_0/rename_pos_closing_entry_fields.py @@ -0,0 +1,6 @@ +from frappe.model.utils.rename_field import rename_field + + +def execute(): + rename_field("POS Closing Entry", "pos_transactions", "pos_invoices") + rename_field("POS Closing Entry", "sales_invoice_transactions", "sales_invoices") diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 5c3e4d79f01..f05a2471eca 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -139,10 +139,7 @@ erpnext.PointOfSale.Controller = class { this.allow_negative_stock = flt(message.allow_negative_stock) || false; }); - const use_sales_invoice_in_pos = await frappe.db.get_single_value( - "Accounts Settings", - "use_sales_invoice_in_pos" - ); + const invoice_doctype = await frappe.db.get_single_value("POS Settings", "invoice_type"); frappe.call({ method: "erpnext.selling.page.point_of_sale.point_of_sale.get_pos_profile_data", @@ -151,7 +148,7 @@ erpnext.PointOfSale.Controller = class { const profile = res.message; Object.assign(this.settings, profile); this.settings.customer_groups = profile.customer_groups.map((group) => group.name); - this.settings.frm_doctype = use_sales_invoice_in_pos ? "Sales Invoice" : "POS Invoice"; + this.settings.frm_doctype = invoice_doctype; this.make_app(); }, });