From bd2061d6f398b28877ea1005a19a6d444b20b7bb Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Mar 2022 11:36:30 +0530 Subject: [PATCH 01/14] fix: prevent multiple save on applying coupon code (cherry picked from commit d5fd8e0ba6f2b6297339dd12bb7cbf1dc0c3155e) --- .../selling/page/point_of_sale/pos_payment.js | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 1e9f6d7d920..326ee59d11a 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -170,20 +170,24 @@ erpnext.PointOfSale.Payment = class { }); frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => { - if (!frm.doc.ignore_pricing_rule && frm.doc.coupon_code) { - frappe.run_serially([ - () => frm.doc.ignore_pricing_rule=1, - () => frm.trigger('ignore_pricing_rule'), - () => frm.doc.ignore_pricing_rule=0, - () => frm.trigger('apply_pricing_rule'), - () => frm.save(), - () => this.update_totals_section(frm.doc) - ]); - } else if (frm.doc.ignore_pricing_rule && frm.doc.coupon_code) { - frappe.show_alert({ - message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."), - indicator: "orange" - }); + if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) { + if (!frm.doc.ignore_pricing_rule) { + frm.applying_pos_coupon_code = true + frappe.run_serially([ + () => frm.doc.ignore_pricing_rule=1, + () => frm.trigger('ignore_pricing_rule'), + () => frm.doc.ignore_pricing_rule=0, + () => frm.trigger('apply_pricing_rule'), + () => frm.save(), + () => this.update_totals_section(frm.doc), + () => (frm.applying_pos_coupon_code = false) + ]); + } else if (frm.doc.ignore_pricing_rule) { + frappe.show_alert({ + message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."), + indicator: "orange" + }); + } } }); From a3a7dc9ce6dfe1b0c5215e710be514097403d7e0 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Mar 2022 12:47:35 +0530 Subject: [PATCH 02/14] fix(pos): customer group filter in customer selector (cherry picked from commit 2f82e237ef650ab49d08dfd3eeaf56f1f299c84a) --- .../selling/page/point_of_sale/point_of_sale.py | 15 ++++++++++++++- .../selling/page/point_of_sale/pos_controller.js | 13 +++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 4efb1a03f7e..26a80763c9b 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -8,7 +8,7 @@ import frappe from frappe.utils.nestedset import get_root_of from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability -from erpnext.accounts.doctype.pos_profile.pos_profile import get_item_groups +from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes, get_item_groups def search_by_term(search_term, warehouse, price_list): @@ -324,3 +324,16 @@ def set_customer_info(fieldname, customer, value=""): contact_doc.set("phone_nos", [{"phone": value, "is_primary_mobile_no": 1}]) frappe.db.set_value("Customer", customer, "mobile_no", value) contact_doc.save() + +@frappe.whitelist() +def get_pos_profile_data(pos_profile): + pos_profile = frappe.get_doc('POS Profile', pos_profile) + pos_profile = pos_profile.as_dict() + + _customer_groups_with_children = [] + for row in pos_profile.customer_groups: + children = get_child_nodes('Customer Group', row.customer_group) + _customer_groups_with_children.extend(children) + + pos_profile.customer_groups = _customer_groups_with_children + return pos_profile \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index ea8459f970b..d66c6e46860 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -119,10 +119,15 @@ erpnext.PointOfSale.Controller = class { this.allow_negative_stock = flt(message.allow_negative_stock) || false; }); - frappe.db.get_doc("POS Profile", this.pos_profile).then((profile) => { - Object.assign(this.settings, profile); - this.settings.customer_groups = profile.customer_groups.map(group => group.customer_group); - this.make_app(); + frappe.call({ + method: "erpnext.selling.page.point_of_sale.point_of_sale.get_pos_profile_data", + args: { "pos_profile": this.pos_profile }, + callback: (res) => { + const profile = res.message; + Object.assign(this.settings, profile); + this.settings.customer_groups = profile.customer_groups.map(group => group.name); + this.make_app(); + } }); } From 3b583c6c48ba559cef329a21340ac1ba20eac42c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Mar 2022 13:24:11 +0530 Subject: [PATCH 03/14] fix(pos): specific case when serialized item not removed (cherry picked from commit 4afb47e869b70c66a0fa51934a3c65de54f98b4e) --- .../selling/page/point_of_sale/pos_controller.js | 4 ++-- .../selling/page/point_of_sale/pos_item_details.js | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index d66c6e46860..49e85ecc7a0 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -560,7 +560,7 @@ erpnext.PointOfSale.Controller = class { if (this.item_details.$component.is(':visible')) this.edit_item_details_of(item_row); - if (this.check_serial_batch_selection_needed(item_row)) + if (this.check_serial_batch_selection_needed(item_row) && !this.item_details.$component.is(':visible')) this.edit_item_details_of(item_row); } @@ -709,7 +709,7 @@ erpnext.PointOfSale.Controller = class { frappe.dom.freeze(); const { doctype, name, current_item } = this.item_details; - frappe.model.set_value(doctype, name, 'qty', 0) + return frappe.model.set_value(doctype, name, 'qty', 0) .then(() => { frappe.model.clear_doc(doctype, name); this.update_cart_html(current_item, true); diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index a3ad0025943..1d720f7291a 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -60,12 +60,18 @@ erpnext.PointOfSale.ItemDetails = class { return item && item.name == this.current_item.name; } - toggle_item_details_section(item) { + async toggle_item_details_section(item) { const current_item_changed = !this.compare_with_current_item(item); // if item is null or highlighted cart item is clicked twice const hide_item_details = !Boolean(item) || !current_item_changed; + if ((!hide_item_details && current_item_changed) || hide_item_details) { + // if item details is being closed OR if item details is opened but item is changed + // in both cases, if the current item is a serialized item, then validate and remove the item + await this.validate_serial_batch_item(); + } + this.events.toggle_item_selector(!hide_item_details); this.toggle_component(!hide_item_details); @@ -83,7 +89,6 @@ erpnext.PointOfSale.ItemDetails = class { this.render_form(item); this.events.highlight_cart_item(item); } else { - this.validate_serial_batch_item(); this.current_item = {}; } } @@ -103,11 +108,11 @@ erpnext.PointOfSale.ItemDetails = class { (serialized && batched && (no_batch_selected || no_serial_selected))) { frappe.show_alert({ - message: __("Item will be removed since no serial / batch no selected."), + message: __("Item is removed since no serial / batch no selected."), indicator: 'orange' }); frappe.utils.play_sound("cancel"); - this.events.remove_item_from_cart(); + return this.events.remove_item_from_cart(); } } From 76f83ea559a417c0c830517eed80dde958df983e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Mar 2022 13:53:58 +0530 Subject: [PATCH 04/14] fix(pos): allow validating stock on save (cherry picked from commit aff74087755cfb6c2e9a582886c26267b6fc5667) # Conflicts: # erpnext/accounts/doctype/pos_invoice/pos_invoice.py --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 13 +++++++++++++ .../accounts/doctype/pos_profile/pos_profile.json | 13 +++++++++++-- .../selling/page/point_of_sale/pos_controller.js | 11 +++++++++-- erpnext/selling/page/point_of_sale/pos_payment.js | 2 +- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 9115ee88541..a9be620b993 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -212,11 +212,24 @@ class POSInvoice(SalesInvoice): frappe.throw(error_msg, title=_("Invalid Item"), as_list=True) def validate_stock_availablility(self): +<<<<<<< HEAD if self.is_return or self.docstatus != 1: return allow_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock") for d in self.get("items"): is_service_item = not (frappe.db.get_value("Item", d.get("item_code"), "is_stock_item")) +======= + if self.is_return: + return + + if self.docstatus.is_draft() and not frappe.db.get_value('POS Profile', self.pos_profile, 'validate_stock_on_save'): + return + + from erpnext.stock.stock_ledger import is_negative_stock_allowed + + for d in self.get('items'): + is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item')) +>>>>>>> aff7408775 (fix(pos): allow validating stock on save) if is_service_item: return if d.serial_no: diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index 9c9f37bba27..11646a6517d 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -22,6 +22,7 @@ "hide_images", "hide_unavailable_items", "auto_add_item_to_cart", + "validate_stock_on_save", "column_break_16", "update_stock", "ignore_pricing_rule", @@ -351,6 +352,12 @@ { "fieldname": "column_break_25", "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "validate_stock_on_save", + "fieldtype": "Check", + "label": "Validate Stock on Save" } ], "icon": "icon-cog", @@ -378,10 +385,11 @@ "link_fieldname": "pos_profile" } ], - "modified": "2021-10-14 14:17:00.469298", + "modified": "2022-03-21 13:29:28.480533", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", + "naming_rule": "Set by user", "owner": "Administrator", "permissions": [ { @@ -404,5 +412,6 @@ } ], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 49e85ecc7a0..6974bed4f1f 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -720,7 +720,14 @@ erpnext.PointOfSale.Controller = class { } async save_and_checkout() { - this.frm.is_dirty() && await this.frm.save(); - this.payment.checkout(); + if (this.frm.is_dirty()) { + // only move to payment section if save is successful + frappe.route_hooks.after_save = () => this.payment.checkout(); + return this.frm.save( + null, null, null, () => this.cart.toggle_checkout_btn(true) // show checkout button on error + ); + } else { + this.payment.checkout(); + } } }; diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 326ee59d11a..b4ece46e6e1 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -172,7 +172,7 @@ erpnext.PointOfSale.Payment = class { frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => { if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) { if (!frm.doc.ignore_pricing_rule) { - frm.applying_pos_coupon_code = true + frm.applying_pos_coupon_code = true; frappe.run_serially([ () => frm.doc.ignore_pricing_rule=1, () => frm.trigger('ignore_pricing_rule'), From 36845a87e0fb86cf1d385a9e17c3d925c62898d2 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 22 Mar 2022 17:41:49 +0530 Subject: [PATCH 05/14] fix(pos): remove returned sr. nos. from pos reserved sr. nos. list (cherry picked from commit f2ae63cbfdc0262f45ccae5991927e49e5c38c4c) # Conflicts: # erpnext/accounts/doctype/pos_invoice/pos_invoice.py # erpnext/stock/doctype/serial_no/serial_no.py --- .../doctype/pos_invoice/pos_invoice.json | 4 +- .../doctype/pos_invoice/pos_invoice.py | 10 ++- .../doctype/pos_invoice/test_pos_invoice.py | 72 +++++++++++++++++++ erpnext/stock/doctype/serial_no/serial_no.py | 55 +++++++++++++- 4 files changed, 137 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index 0c6e7edeb02..b8500270d1a 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -264,7 +264,6 @@ "print_hide": 1 }, { - "allow_on_submit": 1, "default": "0", "fieldname": "is_return", "fieldtype": "Check", @@ -1573,7 +1572,7 @@ "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2021-10-05 12:11:53.871828", + "modified": "2022-03-22 13:00:24.166684", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", @@ -1623,6 +1622,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "customer", "title_field": "title", "track_changes": 1, diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index a9be620b993..8df2e3587df 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -17,7 +17,11 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( ) from erpnext.accounts.party import get_due_date, get_party_account from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty -from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos +from erpnext.stock.doctype.serial_no.serial_no import ( + get_delivered_serial_nos, + get_pos_reserved_serial_nos, + get_serial_nos, +) class POSInvoice(SalesInvoice): @@ -179,12 +183,16 @@ class POSInvoice(SalesInvoice): ) def validate_delivered_serial_nos(self, item): +<<<<<<< HEAD serial_nos = get_serial_nos(item.serial_no) delivered_serial_nos = frappe.db.get_list( "Serial No", {"item_code": item.item_code, "name": ["in", serial_nos], "sales_invoice": ["is", "set"]}, pluck="name", ) +======= + delivered_serial_nos = get_delivered_serial_nos(item.serial_no) +>>>>>>> f2ae63cbfd (fix(pos): remove returned sr. nos. from pos reserved sr. nos. list) if delivered_serial_nos: bold_delivered_serial_nos = frappe.bold(", ".join(delivered_serial_nos)) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index ee35614cdce..0925614cbc7 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -765,6 +765,78 @@ class TestPOSInvoice(unittest.TestCase): pos_inv.delete() pr.delete() + def test_delivered_serial_no_case(self): + from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import ( + init_user_and_profile, + ) + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + + frappe.db.savepoint('before_test_delivered_serial_no_case') + try: + se = make_serialized_item() + serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] + + dn = create_delivery_note( + item_code="_Test Serialized Item With Series", serial_no=serial_no + ) + + delivery_document_no = frappe.db.get_value("Serial No", serial_no, "delivery_document_no") + self.assertEquals(delivery_document_no, dn.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=True + ) + + self.assertRaises(frappe.ValidationError, pos_inv.submit) + + finally: + frappe.db.rollback(save_point='before_test_delivered_serial_no_case') + frappe.set_user("Administrator") + + def test_returned_serial_no_case(self): + from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import ( + init_user_and_profile, + ) + from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos + from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + + frappe.db.savepoint('before_test_returned_serial_no_case') + try: + se = make_serialized_item() + serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] + + init_user_and_profile() + + pos_inv = create_pos_invoice( + item_code="_Test Serialized Item With Series", + serial_no=serial_no, + qty=1, + rate=100, + ) + + pos_return = make_sales_return(pos_inv.name) + pos_return.flags.ignore_validate = True + pos_return.insert() + pos_return.submit() + + pos_reserved_serial_nos = get_pos_reserved_serial_nos({ + 'item_code': '_Test Serialized Item With Series', + 'warehouse': '_Test Warehouse - _TC' + }) + self.assertTrue(serial_no not in pos_reserved_serial_nos) + + finally: + frappe.db.rollback(save_point='before_test_returned_serial_no_case') + frappe.set_user("Administrator") def create_pos_invoice(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index bc30878789f..e9a5d53fdd7 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -820,11 +820,35 @@ def auto_fetch_serial_number( return sorted([d.get("name") for d in serial_numbers]) +def get_delivered_serial_nos(serial_nos): + ''' + Returns serial numbers that delivered from the list of serial numbers + ''' + from frappe.query_builder.functions import Coalesce + + SerialNo = frappe.qb.DocType("Serial No") + serial_nos = get_serial_nos(serial_nos) + query = ( + frappe.qb + .from_(SerialNo) + .select(SerialNo.name) + .where( + (SerialNo.name.isin(serial_nos)) + & (Coalesce(SerialNo.delivery_document_type, "") != "") + ) + ) + + result = query.run() + if result and len(result) > 0: + delivered_serial_nos = [row[0] for row in result] + return delivered_serial_nos + @frappe.whitelist() def get_pos_reserved_serial_nos(filters): if isinstance(filters, str): filters = json.loads(filters) +<<<<<<< HEAD pos_transacted_sr_nos = frappe.db.sql( """select item.serial_no as serial_no from `tabPOS Invoice` p, `tabPOS Invoice Item` item @@ -839,10 +863,39 @@ def get_pos_reserved_serial_nos(filters): filters, as_dict=1, ) +======= + POSInvoice = frappe.qb.DocType("POS Invoice") + POSInvoiceItem = frappe.qb.DocType("POS Invoice Item") + query = frappe.qb.from_( + POSInvoice + ).from_( + POSInvoiceItem + ).select( + POSInvoice.is_return, + POSInvoiceItem.serial_no + ).where( + (POSInvoice.name == POSInvoiceItem.parent) + & (POSInvoice.docstatus == 1) + & (POSInvoiceItem.docstatus == 1) + & (POSInvoiceItem.item_code == filters.get('item_code')) + & (POSInvoiceItem.warehouse == filters.get('warehouse')) + & (POSInvoiceItem.serial_no.isnotnull()) + & (POSInvoiceItem.serial_no != '') + ) + + pos_transacted_sr_nos = query.run(as_dict=True) +>>>>>>> f2ae63cbfd (fix(pos): remove returned sr. nos. from pos reserved sr. nos. list) reserved_sr_nos = [] + returned_sr_nos = [] for d in pos_transacted_sr_nos: - reserved_sr_nos += get_serial_nos(d.serial_no) + if d.is_return == 0: + reserved_sr_nos += get_serial_nos(d.serial_no) + elif d.is_return == 1: + returned_sr_nos += get_serial_nos(d.serial_no) + + for sr_no in returned_sr_nos: + reserved_sr_nos.remove(sr_no) return reserved_sr_nos From 3bb0716dffa3d72732d3e0ce70a870635e440502 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 24 Mar 2022 16:20:37 +0530 Subject: [PATCH 06/14] fix(pos): cannot close the pos if sr. no. is sold & returned (cherry picked from commit cf51a0a1b8ec45bf653c9478bd57cee676b384d9) # Conflicts: # erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py # erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py --- .../pos_invoice_merge_log.py | 65 ++++++++++++++++++- .../test_pos_invoice_merge_log.py | 65 +++++++++++++++++++ .../pos_invoice_reference.json | 23 ++++++- 3 files changed, 149 insertions(+), 4 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 89af6ad4e57..d97ce1cffc0 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 @@ -66,7 +66,7 @@ class POSInvoiceMergeLog(Document): frappe.throw(msg) def on_submit(self): - pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] + pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] 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] @@ -83,7 +83,7 @@ class POSInvoiceMergeLog(Document): self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note) def on_cancel(self): - pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] + pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] self.update_pos_invoices(pos_invoice_docs) self.cancel_linked_invoices() @@ -279,11 +279,16 @@ def get_all_unconsolidated_invoices(): "status": ["not in", ["Consolidated"]], "docstatus": 1, } +<<<<<<< HEAD pos_invoices = frappe.db.get_all( "POS Invoice", filters=filters, fields=["name as pos_invoice", "posting_date", "grand_total", "customer"], ) +======= + pos_invoices = frappe.db.get_all('POS Invoice', filters=filters, + fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer', 'is_return', 'return_against']) +>>>>>>> cf51a0a1b8 (fix(pos): cannot close the pos if sr. no. is sold & returned) return pos_invoices @@ -326,6 +331,7 @@ def unconsolidate_pos_invoices(closing_entry): else: cancel_merge_logs(merge_logs, closing_entry) +<<<<<<< HEAD def create_merge_logs(invoice_by_customer, closing_entry=None): try: @@ -340,6 +346,61 @@ def create_merge_logs(invoice_by_customer, closing_entry=None): merge_log.set("pos_invoices", invoices) merge_log.save(ignore_permissions=True) merge_log.submit() +======= +def split_invoices(invoices): + ''' + Splits invoices into multiple groups + Use-case: + If a serial no is sold and later it is returned + then split the invoices such that the selling entry is merged first and then the return entry + ''' + # Input + # invoices = [ + # {'pos_invoice': 'Invoice with SR#1 & SR#2', 'is_return': 0}, + # {'pos_invoice': 'Invoice with SR#1', 'is_return': 1}, + # {'pos_invoice': 'Invoice with SR#2', 'is_return': 0} + # ] + # Output + # _invoices = [ + # [{'pos_invoice': 'Invoice with SR#1 & SR#2', 'is_return': 0}], + # [{'pos_invoice': 'Invoice with SR#1', 'is_return': 1}, {'pos_invoice': 'Invoice with SR#2', 'is_return': 0}], + # ] + + _invoices = [] + special_invoices = [] + pos_return_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in invoices if d.is_return and d.return_against] + for pos_invoice in pos_return_docs: + for item in pos_invoice.items: + if not item.serial_no: continue + + return_against_is_added = any(d for d in _invoices if d.pos_invoice == pos_invoice.return_against) + if return_against_is_added: break + + return_against_is_consolidated = frappe.db.get_value('POS Invoice', pos_invoice.return_against, 'status', cache=True) == 'Consolidated' + if return_against_is_consolidated: break + + pos_invoice_row = [d for d in invoices if d.pos_invoice == pos_invoice.return_against] + _invoices.append(pos_invoice_row) + special_invoices.append(pos_invoice.return_against) + break + + _invoices.append([d for d in invoices if d.pos_invoice not in special_invoices]) + + return _invoices + +def create_merge_logs(invoice_by_customer, closing_entry=None): + try: + for customer, invoices in invoice_by_customer.items(): + for _invoices in split_invoices(invoices): + merge_log = frappe.new_doc('POS Invoice Merge Log') + merge_log.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate() + merge_log.customer = customer + merge_log.pos_closing_entry = closing_entry.get('name') if closing_entry else None + + merge_log.set('pos_invoices', _invoices) + merge_log.save(ignore_permissions=True) + merge_log.submit() +>>>>>>> cf51a0a1b8 (fix(pos): cannot close the pos if sr. no. is sold & returned) if closing_entry: closing_entry.set_status(update=True, status="Submitted") 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 c6d8179c40c..e3cd2863f70 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 @@ -391,3 +391,68 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") +<<<<<<< HEAD +======= + + def test_serial_no_case_1(self): + ''' + Create a POS Invoice with serial no + Create a Return Invoice with serial no + Create a POS Invoice with serial no again + Consolidate the invoices + + The first POS Invoice should be consolidated with a separate single Merge Log + The second and third POS Invoice should be consolidated with a single Merge Log + ''' + + from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + + frappe.db.sql("delete from `tabPOS Invoice`") + + try: + se = make_serialized_item() + serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] + + 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.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_inv.submit() + + consolidate_pos_invoices() + + 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`") +>>>>>>> cf51a0a1b8 (fix(pos): cannot close the pos if sr. no. is sold & returned) diff --git a/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json b/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json index 205c4ede901..387c4b0f360 100644 --- a/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json +++ b/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json @@ -9,7 +9,9 @@ "posting_date", "column_break_3", "customer", - "grand_total" + "grand_total", + "is_return", + "return_against" ], "fields": [ { @@ -48,11 +50,27 @@ "in_list_view": 1, "label": "Amount", "reqd": 1 + }, + { + "default": "0", + "fetch_from": "pos_invoice.is_return", + "fieldname": "is_return", + "fieldtype": "Check", + "label": "Is Return", + "read_only": 1 + }, + { + "fetch_from": "pos_invoice.return_against", + "fieldname": "return_against", + "fieldtype": "Link", + "label": "Return Against", + "options": "POS Invoice", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2020-05-29 15:08:42.194979", + "modified": "2022-03-24 13:32:02.366257", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Reference", @@ -61,5 +79,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From 650274e973c9f143e3499c0c9e8b86fa4bedca74 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 24 Mar 2022 17:56:27 +0530 Subject: [PATCH 07/14] fix: sider issues (cherry picked from commit cb4873c019f7694f64b94b5845e2fa73a602103a) --- .../pos_invoice_merge_log/pos_invoice_merge_log.py | 9 ++++++--- erpnext/stock/doctype/serial_no/serial_no.py | 11 +++-------- 2 files changed, 9 insertions(+), 11 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 d97ce1cffc0..77f9755de16 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 @@ -371,13 +371,16 @@ def split_invoices(invoices): pos_return_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in invoices if d.is_return and d.return_against] for pos_invoice in pos_return_docs: for item in pos_invoice.items: - if not item.serial_no: continue + if not item.serial_no: + continue return_against_is_added = any(d for d in _invoices if d.pos_invoice == pos_invoice.return_against) - if return_against_is_added: break + if return_against_is_added: + break return_against_is_consolidated = frappe.db.get_value('POS Invoice', pos_invoice.return_against, 'status', cache=True) == 'Consolidated' - if return_against_is_consolidated: break + if return_against_is_consolidated: + break pos_invoice_row = [d for d in invoices if d.pos_invoice == pos_invoice.return_against] _invoices.append(pos_invoice_row) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index e9a5d53fdd7..3b4c358e425 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -828,14 +828,9 @@ def get_delivered_serial_nos(serial_nos): SerialNo = frappe.qb.DocType("Serial No") serial_nos = get_serial_nos(serial_nos) - query = ( - frappe.qb - .from_(SerialNo) - .select(SerialNo.name) - .where( - (SerialNo.name.isin(serial_nos)) - & (Coalesce(SerialNo.delivery_document_type, "") != "") - ) + query = frappe.qb.select(SerialNo.name).from_(SerialNo).where( + (SerialNo.name.isin(serial_nos)) + & (Coalesce(SerialNo.delivery_document_type, "") != "") ) result = query.run() From cf3e09588fdb803b4b084d4eecefc6dc78c01fdd Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 24 Mar 2022 17:59:34 +0530 Subject: [PATCH 08/14] fix: test cases (cherry picked from commit 1b556d1c5353c9fca94f9d00496776e1c2a69839) --- erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py | 1 + .../doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 0925614cbc7..0493b8a90a7 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -439,6 +439,7 @@ class TestPOSInvoice(unittest.TestCase): ) si.get("items")[0].serial_no = serial_nos[0] + si.update_stock = 1 si.insert() si.submit() diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index e3cd2863f70..71cb87f75cd 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 @@ -442,7 +442,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): pos_inv2.append('payments', { 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 100 }) - pos_inv.submit() + pos_inv2.submit() consolidate_pos_invoices() From 82aea2b998d3100c23fc2a6de6ed65c2f7eb488a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 25 Mar 2022 10:51:30 +0530 Subject: [PATCH 09/14] fix: set is_return & return_against in POS Invoice Reference table (cherry picked from commit 16253a2f7207d8c4182e92a45247144fe24489db) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 9 +++++ ...eturn_against_in_pos_invoice_references.py | 38 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 50050afd316..23e86332f74 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -352,8 +352,17 @@ erpnext.patches.v13_0.update_reserved_qty_closed_wo erpnext.patches.v13_0.amazon_mws_deprecation_warning erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr erpnext.patches.v13_0.update_accounts_in_loan_docs +<<<<<<< HEAD erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022 erpnext.patches.v13_0.rename_non_profit_fields erpnext.patches.v13_0.enable_ksa_vat_docs #1 erpnext.patches.v13_0.create_gst_custom_fields_in_quotation erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances +======= +erpnext.patches.v14_0.update_batch_valuation_flag +erpnext.patches.v14_0.delete_non_profit_doctypes +erpnext.patches.v14_0.update_employee_advance_status +erpnext.patches.v13_0.add_cost_center_in_loans +erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items +erpnext.patches.v13_0.set_return_against_in_pos_invoice_references +>>>>>>> 16253a2f72 (fix: set is_return & return_against in POS Invoice Reference table) diff --git a/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py b/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py new file mode 100644 index 00000000000..6c24f520274 --- /dev/null +++ b/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py @@ -0,0 +1,38 @@ +import frappe + + +def execute(): + ''' + Fetch and Set is_return & return_against from POS Invoice in POS Invoice References table. + ''' + + POSClosingEntry = frappe.qb.DocType("POS Closing Entry") + open_pos_closing_entries = ( + frappe.qb + .from_(POSClosingEntry) + .select(POSClosingEntry.name) + .where(POSClosingEntry.docstatus == 0) + .run(pluck=True) + ) + + if not open_pos_closing_entries: + return + + POSInvoiceReference = frappe.qb.DocType("POS Invoice Reference") + POSInvoice = frappe.qb.DocType("POS Invoice") + pos_invoice_references = ( + frappe.qb + .from_(POSInvoiceReference) + .join(POSInvoice) + .on(POSInvoiceReference.pos_invoice == POSInvoice.name) + .select(POSInvoiceReference.name, POSInvoice.is_return, POSInvoice.return_against) + .where(POSInvoiceReference.parent.isin(open_pos_closing_entries)) + .run(as_dict=True) + ) + + for row in pos_invoice_references: + frappe.db.set_value("POS Invoice Reference", row.name, "is_return", row.is_return) + if row.is_return: + frappe.db.set_value("POS Invoice Reference", row.name, "return_against", row.return_against) + else: + frappe.db.set_value("POS Invoice Reference", row.name, "return_against", None) From 47567c66c11a9c8013d6fce36aa32733c6d396e6 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 25 Mar 2022 14:26:12 +0530 Subject: [PATCH 10/14] chore: ignore rules for QB formatting (cherry picked from commit e0c36d87e0198392c5a7369c31690f5922ea5d64) --- .flake8 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.flake8 b/.flake8 index 5735456ae7d..4ff88403244 100644 --- a/.flake8 +++ b/.flake8 @@ -29,6 +29,8 @@ ignore = B950, W191, E124, # closing bracket, irritating while writing QB code + E131, # continuation line unaligned for hanging indent + E123, # closing bracket does not match indentation of opening bracket's line max-line-length = 200 exclude=.github/helper/semgrep_rules From 0bafec23844727b27b8bf7c827fdd9129af92868 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 31 Mar 2022 14:24:42 +0530 Subject: [PATCH 11/14] fix: merge conflicts --- .../doctype/pos_invoice/pos_invoice.py | 27 ++------ .../pos_invoice_merge_log.py | 69 ++++++++++--------- .../test_pos_invoice_merge_log.py | 35 ++++------ erpnext/patches.txt | 8 --- erpnext/stock/doctype/serial_no/serial_no.py | 57 ++++++--------- 5 files changed, 75 insertions(+), 121 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 8df2e3587df..885e3882287 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -183,16 +183,7 @@ class POSInvoice(SalesInvoice): ) def validate_delivered_serial_nos(self, item): -<<<<<<< HEAD - serial_nos = get_serial_nos(item.serial_no) - delivered_serial_nos = frappe.db.get_list( - "Serial No", - {"item_code": item.item_code, "name": ["in", serial_nos], "sales_invoice": ["is", "set"]}, - pluck="name", - ) -======= delivered_serial_nos = get_delivered_serial_nos(item.serial_no) ->>>>>>> f2ae63cbfd (fix(pos): remove returned sr. nos. from pos reserved sr. nos. list) if delivered_serial_nos: bold_delivered_serial_nos = frappe.bold(", ".join(delivered_serial_nos)) @@ -220,24 +211,18 @@ class POSInvoice(SalesInvoice): frappe.throw(error_msg, title=_("Invalid Item"), as_list=True) def validate_stock_availablility(self): -<<<<<<< HEAD - if self.is_return or self.docstatus != 1: - return - allow_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock") - for d in self.get("items"): - is_service_item = not (frappe.db.get_value("Item", d.get("item_code"), "is_stock_item")) -======= if self.is_return: return - if self.docstatus.is_draft() and not frappe.db.get_value('POS Profile', self.pos_profile, 'validate_stock_on_save'): + if self.docstatus.is_draft() and not frappe.db.get_value( + "POS Profile", self.pos_profile, "validate_stock_on_save" + ): return - from erpnext.stock.stock_ledger import is_negative_stock_allowed + allow_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock") - for d in self.get('items'): - is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item')) ->>>>>>> aff7408775 (fix(pos): allow validating stock on save) + for d in self.get("items"): + is_service_item = not (frappe.db.get_value("Item", d.get("item_code"), "is_stock_item")) if is_service_item: return if d.serial_no: 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 77f9755de16..d3a81fe61dc 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 @@ -5,7 +5,6 @@ import json import frappe -import six from frappe import _ from frappe.core.page.background_jobs.background_jobs import get_info from frappe.model.document import Document @@ -66,7 +65,9 @@ class POSInvoiceMergeLog(Document): frappe.throw(msg) def on_submit(self): - pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] + pos_invoice_docs = [ + frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices + ] 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] @@ -83,7 +84,9 @@ class POSInvoiceMergeLog(Document): self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note) def on_cancel(self): - pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] + pos_invoice_docs = [ + frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices + ] self.update_pos_invoices(pos_invoice_docs) self.cancel_linked_invoices() @@ -279,16 +282,18 @@ def get_all_unconsolidated_invoices(): "status": ["not in", ["Consolidated"]], "docstatus": 1, } -<<<<<<< HEAD pos_invoices = frappe.db.get_all( "POS Invoice", filters=filters, - fields=["name as pos_invoice", "posting_date", "grand_total", "customer"], + fields=[ + "name as pos_invoice", + "posting_date", + "grand_total", + "customer", + "is_return", + "return_against", + ], ) -======= - pos_invoices = frappe.db.get_all('POS Invoice', filters=filters, - fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer', 'is_return', 'return_against']) ->>>>>>> cf51a0a1b8 (fix(pos): cannot close the pos if sr. no. is sold & returned) return pos_invoices @@ -331,29 +336,14 @@ def unconsolidate_pos_invoices(closing_entry): else: cancel_merge_logs(merge_logs, closing_entry) -<<<<<<< HEAD -def create_merge_logs(invoice_by_customer, closing_entry=None): - try: - for customer, invoices in six.iteritems(invoice_by_customer): - merge_log = frappe.new_doc("POS Invoice Merge Log") - merge_log.posting_date = ( - getdate(closing_entry.get("posting_date")) if closing_entry else nowdate() - ) - merge_log.customer = customer - merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None - - merge_log.set("pos_invoices", invoices) - merge_log.save(ignore_permissions=True) - merge_log.submit() -======= def split_invoices(invoices): - ''' + """ Splits invoices into multiple groups Use-case: If a serial no is sold and later it is returned then split the invoices such that the selling entry is merged first and then the return entry - ''' + """ # Input # invoices = [ # {'pos_invoice': 'Invoice with SR#1 & SR#2', 'is_return': 0}, @@ -368,17 +358,26 @@ def split_invoices(invoices): _invoices = [] special_invoices = [] - pos_return_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in invoices if d.is_return and d.return_against] + pos_return_docs = [ + frappe.get_cached_doc("POS Invoice", d.pos_invoice) + for d in invoices + if d.is_return and d.return_against + ] for pos_invoice in pos_return_docs: for item in pos_invoice.items: if not item.serial_no: continue - return_against_is_added = any(d for d in _invoices if d.pos_invoice == pos_invoice.return_against) + return_against_is_added = any( + d for d in _invoices if d.pos_invoice == pos_invoice.return_against + ) if return_against_is_added: break - return_against_is_consolidated = frappe.db.get_value('POS Invoice', pos_invoice.return_against, 'status', cache=True) == 'Consolidated' + return_against_is_consolidated = ( + frappe.db.get_value("POS Invoice", pos_invoice.return_against, "status", cache=True) + == "Consolidated" + ) if return_against_is_consolidated: break @@ -391,19 +390,21 @@ def split_invoices(invoices): return _invoices + def create_merge_logs(invoice_by_customer, closing_entry=None): try: for customer, invoices in invoice_by_customer.items(): for _invoices in split_invoices(invoices): - merge_log = frappe.new_doc('POS Invoice Merge Log') - merge_log.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate() + merge_log = frappe.new_doc("POS Invoice Merge Log") + merge_log.posting_date = ( + getdate(closing_entry.get("posting_date")) if closing_entry else nowdate() + ) merge_log.customer = customer - merge_log.pos_closing_entry = closing_entry.get('name') if closing_entry else None + merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None - merge_log.set('pos_invoices', _invoices) + merge_log.set("pos_invoices", _invoices) merge_log.save(ignore_permissions=True) merge_log.submit() ->>>>>>> cf51a0a1b8 (fix(pos): cannot close the pos if sr. no. is sold & returned) if closing_entry: closing_entry.set_status(update=True, status="Submitted") 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 71cb87f75cd..9e696f18b6a 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 @@ -391,11 +391,9 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") -<<<<<<< HEAD -======= def test_serial_no_case_1(self): - ''' + """ Create a POS Invoice with serial no Create a Return Invoice with serial no Create a POS Invoice with serial no again @@ -403,7 +401,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): The first POS Invoice should be consolidated with a separate single Merge Log The second and third POS Invoice should be consolidated with a single Merge Log - ''' + """ from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item @@ -417,15 +415,13 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): 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 + 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.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) pos_inv.submit() pos_inv_cn = make_sales_return(pos_inv.name) @@ -433,15 +429,13 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): 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 + 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.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) pos_inv2.submit() consolidate_pos_invoices() @@ -455,4 +449,3 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") ->>>>>>> cf51a0a1b8 (fix(pos): cannot close the pos if sr. no. is sold & returned) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 23e86332f74..98e07783c11 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -352,17 +352,9 @@ erpnext.patches.v13_0.update_reserved_qty_closed_wo erpnext.patches.v13_0.amazon_mws_deprecation_warning erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr erpnext.patches.v13_0.update_accounts_in_loan_docs -<<<<<<< HEAD erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022 erpnext.patches.v13_0.rename_non_profit_fields erpnext.patches.v13_0.enable_ksa_vat_docs #1 erpnext.patches.v13_0.create_gst_custom_fields_in_quotation erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances -======= -erpnext.patches.v14_0.update_batch_valuation_flag -erpnext.patches.v14_0.delete_non_profit_doctypes -erpnext.patches.v14_0.update_employee_advance_status -erpnext.patches.v13_0.add_cost_center_in_loans -erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items erpnext.patches.v13_0.set_return_against_in_pos_invoice_references ->>>>>>> 16253a2f72 (fix: set is_return & return_against in POS Invoice Reference table) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 3b4c358e425..6d3969892f2 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -821,16 +821,17 @@ def auto_fetch_serial_number( def get_delivered_serial_nos(serial_nos): - ''' + """ Returns serial numbers that delivered from the list of serial numbers - ''' + """ from frappe.query_builder.functions import Coalesce SerialNo = frappe.qb.DocType("Serial No") serial_nos = get_serial_nos(serial_nos) - query = frappe.qb.select(SerialNo.name).from_(SerialNo).where( - (SerialNo.name.isin(serial_nos)) - & (Coalesce(SerialNo.delivery_document_type, "") != "") + query = ( + frappe.qb.select(SerialNo.name) + .from_(SerialNo) + .where((SerialNo.name.isin(serial_nos)) & (Coalesce(SerialNo.delivery_document_type, "") != "")) ) result = query.run() @@ -838,48 +839,30 @@ def get_delivered_serial_nos(serial_nos): delivered_serial_nos = [row[0] for row in result] return delivered_serial_nos + @frappe.whitelist() def get_pos_reserved_serial_nos(filters): if isinstance(filters, str): filters = json.loads(filters) -<<<<<<< HEAD - pos_transacted_sr_nos = frappe.db.sql( - """select item.serial_no as serial_no - from `tabPOS Invoice` p, `tabPOS Invoice Item` item - where p.name = item.parent - and p.consolidated_invoice is NULL - and p.docstatus = 1 - and item.docstatus = 1 - and item.item_code = %(item_code)s - and item.warehouse = %(warehouse)s - and item.serial_no is NOT NULL and item.serial_no != '' - """, - filters, - as_dict=1, - ) -======= POSInvoice = frappe.qb.DocType("POS Invoice") POSInvoiceItem = frappe.qb.DocType("POS Invoice Item") - query = frappe.qb.from_( - POSInvoice - ).from_( - POSInvoiceItem - ).select( - POSInvoice.is_return, - POSInvoiceItem.serial_no - ).where( - (POSInvoice.name == POSInvoiceItem.parent) - & (POSInvoice.docstatus == 1) - & (POSInvoiceItem.docstatus == 1) - & (POSInvoiceItem.item_code == filters.get('item_code')) - & (POSInvoiceItem.warehouse == filters.get('warehouse')) - & (POSInvoiceItem.serial_no.isnotnull()) - & (POSInvoiceItem.serial_no != '') + query = ( + frappe.qb.from_(POSInvoice) + .from_(POSInvoiceItem) + .select(POSInvoice.is_return, POSInvoiceItem.serial_no) + .where( + (POSInvoice.name == POSInvoiceItem.parent) + & (POSInvoice.docstatus == 1) + & (POSInvoiceItem.docstatus == 1) + & (POSInvoiceItem.item_code == filters.get("item_code")) + & (POSInvoiceItem.warehouse == filters.get("warehouse")) + & (POSInvoiceItem.serial_no.isnotnull()) + & (POSInvoiceItem.serial_no != "") + ) ) pos_transacted_sr_nos = query.run(as_dict=True) ->>>>>>> f2ae63cbfd (fix(pos): remove returned sr. nos. from pos reserved sr. nos. list) reserved_sr_nos = [] returned_sr_nos = [] From ab7417c26ab2653326bbfbe31c6ef3170cdff8aa Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 31 Mar 2022 14:38:40 +0530 Subject: [PATCH 12/14] fix: invalid keyword argument 'pluck' --- ...eturn_against_in_pos_invoice_references.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py b/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py index 6c24f520274..6af9617bcee 100644 --- a/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py +++ b/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py @@ -2,18 +2,19 @@ import frappe def execute(): - ''' + """ Fetch and Set is_return & return_against from POS Invoice in POS Invoice References table. - ''' + """ POSClosingEntry = frappe.qb.DocType("POS Closing Entry") open_pos_closing_entries = ( - frappe.qb - .from_(POSClosingEntry) - .select(POSClosingEntry.name) - .where(POSClosingEntry.docstatus == 0) - .run(pluck=True) - ) + frappe.qb.from_(POSClosingEntry) + .select(POSClosingEntry.name) + .where(POSClosingEntry.docstatus == 0) + .run() + ) + if open_pos_closing_entries: + open_pos_closing_entries = [d[0] for d in open_pos_closing_entries] if not open_pos_closing_entries: return @@ -21,13 +22,12 @@ def execute(): POSInvoiceReference = frappe.qb.DocType("POS Invoice Reference") POSInvoice = frappe.qb.DocType("POS Invoice") pos_invoice_references = ( - frappe.qb - .from_(POSInvoiceReference) - .join(POSInvoice) - .on(POSInvoiceReference.pos_invoice == POSInvoice.name) - .select(POSInvoiceReference.name, POSInvoice.is_return, POSInvoice.return_against) - .where(POSInvoiceReference.parent.isin(open_pos_closing_entries)) - .run(as_dict=True) + frappe.qb.from_(POSInvoiceReference) + .join(POSInvoice) + .on(POSInvoiceReference.pos_invoice == POSInvoice.name) + .select(POSInvoiceReference.name, POSInvoice.is_return, POSInvoice.return_against) + .where(POSInvoiceReference.parent.isin(open_pos_closing_entries)) + .run(as_dict=True) ) for row in pos_invoice_references: From 8d315a6573e21456f280268a1113b1cc9042a75a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 31 Mar 2022 16:17:51 +0530 Subject: [PATCH 13/14] fix: 'int' object has no attribute 'is_draft' --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 885e3882287..96975e9d116 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -214,7 +214,7 @@ class POSInvoice(SalesInvoice): if self.is_return: return - if self.docstatus.is_draft() and not frappe.db.get_value( + if self.docstatus == 0 and not frappe.db.get_value( "POS Profile", self.pos_profile, "validate_stock_on_save" ): return From 9335578a0b9bac6a8a0a3d9455d1e7b23af12f74 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 1 Apr 2022 11:02:18 +0530 Subject: [PATCH 14/14] fix: linting errors --- .../doctype/pos_invoice/test_pos_invoice.py | 38 +++++++++---------- .../page/point_of_sale/point_of_sale.py | 7 ++-- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 0493b8a90a7..70f128e0e39 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -774,14 +774,12 @@ class TestPOSInvoice(unittest.TestCase): from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item - frappe.db.savepoint('before_test_delivered_serial_no_case') + frappe.db.savepoint("before_test_delivered_serial_no_case") try: se = make_serialized_item() serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] - dn = create_delivery_note( - item_code="_Test Serialized Item With Series", serial_no=serial_no - ) + dn = create_delivery_note(item_code="_Test Serialized Item With Series", serial_no=serial_no) delivery_document_no = frappe.db.get_value("Serial No", serial_no, "delivery_document_no") self.assertEquals(delivery_document_no, dn.name) @@ -789,17 +787,17 @@ class TestPOSInvoice(unittest.TestCase): 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=True + item_code="_Test Serialized Item With Series", + serial_no=serial_no, + qty=1, + rate=100, + do_not_submit=True, ) self.assertRaises(frappe.ValidationError, pos_inv.submit) finally: - frappe.db.rollback(save_point='before_test_delivered_serial_no_case') + frappe.db.rollback(save_point="before_test_delivered_serial_no_case") frappe.set_user("Administrator") def test_returned_serial_no_case(self): @@ -810,7 +808,7 @@ class TestPOSInvoice(unittest.TestCase): from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item - frappe.db.savepoint('before_test_returned_serial_no_case') + frappe.db.savepoint("before_test_returned_serial_no_case") try: se = make_serialized_item() serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] @@ -818,10 +816,10 @@ class TestPOSInvoice(unittest.TestCase): init_user_and_profile() pos_inv = create_pos_invoice( - item_code="_Test Serialized Item With Series", - serial_no=serial_no, - qty=1, - rate=100, + item_code="_Test Serialized Item With Series", + serial_no=serial_no, + qty=1, + rate=100, ) pos_return = make_sales_return(pos_inv.name) @@ -829,16 +827,16 @@ class TestPOSInvoice(unittest.TestCase): pos_return.insert() pos_return.submit() - pos_reserved_serial_nos = get_pos_reserved_serial_nos({ - 'item_code': '_Test Serialized Item With Series', - 'warehouse': '_Test Warehouse - _TC' - }) + pos_reserved_serial_nos = get_pos_reserved_serial_nos( + {"item_code": "_Test Serialized Item With Series", "warehouse": "_Test Warehouse - _TC"} + ) self.assertTrue(serial_no not in pos_reserved_serial_nos) finally: - frappe.db.rollback(save_point='before_test_returned_serial_no_case') + frappe.db.rollback(save_point="before_test_returned_serial_no_case") frappe.set_user("Administrator") + def create_pos_invoice(**args): args = frappe._dict(args) pos_profile = None diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 26a80763c9b..bf629824ad9 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -325,15 +325,16 @@ def set_customer_info(fieldname, customer, value=""): frappe.db.set_value("Customer", customer, "mobile_no", value) contact_doc.save() + @frappe.whitelist() def get_pos_profile_data(pos_profile): - pos_profile = frappe.get_doc('POS Profile', pos_profile) + pos_profile = frappe.get_doc("POS Profile", pos_profile) pos_profile = pos_profile.as_dict() _customer_groups_with_children = [] for row in pos_profile.customer_groups: - children = get_child_nodes('Customer Group', row.customer_group) + children = get_child_nodes("Customer Group", row.customer_group) _customer_groups_with_children.extend(children) pos_profile.customer_groups = _customer_groups_with_children - return pos_profile \ No newline at end of file + return pos_profile