From b825179286dd5e76a49f265eae9d5c4ce5a1b045 Mon Sep 17 00:00:00 2001 From: maharshivpatel <39730881+maharshivpatel@users.noreply.github.com> Date: Sat, 7 May 2022 18:46:12 +0530 Subject: [PATCH 01/27] feat(india): store e-way bill auto calculated distance in sales invoice (#30908) --- erpnext/regional/india/e_invoice/einvoice.js | 20 +++++++++++++++++--- erpnext/regional/india/e_invoice/utils.py | 13 +++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 17b018c65b..58f8e117bc 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -99,8 +99,21 @@ erpnext.setup_einvoice_actions = (doctype) => { ...data }, freeze: true, - callback: () => frm.reload_doc() || d.hide(), - error: () => d.hide() + callback: () => { + frappe.show_alert({ + message: __('E-Way Bill Generated successfully'), + indicator: 'green' + }, 7); + frm.reload_doc(); + d.hide(); + }, + error: () => { + frappe.show_alert({ + message: __('E-Way Bill was not Generated'), + indicator: 'red' + }, 7); + d.hide(); + } }); }, primary_action_label: __('Submit') @@ -202,7 +215,8 @@ const get_ewaybill_fields = (frm) => { 'fieldname': 'distance', 'label': 'Distance (in km)', 'fieldtype': 'Float', - 'default': frm.doc.distance + 'default': frm.doc.distance, + 'description': 'Set as zero to auto calculate distance using pin codes', }, { 'fieldname': 'transporter_col_break', diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index cfe9cee14a..4e6c9a530e 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -1113,6 +1113,19 @@ class GSPConnector: self.invoice.eway_bill_validity = res.get("result").get("EwbValidTill") self.invoice.eway_bill_cancelled = 0 self.invoice.update(args) + if res.get("info"): + info = res.get("info") + # when we have more features (responses) in eway bill, we can add them using below forloop. + for msg in info: + if msg.get("InfCd") == "EWBPPD": + pin_to_pin_distance = int(re.search(r"\d+", msg.get("Desc")).group()) + frappe.msgprint( + _("Auto Calculated Distance is {} KM.").format(str(pin_to_pin_distance)), + title="Notification", + indicator="green", + alert=True, + ) + self.invoice.distance = flt(pin_to_pin_distance) self.invoice.flags.updater_reference = { "doctype": self.invoice.doctype, "docname": self.invoice.name, From b8dc40b7a0713de6a44ff03cb8aab03e21483561 Mon Sep 17 00:00:00 2001 From: maharshivpatel <39730881+maharshivpatel@users.noreply.github.com> Date: Sat, 7 May 2022 18:59:27 +0530 Subject: [PATCH 02/27] feat(india): cancel e-way bill is enabled with e-invoicing APIs. (#30888) --- erpnext/regional/india/e_invoice/einvoice.js | 67 ++++++++++++++------ erpnext/regional/india/e_invoice/utils.py | 13 ++-- 2 files changed, 53 insertions(+), 27 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 58f8e117bc..763e65784a 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -149,27 +149,58 @@ erpnext.setup_einvoice_actions = (doctype) => { } if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { + const fields = [ + { + "label": "Reason", + "fieldname": "reason", + "fieldtype": "Select", + "reqd": 1, + "default": "1-Duplicate", + "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] + }, + { + "label": "Remark", + "fieldname": "remark", + "fieldtype": "Data", + "reqd": 1 + } + ]; const action = () => { - let message = __('Cancellation of e-way bill is currently not supported.') + ' '; - message += '

'; - message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.'); - - const dialog = frappe.msgprint({ - title: __('Update E-Way Bill Cancelled Status?'), - message: message, - indicator: 'orange', - primary_action: { - action: function() { - frappe.call({ - method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', - args: { doctype, docname: name }, - freeze: true, - callback: () => frm.reload_doc() || dialog.hide() - }); - } + const d = new frappe.ui.Dialog({ + title: __('Cancel E-Way Bill'), + fields: fields, + primary_action: function() { + const data = d.get_values(); + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', + args: { + doctype, + docname: name, + eway_bill: ewaybill, + reason: data.reason.split('-')[0], + remark: data.remark + }, + freeze: true, + callback: () => { + frappe.show_alert({ + message: __('E-Way Bill Cancelled successfully'), + indicator: 'green' + }, 7); + frm.reload_doc(); + d.hide(); + }, + error: () => { + frappe.show_alert({ + message: __('E-Way Bill was not Cancelled'), + indicator: 'red' + }, 7); + d.hide(); + } + }); }, - primary_action_label: __('Yes') + primary_action_label: __('Submit') }); + d.show(); }; add_custom_button(__("Cancel E-Way Bill"), action); } diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 4e6c9a530e..53d3211d3b 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -792,7 +792,7 @@ class GSPConnector: self.irn_details_url = self.base_url + "/enriched/ei/api/invoice/irn" self.generate_irn_url = self.base_url + "/enriched/ei/api/invoice" self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin" - self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB" + self.cancel_ewaybill_url = self.base_url + "/enriched/ei/api/ewayapi" self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill" def set_invoice(self): @@ -1148,7 +1148,6 @@ class GSPConnector: headers = self.get_headers() data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4) headers["username"] = headers["user_name"] - del headers["user_name"] try: res = self.make_request("post", self.cancel_ewaybill_url, headers, data) if res.get("success"): @@ -1313,13 +1312,9 @@ def generate_eway_bill(doctype, docname, **kwargs): @frappe.whitelist() -def cancel_eway_bill(doctype, docname): - # TODO: uncomment when eway_bill api from Adequare is enabled - # gsp_connector = GSPConnector(doctype, docname) - # gsp_connector.cancel_eway_bill(eway_bill, reason, remark) - - frappe.db.set_value(doctype, docname, "ewaybill", "") - frappe.db.set_value(doctype, docname, "eway_bill_cancelled", 1) +def cancel_eway_bill(doctype, docname, eway_bill, reason, remark): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.cancel_eway_bill(eway_bill, reason, remark) @frappe.whitelist() From d99b4e29b95db9c3ae9a2852a9757977e4912096 Mon Sep 17 00:00:00 2001 From: Govind S Menokee Date: Wed, 2 Mar 2022 22:29:19 +0530 Subject: [PATCH 03/27] fix: HSN-wise-summary of outward supplies Updated Report Report changes done in order to meet the specification as per govt guideline - [GUIDELINE](https://taxguru.in/goods-and-service-tax/12-points-note-filing-gstr-1-01st-2021-onwards.html) (cherry picked from commit 363752510ead7d3b86693d3659b2157753f2762d) # Conflicts: # erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py --- .../hsn_wise_summary_of_outward_supplies.py | 81 ++++++++++++++++--- 1 file changed, 72 insertions(+), 9 deletions(-) diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index 09f2df1226..bc1e479c46 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -32,7 +32,7 @@ def _execute(filters=None): added_item = [] for d in item_list: if (d.parent, d.item_code) not in added_item: - row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty] + row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty, d.tax_rate] total_tax = 0 for tax in tax_columns: item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {}) @@ -40,11 +40,9 @@ def _execute(filters=None): row += [d.base_net_amount + total_tax] row += [d.base_net_amount] - for tax in tax_columns: item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {}) row += [item_tax.get("tax_amount", 0)] - data.append(row) added_item.append((d.parent, d.item_code)) if data: @@ -59,7 +57,41 @@ def get_columns(): "label": _("HSN/SAC"), "fieldtype": "Link", "options": "GST HSN Code", +<<<<<<< HEAD "width": 100, +======= + "width": 100 + }, + { + "fieldname": "description", + "label": _("Description"), + "fieldtype": "Data", + "width": 300 + }, + { + "fieldname": "stock_uom", + "label": _("Stock UOM"), + "fieldtype": "Data", + "width": 100 + }, + { + "fieldname": "stock_qty", + "label": _("Stock Qty"), + "fieldtype": "Float", + "width": 90 + }, + { + "fieldname": "tax_rate", + "label": _("Tax Rate"), + "fieldtype": "Data", + "width": 90 + }, + { + "fieldname": "total_amount", + "label": _("Total Amount"), + "fieldtype": "Currency", + "width": 120 +>>>>>>> 363752510e (fix: HSN-wise-summary of outward supplies Updated Report) }, {"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 300}, {"fieldname": "stock_uom", "label": _("Stock UOM"), "fieldtype": "Data", "width": 100}, @@ -106,14 +138,24 @@ def get_items(filters): sum(`tabSales Invoice Item`.stock_qty) as stock_qty, sum(`tabSales Invoice Item`.base_net_amount) as base_net_amount, sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate, - `tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code, - `tabGST HSN Code`.description - from `tabSales Invoice`, `tabSales Invoice Item`, `tabGST HSN Code` - where `tabSales Invoice`.name = `tabSales Invoice Item`.parent + `tabSales Invoice Item`.parent, + `tabSales Invoice Item`.item_code, + `tabGST HSN Code`.description, + json_extract(`tabSales Taxes and Charges`.item_wise_tax_detail, + concat('$."' , `tabSales Invoice Item`.item_code, '"[0]')) * count(distinct `tabSales Taxes and Charges`.name) as tax_rate + from + `tabSales Invoice`, + `tabSales Invoice Item`, + `tabGST HSN Code`, + `tabSales Taxes and Charges` + where + `tabSales Invoice`.name = `tabSales Invoice Item`.parent + and `tabSales Taxes and Charges`.parent = `tabSales Invoice`.name and `tabSales Invoice`.docstatus = 1 and `tabSales Invoice Item`.gst_hsn_code is not NULL and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s group by +<<<<<<< HEAD `tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code """ @@ -121,6 +163,11 @@ def get_items(filters): filters, as_dict=1, ) +======= + `tabSales Invoice Item`.parent, + `tabSales Invoice Item`.item_code + """ % (conditions, match_conditions), filters, as_dict=1) +>>>>>>> 363752510e (fix: HSN-wise-summary of outward supplies Updated Report) return items @@ -213,8 +260,10 @@ def get_merged_data(columns, data): result = [] for row in data: - merged_hsn_dict.setdefault(row[0], {}) + key = row[0] + '-' + str(row[4]) + merged_hsn_dict.setdefault(key, {}) for i, d in enumerate(columns): +<<<<<<< HEAD if d["fieldtype"] not in ("Int", "Float", "Currency"): merged_hsn_dict[row[0]][d["fieldname"]] = row[i] else: @@ -222,6 +271,15 @@ def get_merged_data(columns, data): merged_hsn_dict[row[0]][d["fieldname"]] += row[i] else: merged_hsn_dict[row[0]][d["fieldname"]] = row[i] +======= + if d['fieldtype'] not in ('Int', 'Float', 'Currency'): + merged_hsn_dict[key][d['fieldname']] = row[i] + else: + if merged_hsn_dict.get(key, {}).get(d['fieldname'], ''): + merged_hsn_dict[key][d['fieldname']] += row[i] + else: + merged_hsn_dict[key][d['fieldname']] = row[i] +>>>>>>> 363752510e (fix: HSN-wise-summary of outward supplies Updated Report) for key, value in merged_hsn_dict.items(): result.append(value) @@ -240,9 +298,14 @@ def get_json(filters, report_name, data): fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) +<<<<<<< HEAD gst_json = {"version": "GST2.3.4", "hash": "hash", "gstin": gstin, "fp": fp} gst_json["hsn"] = {"data": get_hsn_wise_json_data(filters, report_data)} +======= + gst_json = {"version": "GST3.0.3", + "hash": "hash", "gstin": gstin, "fp": fp} +>>>>>>> 363752510e (fix: HSN-wise-summary of outward supplies Updated Report) return {"report_name": report_name, "data": gst_json} @@ -271,7 +334,7 @@ def get_hsn_wise_json_data(filters, report_data): "desc": hsn.get("description"), "uqc": hsn.get("stock_uom").upper(), "qty": hsn.get("stock_qty"), - "val": flt(hsn.get("total_amount"), 2), + "rt": flt(hsn.get("tax_rate"), 2), "txval": flt(hsn.get("taxable_amount", 2)), "iamt": 0.0, "camt": 0.0, From 3ae4b9033fc14c6fa10bc32297760fcc351cde1e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 7 May 2022 22:44:53 +0530 Subject: [PATCH 04/27] chore: Remove extra columns (cherry picked from commit b0e929b8ae8780ada76ea6b699a2b4d840b31487) # Conflicts: # erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py --- .../hsn_wise_summary_of_outward_supplies.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index bc1e479c46..057474782d 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -96,6 +96,10 @@ def get_columns(): {"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 300}, {"fieldname": "stock_uom", "label": _("Stock UOM"), "fieldtype": "Data", "width": 100}, {"fieldname": "stock_qty", "label": _("Stock Qty"), "fieldtype": "Float", "width": 90}, +<<<<<<< HEAD +======= + {"fieldname": "tax_rate", "label": _("Tax Rate"), "fieldtype": "Data", "width": 90}, +>>>>>>> b0e929b8ae (chore: Remove extra columns) {"fieldname": "total_amount", "label": _("Total Amount"), "fieldtype": "Currency", "width": 120}, { "fieldname": "taxable_amount", @@ -256,6 +260,7 @@ def get_tax_accounts( def get_merged_data(columns, data): + print(data) merged_hsn_dict = {} # to group same hsn under one key and perform row addition result = [] From 85c137b3aa55750005901799a005a0f69d9dc27a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 7 May 2022 22:53:59 +0530 Subject: [PATCH 05/27] chore: Remove print statement (cherry picked from commit a8fbd2451b498ad49356cf0e6a710616ab802465) --- .../hsn_wise_summary_of_outward_supplies.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index 057474782d..7323def2a4 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -260,7 +260,6 @@ def get_tax_accounts( def get_merged_data(columns, data): - print(data) merged_hsn_dict = {} # to group same hsn under one key and perform row addition result = [] From 95e41b0d4815da3365f9fb1166881c6ec0e9f94f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 8 May 2022 22:04:00 +0530 Subject: [PATCH 06/27] chore: correct version in hooks [skip ci] --- erpnext/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 066249ba22..d1b5113e2b 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -12,7 +12,7 @@ source_link = "https://github.com/frappe/erpnext" app_logo_url = "/assets/erpnext/images/erpnext-logo.svg" -develop_version = "13.x.x-develop" +develop_version = "14.x.x-develop" app_include_js = "erpnext.bundle.js" app_include_css = "erpnext.bundle.css" From c9f6405b2a30fd4eefd0fa90f1101f1247cdf584 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 9 May 2022 10:33:03 +0530 Subject: [PATCH 07/27] chore: Resolve conflicts --- .../hsn_wise_summary_of_outward_supplies.py | 74 ++----------------- 1 file changed, 8 insertions(+), 66 deletions(-) diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index 7323def2a4..3f42668862 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -57,49 +57,12 @@ def get_columns(): "label": _("HSN/SAC"), "fieldtype": "Link", "options": "GST HSN Code", -<<<<<<< HEAD "width": 100, -======= - "width": 100 - }, - { - "fieldname": "description", - "label": _("Description"), - "fieldtype": "Data", - "width": 300 - }, - { - "fieldname": "stock_uom", - "label": _("Stock UOM"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "stock_qty", - "label": _("Stock Qty"), - "fieldtype": "Float", - "width": 90 - }, - { - "fieldname": "tax_rate", - "label": _("Tax Rate"), - "fieldtype": "Data", - "width": 90 - }, - { - "fieldname": "total_amount", - "label": _("Total Amount"), - "fieldtype": "Currency", - "width": 120 ->>>>>>> 363752510e (fix: HSN-wise-summary of outward supplies Updated Report) }, {"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 300}, {"fieldname": "stock_uom", "label": _("Stock UOM"), "fieldtype": "Data", "width": 100}, {"fieldname": "stock_qty", "label": _("Stock Qty"), "fieldtype": "Float", "width": 90}, -<<<<<<< HEAD -======= {"fieldname": "tax_rate", "label": _("Tax Rate"), "fieldtype": "Data", "width": 90}, ->>>>>>> b0e929b8ae (chore: Remove extra columns) {"fieldname": "total_amount", "label": _("Total Amount"), "fieldtype": "Currency", "width": 120}, { "fieldname": "taxable_amount", @@ -159,19 +122,13 @@ def get_items(filters): and `tabSales Invoice Item`.gst_hsn_code is not NULL and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s group by -<<<<<<< HEAD - `tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code - + `tabSales Invoice Item`.parent, + `tabSales Invoice Item`.item_code """ % (conditions, match_conditions), filters, as_dict=1, ) -======= - `tabSales Invoice Item`.parent, - `tabSales Invoice Item`.item_code - """ % (conditions, match_conditions), filters, as_dict=1) ->>>>>>> 363752510e (fix: HSN-wise-summary of outward supplies Updated Report) return items @@ -264,26 +221,16 @@ def get_merged_data(columns, data): result = [] for row in data: - key = row[0] + '-' + str(row[4]) + key = row[0] + "-" + str(row[4]) merged_hsn_dict.setdefault(key, {}) for i, d in enumerate(columns): -<<<<<<< HEAD if d["fieldtype"] not in ("Int", "Float", "Currency"): - merged_hsn_dict[row[0]][d["fieldname"]] = row[i] + merged_hsn_dict[key][d["fieldname"]] = row[i] else: - if merged_hsn_dict.get(row[0], {}).get(d["fieldname"], ""): - merged_hsn_dict[row[0]][d["fieldname"]] += row[i] + if merged_hsn_dict.get(key, {}).get(d["fieldname"], ""): + merged_hsn_dict[key][d["fieldname"]] += row[i] else: - merged_hsn_dict[row[0]][d["fieldname"]] = row[i] -======= - if d['fieldtype'] not in ('Int', 'Float', 'Currency'): - merged_hsn_dict[key][d['fieldname']] = row[i] - else: - if merged_hsn_dict.get(key, {}).get(d['fieldname'], ''): - merged_hsn_dict[key][d['fieldname']] += row[i] - else: - merged_hsn_dict[key][d['fieldname']] = row[i] ->>>>>>> 363752510e (fix: HSN-wise-summary of outward supplies Updated Report) + merged_hsn_dict[key][d["fieldname"]] = row[i] for key, value in merged_hsn_dict.items(): result.append(value) @@ -302,14 +249,9 @@ def get_json(filters, report_name, data): fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) -<<<<<<< HEAD - gst_json = {"version": "GST2.3.4", "hash": "hash", "gstin": gstin, "fp": fp} + gst_json = {"version": "GST3.0.3", "hash": "hash", "gstin": gstin, "fp": fp} gst_json["hsn"] = {"data": get_hsn_wise_json_data(filters, report_data)} -======= - gst_json = {"version": "GST3.0.3", - "hash": "hash", "gstin": gstin, "fp": fp} ->>>>>>> 363752510e (fix: HSN-wise-summary of outward supplies Updated Report) return {"report_name": report_name, "data": gst_json} From 7c839c45032e9f39fee76996745a172b44b97c9b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 6 May 2022 12:09:08 +0530 Subject: [PATCH 08/27] fix: double future qty updates update_qty_in_future_sle is reprocessing rows which are already processed by process_sle_against_current_voucher --- .../test_stock_ledger_entry.py | 36 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 10 +++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index 5850ec7be6..4e2fc83a9d 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -1183,6 +1183,42 @@ class TestStockLedgerEntry(FrappeTestCase): backdated.cancel() self.assertEqual([1], ordered_qty_after_transaction()) + def test_timestamp_clash(self): + + item = make_item().name + warehouse = "_Test Warehouse - _TC" + + reciept = make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=100, + rate=10, + posting_date="2021-01-01", + posting_time="01:00:00", + ) + + consumption = make_stock_entry( + item_code=item, + from_warehouse=warehouse, + qty=50, + posting_date="2021-01-01", + posting_time="02:00:00.1234", # ms are possible when submitted without editing posting time + ) + + backdated_receipt = make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=100, + posting_date="2021-01-01", + rate=10, + posting_time="02:00:00", # same posting time as consumption but ms part stripped + ) + + try: + backdated_receipt.cancel() + except Exception as e: + self.fail("Double processing of qty for clashing timestamp.") + def create_repack_entry(**args): args = frappe._dict(args) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 7e5c231d9c..5a270d1ec0 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1303,6 +1303,8 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): datetime_limit_condition = "" qty_shift = args.actual_qty + args["time_format"] = "%H:%i:%s" + # find difference/shift in qty caused by stock reconciliation if args.voucher_type == "Stock Reconciliation": qty_shift = get_stock_reco_qty_shift(args) @@ -1323,12 +1325,8 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): and warehouse = %(warehouse)s and voucher_no != %(voucher_no)s and is_cancelled = 0 - and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s) - or ( - timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s) - and creation > %(creation)s - ) - ) + and timestamp(posting_date, time_format(posting_time, %(time_format)s)) + > timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) {datetime_limit_condition} """.format( qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition From ae842d81455ccd27045a9a9f88cc9eb819e3f5e4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 6 May 2022 13:42:09 +0530 Subject: [PATCH 09/27] chore: remove datettime formatting from debug report This hides some information that would otherwise help during debugging --- .../stock_ledger_invariant_check.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py index 837c4a6d15..ed0e2fc31b 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py @@ -111,17 +111,17 @@ def get_columns(): }, { "fieldname": "posting_date", - "fieldtype": "Date", + "fieldtype": "Data", "label": _("Posting Date"), }, { "fieldname": "posting_time", - "fieldtype": "Time", + "fieldtype": "Data", "label": _("Posting Time"), }, { "fieldname": "creation", - "fieldtype": "Datetime", + "fieldtype": "Data", "label": _("Creation"), }, { From f92bc4dd3350ea1b7d465c9791e7051dced0df27 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 9 May 2022 12:15:02 +0530 Subject: [PATCH 10/27] fix: remove check for already allocated earned leaves (#30931) * fix: remove check for already allocated earned leaves * fix: do not set New Leaves Allocated field as read-only for earned leaves - removing this until there's a better way to update existing allocations --- .../leave_allocation/leave_allocation.js | 9 --- .../test_leave_application.py | 4 +- .../test_leave_policy_assignment.py | 74 ++----------------- erpnext/hr/utils.py | 33 ++++----- 4 files changed, 21 insertions(+), 99 deletions(-) diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.js b/erpnext/hr/doctype/leave_allocation/leave_allocation.js index aef4412251..9742387c16 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.js +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.js @@ -34,15 +34,6 @@ frappe.ui.form.on("Leave Allocation", { }); } } - - // make new leaves allocated field read only if allocation is created via leave policy assignment - // and leave type is earned leave, since these leaves would be allocated via the scheduler - if (frm.doc.leave_policy_assignment) { - frappe.db.get_value("Leave Type", frm.doc.leave_type, "is_earned_leave", (r) => { - if (r && cint(r.is_earned_leave)) - frm.set_df_property("new_leaves_allocated", "read_only", 1); - }); - } }, expire_allocation: function(frm) { diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 4c39e15c93..7506c61108 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -745,7 +745,7 @@ class TestLeaveApplication(unittest.TestCase): i = 0 while i < 14: - allocate_earned_leaves(ignore_duplicates=True) + allocate_earned_leaves() i += 1 self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6) @@ -753,7 +753,7 @@ class TestLeaveApplication(unittest.TestCase): frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0) i = 0 while i < 6: - allocate_earned_leaves(ignore_duplicates=True) + allocate_earned_leaves() i += 1 self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9) diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py index 9780828557..031ed0e901 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py @@ -4,6 +4,7 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, add_months, get_first_day, get_last_day, getdate from erpnext.hr.doctype.leave_application.test_leave_application import ( @@ -18,7 +19,7 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import ( test_dependencies = ["Employee"] -class TestLeavePolicyAssignment(unittest.TestCase): +class TestLeavePolicyAssignment(FrappeTestCase): def setUp(self): for doctype in [ "Leave Period", @@ -39,6 +40,9 @@ class TestLeavePolicyAssignment(unittest.TestCase): leave_policy = create_leave_policy() leave_policy.submit() + self.employee.date_of_joining = get_first_day(leave_period.from_date) + self.employee.save() + data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, @@ -188,19 +192,6 @@ class TestLeavePolicyAssignment(unittest.TestCase): ) self.assertEqual(leaves_allocated, 3) - # if the daily job is not completed yet, there is another check present - # to ensure leave is not already allocated to avoid duplication - from erpnext.hr.utils import allocate_earned_leaves - - allocate_earned_leaves() - - leaves_allocated = frappe.db.get_value( - "Leave Allocation", - {"leave_policy_assignment": leave_policy_assignments[0]}, - "total_leaves_allocated", - ) - self.assertEqual(leaves_allocated, 3) - def test_earned_leave_alloc_for_passed_months_with_cf_leaves_based_on_leave_period(self): from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation @@ -242,20 +233,6 @@ class TestLeavePolicyAssignment(unittest.TestCase): self.assertEqual(details.unused_leaves, 5) self.assertEqual(details.total_leaves_allocated, 7) - # if the daily job is not completed yet, there is another check present - # to ensure leave is not already allocated to avoid duplication - from erpnext.hr.utils import is_earned_leave_already_allocated - - frappe.flags.current_date = get_last_day(getdate()) - - allocation = frappe.get_doc("Leave Allocation", details.name) - # 1 leave is still pending to be allocated, irrespective of carry forwarded leaves - self.assertFalse( - is_earned_leave_already_allocated( - allocation, leave_policy.leave_policy_details[0].annual_allocation - ) - ) - def test_earned_leave_alloc_for_passed_months_based_on_joining_date(self): # tests leave alloc for earned leaves for assignment based on joining date in policy assignment leave_type = create_earned_leave_type("Test Earned Leave") @@ -288,19 +265,6 @@ class TestLeavePolicyAssignment(unittest.TestCase): self.assertEqual(effective_from, self.employee.date_of_joining) self.assertEqual(leaves_allocated, 3) - # to ensure leave is not already allocated to avoid duplication - from erpnext.hr.utils import allocate_earned_leaves - - frappe.flags.current_date = get_last_day(getdate()) - allocate_earned_leaves() - - leaves_allocated = frappe.db.get_value( - "Leave Allocation", - {"leave_policy_assignment": leave_policy_assignments[0]}, - "total_leaves_allocated", - ) - self.assertEqual(leaves_allocated, 3) - def test_grant_leaves_on_doj_for_earned_leaves_based_on_leave_period(self): # tests leave alloc based on leave period for earned leaves with "based on doj" configuration in leave type leave_period, leave_policy = setup_leave_period_and_policy( @@ -330,20 +294,6 @@ class TestLeavePolicyAssignment(unittest.TestCase): ) self.assertEqual(leaves_allocated, 3) - # if the daily job is not completed yet, there is another check present - # to ensure leave is not already allocated to avoid duplication - from erpnext.hr.utils import allocate_earned_leaves - - frappe.flags.current_date = get_first_day(getdate()) - allocate_earned_leaves() - - leaves_allocated = frappe.db.get_value( - "Leave Allocation", - {"leave_policy_assignment": leave_policy_assignments[0]}, - "total_leaves_allocated", - ) - self.assertEqual(leaves_allocated, 3) - def test_grant_leaves_on_doj_for_earned_leaves_based_on_joining_date(self): # tests leave alloc based on joining date for earned leaves with "based on doj" configuration in leave type leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj=True) @@ -377,21 +327,7 @@ class TestLeavePolicyAssignment(unittest.TestCase): self.assertEqual(effective_from, self.employee.date_of_joining) self.assertEqual(leaves_allocated, 3) - # to ensure leave is not already allocated to avoid duplication - from erpnext.hr.utils import allocate_earned_leaves - - frappe.flags.current_date = get_first_day(getdate()) - allocate_earned_leaves() - - leaves_allocated = frappe.db.get_value( - "Leave Allocation", - {"leave_policy_assignment": leave_policy_assignments[0]}, - "total_leaves_allocated", - ) - self.assertEqual(leaves_allocated, 3) - def tearDown(self): - frappe.db.rollback() frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj) frappe.flags.current_date = None diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 4e30b0ff6e..269e4aae31 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -269,7 +269,7 @@ def generate_leave_encashment(): create_leave_encashment(leave_allocation=leave_allocation) -def allocate_earned_leaves(ignore_duplicates=False): +def allocate_earned_leaves(): """Allocate earned leaves to Employees""" e_leave_types = get_earned_leaves() today = getdate() @@ -305,14 +305,10 @@ def allocate_earned_leaves(ignore_duplicates=False): if check_effective_date( from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining ): - update_previous_leave_allocation( - allocation, annual_allocation, e_leave_type, ignore_duplicates - ) + update_previous_leave_allocation(allocation, annual_allocation, e_leave_type) -def update_previous_leave_allocation( - allocation, annual_allocation, e_leave_type, ignore_duplicates=False -): +def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type): earned_leaves = get_monthly_earned_leave( annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding ) @@ -326,20 +322,19 @@ def update_previous_leave_allocation( if new_allocation != allocation.total_leaves_allocated: today_date = today() - if ignore_duplicates or not is_earned_leave_already_allocated(allocation, annual_allocation): - allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) - create_additional_leave_ledger_entry(allocation, earned_leaves, today_date) + allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) + create_additional_leave_ledger_entry(allocation, earned_leaves, today_date) - if e_leave_type.based_on_date_of_joining: - text = _("allocated {0} leave(s) via scheduler on {1} based on the date of joining").format( - frappe.bold(earned_leaves), frappe.bold(formatdate(today_date)) - ) - else: - text = _("allocated {0} leave(s) via scheduler on {1}").format( - frappe.bold(earned_leaves), frappe.bold(formatdate(today_date)) - ) + if e_leave_type.based_on_date_of_joining: + text = _("allocated {0} leave(s) via scheduler on {1} based on the date of joining").format( + frappe.bold(earned_leaves), frappe.bold(formatdate(today_date)) + ) + else: + text = _("allocated {0} leave(s) via scheduler on {1}").format( + frappe.bold(earned_leaves), frappe.bold(formatdate(today_date)) + ) - allocation.add_comment(comment_type="Info", text=text) + allocation.add_comment(comment_type="Info", text=text) def get_monthly_earned_leave(annual_leaves, frequency, rounding): From 7e2fbc050a7e83bbb1a1f5f6a3f0df77155dca50 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 9 May 2022 11:13:31 +0530 Subject: [PATCH 11/27] fix: sort before picking next stock reco --- .../test_stock_reconciliation.py | 70 +++++++------------ erpnext/stock/stock_ledger.py | 7 +- 2 files changed, 30 insertions(+), 47 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 1e59aae9a8..d3d086dcdd 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -31,6 +31,7 @@ class TestStockReconciliation(FrappeTestCase): def tearDown(self): frappe.local.future_sle = {} + frappe.flags.pop("dont_execute_stock_reposts", None) def test_reco_for_fifo(self): self._test_reco_sle_gle("FIFO") @@ -306,6 +307,7 @@ class TestStockReconciliation(FrappeTestCase): ------------------------------------------- Var | Doc | Qty | Balance ------------------------------------------- + PR5 | PR | 10 | 10 (posting date: today-4) [backdated] SR5 | Reco | 0 | 8 (posting date: today-4) [backdated] PR1 | PR | 10 | 18 (posting date: today-3) PR2 | PR | 1 | 19 (posting date: today-2) @@ -315,6 +317,14 @@ class TestStockReconciliation(FrappeTestCase): item_code = make_item().name warehouse = "_Test Warehouse - _TC" + frappe.flags.dont_execute_stock_reposts = True + + def assertBalance(doc, qty_after_transaction): + sle_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": doc.name, "is_cancelled": 0}, "qty_after_transaction" + ) + self.assertEqual(sle_balance, qty_after_transaction) + pr1 = make_purchase_receipt( item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3) ) @@ -324,62 +334,37 @@ class TestStockReconciliation(FrappeTestCase): pr3 = make_purchase_receipt( item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=nowdate() ) - - pr1_balance = frappe.db.get_value( - "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction" - ) - pr3_balance = frappe.db.get_value( - "Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction" - ) - self.assertEqual(pr1_balance, 10) - self.assertEqual(pr3_balance, 12) + assertBalance(pr1, 10) + assertBalance(pr3, 12) # post backdated stock reco in between sr4 = create_stock_reconciliation( item_code=item_code, warehouse=warehouse, qty=6, rate=100, posting_date=add_days(nowdate(), -1) ) - pr3_balance = frappe.db.get_value( - "Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction" - ) - self.assertEqual(pr3_balance, 7) + assertBalance(pr3, 7) # post backdated stock reco at the start sr5 = create_stock_reconciliation( item_code=item_code, warehouse=warehouse, qty=8, rate=100, posting_date=add_days(nowdate(), -4) ) - pr1_balance = frappe.db.get_value( - "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction" + assertBalance(pr1, 18) + assertBalance(pr2, 19) + assertBalance(sr4, 6) # check if future stock reco is unaffected + + # Make a backdated receipt and check only entries till first SR are affected + pr5 = make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -5) ) - pr2_balance = frappe.db.get_value( - "Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "qty_after_transaction" - ) - sr4_balance = frappe.db.get_value( - "Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, "qty_after_transaction" - ) - self.assertEqual(pr1_balance, 18) - self.assertEqual(pr2_balance, 19) - self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected + assertBalance(pr5, 10) + # check if future stock reco is unaffected + assertBalance(sr4, 6) + assertBalance(sr5, 8) # cancel backdated stock reco and check future impact sr5.cancel() - pr1_balance = frappe.db.get_value( - "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction" - ) - pr2_balance = frappe.db.get_value( - "Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "qty_after_transaction" - ) - sr4_balance = frappe.db.get_value( - "Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, "qty_after_transaction" - ) - self.assertEqual(pr1_balance, 10) - self.assertEqual(pr2_balance, 11) - self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected - - # teardown - sr4.cancel() - pr3.cancel() - pr2.cancel() - pr1.cancel() + assertBalance(pr1, 10) + assertBalance(pr2, 11) + assertBalance(sr4, 6) # check if future stock reco is unaffected @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_backdated_stock_reco_future_negative_stock(self): @@ -485,7 +470,6 @@ class TestStockReconciliation(FrappeTestCase): # repost will make this test useless, qty should update in realtime without reposts frappe.flags.dont_execute_stock_reposts = True - self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts") item_code = make_item().name warehouse = "_Test Warehouse - _TC" diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 5a270d1ec0..4789b52d50 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1317,7 +1317,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): datetime_limit_condition = get_datetime_limit_condition(detail) frappe.db.sql( - """ + f""" update `tabStock Ledger Entry` set qty_after_transaction = qty_after_transaction + {qty_shift} where @@ -1328,9 +1328,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): and timestamp(posting_date, time_format(posting_time, %(time_format)s)) > timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) {datetime_limit_condition} - """.format( - qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition - ), + """, args, ) @@ -1381,6 +1379,7 @@ def get_next_stock_reco(args): and creation > %(creation)s ) ) + order by timestamp(posting_date, posting_time) asc, creation asc limit 1 """, args, From 867494edadd7399167e12ef6ec72b52bd2d7d2f8 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 9 May 2022 15:05:05 +0530 Subject: [PATCH 12/27] fix(pos): creating pos returns resets pricing rules & discounts (#30935) --- erpnext/selling/page/point_of_sale/pos_controller.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 7a6838680f..cb4bd51415 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -479,16 +479,20 @@ erpnext.PointOfSale.Controller = class { frappe.dom.freeze(); this.frm = this.get_new_frm(this.frm); this.frm.doc.items = []; - const res = await frappe.call({ + return frappe.call({ method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return", args: { 'source_name': doc.name, 'target_doc': this.frm.doc + }, + callback: (r) => { + frappe.model.sync(r.message); + frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = false; + this.set_pos_profile_data().then(() => { + frappe.dom.unfreeze(); + }); } }); - frappe.model.sync(res.message); - await this.set_pos_profile_data(); - frappe.dom.unfreeze(); } set_pos_profile_data() { From ebbe27c183370bc64c857e19de2340fb45fa4392 Mon Sep 17 00:00:00 2001 From: HarryPaulo Date: Mon, 9 May 2022 06:44:57 -0300 Subject: [PATCH 13/27] fix: subtract change_amount from paid_amount field on POS Register (#30922) --- erpnext/accounts/report/pos_register/pos_register.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/pos_register/pos_register.py b/erpnext/accounts/report/pos_register/pos_register.py index 1bda0d8ae3..9c0aba332e 100644 --- a/erpnext/accounts/report/pos_register/pos_register.py +++ b/erpnext/accounts/report/pos_register/pos_register.py @@ -62,7 +62,7 @@ def get_pos_entries(filters, group_by_field): """ SELECT p.posting_date, p.name as pos_invoice, p.pos_profile, - p.owner, p.base_grand_total as grand_total, p.base_paid_amount as paid_amount, + p.owner, p.base_grand_total as grand_total, p.base_paid_amount - p.change_amount as paid_amount, p.customer, p.is_return {select_mop_field} FROM `tabPOS Invoice` p {from_sales_invoice_payment} From e5ebbf479900e4fded66288ad5d1cfd024ade70c Mon Sep 17 00:00:00 2001 From: maharshivpatel <39730881+maharshivpatel@users.noreply.github.com> Date: Mon, 9 May 2022 18:20:20 +0530 Subject: [PATCH 14/27] fix(india): re-arrange e-way bill dialog fields (#30920) --- erpnext/regional/india/e_invoice/einvoice.js | 130 ++++++++++--------- 1 file changed, 72 insertions(+), 58 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 763e65784a..c4b27a5d63 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -211,86 +211,100 @@ erpnext.setup_einvoice_actions = (doctype) => { const get_ewaybill_fields = (frm) => { return [ { - 'fieldname': 'transporter', - 'label': 'Transporter', - 'fieldtype': 'Link', - 'options': 'Supplier', - 'default': frm.doc.transporter + fieldname: "eway_part_a_section_break", + fieldtype: "Section Break", + label: "Part A", }, { - 'fieldname': 'gst_transporter_id', - 'label': 'GST Transporter ID', - 'fieldtype': 'Data', - 'default': frm.doc.gst_transporter_id + fieldname: "transporter", + label: "Transporter", + fieldtype: "Link", + options: "Supplier", + default: frm.doc.transporter, }, { - 'fieldname': 'driver', - 'label': 'Driver', - 'fieldtype': 'Link', - 'options': 'Driver', - 'default': frm.doc.driver + fieldname: "transporter_name", + label: "Transporter Name", + fieldtype: "Data", + read_only: 1, + default: frm.doc.transporter_name, + depends_on: "transporter", }, { - 'fieldname': 'lr_no', - 'label': 'Transport Receipt No', - 'fieldtype': 'Data', - 'default': frm.doc.lr_no + fieldname: "part_a_column_break", + fieldtype: "Column Break", }, { - 'fieldname': 'vehicle_no', - 'label': 'Vehicle No', - 'fieldtype': 'Data', - 'default': frm.doc.vehicle_no + fieldname: "gst_transporter_id", + label: "GST Transporter ID", + fieldtype: "Data", + default: frm.doc.gst_transporter_id, }, { - 'fieldname': 'distance', - 'label': 'Distance (in km)', - 'fieldtype': 'Float', - 'default': frm.doc.distance, - 'description': 'Set as zero to auto calculate distance using pin codes', + fieldname: "distance", + label: "Distance (in km)", + fieldtype: "Float", + default: frm.doc.distance, + description: 'Set as zero to auto calculate distance using pin codes', }, { - 'fieldname': 'transporter_col_break', - 'fieldtype': 'Column Break', + fieldname: "eway_part_b_section_break", + fieldtype: "Section Break", + label: "Part B", }, { - 'fieldname': 'transporter_name', - 'label': 'Transporter Name', - 'fieldtype': 'Data', - 'read_only': 1, - 'default': frm.doc.transporter_name, - 'depends_on': 'transporter' + fieldname: "mode_of_transport", + label: "Mode of Transport", + fieldtype: "Select", + options: `\nRoad\nAir\nRail\nShip`, + default: frm.doc.mode_of_transport, }, { - 'fieldname': 'mode_of_transport', - 'label': 'Mode of Transport', - 'fieldtype': 'Select', - 'options': `\nRoad\nAir\nRail\nShip`, - 'default': frm.doc.mode_of_transport + fieldname: "gst_vehicle_type", + label: "GST Vehicle Type", + fieldtype: "Select", + options: `Regular\nOver Dimensional Cargo (ODC)`, + depends_on: 'eval:(doc.mode_of_transport === "Road")', + default: frm.doc.gst_vehicle_type, }, { - 'fieldname': 'driver_name', - 'label': 'Driver Name', - 'fieldtype': 'Data', - 'fetch_from': 'driver.full_name', - 'read_only': 1, - 'default': frm.doc.driver_name, - 'depends_on': 'driver' + fieldname: "vehicle_no", + label: "Vehicle No", + fieldtype: "Data", + default: frm.doc.vehicle_no, }, { - 'fieldname': 'lr_date', - 'label': 'Transport Receipt Date', - 'fieldtype': 'Date', - 'default': frm.doc.lr_date + fieldname: "part_b_column_break", + fieldtype: "Column Break", }, { - 'fieldname': 'gst_vehicle_type', - 'label': 'GST Vehicle Type', - 'fieldtype': 'Select', - 'options': `Regular\nOver Dimensional Cargo (ODC)`, - 'depends_on': 'eval:(doc.mode_of_transport === "Road")', - 'default': frm.doc.gst_vehicle_type - } + fieldname: "lr_date", + label: "Transport Receipt Date", + fieldtype: "Date", + default: frm.doc.lr_date, + }, + { + fieldname: "lr_no", + label: "Transport Receipt No", + fieldtype: "Data", + default: frm.doc.lr_no, + }, + { + fieldname: "driver", + label: "Driver", + fieldtype: "Link", + options: "Driver", + default: frm.doc.driver, + }, + { + fieldname: "driver_name", + label: "Driver Name", + fieldtype: "Data", + fetch_from: "driver.full_name", + read_only: 1, + default: frm.doc.driver_name, + depends_on: "driver", + }, ]; }; From 577df1753e3fc9a6d7ab89f71d9fe8a462fc78ad Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 27 Aug 2020 20:34:51 +0530 Subject: [PATCH 15/27] fix: Unlink and delete batch created from stock reco on cancel (cherry picked from commit fc353231065f1c33890d5b7e55c2e4e2506d986d) --- .../stock/doctype/stock_reconciliation/stock_reconciliation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 5d5a27f1ea..cb9aec2563 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -62,6 +62,7 @@ class StockReconciliation(StockController): self.make_sle_on_cancel() self.make_gl_entries_on_cancel() self.repost_future_sle_and_gle() + self.delete_auto_created_batches() def remove_items_with_no_change(self): """Remove items if qty or rate is not changed""" From 3bc3cf34eb47a13437754da6b1d19dd83ad5fc16 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 2 Sep 2020 15:45:02 +0530 Subject: [PATCH 16/27] chore: Tests for Stock Entry (cherry picked from commit a144548db95ec829b953ca6305fe898b1a9cdeb0) --- .../doctype/stock_entry/test_stock_entry.py | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 3ccd3420e3..60b0f3867c 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -652,6 +652,100 @@ class TestStockEntry(FrappeTestCase): serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] self.assertFalse(frappe.db.get_value("Serial No", serial_no, "warehouse")) + def test_serial_batch_item_stock_entry(self): + """ + Behaviour: 1) Submit Stock Entry (Receipt) with Serial & Batched Item + 2) Cancel same Stock Entry + Expected Result: 1) Batch is created with Reference in Serial No + 2) Batch is deleted and Serial No is Inactive + """ + from erpnext.stock.doctype.batch.batch import get_batch_qty + + item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'}) + if not item: + item = create_item("Batched and Serialised Item") + item.has_batch_no = 1 + item.create_new_batch = 1 + item.has_serial_no = 1 + item.batch_number_series = "B-BATCH-.##" + item.serial_no_series = "S-.####" + item.save() + else: + item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'}) + + se = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100) + batch_no = se.items[0].batch_no + serial_no = get_serial_nos(se.items[0].serial_no)[0] + batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code) + + batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no") + self.assertEqual(batch_in_serial_no, batch_no) + + self.assertEqual(batch_qty, 1) + + se.cancel() + + batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no") + self.assertEqual(batch_in_serial_no, None) + + self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Inactive") + self.assertEqual(frappe.db.exists("Batch", batch_no), None) + + def test_serial_batch_item_qty_deduction(self): + """ + Behaviour: Create 2 Stock Entries, both adding Serial Nos to same batch + Expected Result: 1) Cancelling first Stock Entry (origin transaction of created batch) + should throw a Link Exists Error + 2) Cancelling second Stock Entry should make Serial Nos that are, linked to mentioned batch + and in that transaction only, Inactive. + """ + from erpnext.stock.doctype.batch.batch import get_batch_qty + + item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'}) + if not item: + item = create_item("Batched and Serialised Item") + item.has_batch_no = 1 + item.create_new_batch = 1 + item.has_serial_no = 1 + item.batch_number_series = "B-BATCH-.##" + item.serial_no_series = "S-.####" + item.save() + else: + item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'}) + + se1 = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100) + batch_no = se1.items[0].batch_no + serial_no1 = get_serial_nos(se1.items[0].serial_no)[0] + + # Check Source (Origin) Document of Batch + self.assertEqual(frappe.db.get_value("Batch", batch_no, "reference_name"), se1.name) + + se2 = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100, + batch_no=batch_no) + serial_no2 = get_serial_nos(se2.items[0].serial_no)[0] + + batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code) + self.assertEqual(batch_qty, 2) + frappe.db.commit() + + # Cancelling Origin Document + self.assertRaises(frappe.LinkExistsError, se1.cancel) + frappe.db.rollback() + + se2.cancel() + + # Check decrease in Batch Qty + batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code) + self.assertEqual(batch_qty, 1) + + # Check if Serial No from Stock Entry 1 is intact + self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "batch_no"), batch_no) + self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "status"), "Active") + + # Check id Serial No from Stock Entry 2 is Unlinked and Inactive + self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "batch_no"), None) + self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "status"), "Inactive") + def test_warehouse_company_validation(self): company = frappe.db.get_value("Warehouse", "_Test Warehouse 2 - _TC1", "company") frappe.get_doc("User", "test2@example.com").add_roles( From 14ea40d270e1522ca86f55f764041b0b85b85aaa Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 2 Sep 2020 19:21:16 +0530 Subject: [PATCH 17/27] chore: Tests for Stock Reconciliation (cherry picked from commit 5bc5af1066e0bb4bead98598ed4bf9911d340734) --- .../doctype/stock_entry/test_stock_entry.py | 47 ++++--- .../test_stock_reconciliation.py | 115 +++++++++++++++++- 2 files changed, 137 insertions(+), 25 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 60b0f3867c..09f6b7bc01 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -654,14 +654,14 @@ class TestStockEntry(FrappeTestCase): def test_serial_batch_item_stock_entry(self): """ - Behaviour: 1) Submit Stock Entry (Receipt) with Serial & Batched Item - 2) Cancel same Stock Entry - Expected Result: 1) Batch is created with Reference in Serial No - 2) Batch is deleted and Serial No is Inactive + Behaviour: 1) Submit Stock Entry (Receipt) with Serial & Batched Item + 2) Cancel same Stock Entry + Expected Result: 1) Batch is created with Reference in Serial No + 2) Batch is deleted and Serial No is Inactive """ from erpnext.stock.doctype.batch.batch import get_batch_qty - item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'}) + item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"}) if not item: item = create_item("Batched and Serialised Item") item.has_batch_no = 1 @@ -671,9 +671,11 @@ class TestStockEntry(FrappeTestCase): item.serial_no_series = "S-.####" item.save() else: - item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'}) + item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"}) - se = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100) + se = make_stock_entry( + item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100 + ) batch_no = se.items[0].batch_no serial_no = get_serial_nos(se.items[0].serial_no)[0] batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code) @@ -693,15 +695,15 @@ class TestStockEntry(FrappeTestCase): def test_serial_batch_item_qty_deduction(self): """ - Behaviour: Create 2 Stock Entries, both adding Serial Nos to same batch - Expected Result: 1) Cancelling first Stock Entry (origin transaction of created batch) - should throw a Link Exists Error - 2) Cancelling second Stock Entry should make Serial Nos that are, linked to mentioned batch - and in that transaction only, Inactive. + Behaviour: Create 2 Stock Entries, both adding Serial Nos to same batch + Expected Result: 1) Cancelling first Stock Entry (origin transaction of created batch) + should throw a LinkExistsError + 2) Cancelling second Stock Entry should make Serial Nos that are, linked to mentioned batch + and in that transaction only, Inactive. """ from erpnext.stock.doctype.batch.batch import get_batch_qty - item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'}) + item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"}) if not item: item = create_item("Batched and Serialised Item") item.has_batch_no = 1 @@ -711,24 +713,31 @@ class TestStockEntry(FrappeTestCase): item.serial_no_series = "S-.####" item.save() else: - item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'}) + item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"}) - se1 = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100) + se1 = make_stock_entry( + item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100 + ) batch_no = se1.items[0].batch_no serial_no1 = get_serial_nos(se1.items[0].serial_no)[0] # Check Source (Origin) Document of Batch self.assertEqual(frappe.db.get_value("Batch", batch_no, "reference_name"), se1.name) - se2 = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100, - batch_no=batch_no) + se2 = make_stock_entry( + item_code=item.item_code, + target="_Test Warehouse - _TC", + qty=1, + basic_rate=100, + batch_no=batch_no, + ) serial_no2 = get_serial_nos(se2.items[0].serial_no)[0] batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code) self.assertEqual(batch_qty, 2) frappe.db.commit() - # Cancelling Origin Document + # Cancelling Origin Document of Batch self.assertRaises(frappe.LinkExistsError, se1.cancel) frappe.db.rollback() @@ -742,7 +751,7 @@ class TestStockEntry(FrappeTestCase): self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "batch_no"), batch_no) self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "status"), "Active") - # Check id Serial No from Stock Entry 2 is Unlinked and Inactive + # Check if Serial No from Stock Entry 2 is Unlinked and Inactive self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "batch_no"), None) self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "status"), "Inactive") diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 1e59aae9a8..02acdb3480 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -250,7 +250,7 @@ class TestStockReconciliation(FrappeTestCase): warehouse = "_Test Warehouse for Stock Reco2 - _TC" sr = create_stock_reconciliation( - item_code=item_code, warehouse=warehouse, qty=5, rate=200, do_not_submit=1 + item_code=item_code, warehouse=warehouse, qty=5, rate=200, do_not_save=1 ) sr.save() sr.submit() @@ -288,6 +288,107 @@ class TestStockReconciliation(FrappeTestCase): stock_doc = frappe.get_doc("Stock Reconciliation", d) stock_doc.cancel() + def test_stock_reco_for_serial_and_batch_item(self): + item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"}) + if not item: + item = create_item("Batched and Serialised Item") + item.has_batch_no = 1 + item.create_new_batch = 1 + item.has_serial_no = 1 + item.batch_number_series = "B-BATCH-.##" + item.serial_no_series = "S-.####" + item.save() + else: + item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"}) + + warehouse = "_Test Warehouse for Stock Reco2 - _TC" + + sr = create_stock_reconciliation(item_code=item.item_code, warehouse=warehouse, qty=1, rate=100) + + batch_no = sr.items[0].batch_no + + serial_nos = get_serial_nos(sr.items[0].serial_no) + self.assertEqual(len(serial_nos), 1) + self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "batch_no"), batch_no) + + sr.cancel() + + self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "status"), "Inactive") + self.assertEqual(frappe.db.exists("Batch", batch_no), None) + + if frappe.db.exists("Serial No", serial_nos[0]): + frappe.delete_doc("Serial No", serial_nos[0]) + + def test_stock_reco_for_serial_and_batch_item_with_future_dependent_entry(self): + """ + Behaviour: 1) Create Stock Reconciliation, which will be the origin document + of a new batch having a serial no + 2) Create a Stock Entry that adds a serial no to the same batch following this + Stock Reconciliation + 3) Cancel Stock Reconciliation + 4) Cancel Stock Entry + Expected Result: 3) Cancelling the Stock Reco throws a LinkExistsError since + Stock Entry is dependent on the batch involved + 4) Serial No only in the Stock Entry is Inactive and Batch qty decreases + """ + from erpnext.stock.doctype.batch.batch import get_batch_qty + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"}) + if not item: + item = create_item("Batched and Serialised Item") + item.has_batch_no = 1 + item.create_new_batch = 1 + item.has_serial_no = 1 + item.batch_number_series = "B-BATCH-.##" + item.serial_no_series = "S-.####" + item.save() + else: + item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"}) + + warehouse = "_Test Warehouse for Stock Reco2 - _TC" + + stock_reco = create_stock_reconciliation( + item_code=item.item_code, warehouse=warehouse, qty=1, rate=100 + ) + batch_no = stock_reco.items[0].batch_no + serial_no = get_serial_nos(stock_reco.items[0].serial_no)[0] + + stock_entry = make_stock_entry( + item_code=item.item_code, target=warehouse, qty=1, basic_rate=100, batch_no=batch_no + ) + serial_no_2 = get_serial_nos(stock_entry.items[0].serial_no)[0] + + # Check Batch qty after 2 transactions + batch_qty = get_batch_qty(batch_no, warehouse, item.item_code) + self.assertEqual(batch_qty, 2) + frappe.db.commit() + + # Cancelling Origin Document of Batch + self.assertRaises(frappe.LinkExistsError, stock_reco.cancel) + frappe.db.rollback() + + stock_entry.cancel() + + # Check Batch qty after cancellation + batch_qty = get_batch_qty(batch_no, warehouse, item.item_code) + self.assertEqual(batch_qty, 1) + + # Check if Serial No from Stock Reconcilation is intact + self.assertEqual(frappe.db.get_value("Serial No", serial_no, "batch_no"), batch_no) + self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Active") + + # Check if Serial No from Stock Entry is Unlinked and Inactive + self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "batch_no"), None) + self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "status"), "Inactive") + + stock_reco.load_from_db() + stock_reco.cancel() + + for sn in (serial_no, serial_no_2): + if frappe.db.exists("Serial No", sn): + frappe.delete_doc("Serial No", sn) + def test_customer_provided_items(self): item_code = "Stock-Reco-customer-Item-100" create_item( @@ -684,11 +785,13 @@ def create_stock_reconciliation(**args): }, ) - try: - if not args.do_not_submit: - sr.submit() - except EmptyStockReconciliationItemsError: - pass + if not args.do_not_save: + sr.insert() + try: + if not args.do_not_submit: + sr.submit() + except EmptyStockReconciliationItemsError: + pass return sr From de3d90c5ab8866cf3e2fd5b77779580346a0156b Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 3 Sep 2020 14:41:05 +0530 Subject: [PATCH 18/27] fix: Item Alternative Test (cherry picked from commit 964de1fc69b32d377f8c2a6f15d6ab7381669606) --- .../item_alternative/test_item_alternative.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py index 32c58c5ae1..b8f4803c26 100644 --- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py @@ -16,6 +16,9 @@ from erpnext.manufacturing.doctype.production_plan.test_production_plan import m from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( + EmptyStockReconciliationItemsError, +) from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, ) @@ -180,9 +183,12 @@ def make_items(): if not frappe.db.exists("Item", item_code): create_item(item_code) - create_stock_reconciliation( - item_code="Test FG A RW 1", warehouse="_Test Warehouse - _TC", qty=10, rate=2000 - ) + try: + create_stock_reconciliation( + item_code="Test FG A RW 1", warehouse="_Test Warehouse - _TC", qty=10, rate=2000 + ) + except EmptyStockReconciliationItemsError: + pass if frappe.db.exists("Item", "Test FG A RW 1"): doc = frappe.get_doc("Item", "Test FG A RW 1") From 64f3b4d68bc3872d7fa49435e8d27ad2ea2d3b4e Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 3 May 2022 13:00:00 +0530 Subject: [PATCH 19/27] fix: Wrap SLE actual_qty in `flt` to avoid NoneType operation - Since Batch cancellation SLEs do not set qtys (will fix separately), `merge_similar_entries` gets `actual_qty` as None - This causes NoneType operation error on tests that cancel batch-serial reco - Modified tests to avoid using commit and rollback explicitly (cherry picked from commit d53228b153437f9dedcb1229bf579411f3122729) --- .../doctype/stock_entry/test_stock_entry.py | 12 ++-- .../stock_reconciliation.py | 2 +- .../test_stock_reconciliation.py | 67 ++++++------------- 3 files changed, 29 insertions(+), 52 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 09f6b7bc01..7dae283ac4 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -655,9 +655,9 @@ class TestStockEntry(FrappeTestCase): def test_serial_batch_item_stock_entry(self): """ Behaviour: 1) Submit Stock Entry (Receipt) with Serial & Batched Item - 2) Cancel same Stock Entry + 2) Cancel same Stock Entry Expected Result: 1) Batch is created with Reference in Serial No - 2) Batch is deleted and Serial No is Inactive + 2) Batch is deleted and Serial No is Inactive """ from erpnext.stock.doctype.batch.batch import get_batch_qty @@ -696,10 +696,10 @@ class TestStockEntry(FrappeTestCase): def test_serial_batch_item_qty_deduction(self): """ Behaviour: Create 2 Stock Entries, both adding Serial Nos to same batch - Expected Result: 1) Cancelling first Stock Entry (origin transaction of created batch) - should throw a LinkExistsError - 2) Cancelling second Stock Entry should make Serial Nos that are, linked to mentioned batch - and in that transaction only, Inactive. + Expected: 1) Cancelling first Stock Entry (origin transaction of created batch) + should throw a LinkExistsError + 2) Cancelling second Stock Entry should make Serial Nos that are, linked to mentioned batch + and in that transaction only, Inactive. """ from erpnext.stock.doctype.batch.batch import get_batch_qty diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index cb9aec2563..bd60cf0a5a 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -457,7 +457,7 @@ class StockReconciliation(StockController): key = (d.item_code, d.warehouse) if key not in merge_similar_entries: - d.total_amount = d.actual_qty * d.valuation_rate + d.total_amount = flt(d.actual_qty) * d.valuation_rate merge_similar_entries[key] = d elif d.serial_no: data = merge_similar_entries[key] diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 02acdb3480..6d5340ef11 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -289,17 +289,13 @@ class TestStockReconciliation(FrappeTestCase): stock_doc.cancel() def test_stock_reco_for_serial_and_batch_item(self): - item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"}) - if not item: - item = create_item("Batched and Serialised Item") - item.has_batch_no = 1 - item.create_new_batch = 1 - item.has_serial_no = 1 - item.batch_number_series = "B-BATCH-.##" - item.serial_no_series = "S-.####" - item.save() - else: - item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"}) + item = create_item("_TestBatchSerialItemReco") + item.has_batch_no = 1 + item.create_new_batch = 1 + item.has_serial_no = 1 + item.batch_number_series = "TBS-BATCH-.##" + item.serial_no_series = "TBS-.####" + item.save() warehouse = "_Test Warehouse for Stock Reco2 - _TC" @@ -316,35 +312,25 @@ class TestStockReconciliation(FrappeTestCase): self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "status"), "Inactive") self.assertEqual(frappe.db.exists("Batch", batch_no), None) - if frappe.db.exists("Serial No", serial_nos[0]): - frappe.delete_doc("Serial No", serial_nos[0]) - def test_stock_reco_for_serial_and_batch_item_with_future_dependent_entry(self): """ Behaviour: 1) Create Stock Reconciliation, which will be the origin document - of a new batch having a serial no - 2) Create a Stock Entry that adds a serial no to the same batch following this + of a new batch having a serial no + 2) Create a Stock Entry that adds a serial no to the same batch following this Stock Reconciliation - 3) Cancel Stock Reconciliation - 4) Cancel Stock Entry - Expected Result: 3) Cancelling the Stock Reco throws a LinkExistsError since - Stock Entry is dependent on the batch involved - 4) Serial No only in the Stock Entry is Inactive and Batch qty decreases + 3) Cancel Stock Entry + Expected Result: 3) Serial No only in the Stock Entry is Inactive and Batch qty decreases """ from erpnext.stock.doctype.batch.batch import get_batch_qty from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry - item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"}) - if not item: - item = create_item("Batched and Serialised Item") - item.has_batch_no = 1 - item.create_new_batch = 1 - item.has_serial_no = 1 - item.batch_number_series = "B-BATCH-.##" - item.serial_no_series = "S-.####" - item.save() - else: - item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"}) + item = create_item("_TestBatchSerialItemDependentReco") + item.has_batch_no = 1 + item.create_new_batch = 1 + item.has_serial_no = 1 + item.batch_number_series = "TBSD-BATCH-.##" + item.serial_no_series = "TBSD-.####" + item.save() warehouse = "_Test Warehouse for Stock Reco2 - _TC" @@ -352,7 +338,7 @@ class TestStockReconciliation(FrappeTestCase): item_code=item.item_code, warehouse=warehouse, qty=1, rate=100 ) batch_no = stock_reco.items[0].batch_no - serial_no = get_serial_nos(stock_reco.items[0].serial_no)[0] + reco_serial_no = get_serial_nos(stock_reco.items[0].serial_no)[0] stock_entry = make_stock_entry( item_code=item.item_code, target=warehouse, qty=1, basic_rate=100, batch_no=batch_no @@ -362,12 +348,8 @@ class TestStockReconciliation(FrappeTestCase): # Check Batch qty after 2 transactions batch_qty = get_batch_qty(batch_no, warehouse, item.item_code) self.assertEqual(batch_qty, 2) - frappe.db.commit() - - # Cancelling Origin Document of Batch - self.assertRaises(frappe.LinkExistsError, stock_reco.cancel) - frappe.db.rollback() + # Cancel latest stock document stock_entry.cancel() # Check Batch qty after cancellation @@ -375,20 +357,15 @@ class TestStockReconciliation(FrappeTestCase): self.assertEqual(batch_qty, 1) # Check if Serial No from Stock Reconcilation is intact - self.assertEqual(frappe.db.get_value("Serial No", serial_no, "batch_no"), batch_no) - self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Active") + self.assertEqual(frappe.db.get_value("Serial No", reco_serial_no, "batch_no"), batch_no) + self.assertEqual(frappe.db.get_value("Serial No", reco_serial_no, "status"), "Active") # Check if Serial No from Stock Entry is Unlinked and Inactive self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "batch_no"), None) self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "status"), "Inactive") - stock_reco.load_from_db() stock_reco.cancel() - for sn in (serial_no, serial_no_2): - if frappe.db.exists("Serial No", sn): - frappe.delete_doc("Serial No", sn) - def test_customer_provided_items(self): item_code = "Stock-Reco-customer-Item-100" create_item( From 9fd673e498e4204176bcde10dedba40834b82f5c Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 4 May 2022 13:00:00 +0530 Subject: [PATCH 20/27] fix: Remove commit from stock entry test. The assertion is not important (cherry picked from commit c449b35cc11615fc7d9e616c2038b9e90c3957d6) --- erpnext/stock/doctype/stock_entry/test_stock_entry.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 7dae283ac4..8745ced575 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -735,11 +735,6 @@ class TestStockEntry(FrappeTestCase): batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code) self.assertEqual(batch_qty, 2) - frappe.db.commit() - - # Cancelling Origin Document of Batch - self.assertRaises(frappe.LinkExistsError, se1.cancel) - frappe.db.rollback() se2.cancel() From a65b20d97629f5a05d257346f86575dca1c62027 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 5 May 2022 13:00:00 +0530 Subject: [PATCH 21/27] style: Spaces to Tabs (cherry picked from commit a2fff8741e7ea928c4152d8f867db3ef9eb08b19) --- erpnext/stock/doctype/stock_entry/test_stock_entry.py | 10 +++++----- .../stock_reconciliation/test_stock_reconciliation.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 8745ced575..b9c57c1ecf 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -655,9 +655,9 @@ class TestStockEntry(FrappeTestCase): def test_serial_batch_item_stock_entry(self): """ Behaviour: 1) Submit Stock Entry (Receipt) with Serial & Batched Item - 2) Cancel same Stock Entry + 2) Cancel same Stock Entry Expected Result: 1) Batch is created with Reference in Serial No - 2) Batch is deleted and Serial No is Inactive + 2) Batch is deleted and Serial No is Inactive """ from erpnext.stock.doctype.batch.batch import get_batch_qty @@ -697,9 +697,9 @@ class TestStockEntry(FrappeTestCase): """ Behaviour: Create 2 Stock Entries, both adding Serial Nos to same batch Expected: 1) Cancelling first Stock Entry (origin transaction of created batch) - should throw a LinkExistsError - 2) Cancelling second Stock Entry should make Serial Nos that are, linked to mentioned batch - and in that transaction only, Inactive. + should throw a LinkExistsError + 2) Cancelling second Stock Entry should make Serial Nos that are, linked to mentioned batch + and in that transaction only, Inactive. """ from erpnext.stock.doctype.batch.batch import get_batch_qty diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 6d5340ef11..e7b89b18a8 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -315,10 +315,10 @@ class TestStockReconciliation(FrappeTestCase): def test_stock_reco_for_serial_and_batch_item_with_future_dependent_entry(self): """ Behaviour: 1) Create Stock Reconciliation, which will be the origin document - of a new batch having a serial no - 2) Create a Stock Entry that adds a serial no to the same batch following this - Stock Reconciliation - 3) Cancel Stock Entry + of a new batch having a serial no + 2) Create a Stock Entry that adds a serial no to the same batch following this + Stock Reconciliation + 3) Cancel Stock Entry Expected Result: 3) Serial No only in the Stock Entry is Inactive and Batch qty decreases """ from erpnext.stock.doctype.batch.batch import get_batch_qty From 05dd1d6d15c6c8c66165e9f267078c3cf9aec10e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 9 May 2022 19:26:13 +0530 Subject: [PATCH 22/27] refactor: tax rule validity query (#30934) --- erpnext/accounts/doctype/tax_rule/tax_rule.py | 16 +++++++--------- .../shopping_cart/test_shopping_cart.py | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.py b/erpnext/accounts/doctype/tax_rule/tax_rule.py index 5bfca96bb1..4d201292ed 100644 --- a/erpnext/accounts/doctype/tax_rule/tax_rule.py +++ b/erpnext/accounts/doctype/tax_rule/tax_rule.py @@ -163,17 +163,15 @@ def get_party_details(party, party_type, args=None): def get_tax_template(posting_date, args): """Get matching tax rule""" args = frappe._dict(args) - from_date = to_date = posting_date - if not posting_date: - from_date = "1900-01-01" - to_date = "4000-01-01" + conditions = [] - conditions = [ - """(from_date is null or from_date <= '{0}') - and (to_date is null or to_date >= '{1}')""".format( - from_date, to_date + if posting_date: + conditions.append( + f"""(from_date is null or from_date <= '{posting_date}') + and (to_date is null or to_date >= '{posting_date}')""" ) - ] + else: + conditions.append("(from_date is null) and (to_date is null)") conditions.append( "ifnull(tax_category, '') = {0}".format(frappe.db.escape(cstr(args.get("tax_category")))) diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py index 437ebeac06..30d9ffcc4b 100644 --- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py +++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py @@ -139,7 +139,7 @@ class TestShoppingCart(unittest.TestCase): tax_rule_master = set_taxes( quotation.party_name, "Customer", - quotation.transaction_date, + None, quotation.company, customer_group=None, supplier_group=None, From 51fcbe58267ed164e3b632ed95e078c9610d4315 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 9 May 2022 21:29:58 +0530 Subject: [PATCH 23/27] feat(HR): Leave Type configuration to allow over allocation (#30940) --- .../leave_allocation/leave_allocation.py | 13 +++++++++- .../leave_allocation/test_leave_allocation.py | 24 ++++++++++++++++++- erpnext/hr/doctype/leave_type/leave_type.json | 12 +++++++++- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index 27479a5e81..8fae2a9a88 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -254,7 +254,18 @@ class LeaveAllocation(Document): # Adding a day to include To Date in the difference date_difference = date_diff(self.to_date, self.from_date) + 1 if date_difference < self.total_leaves_allocated: - frappe.throw(_("Total allocated leaves are more than days in the period"), OverAllocationError) + if frappe.db.get_value("Leave Type", self.leave_type, "allow_over_allocation"): + frappe.msgprint( + _("Total Leaves Allocated are more than the number of days in the allocation period"), + indicator="orange", + alert=True, + ) + else: + frappe.throw( + _("Total Leaves Allocated are more than the number of days in the allocation period"), + exc=OverAllocationError, + title=_("Over Allocation"), + ) def create_leave_ledger_entry(self, submit=True): if self.unused_leaves: diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py index a1d39d4423..b4a42d375c 100644 --- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py @@ -69,22 +69,44 @@ class TestLeaveAllocation(FrappeTestCase): self.assertRaises(frappe.ValidationError, doc.save) def test_validation_for_over_allocation(self): + leave_type = create_leave_type(leave_type_name="Test Over Allocation", is_carry_forward=1) + leave_type.save() + doc = frappe.get_doc( { "doctype": "Leave Allocation", "__islocal": 1, "employee": self.employee.name, "employee_name": self.employee.employee_name, - "leave_type": "_Test Leave Type", + "leave_type": leave_type.name, "from_date": getdate("2015-09-1"), "to_date": getdate("2015-09-30"), "new_leaves_allocated": 35, + "carry_forward": 1, } ) # allocated leave more than period self.assertRaises(OverAllocationError, doc.save) + leave_type.allow_over_allocation = 1 + leave_type.save() + + # allows creating a leave allocation with more leave days than period days + doc = frappe.get_doc( + { + "doctype": "Leave Allocation", + "__islocal": 1, + "employee": self.employee.name, + "employee_name": self.employee.employee_name, + "leave_type": leave_type.name, + "from_date": getdate("2015-09-1"), + "to_date": getdate("2015-09-30"), + "new_leaves_allocated": 35, + "carry_forward": 1, + } + ).insert() + def test_validation_for_over_allocation_post_submission(self): allocation = frappe.get_doc( { diff --git a/erpnext/hr/doctype/leave_type/leave_type.json b/erpnext/hr/doctype/leave_type/leave_type.json index 06ca4cdedb..d40ff09619 100644 --- a/erpnext/hr/doctype/leave_type/leave_type.json +++ b/erpnext/hr/doctype/leave_type/leave_type.json @@ -19,6 +19,7 @@ "fraction_of_daily_salary_per_leave", "is_optional_leave", "allow_negative", + "allow_over_allocation", "include_holiday", "is_compensatory", "carry_forward_section", @@ -211,15 +212,23 @@ "fieldtype": "Float", "label": "Fraction of Daily Salary per Leave", "mandatory_depends_on": "eval:doc.is_ppl == 1" + }, + { + "default": "0", + "description": "Allows allocating more leaves than the number of days in the allocation period.", + "fieldname": "allow_over_allocation", + "fieldtype": "Check", + "label": "Allow Over Allocation" } ], "icon": "fa fa-flag", "idx": 1, "links": [], - "modified": "2021-10-02 11:59:40.503359", + "modified": "2022-05-09 05:01:38.957545", "modified_by": "Administrator", "module": "HR", "name": "Leave Type", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -251,5 +260,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From 3f41cb762da0141b0118a51f1b1b56928f403dd1 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 9 May 2022 14:58:37 +0530 Subject: [PATCH 24/27] fix: allow to use formatting for the field to_discuss in opportunity --- erpnext/crm/doctype/opportunity/opportunity.py | 14 +++++++------- .../doctype/opportunity/test_opportunity.py | 18 +++++++++++++++++- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 19b4d68e1c..b590177562 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -9,7 +9,7 @@ from frappe import _ from frappe.email.inbox import link_communication_to_document from frappe.model.mapper import get_mapped_doc from frappe.query_builder import DocType -from frappe.utils import cint, cstr, flt, get_fullname +from frappe.utils import cint, flt, get_fullname from erpnext.crm.utils import add_link_in_communication, copy_comments from erpnext.setup.utils import get_exchange_rate @@ -215,20 +215,20 @@ class Opportunity(TransactionBase): if self.party_name and self.opportunity_from == "Customer": if self.contact_person: - opts.description = "Contact " + cstr(self.contact_person) + opts.description = f"Contact {self.contact_person}" else: - opts.description = "Contact customer " + cstr(self.party_name) + opts.description = f"Contact customer {self.party_name}" elif self.party_name and self.opportunity_from == "Lead": if self.contact_display: - opts.description = "Contact " + cstr(self.contact_display) + opts.description = f"Contact {self.contact_display}" else: - opts.description = "Contact lead " + cstr(self.party_name) + opts.description = f"Contact lead {self.party_name}" opts.subject = opts.description - opts.description += ". By : " + cstr(self.contact_by) + opts.description += f". By : {self.contact_by}" if self.to_discuss: - opts.description += " To Discuss : " + cstr(self.to_discuss) + opts.description += f" To Discuss : {frappe.render_template(self.to_discuss, {'doc': self})}" super(Opportunity, self).add_calendar_event(opts, force) diff --git a/erpnext/crm/doctype/opportunity/test_opportunity.py b/erpnext/crm/doctype/opportunity/test_opportunity.py index 481c7f1e26..4a18e940bc 100644 --- a/erpnext/crm/doctype/opportunity/test_opportunity.py +++ b/erpnext/crm/doctype/opportunity/test_opportunity.py @@ -4,7 +4,7 @@ import unittest import frappe -from frappe.utils import now_datetime, random_string, today +from frappe.utils import add_days, now_datetime, random_string, today from erpnext.crm.doctype.lead.lead import make_customer from erpnext.crm.doctype.lead.test_lead import make_lead @@ -97,6 +97,22 @@ class TestOpportunity(unittest.TestCase): self.assertEqual(quotation_comment_count, 4) self.assertEqual(quotation_communication_count, 4) + def test_render_template_for_to_discuss(self): + doc = make_opportunity(with_items=0, opportunity_from="Lead") + doc.contact_by = "test@example.com" + doc.contact_date = add_days(today(), days=2) + doc.to_discuss = "{{ doc.name }} test data" + doc.save() + + event = frappe.get_all( + "Event Participants", + fields=["parent"], + filters={"reference_doctype": doc.doctype, "reference_docname": doc.name}, + ) + + event_description = frappe.db.get_value("Event", event[0].parent, "description") + self.assertTrue(doc.name in event_description) + def make_opportunity_from_lead(): new_lead_email_id = "new{}@example.com".format(random_string(5)) From cf13a20438bca4094d6ce87f8268d59eddbb1c32 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 10 May 2022 12:07:47 +0530 Subject: [PATCH 25/27] feat(india): generate qrcode button for e-invoice (#30939) --- .../sales_invoice/test_sales_invoice.py | 1 + erpnext/regional/india/e_invoice/einvoice.js | 23 ++++++++ erpnext/regional/india/e_invoice/utils.py | 57 ++++++++++++++++--- 3 files changed, 73 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index f2a696d565..a0c0eccff1 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2648,6 +2648,7 @@ class TestSalesInvoice(unittest.TestCase): # reset einvoice_settings = frappe.get_doc("E Invoice Settings") einvoice_settings.enable = 0 + einvoice_settings.save() frappe.flags.country = country def test_einvoice_json(self): diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index c4b27a5d63..ea56d07d6d 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -204,6 +204,29 @@ erpnext.setup_einvoice_actions = (doctype) => { }; add_custom_button(__("Cancel E-Way Bill"), action); } + + if (irn && !irn_cancelled) { + const action = () => { + const dialog = frappe.msgprint({ + title: __("Generate QRCode"), + message: __("Generate and attach QR Code using IRN?"), + primary_action: { + action: function() { + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.generate_qrcode', + args: { doctype, docname: name }, + freeze: true, + callback: () => frm.reload_doc() || dialog.hide(), + error: () => dialog.hide() + }); + } + }, + primary_action_label: __('Yes') + }); + dialog.show(); + }; + add_custom_button(__("Generate QRCode"), action); + } } }); }; diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 53d3211d3b..417f883a16 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -794,6 +794,7 @@ class GSPConnector: self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin" self.cancel_ewaybill_url = self.base_url + "/enriched/ei/api/ewayapi" self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill" + self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image" def set_invoice(self): self.invoice = None @@ -857,8 +858,8 @@ class GSPConnector: return res def auto_refresh_token(self): - self.fetch_auth_token() self.token_auto_refreshed = True + self.fetch_auth_token() def log_request(self, url, headers, data, res): headers.update({"password": self.credentials.password}) @@ -998,6 +999,37 @@ class GSPConnector: return failed + def fetch_and_attach_qrcode_from_irn(self): + qrcode = self.get_qrcode_from_irn(self.invoice.irn) + if qrcode: + qrcode_file = self.create_qr_code_file(qrcode) + frappe.db.set_value("Sales Invoice", self.invoice.name, "qrcode_image", qrcode_file.file_url) + frappe.msgprint(_("QR Code attached to the invoice"), alert=True) + else: + frappe.msgprint(_("QR Code not found for the IRN"), alert=True) + + def get_qrcode_from_irn(self, irn): + import requests + + headers = self.get_headers() + headers.update({"width": "215", "height": "215", "imgtype": "jpg", "irn": irn}) + + try: + # using requests.get instead of make_request to avoid parsing the response + res = requests.get(self.get_qrcode_url, headers=headers) + self.log_request(self.get_qrcode_url, headers, None, None) + if res.status_code == 200: + return res.content + else: + raise RequestFailed(str(res.content, "utf-8")) + + except RequestFailed as e: + self.raise_error(errors=str(e)) + + except Exception: + log_error() + self.raise_error() + def get_irn_details(self, irn): headers = self.get_headers() @@ -1198,8 +1230,6 @@ class GSPConnector: return errors def raise_error(self, raise_exception=False, errors=None): - if errors is None: - errors = [] title = _("E Invoice Request Failed") if errors: frappe.throw(errors, title=title, as_list=1) @@ -1240,13 +1270,18 @@ class GSPConnector: def attach_qrcode_image(self): qrcode = self.invoice.signed_qr_code - doctype = self.invoice.doctype - docname = self.invoice.name - filename = "QRCode_{}.png".format(docname).replace(os.path.sep, "__") qr_image = io.BytesIO() url = qrcreate(qrcode, error="L") url.png(qr_image, scale=2, quiet_zone=1) + qrcode_file = self.create_qr_code_file(qr_image.getvalue()) + self.invoice.qrcode_image = qrcode_file.file_url + + def create_qr_code_file(self, qr_image): + doctype = self.invoice.doctype + docname = self.invoice.name + filename = "QRCode_{}.png".format(docname).replace(os.path.sep, "__") + _file = frappe.get_doc( { "doctype": "File", @@ -1255,12 +1290,12 @@ class GSPConnector: "attached_to_name": docname, "attached_to_field": "qrcode_image", "is_private": 0, - "content": qr_image.getvalue(), + "content": qr_image, } ) _file.save() frappe.db.commit() - self.invoice.qrcode_image = _file.file_url + return _file def update_invoice(self): self.invoice.flags.ignore_validate_update_after_submit = True @@ -1305,6 +1340,12 @@ def cancel_irn(doctype, docname, irn, reason, remark): gsp_connector.cancel_irn(irn, reason, remark) +@frappe.whitelist() +def generate_qrcode(doctype, docname): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.fetch_and_attach_qrcode_from_irn() + + @frappe.whitelist() def generate_eway_bill(doctype, docname, **kwargs): gsp_connector = GSPConnector(doctype, docname) From 8dd046cc51b79b9c23f915862fbdfd5e1f139670 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 10 May 2022 13:39:41 +0530 Subject: [PATCH 26/27] fix(india): invoice type for a debit note e-invoice (#30948) --- erpnext/regional/india/e_invoice/utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 417f883a16..ed1002a129 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -167,7 +167,12 @@ def get_doc_details(invoice): title=_("Not Allowed"), ) - invoice_type = "CRN" if invoice.is_return else "INV" + if invoice.is_return: + invoice_type = "CRN" + elif invoice.is_debit_note: + invoice_type = "DBN" + else: + invoice_type = "INV" invoice_name = invoice.name invoice_date = format_date(invoice.posting_date, "dd/mm/yyyy") From 7bf0e4f8e562f182bf0fc306552f33dfb33a193e Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 10 May 2022 15:25:08 +0530 Subject: [PATCH 27/27] fix(charts): Pass fieldtype for chart data in selling reports --- .../item_wise_sales_history/item_wise_sales_history.py | 1 + erpnext/selling/report/quotation_trends/quotation_trends.py | 1 + erpnext/selling/report/sales_analytics/sales_analytics.py | 5 +++++ .../selling/report/sales_order_trends/sales_order_trends.py | 1 + 4 files changed, 8 insertions(+) diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py index 091c20c917..e10df2acbb 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py @@ -238,4 +238,5 @@ def get_chart_data(data): "datasets": [{"name": _("Total Sales Amount"), "values": datapoints[:30]}], }, "type": "bar", + "fieldtype": "Currency", } diff --git a/erpnext/selling/report/quotation_trends/quotation_trends.py b/erpnext/selling/report/quotation_trends/quotation_trends.py index 4e0758d7cd..4d71ce77c4 100644 --- a/erpnext/selling/report/quotation_trends/quotation_trends.py +++ b/erpnext/selling/report/quotation_trends/quotation_trends.py @@ -54,4 +54,5 @@ def get_chart_data(data, conditions, filters): }, "type": "line", "lineOptions": {"regionFill": 1}, + "fieldtype": "Currency", } diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index 1a2476a9da..9d7d806c71 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -415,3 +415,8 @@ class Analytics(object): else: labels = [d.get("label") for d in self.columns[1 : length - 1]] self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"} + + if self.filters["value_quantity"] == "Value": + self.chart["fieldtype"] = "Currency" + else: + self.chart["fieldtype"] = "Float" diff --git a/erpnext/selling/report/sales_order_trends/sales_order_trends.py b/erpnext/selling/report/sales_order_trends/sales_order_trends.py index 719f1c5274..18f448c7cd 100644 --- a/erpnext/selling/report/sales_order_trends/sales_order_trends.py +++ b/erpnext/selling/report/sales_order_trends/sales_order_trends.py @@ -51,4 +51,5 @@ def get_chart_data(data, conditions, filters): }, "type": "line", "lineOptions": {"regionFill": 1}, + "fieldtype": "Currency", }