From 8ba4ac3b8626dccbce3c9d1e4ade0ee381f6f80d Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Wed, 12 Mar 2025 10:44:21 +0530 Subject: [PATCH] fix: credit note creation during pos invoice consolidation (#46277) * fix: credit note creation during pos invoice consolidation * fix: added check to skip merging empty list of return pos invoices * fix: sql query * fix: using return invoice name instead of return invoice object * fix: added pos invoice field in sales invoice item --- .../pos_invoice_merge_log.py | 146 +++++++++++------- .../sales_invoice_item.json | 29 +++- .../sales_invoice_item/sales_invoice_item.py | 2 + 3 files changed, 117 insertions(+), 60 deletions(-) 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 6fffd42b654..468a61d576b 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 @@ -8,6 +8,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.model.mapper import map_child_doc, map_doc +from frappe.query_builder import DocType from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime from frappe.utils.background_jobs import enqueue, is_job_enqueued from frappe.utils.scheduler import is_scheduler_inactive @@ -119,17 +120,18 @@ class POSInvoiceMergeLog(Document): returns = [d for d in pos_invoice_docs if d.get("is_return") == 1] sales = [d for d in pos_invoice_docs if d.get("is_return") == 0] - sales_invoice, credit_note = "", "" + sales_invoice, credit_notes = "", {} sales_invoice_doc = None if sales: sales_invoice_doc = self.process_merging_into_sales_invoice(sales) sales_invoice = sales_invoice_doc.name if returns: - credit_note = self.process_merging_into_credit_note(returns, sales_invoice_doc) + distinguished_returns = self.distinguish_return_pos_invoices(returns, sales_invoice_doc) + credit_notes = self.process_merging_into_credit_notes(distinguished_returns) self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log - self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note) + self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_notes) def on_cancel(self): pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] @@ -159,34 +161,50 @@ class POSInvoiceMergeLog(Document): return sales_invoice - def process_merging_into_credit_note(self, data, sales_invoice_doc=None): - credit_note = self.get_new_sales_invoice() - credit_note.is_return = 1 + def process_merging_into_credit_notes(self, data): + credit_notes = {} + for key, value in data.items(): + if not value: + continue - credit_note = self.merge_pos_invoice_into(credit_note, data) - referenes = {} + credit_note = self.get_new_sales_invoice() + credit_note.is_return = 1 - if sales_invoice_doc: - credit_note.return_against = sales_invoice_doc.name + credit_note = self.merge_pos_invoice_into(credit_note, value) + credit_note.return_against = key - for d in sales_invoice_doc.items: - referenes[d.item_code] = d.name + credit_note.is_consolidated = 1 + credit_note.set_posting_time = 1 + credit_note.posting_date = getdate(self.posting_date) + credit_note.posting_time = get_time(self.posting_time) + # TODO: return could be against multiple sales invoice which could also have been consolidated? + # credit_note.return_against = self.consolidated_invoice + credit_note.save() + credit_note.submit() - for d in credit_note.items: - d.sales_invoice_item = referenes.get(d.item_code) + self.consolidated_credit_note = credit_note.name + credit_notes[credit_note.name] = [d.name for d in value] - credit_note.is_consolidated = 1 - credit_note.set_posting_time = 1 - credit_note.posting_date = getdate(self.posting_date) - credit_note.posting_time = get_time(self.posting_time) - # TODO: return could be against multiple sales invoice which could also have been consolidated? - # credit_note.return_against = self.consolidated_invoice - credit_note.save() - credit_note.submit() + return credit_notes - self.consolidated_credit_note = credit_note.name + def distinguish_return_pos_invoices(self, data, sales_invoice_doc=None): + return_invoices = {} - return credit_note.name + return_invoices[sales_invoice_doc.name if sales_invoice_doc else None] = [] + + for doc in data: + sales_invoices_of_return_against = frappe.db.get_value( + "POS Invoice", doc.return_against, "consolidated_invoice" + ) + if sales_invoices_of_return_against: + if sales_invoices_of_return_against in return_invoices: + return_invoices[sales_invoices_of_return_against].append(doc) + else: + return_invoices[sales_invoices_of_return_against] = [doc] + else: + return_invoices[sales_invoice_doc.name if sales_invoice_doc else None].append(doc) + + return return_invoices def merge_pos_invoice_into(self, invoice, data): items, payments, taxes = [], [], [] @@ -212,33 +230,20 @@ class POSInvoiceMergeLog(Document): loyalty_amount_sum += doc.loyalty_amount for item in doc.get("items"): - found = False - for i in items: - if ( - i.item_code == item.item_code - and not i.serial_and_batch_bundle - and not i.serial_no - and not i.batch_no - and 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"}) - if item.serial_and_batch_bundle: - si_item.serial_and_batch_bundle = item.serial_and_batch_bundle - items.append(si_item) + 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"}) + si_item.pos_invoice = doc.name + si_item.pos_invoice_item = item.name + if doc.is_return: + si_item.sales_invoice_item = get_sales_invoice_item( + doc.return_against, item.pos_invoice_item + ) + if item.serial_and_batch_bundle: + si_item.serial_and_batch_bundle = item.serial_and_batch_bundle + items.append(si_item) for tax in doc.get("taxes"): found = False @@ -328,16 +333,16 @@ class POSInvoiceMergeLog(Document): return sales_invoice - def update_pos_invoices(self, invoice_docs, sales_invoice="", credit_note=""): + def update_pos_invoices(self, invoice_docs, sales_invoice="", credit_notes=None): for doc in invoice_docs: doc.load_from_db() - doc.update( - { - "consolidated_invoice": None - if self.docstatus == 2 - else (credit_note if doc.is_return else sales_invoice) - } - ) + inv = sales_invoice + if doc.is_return: + for key, value in credit_notes.items(): + if doc.name in value: + inv = key + break + doc.update({"consolidated_invoice": None if self.docstatus == 2 else inv}) doc.set_status(update=True) doc.save() @@ -628,3 +633,26 @@ def get_error_message(message) -> str: return message["message"] except Exception: return str(message) + + +def get_sales_invoice_item(return_against_pos_invoice, pos_invoice_item): + try: + SalesInvoice = DocType("Sales Invoice") + SalesInvoiceItem = DocType("Sales Invoice Item") + + query = ( + frappe.qb.from_(SalesInvoice) + .from_(SalesInvoiceItem) + .select(SalesInvoiceItem.name) + .where( + (SalesInvoice.name == SalesInvoiceItem.parent) + & (SalesInvoice.is_return == 0) + & (SalesInvoiceItem.pos_invoice == return_against_pos_invoice) + & (SalesInvoiceItem.pos_invoice_item == pos_invoice_item) + ) + ) + + result = query.run(as_dict=True) + return result[0].name if result else None + except Exception: + return None diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 27da8dc9ba2..33e1ee73963 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -106,6 +106,9 @@ "delivery_note", "dn_detail", "delivered_qty", + "column_break_vwhb", + "pos_invoice", + "pos_invoice_item", "internal_transfer_section", "purchase_order", "column_break_92", @@ -952,18 +955,42 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "pos_invoice_item", + "fieldtype": "Data", + "ignore_user_permissions": 1, + "label": "POS Invoice Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_vwhb", + "fieldtype": "Column Break" + }, + { + "fieldname": "pos_invoice", + "fieldtype": "Link", + "label": "POS Invoice", + "no_copy": 1, + "options": "POS Invoice", + "print_hide": 1, + "search_index": 1 } ], + "grid_page_length": 50, "idx": 1, "istable": 1, "links": [], - "modified": "2024-11-25 16:27:33.287341", + "modified": "2025-03-05 12:22:11.801692", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", "naming_rule": "Random", "owner": "Administrator", "permissions": [], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [] diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py index 8e8a8e49fa1..e708bdadfaa 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py @@ -71,6 +71,8 @@ class SalesInvoiceItem(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data + pos_invoice: DF.Link | None + pos_invoice_item: DF.Data | None price_list_rate: DF.Currency pricing_rules: DF.SmallText | None project: DF.Link | None