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",
}