From 1bac7930834d6f688950e836c45305a62e7ecb3f Mon Sep 17 00:00:00 2001
From: ruthra
Date: Tue, 4 Jan 2022 15:53:41 +0530
Subject: [PATCH 001/447] feat: Payment Terms Status report
- calculate status at runtime for payment terms based on invoices
- invoices are used in FIFO method
---
.../__init__.py | 0
.../payment_terms_status_for_sales_order.js | 84 +++++++
.../payment_terms_status_for_sales_order.json | 38 ++++
.../payment_terms_status_for_sales_order.py | 211 ++++++++++++++++++
...st_payment_terms_status_for_sales_order.py | 119 ++++++++++
5 files changed, 452 insertions(+)
create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/__init__.py
create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js
create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json
create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/__init__.py b/erpnext/selling/report/payment_terms_status_for_sales_order/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js
new file mode 100644
index 0000000000..0450631a3b
--- /dev/null
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js
@@ -0,0 +1,84 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+function get_filters() {
+ let filters = [
+ {
+ "fieldname":"company",
+ "label": __("Company"),
+ "fieldtype": "Link",
+ "options": "Company",
+ "default": frappe.defaults.get_user_default("Company"),
+ "reqd": 1
+ },
+ {
+ "fieldname":"period_start_date",
+ "label": __("Start Date"),
+ "fieldtype": "Date",
+ "reqd": 1,
+ "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1)
+ },
+ {
+ "fieldname":"period_end_date",
+ "label": __("End Date"),
+ "fieldtype": "Date",
+ "reqd": 1,
+ "default": frappe.datetime.get_today()
+ },
+ {
+ "fieldname":"sales_order",
+ "label": __("Sales Order"),
+ "fieldtype": "MultiSelectList",
+ "width": 100,
+ "options": "Sales Order",
+ "get_data": function(txt) {
+ return frappe.db.get_link_options("Sales Order", txt, this.filters());
+ },
+ "filters": () => {
+ return {
+ docstatus: 1,
+ payment_terms_template: ['not in', ['']],
+ company: frappe.query_report.get_filter_value("company"),
+ transaction_date: ['between', [frappe.query_report.get_filter_value("period_start_date"), frappe.query_report.get_filter_value("period_end_date")]]
+ }
+ },
+ on_change: function(){
+ frappe.query_report.refresh();
+ }
+ }
+ ]
+
+ return filters;
+}
+
+frappe.query_reports["Payment Terms Status for Sales Order"] = {
+ "filters": get_filters(),
+ "formatter": function(value, row, column, data, default_formatter){
+ if(column.fieldname == 'invoices' && value) {
+ invoices = value.split(',');
+ const invoice_formatter = (prev_value, curr_value) => {
+ if(prev_value != "") {
+ return prev_value + ", " + default_formatter(curr_value, row, column, data);
+ }
+ else {
+ return default_formatter(curr_value, row, column, data);
+ }
+ }
+ return invoices.reduce(invoice_formatter, "")
+ }
+ else if (column.fieldname == 'paid_amount' && value){
+ formatted_value = default_formatter(value, row, column, data);
+ if(value > 0) {
+ formatted_value = "" + formatted_value + " "
+ }
+ return formatted_value;
+ }
+ else if (column.fieldname == 'status' && value == 'Completed'){
+ return "" + default_formatter(value, row, column, data) + " ";
+ }
+
+ return default_formatter(value, row, column, data);
+ },
+
+};
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json
new file mode 100644
index 0000000000..850fa4dc47
--- /dev/null
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json
@@ -0,0 +1,38 @@
+{
+ "add_total_row": 1,
+ "columns": [],
+ "creation": "2021-12-28 10:39:34.533964",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-12-30 10:42:06.058457",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "Payment Terms Status for Sales Order",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Sales Order",
+ "report_name": "Payment Terms Status for Sales Order",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Sales User"
+ },
+ {
+ "role": "Sales Manager"
+ },
+ {
+ "role": "Maintenance User"
+ },
+ {
+ "role": "Accounts User"
+ },
+ {
+ "role": "Stock User"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
new file mode 100644
index 0000000000..aa2f757218
--- /dev/null
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
@@ -0,0 +1,211 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# License: MIT. See LICENSE
+
+import frappe
+from frappe import _, qb, query_builder
+from frappe.query_builder import functions
+
+
+def get_columns():
+ columns = [
+ {
+ "label": _("Sales Order"),
+ "fieldname": "name",
+ "fieldtype": "Link",
+ "options": "Sales Order",
+ "read_only": 1,
+ },
+ {
+ "label": _("Submitted"),
+ "fieldname": "submitted",
+ "fieldtype": "Date",
+ "read_only": 1
+ },
+ {
+ "label": _("Payment Term"),
+ "fieldname": "payment_term",
+ "fieldtype": "Data",
+ "read_only": 1
+ },
+ {
+ "label": _("Description"),
+ "fieldname": "description",
+ "fieldtype": "Data",
+ "read_only": 1
+ },
+ {
+ "label": _("Due Date"),
+ "fieldname": "due_date",
+ "fieldtype": "Date",
+ "read_only": 1
+ },
+ {
+ "label": _("Invoice Portion"),
+ "fieldname": "invoice_portion",
+ "fieldtype": "Percent",
+ "read_only": 1,
+ },
+ {
+ "label": _("Payment Amount"),
+ "fieldname": "payment_amount",
+ "fieldtype": "Currency",
+ "read_only": 1,
+ },
+ {
+ "label": _("Paid Amount"),
+ "fieldname": "paid_amount",
+ "fieldtype": "Currency",
+ "read_only": 1
+ },
+ {
+ "label": _("Invoices"),
+ "fieldname": "invoices",
+ "fieldtype": "Link",
+ "options": "Sales Invoice",
+ "read_only": 1,
+ },
+ {
+ "label": _("Status"),
+ "fieldname": "status",
+ "fieldtype": "Data",
+ "read_only": 1
+ }
+ ]
+ return columns
+
+
+def get_conditions(filters):
+ """
+ Convert filter options to conditions used in query
+ """
+ filters = frappe._dict(filters) if filters else frappe._dict({})
+ conditions = frappe._dict({})
+
+ conditions.company = filters.company or frappe.defaults.get_user_default("company")
+ conditions.end_date = filters.period_end_date or frappe.utils.today()
+ conditions.start_date = filters.period_start_date or frappe.utils.add_months(
+ conditions.end_date, -1
+ )
+ conditions.sales_order = filters.sales_order or []
+
+ return conditions
+
+
+def get_so_with_invoices(filters):
+ """
+ Get Sales Order with payment terms template with their associated Invoices
+ """
+ sorders = []
+
+ so = qb.DocType("Sales Order")
+ ps = qb.DocType("Payment Schedule")
+ datediff = query_builder.CustomFunction("DATEDIFF", ["cur_date", "due_date"])
+ ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"])
+
+ conditions = get_conditions(filters)
+ query_so = (
+ qb.from_(so)
+ .join(ps)
+ .on(ps.parent == so.name)
+ .select(
+ so.name,
+ so.transaction_date.as_("submitted"),
+ ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"),
+ ps.payment_term,
+ ps.description,
+ ps.due_date,
+ ps.invoice_portion,
+ ps.payment_amount,
+ ps.paid_amount,
+ )
+ .where(
+ (so.docstatus == 1)
+ & (so.payment_terms_template != "NULL")
+ & (so.company == conditions.company)
+ & (so.transaction_date[conditions.start_date : conditions.end_date])
+ )
+ .orderby(so.name, so.transaction_date, ps.due_date)
+ )
+
+ if conditions.sales_order != []:
+ query_so = query_so.where(so.name.isin(conditions.sales_order))
+
+ sorders = query_so.run(as_dict=True)
+
+ invoices = []
+ if sorders != []:
+ soi = qb.DocType("Sales Order Item")
+ si = qb.DocType("Sales Invoice")
+ sii = qb.DocType("Sales Invoice Item")
+ query_inv = (
+ qb.from_(sii)
+ .right_join(si)
+ .on(si.name == sii.parent)
+ .inner_join(soi)
+ .on(soi.name == sii.so_detail)
+ .select(sii.sales_order, sii.parent.as_("invoice"), si.base_net_total.as_("invoice_amount"))
+ .where((sii.sales_order.isin([x.name for x in sorders])) & (si.docstatus == 1))
+ .groupby(sii.parent)
+ )
+ invoices = query_inv.run(as_dict=True)
+
+ return sorders, invoices
+
+
+def set_payment_terms_statuses(sales_orders, invoices):
+ """
+ compute status for payment terms with associated sales invoice using FIFO
+ """
+
+ for so in sales_orders:
+ for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]:
+ if so.payment_amount - so.paid_amount > 0:
+ amount = so.payment_amount - so.paid_amount
+ if inv.invoice_amount >= amount:
+ inv.invoice_amount -= amount
+ so.paid_amount += amount
+ if so.invoices:
+ so.invoices = so.invoices + "," + inv.invoice
+ else:
+ so.invoices = inv.invoice
+ so.status = "Completed"
+ break
+ else:
+ so.paid_amount += inv.invoice_amount
+ inv.invoice_amount = 0
+ if so.invoices:
+ so.invoices = so.invoices + "," + inv.invoice
+ else:
+ so.invoices = inv.invoice
+ so.status = "Partly Paid"
+
+ return sales_orders, invoices
+
+
+def prepare_chart(s_orders):
+ if len(set([x.name for x in s_orders])) == 1:
+ chart = {
+ "data": {
+ "labels": [term.payment_term for term in s_orders],
+ "datasets": [
+ {"name": "Payment Amount", "values": [x.payment_amount for x in s_orders],},
+ {"name": "Paid Amount", "values": [x.paid_amount for x in s_orders],},
+ ],
+ },
+ "type": "bar",
+ }
+ return chart
+
+
+def execute(filters=None):
+ columns = get_columns()
+ sales_orders, so_invoices = get_so_with_invoices(filters)
+ sales_orders, so_invoices = set_payment_terms_statuses(sales_orders, so_invoices)
+
+ prepare_chart(sales_orders)
+
+ data = sales_orders
+ message = []
+ chart = prepare_chart(sales_orders)
+
+ return columns, data, message, chart
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
new file mode 100644
index 0000000000..e9dba84f3a
--- /dev/null
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
@@ -0,0 +1,119 @@
+import datetime
+import unittest
+
+import frappe
+from frappe import qb
+from frappe.utils import add_days
+
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
+from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
+from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order import (
+ execute,
+)
+from erpnext.stock.doctype.item.test_item import create_item
+from erpnext.tests.utils import ERPNextTestCase
+
+test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template"]
+
+
+class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase):
+ def test_payment_terms_status(self):
+ # disable Must be a whole number
+ nos = frappe.get_doc("UOM", "Nos")
+ nos.must_be_whole_number = 0
+ nos.save()
+
+ template = None
+ if frappe.db.exists("Payment Terms Template", "_Test 50-50"):
+ template = frappe.get_doc("Payment Terms Template", "_Test 50-50")
+ else:
+ template = frappe.get_doc(
+ {
+ "doctype": "Payment Terms Template",
+ "template_name": "_Test 50-50",
+ "terms": [
+ {
+ "doctype": "Payment Terms Template Detail",
+ "due_date_based_on": "Day(s) after invoice date",
+ "payment_term_name": "_Test 50% on 15 Days",
+ "description": "_Test 50-50",
+ "invoice_portion": 50,
+ "credit_days": 15,
+ },
+ {
+ "doctype": "Payment Terms Template Detail",
+ "due_date_based_on": "Day(s) after invoice date",
+ "payment_term_name": "_Test 50% on 30 Days",
+ "description": "_Test 50-50",
+ "invoice_portion": 50,
+ "credit_days": 30,
+ },
+ ],
+ }
+ )
+ template.insert()
+
+ # item = create_item(item_code="_Test Excavator", is_stock_item=0, valuation_rate=1000000)
+ item = create_item(item_code="_Test Excavator", is_stock_item=0)
+ so = make_sales_order(
+ transaction_date="2021-06-15",
+ delivery_date=add_days("2021-06-15", -30),
+ item=item.item_code,
+ qty=1,
+ rate=1000000,
+ po_no=54321,
+ do_not_save=True,
+ )
+ so.payment_terms_template = template.name
+ so.save()
+ so.submit()
+
+ # make invoice with 60% of the total sales order value
+ sinv = make_sales_invoice(so.name)
+ # sinv.posting_date = "2021-06-29"
+ sinv.items[0].qty *= 0.60
+ sinv.insert()
+ sinv.submit()
+
+ columns, data, message, chart = execute(
+ {
+ "company": "_Test Company",
+ "period_start_date": "2021-06-01",
+ "period_end_date": "2021-06-30",
+ "sales_order": [so.name],
+ }
+ )
+
+ # revert changes to Nos
+ nos.must_be_whole_number = 1
+ nos.save()
+
+ expected_value = [
+ {
+ "name": so.name,
+ "submitted": datetime.date(2021, 6, 15),
+ "status": "Completed",
+ "payment_term": None,
+ "description": "_Test 50-50",
+ "due_date": datetime.date(2021, 6, 30),
+ "invoice_portion": 50.0,
+ "payment_amount": 500000.0,
+ "paid_amount": 500000.0,
+ "invoices": sinv.name,
+ },
+ {
+ "name": so.name,
+ "submitted": datetime.date(2021, 6, 15),
+ "status": "Partly Paid",
+ "payment_term": None,
+ "description": "_Test 50-50",
+ "due_date": datetime.date(2021, 7, 15),
+ "invoice_portion": 50.0,
+ "payment_amount": 500000.0,
+ "paid_amount": 100000.0,
+ "invoices": sinv.name,
+ },
+ ]
+
+ self.assertEqual(data, expected_value)
From 9f1e68801d527628551984402fd0c06e401084d8 Mon Sep 17 00:00:00 2001
From: ruthra
Date: Wed, 5 Jan 2022 10:11:19 +0530
Subject: [PATCH 002/447] test: fix failing test case payment terms status
---
.../test_payment_terms_status_for_sales_order.py | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
index e9dba84f3a..19c01f2d43 100644
--- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
@@ -21,8 +21,7 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase):
def test_payment_terms_status(self):
# disable Must be a whole number
nos = frappe.get_doc("UOM", "Nos")
- nos.must_be_whole_number = 0
- nos.save()
+ nos.db_set("must_be_whole_number", 0, commit=True)
template = None
if frappe.db.exists("Payment Terms Template", "_Test 50-50"):
@@ -62,9 +61,9 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase):
item=item.item_code,
qty=1,
rate=1000000,
- po_no=54321,
do_not_save=True,
)
+ so.po_no = ""
so.payment_terms_template = template.name
so.save()
so.submit()
@@ -86,8 +85,7 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase):
)
# revert changes to Nos
- nos.must_be_whole_number = 1
- nos.save()
+ nos.db_set("must_be_whole_number", 1, commit=True)
expected_value = [
{
From edd980acdc9e51f74eb6b70a793ae17b2e827710 Mon Sep 17 00:00:00 2001
From: ruthra
Date: Wed, 5 Jan 2022 10:43:20 +0530
Subject: [PATCH 003/447] refactor: remove unused imports
---
.../test_payment_terms_status_for_sales_order.py | 3 ---
1 file changed, 3 deletions(-)
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
index 19c01f2d43..4f27a5683d 100644
--- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
@@ -1,11 +1,8 @@
import datetime
-import unittest
import frappe
-from frappe import qb
from frappe.utils import add_days
-from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order import (
From 4535a7a301f76fa3b867902f19e806dcb01bdb75 Mon Sep 17 00:00:00 2001
From: ruthra
Date: Wed, 5 Jan 2022 10:46:32 +0530
Subject: [PATCH 004/447] test: qty and rate changed to remove need for
fractional Nos
---
.../test_payment_terms_status_for_sales_order.py | 13 +++----------
1 file changed, 3 insertions(+), 10 deletions(-)
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
index 4f27a5683d..5d6e91e8a5 100644
--- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
@@ -16,9 +16,6 @@ test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Temp
class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase):
def test_payment_terms_status(self):
- # disable Must be a whole number
- nos = frappe.get_doc("UOM", "Nos")
- nos.db_set("must_be_whole_number", 0, commit=True)
template = None
if frappe.db.exists("Payment Terms Template", "_Test 50-50"):
@@ -56,8 +53,8 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase):
transaction_date="2021-06-15",
delivery_date=add_days("2021-06-15", -30),
item=item.item_code,
- qty=1,
- rate=1000000,
+ qty=10,
+ rate=100000,
do_not_save=True,
)
so.po_no = ""
@@ -67,8 +64,7 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase):
# make invoice with 60% of the total sales order value
sinv = make_sales_invoice(so.name)
- # sinv.posting_date = "2021-06-29"
- sinv.items[0].qty *= 0.60
+ sinv.items[0].qty = 6
sinv.insert()
sinv.submit()
@@ -81,9 +77,6 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase):
}
)
- # revert changes to Nos
- nos.db_set("must_be_whole_number", 1, commit=True)
-
expected_value = [
{
"name": so.name,
From 0c0a9ed96d3b11741cf506c95f9e5e16fcdcfe68 Mon Sep 17 00:00:00 2001
From: ChillarAnand
Date: Wed, 5 Jan 2022 09:17:46 +0530
Subject: [PATCH 005/447] refactor: Remove non-profit domain from ERPNext
---
erpnext/domains/non_profit.py | 22 -
erpnext/hooks.py | 16 -
erpnext/modules.txt | 1 -
erpnext/non_profit/__init__.py | 0
erpnext/non_profit/doctype/__init__.py | 0
.../certification_application/__init__.py | 0
.../certification_application.js | 8 -
.../certification_application.json | 323 -------
.../certification_application.py | 9 -
.../test_certification_application.py | 8 -
.../doctype/certified_consultant/__init__.py | 0
.../certified_consultant.js | 8 -
.../certified_consultant.json | 724 ---------------
.../certified_consultant.py | 9 -
.../test_certified_consultant.py | 8 -
.../non_profit/doctype/chapter/__init__.py | 0
erpnext/non_profit/doctype/chapter/chapter.js | 8 -
.../non_profit/doctype/chapter/chapter.json | 397 --------
erpnext/non_profit/doctype/chapter/chapter.py | 49 -
.../doctype/chapter/templates/chapter.html | 79 --
.../chapter/templates/chapter_row.html | 25 -
.../doctype/chapter/test_chapter.py | 8 -
.../doctype/chapter_member/__init__.py | 0
.../chapter_member/chapter_member.json | 199 ----
.../doctype/chapter_member/chapter_member.py | 9 -
.../non_profit/doctype/donation/__init__.py | 0
.../non_profit/doctype/donation/donation.js | 26 -
.../non_profit/doctype/donation/donation.json | 156 ----
.../non_profit/doctype/donation/donation.py | 220 -----
.../doctype/donation/donation_dashboard.py | 16 -
.../doctype/donation/test_donation.py | 77 --
erpnext/non_profit/doctype/donor/__init__.py | 0
erpnext/non_profit/doctype/donor/donor.js | 17 -
erpnext/non_profit/doctype/donor/donor.json | 110 ---
erpnext/non_profit/doctype/donor/donor.py | 17 -
.../non_profit/doctype/donor/donor_list.js | 3 -
.../non_profit/doctype/donor/test_donor.py | 8 -
.../non_profit/doctype/donor_type/__init__.py | 0
.../doctype/donor_type/donor_type.js | 8 -
.../doctype/donor_type/donor_type.json | 94 --
.../doctype/donor_type/donor_type.py | 9 -
.../doctype/donor_type/test_donor_type.py | 8 -
.../doctype/grant_application/__init__.py | 0
.../grant_application/grant_application.js | 27 -
.../grant_application/grant_application.json | 851 ------------------
.../grant_application/grant_application.py | 58 --
.../templates/grant_application.html | 68 --
.../templates/grant_application_row.html | 11 -
.../test_grant_application.py | 8 -
erpnext/non_profit/doctype/member/__init__.py | 0
erpnext/non_profit/doctype/member/member.js | 64 --
erpnext/non_profit/doctype/member/member.json | 210 -----
erpnext/non_profit/doctype/member/member.py | 185 ----
.../doctype/member/member_dashboard.py | 22 -
.../non_profit/doctype/member/member_list.js | 3 -
.../non_profit/doctype/member/test_member.py | 8 -
.../non_profit/doctype/membership/__init__.py | 0
.../doctype/membership/membership.js | 41 -
.../doctype/membership/membership.json | 184 ----
.../doctype/membership/membership_list.js | 15 -
.../doctype/membership/test_membership.py | 164 ----
.../doctype/membership_type/__init__.py | 0
.../membership_type/membership_type.js | 22 -
.../membership_type/membership_type.json | 71 --
.../membership_type/membership_type.py | 18 -
.../membership_type/test_membership_type.py | 8 -
.../doctype/non_profit_settings/__init__.py | 0
.../non_profit_settings.js | 133 ---
.../non_profit_settings.json | 273 ------
.../non_profit_settings.py | 38 -
.../test_non_profit_settings.py | 9 -
.../non_profit/doctype/volunteer/__init__.py | 0
.../doctype/volunteer/test_volunteer.py | 8 -
.../non_profit/doctype/volunteer/volunteer.js | 17 -
.../doctype/volunteer/volunteer.json | 148 ---
.../non_profit/doctype/volunteer/volunteer.py | 12 -
.../doctype/volunteer_skill/__init__.py | 0
.../volunteer_skill/volunteer_skill.json | 73 --
.../volunteer_skill/volunteer_skill.py | 9 -
.../doctype/volunteer_type/__init__.py | 0
.../volunteer_type/test_volunteer_type.py | 8 -
.../doctype/volunteer_type/volunteer_type.js | 8 -
.../volunteer_type/volunteer_type.json | 94 --
.../doctype/volunteer_type/volunteer_type.py | 9 -
erpnext/non_profit/report/__init__.py | 0
.../report/expiring_memberships/__init__.py | 0
.../expiring_memberships.js | 24 -
.../expiring_memberships.json | 27 -
.../expiring_memberships.py | 34 -
erpnext/non_profit/utils.py | 12 -
erpnext/non_profit/web_form/__init__.py | 0
.../certification_application/__init__.py | 0
.../certification_application.js | 16 -
.../certification_application.json | 79 --
.../certification_application.py | 3 -
.../certification_application_usd/__init__.py | 0
.../certification_application_usd.js | 16 -
.../certification_application_usd.json | 80 --
.../certification_application_usd.py | 3 -
.../web_form/grant_application/__init__.py | 0
.../grant_application/grant_application.js | 3 -
.../grant_application/grant_application.json | 108 ---
.../grant_application/grant_application.py | 4 -
.../workspace/non_profit/non_profit.json | 272 ------
erpnext/patches.txt | 2 +
.../v13_0/non_profit_deprecation_warning.py | 10 +
.../v14_0/delete_non_profit_doctypes.py | 19 +
erpnext/regional/india/setup.py | 8 -
.../operations/install_fixtures.py | 1 -
109 files changed, 31 insertions(+), 6246 deletions(-)
delete mode 100644 erpnext/domains/non_profit.py
delete mode 100644 erpnext/non_profit/__init__.py
delete mode 100644 erpnext/non_profit/doctype/__init__.py
delete mode 100644 erpnext/non_profit/doctype/certification_application/__init__.py
delete mode 100644 erpnext/non_profit/doctype/certification_application/certification_application.js
delete mode 100644 erpnext/non_profit/doctype/certification_application/certification_application.json
delete mode 100644 erpnext/non_profit/doctype/certification_application/certification_application.py
delete mode 100644 erpnext/non_profit/doctype/certification_application/test_certification_application.py
delete mode 100644 erpnext/non_profit/doctype/certified_consultant/__init__.py
delete mode 100644 erpnext/non_profit/doctype/certified_consultant/certified_consultant.js
delete mode 100644 erpnext/non_profit/doctype/certified_consultant/certified_consultant.json
delete mode 100644 erpnext/non_profit/doctype/certified_consultant/certified_consultant.py
delete mode 100644 erpnext/non_profit/doctype/certified_consultant/test_certified_consultant.py
delete mode 100644 erpnext/non_profit/doctype/chapter/__init__.py
delete mode 100644 erpnext/non_profit/doctype/chapter/chapter.js
delete mode 100644 erpnext/non_profit/doctype/chapter/chapter.json
delete mode 100644 erpnext/non_profit/doctype/chapter/chapter.py
delete mode 100644 erpnext/non_profit/doctype/chapter/templates/chapter.html
delete mode 100644 erpnext/non_profit/doctype/chapter/templates/chapter_row.html
delete mode 100644 erpnext/non_profit/doctype/chapter/test_chapter.py
delete mode 100644 erpnext/non_profit/doctype/chapter_member/__init__.py
delete mode 100644 erpnext/non_profit/doctype/chapter_member/chapter_member.json
delete mode 100644 erpnext/non_profit/doctype/chapter_member/chapter_member.py
delete mode 100644 erpnext/non_profit/doctype/donation/__init__.py
delete mode 100644 erpnext/non_profit/doctype/donation/donation.js
delete mode 100644 erpnext/non_profit/doctype/donation/donation.json
delete mode 100644 erpnext/non_profit/doctype/donation/donation.py
delete mode 100644 erpnext/non_profit/doctype/donation/donation_dashboard.py
delete mode 100644 erpnext/non_profit/doctype/donation/test_donation.py
delete mode 100644 erpnext/non_profit/doctype/donor/__init__.py
delete mode 100644 erpnext/non_profit/doctype/donor/donor.js
delete mode 100644 erpnext/non_profit/doctype/donor/donor.json
delete mode 100644 erpnext/non_profit/doctype/donor/donor.py
delete mode 100644 erpnext/non_profit/doctype/donor/donor_list.js
delete mode 100644 erpnext/non_profit/doctype/donor/test_donor.py
delete mode 100644 erpnext/non_profit/doctype/donor_type/__init__.py
delete mode 100644 erpnext/non_profit/doctype/donor_type/donor_type.js
delete mode 100644 erpnext/non_profit/doctype/donor_type/donor_type.json
delete mode 100644 erpnext/non_profit/doctype/donor_type/donor_type.py
delete mode 100644 erpnext/non_profit/doctype/donor_type/test_donor_type.py
delete mode 100644 erpnext/non_profit/doctype/grant_application/__init__.py
delete mode 100644 erpnext/non_profit/doctype/grant_application/grant_application.js
delete mode 100644 erpnext/non_profit/doctype/grant_application/grant_application.json
delete mode 100644 erpnext/non_profit/doctype/grant_application/grant_application.py
delete mode 100644 erpnext/non_profit/doctype/grant_application/templates/grant_application.html
delete mode 100644 erpnext/non_profit/doctype/grant_application/templates/grant_application_row.html
delete mode 100644 erpnext/non_profit/doctype/grant_application/test_grant_application.py
delete mode 100644 erpnext/non_profit/doctype/member/__init__.py
delete mode 100644 erpnext/non_profit/doctype/member/member.js
delete mode 100644 erpnext/non_profit/doctype/member/member.json
delete mode 100644 erpnext/non_profit/doctype/member/member.py
delete mode 100644 erpnext/non_profit/doctype/member/member_dashboard.py
delete mode 100644 erpnext/non_profit/doctype/member/member_list.js
delete mode 100644 erpnext/non_profit/doctype/member/test_member.py
delete mode 100644 erpnext/non_profit/doctype/membership/__init__.py
delete mode 100644 erpnext/non_profit/doctype/membership/membership.js
delete mode 100644 erpnext/non_profit/doctype/membership/membership.json
delete mode 100644 erpnext/non_profit/doctype/membership/membership_list.js
delete mode 100644 erpnext/non_profit/doctype/membership/test_membership.py
delete mode 100644 erpnext/non_profit/doctype/membership_type/__init__.py
delete mode 100644 erpnext/non_profit/doctype/membership_type/membership_type.js
delete mode 100644 erpnext/non_profit/doctype/membership_type/membership_type.json
delete mode 100644 erpnext/non_profit/doctype/membership_type/membership_type.py
delete mode 100644 erpnext/non_profit/doctype/membership_type/test_membership_type.py
delete mode 100644 erpnext/non_profit/doctype/non_profit_settings/__init__.py
delete mode 100644 erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js
delete mode 100644 erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json
delete mode 100644 erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py
delete mode 100644 erpnext/non_profit/doctype/non_profit_settings/test_non_profit_settings.py
delete mode 100644 erpnext/non_profit/doctype/volunteer/__init__.py
delete mode 100644 erpnext/non_profit/doctype/volunteer/test_volunteer.py
delete mode 100644 erpnext/non_profit/doctype/volunteer/volunteer.js
delete mode 100644 erpnext/non_profit/doctype/volunteer/volunteer.json
delete mode 100644 erpnext/non_profit/doctype/volunteer/volunteer.py
delete mode 100644 erpnext/non_profit/doctype/volunteer_skill/__init__.py
delete mode 100644 erpnext/non_profit/doctype/volunteer_skill/volunteer_skill.json
delete mode 100644 erpnext/non_profit/doctype/volunteer_skill/volunteer_skill.py
delete mode 100644 erpnext/non_profit/doctype/volunteer_type/__init__.py
delete mode 100644 erpnext/non_profit/doctype/volunteer_type/test_volunteer_type.py
delete mode 100644 erpnext/non_profit/doctype/volunteer_type/volunteer_type.js
delete mode 100644 erpnext/non_profit/doctype/volunteer_type/volunteer_type.json
delete mode 100644 erpnext/non_profit/doctype/volunteer_type/volunteer_type.py
delete mode 100644 erpnext/non_profit/report/__init__.py
delete mode 100644 erpnext/non_profit/report/expiring_memberships/__init__.py
delete mode 100644 erpnext/non_profit/report/expiring_memberships/expiring_memberships.js
delete mode 100644 erpnext/non_profit/report/expiring_memberships/expiring_memberships.json
delete mode 100644 erpnext/non_profit/report/expiring_memberships/expiring_memberships.py
delete mode 100644 erpnext/non_profit/utils.py
delete mode 100644 erpnext/non_profit/web_form/__init__.py
delete mode 100644 erpnext/non_profit/web_form/certification_application/__init__.py
delete mode 100644 erpnext/non_profit/web_form/certification_application/certification_application.js
delete mode 100644 erpnext/non_profit/web_form/certification_application/certification_application.json
delete mode 100644 erpnext/non_profit/web_form/certification_application/certification_application.py
delete mode 100644 erpnext/non_profit/web_form/certification_application_usd/__init__.py
delete mode 100644 erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.js
delete mode 100644 erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.json
delete mode 100644 erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.py
delete mode 100644 erpnext/non_profit/web_form/grant_application/__init__.py
delete mode 100644 erpnext/non_profit/web_form/grant_application/grant_application.js
delete mode 100644 erpnext/non_profit/web_form/grant_application/grant_application.json
delete mode 100644 erpnext/non_profit/web_form/grant_application/grant_application.py
delete mode 100644 erpnext/non_profit/workspace/non_profit/non_profit.json
create mode 100644 erpnext/patches/v13_0/non_profit_deprecation_warning.py
create mode 100644 erpnext/patches/v14_0/delete_non_profit_doctypes.py
diff --git a/erpnext/domains/non_profit.py b/erpnext/domains/non_profit.py
deleted file mode 100644
index d9fc5e5df0..0000000000
--- a/erpnext/domains/non_profit.py
+++ /dev/null
@@ -1,22 +0,0 @@
-data = {
- 'desktop_icons': [
- 'Non Profit',
- 'Member',
- 'Donor',
- 'Volunteer',
- 'Grant Application',
- 'Accounts',
- 'Buying',
- 'HR',
- 'ToDo'
- ],
- 'restricted_roles': [
- 'Non Profit Manager',
- 'Non Profit Member',
- 'Non Profit Portal User'
- ],
- 'modules': [
- 'Non Profit'
- ],
- 'default_portal_role': 'Non Profit Manager'
-}
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index f014b0e1e9..4502f2b97b 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -69,7 +69,6 @@ domains = {
'Education': 'erpnext.domains.education',
'Hospitality': 'erpnext.domains.hospitality',
'Manufacturing': 'erpnext.domains.manufacturing',
- 'Non Profit': 'erpnext.domains.non_profit',
'Retail': 'erpnext.domains.retail',
'Services': 'erpnext.domains.services',
}
@@ -176,7 +175,6 @@ standard_portal_menu_items = [
{"title": _("Fees"), "route": "/fees", "reference_doctype": "Fees", "role":"Student"},
{"title": _("Newsletter"), "route": "/newsletters", "reference_doctype": "Newsletter"},
{"title": _("Admission"), "route": "/admissions", "reference_doctype": "Student Admission", "role": "Student"},
- {"title": _("Certification"), "route": "/certification", "reference_doctype": "Certification Application", "role": "Non Profit Portal User"},
{"title": _("Material Request"), "route": "/material-requests", "reference_doctype": "Material Request", "role": "Customer"},
{"title": _("Appointment Booking"), "route": "/book_appointment"},
]
@@ -373,7 +371,6 @@ scheduler_events = {
"erpnext.selling.doctype.quotation.quotation.set_expired_status",
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
- "erpnext.non_profit.doctype.membership.membership.set_expired_status",
"erpnext.hr.doctype.interview.interview.send_daily_feedback_reminder"
],
"daily_long": [
@@ -566,19 +563,6 @@ global_search_doctypes = {
{'doctype': 'Assessment Code', 'index': 39},
{'doctype': 'Discussion', 'index': 40},
],
- "Non Profit": [
- {'doctype': 'Certified Consultant', 'index': 1},
- {'doctype': 'Certification Application', 'index': 2},
- {'doctype': 'Volunteer', 'index': 3},
- {'doctype': 'Membership', 'index': 4},
- {'doctype': 'Member', 'index': 5},
- {'doctype': 'Donor', 'index': 6},
- {'doctype': 'Chapter', 'index': 7},
- {'doctype': 'Grant Application', 'index': 8},
- {'doctype': 'Volunteer Type', 'index': 9},
- {'doctype': 'Donor Type', 'index': 10},
- {'doctype': 'Membership Type', 'index': 11}
- ],
"Hospitality": [
{'doctype': 'Hotel Room', 'index': 0},
{'doctype': 'Hotel Room Reservation', 'index': 1},
diff --git a/erpnext/modules.txt b/erpnext/modules.txt
index ae0bb2d5c9..cd1586c290 100644
--- a/erpnext/modules.txt
+++ b/erpnext/modules.txt
@@ -17,7 +17,6 @@ Education
Regional
Restaurant
ERPNext Integrations
-Non Profit
Hotels
Quality Management
Communication
diff --git a/erpnext/non_profit/__init__.py b/erpnext/non_profit/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/doctype/__init__.py b/erpnext/non_profit/doctype/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/doctype/certification_application/__init__.py b/erpnext/non_profit/doctype/certification_application/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/doctype/certification_application/certification_application.js b/erpnext/non_profit/doctype/certification_application/certification_application.js
deleted file mode 100644
index 1e6a9a4b01..0000000000
--- a/erpnext/non_profit/doctype/certification_application/certification_application.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Certification Application', {
- refresh: function(frm) {
-
- }
-});
diff --git a/erpnext/non_profit/doctype/certification_application/certification_application.json b/erpnext/non_profit/doctype/certification_application/certification_application.json
deleted file mode 100644
index f562fa6734..0000000000
--- a/erpnext/non_profit/doctype/certification_application/certification_application.json
+++ /dev/null
@@ -1,323 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "NPO-CAPP-.YYYY.-.#####",
- "beta": 0,
- "creation": "2018-06-08 16:12:42.091729",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "name_of_applicant",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Name of Applicant",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "email",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Email",
- "length": 0,
- "no_copy": 0,
- "options": "User",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_1",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "certification_status",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Certification Status",
- "length": 0,
- "no_copy": 0,
- "options": "Yet to appear\nCertified\nNot Certified",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "payment_details",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Payment Details",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "paid",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Paid",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "currency",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Currency",
- "length": 0,
- "no_copy": 0,
- "options": "USD\nINR",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "amount",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-11-04 03:36:35.337403",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Certification Application",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Non Profit",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/certification_application/certification_application.py b/erpnext/non_profit/doctype/certification_application/certification_application.py
deleted file mode 100644
index cbbe191fba..0000000000
--- a/erpnext/non_profit/doctype/certification_application/certification_application.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class CertificationApplication(Document):
- pass
diff --git a/erpnext/non_profit/doctype/certification_application/test_certification_application.py b/erpnext/non_profit/doctype/certification_application/test_certification_application.py
deleted file mode 100644
index 8687b4daf4..0000000000
--- a/erpnext/non_profit/doctype/certification_application/test_certification_application.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestCertificationApplication(unittest.TestCase):
- pass
diff --git a/erpnext/non_profit/doctype/certified_consultant/__init__.py b/erpnext/non_profit/doctype/certified_consultant/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/doctype/certified_consultant/certified_consultant.js b/erpnext/non_profit/doctype/certified_consultant/certified_consultant.js
deleted file mode 100644
index cd004c3489..0000000000
--- a/erpnext/non_profit/doctype/certified_consultant/certified_consultant.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Certified Consultant', {
- refresh: function(frm) {
-
- }
-});
diff --git a/erpnext/non_profit/doctype/certified_consultant/certified_consultant.json b/erpnext/non_profit/doctype/certified_consultant/certified_consultant.json
deleted file mode 100644
index d77f1b2569..0000000000
--- a/erpnext/non_profit/doctype/certified_consultant/certified_consultant.json
+++ /dev/null
@@ -1,724 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "NPO-CONS-.YYYY.-.#####",
- "beta": 0,
- "creation": "2018-06-13 17:27:19.838334",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "name_of_consultant",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Name of Consultant",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "country",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Country",
- "length": 0,
- "no_copy": 0,
- "options": "Country",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "email",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Email",
- "length": 0,
- "no_copy": 0,
- "options": "User",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "phone",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Phone",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "website_url",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Website",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "address",
- "fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Address",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break1",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "image",
- "fieldtype": "Attach Image",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Image",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "certification_application",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Certification Application",
- "length": 0,
- "no_copy": 0,
- "options": "Certification Application",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break1",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Certification Validity",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "from_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "From",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_beak2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "to_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "To",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break2",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "introduction",
- "fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Introduction",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "details",
- "fieldtype": "Text Editor",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Details",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break3",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "discuss_id",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Discuss ID",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "github_id",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "GitHub ID",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "show_in_website",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Show in Website",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-11-04 03:36:47.386618",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Certified Consultant",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Non Profit Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Non Profit",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/certified_consultant/certified_consultant.py b/erpnext/non_profit/doctype/certified_consultant/certified_consultant.py
deleted file mode 100644
index 47361cc39e..0000000000
--- a/erpnext/non_profit/doctype/certified_consultant/certified_consultant.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class CertifiedConsultant(Document):
- pass
diff --git a/erpnext/non_profit/doctype/certified_consultant/test_certified_consultant.py b/erpnext/non_profit/doctype/certified_consultant/test_certified_consultant.py
deleted file mode 100644
index d10353c1e4..0000000000
--- a/erpnext/non_profit/doctype/certified_consultant/test_certified_consultant.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestCertifiedConsultant(unittest.TestCase):
- pass
diff --git a/erpnext/non_profit/doctype/chapter/__init__.py b/erpnext/non_profit/doctype/chapter/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/doctype/chapter/chapter.js b/erpnext/non_profit/doctype/chapter/chapter.js
deleted file mode 100644
index c8b6d4a644..0000000000
--- a/erpnext/non_profit/doctype/chapter/chapter.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Chapter', {
- refresh: function() {
-
- }
-});
diff --git a/erpnext/non_profit/doctype/chapter/chapter.json b/erpnext/non_profit/doctype/chapter/chapter.json
deleted file mode 100644
index 86cba9a178..0000000000
--- a/erpnext/non_profit/doctype/chapter/chapter.json
+++ /dev/null
@@ -1,397 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 1,
- "allow_import": 0,
- "allow_rename": 1,
- "autoname": "prompt",
- "beta": 0,
- "creation": "2017-09-14 13:36:03.904702",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "chapter_head",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Chapter Head",
- "length": 0,
- "no_copy": 0,
- "options": "Member",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "region",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Region",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_5",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "introduction",
- "fieldtype": "Text Editor",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Introduction",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "meetup_embed_html",
- "fieldtype": "Code",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Meetup Embed HTML",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "address",
- "fieldtype": "Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Address",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "chapters/chapter_name\nleave blank automatically set after saving chapter.",
- "fieldname": "route",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Route",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "published",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Published",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 1,
- "columns": 0,
- "fieldname": "chapter_members",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Chapter Members",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "members",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Members",
- "length": 0,
- "no_copy": 0,
- "options": "Chapter Member",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- }
- ],
- "has_web_view": 1,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_published_field": "published",
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2017-12-14 12:59:31.424240",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Chapter",
- "name_case": "Title Case",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Non Profit Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Non Profit",
- "route": "chapters",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/chapter/chapter.py b/erpnext/non_profit/doctype/chapter/chapter.py
deleted file mode 100644
index c01b1ef3e4..0000000000
--- a/erpnext/non_profit/doctype/chapter/chapter.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import frappe
-from frappe.website.website_generator import WebsiteGenerator
-
-
-class Chapter(WebsiteGenerator):
- _website = frappe._dict(
- condition_field = "published",
- )
-
- def get_context(self, context):
- context.no_cache = True
- context.show_sidebar = True
- context.parents = [dict(label='View All Chapters',
- route='chapters', title='View Chapters')]
-
- def validate(self):
- if not self.route: #pylint: disable=E0203
- self.route = 'chapters/' + self.scrub(self.name)
-
- def enable(self):
- chapter = frappe.get_doc('Chapter', frappe.form_dict.name)
- chapter.append('members', dict(enable=self.value))
- chapter.save(ignore_permissions=1)
- frappe.db.commit()
-
-
-def get_list_context(context):
- context.allow_guest = True
- context.no_cache = True
- context.show_sidebar = True
- context.title = 'All Chapters'
- context.no_breadcrumbs = True
- context.order_by = 'creation desc'
-
-
-@frappe.whitelist()
-def leave(title, user_id, leave_reason):
- chapter = frappe.get_doc("Chapter", title)
- for member in chapter.members:
- if member.user == user_id:
- member.enabled = 0
- member.leave_reason = leave_reason
- chapter.save(ignore_permissions=1)
- frappe.db.commit()
- return "Thank you for Feedback"
diff --git a/erpnext/non_profit/doctype/chapter/templates/chapter.html b/erpnext/non_profit/doctype/chapter/templates/chapter.html
deleted file mode 100644
index 321828f73f..0000000000
--- a/erpnext/non_profit/doctype/chapter/templates/chapter.html
+++ /dev/null
@@ -1,79 +0,0 @@
-{% extends "templates/web.html" %}
-
-{% block page_content %}
-{{ title }}
-{{ introduction }}
-{% if meetup_embed_html %}
- {{ meetup_embed_html }}
-{% endif %}
-Member Details
-
-{% if members %}
-
- {% set index = [1] %}
- {% for user in members %}
- {% if user.enabled == 1 %}
-
-
-
-
-
-
- {{ index|length }}. {{ frappe.db.get_value('User', user.user, 'full_name') }}
-
-
-
-
-
-
- {% if user.introduction %}
- {{ user.introduction }}
- {% endif %}
-
-
-
-
-
- {% set __ = index.append(1) %}
- {% endif %}
- {% endfor %}
-
-{% else %}
- No member yet.
-{% endif %}
-
-Chapter Head
-
-
-
- {% set doc = frappe.get_doc('Member',chapter_head) %}
-
- Name
- {{ doc.member_name }}
-
-
- Email
- {{ frappe.db.get_value('User', doc.email, 'email') or '' }}
-
-
- Phone
- {{ frappe.db.get_value('User', doc.email, 'phone') or '' }}
-
-
-
-
-{% if address %}
-Address
-
-{% endif %}
-
-Join this Chapter
-Leave this Chapter
-
-{% endblock %}
diff --git a/erpnext/non_profit/doctype/chapter/templates/chapter_row.html b/erpnext/non_profit/doctype/chapter/templates/chapter_row.html
deleted file mode 100644
index cad34fa5be..0000000000
--- a/erpnext/non_profit/doctype/chapter/templates/chapter_row.html
+++ /dev/null
@@ -1,25 +0,0 @@
-{% if doc.published %}
-
-{% endif %}
diff --git a/erpnext/non_profit/doctype/chapter/test_chapter.py b/erpnext/non_profit/doctype/chapter/test_chapter.py
deleted file mode 100644
index 98601efcf2..0000000000
--- a/erpnext/non_profit/doctype/chapter/test_chapter.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestChapter(unittest.TestCase):
- pass
diff --git a/erpnext/non_profit/doctype/chapter_member/__init__.py b/erpnext/non_profit/doctype/chapter_member/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/doctype/chapter_member/chapter_member.json b/erpnext/non_profit/doctype/chapter_member/chapter_member.json
deleted file mode 100644
index 478bfd9331..0000000000
--- a/erpnext/non_profit/doctype/chapter_member/chapter_member.json
+++ /dev/null
@@ -1,199 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-09-14 13:38:04.296375",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "user",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "User",
- "length": 0,
- "no_copy": 0,
- "options": "User",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "introduction",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Introduction",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "website_url",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Website URL",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "default": "1",
- "fieldname": "enabled",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Enabled",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "leave_reason",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Leave Reason",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-03-07 05:36:51.664816",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Chapter Member",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Non Profit",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/chapter_member/chapter_member.py b/erpnext/non_profit/doctype/chapter_member/chapter_member.py
deleted file mode 100644
index 80c0446ee5..0000000000
--- a/erpnext/non_profit/doctype/chapter_member/chapter_member.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class ChapterMember(Document):
- pass
diff --git a/erpnext/non_profit/doctype/donation/__init__.py b/erpnext/non_profit/doctype/donation/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/doctype/donation/donation.js b/erpnext/non_profit/doctype/donation/donation.js
deleted file mode 100644
index 10e8220144..0000000000
--- a/erpnext/non_profit/doctype/donation/donation.js
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Donation', {
- refresh: function(frm) {
- if (frm.doc.docstatus === 1 && !frm.doc.paid) {
- frm.add_custom_button(__('Create Payment Entry'), function() {
- frm.events.make_payment_entry(frm);
- });
- }
- },
-
- make_payment_entry: function(frm) {
- return frappe.call({
- method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry',
- args: {
- 'dt': frm.doc.doctype,
- 'dn': frm.doc.name
- },
- callback: function(r) {
- var doc = frappe.model.sync(r.message);
- frappe.set_route('Form', doc[0].doctype, doc[0].name);
- }
- });
- },
-});
diff --git a/erpnext/non_profit/doctype/donation/donation.json b/erpnext/non_profit/doctype/donation/donation.json
deleted file mode 100644
index 6759569d54..0000000000
--- a/erpnext/non_profit/doctype/donation/donation.json
+++ /dev/null
@@ -1,156 +0,0 @@
-{
- "actions": [],
- "autoname": "naming_series:",
- "creation": "2021-02-17 10:28:52.645731",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "naming_series",
- "donor",
- "donor_name",
- "email",
- "column_break_4",
- "company",
- "date",
- "payment_details_section",
- "paid",
- "amount",
- "mode_of_payment",
- "razorpay_payment_id",
- "amended_from"
- ],
- "fields": [
- {
- "fieldname": "donor",
- "fieldtype": "Link",
- "label": "Donor",
- "options": "Donor",
- "reqd": 1
- },
- {
- "fetch_from": "donor.donor_name",
- "fieldname": "donor_name",
- "fieldtype": "Data",
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Donor Name",
- "read_only": 1
- },
- {
- "fetch_from": "donor.email",
- "fieldname": "email",
- "fieldtype": "Data",
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Email",
- "read_only": 1
- },
- {
- "fieldname": "column_break_4",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "date",
- "fieldtype": "Date",
- "label": "Date",
- "reqd": 1
- },
- {
- "fieldname": "payment_details_section",
- "fieldtype": "Section Break",
- "label": "Payment Details"
- },
- {
- "fieldname": "amount",
- "fieldtype": "Currency",
- "label": "Amount",
- "reqd": 1
- },
- {
- "fieldname": "mode_of_payment",
- "fieldtype": "Link",
- "label": "Mode of Payment",
- "options": "Mode of Payment"
- },
- {
- "fieldname": "razorpay_payment_id",
- "fieldtype": "Data",
- "label": "Razorpay Payment ID",
- "read_only": 1
- },
- {
- "fieldname": "naming_series",
- "fieldtype": "Select",
- "label": "Naming Series",
- "options": "NPO-DTN-.YYYY.-"
- },
- {
- "default": "0",
- "fieldname": "paid",
- "fieldtype": "Check",
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Paid"
- },
- {
- "fieldname": "company",
- "fieldtype": "Link",
- "label": "Company",
- "options": "Company",
- "reqd": 1
- },
- {
- "fieldname": "amended_from",
- "fieldtype": "Link",
- "label": "Amended From",
- "no_copy": 1,
- "options": "Donation",
- "print_hide": 1,
- "read_only": 1
- }
- ],
- "index_web_pages_for_search": 1,
- "is_submittable": 1,
- "links": [],
- "modified": "2021-03-11 10:53:11.269005",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Donation",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "select": 1,
- "share": 1,
- "submit": 1,
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Non Profit Manager",
- "select": 1,
- "share": 1,
- "submit": 1,
- "write": 1
- }
- ],
- "search_fields": "donor_name, email",
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "donor_name",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/donation/donation.py b/erpnext/non_profit/doctype/donation/donation.py
deleted file mode 100644
index 54bc94b755..0000000000
--- a/erpnext/non_profit/doctype/donation/donation.py
+++ /dev/null
@@ -1,220 +0,0 @@
-# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import json
-
-import frappe
-from frappe import _
-from frappe.email import sendmail_to_system_managers
-from frappe.model.document import Document
-from frappe.utils import flt, get_link_to_form, getdate
-
-from erpnext.non_profit.doctype.membership.membership import verify_signature
-
-
-class Donation(Document):
- def validate(self):
- if not self.donor or not frappe.db.exists('Donor', self.donor):
- # for web forms
- user_type = frappe.db.get_value('User', frappe.session.user, 'user_type')
- if user_type == 'Website User':
- self.create_donor_for_website_user()
- else:
- frappe.throw(_('Please select a Member'))
-
- def create_donor_for_website_user(self):
- donor_name = frappe.get_value('Donor', dict(email=frappe.session.user))
-
- if not donor_name:
- user = frappe.get_doc('User', frappe.session.user)
- donor = frappe.get_doc(dict(
- doctype='Donor',
- donor_type=self.get('donor_type'),
- email=frappe.session.user,
- member_name=user.get_fullname()
- )).insert(ignore_permissions=True)
- donor_name = donor.name
-
- if self.get('__islocal'):
- self.donor = donor_name
-
- def on_payment_authorized(self, *args, **kwargs):
- self.load_from_db()
- self.create_payment_entry()
-
- def create_payment_entry(self, date=None):
- settings = frappe.get_doc('Non Profit Settings')
- if not settings.automate_donation_payment_entries:
- return
-
- if not settings.donation_payment_account:
- frappe.throw(_('You need to set Payment Account for Donation in {0}').format(
- get_link_to_form('Non Profit Settings', 'Non Profit Settings')))
-
- from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
-
- frappe.flags.ignore_account_permission = True
- pe = get_payment_entry(dt=self.doctype, dn=self.name)
- frappe.flags.ignore_account_permission = False
- pe.paid_from = settings.donation_debit_account
- pe.paid_to = settings.donation_payment_account
- pe.posting_date = date or getdate()
- pe.reference_no = self.name
- pe.reference_date = date or getdate()
- pe.flags.ignore_mandatory = True
- pe.insert()
- pe.submit()
-
-
-@frappe.whitelist(allow_guest=True)
-def capture_razorpay_donations(*args, **kwargs):
- """
- Creates Donation from Razorpay Webhook Request Data on payment.captured event
- Creates Donor from email if not found
- """
- data = frappe.request.get_data(as_text=True)
-
- try:
- verify_signature(data, endpoint='Donation')
- except Exception as e:
- log = frappe.log_error(e, 'Donation Webhook Verification Error')
- notify_failure(log)
- return { 'status': 'Failed', 'reason': e }
-
- if isinstance(data, str):
- data = json.loads(data)
- data = frappe._dict(data)
-
- payment = data.payload.get('payment', {}).get('entity', {})
- payment = frappe._dict(payment)
-
- try:
- if not data.event == 'payment.captured':
- return
-
- # to avoid capturing subscription payments as donations
- if payment.description and 'subscription' in str(payment.description).lower():
- return
-
- donor = get_donor(payment.email)
- if not donor:
- donor = create_donor(payment)
-
- donation = create_donation(donor, payment)
- donation.run_method('create_payment_entry')
-
- except Exception as e:
- message = '{0}\n\n{1}\n\n{2}: {3}'.format(e, frappe.get_traceback(), _('Payment ID'), payment.id)
- log = frappe.log_error(message, _('Error creating donation entry for {0}').format(donor.name))
- notify_failure(log)
- return { 'status': 'Failed', 'reason': e }
-
- return { 'status': 'Success' }
-
-
-def create_donation(donor, payment):
- if not frappe.db.exists('Mode of Payment', payment.method):
- create_mode_of_payment(payment.method)
-
- company = get_company_for_donations()
- donation = frappe.get_doc({
- 'doctype': 'Donation',
- 'company': company,
- 'donor': donor.name,
- 'donor_name': donor.donor_name,
- 'email': donor.email,
- 'date': getdate(),
- 'amount': flt(payment.amount) / 100, # Convert to rupees from paise
- 'mode_of_payment': payment.method,
- 'razorpay_payment_id': payment.id
- }).insert(ignore_mandatory=True)
-
- donation.submit()
- return donation
-
-
-def get_donor(email):
- donors = frappe.get_all('Donor',
- filters={'email': email},
- order_by='creation desc')
-
- try:
- return frappe.get_doc('Donor', donors[0]['name'])
- except Exception:
- return None
-
-
-@frappe.whitelist()
-def create_donor(payment):
- donor_details = frappe._dict(payment)
- donor_type = frappe.db.get_single_value('Non Profit Settings', 'default_donor_type')
-
- donor = frappe.new_doc('Donor')
- donor.update({
- 'donor_name': donor_details.email,
- 'donor_type': donor_type,
- 'email': donor_details.email,
- 'contact': donor_details.contact
- })
-
- if donor_details.get('notes'):
- donor = get_additional_notes(donor, donor_details)
-
- donor.insert(ignore_mandatory=True)
- return donor
-
-
-def get_company_for_donations():
- company = frappe.db.get_single_value('Non Profit Settings', 'donation_company')
- if not company:
- from erpnext.non_profit.utils import get_company
- company = get_company()
- return company
-
-
-def get_additional_notes(donor, donor_details):
- if type(donor_details.notes) == dict:
- for k, v in donor_details.notes.items():
- notes = '\n'.join('{}: {}'.format(k, v))
-
- # extract donor name from notes
- if 'name' in k.lower():
- donor.update({
- 'donor_name': donor_details.notes.get(k)
- })
-
- # extract pan from notes
- if 'pan' in k.lower():
- donor.update({
- 'pan_number': donor_details.notes.get(k)
- })
-
- donor.add_comment('Comment', notes)
-
- elif type(donor_details.notes) == str:
- donor.add_comment('Comment', donor_details.notes)
-
- return donor
-
-
-def create_mode_of_payment(method):
- frappe.get_doc({
- 'doctype': 'Mode of Payment',
- 'mode_of_payment': method
- }).insert(ignore_mandatory=True)
-
-
-def notify_failure(log):
- try:
- content = '''
- Dear System Manager,
- Razorpay webhook for creating donation failed due to some reason.
- Please check the error log linked below
- Error Log: {0}
- Regards, Administrator
- '''.format(get_link_to_form('Error Log', log.name))
-
- sendmail_to_system_managers(_('[Important] [ERPNext] Razorpay donation webhook failed, please check.'), content)
- except Exception:
- pass
diff --git a/erpnext/non_profit/doctype/donation/donation_dashboard.py b/erpnext/non_profit/doctype/donation/donation_dashboard.py
deleted file mode 100644
index 492ad62171..0000000000
--- a/erpnext/non_profit/doctype/donation/donation_dashboard.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from frappe import _
-
-
-def get_data():
- return {
- 'fieldname': 'donation',
- 'non_standard_fieldnames': {
- 'Payment Entry': 'reference_name'
- },
- 'transactions': [
- {
- 'label': _('Payment'),
- 'items': ['Payment Entry']
- }
- ]
- }
diff --git a/erpnext/non_profit/doctype/donation/test_donation.py b/erpnext/non_profit/doctype/donation/test_donation.py
deleted file mode 100644
index 5fa731a6aa..0000000000
--- a/erpnext/non_profit/doctype/donation/test_donation.py
+++ /dev/null
@@ -1,77 +0,0 @@
-# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-import frappe
-
-from erpnext.non_profit.doctype.donation.donation import create_donation
-
-
-class TestDonation(unittest.TestCase):
- def setUp(self):
- create_donor_type()
- settings = frappe.get_doc('Non Profit Settings')
- settings.company = '_Test Company'
- settings.donation_company = '_Test Company'
- settings.default_donor_type = '_Test Donor'
- settings.automate_donation_payment_entries = 1
- settings.donation_debit_account = 'Debtors - _TC'
- settings.donation_payment_account = 'Cash - _TC'
- settings.creation_user = 'Administrator'
- settings.flags.ignore_permissions = True
- settings.save()
-
- def test_payment_entry_for_donations(self):
- donor = create_donor()
- create_mode_of_payment()
- payment = frappe._dict({
- 'amount': 100,
- 'method': 'Debit Card',
- 'id': 'pay_MeXAmsgeKOhq7O'
- })
- donation = create_donation(donor, payment)
-
- self.assertTrue(donation.name)
-
- # Naive test to check if at all payment entry is generated
- # This method is actually triggered from Payment Gateway
- # In any case if details were missing, this would throw an error
- donation.on_payment_authorized()
- donation.reload()
-
- self.assertEqual(donation.paid, 1)
- self.assertTrue(frappe.db.exists('Payment Entry', {'reference_no': donation.name}))
-
-
-def create_donor_type():
- if not frappe.db.exists('Donor Type', '_Test Donor'):
- frappe.get_doc({
- 'doctype': 'Donor Type',
- 'donor_type': '_Test Donor'
- }).insert()
-
-
-def create_donor():
- donor = frappe.db.exists('Donor', 'donor@test.com')
- if donor:
- return frappe.get_doc('Donor', 'donor@test.com')
- else:
- return frappe.get_doc({
- 'doctype': 'Donor',
- 'donor_name': '_Test Donor',
- 'donor_type': '_Test Donor',
- 'email': 'donor@test.com'
- }).insert()
-
-
-def create_mode_of_payment():
- if not frappe.db.exists('Mode of Payment', 'Debit Card'):
- frappe.get_doc({
- 'doctype': 'Mode of Payment',
- 'mode_of_payment': 'Debit Card',
- 'accounts': [{
- 'company': '_Test Company',
- 'default_account': 'Cash - _TC'
- }]
- }).insert()
diff --git a/erpnext/non_profit/doctype/donor/__init__.py b/erpnext/non_profit/doctype/donor/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/doctype/donor/donor.js b/erpnext/non_profit/doctype/donor/donor.js
deleted file mode 100644
index 090d5af32e..0000000000
--- a/erpnext/non_profit/doctype/donor/donor.js
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Donor', {
- refresh: function(frm) {
- frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Donor'};
-
- frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal);
-
- if(!frm.doc.__islocal) {
- frappe.contacts.render_address_and_contact(frm);
- } else {
- frappe.contacts.clear_address_and_contact(frm);
- }
-
- }
-});
diff --git a/erpnext/non_profit/doctype/donor/donor.json b/erpnext/non_profit/doctype/donor/donor.json
deleted file mode 100644
index 72f24ef922..0000000000
--- a/erpnext/non_profit/doctype/donor/donor.json
+++ /dev/null
@@ -1,110 +0,0 @@
-{
- "actions": [],
- "allow_rename": 1,
- "autoname": "field:email",
- "creation": "2017-09-19 16:20:27.510196",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "donor_name",
- "column_break_5",
- "donor_type",
- "email",
- "image",
- "address_contacts",
- "address_html",
- "column_break_9",
- "contact_html"
- ],
- "fields": [
- {
- "fieldname": "donor_name",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Donor Name",
- "reqd": 1
- },
- {
- "fieldname": "column_break_5",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "donor_type",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Donor Type",
- "options": "Donor Type",
- "reqd": 1
- },
- {
- "fieldname": "email",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Email",
- "reqd": 1,
- "unique": 1
- },
- {
- "fieldname": "image",
- "fieldtype": "Attach Image",
- "hidden": 1,
- "label": "Image",
- "no_copy": 1,
- "print_hide": 1
- },
- {
- "depends_on": "eval:!doc.__islocal;",
- "fieldname": "address_contacts",
- "fieldtype": "Section Break",
- "label": "Address and Contact",
- "options": "fa fa-map-marker"
- },
- {
- "fieldname": "address_html",
- "fieldtype": "HTML",
- "label": "Address HTML"
- },
- {
- "fieldname": "column_break_9",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "contact_html",
- "fieldtype": "HTML",
- "label": "Contact HTML"
- }
- ],
- "image_field": "image",
- "links": [
- {
- "link_doctype": "Donation",
- "link_fieldname": "donor"
- }
- ],
- "modified": "2021-02-17 16:36:33.470731",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Donor",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Non Profit Manager",
- "share": 1,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "restrict_to_domain": "Non Profit",
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "donor_name",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/donor/donor.py b/erpnext/non_profit/doctype/donor/donor.py
deleted file mode 100644
index 058321b159..0000000000
--- a/erpnext/non_profit/doctype/donor/donor.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.contacts.address_and_contact import load_address_and_contact
-from frappe.model.document import Document
-
-
-class Donor(Document):
- def onload(self):
- """Load address and contacts in `__onload`"""
- load_address_and_contact(self)
-
- def validate(self):
- from frappe.utils import validate_email_address
- if self.email:
- validate_email_address(self.email.strip(), True)
diff --git a/erpnext/non_profit/doctype/donor/donor_list.js b/erpnext/non_profit/doctype/donor/donor_list.js
deleted file mode 100644
index 31d4d292e7..0000000000
--- a/erpnext/non_profit/doctype/donor/donor_list.js
+++ /dev/null
@@ -1,3 +0,0 @@
-frappe.listview_settings['Donor'] = {
- add_fields: ["donor_name", "donor_type", "image"],
-};
diff --git a/erpnext/non_profit/doctype/donor/test_donor.py b/erpnext/non_profit/doctype/donor/test_donor.py
deleted file mode 100644
index fe591c8e72..0000000000
--- a/erpnext/non_profit/doctype/donor/test_donor.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestDonor(unittest.TestCase):
- pass
diff --git a/erpnext/non_profit/doctype/donor_type/__init__.py b/erpnext/non_profit/doctype/donor_type/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/doctype/donor_type/donor_type.js b/erpnext/non_profit/doctype/donor_type/donor_type.js
deleted file mode 100644
index 7b1fd4fe89..0000000000
--- a/erpnext/non_profit/doctype/donor_type/donor_type.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Donor Type', {
- refresh: function() {
-
- }
-});
diff --git a/erpnext/non_profit/doctype/donor_type/donor_type.json b/erpnext/non_profit/doctype/donor_type/donor_type.json
deleted file mode 100644
index 07118fdc82..0000000000
--- a/erpnext/non_profit/doctype/donor_type/donor_type.json
+++ /dev/null
@@ -1,94 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:donor_type",
- "beta": 0,
- "creation": "2017-09-19 16:19:16.639635",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "donor_type",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Donor Type",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2017-12-05 07:04:36.757595",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Donor Type",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Non Profit Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Non Profit",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/donor_type/donor_type.py b/erpnext/non_profit/doctype/donor_type/donor_type.py
deleted file mode 100644
index 17dca899d5..0000000000
--- a/erpnext/non_profit/doctype/donor_type/donor_type.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class DonorType(Document):
- pass
diff --git a/erpnext/non_profit/doctype/donor_type/test_donor_type.py b/erpnext/non_profit/doctype/donor_type/test_donor_type.py
deleted file mode 100644
index d433733ee2..0000000000
--- a/erpnext/non_profit/doctype/donor_type/test_donor_type.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestDonorType(unittest.TestCase):
- pass
diff --git a/erpnext/non_profit/doctype/grant_application/__init__.py b/erpnext/non_profit/doctype/grant_application/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/doctype/grant_application/grant_application.js b/erpnext/non_profit/doctype/grant_application/grant_application.js
deleted file mode 100644
index 70f319b828..0000000000
--- a/erpnext/non_profit/doctype/grant_application/grant_application.js
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Grant Application', {
- refresh: function(frm) {
- frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Grant Application'};
-
- frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal);
-
- if(!frm.doc.__islocal) {
- frappe.contacts.render_address_and_contact(frm);
- } else {
- frappe.contacts.clear_address_and_contact(frm);
- }
-
- if(frm.doc.status == 'Received' && !frm.doc.email_notification_sent){
- frm.add_custom_button(__("Send Grant Review Email"), function() {
- frappe.call({
- method: "erpnext.non_profit.doctype.grant_application.grant_application.send_grant_review_emails",
- args: {
- grant_application: frm.doc.name
- }
- });
- });
- }
- }
-});
diff --git a/erpnext/non_profit/doctype/grant_application/grant_application.json b/erpnext/non_profit/doctype/grant_application/grant_application.json
deleted file mode 100644
index 2eb2087925..0000000000
--- a/erpnext/non_profit/doctype/grant_application/grant_application.json
+++ /dev/null
@@ -1,851 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 1,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "",
- "beta": 0,
- "creation": "2017-09-21 12:02:01.206913",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "applicant_type",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Applicant Type",
- "length": 0,
- "no_copy": 0,
- "options": "Individual\nOrganization",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "applicant_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.applicant_type=='Organization'",
- "fieldname": "contact_person",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Contact Person",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "email",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Email",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_5",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Open",
- "fieldname": "status",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Status",
- "length": 0,
- "no_copy": 0,
- "options": "Open\nReceived\nIn Progress\nApproved\nRejected\nExpired\nWithdrawn",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "website_url",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Website URL",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "company",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Company",
- "length": 0,
- "no_copy": 0,
- "options": "Company",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "address_contacts",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Address and Contact",
- "length": 0,
- "no_copy": 0,
- "options": "fa fa-map-marker",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "address_html",
- "fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Address HTML",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_9",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "contact_html",
- "fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Contact HTML",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "grant_application_details",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Grant Application Details ",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "grant_description",
- "fieldtype": "Long Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Grant Description",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_15",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Requested Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "has_any_past_grant_record",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Has any past Grant Record",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_17",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "route",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Route",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "published",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Show on Website",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "assessment_result",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Assessment Result",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "assessment_mark",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Assessment Mark (Out of 10)",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "note",
- "fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Note",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_24",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "assessment_manager",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Assessment Manager",
- "length": 0,
- "no_copy": 0,
- "options": "User",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "email_notification_sent",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Email Notification Sent",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- }
- ],
- "has_web_view": 1,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_field": "",
- "image_view": 0,
- "in_create": 0,
- "is_published_field": "published",
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2017-12-06 12:39:57.677899",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Grant Application",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Non Profit Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Non Profit",
- "route": "grant-application",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "applicant_name",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/grant_application/grant_application.py b/erpnext/non_profit/doctype/grant_application/grant_application.py
deleted file mode 100644
index cc5e1b1442..0000000000
--- a/erpnext/non_profit/doctype/grant_application/grant_application.py
+++ /dev/null
@@ -1,58 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import frappe
-from frappe import _
-from frappe.contacts.address_and_contact import load_address_and_contact
-from frappe.utils import get_url
-from frappe.website.website_generator import WebsiteGenerator
-
-
-class GrantApplication(WebsiteGenerator):
- _website = frappe._dict(
- condition_field = "published",
- )
-
- def validate(self):
- if not self.route: #pylint: disable=E0203
- self.route = 'grant-application/' + self.scrub(self.name)
-
- def onload(self):
- """Load address and contacts in `__onload`"""
- load_address_and_contact(self)
-
- def get_context(self, context):
- context.no_cache = True
- context.show_sidebar = True
- context.parents = [dict(label='View All Grant Applications',
- route='grant-application', title='View Grants')]
-
-def get_list_context(context):
- context.allow_guest = True
- context.no_cache = True
- context.no_breadcrumbs = True
- context.show_sidebar = True
- context.order_by = 'creation desc'
- context.introduction ='''
- Apply for new Grant Application '''
-
-@frappe.whitelist()
-def send_grant_review_emails(grant_application):
- grant = frappe.get_doc("Grant Application", grant_application)
- url = get_url('grant-application/{0}'.format(grant_application))
- frappe.sendmail(
- recipients= grant.assessment_manager,
- sender=frappe.session.user,
- subject='Grant Application for {0}'.format(grant.applicant_name),
- message=' Please Review this grant application
' + url,
- reference_doctype=grant.doctype,
- reference_name=grant.name
- )
-
- grant.status = 'In Progress'
- grant.email_notification_sent = 1
- grant.save()
- frappe.db.commit()
-
- frappe.msgprint(_("Review Invitation Sent"))
diff --git a/erpnext/non_profit/doctype/grant_application/templates/grant_application.html b/erpnext/non_profit/doctype/grant_application/templates/grant_application.html
deleted file mode 100644
index 52e8469284..0000000000
--- a/erpnext/non_profit/doctype/grant_application/templates/grant_application.html
+++ /dev/null
@@ -1,68 +0,0 @@
-{% extends "templates/web.html" %}
-
-{% block page_content %}
- {{ applicant_name }}
- {% if frappe.user == owner %}
- Edit Grant
- {% endif %}
-
-
-
- Organization/Indvidual
- {{ applicant_type }}
-
-
- Grant Applicant Name
- {{ applicant_name}}
-
-
- Date
- {{ frappe.format_date(creation) }}
-
-
- Status
- {{ status }}
-
-
- Email
- {{ email }}
-
-
- Q. Please outline your current situation and why you are applying for a grant?
- {{ grant_description }}
- Q. Requested grant amount
- {{ amount }}
- Q. Have you recevied grant from us before?
- {{ has_any_past_grant_record }}
- Contact
- {% if frappe.user != 'Guest' %}
-
- {% if contact_person %}
-
- Contact Person
- {{ contact_person }}
-
- {% endif %}
-
- Email
- {{ email }}
-
-
- {% else %}
- You must register and login to view contact details
- {% endif %}
-
- {% if frappe.session.user == assessment_manager %}
- {% if assessment_scale %}
- Assessment Review done
- {% endif %}
- {% else %}
- Post a New Grant
- {% endif %}
-{% endblock %}
-{% block style %}
-
-
-{% endblock %}
diff --git a/erpnext/non_profit/doctype/grant_application/templates/grant_application_row.html b/erpnext/non_profit/doctype/grant_application/templates/grant_application_row.html
deleted file mode 100644
index e375b16154..0000000000
--- a/erpnext/non_profit/doctype/grant_application/templates/grant_application_row.html
+++ /dev/null
@@ -1,11 +0,0 @@
-{% if doc.published %}
-
-{% endif %}
diff --git a/erpnext/non_profit/doctype/grant_application/test_grant_application.py b/erpnext/non_profit/doctype/grant_application/test_grant_application.py
deleted file mode 100644
index ef267d7af8..0000000000
--- a/erpnext/non_profit/doctype/grant_application/test_grant_application.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestGrantApplication(unittest.TestCase):
- pass
diff --git a/erpnext/non_profit/doctype/member/__init__.py b/erpnext/non_profit/doctype/member/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/doctype/member/member.js b/erpnext/non_profit/doctype/member/member.js
deleted file mode 100644
index e58ec0f5ee..0000000000
--- a/erpnext/non_profit/doctype/member/member.js
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Member', {
- setup: function(frm) {
- frappe.db.get_single_value('Non Profit Settings', 'enable_razorpay_for_memberships').then(val => {
- if (val && (frm.doc.subscription_id || frm.doc.customer_id)) {
- frm.set_df_property('razorpay_details_section', 'hidden', false);
- }
- })
- },
-
- refresh: function(frm) {
-
- frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Member'};
-
- frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal);
-
- if(!frm.doc.__islocal) {
- frappe.contacts.render_address_and_contact(frm);
-
- // custom buttons
- frm.add_custom_button(__('Accounting Ledger'), function() {
- frappe.set_route('query-report', 'General Ledger',
- {party_type:'Member', party:frm.doc.name});
- });
-
- frm.add_custom_button(__('Accounts Receivable'), function() {
- frappe.set_route('query-report', 'Accounts Receivable', {member:frm.doc.name});
- });
-
- if (!frm.doc.customer) {
- frm.add_custom_button(__('Create Customer'), () => {
- frm.call('make_customer_and_link').then(() => {
- frm.reload_doc();
- });
- });
- }
-
- // indicator
- erpnext.utils.set_party_dashboard_indicators(frm);
-
- } else {
- frappe.contacts.clear_address_and_contact(frm);
- }
-
- frappe.call({
- method:"frappe.client.get_value",
- args:{
- 'doctype':"Membership",
- 'filters':{'member': frm.doc.name},
- 'fieldname':[
- 'to_date'
- ]
- },
- callback: function (data) {
- if(data.message) {
- frappe.model.set_value(frm.doctype,frm.docname,
- "membership_expiry_date", data.message.to_date);
- }
- }
- });
- }
-});
diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json
deleted file mode 100644
index 7c1baf1a8d..0000000000
--- a/erpnext/non_profit/doctype/member/member.json
+++ /dev/null
@@ -1,210 +0,0 @@
-{
- "actions": [],
- "allow_rename": 1,
- "autoname": "naming_series:",
- "creation": "2017-09-11 09:24:52.898356",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "naming_series",
- "member_name",
- "membership_expiry_date",
- "column_break_5",
- "membership_type",
- "email_id",
- "image",
- "customer_section",
- "customer",
- "customer_name",
- "supplier_section",
- "supplier",
- "address_contacts",
- "address_html",
- "column_break_9",
- "contact_html",
- "razorpay_details_section",
- "subscription_id",
- "customer_id",
- "subscription_status",
- "column_break_21",
- "subscription_start",
- "subscription_end"
- ],
- "fields": [
- {
- "fieldname": "naming_series",
- "fieldtype": "Select",
- "label": "Series",
- "options": "NPO-MEM-.YYYY.-",
- "reqd": 1
- },
- {
- "fieldname": "member_name",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Member Name",
- "reqd": 1
- },
- {
- "fieldname": "membership_expiry_date",
- "fieldtype": "Date",
- "label": "Membership Expiry Date"
- },
- {
- "fieldname": "column_break_5",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "membership_type",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Membership Type",
- "options": "Membership Type",
- "reqd": 1
- },
- {
- "fieldname": "image",
- "fieldtype": "Attach Image",
- "hidden": 1,
- "label": "Image",
- "no_copy": 1,
- "print_hide": 1
- },
- {
- "collapsible": 1,
- "fieldname": "customer_section",
- "fieldtype": "Section Break",
- "label": "Customer"
- },
- {
- "fieldname": "customer",
- "fieldtype": "Link",
- "label": "Customer",
- "options": "Customer"
- },
- {
- "fetch_from": "customer.customer_name",
- "fieldname": "customer_name",
- "fieldtype": "Data",
- "label": "Customer Name",
- "read_only": 1
- },
- {
- "collapsible": 1,
- "fieldname": "supplier_section",
- "fieldtype": "Section Break",
- "label": "Supplier"
- },
- {
- "fieldname": "supplier",
- "fieldtype": "Link",
- "label": "Supplier",
- "options": "Supplier"
- },
- {
- "depends_on": "eval:!doc.__islocal;",
- "fieldname": "address_contacts",
- "fieldtype": "Section Break",
- "label": "Address and Contact",
- "options": "fa fa-map-marker"
- },
- {
- "fieldname": "address_html",
- "fieldtype": "HTML",
- "label": "Address HTML"
- },
- {
- "fieldname": "column_break_9",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "contact_html",
- "fieldtype": "HTML",
- "label": "Contact HTML"
- },
- {
- "fieldname": "email_id",
- "fieldtype": "Data",
- "label": "Email Address",
- "options": "Email"
- },
- {
- "fieldname": "subscription_id",
- "fieldtype": "Data",
- "label": "Subscription ID",
- "read_only": 1
- },
- {
- "fieldname": "customer_id",
- "fieldtype": "Data",
- "label": "Customer ID",
- "read_only": 1
- },
- {
- "fieldname": "razorpay_details_section",
- "fieldtype": "Section Break",
- "hidden": 1,
- "label": "Razorpay Details"
- },
- {
- "fieldname": "column_break_21",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "subscription_start",
- "fieldtype": "Date",
- "label": "Subscription Start "
- },
- {
- "fieldname": "subscription_end",
- "fieldtype": "Date",
- "label": "Subscription End"
- },
- {
- "fieldname": "subscription_status",
- "fieldtype": "Select",
- "label": "Subscription Status",
- "options": "\nActive\nHalted"
- }
- ],
- "image_field": "image",
- "links": [],
- "modified": "2021-07-11 14:27:26.368039",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Member",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Non Profit Manager",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Non Profit Member",
- "share": 1,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "restrict_to_domain": "Non Profit",
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "member_name",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py
deleted file mode 100644
index 4d80e57ecc..0000000000
--- a/erpnext/non_profit/doctype/member/member.py
+++ /dev/null
@@ -1,185 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import frappe
-from frappe import _
-from frappe.contacts.address_and_contact import load_address_and_contact
-from frappe.integrations.utils import get_payment_gateway_controller
-from frappe.model.document import Document
-from frappe.utils import cint, get_link_to_form
-
-from erpnext.non_profit.doctype.membership_type.membership_type import get_membership_type
-
-
-class Member(Document):
- def onload(self):
- """Load address and contacts in `__onload`"""
- load_address_and_contact(self)
-
-
- def validate(self):
- if self.email_id:
- self.validate_email_type(self.email_id)
-
- def validate_email_type(self, email):
- from frappe.utils import validate_email_address
- validate_email_address(email.strip(), True)
-
- def setup_subscription(self):
- non_profit_settings = frappe.get_doc('Non Profit Settings')
- if not non_profit_settings.enable_razorpay_for_memberships:
- frappe.throw(_('Please check Enable Razorpay for Memberships in {0} to setup subscription')).format(
- get_link_to_form('Non Profit Settings', 'Non Profit Settings'))
-
- controller = get_payment_gateway_controller("Razorpay")
- settings = controller.get_settings({})
-
- plan_id = frappe.get_value("Membership Type", self.membership_type, "razorpay_plan_id")
-
- if not plan_id:
- frappe.throw(_("Please setup Razorpay Plan ID"))
-
- subscription_details = {
- "plan_id": plan_id,
- "billing_frequency": cint(non_profit_settings.billing_frequency),
- "customer_notify": 1
- }
-
- args = {
- 'subscription_details': subscription_details
- }
-
- subscription = controller.setup_subscription(settings, **args)
-
- return subscription
-
- @frappe.whitelist()
- def make_customer_and_link(self):
- if self.customer:
- frappe.msgprint(_("A customer is already linked to this Member"))
-
- customer = create_customer(frappe._dict({
- 'fullname': self.member_name,
- 'email': self.email_id,
- 'phone': None
- }))
-
- self.customer = customer
- self.save()
- frappe.msgprint(_("Customer {0} has been created succesfully.").format(self.customer))
-
-
-def get_or_create_member(user_details):
- member_list = frappe.get_all("Member", filters={'email': user_details.email, 'membership_type': user_details.plan_id})
- if member_list and member_list[0]:
- return member_list[0]['name']
- else:
- return create_member(user_details)
-
-def create_member(user_details):
- user_details = frappe._dict(user_details)
- member = frappe.new_doc("Member")
- member.update({
- "member_name": user_details.fullname,
- "email_id": user_details.email,
- "pan_number": user_details.pan or None,
- "membership_type": user_details.plan_id,
- "customer_id": user_details.customer_id or None,
- "subscription_id": user_details.subscription_id or None,
- "subscription_status": user_details.subscription_status or ""
- })
-
- member.insert(ignore_permissions=True)
- member.customer = create_customer(user_details, member.name)
- member.save(ignore_permissions=True)
-
- return member
-
-def create_customer(user_details, member=None):
- customer = frappe.new_doc("Customer")
- customer.customer_name = user_details.fullname
- customer.customer_type = "Individual"
- customer.flags.ignore_mandatory = True
- customer.insert(ignore_permissions=True)
-
- try:
- contact = frappe.new_doc("Contact")
- contact.first_name = user_details.fullname
- if user_details.mobile:
- contact.add_phone(user_details.mobile, is_primary_phone=1, is_primary_mobile_no=1)
- if user_details.email:
- contact.add_email(user_details.email, is_primary=1)
- contact.insert(ignore_permissions=True)
-
- contact.append("links", {
- "link_doctype": "Customer",
- "link_name": customer.name
- })
-
- if member:
- contact.append("links", {
- "link_doctype": "Member",
- "link_name": member
- })
-
- contact.save(ignore_permissions=True)
-
- except frappe.DuplicateEntryError:
- return customer.name
-
- except Exception as e:
- frappe.log_error(frappe.get_traceback(), _("Contact Creation Failed"))
- pass
-
- return customer.name
-
-@frappe.whitelist(allow_guest=True)
-def create_member_subscription_order(user_details):
- """Create Member subscription and order for payment
-
- Args:
- user_details (TYPE): Description
-
- Returns:
- Dictionary: Dictionary with subscription details
- {
- 'subscription_details': {
- 'plan_id': 'plan_EXwyxDYDCj3X4v',
- 'billing_frequency': 24,
- 'customer_notify': 1
- },
- 'subscription_id': 'sub_EZycCvXFvqnC6p'
- }
- """
-
- user_details = frappe._dict(user_details)
- member = get_or_create_member(user_details)
-
- subscription = member.setup_subscription()
-
- member.subscription_id = subscription.get('subscription_id')
- member.save(ignore_permissions=True)
-
- return subscription
-
-@frappe.whitelist()
-def register_member(fullname, email, rzpay_plan_id, subscription_id, pan=None, mobile=None):
- plan = get_membership_type(rzpay_plan_id)
- if not plan:
- raise frappe.DoesNotExistError
-
- member = frappe.db.exists("Member", {'email': email, 'subscription_id': subscription_id })
- if member:
- return member
- else:
- member = create_member(dict(
- fullname=fullname,
- email=email,
- plan_id=plan,
- subscription_id=subscription_id,
- pan=pan,
- mobile=mobile
- ))
-
- return member.name
diff --git a/erpnext/non_profit/doctype/member/member_dashboard.py b/erpnext/non_profit/doctype/member/member_dashboard.py
deleted file mode 100644
index 0e31e3ceb8..0000000000
--- a/erpnext/non_profit/doctype/member/member_dashboard.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from frappe import _
-
-
-def get_data():
- return {
- 'heatmap': True,
- 'heatmap_message': _('Member Activity'),
- 'fieldname': 'member',
- 'non_standard_fieldnames': {
- 'Bank Account': 'party'
- },
- 'transactions': [
- {
- 'label': _('Membership Details'),
- 'items': ['Membership']
- },
- {
- 'label': _('Fee'),
- 'items': ['Bank Account']
- }
- ]
- }
diff --git a/erpnext/non_profit/doctype/member/member_list.js b/erpnext/non_profit/doctype/member/member_list.js
deleted file mode 100644
index 8e41e7fdde..0000000000
--- a/erpnext/non_profit/doctype/member/member_list.js
+++ /dev/null
@@ -1,3 +0,0 @@
-frappe.listview_settings['Member'] = {
- add_fields: ["member_name", "membership_type", "image"],
-};
diff --git a/erpnext/non_profit/doctype/member/test_member.py b/erpnext/non_profit/doctype/member/test_member.py
deleted file mode 100644
index 46f14ed131..0000000000
--- a/erpnext/non_profit/doctype/member/test_member.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestMember(unittest.TestCase):
- pass
diff --git a/erpnext/non_profit/doctype/membership/__init__.py b/erpnext/non_profit/doctype/membership/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js
deleted file mode 100644
index 31872048a0..0000000000
--- a/erpnext/non_profit/doctype/membership/membership.js
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Membership', {
- setup: function(frm) {
- frappe.db.get_single_value("Non Profit Settings", "enable_razorpay_for_memberships").then(val => {
- if (val) frm.set_df_property("razorpay_details_section", "hidden", false);
- })
- },
-
- refresh: function(frm) {
- if (frm.doc.__islocal)
- return;
-
- !frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => {
- frm.call({
- doc: frm.doc,
- method: "generate_invoice",
- args: {save: true},
- freeze: true,
- freeze_message: __("Creating Membership Invoice"),
- callback: function(r) {
- if (r.invoice)
- frm.reload_doc();
- }
- });
- });
-
- frappe.db.get_single_value("Non Profit Settings", "send_email").then(val => {
- if (val) frm.add_custom_button("Send Acknowledgement", () => {
- frm.call("send_acknowlement").then(() => {
- frm.reload_doc();
- });
- });
- })
- },
-
- onload: function(frm) {
- frm.add_fetch("membership_type", "amount", "amount");
- }
-});
diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json
deleted file mode 100644
index 11d32f9c2b..0000000000
--- a/erpnext/non_profit/doctype/membership/membership.json
+++ /dev/null
@@ -1,184 +0,0 @@
-{
- "actions": [],
- "autoname": "NPO-MSH-.YYYY.-.#####",
- "creation": "2017-09-11 11:39:18.492184",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "member",
- "member_name",
- "membership_type",
- "column_break_3",
- "company",
- "membership_status",
- "membership_validity_section",
- "from_date",
- "to_date",
- "column_break_8",
- "member_since_date",
- "payment_details",
- "paid",
- "currency",
- "amount",
- "invoice",
- "razorpay_details_section",
- "subscription_id",
- "payment_id"
- ],
- "fields": [
- {
- "fieldname": "member",
- "fieldtype": "Link",
- "label": "Member",
- "options": "Member"
- },
- {
- "fieldname": "membership_type",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Membership Type",
- "options": "Membership Type",
- "reqd": 1
- },
- {
- "fieldname": "column_break_3",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "membership_status",
- "fieldtype": "Select",
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Membership Status",
- "options": "New\nCurrent\nExpired\nPending\nCancelled"
- },
- {
- "fieldname": "membership_validity_section",
- "fieldtype": "Section Break",
- "label": "Validity"
- },
- {
- "fieldname": "from_date",
- "fieldtype": "Date",
- "in_list_view": 1,
- "label": "From",
- "reqd": 1
- },
- {
- "fieldname": "to_date",
- "fieldtype": "Date",
- "in_list_view": 1,
- "label": "To",
- "reqd": 1
- },
- {
- "fieldname": "column_break_8",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "member_since_date",
- "fieldtype": "Date",
- "label": "Member Since"
- },
- {
- "fieldname": "payment_details",
- "fieldtype": "Section Break",
- "label": "Payment Details"
- },
- {
- "default": "0",
- "fieldname": "paid",
- "fieldtype": "Check",
- "label": "Paid"
- },
- {
- "fieldname": "currency",
- "fieldtype": "Link",
- "label": "Currency",
- "options": "Currency"
- },
- {
- "fieldname": "amount",
- "fieldtype": "Float",
- "label": "Amount"
- },
- {
- "fieldname": "razorpay_details_section",
- "fieldtype": "Section Break",
- "hidden": 1,
- "label": "Razorpay Details"
- },
- {
- "fieldname": "subscription_id",
- "fieldtype": "Data",
- "label": "Subscription ID",
- "read_only": 1
- },
- {
- "fieldname": "payment_id",
- "fieldtype": "Data",
- "label": "Payment ID",
- "read_only": 1
- },
- {
- "fieldname": "invoice",
- "fieldtype": "Link",
- "label": "Invoice",
- "options": "Sales Invoice"
- },
- {
- "fetch_from": "member.member_name",
- "fieldname": "member_name",
- "fieldtype": "Data",
- "label": "Member Name",
- "read_only": 1
- },
- {
- "fieldname": "company",
- "fieldtype": "Link",
- "label": "Company",
- "options": "Company",
- "reqd": 1
- }
- ],
- "index_web_pages_for_search": 1,
- "links": [],
- "modified": "2021-02-19 14:33:44.925122",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Membership",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Non Profit Manager",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Non Profit Member",
- "share": 1,
- "write": 1
- }
- ],
- "restrict_to_domain": "Non Profit",
- "search_fields": "member, member_name",
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "member_name",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership/membership_list.js b/erpnext/non_profit/doctype/membership/membership_list.js
deleted file mode 100644
index a959159899..0000000000
--- a/erpnext/non_profit/doctype/membership/membership_list.js
+++ /dev/null
@@ -1,15 +0,0 @@
-frappe.listview_settings['Membership'] = {
- get_indicator: function(doc) {
- if (doc.membership_status == 'New') {
- return [__('New'), 'blue', 'membership_status,=,New'];
- } else if (doc.membership_status === 'Current') {
- return [__('Current'), 'green', 'membership_status,=,Current'];
- } else if (doc.membership_status === 'Pending') {
- return [__('Pending'), 'yellow', 'membership_status,=,Pending'];
- } else if (doc.membership_status === 'Expired') {
- return [__('Expired'), 'grey', 'membership_status,=,Expired'];
- } else {
- return [__('Cancelled'), 'red', 'membership_status,=,Cancelled'];
- }
- }
-};
diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py
deleted file mode 100644
index fbe344c6a1..0000000000
--- a/erpnext/non_profit/doctype/membership/test_membership.py
+++ /dev/null
@@ -1,164 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-import frappe
-from frappe.utils import add_months, nowdate
-
-import erpnext
-from erpnext.non_profit.doctype.member.member import create_member
-from erpnext.non_profit.doctype.membership.membership import update_halted_razorpay_subscription
-
-
-class TestMembership(unittest.TestCase):
- def setUp(self):
- plan = setup_membership()
-
- # make test member
- self.member_doc = create_member(
- frappe._dict({
- "fullname": "_Test_Member",
- "email": "_test_member_erpnext@example.com",
- "plan_id": plan.name,
- "subscription_id": "sub_DEX6xcJ1HSW4CR",
- "customer_id": "cust_C0WlbKhp3aLA7W",
- "subscription_status": "Active"
- })
- )
- self.member_doc.make_customer_and_link()
- self.member = self.member_doc.name
-
- def test_auto_generate_invoice_and_payment_entry(self):
- entry = make_membership(self.member)
-
- # Naive test to see if at all invoice was generated and attached to member
- # In any case if details were missing, the invoicing would throw an error
- invoice = entry.generate_invoice(save=True)
- self.assertEqual(invoice.name, entry.invoice)
-
- def test_renew_within_30_days(self):
- # create a membership for two months
- # Should work fine
- make_membership(self.member, { "from_date": nowdate() })
- make_membership(self.member, { "from_date": add_months(nowdate(), 1) })
-
- from frappe.utils.user import add_role
- add_role("test@example.com", "Non Profit Manager")
- frappe.set_user("test@example.com")
-
- # create next membership with expiry not within 30 days
- self.assertRaises(frappe.ValidationError, make_membership, self.member, {
- "from_date": add_months(nowdate(), 2),
- })
-
- frappe.set_user("Administrator")
- # create the same membership but as administrator
- make_membership(self.member, {
- "from_date": add_months(nowdate(), 2),
- "to_date": add_months(nowdate(), 3),
- })
-
- def test_halted_memberships(self):
- make_membership(self.member, {
- "from_date": add_months(nowdate(), 2),
- "to_date": add_months(nowdate(), 3)
- })
-
- self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Active")
- payload = get_subscription_payload()
- update_halted_razorpay_subscription(data=payload)
- self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Halted")
-
- def tearDown(self):
- frappe.db.rollback()
-
-def set_config(key, value):
- frappe.db.set_value("Non Profit Settings", None, key, value)
-
-def make_membership(member, payload={}):
- data = {
- "doctype": "Membership",
- "member": member,
- "membership_status": "Current",
- "membership_type": "_rzpy_test_milythm",
- "currency": "INR",
- "paid": 1,
- "from_date": nowdate(),
- "amount": 100
- }
- data.update(payload)
- membership = frappe.get_doc(data)
- membership.insert(ignore_permissions=True, ignore_if_duplicate=True)
- return membership
-
-def create_item(item_code):
- if not frappe.db.exists("Item", item_code):
- item = frappe.new_doc("Item")
- item.item_code = item_code
- item.item_name = item_code
- item.stock_uom = "Nos"
- item.description = item_code
- item.item_group = "All Item Groups"
- item.is_stock_item = 0
- item.save()
- else:
- item = frappe.get_doc("Item", item_code)
- return item
-
-def setup_membership():
- # Get default company
- company = frappe.get_doc("Company", erpnext.get_default_company())
-
- # update non profit settings
- settings = frappe.get_doc("Non Profit Settings")
- # Enable razorpay
- settings.enable_razorpay_for_memberships = 1
- settings.billing_cycle = "Monthly"
- settings.billing_frequency = 24
- # Enable invoicing
- settings.allow_invoicing = 1
- settings.automate_membership_payment_entries = 1
- settings.company = company.name
- settings.donation_company = company.name
- settings.membership_payment_account = company.default_cash_account
- settings.membership_debit_account = company.default_receivable_account
- settings.flags.ignore_mandatory = True
- settings.save()
-
- # make test plan
- if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"):
- plan = frappe.new_doc("Membership Type")
- plan.membership_type = "_rzpy_test_milythm"
- plan.amount = 100
- plan.razorpay_plan_id = "_rzpy_test_milythm"
- plan.linked_item = create_item("_Test Item for Non Profit Membership").name
- plan.insert()
- else:
- plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
-
- return plan
-
-def get_subscription_payload():
- return {
- "entity": "event",
- "account_id": "acc_BFQ7uQEaa7j2z7",
- "event": "subscription.halted",
- "contains": [
- "subscription"
- ],
- "payload": {
- "subscription": {
- "entity": {
- "id": "sub_DEX6xcJ1HSW4CR",
- "entity": "subscription",
- "plan_id": "_rzpy_test_milythm",
- "customer_id": "cust_C0WlbKhp3aLA7W",
- "status": "halted",
- "notes": {
- "Important": "Notes for Internal Reference"
- },
- }
- }
- }
- }
diff --git a/erpnext/non_profit/doctype/membership_type/__init__.py b/erpnext/non_profit/doctype/membership_type/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.js b/erpnext/non_profit/doctype/membership_type/membership_type.js
deleted file mode 100644
index 2f2427629c..0000000000
--- a/erpnext/non_profit/doctype/membership_type/membership_type.js
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Membership Type', {
- refresh: function (frm) {
- frappe.db.get_single_value('Non Profit Settings', 'enable_razorpay_for_memberships').then(val => {
- if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false);
- });
-
- frappe.db.get_single_value('Non Profit Settings', 'allow_invoicing').then(val => {
- if (val) frm.set_df_property('linked_item', 'hidden', false);
- });
-
- frm.set_query('linked_item', () => {
- return {
- filters: {
- is_stock_item: 0
- }
- };
- });
- }
-});
diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.json b/erpnext/non_profit/doctype/membership_type/membership_type.json
deleted file mode 100644
index 6ce1ecde12..0000000000
--- a/erpnext/non_profit/doctype/membership_type/membership_type.json
+++ /dev/null
@@ -1,71 +0,0 @@
-{
- "actions": [],
- "autoname": "field:membership_type",
- "creation": "2017-09-18 12:56:56.343999",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "membership_type",
- "amount",
- "razorpay_plan_id",
- "linked_item"
- ],
- "fields": [
- {
- "fieldname": "membership_type",
- "fieldtype": "Data",
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Membership Type",
- "reqd": 1,
- "unique": 1
- },
- {
- "fieldname": "amount",
- "fieldtype": "Float",
- "in_list_view": 1,
- "label": "Amount",
- "reqd": 1
- },
- {
- "fieldname": "razorpay_plan_id",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Razorpay Plan ID",
- "unique": 1
- },
- {
- "fieldname": "linked_item",
- "fieldtype": "Link",
- "label": "Linked Item",
- "options": "Item",
- "unique": 1
- }
- ],
- "links": [],
- "modified": "2020-08-05 15:21:43.595745",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Membership Type",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Non Profit Manager",
- "share": 1,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "restrict_to_domain": "Non Profit",
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.py b/erpnext/non_profit/doctype/membership_type/membership_type.py
deleted file mode 100644
index b446421571..0000000000
--- a/erpnext/non_profit/doctype/membership_type/membership_type.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import frappe
-from frappe import _
-from frappe.model.document import Document
-
-
-class MembershipType(Document):
- def validate(self):
- if self.linked_item:
- is_stock_item = frappe.db.get_value("Item", self.linked_item, "is_stock_item")
- if is_stock_item:
- frappe.throw(_("The Linked Item should be a service item"))
-
-def get_membership_type(razorpay_id):
- return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id})
diff --git a/erpnext/non_profit/doctype/membership_type/test_membership_type.py b/erpnext/non_profit/doctype/membership_type/test_membership_type.py
deleted file mode 100644
index 98bc087acd..0000000000
--- a/erpnext/non_profit/doctype/membership_type/test_membership_type.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestMembershipType(unittest.TestCase):
- pass
diff --git a/erpnext/non_profit/doctype/non_profit_settings/__init__.py b/erpnext/non_profit/doctype/non_profit_settings/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js
deleted file mode 100644
index 4c4ca9834b..0000000000
--- a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js
+++ /dev/null
@@ -1,133 +0,0 @@
-// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on("Non Profit Settings", {
- refresh: function(frm) {
- frm.set_query("inv_print_format", function() {
- return {
- filters: {
- "doc_type": "Sales Invoice"
- }
- };
- });
-
- frm.set_query("membership_print_format", function() {
- return {
- filters: {
- "doc_type": "Membership"
- }
- };
- });
-
- frm.set_query("membership_debit_account", function() {
- return {
- filters: {
- "account_type": "Receivable",
- "is_group": 0,
- "company": frm.doc.company
- }
- };
- });
-
- frm.set_query("donation_debit_account", function() {
- return {
- filters: {
- "account_type": "Receivable",
- "is_group": 0,
- "company": frm.doc.donation_company
- }
- };
- });
-
- frm.set_query("membership_payment_account", function () {
- var account_types = ["Bank", "Cash"];
- return {
- filters: {
- "account_type": ["in", account_types],
- "is_group": 0,
- "company": frm.doc.company
- }
- };
- });
-
- frm.set_query("donation_payment_account", function () {
- var account_types = ["Bank", "Cash"];
- return {
- filters: {
- "account_type": ["in", account_types],
- "is_group": 0,
- "company": frm.doc.donation_company
- }
- };
- });
-
- let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership";
-
- frm.set_intro(__("You can learn more about memberships in the manual. ") + `${__('ERPNext Docs')} `, true);
- frm.trigger("setup_buttons_for_membership");
- frm.trigger("setup_buttons_for_donation");
- },
-
- setup_buttons_for_membership: function(frm) {
- let label;
-
- if (frm.doc.membership_webhook_secret) {
-
- frm.add_custom_button(__("Copy Webhook URL"), () => {
- frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.membership.membership.trigger_razorpay_subscription`);
- }, __("Memberships"));
-
- frm.add_custom_button(__("Revoke Key"), () => {
- frm.call("revoke_key", {
- key: "membership_webhook_secret"
- }).then(() => {
- frm.refresh();
- });
- }, __("Memberships"));
-
- label = __("Regenerate Webhook Secret");
-
- } else {
- label = __("Generate Webhook Secret");
- }
-
- frm.add_custom_button(label, () => {
- frm.call("generate_webhook_secret", {
- field: "membership_webhook_secret"
- }).then(() => {
- frm.refresh();
- });
- }, __("Memberships"));
- },
-
- setup_buttons_for_donation: function(frm) {
- let label;
-
- if (frm.doc.donation_webhook_secret) {
- label = __("Regenerate Webhook Secret");
-
- frm.add_custom_button(__("Copy Webhook URL"), () => {
- frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.donation.donation.capture_razorpay_donations`);
- }, __("Donations"));
-
- frm.add_custom_button(__("Revoke Key"), () => {
- frm.call("revoke_key", {
- key: "donation_webhook_secret"
- }).then(() => {
- frm.refresh();
- });
- }, __("Donations"));
-
- } else {
- label = __("Generate Webhook Secret");
- }
-
- frm.add_custom_button(label, () => {
- frm.call("generate_webhook_secret", {
- field: "donation_webhook_secret"
- }).then(() => {
- frm.refresh();
- });
- }, __("Donations"));
- }
-});
diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json
deleted file mode 100644
index 25ff0c1bb0..0000000000
--- a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json
+++ /dev/null
@@ -1,273 +0,0 @@
-{
- "actions": [],
- "creation": "2020-03-29 12:57:03.005120",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "enable_razorpay_for_memberships",
- "razorpay_settings_section",
- "billing_cycle",
- "billing_frequency",
- "membership_webhook_secret",
- "column_break_6",
- "allow_invoicing",
- "automate_membership_invoicing",
- "automate_membership_payment_entries",
- "company",
- "membership_debit_account",
- "membership_payment_account",
- "column_break_9",
- "send_email",
- "send_invoice",
- "membership_print_format",
- "inv_print_format",
- "email_template",
- "donation_settings_section",
- "donation_company",
- "default_donor_type",
- "donation_webhook_secret",
- "column_break_22",
- "automate_donation_payment_entries",
- "donation_debit_account",
- "donation_payment_account",
- "section_break_27",
- "creation_user"
- ],
- "fields": [
- {
- "fieldname": "billing_cycle",
- "fieldtype": "Select",
- "label": "Billing Cycle",
- "options": "Monthly\nYearly"
- },
- {
- "depends_on": "eval:doc.enable_razorpay_for_memberships",
- "fieldname": "razorpay_settings_section",
- "fieldtype": "Section Break",
- "label": "RazorPay Settings for Memberships"
- },
- {
- "description": "The number of billing cycles for which the customer should be charged. For example, if a customer is buying a 1-year membership that should be billed on a monthly basis, this value should be 12.",
- "fieldname": "billing_frequency",
- "fieldtype": "Int",
- "label": "Billing Frequency"
- },
- {
- "fieldname": "column_break_6",
- "fieldtype": "Section Break",
- "label": "Membership Invoicing"
- },
- {
- "fieldname": "column_break_9",
- "fieldtype": "Column Break"
- },
- {
- "description": "This company will be set for the Memberships created via webhook.",
- "fieldname": "company",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Company",
- "options": "Company",
- "reqd": 1
- },
- {
- "default": "0",
- "depends_on": "eval:doc.allow_invoicing && doc.send_email",
- "fieldname": "send_invoice",
- "fieldtype": "Check",
- "label": "Send Invoice with Email"
- },
- {
- "default": "0",
- "fieldname": "send_email",
- "fieldtype": "Check",
- "label": "Send Membership Acknowledgement"
- },
- {
- "depends_on": "eval: doc.send_invoice",
- "fieldname": "inv_print_format",
- "fieldtype": "Link",
- "label": "Invoice Print Format",
- "mandatory_depends_on": "eval: doc.send_invoice",
- "options": "Print Format"
- },
- {
- "depends_on": "eval:doc.send_email",
- "fieldname": "membership_print_format",
- "fieldtype": "Link",
- "label": "Membership Print Format",
- "options": "Print Format"
- },
- {
- "depends_on": "eval:doc.send_email",
- "fieldname": "email_template",
- "fieldtype": "Link",
- "label": "Email Template",
- "mandatory_depends_on": "eval:doc.send_email",
- "options": "Email Template"
- },
- {
- "default": "0",
- "fieldname": "allow_invoicing",
- "fieldtype": "Check",
- "label": "Allow Invoicing for Memberships",
- "mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.allow_invoicing",
- "description": "Automatically create an invoice when payment is authorized from a web form entry",
- "fieldname": "automate_membership_invoicing",
- "fieldtype": "Check",
- "label": "Automate Invoicing for Web Forms"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.allow_invoicing",
- "description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.",
- "fieldname": "automate_membership_payment_entries",
- "fieldtype": "Check",
- "label": "Automate Payment Entry Creation"
- },
- {
- "default": "0",
- "fieldname": "enable_razorpay_for_memberships",
- "fieldtype": "Check",
- "label": "Enable RazorPay For Memberships"
- },
- {
- "depends_on": "eval:doc.automate_membership_payment_entries",
- "description": "Account for accepting membership payments",
- "fieldname": "membership_payment_account",
- "fieldtype": "Link",
- "label": "Membership Payment To",
- "mandatory_depends_on": "eval:doc.automate_membership_payment_entries",
- "options": "Account"
- },
- {
- "fieldname": "membership_webhook_secret",
- "fieldtype": "Password",
- "label": "Membership Webhook Secret",
- "read_only": 1
- },
- {
- "fieldname": "donation_webhook_secret",
- "fieldtype": "Password",
- "label": "Donation Webhook Secret",
- "read_only": 1
- },
- {
- "depends_on": "automate_donation_payment_entries",
- "description": "Account for accepting donation payments",
- "fieldname": "donation_payment_account",
- "fieldtype": "Link",
- "label": "Donation Payment To",
- "mandatory_depends_on": "automate_donation_payment_entries",
- "options": "Account"
- },
- {
- "default": "0",
- "description": "Auto creates Payment Entry for Donations created from web forms.",
- "fieldname": "automate_donation_payment_entries",
- "fieldtype": "Check",
- "label": "Automate Donation Payment Entries"
- },
- {
- "depends_on": "eval:doc.allow_invoicing",
- "fieldname": "membership_debit_account",
- "fieldtype": "Link",
- "label": "Debit Account",
- "mandatory_depends_on": "eval:doc.allow_invoicing",
- "options": "Account"
- },
- {
- "depends_on": "automate_donation_payment_entries",
- "fieldname": "donation_debit_account",
- "fieldtype": "Link",
- "label": "Debit Account",
- "mandatory_depends_on": "automate_donation_payment_entries",
- "options": "Account"
- },
- {
- "description": "This company will be set for the Donations created via webhook.",
- "fieldname": "donation_company",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Company",
- "options": "Company",
- "reqd": 1
- },
- {
- "fieldname": "donation_settings_section",
- "fieldtype": "Section Break",
- "label": "Donation Settings"
- },
- {
- "fieldname": "column_break_22",
- "fieldtype": "Column Break"
- },
- {
- "description": "This Donor Type will be set for the Donor created via Donation web form entry.",
- "fieldname": "default_donor_type",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Default Donor Type",
- "options": "Donor Type",
- "reqd": 1
- },
- {
- "fieldname": "section_break_27",
- "fieldtype": "Section Break"
- },
- {
- "description": "The user that will be used to create Donations, Memberships, Invoices, and Payment Entries. This user should have the relevant permissions.",
- "fieldname": "creation_user",
- "fieldtype": "Link",
- "label": "Creation User",
- "options": "User",
- "reqd": 1
- }
- ],
- "index_web_pages_for_search": 1,
- "issingle": 1,
- "links": [],
- "modified": "2021-03-11 10:43:38.124240",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Non Profit Settings",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "Non Profit Manager",
- "share": 1,
- "write": 1
- },
- {
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "Non Profit Member",
- "share": 1
- }
- ],
- "quick_entry": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py
deleted file mode 100644
index ace6605542..0000000000
--- a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import frappe
-from frappe import _
-from frappe.integrations.utils import get_payment_gateway_controller
-from frappe.model.document import Document
-
-
-class NonProfitSettings(Document):
- @frappe.whitelist()
- def generate_webhook_secret(self, field="membership_webhook_secret"):
- key = frappe.generate_hash(length=20)
- self.set(field, key)
- self.save()
-
- secret_for = "Membership" if field == "membership_webhook_secret" else "Donation"
-
- frappe.msgprint(
- _("Here is your webhook secret for {0} API, this will be shown to you only once.").format(secret_for) + " " + key,
- _("Webhook Secret")
- )
-
- @frappe.whitelist()
- def revoke_key(self, key):
- self.set(key, None)
- self.save()
-
- def get_webhook_secret(self, endpoint="Membership"):
- fieldname = "membership_webhook_secret" if endpoint == "Membership" else "donation_webhook_secret"
- return self.get_password(fieldname=fieldname, raise_exception=False)
-
-@frappe.whitelist()
-def get_plans_for_membership(*args, **kwargs):
- controller = get_payment_gateway_controller("Razorpay")
- plans = controller.get_plans()
- return [plan.get("item") for plan in plans.get("items")]
diff --git a/erpnext/non_profit/doctype/non_profit_settings/test_non_profit_settings.py b/erpnext/non_profit/doctype/non_profit_settings/test_non_profit_settings.py
deleted file mode 100644
index 51d1ba02eb..0000000000
--- a/erpnext/non_profit/doctype/non_profit_settings/test_non_profit_settings.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-# import frappe
-import unittest
-
-
-class TestNonProfitSettings(unittest.TestCase):
- pass
diff --git a/erpnext/non_profit/doctype/volunteer/__init__.py b/erpnext/non_profit/doctype/volunteer/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/doctype/volunteer/test_volunteer.py b/erpnext/non_profit/doctype/volunteer/test_volunteer.py
deleted file mode 100644
index 0a0ab2cf34..0000000000
--- a/erpnext/non_profit/doctype/volunteer/test_volunteer.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestVolunteer(unittest.TestCase):
- pass
diff --git a/erpnext/non_profit/doctype/volunteer/volunteer.js b/erpnext/non_profit/doctype/volunteer/volunteer.js
deleted file mode 100644
index ac93d8c801..0000000000
--- a/erpnext/non_profit/doctype/volunteer/volunteer.js
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Volunteer', {
- refresh: function(frm) {
-
- frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Volunteer'};
-
- frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal);
-
- if(!frm.doc.__islocal) {
- frappe.contacts.render_address_and_contact(frm);
- } else {
- frappe.contacts.clear_address_and_contact(frm);
- }
- }
-});
diff --git a/erpnext/non_profit/doctype/volunteer/volunteer.json b/erpnext/non_profit/doctype/volunteer/volunteer.json
deleted file mode 100644
index 08b7f87b2a..0000000000
--- a/erpnext/non_profit/doctype/volunteer/volunteer.json
+++ /dev/null
@@ -1,148 +0,0 @@
-{
- "actions": [],
- "allow_rename": 1,
- "autoname": "field:email",
- "creation": "2017-09-19 16:16:45.676019",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "volunteer_name",
- "column_break_5",
- "volunteer_type",
- "email",
- "image",
- "address_contacts",
- "address_html",
- "column_break_9",
- "contact_html",
- "volunteer_availability_and_skills_details",
- "availability",
- "availability_timeslot",
- "column_break_12",
- "volunteer_skills",
- "section_break_15",
- "note"
- ],
- "fields": [
- {
- "fieldname": "volunteer_name",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Volunteer Name",
- "reqd": 1
- },
- {
- "fieldname": "column_break_5",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "volunteer_type",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Volunteer Type",
- "options": "Volunteer Type",
- "reqd": 1
- },
- {
- "fieldname": "email",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Email",
- "reqd": 1,
- "unique": 1
- },
- {
- "fieldname": "image",
- "fieldtype": "Attach Image",
- "hidden": 1,
- "label": "Image",
- "no_copy": 1,
- "print_hide": 1
- },
- {
- "depends_on": "eval:!doc.__islocal;",
- "fieldname": "address_contacts",
- "fieldtype": "Section Break",
- "label": "Address and Contact",
- "options": "fa fa-map-marker"
- },
- {
- "fieldname": "address_html",
- "fieldtype": "HTML",
- "label": "Address HTML"
- },
- {
- "fieldname": "column_break_9",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "contact_html",
- "fieldtype": "HTML",
- "label": "Contact HTML"
- },
- {
- "fieldname": "volunteer_availability_and_skills_details",
- "fieldtype": "Section Break",
- "label": "Availability and Skills"
- },
- {
- "fieldname": "availability",
- "fieldtype": "Select",
- "label": "Availability",
- "options": "\nWeekly\nWeekdays\nWeekends"
- },
- {
- "fieldname": "availability_timeslot",
- "fieldtype": "Select",
- "label": "Availability Timeslot",
- "options": "\nMorning\nAfternoon\nEvening\nAnytime"
- },
- {
- "fieldname": "column_break_12",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "volunteer_skills",
- "fieldtype": "Table",
- "label": "Volunteer Skills",
- "options": "Volunteer Skill"
- },
- {
- "fieldname": "section_break_15",
- "fieldtype": "Section Break"
- },
- {
- "fieldname": "note",
- "fieldtype": "Long Text",
- "label": "Note"
- }
- ],
- "image_field": "image",
- "links": [],
- "modified": "2020-09-16 23:45:15.595952",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Volunteer",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Non Profit Manager",
- "share": 1,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "restrict_to_domain": "Non Profit",
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "volunteer_name",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/volunteer/volunteer.py b/erpnext/non_profit/doctype/volunteer/volunteer.py
deleted file mode 100644
index b44d67dae3..0000000000
--- a/erpnext/non_profit/doctype/volunteer/volunteer.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.contacts.address_and_contact import load_address_and_contact
-from frappe.model.document import Document
-
-
-class Volunteer(Document):
- def onload(self):
- """Load address and contacts in `__onload`"""
- load_address_and_contact(self)
diff --git a/erpnext/non_profit/doctype/volunteer_skill/__init__.py b/erpnext/non_profit/doctype/volunteer_skill/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/doctype/volunteer_skill/volunteer_skill.json b/erpnext/non_profit/doctype/volunteer_skill/volunteer_skill.json
deleted file mode 100644
index 7d210aa7bd..0000000000
--- a/erpnext/non_profit/doctype/volunteer_skill/volunteer_skill.json
+++ /dev/null
@@ -1,73 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-09-20 15:26:26.453435",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "volunteer_skill",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Volunteer Skill",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2017-12-06 11:54:14.396354",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Volunteer Skill",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Non Profit",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/volunteer_skill/volunteer_skill.py b/erpnext/non_profit/doctype/volunteer_skill/volunteer_skill.py
deleted file mode 100644
index fe7251876d..0000000000
--- a/erpnext/non_profit/doctype/volunteer_skill/volunteer_skill.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class VolunteerSkill(Document):
- pass
diff --git a/erpnext/non_profit/doctype/volunteer_type/__init__.py b/erpnext/non_profit/doctype/volunteer_type/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/doctype/volunteer_type/test_volunteer_type.py b/erpnext/non_profit/doctype/volunteer_type/test_volunteer_type.py
deleted file mode 100644
index cef27c83a5..0000000000
--- a/erpnext/non_profit/doctype/volunteer_type/test_volunteer_type.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestVolunteerType(unittest.TestCase):
- pass
diff --git a/erpnext/non_profit/doctype/volunteer_type/volunteer_type.js b/erpnext/non_profit/doctype/volunteer_type/volunteer_type.js
deleted file mode 100644
index 5c17505be9..0000000000
--- a/erpnext/non_profit/doctype/volunteer_type/volunteer_type.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Volunteer Type', {
- refresh: function() {
-
- }
-});
diff --git a/erpnext/non_profit/doctype/volunteer_type/volunteer_type.json b/erpnext/non_profit/doctype/volunteer_type/volunteer_type.json
deleted file mode 100644
index 256b25fe91..0000000000
--- a/erpnext/non_profit/doctype/volunteer_type/volunteer_type.json
+++ /dev/null
@@ -1,94 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "prompt",
- "beta": 0,
- "creation": "2017-09-19 16:13:07.763273",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "amount",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2017-12-06 11:52:08.800425",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Volunteer Type",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Non Profit Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Non Profit",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/volunteer_type/volunteer_type.py b/erpnext/non_profit/doctype/volunteer_type/volunteer_type.py
deleted file mode 100644
index 3b1ae1a88e..0000000000
--- a/erpnext/non_profit/doctype/volunteer_type/volunteer_type.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class VolunteerType(Document):
- pass
diff --git a/erpnext/non_profit/report/__init__.py b/erpnext/non_profit/report/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/report/expiring_memberships/__init__.py b/erpnext/non_profit/report/expiring_memberships/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/report/expiring_memberships/expiring_memberships.js b/erpnext/non_profit/report/expiring_memberships/expiring_memberships.js
deleted file mode 100644
index be3a2438fc..0000000000
--- a/erpnext/non_profit/report/expiring_memberships/expiring_memberships.js
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-/* eslint-disable */
-
-frappe.query_reports["Expiring Memberships"] = {
- "filters": [
- {
- "fieldname": "fiscal_year",
- "label": __("Fiscal Year"),
- "fieldtype": "Link",
- "options": "Fiscal Year",
- "default": frappe.defaults.get_user_default("fiscal_year"),
- "reqd": 1
- },
- {
- "fieldname":"month",
- "label": __("Month"),
- "fieldtype": "Select",
- "options": "Jan\nFeb\nMar\nApr\nMay\nJun\nJul\nAug\nSep\nOct\nNov\nDec",
- "default": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov",
- "Dec"][frappe.datetime.str_to_obj(frappe.datetime.get_today()).getMonth()],
- }
- ]
-}
diff --git a/erpnext/non_profit/report/expiring_memberships/expiring_memberships.json b/erpnext/non_profit/report/expiring_memberships/expiring_memberships.json
deleted file mode 100644
index c311057201..0000000000
--- a/erpnext/non_profit/report/expiring_memberships/expiring_memberships.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "add_total_row": 0,
- "apply_user_permissions": 1,
- "creation": "2018-05-24 11:44:08.942809",
- "disabled": 0,
- "docstatus": 0,
- "doctype": "Report",
- "idx": 0,
- "is_standard": "Yes",
- "letter_head": "ERPNext Foundation",
- "modified": "2018-05-24 11:44:08.942809",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Expiring Memberships",
- "owner": "Administrator",
- "ref_doctype": "Membership",
- "report_name": "Expiring Memberships",
- "report_type": "Script Report",
- "roles": [
- {
- "role": "Non Profit Manager"
- },
- {
- "role": "Non Profit Member"
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/report/expiring_memberships/expiring_memberships.py b/erpnext/non_profit/report/expiring_memberships/expiring_memberships.py
deleted file mode 100644
index 3ddbfdc3b0..0000000000
--- a/erpnext/non_profit/report/expiring_memberships/expiring_memberships.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import frappe
-from frappe import _
-
-
-def execute(filters=None):
- columns = get_columns(filters)
- data = get_data(filters)
- return columns, data
-
-def get_columns(filters):
- return [
- _("Membership Type") + ":Link/Membership Type:100", _("Membership ID") + ":Link/Membership:140",
- _("Member ID") + ":Link/Member:140", _("Member Name") + ":Data:140", _("Email") + ":Data:140",
- _("Expiring On") + ":Date:120"
- ]
-
-def get_data(filters):
-
- filters["month"] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"].index(filters.month) + 1
-
- return frappe.db.sql("""
- select ms.membership_type,ms.name,m.name,m.member_name,m.email,ms.max_membership_date
- from `tabMember` m
- inner join (select name,membership_type,max(to_date) as max_membership_date,member
- from `tabMembership`
- where paid = 1
- group by member
- order by max_membership_date asc) ms
- on m.name = ms.member
- where month(max_membership_date) = %(month)s and year(max_membership_date) = %(year)s """,{'month': filters.get('month'),'year':filters.get('fiscal_year')})
diff --git a/erpnext/non_profit/utils.py b/erpnext/non_profit/utils.py
deleted file mode 100644
index 47ea5f5783..0000000000
--- a/erpnext/non_profit/utils.py
+++ /dev/null
@@ -1,12 +0,0 @@
-import frappe
-
-
-def get_company():
- company = frappe.defaults.get_defaults().company
- if company:
- return company
- else:
- company = frappe.get_list("Company", limit=1)
- if company:
- return company[0].name
- return None
diff --git a/erpnext/non_profit/web_form/__init__.py b/erpnext/non_profit/web_form/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/web_form/certification_application/__init__.py b/erpnext/non_profit/web_form/certification_application/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/web_form/certification_application/certification_application.js b/erpnext/non_profit/web_form/certification_application/certification_application.js
deleted file mode 100644
index 8b455edafa..0000000000
--- a/erpnext/non_profit/web_form/certification_application/certification_application.js
+++ /dev/null
@@ -1,16 +0,0 @@
-frappe.ready(function() {
- // bind events here
- $(".page-header-actions-block .btn-primary, .page-header-actions-block .btn-default").addClass('hidden');
- $(".text-right .btn-primary").addClass('hidden');
-
- if (frappe.utils.get_url_arg('name')) {
- $('.page-content .btn-form-submit').addClass('hidden');
- } else {
- user_name = frappe.full_name
- user_email_id = frappe.session.user
- $('[data-fieldname="currency"]').val("INR");
- $('[data-fieldname="name_of_applicant"]').val(user_name);
- $('[data-fieldname="email"]').val(user_email_id);
- $('[data-fieldname="amount"]').val(20000);
- }
-})
diff --git a/erpnext/non_profit/web_form/certification_application/certification_application.json b/erpnext/non_profit/web_form/certification_application/certification_application.json
deleted file mode 100644
index 5fda978fba..0000000000
--- a/erpnext/non_profit/web_form/certification_application/certification_application.json
+++ /dev/null
@@ -1,79 +0,0 @@
-{
- "accept_payment": 1,
- "allow_comments": 0,
- "allow_delete": 0,
- "allow_edit": 0,
- "allow_incomplete": 0,
- "allow_multiple": 1,
- "allow_print": 0,
- "amount": 0.0,
- "amount_based_on_field": 1,
- "amount_field": "amount",
- "creation": "2018-06-08 16:24:05.805225",
- "doc_type": "Certification Application",
- "docstatus": 0,
- "doctype": "Web Form",
- "idx": 0,
- "introduction_text": "",
- "is_standard": 1,
- "login_required": 1,
- "max_attachment_size": 0,
- "modified": "2018-06-11 16:11:14.544987",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "certification-application",
- "owner": "Administrator",
- "payment_button_help": "Pay for your certification using RazorPay",
- "payment_button_label": "Pay Now",
- "payment_gateway": "Razorpay",
- "published": 1,
- "route": "certification-application",
- "show_sidebar": 1,
- "sidebar_items": [],
- "success_url": "/certification-application",
- "title": "Certification Application",
- "web_form_fields": [
- {
- "fieldname": "name_of_applicant",
- "fieldtype": "Data",
- "hidden": 0,
- "label": "Name of Applicant",
- "max_length": 0,
- "max_value": 0,
- "read_only": 0,
- "reqd": 0
- },
- {
- "fieldname": "email",
- "fieldtype": "Link",
- "hidden": 0,
- "label": "Email",
- "max_length": 0,
- "max_value": 0,
- "options": "User",
- "read_only": 1,
- "reqd": 1
- },
- {
- "fieldname": "currency",
- "fieldtype": "Select",
- "hidden": 0,
- "label": "Currency",
- "max_length": 0,
- "max_value": 0,
- "options": "USD\nINR",
- "read_only": 1,
- "reqd": 0
- },
- {
- "fieldname": "amount",
- "fieldtype": "Float",
- "hidden": 0,
- "label": "Amount",
- "max_length": 0,
- "max_value": 0,
- "read_only": 1,
- "reqd": 0
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/web_form/certification_application/certification_application.py b/erpnext/non_profit/web_form/certification_application/certification_application.py
deleted file mode 100644
index 02e3e93333..0000000000
--- a/erpnext/non_profit/web_form/certification_application/certification_application.py
+++ /dev/null
@@ -1,3 +0,0 @@
-def get_context(context):
- # do your magic here
- pass
diff --git a/erpnext/non_profit/web_form/certification_application_usd/__init__.py b/erpnext/non_profit/web_form/certification_application_usd/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.js b/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.js
deleted file mode 100644
index 005d1dd6c1..0000000000
--- a/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.js
+++ /dev/null
@@ -1,16 +0,0 @@
-frappe.ready(function() {
- // bind events here
- $(".page-header-actions-block .btn-primary, .page-header-actions-block .btn-default").addClass('hidden');
- $(".text-right .btn-primary").addClass('hidden');
-
- if (frappe.utils.get_url_arg('name')) {
- $('.page-content .btn-form-submit').addClass('hidden');
- } else {
- user_name = frappe.full_name
- user_email_id = frappe.session.user
- $('[data-fieldname="currency"]').val("USD");
- $('[data-fieldname="name_of_applicant"]').val(user_name);
- $('[data-fieldname="email"]').val(user_email_id);
- $('[data-fieldname="amount"]').val(300);
- }
-})
diff --git a/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.json b/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.json
deleted file mode 100644
index 266109f580..0000000000
--- a/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.json
+++ /dev/null
@@ -1,80 +0,0 @@
-{
- "accept_payment": 1,
- "allow_comments": 0,
- "allow_delete": 0,
- "allow_edit": 0,
- "allow_incomplete": 0,
- "allow_multiple": 1,
- "allow_print": 0,
- "amount": 0.0,
- "amount_based_on_field": 1,
- "amount_field": "amount",
- "creation": "2018-06-13 09:22:48.262441",
- "currency": "USD",
- "doc_type": "Certification Application",
- "docstatus": 0,
- "doctype": "Web Form",
- "idx": 0,
- "introduction_text": "",
- "is_standard": 1,
- "login_required": 1,
- "max_attachment_size": 0,
- "modified": "2018-06-13 09:26:35.502064",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "certification-application-usd",
- "owner": "Administrator",
- "payment_button_help": "Pay for your certification using PayPal",
- "payment_button_label": "Pay Now",
- "payment_gateway": "PayPal",
- "published": 1,
- "route": "certification-application-usd",
- "show_sidebar": 1,
- "sidebar_items": [],
- "success_url": "/certification-application-usd",
- "title": "Certification Application USD",
- "web_form_fields": [
- {
- "fieldname": "name_of_applicant",
- "fieldtype": "Data",
- "hidden": 0,
- "label": "Name of Applicant",
- "max_length": 0,
- "max_value": 0,
- "read_only": 0,
- "reqd": 0
- },
- {
- "fieldname": "email",
- "fieldtype": "Link",
- "hidden": 0,
- "label": "Email",
- "max_length": 0,
- "max_value": 0,
- "options": "User",
- "read_only": 1,
- "reqd": 1
- },
- {
- "fieldname": "currency",
- "fieldtype": "Select",
- "hidden": 0,
- "label": "Currency",
- "max_length": 0,
- "max_value": 0,
- "options": "USD\nINR",
- "read_only": 1,
- "reqd": 0
- },
- {
- "fieldname": "amount",
- "fieldtype": "Float",
- "hidden": 0,
- "label": "Amount",
- "max_length": 0,
- "max_value": 0,
- "read_only": 1,
- "reqd": 0
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.py b/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.py
deleted file mode 100644
index 02e3e93333..0000000000
--- a/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.py
+++ /dev/null
@@ -1,3 +0,0 @@
-def get_context(context):
- # do your magic here
- pass
diff --git a/erpnext/non_profit/web_form/grant_application/__init__.py b/erpnext/non_profit/web_form/grant_application/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/non_profit/web_form/grant_application/grant_application.js b/erpnext/non_profit/web_form/grant_application/grant_application.js
deleted file mode 100644
index f09e540919..0000000000
--- a/erpnext/non_profit/web_form/grant_application/grant_application.js
+++ /dev/null
@@ -1,3 +0,0 @@
-frappe.ready(function() {
- // bind events here
-});
diff --git a/erpnext/non_profit/web_form/grant_application/grant_application.json b/erpnext/non_profit/web_form/grant_application/grant_application.json
deleted file mode 100644
index 73c9445500..0000000000
--- a/erpnext/non_profit/web_form/grant_application/grant_application.json
+++ /dev/null
@@ -1,108 +0,0 @@
-{
- "accept_payment": 0,
- "allow_comments": 0,
- "allow_delete": 1,
- "allow_edit": 1,
- "allow_incomplete": 0,
- "allow_multiple": 1,
- "allow_print": 0,
- "amount": 0.0,
- "amount_based_on_field": 0,
- "creation": "2017-10-30 15:57:10.825188",
- "currency": "INR",
- "doc_type": "Grant Application",
- "docstatus": 0,
- "doctype": "Web Form",
- "idx": 0,
- "introduction_text": "Share as many details as you can to get quick response from organization",
- "is_standard": 1,
- "login_required": 1,
- "max_attachment_size": 0,
- "modified": "2017-12-06 12:32:16.893289",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "grant-application",
- "owner": "Administrator",
- "payment_button_label": "Buy Now",
- "published": 1,
- "route": "my-grant",
- "show_sidebar": 1,
- "sidebar_items": [],
- "success_url": "/grant-application",
- "title": "Grant Application",
- "web_form_fields": [
- {
- "fieldname": "applicant_type",
- "fieldtype": "Select",
- "hidden": 0,
- "label": "Applicant Type",
- "max_length": 0,
- "max_value": 0,
- "options": "Individual\nOrganization",
- "read_only": 0,
- "reqd": 1
- },
- {
- "fieldname": "applicant_name",
- "fieldtype": "Data",
- "hidden": 0,
- "label": "Name",
- "max_length": 0,
- "max_value": 0,
- "read_only": 0,
- "reqd": 1
- },
- {
- "fieldname": "email",
- "fieldtype": "Data",
- "hidden": 0,
- "label": "Email Address",
- "max_length": 0,
- "max_value": 0,
- "read_only": 0,
- "reqd": 1
- },
- {
- "description": "",
- "fieldname": "grant_description",
- "fieldtype": "Text",
- "hidden": 0,
- "label": "Please outline your current situation and why you are applying for a grant?",
- "max_length": 0,
- "max_value": 0,
- "read_only": 0,
- "reqd": 1
- },
- {
- "fieldname": "amount",
- "fieldtype": "Float",
- "hidden": 0,
- "label": "Requested Amount",
- "max_length": 0,
- "max_value": 0,
- "read_only": 0,
- "reqd": 0
- },
- {
- "fieldname": "has_any_past_grant_record",
- "fieldtype": "Check",
- "hidden": 0,
- "label": "Have you received any grant from us before?",
- "max_length": 0,
- "max_value": 0,
- "options": "",
- "read_only": 0,
- "reqd": 0
- },
- {
- "fieldname": "published",
- "fieldtype": "Check",
- "hidden": 0,
- "label": "Show on Website",
- "max_length": 0,
- "max_value": 0,
- "read_only": 0,
- "reqd": 0
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/web_form/grant_application/grant_application.py b/erpnext/non_profit/web_form/grant_application/grant_application.py
deleted file mode 100644
index 3dfb381f65..0000000000
--- a/erpnext/non_profit/web_form/grant_application/grant_application.py
+++ /dev/null
@@ -1,4 +0,0 @@
-def get_context(context):
- context.no_cache = True
- context.parents = [dict(label='View All ',
- route='grant-application', title='View All')]
diff --git a/erpnext/non_profit/workspace/non_profit/non_profit.json b/erpnext/non_profit/workspace/non_profit/non_profit.json
deleted file mode 100644
index ba2f919d01..0000000000
--- a/erpnext/non_profit/workspace/non_profit/non_profit.json
+++ /dev/null
@@ -1,272 +0,0 @@
-{
- "charts": [],
- "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Member\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Non Profit Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Membership\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Chapter\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Chapter Member\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loan Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Grant Application\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Membership\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Volunteer\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Chapter\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Donation\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tax Exemption Certification (India)\", \"col\": 4}}]",
- "creation": "2020-03-02 17:23:47.811421",
- "docstatus": 0,
- "doctype": "Workspace",
- "for_user": "",
- "hide_custom": 0,
- "icon": "non-profit",
- "idx": 0,
- "label": "Non Profit",
- "links": [
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Loan Management",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Loan Type",
- "link_count": 0,
- "link_to": "Loan Type",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Loan Application",
- "link_count": 0,
- "link_to": "Loan Application",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Loan",
- "link_count": 0,
- "link_to": "Loan",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Grant Application",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Grant Application",
- "link_count": 0,
- "link_to": "Grant Application",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Membership",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Member",
- "link_count": 0,
- "link_to": "Member",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Membership",
- "link_count": 0,
- "link_to": "Membership",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Membership Type",
- "link_count": 0,
- "link_to": "Membership Type",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Membership Settings",
- "link_count": 0,
- "link_to": "Non Profit Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Volunteer",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Volunteer",
- "link_count": 0,
- "link_to": "Volunteer",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Volunteer Type",
- "link_count": 0,
- "link_to": "Volunteer Type",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Chapter",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Chapter",
- "link_count": 0,
- "link_to": "Chapter",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Donation",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Donor",
- "link_count": 0,
- "link_to": "Donor",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Donor Type",
- "link_count": 0,
- "link_to": "Donor Type",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Donation",
- "link_count": 0,
- "link_to": "Donation",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Tax Exemption Certification (India)",
- "link_count": 0,
- "link_type": "DocType",
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Tax Exemption 80G Certificate",
- "link_count": 0,
- "link_to": "Tax Exemption 80G Certificate",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- }
- ],
- "modified": "2021-08-05 12:16:01.146207",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Non Profit",
- "owner": "Administrator",
- "parent_page": "",
- "public": 1,
- "restrict_to_domain": "Non Profit",
- "roles": [],
- "sequence_id": 18,
- "shortcuts": [
- {
- "label": "Member",
- "link_to": "Member",
- "type": "DocType"
- },
- {
- "label": "Non Profit Settings",
- "link_to": "Non Profit Settings",
- "type": "DocType"
- },
- {
- "label": "Membership",
- "link_to": "Membership",
- "type": "DocType"
- },
- {
- "label": "Chapter",
- "link_to": "Chapter",
- "type": "DocType"
- },
- {
- "label": "Chapter Member",
- "link_to": "Chapter Member",
- "type": "DocType"
- }
- ],
- "title": "Non Profit"
-}
\ No newline at end of file
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 268db40a8e..970a8f9b5d 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -323,3 +323,5 @@ execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings'
erpnext.patches.v14_0.set_payroll_cost_centers
erpnext.patches.v13_0.agriculture_deprecation_warning
erpnext.patches.v14_0.delete_agriculture_doctypes
+erpnext.patches.v13_0.non_profit_deprecation_warning
+erpnext.patches.v14_0.delete_non_profit_doctypes
diff --git a/erpnext/patches/v13_0/non_profit_deprecation_warning.py b/erpnext/patches/v13_0/non_profit_deprecation_warning.py
new file mode 100644
index 0000000000..c170de5c1c
--- /dev/null
+++ b/erpnext/patches/v13_0/non_profit_deprecation_warning.py
@@ -0,0 +1,10 @@
+import click
+
+
+def execute():
+
+ click.secho(
+ "Non Profit Domain is moved to a separate app and will be removed from ERPNext in version-14.\n"
+ "When upgrading to ERPNext version-14, please install the app to continue using the Agriculture domain: https://github.com/frappe/non_profit",
+ fg="yellow",
+ )
diff --git a/erpnext/patches/v14_0/delete_non_profit_doctypes.py b/erpnext/patches/v14_0/delete_non_profit_doctypes.py
new file mode 100644
index 0000000000..3b3dbe4576
--- /dev/null
+++ b/erpnext/patches/v14_0/delete_non_profit_doctypes.py
@@ -0,0 +1,19 @@
+import frappe
+
+
+def execute():
+ frappe.delete_doc("Module Def", "Non Profit", ignore_missing=True, force=True)
+
+ frappe.delete_doc("Workspace", "Non Profit", ignore_missing=True, force=True)
+
+ reports = frappe.get_all("Report", {"module": "Non Profit", "is_standard": "Yes"}, pluck='name')
+ for report in reports:
+ frappe.delete_doc("Report", report, ignore_missing=True, force=True)
+
+ dashboards = frappe.get_all("Dashboard", {"module": "Non Profit", "is_standard": 1}, pluck='name')
+ for dashboard in dashboards:
+ frappe.delete_doc("Dashboard", dashboard, ignore_missing=True, force=True)
+
+ doctypes = frappe.get_all("DocType", {"module": "Non Profit", "custom": 0}, pluck='name')
+ for doctype in doctypes:
+ frappe.delete_doc("DocType", doctype, ignore_missing=True)
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index c0dcb70b92..fa4e454f33 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -679,14 +679,6 @@ def get_custom_fields():
'insert_after': 'email_id'
}
],
- 'Donor': [
- {
- 'fieldname': 'pan_number',
- 'label': 'PAN Details',
- 'fieldtype': 'Data',
- 'insert_after': 'email'
- }
- ],
'Finance Book': [
{
'fieldname': 'for_income_tax',
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index 336b51c0ab..906db561db 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -198,7 +198,6 @@ def install(country=None):
{'doctype': "Party Type", "party_type": "Member", "account_type": "Receivable"},
{'doctype': "Party Type", "party_type": "Shareholder", "account_type": "Payable"},
{'doctype': "Party Type", "party_type": "Student", "account_type": "Receivable"},
- {'doctype': "Party Type", "party_type": "Donor", "account_type": "Receivable"},
{'doctype': "Opportunity Type", "name": _("Sales")},
{'doctype': "Opportunity Type", "name": _("Support")},
From e04b3aaf7a2cb0792436a7e5f868572cb35c39f4 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Tue, 18 Jan 2022 14:36:22 +0530
Subject: [PATCH 006/447] feat(Employee Advance): add 'Returned' and 'Partly
Claimed and Returned' status
---
.../employee_advance/employee_advance.json | 42 +++++++++++++++++--
.../employee_advance/employee_advance.py | 39 ++++++++++-------
.../hr/doctype/expense_claim/expense_claim.js | 2 +-
.../hr/doctype/expense_claim/expense_claim.py | 31 +++++++++-----
4 files changed, 85 insertions(+), 29 deletions(-)
diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json
index 04754530c3..b0501830cc 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance.json
+++ b/erpnext/hr/doctype/employee_advance/employee_advance.json
@@ -2,7 +2,7 @@
"actions": [],
"allow_import": 1,
"autoname": "naming_series:",
- "creation": "2017-10-09 14:26:29.612365",
+ "creation": "2022-01-17 18:36:51.450395",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
@@ -121,7 +121,7 @@
"fieldtype": "Select",
"label": "Status",
"no_copy": 1,
- "options": "Draft\nPaid\nUnpaid\nClaimed\nCancelled",
+ "options": "Draft\nPaid\nUnpaid\nClaimed\nReturned\nPartly Claimed and Returned\nCancelled",
"read_only": 1
},
{
@@ -200,7 +200,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2021-09-11 18:38:38.617478",
+ "modified": "2022-01-17 19:33:52.345823",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Advance",
@@ -237,5 +237,41 @@
"search_fields": "employee,employee_name",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [
+ {
+ "color": "Red",
+ "custom": 1,
+ "title": "Draft"
+ },
+ {
+ "color": "Green",
+ "custom": 1,
+ "title": "Paid"
+ },
+ {
+ "color": "Orange",
+ "custom": 1,
+ "title": "Unpaid"
+ },
+ {
+ "color": "Blue",
+ "custom": 1,
+ "title": "Claimed"
+ },
+ {
+ "color": "Gray",
+ "title": "Returned"
+ },
+ {
+ "color": "Yellow",
+ "title": "Partly Claimed and Returned"
+ },
+ {
+ "color": "Red",
+ "custom": 1,
+ "title": "Cancelled"
+ }
+ ],
+ "title_field": "employee_name",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py
index 7aac2b63ed..e17eb214a1 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance.py
+++ b/erpnext/hr/doctype/employee_advance/employee_advance.py
@@ -28,18 +28,31 @@ class EmployeeAdvance(Document):
def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry')
- def set_status(self):
+ def set_status(self, update=False):
+ precision = self.precision("paid_amount")
+ total_amount = flt(flt(self.claimed_amount) + flt(self.return_amount), precision)
+ status = None
+
if self.docstatus == 0:
- self.status = "Draft"
- if self.docstatus == 1:
- if self.claimed_amount and flt(self.claimed_amount) == flt(self.paid_amount):
- self.status = "Claimed"
- elif self.paid_amount and self.advance_amount == flt(self.paid_amount):
- self.status = "Paid"
+ status = "Draft"
+ elif self.docstatus == 1:
+ if flt(self.claimed_amount) > 0 and flt(self.claimed_amount, precision) == flt(self.paid_amount, precision):
+ status = "Claimed"
+ elif flt(self.return_amount) > 0 and flt(self.return_amount, precision) == flt(self.paid_amount, precision):
+ status = "Returned"
+ elif flt(self.claimed_amount) > 0 and (flt(self.return_amount) > 0) and total_amount == flt(self.paid_amount, precision):
+ status = "Partly Claimed and Returned"
+ elif flt(self.paid_amount) > 0 and flt(self.advance_amount, precision) == flt(self.paid_amount, precision):
+ status = "Paid"
else:
- self.status = "Unpaid"
+ status = "Unpaid"
elif self.docstatus == 2:
- self.status = "Cancelled"
+ status = "Cancelled"
+
+ if update:
+ self.db_set("status", status)
+ else:
+ self.status = status
def set_total_advance_paid(self):
gle = frappe.qb.DocType("GL Entry")
@@ -85,9 +98,7 @@ class EmployeeAdvance(Document):
self.db_set("paid_amount", paid_amount)
self.db_set("return_amount", return_amount)
- self.set_status()
- frappe.db.set_value("Employee Advance", self.name , "status", self.status)
-
+ self.set_status(update=True)
def update_claimed_amount(self):
claimed_amount = frappe.db.sql("""
@@ -103,8 +114,8 @@ class EmployeeAdvance(Document):
frappe.db.set_value("Employee Advance", self.name, "claimed_amount", flt(claimed_amount))
self.reload()
- self.set_status()
- frappe.db.set_value("Employee Advance", self.name, "status", self.status)
+ self.set_status(update=True)
+
@frappe.whitelist()
def get_pending_amount(employee, posting_date):
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js
index 047945787d..af80b63845 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.js
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.js
@@ -171,7 +171,7 @@ frappe.ui.form.on("Expense Claim", {
['docstatus', '=', 1],
['employee', '=', frm.doc.employee],
['paid_amount', '>', 0],
- ['status', '!=', 'Claimed']
+ ['status', 'not in', ['Claimed', 'Returned', 'Partly Claimed and Returned']]
]
};
});
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py
index 7e3898b7d5..2d2bb093ce 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.py
@@ -341,18 +341,27 @@ def get_expense_claim_account(expense_claim_type, company):
@frappe.whitelist()
def get_advances(employee, advance_id=None):
- if not advance_id:
- condition = 'docstatus=1 and employee={0} and paid_amount > 0 and paid_amount > claimed_amount + return_amount'.format(frappe.db.escape(employee))
- else:
- condition = 'name={0}'.format(frappe.db.escape(advance_id))
+ advance = frappe.qb.DocType("Employee Advance")
- return frappe.db.sql("""
- select
- name, posting_date, paid_amount, claimed_amount, advance_account
- from
- `tabEmployee Advance`
- where {0}
- """.format(condition), as_dict=1)
+ query = (
+ frappe.qb.from_(advance)
+ .select(
+ advance.name, advance.posting_date, advance.paid_amount,
+ advance.claimed_amount, advance.advance_account
+ )
+ )
+
+ if not advance_id:
+ query = query.where(
+ (advance.docstatus == 1)
+ & (advance.employee == employee)
+ & (advance.paid_amount > 0)
+ & (advance.status.notin(["Claimed", "Returned", "Partly Claimed and Returned"]))
+ )
+ else:
+ query = query.where(advance.name == advance_id)
+
+ return query.run(as_dict=True)
@frappe.whitelist()
From bf30932de0524c42ab3d9718e3d19a73e1cb2d39 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Tue, 18 Jan 2022 14:36:38 +0530
Subject: [PATCH 007/447] patch: Employee Advance return statuses
---
erpnext/patches.txt | 3 ++-
.../v14_0/update_employee_advance_status.py | 26 +++++++++++++++++++
2 files changed, 28 insertions(+), 1 deletion(-)
create mode 100644 erpnext/patches/v14_0/update_employee_advance_status.py
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index fa62b7fc27..ed39c204f6 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -326,4 +326,5 @@ erpnext.patches.v13_0.agriculture_deprecation_warning
erpnext.patches.v14_0.delete_agriculture_doctypes
erpnext.patches.v13_0.update_exchange_rate_settings
erpnext.patches.v14_0.rearrange_company_fields
-erpnext.patches.v14_0.update_leave_notification_template
\ No newline at end of file
+erpnext.patches.v14_0.update_leave_notification_template
+erpnext.patches.v14_0.update_employee_advance_status
\ No newline at end of file
diff --git a/erpnext/patches/v14_0/update_employee_advance_status.py b/erpnext/patches/v14_0/update_employee_advance_status.py
new file mode 100644
index 0000000000..a20e35a9f6
--- /dev/null
+++ b/erpnext/patches/v14_0/update_employee_advance_status.py
@@ -0,0 +1,26 @@
+import frappe
+
+
+def execute():
+ frappe.reload_doc('hr', 'doctype', 'employee_advance')
+
+ advance = frappe.qb.DocType('Employee Advance')
+ (frappe.qb
+ .update(advance)
+ .set(advance.status, 'Returned')
+ .where(
+ (advance.docstatus == 1)
+ & ((advance.return_amount) & (advance.paid_amount == advance.return_amount))
+ & (advance.status == 'Paid')
+ )
+ ).run()
+
+ (frappe.qb
+ .update(advance)
+ .set(advance.status, 'Partly Claimed and Returned')
+ .where(
+ (advance.docstatus == 1)
+ & ((advance.claimed_amount & advance.return_amount) & (advance.paid_amount == (advance.return_amount + advance.claimed_amount)))
+ & (advance.status == 'Paid')
+ )
+ ).run()
\ No newline at end of file
From 0843d4388569616803a562a6928d6f1204f0e733 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Tue, 18 Jan 2022 14:37:45 +0530
Subject: [PATCH 008/447] fix(Expense Claim): validate advances after setting
totals
---
erpnext/hr/doctype/expense_claim/expense_claim.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py
index 2d2bb093ce..5146a5be90 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.py
@@ -23,10 +23,10 @@ class ExpenseClaim(AccountsController):
def validate(self):
validate_active_employee(self.employee)
- self.validate_advances()
+ set_employee_name(self)
self.validate_sanctioned_amount()
self.calculate_total_amount()
- set_employee_name(self)
+ self.validate_advances()
self.set_expense_account(validate=True)
self.set_payable_account()
self.set_cost_center()
From 85be0d22d4300a40f46b7268f7c56d8e7facbd40 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Tue, 18 Jan 2022 14:38:16 +0530
Subject: [PATCH 009/447] fix: employee advance status update on return via
additional salary
---
erpnext/payroll/doctype/additional_salary/additional_salary.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py
index bf8bd05fcc..d618568416 100644
--- a/erpnext/payroll/doctype/additional_salary/additional_salary.py
+++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py
@@ -105,6 +105,8 @@ class AdditionalSalary(Document):
return_amount += self.amount
frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", return_amount)
+ advance = frappe.get_doc("Employee Advance", self.ref_docname)
+ advance.set_status(update=True)
def update_employee_referral(self, cancel=False):
if self.ref_doctype == "Employee Referral":
From 17b1f5f256ff63d34b3c20b4792cd77ae75402e0 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Tue, 18 Jan 2022 18:35:25 +0530
Subject: [PATCH 010/447] test: employee advance status
---
.../employee_advance/employee_advance.py | 6 +-
.../employee_advance/test_employee_advance.py | 97 ++++++++++++++++++-
2 files changed, 100 insertions(+), 3 deletions(-)
diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py
index e17eb214a1..f63bb86129 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance.py
+++ b/erpnext/hr/doctype/employee_advance/employee_advance.py
@@ -233,7 +233,8 @@ def make_return_entry(employee, company, employee_advance_name, return_amount,
'reference_name': employee_advance_name,
'party_type': 'Employee',
'party': employee,
- 'is_advance': 'Yes'
+ 'is_advance': 'Yes',
+ 'cost_center': erpnext.get_default_cost_center(company)
})
bank_amount = flt(return_amount) if bank_cash_account.account_currency==currency \
@@ -244,7 +245,8 @@ def make_return_entry(employee, company, employee_advance_name, return_amount,
"debit_in_account_currency": bank_amount,
"account_currency": bank_cash_account.account_currency,
"account_type": bank_cash_account.account_type,
- "exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1
+ "exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1,
+ "cost_center": erpnext.get_default_cost_center(company)
})
return je.as_dict()
diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py
index 5f2e720eb4..5f3a66a04f 100644
--- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py
+++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py
@@ -4,7 +4,7 @@
import unittest
import frappe
-from frappe.utils import nowdate
+from frappe.utils import flt, nowdate
import erpnext
from erpnext.hr.doctype.employee.test_employee import make_employee
@@ -12,6 +12,12 @@ from erpnext.hr.doctype.employee_advance.employee_advance import (
EmployeeAdvanceOverPayment,
create_return_through_additional_salary,
make_bank_entry,
+ make_return_entry,
+)
+from erpnext.hr.doctype.expense_claim.expense_claim import get_advances
+from erpnext.hr.doctype.expense_claim.test_expense_claim import (
+ get_payable_account,
+ make_expense_claim,
)
from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
@@ -52,9 +58,75 @@ class TestEmployeeAdvance(unittest.TestCase):
self.assertEqual(advance.paid_amount, 0)
self.assertEqual(advance.status, "Unpaid")
+ def test_claimed_and_returned_status(self):
+ # Claimed Status check, full amount claimed
+ payable_account = get_payable_account("_Test Company")
+ claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
+
+ advance = make_employee_advance(claim.employee)
+ pe = make_payment_entry(advance)
+ pe.submit()
+
+ claim = get_advances_for_claim(claim, advance.name)
+ claim.save()
+ claim.submit()
+
+ advance.reload()
+ self.assertEqual(advance.claimed_amount, 1000)
+ self.assertEqual(advance.status, "Claimed")
+
+ # cancel claim; status should be Paid
+ claim.cancel()
+ advance.reload()
+ self.assertEqual(advance.claimed_amount, 0)
+ self.assertEqual(advance.status, "Paid")
+
+ # Partly Claimed and Returned status check
+ # 500 Claimed, 500 Returned
+ claim = make_expense_claim(payable_account, 500, 500, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
+
+ advance = make_employee_advance(claim.employee)
+ pe = make_payment_entry(advance)
+ pe.submit()
+
+ claim = get_advances_for_claim(claim, advance.name, amount=500)
+ claim.save()
+ claim.submit()
+
+ advance.reload()
+ self.assertEqual(advance.claimed_amount, 500)
+ self.assertEqual(advance.status, "Paid")
+
+ entry = make_return_entry(
+ employee=advance.employee,
+ company=advance.company,
+ employee_advance_name=advance.name,
+ return_amount=flt(advance.paid_amount - advance.claimed_amount),
+ advance_account=advance.advance_account,
+ mode_of_payment=advance.mode_of_payment,
+ currency=advance.currency,
+ exchange_rate=advance.exchange_rate
+ )
+
+ entry = frappe.get_doc(entry)
+ entry.insert()
+ entry.submit()
+
+ advance.reload()
+ self.assertEqual(advance.return_amount, 500)
+ self.assertEqual(advance.status, "Partly Claimed and Returned")
+
+ # Cancel return entry; status should change to Paid
+ entry.cancel()
+ advance.reload()
+ self.assertEqual(advance.return_amount, 0)
+ self.assertEqual(advance.status, "Paid")
+
def test_repay_unclaimed_amount_from_salary(self):
employee_name = make_employee("_T@employe.advance")
advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1})
+ pe = make_payment_entry(advance)
+ pe.submit()
args = {"type": "Deduction"}
create_salary_component("Advance Salary - Deduction", **args)
@@ -82,11 +154,13 @@ class TestEmployeeAdvance(unittest.TestCase):
advance.reload()
self.assertEqual(advance.return_amount, 1000)
+ self.assertEqual(advance.status, "Returned")
# update advance return amount on additional salary cancellation
additional_salary.cancel()
advance.reload()
self.assertEqual(advance.return_amount, 700)
+ self.assertEqual(advance.status, "Paid")
def tearDown(self):
frappe.db.rollback()
@@ -118,3 +192,24 @@ def make_employee_advance(employee_name, args=None):
doc.submit()
return doc
+
+
+def get_advances_for_claim(claim, advance_name, amount=None):
+ advances = get_advances(claim.employee, advance_name)
+
+ for entry in advances:
+ if amount:
+ allocated_amount = amount
+ else:
+ allocated_amount = flt(entry.paid_amount) - flt(entry.claimed_amount)
+
+ claim.append("advances", {
+ "employee_advance": entry.name,
+ "posting_date": entry.posting_date,
+ "advance_account": entry.advance_account,
+ "advance_paid": entry.paid_amount,
+ "unclaimed_amount": allocated_amount,
+ "allocated_amount": allocated_amount
+ })
+
+ return claim
\ No newline at end of file
From f423de530a4140bfb84e93dbfa0f1140dcaa8e9b Mon Sep 17 00:00:00 2001
From: ChillarAnand
Date: Thu, 20 Jan 2022 11:02:30 +0530
Subject: [PATCH 011/447] refactor: Code cleanup
---
.../doctype/membership/membership.py | 415 ------------------
erpnext/regional/india/setup.py | 8 -
.../operations/install_fixtures.py | 1 -
3 files changed, 424 deletions(-)
delete mode 100644 erpnext/non_profit/doctype/membership/membership.py
diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py
deleted file mode 100644
index f9b295a223..0000000000
--- a/erpnext/non_profit/doctype/membership/membership.py
+++ /dev/null
@@ -1,415 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import json
-from datetime import datetime
-
-import frappe
-from frappe import _
-from frappe.email import sendmail_to_system_managers
-from frappe.model.document import Document
-from frappe.utils import add_days, add_months, add_years, get_link_to_form, getdate, nowdate
-
-import erpnext
-from erpnext.non_profit.doctype.member.member import create_member
-
-
-class Membership(Document):
- def validate(self):
- if not self.member or not frappe.db.exists("Member", self.member):
- # for web forms
- user_type = frappe.db.get_value("User", frappe.session.user, "user_type")
- if user_type == "Website User":
- self.create_member_from_website_user()
- else:
- frappe.throw(_("Please select a Member"))
-
- self.validate_membership_period()
-
- def create_member_from_website_user(self):
- member_name = frappe.get_value("Member", dict(email_id=frappe.session.user))
-
- if not member_name:
- user = frappe.get_doc("User", frappe.session.user)
- member = frappe.get_doc(dict(
- doctype="Member",
- email_id=frappe.session.user,
- membership_type=self.membership_type,
- member_name=user.get_fullname()
- )).insert(ignore_permissions=True)
- member_name = member.name
-
- if self.get("__islocal"):
- self.member = member_name
-
- def validate_membership_period(self):
- # get last membership (if active)
- last_membership = erpnext.get_last_membership(self.member)
-
- # if person applied for offline membership
- if last_membership and last_membership.name != self.name and not frappe.session.user == "Administrator":
- # if last membership does not expire in 30 days, then do not allow to renew
- if getdate(add_days(last_membership.to_date, -30)) > getdate(nowdate()) :
- frappe.throw(_("You can only renew if your membership expires within 30 days"))
-
- self.from_date = add_days(last_membership.to_date, 1)
- elif frappe.session.user == "Administrator":
- self.from_date = self.from_date
- else:
- self.from_date = nowdate()
-
- if frappe.db.get_single_value("Non Profit Settings", "billing_cycle") == "Yearly":
- self.to_date = add_years(self.from_date, 1)
- else:
- self.to_date = add_months(self.from_date, 1)
-
- def on_payment_authorized(self, status_changed_to=None):
- if status_changed_to not in ("Completed", "Authorized"):
- return
- self.load_from_db()
- self.db_set("paid", 1)
- settings = frappe.get_doc("Non Profit Settings")
- if settings.allow_invoicing and settings.automate_membership_invoicing:
- self.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True)
-
-
- @frappe.whitelist()
- def generate_invoice(self, save=True, with_payment_entry=False):
- if not (self.paid or self.currency or self.amount):
- frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details"))
-
- if self.invoice:
- frappe.throw(_("An invoice is already linked to this document"))
-
- member = frappe.get_doc("Member", self.member)
- if not member.customer:
- frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member)))
-
- plan = frappe.get_doc("Membership Type", self.membership_type)
- settings = frappe.get_doc("Non Profit Settings")
- self.validate_membership_type_and_settings(plan, settings)
-
- invoice = make_invoice(self, member, plan, settings)
- self.reload()
- self.invoice = invoice.name
-
- if with_payment_entry:
- self.make_payment_entry(settings, invoice)
-
- if save:
- self.save()
-
- return invoice
-
- def validate_membership_type_and_settings(self, plan, settings):
- settings_link = get_link_to_form("Membership Type", self.membership_type)
-
- if not settings.membership_debit_account:
- frappe.throw(_("You need to set Debit Account in {0}").format(settings_link))
-
- if not settings.company:
- frappe.throw(_("You need to set Default Company for invoicing in {0}").format(settings_link))
-
- if not plan.linked_item:
- frappe.throw(_("Please set a Linked Item for the Membership Type {0}").format(
- get_link_to_form("Membership Type", self.membership_type)))
-
- def make_payment_entry(self, settings, invoice):
- if not settings.membership_payment_account:
- frappe.throw(_("You need to set Payment Account for Membership in {0}").format(
- get_link_to_form("Non Profit Settings", "Non Profit Settings")))
-
- from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
- frappe.flags.ignore_account_permission = True
- pe = get_payment_entry(dt="Sales Invoice", dn=invoice.name, bank_amount=invoice.grand_total)
- frappe.flags.ignore_account_permission=False
- pe.paid_to = settings.membership_payment_account
- pe.reference_no = self.name
- pe.reference_date = getdate()
- pe.flags.ignore_mandatory = True
- pe.save()
- pe.submit()
-
- @frappe.whitelist()
- def send_acknowlement(self):
- settings = frappe.get_doc("Non Profit Settings")
- if not settings.send_email:
- frappe.throw(_("You need to enable Send Acknowledge Email in {0}").format(
- get_link_to_form("Non Profit Settings", "Non Profit Settings")))
-
- member = frappe.get_doc("Member", self.member)
- if not member.email_id:
- frappe.throw(_("Email address of member {0} is missing").format(frappe.utils.get_link_to_form("Member", self.member)))
-
- plan = frappe.get_doc("Membership Type", self.membership_type)
- email = member.email_id
- attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)]
-
- if self.invoice and settings.send_invoice:
- attachments.append(frappe.attach_print("Sales Invoice", self.invoice, print_format=settings.inv_print_format))
-
- email_template = frappe.get_doc("Email Template", settings.email_template)
- context = { "doc": self, "member": member}
-
- email_args = {
- "recipients": [email],
- "message": frappe.render_template(email_template.get("response"), context),
- "subject": frappe.render_template(email_template.get("subject"), context),
- "attachments": attachments,
- "reference_doctype": self.doctype,
- "reference_name": self.name
- }
-
- if not frappe.flags.in_test:
- frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
- else:
- frappe.sendmail(**email_args)
-
- def generate_and_send_invoice(self):
- self.generate_invoice(save=False)
- self.send_acknowlement()
-
-
-def make_invoice(membership, member, plan, settings):
- invoice = frappe.get_doc({
- "doctype": "Sales Invoice",
- "customer": member.customer,
- "debit_to": settings.membership_debit_account,
- "currency": membership.currency,
- "company": settings.company,
- "is_pos": 0,
- "items": [
- {
- "item_code": plan.linked_item,
- "rate": membership.amount,
- "qty": 1
- }
- ]
- })
- invoice.set_missing_values()
- invoice.insert()
- invoice.submit()
-
- frappe.msgprint(_("Sales Invoice created successfully"))
-
- return invoice
-
-
-def get_member_based_on_subscription(subscription_id, email=None, customer_id=None):
- filters = {"subscription_id": subscription_id}
- if email:
- filters.update({"email_id": email})
- if customer_id:
- filters.update({"customer_id": customer_id})
-
- members = frappe.get_all("Member", filters=filters, order_by="creation desc")
-
- try:
- return frappe.get_doc("Member", members[0]["name"])
- except Exception:
- return None
-
-
-def verify_signature(data, endpoint="Membership"):
- signature = frappe.request.headers.get("X-Razorpay-Signature")
-
- settings = frappe.get_doc("Non Profit Settings")
- key = settings.get_webhook_secret(endpoint)
-
- controller = frappe.get_doc("Razorpay Settings")
-
- controller.verify_signature(data, signature, key)
- frappe.set_user(settings.creation_user)
-
-
-@frappe.whitelist(allow_guest=True)
-def trigger_razorpay_subscription(*args, **kwargs):
- data = frappe.request.get_data(as_text=True)
- data = process_request_data(data)
-
- subscription = data.payload.get("subscription", {}).get("entity", {})
- subscription = frappe._dict(subscription)
-
- payment = data.payload.get("payment", {}).get("entity", {})
- payment = frappe._dict(payment)
-
- try:
- if not data.event == "subscription.charged":
- return
-
- member = get_member_based_on_subscription(subscription.id, payment.email)
- if not member:
- member = create_member(frappe._dict({
- "fullname": payment.email,
- "email": payment.email,
- "plan_id": get_plan_from_razorpay_id(subscription.plan_id)
- }))
-
- member.subscription_id = subscription.id
- member.customer_id = payment.customer_id
-
- if subscription.get("notes"):
- member = get_additional_notes(member, subscription)
-
- company = get_company_for_memberships()
- # Update Membership
- membership = frappe.new_doc("Membership")
- membership.update({
- "company": company,
- "member": member.name,
- "membership_status": "Current",
- "membership_type": member.membership_type,
- "currency": "INR",
- "paid": 1,
- "payment_id": payment.id,
- "from_date": datetime.fromtimestamp(subscription.current_start),
- "to_date": datetime.fromtimestamp(subscription.current_end),
- "amount": payment.amount / 100 # Convert to rupees from paise
- })
- membership.flags.ignore_mandatory = True
- membership.insert()
-
- # Update membership values
- member.subscription_start = datetime.fromtimestamp(subscription.start_at)
- member.subscription_end = datetime.fromtimestamp(subscription.end_at)
- member.subscription_status = "Active"
- member.flags.ignore_mandatory = True
- member.save()
-
- settings = frappe.get_doc("Non Profit Settings")
- if settings.allow_invoicing and settings.automate_membership_invoicing:
- membership.reload()
- membership.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True)
-
- except Exception as e:
- message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), _("Payment ID"), payment.id)
- log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name))
- notify_failure(log)
- return {"status": "Failed", "reason": e}
-
- return {"status": "Success"}
-
-
-@frappe.whitelist(allow_guest=True)
-def update_halted_razorpay_subscription(*args, **kwargs):
- """
- When all retries have been exhausted, Razorpay moves the subscription to the halted state.
- The customer has to manually retry the charge or change the card linked to the subscription,
- for the subscription to move back to the active state.
- """
- if frappe.request:
- data = frappe.request.get_data(as_text=True)
- data = process_request_data(data)
- elif frappe.flags.in_test:
- data = kwargs.get("data")
- data = frappe._dict(data)
- else:
- return
-
- if not data.event == "subscription.halted":
- return
-
- subscription = data.payload.get("subscription", {}).get("entity", {})
- subscription = frappe._dict(subscription)
-
- try:
- member = get_member_based_on_subscription(subscription.id, customer_id=subscription.customer_id)
- if not member:
- frappe.throw(_("Member with Razorpay Subscription ID {0} not found").format(subscription.id))
-
- member.subscription_status = "Halted"
- member.flags.ignore_mandatory = True
- member.save()
-
- if subscription.get("notes"):
- member = get_additional_notes(member, subscription)
-
- except Exception as e:
- message = "{0}\n\n{1}".format(e, frappe.get_traceback())
- log = frappe.log_error(message, _("Error updating halted status for member {0}").format(member.name))
- notify_failure(log)
- return {"status": "Failed", "reason": e}
-
- return {"status": "Success"}
-
-
-def process_request_data(data):
- try:
- verify_signature(data)
- except Exception as e:
- log = frappe.log_error(e, "Membership Webhook Verification Error")
- notify_failure(log)
- return {"status": "Failed", "reason": e}
-
- if isinstance(data, str):
- data = json.loads(data)
- data = frappe._dict(data)
-
- return data
-
-
-def get_company_for_memberships():
- company = frappe.db.get_single_value("Non Profit Settings", "company")
- if not company:
- from erpnext.non_profit.utils import get_company
- company = get_company()
- return company
-
-
-def get_additional_notes(member, subscription):
- if type(subscription.notes) == dict:
- for k, v in subscription.notes.items():
- notes = "\n".join("{}: {}".format(k, v))
-
- # extract member name from notes
- if "name" in k.lower():
- member.update({
- "member_name": subscription.notes.get(k)
- })
-
- # extract pan number from notes
- if "pan" in k.lower():
- member.update({
- "pan_number": subscription.notes.get(k)
- })
-
- member.add_comment("Comment", notes)
-
- elif type(subscription.notes) == str:
- member.add_comment("Comment", subscription.notes)
-
- return member
-
-
-def notify_failure(log):
- try:
- content = """
- Dear System Manager,
- Razorpay webhook for creating renewing membership subscription failed due to some reason.
- Please check the following error log linked below
- Error Log: {0}
- Regards, Administrator
- """.format(get_link_to_form("Error Log", log.name))
-
- sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content)
- except Exception:
- pass
-
-
-def get_plan_from_razorpay_id(plan_id):
- plan = frappe.get_all("Membership Type", filters={"razorpay_plan_id": plan_id}, order_by="creation desc")
-
- try:
- return plan[0]["name"]
- except Exception:
- return None
-
-
-def set_expired_status():
- frappe.db.sql("""
- UPDATE
- `tabMembership` SET `membership_status` = 'Expired'
- WHERE
- `membership_status` not in ('Cancelled') AND `to_date` < %s
- """, (nowdate()))
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index fa4e454f33..ecd9294342 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -671,14 +671,6 @@ def get_custom_fields():
'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)'
}
],
- 'Member': [
- {
- 'fieldname': 'pan_number',
- 'label': 'PAN Details',
- 'fieldtype': 'Data',
- 'insert_after': 'email_id'
- }
- ],
'Finance Book': [
{
'fieldname': 'for_income_tax',
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index 906db561db..bf3525998c 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -195,7 +195,6 @@ def install(country=None):
{'doctype': "Party Type", "party_type": "Customer", "account_type": "Receivable"},
{'doctype': "Party Type", "party_type": "Supplier", "account_type": "Payable"},
{'doctype': "Party Type", "party_type": "Employee", "account_type": "Payable"},
- {'doctype': "Party Type", "party_type": "Member", "account_type": "Receivable"},
{'doctype': "Party Type", "party_type": "Shareholder", "account_type": "Payable"},
{'doctype': "Party Type", "party_type": "Student", "account_type": "Receivable"},
From 4bdf6248aae8140bd3c46846044d9dfe97fcacc6 Mon Sep 17 00:00:00 2001
From: ChillarAnand
Date: Mon, 24 Jan 2022 11:06:11 +0530
Subject: [PATCH 012/447] refactor: Remove regional setup for non-profit
---
.../tax_exemption_80g_certificate/__init__.py | 0
.../tax_exemption_80g_certificate.js | 67 ----
.../tax_exemption_80g_certificate.json | 297 ------------------
.../tax_exemption_80g_certificate.py | 104 ------
.../test_tax_exemption_80g_certificate.py | 106 -------
.../__init__.py | 0
.../tax_exemption_80g_certificate_detail.json | 66 ----
.../tax_exemption_80g_certificate_detail.py | 10 -
8 files changed, 650 deletions(-)
delete mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate/__init__.py
delete mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js
delete mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json
delete mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
delete mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py
delete mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate_detail/__init__.py
delete mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json
delete mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/__init__.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js
deleted file mode 100644
index 54cde9c0cf..0000000000
--- a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Tax Exemption 80G Certificate', {
- refresh: function(frm) {
- if (frm.doc.donor) {
- frm.set_query('donation', function() {
- return {
- filters: {
- docstatus: 1,
- donor: frm.doc.donor
- }
- };
- });
- }
- },
-
- recipient: function(frm) {
- if (frm.doc.recipient === 'Donor') {
- frm.set_value({
- 'member': '',
- 'member_name': '',
- 'member_email': '',
- 'member_pan_number': '',
- 'fiscal_year': '',
- 'total': 0,
- 'payments': []
- });
- } else {
- frm.set_value({
- 'donor': '',
- 'donor_name': '',
- 'donor_email': '',
- 'donor_pan_number': '',
- 'donation': '',
- 'date_of_donation': '',
- 'amount': 0,
- 'mode_of_payment': '',
- 'razorpay_payment_id': ''
- });
- }
- },
-
- get_payments: function(frm) {
- frm.call({
- doc: frm.doc,
- method: 'get_payments',
- freeze: true
- });
- },
-
- company: function(frm) {
- if ((frm.doc.member || frm.doc.donor) && frm.doc.company) {
- frm.call({
- doc: frm.doc,
- method: 'set_company_address',
- freeze: true
- });
- }
- },
-
- donation: function(frm) {
- if (frm.doc.recipient === 'Donor' && !frm.doc.donor) {
- frappe.msgprint(__('Please select donor first'));
- }
- }
-});
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json
deleted file mode 100644
index 9eee722f42..0000000000
--- a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json
+++ /dev/null
@@ -1,297 +0,0 @@
-{
- "actions": [],
- "autoname": "naming_series:",
- "creation": "2021-02-15 12:37:21.577042",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "naming_series",
- "recipient",
- "member",
- "member_name",
- "member_email",
- "member_pan_number",
- "donor",
- "donor_name",
- "donor_email",
- "donor_pan_number",
- "column_break_4",
- "date",
- "fiscal_year",
- "section_break_11",
- "company",
- "company_address",
- "company_address_display",
- "column_break_14",
- "company_pan_number",
- "company_80g_number",
- "company_80g_wef",
- "title",
- "section_break_6",
- "get_payments",
- "payments",
- "total",
- "donation_details_section",
- "donation",
- "date_of_donation",
- "amount",
- "column_break_27",
- "mode_of_payment",
- "razorpay_payment_id"
- ],
- "fields": [
- {
- "fieldname": "recipient",
- "fieldtype": "Select",
- "in_list_view": 1,
- "label": "Certificate Recipient",
- "options": "Member\nDonor",
- "reqd": 1
- },
- {
- "depends_on": "eval:doc.recipient === \"Member\";",
- "fieldname": "member",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Member",
- "mandatory_depends_on": "eval:doc.recipient === \"Member\";",
- "options": "Member"
- },
- {
- "depends_on": "eval:doc.recipient === \"Member\";",
- "fetch_from": "member.member_name",
- "fieldname": "member_name",
- "fieldtype": "Data",
- "label": "Member Name",
- "read_only": 1
- },
- {
- "depends_on": "eval:doc.recipient === \"Donor\";",
- "fieldname": "donor",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Donor",
- "mandatory_depends_on": "eval:doc.recipient === \"Donor\";",
- "options": "Donor"
- },
- {
- "fieldname": "column_break_4",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "date",
- "fieldtype": "Date",
- "label": "Date",
- "reqd": 1
- },
- {
- "depends_on": "eval:doc.recipient === \"Member\";",
- "fieldname": "section_break_6",
- "fieldtype": "Section Break"
- },
- {
- "fieldname": "payments",
- "fieldtype": "Table",
- "label": "Payments",
- "options": "Tax Exemption 80G Certificate Detail"
- },
- {
- "fieldname": "total",
- "fieldtype": "Currency",
- "in_list_view": 1,
- "label": "Total",
- "read_only": 1
- },
- {
- "depends_on": "eval:doc.recipient === \"Member\";",
- "fieldname": "fiscal_year",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Fiscal Year",
- "options": "Fiscal Year"
- },
- {
- "fieldname": "company",
- "fieldtype": "Link",
- "label": "Company",
- "options": "Company",
- "reqd": 1
- },
- {
- "fieldname": "get_payments",
- "fieldtype": "Button",
- "label": "Get Memberships"
- },
- {
- "fieldname": "naming_series",
- "fieldtype": "Select",
- "label": "Naming Series",
- "options": "NPO-80G-.YYYY.-"
- },
- {
- "fieldname": "section_break_11",
- "fieldtype": "Section Break",
- "label": "Company Details"
- },
- {
- "fieldname": "company_address",
- "fieldtype": "Link",
- "label": "Company Address",
- "options": "Address"
- },
- {
- "fieldname": "column_break_14",
- "fieldtype": "Column Break"
- },
- {
- "fetch_from": "company.pan_details",
- "fieldname": "company_pan_number",
- "fieldtype": "Data",
- "label": "PAN Number",
- "read_only": 1
- },
- {
- "fieldname": "company_address_display",
- "fieldtype": "Small Text",
- "hidden": 1,
- "label": "Company Address Display",
- "print_hide": 1,
- "read_only": 1
- },
- {
- "fetch_from": "company.company_80g_number",
- "fieldname": "company_80g_number",
- "fieldtype": "Data",
- "label": "80G Number",
- "read_only": 1
- },
- {
- "fetch_from": "company.with_effect_from",
- "fieldname": "company_80g_wef",
- "fieldtype": "Date",
- "label": "80G With Effect From",
- "read_only": 1
- },
- {
- "depends_on": "eval:doc.recipient === \"Donor\";",
- "fieldname": "donation_details_section",
- "fieldtype": "Section Break",
- "label": "Donation Details"
- },
- {
- "fieldname": "donation",
- "fieldtype": "Link",
- "label": "Donation",
- "mandatory_depends_on": "eval:doc.recipient === \"Donor\";",
- "options": "Donation"
- },
- {
- "fetch_from": "donation.amount",
- "fieldname": "amount",
- "fieldtype": "Currency",
- "label": "Amount",
- "read_only": 1
- },
- {
- "fetch_from": "donation.mode_of_payment",
- "fieldname": "mode_of_payment",
- "fieldtype": "Link",
- "label": "Mode of Payment",
- "options": "Mode of Payment",
- "read_only": 1
- },
- {
- "fetch_from": "donation.razorpay_payment_id",
- "fieldname": "razorpay_payment_id",
- "fieldtype": "Data",
- "label": "RazorPay Payment ID",
- "read_only": 1
- },
- {
- "fetch_from": "donation.date",
- "fieldname": "date_of_donation",
- "fieldtype": "Date",
- "label": "Date of Donation",
- "read_only": 1
- },
- {
- "fieldname": "column_break_27",
- "fieldtype": "Column Break"
- },
- {
- "depends_on": "eval:doc.recipient === \"Donor\";",
- "fetch_from": "donor.donor_name",
- "fieldname": "donor_name",
- "fieldtype": "Data",
- "label": "Donor Name",
- "read_only": 1
- },
- {
- "depends_on": "eval:doc.recipient === \"Donor\";",
- "fetch_from": "donor.email",
- "fieldname": "donor_email",
- "fieldtype": "Data",
- "label": "Email",
- "read_only": 1
- },
- {
- "depends_on": "eval:doc.recipient === \"Member\";",
- "fetch_from": "member.email_id",
- "fieldname": "member_email",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Email",
- "read_only": 1
- },
- {
- "depends_on": "eval:doc.recipient === \"Member\";",
- "fetch_from": "member.pan_number",
- "fieldname": "member_pan_number",
- "fieldtype": "Data",
- "label": "PAN Details",
- "read_only": 1
- },
- {
- "depends_on": "eval:doc.recipient === \"Donor\";",
- "fetch_from": "donor.pan_number",
- "fieldname": "donor_pan_number",
- "fieldtype": "Data",
- "label": "PAN Details",
- "read_only": 1
- },
- {
- "fieldname": "title",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Title",
- "print_hide": 1
- }
- ],
- "index_web_pages_for_search": 1,
- "links": [],
- "modified": "2021-02-22 00:03:34.215633",
- "modified_by": "Administrator",
- "module": "Regional",
- "name": "Tax Exemption 80G Certificate",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "search_fields": "member, member_name",
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "title",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
deleted file mode 100644
index 0f0897841b..0000000000
--- a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
+++ /dev/null
@@ -1,104 +0,0 @@
-# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import frappe
-from frappe import _
-from frappe.contacts.doctype.address.address import get_company_address
-from frappe.model.document import Document
-from frappe.utils import flt, get_link_to_form, getdate
-
-from erpnext.accounts.utils import get_fiscal_year
-
-
-class TaxExemption80GCertificate(Document):
- def validate(self):
- self.validate_date()
- self.validate_duplicates()
- self.validate_company_details()
- self.set_company_address()
- self.calculate_total()
- self.set_title()
-
- def validate_date(self):
- if self.recipient == 'Member':
- if getdate(self.date):
- fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True)
-
- if not (fiscal_year.year_start_date <= getdate(self.date) \
- <= fiscal_year.year_end_date):
- frappe.throw(_('The Certificate Date is not in the Fiscal Year {0}').format(frappe.bold(self.fiscal_year)))
-
- def validate_duplicates(self):
- if self.recipient == 'Donor':
- certificate = frappe.db.exists(self.doctype, {
- 'donation': self.donation,
- 'name': ('!=', self.name)
- })
- if certificate:
- frappe.throw(_('An 80G Certificate {0} already exists for the donation {1}').format(
- get_link_to_form(self.doctype, certificate), frappe.bold(self.donation)
- ), title=_('Duplicate Certificate'))
-
- def validate_company_details(self):
- fields = ['company_80g_number', 'with_effect_from', 'pan_details']
- company_details = frappe.db.get_value('Company', self.company, fields, as_dict=True)
- if not company_details.company_80g_number:
- frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('80G Number'),
- get_link_to_form('Company', self.company)))
-
- if not company_details.pan_details:
- frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('PAN Number'),
- get_link_to_form('Company', self.company)))
-
- @frappe.whitelist()
- def set_company_address(self):
- address = get_company_address(self.company)
- self.company_address = address.company_address
- self.company_address_display = address.company_address_display
-
- def calculate_total(self):
- if self.recipient == 'Donor':
- return
-
- total = 0
- for entry in self.payments:
- total += flt(entry.amount)
- self.total = total
-
- def set_title(self):
- if self.recipient == 'Member':
- self.title = self.member_name
- else:
- self.title = self.donor_name
-
- @frappe.whitelist()
- def get_payments(self):
- if not self.member:
- frappe.throw(_('Please select a Member first.'))
-
- fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True)
-
- memberships = frappe.db.get_all('Membership', {
- 'member': self.member,
- 'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
- 'membership_status': ('!=', 'Cancelled')
- }, ['from_date', 'amount', 'name', 'invoice', 'payment_id'], order_by='from_date')
-
- if not memberships:
- frappe.msgprint(_('No Membership Payments found against the Member {0}').format(self.member))
-
- total = 0
- self.payments = []
-
- for doc in memberships:
- self.append('payments', {
- 'date': doc.from_date,
- 'amount': doc.amount,
- 'invoice_id': doc.invoice,
- 'razorpay_payment_id': doc.payment_id,
- 'membership': doc.name
- })
- total += flt(doc.amount)
-
- self.total = total
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py
deleted file mode 100644
index 6fa3b85d06..0000000000
--- a/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py
+++ /dev/null
@@ -1,106 +0,0 @@
-# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-import frappe
-from frappe.utils import getdate
-
-from erpnext.accounts.utils import get_fiscal_year
-from erpnext.non_profit.doctype.donation.donation import create_donation
-from erpnext.non_profit.doctype.donation.test_donation import (
- create_donor,
- create_donor_type,
- create_mode_of_payment,
-)
-from erpnext.non_profit.doctype.member.member import create_member
-from erpnext.non_profit.doctype.membership.test_membership import make_membership, setup_membership
-
-
-class TestTaxExemption80GCertificate(unittest.TestCase):
- def setUp(self):
- frappe.db.sql('delete from `tabTax Exemption 80G Certificate`')
- frappe.db.sql('delete from `tabMembership`')
- create_donor_type()
- settings = frappe.get_doc('Non Profit Settings')
- settings.company = '_Test Company'
- settings.donation_company = '_Test Company'
- settings.default_donor_type = '_Test Donor'
- settings.creation_user = 'Administrator'
- settings.save()
-
- company = frappe.get_doc('Company', '_Test Company')
- company.pan_details = 'BBBTI3374C'
- company.company_80g_number = 'NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087'
- company.with_effect_from = getdate()
- company.save()
-
- def test_duplicate_donation_certificate(self):
- donor = create_donor()
- create_mode_of_payment()
- payment = frappe._dict({
- 'amount': 100,
- 'method': 'Debit Card',
- 'id': 'pay_MeXAmsgeKOhq7O'
- })
- donation = create_donation(donor, payment)
-
- args = frappe._dict({
- 'recipient': 'Donor',
- 'donor': donor.name,
- 'donation': donation.name
- })
- certificate = create_80g_certificate(args)
- certificate.insert()
-
- # check company details
- self.assertEqual(certificate.company_pan_number, 'BBBTI3374C')
- self.assertEqual(certificate.company_80g_number, 'NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087')
-
- # check donation details
- self.assertEqual(certificate.amount, donation.amount)
-
- duplicate_certificate = create_80g_certificate(args)
- # duplicate validation
- self.assertRaises(frappe.ValidationError, duplicate_certificate.insert)
-
- def test_membership_80g_certificate(self):
- plan = setup_membership()
-
- # make test member
- member_doc = create_member(frappe._dict({
- 'fullname': "_Test_Member",
- 'email': "_test_member_erpnext@example.com",
- 'plan_id': plan.name
- }))
- member_doc.make_customer_and_link()
- member = member_doc.name
-
- membership = make_membership(member, { "from_date": getdate() })
- invoice = membership.generate_invoice(save=True)
-
- args = frappe._dict({
- 'recipient': 'Member',
- 'member': member,
- 'fiscal_year': get_fiscal_year(getdate(), as_dict=True).get('name')
- })
- certificate = create_80g_certificate(args)
- certificate.get_payments()
- certificate.insert()
-
- self.assertEqual(len(certificate.payments), 1)
- self.assertEqual(certificate.payments[0].amount, membership.amount)
- self.assertEqual(certificate.payments[0].invoice_id, invoice.name)
-
-
-def create_80g_certificate(args):
- certificate = frappe.get_doc({
- 'doctype': 'Tax Exemption 80G Certificate',
- 'recipient': args.recipient,
- 'date': getdate(),
- 'company': '_Test Company'
- })
-
- certificate.update(args)
-
- return certificate
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/__init__.py b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json
deleted file mode 100644
index dfa817dd27..0000000000
--- a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json
+++ /dev/null
@@ -1,66 +0,0 @@
-{
- "actions": [],
- "creation": "2021-02-15 12:43:52.754124",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "date",
- "amount",
- "invoice_id",
- "column_break_4",
- "razorpay_payment_id",
- "membership"
- ],
- "fields": [
- {
- "fieldname": "date",
- "fieldtype": "Date",
- "in_list_view": 1,
- "label": "Date",
- "reqd": 1
- },
- {
- "fieldname": "amount",
- "fieldtype": "Currency",
- "in_list_view": 1,
- "label": "Amount",
- "reqd": 1
- },
- {
- "fieldname": "invoice_id",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Invoice ID",
- "options": "Sales Invoice",
- "reqd": 1
- },
- {
- "fieldname": "razorpay_payment_id",
- "fieldtype": "Data",
- "label": "Razorpay Payment ID"
- },
- {
- "fieldname": "membership",
- "fieldtype": "Link",
- "label": "Membership",
- "options": "Membership"
- },
- {
- "fieldname": "column_break_4",
- "fieldtype": "Column Break"
- }
- ],
- "index_web_pages_for_search": 1,
- "istable": 1,
- "links": [],
- "modified": "2021-02-15 16:35:10.777587",
- "modified_by": "Administrator",
- "module": "Regional",
- "name": "Tax Exemption 80G Certificate Detail",
- "owner": "Administrator",
- "permissions": [],
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py
deleted file mode 100644
index bb7f07f688..0000000000
--- a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-# import frappe
-from frappe.model.document import Document
-
-
-class TaxExemption80GCertificateDetail(Document):
- pass
From 16ca81d6a61be4bf7ef2df9ae488c9a49076244c Mon Sep 17 00:00:00 2001
From: ChillarAnand
Date: Thu, 27 Jan 2022 10:05:40 +0530
Subject: [PATCH 013/447] refactor: Remove non-profit code in ERPNext
---
.../doctype/payment_entry/payment_entry.js | 16 +++-----------
.../doctype/payment_entry/payment_entry.py | 22 +------------------
2 files changed, 4 insertions(+), 34 deletions(-)
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 3be3925b5a..a9bc0280bc 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -114,8 +114,6 @@ frappe.ui.form.on('Payment Entry', {
var doctypes = ["Expense Claim", "Journal Entry"];
} else if (frm.doc.party_type == "Student") {
var doctypes = ["Fees"];
- } else if (frm.doc.party_type == "Donor") {
- var doctypes = ["Donation"];
} else {
var doctypes = ["Journal Entry"];
}
@@ -144,7 +142,7 @@ frappe.ui.form.on('Payment Entry', {
const child = locals[cdt][cdn];
const filters = {"docstatus": 1, "company": doc.company};
const party_type_doctypes = ['Sales Invoice', 'Sales Order', 'Purchase Invoice',
- 'Purchase Order', 'Expense Claim', 'Fees', 'Dunning', 'Donation'];
+ 'Purchase Order', 'Expense Claim', 'Fees', 'Dunning'];
if (in_list(party_type_doctypes, child.reference_doctype)) {
filters[doc.party_type.toLowerCase()] = doc.party;
@@ -747,8 +745,7 @@ frappe.ui.form.on('Payment Entry', {
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") ||
- (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") ||
- (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Donor")
+ (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student")
) {
if(total_positive_outstanding > total_negative_outstanding)
if (!frm.doc.paid_amount)
@@ -791,8 +788,7 @@ frappe.ui.form.on('Payment Entry', {
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") ||
- (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") ||
- (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Donor")
+ (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student")
) {
if(total_positive_outstanding_including_order > paid_amount) {
var remaining_outstanding = total_positive_outstanding_including_order - paid_amount;
@@ -951,12 +947,6 @@ frappe.ui.form.on('Payment Entry', {
frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Expense Claim or Journal Entry", [row.idx]));
return false;
}
-
- if (frm.doc.party_type == "Donor" && row.reference_doctype != "Donation") {
- frappe.model.set_value(row.doctype, row.name, "reference_doctype", null);
- frappe.msgprint(__("Row #{0}: Reference Document Type must be Donation", [row.idx]));
- return false;
- }
}
if (row) {
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 02a144d3e7..286052cf3a 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -91,7 +91,6 @@ class PaymentEntry(AccountsController):
self.update_expense_claim()
self.update_outstanding_amounts()
self.update_advance_paid()
- self.update_donation()
self.update_payment_schedule()
self.set_status()
@@ -101,7 +100,6 @@ class PaymentEntry(AccountsController):
self.update_expense_claim()
self.update_outstanding_amounts()
self.update_advance_paid()
- self.update_donation(cancel=1)
self.delink_advance_entry_references()
self.update_payment_schedule(cancel=1)
self.set_payment_req_status()
@@ -284,8 +282,6 @@ class PaymentEntry(AccountsController):
valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance", "Gratuity")
elif self.party_type == "Shareholder":
valid_reference_doctypes = ("Journal Entry")
- elif self.party_type == "Donor":
- valid_reference_doctypes = ("Donation")
for d in self.get("references"):
if not d.allocated_amount:
@@ -843,13 +839,6 @@ class PaymentEntry(AccountsController):
else:
update_reimbursed_amount(doc, d.allocated_amount)
- def update_donation(self, cancel=0):
- if self.payment_type == "Receive" and self.party_type == "Donor" and self.party:
- for d in self.get("references"):
- if d.reference_doctype=="Donation" and d.reference_name:
- is_paid = 0 if cancel else 1
- frappe.db.set_value("Donation", d.reference_name, "paid", is_paid)
-
def on_recurring(self, reference_doc, auto_repeat_doc):
self.reference_no = reference_doc.name
self.reference_date = nowdate()
@@ -1337,10 +1326,6 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
total_amount = ref_doc.get("grand_total")
exchange_rate = 1
outstanding_amount = ref_doc.get("outstanding_amount")
- elif reference_doctype == "Donation":
- total_amount = ref_doc.get("amount")
- outstanding_amount = total_amount
- exchange_rate = 1
elif reference_doctype == "Dunning":
total_amount = ref_doc.get("dunning_amount")
exchange_rate = 1
@@ -1611,8 +1596,6 @@ def set_party_type(dt):
party_type = "Employee"
elif dt == "Fees":
party_type = "Student"
- elif dt == "Donation":
- party_type = "Donor"
return party_type
def set_party_account(dt, dn, doc, party_type):
@@ -1640,7 +1623,7 @@ def set_party_account_currency(dt, party_account, doc):
return party_account_currency
def set_payment_type(dt, doc):
- if (dt in ("Sales Order", "Donation") or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \
+ if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \
or (dt=="Purchase Invoice" and doc.outstanding_amount < 0):
payment_type = "Receive"
else:
@@ -1673,9 +1656,6 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre
elif dt == "Dunning":
grand_total = doc.grand_total
outstanding_amount = doc.grand_total
- elif dt == "Donation":
- grand_total = doc.amount
- outstanding_amount = doc.amount
elif dt == "Gratuity":
grand_total = doc.amount
outstanding_amount = flt(doc.amount) - flt(doc.paid_amount)
From 538b64b1fa460327236ef8b260aec6c52adf21ff Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Mon, 31 Jan 2022 21:50:42 +0530
Subject: [PATCH 014/447] refactor: Employee Leave Balance Report
- incorrect opening balance on boundary allocation dates
- carry forwarded leaves accounted in leaves allocated column, should be part of opening balance
- leaves allocated column also adds expired leave allocations causing negative balances, should only consider non-expiry records
---
.../employee_leave_balance.py | 139 +++++++++++++-----
1 file changed, 103 insertions(+), 36 deletions(-)
diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
index b375b18b07..6280ef323b 100644
--- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
@@ -6,8 +6,9 @@ from itertools import groupby
import frappe
from frappe import _
-from frappe.utils import add_days
+from frappe.utils import add_days, date_diff, getdate
+from erpnext.hr.doctype.leave_allocation.leave_allocation import get_previous_allocation
from erpnext.hr.doctype.leave_application.leave_application import (
get_leave_balance_on,
get_leaves_for_period,
@@ -46,27 +47,27 @@ def get_columns():
'label': _('Opening Balance'),
'fieldtype': 'float',
'fieldname': 'opening_balance',
- 'width': 130,
+ 'width': 150,
}, {
- 'label': _('Leave Allocated'),
+ 'label': _('New Leave(s) Allocated'),
'fieldtype': 'float',
'fieldname': 'leaves_allocated',
- 'width': 130,
+ 'width': 200,
}, {
- 'label': _('Leave Taken'),
+ 'label': _('Leave(s) Taken'),
'fieldtype': 'float',
'fieldname': 'leaves_taken',
- 'width': 130,
+ 'width': 150,
}, {
- 'label': _('Leave Expired'),
+ 'label': _('Leave(s) Expired'),
'fieldtype': 'float',
'fieldname': 'leaves_expired',
- 'width': 130,
+ 'width': 150,
}, {
'label': _('Closing Balance'),
'fieldtype': 'float',
'fieldname': 'closing_balance',
- 'width': 130,
+ 'width': 150,
}]
return columns
@@ -108,10 +109,9 @@ def get_data(filters):
leaves_taken = get_leaves_for_period(employee.name, leave_type,
filters.from_date, filters.to_date) * -1
- new_allocation, expired_leaves = get_allocated_and_expired_leaves(filters.from_date, filters.to_date, employee.name, leave_type)
-
-
- opening = get_leave_balance_on(employee.name, leave_type, add_days(filters.from_date, -1)) #allocation boundary condition
+ new_allocation, expired_leaves, carry_forwarded_leaves = get_allocated_and_expired_leaves(
+ filters.from_date, filters.to_date, employee.name, leave_type)
+ opening = get_opening_balance(employee.name, leave_type, filters, carry_forwarded_leaves)
row.leaves_allocated = new_allocation
row.leaves_expired = expired_leaves - leaves_taken if expired_leaves - leaves_taken > 0 else 0
@@ -125,6 +125,55 @@ def get_data(filters):
return data
+
+def get_opening_balance(employee, leave_type, filters, carry_forwarded_leaves):
+ # allocation boundary condition
+ # opening balance is the closing leave balance 1 day before the filter start date
+ opening_balance_date = add_days(filters.from_date, -1)
+ allocation = get_previous_allocation(filters.from_date, leave_type, employee)
+
+ if allocation and allocation.get("to_date") and opening_balance_date and \
+ getdate(allocation.get("to_date")) == getdate(opening_balance_date):
+ # if opening balance date is same as the previous allocation's expiry
+ # then opening balance should only consider carry forwarded leaves
+ opening_balance = carry_forwarded_leaves
+ else:
+ # else directly get closing leave balance on the previous day
+ opening_balance = get_closing_balance_on(opening_balance_date, employee, leave_type, filters)
+
+ return opening_balance
+
+
+def get_closing_balance_on(date, employee, leave_type, filters):
+ closing_balance = get_leave_balance_on(employee, leave_type, date)
+ leave_allocation = get_leave_allocation_for_date(employee, leave_type, date)
+ if leave_allocation:
+ # if balance is greater than the days remaining for leave allocation's end date
+ # then balance should be = remaining days
+ remaining_days = date_diff(leave_allocation[0].to_date, filters.from_date) + 1
+ if remaining_days < closing_balance:
+ closing_balance = remaining_days
+
+ return closing_balance
+
+
+def get_leave_allocation_for_date(employee, leave_type, date):
+ allocation = frappe.qb.DocType('Leave Allocation')
+ records = (
+ frappe.qb.from_(allocation)
+ .select(
+ allocation.name, allocation.to_date
+ ).where(
+ (allocation.docstatus == 1)
+ & (allocation.employee == employee)
+ & (allocation.leave_type == leave_type)
+ & ((allocation.from_date <= date) & (allocation.to_date >= date))
+ )
+ ).run(as_dict=True)
+
+ return records
+
+
def get_conditions(filters):
conditions={
'status': 'Active',
@@ -140,6 +189,7 @@ def get_conditions(filters):
return conditions
+
def get_department_leave_approver_map(department=None):
# get current department and all its child
@@ -171,39 +221,55 @@ def get_department_leave_approver_map(department=None):
return approvers
+
def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type):
-
- from frappe.utils import getdate
-
new_allocation = 0
expired_leaves = 0
+ carry_forwarded_leaves = 0
- records= frappe.db.sql("""
- SELECT
- employee, leave_type, from_date, to_date, leaves, transaction_name,
- transaction_type, is_carry_forward, is_expired
- FROM `tabLeave Ledger Entry`
- WHERE employee=%(employee)s AND leave_type=%(leave_type)s
- AND docstatus=1
- AND transaction_type = 'Leave Allocation'
- AND (from_date between %(from_date)s AND %(to_date)s
- OR to_date between %(from_date)s AND %(to_date)s
- OR (from_date < %(from_date)s AND to_date > %(to_date)s))
- """, {
- "from_date": from_date,
- "to_date": to_date,
- "employee": employee,
- "leave_type": leave_type
- }, as_dict=1)
+ records = get_leave_ledger_entries(from_date, to_date, employee, leave_type)
for record in records:
if record.to_date < getdate(to_date):
expired_leaves += record.leaves
- if record.from_date >= getdate(from_date):
- new_allocation += record.leaves
+ # new allocation records with `is_expired=1` are created when leave expires
+ # these new records should not be considered, else it leads to negative leave balance
+ if record.is_expired:
+ continue
+
+ if record.from_date >= getdate(from_date):
+ if record.is_carry_forward:
+ carry_forwarded_leaves += record.leaves
+ else:
+ new_allocation += record.leaves
+
+ return new_allocation, expired_leaves, carry_forwarded_leaves
+
+
+def get_leave_ledger_entries(from_date, to_date, employee, leave_type):
+ ledger = frappe.qb.DocType('Leave Ledger Entry')
+ records = (
+ frappe.qb.from_(ledger)
+ .select(
+ ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date,
+ ledger.leaves, ledger.transaction_name, ledger.transaction_type,
+ ledger.is_carry_forward, ledger.is_expired
+ ).where(
+ (ledger.docstatus == 1)
+ & (ledger.transaction_type == 'Leave Allocation')
+ & (ledger.employee == employee)
+ & (ledger.leave_type == leave_type)
+ & (
+ (ledger.from_date[from_date: to_date])
+ | (ledger.to_date[from_date: to_date])
+ | ((ledger.from_date < from_date) & (ledger.to_date > to_date))
+ )
+ )
+ ).run(as_dict=True)
+
+ return records
- return new_allocation, expired_leaves
def get_chart_data(data):
labels = []
@@ -224,6 +290,7 @@ def get_chart_data(data):
return chart
+
def get_dataset_for_chart(employee_data, datasets, labels):
leaves = []
employee_data = sorted(employee_data, key=lambda k: k['employee_name'])
From 899ca8b3849995ac9164e70c4d5c73d6ebdb106a Mon Sep 17 00:00:00 2001
From: ChillarAnand
Date: Mon, 31 Jan 2022 17:43:01 +0530
Subject: [PATCH 015/447] refactor: Code cleanup
---
.../workspace/non_profit/non_profit.json | 272 ------------------
.../v14_0/delete_non_profit_doctypes.py | 20 ++
.../80g_certificate_for_donation.json | 26 --
.../80g_certificate_for_donation/__init__.py | 0
.../80g_certificate_for_membership.json | 26 --
.../__init__.py | 0
6 files changed, 20 insertions(+), 324 deletions(-)
delete mode 100644 erpnext/non_profit/workspace/non_profit/non_profit.json
delete mode 100644 erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json
delete mode 100644 erpnext/regional/print_format/80g_certificate_for_donation/__init__.py
delete mode 100644 erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json
delete mode 100644 erpnext/regional/print_format/80g_certificate_for_membership/__init__.py
diff --git a/erpnext/non_profit/workspace/non_profit/non_profit.json b/erpnext/non_profit/workspace/non_profit/non_profit.json
deleted file mode 100644
index fc90475fb3..0000000000
--- a/erpnext/non_profit/workspace/non_profit/non_profit.json
+++ /dev/null
@@ -1,272 +0,0 @@
-{
- "charts": [],
- "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Member\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Non Profit Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Membership\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chapter\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chapter Member\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loan Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Grant Application\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Membership\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Volunteer\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Chapter\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Donation\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tax Exemption Certification (India)\",\"col\":4}}]",
- "creation": "2020-03-02 17:23:47.811421",
- "docstatus": 0,
- "doctype": "Workspace",
- "for_user": "",
- "hide_custom": 0,
- "icon": "non-profit",
- "idx": 0,
- "label": "Non Profit",
- "links": [
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Loan Management",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Loan Type",
- "link_count": 0,
- "link_to": "Loan Type",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Loan Application",
- "link_count": 0,
- "link_to": "Loan Application",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Loan",
- "link_count": 0,
- "link_to": "Loan",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Grant Application",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Grant Application",
- "link_count": 0,
- "link_to": "Grant Application",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Membership",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Member",
- "link_count": 0,
- "link_to": "Member",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Membership",
- "link_count": 0,
- "link_to": "Membership",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Membership Type",
- "link_count": 0,
- "link_to": "Membership Type",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Membership Settings",
- "link_count": 0,
- "link_to": "Non Profit Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Volunteer",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Volunteer",
- "link_count": 0,
- "link_to": "Volunteer",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Volunteer Type",
- "link_count": 0,
- "link_to": "Volunteer Type",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Chapter",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Chapter",
- "link_count": 0,
- "link_to": "Chapter",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Donation",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Donor",
- "link_count": 0,
- "link_to": "Donor",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Donor Type",
- "link_count": 0,
- "link_to": "Donor Type",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Donation",
- "link_count": 0,
- "link_to": "Donation",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Tax Exemption Certification (India)",
- "link_count": 0,
- "link_type": "DocType",
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Tax Exemption 80G Certificate",
- "link_count": 0,
- "link_to": "Tax Exemption 80G Certificate",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- }
- ],
- "modified": "2022-01-13 17:40:50.220877",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Non Profit",
- "owner": "Administrator",
- "parent_page": "",
- "public": 1,
- "restrict_to_domain": "Non Profit",
- "roles": [],
- "sequence_id": 18.0,
- "shortcuts": [
- {
- "label": "Member",
- "link_to": "Member",
- "type": "DocType"
- },
- {
- "label": "Non Profit Settings",
- "link_to": "Non Profit Settings",
- "type": "DocType"
- },
- {
- "label": "Membership",
- "link_to": "Membership",
- "type": "DocType"
- },
- {
- "label": "Chapter",
- "link_to": "Chapter",
- "type": "DocType"
- },
- {
- "label": "Chapter Member",
- "link_to": "Chapter Member",
- "type": "DocType"
- }
- ],
- "title": "Non Profit"
-}
\ No newline at end of file
diff --git a/erpnext/patches/v14_0/delete_non_profit_doctypes.py b/erpnext/patches/v14_0/delete_non_profit_doctypes.py
index 3b3dbe4576..86355a6426 100644
--- a/erpnext/patches/v14_0/delete_non_profit_doctypes.py
+++ b/erpnext/patches/v14_0/delete_non_profit_doctypes.py
@@ -6,6 +6,14 @@ def execute():
frappe.delete_doc("Workspace", "Non Profit", ignore_missing=True, force=True)
+ print_formats = frappe.get_all("Print Format", {"module": "Non Profit", "standard": "Yes"}, pluck='name')
+ for print_format in print_formats:
+ frappe.delete_doc("Print Format", print_format, ignore_missing=True, force=True)
+
+ print_formats = ['80G Certificate for Membership', '80G Certificate for Donation']
+ for print_format in print_formats:
+ frappe.delete_doc("Print Format", print_format, ignore_missing=True, force=True)
+
reports = frappe.get_all("Report", {"module": "Non Profit", "is_standard": "Yes"}, pluck='name')
for report in reports:
frappe.delete_doc("Report", report, ignore_missing=True, force=True)
@@ -17,3 +25,15 @@ def execute():
doctypes = frappe.get_all("DocType", {"module": "Non Profit", "custom": 0}, pluck='name')
for doctype in doctypes:
frappe.delete_doc("DocType", doctype, ignore_missing=True)
+
+ doctypes = ['Tax Exemption 80G Certificate', 'Tax Exemption 80G Certificate Detail']
+ for doctype in doctypes:
+ frappe.delete_doc("DocType", doctype, ignore_missing=True)
+
+ custom_fields = [
+ {"dt": "Member", "fieldname": "pan_number"},
+ {"dt": "Donor", "fieldname": "pan_number"},
+ ]
+ for field in custom_fields:
+ custom_field = frappe.db.get_value("Custom Field", field)
+ frappe.delete_doc("Custom Field", custom_field, ignore_missing=True)
diff --git a/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json b/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json
deleted file mode 100644
index a8da0bd209..0000000000
--- a/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "absolute_value": 0,
- "align_labels_right": 0,
- "creation": "2021-02-22 00:17:33.878581",
- "css": ".details {\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n line-height: 150%;\n}\n\n.certificate-footer {\n font-size: 15px;\n font-family: Tahoma, sans-serif;\n line-height: 140%;\n margin-top: 120px;\n}\n\n.company-address {\n color: #666666;\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n}",
- "custom_format": 1,
- "default_print_language": "en",
- "disabled": 0,
- "doc_type": "Tax Exemption 80G Certificate",
- "docstatus": 0,
- "doctype": "Print Format",
- "font": "Default",
- "html": "{% if letter_head and not no_letterhead -%}\n {{ letter_head }}
\n{%- endif %}\n\n\n
{{ doc.company }} 80G Donor Certificate \n
\n \n\n\n
{{ _(\"Certificate No. : \") }} {{ doc.name }}
\n
\n \t{{ _(\"Date\") }} : {{ doc.get_formatted(\"date\") }} \n
\n
\n \n
\n\n This is to confirm that the {{ doc.company }} received an amount of
{{doc.get_formatted(\"amount\")}} \n from
{{ doc.donor_name }} \n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n\n via the Mode of Payment {{doc.mode_of_payment}}\n\n {% if doc.razorpay_payment_id -%}\n bearing RazorPay Payment ID {{ doc.razorpay_payment_id }}\n {%- endif %}\n\n on {{ doc.get_formatted(\"date_of_donation\") }}\n
\n \n
\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n
\n\n
\n
\n\n \n {{doc.company_address_display }}
\n\n",
- "idx": 0,
- "line_breaks": 0,
- "modified": "2021-02-22 00:20:08.516600",
- "modified_by": "Administrator",
- "module": "Regional",
- "name": "80G Certificate for Donation",
- "owner": "Administrator",
- "print_format_builder": 0,
- "print_format_type": "Jinja",
- "raw_printing": 0,
- "show_section_headings": 0,
- "standard": "Yes"
-}
\ No newline at end of file
diff --git a/erpnext/regional/print_format/80g_certificate_for_donation/__init__.py b/erpnext/regional/print_format/80g_certificate_for_donation/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json b/erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json
deleted file mode 100644
index f1b15aab29..0000000000
--- a/erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "absolute_value": 0,
- "align_labels_right": 0,
- "creation": "2021-02-15 16:53:55.026611",
- "css": ".details {\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n line-height: 150%;\n}\n\n.certificate-footer {\n font-size: 15px;\n font-family: Tahoma, sans-serif;\n line-height: 140%;\n margin-top: 120px;\n}\n\n.company-address {\n color: #666666;\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n}",
- "custom_format": 1,
- "default_print_language": "en",
- "disabled": 0,
- "doc_type": "Tax Exemption 80G Certificate",
- "docstatus": 0,
- "doctype": "Print Format",
- "font": "Default",
- "html": "{% if letter_head and not no_letterhead -%}\n {{ letter_head }}
\n{%- endif %}\n\n\n
{{ doc.company }} Members 80G Donor Certificate \n Financial Cycle {{ doc.fiscal_year }} \n\n \n\n\n
{{ _(\"Certificate No. : \") }} {{ doc.name }}
\n
\n \t{{ _(\"Date\") }} : {{ doc.get_formatted(\"date\") }} \n
\n
\n \n
\n This is to confirm that the {{ doc.company }} received a total amount of
{{doc.get_formatted(\"total\")}} \n from
{{ doc.member_name }} \n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n as per the payment details given below:\n \n
\n
\n \t\n \t\t\n \t\t\t{{ _(\"Date\") }} \n \t\t\t{{ _(\"Amount\") }} \n \t\t\t{{ _(\"Invoice ID\") }} \n \t\t \n \t \n \t\n \t\t{%- for payment in doc.payments -%}\n \t\t\n \t\t\t {{ payment.date }} \n \t\t\t{{ payment.get_formatted(\"amount\") }} \n \t\t\t{{ payment.invoice_id }} \n \t\t \n \t\t{%- endfor -%}\n \t \n
\n \n
\n \n
\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n
\n\n
\n
\n\n \n {{doc.company_address_display }}
\n\n",
- "idx": 0,
- "line_breaks": 0,
- "modified": "2021-02-21 23:29:00.778973",
- "modified_by": "Administrator",
- "module": "Regional",
- "name": "80G Certificate for Membership",
- "owner": "Administrator",
- "print_format_builder": 0,
- "print_format_type": "Jinja",
- "raw_printing": 0,
- "show_section_headings": 0,
- "standard": "Yes"
-}
\ No newline at end of file
diff --git a/erpnext/regional/print_format/80g_certificate_for_membership/__init__.py b/erpnext/regional/print_format/80g_certificate_for_membership/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
From dddeef71799bdc9c0c787fc1fee9dd05e3c998e0 Mon Sep 17 00:00:00 2001
From: ChillarAnand
Date: Thu, 3 Feb 2022 16:41:36 +0530
Subject: [PATCH 016/447] chore: Clean up whitespace
---
erpnext/patches.txt | 1 -
1 file changed, 1 deletion(-)
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 09cfdcee82..a8134f20fc 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -336,7 +336,6 @@ erpnext.patches.v13_0.enable_provisional_accounting
erpnext.patches.v13_0.non_profit_deprecation_warning
erpnext.patches.v14_0.delete_non_profit_doctypes
-
[post_model_sync]
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template
From 1ea749cf1a8f4f25cb5465ac607f242779e0aaac Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Thu, 3 Feb 2022 23:34:46 +0530
Subject: [PATCH 017/447] fix: expired leaves not calculated properly because
of newly created expiry ledger entries
---
.../report/employee_leave_balance/employee_leave_balance.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
index 6280ef323b..5172fb8fc2 100644
--- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
@@ -230,14 +230,14 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type):
records = get_leave_ledger_entries(from_date, to_date, employee, leave_type)
for record in records:
- if record.to_date < getdate(to_date):
- expired_leaves += record.leaves
-
# new allocation records with `is_expired=1` are created when leave expires
# these new records should not be considered, else it leads to negative leave balance
if record.is_expired:
continue
+ if record.to_date < getdate(to_date):
+ expired_leaves += record.leaves
+
if record.from_date >= getdate(from_date):
if record.is_carry_forward:
carry_forwarded_leaves += record.leaves
From 26b40e7cfd6649a08e430ce5ce9000f61cd09d89 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Fri, 4 Feb 2022 00:01:05 +0530
Subject: [PATCH 018/447] refactor: Leaves Taken calculation
- fix issue where Leaves Taken also adds leaves expiring on boundary date as leaves taken, causing ambiguity
- remove unnecessary `skip_expiry_leaves` function
- `get_allocation_expiry` considering cancelled entries too
---
.../doctype/leave_application/leave_application.py | 14 +++++---------
.../leave_ledger_entry/leave_ledger_entry.py | 2 +-
2 files changed, 6 insertions(+), 10 deletions(-)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 70250f5bcf..0cda5e267c 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -480,7 +480,8 @@ def get_allocation_expiry(employee, leave_type, to_date, from_date):
'leave_type': leave_type,
'is_carry_forward': 1,
'transaction_type': 'Leave Allocation',
- 'to_date': ['between', (from_date, to_date)]
+ 'to_date': ['between', (from_date, to_date)],
+ 'docstatus': 1
},fields=['to_date'])
return expiry[0]['to_date'] if expiry else None
@@ -636,18 +637,18 @@ def get_remaining_leaves(allocation, leaves_taken, date, expiry):
return _get_remaining_leaves(total_leaves, allocation.to_date)
-def get_leaves_for_period(employee, leave_type, from_date, to_date, do_not_skip_expired_leaves=False):
+def get_leaves_for_period(employee, leave_type, from_date, to_date, skip_expired_leaves=True):
leave_entries = get_leave_entries(employee, leave_type, from_date, to_date)
leave_days = 0
for leave_entry in leave_entries:
inclusive_period = leave_entry.from_date >= getdate(from_date) and leave_entry.to_date <= getdate(to_date)
- if inclusive_period and leave_entry.transaction_type == 'Leave Encashment':
+ if inclusive_period and leave_entry.transaction_type == 'Leave Encashment':
leave_days += leave_entry.leaves
elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' and leave_entry.is_expired \
- and (do_not_skip_expired_leaves or not skip_expiry_leaves(leave_entry, to_date)):
+ and not skip_expired_leaves:
leave_days += leave_entry.leaves
elif leave_entry.transaction_type == 'Leave Application':
@@ -669,11 +670,6 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date, do_not_skip_
return leave_days
-def skip_expiry_leaves(leave_entry, date):
- ''' Checks whether the expired leaves coincide with the to_date of leave balance check.
- This allows backdated leave entry creation for non carry forwarded allocation '''
- end_date = frappe.db.get_value("Leave Allocation", {'name': leave_entry.transaction_name}, ['to_date'])
- return True if end_date == date and not leave_entry.is_carry_forward else False
def get_leave_entries(employee, leave_type, from_date, to_date):
''' Returns leave entries between from_date and to_date. '''
diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py
index 5c5299ea7e..a5923e0021 100644
--- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py
+++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py
@@ -171,7 +171,7 @@ def expire_carried_forward_allocation(allocation):
''' Expires remaining leaves in the on carried forward allocation '''
from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period
leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type,
- allocation.from_date, allocation.to_date, do_not_skip_expired_leaves=True)
+ allocation.from_date, allocation.to_date, skip_expired_leaves=False)
leaves = flt(allocation.leaves) + flt(leaves_taken)
# allow expired leaves entry to be created
From 55ac8519bfecdb42f45ad255835070097544dfcb Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Fri, 4 Feb 2022 12:29:00 +0530
Subject: [PATCH 019/447] refactor: balance in Balance Summary report near
allocation expiry date
- Leave Balance shows minimum leaves remaining after comparing with remaining days for allocation expiry causing ambiguity
- refactor remaining leaves calculation to return both, actual leave balance and balance for consumption
- show actual balance in leave application, use balance for consumption only in validations
---
.../leave_application/leave_application.py | 68 +++++++++++++------
1 file changed, 47 insertions(+), 21 deletions(-)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 0cda5e267c..ca376dca61 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -29,11 +29,13 @@ from erpnext.hr.utils import (
validate_active_employee,
)
+from typing import Dict
class LeaveDayBlockedError(frappe.ValidationError): pass
class OverlapError(frappe.ValidationError): pass
class AttendanceAlreadyMarkedError(frappe.ValidationError): pass
class NotAnOptionalHoliday(frappe.ValidationError): pass
+class InsufficientLeaveBalanceError(frappe.ValidationError): pass
from frappe.model.document import Document
@@ -260,15 +262,18 @@ class LeaveApplication(Document):
frappe.throw(_("The day(s) on which you are applying for leave are holidays. You need not apply for leave."))
if not is_lwp(self.leave_type):
- self.leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date, self.to_date,
- consider_all_leaves_in_the_allocation_period=True)
- if self.status != "Rejected" and (self.leave_balance < self.total_leave_days or not self.leave_balance):
+ leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date, self.to_date,
+ consider_all_leaves_in_the_allocation_period=True, for_consumption=True)
+ self.leave_balance = leave_balance.get("leave_balance")
+ leave_balance_for_consumption = leave_balance.get("leave_balance_for_consumption")
+
+ if self.status != "Rejected" and (leave_balance_for_consumption < self.total_leave_days or not leave_balance_for_consumption):
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
- frappe.msgprint(_("Note: There is not enough leave balance for Leave Type {0}")
- .format(self.leave_type))
+ frappe.msgprint(_("Insufficient leave balance for Leave Type {0}")
+ .format(frappe.bold(self.leave_type)), title=_("Warning"), indicator="orange")
else:
- frappe.throw(_("There is not enough leave balance for Leave Type {0}")
- .format(self.leave_type))
+ frappe.throw(_("Insufficient leave balance for Leave Type {0}")
+ .format(self.leave_type), InsufficientLeaveBalanceError, title=_("Insufficient Balance"))
def validate_leave_overlap(self):
if not self.name:
@@ -425,7 +430,7 @@ class LeaveApplication(Document):
if self.status != 'Approved' and submit:
return
- expiry_date = get_allocation_expiry(self.employee, self.leave_type,
+ expiry_date = get_allocation_expiry_for_cf_leaves(self.employee, self.leave_type,
self.to_date, self.from_date)
lwp = frappe.db.get_value("Leave Type", self.leave_type, "is_lwp")
@@ -472,7 +477,7 @@ class LeaveApplication(Document):
create_leave_ledger_entry(self, args, submit)
-def get_allocation_expiry(employee, leave_type, to_date, from_date):
+def get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, from_date):
''' Returns expiry of carry forward allocation in leave ledger entry '''
expiry = frappe.get_all("Leave Ledger Entry",
filters={
@@ -544,7 +549,8 @@ def get_leave_details(employee, date):
return ret
@frappe.whitelist()
-def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_leaves_in_the_allocation_period=False):
+def get_leave_balance_on(employee, leave_type, date, to_date=None,
+ consider_all_leaves_in_the_allocation_period=False, for_consumption=False):
'''
Returns leave balance till date
:param employee: employee name
@@ -552,6 +558,11 @@ def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_
:param date: date to check balance on
:param to_date: future date to check for allocation expiry
:param consider_all_leaves_in_the_allocation_period: consider all leaves taken till the allocation end date
+ :param for_consumption: flag to check if leave balance is required for consumption or display
+ eg: employee has leave balance = 10 but allocation is expiring in 1 day so employee can only consume 1 leave
+ in this case leave_balance = 10 but leave_balance_for_consumption = 1
+ if True, returns a dict eg: {'leave_balance': 10, 'leave_balance_for_consumption': 1}
+ else, returns leave_balance (in this case 10)
'''
if not to_date:
@@ -561,11 +572,17 @@ def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_
allocation = allocation_records.get(leave_type, frappe._dict())
end_date = allocation.to_date if consider_all_leaves_in_the_allocation_period else date
- expiry = get_allocation_expiry(employee, leave_type, to_date, date)
+ cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date)
leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date)
- return get_remaining_leaves(allocation, leaves_taken, date, expiry)
+ remaining_leaves = get_remaining_leaves(allocation, leaves_taken, date, cf_expiry)
+
+ if for_consumption:
+ return remaining_leaves
+ else:
+ return remaining_leaves.get('leave_balance')
+
def get_leave_allocation_records(employee, date, leave_type=None):
''' returns the total allocated leaves and carry forwarded leaves based on ledger entries '''
@@ -617,25 +634,34 @@ def get_pending_leaves_for_period(employee, leave_type, from_date, to_date):
}, fields=['SUM(total_leave_days) as leaves'])[0]
return leaves['leaves'] if leaves['leaves'] else 0.0
-def get_remaining_leaves(allocation, leaves_taken, date, expiry):
- ''' Returns minimum leaves remaining after comparing with remaining days for allocation expiry '''
+def get_remaining_leaves(allocation, leaves_taken, date, cf_expiry) -> Dict[str, float]:
+ '''Returns a dict of leave_balance and leave_balance_for_consumption
+ leave_balance returns the available leave balance
+ leave_balance_for_consumption returns the minimum leaves remaining after comparing with remaining days for allocation expiry
+ '''
def _get_remaining_leaves(remaining_leaves, end_date):
-
+ ''' Returns minimum leaves remaining after comparing with remaining days for allocation expiry '''
if remaining_leaves > 0:
remaining_days = date_diff(end_date, date) + 1
remaining_leaves = min(remaining_days, remaining_leaves)
return remaining_leaves
- total_leaves = flt(allocation.total_leaves_allocated) + flt(leaves_taken)
+ leave_balance = leave_balance_for_consumption = flt(allocation.total_leaves_allocated) + flt(leaves_taken)
- if expiry and allocation.unused_leaves:
- remaining_leaves = flt(allocation.unused_leaves) + flt(leaves_taken)
- remaining_leaves = _get_remaining_leaves(remaining_leaves, expiry)
+ # balance for carry forwarded leaves
+ if cf_expiry and allocation.unused_leaves:
+ cf_leaves = flt(allocation.unused_leaves) + flt(leaves_taken)
+ remaining_cf_leaves = _get_remaining_leaves(cf_leaves, cf_expiry)
- total_leaves = flt(allocation.new_leaves_allocated) + flt(remaining_leaves)
+ leave_balance = flt(allocation.new_leaves_allocated) + flt(cf_leaves)
+ leave_balance_for_consumption = flt(allocation.new_leaves_allocated) + flt(remaining_cf_leaves)
- return _get_remaining_leaves(total_leaves, allocation.to_date)
+ remaining_leaves = _get_remaining_leaves(leave_balance_for_consumption, allocation.to_date)
+ return {
+ 'leave_balance': leave_balance,
+ 'leave_balance_for_consumption': remaining_leaves
+ }
def get_leaves_for_period(employee, leave_type, from_date, to_date, skip_expired_leaves=True):
leave_entries = get_leave_entries(employee, leave_type, from_date, to_date)
From b5c686ac4035491f5e2bf3a4709f54f94c04dd06 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Fri, 4 Feb 2022 12:39:58 +0530
Subject: [PATCH 020/447] fix: sort imports, sider issues
---
erpnext/hr/doctype/leave_application/leave_application.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index ca376dca61..dbb3db36f4 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
+from typing import Dict
import frappe
from frappe import _
@@ -29,13 +30,13 @@ from erpnext.hr.utils import (
validate_active_employee,
)
-from typing import Dict
class LeaveDayBlockedError(frappe.ValidationError): pass
class OverlapError(frappe.ValidationError): pass
class AttendanceAlreadyMarkedError(frappe.ValidationError): pass
class NotAnOptionalHoliday(frappe.ValidationError): pass
-class InsufficientLeaveBalanceError(frappe.ValidationError): pass
+class InsufficientLeaveBalanceError(frappe.ValidationError):
+ pass
from frappe.model.document import Document
From dbfa463738a7f91f9d5c21a700f604f3325cd923 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Fri, 4 Feb 2022 13:04:25 +0530
Subject: [PATCH 021/447] fix: show actual balance instead of consumption
balance in opening balance
- not changing opening balance based on remaining days
---
.../employee_leave_balance.py | 66 +++++--------------
1 file changed, 16 insertions(+), 50 deletions(-)
diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
index 5172fb8fc2..3f0337e508 100644
--- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
@@ -138,42 +138,12 @@ def get_opening_balance(employee, leave_type, filters, carry_forwarded_leaves):
# then opening balance should only consider carry forwarded leaves
opening_balance = carry_forwarded_leaves
else:
- # else directly get closing leave balance on the previous day
- opening_balance = get_closing_balance_on(opening_balance_date, employee, leave_type, filters)
+ # else directly get leave balance on the previous day
+ opening_balance = get_leave_balance_on(employee, leave_type, opening_balance_date)
return opening_balance
-def get_closing_balance_on(date, employee, leave_type, filters):
- closing_balance = get_leave_balance_on(employee, leave_type, date)
- leave_allocation = get_leave_allocation_for_date(employee, leave_type, date)
- if leave_allocation:
- # if balance is greater than the days remaining for leave allocation's end date
- # then balance should be = remaining days
- remaining_days = date_diff(leave_allocation[0].to_date, filters.from_date) + 1
- if remaining_days < closing_balance:
- closing_balance = remaining_days
-
- return closing_balance
-
-
-def get_leave_allocation_for_date(employee, leave_type, date):
- allocation = frappe.qb.DocType('Leave Allocation')
- records = (
- frappe.qb.from_(allocation)
- .select(
- allocation.name, allocation.to_date
- ).where(
- (allocation.docstatus == 1)
- & (allocation.employee == employee)
- & (allocation.leave_type == leave_type)
- & ((allocation.from_date <= date) & (allocation.to_date >= date))
- )
- ).run(as_dict=True)
-
- return records
-
-
def get_conditions(filters):
conditions={
'status': 'Active',
@@ -191,28 +161,24 @@ def get_conditions(filters):
def get_department_leave_approver_map(department=None):
-
# get current department and all its child
department_list = frappe.get_list('Department',
- filters={
- 'disabled': 0
- },
- or_filters={
- 'name': department,
- 'parent_department': department
- },
- fields=['name'],
- pluck='name'
- )
+ filters={'disabled': 0},
+ or_filters={
+ 'name': department,
+ 'parent_department': department
+ },
+ pluck='name'
+ )
# retrieve approvers list from current department and from its subsequent child departments
approver_list = frappe.get_all('Department Approver',
- filters={
- 'parentfield': 'leave_approvers',
- 'parent': ('in', department_list)
- },
- fields=['parent', 'approver'],
- as_list=1
- )
+ filters={
+ 'parentfield': 'leave_approvers',
+ 'parent': ('in', department_list)
+ },
+ fields=['parent', 'approver'],
+ as_list=True
+ )
approvers = {}
From 26bd3053d190df07e8b75e0e86203050047b25cf Mon Sep 17 00:00:00 2001
From: marination
Date: Fri, 4 Feb 2022 17:34:56 +0530
Subject: [PATCH 022/447] perf: Weed out disabled variants via sql query
instead of pythonic looping separately
- If the number of variants are large (almost 2lakhs), the query to get variants and attribute data takes time
- If the no.of disabled attributes is large as well, the list comprehension weeding out disabled variants takes forever
- We dont need to loop over the variants data so many times
- Avoid any `if a in list(b)` is best when the iterables have tremendous data
---
.../variant_selector/item_variants_cache.py | 36 +++++++++++++------
1 file changed, 25 insertions(+), 11 deletions(-)
diff --git a/erpnext/e_commerce/variant_selector/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py
index bb6b3ef37f..9b22255d9a 100644
--- a/erpnext/e_commerce/variant_selector/item_variants_cache.py
+++ b/erpnext/e_commerce/variant_selector/item_variants_cache.py
@@ -66,25 +66,39 @@ class ItemVariantsCacheManager:
)
]
- # join with Website Item
- item_variants_data = frappe.get_all(
- 'Item Variant Attribute',
- {'variant_of': parent_item_code},
- ['parent', 'attribute', 'attribute_value'],
- order_by='name',
- as_list=1
+ # Get Variants and tehir Attributes that are not disabled
+ iva = frappe.qb.DocType("Item Variant Attribute")
+ item = frappe.qb.DocType("Item")
+ query = (
+ frappe.qb.from_(iva)
+ .join(item).on(item.name == iva.parent)
+ .select(
+ iva.parent, iva.attribute, iva.attribute_value
+ ).where(
+ (iva.variant_of == parent_item_code)
+ & (item.disabled == 0)
+ ).orderby(iva.name)
)
+ item_variants_data = query.run()
- disabled_items = set(
- [i.name for i in frappe.db.get_all('Item', {'disabled': 1})]
- )
+ # item_variants_data = frappe.get_all(
+ # 'Item Variant Attribute',
+ # {'variant_of': parent_item_code},
+ # ['parent', 'attribute', 'attribute_value'],
+ # order_by='name',
+ # as_list=1
+ # )
+
+ # disabled_items = set(
+ # [i.name for i in frappe.db.get_all('Item', {'disabled': 1})]
+ # )
attribute_value_item_map = frappe._dict()
item_attribute_value_map = frappe._dict()
# dont consider variants that are disabled
# pull all other variants
- item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items]
+ # item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items]
for row in item_variants_data:
item_code, attribute, attribute_value = row
From 363ed9ccba3f848908113e6d728735a1c894aec8 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Sat, 5 Feb 2022 14:06:18 +0100
Subject: [PATCH 023/447] =?UTF-8?q?revert:=20BU=20Schl=C3=BCssel=20(a21f76?=
=?UTF-8?q?f)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
erpnext/regional/report/datev/datev.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py
index beac7ed65c..92a10c288f 100644
--- a/erpnext/regional/report/datev/datev.py
+++ b/erpnext/regional/report/datev/datev.py
@@ -343,8 +343,7 @@ def run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=1):
/* against number or, if empty, party against number */
%(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)',
- /* disable automatic VAT deduction */
- '40' as 'BU-Schlüssel',
+ '' as 'BU-Schlüssel',
gl.posting_date as 'Belegdatum',
gl.voucher_no as 'Belegfeld 1',
From 4284017e9de3b033156ca6947665bc99f0daefc3 Mon Sep 17 00:00:00 2001
From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com>
Date: Mon, 7 Feb 2022 12:26:01 +0530
Subject: [PATCH 024/447] fix: Copyright info
---
.../payment_terms_status_for_sales_order.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js
index 0450631a3b..0e36b3fe3d 100644
--- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
From a64228741d065f7ac33b3208d3a704616250f925 Mon Sep 17 00:00:00 2001
From: marination
Date: Tue, 8 Feb 2022 11:15:19 +0530
Subject: [PATCH 025/447] fix: Trim spaces from attributes (multi-variant
creation) & explicit method for building cache
- Multiple Item Variants creation fails due to extra spaces in attributes from popup. Clean them before passing to server side
- Mention explicit method to build variants cache to avoid ambiguity between old method path (pre-refactor)
---
erpnext/e_commerce/variant_selector/item_variants_cache.py | 5 ++++-
erpnext/stock/doctype/item/item.js | 2 +-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/erpnext/e_commerce/variant_selector/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py
index 9b22255d9a..3aefc446c2 100644
--- a/erpnext/e_commerce/variant_selector/item_variants_cache.py
+++ b/erpnext/e_commerce/variant_selector/item_variants_cache.py
@@ -138,4 +138,7 @@ def build_cache(item_code):
def enqueue_build_cache(item_code):
if frappe.cache().hget('item_cache_build_in_progress', item_code):
return
- frappe.enqueue(build_cache, item_code=item_code, queue='long')
+ frappe.enqueue(
+ "erpnext.e_commerce.variant_selector.item_variants_cache.build_cache",
+ item_code=item_code, queue='long'
+ )
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 2a30ca11fb..dfc09181ca 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -545,7 +545,7 @@ $.extend(erpnext.item, {
let selected_attributes = {};
me.multiple_variant_dialog.$wrapper.find('.form-column').each((i, col) => {
if(i===0) return;
- let attribute_name = $(col).find('label').html();
+ let attribute_name = $(col).find('label').html().trim();
selected_attributes[attribute_name] = [];
let checked_opts = $(col).find('.checkbox input');
checked_opts.each((i, opt) => {
From 4f5a0b8941101f759f2d1af33d952a1bfdfd3cf4 Mon Sep 17 00:00:00 2001
From: marination
Date: Tue, 8 Feb 2022 12:02:02 +0530
Subject: [PATCH 026/447] chore: Fix flaky test `test_exact_match_with_price`
- Clear cart settings in cache to avoid stale values
---
erpnext/e_commerce/variant_selector/test_variant_selector.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py
index b83961e6e1..4d907c6221 100644
--- a/erpnext/e_commerce/variant_selector/test_variant_selector.py
+++ b/erpnext/e_commerce/variant_selector/test_variant_selector.py
@@ -104,6 +104,8 @@ class TestVariantSelector(ERPNextTestCase):
})
make_web_item_price(item_code="Test-Tshirt-Temp-S-R", price_list_rate=100)
+
+ frappe.local.shopping_cart_settings = None # clear cached settings values
next_values = get_next_attribute_and_values(
"Test-Tshirt-Temp",
selected_attributes={"Test Size": "Small", "Test Colour": "Red"}
From d636c3fb04ae5b1b95c67052bfb5f894e4cbf4f4 Mon Sep 17 00:00:00 2001
From: Devin Slauenwhite
Date: Wed, 9 Feb 2022 10:52:38 -0500
Subject: [PATCH 027/447] test: many users linked to customer shopping cart
---
.../shopping_cart/test_shopping_cart.py | 25 +++++++++++--------
erpnext/tests/utils.py | 14 +++++++++++
2 files changed, 29 insertions(+), 10 deletions(-)
diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
index 8519e68d09..05c874af98 100644
--- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
+++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
@@ -57,13 +57,19 @@ class TestShoppingCart(unittest.TestCase):
return quotation
def test_get_cart_customer(self):
- self.login_as_customer()
+ def validate_quotation():
+ # test if quotation with customer is fetched
+ quotation = _get_cart_quotation()
+ self.assertEqual(quotation.quotation_to, "Customer")
+ self.assertEqual(quotation.party_name, "_Test Customer")
+ self.assertEqual(quotation.contact_email, frappe.session.user)
+ return quotation
- # test if quotation with customer is fetched
- quotation = _get_cart_quotation()
- self.assertEqual(quotation.quotation_to, "Customer")
- self.assertEqual(quotation.party_name, "_Test Customer")
- self.assertEqual(quotation.contact_email, frappe.session.user)
+ self.login_as_customer("test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer")
+ validate_quotation()
+
+ self.login_as_customer()
+ quotation = validate_quotation()
return quotation
@@ -254,10 +260,9 @@ class TestShoppingCart(unittest.TestCase):
self.create_user_if_not_exists("test_cart_user@example.com")
frappe.set_user("test_cart_user@example.com")
- def login_as_customer(self):
- self.create_user_if_not_exists("test_contact_customer@example.com",
- "_Test Contact For _Test Customer")
- frappe.set_user("test_contact_customer@example.com")
+ def login_as_customer(self, email="test_contact_customer@example.com", name="_Test Contact For _Test Customer"):
+ self.create_user_if_not_exists(email, name)
+ frappe.set_user(email)
def clear_existing_quotations(self):
quotations = frappe.get_all("Quotation", filters={
diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py
index 2bd7e9e71d..40c95eb7a3 100644
--- a/erpnext/tests/utils.py
+++ b/erpnext/tests/utils.py
@@ -66,6 +66,20 @@ def create_test_contact_and_address():
contact.add_phone("+91 0000000000", is_primary_phone=True)
contact.insert()
+ contact_two = frappe.get_doc({
+ "doctype": 'Contact',
+ "first_name": "_Test Contact 2 for _Test Customer",
+ "links": [
+ {
+ "link_doctype": "Customer",
+ "link_name": "_Test Customer"
+ }
+ ]
+ })
+ contact_two.add_email("test_contact_two_customer@example.com", is_primary=True)
+ contact_two.add_phone("+92 0000000000", is_primary_phone=True)
+ contact_two.insert()
+
@contextmanager
def change_settings(doctype, settings_dict):
From 34ad5b1abf534f55876fcfac2852df2ea42ecb41 Mon Sep 17 00:00:00 2001
From: Devin Slauenwhite
Date: Wed, 9 Feb 2022 10:53:24 -0500
Subject: [PATCH 028/447] fix: get cart for logged in user.
---
erpnext/e_commerce/shopping_cart/cart.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py
index 458cf69af7..372aed0b95 100644
--- a/erpnext/e_commerce/shopping_cart/cart.py
+++ b/erpnext/e_commerce/shopping_cart/cart.py
@@ -310,7 +310,7 @@ def _get_cart_quotation(party=None):
party = get_party()
quotation = frappe.get_all("Quotation", fields=["name"], filters=
- {"party_name": party.name, "order_type": "Shopping Cart", "docstatus": 0},
+ {"party_name": party.name, "contact_email": frappe.session.user, "order_type": "Shopping Cart", "docstatus": 0},
order_by="modified desc", limit_page_length=1)
if quotation:
From 49bee568a1d5bcfc315f539af734bc55a84a35ef Mon Sep 17 00:00:00 2001
From: Devin Slauenwhite
Date: Wed, 9 Feb 2022 12:03:05 -0500
Subject: [PATCH 029/447] fix: get cart items for logged in user.
---
erpnext/e_commerce/product_data_engine/query.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/e_commerce/product_data_engine/query.py b/erpnext/e_commerce/product_data_engine/query.py
index 007bf8b348..cfc3c7b357 100644
--- a/erpnext/e_commerce/product_data_engine/query.py
+++ b/erpnext/e_commerce/product_data_engine/query.py
@@ -264,7 +264,7 @@ class ProductQuery:
customer = get_customer(silent=True)
if customer:
quotation = frappe.get_all("Quotation", fields=["name"], filters=
- {"party_name": customer, "order_type": "Shopping Cart", "docstatus": 0},
+ {"party_name": customer, "contact_email": frappe.session.user, "order_type": "Shopping Cart", "docstatus": 0},
order_by="modified desc", limit_page_length=1)
if quotation:
items = frappe.get_all(
@@ -298,4 +298,4 @@ class ProductQuery:
# slice results manually
result[:self.page_length]
- return result
\ No newline at end of file
+ return result
From da73685f7172290151a279f8cf796628dbf6617e Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 10 Feb 2022 13:07:51 +0530
Subject: [PATCH 030/447] fix: Multiple fixes in Gross Profit report
---
.../report/gross_profit/gross_profit.js | 10 +++--
.../report/gross_profit/gross_profit.py | 43 +++++++++++++------
2 files changed, 36 insertions(+), 17 deletions(-)
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js
index 685f2d6176..c8a9a228c6 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.js
+++ b/erpnext/accounts/report/gross_profit/gross_profit.js
@@ -8,20 +8,22 @@ frappe.query_reports["Gross Profit"] = {
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
- "reqd": 1,
- "default": frappe.defaults.get_user_default("Company")
+ "default": frappe.defaults.get_user_default("Company"),
+ "reqd": 1
},
{
"fieldname":"from_date",
"label": __("From Date"),
"fieldtype": "Date",
- "default": frappe.defaults.get_user_default("year_start_date")
+ "default": frappe.defaults.get_user_default("year_start_date"),
+ "reqd": 1
},
{
"fieldname":"to_date",
"label": __("To Date"),
"fieldtype": "Date",
- "default": frappe.defaults.get_user_default("year_end_date")
+ "default": frappe.defaults.get_user_default("year_end_date"),
+ "reqd": 1
},
{
"fieldname":"sales_invoice",
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index 84effc0f46..225b7c6426 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -369,20 +369,37 @@ class GrossProfitGenerator(object):
return self.average_buying_rate[item_code]
def get_last_purchase_rate(self, item_code, row):
- condition = ''
- if row.project:
- condition += " AND a.project=%s" % (frappe.db.escape(row.project))
- elif row.cost_center:
- condition += " AND a.cost_center=%s" % (frappe.db.escape(row.cost_center))
- if self.filters.to_date:
- condition += " AND modified='%s'" % (self.filters.to_date)
+ purchase_invoice = frappe.qb.DocType("Purchase Invoice")
+ purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
- last_purchase_rate = frappe.db.sql("""
- select (a.base_rate / a.conversion_factor)
- from `tabPurchase Invoice Item` a
- where a.item_code = %s and a.docstatus=1
- {0}
- order by a.modified desc limit 1""".format(condition), item_code)
+ query = (frappe.qb.from_(purchase_invoice_item)
+ .inner_join(
+ purchase_invoice
+ ).on(
+ purchase_invoice.name == purchase_invoice_item.parent
+ ).select(
+ purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor
+ ).where(
+ purchase_invoice.docstatus == 1
+ ).where(
+ purchase_invoice.posting_date <= self.filters.to_date
+ ).where(
+ purchase_invoice_item.item_code == item_code
+ ))
+
+ if row.project:
+ query.where(
+ purchase_invoice_item.item_code == row.project
+ )
+
+ if row.cost_center:
+ query.where(
+ purchase_invoice_item.cost_center == row.cost_center
+ )
+
+ query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc)
+ query.limit(1)
+ last_purchase_rate = query.run()
return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0
From 2172ab2d37d8be0c43d1f885a40657d352d255b4 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Fri, 11 Feb 2022 14:48:39 +0530
Subject: [PATCH 031/447] fix: Update columns in new format
---
.../report/gross_profit/gross_profit.json | 4 +-
.../report/gross_profit/gross_profit.py | 80 ++++++-------------
2 files changed, 28 insertions(+), 56 deletions(-)
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.json b/erpnext/accounts/report/gross_profit/gross_profit.json
index 76c560ad24..0730ffd77e 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.json
+++ b/erpnext/accounts/report/gross_profit/gross_profit.json
@@ -1,5 +1,5 @@
{
- "add_total_row": 0,
+ "add_total_row": 1,
"columns": [],
"creation": "2013-02-25 17:03:34",
"disable_prepared_report": 0,
@@ -9,7 +9,7 @@
"filters": [],
"idx": 3,
"is_standard": "Yes",
- "modified": "2021-11-13 19:14:23.730198",
+ "modified": "2022-02-11 10:18:36.956558",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Gross Profit",
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index 225b7c6426..c403b76f87 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -70,43 +70,42 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
data.append(row)
def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data):
- for idx, src in enumerate(gross_profit_data.grouped_data):
+ for src in gross_profit_data.grouped_data:
row = []
for col in group_wise_columns.get(scrub(filters.group_by)):
row.append(src.get(col))
row.append(filters.currency)
- if idx == len(gross_profit_data.grouped_data)-1:
- row[0] = "Total"
data.append(row)
def get_columns(group_wise_columns, filters):
columns = []
column_map = frappe._dict({
- "parent": _("Sales Invoice") + ":Link/Sales Invoice:120",
- "invoice_or_item": _("Sales Invoice") + ":Link/Sales Invoice:120",
- "posting_date": _("Posting Date") + ":Date:100",
- "posting_time": _("Posting Time") + ":Data:100",
- "item_code": _("Item Code") + ":Link/Item:100",
- "item_name": _("Item Name") + ":Data:100",
- "item_group": _("Item Group") + ":Link/Item Group:100",
- "brand": _("Brand") + ":Link/Brand:100",
- "description": _("Description") +":Data:100",
- "warehouse": _("Warehouse") + ":Link/Warehouse:100",
- "qty": _("Qty") + ":Float:80",
- "base_rate": _("Avg. Selling Rate") + ":Currency/currency:100",
- "buying_rate": _("Valuation Rate") + ":Currency/currency:100",
- "base_amount": _("Selling Amount") + ":Currency/currency:100",
- "buying_amount": _("Buying Amount") + ":Currency/currency:100",
- "gross_profit": _("Gross Profit") + ":Currency/currency:100",
- "gross_profit_percent": _("Gross Profit %") + ":Percent:100",
- "project": _("Project") + ":Link/Project:100",
- "sales_person": _("Sales person"),
- "allocated_amount": _("Allocated Amount") + ":Currency/currency:100",
- "customer": _("Customer") + ":Link/Customer:100",
- "customer_group": _("Customer Group") + ":Link/Customer Group:100",
- "territory": _("Territory") + ":Link/Territory:100"
+ "parent": {"label": _('Sales Invoice'), "fieldname": "parent_invoice", "fieldtype": "Link", "options": "Sales Invoice", "width": 120},
+ "invoice_or_item": {"label": _('Sales Invoice'), "fieldtype": "Link", "options": "Sales Invoice", "width": 120},
+ "posting_date": {"label": _('Posting Date'), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
+ "posting_time": {"label": _('Posting Time'), "fieldname": "posting_time", "fieldtype": "Data", "width": 100},
+ "item_code": {"label": _('Item Code'), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 100},
+ "item_name": {"label": _('Item Name'), "fieldname": "item_name", "fieldtype": "Data", "width": 100},
+ "item_group": {"label": _('Item Group'), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100},
+ "brand": {"label": _('Brand'), "fieldtype": "Link", "options": "Brand", "width": 100},
+ "description": {"label": _('Description'), "fieldname": "description", "fieldtype": "Data", "width": 100},
+ "warehouse": {"label": _('Warehouse'), "fieldname": "warehouse", "fieldtype": "Link", "options": "warehouse", "width": 100},
+ "qty": {"label": _('Qty'), "fieldname": "qty", "fieldtype": "Float", "width": 80},
+ "base_rate": {"label": _('Avg. Selling Rate'), "fieldname": "avg._selling_rate", "fieldtype": "Currency", "options": "currency", "width": 100},
+ "buying_rate": {"label": _('Valuation Rate'), "fieldname": "valuation_rate", "fieldtype": "Currency", "options": "currency", "width": 100},
+ "base_amount": {"label": _('Selling Amount'), "fieldname": "selling_amount", "fieldtype": "Currency", "options": "currency", "width": 100},
+ "buying_amount": {"label": _('Buying Amount'), "fieldname": "buying_amount", "fieldtype": "Currency", "options": "currency", "width": 100},
+ "gross_profit": {"label": _('Gross Profit'), "fieldname": "gross_profit", "fieldtype": "Currency", "options": "currency", "width": 100},
+ "gross_profit_percent": {"label": _('Gross Profit Percent'), "fieldname": "gross_profit_%",
+ "fieldtype": "Percent", "width": 100},
+ "project": {"label": _('Project'), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100},
+ "sales_person": {"label": _('Sales Person'), "fieldname": "sales_person", "fieldtype": "Data","width": 100},
+ "allocated_amount": {"label": _('Allocated Amount'), "fieldname": "allocated_amount", "fieldtype": "Currency", "options": "currency", "width": 100},
+ "customer": {"label": _('Customer'), "fieldname": "customer", "fieldtype": "Link", "options": "Customer", "width": 100},
+ "customer_group": {"label": _('Customer Group'), "fieldname": "customer_group", "fieldtype": "Link", "options": "customer", "width": 100},
+ "territory": {"label": _('Territory'), "fieldname": "territory", "fieldtype": "Link", "options": "territory", "width": 100},
})
for col in group_wise_columns.get(scrub(filters.group_by)):
@@ -223,16 +222,6 @@ class GrossProfitGenerator(object):
self.get_average_rate_based_on_group_by()
def get_average_rate_based_on_group_by(self):
- # sum buying / selling totals for group
- self.totals = frappe._dict(
- qty=0,
- base_amount=0,
- buying_amount=0,
- gross_profit=0,
- gross_profit_percent=0,
- base_rate=0,
- buying_rate=0
- )
for key in list(self.grouped):
if self.filters.get("group_by") != "Invoice":
for i, row in enumerate(self.grouped[key]):
@@ -244,7 +233,6 @@ class GrossProfitGenerator(object):
new_row.base_amount += flt(row.base_amount, self.currency_precision)
new_row = self.set_average_rate(new_row)
self.grouped_data.append(new_row)
- self.add_to_totals(new_row)
else:
for i, row in enumerate(self.grouped[key]):
if row.indent == 1.0:
@@ -258,17 +246,6 @@ class GrossProfitGenerator(object):
if (flt(row.qty) or row.base_amount):
row = self.set_average_rate(row)
self.grouped_data.append(row)
- self.add_to_totals(row)
-
- self.set_average_gross_profit(self.totals)
-
- if self.filters.get("group_by") == "Invoice":
- self.totals.indent = 0.0
- self.totals.parent_invoice = ""
- self.totals.invoice_or_item = "Total"
- self.si_list.append(self.totals)
- else:
- self.grouped_data.append(self.totals)
def is_not_invoice_row(self, row):
return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get("group_by") != "Invoice"
@@ -284,11 +261,6 @@ class GrossProfitGenerator(object):
new_row.gross_profit_percent = flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) \
if new_row.base_amount else 0
- def add_to_totals(self, new_row):
- for key in self.totals:
- if new_row.get(key):
- self.totals[key] += new_row[key]
-
def get_returned_invoice_items(self):
returned_invoices = frappe.db.sql("""
select
@@ -389,7 +361,7 @@ class GrossProfitGenerator(object):
if row.project:
query.where(
- purchase_invoice_item.item_code == row.project
+ purchase_invoice_item.project == row.project
)
if row.cost_center:
From 07bcbc6c7e10f977bc5a6ff8f5b48d91ec9b2b70 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Sat, 12 Feb 2022 19:05:03 +0530
Subject: [PATCH 032/447] fix: Remove unused param
---
erpnext/accounts/report/gross_profit/gross_profit.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index c403b76f87..ebb929aaac 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -172,7 +172,7 @@ class GrossProfitGenerator(object):
buying_amount = 0
for row in reversed(self.si_list):
- if self.skip_row(row, self.product_bundles):
+ if self.skip_row(row):
continue
row.base_amount = flt(row.base_net_amount, self.currency_precision)
@@ -278,7 +278,7 @@ class GrossProfitGenerator(object):
self.returned_invoices.setdefault(inv.return_against, frappe._dict())\
.setdefault(inv.item_code, []).append(inv)
- def skip_row(self, row, product_bundles):
+ def skip_row(self, row):
if self.filters.get("group_by") != "Invoice":
if not row.get(scrub(self.filters.get("group_by", ""))):
return True
From ae613008be59334e5ff72882ef9d70355f56805e Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Sat, 12 Feb 2022 21:54:22 +0530
Subject: [PATCH 033/447] fix: Error in consolidated financial statements
---
.../consolidated_financial_statement.py | 25 +++++++++++++------
1 file changed, 17 insertions(+), 8 deletions(-)
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index 758e3e9337..62bf156219 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -367,7 +367,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0)
def get_account_heads(root_type, companies, filters):
- accounts = get_accounts(root_type, filters)
+ accounts = get_accounts(root_type, companies)
if not accounts:
return None, None, None
@@ -396,7 +396,7 @@ def update_parent_account_names(accounts):
for account in accounts:
if account.parent_account:
- account["parent_account_name"] = name_to_account_map[account.parent_account]
+ account["parent_account_name"] = name_to_account_map.get(account.parent_account)
return accounts
@@ -419,12 +419,21 @@ def get_subsidiary_companies(company):
return frappe.db.sql_list("""select name from `tabCompany`
where lft >= {0} and rgt <= {1} order by lft, rgt""".format(lft, rgt))
-def get_accounts(root_type, filters):
- return frappe.db.sql(""" select name, is_group, company,
- parent_account, lft, rgt, root_type, report_type, account_name, account_number
- from
- `tabAccount` where company = %s and root_type = %s
- """ , (filters.get('company'), root_type), as_dict=1)
+def get_accounts(root_type, companies):
+ accounts = []
+ added_accounts = []
+
+ for company in companies:
+ for account in frappe.db.sql(""" select name, is_group, company,
+ parent_account, lft, rgt, root_type, report_type, account_name, account_number
+ from
+ `tabAccount` where company = %s and root_type = %s
+ """ , (company, root_type), as_dict=1):
+ if account.account_name not in added_accounts:
+ accounts.append(account)
+ added_accounts.append(account.account_name)
+
+ return accounts
def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters):
data = []
From dbd29da189145cb059ee88707e62c7d1888ed91a Mon Sep 17 00:00:00 2001
From: Wolfram Schmidt
Date: Sun, 13 Feb 2022 13:11:31 +0100
Subject: [PATCH 034/447] Translation for DocType
https://testsystem.frappe.cloud/app/milestone
---
erpnext/translations/de.csv | 1 +
1 file changed, 1 insertion(+)
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index cf73564b9e..f345a87d03 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -1597,6 +1597,7 @@ Method,Methode,
Middle Income,Mittleres Einkommen,
Middle Name,Zweiter Vorname,
Middle Name (Optional),Weiterer Vorname (optional),
+Milestonde,Meilenstein,
Min Amt can not be greater than Max Amt,Min. Amt kann nicht größer als Max. Amt sein,
Min Qty can not be greater than Max Qty,Mindestmenge kann nicht größer als Maximalmenge sein,
Minimum Lead Age (Days),Mindest Lead-Alter (in Tagen),
From 615dd9decd1947eb8203d0b2145138044c2522a5 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Sun, 13 Feb 2022 19:24:10 +0530
Subject: [PATCH 035/447] fix: Patch fixes
---
.../v14_0/update_opportunity_currency_fields.py | 12 ++++--------
erpnext/regional/saudi_arabia/setup.py | 2 +-
2 files changed, 5 insertions(+), 9 deletions(-)
diff --git a/erpnext/patches/v14_0/update_opportunity_currency_fields.py b/erpnext/patches/v14_0/update_opportunity_currency_fields.py
index 13071478c8..82213fff6c 100644
--- a/erpnext/patches/v14_0/update_opportunity_currency_fields.py
+++ b/erpnext/patches/v14_0/update_opportunity_currency_fields.py
@@ -6,8 +6,8 @@ from erpnext.setup.utils import get_exchange_rate
def execute():
- frappe.reload_doc('crm', 'doctype', 'opportunity')
- frappe.reload_doc('crm', 'doctype', 'opportunity_item')
+ frappe.reload_doc('crm', 'doctype', 'opportunity', force=True)
+ frappe.reload_doc('crm', 'doctype', 'opportunity_item', force=True)
opportunities = frappe.db.get_list('Opportunity', filters={
'opportunity_amount': ['>', 0]
@@ -20,15 +20,11 @@ def execute():
if opportunity.currency != company_currency:
conversion_rate = get_exchange_rate(opportunity.currency, company_currency)
base_opportunity_amount = flt(conversion_rate) * flt(opportunity.opportunity_amount)
- grand_total = flt(opportunity.opportunity_amount)
- base_grand_total = flt(conversion_rate) * flt(opportunity.opportunity_amount)
else:
conversion_rate = 1
- base_opportunity_amount = grand_total = base_grand_total = flt(opportunity.opportunity_amount)
+ base_opportunity_amount = flt(opportunity.opportunity_amount)
frappe.db.set_value('Opportunity', opportunity.name, {
'conversion_rate': conversion_rate,
- 'base_opportunity_amount': base_opportunity_amount,
- 'grand_total': grand_total,
- 'base_grand_total': base_grand_total
+ 'base_opportunity_amount': base_opportunity_amount
}, update_modified=False)
diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py
index 15d524d5b8..d2ef6f3f17 100644
--- a/erpnext/regional/saudi_arabia/setup.py
+++ b/erpnext/regional/saudi_arabia/setup.py
@@ -102,7 +102,7 @@ def make_custom_fields():
]
}
- create_custom_fields(custom_fields, update=True)
+ create_custom_fields(custom_fields, ignore_validate=True, update=True)
def update_regional_tax_settings(country, company):
create_ksa_vat_setting(company)
From bc244d074062d23be99922a370564bba13e15890 Mon Sep 17 00:00:00 2001
From: ruthra kumar
Date: Tue, 8 Feb 2022 18:53:08 +0530
Subject: [PATCH 036/447] refactor: currency field and code cleanup
---
.../payment_terms_status_for_sales_order.py | 25 +++++++++----------
1 file changed, 12 insertions(+), 13 deletions(-)
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
index aa2f757218..4eafa9b2ef 100644
--- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
@@ -13,62 +13,60 @@ def get_columns():
"fieldname": "name",
"fieldtype": "Link",
"options": "Sales Order",
- "read_only": 1,
},
{
- "label": _("Submitted"),
+ "label": _("Posting Date"),
"fieldname": "submitted",
"fieldtype": "Date",
- "read_only": 1
},
{
"label": _("Payment Term"),
"fieldname": "payment_term",
"fieldtype": "Data",
- "read_only": 1
},
{
"label": _("Description"),
"fieldname": "description",
"fieldtype": "Data",
- "read_only": 1
},
{
"label": _("Due Date"),
"fieldname": "due_date",
"fieldtype": "Date",
- "read_only": 1
},
{
"label": _("Invoice Portion"),
"fieldname": "invoice_portion",
"fieldtype": "Percent",
- "read_only": 1,
},
{
"label": _("Payment Amount"),
"fieldname": "payment_amount",
"fieldtype": "Currency",
- "read_only": 1,
+ "options": "currency",
},
{
"label": _("Paid Amount"),
"fieldname": "paid_amount",
"fieldtype": "Currency",
- "read_only": 1
+ "options": "currency",
},
{
"label": _("Invoices"),
"fieldname": "invoices",
"fieldtype": "Link",
"options": "Sales Invoice",
- "read_only": 1,
},
{
"label": _("Status"),
"fieldname": "status",
"fieldtype": "Data",
- "read_only": 1
+ },
+ {
+ "label": _("Currency"),
+ "fieldname": "currency",
+ "fieldtype": "Currency",
+ "hidden": 1
}
]
return columns
@@ -152,12 +150,13 @@ def get_so_with_invoices(filters):
return sorders, invoices
-def set_payment_terms_statuses(sales_orders, invoices):
+def set_payment_terms_statuses(sales_orders, invoices, filters):
"""
compute status for payment terms with associated sales invoice using FIFO
"""
for so in sales_orders:
+ so.currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency')
for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]:
if so.payment_amount - so.paid_amount > 0:
amount = so.payment_amount - so.paid_amount
@@ -200,7 +199,7 @@ def prepare_chart(s_orders):
def execute(filters=None):
columns = get_columns()
sales_orders, so_invoices = get_so_with_invoices(filters)
- sales_orders, so_invoices = set_payment_terms_statuses(sales_orders, so_invoices)
+ sales_orders, so_invoices = set_payment_terms_statuses(sales_orders, so_invoices, filters)
prepare_chart(sales_orders)
From 973f6b1bbd53594e5b2a51a1dcdf7d9e38dd46a8 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Mon, 14 Feb 2022 22:14:17 +0530
Subject: [PATCH 037/447] fix: Gross profit for credit notes
---
erpnext/accounts/report/gross_profit/gross_profit.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index ebb929aaac..b03bb9bb13 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -282,8 +282,8 @@ class GrossProfitGenerator(object):
if self.filters.get("group_by") != "Invoice":
if not row.get(scrub(self.filters.get("group_by", ""))):
return True
- elif row.get("is_return") == 1:
- return True
+
+ return False
def get_buying_amount_from_product_bundle(self, row, product_bundle):
buying_amount = 0.0
From 42cdd6d2379d68efb592a5c8a8148979dce8cf1e Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Tue, 15 Feb 2022 12:05:51 +0530
Subject: [PATCH 038/447] fix: Remove commented out code
---
.../consolidated_financial_statement.py | 3 ---
1 file changed, 3 deletions(-)
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index 62bf156219..dad7384fea 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -354,9 +354,6 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
if d.parent_account:
account = d.parent_account_name
- # if not accounts_by_name.get(account):
- # continue
-
for company in companies:
accounts_by_name[account][company] = \
accounts_by_name[account].get(company, 0.0) + d.get(company, 0.0)
From fec40aac7a25c383e384f29471f9ea82382524b2 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Tue, 15 Feb 2022 12:15:35 +0530
Subject: [PATCH 039/447] fix: Linting issues
---
.../consolidated_financial_statement.py | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index dad7384fea..1e20f7be3e 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -421,11 +421,9 @@ def get_accounts(root_type, companies):
added_accounts = []
for company in companies:
- for account in frappe.db.sql(""" select name, is_group, company,
- parent_account, lft, rgt, root_type, report_type, account_name, account_number
- from
- `tabAccount` where company = %s and root_type = %s
- """ , (company, root_type), as_dict=1):
+ for account in frappe.get_all("Account", fields=["name", "is_group", "company",
+ "parent_account", "lft", "rgt", "root_type", "report_type", "account_name", "account_number"],
+ filters={"company": company, "root_type": root_type}):
if account.account_name not in added_accounts:
accounts.append(account)
added_accounts.append(account.account_name)
From 85ed0fb8d6ef45197bfef4a71cb8f02355d61930 Mon Sep 17 00:00:00 2001
From: ruthra kumar
Date: Tue, 15 Feb 2022 12:20:06 +0530
Subject: [PATCH 040/447] fix: default to company currency in report output
---
.../payment_terms_status_for_sales_order.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
index 4eafa9b2ef..d0902e111a 100644
--- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
@@ -41,7 +41,7 @@ def get_columns():
},
{
"label": _("Payment Amount"),
- "fieldname": "payment_amount",
+ "fieldname": "base_payment_amount",
"fieldtype": "Currency",
"options": "currency",
},
@@ -113,7 +113,7 @@ def get_so_with_invoices(filters):
ps.description,
ps.due_date,
ps.invoice_portion,
- ps.payment_amount,
+ ps.base_payment_amount,
ps.paid_amount,
)
.where(
@@ -141,7 +141,7 @@ def get_so_with_invoices(filters):
.on(si.name == sii.parent)
.inner_join(soi)
.on(soi.name == sii.so_detail)
- .select(sii.sales_order, sii.parent.as_("invoice"), si.base_net_total.as_("invoice_amount"))
+ .select(sii.sales_order, sii.parent.as_("invoice"), si.base_grand_total.as_("invoice_amount"))
.where((sii.sales_order.isin([x.name for x in sorders])) & (si.docstatus == 1))
.groupby(sii.parent)
)
@@ -158,8 +158,8 @@ def set_payment_terms_statuses(sales_orders, invoices, filters):
for so in sales_orders:
so.currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency')
for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]:
- if so.payment_amount - so.paid_amount > 0:
- amount = so.payment_amount - so.paid_amount
+ if so.base_payment_amount - so.paid_amount > 0:
+ amount = so.base_payment_amount - so.paid_amount
if inv.invoice_amount >= amount:
inv.invoice_amount -= amount
so.paid_amount += amount
@@ -187,7 +187,7 @@ def prepare_chart(s_orders):
"data": {
"labels": [term.payment_term for term in s_orders],
"datasets": [
- {"name": "Payment Amount", "values": [x.payment_amount for x in s_orders],},
+ {"name": "Payment Amount", "values": [x.base_payment_amount for x in s_orders],},
{"name": "Paid Amount", "values": [x.paid_amount for x in s_orders],},
],
},
From a4b8d673232fd313396788ef745e67572c235dcc Mon Sep 17 00:00:00 2001
From: ruthra kumar
Date: Tue, 15 Feb 2022 12:20:41 +0530
Subject: [PATCH 041/447] refactor: create invoices list without if else
---
.../payment_terms_status_for_sales_order.py | 11 +++--------
1 file changed, 3 insertions(+), 8 deletions(-)
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
index d0902e111a..e6a56eea31 100644
--- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
@@ -157,25 +157,20 @@ def set_payment_terms_statuses(sales_orders, invoices, filters):
for so in sales_orders:
so.currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency')
+ so.invoices = ""
for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]:
if so.base_payment_amount - so.paid_amount > 0:
amount = so.base_payment_amount - so.paid_amount
if inv.invoice_amount >= amount:
inv.invoice_amount -= amount
so.paid_amount += amount
- if so.invoices:
- so.invoices = so.invoices + "," + inv.invoice
- else:
- so.invoices = inv.invoice
+ so.invoices += "," + inv.invoice
so.status = "Completed"
break
else:
so.paid_amount += inv.invoice_amount
inv.invoice_amount = 0
- if so.invoices:
- so.invoices = so.invoices + "," + inv.invoice
- else:
- so.invoices = inv.invoice
+ so.invoices += "," + inv.invoice
so.status = "Partly Paid"
return sales_orders, invoices
From 18731622c43f3b8f7d792d4bb4139eb7cdda39d9 Mon Sep 17 00:00:00 2001
From: marination
Date: Tue, 15 Feb 2022 12:40:39 +0530
Subject: [PATCH 042/447] fix: Update SO via Work Order made from MR (attached
to SO)
- Add SO Item reference in WO from MR (that was made from SO)
---
erpnext/stock/doctype/material_request/material_request.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 103e8d6a88..b39328f85b 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -533,6 +533,7 @@ def raise_work_orders(material_request):
"stock_uom": d.stock_uom,
"expected_delivery_date": d.schedule_date,
"sales_order": d.sales_order,
+ "sales_order_item": d.get("sales_order_item"),
"bom_no": get_item_details(d.item_code).bom_no,
"material_request": mr.name,
"material_request_item": d.name,
From f9d52e73469ea298e3a2d39d893f2da5e6baf9aa Mon Sep 17 00:00:00 2001
From: marination
Date: Tue, 15 Feb 2022 13:38:15 +0530
Subject: [PATCH 043/447] test: SO > MR > WO flow
---
.../doctype/sales_order/test_sales_order.py | 44 ++++++++++++++++++-
1 file changed, 43 insertions(+), 1 deletion(-)
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index acf048e116..e6628d9518 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -6,7 +6,7 @@ import json
import frappe
import frappe.permissions
from frappe.core.doctype.user_permission.test_user_permission import create_user
-from frappe.utils import add_days, flt, getdate, nowdate
+from frappe.utils import add_days, flt, getdate, nowdate, today
from erpnext.controllers.accounts_controller import update_child_qty_rate
from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
@@ -1399,6 +1399,48 @@ class TestSalesOrder(ERPNextTestCase):
so.load_from_db()
self.assertEqual(so.billing_status, 'Fully Billed')
+ def test_so_back_updated_from_wo_via_mr(self):
+ "SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO."
+ from erpnext.stock.doctype.material_request.material_request import raise_work_orders
+ from erpnext.manufacturing.doctype.work_order.work_order import (
+ make_stock_entry as make_se_from_wo,
+ )
+
+ so = make_sales_order(item_list=[{"item_code": "_Test FG Item","qty": 2, "rate":100}])
+
+ mr = make_material_request(so.name)
+ mr.material_request_type = "Manufacture"
+ mr.schedule_date = today()
+ mr.submit()
+
+ # WO from MR
+ wo_name = raise_work_orders(mr.name)[0]
+ wo = frappe.get_doc("Work Order", wo_name)
+ wo.wip_warehouse = "Work In Progress - _TC"
+ wo.skip_transfer = True
+
+ self.assertEqual(wo.sales_order, so.name)
+ self.assertEqual(wo.sales_order_item, so.items[0].name)
+
+ wo.submit()
+ make_stock_entry(item_code="_Test Item", # Stock RM
+ target="Work In Progress - _TC",
+ qty=4, basic_rate=100
+ )
+ make_stock_entry(item_code="_Test Item Home Desktop 100", # Stock RM
+ target="Work In Progress - _TC",
+ qty=4, basic_rate=100
+ )
+
+ se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 2))
+ se.submit() # Finish WO
+
+ mr.reload()
+ wo.reload()
+ so.reload()
+ self.assertEqual(so.items[0].work_order_qty, wo.produced_qty)
+ self.assertEqual(mr.status, "Manufactured")
+
def automatically_fetch_payment_terms(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")
accounts_settings.automatically_fetch_payment_terms = enable
From 0ca58d762715fd10c751c4497f3037908f4dfb20 Mon Sep 17 00:00:00 2001
From: marination
Date: Tue, 15 Feb 2022 14:20:54 +0530
Subject: [PATCH 044/447] chore: Patch to update SO work_order_qty and Linter
fix
---
erpnext/patches.txt | 1 +
.../v14_0/set_work_order_qty_in_so_from_mr.py | 36 +++++++++++++++++++
.../doctype/sales_order/test_sales_order.py | 2 +-
3 files changed, 38 insertions(+), 1 deletion(-)
create mode 100644 erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index d104bc003c..c26451a30c 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -352,3 +352,4 @@ erpnext.patches.v13_0.shopping_cart_to_ecommerce
erpnext.patches.v13_0.update_disbursement_account
erpnext.patches.v13_0.update_reserved_qty_closed_wo
erpnext.patches.v14_0.delete_amazon_mws_doctype
+erpnext.patches.v14_0.set_work_order_qty_in_so_from_mr
diff --git a/erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py b/erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py
new file mode 100644
index 0000000000..f097ab9297
--- /dev/null
+++ b/erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py
@@ -0,0 +1,36 @@
+import frappe
+
+
+def execute():
+ """
+ 1. Get submitted Work Orders with MR, MR Item and SO set
+ 2. Get SO Item detail from MR Item detail in WO, and set in WO
+ 3. Update work_order_qty in SO
+ """
+ work_order = frappe.qb.DocType("Work Order")
+ query = (
+ frappe.qb.from_(work_order)
+ .select(
+ work_order.name, work_order.produced_qty,
+ work_order.material_request,
+ work_order.material_request_item,
+ work_order.sales_order
+ ).where(
+ (work_order.material_request.isnotnull())
+ & (work_order.material_request_item.isnotnull())
+ & (work_order.sales_order.isnotnull())
+ & (work_order.docstatus == 1)
+ & (work_order.produced_qty > 0)
+ )
+ )
+ results = query.run(as_dict=True)
+
+ for row in results:
+ so_item = frappe.get_value(
+ "Material Request Item", row.material_request_item, "sales_order_item"
+ )
+ frappe.db.set_value("Work Order", row.name, "sales_order_item", so_item)
+
+ if so_item:
+ wo = frappe.get_doc("Work Order", row.name)
+ wo.update_work_order_qty_in_so()
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index e6628d9518..73c5bd299a 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -1401,10 +1401,10 @@ class TestSalesOrder(ERPNextTestCase):
def test_so_back_updated_from_wo_via_mr(self):
"SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO."
- from erpnext.stock.doctype.material_request.material_request import raise_work_orders
from erpnext.manufacturing.doctype.work_order.work_order import (
make_stock_entry as make_se_from_wo,
)
+ from erpnext.stock.doctype.material_request.material_request import raise_work_orders
so = make_sales_order(item_list=[{"item_code": "_Test FG Item","qty": 2, "rate":100}])
From 49fdc6c52e9752362b754f1615ca77ac9e09b418 Mon Sep 17 00:00:00 2001
From: ruthra kumar
Date: Tue, 15 Feb 2022 17:20:29 +0530
Subject: [PATCH 045/447] test: refactor and fix failing test case
---
...st_payment_terms_status_for_sales_order.py | 27 ++++++++++++-------
1 file changed, 18 insertions(+), 9 deletions(-)
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
index 5d6e91e8a5..ee6cee3be8 100644
--- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
@@ -15,8 +15,8 @@ test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Temp
class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase):
- def test_payment_terms_status(self):
-
+ def create_payment_terms_template(self):
+ # create template for 50-50 payments
template = None
if frappe.db.exists("Payment Terms Template", "_Test 50-50"):
template = frappe.get_doc("Payment Terms Template", "_Test 50-50")
@@ -46,8 +46,10 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase):
}
)
template.insert()
+ self.template = template
- # item = create_item(item_code="_Test Excavator", is_stock_item=0, valuation_rate=1000000)
+ def test_payment_terms_status(self):
+ self.create_payment_terms_template()
item = create_item(item_code="_Test Excavator", is_stock_item=0)
so = make_sales_order(
transaction_date="2021-06-15",
@@ -58,16 +60,19 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase):
do_not_save=True,
)
so.po_no = ""
- so.payment_terms_template = template.name
+ so.taxes_and_charges = ""
+ so.taxes = ""
+ so.payment_terms_template = self.template.name
so.save()
so.submit()
# make invoice with 60% of the total sales order value
sinv = make_sales_invoice(so.name)
+ sinv.taxes_and_charges = ""
+ sinv.taxes = ""
sinv.items[0].qty = 6
sinv.insert()
sinv.submit()
-
columns, data, message, chart = execute(
{
"company": "_Test Company",
@@ -86,9 +91,10 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase):
"description": "_Test 50-50",
"due_date": datetime.date(2021, 6, 30),
"invoice_portion": 50.0,
- "payment_amount": 500000.0,
+ "currency": "INR",
+ "base_payment_amount": 500000.0,
"paid_amount": 500000.0,
- "invoices": sinv.name,
+ "invoices": ","+sinv.name,
},
{
"name": so.name,
@@ -98,10 +104,13 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase):
"description": "_Test 50-50",
"due_date": datetime.date(2021, 7, 15),
"invoice_portion": 50.0,
- "payment_amount": 500000.0,
+ "currency": "INR",
+ "base_payment_amount": 500000.0,
"paid_amount": 100000.0,
- "invoices": sinv.name,
+ "invoices": ","+sinv.name,
},
]
+ self.assertEqual(data, expected_value)
+
self.assertEqual(data, expected_value)
From 48f37c76594fad1cd64cd44b7126d6ef1ddd5bd1 Mon Sep 17 00:00:00 2001
From: ruthra kumar
Date: Tue, 15 Feb 2022 17:21:08 +0530
Subject: [PATCH 046/447] test: added test for alternate currency
- Sales Order and Invoice will be submitted in USD with exchange rate
of 70 with the default company currency
- Report will display in defauly company currency
---
...st_payment_terms_status_for_sales_order.py | 82 +++++++++++++++++++
1 file changed, 82 insertions(+)
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
index ee6cee3be8..cad41e1dc0 100644
--- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
@@ -112,5 +112,87 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase):
]
self.assertEqual(data, expected_value)
+ def create_exchange_rate(self, date):
+ # make an entry in Currency Exchange list. serves as a static exchange rate
+ if frappe.db.exists({'doctype': "Currency Exchange",'date': date,'from_currency': 'USD', 'to_currency':'INR'}):
+ return
+ else:
+ doc = frappe.get_doc({
+ 'doctype': "Currency Exchange",
+ 'date': date,
+ 'from_currency': 'USD',
+ 'to_currency': frappe.get_cached_value("Company", '_Test Company','default_currency'),
+ 'exchange_rate': 70,
+ 'for_buying': True,
+ 'for_selling': True
+ })
+ doc.insert()
+ def test_alternate_currency(self):
+ transaction_date = "2021-06-15"
+ self.create_payment_terms_template()
+ self.create_exchange_rate(transaction_date)
+ item = create_item(item_code="_Test Excavator", is_stock_item=0)
+ so = make_sales_order(
+ transaction_date=transaction_date,
+ currency="USD",
+ delivery_date=add_days(transaction_date, -30),
+ item=item.item_code,
+ qty=10,
+ rate=10000,
+ do_not_save=True,
+ )
+ so.po_no = ""
+ so.taxes_and_charges = ""
+ so.taxes = ""
+ so.payment_terms_template = self.template.name
+ so.save()
+ so.submit()
+
+ # make invoice with 60% of the total sales order value
+ sinv = make_sales_invoice(so.name)
+ sinv.currency = "USD"
+ sinv.taxes_and_charges = ""
+ sinv.taxes = ""
+ sinv.items[0].qty = 6
+ sinv.insert()
+ sinv.submit()
+ columns, data, message, chart = execute(
+ {
+ "company": "_Test Company",
+ "period_start_date": "2021-06-01",
+ "period_end_date": "2021-06-30",
+ "sales_order": [so.name],
+ }
+ )
+
+ # report defaults to company currency.
+ expected_value = [
+ {
+ "name": so.name,
+ "submitted": datetime.date(2021, 6, 15),
+ "status": "Completed",
+ "payment_term": None,
+ "description": "_Test 50-50",
+ "due_date": datetime.date(2021, 6, 30),
+ "invoice_portion": 50.0,
+ "currency": frappe.get_cached_value("Company", '_Test Company','default_currency'),
+ "base_payment_amount": 3500000.0,
+ "paid_amount": 3500000.0,
+ "invoices": ","+sinv.name,
+ },
+ {
+ "name": so.name,
+ "submitted": datetime.date(2021, 6, 15),
+ "status": "Partly Paid",
+ "payment_term": None,
+ "description": "_Test 50-50",
+ "due_date": datetime.date(2021, 7, 15),
+ "invoice_portion": 50.0,
+ "currency": frappe.get_cached_value("Company", '_Test Company','default_currency'),
+ "base_payment_amount": 3500000.0,
+ "paid_amount": 700000.0,
+ "invoices": ","+sinv.name,
+ },
+ ]
self.assertEqual(data, expected_value)
From 799671c7482fa8bca12a24636ea0000579ca9537 Mon Sep 17 00:00:00 2001
From: marination
Date: Tue, 15 Feb 2022 18:10:57 +0530
Subject: [PATCH 047/447] fix: Transfer Bucket logic for Repack Entry with
split batch rows
---
.../stock/report/stock_ageing/stock_ageing.py | 30 ++++++++++++++++---
1 file changed, 26 insertions(+), 4 deletions(-)
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index a89a4038c2..9866e63fb5 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -286,10 +286,11 @@ class FIFOSlots:
def __compute_incoming_stock(self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List):
"Update FIFO Queue on inward stock."
- if self.transferred_item_details.get(transfer_key):
- # inward/outward from same voucher, item & warehouse
- slot = self.transferred_item_details[transfer_key].pop(0)
- fifo_queue.append(slot)
+ transfer_data = self.transferred_item_details.get(transfer_key)
+ if transfer_data:
+ # [Repack] inward/outward from same voucher, item & warehouse
+ # consume transfer data and add stock to fifo queue
+ self.__adjust_incoming_transfer_qty(transfer_data, fifo_queue, row)
else:
if not serial_nos:
if fifo_queue and flt(fifo_queue[0][0]) < 0:
@@ -333,6 +334,27 @@ class FIFOSlots:
self.transferred_item_details[transfer_key].append([qty_to_pop, slot[1]])
qty_to_pop = 0
+ def __adjust_incoming_transfer_qty(self, transfer_data: Dict, fifo_queue: List, row: Dict):
+ "Add previously removed stock back to FIFO Queue."
+ transfer_qty_to_pop = flt(row.actual_qty)
+ first_bucket_qty = transfer_data[0][0]
+ first_bucket_date = transfer_data[0][1]
+
+ while transfer_qty_to_pop:
+ if transfer_data and 0 > first_bucket_qty <= transfer_qty_to_pop:
+ # bucket qty is not enough, consume whole
+ transfer_qty_to_pop -= first_bucket_qty
+ slot = transfer_data.pop(0)
+ fifo_queue.append(slot)
+ elif not transfer_data:
+ # transfer bucket is empty, extra incoming qty
+ fifo_queue.append([transfer_qty_to_pop, row.posting_date])
+ else:
+ # ample bucket qty to consume
+ first_bucket_qty -= transfer_qty_to_pop
+ fifo_queue.append([transfer_qty_to_pop, first_bucket_date])
+ transfer_qty_to_pop = 0
+
def __update_balances(self, row: Dict, key: Union[Tuple, str]):
self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction
From ea3b7de867fdcc565567ec9ca1b7925116e16f2f Mon Sep 17 00:00:00 2001
From: marination
Date: Tue, 15 Feb 2022 18:41:42 +0530
Subject: [PATCH 048/447] test: Stock Ageing FIFO buckets for Repack entry with
same item
---
.../report/stock_ageing/test_stock_ageing.py | 153 ++++++++++++++++++
1 file changed, 153 insertions(+)
diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
index 66d2f6b753..3055332540 100644
--- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
@@ -236,6 +236,159 @@ class TestStockAgeing(ERPNextTestCase):
item_wh_balances = [item_wh_wise_slots.get(i).get("qty_after_transaction") for i in item_wh_wise_slots]
self.assertEqual(sum(item_wh_balances), item_result["qty_after_transaction"])
+ def test_repack_entry_same_item_split_rows(self):
+ """
+ Split consumption rows and have single repacked item row (same warehouse).
+ Ledger:
+ Item | Qty | Voucher
+ ------------------------
+ Item 1 | 500 | 001
+ Item 1 | -50 | 002 (repack)
+ Item 1 | -50 | 002 (repack)
+ Item 1 | 100 | 002 (repack)
+
+ Case most likely for batch items. Test time bucket computation.
+ """
+ sle = [
+ frappe._dict( # stock up item
+ name="Flask Item",
+ actual_qty=500, qty_after_transaction=500,
+ warehouse="WH 1",
+ posting_date="2021-12-03", voucher_type="Stock Entry",
+ voucher_no="001",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=(-50), qty_after_transaction=450,
+ warehouse="WH 1",
+ posting_date="2021-12-04", voucher_type="Stock Entry",
+ voucher_no="002",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=(-50), qty_after_transaction=400,
+ warehouse="WH 1",
+ posting_date="2021-12-04", voucher_type="Stock Entry",
+ voucher_no="002",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=100, qty_after_transaction=500,
+ warehouse="WH 1",
+ posting_date="2021-12-04", voucher_type="Stock Entry",
+ voucher_no="002",
+ has_serial_no=False, serial_no=None
+ ),
+ ]
+ slots = FIFOSlots(self.filters, sle).generate()
+ item_result = slots["Flask Item"]
+ queue = item_result["fifo_queue"]
+
+ self.assertEqual(item_result["total_qty"], 500.0)
+ self.assertEqual(queue[0][0], 400.0)
+ self.assertEqual(queue[1][0], 100.0)
+ # check if time buckets add up to balance qty
+ self.assertEqual(sum([i[0] for i in queue]), 500.0)
+
+ def test_repack_entry_same_item_overconsume(self):
+ """
+ Over consume item and have less repacked item qty (same warehouse).
+ Ledger:
+ Item | Qty | Voucher
+ ------------------------
+ Item 1 | 500 | 001
+ Item 1 | -100 | 002 (repack)
+ Item 1 | 50 | 002 (repack)
+
+ Case most likely for batch items. Test time bucket computation.
+ """
+ sle = [
+ frappe._dict( # stock up item
+ name="Flask Item",
+ actual_qty=500, qty_after_transaction=500,
+ warehouse="WH 1",
+ posting_date="2021-12-03", voucher_type="Stock Entry",
+ voucher_no="001",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=(-100), qty_after_transaction=400,
+ warehouse="WH 1",
+ posting_date="2021-12-04", voucher_type="Stock Entry",
+ voucher_no="002",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=50, qty_after_transaction=450,
+ warehouse="WH 1",
+ posting_date="2021-12-04", voucher_type="Stock Entry",
+ voucher_no="002",
+ has_serial_no=False, serial_no=None
+ ),
+ ]
+ slots = FIFOSlots(self.filters, sle).generate()
+ item_result = slots["Flask Item"]
+ queue = item_result["fifo_queue"]
+
+ self.assertEqual(item_result["total_qty"], 450.0)
+ self.assertEqual(queue[0][0], 400.0)
+ self.assertEqual(queue[1][0], 50.0)
+ # check if time buckets add up to balance qty
+ self.assertEqual(sum([i[0] for i in queue]), 450.0)
+
+ def test_repack_entry_same_item_overproduce(self):
+ """
+ Under consume item and have more repacked item qty (same warehouse).
+ Ledger:
+ Item | Qty | Voucher
+ ------------------------
+ Item 1 | 500 | 001
+ Item 1 | -50 | 002 (repack)
+ Item 1 | 100 | 002 (repack)
+
+ Case most likely for batch items. Test time bucket computation.
+ """
+ sle = [
+ frappe._dict( # stock up item
+ name="Flask Item",
+ actual_qty=500, qty_after_transaction=500,
+ warehouse="WH 1",
+ posting_date="2021-12-03", voucher_type="Stock Entry",
+ voucher_no="001",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=(-50), qty_after_transaction=450,
+ warehouse="WH 1",
+ posting_date="2021-12-04", voucher_type="Stock Entry",
+ voucher_no="002",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=100, qty_after_transaction=550,
+ warehouse="WH 1",
+ posting_date="2021-12-04", voucher_type="Stock Entry",
+ voucher_no="002",
+ has_serial_no=False, serial_no=None
+ ),
+ ]
+ slots = FIFOSlots(self.filters, sle).generate()
+ item_result = slots["Flask Item"]
+ queue = item_result["fifo_queue"]
+
+ self.assertEqual(item_result["total_qty"], 550.0)
+ self.assertEqual(queue[0][0], 450.0)
+ self.assertEqual(queue[1][0], 100.0)
+ # check if time buckets add up to balance qty
+ self.assertEqual(sum([i[0] for i in queue]), 550.0)
+
def generate_item_and_item_wh_wise_slots(filters, sle):
"Return results with and without 'show_warehouse_wise_stock'"
item_wise_slots = FIFOSlots(filters, sle).generate()
From f6233e77c6c2cbfeec6aeb82a73c1bbcbaa8f5da Mon Sep 17 00:00:00 2001
From: marination
Date: Tue, 15 Feb 2022 20:30:16 +0530
Subject: [PATCH 049/447] chore: Add transfer bucket working to .md file
---
.../stock_ageing/stock_ageing_fifo_logic.md | 37 ++++++++++++++++++-
1 file changed, 36 insertions(+), 1 deletion(-)
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md b/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md
index 9e9bed48e3..3d759dd998 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md
+++ b/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md
@@ -71,4 +71,39 @@ Date | Qty | Queue
2nd | -60 | [[-10, 1-12-2021]]
3rd | +5 | [[-5, 3-12-2021]]
4th | +10 | [[5, 4-12-2021]]
-4th | +20 | [[5, 4-12-2021], [20, 4-12-2021]]
\ No newline at end of file
+4th | +20 | [[5, 4-12-2021], [20, 4-12-2021]]
+
+### Concept of Transfer Qty Bucket
+In the case of **Repack**, Quantity that comes in, isn't really incoming. It is just new stock repurposed from old stock, due to incoming-outgoing of the same warehouse.
+
+Here, stock is consumed from the FIFO Queue. It is then re-added back to the queue.
+While adding stock back to the queue we need to know how much to add.
+For this we need to keep track of how much was previously consumed.
+Hence we use **Transfer Qty Bucket**.
+
+While re-adding stock, we try to add buckets that were consumed earlier (date intact), to maintain correctness.
+
+#### Case 1: Same Item-Warehouse in Repack
+Eg:
+-------------------------------------------------------------------------------------
+Date | Qty | Voucher | FIFO Queue | Transfer Qty Buckets
+-------------------------------------------------------------------------------------
+1st | +500 | PR | [[500, 1-12-2021]] |
+2nd | -50 | Repack | [[450, 1-12-2021]] | [[50, 1-12-2021]]
+2nd | +50 | Repack | [[450, 1-12-2021], [50, 1-12-2021]] | []
+
+- The balance at the end is restored back to 500
+- However, the initial 500 qty bucket is now split into 450 and 50, with the same date
+- The net effect is the same as that before the Repack
+
+#### Case 2: Same Item-Warehouse in Repack with Split Consumption rows
+Eg:
+-------------------------------------------------------------------------------------
+Date | Qty | Voucher | FIFO Queue | Transfer Qty Buckets
+-------------------------------------------------------------------------------------
+1st | +500 | PR | [[500, 1-12-2021]] |
+2nd | -50 | Repack | [[450, 1-12-2021]] | [[50, 1-12-2021]]
+2nd | -50 | Repack | [[400, 1-12-2021]] | [[50, 1-12-2021],
+- | | | |[50, 1-12-2021]]
+2nd | +100 | Repack | [[400, 1-12-2021], [50, 1-12-2021], | []
+- | | | [50, 1-12-2021]] |
From 29c576e144489072c992e9b5bdfe4c9359639ef8 Mon Sep 17 00:00:00 2001
From: marination
Date: Wed, 16 Feb 2022 12:41:39 +0530
Subject: [PATCH 050/447] chore: Remove commented out code
---
.../variant_selector/item_variants_cache.py | 16 ----------------
1 file changed, 16 deletions(-)
diff --git a/erpnext/e_commerce/variant_selector/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py
index 3aefc446c2..3107c019e6 100644
--- a/erpnext/e_commerce/variant_selector/item_variants_cache.py
+++ b/erpnext/e_commerce/variant_selector/item_variants_cache.py
@@ -81,25 +81,9 @@ class ItemVariantsCacheManager:
)
item_variants_data = query.run()
- # item_variants_data = frappe.get_all(
- # 'Item Variant Attribute',
- # {'variant_of': parent_item_code},
- # ['parent', 'attribute', 'attribute_value'],
- # order_by='name',
- # as_list=1
- # )
-
- # disabled_items = set(
- # [i.name for i in frappe.db.get_all('Item', {'disabled': 1})]
- # )
-
attribute_value_item_map = frappe._dict()
item_attribute_value_map = frappe._dict()
- # dont consider variants that are disabled
- # pull all other variants
- # item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items]
-
for row in item_variants_data:
item_code, attribute, attribute_value = row
# (attr, value) => [item1, item2]
From a26183e205effa11d1fae7a3d6cb96c7db100e07 Mon Sep 17 00:00:00 2001
From: Kenneth Sequeira <33246109+kennethsequeira@users.noreply.github.com>
Date: Wed, 16 Feb 2022 13:02:36 +0530
Subject: [PATCH 051/447] fix: add supported currencies (#29805)
---
.../doctype/gocardless_settings/gocardless_settings.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
index a8119ac86c..f02f76e18b 100644
--- a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
+++ b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
@@ -13,7 +13,7 @@ from frappe.utils import call_hook_method, cint, flt, get_url
class GoCardlessSettings(Document):
- supported_currencies = ["EUR", "DKK", "GBP", "SEK"]
+ supported_currencies = ["EUR", "DKK", "GBP", "SEK", "AUD", "NZD", "CAD", "USD"]
def validate(self):
self.initialize_client()
@@ -80,7 +80,7 @@ class GoCardlessSettings(Document):
def validate_transaction_currency(self, currency):
if currency not in self.supported_currencies:
- frappe.throw(_("Please select another payment method. Stripe does not support transactions in currency '{0}'").format(currency))
+ frappe.throw(_("Please select another payment method. Go Cardless does not support transactions in currency '{0}'").format(currency))
def get_payment_url(self, **kwargs):
return get_url("./integrations/gocardless_checkout?{0}".format(urlencode(kwargs)))
From 235b0715bfed6197b26cd8d611daa75e9fd7cefb Mon Sep 17 00:00:00 2001
From: Wolfram Schmidt
Date: Wed, 16 Feb 2022 12:50:07 +0100
Subject: [PATCH 052/447] fix: allow renaming and merging (#29830)
---
.../opportunity_lost_reason/opportunity_lost_reason.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json b/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json
index 8a8d4252da..0cfcf0e0ea 100644
--- a/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json
+++ b/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json
@@ -3,7 +3,7 @@
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
- "allow_rename": 0,
+ "allow_rename": 1,
"autoname": "field:lost_reason",
"beta": 0,
"creation": "2018-12-28 14:48:51.044975",
@@ -57,7 +57,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2018-12-28 14:49:43.336437",
+ "modified": "2022-02-16 10:49:43.336437",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity Lost Reason",
@@ -150,4 +150,4 @@
"track_changes": 0,
"track_seen": 0,
"track_views": 0
-}
\ No newline at end of file
+}
From 06d36c6143dbe834ca8f8a15dc81349f270b3d7d Mon Sep 17 00:00:00 2001
From: marination
Date: Wed, 16 Feb 2022 17:40:16 +0530
Subject: [PATCH 053/447] chore: Move patch that updates SO from WO to v13
---
erpnext/patches.txt | 2 +-
.../{v14_0 => v13_0}/set_work_order_qty_in_so_from_mr.py | 0
2 files changed, 1 insertion(+), 1 deletion(-)
rename erpnext/patches/{v14_0 => v13_0}/set_work_order_qty_in_so_from_mr.py (100%)
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index c26451a30c..9f6d0f5854 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -352,4 +352,4 @@ erpnext.patches.v13_0.shopping_cart_to_ecommerce
erpnext.patches.v13_0.update_disbursement_account
erpnext.patches.v13_0.update_reserved_qty_closed_wo
erpnext.patches.v14_0.delete_amazon_mws_doctype
-erpnext.patches.v14_0.set_work_order_qty_in_so_from_mr
+erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
diff --git a/erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py b/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py
similarity index 100%
rename from erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py
rename to erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py
From 02e77029faed67ffff3e395c1de132cf15a14a03 Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Wed, 16 Feb 2022 18:15:57 +0530
Subject: [PATCH 054/447] fix: added item name in the excel sheet
---
.../doctype/production_plan/production_plan.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 4290ca3e4c..676481ac0f 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -588,7 +588,8 @@ def download_raw_materials(doc, warehouses=None):
if isinstance(doc, str):
doc = frappe._dict(json.loads(doc))
- item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM',
+ item_list = [['Item Code', 'Item Name', 'Description',
+ 'Stock UOM', 'Warehouse', 'Required Qty as per BOM',
'Projected Qty', 'Available Qty In Hand', 'Ordered Qty', 'Planned Qty',
'Reserved Qty for Production', 'Safety Stock', 'Required Qty']]
@@ -597,7 +598,8 @@ def download_raw_materials(doc, warehouses=None):
items = get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True)
for d in items:
- item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('warehouse'),
+ item_list.append([d.get('item_code'), d.get('item_name'),
+ d.get('description'), d.get('stock_uom'), d.get('warehouse'),
d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'),
d.get('planned_qty'), d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')])
From 72fe5590313f82ee22421339ca780ba9d4249ed1 Mon Sep 17 00:00:00 2001
From: ChillarAnand
Date: Thu, 17 Feb 2022 08:33:15 +0530
Subject: [PATCH 055/447] refactor: Remove non profit templates
---
.../templates/pages/non_profit/__init__.py | 0
.../pages/non_profit/join-chapter.html | 59 -------------------
.../pages/non_profit/join_chapter.js | 12 ----
.../pages/non_profit/join_chapter.py | 23 --------
.../pages/non_profit/leave-chapter.html | 42 -------------
.../pages/non_profit/leave_chapter.py | 8 ---
6 files changed, 144 deletions(-)
delete mode 100644 erpnext/templates/pages/non_profit/__init__.py
delete mode 100644 erpnext/templates/pages/non_profit/join-chapter.html
delete mode 100644 erpnext/templates/pages/non_profit/join_chapter.js
delete mode 100644 erpnext/templates/pages/non_profit/join_chapter.py
delete mode 100644 erpnext/templates/pages/non_profit/leave-chapter.html
delete mode 100644 erpnext/templates/pages/non_profit/leave_chapter.py
diff --git a/erpnext/templates/pages/non_profit/__init__.py b/erpnext/templates/pages/non_profit/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/templates/pages/non_profit/join-chapter.html b/erpnext/templates/pages/non_profit/join-chapter.html
deleted file mode 100644
index 4923efc4e8..0000000000
--- a/erpnext/templates/pages/non_profit/join-chapter.html
+++ /dev/null
@@ -1,59 +0,0 @@
-{% extends "templates/web.html" %}
-
-{% block page_content %}
-
-{% macro chapter_button() %}
-
- Go to Chapter Page
-{% endmacro %}
-{% if frappe.session.user=='Guest' %}
- Please signup and login to join this chapter
- Login
-{% else %}
- {% if already_member %}
- You are already a member of {{ chapter.name }}!
- {{ chapter_button() }}
- Leave Chapter
- {% else %}
- {% if request.method=='POST' %}
- Welcome to chapter {{ chapter.name }}!
- {{ chapter_button() }}
- {% else %}
-
- {% endif %}
- {% endif %}
-
-{% endif %}
-
-{% endblock %}
diff --git a/erpnext/templates/pages/non_profit/join_chapter.js b/erpnext/templates/pages/non_profit/join_chapter.js
deleted file mode 100644
index e2bc8bca71..0000000000
--- a/erpnext/templates/pages/non_profit/join_chapter.js
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (c) 2017, EOSSF and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Chapter Member', {
- onsubmit: function (frm) {
- console.log("here" + frappe.session.user)
- // body...
- }
- refresh: function(frm) {
-
- }
-});
diff --git a/erpnext/templates/pages/non_profit/join_chapter.py b/erpnext/templates/pages/non_profit/join_chapter.py
deleted file mode 100644
index 7caf87db2b..0000000000
--- a/erpnext/templates/pages/non_profit/join_chapter.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import frappe
-
-
-def get_context(context):
- context.no_cache = True
- chapter = frappe.get_doc('Chapter', frappe.form_dict.name)
- if frappe.session.user!='Guest':
- if frappe.session.user in [d.user for d in chapter.members if d.enabled == 1]:
- context.already_member = True
- else:
- if frappe.request.method=='GET':
- pass
- elif frappe.request.method=='POST':
- chapter.append('members', dict(
- user=frappe.session.user,
- introduction=frappe.form_dict.introduction,
- website_url=frappe.form_dict.website_url,
- enabled=1
- ))
- chapter.save(ignore_permissions=1)
- frappe.db.commit()
-
- context.chapter = chapter
diff --git a/erpnext/templates/pages/non_profit/leave-chapter.html b/erpnext/templates/pages/non_profit/leave-chapter.html
deleted file mode 100644
index fd7658b3b1..0000000000
--- a/erpnext/templates/pages/non_profit/leave-chapter.html
+++ /dev/null
@@ -1,42 +0,0 @@
-{% extends "templates/web.html" %}
-{% block page_content %}
-
- {% if member_deleted %}
- You are not a member of {{ chapter.name }}!
-
-
-
- Why do you want to leave this chapter
-
-
- Submit
-
-
-
- Please signup and login to join this chapter
-
- Become Member agian
- {% endif %}
-
-{% endblock %}
diff --git a/erpnext/templates/pages/non_profit/leave_chapter.py b/erpnext/templates/pages/non_profit/leave_chapter.py
deleted file mode 100644
index 65908e1dd9..0000000000
--- a/erpnext/templates/pages/non_profit/leave_chapter.py
+++ /dev/null
@@ -1,8 +0,0 @@
-import frappe
-
-
-def get_context(context):
- context.no_cache = True
- chapter = frappe.get_doc('Chapter', frappe.form_dict.name)
- context.member_deleted = True
- context.chapter = chapter
From b000e93744c2730517172717ed63048bab50d62f Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 16 Feb 2022 20:04:45 +0530
Subject: [PATCH 056/447] fix: avoid updating items table if no change due to
putaway
---
.../doctype/putaway_rule/putaway_rule.py | 37 ++++++++++++++++++-
1 file changed, 35 insertions(+), 2 deletions(-)
diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py
index 523ba120de..4e472a92dc 100644
--- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py
+++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py
@@ -9,7 +9,7 @@ from collections import defaultdict
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import cint, floor, flt, nowdate
+from frappe.utils import cint, cstr, floor, flt, nowdate
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.utils import get_stock_balance
@@ -142,11 +142,44 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
if items_not_accomodated:
show_unassigned_items_message(items_not_accomodated)
- items[:] = updated_table if updated_table else items # modify items table
+ if updated_table and _items_changed(items, updated_table, doctype):
+ items[:] = updated_table
+ frappe.msgprint(_("Applied putaway rules."), alert=True)
if sync and json.loads(sync): # sync with client side
return items
+def _items_changed(old, new, doctype: str) -> bool:
+ """ Check if any items changed by application of putaway rules.
+
+ If not, changing item table can have side effects since `name` items also changes.
+ """
+ if len(old) != len(new):
+ return True
+
+ old = [frappe._dict(item) if isinstance(item, dict) else item for item in old]
+
+ if doctype == "Stock Entry":
+ compare_keys = ("item_code", "t_warehouse", "transfer_qty", "serial_no")
+ sort_key = lambda item: (item.item_code, cstr(item.t_warehouse), # noqa
+ flt(item.transfer_qty), cstr(item.serial_no))
+ else:
+ # purchase receipt / invoice
+ compare_keys = ("item_code", "warehouse", "stock_qty", "received_qty", "serial_no")
+ sort_key = lambda item: (item.item_code, cstr(item.warehouse), # noqa
+ flt(item.stock_qty), flt(item.received_qty), cstr(item.serial_no))
+
+ old_sorted = sorted(old, key=sort_key)
+ new_sorted = sorted(new, key=sort_key)
+
+ # Once sorted by all relevant keys both tables should align if they are same.
+ for old_item, new_item in zip(old_sorted, new_sorted):
+ for key in compare_keys:
+ if old_item.get(key) != new_item.get(key):
+ return True
+ return False
+
+
def get_ordered_putaway_rules(item_code, company, source_warehouse=None):
"""Returns an ordered list of putaway rules to apply on an item."""
filters = {
From d9fc3f3d902a98dc9b1c1ab6814c66b170e18a04 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 17 Feb 2022 11:08:50 +0530
Subject: [PATCH 057/447] test: putaway rule re-application shouldn't do
anything
---
.../doctype/putaway_rule/test_putaway_rule.py | 26 +++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
index bd4d811e76..ff1c19a827 100644
--- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
+++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
@@ -35,6 +35,18 @@ class TestPutawayRule(ERPNextTestCase):
new_uom.uom_name = "Bag"
new_uom.save()
+ def assertUnchangedItemsOnResave(self, doc):
+ """ Check if same items remain even after reapplication of rules.
+
+ This is required since some business logic like subcontracting
+ depends on `name` of items to be same if item isn't changed.
+ """
+ doc.reload()
+ old_items = {d.name for d in doc.items}
+ doc.save()
+ new_items = {d.name for d in doc.items}
+ self.assertSetEqual(old_items, new_items)
+
def test_putaway_rules_priority(self):
"""Test if rule is applied by priority, irrespective of free space."""
rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200,
@@ -50,6 +62,8 @@ class TestPutawayRule(ERPNextTestCase):
self.assertEqual(pr.items[1].qty, 100)
self.assertEqual(pr.items[1].warehouse, self.warehouse_2)
+ self.assertUnchangedItemsOnResave(pr)
+
pr.delete()
rule_1.delete()
rule_2.delete()
@@ -162,6 +176,8 @@ class TestPutawayRule(ERPNextTestCase):
# leftover space was for 500 kg (0.5 Bag)
# Since Bag is a whole UOM, 1(out of 2) Bag will be unassigned
+ self.assertUnchangedItemsOnResave(pr)
+
pr.delete()
rule_1.delete()
rule_2.delete()
@@ -196,6 +212,8 @@ class TestPutawayRule(ERPNextTestCase):
self.assertEqual(pr.items[1].warehouse, self.warehouse_1)
self.assertEqual(pr.items[1].putaway_rule, rule_1.name)
+ self.assertUnchangedItemsOnResave(pr)
+
pr.delete()
rule_1.delete()
@@ -239,6 +257,8 @@ class TestPutawayRule(ERPNextTestCase):
self.assertEqual(stock_entry_item.qty, 100) # unassigned 100 out of 200 Kg
self.assertEqual(stock_entry_item.putaway_rule, rule_2.name)
+ self.assertUnchangedItemsOnResave(stock_entry)
+
stock_entry.delete()
rule_1.delete()
rule_2.delete()
@@ -294,6 +314,8 @@ class TestPutawayRule(ERPNextTestCase):
self.assertEqual(stock_entry.items[2].qty, 200)
self.assertEqual(stock_entry.items[2].putaway_rule, rule_2.name)
+ self.assertUnchangedItemsOnResave(stock_entry)
+
stock_entry.delete()
rule_1.delete()
rule_2.delete()
@@ -344,6 +366,8 @@ class TestPutawayRule(ERPNextTestCase):
self.assertEqual(stock_entry.items[1].serial_no, "\n".join(serial_nos[3:]))
self.assertEqual(stock_entry.items[1].batch_no, "BOTTL-BATCH-1")
+ self.assertUnchangedItemsOnResave(stock_entry)
+
stock_entry.delete()
pr.cancel()
rule_1.delete()
@@ -366,6 +390,8 @@ class TestPutawayRule(ERPNextTestCase):
self.assertEqual(stock_entry_item.qty, 100)
self.assertEqual(stock_entry_item.putaway_rule, rule_1.name)
+ self.assertUnchangedItemsOnResave(stock_entry)
+
stock_entry.delete()
rule_1.delete()
rule_2.delete()
From b5ff9b27bdf762c4118974d69ef8b78b2c228348 Mon Sep 17 00:00:00 2001
From: ChillarAnand
Date: Thu, 17 Feb 2022 08:33:55 +0530
Subject: [PATCH 058/447] refactor: Clean up non-profit setup
---
erpnext/__init__.py | 8 ------
erpnext/patches.txt | 2 +-
.../v14_0/delete_non_profit_doctypes.py | 28 ++++++++++++++-----
erpnext/regional/india/setup.py | 9 ------
4 files changed, 22 insertions(+), 25 deletions(-)
diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index 0b4696c803..a44c8fabe3 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -130,11 +130,3 @@ def allow_regional(fn):
return fn(*args, **kwargs)
return caller
-
-def get_last_membership(member):
- '''Returns last membership if exists'''
- last_membership = frappe.get_all('Membership', 'name,to_date,membership_type',
- dict(member=member, paid=1), order_by='to_date desc', limit=1)
-
- if last_membership:
- return last_membership[0]
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index a8134f20fc..7bc70714a2 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -334,7 +334,6 @@ erpnext.patches.v13_0.update_asset_quantity_field
erpnext.patches.v13_0.delete_bank_reconciliation_detail
erpnext.patches.v13_0.enable_provisional_accounting
erpnext.patches.v13_0.non_profit_deprecation_warning
-erpnext.patches.v14_0.delete_non_profit_doctypes
[post_model_sync]
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
@@ -351,3 +350,4 @@ erpnext.patches.v12_0.add_company_link_to_einvoice_settings
erpnext.patches.v14_0.migrate_cost_center_allocations
erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template
erpnext.patches.v13_0.shopping_cart_to_ecommerce
+erpnext.patches.v14_0.delete_non_profit_doctypes
diff --git a/erpnext/patches/v14_0/delete_non_profit_doctypes.py b/erpnext/patches/v14_0/delete_non_profit_doctypes.py
index 86355a6426..39f2fecf08 100644
--- a/erpnext/patches/v14_0/delete_non_profit_doctypes.py
+++ b/erpnext/patches/v14_0/delete_non_profit_doctypes.py
@@ -30,10 +30,24 @@ def execute():
for doctype in doctypes:
frappe.delete_doc("DocType", doctype, ignore_missing=True)
- custom_fields = [
- {"dt": "Member", "fieldname": "pan_number"},
- {"dt": "Donor", "fieldname": "pan_number"},
- ]
- for field in custom_fields:
- custom_field = frappe.db.get_value("Custom Field", field)
- frappe.delete_doc("Custom Field", custom_field, ignore_missing=True)
+ forms = ['grant-application', 'certification-application', 'certification-application-usd']
+ for form in forms:
+ frappe.delete_doc("Web Form", form, ignore_missing=True)
+
+ custom_fields = {
+ 'Member': ['pan_number'],
+ 'Donor': ['pan_number'],
+ 'Company': [
+ 'non_profit_section', 'company_80g_number', 'with_effect_from',
+ 'non_profit_column_break', 'pan_details'
+ ],
+ }
+
+ for doc, fields in custom_fields.items():
+ filters = {
+ 'dt': doc,
+ 'fieldname': ['in', fields]
+ }
+ records = frappe.get_all('Custom Field', filters=filters, pluck='name')
+ for record in records:
+ frappe.delete_doc('Custom Field', record, ignore_missing=True, force=True)
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index c2e83045c7..8f3ef62c12 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -610,15 +610,6 @@ def get_custom_fields():
dict(fieldname='hra_column_break', fieldtype='Column Break', insert_after='hra_component'),
dict(fieldname='arrear_component', label='Arrear Component',
fieldtype='Link', options='Salary Component', insert_after='hra_column_break'),
- dict(fieldname='non_profit_section', label='Non Profit Settings',
- fieldtype='Section Break', insert_after='arrear_component', collapsible=1),
- dict(fieldname='company_80g_number', label='80G Number',
- fieldtype='Data', insert_after='non_profit_section'),
- dict(fieldname='with_effect_from', label='80G With Effect From',
- fieldtype='Date', insert_after='company_80g_number'),
- dict(fieldname='non_profit_column_break', fieldtype='Column Break', insert_after='with_effect_from'),
- dict(fieldname='pan_details', label='PAN Number',
- fieldtype='Data', insert_after='non_profit_column_break')
],
'Employee Tax Exemption Declaration':[
dict(fieldname='hra_section', label='HRA Exemption',
From 60674e52b8a08dc5785da73e9ce418fad00d836c Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Thu, 17 Feb 2022 14:14:47 +0530
Subject: [PATCH 059/447] fix: currency in bank reconciliation tool
---
.../bank_reconciliation_tool/bank_reconciliation_tool.js | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
index dbf362234e..46ba27c004 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
@@ -64,6 +64,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
"account_currency",
(r) => {
frm.currency = r.account_currency;
+ frm.trigger("render_chart");
}
);
}
@@ -128,7 +129,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
}
},
- render_chart(frm) {
+ render_chart: frappe.utils.debounce((frm) => {
frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager(
{
$reconciliation_tool_cards: frm.get_field(
@@ -140,7 +141,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
currency: frm.currency,
}
);
- },
+ }, 500),
render(frm) {
if (frm.doc.bank_account) {
From db93f26f20fe315e46324bfb36de759637f918bc Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 17 Feb 2022 14:24:52 +0530
Subject: [PATCH 060/447] fix: production plan status should consider qty + WO
status
---
.../production_plan/production_plan.py | 24 +++++++++++++------
.../production_plan/test_production_plan.py | 17 ++++++++++---
2 files changed, 31 insertions(+), 10 deletions(-)
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index b1c86bcbf8..80003dab78 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -319,7 +319,7 @@ class ProductionPlan(Document):
if self.total_produced_qty > 0:
self.status = "In Process"
- if self.check_have_work_orders_completed():
+ if self.all_items_completed():
self.status = "Completed"
if self.status != 'Completed':
@@ -591,14 +591,24 @@ class ProductionPlan(Document):
self.append("sub_assembly_items", data)
- def check_have_work_orders_completed(self):
- wo_status = frappe.db.get_list(
+ def all_items_completed(self):
+ all_items_produced = all(flt(d.planned_qty) - flt(d.produced_qty) < 0.000001
+ for d in self.po_items)
+ if not all_items_produced:
+ return False
+
+ wo_status = frappe.get_all(
"Work Order",
- filters={"production_plan": self.name},
+ filters={
+ "production_plan": self.name,
+ "status": ("not in", ["Closed", "Stopped"]),
+ "docstatus": ("<", 2),
+ },
fields="status",
- pluck="status"
+ pluck="status",
)
- return all(s == "Completed" for s in wo_status)
+ all_work_orders_completed = all(s == "Completed" for s in wo_status)
+ return all_work_orders_completed
@frappe.whitelist()
def download_raw_materials(doc, warehouses=None):
@@ -1046,4 +1056,4 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
def set_default_warehouses(row, default_warehouses):
for field in ['wip_warehouse', 'fg_warehouse']:
if not row.get(field):
- row[field] = default_warehouses.get(field)
\ No newline at end of file
+ row[field] = default_warehouses.get(field)
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index afa1501efc..d88e10a564 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -409,9 +409,6 @@ class TestProductionPlan(ERPNextTestCase):
boms = {
"Assembly": {
"SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
- "SubAssembly2": {"ChildPart3": {}},
- "SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}},
- "ChildPart5": {},
"ChildPart6": {},
"SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}},
},
@@ -591,6 +588,20 @@ class TestProductionPlan(ERPNextTestCase):
pln.reload()
self.assertEqual(pln.po_items[0].pending_qty, 1)
+ def test_qty_based_status(self):
+ pp = frappe.new_doc("Production Plan")
+ pp.po_items = [
+ frappe._dict(planned_qty=5, produce_qty=4)
+ ]
+ self.assertFalse(pp.all_items_completed())
+
+ pp.po_items = [
+ frappe._dict(planned_qty=5, produce_qty=10),
+ frappe._dict(planned_qty=5, produce_qty=4)
+ ]
+ self.assertFalse(pp.all_items_completed())
+
+
def create_production_plan(**args):
"""
sales_order (obj): Sales Order Doc Object
From fb59247182a4511fbdec106cc0ca0f68253b8579 Mon Sep 17 00:00:00 2001
From: ChillarAnand
Date: Thu, 17 Feb 2022 15:15:10 +0530
Subject: [PATCH 061/447] fix: Delete party type records in patch
---
erpnext/patches/v14_0/delete_non_profit_doctypes.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/erpnext/patches/v14_0/delete_non_profit_doctypes.py b/erpnext/patches/v14_0/delete_non_profit_doctypes.py
index 39f2fecf08..5b1cfefa4b 100644
--- a/erpnext/patches/v14_0/delete_non_profit_doctypes.py
+++ b/erpnext/patches/v14_0/delete_non_profit_doctypes.py
@@ -34,6 +34,13 @@ def execute():
for form in forms:
frappe.delete_doc("Web Form", form, ignore_missing=True)
+ custom_records = [
+ {"doctype": "Party Type", "name": "Member"},
+ {"doctype": "Party Type", "name": "Donor"},
+ ]
+ for record in custom_records:
+ frappe.delete_doc(record['doctype'], record['name'], ignore_missing=True)
+
custom_fields = {
'Member': ['pan_number'],
'Donor': ['pan_number'],
From 274399978572b1f2e80fd2a1db2663efa544fcf7 Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Thu, 17 Feb 2022 15:59:12 +0530
Subject: [PATCH 062/447] fix: coupon code is applied even if
ignore_pricing_rule is enabled
---
erpnext/public/js/controllers/transaction.js | 20 +++++-----------
.../selling/page/point_of_sale/pos_payment.js | 23 +++++++++++++++++++
2 files changed, 29 insertions(+), 14 deletions(-)
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index aa3e2f30d7..136e1edb6b 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -2284,20 +2284,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
coupon_code() {
var me = this;
- if (this.frm.doc.coupon_code) {
- frappe.run_serially([
- () => this.frm.doc.ignore_pricing_rule=1,
- () => me.ignore_pricing_rule(),
- () => this.frm.doc.ignore_pricing_rule=0,
- () => me.apply_pricing_rule(),
- () => this.frm.save()
- ]);
- } else {
- frappe.run_serially([
- () => this.frm.doc.ignore_pricing_rule=1,
- () => me.ignore_pricing_rule()
- ]);
- }
+ frappe.run_serially([
+ () => this.frm.doc.ignore_pricing_rule=1,
+ () => me.ignore_pricing_rule(),
+ () => this.frm.doc.ignore_pricing_rule=0,
+ () => me.apply_pricing_rule()
+ ]);
}
};
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index b9b65591dc..9650bc88a4 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -169,6 +169,29 @@ erpnext.PointOfSale.Payment = class {
}
});
+ frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => {
+ if (!frm.doc.ignore_pricing_rule) {
+ if (frm.doc.coupon_code) {
+ frappe.run_serially([
+ () => frm.doc.ignore_pricing_rule=1,
+ () => frm.trigger('ignore_pricing_rule'),
+ () => frm.doc.ignore_pricing_rule=0,
+ () => frm.trigger('apply_pricing_rule'),
+ () => frm.save(),
+ () => this.update_totals_section(frm.doc)
+ ]);
+ } else {
+ frappe.run_serially([
+ () => frm.doc.ignore_pricing_rule=1,
+ () => frm.trigger('ignore_pricing_rule'),
+ () => frm.doc.ignore_pricing_rule=0,
+ () => frm.save(),
+ () => this.update_totals_section(frm.doc)
+ ]);
+ }
+ }
+ });
+
this.setup_listener_for_payments();
this.$payment_modes.on('click', '.shortcut', function() {
From 229db14b7e28d2ac0179052e7b792e06c5c9e22d Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 17 Feb 2022 15:22:36 +0530
Subject: [PATCH 063/447] ci: move some tasks to background
- wkhtml download
- asset building
---
.github/helper/install.sh | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index eab6d50e79..859146bbcd 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -40,10 +40,14 @@ if [ "$DB" == "postgres" ];then
echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres;
fi
-wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
-tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
-sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
-sudo chmod o+x /usr/local/bin/wkhtmltopdf
+
+install_whktml() {
+ wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
+ tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
+ sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
+ sudo chmod o+x /usr/local/bin/wkhtmltopdf
+}
+install_whktml &
cd ~/frappe-bench || exit
@@ -57,5 +61,5 @@ bench get-app erpnext "${GITHUB_WORKSPACE}"
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
bench start &> bench_run_logs.txt &
+CI=Yes bench build --app frappe &
bench --site test_site reinstall --yes
-bench build --app frappe
From 358734dceabbd503cbd2ae3d8401fda960140a80 Mon Sep 17 00:00:00 2001
From: ChillarAnand
Date: Thu, 17 Feb 2022 16:31:27 +0530
Subject: [PATCH 064/447] fix: Skip deletion if linked docs exists
---
erpnext/patches/v14_0/delete_non_profit_doctypes.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/erpnext/patches/v14_0/delete_non_profit_doctypes.py b/erpnext/patches/v14_0/delete_non_profit_doctypes.py
index 5b1cfefa4b..d53aecca92 100644
--- a/erpnext/patches/v14_0/delete_non_profit_doctypes.py
+++ b/erpnext/patches/v14_0/delete_non_profit_doctypes.py
@@ -39,7 +39,10 @@ def execute():
{"doctype": "Party Type", "name": "Donor"},
]
for record in custom_records:
- frappe.delete_doc(record['doctype'], record['name'], ignore_missing=True)
+ try:
+ frappe.delete_doc(record['doctype'], record['name'], ignore_missing=True)
+ except frappe.LinkExistsError:
+ pass
custom_fields = {
'Member': ['pan_number'],
From e2e998fbd9baa6015bc9c376dd5b6db7ae6cae49 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Thu, 17 Feb 2022 12:00:19 +0530
Subject: [PATCH 065/447] fix(Timesheet): convert time logs to datetime while
checking for overlap
---
.../projects/doctype/timesheet/timesheet.py | 39 ++++++++++++-------
1 file changed, 26 insertions(+), 13 deletions(-)
diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py
index dd0b5f90f4..fa0411e0f8 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.py
+++ b/erpnext/projects/doctype/timesheet/timesheet.py
@@ -7,7 +7,7 @@ import json
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import add_to_date, flt, getdate, time_diff_in_hours
+from frappe.utils import add_to_date, flt, get_datetime, getdate, time_diff_in_hours
from erpnext.controllers.queries import get_match_cond
from erpnext.hr.utils import validate_active_employee
@@ -145,7 +145,7 @@ class Timesheet(Document):
if not (data.from_time and data.hours):
return
- _to_time = add_to_date(data.from_time, hours=data.hours, as_datetime=True)
+ _to_time = get_datetime(add_to_date(data.from_time, hours=data.hours, as_datetime=True))
if data.to_time != _to_time:
data.to_time = _to_time
@@ -186,24 +186,37 @@ class Timesheet(Document):
and ts.docstatus < 2""".format(cond),
{
"val": value,
- "from_time": args.from_time,
- "to_time": args.to_time,
+ "from_time": get_datetime(args.from_time),
+ "to_time": get_datetime(args.to_time),
"name": args.name or "No Name",
"parent": args.parent or "No Name"
}, as_dict=True)
- # check internal overlap
- for time_log in self.time_logs:
- if not (time_log.from_time and time_log.to_time
- and args.from_time and args.to_time): continue
- if (fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and \
- args.idx != time_log.idx and ((args.from_time > time_log.from_time and args.from_time < time_log.to_time) or
- (args.to_time > time_log.from_time and args.to_time < time_log.to_time) or
- (args.from_time <= time_log.from_time and args.to_time >= time_log.to_time)):
- return self
+ if self.check_internal_overlap(fieldname, args):
+ return self
return existing[0] if existing else None
+ def check_internal_overlap(self, fieldname, args):
+ for time_log in self.time_logs:
+ if not (time_log.from_time and time_log.to_time
+ and args.from_time and args.to_time):
+ continue
+
+ from_time = get_datetime(time_log.from_time)
+ to_time = get_datetime(time_log.to_time)
+ args_from_time = get_datetime(args.from_time)
+ args_to_time = get_datetime(args.to_time)
+
+ if (fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and \
+ args.idx != time_log.idx and (
+ (args_from_time > from_time and args_from_time < to_time)
+ or (args_to_time > from_time and args_to_time < to_time)
+ or (args_from_time <= from_time and args_to_time >= to_time)
+ ):
+ return True
+ return False
+
def update_cost(self):
for data in self.time_logs:
if data.activity_type or data.is_billable:
From 3ec9acf8f7c8fd08e5709ac0f352728f6a9d6cfa Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Thu, 17 Feb 2022 14:39:17 +0530
Subject: [PATCH 066/447] fix: convert overlap raw query to frappe.qb
---
.../projects/doctype/timesheet/timesheet.py | 52 +++++++++++--------
1 file changed, 31 insertions(+), 21 deletions(-)
diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py
index fa0411e0f8..c43be8cbd8 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.py
+++ b/erpnext/projects/doctype/timesheet/timesheet.py
@@ -171,26 +171,35 @@ class Timesheet(Document):
.format(args.idx, self.name, existing.name), OverlapError)
def get_overlap_for(self, fieldname, args, value):
- cond = "ts.`{0}`".format(fieldname)
- if fieldname == 'workstation':
- cond = "tsd.`{0}`".format(fieldname)
+ timesheet = frappe.qb.DocType("Timesheet")
+ timelog = frappe.qb.DocType("Timesheet Detail")
- existing = frappe.db.sql("""select ts.name as name, tsd.from_time as from_time, tsd.to_time as to_time from
- `tabTimesheet Detail` tsd, `tabTimesheet` ts where {0}=%(val)s and tsd.parent = ts.name and
- (
- (%(from_time)s > tsd.from_time and %(from_time)s < tsd.to_time) or
- (%(to_time)s > tsd.from_time and %(to_time)s < tsd.to_time) or
- (%(from_time)s <= tsd.from_time and %(to_time)s >= tsd.to_time))
- and tsd.name!=%(name)s
- and ts.name!=%(parent)s
- and ts.docstatus < 2""".format(cond),
- {
- "val": value,
- "from_time": get_datetime(args.from_time),
- "to_time": get_datetime(args.to_time),
- "name": args.name or "No Name",
- "parent": args.parent or "No Name"
- }, as_dict=True)
+ from_time = get_datetime(args.from_time)
+ to_time = get_datetime(args.to_time)
+
+ query = (
+ frappe.qb.from_(timesheet)
+ .join(timelog)
+ .on(timelog.parent == timesheet.name)
+ .select(timesheet.name.as_('name'), timelog.from_time.as_('from_time'), timelog.to_time.as_('to_time'))
+ .where(
+ (timelog.name != (args.name or "No Name"))
+ & (timesheet.name != (args.parent or "No Name"))
+ & (timesheet.docstatus < 2)
+ & (
+ ((from_time > timelog.from_time) & (from_time < timelog.to_time))
+ | ((to_time > timelog.from_time) & (to_time < timelog.to_time))
+ | ((from_time <= timelog.from_time) & (to_time >= timelog.to_time))
+ )
+ )
+ )
+
+ if fieldname == "workstation":
+ query = query.where(timelog[fieldname] == value)
+ else:
+ query = query.where(timesheet[fieldname] == value)
+
+ existing = query.run(as_dict=True)
if self.check_internal_overlap(fieldname, args):
return self
@@ -208,12 +217,13 @@ class Timesheet(Document):
args_from_time = get_datetime(args.from_time)
args_to_time = get_datetime(args.to_time)
- if (fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and \
+ if ((fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and
args.idx != time_log.idx and (
(args_from_time > from_time and args_from_time < to_time)
or (args_to_time > from_time and args_to_time < to_time)
or (args_from_time <= from_time and args_to_time >= to_time)
- ):
+ )
+ ):
return True
return False
From 47ff968253ff7c4e7ca4e7769ccc29d93a8f71f2 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Thu, 17 Feb 2022 14:39:26 +0530
Subject: [PATCH 067/447] test: timesheet not overlapping with continuous
timelogs
---
.../doctype/timesheet/test_timesheet.py | 29 +++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py
index 989bcd1670..8b60357021 100644
--- a/erpnext/projects/doctype/timesheet/test_timesheet.py
+++ b/erpnext/projects/doctype/timesheet/test_timesheet.py
@@ -151,6 +151,35 @@ class TestTimesheet(unittest.TestCase):
settings.ignore_employee_time_overlap = initial_setting
settings.save()
+ def test_timesheet_not_overlapping_with_continuous_timelogs(self):
+ emp = make_employee("test_employee_6@salary.com")
+
+ update_activity_type("_Test Activity Type")
+ timesheet = frappe.new_doc("Timesheet")
+ timesheet.employee = emp
+ timesheet.append(
+ 'time_logs',
+ {
+ "billable": 1,
+ "activity_type": "_Test Activity Type",
+ "from_time": now_datetime(),
+ "to_time": now_datetime() + datetime.timedelta(hours=3),
+ "company": "_Test Company"
+ }
+ )
+ timesheet.append(
+ 'time_logs',
+ {
+ "billable": 1,
+ "activity_type": "_Test Activity Type",
+ "from_time": now_datetime() + datetime.timedelta(hours=3),
+ "to_time": now_datetime() + datetime.timedelta(hours=4),
+ "company": "_Test Company"
+ }
+ )
+
+ timesheet.save() # should not throw an error
+
def test_to_time(self):
emp = make_employee("test_employee_6@salary.com")
from_time = now_datetime()
From bef46e2b645f17eca8c1cd6ebe74e2845f6ea64f Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Thu, 17 Feb 2022 16:59:14 +0530
Subject: [PATCH 068/447] chore: remove unused code and fields related to
workstation from Timesheet Detail
---
.../projects/doctype/timesheet/timesheet.py | 22 +++------
.../timesheet_detail/timesheet_detail.json | 48 ++-----------------
2 files changed, 10 insertions(+), 60 deletions(-)
diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py
index c43be8cbd8..b44d501743 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.py
+++ b/erpnext/projects/doctype/timesheet/timesheet.py
@@ -177,7 +177,7 @@ class Timesheet(Document):
from_time = get_datetime(args.from_time)
to_time = get_datetime(args.to_time)
- query = (
+ existing = (
frappe.qb.from_(timesheet)
.join(timelog)
.on(timelog.parent == timesheet.name)
@@ -186,20 +186,14 @@ class Timesheet(Document):
(timelog.name != (args.name or "No Name"))
& (timesheet.name != (args.parent or "No Name"))
& (timesheet.docstatus < 2)
+ & (timesheet[fieldname] == value)
& (
((from_time > timelog.from_time) & (from_time < timelog.to_time))
| ((to_time > timelog.from_time) & (to_time < timelog.to_time))
| ((from_time <= timelog.from_time) & (to_time >= timelog.to_time))
)
)
- )
-
- if fieldname == "workstation":
- query = query.where(timelog[fieldname] == value)
- else:
- query = query.where(timesheet[fieldname] == value)
-
- existing = query.run(as_dict=True)
+ ).run(as_dict=True)
if self.check_internal_overlap(fieldname, args):
return self
@@ -217,12 +211,10 @@ class Timesheet(Document):
args_from_time = get_datetime(args.from_time)
args_to_time = get_datetime(args.to_time)
- if ((fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and
- args.idx != time_log.idx and (
- (args_from_time > from_time and args_from_time < to_time)
- or (args_to_time > from_time and args_to_time < to_time)
- or (args_from_time <= from_time and args_to_time >= to_time)
- )
+ if (args.get(fieldname) == time_log.get(fieldname)) and (args.idx != time_log.idx) and (
+ (args_from_time > from_time and args_from_time < to_time)
+ or (args_to_time > from_time and args_to_time < to_time)
+ or (args_from_time <= from_time and args_to_time >= to_time)
):
return True
return False
diff --git a/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json b/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json
index ee04c612c9..90fdb83331 100644
--- a/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json
+++ b/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json
@@ -14,12 +14,6 @@
"to_time",
"hours",
"completed",
- "section_break_7",
- "completed_qty",
- "workstation",
- "column_break_12",
- "operation",
- "operation_id",
"project_details",
"project",
"project_name",
@@ -83,43 +77,6 @@
"fieldtype": "Check",
"label": "Completed"
},
- {
- "fieldname": "section_break_7",
- "fieldtype": "Section Break"
- },
- {
- "depends_on": "eval:parent.work_order",
- "fieldname": "completed_qty",
- "fieldtype": "Float",
- "label": "Completed Qty"
- },
- {
- "depends_on": "eval:parent.work_order",
- "fieldname": "workstation",
- "fieldtype": "Link",
- "label": "Workstation",
- "options": "Workstation",
- "read_only": 1
- },
- {
- "fieldname": "column_break_12",
- "fieldtype": "Column Break"
- },
- {
- "depends_on": "eval:parent.work_order",
- "fieldname": "operation",
- "fieldtype": "Link",
- "label": "Operation",
- "options": "Operation",
- "read_only": 1
- },
- {
- "depends_on": "eval:parent.work_order",
- "fieldname": "operation_id",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Operation Id"
- },
{
"fieldname": "project_details",
"fieldtype": "Section Break"
@@ -267,7 +224,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-05-18 12:19:33.205940",
+ "modified": "2022-02-17 16:53:34.878798",
"modified_by": "Administrator",
"module": "Projects",
"name": "Timesheet Detail",
@@ -275,5 +232,6 @@
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
- "sort_order": "ASC"
+ "sort_order": "ASC",
+ "states": []
}
\ No newline at end of file
From 555b1335f65cca4f77c28294e153002a39e114a4 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 17 Feb 2022 19:15:30 +0530
Subject: [PATCH 069/447] feat: Bank Reconciliation for loan documents
---
.../bank_reconciliation_tool.py | 73 ++++++++++++++++++-
.../loan_disbursement/loan_disbursement.json | 47 ++++++++++--
.../loan_disbursement/loan_disbursement.py | 12 +--
.../loan_repayment/loan_repayment.json | 52 ++++++++++++-
.../doctype/loan_repayment/loan_repayment.py | 24 +++---
.../dialog_manager.js | 17 ++++-
6 files changed, 190 insertions(+), 35 deletions(-)
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index 4211bd0169..26078d6329 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -275,6 +275,10 @@ def check_matching(bank_account, company, transaction, document_types):
}
matching_vouchers = []
+
+ matching_vouchers.extend(get_loan_vouchers(bank_account, transaction,
+ document_types, filters))
+
for query in subquery:
matching_vouchers.extend(
frappe.db.sql(query, filters,)
@@ -311,6 +315,74 @@ def get_queries(bank_account, company, transaction, document_types):
return queries
+def get_loan_vouchers(bank_account, transaction, document_types, filters):
+ vouchers = []
+ amount_condition = True if "exact_match" in document_types else False
+
+ if transaction.withdrawal > 0 and "loan_disbursement" in document_types:
+ vouchers.append(get_ld_matching_query(bank_account, amount_condition, filters))
+
+ if transaction.deposit > 0 and "loan_repayment" in document_types:
+ vouchers.append(get_lr_matching_query(bank_account, amount_condition, filters))
+
+def get_ld_matching_query(bank_account, amount_condition, filters):
+ loan_disbursement = frappe.qb.DocType("Loan Disbursement")
+ query = frappe.qb.from_(loan_disbursement).select(
+ loan_disbursement.name,
+ loan_disbursement.disbursed_amount,
+ loan_disbursement.reference_number,
+ loan_disbursement.reference_date,
+ loan_disbursement.applicant_type,
+ loan_disbursement.disbursement_date
+ ).where(
+ loan_disbursement.docstatus == 1
+ ).where(
+ loan_disbursement.clearance_date.isnull()
+ ).where(
+ loan_disbursement.disbursement_account == bank_account
+ )
+
+ if amount_condition:
+ query.where(
+ loan_disbursement.disbursed_amount == filters.get('amount')
+ )
+ else:
+ query.where(
+ loan_disbursement.disbursed_amount <= filters.get('amount')
+ )
+
+ vouchers = query.run(as_dict=1)
+ return vouchers
+
+def get_lr_matching_query(bank_account, amount_condition, filters):
+ loan_repayment = frappe.qb.DocType("Loan Repayment")
+ query = frappe.qb.from_(loan_repayment).select(
+ loan_repayment.name,
+ loan_repayment.paid_amount,
+ loan_repayment.reference_number,
+ loan_repayment.reference_date,
+ loan_repayment.applicant_type,
+ loan_repayment.posting_date
+ ).where(
+ loan_repayment.docstatus == 1
+ ).where(
+ loan_repayment.clearance_date.isnull()
+ ).where(
+ loan_repayment.disbursement_account == bank_account
+ )
+
+ if amount_condition:
+ query.where(
+ loan_repayment.paid_amount == filters.get('amount')
+ )
+ else:
+ query.where(
+ loan_repayment.paid_amount <= filters.get('amount')
+ )
+
+ vouchers = query.run(as_dict=1)
+ return vouchers
+
def get_pe_matching_query(amount_condition, account_from_to, transaction):
# get matching payment entries query
if transaction.deposit > 0:
@@ -348,7 +420,6 @@ def get_je_matching_query(amount_condition, transaction):
# We have mapping at the bank level
# So one bank could have both types of bank accounts like asset and liability
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
- company_account = frappe.get_value("Bank Account", transaction.bank_account, "account")
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
return f"""
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
index 7811d56a75..50926d7726 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
@@ -14,11 +14,15 @@
"applicant",
"section_break_7",
"disbursement_date",
+ "clearance_date",
"column_break_8",
"disbursed_amount",
"accounting_dimensions_section",
"cost_center",
- "customer_details_section",
+ "accounting_details",
+ "disbursement_account",
+ "column_break_16",
+ "loan_account",
"bank_account",
"disbursement_references_section",
"reference_date",
@@ -106,11 +110,6 @@
"fieldtype": "Section Break",
"label": "Disbursement Details"
},
- {
- "fieldname": "customer_details_section",
- "fieldtype": "Section Break",
- "label": "Customer Details"
- },
{
"fetch_from": "against_loan.applicant_type",
"fieldname": "applicant_type",
@@ -149,15 +148,48 @@
"fieldname": "reference_number",
"fieldtype": "Data",
"label": "Reference Number"
+ },
+ {
+ "fieldname": "clearance_date",
+ "fieldtype": "Date",
+ "label": "Clearance Date",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "accounting_details",
+ "fieldtype": "Section Break",
+ "label": "Accounting Details"
+ },
+ {
+ "fetch_from": "against_loan.disbursement_account",
+ "fieldname": "disbursement_account",
+ "fieldtype": "Link",
+ "label": "Disbursement Account",
+ "options": "Account",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_16",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "against_loan.loan_account",
+ "fieldname": "loan_account",
+ "fieldtype": "Link",
+ "label": "Loan Account",
+ "options": "Account",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-04-19 18:09:32.175355",
+ "modified": "2022-02-17 18:23:44.157598",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Disbursement",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -194,5 +226,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
index df3aadfb18..54a03b92b5 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
@@ -42,9 +42,6 @@ class LoanDisbursement(AccountsController):
if not self.posting_date:
self.posting_date = self.disbursement_date or nowdate()
- if not self.bank_account and self.applicant_type == "Customer":
- self.bank_account = frappe.db.get_value("Customer", self.applicant, "default_bank_account")
-
def validate_disbursal_amount(self):
possible_disbursal_amount = get_disbursal_amount(self.against_loan)
@@ -117,12 +114,11 @@ class LoanDisbursement(AccountsController):
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
- loan_details = frappe.get_doc("Loan", self.against_loan)
gle_map.append(
self.get_gl_dict({
- "account": loan_details.loan_account,
- "against": loan_details.disbursement_account,
+ "account": self.loan_account,
+ "against": self.disbursement_account,
"debit": self.disbursed_amount,
"debit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan",
@@ -137,8 +133,8 @@ class LoanDisbursement(AccountsController):
gle_map.append(
self.get_gl_dict({
- "account": loan_details.disbursement_account,
- "against": loan_details.loan_account,
+ "account": self.disbursement_account,
+ "against": self.loan_account,
"credit": self.disbursed_amount,
"credit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan",
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
index 93ef217042..766602de86 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
@@ -1,7 +1,7 @@
{
"actions": [],
"autoname": "LM-REP-.####",
- "creation": "2019-09-03 14:44:39.977266",
+ "creation": "2022-01-25 10:30:02.767941",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
@@ -13,6 +13,7 @@
"column_break_3",
"company",
"posting_date",
+ "clearance_date",
"rate_of_interest",
"payroll_payable_account",
"is_term_loan",
@@ -37,7 +38,12 @@
"total_penalty_paid",
"total_interest_paid",
"repayment_details",
- "amended_from"
+ "amended_from",
+ "accounting_details_section",
+ "repayment_account",
+ "penalty_income_account",
+ "column_break_36",
+ "loan_account"
],
"fields": [
{
@@ -260,12 +266,52 @@
"fieldname": "repay_from_salary",
"fieldtype": "Check",
"label": "Repay From Salary"
+ },
+ {
+ "fieldname": "clearance_date",
+ "fieldtype": "Date",
+ "label": "Clearance Date",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "accounting_details_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Details"
+ },
+ {
+ "fetch_from": "against_loan.payment_account",
+ "fieldname": "repayment_account",
+ "fieldtype": "Link",
+ "label": "Repayment Account",
+ "options": "Account",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_36",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "against_loan.loan_account",
+ "fieldname": "loan_account",
+ "fieldtype": "Link",
+ "label": "Loan Account",
+ "options": "Account",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "against_loan.penalty_income_account",
+ "fieldname": "penalty_income_account",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Penalty Income Account",
+ "options": "Account"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-01-06 01:51:06.707782",
+ "modified": "2022-02-17 19:10:07.742298",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Repayment",
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index f3ed611255..67c2b1ee14 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -310,7 +310,6 @@ class LoanRepayment(AccountsController):
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
- loan_details = frappe.get_doc("Loan", self.against_loan)
if self.shortfall_amount and self.amount_paid > self.shortfall_amount:
remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(self.shortfall_amount,
@@ -323,13 +322,13 @@ class LoanRepayment(AccountsController):
if self.repay_from_salary:
payment_account = self.payroll_payable_account
else:
- payment_account = loan_details.payment_account
+ payment_account = self.payment_account
if self.total_penalty_paid:
gle_map.append(
self.get_gl_dict({
- "account": loan_details.loan_account,
- "against": loan_details.payment_account,
+ "account": self.loan_account,
+ "against": payment_account,
"debit": self.total_penalty_paid,
"debit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan",
@@ -344,8 +343,8 @@ class LoanRepayment(AccountsController):
gle_map.append(
self.get_gl_dict({
- "account": loan_details.penalty_income_account,
- "against": loan_details.loan_account,
+ "account": self.penalty_income_account,
+ "against": self.loan_account,
"credit": self.total_penalty_paid,
"credit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan",
@@ -359,8 +358,7 @@ class LoanRepayment(AccountsController):
gle_map.append(
self.get_gl_dict({
"account": payment_account,
- "against": loan_details.loan_account + ", " + loan_details.interest_income_account
- + ", " + loan_details.penalty_income_account,
+ "against": self.loan_account + ", " + self.penalty_income_account,
"debit": self.amount_paid,
"debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
@@ -368,16 +366,16 @@ class LoanRepayment(AccountsController):
"remarks": remarks,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date),
- "party_type": loan_details.applicant_type if self.repay_from_salary else '',
- "party": loan_details.applicant if self.repay_from_salary else ''
+ "party_type": self.applicant_type if self.repay_from_salary else '',
+ "party": self.applicant if self.repay_from_salary else ''
})
)
gle_map.append(
self.get_gl_dict({
- "account": loan_details.loan_account,
- "party_type": loan_details.applicant_type,
- "party": loan_details.applicant,
+ "account": self.loan_account,
+ "party_type": self.applicant_type,
+ "party": self.applicant,
"against": payment_account,
"credit": self.amount_paid,
"credit_in_account_currency": self.amount_paid,
diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
index ca73393c54..214a1be134 100644
--- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
+++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
@@ -181,6 +181,12 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
fieldname: "journal_entry",
onchange: () => this.update_options(),
},
+ {
+ fieldtype: "Check",
+ label: "Loan Repayment",
+ fieldname: "loan_repayment",
+ onchange: () => this.update_options(),
+ },
{
fieldname: "column_break_5",
fieldtype: "Column Break",
@@ -191,13 +197,18 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
fieldname: "sales_invoice",
onchange: () => this.update_options(),
},
-
{
fieldtype: "Check",
label: "Purchase Invoice",
fieldname: "purchase_invoice",
onchange: () => this.update_options(),
},
+ {
+ fieldtype: "Check",
+ label: "Show Only Exact Amount",
+ fieldname: "exact_match",
+ onchange: () => this.update_options(),
+ },
{
fieldname: "column_break_5",
fieldtype: "Column Break",
@@ -210,8 +221,8 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
},
{
fieldtype: "Check",
- label: "Show Only Exact Amount",
- fieldname: "exact_match",
+ label: "Loan Disbursement",
+ fieldname: "loan_disbursement",
onchange: () => this.update_options(),
},
{
From c36bd7e1a6fe48c5fff4765e843571a0d6560dd1 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 17 Feb 2022 19:25:00 +0530
Subject: [PATCH 070/447] fix: avoid creating bins without item-wh
Co-Authored-By: Shadrak Gurupnor <30501401+shadrak98@users.noreply.github.com>
Co-Authored-By: Saurabh
---
erpnext/controllers/accounts_controller.py | 3 ++-
erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py | 2 ++
erpnext/patches/v4_2/repost_reserved_qty.py | 8 +++++---
erpnext/patches/v4_2/update_requested_and_ordered_qty.py | 2 ++
4 files changed, 11 insertions(+), 4 deletions(-)
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 994b903b32..d05787fdfb 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1955,7 +1955,8 @@ def update_bin_on_delete(row, doctype):
qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse)
- update_bin_qty(row.item_code, row.warehouse, qty_dict)
+ if row.warehouse:
+ update_bin_qty(row.item_code, row.warehouse, qty_dict)
def validate_and_delete_children(parent, data):
deleted_children = []
diff --git a/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py b/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py
index 9b083cafb3..8dec9ff381 100644
--- a/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py
+++ b/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py
@@ -9,6 +9,8 @@ def execute():
FROM `tabBin`""",as_dict=1)
for entry in bin_details:
+ if not (entry.item_code and entry.warehouse):
+ continue
update_bin_qty(entry.get("item_code"), entry.get("warehouse"), {
"indented_qty": get_indented_qty(entry.get("item_code"), entry.get("warehouse"))
})
diff --git a/erpnext/patches/v4_2/repost_reserved_qty.py b/erpnext/patches/v4_2/repost_reserved_qty.py
index c2ca9be64a..ed4b19d07d 100644
--- a/erpnext/patches/v4_2/repost_reserved_qty.py
+++ b/erpnext/patches/v4_2/repost_reserved_qty.py
@@ -29,9 +29,11 @@ def execute():
""")
for item_code, warehouse in repost_for:
- update_bin_qty(item_code, warehouse, {
- "reserved_qty": get_reserved_qty(item_code, warehouse)
- })
+ if not (item_code and warehouse):
+ continue
+ update_bin_qty(item_code, warehouse, {
+ "reserved_qty": get_reserved_qty(item_code, warehouse)
+ })
frappe.db.sql("""delete from tabBin
where exists(
diff --git a/erpnext/patches/v4_2/update_requested_and_ordered_qty.py b/erpnext/patches/v4_2/update_requested_and_ordered_qty.py
index 42b0b04076..dd79410ba5 100644
--- a/erpnext/patches/v4_2/update_requested_and_ordered_qty.py
+++ b/erpnext/patches/v4_2/update_requested_and_ordered_qty.py
@@ -14,6 +14,8 @@ def execute():
union
select item_code, warehouse from `tabStock Ledger Entry`) a"""):
try:
+ if not (item_code and warehouse):
+ continue
count += 1
update_bin_qty(item_code, warehouse, {
"indented_qty": get_indented_qty(item_code, warehouse),
From 87b074ac0966ab26bf776c720fcb96b92a451d55 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 17 Feb 2022 22:01:00 +0530
Subject: [PATCH 071/447] fix: GSTIN filter for GSTR-1 report
---
erpnext/regional/report/gstr_1/gstr_1.js | 23 ++++++++++++++++++++---
erpnext/regional/report/gstr_1/gstr_1.py | 23 ++++++++++++++++++++++-
2 files changed, 42 insertions(+), 4 deletions(-)
diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js
index 4b98978f13..1766fdb2ec 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.js
+++ b/erpnext/regional/report/gstr_1/gstr_1.js
@@ -17,7 +17,7 @@ frappe.query_reports["GSTR-1"] = {
"fieldtype": "Link",
"options": "Address",
"get_query": function () {
- var company = frappe.query_report.get_filter_value('company');
+ let company = frappe.query_report.get_filter_value('company');
if (company) {
return {
"query": 'frappe.contacts.doctype.address.address.address_query',
@@ -26,6 +26,11 @@ frappe.query_reports["GSTR-1"] = {
}
}
},
+ {
+ "fieldname": "company_gstin",
+ "label": __("Company GSTIN"),
+ "fieldtype": "Select"
+ },
{
"fieldname": "from_date",
"label": __("From Date"),
@@ -60,10 +65,22 @@ frappe.query_reports["GSTR-1"] = {
}
],
onload: function (report) {
+ let filters = report.get_values();
+
+ frappe.call({
+ method: 'erpnext.regional.report.gstr_1.gstr_1.get_company_gstins',
+ args: {
+ company: filters.company
+ },
+ callback: function(r) {
+ console.log(r.message);
+ frappe.query_report.page.fields_dict.company_gstin.df.options = r.message;
+ frappe.query_report.page.fields_dict.company_gstin.refresh();
+ }
+ });
+
report.page.add_inner_button(__("Download as JSON"), function () {
- var filters = report.get_values();
-
frappe.call({
method: 'erpnext.regional.report.gstr_1.gstr_1.get_json',
args: {
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index ce2ffb4010..8fcb6bb444 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -253,7 +253,8 @@ class Gstr1Report(object):
for opts in (("company", " and company=%(company)s"),
("from_date", " and posting_date>=%(from_date)s"),
("to_date", " and posting_date<=%(to_date)s"),
- ("company_address", " and company_address=%(company_address)s")):
+ ("company_address", " and company_address=%(company_address)s"),
+ ("company_gstin", " and company_gstin=%(company_gstin)s")):
if self.filters.get(opts[0]):
conditions += opts[1]
@@ -1192,3 +1193,23 @@ def is_inter_state(invoice_detail):
return True
else:
return False
+
+
+@frappe.whitelist()
+def get_company_gstins(company):
+ address = frappe.qb.DocType("Address")
+ links = frappe.qb.DocType("Dynamic Link")
+
+ addresses = frappe.qb.from_(address).inner_join(links).on(
+ address.name == links.parent
+ ).select(
+ address.gstin
+ ).where(
+ links.link_doctype == 'Company'
+ ).where(
+ links.link_name == company
+ ).run(as_dict=1)
+
+ address_list = [''] + [d.gstin for d in addresses]
+
+ return address_list
\ No newline at end of file
From 1617e0d0e6d7f8f3cbffab4edaf388b5aa6db4b4 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 17 Feb 2022 22:52:53 +0530
Subject: [PATCH 072/447] fix: Remove reload doc
---
erpnext/patches/v14_0/update_opportunity_currency_fields.py | 3 ---
1 file changed, 3 deletions(-)
diff --git a/erpnext/patches/v14_0/update_opportunity_currency_fields.py b/erpnext/patches/v14_0/update_opportunity_currency_fields.py
index 82213fff6c..75049a6e8a 100644
--- a/erpnext/patches/v14_0/update_opportunity_currency_fields.py
+++ b/erpnext/patches/v14_0/update_opportunity_currency_fields.py
@@ -6,9 +6,6 @@ from erpnext.setup.utils import get_exchange_rate
def execute():
- frappe.reload_doc('crm', 'doctype', 'opportunity', force=True)
- frappe.reload_doc('crm', 'doctype', 'opportunity_item', force=True)
-
opportunities = frappe.db.get_list('Opportunity', filters={
'opportunity_amount': ['>', 0]
}, fields=['name', 'company', 'currency', 'opportunity_amount'])
From 3a966d4dbe3cd868bcb01d4951b236cad154605c Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 17 Feb 2022 23:18:07 +0530
Subject: [PATCH 073/447] fix: Move patch to post sync
---
erpnext/patches.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index d300340671..33366867f2 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -329,7 +329,6 @@ execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings'
erpnext.patches.v14_0.set_payroll_cost_centers
erpnext.patches.v13_0.agriculture_deprecation_warning
erpnext.patches.v13_0.hospitality_deprecation_warning
-erpnext.patches.v13_0.update_exchange_rate_settings
erpnext.patches.v13_0.update_asset_quantity_field
erpnext.patches.v13_0.delete_bank_reconciliation_detail
erpnext.patches.v13_0.enable_provisional_accounting
@@ -351,3 +350,4 @@ erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template
erpnext.patches.v13_0.shopping_cart_to_ecommerce
erpnext.patches.v13_0.update_disbursement_account
erpnext.patches.v13_0.update_reserved_qty_closed_wo
+erpnext.patches.v13_0.update_exchange_rate_settings
From d3fbbcfed39570fbad52a77b2533c2b72da8679f Mon Sep 17 00:00:00 2001
From: marination
Date: Thu, 17 Feb 2022 14:30:00 +0530
Subject: [PATCH 074/447] fix: Precision of available qty and negative stock in
transfer bucket
- Maintain only positive values in transfer bucket
- Use it to neutralize/add stock to fifo queue
---
.../stock/report/stock_ageing/stock_ageing.py | 41 ++++++++++++-------
1 file changed, 26 insertions(+), 15 deletions(-)
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index 9866e63fb5..60f9e959c8 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -28,6 +28,7 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li
"Returns ordered, formatted data with ranges."
_func = itemgetter(1)
data = []
+ precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
for item, item_dict in item_details.items():
earliest_age, latest_age = 0, 0
@@ -48,10 +49,13 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li
if filters.get("show_warehouse_wise_stock"):
row.append(details.warehouse)
- row.extend([item_dict.get("total_qty"), average_age,
+ row.extend([
+ flt(item_dict.get("total_qty"), precision),
+ average_age,
range1, range2, range3, above_range3,
earliest_age, latest_age,
- details.stock_uom])
+ details.stock_uom
+ ])
data.append(row)
@@ -288,13 +292,14 @@ class FIFOSlots:
transfer_data = self.transferred_item_details.get(transfer_key)
if transfer_data:
- # [Repack] inward/outward from same voucher, item & warehouse
+ # inward/outward from same voucher, item & warehouse
+ # eg: Repack with same item, Stock reco for batch item
# consume transfer data and add stock to fifo queue
self.__adjust_incoming_transfer_qty(transfer_data, fifo_queue, row)
else:
if not serial_nos:
- if fifo_queue and flt(fifo_queue[0][0]) < 0:
- # neutralize negative stock by adding positive stock
+ if fifo_queue and flt(fifo_queue[0][0]) <= 0:
+ # neutralize 0/negative stock by adding positive stock
fifo_queue[0][0] += flt(row.actual_qty)
fifo_queue[0][1] = row.posting_date
else:
@@ -325,7 +330,7 @@ class FIFOSlots:
elif not fifo_queue:
# negative stock, no balance but qty yet to consume
fifo_queue.append([-(qty_to_pop), row.posting_date])
- self.transferred_item_details[transfer_key].append([row.actual_qty, row.posting_date])
+ self.transferred_item_details[transfer_key].append([qty_to_pop, row.posting_date])
qty_to_pop = 0
else:
# qty to pop < slot qty, ample balance
@@ -337,22 +342,28 @@ class FIFOSlots:
def __adjust_incoming_transfer_qty(self, transfer_data: Dict, fifo_queue: List, row: Dict):
"Add previously removed stock back to FIFO Queue."
transfer_qty_to_pop = flt(row.actual_qty)
- first_bucket_qty = transfer_data[0][0]
- first_bucket_date = transfer_data[0][1]
+
+ def add_to_fifo_queue(slot):
+ if fifo_queue and flt(fifo_queue[0][0]) <= 0:
+ # neutralize 0/negative stock by adding positive stock
+ fifo_queue[0][0] += flt(slot[0])
+ fifo_queue[0][1] = slot[1]
+ else:
+ fifo_queue.append(slot)
while transfer_qty_to_pop:
- if transfer_data and 0 > first_bucket_qty <= transfer_qty_to_pop:
+ if transfer_data and 0 < transfer_data[0][0] <= transfer_qty_to_pop:
# bucket qty is not enough, consume whole
- transfer_qty_to_pop -= first_bucket_qty
- slot = transfer_data.pop(0)
- fifo_queue.append(slot)
+ transfer_qty_to_pop -= transfer_data[0][0]
+ add_to_fifo_queue(transfer_data.pop(0))
elif not transfer_data:
# transfer bucket is empty, extra incoming qty
- fifo_queue.append([transfer_qty_to_pop, row.posting_date])
+ add_to_fifo_queue([transfer_qty_to_pop, row.posting_date])
+ transfer_qty_to_pop = 0
else:
# ample bucket qty to consume
- first_bucket_qty -= transfer_qty_to_pop
- fifo_queue.append([transfer_qty_to_pop, first_bucket_date])
+ transfer_data[0][0] -= transfer_qty_to_pop
+ add_to_fifo_queue([transfer_qty_to_pop, transfer_data[0][1]])
transfer_qty_to_pop = 0
def __update_balances(self, row: Dict, key: Union[Tuple, str]):
From ed4a6c6cc63ca37a6033f9f87c35cd26aaa2cb43 Mon Sep 17 00:00:00 2001
From: marination
Date: Fri, 18 Feb 2022 18:52:42 +0530
Subject: [PATCH 075/447] fix: Range Qty precision
---
erpnext/stock/report/stock_ageing/stock_ageing.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index 60f9e959c8..97a740e184 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -12,6 +12,7 @@ from frappe.utils import cint, date_diff, flt
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Filters = frappe._dict
+precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
def execute(filters: Filters = None) -> Tuple:
to_date = filters["to_date"]
@@ -28,7 +29,6 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li
"Returns ordered, formatted data with ranges."
_func = itemgetter(1)
data = []
- precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
for item, item_dict in item_details.items():
earliest_age, latest_age = 0, 0
@@ -83,13 +83,13 @@ def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: D
qty = flt(item[0]) if not item_dict["has_serial_no"] else 1.0
if age <= filters.range1:
- range1 += qty
+ range1 = flt(range1 + qty, precision)
elif age <= filters.range2:
- range2 += qty
+ range2 = flt(range2 + qty, precision)
elif age <= filters.range3:
- range3 += qty
+ range3 = flt(range3 + qty, precision)
else:
- above_range3 += qty
+ above_range3 = flt(above_range3 + qty, precision)
return range1, range2, range3, above_range3
From d5be536740642d0bef9ea23151a41ce2657b9cd2 Mon Sep 17 00:00:00 2001
From: marination
Date: Fri, 18 Feb 2022 18:53:05 +0530
Subject: [PATCH 076/447] test: Negative Stock, over consumption & over
production with split rows, balance precision
---
.../report/stock_ageing/test_stock_ageing.py | 221 +++++++++++++++++-
1 file changed, 217 insertions(+), 4 deletions(-)
diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
index 3055332540..3fc357e8d4 100644
--- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
@@ -3,7 +3,7 @@
import frappe
-from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots
+from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, format_report_data
from erpnext.tests.utils import ERPNextTestCase
@@ -11,7 +11,8 @@ class TestStockAgeing(ERPNextTestCase):
def setUp(self) -> None:
self.filters = frappe._dict(
company="_Test Company",
- to_date="2021-12-10"
+ to_date="2021-12-10",
+ range1=30, range2=60, range3=90
)
def test_normal_inward_outward_queue(self):
@@ -289,7 +290,8 @@ class TestStockAgeing(ERPNextTestCase):
self.assertEqual(item_result["total_qty"], 500.0)
self.assertEqual(queue[0][0], 400.0)
- self.assertEqual(queue[1][0], 100.0)
+ self.assertEqual(queue[1][0], 50.0)
+ self.assertEqual(queue[2][0], 50.0)
# check if time buckets add up to balance qty
self.assertEqual(sum([i[0] for i in queue]), 500.0)
@@ -341,6 +343,63 @@ class TestStockAgeing(ERPNextTestCase):
# check if time buckets add up to balance qty
self.assertEqual(sum([i[0] for i in queue]), 450.0)
+ def test_repack_entry_same_item_overconsume_with_split_rows(self):
+ """
+ Over consume item and have less repacked item qty (same warehouse).
+ Ledger:
+ Item | Qty | Voucher
+ ------------------------
+ Item 1 | 20 | 001
+ Item 1 | -50 | 002 (repack)
+ Item 1 | -50 | 002 (repack)
+ Item 1 | 50 | 002 (repack)
+ """
+ sle = [
+ frappe._dict( # stock up item
+ name="Flask Item",
+ actual_qty=20, qty_after_transaction=20,
+ warehouse="WH 1",
+ posting_date="2021-12-03", voucher_type="Stock Entry",
+ voucher_no="001",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=(-50), qty_after_transaction=(-30),
+ warehouse="WH 1",
+ posting_date="2021-12-04", voucher_type="Stock Entry",
+ voucher_no="002",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=(-50), qty_after_transaction=(-80),
+ warehouse="WH 1",
+ posting_date="2021-12-04", voucher_type="Stock Entry",
+ voucher_no="002",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=50, qty_after_transaction=(-30),
+ warehouse="WH 1",
+ posting_date="2021-12-04", voucher_type="Stock Entry",
+ voucher_no="002",
+ has_serial_no=False, serial_no=None
+ ),
+ ]
+ fifo_slots = FIFOSlots(self.filters, sle)
+ slots = fifo_slots.generate()
+ item_result = slots["Flask Item"]
+ queue = item_result["fifo_queue"]
+
+ self.assertEqual(item_result["total_qty"], -30.0)
+ self.assertEqual(queue[0][0], -30.0)
+
+ # check transfer bucket
+ transfer_bucket = fifo_slots.transferred_item_details[('002', 'Flask Item', 'WH 1')]
+ self.assertEqual(transfer_bucket[0][0], 50)
+
def test_repack_entry_same_item_overproduce(self):
"""
Under consume item and have more repacked item qty (same warehouse).
@@ -385,10 +444,164 @@ class TestStockAgeing(ERPNextTestCase):
self.assertEqual(item_result["total_qty"], 550.0)
self.assertEqual(queue[0][0], 450.0)
- self.assertEqual(queue[1][0], 100.0)
+ self.assertEqual(queue[1][0], 50.0)
+ self.assertEqual(queue[2][0], 50.0)
# check if time buckets add up to balance qty
self.assertEqual(sum([i[0] for i in queue]), 550.0)
+ def test_repack_entry_same_item_overproduce_with_split_rows(self):
+ """
+ Over consume item and have less repacked item qty (same warehouse).
+ Ledger:
+ Item | Qty | Voucher
+ ------------------------
+ Item 1 | 20 | 001
+ Item 1 | -50 | 002 (repack)
+ Item 1 | 50 | 002 (repack)
+ Item 1 | 50 | 002 (repack)
+ """
+ sle = [
+ frappe._dict( # stock up item
+ name="Flask Item",
+ actual_qty=20, qty_after_transaction=20,
+ warehouse="WH 1",
+ posting_date="2021-12-03", voucher_type="Stock Entry",
+ voucher_no="001",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=(-50), qty_after_transaction=(-30),
+ warehouse="WH 1",
+ posting_date="2021-12-04", voucher_type="Stock Entry",
+ voucher_no="002",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=50, qty_after_transaction=20,
+ warehouse="WH 1",
+ posting_date="2021-12-04", voucher_type="Stock Entry",
+ voucher_no="002",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=50, qty_after_transaction=70,
+ warehouse="WH 1",
+ posting_date="2021-12-04", voucher_type="Stock Entry",
+ voucher_no="002",
+ has_serial_no=False, serial_no=None
+ ),
+ ]
+ fifo_slots = FIFOSlots(self.filters, sle)
+ slots = fifo_slots.generate()
+ item_result = slots["Flask Item"]
+ queue = item_result["fifo_queue"]
+
+ self.assertEqual(item_result["total_qty"], 70.0)
+ self.assertEqual(queue[0][0], 20.0)
+ self.assertEqual(queue[1][0], 50.0)
+
+ # check transfer bucket
+ transfer_bucket = fifo_slots.transferred_item_details[('002', 'Flask Item', 'WH 1')]
+ self.assertFalse(transfer_bucket)
+
+ def test_negative_stock_same_voucher(self):
+ """
+ Test negative stock scenario in transfer bucket via repack entry (same wh).
+ Ledger:
+ Item | Qty | Voucher
+ ------------------------
+ Item 1 | -50 | 001
+ Item 1 | -50 | 001
+ Item 1 | 30 | 001
+ Item 1 | 80 | 001
+ """
+ sle = [
+ frappe._dict( # stock up item
+ name="Flask Item",
+ actual_qty=(-50), qty_after_transaction=(-50),
+ warehouse="WH 1",
+ posting_date="2021-12-01", voucher_type="Stock Entry",
+ voucher_no="001",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict( # stock up item
+ name="Flask Item",
+ actual_qty=(-50), qty_after_transaction=(-100),
+ warehouse="WH 1",
+ posting_date="2021-12-01", voucher_type="Stock Entry",
+ voucher_no="001",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict( # stock up item
+ name="Flask Item",
+ actual_qty=30, qty_after_transaction=(-70),
+ warehouse="WH 1",
+ posting_date="2021-12-01", voucher_type="Stock Entry",
+ voucher_no="001",
+ has_serial_no=False, serial_no=None
+ ),
+ ]
+ fifo_slots = FIFOSlots(self.filters, sle)
+ slots = fifo_slots.generate()
+ item_result = slots["Flask Item"]
+
+ # check transfer bucket
+ transfer_bucket = fifo_slots.transferred_item_details[('001', 'Flask Item', 'WH 1')]
+ self.assertEqual(transfer_bucket[0][0], 20)
+ self.assertEqual(transfer_bucket[1][0], 50)
+ self.assertEqual(item_result["fifo_queue"][0][0], -70.0)
+
+ sle.append(frappe._dict(
+ name="Flask Item",
+ actual_qty=80, qty_after_transaction=10,
+ warehouse="WH 1",
+ posting_date="2021-12-01", voucher_type="Stock Entry",
+ voucher_no="001",
+ has_serial_no=False, serial_no=None
+ ))
+
+ fifo_slots = FIFOSlots(self.filters, sle)
+ slots = fifo_slots.generate()
+ item_result = slots["Flask Item"]
+
+ transfer_bucket = fifo_slots.transferred_item_details[('001', 'Flask Item', 'WH 1')]
+ self.assertFalse(transfer_bucket)
+ self.assertEqual(item_result["fifo_queue"][0][0], 10.0)
+
+ def test_precision(self):
+ "Test if final balance qty is rounded off correctly."
+ sle = [
+ frappe._dict( # stock up item
+ name="Flask Item",
+ actual_qty=0.3, qty_after_transaction=0.3,
+ warehouse="WH 1",
+ posting_date="2021-12-01", voucher_type="Stock Entry",
+ voucher_no="001",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict( # stock up item
+ name="Flask Item",
+ actual_qty=0.6, qty_after_transaction=0.9,
+ warehouse="WH 1",
+ posting_date="2021-12-01", voucher_type="Stock Entry",
+ voucher_no="001",
+ has_serial_no=False, serial_no=None
+ ),
+ ]
+
+ slots = FIFOSlots(self.filters, sle).generate()
+ report_data = format_report_data(self.filters, slots, self.filters["to_date"])
+ row = report_data[0] # first row in report
+ bal_qty = row[5]
+ range_qty_sum = sum([i for i in row[7:11]]) # get sum of range balance
+
+ # check if value of Available Qty column matches with range bucket post format
+ self.assertEqual(bal_qty, 0.9)
+ self.assertEqual(bal_qty, range_qty_sum)
+
def generate_item_and_item_wh_wise_slots(filters, sle):
"Return results with and without 'show_warehouse_wise_stock'"
item_wise_slots = FIFOSlots(filters, sle).generate()
From 5a2b571aa9e0f448d2030e1901dfb9ec3e547d46 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Fri, 18 Feb 2022 20:05:49 +0530
Subject: [PATCH 077/447] fix: Validate party account with company
---
.../accounts/doctype/payment_entry/payment_entry.py | 2 +-
.../sales_taxes_and_charges_template.py | 2 +-
erpnext/accounts/party.py | 5 ++++-
erpnext/controllers/accounts_controller.py | 12 +++++-------
4 files changed, 11 insertions(+), 10 deletions(-)
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 02a144d3e7..0d8f079d7a 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -1077,7 +1077,7 @@ def get_outstanding_reference_documents(args):
if d.voucher_type in ("Purchase Invoice"):
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
- # Get all SO / PO which are not fully billed or aginst which full advance not paid
+ # Get all SO / PO which are not fully billed or against which full advance not paid
orders_to_be_billed = []
if (args.get("party_type") != "Student"):
orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"),
diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
index b5909447dc..1d30934df9 100644
--- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
+++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
@@ -46,7 +46,7 @@ def valdiate_taxes_and_charges_template(doc):
for tax in doc.get("taxes"):
validate_taxes_and_charges(tax)
- validate_account_head(tax, doc)
+ validate_account_head(tax.idx, tax.account_head, doc.company)
validate_cost_center(tax, doc)
validate_inclusive_tax(tax, doc)
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index c13bc23c15..d6f6c5bcb6 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -307,7 +307,7 @@ def validate_party_gle_currency(party_type, party, company, party_account_curren
.format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency)
def validate_party_accounts(doc):
-
+ from erpnext.controllers.accounts_controller import validate_account_head
companies = []
for account in doc.get("accounts"):
@@ -330,6 +330,9 @@ def validate_party_accounts(doc):
if doc.default_currency != party_account_currency and doc.default_currency != company_default_currency:
frappe.throw(_("Billing currency must be equal to either default company's currency or party account currency"))
+ # validate if account is mapped for same company
+ validate_account_head(account.idx, account.account, account.company)
+
@frappe.whitelist()
def get_due_date(posting_date, party_type, party, company=None, bill_date=None):
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index d05787fdfb..7913a39329 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1566,13 +1566,12 @@ def validate_taxes_and_charges(tax):
tax.rate = None
-def validate_account_head(tax, doc):
- company = frappe.get_cached_value('Account',
- tax.account_head, 'company')
+def validate_account_head(idx, account, company):
+ account_company = frappe.get_cached_value('Account', account, 'company')
- if company != doc.company:
+ if account_company != company:
frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}')
- .format(tax.idx, frappe.bold(tax.account_head), frappe.bold(doc.company)), title=_('Invalid Account'))
+ .format(idx, frappe.bold(account), frappe.bold(company)), title=_('Invalid Account'))
def validate_cost_center(tax, doc):
@@ -1955,8 +1954,7 @@ def update_bin_on_delete(row, doctype):
qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse)
- if row.warehouse:
- update_bin_qty(row.item_code, row.warehouse, qty_dict)
+ update_bin_qty(row.item_code, row.warehouse, qty_dict)
def validate_and_delete_children(parent, data):
deleted_children = []
From 1aa12fb3f1bee18a8a58d11954acd8112e96261d Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Sat, 19 Feb 2022 19:19:32 +0530
Subject: [PATCH 078/447] fix: Ledger entries on LIA for term loans
---
.../loan_interest_accrual.py | 33 -------------------
1 file changed, 33 deletions(-)
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
index 0de073f85d..1c800a06da 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
@@ -74,39 +74,6 @@ class LoanInterestAccrual(AccountsController):
})
)
- if self.payable_principal_amount:
- gle_map.append(
- self.get_gl_dict({
- "account": self.loan_account,
- "party_type": self.applicant_type,
- "party": self.applicant,
- "against": self.interest_income_account,
- "debit": self.payable_principal_amount,
- "debit_in_account_currency": self.interest_amount,
- "against_voucher_type": "Loan",
- "against_voucher": self.loan,
- "remarks": _("Interest accrued from {0} to {1} against loan: {2}").format(
- self.last_accrual_date, self.posting_date, self.loan),
- "cost_center": erpnext.get_default_cost_center(self.company),
- "posting_date": self.posting_date
- })
- )
-
- gle_map.append(
- self.get_gl_dict({
- "account": self.interest_income_account,
- "against": self.loan_account,
- "credit": self.payable_principal_amount,
- "credit_in_account_currency": self.interest_amount,
- "against_voucher_type": "Loan",
- "against_voucher": self.loan,
- "remarks": ("Interest accrued from {0} to {1} against loan: {2}").format(
- self.last_accrual_date, self.posting_date, self.loan),
- "cost_center": erpnext.get_default_cost_center(self.company),
- "posting_date": self.posting_date
- })
- )
-
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj)
From a28ec89507fd42bf100b6a64c6bcdeef55f4b032 Mon Sep 17 00:00:00 2001
From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com>
Date: Sat, 19 Feb 2022 19:35:57 +0530
Subject: [PATCH 079/447] Update gstr_1.js
---
erpnext/regional/report/gstr_1/gstr_1.js | 1 -
1 file changed, 1 deletion(-)
diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js
index 1766fdb2ec..9999a6d167 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.js
+++ b/erpnext/regional/report/gstr_1/gstr_1.js
@@ -73,7 +73,6 @@ frappe.query_reports["GSTR-1"] = {
company: filters.company
},
callback: function(r) {
- console.log(r.message);
frappe.query_report.page.fields_dict.company_gstin.df.options = r.message;
frappe.query_report.page.fields_dict.company_gstin.refresh();
}
From d188fcc06698c32342873db8cec32884434c53bd Mon Sep 17 00:00:00 2001
From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com>
Date: Sat, 19 Feb 2022 19:36:43 +0530
Subject: [PATCH 080/447] chore: remove console statements
From fa38c291bd577b40f0d5007470108596d392f89b Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Mon, 21 Feb 2022 11:38:16 +0530
Subject: [PATCH 081/447] fix(pos): removal of coupon code
---
erpnext/selling/page/point_of_sale/pos_payment.js | 8 --------
1 file changed, 8 deletions(-)
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index 9650bc88a4..4d75e6ef1b 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -180,14 +180,6 @@ erpnext.PointOfSale.Payment = class {
() => frm.save(),
() => this.update_totals_section(frm.doc)
]);
- } else {
- frappe.run_serially([
- () => frm.doc.ignore_pricing_rule=1,
- () => frm.trigger('ignore_pricing_rule'),
- () => frm.doc.ignore_pricing_rule=0,
- () => frm.save(),
- () => this.update_totals_section(frm.doc)
- ]);
}
}
});
From a0bdcbd0cd551895af63955343f517051917c8eb Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Mon, 21 Feb 2022 11:44:00 +0530
Subject: [PATCH 082/447] fix: Add patch for account fields
---
erpnext/patches.txt | 1 +
.../v13_0/update_accounts_in_loan_docs.py | 37 +++++++++++++++++++
2 files changed, 38 insertions(+)
create mode 100644 erpnext/patches/v13_0/update_accounts_in_loan_docs.py
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index d104bc003c..b24bf0a7e0 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -352,3 +352,4 @@ erpnext.patches.v13_0.shopping_cart_to_ecommerce
erpnext.patches.v13_0.update_disbursement_account
erpnext.patches.v13_0.update_reserved_qty_closed_wo
erpnext.patches.v14_0.delete_amazon_mws_doctype
+erpnext.patches.v13_0.update_accounts_in_loan_docs
diff --git a/erpnext/patches/v13_0/update_accounts_in_loan_docs.py b/erpnext/patches/v13_0/update_accounts_in_loan_docs.py
new file mode 100644
index 0000000000..440f912be2
--- /dev/null
+++ b/erpnext/patches/v13_0/update_accounts_in_loan_docs.py
@@ -0,0 +1,37 @@
+import frappe
+
+
+def execute():
+ ld = frappe.qb.DocType("Loan Disbursement").as_("ld")
+ lr = frappe.qb.DocType("Loan Repayment").as_("lr")
+ loan = frappe.qb.DocType("Loan")
+
+ frappe.qb.update(
+ ld
+ ).inner_join(
+ loan
+ ).on(
+ loan.name == ld.against_loan
+ ).set(
+ ld.disbursement_account, loan.disbursement_account
+ ).set(
+ ld.loan_account, loan.loan_account
+ ).where(
+ ld.docstatus < 2
+ ).run()
+
+ frappe.qb.update(
+ lr
+ ).inner_join(
+ loan
+ ).on(
+ loan.name == lr.against_loan
+ ).set(
+ lr.payment_account, loan.payment_account
+ ).set(
+ lr.loan_account, loan.loan_account
+ ).set(
+ lr.penalty_income_account, loan.penalty_income_account
+ ).where(
+ lr.docstatus < 2
+ ).run()
From 295cbb0ff22b04c705148d727d96f70b836fee93 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Mon, 21 Feb 2022 11:45:23 +0530
Subject: [PATCH 083/447] fix: Update queries in Bank Reconciliation Tool
---
.../bank_reconciliation_tool.py | 57 ++++++++++++++++---
.../bank_transaction/bank_transaction.py | 13 ++++-
.../loan_repayment/loan_repayment.json | 6 +-
3 files changed, 63 insertions(+), 13 deletions(-)
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index 26078d6329..f3351ddcba 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -7,6 +7,7 @@ import json
import frappe
from frappe import _
from frappe.model.document import Document
+from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt
from erpnext import get_company_currency
@@ -320,14 +321,34 @@ def get_loan_vouchers(bank_account, transaction, document_types, filters):
amount_condition = True if "exact_match" in document_types else False
if transaction.withdrawal > 0 and "loan_disbursement" in document_types:
- vouchers.append(get_ld_matching_query(bank_account, amount_condition, filters))
+ vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters))
if transaction.deposit > 0 and "loan_repayment" in document_types:
- vouchers.append(get_lr_matching_query(bank_account, amount_condition, filters))
+ vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters))
+
+ return vouchers
def get_ld_matching_query(bank_account, amount_condition, filters):
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
+ matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
+ matching_party = loan_disbursement.applicant_type == filters.get("party_type") and \
+ loan_disbursement.applicant == filters.get("party")
+
+ rank = (
+ frappe.qb.terms.Case()
+ .when(matching_reference, 1)
+ .else_(0)
+ )
+
+ rank1 = (
+ frappe.qb.terms.Case()
+ .when(matching_party, 1)
+ .else_(0)
+ )
+
query = frappe.qb.from_(loan_disbursement).select(
+ rank + rank1 + 1,
+ ConstantColumn("Loan Disbursement").as_("doctype"),
loan_disbursement.name,
loan_disbursement.disbursed_amount,
loan_disbursement.reference_number,
@@ -351,14 +372,33 @@ def get_ld_matching_query(bank_account, amount_condition, filters):
loan_disbursement.disbursed_amount <= filters.get('amount')
)
- vouchers = query.run(as_dict=1)
+ vouchers = query.run(as_list=True)
+
return vouchers
def get_lr_matching_query(bank_account, amount_condition, filters):
loan_repayment = frappe.qb.DocType("Loan Repayment")
+ matching_reference = loan_repayment.reference_number == filters.get("reference_number")
+ matching_party = loan_repayment.applicant_type == filters.get("party_type") and \
+ loan_repayment.applicant == filters.get("party")
+
+ rank = (
+ frappe.qb.terms.Case()
+ .when(matching_reference, 1)
+ .else_(0)
+ )
+
+ rank1 = (
+ frappe.qb.terms.Case()
+ .when(matching_party, 1)
+ .else_(0)
+ )
+
query = frappe.qb.from_(loan_repayment).select(
+ rank + rank1 + 1,
+ ConstantColumn("Loan Repayment").as_("doctype"),
loan_repayment.name,
- loan_repayment.paid_amount,
+ loan_repayment.amount_paid,
loan_repayment.reference_number,
loan_repayment.reference_date,
loan_repayment.applicant_type,
@@ -368,19 +408,20 @@ def get_lr_matching_query(bank_account, amount_condition, filters):
).where(
loan_repayment.clearance_date.isnull()
).where(
- loan_repayment.disbursement_account == bank_account
+ loan_repayment.payment_account == bank_account
)
if amount_condition:
query.where(
- loan_repayment.paid_amount == filters.get('amount')
+ loan_repayment.amount_paid == filters.get('amount')
)
else:
query.where(
- loan_repayment.paid_amount <= filters.get('amount')
+ loan_repayment.amount_paid <= filters.get('amount')
)
- vouchers = query.run(as_dict=1)
+ vouchers = query.run()
+
return vouchers
def get_pe_matching_query(amount_condition, account_from_to, transaction):
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 51e1d6e9a0..da944fa4ce 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -49,7 +49,8 @@ class BankTransaction(StatusUpdater):
def clear_linked_payment_entries(self, for_cancel=False):
for payment_entry in self.payment_entries:
- if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]:
+ if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim", "Loan Repayment",
+ "Loan Disbursement"]:
self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
elif payment_entry.payment_document == "Sales Invoice":
@@ -104,6 +105,7 @@ def get_total_allocated_amount(payment_entry):
bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True)
def get_paid_amount(payment_entry, currency, bank_account):
+ print(payment_entry.payment_document, "#@#@#@")
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
paid_amount_field = "paid_amount"
@@ -116,11 +118,18 @@ def get_paid_amount(payment_entry, currency, bank_account):
payment_entry.payment_entry, paid_amount_field)
elif payment_entry.payment_document == "Journal Entry":
- return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account}, "sum(credit_in_account_currency)")
+ return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account},
+ "sum(credit_in_account_currency)")
elif payment_entry.payment_document == "Expense Claim":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed")
+ elif payment_entry.payment_document == "Loan Disbursement":
+ return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "disbursed_amount")
+
+ elif payment_entry.payment_document == "Loan Repayment":
+ return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "amount_paid")
+
else:
frappe.throw("Please reconcile {0}: {1} manually".format(payment_entry.payment_document, payment_entry.payment_entry))
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
index 766602de86..480e010b49 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
@@ -40,7 +40,7 @@
"repayment_details",
"amended_from",
"accounting_details_section",
- "repayment_account",
+ "payment_account",
"penalty_income_account",
"column_break_36",
"loan_account"
@@ -281,7 +281,7 @@
},
{
"fetch_from": "against_loan.payment_account",
- "fieldname": "repayment_account",
+ "fieldname": "payment_account",
"fieldtype": "Link",
"label": "Repayment Account",
"options": "Account",
@@ -311,7 +311,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-02-17 19:10:07.742298",
+ "modified": "2022-02-18 19:10:07.742298",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Repayment",
From 0b5e618e3ab206f7ae080f570a736a87fcbccf2d Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Mon, 21 Feb 2022 11:46:44 +0530
Subject: [PATCH 084/447] fix: Update bank reconciliation statement
---
.../bank_reconciliation_statement.py | 105 ++++++++++++++++--
1 file changed, 95 insertions(+), 10 deletions(-)
diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py
index 6c401fb8f3..b72d266977 100644
--- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py
+++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py
@@ -4,7 +4,12 @@
import frappe
from frappe import _
-from frappe.utils import flt, getdate, nowdate
+from frappe.query_builder.custom import ConstantColumn
+from frappe.query_builder.functions import Sum
+from frappe.utils import flt, getdate
+from pypika import CustomFunction
+
+from erpnext.accounts.utils import get_balance_on
def execute(filters=None):
@@ -18,7 +23,6 @@ def execute(filters=None):
data = get_entries(filters)
- from erpnext.accounts.utils import get_balance_on
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
total_debit, total_credit = 0,0
@@ -118,7 +122,21 @@ def get_columns():
]
def get_entries(filters):
- journal_entries = frappe.db.sql("""
+ journal_entries = get_journal_entries(filters)
+
+ payment_entries = get_payment_entries(filters)
+
+ loan_entries = get_loan_entries(filters)
+
+ pos_entries = []
+ if filters.include_pos_transactions:
+ pos_entries = get_pos_entries(filters)
+
+ return sorted(list(payment_entries)+list(journal_entries+list(pos_entries) + list(loan_entries)),
+ key=lambda k: getdate(k['posting_date']))
+
+def get_journal_entries(filters):
+ return frappe.db.sql("""
select "Journal Entry" as payment_document, jv.posting_date,
jv.name as payment_entry, jvd.debit_in_account_currency as debit,
jvd.credit_in_account_currency as credit, jvd.against_account,
@@ -130,7 +148,8 @@ def get_entries(filters):
and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s
and ifnull(jv.is_opening, 'No') = 'No'""", filters, as_dict=1)
- payment_entries = frappe.db.sql("""
+def get_payment_entries(filters):
+ return frappe.db.sql("""
select
"Payment Entry" as payment_document, name as payment_entry,
reference_no, reference_date as ref_date,
@@ -145,9 +164,8 @@ def get_entries(filters):
and ifnull(clearance_date, '4000-01-01') > %(report_date)s
""", filters, as_dict=1)
- pos_entries = []
- if filters.include_pos_transactions:
- pos_entries = frappe.db.sql("""
+def get_pos_entries(filters):
+ return frappe.db.sql("""
select
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
si.posting_date, si.debit_to as against_account, sip.clearance_date,
@@ -161,8 +179,42 @@ def get_entries(filters):
si.posting_date ASC, si.name DESC
""", filters, as_dict=1)
- return sorted(list(payment_entries)+list(journal_entries+list(pos_entries)),
- key=lambda k: k['posting_date'] or getdate(nowdate()))
+def get_loan_entries(filters):
+ loan_docs = []
+ for doctype in ["Loan Disbursement", "Loan Repayment"]:
+ loan_doc = frappe.qb.DocType(doctype)
+ ifnull = CustomFunction('IFNULL', ['value', 'default'])
+
+ if doctype == "Loan Disbursement":
+ amount_field = (loan_doc.disbursed_amount).as_("credit")
+ posting_date = (loan_doc.disbursement_date).as_("posting_date")
+ account = loan_doc.disbursement_account
+ else:
+ amount_field = (loan_doc.amount_paid).as_("debit")
+ posting_date = (loan_doc.posting_date).as_("posting_date")
+ account = loan_doc.payment_account
+
+ entries = frappe.qb.from_(loan_doc).select(
+ ConstantColumn(doctype).as_("payment_document"),
+ (loan_doc.name).as_("payment_entry"),
+ (loan_doc.reference_number).as_("reference_no"),
+ (loan_doc.reference_date).as_("ref_date"),
+ amount_field,
+ posting_date,
+ ).where(
+ loan_doc.docstatus == 1
+ ).where(
+ account == filters.get('account')
+ ).where(
+ posting_date <= getdate(filters.get('report_date'))
+ ).where(
+ ifnull(loan_doc.clearance_date, '4000-01-01') > getdate(filters.get('report_date'))
+ ).run(as_dict=1)
+
+ loan_docs.extend(entries)
+
+ return loan_docs
+
def get_amounts_not_reflected_in_system(filters):
je_amount = frappe.db.sql("""
@@ -182,7 +234,40 @@ def get_amounts_not_reflected_in_system(filters):
pe_amount = flt(pe_amount[0][0]) if pe_amount else 0.0
- return je_amount + pe_amount
+ loan_amount = get_loan_amount(filters)
+
+ return je_amount + pe_amount + loan_amount
+
+def get_loan_amount(filters):
+ total_amount = 0
+ for doctype in ["Loan Disbursement", "Loan Repayment"]:
+ loan_doc = frappe.qb.DocType(doctype)
+ ifnull = CustomFunction('IFNULL', ['value', 'default'])
+
+ if doctype == "Loan Disbursement":
+ amount_field = Sum(loan_doc.disbursed_amount)
+ posting_date = (loan_doc.disbursement_date).as_("posting_date")
+ account = loan_doc.disbursement_account
+ else:
+ amount_field = Sum(loan_doc.amount_paid)
+ posting_date = (loan_doc.posting_date).as_("posting_date")
+ account = loan_doc.payment_account
+
+ amount = frappe.qb.from_(loan_doc).select(
+ amount_field
+ ).where(
+ loan_doc.docstatus == 1
+ ).where(
+ account == filters.get('account')
+ ).where(
+ posting_date > getdate(filters.get('report_date'))
+ ).where(
+ ifnull(loan_doc.clearance_date, '4000-01-01') <= getdate(filters.get('report_date'))
+ ).run()[0][0]
+
+ total_amount += flt(amount)
+
+ return amount
def get_balance_row(label, amount, account_currency):
if amount > 0:
From c5808543c83ea43f62784331fb7c513543e454f0 Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Mon, 21 Feb 2022 12:41:08 +0530
Subject: [PATCH 085/447] fix(asset): no. of depreciation booked cannot be
equal to total no. of depreciations
---
erpnext/assets/doctype/asset/asset.py | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 6e87426ccb..ea473fa7bb 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -417,11 +417,12 @@ class Asset(AccountsController):
def validate_asset_finance_books(self, row):
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount")
- .format(row.idx))
+ .format(row.idx), title=_("Invalid Schedule"))
if not row.depreciation_start_date:
if not self.available_for_use_date:
- frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx))
+ frappe.throw(_("Row {0}: Depreciation Start Date is required")
+ .format(row.idx), title=_("Invalid Schedule"))
row.depreciation_start_date = get_last_day(self.available_for_use_date)
if not self.is_existing_asset:
@@ -439,8 +440,9 @@ class Asset(AccountsController):
else:
self.number_of_depreciations_booked = 0
- if cint(self.number_of_depreciations_booked) > cint(row.total_number_of_depreciations):
- frappe.throw(_("Number of Depreciations Booked cannot be greater than Total Number of Depreciations"))
+ if flt(row.total_number_of_depreciations) <= cint(self.number_of_depreciations_booked):
+ frappe.throw(_("Row {0}: Total Number of Depreciations cannot be less than or equal to Number of Depreciations Booked")
+ .format(row.idx), title=_("Invalid Schedule"))
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date):
frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date")
From 780694f6e2d686ca7d037556a52e097802814266 Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Mon, 21 Feb 2022 12:45:52 +0530
Subject: [PATCH 086/447] test: number_of_depr_booked = total_number_of_depr
---
erpnext/assets/doctype/asset/test_asset.py | 18 +++++++++++++++++-
1 file changed, 17 insertions(+), 1 deletion(-)
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index c08dc21a8f..ddbff89fc7 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -873,8 +873,9 @@ class TestDepreciationBasics(AssetSetup):
self.assertRaises(frappe.ValidationError, asset.save)
def test_number_of_depreciations(self):
- """Tests if an error is raised when number_of_depreciations_booked > total_number_of_depreciations."""
+ """Tests if an error is raised when number_of_depreciations_booked >= total_number_of_depreciations."""
+ # number_of_depreciations_booked > total_number_of_depreciations
asset = create_asset(
item_code = "Macbook Pro",
calculate_depreciation = 1,
@@ -889,6 +890,21 @@ class TestDepreciationBasics(AssetSetup):
self.assertRaises(frappe.ValidationError, asset.save)
+ # number_of_depreciations_booked = total_number_of_depreciations
+ asset_2 = create_asset(
+ item_code = "Macbook Pro",
+ calculate_depreciation = 1,
+ available_for_use_date = "2019-12-31",
+ total_number_of_depreciations = 5,
+ expected_value_after_useful_life = 10000,
+ depreciation_start_date = "2020-07-01",
+ opening_accumulated_depreciation = 10000,
+ number_of_depreciations_booked = 5,
+ do_not_save = 1
+ )
+
+ self.assertRaises(frappe.ValidationError, asset_2.save)
+
def test_depreciation_start_date_is_before_purchase_date(self):
asset = create_asset(
item_code = "Macbook Pro",
From a82cf7214e301a3f70513e308d1625a726a1beea Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Mon, 21 Feb 2022 13:58:56 +0530
Subject: [PATCH 087/447] fix: Total Credit amount in TDS Payable monthly
report
---
.../accounts/report/tds_payable_monthly/tds_payable_monthly.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
index 57f79748f0..e6cbff5d42 100644
--- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
+++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
@@ -43,7 +43,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
if entry.account in tds_accounts:
tds_deducted += (entry.credit - entry.debit)
- total_amount_credited += (entry.credit - entry.debit)
+ total_amount_credited += entry.credit
if tds_deducted:
row = {
From e952cce17d8931054575de2e430f6000ae80ef9f Mon Sep 17 00:00:00 2001
From: Marica
Date: Mon, 21 Feb 2022 14:22:14 +0530
Subject: [PATCH 088/447] chore: Show 'Produced Qty' field in Sales Order Item
(#29903)
---
.../selling/doctype/sales_order_item/sales_order_item.json | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
index 95f6c4e96d..080d517d13 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -83,8 +83,8 @@
"planned_qty",
"column_break_69",
"work_order_qty",
- "delivered_qty",
"produced_qty",
+ "delivered_qty",
"returned_qty",
"shopping_cart_section",
"additional_notes",
@@ -701,10 +701,8 @@
"width": "50px"
},
{
- "description": "For Production",
"fieldname": "produced_qty",
"fieldtype": "Float",
- "hidden": 1,
"label": "Produced Quantity",
"oldfieldname": "produced_qty",
"oldfieldtype": "Currency",
@@ -802,7 +800,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-10-05 12:27:25.014789",
+ "modified": "2022-02-21 13:55:08.883104",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",
@@ -811,5 +809,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
From 1f9ce92011b4bfff27efeb8bf8542c9b716b5251 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 21 Feb 2022 14:29:54 +0530
Subject: [PATCH 089/447] ci: moar backport labels
[skip ci]
---
.mergify.yml | 34 ++++++++++++++++++++++++++++++++--
1 file changed, 32 insertions(+), 2 deletions(-)
diff --git a/.mergify.yml b/.mergify.yml
index f3d04096cf..b7d1df4524 100644
--- a/.mergify.yml
+++ b/.mergify.yml
@@ -14,9 +14,39 @@ pull_request_rules:
close:
comment:
message: |
- @{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
+ @{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
+ - name: backport to develop
+ conditions:
+ - label="backport develop"
+ actions:
+ backport:
+ branches:
+ - develop
+ assignees:
+ - "{{ author }}"
+
+ - name: backport to version-14-hotfix
+ conditions:
+ - label="backport version-14-hotfix"
+ actions:
+ backport:
+ branches:
+ - version-14-hotfix
+ assignees:
+ - "{{ author }}"
+
+ - name: backport to version-14-pre-release
+ conditions:
+ - label="backport version-14-pre-release"
+ actions:
+ backport:
+ branches:
+ - version-14-pre-release
+ assignees:
+ - "{{ author }}"
+
- name: backport to version-13-hotfix
conditions:
- label="backport version-13-hotfix"
@@ -55,4 +85,4 @@ pull_request_rules:
branches:
- version-12-pre-release
assignees:
- - "{{ author }}"
\ No newline at end of file
+ - "{{ author }}"
From 3a5dbfab505866fb84d02ea61aecc7d4456fa251 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 21 Feb 2022 10:55:55 +0530
Subject: [PATCH 090/447] fix: make cashflow mapping template child doctype
---
.../cash_flow_mapping_template_details.json | 118 +++++-------------
1 file changed, 29 insertions(+), 89 deletions(-)
diff --git a/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json b/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json
index 22cf797fc3..a2487c5543 100644
--- a/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json
+++ b/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json
@@ -1,94 +1,34 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:mapping",
- "beta": 0,
- "creation": "2018-02-08 10:18:48.513608",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2022-02-11 11:25:05.336846",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "mapping"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "mapping",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Mapping",
- "length": 0,
- "no_copy": 0,
- "options": "Cash Flow Mapping",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "mapping",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Mapping",
+ "options": "Cash Flow Mapping",
+ "reqd": 1,
+ "unique": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-02-08 10:33:39.413930",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Cash Flow Mapping Template Details",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2022-02-21 03:34:57.902332",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Cash Flow Mapping Template Details",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
}
\ No newline at end of file
From e3ea431ef39074b77e9089b19bac4bffc1a54e6e Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 21 Feb 2022 10:56:14 +0530
Subject: [PATCH 091/447] test: test all form loads
---
erpnext/tests/test_zform_loads.py | 29 +++++++++++++++++++++++++++++
1 file changed, 29 insertions(+)
create mode 100644 erpnext/tests/test_zform_loads.py
diff --git a/erpnext/tests/test_zform_loads.py b/erpnext/tests/test_zform_loads.py
new file mode 100644
index 0000000000..8414acf7d8
--- /dev/null
+++ b/erpnext/tests/test_zform_loads.py
@@ -0,0 +1,29 @@
+""" dumb test to check all function calls on known form loads """
+
+import unittest
+
+import frappe
+from frappe.desk.form.load import getdoc
+
+
+class TestFormLoads(unittest.TestCase):
+
+ def test_load(self):
+ doctypes = frappe.get_all("DocType", {"istable": 0, "issingle": 0, "is_virtual": 0}, pluck="name")
+
+ for doctype in doctypes:
+ last_doc = frappe.db.get_value(doctype, {}, "name", order_by="modified desc")
+ if not last_doc:
+ continue
+ with self.subTest(msg=f"Loading {doctype} - {last_doc}", doctype=doctype, last_doc=last_doc):
+ try:
+ # reset previous response
+ frappe.response = frappe._dict({"docs":[]})
+ frappe.response.docinfo = None
+
+ getdoc(doctype, last_doc)
+ except Exception as e:
+ self.fail(f"Failed to load {doctype} - {last_doc}: {e}")
+
+ self.assertTrue(frappe.response.docs, msg=f"expected document in reponse, found: {frappe.response.docs}")
+ self.assertTrue(frappe.response.docinfo, msg=f"expected docinfo in reponse, found: {frappe.response.docinfo}")
From afc81351b7daa2c245f9ac96a42c54c302da1e8f Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 21 Feb 2022 12:49:06 +0530
Subject: [PATCH 092/447] test: only test erpnext doctypes
Co-authored-by: gavin
---
.../cash_flow_mapping_template_details.json | 2 +-
erpnext/tests/test_zform_loads.py | 3 ++-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json b/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json
index a2487c5543..02c6875fb3 100644
--- a/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json
+++ b/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json
@@ -1,6 +1,6 @@
{
"actions": [],
- "creation": "2022-02-11 11:25:05.336846",
+ "creation": "2018-02-08 10:18:48.513608",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
diff --git a/erpnext/tests/test_zform_loads.py b/erpnext/tests/test_zform_loads.py
index 8414acf7d8..b6fb636687 100644
--- a/erpnext/tests/test_zform_loads.py
+++ b/erpnext/tests/test_zform_loads.py
@@ -9,7 +9,8 @@ from frappe.desk.form.load import getdoc
class TestFormLoads(unittest.TestCase):
def test_load(self):
- doctypes = frappe.get_all("DocType", {"istable": 0, "issingle": 0, "is_virtual": 0}, pluck="name")
+ erpnext_modules = frappe.get_all("Module Def", filters={"app_name": "erpnext"}, pluck="name")
+ doctypes = frappe.get_all("DocType", {"istable": 0, "issingle": 0, "is_virtual": 0, "module": ("in", erpnext_modules)}, pluck="name")
for doctype in doctypes:
last_doc = frappe.db.get_value(doctype, {}, "name", order_by="modified desc")
From 28cc2dbb72fc3d716ffcb19b039dccd67c13eb33 Mon Sep 17 00:00:00 2001
From: marination
Date: Mon, 21 Feb 2022 16:14:40 +0530
Subject: [PATCH 093/447] fix: Block merging items if both have product bundles
---
erpnext/stock/doctype/item/item.py | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index b9e8b3f2f1..d984d6eb99 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -398,6 +398,7 @@ class Item(Document):
if merge:
self.validate_properties_before_merge(new_name)
+ self.validate_duplicate_product_bundles_before_merge(old_name, new_name)
self.validate_duplicate_website_item_before_merge(old_name, new_name)
def after_rename(self, old_name, new_name, merge):
@@ -462,6 +463,18 @@ class Item(Document):
msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])
frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
+ def validate_duplicate_product_bundles_before_merge(self, old_name, new_name):
+ "Block merge if both old and new items have product bundles."
+ bundle = frappe.get_value("Product Bundle",filters={"new_item_code": old_name})
+ if bundle:
+ bundle_link = get_link_to_form("Product Bundle", bundle)
+ old_name, new_name = frappe.bold(old_name), frappe.bold(new_name)
+
+ msg = _("Please delete Product Bundle {0}, before merging {1} into {2}").format(
+ bundle_link, old_name, new_name
+ )
+ frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
+
def validate_duplicate_website_item_before_merge(self, old_name, new_name):
"""
Block merge if both old and new items have website items against them.
@@ -479,8 +492,9 @@ class Item(Document):
old_web_item = [d.get("name") for d in web_items if d.get("item_code") == old_name][0]
web_item_link = get_link_to_form("Website Item", old_web_item)
+ old_name, new_name = frappe.bold(old_name), frappe.bold(new_name)
- msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} and {new_name}"
+ msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} into {new_name}"
frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError)
def set_last_purchase_rate(self, new_name):
From 530f9f70291758d51babd7ec4f52eefe1a899ef1 Mon Sep 17 00:00:00 2001
From: marination
Date: Mon, 21 Feb 2022 16:48:04 +0530
Subject: [PATCH 094/447] test: Item Merging with Product Bundles
---
erpnext/stock/doctype/item/test_item.py | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index fd4df42187..6f5f1ff786 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -15,6 +15,7 @@ from erpnext.controllers.item_variant import (
get_variant,
)
from erpnext.stock.doctype.item.item import (
+ DataValidationError,
InvalidBarcode,
StockExistsForTemplate,
get_item_attribute,
@@ -388,6 +389,25 @@ class TestItem(ERPNextTestCase):
self.assertTrue(frappe.db.get_value("Bin",
{"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"}))
+ def test_item_merging_with_product_bundle(self):
+ from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
+
+ create_item("Test Item Bundle Item 1", is_stock_item=False)
+ create_item("Test Item Bundle Item 2", is_stock_item=False)
+ create_item("Test Item inside Bundle")
+ bundle_items = ["Test Item inside Bundle"]
+
+ bundle1 = make_product_bundle("Test Item Bundle Item 1", bundle_items, qty=2)
+ make_product_bundle("Test Item Bundle Item 2", bundle_items, qty=2)
+
+ with self.assertRaises(DataValidationError):
+ frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True)
+
+ bundle1.delete()
+ frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True)
+
+ self.assertFalse(frappe.db.exists("Item", "Test Item Bundle Item 1"))
+
def test_uom_conversion_factor(self):
if frappe.db.exists('Item', 'Test Item UOM'):
frappe.delete_doc('Item', 'Test Item UOM')
From a4c6cb9f12f0ff931909a15b657b62a4bc85a20b Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Mon, 21 Feb 2022 17:08:25 +0530
Subject: [PATCH 095/447] fix: Remove print statements
---
erpnext/accounts/doctype/bank_transaction/bank_transaction.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index da944fa4ce..a476cab55f 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -105,7 +105,6 @@ def get_total_allocated_amount(payment_entry):
bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True)
def get_paid_amount(payment_entry, currency, bank_account):
- print(payment_entry.payment_document, "#@#@#@")
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
paid_amount_field = "paid_amount"
From 00e8565868e3bb8a1547abeedd2d158a9b7e5bf4 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 21 Feb 2022 17:41:23 +0530
Subject: [PATCH 096/447] fix: round off increments in numeric item variant
---
erpnext/stock/doctype/item/item.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index dfc09181ca..ffea9c2d6e 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -594,7 +594,7 @@ $.extend(erpnext.item, {
const increment = r.message.increment;
let values = [];
- for(var i = from; i <= to; i += increment) {
+ for(var i = from; i <= to; i = flt(i + increment, 6)) {
values.push(i);
}
attr_val_fields[d.attribute] = values;
From f4af75f60b7bb594df4f9a6e6d0cb1ad949dfa33 Mon Sep 17 00:00:00 2001
From: 18alantom <2.alan.tom@gmail.com>
Date: Tue, 15 Feb 2022 11:51:52 +0530
Subject: [PATCH 097/447] feat: batchwise valuation flag
This is required to avoid breaking behaviour in valuation
of old batches
---
erpnext/patches.txt | 1 +
.../patches/v14_0/update_batch_valuation_flag.py | 12 ++++++++++++
erpnext/stock/doctype/batch/batch.json | 16 +++++++++++++++-
3 files changed, 28 insertions(+), 1 deletion(-)
create mode 100644 erpnext/patches/v14_0/update_batch_valuation_flag.py
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index a93ceca437..52c29b22b9 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -353,3 +353,4 @@ erpnext.patches.v13_0.update_reserved_qty_closed_wo
erpnext.patches.v13_0.update_exchange_rate_settings
erpnext.patches.v14_0.delete_amazon_mws_doctype
erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
+erpnext.patches.v14_0.update_batch_valuation_flag
diff --git a/erpnext/patches/v14_0/update_batch_valuation_flag.py b/erpnext/patches/v14_0/update_batch_valuation_flag.py
new file mode 100644
index 0000000000..d9f08d8d97
--- /dev/null
+++ b/erpnext/patches/v14_0/update_batch_valuation_flag.py
@@ -0,0 +1,12 @@
+import frappe
+
+
+def execute():
+ """
+ - Don't use batchwise valuation for existing batches.
+ - Only batches created after this patch shoule use it.
+ """
+ frappe.db.sql("""
+ UPDATE `tabBatch`
+ SET use_batchwise_valuation=0
+ """)
diff --git a/erpnext/stock/doctype/batch/batch.json b/erpnext/stock/doctype/batch/batch.json
index fc4cf1dbdb..0d28ea0919 100644
--- a/erpnext/stock/doctype/batch/batch.json
+++ b/erpnext/stock/doctype/batch/batch.json
@@ -9,6 +9,8 @@
"field_order": [
"sb_disabled",
"disabled",
+ "column_break_24",
+ "use_batchwise_valuation",
"sb_batch",
"batch_id",
"item",
@@ -186,6 +188,18 @@
"fieldtype": "Float",
"label": "Produced Qty",
"read_only": 1
+ },
+ {
+ "fieldname": "column_break_24",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "1",
+ "fieldname": "use_batchwise_valuation",
+ "fieldtype": "Check",
+ "label": "Use Batch-wise Valuation",
+ "read_only": 1,
+ "set_only_once": 1
}
],
"icon": "fa fa-archive",
@@ -193,7 +207,7 @@
"image_field": "image",
"links": [],
"max_attachments": 5,
- "modified": "2021-07-08 16:22:01.343105",
+ "modified": "2021-10-11 13:38:12.806976",
"modified_by": "Administrator",
"module": "Stock",
"name": "Batch",
From ce0514c8db17d59f2f84b3f6c263cd7e5877a049 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 15 Feb 2022 11:41:41 +0530
Subject: [PATCH 098/447] feat: batch wise valuation rates
start with most used case: negative inventory isn't enabled
- simple addition of qty and value when new batch qty is added
- fetch outgoing rate from stock movement of specific batch
---
erpnext/stock/doctype/batch/test_batch.py | 46 ++++++++++++++++++++
erpnext/stock/stock_ledger.py | 52 +++++++++++++++++++++++
2 files changed, 98 insertions(+)
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index 0a663c2a18..e7d04db454 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -7,6 +7,7 @@ from frappe.utils import cint, flt
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.get_item_details import get_item_details
from erpnext.tests.utils import ERPNextTestCase
@@ -300,6 +301,51 @@ class TestBatch(ERPNextTestCase):
details = get_item_details(args)
self.assertEqual(details.get('price_list_rate'), 400)
+
+ def test_basic_batch_wise_valuation(self, batch_qty = 100):
+ item_code = "_TestBatchWiseVal"
+ warehouse = "_Test Warehouse - _TC"
+ self.make_batch_item(item_code)
+
+ rates = [42, 420]
+
+ batches = {}
+ for rate in rates:
+ se = make_stock_entry(item_code=item_code, qty=10, rate=rate, target=warehouse)
+ batches[se.items[0].batch_no] = rate
+
+ LOW, HIGH = list(batches.keys())
+
+ # consume things out of order
+ consumption_plan = [
+ (HIGH, 1),
+ (LOW, 2),
+ (HIGH, 2),
+ (HIGH, 4),
+ (LOW, 6),
+ ]
+
+ stock_value = sum(rates) * 10
+ qty_after_transaction = 20
+ for batch, qty in consumption_plan:
+ # consume out of order
+ se = make_stock_entry(item_code=item_code, source=warehouse, qty=qty, batch_no=batch)
+
+ sle = frappe.get_last_doc("Stock Ledger Entry", {"is_cancelled": 0, "voucher_no": se.name})
+
+ stock_value_difference = sle.actual_qty * batches[sle.batch_no]
+ self.assertAlmostEqual(sle.stock_value_difference, stock_value_difference)
+
+ stock_value += stock_value_difference
+ self.assertAlmostEqual(sle.stock_value, stock_value)
+
+ qty_after_transaction += sle.actual_qty
+ self.assertAlmostEqual(sle.qty_after_transaction, qty_after_transaction)
+ self.assertAlmostEqual(sle.valuation_rate, stock_value / qty_after_transaction)
+
+ self.assertEqual(sle.stock_queue, []) # queues don't apply on batched items
+
+
def create_batch(item_code, rate, create_item_price_for_batch):
pi = make_purchase_invoice(company="_Test Company",
warehouse= "Stores - _TC", cost_center = "Main - _TC", update_stock=1,
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 00ca81f2b4..c33cc12c2f 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -447,6 +447,8 @@ class update_entries_after(object):
self.wh_data.qty_after_transaction = sle.qty_after_transaction
self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
+ elif sle.batch_no and frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True):
+ self.update_batched_values(sle)
else:
if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no:
# assert
@@ -481,6 +483,7 @@ class update_entries_after(object):
if not self.args.get("sle_id"):
self.update_outgoing_rate_on_transaction(sle)
+
def validate_negative_stock(self, sle):
"""
validate negative stock for entries current datetime onwards
@@ -736,7 +739,22 @@ class update_entries_after(object):
if not self.wh_data.stock_queue:
self.wh_data.stock_queue.append([0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate])
+ def update_batched_values(self, sle):
+ incoming_rate = flt(sle.incoming_rate)
+ actual_qty = flt(sle.actual_qty)
+ self.wh_data.qty_after_transaction += actual_qty
+
+ if actual_qty > 0:
+ stock_value_difference = incoming_rate * actual_qty
+ self.wh_data.stock_value += stock_value_difference
+ else:
+ outgoing_rate = _get_batch_outgoing_rate(item_code=sle.item_code, warehouse=sle.warehouse, batch_no=sle.batch_no, posting_date=sle.posting_date, posting_time=sle.posting_time, creation=sle.creation)
+ stock_value_difference = outgoing_rate * actual_qty
+ self.wh_data.stock_value += stock_value_difference
+
+ if self.wh_data.qty_after_transaction:
+ self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
ref_item_dt = ""
@@ -897,6 +915,40 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
['item_code', 'warehouse', 'posting_date', 'posting_time', 'timestamp(posting_date, posting_time) as timestamp'],
as_dict=1)
+def _get_batch_outgoing_rate(item_code, warehouse, batch_no, posting_date, posting_time, creation):
+
+ batch_details = frappe.db.sql("""
+ select sum(stock_value_difference) as batch_value, sum(actual_qty) as batch_qty
+ from `tabStock Ledger Entry`
+ where
+ item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and batch_no = %(batch_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
+ )
+ )
+ """,
+ {
+ "item_code": item_code,
+ "warehouse": warehouse,
+ "batch_no": batch_no,
+ "posting_date": posting_date,
+ "posting_time": posting_time,
+ "creation": creation,
+ },
+ as_dict=True
+ )
+
+ if batch_details and batch_details[0].batch_qty:
+ return batch_details[0].batch_value / batch_details[0].batch_qty
+
+
+
def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True):
From 342d09a671c522031f73ba777950c70983cea31a Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Feb 2022 14:28:51 +0530
Subject: [PATCH 099/447] feat: get_valuation_rate batch wise
This function is used to show valuation rate on frontend and also as
fallback in case values aren't available. Add "batch_no" param to get
batch specific valuation rates.
Co-Authored-By: Alan Tom <2.alan.tom@gmail.com>
---
erpnext/controllers/buying_controller.py | 1 +
.../controllers/sales_and_purchase_return.py | 1 +
erpnext/controllers/selling_controller.py | 1 +
erpnext/public/js/controllers/transaction.js | 1 +
erpnext/stock/doctype/batch/test_batch.py | 39 +++++++++++++++++
.../stock/doctype/stock_entry/stock_entry.js | 3 ++
.../stock/doctype/stock_entry/stock_entry.py | 3 +-
erpnext/stock/stock_ledger.py | 43 +++++++++++++------
erpnext/stock/utils.py | 2 +-
9 files changed, 79 insertions(+), 15 deletions(-)
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index a181af7313..b831557200 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -249,6 +249,7 @@ class BuyingController(StockController, Subcontracting):
"posting_time": self.get('posting_time'),
"qty": -1 * flt(d.get('stock_qty')),
"serial_no": d.get('serial_no'),
+ "batch_no": d.get("batch_no"),
"company": self.company,
"voucher_type": self.doctype,
"voucher_no": self.name,
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index df3c5f10c1..8c3aab442b 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -420,6 +420,7 @@ def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None
"posting_time": sle.get('posting_time'),
"qty": sle.actual_qty,
"serial_no": sle.get('serial_no'),
+ "batch_no": sle.get("batch_no"),
"company": sle.company,
"voucher_type": sle.voucher_type,
"voucher_no": sle.voucher_no
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 31b2209399..e918cde7c4 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -394,6 +394,7 @@ class SellingController(StockController):
"posting_time": self.get('posting_time') or nowtime(),
"qty": qty if cint(self.get("is_return")) else (-1 * qty),
"serial_no": d.get('serial_no'),
+ "batch_no": d.get("batch_no"),
"company": self.company,
"voucher_type": self.doctype,
"voucher_no": self.name,
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 136e1edb6b..933ced0bd7 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -719,6 +719,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
'posting_time': posting_time,
'qty': item.qty * item.conversion_factor,
'serial_no': item.serial_no,
+ 'batch_no': item.batch_no,
'voucher_type': voucher_type,
'company': company,
'allow_zero_valuation_rate': item.allow_zero_valuation_rate
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index e7d04db454..73a48b3f13 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -8,7 +8,11 @@ from frappe.utils import cint, flt
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
+ create_stock_reconciliation,
+)
from erpnext.stock.get_item_details import get_item_details
+from erpnext.stock.stock_ledger import get_valuation_rate
from erpnext.tests.utils import ERPNextTestCase
@@ -345,6 +349,41 @@ class TestBatch(ERPNextTestCase):
self.assertEqual(sle.stock_queue, []) # queues don't apply on batched items
+ def test_moving_batch_valuation_rates(self):
+ item_code = "_TestBatchWiseVal"
+ warehouse = "_Test Warehouse - _TC"
+ self.make_batch_item(item_code)
+
+ def assertValuation(expected):
+ actual = get_valuation_rate(item_code, warehouse, "voucher_type", "voucher_no", batch_no=batch_no)
+ self.assertAlmostEqual(actual, expected)
+
+ se = make_stock_entry(item_code=item_code, qty=100, rate=10, target=warehouse)
+ batch_no = se.items[0].batch_no
+ assertValuation(10)
+
+ # consumption should never affect current valuation rate
+ make_stock_entry(item_code=item_code, qty=20, source=warehouse)
+ assertValuation(10)
+
+ make_stock_entry(item_code=item_code, qty=30, source=warehouse)
+ assertValuation(10)
+
+ # 50 * 10 = 500 current value, add more item with higher valuation
+ make_stock_entry(item_code=item_code, qty=50, rate=20, target=warehouse, batch_no=batch_no)
+ assertValuation(15)
+
+ # consuming again shouldn't do anything
+ make_stock_entry(item_code=item_code, qty=20, source=warehouse)
+ assertValuation(15)
+
+ # reset rate with stock reconiliation
+ create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=10, rate=25, batch_no=batch_no)
+ assertValuation(25)
+
+ make_stock_entry(item_code=item_code, qty=20, rate=20, target=warehouse, batch_no=batch_no)
+ assertValuation((20 * 20 + 10 * 25) / (10 + 20))
+
def create_batch(item_code, rate, create_item_price_for_batch):
pi = make_purchase_invoice(company="_Test Company",
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index c4b8131305..5c9da3a205 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -425,6 +425,7 @@ frappe.ui.form.on('Stock Entry', {
'posting_time' : frm.doc.posting_time,
'warehouse' : cstr(item.s_warehouse) || cstr(item.t_warehouse),
'serial_no' : item.serial_no,
+ 'batch_no' : item.batch_no,
'company' : frm.doc.company,
'qty' : item.s_warehouse ? -1*flt(item.transfer_qty) : flt(item.transfer_qty),
'voucher_type' : frm.doc.doctype,
@@ -457,6 +458,7 @@ frappe.ui.form.on('Stock Entry', {
'warehouse': cstr(child.s_warehouse) || cstr(child.t_warehouse),
'transfer_qty': child.transfer_qty,
'serial_no': child.serial_no,
+ 'batch_no': child.batch_no,
'qty': child.s_warehouse ? -1* child.transfer_qty : child.transfer_qty,
'posting_date': frm.doc.posting_date,
'posting_time': frm.doc.posting_time,
@@ -680,6 +682,7 @@ frappe.ui.form.on('Stock Entry Detail', {
'warehouse' : cstr(d.s_warehouse) || cstr(d.t_warehouse),
'transfer_qty' : d.transfer_qty,
'serial_no' : d.serial_no,
+ 'batch_no' : d.batch_no,
'bom_no' : d.bom_no,
'expense_account' : d.expense_account,
'cost_center' : d.cost_center,
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 9ba007a186..99cf4de5de 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -510,7 +510,7 @@ class StockEntry(StockController):
d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse,
self.doctype, self.name, d.allow_zero_valuation_rate,
currency=erpnext.get_company_currency(self.company), company=self.company,
- raise_error_if_no_rate=raise_error_if_no_rate)
+ raise_error_if_no_rate=raise_error_if_no_rate, batch_no=d.batch_no)
d.basic_rate = flt(d.basic_rate, d.precision("basic_rate"))
if d.is_process_loss:
@@ -541,6 +541,7 @@ class StockEntry(StockController):
"posting_time": self.posting_time,
"qty": item.s_warehouse and -1*flt(item.transfer_qty) or flt(item.transfer_qty),
"serial_no": item.serial_no,
+ "batch_no": item.batch_no,
"voucher_type": self.doctype,
"voucher_no": self.name,
"company": self.company,
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index c33cc12c2f..53bfed8722 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -634,7 +634,7 @@ class update_entries_after(object):
if not allow_zero_rate:
self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
- currency=erpnext.get_company_currency(sle.company), company=sle.company)
+ currency=erpnext.get_company_currency(sle.company), company=sle.company, batch_no=sle.batch_no)
def get_incoming_value_for_serial_nos(self, sle, serial_nos):
# get rate from serial nos within same company
@@ -702,7 +702,7 @@ class update_entries_after(object):
if not allow_zero_valuation_rate:
self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
- currency=erpnext.get_company_currency(sle.company), company=sle.company)
+ currency=erpnext.get_company_currency(sle.company), company=sle.company, batch_no=sle.batch_no)
def update_queue_values(self, sle):
incoming_rate = flt(sle.incoming_rate)
@@ -722,7 +722,7 @@ class update_entries_after(object):
if not allow_zero_valuation_rate:
return get_valuation_rate(sle.item_code, sle.warehouse,
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
- currency=erpnext.get_company_currency(sle.company), company=sle.company)
+ currency=erpnext.get_company_currency(sle.company), company=sle.company, batch_no=sle.batch_no)
else:
return 0.0
@@ -950,21 +950,38 @@ def _get_batch_outgoing_rate(item_code, warehouse, batch_no, posting_date, posti
def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
- allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True):
+ allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True, batch_no=None):
if not company:
company = frappe.get_cached_value("Warehouse", warehouse, "company")
+ last_valuation_rate = None
+
+ # Get moving average rate of a specific batch number
+ if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
+ last_valuation_rate = frappe.db.sql("""
+ select sum(stock_value_difference) / sum(actual_qty)
+ from `tabStock Ledger Entry`
+ where
+ item_code = %s
+ AND warehouse = %s
+ AND batch_no = %s
+ AND is_cancelled = 0
+ AND NOT (voucher_no = %s AND voucher_type = %s)
+ """,
+ (item_code, warehouse, batch_no, voucher_no, voucher_type))
+
# Get valuation rate from last sle for the same item and warehouse
- last_valuation_rate = frappe.db.sql("""select valuation_rate
- from `tabStock Ledger Entry` force index (item_warehouse)
- where
- item_code = %s
- AND warehouse = %s
- AND valuation_rate >= 0
- AND is_cancelled = 0
- AND NOT (voucher_no = %s AND voucher_type = %s)
- order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type))
+ if not last_valuation_rate or last_valuation_rate[0][0] is None:
+ last_valuation_rate = frappe.db.sql("""select valuation_rate
+ from `tabStock Ledger Entry` force index (item_warehouse)
+ where
+ item_code = %s
+ AND warehouse = %s
+ AND valuation_rate >= 0
+ AND is_cancelled = 0
+ AND NOT (voucher_no = %s AND voucher_type = %s)
+ order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type))
if not last_valuation_rate:
# Get valuation rate from last sle for the item against any warehouse
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 7263e39cc9..3be252e593 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -231,7 +231,7 @@ def get_incoming_rate(args, raise_error_if_no_rate=True):
in_rate = get_valuation_rate(args.get('item_code'), args.get('warehouse'),
args.get('voucher_type'), voucher_no, args.get('allow_zero_valuation'),
currency=erpnext.get_company_currency(args.get('company')), company=args.get('company'),
- raise_error_if_no_rate=raise_error_if_no_rate)
+ raise_error_if_no_rate=raise_error_if_no_rate, batch_no=args.get("batch_no"))
return flt(in_rate)
From ab926521bd0c9802666032cb3c32aa803655bde0 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Feb 2022 15:37:03 +0530
Subject: [PATCH 100/447] fix: correct incoming rate for batched items
---
erpnext/stock/stock_ledger.py | 5 ++---
erpnext/stock/utils.py | 22 ++++++++++++++++++----
2 files changed, 20 insertions(+), 7 deletions(-)
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 53bfed8722..4748ad4e46 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -749,7 +749,7 @@ class update_entries_after(object):
stock_value_difference = incoming_rate * actual_qty
self.wh_data.stock_value += stock_value_difference
else:
- outgoing_rate = _get_batch_outgoing_rate(item_code=sle.item_code, warehouse=sle.warehouse, batch_no=sle.batch_no, posting_date=sle.posting_date, posting_time=sle.posting_time, creation=sle.creation)
+ outgoing_rate = get_batch_incoming_rate(item_code=sle.item_code, warehouse=sle.warehouse, batch_no=sle.batch_no, posting_date=sle.posting_date, posting_time=sle.posting_time, creation=sle.creation)
stock_value_difference = outgoing_rate * actual_qty
self.wh_data.stock_value += stock_value_difference
@@ -915,7 +915,7 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
['item_code', 'warehouse', 'posting_date', 'posting_time', 'timestamp(posting_date, posting_time) as timestamp'],
as_dict=1)
-def _get_batch_outgoing_rate(item_code, warehouse, batch_no, posting_date, posting_time, creation):
+def get_batch_incoming_rate(item_code, warehouse, batch_no, posting_date, posting_time, creation=None):
batch_details = frappe.db.sql("""
select sum(stock_value_difference) as batch_value, sum(actual_qty) as batch_qty
@@ -948,7 +948,6 @@ def _get_batch_outgoing_rate(item_code, warehouse, batch_no, posting_date, posti
return batch_details[0].batch_value / batch_details[0].batch_qty
-
def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True, batch_no=None):
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 3be252e593..e2bd2f197d 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -209,13 +209,28 @@ def _create_bin(item_code, warehouse):
@frappe.whitelist()
def get_incoming_rate(args, raise_error_if_no_rate=True):
"""Get Incoming Rate based on valuation method"""
- from erpnext.stock.stock_ledger import get_previous_sle, get_valuation_rate
+ from erpnext.stock.stock_ledger import (
+ get_batch_incoming_rate,
+ get_previous_sle,
+ get_valuation_rate,
+ )
if isinstance(args, str):
args = json.loads(args)
- in_rate = 0
+ voucher_no = args.get('voucher_no') or args.get('name')
+
+ in_rate = None
if (args.get("serial_no") or "").strip():
in_rate = get_avg_purchase_rate(args.get("serial_no"))
+ elif args.get("batch_no") and \
+ frappe.db.get_value("Batch", args.get("batch_no"), "use_batchwise_valuation", cache=True):
+ in_rate = get_batch_incoming_rate(
+ item_code=args.get('item_code'),
+ warehouse=args.get('warehouse'),
+ batch_no=args.get("batch_no"),
+ posting_date=args.get("posting_date"),
+ posting_time=args.get("posting_time"),
+ )
else:
valuation_method = get_valuation_method(args.get("item_code"))
previous_sle = get_previous_sle(args)
@@ -226,8 +241,7 @@ def get_incoming_rate(args, raise_error_if_no_rate=True):
elif valuation_method == 'Moving Average':
in_rate = previous_sle.get('valuation_rate') or 0
- if not in_rate:
- voucher_no = args.get('voucher_no') or args.get('name')
+ if in_rate is None:
in_rate = get_valuation_rate(args.get('item_code'), args.get('warehouse'),
args.get('voucher_type'), voucher_no, args.get('allow_zero_valuation'),
currency=erpnext.get_company_currency(args.get('company')), company=args.get('company'),
From 102fff24c886b49d08776307d513d68ffd56e918 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Feb 2022 15:51:04 +0530
Subject: [PATCH 101/447] refactor: convert query to QB and make creation
optional
---
erpnext/stock/doctype/batch/test_batch.py | 4 +-
erpnext/stock/stock_ledger.py | 53 ++++++++++++-----------
2 files changed, 30 insertions(+), 27 deletions(-)
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index 73a48b3f13..6495b56e92 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -1,6 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
+import json
+
import frappe
from frappe.exceptions import ValidationError
from frappe.utils import cint, flt
@@ -347,7 +349,7 @@ class TestBatch(ERPNextTestCase):
self.assertAlmostEqual(sle.qty_after_transaction, qty_after_transaction)
self.assertAlmostEqual(sle.valuation_rate, stock_value / qty_after_transaction)
- self.assertEqual(sle.stock_queue, []) # queues don't apply on batched items
+ self.assertEqual(json.loads(sle.stock_queue), []) # queues don't apply on batched items
def test_moving_batch_valuation_rates(self):
item_code = "_TestBatchWiseVal"
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 4748ad4e46..cacec408ce 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -8,7 +8,9 @@ from typing import Optional
import frappe
from frappe import _
from frappe.model.meta import get_field_precision
+from frappe.query_builder.functions import Sum
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
+from pypika import CustomFunction
import erpnext
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
@@ -24,7 +26,6 @@ class NegativeStockError(frappe.ValidationError): pass
class SerialNoExistsInFutureTransaction(frappe.ValidationError):
pass
-_exceptions = frappe.local('stockledger_exceptions')
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
from erpnext.controllers.stock_controller import future_sle_exists
@@ -917,32 +918,32 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
def get_batch_incoming_rate(item_code, warehouse, batch_no, posting_date, posting_time, creation=None):
- batch_details = frappe.db.sql("""
- select sum(stock_value_difference) as batch_value, sum(actual_qty) as batch_qty
- from `tabStock Ledger Entry`
- where
- item_code = %(item_code)s
- and warehouse = %(warehouse)s
- and batch_no = %(batch_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
- )
+ Timestamp = CustomFunction('timestamp', ['date', 'time'])
+
+ sle = frappe.qb.DocType("Stock Ledger Entry")
+
+ timestamp_condition = (Timestamp(sle.posting_date, sle.posting_time) < Timestamp(posting_date, posting_time))
+ if creation:
+ timestamp_condition |= (
+ (Timestamp(sle.posting_date, sle.posting_time) == Timestamp(posting_date, posting_time))
+ & (sle.creation < creation)
)
- """,
- {
- "item_code": item_code,
- "warehouse": warehouse,
- "batch_no": batch_no,
- "posting_date": posting_date,
- "posting_time": posting_time,
- "creation": creation,
- },
- as_dict=True
- )
+
+ batch_details = (
+ frappe.qb
+ .from_(sle)
+ .select(
+ Sum(sle.stock_value_difference).as_("batch_value"),
+ Sum(sle.actual_qty).as_("batch_qty")
+ )
+ .where(
+ (sle.item_code == item_code)
+ & (sle.warehouse == warehouse)
+ & (sle.batch_no == batch_no)
+ & (sle.is_cancelled == 0)
+ )
+ .where(timestamp_condition)
+ ).run(as_dict=True)
if batch_details and batch_details[0].batch_qty:
return batch_details[0].batch_value / batch_details[0].batch_qty
From d130233ffc79d085b61bc1b63956d18c03de7a88 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Feb 2022 16:14:15 +0530
Subject: [PATCH 102/447] test: fix expected test failures
---
.../stock_reconciliation/test_stock_reconciliation.py | 11 ++++++-----
erpnext/stock/stock_ledger.py | 1 +
2 files changed, 7 insertions(+), 5 deletions(-)
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 86af0a0cf3..2ffe127d9a 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -200,7 +200,6 @@ class TestStockReconciliation(ERPNextTestCase):
def test_stock_reco_for_batch_item(self):
to_delete_records = []
- to_delete_serial_nos = []
# Add new serial nos
item_code = "Stock-Reco-batch-Item-1"
@@ -208,20 +207,22 @@ class TestStockReconciliation(ERPNextTestCase):
sr = create_stock_reconciliation(item_code=item_code,
warehouse = warehouse, qty=5, rate=200, do_not_submit=1)
- sr.save(ignore_permissions=True)
+ sr.save()
sr.submit()
- self.assertTrue(sr.items[0].batch_no)
+ batch_no = sr.items[0].batch_no
+ self.assertTrue(batch_no)
to_delete_records.append(sr.name)
sr1 = create_stock_reconciliation(item_code=item_code,
- warehouse = warehouse, qty=6, rate=300, batch_no=sr.items[0].batch_no)
+ warehouse = warehouse, qty=6, rate=300, batch_no=batch_no)
args = {
"item_code": item_code,
"warehouse": warehouse,
"posting_date": nowdate(),
"posting_time": nowtime(),
+ "batch_no": batch_no,
}
valuation_rate = get_incoming_rate(args)
@@ -230,7 +231,7 @@ class TestStockReconciliation(ERPNextTestCase):
sr2 = create_stock_reconciliation(item_code=item_code,
- warehouse = warehouse, qty=0, rate=0, batch_no=sr.items[0].batch_no)
+ warehouse = warehouse, qty=0, rate=0, batch_no=batch_no)
stock_value = get_stock_value_on(warehouse, nowdate(), item_code)
self.assertEqual(stock_value, 0)
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index cacec408ce..2dd26643f7 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -751,6 +751,7 @@ class update_entries_after(object):
self.wh_data.stock_value += stock_value_difference
else:
outgoing_rate = get_batch_incoming_rate(item_code=sle.item_code, warehouse=sle.warehouse, batch_no=sle.batch_no, posting_date=sle.posting_date, posting_time=sle.posting_time, creation=sle.creation)
+ # TODO: negative stock handling
stock_value_difference = outgoing_rate * actual_qty
self.wh_data.stock_value += stock_value_difference
From 312db429e4605d6d0ce47d1034662fdf0ec053b7 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Feb 2022 16:26:17 +0530
Subject: [PATCH 103/447] refactor: use qb for patching flag
---
erpnext/patches/v14_0/update_batch_valuation_flag.py | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/erpnext/patches/v14_0/update_batch_valuation_flag.py b/erpnext/patches/v14_0/update_batch_valuation_flag.py
index d9f08d8d97..55c8c48aa2 100644
--- a/erpnext/patches/v14_0/update_batch_valuation_flag.py
+++ b/erpnext/patches/v14_0/update_batch_valuation_flag.py
@@ -6,7 +6,6 @@ def execute():
- Don't use batchwise valuation for existing batches.
- Only batches created after this patch shoule use it.
"""
- frappe.db.sql("""
- UPDATE `tabBatch`
- SET use_batchwise_valuation=0
- """)
+
+ batch = frappe.qb.DocType("Batch")
+ frappe.qb.update(batch).set(batch.use_batchwise_valuation, 0).run()
From 683ef8a60397b728bd18e1a3c3c317e2f155793c Mon Sep 17 00:00:00 2001
From: 18alantom <2.alan.tom@gmail.com>
Date: Sat, 19 Feb 2022 16:19:30 +0530
Subject: [PATCH 104/447] test: more tests for batchwise valuation
Co-Authored-By: Ankush Menat
---
.../purchase_receipt/test_purchase_receipt.py | 1 +
.../test_stock_ledger_entry.py | 278 ++++++++++++++++++
2 files changed, 279 insertions(+)
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 5ab7929a2a..d481689c13 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -1540,6 +1540,7 @@ def make_purchase_receipt(**args):
"conversion_factor": args.conversion_factor or 1.0,
"stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0),
"serial_no": args.serial_no,
+ "batch_no": args.batch_no,
"stock_uom": args.stock_uom or "_Test UOM",
"uom": uom,
"cost_center": args.cost_center or frappe.get_cached_value('Company', pr.company, 'cost_center'),
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 a1030d5496..60fea9613a 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
@@ -1,6 +1,10 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
+import json
+from operator import itemgetter
+from uuid import uuid4
+
import frappe
from frappe.core.page.permission_manager.permission_manager import reset
from frappe.utils import add_days, today
@@ -349,6 +353,170 @@ class TestStockLedgerEntry(ERPNextTestCase):
frappe.set_user("Administrator")
user.remove_roles("Stock Manager")
+ def test_batchwise_item_valuation_moving_average(self):
+ suffix = get_unique_suffix()
+ item, warehouses, batches = setup_item_valuation_test(
+ valuation_method="Moving Average", suffix=suffix
+ )
+
+ # Incoming Entries for Stock Value check
+ pr_entry_list = [
+ (item, warehouses[0], batches[0], 1, 100),
+ (item, warehouses[0], batches[1], 1, 50),
+ (item, warehouses[0], batches[0], 1, 150),
+ (item, warehouses[0], batches[1], 1, 100),
+ ]
+ prs = create_purchase_receipt_entries_for_batchwise_item_valuation_test(pr_entry_list)
+ sle_details = fetch_sle_details_for_doc_list(prs, ['stock_value'])
+ sv_list = [d['stock_value'] for d in sle_details]
+ expected_sv = [100, 150, 300, 400]
+ self.assertEqual(expected_sv, sv_list, "Incorrect 'Stock Value' values")
+
+ # Outgoing Entries for Stock Value Difference check
+ dn_entry_list = [
+ (item, warehouses[0], batches[1], 1, 200),
+ (item, warehouses[0], batches[0], 1, 200),
+ (item, warehouses[0], batches[1], 1, 200),
+ (item, warehouses[0], batches[0], 1, 200)
+ ]
+ dns = create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list)
+ sle_details = fetch_sle_details_for_doc_list(dns, ['stock_value_difference'])
+ svd_list = [-1 * d['stock_value_difference'] for d in sle_details]
+ expected_incoming_rates = expected_abs_svd = [75, 125, 75, 125]
+
+ self.assertEqual(expected_abs_svd, svd_list, "Incorrect 'Stock Value Difference' values")
+ for dn, incoming_rate in zip(dns, expected_incoming_rates):
+ self.assertEqual(
+ dn.items[0].incoming_rate, incoming_rate,
+ "Incorrect 'Incoming Rate' values fetched for DN items"
+ )
+
+
+ def assertSLEs(self, doc, expected_sles):
+ """ Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line"""
+ sles = frappe.get_all("Stock Ledger Entry", fields=["*"],
+ filters={"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled":0},
+ order_by="timestamp(posting_date, posting_time), creation")
+
+ for exp_sle, act_sle in zip(expected_sles, sles):
+ for k, v in exp_sle.items():
+ self.assertEqual(v, act_sle[k], msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}")
+
+ def test_batchwise_item_valuation_stock_reco(self):
+ suffix = get_unique_suffix()
+ item, warehouses, batches = setup_item_valuation_test(
+ valuation_method="FIFO", suffix=suffix
+ )
+ state = {
+ "stock_value" : 0.0,
+ "qty": 0.0
+ }
+ def update_invariants(exp_sles):
+ for sle in exp_sles:
+ state["stock_value"] += sle["stock_value_difference"]
+ state["qty"] += sle["actual_qty"]
+ sle["stock_value"] = state["stock_value"]
+ sle["qty_after_transaction"] = state["qty"]
+
+ osr1 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=10, rate=100, batch_no=batches[1])
+ expected_sles = [
+ {"actual_qty": 10, "stock_value_difference": 1000},
+ ]
+ update_invariants(expected_sles)
+ self.assertSLEs(osr1, expected_sles)
+
+ osr2 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=13, rate=200, batch_no=batches[0])
+ expected_sles = [
+ {"actual_qty": 13, "stock_value_difference": 200*13},
+ ]
+ update_invariants(expected_sles)
+ self.assertSLEs(osr2, expected_sles)
+
+ sr1 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=5, rate=50, batch_no=batches[1])
+
+ expected_sles = [
+ {"actual_qty": -10, "stock_value_difference": -10 * 100},
+ {"actual_qty": 5, "stock_value_difference": 250}
+ ]
+ update_invariants(expected_sles)
+ self.assertSLEs(sr1, expected_sles)
+
+ sr2 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=20, rate=75, batch_no=batches[0])
+ expected_sles = [
+ {"actual_qty": -13, "stock_value_difference": -13 * 200},
+ {"actual_qty": 20, "stock_value_difference": 20 * 75}
+ ]
+ update_invariants(expected_sles)
+ self.assertSLEs(sr2, expected_sles)
+
+ def test_legacy_item_valuation_stock_entry(self):
+ suffix = get_unique_suffix()
+ columns = [
+ 'stock_value_difference',
+ 'stock_value',
+ 'actual_qty',
+ 'qty_after_transaction',
+ 'stock_queue',
+ ]
+ item, warehouses, batches = setup_item_valuation_test(
+ valuation_method="FIFO", suffix=suffix, use_batchwise_valuation=0
+ )
+
+ def check_sle_details_against_expected(sle_details, expected_sle_details, detail, columns):
+ for i, (sle_vals, ex_sle_vals) in enumerate(zip(sle_details, expected_sle_details)):
+ for col, sle_val, ex_sle_val in zip(columns, sle_vals, ex_sle_vals):
+ if col == 'stock_queue':
+ sle_val = get_stock_value_from_q(sle_val)
+ ex_sle_val = get_stock_value_from_q(ex_sle_val)
+ self.assertEqual(
+ sle_val, ex_sle_val,
+ f"Incorrect {col} value on transaction #: {i} in {detail}"
+ )
+
+ # List used to defer assertions to prevent commits cause of error skipped rollback
+ details_list = []
+
+
+ # Test Material Receipt Entries
+ se_entry_list_mr = [
+ (item, None, warehouses[0], batches[0], 1, 50, "2021-01-21"),
+ (item, None, warehouses[0], batches[1], 1, 100, "2021-01-23"),
+ ]
+ ses = create_stock_entry_entries_for_batchwise_item_valuation_test(
+ se_entry_list_mr, "Material Receipt"
+ )
+ sle_details = fetch_sle_details_for_doc_list(ses, columns=columns, as_dict=0)
+ expected_sle_details = [
+ (50.0, 50.0, 1.0, 1.0, '[[1.0, 50.0]]'),
+ (100.0, 150.0, 1.0, 2.0, '[[1.0, 50.0], [1.0, 100.0]]'),
+ ]
+ details_list.append((
+ sle_details, expected_sle_details,
+ "Material Receipt Entries", columns
+ ))
+
+
+ # Test Material Issue Entries
+ se_entry_list_mi = [
+ (item, warehouses[0], None, batches[1], 1, None, "2021-01-29"),
+ ]
+ ses = create_stock_entry_entries_for_batchwise_item_valuation_test(
+ se_entry_list_mi, "Material Issue"
+ )
+ sle_details = fetch_sle_details_for_doc_list(ses, columns=columns, as_dict=0)
+ expected_sle_details = [
+ (-50.0, 100.0, -1.0, 1.0, '[[1, 100.0]]')
+ ]
+ details_list.append((
+ sle_details, expected_sle_details,
+ "Material Issue Entries", columns
+ ))
+
+
+ # Run assertions
+ for details in details_list:
+ check_sle_details_against_expected(*details)
+
def create_repack_entry(**args):
args = frappe._dict(args)
@@ -412,3 +580,113 @@ def create_items():
make_item(d, properties=properties)
return items
+
+def setup_item_valuation_test(valuation_method, suffix, use_batchwise_valuation=1, batches_list=['X', 'Y']):
+ from erpnext.stock.doctype.batch.batch import make_batch
+ from erpnext.stock.doctype.item.test_item import make_item
+ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+
+ item = make_item(
+ f"IV - Test Item {valuation_method} {suffix}",
+ dict(valuation_method=valuation_method, has_batch_no=1)
+ )
+ warehouses = [create_warehouse(f"IV - Test Warehouse {i}") for i in ['J', 'K']]
+ batches = [f"IV - Test Batch {i} {valuation_method} {suffix}" for i in batches_list]
+
+ for i, batch_id in enumerate(batches):
+ if not frappe.db.exists("Batch", batch_id):
+ ubw = use_batchwise_valuation
+ if isinstance(use_batchwise_valuation, (list, tuple)):
+ ubw = use_batchwise_valuation[i]
+ make_batch(
+ frappe._dict(
+ batch_id=batch_id,
+ item=item.item_code,
+ use_batchwise_valuation=ubw
+ )
+ )
+
+ return item.item_code, warehouses, batches
+
+def create_purchase_receipt_entries_for_batchwise_item_valuation_test(pr_entry_list):
+ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+ prs = []
+
+ for item, warehouse, batch_no, qty, rate in pr_entry_list:
+ pr = make_purchase_receipt(item=item, warehouse=warehouse, qty=qty, rate=rate, batch_no=batch_no)
+ prs.append(pr)
+
+ return prs
+
+def create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list):
+ from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
+ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
+ dns = []
+ for item, warehouse, batch_no, qty, rate in dn_entry_list:
+ so = make_sales_order(
+ rate=rate,
+ qty=qty,
+ item=item,
+ warehouse=warehouse,
+ against_blanket_order=0
+ )
+
+ dn = make_delivery_note(so.name)
+ dn.items[0].batch_no = batch_no
+ dn.insert()
+ dn.submit()
+ dns.append(dn)
+ return dns
+
+def fetch_sle_details_for_doc_list(doc_list, columns, as_dict=1):
+ return frappe.db.sql(f"""
+ SELECT { ', '.join(columns)}
+ FROM `tabStock Ledger Entry`
+ WHERE
+ voucher_no IN %(voucher_nos)s
+ and docstatus = 1
+ ORDER BY timestamp(posting_date, posting_time) ASC, CREATION ASC
+ """, dict(
+ voucher_nos=[doc.name for doc in doc_list]
+ ), as_dict=as_dict)
+
+def get_stock_value_from_q(q):
+ return sum(r*q for r,q in json.loads(q))
+
+def create_stock_entry_entries_for_batchwise_item_valuation_test(se_entry_list, purpose):
+ ses = []
+ for item, source, target, batch, qty, rate, posting_date in se_entry_list:
+ args = dict(
+ item_code=item,
+ qty=qty,
+ company="_Test Company",
+ batch_no=batch,
+ posting_date=posting_date,
+ purpose=purpose
+ )
+
+ if purpose == "Material Receipt":
+ args.update(
+ dict(to_warehouse=target, rate=rate)
+ )
+
+ elif purpose == "Material Issue":
+ args.update(
+ dict(from_warehouse=source)
+ )
+
+ elif purpose == "Material Transfer":
+ args.update(
+ dict(from_warehouse=source, to_warehouse=target)
+ )
+
+ else:
+ raise ValueError(f"Invalid purpose: {purpose}")
+ ses.append(make_stock_entry(**args))
+
+ return ses
+
+def get_unique_suffix():
+ # Used to isolate valuation sensitive
+ # tests to prevent future tests from failing.
+ return str(uuid4())[:8].upper()
From 5718777a2b3018e07ea310e87e5a2ea26ff3eb1b Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Feb 2022 18:36:16 +0530
Subject: [PATCH 105/447] fix: consider batch_no when getting incoming rate
---
erpnext/controllers/buying_controller.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index b831557200..b740476481 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -279,7 +279,8 @@ class BuyingController(StockController, Subcontracting):
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * d.consumed_qty,
- "serial_no": d.serial_no
+ "serial_no": d.serial_no,
+ "batch_no": d.batch_no,
})
if rate > 0:
From 60b8bae85f00b6a6bf4a26c7604e28e0b075bb52 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Feb 2022 19:18:35 +0530
Subject: [PATCH 106/447] test: batch wise valuation for transfer and
intermediate
---
.../test_stock_ledger_entry.py | 99 ++++++++++++++++---
1 file changed, 86 insertions(+), 13 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 60fea9613a..c298b5a096 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
@@ -354,10 +354,7 @@ class TestStockLedgerEntry(ERPNextTestCase):
user.remove_roles("Stock Manager")
def test_batchwise_item_valuation_moving_average(self):
- suffix = get_unique_suffix()
- item, warehouses, batches = setup_item_valuation_test(
- valuation_method="Moving Average", suffix=suffix
- )
+ item, warehouses, batches = setup_item_valuation_test(valuation_method="Moving Average")
# Incoming Entries for Stock Value check
pr_entry_list = [
@@ -403,10 +400,7 @@ class TestStockLedgerEntry(ERPNextTestCase):
self.assertEqual(v, act_sle[k], msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}")
def test_batchwise_item_valuation_stock_reco(self):
- suffix = get_unique_suffix()
- item, warehouses, batches = setup_item_valuation_test(
- valuation_method="FIFO", suffix=suffix
- )
+ item, warehouses, batches = setup_item_valuation_test()
state = {
"stock_value" : 0.0,
"qty": 0.0
@@ -449,8 +443,86 @@ class TestStockLedgerEntry(ERPNextTestCase):
update_invariants(expected_sles)
self.assertSLEs(sr2, expected_sles)
+ def test_batch_wise_valuation_across_warehouse(self):
+ item_code, warehouses, batches = setup_item_valuation_test()
+ source = warehouses[0]
+ target = warehouses[1]
+
+ unrelated_batch = make_stock_entry(item_code=item_code, target=source, batch_no=batches[1],
+ qty=5, rate=10)
+ self.assertSLEs(unrelated_batch, [
+ {"actual_qty": 5, "stock_value_difference": 10 * 5},
+ ])
+
+ reciept = make_stock_entry(item_code=item_code, target=source, batch_no=batches[0], qty=5, rate=10)
+ self.assertSLEs(reciept, [
+ {"actual_qty": 5, "stock_value_difference": 10 * 5},
+ ])
+
+ transfer = make_stock_entry(item_code=item_code, source=source, target=target, batch_no=batches[0], qty=5)
+ self.assertSLEs(transfer, [
+ {"actual_qty": -5, "stock_value_difference": -10 * 5, "warehouse": source},
+ {"actual_qty": 5, "stock_value_difference": 10 * 5, "warehouse": target}
+ ])
+
+ backdated_receipt = make_stock_entry(item_code=item_code, target=source, batch_no=batches[0],
+ qty=5, rate=20, posting_date=add_days(today(), -1))
+ self.assertSLEs(backdated_receipt, [
+ {"actual_qty": 5, "stock_value_difference": 20 * 5},
+ ])
+
+ # check reposted average rate in *future* transfer
+ self.assertSLEs(transfer, [
+ {"actual_qty": -5, "stock_value_difference": -15 * 5, "warehouse": source, "stock_value": 15 * 5 + 10 * 5},
+ {"actual_qty": 5, "stock_value_difference": 15 * 5, "warehouse": target, "stock_value": 15 * 5}
+ ])
+
+ transfer_unrelated = make_stock_entry(item_code=item_code, source=source,
+ target=target, batch_no=batches[1], qty=5)
+ self.assertSLEs(transfer_unrelated, [
+ {"actual_qty": -5, "stock_value_difference": -10 * 5, "warehouse": source, "stock_value": 15 * 5},
+ {"actual_qty": 5, "stock_value_difference": 10 * 5, "warehouse": target, "stock_value": 15 * 5 + 10 * 5}
+ ])
+
+ def test_intermediate_average_batch_wise_valuation(self):
+ """ A batch has moving average up until posting time,
+ check if same is respected when backdated entry is inserted in middle"""
+ item_code, warehouses, batches = setup_item_valuation_test()
+ warehouse = warehouses[0]
+
+ batch = batches[0]
+
+ yesterday = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batch,
+ qty=1, rate=10, posting_date=add_days(today(), -1))
+ self.assertSLEs(yesterday, [
+ {"actual_qty": 1, "stock_value_difference": 10},
+ ])
+
+ tomorrow = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0],
+ qty=1, rate=30, posting_date=add_days(today(), 1))
+ self.assertSLEs(tomorrow, [
+ {"actual_qty": 1, "stock_value_difference": 30},
+ ])
+
+ create_today = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0],
+ qty=1, rate=20)
+ self.assertSLEs(create_today, [
+ {"actual_qty": 1, "stock_value_difference": 20},
+ ])
+
+ consume_today = make_stock_entry(item_code=item_code, source=warehouse, batch_no=batches[0],
+ qty=1)
+ self.assertSLEs(consume_today, [
+ {"actual_qty": -1, "stock_value_difference": -15},
+ ])
+
+ consume_tomorrow = make_stock_entry(item_code=item_code, source=warehouse, batch_no=batches[0],
+ qty=2, posting_date=add_days(today(), 2))
+ self.assertSLEs(consume_tomorrow, [
+ {"stock_value_difference": -(30 + 15), "stock_value": 0, "qty_after_transaction": 0},
+ ])
+
def test_legacy_item_valuation_stock_entry(self):
- suffix = get_unique_suffix()
columns = [
'stock_value_difference',
'stock_value',
@@ -458,9 +530,7 @@ class TestStockLedgerEntry(ERPNextTestCase):
'qty_after_transaction',
'stock_queue',
]
- item, warehouses, batches = setup_item_valuation_test(
- valuation_method="FIFO", suffix=suffix, use_batchwise_valuation=0
- )
+ item, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0)
def check_sle_details_against_expected(sle_details, expected_sle_details, detail, columns):
for i, (sle_vals, ex_sle_vals) in enumerate(zip(sle_details, expected_sle_details)):
@@ -581,11 +651,14 @@ def create_items():
return items
-def setup_item_valuation_test(valuation_method, suffix, use_batchwise_valuation=1, batches_list=['X', 'Y']):
+def setup_item_valuation_test(valuation_method="FIFO", suffix=None, use_batchwise_valuation=1, batches_list=['X', 'Y']):
from erpnext.stock.doctype.batch.batch import make_batch
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+ if not suffix:
+ suffix = get_unique_suffix()
+
item = make_item(
f"IV - Test Item {valuation_method} {suffix}",
dict(valuation_method=valuation_method, has_batch_no=1)
From c5bd34d2383982e99db825cef1b5ec8215ccabee Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Feb 2022 19:21:12 +0530
Subject: [PATCH 107/447] test: multi-batch stock entry
---
.../doctype/stock_entry/test_stock_entry.py | 46 +++++++++++++++++++
1 file changed, 46 insertions(+)
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 306f2c3e69..6c6513beff 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -1107,6 +1107,52 @@ class TestStockEntry(ERPNextTestCase):
posting_date='2021-09-02', # backdated consumption of 2nd batch
purpose='Material Issue')
+ def test_multi_batch_value_diff(self):
+ """ Test value difference on stock entry in case of multi-batch.
+ | Stock entry | batch | qty | rate | value diff on SE |
+ | --- | --- | --- | --- | --- |
+ | receipt | A | 1 | 10 | 30 |
+ | receipt | B | 1 | 20 | |
+ | issue | A | -1 | 10 | -30 (to assert after submit) |
+ | issue | B | -1 | 20 | |
+ """
+ from erpnext.stock.doctype.batch.test_batch import TestBatch
+
+ batch_nos = []
+
+ item_code = '_TestMultibatchFifo'
+ TestBatch.make_batch_item(item_code)
+ warehouse = '_Test Warehouse - _TC'
+ receipt = make_stock_entry(
+ item_code=item_code,
+ qty=1,
+ rate=10,
+ to_warehouse=warehouse,
+ purpose='Material Receipt',
+ do_not_save=True
+ )
+ receipt.append("items", frappe.copy_doc(receipt.items[0], ignore_no_copy=False).update({"basic_rate": 20}) )
+ receipt.save()
+ receipt.submit()
+ batch_nos.extend(row.batch_no for row in receipt.items)
+ self.assertEqual(receipt.value_difference, 30)
+
+ issue = make_stock_entry(
+ item_code=item_code,
+ qty=1,
+ from_warehouse=warehouse,
+ purpose='Material Issue',
+ do_not_save=True
+ )
+ issue.append("items", frappe.copy_doc(issue.items[0], ignore_no_copy=False))
+ for row, batch_no in zip(issue.items, batch_nos):
+ row.batch_no = batch_no
+ issue.save()
+ issue.submit()
+
+ issue.reload() # reload because reposting current voucher updates rate
+ self.assertEqual(issue.value_difference, -30)
+
def make_serialized_item(**args):
args = frappe._dict(args)
se = frappe.copy_doc(test_records[0])
From d7ca83ef0b42af42bca94e43c18c26cbf8e19ed3 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Feb 2022 19:35:33 +0530
Subject: [PATCH 108/447] refactor: code duplication for fallback rates
---
erpnext/stock/stock_ledger.py | 19 ++++++++++---------
1 file changed, 10 insertions(+), 9 deletions(-)
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 2dd26643f7..9339b3ea23 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -633,9 +633,7 @@ class update_entries_after(object):
if not self.wh_data.valuation_rate and sle.voucher_detail_no:
allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_rate:
- self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
- sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
- currency=erpnext.get_company_currency(sle.company), company=sle.company, batch_no=sle.batch_no)
+ self.wh_data.valuation_rate = self.get_fallback_rate(sle)
def get_incoming_value_for_serial_nos(self, sle, serial_nos):
# get rate from serial nos within same company
@@ -701,9 +699,7 @@ class update_entries_after(object):
if not self.wh_data.valuation_rate and sle.voucher_detail_no:
allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_valuation_rate:
- self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
- sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
- currency=erpnext.get_company_currency(sle.company), company=sle.company, batch_no=sle.batch_no)
+ self.wh_data.valuation_rate = self.get_fallback_rate(sle)
def update_queue_values(self, sle):
incoming_rate = flt(sle.incoming_rate)
@@ -721,9 +717,7 @@ class update_entries_after(object):
def rate_generator() -> float:
allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_valuation_rate:
- return get_valuation_rate(sle.item_code, sle.warehouse,
- sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
- currency=erpnext.get_company_currency(sle.company), company=sle.company, batch_no=sle.batch_no)
+ return self.get_fallback_rate(sle)
else:
return 0.0
@@ -771,6 +765,13 @@ class update_entries_after(object):
else:
return 0
+ def get_fallback_rate(self, sle) -> float:
+ """When exact incoming rate isn't available use any of other "average" rates as fallback.
+ This should only get used for negative stock."""
+ return get_valuation_rate(sle.item_code, sle.warehouse,
+ sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
+ currency=erpnext.get_company_currency(sle.company), company=sle.company, batch_no=sle.batch_no)
+
def get_sle_before_datetime(self, args):
"""get previous stock ledger entry before current time-bucket"""
sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
From aba7a7ce4e4dc1fb264023db0034df5e906b5571 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Feb 2022 19:36:28 +0530
Subject: [PATCH 109/447] fix: handle negative inventory inside a batch
---
erpnext/stock/stock_ledger.py | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 9339b3ea23..edbe755329 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -742,13 +742,17 @@ class update_entries_after(object):
if actual_qty > 0:
stock_value_difference = incoming_rate * actual_qty
- self.wh_data.stock_value += stock_value_difference
else:
outgoing_rate = get_batch_incoming_rate(item_code=sle.item_code, warehouse=sle.warehouse, batch_no=sle.batch_no, posting_date=sle.posting_date, posting_time=sle.posting_time, creation=sle.creation)
- # TODO: negative stock handling
+ if outgoing_rate is None:
+ # This can *only* happen if qty available for the batch is zero.
+ # in such case fall back various other rates.
+ # future entries will correct the overall accounting as each
+ # batch individually uses moving average rates.
+ outgoing_rate = self.get_fallback_rate(sle)
stock_value_difference = outgoing_rate * actual_qty
- self.wh_data.stock_value += stock_value_difference
+ self.wh_data.stock_value += stock_value_difference
if self.wh_data.qty_after_transaction:
self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
From b534fee2c7220390ed749d9ee87759663558a019 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Feb 2022 20:58:36 +0530
Subject: [PATCH 110/447] refactor: use queue difference instead of actual
values
---
erpnext/stock/stock_ledger.py | 19 ++++++++++++-------
erpnext/stock/tests/test_valuation.py | 12 ++++++------
erpnext/stock/valuation.py | 12 ++++++------
3 files changed, 24 insertions(+), 19 deletions(-)
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index edbe755329..677266ee0c 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -19,7 +19,7 @@ from erpnext.stock.utils import (
get_or_make_bin,
get_valuation_method,
)
-from erpnext.stock.valuation import FIFOValuation, LIFOValuation
+from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
class NegativeStockError(frappe.ValidationError): pass
@@ -465,7 +465,6 @@ class update_entries_after(object):
self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
else:
self.update_queue_values(sle)
- self.wh_data.qty_after_transaction += flt(sle.actual_qty)
# rounding as per precision
self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision)
@@ -706,11 +705,15 @@ class update_entries_after(object):
actual_qty = flt(sle.actual_qty)
outgoing_rate = flt(sle.outgoing_rate)
+ self.wh_data.qty_after_transaction = round_off_if_near_zero(self.wh_data.qty_after_transaction + actual_qty)
+
if self.valuation_method == "LIFO":
stock_queue = LIFOValuation(self.wh_data.stock_queue)
else:
stock_queue = FIFOValuation(self.wh_data.stock_queue)
+ _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
+
if actual_qty > 0:
stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
else:
@@ -723,17 +726,19 @@ class update_entries_after(object):
stock_queue.remove_stock(qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator)
- stock_qty, stock_value = stock_queue.get_total_stock_and_value()
+ _qty, stock_value = stock_queue.get_total_stock_and_value()
+
+ stock_value_difference = stock_value - prev_stock_value
self.wh_data.stock_queue = stock_queue.state
- self.wh_data.stock_value = stock_value
- if stock_qty:
- self.wh_data.valuation_rate = stock_value / stock_qty
-
+ self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference)
if not self.wh_data.stock_queue:
self.wh_data.stock_queue.append([0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate])
+ if self.wh_data.qty_after_transaction:
+ self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
+
def update_batched_values(self, sle):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
diff --git a/erpnext/stock/tests/test_valuation.py b/erpnext/stock/tests/test_valuation.py
index 648d4406ca..bdb768f1ad 100644
--- a/erpnext/stock/tests/test_valuation.py
+++ b/erpnext/stock/tests/test_valuation.py
@@ -7,7 +7,7 @@ from hypothesis import strategies as st
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
-from erpnext.stock.valuation import FIFOValuation, LIFOValuation, _round_off_if_near_zero
+from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
from erpnext.tests.utils import ERPNextTestCase
qty_gen = st.floats(min_value=-1e6, max_value=1e6)
@@ -113,11 +113,11 @@ class TestFIFOValuation(unittest.TestCase):
self.assertTotalQty(0)
def test_rounding_off_near_zero(self):
- self.assertEqual(_round_off_if_near_zero(0), 0)
- self.assertEqual(_round_off_if_near_zero(1), 1)
- self.assertEqual(_round_off_if_near_zero(-1), -1)
- self.assertEqual(_round_off_if_near_zero(-1e-8), 0)
- self.assertEqual(_round_off_if_near_zero(1e-8), 0)
+ self.assertEqual(round_off_if_near_zero(0), 0)
+ self.assertEqual(round_off_if_near_zero(1), 1)
+ self.assertEqual(round_off_if_near_zero(-1), -1)
+ self.assertEqual(round_off_if_near_zero(-1e-8), 0)
+ self.assertEqual(round_off_if_near_zero(1e-8), 0)
def test_totals(self):
self.queue.add_stock(1, 10)
diff --git a/erpnext/stock/valuation.py b/erpnext/stock/valuation.py
index ee9477ed74..e2bd1ad4df 100644
--- a/erpnext/stock/valuation.py
+++ b/erpnext/stock/valuation.py
@@ -34,7 +34,7 @@ class BinWiseValuation(ABC):
total_qty += flt(qty)
total_value += flt(qty) * flt(rate)
- return _round_off_if_near_zero(total_qty), _round_off_if_near_zero(total_value)
+ return round_off_if_near_zero(total_qty), round_off_if_near_zero(total_value)
def __repr__(self):
return str(self.state)
@@ -136,7 +136,7 @@ class FIFOValuation(BinWiseValuation):
fifo_bin = self.queue[index]
if qty >= fifo_bin[QTY]:
# consume current bin
- qty = _round_off_if_near_zero(qty - fifo_bin[QTY])
+ qty = round_off_if_near_zero(qty - fifo_bin[QTY])
to_consume = self.queue.pop(index)
consumed_bins.append(list(to_consume))
@@ -148,7 +148,7 @@ class FIFOValuation(BinWiseValuation):
break
else:
# qty found in current bin consume it and exit
- fifo_bin[QTY] = _round_off_if_near_zero(fifo_bin[QTY] - qty)
+ fifo_bin[QTY] = round_off_if_near_zero(fifo_bin[QTY] - qty)
consumed_bins.append([qty, fifo_bin[RATE]])
qty = 0
@@ -231,7 +231,7 @@ class LIFOValuation(BinWiseValuation):
stock_bin = self.stack[index]
if qty >= stock_bin[QTY]:
# consume current bin
- qty = _round_off_if_near_zero(qty - stock_bin[QTY])
+ qty = round_off_if_near_zero(qty - stock_bin[QTY])
to_consume = self.stack.pop(index)
consumed_bins.append(list(to_consume))
@@ -243,14 +243,14 @@ class LIFOValuation(BinWiseValuation):
break
else:
# qty found in current bin consume it and exit
- stock_bin[QTY] = _round_off_if_near_zero(stock_bin[QTY] - qty)
+ stock_bin[QTY] = round_off_if_near_zero(stock_bin[QTY] - qty)
consumed_bins.append([qty, stock_bin[RATE]])
qty = 0
return consumed_bins
-def _round_off_if_near_zero(number: float, precision: int = 7) -> float:
+def round_off_if_near_zero(number: float, precision: int = 7) -> float:
"""Rounds off the number to zero only if number is close to zero for decimal
specified in precision. Precision defaults to 7.
"""
From b1555fd477923a968a203c2fde68e754777a1e08 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Feb 2022 21:21:39 +0530
Subject: [PATCH 111/447] chore: batch flag and consumption rate in invariant
report
---
.../stock_ledger_invariant_check.py | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 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 cb35bf75d1..7826d34422 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
@@ -60,6 +60,9 @@ def add_invariant_check_fields(sles):
fifo_qty += qty
fifo_value += qty * rate
+ if sle.actual_qty < 0:
+ sle.consumption_rate = sle.stock_value_difference / sle.actual_qty
+
balance_qty += sle.actual_qty
balance_stock_value += sle.stock_value_difference
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
@@ -90,6 +93,9 @@ def add_invariant_check_fields(sles):
sle.fifo_stock_diff = sle.fifo_stock_value - sles[idx - 1].fifo_stock_value
sle.fifo_difference_diff = sle.fifo_stock_diff - sle.stock_value_difference
+ if sle.batch_no:
+ sle.use_batchwise_valuation = frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True)
+
return sles
@@ -134,6 +140,11 @@ def get_columns():
"label": "Batch",
"options": "Batch",
},
+ {
+ "fieldname": "use_batchwise_valuation",
+ "fieldtype": "Check",
+ "label": "Batchwise Valuation",
+ },
{
"fieldname": "actual_qty",
"fieldtype": "Float",
@@ -145,9 +156,9 @@ def get_columns():
"label": "Incoming Rate",
},
{
- "fieldname": "outgoing_rate",
+ "fieldname": "consumption_rate",
"fieldtype": "Float",
- "label": "Outgoing Rate",
+ "label": "Consumption Rate",
},
{
"fieldname": "qty_after_transaction",
From 76b395d62ee5f9ffb96e3c3e4920fa6eebaec175 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Feb 2022 22:01:34 +0530
Subject: [PATCH 112/447] test: old/new mix batches valuation consumption
---
.../test_stock_ledger_entry.py | 83 ++++++++++++++++++-
1 file changed, 81 insertions(+), 2 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 c298b5a096..b0df45ffd4 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
@@ -397,7 +397,15 @@ class TestStockLedgerEntry(ERPNextTestCase):
for exp_sle, act_sle in zip(expected_sles, sles):
for k, v in exp_sle.items():
- self.assertEqual(v, act_sle[k], msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}")
+ act_value = act_sle[k]
+ if k == "stock_queue":
+ act_value = json.loads(act_value)
+ if act_value and act_value[0][0] == 0:
+ # ignore empty fifo bins
+ continue
+
+ self.assertEqual(v, act_value, msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}")
+
def test_batchwise_item_valuation_stock_reco(self):
item, warehouses, batches = setup_item_valuation_test()
@@ -587,6 +595,77 @@ class TestStockLedgerEntry(ERPNextTestCase):
for details in details_list:
check_sle_details_against_expected(*details)
+ def test_mixed_valuation_batches(self):
+ item_code, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0)
+ warehouse = warehouses[0]
+
+ state = {
+ "qty": 0.0,
+ "stock_value": 0.0
+ }
+ def update_invariants(exp_sles):
+ for sle in exp_sles:
+ state["stock_value"] += sle["stock_value_difference"]
+ state["qty"] += sle["actual_qty"]
+ sle["stock_value"] = state["stock_value"]
+ sle["qty_after_transaction"] = state["qty"]
+ return exp_sles
+
+ old1 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0],
+ qty=10, rate=10)
+ self.assertSLEs(old1, update_invariants([
+ {"actual_qty": 10, "stock_value_difference": 10*10, "stock_queue": [[10, 10]]},
+ ]))
+ old2 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[1],
+ qty=10, rate=20)
+ self.assertSLEs(old2, update_invariants([
+ {"actual_qty": 10, "stock_value_difference": 10*20, "stock_queue": [[10, 10], [10, 20]]},
+ ]))
+ old3 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0],
+ qty=5, rate=15)
+
+ self.assertSLEs(old3, update_invariants([
+ {"actual_qty": 5, "stock_value_difference": 5*15, "stock_queue": [[10, 10], [10, 20], [5, 15]]},
+ ]))
+
+ new1 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=40)
+ batches.append(new1.items[0].batch_no)
+ # assert old queue remains
+ self.assertSLEs(new1, update_invariants([
+ {"actual_qty": 10, "stock_value_difference": 10*40, "stock_queue": [[10, 10], [10, 20], [5, 15]]},
+ ]))
+
+ new2 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=42)
+ batches.append(new2.items[0].batch_no)
+ self.assertSLEs(new2, update_invariants([
+ {"actual_qty": 10, "stock_value_difference": 10*42, "stock_queue": [[10, 10], [10, 20], [5, 15]]},
+ ]))
+
+ # consume old batch as per FIFO
+ consume_old1 = make_stock_entry(item_code=item_code, source=warehouse, qty=15, batch_no=batches[0])
+ self.assertSLEs(consume_old1, update_invariants([
+ {"actual_qty": -15, "stock_value_difference": -10*10 - 5*20, "stock_queue": [[5, 20], [5, 15]]},
+ ]))
+
+ # consume new batch as per batch
+ consume_new2 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-1])
+ self.assertSLEs(consume_new2, update_invariants([
+ {"actual_qty": -10, "stock_value_difference": -10*42, "stock_queue": [[5, 20], [5, 15]]},
+ ]))
+
+ # finish all old batches
+ consume_old2 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[1])
+ self.assertSLEs(consume_old2, update_invariants([
+ {"actual_qty": -10, "stock_value_difference": -5*20 - 5*15, "stock_queue": []},
+ ]))
+
+ # finish all new batches
+ consume_new1 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-2])
+ self.assertSLEs(consume_new1, update_invariants([
+ {"actual_qty": -10, "stock_value_difference": -10*40, "stock_queue": []},
+ ]))
+
+
def create_repack_entry(**args):
args = frappe._dict(args)
@@ -661,7 +740,7 @@ def setup_item_valuation_test(valuation_method="FIFO", suffix=None, use_batchwis
item = make_item(
f"IV - Test Item {valuation_method} {suffix}",
- dict(valuation_method=valuation_method, has_batch_no=1)
+ dict(valuation_method=valuation_method, has_batch_no=1, create_new_batch=1)
)
warehouses = [create_warehouse(f"IV - Test Warehouse {i}") for i in ['J', 'K']]
batches = [f"IV - Test Batch {i} {valuation_method} {suffix}" for i in batches_list]
From 35483242b3864e09c635979afe7793aac7f12596 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Feb 2022 22:22:27 +0530
Subject: [PATCH 113/447] fix: extend round_off_if_near_zero fix to other
methods
---
erpnext/stock/stock_ledger.py | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 677266ee0c..de6c409d7c 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -743,12 +743,14 @@ class update_entries_after(object):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
- self.wh_data.qty_after_transaction += actual_qty
+ self.wh_data.qty_after_transaction = round_off_if_near_zero(self.wh_data.qty_after_transaction + actual_qty)
if actual_qty > 0:
stock_value_difference = incoming_rate * actual_qty
else:
- outgoing_rate = get_batch_incoming_rate(item_code=sle.item_code, warehouse=sle.warehouse, batch_no=sle.batch_no, posting_date=sle.posting_date, posting_time=sle.posting_time, creation=sle.creation)
+ outgoing_rate = get_batch_incoming_rate(item_code=sle.item_code,
+ warehouse=sle.warehouse, batch_no=sle.batch_no, posting_date=sle.posting_date,
+ posting_time=sle.posting_time, creation=sle.creation)
if outgoing_rate is None:
# This can *only* happen if qty available for the batch is zero.
# in such case fall back various other rates.
@@ -757,7 +759,7 @@ class update_entries_after(object):
outgoing_rate = self.get_fallback_rate(sle)
stock_value_difference = outgoing_rate * actual_qty
- self.wh_data.stock_value += stock_value_difference
+ self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference)
if self.wh_data.qty_after_transaction:
self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
From 609d2fccad2a1b60a1e7ffd93f504f0e1329136d Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sun, 20 Feb 2022 11:35:53 +0530
Subject: [PATCH 114/447] fix: reset stock value if no qty
---
erpnext/stock/stock_ledger.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index de6c409d7c..1b90086440 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -468,6 +468,8 @@ class update_entries_after(object):
# rounding as per precision
self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision)
+ if not self.wh_data.qty_after_transaction:
+ self.wh_data.stock_value = 0.0
stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
self.wh_data.prev_stock_value = self.wh_data.stock_value
From 6b0bc350636776fbec3edc254086462a7670649c Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sun, 20 Feb 2022 12:05:58 +0530
Subject: [PATCH 115/447] test: mixed moving average items
---
.../test_stock_ledger_entry.py | 30 ++++++++++++++++++-
1 file changed, 29 insertions(+), 1 deletion(-)
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 b0df45ffd4..9e819dd658 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
@@ -595,7 +595,7 @@ class TestStockLedgerEntry(ERPNextTestCase):
for details in details_list:
check_sle_details_against_expected(*details)
- def test_mixed_valuation_batches(self):
+ def test_mixed_valuation_batches_fifo(self):
item_code, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0)
warehouse = warehouses[0]
@@ -665,6 +665,34 @@ class TestStockLedgerEntry(ERPNextTestCase):
{"actual_qty": -10, "stock_value_difference": -10*40, "stock_queue": []},
]))
+ def test_mixed_valuation_batches_moving_average(self):
+ item_code, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0, valuation_method="Moving Average")
+ warehouse = warehouses[0]
+
+ make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0],
+ qty=10, rate=10)
+ make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[1],
+ qty=10, rate=20)
+ make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0],
+ qty=5, rate=15)
+
+ new1 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=40)
+ batches.append(new1.items[0].batch_no)
+ new2 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=42)
+ batches.append(new2.items[0].batch_no)
+
+ # consume old batch as per FIFO
+ make_stock_entry(item_code=item_code, source=warehouse, qty=15, batch_no=batches[0])
+ # consume new batch as per batch
+ make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-1])
+ # finish all old batches
+ make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[1])
+
+ # finish all new batches
+ consume_new1 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-2])
+ self.assertSLEs(consume_new1, ([
+ {"stock_value": 0},
+ ]))
def create_repack_entry(**args):
From f38690f7037c75bb1c5a5d946d686b40392a111a Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sun, 20 Feb 2022 12:58:53 +0530
Subject: [PATCH 116/447] fix: check if Moving average item can use batchwise
valuation
---
erpnext/stock/doctype/batch/batch.py | 32 ++++++++++++++++++++++++++++
erpnext/stock/utils.py | 2 +-
2 files changed, 33 insertions(+), 1 deletion(-)
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index 96751d6eae..b5e56ad301 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -6,6 +6,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.naming import make_autoname, revert_series_if_last
+from frappe.query_builder.functions import Sum
from frappe.utils import cint, flt, get_link_to_form
from frappe.utils.data import add_days
from frappe.utils.jinja import render_template
@@ -110,11 +111,15 @@ class Batch(Document):
def validate(self):
self.item_has_batch_enabled()
+ self.set_batchwise_valuation()
def item_has_batch_enabled(self):
if frappe.db.get_value("Item", self.item, "has_batch_no") == 0:
frappe.throw(_("The selected item cannot have Batch"))
+ def set_batchwise_valuation(self):
+ self.use_batchwise_valuation = int(can_use_batchwise_valuation(self.item))
+
def before_save(self):
has_expiry_date, shelf_life_in_days = frappe.db.get_value('Item', self.item, ['has_expiry_date', 'shelf_life_in_days'])
if not self.expiry_date and has_expiry_date and shelf_life_in_days:
@@ -338,3 +343,30 @@ def get_pos_reserved_batch_qty(filters):
flt_reserved_batch_qty = flt(reserved_batch_qty[0][0])
return flt_reserved_batch_qty
+
+def can_use_batchwise_valuation(item_code: str) -> bool:
+ """ Check if item can use batchwise valuation.
+
+ Note: Item with existing moving average batches can't use batchwise valuation
+ until they are exhausted.
+ """
+ from erpnext.stock.stock_ledger import get_valuation_method
+ batch = frappe.qb.DocType("Batch")
+
+ if get_valuation_method(item_code) != "Moving Average":
+ return True
+
+ batch_qty = (
+ frappe.qb
+ .from_(batch)
+ .select(Sum(batch.batch_qty))
+ .where(
+ (batch.use_batchwise_valuation == 0)
+ & (batch.item == item_code)
+ )
+ ).run()
+
+ if batch_qty and batch_qty[0][0]:
+ return False
+
+ return True
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index e2bd2f197d..f85a04f944 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -261,7 +261,7 @@ def get_valuation_method(item_code):
"""get valuation method from item or default"""
val_method = frappe.db.get_value('Item', item_code, 'valuation_method', cache=True)
if not val_method:
- val_method = frappe.db.get_value("Stock Settings", None, "valuation_method") or "FIFO"
+ val_method = frappe.db.get_value("Stock Settings", None, "valuation_method", cache=True) or "FIFO"
return val_method
def get_fifo_rate(previous_stock_queue, qty):
From 75fb5616987066b83b69455b4eb59d1a715b280e Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 21 Feb 2022 11:08:57 +0530
Subject: [PATCH 117/447] test: force correct flag in test data
---
.../doctype/stock_ledger_entry/test_stock_ledger_entry.py | 8 +++++---
1 file changed, 5 insertions(+), 3 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 9e819dd658..c65ed2888e 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
@@ -778,13 +778,15 @@ def setup_item_valuation_test(valuation_method="FIFO", suffix=None, use_batchwis
ubw = use_batchwise_valuation
if isinstance(use_batchwise_valuation, (list, tuple)):
ubw = use_batchwise_valuation[i]
- make_batch(
- frappe._dict(
+ batch = frappe.get_doc(frappe._dict(
+ doctype="Batch",
batch_id=batch_id,
item=item.item_code,
use_batchwise_valuation=ubw
)
- )
+ ).insert()
+ batch.use_batchwise_valuation = ubw
+ batch.db_update()
return item.item_code, warehouses, batches
From af9fa049c749c9f72f0b21a5960111cb6ec57c12 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 21 Feb 2022 12:28:19 +0530
Subject: [PATCH 118/447] fix: batchwise valuation can only be used by
FIFO/LIFO
---
erpnext/stock/doctype/batch/batch.py | 24 ++-------------
.../test_stock_ledger_entry.py | 30 -------------------
2 files changed, 2 insertions(+), 52 deletions(-)
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index b5e56ad301..93e8d41367 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -6,7 +6,6 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.naming import make_autoname, revert_series_if_last
-from frappe.query_builder.functions import Sum
from frappe.utils import cint, flt, get_link_to_form
from frappe.utils.data import add_days
from frappe.utils.jinja import render_template
@@ -347,26 +346,7 @@ def get_pos_reserved_batch_qty(filters):
def can_use_batchwise_valuation(item_code: str) -> bool:
""" Check if item can use batchwise valuation.
- Note: Item with existing moving average batches can't use batchwise valuation
- until they are exhausted.
- """
+ Note: Moving average valuation method can not use batch_wise_valuation."""
from erpnext.stock.stock_ledger import get_valuation_method
- batch = frappe.qb.DocType("Batch")
- if get_valuation_method(item_code) != "Moving Average":
- return True
-
- batch_qty = (
- frappe.qb
- .from_(batch)
- .select(Sum(batch.batch_qty))
- .where(
- (batch.use_batchwise_valuation == 0)
- & (batch.item == item_code)
- )
- ).run()
-
- if batch_qty and batch_qty[0][0]:
- return False
-
- return True
+ return get_valuation_method(item_code) != "Moving Average"
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 c65ed2888e..0864ece995 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
@@ -665,36 +665,6 @@ class TestStockLedgerEntry(ERPNextTestCase):
{"actual_qty": -10, "stock_value_difference": -10*40, "stock_queue": []},
]))
- def test_mixed_valuation_batches_moving_average(self):
- item_code, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0, valuation_method="Moving Average")
- warehouse = warehouses[0]
-
- make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0],
- qty=10, rate=10)
- make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[1],
- qty=10, rate=20)
- make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0],
- qty=5, rate=15)
-
- new1 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=40)
- batches.append(new1.items[0].batch_no)
- new2 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=42)
- batches.append(new2.items[0].batch_no)
-
- # consume old batch as per FIFO
- make_stock_entry(item_code=item_code, source=warehouse, qty=15, batch_no=batches[0])
- # consume new batch as per batch
- make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-1])
- # finish all old batches
- make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[1])
-
- # finish all new batches
- consume_new1 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-2])
- self.assertSLEs(consume_new1, ([
- {"stock_value": 0},
- ]))
-
-
def create_repack_entry(**args):
args = frappe._dict(args)
repack = frappe.new_doc("Stock Entry")
From 9661058cc7daf9802e054f3fcd99c7852ff935a4 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 21 Feb 2022 18:16:10 +0530
Subject: [PATCH 119/447] fix: only set batchwise valuation flag if new batch
---
erpnext/stock/doctype/batch/batch.json | 6 ++++--
erpnext/stock/doctype/batch/batch.py | 13 ++++---------
erpnext/stock/doctype/batch/test_batch.py | 20 ++++++++++++++++++++
3 files changed, 28 insertions(+), 11 deletions(-)
diff --git a/erpnext/stock/doctype/batch/batch.json b/erpnext/stock/doctype/batch/batch.json
index 0d28ea0919..967c5729bf 100644
--- a/erpnext/stock/doctype/batch/batch.json
+++ b/erpnext/stock/doctype/batch/batch.json
@@ -194,7 +194,7 @@
"fieldtype": "Column Break"
},
{
- "default": "1",
+ "default": "0",
"fieldname": "use_batchwise_valuation",
"fieldtype": "Check",
"label": "Use Batch-wise Valuation",
@@ -207,10 +207,11 @@
"image_field": "image",
"links": [],
"max_attachments": 5,
- "modified": "2021-10-11 13:38:12.806976",
+ "modified": "2022-02-21 08:08:23.999236",
"modified_by": "Administrator",
"module": "Stock",
"name": "Batch",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -231,6 +232,7 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "batch_id",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index 93e8d41367..c9b4c147f1 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -117,7 +117,10 @@ class Batch(Document):
frappe.throw(_("The selected item cannot have Batch"))
def set_batchwise_valuation(self):
- self.use_batchwise_valuation = int(can_use_batchwise_valuation(self.item))
+ from erpnext.stock.stock_ledger import get_valuation_method
+
+ if self.is_new() and get_valuation_method(self.item) != "Moving Average":
+ self.use_batchwise_valuation = 1
def before_save(self):
has_expiry_date, shelf_life_in_days = frappe.db.get_value('Item', self.item, ['has_expiry_date', 'shelf_life_in_days'])
@@ -342,11 +345,3 @@ def get_pos_reserved_batch_qty(filters):
flt_reserved_batch_qty = flt(reserved_batch_qty[0][0])
return flt_reserved_batch_qty
-
-def can_use_batchwise_valuation(item_code: str) -> bool:
- """ Check if item can use batchwise valuation.
-
- Note: Moving average valuation method can not use batch_wise_valuation."""
- from erpnext.stock.stock_ledger import get_valuation_method
-
- return get_valuation_method(item_code) != "Moving Average"
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index 6495b56e92..baa03024af 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -6,6 +6,7 @@ import json
import frappe
from frappe.exceptions import ValidationError
from frappe.utils import cint, flt
+from frappe.utils.data import add_to_date, getdate
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty
@@ -387,6 +388,25 @@ class TestBatch(ERPNextTestCase):
assertValuation((20 * 20 + 10 * 25) / (10 + 20))
+ def test_update_batch_properties(self):
+ item_code = "_TestBatchWiseVal"
+ self.make_batch_item(item_code)
+
+ se = make_stock_entry(item_code=item_code, qty=100, rate=10, target="_Test Warehouse - _TC")
+ batch_no = se.items[0].batch_no
+ batch = frappe.get_doc("Batch", batch_no)
+
+ expiry_date = add_to_date(batch.manufacturing_date, days=30)
+
+ batch.expiry_date = expiry_date
+ batch.save()
+
+ batch.reload()
+
+ self.assertEqual(getdate(batch.expiry_date), getdate(expiry_date))
+
+
+
def create_batch(item_code, rate, create_item_price_for_batch):
pi = make_purchase_invoice(company="_Test Company",
warehouse= "Stores - _TC", cost_center = "Main - _TC", update_stock=1,
From e4c4dc402e75d3ec501095fa3e914553fcd07a4d Mon Sep 17 00:00:00 2001
From: Sagar Sharma
Date: Mon, 21 Feb 2022 19:49:19 +0530
Subject: [PATCH 120/447] fix: JobCard TimeLog to_date (#29872)
---
erpnext/manufacturing/doctype/job_card/job_card.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 8d00019b7d..9f4ace296e 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -62,7 +62,7 @@ class JobCard(Document):
if self.get('time_logs'):
for d in self.get('time_logs'):
- if get_datetime(d.from_time) > get_datetime(d.to_time):
+ if d.to_time and get_datetime(d.from_time) > get_datetime(d.to_time):
frappe.throw(_("Row {0}: From time must be less than to time").format(d.idx))
data = self.get_overlap_for(d)
From 87b59fc96c7bb37fcfbce097bd7c8184fce967ba Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Mon, 21 Feb 2022 22:53:29 +0530
Subject: [PATCH 121/447] fix(LMS): program enrollment does not give any
feedback (#29922)
---
erpnext/www/lms/macros/hero.html | 19 ++++++++++---------
1 file changed, 10 insertions(+), 9 deletions(-)
diff --git a/erpnext/www/lms/macros/hero.html b/erpnext/www/lms/macros/hero.html
index e72bfc8175..95ba8f7df2 100644
--- a/erpnext/www/lms/macros/hero.html
+++ b/erpnext/www/lms/macros/hero.html
@@ -11,7 +11,7 @@
{% if frappe.session.user == 'Guest' %}
{{_('Sign Up')}}
{% elif not has_access %}
- {{_('Enroll')}}
+ {{_('Enroll')}}
{% endif %}
@@ -20,34 +20,35 @@
From 4738367d6407e9ffc22ba2c9ef1649573608be50 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Mon, 21 Feb 2022 22:54:46 +0530
Subject: [PATCH 122/447] fix: boarding task dates not set when activity begins
on is set to 0 (#29921)
---
.../employee_boarding_controller.py | 4 +--
.../test_employee_onboarding.py | 32 +++++++++++++------
.../doctype/salary_slip/test_salary_slip.py | 6 ++--
3 files changed, 28 insertions(+), 14 deletions(-)
diff --git a/erpnext/controllers/employee_boarding_controller.py b/erpnext/controllers/employee_boarding_controller.py
index ae2c73758c..dd02ce1748 100644
--- a/erpnext/controllers/employee_boarding_controller.py
+++ b/erpnext/controllers/employee_boarding_controller.py
@@ -104,11 +104,11 @@ class EmployeeBoardingController(Document):
def get_task_dates(self, activity, holiday_list):
start_date = end_date = None
- if activity.begin_on:
+ if activity.begin_on is not None:
start_date = add_days(self.boarding_begins_on, activity.begin_on)
start_date = self.update_if_holiday(start_date, holiday_list)
- if activity.duration:
+ if activity.duration is not None:
end_date = add_days(self.boarding_begins_on, activity.begin_on + activity.duration)
end_date = self.update_if_holiday(end_date, holiday_list)
diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
index 2d129c8acf..0fb821ddb2 100644
--- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
+++ b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
@@ -4,7 +4,7 @@
import unittest
import frappe
-from frappe.utils import getdate
+from frappe.utils import add_days, getdate
from erpnext.hr.doctype.employee_onboarding.employee_onboarding import (
IncompleteTaskError,
@@ -35,6 +35,15 @@ class TestEmployeeOnboarding(unittest.TestCase):
# boarding status
self.assertEqual(onboarding.boarding_status, 'Pending')
+ # start and end dates
+ start_date, end_date = frappe.db.get_value('Task', onboarding.activities[0].task, ['exp_start_date', 'exp_end_date'])
+ self.assertEqual(getdate(start_date), getdate(onboarding.boarding_begins_on))
+ self.assertEqual(getdate(end_date), add_days(start_date, onboarding.activities[0].duration))
+
+ start_date, end_date = frappe.db.get_value('Task', onboarding.activities[1].task, ['exp_start_date', 'exp_end_date'])
+ self.assertEqual(getdate(start_date), add_days(onboarding.boarding_begins_on, onboarding.activities[0].duration))
+ self.assertEqual(getdate(end_date), add_days(start_date, onboarding.activities[1].duration))
+
# complete the task
project = frappe.get_doc('Project', onboarding.project)
for task in frappe.get_all('Task', dict(project=project.name)):
@@ -57,10 +66,7 @@ class TestEmployeeOnboarding(unittest.TestCase):
self.assertEqual(employee.employee_name, 'Test Researcher')
def tearDown(self):
- for entry in frappe.get_all('Employee Onboarding'):
- doc = frappe.get_doc('Employee Onboarding', entry.name)
- doc.cancel()
- doc.delete()
+ frappe.db.rollback()
def get_job_applicant():
@@ -87,23 +93,31 @@ def get_job_offer(applicant_name):
def create_employee_onboarding():
applicant = get_job_applicant()
job_offer = get_job_offer(applicant.name)
- holiday_list = make_holiday_list()
+
+ holiday_list = make_holiday_list('_Test Employee Boarding')
+ holiday_list = frappe.get_doc('Holiday List', holiday_list)
+ holiday_list.holidays = []
+ holiday_list.save()
onboarding = frappe.new_doc('Employee Onboarding')
onboarding.job_applicant = applicant.name
onboarding.job_offer = job_offer.name
onboarding.date_of_joining = onboarding.boarding_begins_on = getdate()
onboarding.company = '_Test Company'
- onboarding.holiday_list = holiday_list
+ onboarding.holiday_list = holiday_list.name
onboarding.designation = 'Researcher'
onboarding.append('activities', {
'activity_name': 'Assign ID Card',
'role': 'HR User',
- 'required_for_employee_creation': 1
+ 'required_for_employee_creation': 1,
+ 'begin_on': 0,
+ 'duration': 1
})
onboarding.append('activities', {
'activity_name': 'Assign a laptop',
- 'role': 'HR User'
+ 'role': 'HR User',
+ 'begin_on': 1,
+ 'duration': 1
})
onboarding.status = 'Pending'
onboarding.insert()
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index daa0f8952b..6a5debf998 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -1019,13 +1019,13 @@ def setup_test():
frappe.db.set_value('HR Settings', None, 'leave_status_notification_template', None)
frappe.db.set_value('HR Settings', None, 'leave_approval_notification_template', None)
-def make_holiday_list():
+def make_holiday_list(holiday_list_name=None):
fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
- holiday_list = frappe.db.exists("Holiday List", "Salary Slip Test Holiday List")
+ holiday_list = frappe.db.exists("Holiday List", holiday_list_name or "Salary Slip Test Holiday List")
if not holiday_list:
holiday_list = frappe.get_doc({
"doctype": "Holiday List",
- "holiday_list_name": "Salary Slip Test Holiday List",
+ "holiday_list_name": holiday_list_name or "Salary Slip Test Holiday List",
"from_date": fiscal_year[1],
"to_date": fiscal_year[2],
"weekly_off": "Sunday"
From 70b960e650bbc1c418eecd14ac42d64a3103a43c Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Tue, 22 Feb 2022 09:16:08 +0530
Subject: [PATCH 123/447] fix: Account filter in PSOA
---
.../process_statement_of_accounts.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index 09aa72352e..1b34d6d1f2 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -73,7 +73,7 @@ def get_report_pdf(doc, consolidated=True):
'to_date': doc.to_date,
'company': doc.company,
'finance_book': doc.finance_book if doc.finance_book else None,
- 'account': doc.account if doc.account else None,
+ 'account': [doc.account] if doc.account else None,
'party_type': 'Customer',
'party': [entry.customer],
'presentation_currency': presentation_currency,
From d011a3f82c5cf9c1dc4fe0561194d47cff6099d0 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Tue, 22 Feb 2022 11:41:09 +0530
Subject: [PATCH 124/447] fix(Salary Slip): TypeError while clearing any amount
field in components (#29931)
---
erpnext/payroll/doctype/salary_slip/salary_slip.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index f727ff4378..d2a39989a6 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -1268,7 +1268,7 @@ class SalarySlip(TransactionBase):
for i, earning in enumerate(self.earnings):
if earning.salary_component == salary_component:
self.earnings[i].amount = wages_amount
- self.gross_pay += self.earnings[i].amount
+ self.gross_pay += flt(self.earnings[i].amount, earning.precision("amount"))
self.net_pay = flt(self.gross_pay) - flt(self.total_deduction)
def compute_year_to_date(self):
From 235fc127b3ecf943176ed9c208425f9bda100798 Mon Sep 17 00:00:00 2001
From: Marica
Date: Tue, 22 Feb 2022 12:53:46 +0530
Subject: [PATCH 125/447] fix: Fetch conversion factor even if it already
existed in row, on item change (#29917)
* fix: Fetch conversion factor even if it already existed in row, on item change
* fix: Retain manually changed conversion factor
- If item code changes, reset conversion factor on client side
- Keep API behavious consistent, if conversion factor is sent, same must come back
- API should not ideally reset values in most cases
---
erpnext/public/js/controllers/transaction.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 933ced0bd7..ae8c0c8c6d 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -525,6 +525,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
item.weight_per_unit = 0;
item.weight_uom = '';
+ item.conversion_factor = 0;
if(['Sales Invoice'].includes(this.frm.doc.doctype)) {
update_stock = cint(me.frm.doc.update_stock);
From 8005fee6569abed607c57d31b26531925fd7e15b Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Tue, 22 Feb 2022 15:06:19 +0530
Subject: [PATCH 126/447] feat: update ordered qty for packed items
---
.../doctype/purchase_order/purchase_order.py | 10 +++++++++
.../purchase_order_item.json | 13 ++++++++++-
.../doctype/sales_order/sales_order.js | 22 ++++++++++++++++++-
.../doctype/sales_order/sales_order.py | 5 +++++
.../doctype/packed_item/packed_item.json | 11 +++++++++-
5 files changed, 58 insertions(+), 3 deletions(-)
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 1b5f35efbb..2e7d3063cc 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -316,6 +316,16 @@ class PurchaseOrder(BuyingController):
'target_ref_field': 'stock_qty',
'source_field': 'stock_qty'
})
+ self.status_updater.append({
+ 'source_dt': 'Purchase Order Item',
+ 'target_dt': 'Packed Item',
+ 'target_field': 'ordered_qty',
+ 'target_parent_dt': 'Sales Order',
+ 'target_parent_field': '',
+ 'join_field': 'sales_order_packed_item',
+ 'target_ref_field': 'qty',
+ 'source_field': 'stock_qty'
+ })
def update_delivered_qty_in_sales_order(self):
"""Update delivered qty in Sales Order for drop ship"""
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
index 87cd57517e..c26d592e3e 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -63,6 +63,7 @@
"material_request_item",
"sales_order",
"sales_order_item",
+ "sales_order_packed_item",
"supplier_quotation",
"supplier_quotation_item",
"col_break5",
@@ -837,21 +838,31 @@
"label": "Product Bundle",
"options": "Product Bundle",
"read_only": 1
+ },
+ {
+ "fieldname": "sales_order_packed_item",
+ "fieldtype": "Data",
+ "label": "Sales Order Packed Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "search_index": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-08-30 20:06:26.712097",
+ "modified": "2022-02-02 13:10:18.398976",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"search_fields": "item_name",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index eb98e6c0bf..f80eaf2757 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -562,6 +562,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
var me = this;
var dialog = new frappe.ui.Dialog({
title: __("Select Items"),
+ size: "large",
fields: [
{
"fieldtype": "Check",
@@ -663,7 +664,8 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
} else {
let po_items = [];
me.frm.doc.items.forEach(d => {
- let pending_qty = (flt(d.stock_qty) - flt(d.ordered_qty)) / flt(d.conversion_factor);
+ let ordered_qty = me.get_ordered_qty(d, me.frm.doc);
+ let pending_qty = (flt(d.stock_qty) - ordered_qty) / flt(d.conversion_factor);
if (pending_qty > 0) {
po_items.push({
"doctype": "Sales Order Item",
@@ -689,6 +691,24 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
dialog.show();
}
+ get_ordered_qty(item, so) {
+ let ordered_qty = item.ordered_qty;
+ if (so.packed_items) {
+ // calculate ordered qty based on packed items in case of product bundle
+ let packed_items = so.packed_items.filter(
+ (pi) => pi.parent_detail_docname == item.name
+ );
+ if (packed_items) {
+ ordered_qty = packed_items.reduce(
+ (sum, pi) => sum + flt(pi.ordered_qty),
+ 0
+ );
+ ordered_qty = ordered_qty / packed_items.length;
+ }
+ }
+ return ordered_qty;
+ }
+
hold_sales_order(){
var me = this;
var d = new frappe.ui.Dialog({
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 0f5b1e3b89..abbb3c9b90 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -877,6 +877,9 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty))
target.project = source_parent.project
+ def update_item_for_packed_item(source, target, source_parent):
+ target.qty = flt(source.qty) - flt(source.ordered_qty)
+
# po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")})
doc = get_mapped_doc("Sales Order", source_name, {
"Sales Order": {
@@ -920,6 +923,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
"Packed Item": {
"doctype": "Purchase Order Item",
"field_map": [
+ ["name", "sales_order_packed_item"],
["parent", "sales_order"],
["uom", "uom"],
["conversion_factor", "conversion_factor"],
@@ -934,6 +938,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
"supplier",
"pricing_rules"
],
+ "postprocess": update_item_for_packed_item,
"condition": lambda doc: doc.parent_item in items_to_map
}
}, target_doc, set_missing_values)
diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json
index d2d4789765..d6e2e9ce2d 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.json
+++ b/erpnext/stock/doctype/packed_item/packed_item.json
@@ -26,6 +26,7 @@
"section_break_13",
"actual_qty",
"projected_qty",
+ "ordered_qty",
"column_break_16",
"incoming_rate",
"page_break",
@@ -224,13 +225,21 @@
"label": "Rate",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "ordered_qty",
+ "fieldtype": "Float",
+ "label": "Ordered Qty",
+ "no_copy": 1,
+ "read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2022-01-28 16:03:30.780111",
+ "modified": "2022-02-22 12:57:45.325488",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packed Item",
From 8e3f1e306d705109a51271ba262b46fe4798a793 Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Tue, 22 Feb 2022 15:34:26 +0530
Subject: [PATCH 127/447] test: po updates packed item's ordered_qty
---
.../doctype/sales_order/test_sales_order.py | 36 +++++++++++++++++++
1 file changed, 36 insertions(+)
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 73c5bd299a..e56e56cea1 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -959,6 +959,42 @@ class TestSalesOrder(ERPNextTestCase):
self.assertEqual(purchase_order.items[0].item_code, "_Test Bundle Item 1")
self.assertEqual(purchase_order.items[1].item_code, "_Test Bundle Item 2")
+ def test_purchase_order_updates_packed_item_ordered_qty(self):
+ """
+ Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order
+ """
+ from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
+
+ product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0})
+ make_item("_Test Bundle Item 1", {"is_stock_item": 1})
+ make_item("_Test Bundle Item 2", {"is_stock_item": 1})
+
+ make_product_bundle("_Test Product Bundle",
+ ["_Test Bundle Item 1", "_Test Bundle Item 2"])
+
+ so_items = [
+ {
+ "item_code": product_bundle.item_code,
+ "warehouse": "",
+ "qty": 2,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ }
+ ]
+
+ so = make_sales_order(item_list=so_items)
+
+ purchase_order = make_purchase_order(so.name, selected_items=so_items)
+ purchase_order.supplier = "_Test Supplier"
+ purchase_order.set_warehouse = "_Test Warehouse - _TC"
+ purchase_order.save()
+ purchase_order.submit()
+
+ so.reload()
+ self.assertEqual(so.packed_items[0].ordered_qty, 2)
+ self.assertEqual(so.packed_items[1].ordered_qty, 2)
+
def test_reserved_qty_for_closing_so(self):
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
fields=["reserved_qty"])
From 3a547cb0d965b8012136d06adc9d7c7b94700660 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Tue, 22 Feb 2022 16:25:32 +0530
Subject: [PATCH 128/447] fix: Item discounts for quotation
---
erpnext/controllers/taxes_and_totals.py | 2 +-
erpnext/selling/doctype/quotation/quotation.js | 1 -
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 2776628227..52190765c9 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -116,7 +116,7 @@ class calculate_taxes_and_totals(object):
if item.discount_percentage == 100:
item.rate = 0.0
elif item.price_list_rate:
- if not item.rate or (item.pricing_rules and item.discount_percentage > 0):
+ if item.pricing_rules or item.discount_percentage > 0:
item.rate = flt(item.price_list_rate *
(1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js
index 0e1a915deb..34e9a52e11 100644
--- a/erpnext/selling/doctype/quotation/quotation.js
+++ b/erpnext/selling/doctype/quotation/quotation.js
@@ -40,7 +40,6 @@ frappe.ui.form.on('Quotation', {
erpnext.selling.QuotationController = class QuotationController extends erpnext.selling.SellingController {
onload(doc, dt, dn) {
- var me = this;
super.onload(doc, dt, dn);
}
party_name() {
From 7f55226a5807645db4f93c8038f1cc03a6fc0ce6 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 22 Feb 2022 16:55:43 +0530
Subject: [PATCH 129/447] fix: remove customer field value when MR is not
customer provided (#29938)
---
.../stock/doctype/material_request/material_request.py | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index b39328f85b..51209acb27 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -56,14 +56,13 @@ class MaterialRequest(BuyingController):
if actual_so_qty and (flt(so_items[so_no][item]) + already_indented > actual_so_qty):
frappe.throw(_("Material Request of maximum {0} can be made for Item {1} against Sales Order {2}").format(actual_so_qty - already_indented, item, so_no))
- # Validate
- # ---------------------
def validate(self):
super(MaterialRequest, self).validate()
self.validate_schedule_date()
self.check_for_on_hold_or_closed_status('Sales Order', 'sales_order')
self.validate_uom_is_integer("uom", "qty")
+ self.validate_material_request_type()
if not self.status:
self.status = "Draft"
@@ -83,6 +82,12 @@ class MaterialRequest(BuyingController):
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
+ def validate_material_request_type(self):
+ """ Validate fields in accordance with selected type """
+
+ if self.material_request_type != "Customer Provided":
+ self.customer = None
+
def set_title(self):
'''Set title as comma separated list of items'''
if not self.title:
From 745f7bc5f0fd014dcc837c41e2058be91166e1b4 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 22 Feb 2022 17:03:11 +0530
Subject: [PATCH 130/447] docs: add human readable specifications for stock
ledger (#29308)
* docs: add human readable specifications for stock ledger
* docs: reposting technical implementation notes
---
erpnext/stock/spec/README.md | 103 ++++++++++++++++++++++++++++++++
erpnext/stock/spec/reposting.md | 38 ++++++++++++
2 files changed, 141 insertions(+)
create mode 100644 erpnext/stock/spec/README.md
create mode 100644 erpnext/stock/spec/reposting.md
diff --git a/erpnext/stock/spec/README.md b/erpnext/stock/spec/README.md
new file mode 100644
index 0000000000..f5a3501fe4
--- /dev/null
+++ b/erpnext/stock/spec/README.md
@@ -0,0 +1,103 @@
+# Implementation notes for Stock Ledger
+
+
+## Important files
+
+- `stock/stock_ledger.py`
+- `controllers/stock_controller.py`
+- `stock/valuation.py`
+
+## What is in an Stock Ledger Entry (SLE)?
+
+Stock Ledger Entry is a single row in the Stock Ledger. It signifies some
+modification of stock for a particular Item in the specified warehouse.
+
+- `item_code`: item for which ledger entry is made
+- `warehouse`: warehouse where inventory is affected
+- `actual_qty`: change in qty
+- `qty_after_transaction`: quantity available after the transaction is processed
+- `incoming_rate`: rate at which inventory was received.
+- `is_cancelled`: if 1 then stock ledger entry is cancelled and should not be used
+for any business logic except for the code that handles cancellation.
+- `posting_date` & `posting_time`: Specify the temporal ordering of stock ledger
+ entries. Ties are broken by `creation` timestamp.
+- `voucher_type`: Many transaction can create SLE, e.g. Stock Entry, Purchase
+ Invoice
+- `voucher_no`: `name` of the transaction that created SLE
+- `voucher_detail_no`: `name` of the child table row from parent transaction
+ that created the SLE.
+- `dependant_sle_voucher_detail_no`: cross-warehouse transfers need this
+ reference in order to update dependent warehouse rates in case of change in
+ rate.
+- `recalculate_rate`: if this is checked in/out rates are recomputed on
+ transactions.
+- `valuation_rate`: current average valuation rate.
+- `stock_value`: current total stock value
+- `stock_value_difference`: stock value difference made between last and current
+ entry. This value is booked in accounting ledger.
+- `stock_queue`: if FIFO/LIFO is used this represents queue/stack maintained for
+ computing incoming rate for inventory getting consumed.
+- `batch_no`: batch no for which stock entry is made; each stock entry can only
+ affect one batch number.
+- `serial_no`: newline separated list of serial numbers that were added (if
+ actual_qty > 0) or else removed. Currently multiple serial nos can have single
+ SLE but this will likely change in future.
+
+
+## Implementation of Stock Ledger
+
+Stock Ledger Entry affects stock of combinations of (item_code, warehouse) and
+optionally batch no if specified. For simplicity, lets avoid batch no. for now.
+
+
+Stock Ledger Entry table stores stock ledger for all combinations of item_code
+and warehouse. So whenever any operations are to be performed on said
+item-warehouse combination stock ledger is filtered and sorted by posting
+datetime. A typical query that will give you individual ledger looks like this:
+
+```sql
+select *
+from `tabStock Ledger Entry` as sle
+where
+ is_cancelled = 0 --- cancelled entries don't affect ledger
+ and item_code = 'item_code' and warehouse = 'warehouse_name'
+order by timestamp(posting_date, posting_time), creation
+```
+
+New entry is just an update to the last entry which is found by looking at last
+row in the filter ledger.
+
+
+### Serial nos
+
+Serial numbers do not follow any valuation method configuration and they are
+consumed at rate they were produced unless they are grouped in which case they
+are consumed at weighted average rate.
+
+
+### Batch Nos
+
+Batches are currently NOT consumed as per batch wise valuation rate, instead
+global FIFO queue for the item is used for valuation rate.
+
+
+## Creation process of SLEs
+
+- SLE creation is usually triggered by Stock Transactions using a method
+ conventionally named `update_stock_ledger()` This might not be defined for
+ stock transaction and could be specified somewhere in inheritance hierarchy of
+ controllers.
+- This method produces SLE objects which are processed by `make_sl_entries` in
+ `stock_ledger.py` which commits the SLE to database.
+- `update_entries_after` class is used to process ONLY the inserted SLE's queue
+ and valuation.
+- The change in qty is propagated to future entries immediately. Valuation and
+ queue for future entries is processed in background using repost item
+ valuation.
+
+
+## Accounting impact
+
+- Accounting impact for stock transaction is handled by `get_gl_entries()`
+ method on controllers. Each transaction has different business logic for
+ booking the accounting impact.
diff --git a/erpnext/stock/spec/reposting.md b/erpnext/stock/spec/reposting.md
new file mode 100644
index 0000000000..b0d59fe9bb
--- /dev/null
+++ b/erpnext/stock/spec/reposting.md
@@ -0,0 +1,38 @@
+# Stock Reposting
+
+Stock "reposting" is process of re-processing Stock Ledger Entry and GL Entries
+in event of backdated stock transaction.
+
+*Backdated stock transaction*: Any stock transaction for which some
+item-warehouse combination has a future transactions.
+
+## Why is this required?
+Stock Ledger is stateful, it maintains queue, qty at any
+point in time. So if you do a backdated transaction all future values change,
+queues need to be re-evaluated etc. Watch Nabin and Rohit's conference
+presentation for explanation: https://www.youtube.com/watch?v=mw3WAnekGIM
+
+## How is this implemented?
+Whenever backdated transaction is detected, instead of
+fully processing it while submitting, the processing is queued using "Repost
+Item Valuation" doctype. Every hour a scheduled job runs and processes this
+queue (for up to maximum of 25 minutes)
+
+
+## Queue implementation
+- "Repost item valuation" (RIV) is automatically submitted from backdated transactions. (check stock_controller.py)
+- Draft and cancelled RIV are ignored.
+- Keep filter of "submitted" documents when doing anything with RIVs.
+- The default status is "Queued".
+- When background job runs, it picks the oldest pending reposts and changes the status to "In Progress" and when it finishes it
+changes to "Completed"
+- There are two more status: "Failed" when reposting failed and "Skipped" when reposting is deemed not necessary so it's skipped.
+- technical detail: Entry point for whole process is "repost_entries" function in repost_item_valuation.py
+
+
+## How to identify broken stock data:
+There are 4 major reports for checking broken stock data:
+- Incorrect balance qty after the transaction - to check if the running total of qty isn't correct.
+- Incorrect stock value report - to check incorrect value books in accounts for stock transactions
+- Incorrect serial no valuation -specific to serial nos
+- Stock ledger invariant check - combined report for checking qty, running total, queue, balance value etc
From 1682a26fe69b9b3fa64293e692e79a553b842ca2 Mon Sep 17 00:00:00 2001
From: Subin Tom
Date: Tue, 22 Feb 2022 17:20:48 +0530
Subject: [PATCH 131/447] fix: Taxjar minor fixes
---
.../taxjar_integration.py | 46 +++++++++++--------
1 file changed, 27 insertions(+), 19 deletions(-)
diff --git a/erpnext/erpnext_integrations/taxjar_integration.py b/erpnext/erpnext_integrations/taxjar_integration.py
index a4e21579e3..14c86d5632 100644
--- a/erpnext/erpnext_integrations/taxjar_integration.py
+++ b/erpnext/erpnext_integrations/taxjar_integration.py
@@ -8,10 +8,6 @@ from frappe.utils import cint, flt
from erpnext import get_default_company, get_region
-TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
-SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head")
-TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
-TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
SUPPORTED_COUNTRY_CODES = ["AT", "AU", "BE", "BG", "CA", "CY", "CZ", "DE", "DK", "EE", "ES", "FI",
"FR", "GB", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "MT", "NL", "PL", "PT", "RO",
"SE", "SI", "SK", "US"]
@@ -35,12 +31,14 @@ def get_client():
if api_key and api_url:
client = taxjar.Client(api_key=api_key, api_url=api_url)
client.set_api_config('headers', {
- 'x-api-version': '2020-08-07'
+ 'x-api-version': '2022-01-24'
})
return client
def create_transaction(doc, method):
+ TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
+
"""Create an order transaction in TaxJar"""
if not TAXJAR_CREATE_TRANSACTIONS:
@@ -51,6 +49,7 @@ def create_transaction(doc, method):
if not client:
return
+ TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
sales_tax = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == TAX_ACCOUNT_HEAD])
if not sales_tax:
@@ -79,6 +78,7 @@ def create_transaction(doc, method):
def delete_transaction(doc, method):
"""Delete an existing TaxJar order transaction"""
+ TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
if not TAXJAR_CREATE_TRANSACTIONS:
return
@@ -92,6 +92,8 @@ def delete_transaction(doc, method):
def get_tax_data(doc):
+ SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head")
+
from_address = get_company_address_details(doc)
from_shipping_state = from_address.get("state")
from_country_code = frappe.db.get_value("Country", from_address.country, "code")
@@ -113,20 +115,20 @@ def get_tax_data(doc):
to_shipping_state = get_state_code(to_address, 'Shipping')
tax_dict = {
- 'from_country': from_country_code,
- 'from_zip': from_address.pincode,
- 'from_state': from_shipping_state,
- 'from_city': from_address.city,
- 'from_street': from_address.address_line1,
- 'to_country': to_country_code,
- 'to_zip': to_address.pincode,
- 'to_city': to_address.city,
- 'to_street': to_address.address_line1,
- 'to_state': to_shipping_state,
- 'shipping': shipping,
- 'amount': doc.net_total,
- 'plugin': 'erpnext',
- 'line_items': line_items
+ "from_country": from_country_code,
+ "from_zip": from_address.pincode,
+ "from_state": from_shipping_state,
+ "from_city": from_address.city,
+ "from_street": from_address.address_line1,
+ "to_country": to_country_code,
+ "to_zip": to_address.pincode,
+ "to_city": to_address.city,
+ "to_street": to_address.address_line1,
+ "to_state": to_shipping_state,
+ "shipping": shipping,
+ "amount": doc.net_total,
+ "plugin": "erpnext",
+ "line_items": line_items
}
return tax_dict
@@ -156,6 +158,9 @@ def get_line_item_dict(item, docstatus):
return tax_dict
def set_sales_tax(doc, method):
+ TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
+ TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
+
if not TAXJAR_CALCULATE_TAX:
return
@@ -206,6 +211,7 @@ def set_sales_tax(doc, method):
doc.run_method("calculate_taxes_and_totals")
def check_for_nexus(doc, tax_dict):
+ TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
if not frappe.db.get_value('TaxJar Nexus', {'region_code': tax_dict["to_state"]}):
for item in doc.get("items"):
item.tax_collectable = flt(0)
@@ -218,6 +224,8 @@ def check_for_nexus(doc, tax_dict):
def check_sales_tax_exemption(doc):
# if the party is exempt from sales tax, then set all tax account heads to zero
+ TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
+
sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \
or frappe.db.has_column("Customer", "exempt_from_sales_tax") \
and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax")
From 5d403449bdcbe514c33b8807b674fd23ba24d93a Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 22 Feb 2022 19:24:49 +0530
Subject: [PATCH 132/447] test: move report tests to subttest (#29945)
Basically failfast=False but for sub-tests
---
erpnext/accounts/test/test_reports.py | 15 ++++++++-------
erpnext/manufacturing/report/test_reports.py | 15 ++++++++-------
erpnext/stock/report/test_reports.py | 15 ++++++++-------
3 files changed, 24 insertions(+), 21 deletions(-)
diff --git a/erpnext/accounts/test/test_reports.py b/erpnext/accounts/test/test_reports.py
index 78c109ab94..4ed966dcb9 100644
--- a/erpnext/accounts/test/test_reports.py
+++ b/erpnext/accounts/test/test_reports.py
@@ -39,10 +39,11 @@ class TestReports(unittest.TestCase):
def test_execute_all_accounts_reports(self):
"""Test that all script report in stock modules are executable with supported filters"""
for report, filter in REPORT_FILTER_TEST_CASES:
- execute_script_report(
- report_name=report,
- module="Accounts",
- filters=filter,
- default_filters=DEFAULT_FILTERS,
- optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
- )
+ with self.subTest(report=report):
+ execute_script_report(
+ report_name=report,
+ module="Accounts",
+ filters=filter,
+ default_filters=DEFAULT_FILTERS,
+ optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
+ )
diff --git a/erpnext/manufacturing/report/test_reports.py b/erpnext/manufacturing/report/test_reports.py
index 9f51ded6c7..e436fdca64 100644
--- a/erpnext/manufacturing/report/test_reports.py
+++ b/erpnext/manufacturing/report/test_reports.py
@@ -55,10 +55,11 @@ class TestManufacturingReports(unittest.TestCase):
def test_execute_all_manufacturing_reports(self):
"""Test that all script report in manufacturing modules are executable with supported filters"""
for report, filter in REPORT_FILTER_TEST_CASES:
- execute_script_report(
- report_name=report,
- module="Manufacturing",
- filters=filter,
- default_filters=DEFAULT_FILTERS,
- optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
- )
+ with self.subTest(report=report):
+ execute_script_report(
+ report_name=report,
+ module="Manufacturing",
+ filters=filter,
+ default_filters=DEFAULT_FILTERS,
+ optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
+ )
diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py
index 525af40b41..76c20798bf 100644
--- a/erpnext/stock/report/test_reports.py
+++ b/erpnext/stock/report/test_reports.py
@@ -73,10 +73,11 @@ class TestReports(unittest.TestCase):
def test_execute_all_stock_reports(self):
"""Test that all script report in stock modules are executable with supported filters"""
for report, filter in REPORT_FILTER_TEST_CASES:
- execute_script_report(
- report_name=report,
- module="Stock",
- filters=filter,
- default_filters=DEFAULT_FILTERS,
- optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
- )
+ with self.subTest(report=report):
+ execute_script_report(
+ report_name=report,
+ module="Stock",
+ filters=filter,
+ default_filters=DEFAULT_FILTERS,
+ optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
+ )
From a61790c00fa2b3c53ba49d930c7d08b3f0213b65 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Tue, 22 Feb 2022 20:58:10 +0530
Subject: [PATCH 133/447] fix: Remove unintended changes
---
erpnext/controllers/accounts_controller.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 7913a39329..a94af10cde 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1954,7 +1954,8 @@ def update_bin_on_delete(row, doctype):
qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse)
- update_bin_qty(row.item_code, row.warehouse, qty_dict)
+ if row.warehouse:
+ update_bin_qty(row.item_code, row.warehouse, qty_dict)
def validate_and_delete_children(parent, data):
deleted_children = []
From b0a1cd6a7bd9f0900d6f723c3b2cbf9037989fcc Mon Sep 17 00:00:00 2001
From: marination
Date: Wed, 23 Feb 2022 00:13:44 +0530
Subject: [PATCH 134/447] chore: Change heart icon to `icon-heart` and change
var `icon-stroke` to accomodate changes in frappe icon
- `icon-heart` got a stroke colour that needs to be overriden via var `icon-stroke
- Use `icon-heart` instead of `icon-heart-active` as the latter has a color fill now
---
erpnext/public/scss/shopping_cart.scss | 8 ++++----
erpnext/templates/includes/navbar/navbar_items.html | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
index 4b645b9dde..666043b219 100644
--- a/erpnext/public/scss/shopping_cart.scss
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -338,14 +338,14 @@ body.product-page {
.btn-add-to-wishlist {
svg use {
- stroke: #F47A7A;
+ --icon-stroke: #F47A7A;
}
}
.btn-view-in-wishlist {
svg use {
fill: #F47A7A;
- stroke: none;
+ --icon-stroke: none;
}
}
@@ -1022,7 +1022,7 @@ body.product-page {
.not-wished {
cursor: pointer;
- stroke: #F47A7A !important;
+ --icon-stroke: #F47A7A !important;
&:hover {
fill: #F47A7A;
@@ -1030,7 +1030,7 @@ body.product-page {
}
.wished {
- stroke: none;
+ --icon-stroke: none;
fill: #F47A7A !important;
}
diff --git a/erpnext/templates/includes/navbar/navbar_items.html b/erpnext/templates/includes/navbar/navbar_items.html
index 327552117b..d7adae562e 100644
--- a/erpnext/templates/includes/navbar/navbar_items.html
+++ b/erpnext/templates/includes/navbar/navbar_items.html
@@ -13,7 +13,7 @@
-
+
From 714325071fd526b653266a52057177fa541764c2 Mon Sep 17 00:00:00 2001
From: marination
Date: Wed, 23 Feb 2022 00:15:39 +0530
Subject: [PATCH 135/447] fix: Remove accidental `as_dict` in frappe.db.get_all
---
erpnext/www/shop-by-category/index.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/erpnext/www/shop-by-category/index.py b/erpnext/www/shop-by-category/index.py
index 394621272b..09f97ba5ef 100644
--- a/erpnext/www/shop-by-category/index.py
+++ b/erpnext/www/shop-by-category/index.py
@@ -62,8 +62,7 @@ def get_category_records(categories):
"parent_item_group": "All Item Groups",
"show_in_website": 1
},
- fields=["name", "parent_item_group", "is_group", "image", "route"],
- as_dict=True
+ fields=["name", "parent_item_group", "is_group", "image", "route"]
)
else:
doctype = frappe.unscrub(category)
@@ -71,7 +70,7 @@ def get_category_records(categories):
if frappe.get_meta(doctype, cached=True).get_field("image"):
fields += ["image"]
- categorical_data[category] = frappe.db.get_all(doctype, fields=fields, as_dict=True)
+ categorical_data[category] = frappe.db.get_all(doctype, fields=fields)
return categorical_data
From 3d6ed4ebdea5db3935dfcaeaa9ad8710ca2f9b1b Mon Sep 17 00:00:00 2001
From: GangaManoj
Date: Tue, 8 Feb 2022 17:36:28 +0530
Subject: [PATCH 136/447] fix: Fetch valuation rate
(cherry picked from commit df71907be453a481a43d2c62a5c076602ab8af07)
---
.../doctype/asset_repair/asset_repair.js | 17 ++++++++++
.../doctype/asset_repair/asset_repair.py | 33 +++++++++++++++++++
.../asset_repair_consumed_item.json | 3 +-
3 files changed, 51 insertions(+), 2 deletions(-)
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js
index d554d52a71..7bd3a7246d 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.js
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.js
@@ -68,6 +68,23 @@ frappe.ui.form.on('Asset Repair', {
});
frappe.ui.form.on('Asset Repair Consumed Item', {
+ item_code: function(frm, cdt, cdn) {
+ var row = locals[cdt][cdn];
+
+ frappe.call ({
+ method: "erpnext.assets.doctype.asset_repair.asset_repair.get_valuation_rate",
+ args: {
+ "item_code": row.item_code,
+ "warehouse": frm.doc.warehouse
+ },
+ callback: function(r) {
+ if(r.message) {
+ frappe.model.set_value(cdt, cdn, 'valuation_rate', r.message[0]);
+ }
+ }
+ });
+ },
+
consumed_quantity: function(frm, cdt, cdn) {
var row = locals[cdt][cdn];
frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate);
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py
index 36848e9f15..86a63fd1bc 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.py
@@ -256,3 +256,36 @@ class AssetRepair(AccountsController):
def get_downtime(failure_date, completion_date):
downtime = time_diff_in_hours(completion_date, failure_date)
return round(downtime, 2)
+
+@frappe.whitelist()
+def get_valuation_rate(item_code, warehouse):
+ last_valuation_rate = frappe.get_all(
+ "Stock Ledger Entry",
+ filters = {
+ "item_code": item_code,
+ "warehouse": warehouse,
+ "valuation_rate": [">=", 0],
+ "docstatus": ["<", 2]
+ },
+ pluck = "valuation_rate",
+ order_by = "posting_date desc, posting_time desc, name desc"
+ )
+
+ if last_valuation_rate:
+ return last_valuation_rate
+ else:
+ valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
+
+ if not valuation_rate:
+ # try Item Standard rate
+ valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
+
+ if not valuation_rate:
+ # try in price list
+ valuation_rate = frappe.db.get_value(
+ "Item Price",
+ dict(item_code=item_code, buying=1),
+ "price_list_rate"
+ )
+
+ return valuation_rate
diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
index f63add1235..3c850c8265 100644
--- a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
+++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
@@ -13,7 +13,6 @@
],
"fields": [
{
- "fetch_from": "item.valuation_rate",
"fieldname": "valuation_rate",
"fieldtype": "Currency",
"in_list_view": 1,
@@ -49,7 +48,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-11-11 18:23:00.492483",
+ "modified": "2022-02-08 17:37:20.028290",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair Consumed Item",
From 5d3b65b9447d241b391eb73fd008007621ceb569 Mon Sep 17 00:00:00 2001
From: GangaManoj
Date: Tue, 8 Feb 2022 17:59:06 +0530
Subject: [PATCH 137/447] fix: Pass value instead of array
(cherry picked from commit e1a9b61103e27099696ae4b186a6575b28c16b06)
---
erpnext/assets/doctype/asset_repair/asset_repair.js | 2 +-
erpnext/assets/doctype/asset_repair/asset_repair.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js
index 7bd3a7246d..18454fe367 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.js
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.js
@@ -79,7 +79,7 @@ frappe.ui.form.on('Asset Repair Consumed Item', {
},
callback: function(r) {
if(r.message) {
- frappe.model.set_value(cdt, cdn, 'valuation_rate', r.message[0]);
+ frappe.model.set_value(cdt, cdn, 'valuation_rate', r.message);
}
}
});
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py
index 86a63fd1bc..d5e3d3c811 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.py
@@ -272,7 +272,7 @@ def get_valuation_rate(item_code, warehouse):
)
if last_valuation_rate:
- return last_valuation_rate
+ return last_valuation_rate[0]
else:
valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
From 414c3b8dd2507318f78b42eea507a2eb1a2e80bb Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Mon, 21 Feb 2022 13:58:01 +0530
Subject: [PATCH 138/447] fix: remove redundant method
(cherry picked from commit 57d5a027fb929803ffd62463da3e5b4611b17ff4)
---
.../doctype/asset_repair/asset_repair.js | 21 +++++++-----
.../doctype/asset_repair/asset_repair.py | 33 -------------------
2 files changed, 13 insertions(+), 41 deletions(-)
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js
index 18454fe367..1e4c4e082d 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.js
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.js
@@ -69,18 +69,23 @@ frappe.ui.form.on('Asset Repair', {
frappe.ui.form.on('Asset Repair Consumed Item', {
item_code: function(frm, cdt, cdn) {
- var row = locals[cdt][cdn];
+ var item = locals[cdt][cdn];
- frappe.call ({
- method: "erpnext.assets.doctype.asset_repair.asset_repair.get_valuation_rate",
+ let item_args = {
+ 'item_code': item.item_code,
+ 'warehouse': frm.doc.warehouse,
+ 'qty': item.consumed_quantity,
+ 'serial_no': item.serial_no,
+ 'company': frm.doc.company
+ }
+
+ frappe.call({
+ method: 'erpnext.stock.utils.get_incoming_rate',
args: {
- "item_code": row.item_code,
- "warehouse": frm.doc.warehouse
+ args: item_args
},
callback: function(r) {
- if(r.message) {
- frappe.model.set_value(cdt, cdn, 'valuation_rate', r.message);
- }
+ frappe.model.set_value(cdt, cdn, 'valuation_rate', r.message);
}
});
},
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py
index d5e3d3c811..36848e9f15 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.py
@@ -256,36 +256,3 @@ class AssetRepair(AccountsController):
def get_downtime(failure_date, completion_date):
downtime = time_diff_in_hours(completion_date, failure_date)
return round(downtime, 2)
-
-@frappe.whitelist()
-def get_valuation_rate(item_code, warehouse):
- last_valuation_rate = frappe.get_all(
- "Stock Ledger Entry",
- filters = {
- "item_code": item_code,
- "warehouse": warehouse,
- "valuation_rate": [">=", 0],
- "docstatus": ["<", 2]
- },
- pluck = "valuation_rate",
- order_by = "posting_date desc, posting_time desc, name desc"
- )
-
- if last_valuation_rate:
- return last_valuation_rate[0]
- else:
- valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
-
- if not valuation_rate:
- # try Item Standard rate
- valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
-
- if not valuation_rate:
- # try in price list
- valuation_rate = frappe.db.get_value(
- "Item Price",
- dict(item_code=item_code, buying=1),
- "price_list_rate"
- )
-
- return valuation_rate
From 3dcc3a349ee63d04840fc7432a17a16d83d957fe Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Mon, 21 Feb 2022 14:01:12 +0530
Subject: [PATCH 139/447] fix: cannot edit valutaion_rate in asset repair
(cherry picked from commit 2aba6c38b36e5fce373800eb129a93eb010c691f)
---
.../asset_repair_consumed_item/asset_repair_consumed_item.json | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
index 3c850c8265..4685a09db6 100644
--- a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
+++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
@@ -16,8 +16,7 @@
"fieldname": "valuation_rate",
"fieldtype": "Currency",
"in_list_view": 1,
- "label": "Valuation Rate",
- "read_only": 1
+ "label": "Valuation Rate"
},
{
"fieldname": "consumed_quantity",
From 8743e394698b3d136a19507042556971ed2234a1 Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Mon, 21 Feb 2022 14:26:21 +0530
Subject: [PATCH 140/447] fix: sider
(cherry picked from commit bc28755978c17d7e85cdc5ffd4a45a629183e2c8)
---
erpnext/assets/doctype/asset_repair/asset_repair.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js
index 1e4c4e082d..3fe6b2d0d5 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.js
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.js
@@ -77,7 +77,7 @@ frappe.ui.form.on('Asset Repair Consumed Item', {
'qty': item.consumed_quantity,
'serial_no': item.serial_no,
'company': frm.doc.company
- }
+ };
frappe.call({
method: 'erpnext.stock.utils.get_incoming_rate',
From aaa84a21ba8c1749735c6510c5f311c3db505aef Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Wed, 23 Feb 2022 10:54:44 +0530
Subject: [PATCH 141/447] fix: Email translations
---
erpnext/translations/de.csv | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index f345a87d03..b882b9d3c1 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -3731,7 +3731,7 @@ Earliest Age,Frühestes Alter,
Edit Details,Details bearbeiten,
Edit Profile,Profil bearbeiten,
Either GST Transporter ID or Vehicle No is required if Mode of Transport is Road,Bei Straßentransport ist entweder die GST-Transporter-ID oder die Fahrzeug-Nr. Erforderlich,
-Email,Email,
+Email,E-Mail,
Email Campaigns,E-Mail-Kampagnen,
Employee ID is linked with another instructor,Die Mitarbeiter-ID ist mit einem anderen Ausbilder verknüpft,
Employee Tax and Benefits,Mitarbeitersteuern und -leistungen,
@@ -6487,7 +6487,7 @@ Select Users,Wählen Sie Benutzer aus,
Send Emails At,Die E-Mails senden um,
Reminder,Erinnerung,
Daily Work Summary Group User,Tägliche Arbeit Zusammenfassung Gruppenbenutzer,
-email,Email,
+email,E-Mail,
Parent Department,Elternabteilung,
Leave Block List,Urlaubssperrenliste,
Days for which Holidays are blocked for this department.,"Tage, an denen eine Urlaubssperre für diese Abteilung gilt.",
From bf8743713dcb318958c01e693c6d1071fd5fc218 Mon Sep 17 00:00:00 2001
From: marination
Date: Wed, 23 Feb 2022 14:03:48 +0530
Subject: [PATCH 142/447] chore: Rollback after each test, due to premature
commit via `remove_user_permission`
- `remove_user_permission` in `test_warehouse_user` calls delete_doc that enqueues dynamic link deletion
- Execution of background job eventually commits
- While in the test suite it runs sequentially in the same thread and commits whatever was done until then
- Which is why the rollback in `tearDownClass` is quite useless here
- This premature commit causes many illegal transactions caught by `assertRaises` to be committed in the db
- This creates faulty/dirty ledgers and breaks reports, as outiside the test suite this shouldn't/wouldn't happen
- Rollback after each test, and for `test_warehouse_user` in particular, manually cancel transaction
---
erpnext/stock/doctype/stock_entry/test_stock_entry.py | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 6c6513beff..7ab41418ae 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -44,6 +44,7 @@ def get_sle(**args):
class TestStockEntry(ERPNextTestCase):
def tearDown(self):
+ frappe.db.rollback()
frappe.set_user("Administrator")
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
@@ -565,6 +566,7 @@ class TestStockEntry(ERPNextTestCase):
st1.set_stock_entry_type()
st1.insert()
st1.submit()
+ st1.cancel()
frappe.set_user("Administrator")
remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com")
@@ -1023,13 +1025,10 @@ class TestStockEntry(ERPNextTestCase):
# Check if FG cost is calculated based on RM total cost
# RM total cost = 200, FG rate = 200/4(FG qty) = 50
- self.assertEqual(se.items[1].basic_rate, 50)
+ self.assertEqual(se.items[1].basic_rate, flt(se.items[0].basic_rate/4))
self.assertEqual(se.value_difference, 0.0)
self.assertEqual(se.total_incoming_value, se.total_outgoing_value)
- # teardown
- se.delete()
-
@change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_future_negative_sle(self):
# Initialize item, batch, warehouse, opening qty
From 9c7df2eec5a4c53545e43cae8a9cd5d9dda1a029 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 22 Feb 2022 20:53:19 +0530
Subject: [PATCH 143/447] fix: ignore duplicates explicitly
---
.../bank_transaction/test_bank_transaction.py | 14 +-
.../report/tax_detail/test_tax_detail.py | 2 +-
erpnext/accounts/utils.py | 2 +-
erpnext/assets/doctype/asset/test_asset.py | 4 +-
.../asset_category/test_asset_category.py | 2 +-
.../buying/doctype/supplier/test_supplier.py | 227 +++++++++---------
.../shopping_cart/test_shopping_cart.py | 2 +-
.../tally_migration/tally_migration.py | 2 +-
erpnext/hr/doctype/employee/employee.py | 2 +-
.../doctype/exit_interview/exit_interview.py | 2 +-
.../doctype/vehicle_log/test_vehicle_log.py | 2 +-
.../homepage_section/test_homepage_section.py | 2 +-
erpnext/regional/india/setup.py | 5 +-
erpnext/setup/utils.py | 2 +-
erpnext/stock/doctype/batch/test_batch.py | 7 +-
15 files changed, 136 insertions(+), 141 deletions(-)
diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
index 72b6893faf..d84b8e07d3 100644
--- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
@@ -109,7 +109,7 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
frappe.get_doc({
"doctype": "Bank",
"bank_name":bank_name,
- }).insert()
+ }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
@@ -119,7 +119,7 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
"account_name":"Checking Account",
"bank": bank_name,
"account": account_name
- }).insert()
+ }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
@@ -184,7 +184,7 @@ def add_vouchers():
"supplier_group":"All Supplier Groups",
"supplier_type": "Company",
"supplier_name": "Conrad Electronic"
- }).insert()
+ }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
@@ -203,7 +203,7 @@ def add_vouchers():
"supplier_group":"All Supplier Groups",
"supplier_type": "Company",
"supplier_name": "Mr G"
- }).insert()
+ }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
@@ -227,7 +227,7 @@ def add_vouchers():
"supplier_group":"All Supplier Groups",
"supplier_type": "Company",
"supplier_name": "Poore Simon's"
- }).insert()
+ }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
@@ -237,7 +237,7 @@ def add_vouchers():
"customer_group":"All Customer Groups",
"customer_type": "Company",
"customer_name": "Poore Simon's"
- }).insert()
+ }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
@@ -266,7 +266,7 @@ def add_vouchers():
"customer_group":"All Customer Groups",
"customer_type": "Company",
"customer_name": "Fayva"
- }).insert()
+ }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
diff --git a/erpnext/accounts/report/tax_detail/test_tax_detail.py b/erpnext/accounts/report/tax_detail/test_tax_detail.py
index bf668ab779..621de825ea 100644
--- a/erpnext/accounts/report/tax_detail/test_tax_detail.py
+++ b/erpnext/accounts/report/tax_detail/test_tax_detail.py
@@ -61,7 +61,7 @@ class TestTaxDetail(unittest.TestCase):
# Create GL Entries:
db_doc.submit()
else:
- db_doc.insert()
+ db_doc.insert(ignore_if_duplicate=True)
except frappe.exceptions.DuplicateEntryError:
pass
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 39e84e3cef..b17b90ba6e 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -847,7 +847,7 @@ def create_payment_gateway_account(gateway, payment_channel="Email"):
"payment_account": bank_account.name,
"currency": bank_account.account_currency,
"payment_channel": payment_channel
- }).insert(ignore_permissions=True)
+ }).insert(ignore_permissions=True, ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
# already exists, due to a reinstall?
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index ddbff89fc7..ffd1065efc 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -1280,7 +1280,7 @@ def create_asset(**args):
if not args.do_not_save:
try:
- asset.save()
+ asset.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
@@ -1321,7 +1321,7 @@ def create_fixed_asset_item(item_code=None, auto_create_assets=1, is_grouped_ass
"is_grouped_asset": is_grouped_asset,
"asset_naming_series": naming_series
})
- item.insert()
+ item.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
return item
diff --git a/erpnext/assets/doctype/asset_category/test_asset_category.py b/erpnext/assets/doctype/asset_category/test_asset_category.py
index 3d19fa39d1..2f52248edb 100644
--- a/erpnext/assets/doctype/asset_category/test_asset_category.py
+++ b/erpnext/assets/doctype/asset_category/test_asset_category.py
@@ -23,7 +23,7 @@ class TestAssetCategory(unittest.TestCase):
})
try:
- asset_category.insert()
+ asset_category.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py
index 13fe9df13e..0fb81b2578 100644
--- a/erpnext/buying/doctype/supplier/test_supplier.py
+++ b/erpnext/buying/doctype/supplier/test_supplier.py
@@ -14,151 +14,150 @@ test_records = frappe.get_test_records('Supplier')
class TestSupplier(unittest.TestCase):
- def test_get_supplier_group_details(self):
- doc = frappe.new_doc("Supplier Group")
- doc.supplier_group_name = "_Testing Supplier Group"
- doc.payment_terms = "_Test Payment Term Template 3"
- doc.accounts = []
- test_account_details = {
- "company": "_Test Company",
- "account": "Creditors - _TC",
- }
- doc.append("accounts", test_account_details)
- doc.save()
- s_doc = frappe.new_doc("Supplier")
- s_doc.supplier_name = "Testing Supplier"
- s_doc.supplier_group = "_Testing Supplier Group"
- s_doc.payment_terms = ""
- s_doc.accounts = []
- s_doc.insert()
- s_doc.get_supplier_group_details()
- self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3")
- self.assertEqual(s_doc.accounts[0].company, "_Test Company")
- self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC")
- s_doc.delete()
- doc.delete()
+ def test_get_supplier_group_details(self):
+ doc = frappe.new_doc("Supplier Group")
+ doc.supplier_group_name = "_Testing Supplier Group"
+ doc.payment_terms = "_Test Payment Term Template 3"
+ doc.accounts = []
+ test_account_details = {
+ "company": "_Test Company",
+ "account": "Creditors - _TC",
+ }
+ doc.append("accounts", test_account_details)
+ doc.save()
+ s_doc = frappe.new_doc("Supplier")
+ s_doc.supplier_name = "Testing Supplier"
+ s_doc.supplier_group = "_Testing Supplier Group"
+ s_doc.payment_terms = ""
+ s_doc.accounts = []
+ s_doc.insert()
+ s_doc.get_supplier_group_details()
+ self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3")
+ self.assertEqual(s_doc.accounts[0].company, "_Test Company")
+ self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC")
+ s_doc.delete()
+ doc.delete()
- def test_supplier_default_payment_terms(self):
- # Payment Term based on Days after invoice date
- frappe.db.set_value(
- "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 3")
+ def test_supplier_default_payment_terms(self):
+ # Payment Term based on Days after invoice date
+ frappe.db.set_value(
+ "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 3")
- due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
- self.assertEqual(due_date, "2016-02-21")
+ due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
+ self.assertEqual(due_date, "2016-02-21")
- due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
- self.assertEqual(due_date, "2017-02-21")
+ due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
+ self.assertEqual(due_date, "2017-02-21")
- # Payment Term based on last day of month
- frappe.db.set_value(
- "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 1")
+ # Payment Term based on last day of month
+ frappe.db.set_value(
+ "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 1")
- due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
- self.assertEqual(due_date, "2016-02-29")
+ due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
+ self.assertEqual(due_date, "2016-02-29")
- due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
- self.assertEqual(due_date, "2017-02-28")
+ due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
+ self.assertEqual(due_date, "2017-02-28")
- frappe.db.set_value("Supplier", "_Test Supplier With Template 1", "payment_terms", "")
+ frappe.db.set_value("Supplier", "_Test Supplier With Template 1", "payment_terms", "")
- # Set credit limit for the supplier group instead of supplier and evaluate the due date
- frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 3")
+ # Set credit limit for the supplier group instead of supplier and evaluate the due date
+ frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 3")
- due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
- self.assertEqual(due_date, "2016-02-21")
+ due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
+ self.assertEqual(due_date, "2016-02-21")
- # Payment terms for Supplier Group instead of supplier and evaluate the due date
- frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 1")
+ # Payment terms for Supplier Group instead of supplier and evaluate the due date
+ frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 1")
- # Leap year
- due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
- self.assertEqual(due_date, "2016-02-29")
- # # Non Leap year
- due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
- self.assertEqual(due_date, "2017-02-28")
+ # Leap year
+ due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
+ self.assertEqual(due_date, "2016-02-29")
+ # # Non Leap year
+ due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
+ self.assertEqual(due_date, "2017-02-28")
- # Supplier with no default Payment Terms Template
- frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "")
- frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", "")
+ # Supplier with no default Payment Terms Template
+ frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "")
+ frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", "")
- due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier")
- self.assertEqual(due_date, "2016-01-22")
- # # Non Leap year
- due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier")
- self.assertEqual(due_date, "2017-01-22")
+ due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier")
+ self.assertEqual(due_date, "2016-01-22")
+ # # Non Leap year
+ due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier")
+ self.assertEqual(due_date, "2017-01-22")
- def test_supplier_disabled(self):
- make_test_records("Item")
+ def test_supplier_disabled(self):
+ make_test_records("Item")
- frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 1)
+ frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 1)
- from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
+ from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
- po = create_purchase_order(do_not_save=True)
+ po = create_purchase_order(do_not_save=True)
- self.assertRaises(PartyDisabled, po.save)
+ self.assertRaises(PartyDisabled, po.save)
- frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 0)
+ frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 0)
- po.save()
+ po.save()
- def test_supplier_country(self):
- # Test that country field exists in Supplier DocType
- supplier = frappe.get_doc('Supplier', '_Test Supplier with Country')
- self.assertTrue('country' in supplier.as_dict())
+ def test_supplier_country(self):
+ # Test that country field exists in Supplier DocType
+ supplier = frappe.get_doc('Supplier', '_Test Supplier with Country')
+ self.assertTrue('country' in supplier.as_dict())
- # Test if test supplier field record is 'Greece'
- self.assertEqual(supplier.country, "Greece")
+ # Test if test supplier field record is 'Greece'
+ self.assertEqual(supplier.country, "Greece")
- # Test update Supplier instance country value
- supplier = frappe.get_doc('Supplier', '_Test Supplier')
- supplier.country = 'Greece'
- supplier.save()
- self.assertEqual(supplier.country, "Greece")
+ # Test update Supplier instance country value
+ supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier.country = 'Greece'
+ supplier.save()
+ self.assertEqual(supplier.country, "Greece")
- def test_party_details_tax_category(self):
- from erpnext.accounts.party import get_party_details
+ def test_party_details_tax_category(self):
+ from erpnext.accounts.party import get_party_details
- frappe.delete_doc_if_exists("Address", "_Test Address With Tax Category-Billing")
+ frappe.delete_doc_if_exists("Address", "_Test Address With Tax Category-Billing")
- # Tax Category without Address
- details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier")
- self.assertEqual(details.tax_category, "_Test Tax Category 1")
+ # Tax Category without Address
+ details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier")
+ self.assertEqual(details.tax_category, "_Test Tax Category 1")
- address = frappe.get_doc(dict(
- doctype='Address',
- address_title='_Test Address With Tax Category',
- tax_category='_Test Tax Category 2',
- address_type='Billing',
- address_line1='Station Road',
- city='_Test City',
- country='India',
- links=[dict(
- link_doctype='Supplier',
- link_name='_Test Supplier With Tax Category'
- )]
- )).insert()
+ address = frappe.get_doc(dict(
+ doctype='Address',
+ address_title='_Test Address With Tax Category',
+ tax_category='_Test Tax Category 2',
+ address_type='Billing',
+ address_line1='Station Road',
+ city='_Test City',
+ country='India',
+ links=[dict(
+ link_doctype='Supplier',
+ link_name='_Test Supplier With Tax Category'
+ )]
+ )).insert()
- # Tax Category with Address
- details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier")
- self.assertEqual(details.tax_category, "_Test Tax Category 2")
+ # Tax Category with Address
+ details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier")
+ self.assertEqual(details.tax_category, "_Test Tax Category 2")
- # Rollback
- address.delete()
+ # Rollback
+ address.delete()
def create_supplier(**args):
- args = frappe._dict(args)
+ args = frappe._dict(args)
- try:
- doc = frappe.get_doc({
- "doctype": "Supplier",
- "supplier_name": args.supplier_name,
- "supplier_group": args.supplier_group or "Services",
- "supplier_type": args.supplier_type or "Company",
- "tax_withholding_category": args.tax_withholding_category
- }).insert()
+ if frappe.db.exists("Supplier", args.supplier_name):
+ return frappe.get_doc("Supplier", args.supplier_name)
- return doc
+ doc = frappe.get_doc({
+ "doctype": "Supplier",
+ "supplier_name": args.supplier_name,
+ "supplier_group": args.supplier_group or "Services",
+ "supplier_type": args.supplier_type or "Company",
+ "tax_withholding_category": args.tax_withholding_category
+ }).insert()
- except frappe.DuplicateEntryError:
- return frappe.get_doc("Supplier", args.supplier_name)
+ return doc
diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
index 8519e68d09..6be8c94ad9 100644
--- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
+++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
@@ -175,7 +175,7 @@ class TestShoppingCart(unittest.TestCase):
def create_tax_rule(self):
tax_rule = frappe.get_test_records("Tax Rule")[0]
try:
- frappe.get_doc(tax_rule).insert()
+ frappe.get_doc(tax_rule).insert(ignore_if_duplicate=True)
except (frappe.DuplicateEntryError, ConflictingTaxRule):
pass
diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
index 54ed6f7d11..26bd19f010 100644
--- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
+++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
@@ -82,7 +82,7 @@ class TallyMigration(Document):
"is_private": True
})
try:
- f.insert()
+ f.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
setattr(self, key, f.file_url)
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index a2df26c3e2..6e52eb97ca 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -142,7 +142,7 @@ class Employee(NestedSet):
"file_url": self.image,
"attached_to_doctype": "User",
"attached_to_name": self.user_id
- }).insert()
+ }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
# already exists
pass
diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py
index 30e19f1c9b..59fb2fd9ca 100644
--- a/erpnext/hr/doctype/exit_interview/exit_interview.py
+++ b/erpnext/hr/doctype/exit_interview/exit_interview.py
@@ -128,4 +128,4 @@ def show_email_summary(email_success, email_failure):
message += _('{0} due to missing email information for employee(s): {1}').format(
frappe.bold('Sending Failed'), ', '.join(email_failure))
- frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True)
\ No newline at end of file
+ frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True)
diff --git a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py
index acd50f278c..abb288723c 100644
--- a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py
+++ b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py
@@ -82,7 +82,7 @@ def get_vehicle(employee_id):
"vehicle_value": flt(500000)
})
try:
- vehicle.insert()
+ vehicle.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
return license_plate
diff --git a/erpnext/portal/doctype/homepage_section/test_homepage_section.py b/erpnext/portal/doctype/homepage_section/test_homepage_section.py
index b30d983adc..c3be146bec 100644
--- a/erpnext/portal/doctype/homepage_section/test_homepage_section.py
+++ b/erpnext/portal/doctype/homepage_section/test_homepage_section.py
@@ -21,7 +21,7 @@ class TestHomepageSection(unittest.TestCase):
{'title': 'Card 2', 'subtitle': 'Subtitle 2', 'content': 'This is test card 2', 'image': 'test.jpg'},
],
'no_of_columns': 3
- }).insert()
+ }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index 074bd527e2..e835690969 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -53,10 +53,7 @@ def create_hsn_codes(data, code_field):
hsn_code.description = d["description"]
hsn_code.hsn_code = d[code_field]
hsn_code.name = d[code_field]
- try:
- hsn_code.db_insert()
- except frappe.DuplicateEntryError:
- pass
+ hsn_code.db_insert(ignore_if_duplicate=True)
def add_custom_roles_for_reports():
for report_name in ('GST Sales Register', 'GST Purchase Register',
diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py
index 4441bb9562..a4f2207f11 100644
--- a/erpnext/setup/utils.py
+++ b/erpnext/setup/utils.py
@@ -155,7 +155,7 @@ def insert_record(records):
doc = frappe.new_doc(r.get("doctype"))
doc.update(r)
try:
- doc.insert(ignore_permissions=True)
+ doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
except frappe.DuplicateEntryError as e:
# pass DuplicateEntryError and continue
if e.args and e.args[0]==doc.doctype and e.args[1]==doc.name:
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index baa03024af..613dd3f14d 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -433,14 +433,13 @@ def create_price_list_for_batch(item_code, batch, rate):
def make_new_batch(**args):
args = frappe._dict(args)
- try:
+ if frappe.db.exists("Batch", args.batch_id):
+ batch = frappe.get_doc("Batch", args.batch_id)
+ else:
batch = frappe.get_doc({
"doctype": "Batch",
"batch_id": args.batch_id,
"item": args.item_code,
}).insert()
- except frappe.DuplicateEntryError:
- batch = frappe.get_doc("Batch", args.batch_id)
-
return batch
From 5ff3705872960190031066dbe34158ad3c659820 Mon Sep 17 00:00:00 2001
From: marination
Date: Wed, 23 Feb 2022 14:54:16 +0530
Subject: [PATCH 144/447] test: Make Variant if absent in
`test_variant_work_order`, keep test atomic
---
erpnext/stock/doctype/stock_entry/test_stock_entry.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 7ab41418ae..c5afa49166 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -691,6 +691,8 @@ class TestStockEntry(ERPNextTestCase):
bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item",
"is_default": 1, "docstatus": 1})
+ make_item_variant() # make variant of _Test Variant Item if absent
+
work_order = frappe.new_doc("Work Order")
work_order.update({
"company": "_Test Company",
From b44cead3179cf200e382895cd415d6a6e79d7fca Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 23 Feb 2022 16:17:41 +0530
Subject: [PATCH 145/447] test: fix flaky stateful tests (#29749)
Co-Authored-By: Marica
---
.../opening_invoice_creation_tool.py | 2 +-
.../test_opening_invoice_creation_tool.py | 39 ++++++++-----------
2 files changed, 17 insertions(+), 24 deletions(-)
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
index ade7f8146b..6e7b80e731 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
@@ -166,7 +166,7 @@ class OpeningInvoiceCreationTool(Document):
frappe.scrub(row.party_type): row.party,
"is_pos": 0,
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
- "update_stock": 0,
+ "update_stock": 0, # important: https://github.com/frappe/erpnext/pull/23559
"invoice_number": row.invoice_number,
"disable_rounded_total": 1
})
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
index 6700e9b975..3eaf6a28f3 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
@@ -1,11 +1,7 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-import unittest
-
import frappe
-from frappe.cache_manager import clear_doctype_cache
-from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
@@ -14,14 +10,17 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
get_temporary_opening_account,
)
+from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
-class TestOpeningInvoiceCreationTool(unittest.TestCase):
- def setUp(self):
+class TestOpeningInvoiceCreationTool(ERPNextTestCase):
+ @classmethod
+ def setUpClass(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company()
create_dimension()
+ return super().setUpClass()
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None):
doc = frappe.get_single("Opening Invoice Creation Tool")
@@ -31,26 +30,20 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
return doc.make_invoices()
def test_opening_sales_invoice_creation(self):
- property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check")
- try:
- invoices = self.make_invoices(company="_Test Opening Invoice Company")
+ invoices = self.make_invoices(company="_Test Opening Invoice Company")
- self.assertEqual(len(invoices), 2)
- expected_value = {
- "keys": ["customer", "outstanding_amount", "status"],
- 0: ["_Test Customer", 300, "Overdue"],
- 1: ["_Test Customer 1", 250, "Overdue"],
- }
- self.check_expected_values(invoices, expected_value)
+ self.assertEqual(len(invoices), 2)
+ expected_value = {
+ "keys": ["customer", "outstanding_amount", "status"],
+ 0: ["_Test Customer", 300, "Overdue"],
+ 1: ["_Test Customer 1", 250, "Overdue"],
+ }
+ self.check_expected_values(invoices, expected_value)
- si = frappe.get_doc("Sales Invoice", invoices[0])
+ si = frappe.get_doc("Sales Invoice", invoices[0])
- # Check if update stock is not enabled
- self.assertEqual(si.update_stock, 0)
-
- finally:
- property_setter.delete()
- clear_doctype_cache("Sales Invoice")
+ # Check if update stock is not enabled
+ self.assertEqual(si.update_stock, 0)
def check_expected_values(self, invoices, expected_value, invoice_type="Sales"):
doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice"
From a33f04ea41087305c95111aa86bf96d3df2e2b36 Mon Sep 17 00:00:00 2001
From: marination
Date: Wed, 23 Feb 2022 16:26:20 +0530
Subject: [PATCH 146/447] fix: Check if both old and new items have bundles
before merging
- If only one has bundle against it, they can be merged
---
erpnext/stock/doctype/item/item.py | 8 +++++---
erpnext/stock/doctype/item/test_item.py | 1 +
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index d984d6eb99..494fb3b8bb 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -465,9 +465,11 @@ class Item(Document):
def validate_duplicate_product_bundles_before_merge(self, old_name, new_name):
"Block merge if both old and new items have product bundles."
- bundle = frappe.get_value("Product Bundle",filters={"new_item_code": old_name})
- if bundle:
- bundle_link = get_link_to_form("Product Bundle", bundle)
+ old_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": old_name})
+ new_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": new_name})
+
+ if old_bundle and new_bundle:
+ bundle_link = get_link_to_form("Product Bundle", old_bundle)
old_name, new_name = frappe.bold(old_name), frappe.bold(new_name)
msg = _("Please delete Product Bundle {0}, before merging {1} into {2}").format(
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 6f5f1ff786..9491e17259 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -397,6 +397,7 @@ class TestItem(ERPNextTestCase):
create_item("Test Item inside Bundle")
bundle_items = ["Test Item inside Bundle"]
+ # make bundles for both items
bundle1 = make_product_bundle("Test Item Bundle Item 1", bundle_items, qty=2)
make_product_bundle("Test Item Bundle Item 2", bundle_items, qty=2)
From 239733acd1ad770fc58b5bedd2cf0bb6e75c569d Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Wed, 23 Feb 2022 20:41:49 +0530
Subject: [PATCH 147/447] fix: Make abbreviation limit to 10
---
erpnext/public/js/setup_wizard.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js
index e746ce9ae0..83b69aebc5 100644
--- a/erpnext/public/js/setup_wizard.js
+++ b/erpnext/public/js/setup_wizard.js
@@ -78,11 +78,11 @@ erpnext.setup.slides_settings = [
slide.get_input("company_name").on("change", function () {
var parts = slide.get_input("company_name").val().split(" ");
var abbr = $.map(parts, function (p) { return p ? p.substr(0, 1) : null }).join("");
- slide.get_field("company_abbr").set_value(abbr.slice(0, 5).toUpperCase());
+ slide.get_field("company_abbr").set_value(abbr.slice(0, 10).toUpperCase());
}).val(frappe.boot.sysdefaults.company_name || "").trigger("change");
slide.get_input("company_abbr").on("change", function () {
- if (slide.get_input("company_abbr").val().length > 5) {
+ if (slide.get_input("company_abbr").val().length > 10) {
frappe.msgprint(__("Company Abbreviation cannot have more than 5 characters"));
slide.get_field("company_abbr").set_value("");
}
@@ -96,7 +96,7 @@ erpnext.setup.slides_settings = [
if (!this.values.company_abbr) {
return false;
}
- if (this.values.company_abbr.length > 5) {
+ if (this.values.company_abbr.length > 10) {
return false;
}
return true;
From b1a46c80d5571d23a749656c98056ba1411b433f Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 24 Feb 2022 13:11:17 +0530
Subject: [PATCH 148/447] fix: Total taxes and charges in payment entry for
multicurrency payments
---
.../doctype/payment_entry/payment_entry.js | 13 ++++++++++---
.../doctype/payment_entry/payment_entry.json | 14 +++++++++++++-
.../doctype/payment_entry/payment_entry.py | 8 ++++++--
3 files changed, 29 insertions(+), 6 deletions(-)
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 3be3925b5a..c14251b7d2 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -196,8 +196,14 @@ frappe.ui.form.on('Payment Entry', {
frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency));
frm.toggle_display("base_paid_amount", frm.doc.paid_from_account_currency != company_currency);
- frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
- (frm.doc.paid_from_account_currency != company_currency));
+
+ if (frm.doc.payment_type == "Pay") {
+ frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
+ (frm.doc.paid_to_account_currency != company_currency));
+ } else {
+ frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
+ (frm.doc.paid_from_account_currency != company_currency));
+ }
frm.toggle_display("base_received_amount", (
frm.doc.paid_to_account_currency != company_currency
@@ -232,7 +238,8 @@ frappe.ui.form.on('Payment Entry', {
var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company).default_currency: "";
frm.set_currency_labels(["base_paid_amount", "base_received_amount", "base_total_allocated_amount",
- "difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax"], company_currency);
+ "difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax",
+ "base_total_taxes_and_charges"], company_currency);
frm.set_currency_labels(["paid_amount"], frm.doc.paid_from_account_currency);
frm.set_currency_labels(["received_amount"], frm.doc.paid_to_account_currency);
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index c8d1db91f5..3fc1adff2d 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -66,7 +66,9 @@
"tax_withholding_category",
"section_break_56",
"taxes",
+ "section_break_60",
"base_total_taxes_and_charges",
+ "column_break_61",
"total_taxes_and_charges",
"deductions_or_loss_section",
"deductions",
@@ -715,12 +717,21 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Paid To Account Type"
+ },
+ {
+ "fieldname": "column_break_61",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_60",
+ "fieldtype": "Section Break",
+ "hide_border": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-11-24 18:58:24.919764",
+ "modified": "2022-02-23 20:08:39.559814",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
@@ -763,6 +774,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "title",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 02a144d3e7..6ec15e1e87 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -945,8 +945,12 @@ class PaymentEntry(AccountsController):
tax.base_total = tax.total * self.source_exchange_rate
- self.total_taxes_and_charges += current_tax_amount
- self.base_total_taxes_and_charges += current_tax_amount * self.source_exchange_rate
+ if self.payment_type == 'Pay':
+ self.base_total_taxes_and_charges += current_tax_amount * self.source_exchange_rate
+ self.total_taxes_and_charges += current_tax_amount * self.target_exchange_rate
+ else:
+ self.base_total_taxes_and_charges += current_tax_amount * self.target_exchange_rate
+ self.total_taxes_and_charges += current_tax_amount * self.source_exchange_rate
if self.get('taxes'):
self.paid_amount_after_tax = self.get('taxes')[-1].base_total
From 03739147049ed78bd2bb43f5f47cfc70d6be43ba Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 24 Feb 2022 14:42:56 +0530
Subject: [PATCH 149/447] fix: Commission not applied while making Sales Order
from Quotation
---
.../doctype/sales_invoice_item/sales_invoice_item.json | 6 ++++--
.../doctype/sales_order_item/sales_order_item.json | 7 +++++--
.../doctype/delivery_note_item/delivery_note_item.json | 9 ++++++---
3 files changed, 15 insertions(+), 7 deletions(-)
diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
index ae9ac35729..2901cf0888 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
@@ -832,6 +832,7 @@
},
{
"default": "0",
+ "fetch_from": "item_code.grant_commission",
"fieldname": "grant_commission",
"fieldtype": "Check",
"label": "Grant Commission",
@@ -841,7 +842,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-10-05 12:24:54.968907",
+ "modified": "2022-02-24 14:41:36.392560",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
@@ -849,5 +850,6 @@
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
index 080d517d13..7e55499533 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -83,8 +83,8 @@
"planned_qty",
"column_break_69",
"work_order_qty",
- "produced_qty",
"delivered_qty",
+ "produced_qty",
"returned_qty",
"shopping_cart_section",
"additional_notes",
@@ -701,8 +701,10 @@
"width": "50px"
},
{
+ "description": "For Production",
"fieldname": "produced_qty",
"fieldtype": "Float",
+ "hidden": 1,
"label": "Produced Quantity",
"oldfieldname": "produced_qty",
"oldfieldtype": "Currency",
@@ -791,6 +793,7 @@
},
{
"default": "0",
+ "fetch_from": "item_code.grant_commission",
"fieldname": "grant_commission",
"fieldtype": "Check",
"label": "Grant Commission",
@@ -800,7 +803,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2022-02-21 13:55:08.883104",
+ "modified": "2022-02-24 14:41:57.325799",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
index 51c88bed61..f1f5d96e62 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -757,6 +757,7 @@
},
{
"default": "0",
+ "fetch_from": "item_code.grant_commission",
"fieldname": "grant_commission",
"fieldtype": "Check",
"label": "Grant Commission",
@@ -767,12 +768,14 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-10-06 12:12:44.018872",
+ "modified": "2022-02-24 14:42:20.211085",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
- "sort_order": "DESC"
-}
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
From 1e139cf9a115228b993c83be29415b1f6971b33e Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Thu, 24 Feb 2022 15:12:12 +0530
Subject: [PATCH 150/447] chore: remove unintentional search index
---
.../buying/doctype/purchase_order_item/purchase_order_item.json | 1 -
1 file changed, 1 deletion(-)
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
index c26d592e3e..2c9fc36794 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -845,7 +845,6 @@
"label": "Sales Order Packed Item",
"no_copy": 1,
"print_hide": 1,
- "search_index": 1
}
],
"idx": 1,
From 2f1709dfef55ed17db3e2ae885aef5588dd7a31a Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Thu, 24 Feb 2022 15:12:35 +0530
Subject: [PATCH 151/447] chore: remove unintentional search index
---
.../buying/doctype/purchase_order_item/purchase_order_item.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
index 2c9fc36794..a18c527644 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -844,7 +844,7 @@
"fieldtype": "Data",
"label": "Sales Order Packed Item",
"no_copy": 1,
- "print_hide": 1,
+ "print_hide": 1
}
],
"idx": 1,
From 6d72aa377f98f07f28312074b453a97e482998e8 Mon Sep 17 00:00:00 2001
From: marination
Date: Thu, 24 Feb 2022 15:20:18 +0530
Subject: [PATCH 152/447] feat: Combine Sub-assembly items in Production Plan
- Combine subassembly rows if same Item, Warehouse, Inhouse/Outhouse Manu.g, BOM No.
---
erpnext/manufacturing/doctype/bom/bom.py | 2 +-
.../production_plan/production_plan.js | 9 +++-
.../production_plan/production_plan.json | 11 ++++-
.../production_plan/production_plan.py | 49 ++++++++++++++++---
4 files changed, 61 insertions(+), 10 deletions(-)
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index d640f3fda7..37d2b9ff97 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -918,7 +918,7 @@ def validate_bom_no(item, bom_no):
frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item))
@frappe.whitelist()
-def get_children(doctype, parent=None, is_root=False, **filters):
+def get_children(parent=None, is_root=False, **filters):
if not parent or parent=="BOM":
frappe.msgprint(_('Please select a BOM'))
return
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index 0babf875e7..c78ff51a7c 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -232,7 +232,7 @@ frappe.ui.form.on('Production Plan', {
});
},
combine_items: function (frm) {
- frm.clear_table('prod_plan_references');
+ frm.clear_table("prod_plan_references");
frappe.call({
method: "get_items",
@@ -247,6 +247,13 @@ frappe.ui.form.on('Production Plan', {
});
},
+ combine_sub_items: (frm) => {
+ if (frm.doc.sub_assembly_items.length > 0) {
+ frm.clear_table("sub_assembly_items");
+ frm.trigger("get_sub_assembly_items");
+ }
+ },
+
get_sub_assembly_items: function(frm) {
frm.dirty();
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json
index 56cf2b4f08..3bfb764ba5 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.json
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json
@@ -36,6 +36,7 @@
"prod_plan_references",
"section_break_24",
"get_sub_assembly_items",
+ "combine_sub_items",
"sub_assembly_items",
"material_request_planning",
"include_non_stock_items",
@@ -340,7 +341,6 @@
{
"fieldname": "prod_plan_references",
"fieldtype": "Table",
- "hidden": 1,
"label": "Production Plan Item Reference",
"options": "Production Plan Item Reference"
},
@@ -370,16 +370,23 @@
"fieldname": "to_delivery_date",
"fieldtype": "Date",
"label": "To Delivery Date"
+ },
+ {
+ "default": "0",
+ "fieldname": "combine_sub_items",
+ "fieldtype": "Check",
+ "label": "Consolidate Sub Assembly Items"
}
],
"icon": "fa fa-calendar",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-09-06 18:35:59.642232",
+ "modified": "2022-02-23 17:16:10.629378",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 80003dab78..48cd753d75 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -21,7 +21,8 @@ from frappe.utils import (
)
from frappe.utils.csvutils import build_csv_response
-from erpnext.manufacturing.doctype.bom.bom import get_children, validate_bom_no
+from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_children
+from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
@@ -570,17 +571,28 @@ class ProductionPlan(Document):
@frappe.whitelist()
def get_sub_assembly_items(self, manufacturing_type=None):
+ "Fetch sub assembly items and optionally combine them."
self.sub_assembly_items = []
+ sub_assembly_items_store = [] # temporary store to process all subassembly items
+
for row in self.po_items:
bom_data = []
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
+ sub_assembly_items_store.extend(bom_data)
- self.sub_assembly_items.sort(key= lambda d: d.bom_level, reverse=True)
- for idx, row in enumerate(self.sub_assembly_items, start=1):
- row.idx = idx
+ if self.combine_sub_items:
+ # Combine subassembly items
+ sub_assembly_items_store = self.combine_subassembly_items(sub_assembly_items_store)
+
+ sub_assembly_items_store.sort(key= lambda d: d.bom_level, reverse=True) # sort by bom level
+
+ for idx, row in enumerate(sub_assembly_items_store):
+ row.idx = idx + 1
+ self.append("sub_assembly_items", row)
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
+ "Modify bom_data, set additional details."
for data in bom_data:
data.qty = data.stock_qty
data.production_plan_item = row.name
@@ -589,7 +601,32 @@ class ProductionPlan(Document):
data.type_of_manufacturing = manufacturing_type or ("Subcontract" if data.is_sub_contracted_item
else "In House")
- self.append("sub_assembly_items", data)
+ def combine_subassembly_items(self, sub_assembly_items_store):
+ "Aggregate if same: Item, Warehouse, Inhouse/Outhouse Manu.g, BOM No."
+ key_wise_data = {}
+ for row in sub_assembly_items_store:
+ key = (
+ row.get("production_item"), row.get("fg_warehouse"),
+ row.get("bom_no"), row.get("type_of_manufacturing")
+ )
+ if key not in key_wise_data:
+ # intialise (item, wh, bom no, man.g type) wise dict
+ key_wise_data[key] = row
+ continue
+
+ existing_row = key_wise_data[key]
+ if existing_row:
+ # if row with same (item, wh, bom no, man.g type) key, merge
+ existing_row.qty += flt(row.qty)
+ existing_row.stock_qty += flt(row.stock_qty)
+ existing_row.bom_level = max(existing_row.bom_level, row.bom_level)
+ continue
+ else:
+ # add row with key
+ key_wise_data[key] = row
+
+ sub_assembly_items_store = [key_wise_data[key] for key in key_wise_data] # unpack into single level list
+ return sub_assembly_items_store
def all_items_completed(self):
all_items_produced = all(flt(d.planned_qty) - flt(d.produced_qty) < 0.000001
@@ -1031,7 +1068,7 @@ def get_item_data(item_code):
}
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
- data = get_children('BOM', parent = bom_no)
+ data = get_bom_children(parent=bom_no)
for d in data:
if d.expandable:
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
From cbb5ffb6feff3e6b47a3d815ab986e90b66b4db3 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 24 Feb 2022 15:58:12 +0530
Subject: [PATCH 153/447] fix: Multi-currency bank reconciliation fixes
---
.../bank_reconciliation_tool/bank_reconciliation_tool.py | 2 +-
.../doctype/bank_transaction/bank_transaction.py | 9 +++++++--
2 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index 4211bd0169..11c94f705d 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -231,7 +231,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
}), transaction.currency, company_account)
if total_amount > transaction.unallocated_amount:
- frappe.throw(_("The Sum Total of Amounts of All Selected Vouchers Should be Less than the Unallocated Amount of the Bank Transaction"))
+ frappe.throw(_("The sum total of amounts of all selected vouchers should be less than the unallocated amount of the bank transaction"))
account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
for voucher in vouchers:
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 51e1d6e9a0..655a1c2265 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -109,8 +109,13 @@ def get_paid_amount(payment_entry, currency, bank_account):
paid_amount_field = "paid_amount"
if payment_entry.payment_document == 'Payment Entry':
doc = frappe.get_doc("Payment Entry", payment_entry.payment_entry)
- paid_amount_field = ("base_paid_amount"
- if doc.paid_to_account_currency == currency else "paid_amount")
+
+ if doc.payment_type == 'Receive':
+ paid_amount_field = ("received_amount"
+ if doc.paid_to_account_currency == currency else "base_received_amount")
+ elif doc.payment_type == 'Pay':
+ paid_amount_field = ("paid_amount"
+ if doc.paid_to_account_currency == currency else "base_paid_amount")
return frappe.db.get_value(payment_entry.payment_document,
payment_entry.payment_entry, paid_amount_field)
From 761e528ba39d5acdbd0bb7dd8cec48705d80cfc3 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Thu, 24 Feb 2022 17:57:39 +0530
Subject: [PATCH 154/447] fix: deprecation warning
---
erpnext/patches/v13_0/non_profit_deprecation_warning.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/patches/v13_0/non_profit_deprecation_warning.py b/erpnext/patches/v13_0/non_profit_deprecation_warning.py
index c170de5c1c..5b54b25a5b 100644
--- a/erpnext/patches/v13_0/non_profit_deprecation_warning.py
+++ b/erpnext/patches/v13_0/non_profit_deprecation_warning.py
@@ -5,6 +5,6 @@ def execute():
click.secho(
"Non Profit Domain is moved to a separate app and will be removed from ERPNext in version-14.\n"
- "When upgrading to ERPNext version-14, please install the app to continue using the Agriculture domain: https://github.com/frappe/non_profit",
+ "When upgrading to ERPNext version-14, please install the app to continue using the Non Profit domain: https://github.com/frappe/non_profit",
fg="yellow",
)
From 04b9091821fbd64b1417883087f6a25bcab5a101 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Thu, 24 Feb 2022 18:10:29 +0530
Subject: [PATCH 155/447] fix: web form deletion
---
erpnext/patches/v14_0/delete_non_profit_doctypes.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/patches/v14_0/delete_non_profit_doctypes.py b/erpnext/patches/v14_0/delete_non_profit_doctypes.py
index d53aecca92..565b10cbb8 100644
--- a/erpnext/patches/v14_0/delete_non_profit_doctypes.py
+++ b/erpnext/patches/v14_0/delete_non_profit_doctypes.py
@@ -32,7 +32,7 @@ def execute():
forms = ['grant-application', 'certification-application', 'certification-application-usd']
for form in forms:
- frappe.delete_doc("Web Form", form, ignore_missing=True)
+ frappe.delete_doc("Web Form", form, ignore_missing=True, force=True)
custom_records = [
{"doctype": "Party Type", "name": "Member"},
From a1ae7fcbc8af9934d31acee1daa8564bcbc6edf6 Mon Sep 17 00:00:00 2001
From: Devin Slauenwhite
Date: Thu, 24 Feb 2022 16:36:30 -0500
Subject: [PATCH 156/447] fix: show fail message
---
erpnext/utilities/doctype/rename_tool/rename_tool.js | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/erpnext/utilities/doctype/rename_tool/rename_tool.js b/erpnext/utilities/doctype/rename_tool/rename_tool.js
index 5553e44ef8..5a2d249b42 100644
--- a/erpnext/utilities/doctype/rename_tool/rename_tool.js
+++ b/erpnext/utilities/doctype/rename_tool/rename_tool.js
@@ -30,7 +30,14 @@ frappe.ui.form.on("Rename Tool", {
select_doctype: frm.doc.select_doctype
},
callback: function(r) {
- frm.get_field("rename_log").$wrapper.html(r.message.join(" "));
+ let html = r.message.join(" ");
+
+ if (r.exc) {
+ r.exc = JSON.parse(r.exc);
+ html += " " + r.exc.join(" ");
+ }
+
+ frm.get_field("rename_log").$wrapper.html(html);
}
});
});
From d3b0ca30c6ae0e979b7bdddbe67018941be8d59b Mon Sep 17 00:00:00 2001
From: marination
Date: Fri, 25 Feb 2022 12:10:11 +0530
Subject: [PATCH 157/447] fix: Get MRs that are yet to be received but fully
ordered in Report
- Remove incorrect query clause that only check if ordered qty < 100
- MR should be visible in report until fully received (cycle complete)
---
.../requested_items_to_order_and_receive.py | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
index f98e5f12c2..2c597f29bf 100644
--- a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
+++ b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
@@ -18,10 +18,10 @@ def execute(filters=None):
columns = get_columns(filters)
conditions = get_conditions(filters)
- #get queried data
+ # get queried data
data = get_data(filters, conditions)
- #prepare data for report and chart views
+ # prepare data for report and chart views
data, chart_data = prepare_data(data, filters)
return columns, data, None, chart_data
@@ -74,10 +74,9 @@ def get_data(filters, conditions):
and mr.material_request_type = "Purchase"
and mr.docstatus = 1
and mr.status != "Stopped"
+ and mr.per_received < 100
{conditions}
group by mr.name, mr_item.item_code
- having
- sum(ifnull(mr_item.ordered_qty, 0)) < sum(ifnull(mr_item.stock_qty, 0))
order by mr.transaction_date, mr.schedule_date""".format(conditions=conditions), as_dict=1)
return data
From 69c34cd7ae128dde56cde10c53b479331c33d56f Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Fri, 25 Feb 2022 14:36:29 +0530
Subject: [PATCH 158/447] fix(pos): mode of payment disappears after save
---
erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 5229d87017..9b3b3aa414 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -439,7 +439,6 @@ class POSInvoice(SalesInvoice):
self.paid_amount = 0
def set_account_for_mode_of_payment(self):
- self.payments = [d for d in self.payments if d.amount or d.base_amount or d.default]
for pay in self.payments:
if not pay.account:
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
From 81514516f3c7106a5b211796bb74ddad0a6add20 Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Fri, 25 Feb 2022 15:18:06 +0530
Subject: [PATCH 159/447] fix(pos): coupon code is applied even if ignore
pricing rule is check
---
erpnext/public/js/controllers/transaction.js | 18 +++++++------
.../selling/page/point_of_sale/pos_payment.js | 25 +++++++++++--------
2 files changed, 25 insertions(+), 18 deletions(-)
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index ae8c0c8c6d..00373a6513 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -2285,13 +2285,17 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
coupon_code() {
- var me = this;
- frappe.run_serially([
- () => this.frm.doc.ignore_pricing_rule=1,
- () => me.ignore_pricing_rule(),
- () => this.frm.doc.ignore_pricing_rule=0,
- () => me.apply_pricing_rule()
- ]);
+ if (this.frm.doc.coupon_code || this.frm._last_coupon_code) {
+ // reset pricing rules if coupon code is set or is unset
+ const _ignore_pricing_rule = this.frm.doc.ignore_pricing_rule;
+ return frappe.run_serially([
+ () => this.frm.doc.ignore_pricing_rule=1,
+ () => this.frm.trigger('ignore_pricing_rule'),
+ () => this.frm.doc.ignore_pricing_rule=_ignore_pricing_rule,
+ () => this.frm.trigger('apply_pricing_rule'),
+ () => this.frm._last_coupon_code = this.frm.doc.coupon_code
+ ]);
+ }
}
};
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index 4d75e6ef1b..1e9f6d7d92 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -170,17 +170,20 @@ erpnext.PointOfSale.Payment = class {
});
frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => {
- if (!frm.doc.ignore_pricing_rule) {
- if (frm.doc.coupon_code) {
- frappe.run_serially([
- () => frm.doc.ignore_pricing_rule=1,
- () => frm.trigger('ignore_pricing_rule'),
- () => frm.doc.ignore_pricing_rule=0,
- () => frm.trigger('apply_pricing_rule'),
- () => frm.save(),
- () => this.update_totals_section(frm.doc)
- ]);
- }
+ if (!frm.doc.ignore_pricing_rule && frm.doc.coupon_code) {
+ frappe.run_serially([
+ () => frm.doc.ignore_pricing_rule=1,
+ () => frm.trigger('ignore_pricing_rule'),
+ () => frm.doc.ignore_pricing_rule=0,
+ () => frm.trigger('apply_pricing_rule'),
+ () => frm.save(),
+ () => this.update_totals_section(frm.doc)
+ ]);
+ } else if (frm.doc.ignore_pricing_rule && frm.doc.coupon_code) {
+ frappe.show_alert({
+ message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."),
+ indicator: "orange"
+ });
}
});
From fe2ced7bee1713067912787b3e238269adf451ac Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Fri, 25 Feb 2022 16:57:59 +0530
Subject: [PATCH 160/447] fix: Regional print format addition
---
erpnext/regional/india/setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index e835690969..2df52a204b 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -111,7 +111,7 @@ def add_permissions():
def add_print_formats():
frappe.reload_doc("regional", "print_format", "gst_tax_invoice")
- frappe.reload_doc("accounts", "print_format", "gst_pos_invoice")
+ frappe.reload_doc("selling", "print_format", "gst_pos_invoice")
frappe.reload_doc("accounts", "print_format", "GST E-Invoice")
frappe.db.set_value("Print Format", "GST POS Invoice", "disabled", 0)
From 36c76c40350cbc030cc2b27a84c1bfebbcad67c3 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Fri, 25 Feb 2022 17:15:57 +0530
Subject: [PATCH 161/447] fix: Failing setup wizard test
---
erpnext/hr/doctype/department/department.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/hr/doctype/department/department.py b/erpnext/hr/doctype/department/department.py
index ed0bfcf0d5..ecc025a6e3 100644
--- a/erpnext/hr/doctype/department/department.py
+++ b/erpnext/hr/doctype/department/department.py
@@ -32,7 +32,7 @@ class Department(NestedSet):
return new
def on_update(self):
- if not frappe.local.flags.ignore_update_nsm:
+ if not frappe.local.flags.ignore_update_nsm or frappe.flags.in_setup_wizard:
super(Department, self).on_update()
def on_trash(self):
From 3e573e9838ef82a5e1b89c3d1dcbd6ce67c361b9 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Fri, 25 Feb 2022 17:18:53 +0530
Subject: [PATCH 162/447] fix: Condition
---
erpnext/hr/doctype/department/department.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/hr/doctype/department/department.py b/erpnext/hr/doctype/department/department.py
index ecc025a6e3..71300c40b0 100644
--- a/erpnext/hr/doctype/department/department.py
+++ b/erpnext/hr/doctype/department/department.py
@@ -32,7 +32,7 @@ class Department(NestedSet):
return new
def on_update(self):
- if not frappe.local.flags.ignore_update_nsm or frappe.flags.in_setup_wizard:
+ if not (frappe.local.flags.ignore_update_nsm or frappe.flags.in_setup_wizard):
super(Department, self).on_update()
def on_trash(self):
From 4f463943990057e8ef43eac07e02dc75b9ff8580 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Fri, 25 Feb 2022 21:18:22 +0530
Subject: [PATCH 163/447] fix: Add parent to department
---
erpnext/hr/doctype/department/test_records.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/hr/doctype/department/test_records.json b/erpnext/hr/doctype/department/test_records.json
index 654925ef93..e3421f28b8 100644
--- a/erpnext/hr/doctype/department/test_records.json
+++ b/erpnext/hr/doctype/department/test_records.json
@@ -1,4 +1,4 @@
[
- {"doctype":"Department", "department_name":"_Test Department", "company": "_Test Company"},
- {"doctype":"Department", "department_name":"_Test Department 1", "company": "_Test Company"}
+ {"doctype":"Department", "department_name":"_Test Department", "company": "_Test Company", "parent_department": "All Departments"},
+ {"doctype":"Department", "department_name":"_Test Department 1", "company": "_Test Company", "parent_department": "All Departments"}
]
\ No newline at end of file
From 71d33081aaf24db12906ec3069270a9df018fa2b Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Fri, 25 Feb 2022 23:01:32 +0530
Subject: [PATCH 164/447] fix: org chart connectors not rendered when Employee
Naming is set to Full Name (#29997)
---
.../js/hierarchy_chart/hierarchy_chart_desktop.js | 14 ++++++++------
.../js/hierarchy_chart/hierarchy_chart_mobile.js | 9 +++++----
2 files changed, 13 insertions(+), 10 deletions(-)
diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js
index 831626aa91..a585aa614f 100644
--- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js
+++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js
@@ -304,12 +304,13 @@ erpnext.HierarchyChart = class {
}
get_child_nodes(node_id) {
+ let me = this;
return new Promise(resolve => {
frappe.call({
- method: this.method,
+ method: me.method,
args: {
parent: node_id,
- company: this.company
+ company: me.company
}
}).then(r => resolve(r.message));
});
@@ -350,12 +351,13 @@ erpnext.HierarchyChart = class {
}
get_all_nodes() {
+ let me = this;
return new Promise(resolve => {
frappe.call({
method: 'erpnext.utilities.hierarchy_chart.get_all_nodes',
args: {
- method: this.method,
- company: this.company
+ method: me.method,
+ company: me.company
},
callback: (r) => {
resolve(r.message);
@@ -427,8 +429,8 @@ erpnext.HierarchyChart = class {
add_connector(parent_id, child_id) {
// using pure javascript for better performance
- const parent_node = document.querySelector(`#${parent_id}`);
- const child_node = document.querySelector(`#${child_id}`);
+ const parent_node = document.getElementById(`${parent_id}`);
+ const child_node = document.getElementById(`${child_id}`);
let path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js
index 0a8ba78f64..52236e7df9 100644
--- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js
+++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js
@@ -235,7 +235,7 @@ erpnext.HierarchyChartMobile = class {
let me = this;
return new Promise(resolve => {
frappe.call({
- method: this.method,
+ method: me.method,
args: {
parent: node_id,
company: me.company,
@@ -286,8 +286,8 @@ erpnext.HierarchyChartMobile = class {
}
add_connector(parent_id, child_id) {
- const parent_node = document.querySelector(`#${parent_id}`);
- const child_node = document.querySelector(`#${child_id}`);
+ const parent_node = document.getElementById(`${parent_id}`);
+ const child_node = document.getElementById(`${child_id}`);
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
@@ -518,7 +518,8 @@ erpnext.HierarchyChartMobile = class {
level.nextAll('li').remove();
let node_object = this.nodes[node.id];
- let current_node = level.find(`#${node.id}`).detach();
+ let current_node = level.find(`[id="${node.id}"]`).detach();
+
current_node.removeClass('active-child active-path');
node_object.expanded = 0;
From 16de29a3cb13b771d41f3b26a2d80de8d2871b92 Mon Sep 17 00:00:00 2001
From: hrzzz
Date: Fri, 25 Feb 2022 16:56:23 -0300
Subject: [PATCH 165/447] fix(translation) - correction for translation
---
erpnext/assets/doctype/asset/asset_dashboard.py | 4 +++-
erpnext/assets/doctype/asset_maintenance/asset_maintenance.js | 2 +-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/erpnext/assets/doctype/asset/asset_dashboard.py b/erpnext/assets/doctype/asset/asset_dashboard.py
index 00d08473d6..1833b0e716 100644
--- a/erpnext/assets/doctype/asset/asset_dashboard.py
+++ b/erpnext/assets/doctype/asset/asset_dashboard.py
@@ -1,3 +1,5 @@
+from frappe import _
+
def get_data():
return {
'non_standard_fieldnames': {
@@ -5,7 +7,7 @@ def get_data():
},
'transactions': [
{
- 'label': ['Movement'],
+ 'label': _('Movement'),
'items': ['Asset Movement']
}
]
diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js
index 52996e9347..5c03b98873 100644
--- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js
+++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js
@@ -48,7 +48,7 @@ frappe.ui.form.on('Asset Maintenance', {
`).appendTo(rows);
From 1e7df5d0c2b4a95787adce9a69cfb082c9ad084e Mon Sep 17 00:00:00 2001
From: Rushabh Mehta
Date: Sat, 26 Feb 2022 11:07:44 +0530
Subject: [PATCH 166/447] fix(minor): student attendance tool query fix
---
.../student_attendance_tool.js | 28 +++++++++++++------
.../student_attendance_tool.py | 18 ++++++------
.../public/js/education/student_button.html | 17 -----------
erpnext/public/js/erpnext.bundle.js | 1 -
4 files changed, 28 insertions(+), 36 deletions(-)
delete mode 100644 erpnext/public/js/education/student_button.html
diff --git a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.js b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.js
index 68e7780039..d8a0304941 100644
--- a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.js
+++ b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.js
@@ -163,16 +163,26 @@ education.StudentsEditor = class StudentsEditor {
);
});
- var htmls = students.map(function(student) {
- return frappe.render_template("student_button", {
- student: student.student,
- student_name: student.student_name,
- group_roll_number: student.group_roll_number,
- status: student.status
- })
- });
+ // make html grid of students
+ let student_html = '';
+ for (let student of students) {
+ student_html += `
+
+
+
+ ${student.group_roll_number} - ${student.student_name}
+
+
+
`;
+ }
- $(htmls.join("")).appendTo(me.wrapper);
+ $(student_html).appendTo(me.wrapper);
}
show_empty_state() {
diff --git a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py
index 7deb6b18da..92bb20ca52 100644
--- a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py
+++ b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py
@@ -24,24 +24,24 @@ def get_student_attendance_records(based_on, date=None, student_group=None, cour
student_list = frappe.get_all("Student Group Student", fields=["student", "student_name", "group_roll_number"],
filters={"parent": student_group, "active": 1}, order_by= "group_roll_number")
- table = frappe.qb.DocType("Student Attendance")
+ StudentAttendance = frappe.qb.DocType("Student Attendance")
if course_schedule:
student_attendance_list = (
- frappe.qb.from_(table)
- .select(table.student, table.status)
+ frappe.qb.from_(StudentAttendance)
+ .select(StudentAttendance.student, StudentAttendance.status)
.where(
- (table.course_schedule == course_schedule)
+ (StudentAttendance.course_schedule == course_schedule)
)
).run(as_dict=True)
else:
student_attendance_list = (
- frappe.qb.from_(table)
- .select(table.student, table.status)
+ frappe.qb.from_(StudentAttendance)
+ .select(StudentAttendance.student, StudentAttendance.status)
.where(
- (table.student_group == student_group)
- & (table.date == date)
- & (table.course_schedule == "") | (table.course_schedule.isnull())
+ (StudentAttendance.student_group == student_group)
+ & (StudentAttendance.date == date)
+ & ((StudentAttendance.course_schedule == "") | (StudentAttendance.course_schedule.isnull()))
)
).run(as_dict=True)
diff --git a/erpnext/public/js/education/student_button.html b/erpnext/public/js/education/student_button.html
deleted file mode 100644
index b64c73a43c..0000000000
--- a/erpnext/public/js/education/student_button.html
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
- {{ group_roll_number }} - {{ student_name }}
-
-
-
diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js
index b3a68b3862..8409e78860 100644
--- a/erpnext/public/js/erpnext.bundle.js
+++ b/erpnext/public/js/erpnext.bundle.js
@@ -16,7 +16,6 @@ import "./templates/item_quick_entry.html";
import "./utils/item_quick_entry";
import "./utils/customer_quick_entry";
import "./utils/supplier_quick_entry";
-import "./education/student_button.html";
import "./education/assessment_result_tool.html";
import "./call_popup/call_popup";
import "./utils/dimension_tree_filter";
From 77ffcd3aed9b6fb99bc6f508897d3ebe6880de15 Mon Sep 17 00:00:00 2001
From: Sagar Sharma
Date: Sat, 26 Feb 2022 11:25:02 +0530
Subject: [PATCH 167/447] fix(ux): make "allow zero valuation rate" readonly if
"s_warehouse" is set (#29681)
* chore: make allow zero valuation rate readonly if s_warehouse is set
* fix: setting the checkbox to zero whenever the source warehouse is set
* fix: remove allow_on_submit and refresh trigger
Co-authored-by: Ankush Menat
---
erpnext/stock/doctype/stock_entry/stock_entry.js | 6 ++++++
.../stock_entry_detail/stock_entry_detail.json | 12 +++++++-----
2 files changed, 13 insertions(+), 5 deletions(-)
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 5c9da3a205..324ca7ac59 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -629,6 +629,12 @@ frappe.ui.form.on('Stock Entry Detail', {
frm.events.set_serial_no(frm, cdt, cdn, () => {
frm.events.get_warehouse_details(frm, cdt, cdn);
});
+
+ // set allow_zero_valuation_rate to 0 if s_warehouse is selected.
+ let item = frappe.get_doc(cdt, cdn);
+ if (item.s_warehouse) {
+ item.allow_zero_valuation_rate = 0;
+ }
},
t_warehouse: function(frm, cdt, cdn) {
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
index df65706c39..83aed904dd 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -1,7 +1,7 @@
{
"actions": [],
"autoname": "hash",
- "creation": "2013-03-29 18:22:12",
+ "creation": "2022-02-05 00:17:49.860824",
"doctype": "DocType",
"document_type": "Other",
"editable_grid": 1,
@@ -340,13 +340,13 @@
"label": "More Information"
},
{
- "allow_on_submit": 1,
"default": "0",
"fieldname": "allow_zero_valuation_rate",
"fieldtype": "Check",
"label": "Allow Zero Valuation Rate",
"no_copy": 1,
- "print_hide": 1
+ "print_hide": 1,
+ "read_only_depends_on": "eval:doc.s_warehouse"
},
{
"allow_on_submit": 1,
@@ -556,12 +556,14 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-06-22 16:47:11.268975",
+ "modified": "2022-02-26 00:51:24.963653",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
- "sort_order": "ASC"
+ "sort_order": "ASC",
+ "states": []
}
\ No newline at end of file
From 2dabda03af4407d39c58c0074656048a0860ce8d Mon Sep 17 00:00:00 2001
From: Rushabh Mehta
Date: Sat, 26 Feb 2022 11:32:54 +0530
Subject: [PATCH 168/447] fix(minor): student_attendance_tool.js fix fetching
UX
---
.../student_attendance_tool.js | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.js b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.js
index d8a0304941..4526585175 100644
--- a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.js
+++ b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.js
@@ -3,6 +3,10 @@
frappe.provide("education");
frappe.ui.form.on('Student Attendance Tool', {
+ setup: (frm) => {
+ frm.students_area = $('')
+ .appendTo(frm.fields_dict.students_html.wrapper);
+ },
onload: function(frm) {
frm.set_query("student_group", function() {
return {
@@ -34,6 +38,7 @@ frappe.ui.form.on('Student Attendance Tool', {
student_group: function(frm) {
if ((frm.doc.student_group && frm.doc.date) || frm.doc.course_schedule) {
+ frm.students_area.find('.student-attendance-checks').html(`
Fetching...
`);
var method = "erpnext.education.doctype.student_attendance_tool.student_attendance_tool.get_student_attendance_records";
frappe.call({
@@ -62,10 +67,6 @@ frappe.ui.form.on('Student Attendance Tool', {
},
get_students: function(frm, students) {
- if (!frm.students_area) {
- frm.students_area = $('
')
- .appendTo(frm.fields_dict.students_html.wrapper);
- }
students = students || [];
frm.students_editor = new education.StudentsEditor(frm, frm.students_area, students);
}
@@ -182,7 +183,7 @@ education.StudentsEditor = class StudentsEditor {
`;
}
- $(student_html).appendTo(me.wrapper);
+ $(`
${student_html}
`).appendTo(me.wrapper);
}
show_empty_state() {
From 96e87fe3c9bb339b6e8046834b1fab6493113096 Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Sat, 26 Feb 2022 12:54:13 +0530
Subject: [PATCH 169/447] fix: validate Work Order qty against Production Plan
(#29721) (#30003)
* fix: validate Work Order qty against Production Plan
* chore: err msg when max_qty is 0
* test: add test for overproduction
* fix: CI
(cherry picked from commit 067ede76ea0851a29e270e150f482368e0afa194)
Co-authored-by: Sagar Sharma
---
.../production_plan/test_production_plan.py | 20 +++++++++++--------
.../doctype/work_order/work_order.py | 15 ++++++++++++++
2 files changed, 27 insertions(+), 8 deletions(-)
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index d88e10a564..2359815813 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -9,6 +9,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_sales_orders,
get_warehouse_list,
)
+from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -466,26 +467,29 @@ class TestProductionPlan(ERPNextTestCase):
bom = make_bom(item=item, raw_materials=raw_materials)
# Create Production Plan
- pln = create_production_plan(item_code=bom.item, planned_qty=10)
+ pln = create_production_plan(item_code=bom.item, planned_qty=5)
# All the created Work Orders
wo_list = []
- # Create and Submit 1st Work Order for 5 qty
- create_work_order(item, pln, 5)
+ # Create and Submit 1st Work Order for 3 qty
+ create_work_order(item, pln, 3)
+ pln.reload()
+ self.assertEqual(pln.po_items[0].ordered_qty, 3)
+
+ # Create and Submit 2nd Work Order for 2 qty
+ create_work_order(item, pln, 2)
pln.reload()
self.assertEqual(pln.po_items[0].ordered_qty, 5)
- # Create and Submit 2nd Work Order for 3 qty
- create_work_order(item, pln, 3)
- pln.reload()
- self.assertEqual(pln.po_items[0].ordered_qty, 8)
+ # Overproduction
+ self.assertRaises(OverProductionError, create_work_order, item=item, pln=pln, qty=2)
# Cancel 1st Work Order
wo1 = frappe.get_doc("Work Order", wo_list[0])
wo1.cancel()
pln.reload()
- self.assertEqual(pln.po_items[0].ordered_qty, 3)
+ self.assertEqual(pln.po_items[0].ordered_qty, 2)
# Cancel 2nd Work Order
wo2 = frappe.get_doc("Work Order", wo_list[1])
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index ed6a0299ef..e1120c723d 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -636,6 +636,21 @@ class WorkOrder(Document):
if not self.qty > 0:
frappe.throw(_("Quantity to Manufacture must be greater than 0."))
+ if self.production_plan and self.production_plan_item:
+ qty_dict = frappe.db.get_value("Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1)
+
+ allowance_qty =flt(frappe.db.get_single_value("Manufacturing Settings",
+ "overproduction_percentage_for_work_order"))/100 * qty_dict.get("planned_qty", 0)
+
+ max_qty = qty_dict.get("planned_qty", 0) + allowance_qty - qty_dict.get("ordered_qty", 0)
+
+ if max_qty < 1:
+ frappe.throw(_("Cannot produce more item for {0}")
+ .format(self.production_item), OverProductionError)
+ elif self.qty > max_qty:
+ frappe.throw(_("Cannot produce more than {0} items for {1}")
+ .format(max_qty, self.production_item), OverProductionError)
+
def validate_transfer_against(self):
if not self.docstatus == 1:
# let user configure operations until they're ready to submit
From c050ce49c2b3ad7b36640edf01099bb9cb002e9d Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Mon, 28 Feb 2022 09:58:12 +0530
Subject: [PATCH 170/447] test: employee leave balance report
- fix expired leaves calculation when filters span across 2 different allocation periods
---
.../doctype/holiday_list/test_holiday_list.py | 22 +++
.../test_leave_application.py | 16 +-
.../employee_leave_balance.py | 10 +-
.../test_employee_leave_balance.py | 162 ++++++++++++++++++
.../doctype/salary_slip/test_salary_slip.py | 11 +-
5 files changed, 208 insertions(+), 13 deletions(-)
create mode 100644 erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py
diff --git a/erpnext/hr/doctype/holiday_list/test_holiday_list.py b/erpnext/hr/doctype/holiday_list/test_holiday_list.py
index c9239edb72..aed901aa6d 100644
--- a/erpnext/hr/doctype/holiday_list/test_holiday_list.py
+++ b/erpnext/hr/doctype/holiday_list/test_holiday_list.py
@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
import unittest
+from contextlib import contextmanager
from datetime import timedelta
import frappe
@@ -30,3 +31,24 @@ def make_holiday_list(name, from_date=getdate()-timedelta(days=10), to_date=getd
"holidays" : holiday_dates
}).insert()
return doc
+
+
+@contextmanager
+def set_holiday_list(holiday_list, company_name):
+ """
+ Context manager for setting holiday list in tests
+ """
+ try:
+ company = frappe.get_doc('Company', company_name)
+ previous_holiday_list = company.default_holiday_list
+
+ company.default_holiday_list = holiday_list
+ company.save()
+
+ yield
+
+ finally:
+ # restore holiday list setup
+ company = frappe.get_doc('Company', company_name)
+ company.default_holiday_list = previous_holiday_list
+ company.save()
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 75e99f8991..3e739768af 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -501,7 +501,7 @@ class TestLeaveApplication(unittest.TestCase):
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=90)
- leave_type.submit()
+ leave_type.insert()
create_carry_forwarded_allocation(employee, leave_type)
@@ -723,19 +723,22 @@ def create_carry_forwarded_allocation(employee, leave_type):
carry_forward=1)
leave_allocation.submit()
-def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None):
+def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None, carry_forward=False, leaves=None):
allocation = frappe.get_doc({
"doctype": "Leave Allocation",
"employee": employee or "_T-Employee-00001",
"leave_type": leave_type or "_Test Leave Type",
"from_date": from_date or "2013-01-01",
"to_date": to_date or "2019-12-31",
- "new_leaves_allocated": 30
+ "new_leaves_allocated": leaves or 30,
+ "carry_forward": carry_forward
})
allocation.insert(ignore_permissions=True)
allocation.submit()
+ return allocation
+
def get_employee():
return frappe.get_doc("Employee", "_T-Employee-00001")
@@ -780,9 +783,10 @@ def allocate_leaves(employee, leave_period, leave_type, new_leaves_allocated, el
allocate_leave.submit()
-def get_first_sunday(holiday_list):
- month_start_date = get_first_day(nowdate())
- month_end_date = get_last_day(nowdate())
+def get_first_sunday(holiday_list, for_date=None):
+ date = for_date or getdate()
+ month_start_date = get_first_day(date)
+ month_end_date = get_last_day(date)
first_sunday = frappe.db.sql("""
select holiday_date from `tabHoliday`
where parent = %s
diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
index 3f0337e508..3a5f2fe962 100644
--- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
@@ -16,6 +16,8 @@ from erpnext.hr.doctype.leave_application.leave_application import (
def execute(filters=None):
+ filters = frappe._dict(filters or {})
+
if filters.to_date <= filters.from_date:
frappe.throw(_('"From Date" can not be greater than or equal to "To Date"'))
@@ -103,7 +105,7 @@ def get_data(filters):
or ("HR Manager" in frappe.get_roles(user)):
if len(active_employees) > 1:
row = frappe._dict()
- row.employee = employee.name,
+ row.employee = employee.name
row.employee_name = employee.employee_name
leaves_taken = get_leaves_for_period(employee.name, leave_type,
@@ -114,7 +116,7 @@ def get_data(filters):
opening = get_opening_balance(employee.name, leave_type, filters, carry_forwarded_leaves)
row.leaves_allocated = new_allocation
- row.leaves_expired = expired_leaves - leaves_taken if expired_leaves - leaves_taken > 0 else 0
+ row.leaves_expired = expired_leaves
row.opening_balance = opening
row.leaves_taken = leaves_taken
@@ -202,7 +204,11 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type):
continue
if record.to_date < getdate(to_date):
+ # leave allocations ending before to_date, reduce leaves taken within that period
+ # since they are already used, they won't expire
expired_leaves += record.leaves
+ expired_leaves += get_leaves_for_period(employee, leave_type,
+ record.from_date, record.to_date)
if record.from_date >= getdate(from_date):
if record.is_carry_forward:
diff --git a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py
new file mode 100644
index 0000000000..05316f165f
--- /dev/null
+++ b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py
@@ -0,0 +1,162 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+
+import unittest
+
+import frappe
+from frappe.utils import (
+ add_days,
+ add_months,
+ get_year_ending,
+ get_year_start,
+ getdate,
+ flt,
+)
+
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.report.employee_leave_balance.employee_leave_balance import execute
+from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
+from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
+from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday, make_allocation_record
+from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list, make_leave_application
+from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
+
+test_records = frappe.get_test_records('Leave Type')
+
+class TestEmployeeLeaveBalance(unittest.TestCase):
+ def setUp(self):
+ for dt in ['Leave Application', 'Leave Allocation', 'Salary Slip', 'Leave Ledger Entry', 'Leave Type']:
+ frappe.db.delete(dt)
+
+ frappe.set_user('Administrator')
+
+ self.employee_id = make_employee('test_emp_leave_balance@example.com', company='_Test Company')
+ self.holiday_list = make_holiday_list('_Test Emp Balance Holiday List', get_year_start(getdate()), get_year_ending(getdate()))
+
+ self.date = getdate()
+ self.year_start = getdate(get_year_start(self.date))
+ self.mid_year = add_months(self.year_start, 6)
+ self.year_end = getdate(get_year_ending(self.date))
+
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ @set_holiday_list('_Test Emp Balance Holiday List', '_Test Company')
+ def test_employee_leave_balance(self):
+ frappe.get_doc(test_records[0]).insert()
+
+ # 5 leaves
+ allocation1 = make_allocation_record(employee=self.employee_id, from_date=add_days(self.year_start, -11),
+ to_date=add_days(self.year_start, -1), leaves=5)
+ # 30 leaves
+ allocation2 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end)
+ # expires 5 leaves
+ process_expired_allocation()
+
+ # 4 days leave
+ first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start)
+ leave_application = make_leave_application(self.employee_id, first_sunday, add_days(first_sunday, 3), '_Test Leave Type')
+ leave_application.reload()
+
+ filters = {
+ 'from_date': allocation1.from_date,
+ 'to_date': allocation2.to_date,
+ 'employee': self.employee_id
+ }
+
+ report = execute(filters)
+
+ expected_data = [{
+ 'leave_type': '_Test Leave Type',
+ 'employee': self.employee_id,
+ 'employee_name': 'test_emp_leave_balance@example.com',
+ 'leaves_allocated': flt(allocation1.new_leaves_allocated + allocation2.new_leaves_allocated),
+ 'leaves_expired': flt(allocation1.new_leaves_allocated),
+ 'opening_balance': flt(0),
+ 'leaves_taken': flt(leave_application.total_leave_days),
+ 'closing_balance': flt(allocation2.new_leaves_allocated - leave_application.total_leave_days),
+ 'indent': 1
+ }]
+
+ self.assertEqual(report[1], expected_data)
+
+ @set_holiday_list('_Test Emp Balance Holiday List', '_Test Company')
+ def test_opening_balance_on_alloc_boundary_dates(self):
+ frappe.get_doc(test_records[0]).insert()
+
+ # 30 leaves allocated
+ allocation1 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end)
+ # 4 days leave application in the first allocation
+ first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start)
+ leave_application = make_leave_application(self.employee_id, first_sunday, add_days(first_sunday, 3), '_Test Leave Type')
+ leave_application.reload()
+
+ # Case 1: opening balance for first alloc boundary
+ filters = {
+ 'from_date': self.year_start,
+ 'to_date': self.year_end,
+ 'employee': self.employee_id
+ }
+ report = execute(filters)
+ self.assertEqual(report[1][0].opening_balance, 0)
+
+ # Case 2: opening balance after leave application date
+ filters = {
+ 'from_date': add_days(leave_application.to_date, 1),
+ 'to_date': self.year_end,
+ 'employee': self.employee_id
+ }
+ report = execute(filters)
+ self.assertEqual(report[1][0].opening_balance, (allocation1.new_leaves_allocated - leave_application.total_leave_days))
+
+ # Case 3: leave balance shows actual balance and not consumption balance as per remaining days near alloc end date
+ # eg: 3 days left for alloc to end, leave balance should still be 26 and not 3
+ filters = {
+ 'from_date': add_days(self.year_end, -3),
+ 'to_date': self.year_end,
+ 'employee': self.employee_id
+ }
+ report = execute(filters)
+ self.assertEqual(report[1][0].opening_balance, (allocation1.new_leaves_allocated - leave_application.total_leave_days))
+
+ @set_holiday_list('_Test Emp Balance Holiday List', '_Test Company')
+ def test_opening_balance_considers_carry_forwarded_leaves(self):
+ leave_type = create_leave_type(
+ leave_type_name="_Test_CF_leave_expiry",
+ is_carry_forward=1)
+ leave_type.insert()
+
+ # 30 leaves allocated for first half of the year
+ allocation1 = make_allocation_record(employee=self.employee_id, from_date=self.year_start,
+ to_date=self.mid_year, leave_type=leave_type.name)
+ # 4 days leave application in the first allocation
+ first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start)
+ leave_application = make_leave_application(self.employee_id, first_sunday, add_days(first_sunday, 3), leave_type.name)
+ leave_application.reload()
+ # 30 leaves allocated for second half of the year + carry forward leaves (26) from the previous allocation
+ allocation2 = make_allocation_record(employee=self.employee_id, from_date=add_days(self.mid_year, 1), to_date=self.year_end,
+ carry_forward=True, leave_type=leave_type.name)
+
+ # Case 1: carry forwarded leaves considered in opening balance for second alloc
+ filters = {
+ 'from_date': add_days(self.mid_year, 1),
+ 'to_date': self.year_end,
+ 'employee': self.employee_id
+ }
+ report = execute(filters)
+ # available leaves from old alloc
+ opening_balance = allocation1.new_leaves_allocated - leave_application.total_leave_days
+ self.assertEqual(report[1][0].opening_balance, opening_balance)
+
+ # Case 2: opening balance one day after alloc boundary = carry forwarded leaves + new leaves alloc
+ filters = {
+ 'from_date': add_days(self.mid_year, 2),
+ 'to_date': self.year_end,
+ 'employee': self.employee_id
+ }
+ report = execute(filters)
+ # available leaves from old alloc
+ opening_balance = allocation2.new_leaves_allocated + (allocation1.new_leaves_allocated - leave_application.total_leave_days)
+ self.assertEqual(report[1][0].opening_balance, opening_balance)
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index bcf981b74d..a4834d995a 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -1010,15 +1010,16 @@ def setup_test():
frappe.db.set_value('HR Settings', None, 'leave_status_notification_template', None)
frappe.db.set_value('HR Settings', None, 'leave_approval_notification_template', None)
-def make_holiday_list():
+def make_holiday_list(list_name=None, from_date=None, to_date=None):
fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
- holiday_list = frappe.db.exists("Holiday List", "Salary Slip Test Holiday List")
+ name = list_name or "Salary Slip Test Holiday List"
+ holiday_list = frappe.db.exists("Holiday List", name)
if not holiday_list:
holiday_list = frappe.get_doc({
"doctype": "Holiday List",
- "holiday_list_name": "Salary Slip Test Holiday List",
- "from_date": fiscal_year[1],
- "to_date": fiscal_year[2],
+ "holiday_list_name": name,
+ "from_date": from_date or fiscal_year[1],
+ "to_date": to_date or fiscal_year[2],
"weekly_off": "Sunday"
}).insert()
holiday_list.get_weekly_off_dates()
From c7d594984a716ad8f84932b9c3f61a09f4f6b33a Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Mon, 28 Feb 2022 10:19:16 +0530
Subject: [PATCH 171/447] chore: remove unused imports, sort imports, fix sider
---
.../employee_leave_balance.py | 30 +++++++++----------
.../test_employee_leave_balance.py | 23 +++++++-------
2 files changed, 26 insertions(+), 27 deletions(-)
diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
index 3a5f2fe962..5c18d11721 100644
--- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
@@ -6,7 +6,7 @@ from itertools import groupby
import frappe
from frappe import _
-from frappe.utils import add_days, date_diff, getdate
+from frappe.utils import add_days, getdate
from erpnext.hr.doctype.leave_allocation.leave_allocation import get_previous_allocation
from erpnext.hr.doctype.leave_application.leave_application import (
@@ -223,21 +223,21 @@ def get_leave_ledger_entries(from_date, to_date, employee, leave_type):
ledger = frappe.qb.DocType('Leave Ledger Entry')
records = (
frappe.qb.from_(ledger)
- .select(
- ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date,
- ledger.leaves, ledger.transaction_name, ledger.transaction_type,
- ledger.is_carry_forward, ledger.is_expired
- ).where(
- (ledger.docstatus == 1)
- & (ledger.transaction_type == 'Leave Allocation')
- & (ledger.employee == employee)
- & (ledger.leave_type == leave_type)
- & (
- (ledger.from_date[from_date: to_date])
- | (ledger.to_date[from_date: to_date])
- | ((ledger.from_date < from_date) & (ledger.to_date > to_date))
- )
+ .select(
+ ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date,
+ ledger.leaves, ledger.transaction_name, ledger.transaction_type,
+ ledger.is_carry_forward, ledger.is_expired
+ ).where(
+ (ledger.docstatus == 1)
+ & (ledger.transaction_type == 'Leave Allocation')
+ & (ledger.employee == employee)
+ & (ledger.leave_type == leave_type)
+ & (
+ (ledger.from_date[from_date: to_date])
+ | (ledger.to_date[from_date: to_date])
+ | ((ledger.from_date < from_date) & (ledger.to_date > to_date))
)
+ )
).run(as_dict=True)
return records
diff --git a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py
index 05316f165f..f844be5ff1 100644
--- a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py
@@ -5,22 +5,21 @@
import unittest
import frappe
-from frappe.utils import (
- add_days,
- add_months,
- get_year_ending,
- get_year_start,
- getdate,
- flt,
-)
+from frappe.utils import add_days, add_months, flt, get_year_ending, get_year_start, getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
-from erpnext.hr.report.employee_leave_balance.employee_leave_balance import execute
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
-from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
-from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday, make_allocation_record
-from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list, make_leave_application
+from erpnext.hr.doctype.leave_application.test_leave_application import (
+ get_first_sunday,
+ make_allocation_record,
+)
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
+from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
+from erpnext.hr.report.employee_leave_balance.employee_leave_balance import execute
+from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
+ make_holiday_list,
+ make_leave_application,
+)
test_records = frappe.get_test_records('Leave Type')
From 88141d6116bb9699d629e6a22deff36a876de185 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Mon, 28 Feb 2022 12:12:52 +0530
Subject: [PATCH 172/447] test: Employee Leave Balance Summary
---
.../test_employee_leave_balance.py | 6 +-
.../employee_leave_balance_summary.py | 1 +
.../test_employee_leave_balance_summary.py | 117 ++++++++++++++++++
3 files changed, 121 insertions(+), 3 deletions(-)
create mode 100644 erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py
diff --git a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py
index f844be5ff1..aecf0a4d4e 100644
--- a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py
@@ -31,13 +31,13 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
frappe.set_user('Administrator')
self.employee_id = make_employee('test_emp_leave_balance@example.com', company='_Test Company')
- self.holiday_list = make_holiday_list('_Test Emp Balance Holiday List', get_year_start(getdate()), get_year_ending(getdate()))
self.date = getdate()
self.year_start = getdate(get_year_start(self.date))
self.mid_year = add_months(self.year_start, 6)
self.year_end = getdate(get_year_ending(self.date))
+ self.holiday_list = make_holiday_list('_Test Emp Balance Holiday List', self.year_start, self.year_end)
def tearDown(self):
frappe.db.rollback()
@@ -56,7 +56,7 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
# 4 days leave
first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start)
- leave_application = make_leave_application(self.employee_id, first_sunday, add_days(first_sunday, 3), '_Test Leave Type')
+ leave_application = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type')
leave_application.reload()
filters = {
@@ -89,7 +89,7 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
allocation1 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end)
# 4 days leave application in the first allocation
first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start)
- leave_application = make_leave_application(self.employee_id, first_sunday, add_days(first_sunday, 3), '_Test Leave Type')
+ leave_application = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type')
leave_application.reload()
# Case 1: opening balance for first alloc boundary
diff --git a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py
index 71c18bb51f..eee2eb816c 100644
--- a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py
+++ b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py
@@ -12,6 +12,7 @@ from erpnext.hr.report.employee_leave_balance.employee_leave_balance import (
def execute(filters=None):
+ filters = frappe._dict(filters or {})
leave_types = frappe.db.sql_list("select name from `tabLeave Type` order by name asc")
columns = get_columns(leave_types)
diff --git a/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py
new file mode 100644
index 0000000000..9b953de0dc
--- /dev/null
+++ b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py
@@ -0,0 +1,117 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+
+import unittest
+
+import frappe
+from frappe.utils import add_days, flt, get_year_ending, get_year_start, getdate
+
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
+from erpnext.hr.doctype.leave_application.test_leave_application import (
+ get_first_sunday,
+ make_allocation_record,
+)
+from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
+from erpnext.hr.report.employee_leave_balance_summary.employee_leave_balance_summary import execute
+from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
+ make_holiday_list,
+ make_leave_application,
+)
+
+test_records = frappe.get_test_records('Leave Type')
+
+class TestEmployeeLeaveBalance(unittest.TestCase):
+ def setUp(self):
+ for dt in ['Leave Application', 'Leave Allocation', 'Salary Slip', 'Leave Ledger Entry', 'Leave Type']:
+ frappe.db.delete(dt)
+
+ frappe.set_user('Administrator')
+
+ self.employee_id = make_employee('test_emp_leave_balance@example.com', company='_Test Company')
+ self.employee_id = make_employee('test_emp_leave_balance@example.com', company='_Test Company')
+
+ self.date = getdate()
+ self.year_start = getdate(get_year_start(self.date))
+ self.year_end = getdate(get_year_ending(self.date))
+
+ self.holiday_list = make_holiday_list('_Test Emp Balance Holiday List', self.year_start, self.year_end)
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ @set_holiday_list('_Test Emp Balance Holiday List', '_Test Company')
+ def test_employee_leave_balance_summary(self):
+ frappe.get_doc(test_records[0]).insert()
+
+ # 5 leaves
+ allocation1 = make_allocation_record(employee=self.employee_id, from_date=add_days(self.year_start, -11),
+ to_date=add_days(self.year_start, -1), leaves=5)
+ # 30 leaves
+ allocation2 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end)
+
+ # 2 days leave within the first allocation
+ leave_application1 = make_leave_application(self.employee_id, add_days(self.year_start, -11), add_days(self.year_start, -10),
+ '_Test Leave Type')
+ leave_application1.reload()
+
+ # expires 3 leaves
+ process_expired_allocation()
+
+ # 4 days leave within the second allocation
+ first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start)
+ leave_application2 = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type')
+ leave_application2.reload()
+
+ filters = {
+ 'date': self.date,
+ 'company': '_Test Company',
+ 'employee': self.employee_id
+ }
+
+ report = execute(filters)
+
+ expected_data = [[
+ self.employee_id,
+ 'test_emp_leave_balance@example.com',
+ frappe.db.get_value('Employee', self.employee_id, 'department'),
+ flt(
+ allocation1.new_leaves_allocated # allocated = 5
+ + allocation2.new_leaves_allocated # allocated = 30
+ - leave_application1.total_leave_days # leaves taken in the 1st alloc = 2
+ - (allocation1.new_leaves_allocated - leave_application1.total_leave_days) # leaves expired from 1st alloc = 3
+ - leave_application2.total_leave_days # leaves taken in the 2nd alloc = 4
+ )
+ ]]
+
+ self.assertEqual(report[1], expected_data)
+
+ @set_holiday_list('_Test Emp Balance Holiday List', '_Test Company')
+ def test_get_leave_balance_near_alloc_expiry(self):
+ frappe.get_doc(test_records[0]).insert()
+
+ # 30 leaves allocated
+ allocation = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end)
+ # 4 days leave application in the first allocation
+ first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start)
+ leave_application = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type')
+ leave_application.reload()
+
+ # Leave balance should show actual balance, and not "consumption balance as per remaining days", near alloc end date
+ # eg: 3 days left for alloc to end, leave balance should still be 26 and not 3
+ filters = {
+ 'date': add_days(self.year_end, -3),
+ 'company': '_Test Company',
+ 'employee': self.employee_id
+ }
+ report = execute(filters)
+
+ expected_data = [[
+ self.employee_id,
+ 'test_emp_leave_balance@example.com',
+ frappe.db.get_value('Employee', self.employee_id, 'department'),
+ flt(allocation.new_leaves_allocated - leave_application.total_leave_days)
+ ]]
+
+ self.assertEqual(report[1], expected_data)
From 4e9a9f35a605f4f886cc9cc920ec4ab4262b9709 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Mon, 28 Feb 2022 12:41:59 +0530
Subject: [PATCH 173/447] test: add correct test case
---
.../doctype/sales_invoice/test_sales_invoice.py | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 941061f2a2..07591e7b1e 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -2429,14 +2429,22 @@ class TestSalesInvoice(unittest.TestCase):
def test_sales_commission(self):
- si = frappe.copy_doc(test_records[0])
+ si = frappe.copy_doc(test_records[2])
+
+ frappe.db.set_value('Item', si.get('items')[0].item_code, 'grant_commission', 1)
+ frappe.db.set_value('Item', si.get('items')[1].item_code, 'grant_commission', 0)
+
item = copy.deepcopy(si.get('items')[0])
item.update({
"qty": 1,
"rate": 500,
- "grant_commission": 1
})
- si.append("items", item)
+
+ item = copy.deepcopy(si.get('items')[1])
+ item.update({
+ "qty": 1,
+ "rate": 500,
+ })
# Test valid values
for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)):
From 88a21ca7d2fd2944912bc3cd48c81185478f3cb3 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 26 Jan 2022 01:01:10 +0530
Subject: [PATCH 174/447] fix: only show child warehouses for transfer
---
erpnext/stock/dashboard/item_dashboard.js | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js
index 37e9e89a0a..204d62355c 100644
--- a/erpnext/stock/dashboard/item_dashboard.js
+++ b/erpnext/stock/dashboard/item_dashboard.js
@@ -213,7 +213,14 @@ erpnext.stock.move_item = function (item, source, target, actual_qty, rate, call
label: __('Target Warehouse'),
fieldtype: 'Link',
options: 'Warehouse',
- reqd: 1
+ reqd: 1,
+ get_query() {
+ return {
+ filters: {
+ is_group: 0
+ }
+ }
+ }
},
{
fieldname: 'qty',
From 0bed5927779c823455e9cd819cba5281d297e1b3 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 26 Jan 2022 01:01:33 +0530
Subject: [PATCH 175/447] fix: open stock entry instead of submitting from item
dashboard
---
erpnext/stock/dashboard/item_dashboard.js | 61 ++++++-----------------
1 file changed, 15 insertions(+), 46 deletions(-)
diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js
index 204d62355c..1f259bd425 100644
--- a/erpnext/stock/dashboard/item_dashboard.js
+++ b/erpnext/stock/dashboard/item_dashboard.js
@@ -259,52 +259,21 @@ erpnext.stock.move_item = function (item, source, target, actual_qty, rate, call
dialog.get_field('target').refresh();
}
- dialog.set_primary_action(__('Submit'), function () {
- var values = dialog.get_values();
- if (!values) {
- return;
- }
- if (source && values.qty > actual_qty) {
- frappe.msgprint(__('Quantity must be less than or equal to {0}', [actual_qty]));
- return;
- }
- if (values.source === values.target) {
- frappe.msgprint(__('Source and target warehouse must be different'));
- }
-
- frappe.call({
- method: 'erpnext.stock.doctype.stock_entry.stock_entry_utils.make_stock_entry',
- args: values,
- btn: dialog.get_primary_btn(),
- freeze: true,
- freeze_message: __('Creating Stock Entry'),
- callback: function (r) {
- frappe.show_alert(__('Stock Entry {0} created',
- ['' + r.message.name + ' ']));
- dialog.hide();
- callback(r);
- },
+ dialog.set_primary_action(__('Create Stock Entry'), function () {
+ frappe.model.with_doctype('Stock Entry', function () {
+ let doc = frappe.model.get_new_doc('Stock Entry');
+ doc.from_warehouse = dialog.get_value('source');
+ doc.to_warehouse = dialog.get_value('target');
+ doc.stock_entry_type = doc.from_warehouse ? "Material Transfer" : "Material Receipt";
+ let row = frappe.model.add_child(doc, 'items');
+ row.item_code = dialog.get_value('item_code');
+ row.f_warehouse = dialog.get_value('target');
+ row.t_warehouse = dialog.get_value('target');
+ row.qty = dialog.get_value('qty');
+ row.conversion_factor = 1;
+ row.transfer_qty = dialog.get_value('qty');
+ row.basic_rate = dialog.get_value('rate');
+ frappe.set_route('Form', doc.doctype, doc.name);
});
});
-
- $('' +
- __("Add more items or open full form") + '
')
- .appendTo(dialog.body)
- .find('.link-open')
- .on('click', function () {
- frappe.model.with_doctype('Stock Entry', function () {
- var doc = frappe.model.get_new_doc('Stock Entry');
- doc.from_warehouse = dialog.get_value('source');
- doc.to_warehouse = dialog.get_value('target');
- var row = frappe.model.add_child(doc, 'items');
- row.item_code = dialog.get_value('item_code');
- row.f_warehouse = dialog.get_value('target');
- row.t_warehouse = dialog.get_value('target');
- row.qty = dialog.get_value('qty');
- row.conversion_factor = 1;
- row.transfer_qty = dialog.get_value('qty');
- row.basic_rate = dialog.get_value('rate');
- frappe.set_route('Form', doc.doctype, doc.name);
- });
- });
};
From 3cc47d5ba5b0b7a0a677e81bf4d092f85b36e969 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 28 Feb 2022 13:04:24 +0530
Subject: [PATCH 176/447] fix: correct fieldname for source warehouse
---
erpnext/stock/dashboard/item_dashboard.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js
index 1f259bd425..c9d5f61f22 100644
--- a/erpnext/stock/dashboard/item_dashboard.js
+++ b/erpnext/stock/dashboard/item_dashboard.js
@@ -267,7 +267,7 @@ erpnext.stock.move_item = function (item, source, target, actual_qty, rate, call
doc.stock_entry_type = doc.from_warehouse ? "Material Transfer" : "Material Receipt";
let row = frappe.model.add_child(doc, 'items');
row.item_code = dialog.get_value('item_code');
- row.f_warehouse = dialog.get_value('target');
+ row.s_warehouse = dialog.get_value('source');
row.t_warehouse = dialog.get_value('target');
row.qty = dialog.get_value('qty');
row.conversion_factor = 1;
From 1d1203d5eca07e40d8ce32248a0d81b0d7130627 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 28 Feb 2022 12:44:39 +0530
Subject: [PATCH 177/447] fix: dont fetch draft/cancelled BOMs
---
.../manufacturing/doctype/production_plan/production_plan.js | 2 +-
erpnext/manufacturing/doctype/work_order/work_order.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index 0babf875e7..c33e64674e 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -49,7 +49,7 @@ frappe.ui.form.on('Production Plan', {
if (d.item_code) {
return {
query: "erpnext.controllers.queries.bom",
- filters:{'item': cstr(d.item_code)}
+ filters:{'item': cstr(d.item_code), 'docstatus': 1}
}
} else frappe.msgprint(__("Please enter Item first"));
}
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index e1120c723d..374ab86cad 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -854,7 +854,7 @@ def get_item_details(item, project = None, skip_bom_info=False):
res = res[0]
if skip_bom_info: return res
- filters = {"item": item, "is_default": 1}
+ filters = {"item": item, "is_default": 1, "docstatus": 1}
if project:
filters = {"item": item, "project": project}
From 942511cfffe9e294e3baaf50206d3781ba59c8bf Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Mon, 28 Feb 2022 14:10:23 +0530
Subject: [PATCH 178/447] fix: leave application dashboard
- total leaves allocated considering cancelled leaves
- optional plural for leave category labels
- show dashboard only once from date is set, else it fetches all allocations till date and generates incorrect balance
- change pending leaves to 'Leaves Pending Approval' for better context
- update labels in Salary Slip Leave Details table
---
.../doctype/leave_application/leave_application.js | 3 ++-
.../doctype/leave_application/leave_application.py | 7 ++++---
.../leave_application_dashboard.html | 12 ++++++------
erpnext/payroll/doctype/salary_slip/salary_slip.py | 2 +-
.../salary_slip_leave/salary_slip_leave.json | 13 +++++++------
5 files changed, 20 insertions(+), 17 deletions(-)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js
index 9e8cb5516f..85997a4087 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.js
+++ b/erpnext/hr/doctype/leave_application/leave_application.js
@@ -52,7 +52,7 @@ frappe.ui.form.on("Leave Application", {
make_dashboard: function(frm) {
var leave_details;
let lwps;
- if (frm.doc.employee) {
+ if (frm.doc.employee && frm.doc.from_date) {
frappe.call({
method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_details",
async: false,
@@ -146,6 +146,7 @@ frappe.ui.form.on("Leave Application", {
},
to_date: function(frm) {
+ frm.trigger("make_dashboard");
frm.trigger("half_day_datepicker");
frm.trigger("calculate_total_days");
},
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index dbb3db36f4..697d35a815 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -521,6 +521,7 @@ def get_leave_details(employee, date):
'to_date': ('>=', date),
'employee': employee,
'leave_type': allocation.leave_type,
+ 'docstatus': 1
}, 'SUM(total_leaves_allocated)') or 0
remaining_leaves = get_leave_balance_on(employee, d, date, to_date = allocation.to_date,
@@ -528,13 +529,13 @@ def get_leave_details(employee, date):
end_date = allocation.to_date
leaves_taken = get_leaves_for_period(employee, d, allocation.from_date, end_date) * -1
- leaves_pending = get_pending_leaves_for_period(employee, d, allocation.from_date, end_date)
+ leaves_pending = get_leaves_pending_approval_for_period(employee, d, allocation.from_date, end_date)
leave_allocation[d] = {
"total_leaves": total_allocated_leaves,
"expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken),
"leaves_taken": leaves_taken,
- "pending_leaves": leaves_pending,
+ "leaves_pending_approval": leaves_pending,
"remaining_leaves": remaining_leaves}
#is used in set query
@@ -621,7 +622,7 @@ def get_leave_allocation_records(employee, date, leave_type=None):
}))
return allocated_leaves
-def get_pending_leaves_for_period(employee, leave_type, from_date, to_date):
+def get_leaves_pending_approval_for_period(employee, leave_type, from_date, to_date):
''' Returns leaves that are pending approval '''
leaves = frappe.get_all("Leave Application",
filters={
diff --git a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html
index 9f667a6835..e755322efd 100644
--- a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html
+++ b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html
@@ -4,11 +4,11 @@
{{ __("Leave Type") }}
- {{ __("Total Allocated Leave") }}
- {{ __("Expired Leave") }}
- {{ __("Used Leave") }}
- {{ __("Pending Leave") }}
- {{ __("Available Leave") }}
+ {{ __("Total Allocated Leave(s)") }}
+ {{ __("Expired Leave(s)") }}
+ {{ __("Used Leave(s)") }}
+ {{ __("Leave(s) Pending Approval") }}
+ {{ __("Available Leave(s)") }}
@@ -18,7 +18,7 @@
{%= value["total_leaves"] %}
{%= value["expired_leaves"] %}
{%= value["leaves_taken"] %}
- {%= value["pending_leaves"] %}
+ {%= value["leaves_pending_approval"] %}
{%= value["remaining_leaves"] %}
{% } %}
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index d2a39989a6..c2a65a4b65 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -1362,7 +1362,7 @@ class SalarySlip(TransactionBase):
'total_allocated_leaves': flt(leave_values.get('total_leaves')),
'expired_leaves': flt(leave_values.get('expired_leaves')),
'used_leaves': flt(leave_values.get('leaves_taken')),
- 'pending_leaves': flt(leave_values.get('pending_leaves')),
+ 'pending_leaves': flt(leave_values.get('leaves_pending_approval')),
'available_leaves': flt(leave_values.get('remaining_leaves'))
})
diff --git a/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json b/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json
index 7ac453b3c3..60ed453938 100644
--- a/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json
+++ b/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json
@@ -26,7 +26,7 @@
"fieldname": "total_allocated_leaves",
"fieldtype": "Float",
"in_list_view": 1,
- "label": "Total Allocated Leave",
+ "label": "Total Allocated Leave(s)",
"no_copy": 1,
"read_only": 1
},
@@ -34,7 +34,7 @@
"fieldname": "expired_leaves",
"fieldtype": "Float",
"in_list_view": 1,
- "label": "Expired Leave",
+ "label": "Expired Leave(s)",
"no_copy": 1,
"read_only": 1
},
@@ -42,7 +42,7 @@
"fieldname": "used_leaves",
"fieldtype": "Float",
"in_list_view": 1,
- "label": "Used Leave",
+ "label": "Used Leave(s)",
"no_copy": 1,
"read_only": 1
},
@@ -50,7 +50,7 @@
"fieldname": "pending_leaves",
"fieldtype": "Float",
"in_list_view": 1,
- "label": "Pending Leave",
+ "label": "Leave(s) Pending Approval",
"no_copy": 1,
"read_only": 1
},
@@ -58,7 +58,7 @@
"fieldname": "available_leaves",
"fieldtype": "Float",
"in_list_view": 1,
- "label": "Available Leave",
+ "label": "Available Leave(s)",
"no_copy": 1,
"read_only": 1
}
@@ -66,7 +66,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-02-19 10:47:48.546724",
+ "modified": "2022-02-28 14:01:32.327204",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Slip Leave",
@@ -74,5 +74,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
From aaa1ae94f255b5acc4dbb197c81c7cf9f5fd4f31 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Mon, 28 Feb 2022 14:26:49 +0530
Subject: [PATCH 179/447] fix: earned leave policy assignment test
---
.../test_leave_policy_assignment.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
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 a19ddce7c0..27e4f148a6 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,7 +4,7 @@
import unittest
import frappe
-from frappe.utils import add_months, get_first_day, get_last_day, getdate
+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 (
get_employee,
@@ -95,9 +95,12 @@ class TestLeavePolicyAssignment(unittest.TestCase):
"leave_policy": leave_policy.name,
"leave_period": leave_period.name
}
+
+ # second last day of the month
+ # leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency
+ frappe.flags.current_date = add_days(get_last_day(getdate()), -1)
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
- # leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency
leaves_allocated = frappe.db.get_value("Leave Allocation", {
"leave_policy_assignment": leave_policy_assignments[0]
}, "total_leaves_allocated")
From 39d3f20d9b45939e1b7a5564a1c79ad99a304953 Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Mon, 28 Feb 2022 14:19:48 +0530
Subject: [PATCH 180/447] fix: removed validation to check zero qty
---
erpnext/controllers/subcontracting.py | 8 --------
1 file changed, 8 deletions(-)
diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py
index 3addb91aaa..c52c688b73 100644
--- a/erpnext/controllers/subcontracting.py
+++ b/erpnext/controllers/subcontracting.py
@@ -363,8 +363,6 @@ class Subcontracting():
return
for row in self.get(self.raw_material_table):
- self.__validate_consumed_qty(row)
-
key = (row.rm_item_code, row.main_item_code, row.purchase_order)
if not self.__transferred_items or not self.__transferred_items.get(key):
return
@@ -372,12 +370,6 @@ class Subcontracting():
self.__validate_batch_no(row, key)
self.__validate_serial_no(row, key)
- def __validate_consumed_qty(self, row):
- if self.backflush_based_on != 'BOM' and flt(row.consumed_qty) == 0.0:
- msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}'
-
- frappe.throw(_(msg),title=_('Consumed Items Qty Check'))
-
def __validate_batch_no(self, row, key):
if row.get('batch_no') and row.get('batch_no') not in self.__transferred_items.get(key).get('batch_no'):
link = get_link_to_form('Purchase Order', row.purchase_order)
From a58dfecb23876120e180e2984009a9cb2d07b720 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Mon, 28 Feb 2022 15:27:24 +0530
Subject: [PATCH 181/447] test: fix test
`test_leave_balance_near_allocaton_expiry`
---
erpnext/hr/doctype/leave_application/leave_application.py | 5 +----
.../hr/doctype/leave_application/test_leave_application.py | 4 +++-
2 files changed, 4 insertions(+), 5 deletions(-)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 697d35a815..f8dbd71e64 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -660,10 +660,7 @@ def get_remaining_leaves(allocation, leaves_taken, date, cf_expiry) -> Dict[str,
leave_balance_for_consumption = flt(allocation.new_leaves_allocated) + flt(remaining_cf_leaves)
remaining_leaves = _get_remaining_leaves(leave_balance_for_consumption, allocation.to_date)
- return {
- 'leave_balance': leave_balance,
- 'leave_balance_for_consumption': remaining_leaves
- }
+ return frappe._dict(leave_balance=leave_balance, leave_balance_for_consumption=remaining_leaves)
def get_leaves_for_period(employee, leave_type, from_date, to_date, skip_expired_leaves=True):
leave_entries = get_leave_entries(employee, leave_type, from_date, to_date)
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 22c7a8f599..c47bbb87af 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -506,8 +506,10 @@ class TestLeaveApplication(unittest.TestCase):
leave_type.insert()
create_carry_forwarded_allocation(employee, leave_type)
+ details = get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8), for_consumption=True)
- self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8)), 21)
+ self.assertEqual(details.leave_balance_for_consumption, 21)
+ self.assertEqual(details.leave_balance, 30)
def test_earned_leaves_creation(self):
From 292fe370dbb1d4c090c78f34bd70ddf95774fe5d Mon Sep 17 00:00:00 2001
From: marination
Date: Mon, 28 Feb 2022 16:35:20 +0530
Subject: [PATCH 182/447] test: combine sub assembly items and make Production
Plan tests atomic
---
.../production_plan/test_production_plan.py | 45 ++++++++++++++++++-
1 file changed, 44 insertions(+), 1 deletion(-)
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 2359815813..8e34e1bca3 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -20,7 +20,7 @@ from erpnext.tests.utils import ERPNextTestCase
class TestProductionPlan(ERPNextTestCase):
- def setUp(self):
+ def setUp(self) -> None:
for item in ['Test Production Item 1', 'Subassembly Item 1',
'Raw Material Item 1', 'Raw Material Item 2']:
create_item(item, valuation_rate=100)
@@ -38,6 +38,9 @@ class TestProductionPlan(ERPNextTestCase):
if not frappe.db.get_value('BOM', {'item': item}):
make_bom(item = item, raw_materials = raw_materials)
+ def tearDown(self) -> None:
+ frappe.db.rollback()
+
def test_production_plan_mr_creation(self):
"Test if MRs are created for unavailable raw materials."
pln = create_production_plan(item_code='Test Production Item 1')
@@ -258,6 +261,46 @@ class TestProductionPlan(ERPNextTestCase):
pln.reload()
pln.cancel()
+ def test_production_plan_combine_subassembly(self):
+ """
+ Test combining Sub assembly items belonging to the same BOM in Prod Plan.
+ 1) Red-Car -> Wheel (sub assembly) > BOM-WHEEL-001
+ 2) Green-Car -> Wheel (sub assembly) > BOM-WHEEL-001
+ """
+ from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
+
+ bom_tree_1 = {
+ "Red-Car": {"Wheel": {"Rubber": {}}}
+ }
+ bom_tree_2 = {
+ "Green-Car": {"Wheel": {"Rubber": {}}}
+ }
+
+ parent_bom_1 = create_nested_bom(bom_tree_1, prefix="")
+ parent_bom_2 = create_nested_bom(bom_tree_2, prefix="")
+
+ # make sure both boms use same subassembly bom
+ subassembly_bom = parent_bom_1.items[0].bom_no
+ frappe.db.set_value("BOM Item", parent_bom_2.items[0].name, "bom_no", subassembly_bom)
+
+ plan = create_production_plan(item_code="Red-Car", use_multi_level_bom=1, do_not_save=True)
+ plan.append("po_items", { # Add Green-Car to Prod Plan
+ 'use_multi_level_bom': 1,
+ 'item_code': "Green-Car",
+ 'bom_no': frappe.db.get_value('Item', "Green-Car", 'default_bom'),
+ 'planned_qty': 1,
+ 'planned_start_date': now_datetime()
+ })
+ plan.get_sub_assembly_items()
+ self.assertTrue(len(plan.sub_assembly_items), 2)
+
+ plan.combine_sub_items = 1
+ plan.get_sub_assembly_items()
+
+ self.assertTrue(len(plan.sub_assembly_items), 1) # check if sub-assembly items merged
+ self.assertEqual(plan.sub_assembly_items[0].qty, 2.0)
+ self.assertEqual(plan.sub_assembly_items[0].stock_qty, 2.0)
+
def test_pp_to_mr_customer_provided(self):
" Test Material Request from Production Plan for Customer Provided Item."
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
From b0d1e6db54ea73af5a2b31d32cd5ba360d0dd7b1 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 28 Feb 2022 16:55:46 +0530
Subject: [PATCH 183/447] test(refactor): use FrappeTestCase
---
.../test_opening_invoice_creation_tool.py | 4 +-
.../shopping_cart/test_shopping_cart.py | 3 +-
.../variant_selector/test_variant_selector.py | 6 +-
.../blanket_order/test_blanket_order.py | 4 +-
erpnext/manufacturing/doctype/bom/test_bom.py | 4 +-
.../bom_update_tool/test_bom_update_tool.py | 4 +-
.../doctype/job_card/test_job_card.py | 4 +-
.../production_plan/test_production_plan.py | 4 +-
.../doctype/routing/test_routing.py | 4 +-
.../doctype/work_order/test_work_order.py | 4 +-
.../doctype/workstation/test_workstation.py | 4 +-
.../selling/doctype/customer/test_customer.py | 5 +-
.../test_party_specific_item.py | 6 +-
.../doctype/quotation/test_quotation.py | 5 +-
.../doctype/sales_order/test_sales_order.py | 4 +-
...st_payment_terms_status_for_sales_order.py | 4 +-
...t_pending_so_items_for_purchase_request.py | 4 +-
.../report/sales_analytics/test_analytics.py | 4 +-
erpnext/stock/doctype/batch/test_batch.py | 4 +-
erpnext/stock/doctype/bin/test_bin.py | 4 +-
.../delivery_note/test_delivery_note.py | 4 +-
.../delivery_trip/test_delivery_trip.py | 5 +-
erpnext/stock/doctype/item/test_item.py | 4 +-
.../item_alternative/test_item_alternative.py | 4 +-
.../item_attribute/test_item_attribute.py | 5 +-
.../doctype/item_price/test_item_price.py | 4 +-
.../test_landed_cost_voucher.py | 4 +-
.../material_request/test_material_request.py | 4 +-
.../doctype/packed_item/test_packed_item.py | 4 +-
.../doctype/packing_slip/test_packing_slip.py | 2 +-
.../stock/doctype/pick_list/test_pick_list.py | 5 +-
.../purchase_receipt/test_purchase_receipt.py | 4 +-
.../doctype/putaway_rule/test_putaway_rule.py | 4 +-
.../test_quality_inspection.py | 4 +-
.../stock/doctype/serial_no/test_serial_no.py | 5 +-
.../stock/doctype/shipment/test_shipment.py | 4 +-
.../doctype/stock_entry/test_stock_entry.py | 4 +-
.../test_stock_ledger_entry.py | 4 +-
.../test_stock_reconciliation.py | 4 +-
.../stock_settings/test_stock_settings.py | 5 +-
.../stock/doctype/warehouse/test_warehouse.py | 4 +-
.../report/stock_ageing/test_stock_ageing.py | 6 +-
.../stock_analytics/test_stock_analytics.py | 5 +-
erpnext/stock/tests/test_valuation.py | 4 +-
erpnext/tests/utils.py | 78 -------------------
45 files changed, 94 insertions(+), 171 deletions(-)
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
index 3eaf6a28f3..77d54a605e 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
@@ -2,6 +2,7 @@
# See license.txt
import frappe
+from frappe.tests.utils import FrappeTestCase
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
@@ -10,11 +11,10 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
get_temporary_opening_account,
)
-from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
-class TestOpeningInvoiceCreationTool(ERPNextTestCase):
+class TestOpeningInvoiceCreationTool(FrappeTestCase):
@classmethod
def setUpClass(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
index 37b3672806..9c389d0d0b 100644
--- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
+++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
@@ -5,6 +5,7 @@
import unittest
import frappe
+from frappe.tests.utils import change_settings
from frappe.utils import add_months, nowdate
from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule
@@ -15,7 +16,7 @@ from erpnext.e_commerce.shopping_cart.cart import (
get_party,
update_cart,
)
-from erpnext.tests.utils import change_settings, create_test_contact_and_address
+from erpnext.tests.utils import create_test_contact_and_address
# test_dependencies = ['Payment Terms Template']
diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py
index 4d907c6221..ee098e16e7 100644
--- a/erpnext/e_commerce/variant_selector/test_variant_selector.py
+++ b/erpnext/e_commerce/variant_selector/test_variant_selector.py
@@ -1,4 +1,5 @@
import frappe
+from frappe.tests.utils import FrappeTestCase
from erpnext.controllers.item_variant import create_variant
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
@@ -7,11 +8,10 @@ from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings imp
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values
from erpnext.stock.doctype.item.test_item import make_item
-from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Item"]
-class TestVariantSelector(ERPNextTestCase):
+class TestVariantSelector(FrappeTestCase):
@classmethod
def setUpClass(cls):
@@ -116,4 +116,4 @@ class TestVariantSelector(ERPNextTestCase):
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
self.assertEqual(price_info["price_list_rate"], 100.0)
- self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00")
\ No newline at end of file
+ self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00")
diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
index eff2344e85..d4d337d841 100644
--- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
+++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
@@ -1,15 +1,15 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_months, today
from erpnext import get_company_currency
-from erpnext.tests.utils import ERPNextTestCase
from .blanket_order import make_order
-class TestBlanketOrder(ERPNextTestCase):
+class TestBlanketOrder(FrappeTestCase):
def setUp(self):
frappe.flags.args = frappe._dict()
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 53437c8012..3cc91b341c 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -7,6 +7,7 @@ from functools import partial
import frappe
from frappe.test_runner import make_test_records
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import cstr, flt
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -17,11 +18,10 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
create_stock_reconciliation,
)
from erpnext.tests.test_subcontracting import set_backflush_based_on
-from erpnext.tests.utils import ERPNextTestCase
test_records = frappe.get_test_records('BOM')
-class TestBOM(ERPNextTestCase):
+class TestBOM(FrappeTestCase):
def setUp(self):
if not frappe.get_value('Item', '_Test Item'):
make_test_records('Item')
diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
index 12576cbf32..b4c625d610 100644
--- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
+++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
@@ -2,15 +2,15 @@
# License: GNU General Public License v3. See license.txt
import frappe
+from frappe.tests.utils import FrappeTestCase
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.stock.doctype.item.test_item import create_item
-from erpnext.tests.utils import ERPNextTestCase
test_records = frappe.get_test_records('BOM')
-class TestBOMUpdateTool(ERPNextTestCase):
+class TestBOMUpdateTool(FrappeTestCase):
def test_replace_bom(self):
current_bom = "BOM-_Test Item Home Desktop Manufactured-001"
diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py
index bb5004ba86..33425d2314 100644
--- a/erpnext/manufacturing/doctype/job_card/test_job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py
@@ -2,6 +2,7 @@
# See license.txt
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import random_string
from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError
@@ -11,10 +12,9 @@ from erpnext.manufacturing.doctype.job_card.job_card import (
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
-from erpnext.tests.utils import ERPNextTestCase
-class TestJobCard(ERPNextTestCase):
+class TestJobCard(FrappeTestCase):
def setUp(self):
make_bom_for_jc_tests()
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 2359815813..b2a41ff5b2 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -1,6 +1,7 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_to_date, flt, now_datetime, nowdate
from erpnext.controllers.item_variant import create_variant
@@ -16,10 +17,9 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
-from erpnext.tests.utils import ERPNextTestCase
-class TestProductionPlan(ERPNextTestCase):
+class TestProductionPlan(FrappeTestCase):
def setUp(self):
for item in ['Test Production Item 1', 'Subassembly Item 1',
'Raw Material Item 1', 'Raw Material Item 2']:
diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py
index 8bd60ea4ac..696d9bca14 100644
--- a/erpnext/manufacturing/doctype/routing/test_routing.py
+++ b/erpnext/manufacturing/doctype/routing/test_routing.py
@@ -2,14 +2,14 @@
# See license.txt
import frappe
from frappe.test_runner import make_test_records
+from frappe.tests.utils import FrappeTestCase
from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.stock.doctype.item.test_item import make_item
-from erpnext.tests.utils import ERPNextTestCase
-class TestRouting(ERPNextTestCase):
+class TestRouting(FrappeTestCase):
@classmethod
def setUpClass(cls):
cls.item_code = "Test Routing Item - A"
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 67c47efb5b..549ec7b4a6 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
import frappe
+from frappe.tests.utils import FrappeTestCase, timeout
from frappe.utils import add_days, add_months, cint, flt, now, today
from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
@@ -21,10 +22,9 @@ from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.stock_entry import test_stock_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.utils import get_bin
-from erpnext.tests.utils import ERPNextTestCase, timeout
-class TestWorkOrder(ERPNextTestCase):
+class TestWorkOrder(FrappeTestCase):
def setUp(self):
self.warehouse = '_Test Warehouse 2 - _TC'
self.item = '_Test Item'
diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py
index c298c0a8db..dd51017bb7 100644
--- a/erpnext/manufacturing/doctype/workstation/test_workstation.py
+++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py
@@ -2,6 +2,7 @@
# See license.txt
import frappe
from frappe.test_runner import make_test_records
+from frappe.tests.utils import FrappeTestCase
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
from erpnext.manufacturing.doctype.routing.test_routing import create_routing, setup_bom
@@ -10,13 +11,12 @@ from erpnext.manufacturing.doctype.workstation.workstation import (
WorkstationHolidayError,
check_if_within_operating_hours,
)
-from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Warehouse"]
test_records = frappe.get_test_records('Workstation')
make_test_records('Workstation')
-class TestWorkstation(ERPNextTestCase):
+class TestWorkstation(FrappeTestCase):
def test_validate_timings(self):
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00")
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00")
diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py
index 5301fd0524..165ee81872 100644
--- a/erpnext/selling/doctype/customer/test_customer.py
+++ b/erpnext/selling/doctype/customer/test_customer.py
@@ -4,12 +4,13 @@
import frappe
from frappe.test_runner import make_test_records
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt
from erpnext.accounts.party import get_due_date
from erpnext.exceptions import PartyDisabled, PartyFrozen
from erpnext.selling.doctype.customer.customer import get_credit_limit, get_customer_outstanding
-from erpnext.tests.utils import ERPNextTestCase, create_test_contact_and_address
+from erpnext.tests.utils import create_test_contact_and_address
test_ignore = ["Price List"]
test_dependencies = ['Payment Term', 'Payment Terms Template']
@@ -17,7 +18,7 @@ test_records = frappe.get_test_records('Customer')
-class TestCustomer(ERPNextTestCase):
+class TestCustomer(FrappeTestCase):
def setUp(self):
if not frappe.get_value('Item', '_Test Item'):
make_test_records('Item')
diff --git a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py
index b951044f33..9b672b4b5d 100644
--- a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py
+++ b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py
@@ -1,12 +1,10 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-import unittest
-
import frappe
+from frappe.tests.utils import FrappeTestCase
from erpnext.controllers.queries import item_query
-from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ['Item', 'Customer', 'Supplier']
@@ -18,7 +16,7 @@ def create_party_specific_item(**args):
psi.based_on_value = args.get('based_on_value')
psi.insert()
-class TestPartySpecificItem(ERPNextTestCase):
+class TestPartySpecificItem(FrappeTestCase):
def setUp(self):
self.customer = frappe.get_last_doc("Customer")
self.supplier = frappe.get_last_doc("Supplier")
diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py
index 4357201d23..a749d9e1f1 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -2,14 +2,13 @@
# License: GNU General Public License v3. See license.txt
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, add_months, flt, getdate, nowdate
-from erpnext.tests.utils import ERPNextTestCase
-
test_dependencies = ["Product Bundle"]
-class TestQuotation(ERPNextTestCase):
+class TestQuotation(FrappeTestCase):
def test_make_quotation_without_terms(self):
quotation = make_quotation(do_not_save=1)
self.assertFalse(quotation.get('payment_schedule'))
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 73c5bd299a..f5a34c0eec 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -6,6 +6,7 @@ import json
import frappe
import frappe.permissions
from frappe.core.doctype.user_permission.test_user_permission import create_user
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, flt, getdate, nowdate, today
from erpnext.controllers.accounts_controller import update_child_qty_rate
@@ -27,10 +28,9 @@ from erpnext.selling.doctype.sales_order.sales_order import (
)
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
-from erpnext.tests.utils import ERPNextTestCase
-class TestSalesOrder(ERPNextTestCase):
+class TestSalesOrder(FrappeTestCase):
@classmethod
def setUpClass(cls):
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
index cad41e1dc0..f7f8a5dbce 100644
--- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
@@ -1,6 +1,7 @@
import datetime
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
@@ -9,12 +10,11 @@ from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_s
execute,
)
from erpnext.stock.doctype.item.test_item import create_item
-from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template"]
-class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase):
+class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
def create_payment_terms_template(self):
# create template for 50-50 payments
template = None
diff --git a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py
index d62915fc66..16162acc8f 100644
--- a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py
+++ b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py
@@ -2,6 +2,7 @@
# For license information, please see license.txt
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_months, nowdate
from erpnext.selling.doctype.sales_order.sales_order import make_material_request
@@ -9,10 +10,9 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
from erpnext.selling.report.pending_so_items_for_purchase_request.pending_so_items_for_purchase_request import (
execute,
)
-from erpnext.tests.utils import ERPNextTestCase
-class TestPendingSOItemsForPurchaseRequest(ERPNextTestCase):
+class TestPendingSOItemsForPurchaseRequest(FrappeTestCase):
def test_result_for_partial_material_request(self):
so = make_sales_order()
mr=make_material_request(so.name)
diff --git a/erpnext/selling/report/sales_analytics/test_analytics.py b/erpnext/selling/report/sales_analytics/test_analytics.py
index f56cce2dfd..564f48fef3 100644
--- a/erpnext/selling/report/sales_analytics/test_analytics.py
+++ b/erpnext/selling/report/sales_analytics/test_analytics.py
@@ -3,13 +3,13 @@
import frappe
+from frappe.tests.utils import FrappeTestCase
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.selling.report.sales_analytics.sales_analytics import execute
-from erpnext.tests.utils import ERPNextTestCase
-class TestAnalytics(ERPNextTestCase):
+class TestAnalytics(FrappeTestCase):
def test_sales_analytics(self):
frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'")
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index 613dd3f14d..5763753853 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -5,6 +5,7 @@ import json
import frappe
from frappe.exceptions import ValidationError
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import cint, flt
from frappe.utils.data import add_to_date, getdate
@@ -16,10 +17,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
)
from erpnext.stock.get_item_details import get_item_details
from erpnext.stock.stock_ledger import get_valuation_rate
-from erpnext.tests.utils import ERPNextTestCase
-class TestBatch(ERPNextTestCase):
+class TestBatch(FrappeTestCase):
def test_item_has_batch_enabled(self):
self.assertRaises(ValidationError, frappe.get_doc({
"doctype": "Batch",
diff --git a/erpnext/stock/doctype/bin/test_bin.py b/erpnext/stock/doctype/bin/test_bin.py
index 250126c6b9..ec0d8a88e3 100644
--- a/erpnext/stock/doctype/bin/test_bin.py
+++ b/erpnext/stock/doctype/bin/test_bin.py
@@ -2,13 +2,13 @@
# See license.txt
import frappe
+from frappe.tests.utils import FrappeTestCase
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.utils import _create_bin
-from erpnext.tests.utils import ERPNextTestCase
-class TestBin(ERPNextTestCase):
+class TestBin(FrappeTestCase):
def test_concurrent_inserts(self):
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index bd18e788ba..16c892128a 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -6,6 +6,7 @@
import json
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import cstr, flt, nowdate, nowtime
from erpnext.accounts.doctype.account.test_account import get_inventory_account
@@ -35,10 +36,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
)
from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse
from erpnext.stock.stock_ledger import get_previous_sle
-from erpnext.tests.utils import ERPNextTestCase
-class TestDeliveryNote(ERPNextTestCase):
+class TestDeliveryNote(FrappeTestCase):
def test_over_billing_against_dn(self):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
diff --git a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py
index 321f48b2c5..dcdff4a0f1 100644
--- a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py
+++ b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py
@@ -4,6 +4,7 @@
import unittest
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, flt, now_datetime, nowdate
import erpnext
@@ -12,10 +13,10 @@ from erpnext.stock.doctype.delivery_trip.delivery_trip import (
make_expense_claim,
notify_customers,
)
-from erpnext.tests.utils import ERPNextTestCase, create_test_contact_and_address
+from erpnext.tests.utils import create_test_contact_and_address
-class TestDeliveryTrip(ERPNextTestCase):
+class TestDeliveryTrip(FrappeTestCase):
def setUp(self):
super().setUp()
driver = create_driver()
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 9491e17259..d7671b1d71 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -6,6 +6,7 @@ import json
import frappe
from frappe.test_runner import make_test_objects
+from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, today
from erpnext.controllers.item_variant import (
@@ -25,7 +26,6 @@ from erpnext.stock.doctype.item.item import (
)
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.get_item_details import get_item_details
-from erpnext.tests.utils import ERPNextTestCase, change_settings
test_ignore = ["BOM"]
test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"]
@@ -53,7 +53,7 @@ def make_item(item_code, properties=None):
return item
-class TestItem(ERPNextTestCase):
+class TestItem(FrappeTestCase):
def setUp(self):
super().setUp()
frappe.flags.attribute_values = None
diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
index 3976af4e88..501c1c1ad3 100644
--- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py
+++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
@@ -4,6 +4,7 @@
import json
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt
from erpnext.buying.doctype.purchase_order.purchase_order import (
@@ -18,10 +19,9 @@ from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
-from erpnext.tests.utils import ERPNextTestCase
-class TestItemAlternative(ERPNextTestCase):
+class TestItemAlternative(FrappeTestCase):
def setUp(self):
super().setUp()
make_items()
diff --git a/erpnext/stock/doctype/item_attribute/test_item_attribute.py b/erpnext/stock/doctype/item_attribute/test_item_attribute.py
index 0b7ca25715..055c22e0c5 100644
--- a/erpnext/stock/doctype/item_attribute/test_item_attribute.py
+++ b/erpnext/stock/doctype/item_attribute/test_item_attribute.py
@@ -6,11 +6,12 @@ import frappe
test_records = frappe.get_test_records('Item Attribute')
+from frappe.tests.utils import FrappeTestCase
+
from erpnext.stock.doctype.item_attribute.item_attribute import ItemAttributeIncrementError
-from erpnext.tests.utils import ERPNextTestCase
-class TestItemAttribute(ERPNextTestCase):
+class TestItemAttribute(FrappeTestCase):
def setUp(self):
super().setUp()
if frappe.db.exists("Item Attribute", "_Test_Length"):
diff --git a/erpnext/stock/doctype/item_price/test_item_price.py b/erpnext/stock/doctype/item_price/test_item_price.py
index f81770e487..6ceba3f8d3 100644
--- a/erpnext/stock/doctype/item_price/test_item_price.py
+++ b/erpnext/stock/doctype/item_price/test_item_price.py
@@ -4,13 +4,13 @@
import frappe
from frappe.test_runner import make_test_records_for_doctype
+from frappe.tests.utils import FrappeTestCase
from erpnext.stock.doctype.item_price.item_price import ItemPriceDuplicateItem
from erpnext.stock.get_item_details import get_price_list_rate_for, process_args
-from erpnext.tests.utils import ERPNextTestCase
-class TestItemPrice(ERPNextTestCase):
+class TestItemPrice(FrappeTestCase):
def setUp(self):
super().setUp()
frappe.db.sql("delete from `tabItem Price`")
diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
index df8cadd7f8..dbaefc1e11 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
@@ -4,6 +4,7 @@
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_to_date, flt, now
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
@@ -14,10 +15,9 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
get_gl_entries,
make_purchase_receipt,
)
-from erpnext.tests.utils import ERPNextTestCase
-class TestLandedCostVoucher(ERPNextTestCase):
+class TestLandedCostVoucher(FrappeTestCase):
def test_landed_cost_voucher(self):
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)
diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py
index 383b0ae806..1cda781617 100644
--- a/erpnext/stock/doctype/material_request/test_material_request.py
+++ b/erpnext/stock/doctype/material_request/test_material_request.py
@@ -6,6 +6,7 @@
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt, today
from erpnext.stock.doctype.item.test_item import create_item
@@ -15,10 +16,9 @@ from erpnext.stock.doctype.material_request.material_request import (
make_supplier_quotation,
raise_work_orders,
)
-from erpnext.tests.utils import ERPNextTestCase
-class TestMaterialRequest(ERPNextTestCase):
+class TestMaterialRequest(FrappeTestCase):
def test_make_purchase_order(self):
mr = frappe.copy_doc(test_records[0]).insert()
diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py
index 2521ac9fe7..94268a8ef3 100644
--- a/erpnext/stock/doctype/packed_item/test_packed_item.py
+++ b/erpnext/stock/doctype/packed_item/test_packed_item.py
@@ -1,6 +1,7 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
+from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_to_date, nowdate
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
@@ -9,10 +10,9 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
-from erpnext.tests.utils import ERPNextTestCase, change_settings
-class TestPackedItem(ERPNextTestCase):
+class TestPackedItem(FrappeTestCase):
"Test impact on Packed Items table in various scenarios."
@classmethod
def setUpClass(cls) -> None:
diff --git a/erpnext/stock/doctype/packing_slip/test_packing_slip.py b/erpnext/stock/doctype/packing_slip/test_packing_slip.py
index 5eb6b7399a..bc405b2099 100644
--- a/erpnext/stock/doctype/packing_slip/test_packing_slip.py
+++ b/erpnext/stock/doctype/packing_slip/test_packing_slip.py
@@ -4,7 +4,7 @@
import unittest
# test_records = frappe.get_test_records('Packing Slip')
-from erpnext.tests.utils import ERPNextTestCase
+from frappe.tests.utils import FrappeTestCase
class TestPackingSlip(unittest.TestCase):
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index 41e3150f0d..f3b6b89784 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -6,16 +6,17 @@ from frappe import _dict
test_dependencies = ['Item', 'Sales Invoice', 'Stock Entry', 'Batch']
+from frappe.tests.utils import FrappeTestCase
+
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
EmptyStockReconciliationItemsError,
)
-from erpnext.tests.utils import ERPNextTestCase
-class TestPickList(ERPNextTestCase):
+class TestPickList(FrappeTestCase):
def test_pick_list_picks_warehouse_for_each_item(self):
try:
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index d481689c13..a24acb1bd8 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -7,6 +7,7 @@ import unittest
from collections import defaultdict
import frappe
+from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, cint, cstr, flt, today
import erpnext
@@ -17,10 +18,9 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchas
from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction
-from erpnext.tests.utils import ERPNextTestCase, change_settings
-class TestPurchaseReceipt(ERPNextTestCase):
+class TestPurchaseReceipt(FrappeTestCase):
def setUp(self):
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)
diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
index ff1c19a827..4e8d71fe5e 100644
--- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
+++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
@@ -2,6 +2,7 @@
# See license.txt
import frappe
+from frappe.tests.utils import FrappeTestCase
from erpnext.stock.doctype.batch.test_batch import make_new_batch
from erpnext.stock.doctype.item.test_item import make_item
@@ -9,10 +10,9 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.get_item_details import get_conversion_factor
-from erpnext.tests.utils import ERPNextTestCase
-class TestPutawayRule(ERPNextTestCase):
+class TestPutawayRule(FrappeTestCase):
def setUp(self):
if not frappe.db.exists("Item", "_Rice"):
make_item("_Rice", {
diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
index 308c62875d..601ca054b5 100644
--- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
@@ -2,6 +2,7 @@
# See license.txt
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import nowdate
from erpnext.controllers.stock_controller import (
@@ -13,12 +14,11 @@ from erpnext.controllers.stock_controller import (
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
-from erpnext.tests.utils import ERPNextTestCase
# test_records = frappe.get_test_records('Quality Inspection')
-class TestQualityInspection(ERPNextTestCase):
+class TestQualityInspection(FrappeTestCase):
def setUp(self):
super().setUp()
create_item("_Test Item with QA")
diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py
index f8cea71725..057a7d4c01 100644
--- a/erpnext/stock/doctype/serial_no/test_serial_no.py
+++ b/erpnext/stock/doctype/serial_no/test_serial_no.py
@@ -18,11 +18,12 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
test_dependencies = ["Item"]
test_records = frappe.get_test_records('Serial No')
+from frappe.tests.utils import FrappeTestCase
+
from erpnext.stock.doctype.serial_no.serial_no import *
-from erpnext.tests.utils import ERPNextTestCase
-class TestSerialNo(ERPNextTestCase):
+class TestSerialNo(FrappeTestCase):
def tearDown(self):
frappe.db.rollback()
diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py
index afe821845a..317abb6d03 100644
--- a/erpnext/stock/doctype/shipment/test_shipment.py
+++ b/erpnext/stock/doctype/shipment/test_shipment.py
@@ -4,12 +4,12 @@
from datetime import date, timedelta
import frappe
+from frappe.tests.utils import FrappeTestCase
from erpnext.stock.doctype.delivery_note.delivery_note import make_shipment
-from erpnext.tests.utils import ERPNextTestCase
-class TestShipment(ERPNextTestCase):
+class TestShipment(FrappeTestCase):
def test_shipment_from_delivery_note(self):
delivery_note = create_test_delivery_note()
delivery_note.submit()
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index c5afa49166..54c0e43c5e 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -6,6 +6,7 @@ import unittest
import frappe
from frappe.permissions import add_user_permission, remove_user_permission
+from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import flt, nowdate, nowtime
from erpnext.accounts.doctype.account.test_account import get_inventory_account
@@ -28,7 +29,6 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
create_stock_reconciliation,
)
from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle
-from erpnext.tests.utils import ERPNextTestCase, change_settings
def get_sle(**args):
@@ -42,7 +42,7 @@ def get_sle(**args):
order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""% condition,
values, as_dict=1)
-class TestStockEntry(ERPNextTestCase):
+class TestStockEntry(FrappeTestCase):
def tearDown(self):
frappe.db.rollback()
frappe.set_user("Administrator")
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 0864ece995..01d25b2e86 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
@@ -7,6 +7,7 @@ from uuid import uuid4
import frappe
from frappe.core.page.permission_manager.permission_manager import reset
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, today
from erpnext.stock.doctype.delivery_note.test_delivery_note import (
@@ -24,10 +25,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
create_stock_reconciliation,
)
from erpnext.stock.stock_ledger import get_previous_sle
-from erpnext.tests.utils import ERPNextTestCase
-class TestStockLedgerEntry(ERPNextTestCase):
+class TestStockLedgerEntry(FrappeTestCase):
def setUp(self):
items = create_items()
reset('Stock Entry')
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 2ffe127d9a..e6b252e856 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -6,6 +6,7 @@
import frappe
+from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, cstr, flt, nowdate, nowtime, random_string
from erpnext.accounts.utils import get_stock_and_account_balance
@@ -19,10 +20,9 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after
from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method
-from erpnext.tests.utils import ERPNextTestCase, change_settings
-class TestStockReconciliation(ERPNextTestCase):
+class TestStockReconciliation(FrappeTestCase):
@classmethod
def setUpClass(cls):
create_batch_or_serial_no_items()
diff --git a/erpnext/stock/doctype/stock_settings/test_stock_settings.py b/erpnext/stock/doctype/stock_settings/test_stock_settings.py
index 072b54b820..13496718ea 100644
--- a/erpnext/stock/doctype/stock_settings/test_stock_settings.py
+++ b/erpnext/stock/doctype/stock_settings/test_stock_settings.py
@@ -4,11 +4,10 @@
import unittest
import frappe
-
-from erpnext.tests.utils import ERPNextTestCase
+from frappe.tests.utils import FrappeTestCase
-class TestStockSettings(ERPNextTestCase):
+class TestStockSettings(FrappeTestCase):
def setUp(self):
super().setUp()
frappe.db.set_value("Stock Settings", None, "clean_description_html", 0)
diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py
index 26db2642e4..cdb771935b 100644
--- a/erpnext/stock/doctype/warehouse/test_warehouse.py
+++ b/erpnext/stock/doctype/warehouse/test_warehouse.py
@@ -3,17 +3,17 @@
import frappe
from frappe.test_runner import make_test_records
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import cint
import erpnext
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
-from erpnext.tests.utils import ERPNextTestCase
test_records = frappe.get_test_records('Warehouse')
-class TestWarehouse(ERPNextTestCase):
+class TestWarehouse(FrappeTestCase):
def setUp(self):
super().setUp()
if not frappe.get_value('Item', '_Test Item'):
diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
index 3fc357e8d4..2630805c62 100644
--- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
@@ -2,12 +2,12 @@
# See license.txt
import frappe
+from frappe.tests.utils import FrappeTestCase
from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, format_report_data
-from erpnext.tests.utils import ERPNextTestCase
-class TestStockAgeing(ERPNextTestCase):
+class TestStockAgeing(FrappeTestCase):
def setUp(self) -> None:
self.filters = frappe._dict(
company="_Test Company",
@@ -610,4 +610,4 @@ def generate_item_and_item_wh_wise_slots(filters, sle):
item_wh_wise_slots = FIFOSlots(filters, sle).generate()
filters.show_warehouse_wise_stock = False
- return item_wise_slots, item_wh_wise_slots
\ No newline at end of file
+ return item_wise_slots, item_wh_wise_slots
diff --git a/erpnext/stock/report/stock_analytics/test_stock_analytics.py b/erpnext/stock/report/stock_analytics/test_stock_analytics.py
index 32df585937..f6c98f914d 100644
--- a/erpnext/stock/report/stock_analytics/test_stock_analytics.py
+++ b/erpnext/stock/report/stock_analytics/test_stock_analytics.py
@@ -1,14 +1,13 @@
import datetime
-import unittest
from frappe import _dict
+from frappe.tests.utils import FrappeTestCase
from erpnext.accounts.utils import get_fiscal_year
from erpnext.stock.report.stock_analytics.stock_analytics import get_period_date_ranges
-from erpnext.tests.utils import ERPNextTestCase
-class TestStockAnalyticsReport(ERPNextTestCase):
+class TestStockAnalyticsReport(FrappeTestCase):
def test_get_period_date_ranges(self):
filters = _dict(range="Monthly", from_date="2020-12-28", to_date="2021-02-06")
diff --git a/erpnext/stock/tests/test_valuation.py b/erpnext/stock/tests/test_valuation.py
index bdb768f1ad..b64ff8e28c 100644
--- a/erpnext/stock/tests/test_valuation.py
+++ b/erpnext/stock/tests/test_valuation.py
@@ -2,13 +2,13 @@ import json
import unittest
import frappe
+from frappe.tests.utils import FrappeTestCase
from hypothesis import given
from hypothesis import strategies as st
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
-from erpnext.tests.utils import ERPNextTestCase
qty_gen = st.floats(min_value=-1e6, max_value=1e6)
value_gen = st.floats(min_value=1, max_value=1e6)
@@ -290,7 +290,7 @@ class TestLIFOValuation(unittest.TestCase):
self.assertTotalQty(total_qty)
self.assertTotalValue(total_value)
-class TestLIFOValuationSLE(ERPNextTestCase):
+class TestLIFOValuationSLE(FrappeTestCase):
ITEM_CODE = "_Test LIFO item"
WAREHOUSE = "_Test Warehouse - _TC"
diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py
index 40c95eb7a3..d795253665 100644
--- a/erpnext/tests/utils.py
+++ b/erpnext/tests/utils.py
@@ -1,10 +1,6 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-import copy
-import signal
-import unittest
-from contextlib import contextmanager
from typing import Any, Dict, NewType, Optional
import frappe
@@ -13,22 +9,6 @@ from frappe.core.doctype.report.report import get_report_module_dotted_path
ReportFilters = Dict[str, Any]
ReportName = NewType("ReportName", str)
-
-class ERPNextTestCase(unittest.TestCase):
- """A sane default test class for ERPNext tests."""
-
-
- @classmethod
- def setUpClass(cls) -> None:
- frappe.db.commit()
- return super().setUpClass()
-
- @classmethod
- def tearDownClass(cls) -> None:
- frappe.db.rollback()
- return super().tearDownClass()
-
-
def create_test_contact_and_address():
frappe.db.sql('delete from tabContact')
frappe.db.sql('delete from `tabContact Email`')
@@ -81,43 +61,6 @@ def create_test_contact_and_address():
contact_two.insert()
-@contextmanager
-def change_settings(doctype, settings_dict):
- """ A context manager to ensure that settings are changed before running
- function and restored after running it regardless of exceptions occured.
- This is useful in tests where you want to make changes in a function but
- don't retain those changes.
- import and use as decorator to cover full function or using `with` statement.
-
- example:
- @change_settings("Stock Settings", {"item_naming_by": "Naming Series"})
- def test_case(self):
- ...
- """
-
- try:
- settings = frappe.get_doc(doctype)
- # remember setting
- previous_settings = copy.deepcopy(settings_dict)
- for key in previous_settings:
- previous_settings[key] = getattr(settings, key)
-
- # change setting
- for key, value in settings_dict.items():
- setattr(settings, key, value)
- settings.save()
- # singles are cached by default, clear to avoid flake
- frappe.db.value_cache[settings] = {}
- yield # yield control to calling function
-
- finally:
- # restore settings
- settings = frappe.get_doc(doctype)
- for key, value in previous_settings.items():
- setattr(settings, key, value)
- settings.save()
-
-
def execute_script_report(
report_name: ReportName,
module: str,
@@ -157,24 +100,3 @@ def execute_script_report(
except Exception:
print(f"Report failed to execute with filters: {test_filter}")
raise
-
-
-
-def timeout(seconds=30, error_message="Test timed out."):
- """ Timeout decorator to ensure a test doesn't run for too long.
-
- adapted from https://stackoverflow.com/a/2282656"""
- def decorator(func):
- def _handle_timeout(signum, frame):
- raise Exception(error_message)
-
- def wrapper(*args, **kwargs):
- signal.signal(signal.SIGALRM, _handle_timeout)
- signal.alarm(seconds)
- try:
- result = func(*args, **kwargs)
- finally:
- signal.alarm(0)
- return result
- return wrapper
- return decorator
From 3f3b1766c2159fbe5733615e5a00a4698d279937 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Mon, 28 Feb 2022 17:30:09 +0530
Subject: [PATCH 184/447] test: get leave details for leave application
dashboard
---
.../leave_application/leave_application.py | 12 ++--
.../test_leave_application.py | 56 ++++++++++++++-----
.../doctype/salary_slip/test_salary_slip.py | 9 +--
3 files changed, 52 insertions(+), 25 deletions(-)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index f8dbd71e64..f98994511a 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -539,16 +539,14 @@ def get_leave_details(employee, date):
"remaining_leaves": remaining_leaves}
#is used in set query
- lwps = frappe.get_list("Leave Type", filters = {"is_lwp": 1})
- lwps = [lwp.name for lwp in lwps]
+ lwp = frappe.get_list("Leave Type", filters={"is_lwp": 1}, pluck="name")
- ret = {
- 'leave_allocation': leave_allocation,
- 'leave_approver': get_leave_approver(employee),
- 'lwps': lwps
+ return {
+ "leave_allocation": leave_allocation,
+ "leave_approver": get_leave_approver(employee),
+ "lwps": lwp
}
- return ret
@frappe.whitelist()
def get_leave_balance_on(employee, leave_type, date, to_date=None,
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index c47bbb87af..287cb5cf94 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -17,12 +17,14 @@ from frappe.utils import (
)
from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
from erpnext.hr.doctype.leave_application.leave_application import (
LeaveDayBlockedError,
NotAnOptionalHoliday,
OverlapError,
get_leave_balance_on,
+ get_leave_details,
)
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
create_assignment_for_multiple_employees,
@@ -33,7 +35,7 @@ from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
make_leave_application,
)
-test_dependencies = ["Leave Allocation", "Leave Block List", "Employee"]
+test_dependencies = ["Leave Type", "Leave Allocation", "Leave Block List", "Employee"]
_test_records = [
{
@@ -72,12 +74,13 @@ _test_records = [
class TestLeaveApplication(unittest.TestCase):
def setUp(self):
for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]:
- frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec
+ frappe.db.delete(dt)
frappe.set_user("Administrator")
set_leave_approver()
- frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'")
+ frappe.db.delete("Attendance", {"employee": "_T-Employee-00001"})
+ self.holiday_list = make_holiday_list()
def tearDown(self):
frappe.db.rollback()
@@ -119,6 +122,7 @@ class TestLeaveApplication(unittest.TestCase):
for d in ('2018-01-01', '2018-01-02', '2018-01-03'):
self.assertTrue(getdate(d) in dates)
+ @set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
def test_attendance_for_include_holidays(self):
# Case 1: leave type with 'Include holidays within leaves as leaves' enabled
frappe.delete_doc_if_exists("Leave Type", "Test Include Holidays", force=1)
@@ -131,18 +135,17 @@ class TestLeaveApplication(unittest.TestCase):
date = getdate()
make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
- holiday_list = make_holiday_list()
employee = get_employee()
- frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list)
- first_sunday = get_first_sunday(holiday_list)
+ first_sunday = get_first_sunday(self.holiday_list)
- leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name)
+ leave_application = make_leave_application(employee.name, add_days(first_sunday, 1), add_days(first_sunday, 4), leave_type.name)
leave_application.reload()
self.assertEqual(leave_application.total_leave_days, 4)
self.assertEqual(frappe.db.count('Attendance', {'leave_application': leave_application.name}), 4)
leave_application.cancel()
+ @set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
def test_attendance_update_for_exclude_holidays(self):
# Case 2: leave type with 'Include holidays within leaves as leaves' disabled
frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1)
@@ -155,10 +158,8 @@ class TestLeaveApplication(unittest.TestCase):
date = getdate()
make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
- holiday_list = make_holiday_list()
employee = get_employee()
- frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list)
- first_sunday = get_first_sunday(holiday_list)
+ first_sunday = get_first_sunday(self.holiday_list)
# already marked attendance on a holiday should be deleted in this case
config = {
@@ -320,16 +321,14 @@ class TestLeaveApplication(unittest.TestCase):
application.half_day_date = "2013-01-05"
application.insert()
+ @set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
def test_optional_leave(self):
leave_period = get_leave_period()
today = nowdate()
holiday_list = 'Test Holiday List for Optional Holiday'
employee = get_employee()
- default_holiday_list = make_holiday_list()
- frappe.db.set_value("Company", employee.company, "default_holiday_list", default_holiday_list)
- first_sunday = get_first_sunday(default_holiday_list)
-
+ first_sunday = get_first_sunday(self.holiday_list)
optional_leave_date = add_days(first_sunday, 1)
if not frappe.db.exists('Holiday List', holiday_list):
@@ -706,6 +705,35 @@ class TestLeaveApplication(unittest.TestCase):
employee.leave_approver = ""
employee.save()
+ @set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
+ def test_get_leave_details_for_dashboard(self):
+ employee = get_employee()
+ date = getdate()
+ year_start = getdate(get_year_start(date))
+ year_end = getdate(get_year_ending(date))
+
+ # ALLOCATION = 30
+ allocation = make_allocation_record(employee=employee.name, from_date=year_start, to_date=year_end)
+
+ # USED LEAVES = 4
+ first_sunday = get_first_sunday(self.holiday_list)
+ leave_application = make_leave_application(employee.name, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type')
+ leave_application.reload()
+
+ # LEAVES PENDING APPROVAL = 1
+ leave_application = make_leave_application(employee.name, add_days(first_sunday, 5), add_days(first_sunday, 5),
+ '_Test Leave Type', submit=False)
+ leave_application.status = 'Open'
+ leave_application.save()
+
+ details = get_leave_details(employee.name, allocation.from_date)
+ leave_allocation = details['leave_allocation']['_Test Leave Type']
+ self.assertEqual(leave_allocation['total_leaves'], 30)
+ self.assertEqual(leave_allocation['leaves_taken'], 4)
+ self.assertEqual(leave_allocation['expired_leaves'], 0)
+ self.assertEqual(leave_allocation['leaves_pending_approval'], 1)
+ self.assertEqual(leave_allocation['remaining_leaves'], 26)
+
def create_carry_forwarded_allocation(employee, leave_type):
# initial leave allocation
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index d34f6a6038..6c9880a00b 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -989,7 +989,7 @@ def create_additional_salary(employee, payroll_period, amount):
}).submit()
return salary_date
-def make_leave_application(employee, from_date, to_date, leave_type, company=None):
+def make_leave_application(employee, from_date, to_date, leave_type, company=None, submit=True):
leave_application = frappe.get_doc(dict(
doctype = 'Leave Application',
employee = employee,
@@ -997,11 +997,12 @@ def make_leave_application(employee, from_date, to_date, leave_type, company=Non
from_date = from_date,
to_date = to_date,
company = company or erpnext.get_default_company() or "_Test Company",
- docstatus = 1,
status = "Approved",
leave_approver = 'test@example.com'
- ))
- leave_application.submit()
+ )).insert()
+
+ if submit:
+ leave_application.submit()
return leave_application
From 829c453cb616107471ed5fd6b0ef7b75bb0420e7 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 28 Feb 2022 17:12:51 +0530
Subject: [PATCH 185/447] test: remove transaction commits from buying module
---
erpnext/buying/doctype/purchase_order/test_purchase_order.py | 4 ++--
.../request_for_quotation/test_request_for_quotation.py | 4 ++--
erpnext/buying/doctype/supplier/test_supplier.py | 5 +++--
.../doctype/supplier_quotation/test_supplier_quotation.py | 4 ++--
.../doctype/supplier_scorecard/test_supplier_scorecard.py | 4 ++--
.../test_supplier_scorecard_criteria.py | 4 ++--
.../test_supplier_scorecard_variable.py | 4 ++--
.../report/procurement_tracker/test_procurement_tracker.py | 4 ++--
.../test_subcontracted_item_to_be_received.py | 4 ++--
.../test_subcontracted_raw_materials_to_be_transferred.py | 4 ++--
10 files changed, 21 insertions(+), 20 deletions(-)
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 645e97ee7c..efa2ab1268 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -3,9 +3,9 @@
import json
-import unittest
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, flt, getdate, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
@@ -27,7 +27,7 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
-class TestPurchaseOrder(unittest.TestCase):
+class TestPurchaseOrder(FrappeTestCase):
def test_make_purchase_receipt(self):
po = create_purchase_order(do_not_submit=True)
self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name)
diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
index 51901991b5..5b2112424c 100644
--- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
@@ -1,9 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-import unittest
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import nowdate
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import (
@@ -16,7 +16,7 @@ from erpnext.stock.doctype.item.test_item import make_item
from erpnext.templates.pages.rfq import check_supplier_has_docname_access
-class TestRequestforQuotation(unittest.TestCase):
+class TestRequestforQuotation(FrappeTestCase):
def test_quote_status(self):
rfq = make_request_for_quotation()
diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py
index 0fb81b2578..7358e2af22 100644
--- a/erpnext/buying/doctype/supplier/test_supplier.py
+++ b/erpnext/buying/doctype/supplier/test_supplier.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-import unittest
import frappe
from frappe.test_runner import make_test_records
@@ -12,8 +11,10 @@ from erpnext.exceptions import PartyDisabled
test_dependencies = ['Payment Term', 'Payment Terms Template']
test_records = frappe.get_test_records('Supplier')
+from frappe.tests.utils import FrappeTestCase
-class TestSupplier(unittest.TestCase):
+
+class TestSupplier(FrappeTestCase):
def test_get_supplier_group_details(self):
doc = frappe.new_doc("Supplier Group")
doc.supplier_group_name = "_Testing Supplier Group"
diff --git a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py
index d48ac7eb3b..a4d45975c3 100644
--- a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py
+++ b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py
@@ -3,12 +3,12 @@
-import unittest
import frappe
+from frappe.tests.utils import FrappeTestCase
-class TestPurchaseOrder(unittest.TestCase):
+class TestPurchaseOrder(FrappeTestCase):
def test_make_purchase_order(self):
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order
diff --git a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py
index 7908c35cbb..8ecc2cd466 100644
--- a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py
+++ b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py
@@ -1,12 +1,12 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-import unittest
import frappe
+from frappe.tests.utils import FrappeTestCase
-class TestSupplierScorecard(unittest.TestCase):
+class TestSupplierScorecard(FrappeTestCase):
def test_create_scorecard(self):
doc = make_supplier_scorecard().insert()
diff --git a/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py b/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py
index dacc982420..7ff84c15e5 100644
--- a/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py
+++ b/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py
@@ -1,12 +1,12 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-import unittest
import frappe
+from frappe.tests.utils import FrappeTestCase
-class TestSupplierScorecardCriteria(unittest.TestCase):
+class TestSupplierScorecardCriteria(FrappeTestCase):
def test_variables_exist(self):
delete_test_scorecards()
for d in test_good_criteria:
diff --git a/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py b/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py
index 4d75981125..32005a37dc 100644
--- a/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py
+++ b/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py
@@ -1,16 +1,16 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-import unittest
import frappe
+from frappe.tests.utils import FrappeTestCase
from erpnext.buying.doctype.supplier_scorecard_variable.supplier_scorecard_variable import (
VariablePathNotFound,
)
-class TestSupplierScorecardVariable(unittest.TestCase):
+class TestSupplierScorecardVariable(FrappeTestCase):
def test_variable_exist(self):
for d in test_existing_variables:
my_doc = frappe.get_doc("Supplier Scorecard Variable", d.get("name"))
diff --git a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py
index 84de8c6743..44524527e3 100644
--- a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py
+++ b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py
@@ -2,10 +2,10 @@
# For license information, please see license.txt
-import unittest
from datetime import datetime
import frappe
+from frappe.tests.utils import FrappeTestCase
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
from erpnext.buying.report.procurement_tracker.procurement_tracker import execute
@@ -14,7 +14,7 @@ from erpnext.stock.doctype.material_request.test_material_request import make_ma
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
-class TestProcurementTracker(unittest.TestCase):
+class TestProcurementTracker(FrappeTestCase):
def test_result_for_procurement_tracker(self):
filters = {
'company': '_Test Procurement Company',
diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py
index 144523ad52..c2b38d38e1 100644
--- a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py
+++ b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py
@@ -3,9 +3,9 @@
# Compiled at: 2019-05-06 09:51:46
# Decompiled by https://python-decompiler.com
-import unittest
import frappe
+from frappe.tests.utils import FrappeTestCase
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -15,7 +15,7 @@ from erpnext.buying.report.subcontracted_item_to_be_received.subcontracted_item_
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
-class TestSubcontractedItemToBeReceived(unittest.TestCase):
+class TestSubcontractedItemToBeReceived(FrappeTestCase):
def test_pending_and_received_qty(self):
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes')
diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py
index 3c203ac23f..fc9acabc81 100644
--- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py
+++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py
@@ -4,9 +4,9 @@
# Decompiled by https://python-decompiler.com
import json
-import unittest
import frappe
+from frappe.tests.utils import FrappeTestCase
from erpnext.buying.doctype.purchase_order.purchase_order import make_rm_stock_entry
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -16,7 +16,7 @@ from erpnext.buying.report.subcontracted_raw_materials_to_be_transferred.subcont
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
-class TestSubcontractedItemToBeTransferred(unittest.TestCase):
+class TestSubcontractedItemToBeTransferred(FrappeTestCase):
def test_pending_and_transferred_qty(self):
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes', supplier_warehouse="_Test Warehouse 1 - _TC")
From 19fb7ead9f249d28403f618297009d498675d224 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Mon, 28 Feb 2022 18:05:58 +0530
Subject: [PATCH 186/447] test: Add test case for payment entry taxes
---
.../doctype/payment_entry/payment_entry.py | 8 ++--
.../payment_entry/test_payment_entry.py | 39 +++++++++++++++++++
2 files changed, 43 insertions(+), 4 deletions(-)
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 6ec15e1e87..eb2019bdf0 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -946,11 +946,11 @@ class PaymentEntry(AccountsController):
tax.base_total = tax.total * self.source_exchange_rate
if self.payment_type == 'Pay':
- self.base_total_taxes_and_charges += current_tax_amount * self.source_exchange_rate
- self.total_taxes_and_charges += current_tax_amount * self.target_exchange_rate
+ self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
+ self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
else:
- self.base_total_taxes_and_charges += current_tax_amount * self.target_exchange_rate
- self.total_taxes_and_charges += current_tax_amount * self.source_exchange_rate
+ self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
+ self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
if self.get('taxes'):
self.paid_amount_after_tax = self.get('taxes')[-1].base_total
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index cc3528e9aa..349b8bb5b1 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -633,6 +633,45 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(flt(expected_party_balance), party_balance)
self.assertEqual(flt(expected_party_account_balance), party_account_balance)
+ def test_multi_currency_payment_entry_with_taxes(self):
+ payment_entry = create_payment_entry(party='_Test Supplier USD', paid_to = '_Test Payable USD - _TC',
+ save=True)
+ payment_entry.append('taxes', {
+ 'account_head': '_Test Account Service Tax - _TC',
+ 'charge_type': 'Actual',
+ 'tax_amount': 10,
+ 'add_deduct_tax': 'Add',
+ 'description': 'Test'
+ })
+
+ payment_entry.save()
+ self.assertEqual(payment_entry.base_total_taxes_and_charges, 10)
+ self.assertEqual(flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2))
+
+def create_payment_entry(**args):
+ payment_entry = frappe.new_doc('Payment Entry')
+ payment_entry.company = args.get('company') or '_Test Company'
+ payment_entry.payment_type = args.get('payment_type') or 'Pay'
+ payment_entry.party_type = args.get('party_type') or 'Supplier'
+ payment_entry.party = args.get('party') or '_Test Supplier'
+ payment_entry.paid_from = args.get('paid_from') or '_Test Bank - _TC'
+ payment_entry.paid_to = args.get('paid_to') or 'Creditors - _TC'
+ payment_entry.paid_amount = args.get('paid_amount') or 1000
+
+ payment_entry.setup_party_account_field()
+ payment_entry.set_missing_values()
+ payment_entry.set_exchange_rate()
+ payment_entry.received_amount = payment_entry.paid_amount / payment_entry.target_exchange_rate
+ payment_entry.reference_no = 'Test001'
+ payment_entry.reference_date = nowdate()
+
+ if args.get('save'):
+ payment_entry.save()
+ if args.get('submit'):
+ payment_entry.submit()
+
+ return payment_entry
+
def create_payment_terms_template():
create_payment_term('Basic Amount Receivable')
From 4e2c284be7904f9d553a6eb8e93e100edcd909e6 Mon Sep 17 00:00:00 2001
From: Rushabh Mehta
Date: Tue, 1 Mar 2022 08:52:42 +0530
Subject: [PATCH 187/447] fix(minor): nix the ugly banner
---
.../includes/footer/footer_powered.html | 28 +------------------
1 file changed, 1 insertion(+), 27 deletions(-)
diff --git a/erpnext/templates/includes/footer/footer_powered.html b/erpnext/templates/includes/footer/footer_powered.html
index 82b2716a92..faf5e9278c 100644
--- a/erpnext/templates/includes/footer/footer_powered.html
+++ b/erpnext/templates/includes/footer/footer_powered.html
@@ -1,27 +1 @@
-{% set domains = frappe.get_doc("Domain Settings").active_domains %}
-{% set links = {
- 'Manufacturing': '/manufacturing',
- 'Services': '/services',
- 'Retail': '/retail',
- 'Distribution': '/distribution',
- 'Non Profit': '/non-profit',
- 'Education': '/education',
- 'Healthcare': '/healthcare',
- 'Agriculture': '/agriculture',
- 'Hospitality': ''
-} %}
-
-{% set link = '' %}
-{% set label = '' %}
-{% if domains %}
- {% set label = domains[0].domain %}
- {% set link = links[label] %}
-{% endif %}
-
-{% if label == "Services" %}
- {% set label = "Service" %}
-{% endif %}
-
-
-
-Powered by ERPNext - {{ '' if domains else 'Open Source' }} ERP Software {{ ('for ' + label + ' Companies') if domains else '' }}
+Powered by ERPNext
From bbc4710fa31357cad038f2b515ae07ed09bd2c5e Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 1 Mar 2022 11:48:28 +0530
Subject: [PATCH 188/447] fix: apply margin on duplicated doc too
---
erpnext/controllers/taxes_and_totals.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 52190765c9..d2a913c74b 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -116,11 +116,11 @@ class calculate_taxes_and_totals(object):
if item.discount_percentage == 100:
item.rate = 0.0
elif item.price_list_rate:
- if item.pricing_rules or item.discount_percentage > 0:
+ if item.pricing_rules or abs(item.discount_percentage) > 0:
item.rate = flt(item.price_list_rate *
(1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
- elif item.discount_amount and item.pricing_rules:
+ elif item.discount_amount or item.pricing_rules:
item.rate = item.price_list_rate - item.discount_amount
if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item', 'POS Invoice Item', 'Purchase Invoice Item', 'Purchase Order Item', 'Purchase Receipt Item']:
From 366120ffeed36c68b9ceb1f677cc4497673b4289 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Tue, 1 Mar 2022 11:56:20 +0530
Subject: [PATCH 189/447] fix: Deferred revenue booking
---
erpnext/accounts/deferred_revenue.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index 9e2cdfffd9..ab1061beeb 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -120,6 +120,7 @@ def get_booking_dates(doc, item, posting_date=None):
prev_gl_entry = frappe.db.sql('''
select name, posting_date from `tabGL Entry` where company=%s and account=%s and
voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
+ and is_cancelled = 0
order by posting_date desc limit 1
''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
@@ -227,6 +228,7 @@ def get_already_booked_amount(doc, item):
gl_entries_details = frappe.db.sql('''
select sum({0}) as total_credit, sum({1}) as total_credit_in_account_currency, voucher_detail_no
from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
+ and is_cancelled = 0
group by voucher_detail_no
'''.format(total_credit_debit, total_credit_debit_currency),
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
@@ -282,7 +284,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
return
# check if books nor frozen till endate:
- if getdate(end_date) >= getdate(accounts_frozen_upto):
+ if accounts_frozen_upto and (end_date) <= getdate(accounts_frozen_upto):
end_date = get_last_day(add_days(accounts_frozen_upto, 1))
if via_journal_entry:
From 25fa6344a6c3dc3af7ecea250d30a421f4016afc Mon Sep 17 00:00:00 2001
From: marination
Date: Tue, 1 Mar 2022 12:24:45 +0530
Subject: [PATCH 190/447] test: Adjust test as per teardown
- make tests use multi level bom in WO, as BOM at setup is used
- earlier, an intermediate BOM made in a test would override the BOM at setup
---
.../doctype/production_plan/test_production_plan.py | 4 +++-
erpnext/manufacturing/doctype/work_order/test_work_order.py | 2 +-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 85ecec2f69..ca8cc7a47e 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -154,7 +154,7 @@ class TestProductionPlan(FrappeTestCase):
use_multi_level_bom=0,
ignore_existing_ordered_qty=0
)
- self.assertTrue(len(pln.mr_items), 0)
+ self.assertTrue(len(pln.mr_items))
sr1.cancel()
sr2.cancel()
@@ -575,6 +575,7 @@ class TestProductionPlan(FrappeTestCase):
wip_warehouse='Work In Progress - _TC',
fg_warehouse='Finished Goods - _TC',
skip_transfer=1,
+ use_multi_level_bom=1,
do_not_submit=True
)
wo.production_plan = pln.name
@@ -619,6 +620,7 @@ class TestProductionPlan(FrappeTestCase):
wip_warehouse='Work In Progress - _TC',
fg_warehouse='Finished Goods - _TC',
skip_transfer=1,
+ use_multi_level_bom=1,
do_not_submit=True
)
wo.production_plan = pln.name
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 549ec7b4a6..bc07d22e83 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -1040,7 +1040,7 @@ def make_wo_order_test_record(**args):
wo_order.scrap_warehouse = args.fg_warehouse or "_Test Scrap Warehouse - _TC"
wo_order.company = args.company or "_Test Company"
wo_order.stock_uom = args.stock_uom or "_Test UOM"
- wo_order.use_multi_level_bom=0
+ wo_order.use_multi_level_bom= args.use_multi_level_bom or 0
wo_order.skip_transfer=args.skip_transfer or 0
wo_order.get_items_and_operations_from_bom()
wo_order.sales_order = args.sales_order or None
From ffbb2c831152686b9ec0b230b3c72b94edce0c6a Mon Sep 17 00:00:00 2001
From: marination
Date: Tue, 1 Mar 2022 12:57:09 +0530
Subject: [PATCH 191/447] fix: Assert False for test that is anticipating an
empty table and remove second arg to assertTrue
---
.../doctype/production_plan/test_production_plan.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index ca8cc7a47e..59cc17a5ee 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -113,7 +113,7 @@ class TestProductionPlan(FrappeTestCase):
item_code='Test Production Item 1',
ignore_existing_ordered_qty=1
)
- self.assertTrue(len(pln.mr_items), 1)
+ self.assertTrue(len(pln.mr_items))
self.assertTrue(flt(pln.mr_items[0].quantity), 1.0)
sr1.cancel()
@@ -154,7 +154,7 @@ class TestProductionPlan(FrappeTestCase):
use_multi_level_bom=0,
ignore_existing_ordered_qty=0
)
- self.assertTrue(len(pln.mr_items))
+ self.assertFalse(len(pln.mr_items))
sr1.cancel()
sr2.cancel()
From d22a1d440c96d5a476ce466074a264362a4c814a Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 1 Mar 2022 13:06:40 +0530
Subject: [PATCH 192/447] fix: track changes on warehouse
---
erpnext/stock/doctype/warehouse/warehouse.json | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json
index 05076b51a3..c695d541bf 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.json
+++ b/erpnext/stock/doctype/warehouse/warehouse.json
@@ -244,7 +244,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2021-12-03 04:40:06.414630",
+ "modified": "2022-03-01 02:37:48.034944",
"modified_by": "Administrator",
"module": "Stock",
"name": "Warehouse",
@@ -301,5 +301,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
- "title_field": "warehouse_name"
+ "states": [],
+ "title_field": "warehouse_name",
+ "track_changes": 1
}
\ No newline at end of file
From ad2c64f3ff3cafa0abcee35639c9e5d4952bccdf Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Tue, 1 Mar 2022 13:32:34 +0530
Subject: [PATCH 193/447] fix: debit credit difference case with rounding
adjustment
---
.../sales_invoice/test_sales_invoice.py | 50 +++++++++++++++++++
erpnext/accounts/general_ledger.py | 2 +-
2 files changed, 51 insertions(+), 1 deletion(-)
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 941061f2a2..83caab43b4 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1595,6 +1595,56 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
+ def test_rounding_adjustment_3(self):
+ si = create_sales_invoice(do_not_save=True)
+ si.items = []
+ for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]:
+ si.append("items", {
+ "item_code": "_Test Item",
+ "gst_hsn_code": "999800",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": d[1],
+ "rate": d[0],
+ "income_account": "Sales - _TC",
+ "cost_center": "_Test Cost Center - _TC"
+ })
+ for tax_account in ["_Test Account VAT - _TC", "_Test Account Service Tax - _TC"]:
+ si.append("taxes", {
+ "charge_type": "On Net Total",
+ "account_head": tax_account,
+ "description": tax_account,
+ "rate": 6,
+ "cost_center": "_Test Cost Center - _TC",
+ "included_in_print_rate": 1
+ })
+ si.save()
+ si.submit()
+ self.assertEqual(si.net_total, 4007.16)
+ self.assertEqual(si.grand_total, 4488.02)
+ self.assertEqual(si.total_taxes_and_charges, 480.86)
+ self.assertEqual(si.rounding_adjustment, -0.02)
+
+ expected_values = dict((d[0], d) for d in [
+ [si.debit_to, 4488.0, 0.0],
+ ["_Test Account Service Tax - _TC", 0.0, 240.43],
+ ["_Test Account VAT - _TC", 0.0, 240.43],
+ ["Sales - _TC", 0.0, 4007.15],
+ ["Round Off - _TC", 0.01, 0]
+ ])
+
+ gl_entries = frappe.db.sql("""select account, debit, credit
+ from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
+ order by account asc""", si.name, as_dict=1)
+
+ debit_credit_diff = 0
+ for gle in gl_entries:
+ self.assertEqual(expected_values[gle.account][0], gle.account)
+ self.assertEqual(expected_values[gle.account][1], gle.debit)
+ self.assertEqual(expected_values[gle.account][2], gle.credit)
+ debit_credit_diff += (gle.debit - gle.credit)
+
+ self.assertEqual(debit_credit_diff, 0)
+
def test_sales_invoice_with_shipping_rule(self):
from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index d24d56b4bb..0cd5e86a8c 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -274,7 +274,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
debit_credit_diff += flt(d.credit)
round_off_account_exists = True
- if round_off_account_exists and abs(debit_credit_diff) <= (1.0 / (10**precision)):
+ if round_off_account_exists and abs(debit_credit_diff) < (1.0 / (10**precision)):
gl_map.remove(round_off_gle)
return
From 47fe87a72f40d1651951cecc97e9ae2ae0115fa4 Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Tue, 1 Mar 2022 14:56:24 +0530
Subject: [PATCH 194/447] fix(test): flaky test_point_of_sale
---
erpnext/tests/test_point_of_sale.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/tests/test_point_of_sale.py b/erpnext/tests/test_point_of_sale.py
index 3299c8885f..38f2c16d93 100644
--- a/erpnext/tests/test_point_of_sale.py
+++ b/erpnext/tests/test_point_of_sale.py
@@ -25,7 +25,7 @@ class TestPointOfSale(unittest.TestCase):
Test Stock and Service Item Search.
"""
- pos_profile = make_pos_profile()
+ pos_profile = make_pos_profile(name="Test POS Profile for Search")
item1 = make_item("Test Search Stock Item", {"is_stock_item": 1})
make_stock_entry(
item_code="Test Search Stock Item",
From 1d1d586d4dd356cf84edaed8a51f934194219d11 Mon Sep 17 00:00:00 2001
From: marination
Date: Tue, 1 Mar 2022 15:42:34 +0530
Subject: [PATCH 195/447] test: Included sub assembly non-merging in test case
---
.../doctype/production_plan/test_production_plan.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 59cc17a5ee..eeab788d5c 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -301,6 +301,11 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(plan.sub_assembly_items[0].qty, 2.0)
self.assertEqual(plan.sub_assembly_items[0].stock_qty, 2.0)
+ # change warehouse in one row, sub-assemblies should not merge
+ plan.po_items[0].warehouse = "Finished Goods - _TC"
+ plan.get_sub_assembly_items()
+ self.assertTrue(len(plan.sub_assembly_items), 2)
+
def test_pp_to_mr_customer_provided(self):
" Test Material Request from Production Plan for Customer Provided Item."
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
From e6952cb7f993c37d4f71be4ba6779c94257656f6 Mon Sep 17 00:00:00 2001
From: marination
Date: Tue, 1 Mar 2022 17:15:04 +0530
Subject: [PATCH 196/447] refactor: Convert to QB, added test file, removed
white space
- Converted mysql raw query to qb
- Test file for Report Requested Items to Order and Receive
- Removed white space and edited copyright year
---
.../requested_items_to_order_and_receive.py | 100 ++++++++++--------
...st_requested_items_to_order_and_receive.py | 68 ++++++++++++
.../material_request/test_material_request.py | 14 +--
.../report/stock_ageing/test_stock_ageing.py | 2 +-
4 files changed, 134 insertions(+), 50 deletions(-)
create mode 100644 erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py
diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
index 2c597f29bf..2923e5bb5f 100644
--- a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
+++ b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
@@ -6,6 +6,7 @@ import copy
import frappe
from frappe import _
+from frappe.query_builder.functions import Sum, Coalesce
from frappe.utils import date_diff, flt, getdate
@@ -16,10 +17,7 @@ def execute(filters=None):
validate_filters(filters)
columns = get_columns(filters)
- conditions = get_conditions(filters)
-
- # get queried data
- data = get_data(filters, conditions)
+ data = get_data(filters)
# prepare data for report and chart views
data, chart_data = prepare_data(data, filters)
@@ -34,52 +32,70 @@ def validate_filters(filters):
elif date_diff(to_date, from_date) < 0:
frappe.throw(_("To Date cannot be before From Date."))
-def get_conditions(filters):
- conditions = ''
+def get_data(filters):
+ mr = frappe.qb.DocType("Material Request")
+ mr_item = frappe.qb.DocType("Material Request Item")
+ query = (
+ frappe.qb.from_(mr)
+ .join(mr_item).on(mr_item.parent == mr.name)
+ .select(
+ mr.name.as_("material_request"),
+ mr.transaction_date.as_("date"),
+ mr_item.schedule_date.as_("required_date"),
+ mr_item.item_code.as_("item_code"),
+ Sum(Coalesce(mr_item.stock_qty, 0)).as_("qty"),
+ Coalesce(mr_item.stock_uom, '').as_("uom"),
+ Sum(Coalesce(mr_item.ordered_qty, 0)).as_("ordered_qty"),
+ Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
+ (
+ Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.received_qty, 0))
+ ).as_("qty_to_receive"),
+ Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
+ (
+ Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.ordered_qty, 0))
+ ).as_("qty_to_order"),
+ mr_item.item_name,
+ mr_item.description,
+ mr.company
+ ).where(
+ (mr.material_request_type == "Purchase")
+ & (mr.docstatus == 1)
+ & (mr.status != "Stopped")
+ & (mr.per_received < 100)
+ )
+ )
+
+ query = get_conditions(filters, query, mr, mr_item) # add conditional conditions
+
+ query = (
+ query.groupby(
+ mr.name, mr_item.item_code
+ ).orderby(
+ mr.transaction_date, mr.schedule_date
+ )
+ )
+ data = query.run(as_dict=True)
+ return data
+
+def get_conditions(filters, query, mr, mr_item):
if filters.get("from_date") and filters.get("to_date"):
- conditions += " and mr.transaction_date between '{0}' and '{1}'".format(filters.get("from_date"),filters.get("to_date"))
-
+ query = (
+ query.where(
+ ( mr.transaction_date >= filters.get("from_date"))
+ & (mr.transaction_date <= filters.get("to_date"))
+ )
+ )
if filters.get("company"):
- conditions += " and mr.company = '{0}'".format(filters.get("company"))
+ query = query.where(mr.company == filters.get("company"))
if filters.get("material_request"):
- conditions += " and mr.name = '{0}'".format(filters.get("material_request"))
+ query = query.where(mr.name == filters.get("material_request"))
if filters.get("item_code"):
- conditions += " and mr_item.item_code = '{0}'".format(filters.get("item_code"))
+ query = query.where(mr_item.item_code == filters.get("item_code"))
- return conditions
-
-def get_data(filters, conditions):
- data = frappe.db.sql("""
- select
- mr.name as material_request,
- mr.transaction_date as date,
- mr_item.schedule_date as required_date,
- mr_item.item_code as item_code,
- sum(ifnull(mr_item.stock_qty, 0)) as qty,
- ifnull(mr_item.stock_uom, '') as uom,
- sum(ifnull(mr_item.ordered_qty, 0)) as ordered_qty,
- sum(ifnull(mr_item.received_qty, 0)) as received_qty,
- (sum(ifnull(mr_item.stock_qty, 0)) - sum(ifnull(mr_item.received_qty, 0))) as qty_to_receive,
- (sum(ifnull(mr_item.stock_qty, 0)) - sum(ifnull(mr_item.ordered_qty, 0))) as qty_to_order,
- mr_item.item_name as item_name,
- mr_item.description as "description",
- mr.company as company
- from
- `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
- where
- mr_item.parent = mr.name
- and mr.material_request_type = "Purchase"
- and mr.docstatus = 1
- and mr.status != "Stopped"
- and mr.per_received < 100
- {conditions}
- group by mr.name, mr_item.item_code
- order by mr.transaction_date, mr.schedule_date""".format(conditions=conditions), as_dict=1)
-
- return data
+ return query
def update_qty_columns(row_to_update, data_row):
fields = ["qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"]
diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py
new file mode 100644
index 0000000000..be2419a7b3
--- /dev/null
+++ b/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py
@@ -0,0 +1,68 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import today, add_days
+
+from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
+from erpnext.buying.report.requested_items_to_order_and_receive.requested_items_to_order_and_receive import (
+ get_data
+)
+from erpnext.stock.doctype.item.test_item import create_item
+from erpnext.stock.doctype.material_request.material_request import make_purchase_order
+
+class TestRequestedItemsToOrderAndReceive(FrappeTestCase):
+ def setUp(self) -> None:
+ create_item("Test MR Report Item")
+ self.setup_material_request() # to order and receive
+ self.setup_material_request(order=True) # to receive (ordered)
+ self.setup_material_request(order=True, receive=True) # complete (ordered & received)
+
+ self.filters = frappe._dict(
+ company="_Test Company", from_date=today(), to_date=add_days(today(), 30),
+ item_code="Test MR Report Item"
+ )
+
+ def tearDown(self) -> None:
+ frappe.db.rollback()
+
+ def test_date_range(self):
+ data = get_data(self.filters)
+ self.assertEqual(len(data), 2) # MRs today should be fetched
+
+ self.filters.from_date = add_days(today(), 1)
+ data = get_data(self.filters)
+ self.assertEqual(len(data), 0) # MRs today should not be fetched as from date is tomorrow
+
+ def test_ordered_received_material_requests(self):
+ data = get_data(self.filters)
+
+ # from the 3 MRs made, only 2 (to receive) should be fetched
+ self.assertEqual(len(data), 2)
+ self.assertEqual(data[0].ordered_qty, 0.0)
+ self.assertEqual(data[1].ordered_qty, 57.0)
+
+ def setup_material_request(self, order=False, receive=False):
+ po = None
+ test_records = frappe.get_test_records('Material Request')
+
+ mr = frappe.copy_doc(test_records[0])
+ mr.transaction_date = today()
+ mr.schedule_date = add_days(today(), 1)
+ for row in mr.items:
+ row.item_code = "Test MR Report Item"
+ row.item_name = "Test MR Report Item"
+ row.description = "Test MR Report Item"
+ row.uom = "Nos"
+ row.schedule_date = add_days(today(), 1)
+ mr.submit()
+
+ if order or receive:
+ po = make_purchase_order(mr.name)
+ po.supplier = "_Test Supplier"
+ po.submit()
+ if receive:
+ pr = make_purchase_receipt(po.name)
+ pr.submit()
+
diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py
index 1cda781617..866f3ab2d5 100644
--- a/erpnext/stock/doctype/material_request/test_material_request.py
+++ b/erpnext/stock/doctype/material_request/test_material_request.py
@@ -626,13 +626,13 @@ class TestMaterialRequest(FrappeTestCase):
mr.schedule_date = today()
if not frappe.db.get_value('UOM Conversion Detail',
- {'parent': item.item_code, 'uom': 'Kg'}):
- item_doc = frappe.get_doc('Item', item.item_code)
- item_doc.append('uoms', {
- 'uom': 'Kg',
- 'conversion_factor': 5
- })
- item_doc.save(ignore_permissions=True)
+ {'parent': item.item_code, 'uom': 'Kg'}):
+ item_doc = frappe.get_doc('Item', item.item_code)
+ item_doc.append('uoms', {
+ 'uom': 'Kg',
+ 'conversion_factor': 5
+ })
+ item_doc.save(ignore_permissions=True)
item.uom = 'Kg'
for item in mr.items:
diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
index 2630805c62..ca963b7486 100644
--- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
From ac425722e206465c34d4029b3e959ac726ebd0ef Mon Sep 17 00:00:00 2001
From: marination
Date: Tue, 1 Mar 2022 17:30:37 +0530
Subject: [PATCH 197/447] fix: Sider and Linter
---
.../requested_items_to_order_and_receive.py | 2 +-
.../test_requested_items_to_order_and_receive.py | 5 +++--
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
index 2923e5bb5f..50fe78ba0f 100644
--- a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
+++ b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
@@ -82,7 +82,7 @@ def get_conditions(filters, query, mr, mr_item):
if filters.get("from_date") and filters.get("to_date"):
query = (
query.where(
- ( mr.transaction_date >= filters.get("from_date"))
+ (mr.transaction_date >= filters.get("from_date"))
& (mr.transaction_date <= filters.get("to_date"))
)
)
diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py
index be2419a7b3..f3c751c5c3 100644
--- a/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py
+++ b/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py
@@ -3,15 +3,16 @@
import frappe
from frappe.tests.utils import FrappeTestCase
-from frappe.utils import today, add_days
+from frappe.utils import add_days, today
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
from erpnext.buying.report.requested_items_to_order_and_receive.requested_items_to_order_and_receive import (
- get_data
+ get_data,
)
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
+
class TestRequestedItemsToOrderAndReceive(FrappeTestCase):
def setUp(self) -> None:
create_item("Test MR Report Item")
From abe580e8b0fc6f5fae720c4ab4713eb2e239abf6 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Tue, 1 Mar 2022 17:37:38 +0530
Subject: [PATCH 198/447] fix: Nil and Exempted values in GSTR-3B Report
---
.../doctype/gstr_3b_report/gstr_3b_report.py | 38 ++++++++++++-------
1 file changed, 25 insertions(+), 13 deletions(-)
diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
index cb79cf8286..d5ff88c7c9 100644
--- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
@@ -127,7 +127,8 @@ class GSTR3BReport(Document):
def get_inward_nil_exempt(self, state):
inward_nil_exempt = frappe.db.sql("""
- SELECT p.place_of_supply, sum(i.base_amount) as base_amount, i.is_nil_exempt, i.is_non_gst
+ SELECT p.place_of_supply, p.supplier_address,
+ i.base_amount, i.is_nil_exempt, i.is_non_gst
FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
WHERE p.docstatus = 1 and p.name = i.parent
and p.is_opening = 'No'
@@ -135,7 +136,7 @@ class GSTR3BReport(Document):
and (i.is_nil_exempt = 1 or i.is_non_gst = 1 or p.gst_category = 'Registered Composition') and
month(p.posting_date) = %s and year(p.posting_date) = %s
and p.company = %s and p.company_gstin = %s
- GROUP BY p.place_of_supply, i.is_nil_exempt, i.is_non_gst""",
+ """,
(self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
inward_nil_exempt_details = {
@@ -149,18 +150,24 @@ class GSTR3BReport(Document):
}
}
+ address_state_map = get_address_state_map()
+
for d in inward_nil_exempt:
- if d.place_of_supply:
- if (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
- and state == d.place_of_supply.split("-")[1]:
- inward_nil_exempt_details["gst"]["intra"] += d.base_amount
- elif (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
- and state != d.place_of_supply.split("-")[1]:
- inward_nil_exempt_details["gst"]["inter"] += d.base_amount
- elif d.is_non_gst == 1 and state == d.place_of_supply.split("-")[1]:
- inward_nil_exempt_details["non_gst"]["intra"] += d.base_amount
- elif d.is_non_gst == 1 and state != d.place_of_supply.split("-")[1]:
- inward_nil_exempt_details["non_gst"]["inter"] += d.base_amount
+ if not d.place_of_supply:
+ d.place_of_supply = "00-" + cstr(state)
+
+ supplier_state = address_state_map.get(d.supplier_address) or state
+
+ if (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
+ and cstr(supplier_state) == cstr(d.place_of_supply.split("-")[1]):
+ inward_nil_exempt_details["gst"]["intra"] += d.base_amount
+ elif (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
+ and cstr(supplier_state) != cstr(d.place_of_supply.split("-")[1]):
+ inward_nil_exempt_details["gst"]["inter"] += d.base_amount
+ elif d.is_non_gst == 1 and cstr(supplier_state) == cstr(d.place_of_supply.split("-")[1]):
+ inward_nil_exempt_details["non_gst"]["intra"] += d.base_amount
+ elif d.is_non_gst == 1 and cstr(supplier_state) != cstr(d.place_of_supply.split("-")[1]):
+ inward_nil_exempt_details["non_gst"]["inter"] += d.base_amount
return inward_nil_exempt_details
@@ -419,6 +426,11 @@ class GSTR3BReport(Document):
return ",".join(missing_field_invoices)
+def get_address_state_map():
+ return frappe._dict(
+ frappe.get_all('Address', fields=['name', 'gst_state'], as_list=1)
+ )
+
def get_json(template):
file_path = os.path.join(os.path.dirname(__file__), '{template}.json'.format(template=template))
with open(file_path, 'r') as f:
From 65bb72703029a38fedc02f2ad8f03ebfadf0b223 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 1 Mar 2022 17:25:42 +0530
Subject: [PATCH 199/447] fix: dont validate empty category
---
.../sales_taxes_and_charges_template.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
index 1d30934df9..8043a1b66f 100644
--- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
+++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
@@ -55,5 +55,8 @@ def validate_disabled(doc):
frappe.throw(_("Disabled template must not be default template"))
def validate_for_tax_category(doc):
+ if not doc.tax_category:
+ return
+
if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0, "name": ["!=", doc.name]}):
frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category)))
From 2eb7921bfff61052a56f09b625d0115875a1a1ee Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Mon, 28 Feb 2022 21:07:08 +0530
Subject: [PATCH 200/447] fix: Exchange rate not getting set in payment entry
(cherry picked from commit 86e6bdf2c9ffc0a656c14663b6f790071a7f3afd)
---
.../doctype/payment_entry/payment_entry.js | 18 ++++++++++++------
1 file changed, 12 insertions(+), 6 deletions(-)
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index cc32a6ccd9..b2b818a214 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -346,6 +346,8 @@ frappe.ui.form.on('Payment Entry', {
}
frm.set_party_account_based_on_party = true;
+ let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
+
return frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details",
args: {
@@ -379,7 +381,11 @@ frappe.ui.form.on('Payment Entry', {
if (r.message.bank_account) {
frm.set_value("bank_account", r.message.bank_account);
}
- }
+ },
+ () => frm.events.set_current_exchange_rate(frm, "source_exchange_rate",
+ frm.doc.paid_from_account_currency, company_currency),
+ () => frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
+ frm.doc.paid_to_account_currency, company_currency)
]);
}
}
@@ -483,14 +489,14 @@ frappe.ui.form.on('Payment Entry', {
},
paid_from_account_currency: function(frm) {
- if(!frm.doc.paid_from_account_currency) return;
- var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
+ if(!frm.doc.paid_from_account_currency || !frm.doc.company) return;
+ let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.paid_from_account_currency == company_currency) {
frm.set_value("source_exchange_rate", 1);
} else if (frm.doc.paid_from){
if (in_list(["Internal Transfer", "Pay"], frm.doc.payment_type)) {
- var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
+ let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
@@ -510,8 +516,8 @@ frappe.ui.form.on('Payment Entry', {
},
paid_to_account_currency: function(frm) {
- if(!frm.doc.paid_to_account_currency) return;
- var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
+ if(!frm.doc.paid_to_account_currency || !frm.doc.company) return;
+ let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
frm.doc.paid_to_account_currency, company_currency);
From 57f92df108ae6c33567a95be22a43015fe3178e5 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Tue, 1 Mar 2022 20:50:36 +0530
Subject: [PATCH 201/447] fix: flake8 issues
---
erpnext/hr/doctype/expense_claim/expense_claim.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py
index 5146a5be90..fe04efbbab 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.py
@@ -345,10 +345,10 @@ def get_advances(employee, advance_id=None):
query = (
frappe.qb.from_(advance)
- .select(
- advance.name, advance.posting_date, advance.paid_amount,
- advance.claimed_amount, advance.advance_account
- )
+ .select(
+ advance.name, advance.posting_date, advance.paid_amount,
+ advance.claimed_amount, advance.advance_account
+ )
)
if not advance_id:
From 9988ec697f056b476ead3be1a371a82c6337be8e Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Tue, 1 Mar 2022 21:40:39 +0530
Subject: [PATCH 202/447] test: test advance filters in expense claim and
cancelled status
---
.../employee_advance/employee_advance.py | 1 +
.../employee_advance/test_employee_advance.py | 38 +++++++++++++++++--
2 files changed, 35 insertions(+), 4 deletions(-)
diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py
index f63bb86129..79d389d440 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance.py
+++ b/erpnext/hr/doctype/employee_advance/employee_advance.py
@@ -27,6 +27,7 @@ class EmployeeAdvance(Document):
def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry')
+ self.set_status(update=True)
def set_status(self, update=False):
precision = self.precision("paid_amount")
diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py
index 5f3a66a04f..e3c1487ca2 100644
--- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py
+++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py
@@ -24,6 +24,9 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_
class TestEmployeeAdvance(unittest.TestCase):
+ def setUp(self):
+ frappe.db.delete("Employee Advance")
+
def test_paid_amount_and_status(self):
employee_name = make_employee("_T@employe.advance")
advance = make_employee_advance(employee_name)
@@ -58,8 +61,12 @@ class TestEmployeeAdvance(unittest.TestCase):
self.assertEqual(advance.paid_amount, 0)
self.assertEqual(advance.status, "Unpaid")
- def test_claimed_and_returned_status(self):
- # Claimed Status check, full amount claimed
+ advance.cancel()
+ advance.reload()
+ self.assertEqual(advance.status, "Cancelled")
+
+ def test_claimed_status(self):
+ # CLAIMED Status check, full amount claimed
payable_account = get_payable_account("_Test Company")
claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
@@ -75,13 +82,26 @@ class TestEmployeeAdvance(unittest.TestCase):
self.assertEqual(advance.claimed_amount, 1000)
self.assertEqual(advance.status, "Claimed")
+ # advance should not be shown in claims
+ advances = get_advances(claim.employee)
+ advances = [entry.name for entry in advances]
+ self.assertTrue(advance.name not in advances)
+
# cancel claim; status should be Paid
claim.cancel()
advance.reload()
self.assertEqual(advance.claimed_amount, 0)
self.assertEqual(advance.status, "Paid")
- # Partly Claimed and Returned status check
+ def test_partly_claimed_and_returned_status(self):
+ payable_account = get_payable_account("_Test Company")
+ claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
+
+ advance = make_employee_advance(claim.employee)
+ pe = make_payment_entry(advance)
+ pe.submit()
+
+ # PARTLY CLAIMED AND RETURNED status check
# 500 Claimed, 500 Returned
claim = make_expense_claim(payable_account, 500, 500, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
@@ -116,12 +136,22 @@ class TestEmployeeAdvance(unittest.TestCase):
self.assertEqual(advance.return_amount, 500)
self.assertEqual(advance.status, "Partly Claimed and Returned")
- # Cancel return entry; status should change to Paid
+ # advance should not be shown in claims
+ advances = get_advances(claim.employee)
+ advances = [entry.name for entry in advances]
+ self.assertTrue(advance.name not in advances)
+
+ # Cancel return entry; status should change to PAID
entry.cancel()
advance.reload()
self.assertEqual(advance.return_amount, 0)
self.assertEqual(advance.status, "Paid")
+ # advance should be shown in claims
+ advances = get_advances(claim.employee)
+ advances = [entry.name for entry in advances]
+ self.assertTrue(advance.name in advances)
+
def test_repay_unclaimed_amount_from_salary(self):
employee_name = make_employee("_T@employe.advance")
advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1})
From d95f8934aa5cafdddd02568841786081f90c214a Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Tue, 1 Mar 2022 23:09:59 +0530
Subject: [PATCH 203/447] fix: Test cases with discount
---
erpnext/controllers/taxes_and_totals.py | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index d2a913c74b..dc58e1181c 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -113,17 +113,24 @@ class calculate_taxes_and_totals(object):
for item in self.doc.get("items"):
self.doc.round_floats_in(item)
+ if not item.rate:
+ item.rate = item.price_list_rate
+
if item.discount_percentage == 100:
item.rate = 0.0
elif item.price_list_rate:
if item.pricing_rules or abs(item.discount_percentage) > 0:
item.rate = flt(item.price_list_rate *
(1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
- item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
+
+ if abs(item.discount_percentage) > 0:
+ item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
+
elif item.discount_amount or item.pricing_rules:
item.rate = item.price_list_rate - item.discount_amount
- if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item', 'POS Invoice Item', 'Purchase Invoice Item', 'Purchase Order Item', 'Purchase Receipt Item']:
+ if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item',
+ 'POS Invoice Item', 'Purchase Invoice Item', 'Purchase Order Item', 'Purchase Receipt Item']:
item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item)
if flt(item.rate_with_margin) > 0:
item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
From efc4b943f8f250de20c806a6dc923710b0f21885 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 2 Mar 2022 11:19:12 +0530
Subject: [PATCH 204/447] fix: ignore serial no during landed cost voucher
---
.../test_landed_cost_voucher.py | 48 +++++++++++++++++++
erpnext/stock/stock_ledger.py | 2 +-
2 files changed, 49 insertions(+), 1 deletion(-)
diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
index dbaefc1e11..6dc4fee569 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
@@ -11,6 +11,7 @@ from erpnext.accounts.doctype.account.test_account import create_account, get_in
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.utils import update_gl_entries_after
from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item
+from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
get_gl_entries,
make_purchase_receipt,
@@ -177,6 +178,53 @@ class TestLandedCostVoucher(FrappeTestCase):
self.assertEqual(serial_no.purchase_rate - serial_no_rate, 5.0)
self.assertEqual(serial_no.warehouse, "Stores - TCP1")
+ def test_serialized_lcv_delivered(self):
+ """In some cases you'd want to deliver before you can know all the
+ landed costs, this should be allowed for serial nos too.
+
+ Case:
+ - receipt a serial no @ X rate
+ - delivery the serial no @ X rate
+ - add LCV to receipt X + Y
+ - LCV should be successful
+ - delivery should reflect X+Y valuation.
+ """
+ serial_no = "LCV_TEST_SR_NO"
+ item_code = "_Test Serialized Item"
+ warehouse = "Stores - TCP1"
+
+ pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
+ warehouse=warehouse, qty=1, rate=200,
+ item_code=item_code, serial_no=serial_no)
+
+ serial_no_rate = frappe.db.get_value("Serial No", serial_no, "purchase_rate")
+
+ # deliver it before creating LCV
+ dn = create_delivery_note(item_code=item_code,
+ company='_Test Company with perpetual inventory', warehouse='Stores - TCP1',
+ serial_no=serial_no, qty=1, rate=500,
+ cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1")
+
+ charges = 10
+ create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges)
+
+ new_purchase_rate = serial_no_rate + charges
+
+ serial_no = frappe.db.get_value("Serial No", serial_no,
+ ["warehouse", "purchase_rate"], as_dict=1)
+
+ self.assertEqual(serial_no.purchase_rate, new_purchase_rate)
+
+ stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
+ filters={
+ "voucher_no": dn.name,
+ "voucher_type": dn.doctype,
+ "is_cancelled": 0 # LCV cancels with same name.
+ },
+ fieldname="stock_value_difference")
+
+ # reposting should update the purchase rate in future delivery
+ self.assertEqual(stock_value_difference, -new_purchase_rate)
def test_landed_cost_voucher_for_odd_numbers (self):
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True)
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 1b90086440..1a80d93c67 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -39,7 +39,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
future_sle_exists(args, sl_entries)
for sle in sl_entries:
- if sle.serial_no:
+ if sle.serial_no and not via_landed_cost_voucher:
validate_serial_no(sle)
if cancel:
From eb8495a401ceb9f1118bdb50f80a836a07994b9b Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 2 Mar 2022 12:01:51 +0530
Subject: [PATCH 205/447] docs: explain make_sl_entries arguments
---
erpnext/stock/stock_ledger.py | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 1a80d93c67..69755529e5 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -28,6 +28,16 @@ class SerialNoExistsInFutureTransaction(frappe.ValidationError):
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
+ """ Create SL entries from SL entry dicts
+
+ args:
+ - allow_negative_stock: disable negative stock valiations if true
+ - via_landed_cost_voucher: landed cost voucher cancels and reposts
+ entries of purchase document. This flag is used to identify if
+ cancellation and repost is happening via landed cost voucher, in
+ such cases certain validations need to be ignored (like negative
+ stock)
+ """
from erpnext.controllers.stock_controller import future_sle_exists
if sl_entries:
cancel = sl_entries[0].get("is_cancelled")
From 5d85b35f41d792879e47d63ec2bf45696ee6553c Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 2 Mar 2022 12:29:03 +0530
Subject: [PATCH 206/447] test: fix flaky bin value test
---
.../doctype/purchase_receipt/test_purchase_receipt.py | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index a24acb1bd8..fa28f2252d 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -161,6 +161,15 @@ class TestPurchaseReceipt(FrappeTestCase):
qty=abs(existing_bin_qty)
)
+ existing_bin_qty, existing_bin_stock_value = frappe.db.get_value(
+ "Bin",
+ {
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC"
+ },
+ ["actual_qty", "stock_value"]
+ )
+
pr = make_purchase_receipt()
stock_value_difference = frappe.db.get_value(
From 98f26e9cbb24c13657e357060f817f6ab3b58780 Mon Sep 17 00:00:00 2001
From: Sagar Vora
Date: Wed, 2 Mar 2022 12:28:22 +0530
Subject: [PATCH 207/447] fix: handle `ImportError` when installing country
fixtures
---
erpnext/setup/doctype/company/company.py | 17 ++++++++---------
1 file changed, 8 insertions(+), 9 deletions(-)
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 95b1e8b9c6..158952484c 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -3,7 +3,6 @@
import json
-import os
import frappe
import frappe.defaults
@@ -422,14 +421,14 @@ def get_name_with_abbr(name, company):
return " - ".join(parts)
def install_country_fixtures(company, country):
- path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country))
- if os.path.exists(path.encode("utf-8")):
- try:
- module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(country))
- frappe.get_attr(module_name)(company, False)
- except Exception as e:
- frappe.log_error()
- frappe.throw(_("Failed to setup defaults for country {0}. Please contact support.").format(frappe.bold(country)))
+ try:
+ module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(country))
+ frappe.get_attr(module_name)(company, False)
+ except ImportError:
+ pass
+ except Exception:
+ frappe.log_error()
+ frappe.throw(_("Failed to setup defaults for country {0}. Please contact support.").format(frappe.bold(country)))
def update_company_current_month_sales(company):
From 89368ece4120411af5dd6cace9128e42cfd51f13 Mon Sep 17 00:00:00 2001
From: Sagar Vora
Date: Wed, 2 Mar 2022 12:40:25 +0530
Subject: [PATCH 208/447] style: use f-string
---
erpnext/setup/doctype/company/company.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 158952484c..36ad8fec9f 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -422,7 +422,7 @@ def get_name_with_abbr(name, company):
def install_country_fixtures(company, country):
try:
- module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(country))
+ module_name = f"erpnext.regional.{frappe.scrub(country)}.setup.setup"
frappe.get_attr(module_name)(company, False)
except ImportError:
pass
From 54b3676f35579840c35fce5690f0202e590dd424 Mon Sep 17 00:00:00 2001
From: marination
Date: Wed, 2 Mar 2022 13:09:34 +0530
Subject: [PATCH 209/447] fix: linter (imports alphabetical)
---
.../requested_items_to_order_and_receive.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
index 50fe78ba0f..60a8f92cc3 100644
--- a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
+++ b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
@@ -6,7 +6,7 @@ import copy
import frappe
from frappe import _
-from frappe.query_builder.functions import Sum, Coalesce
+from frappe.query_builder.functions import Coalesce, Sum
from frappe.utils import date_diff, flt, getdate
From b3b7cdfb49cdc67e2dc3688b472deb5c2294addc Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 1 Mar 2022 14:33:46 +0530
Subject: [PATCH 210/447] test: FIFO transfer for multi-batch transaction
---
.../test_stock_ledger_entry.py | 45 +++++++++++++++++--
1 file changed, 42 insertions(+), 3 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 01d25b2e86..a182a6d7ef 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
@@ -389,10 +389,13 @@ class TestStockLedgerEntry(FrappeTestCase):
)
- def assertSLEs(self, doc, expected_sles):
+ def assertSLEs(self, doc, expected_sles, sle_filters=None):
""" Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line"""
- sles = frappe.get_all("Stock Ledger Entry", fields=["*"],
- filters={"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled":0},
+
+ filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled":0}
+ if sle_filters:
+ filters.update(sle_filters)
+ sles = frappe.get_all("Stock Ledger Entry", fields=["*"], filters=filters,
order_by="timestamp(posting_date, posting_time), creation")
for exp_sle, act_sle in zip(expected_sles, sles):
@@ -665,6 +668,42 @@ class TestStockLedgerEntry(FrappeTestCase):
{"actual_qty": -10, "stock_value_difference": -10*40, "stock_queue": []},
]))
+ def test_fifo_dependent_consumption(self):
+ item = make_item("_TestFifoTransferRates")
+ source = "_Test Warehouse - _TC"
+ target = "Stores - _TC"
+
+ rates = [10 * i for i in range(1, 20)]
+
+ receipt = make_stock_entry(item_code=item.name, target=source, qty=10, do_not_save=True, rate=10)
+ for rate in rates[1:]:
+ row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False)
+ row.basic_rate = rate
+ receipt.append("items", row)
+
+ receipt.save()
+ receipt.submit()
+
+ expected_queues = []
+ for idx, rate in enumerate(rates, start=1):
+ expected_queues.append(
+ {"stock_queue": [[10, 10 * i] for i in range(1, idx + 1)]}
+ )
+ self.assertSLEs(receipt, expected_queues)
+
+ transfer = make_stock_entry(item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10)
+ for rate in rates[1:]:
+ row = frappe.copy_doc(transfer.items[0], ignore_no_copy=False)
+ row.basic_rate = rate
+ transfer.append("items", row)
+
+ transfer.save()
+ transfer.submit()
+
+ # same exact queue should be transferred
+ self.assertSLEs(transfer, expected_queues, sle_filters={"warehouse": target})
+
+
def create_repack_entry(**args):
args = frappe._dict(args)
repack = frappe.new_doc("Stock Entry")
From ccd2ce56b1d6e24005e5a24b11c72e78adb0a4e4 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 1 Mar 2022 14:04:02 +0530
Subject: [PATCH 211/447] fix: FIFO valuation in case of multi-item entries
---
erpnext/stock/stock_ledger.py | 13 ++++++-------
1 file changed, 6 insertions(+), 7 deletions(-)
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 69755529e5..97d54f8984 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -448,8 +448,8 @@ class update_entries_after(object):
return
# Get dynamic incoming/outgoing rate
- if not self.args.get("sle_id"):
- self.get_dynamic_incoming_outgoing_rate(sle)
+ # XXX: performance regression
+ self.get_dynamic_incoming_outgoing_rate(sle)
if get_serial_nos(sle.serial_no):
self.get_serialized_values(sle)
@@ -492,8 +492,8 @@ class update_entries_after(object):
sle.doctype="Stock Ledger Entry"
frappe.get_doc(sle).db_update()
- if not self.args.get("sle_id"):
- self.update_outgoing_rate_on_transaction(sle)
+ # XXX: performance regression
+ self.update_outgoing_rate_on_transaction(sle)
def validate_negative_stock(self, sle):
@@ -576,9 +576,8 @@ class update_entries_after(object):
def update_rate_on_stock_entry(self, sle, outgoing_rate):
frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
- # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
- if not sle.dependant_sle_voucher_detail_no:
- self.recalculate_amounts_in_stock_entry(sle.voucher_no)
+ # XXX: performance regression
+ self.recalculate_amounts_in_stock_entry(sle.voucher_no)
def recalculate_amounts_in_stock_entry(self, voucher_no):
stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
From 2f71c5bccaad5924a3047912761240681698d0ee Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 1 Mar 2022 17:04:10 +0530
Subject: [PATCH 212/447] test: repack FIFO rates
---
.../test_stock_ledger_entry.py | 40 ++++++++++++++++++-
1 file changed, 38 insertions(+), 2 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 a182a6d7ef..684a8d4d7c 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
@@ -392,7 +392,7 @@ class TestStockLedgerEntry(FrappeTestCase):
def assertSLEs(self, doc, expected_sles, sle_filters=None):
""" Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line"""
- filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled":0}
+ filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0}
if sle_filters:
filters.update(sle_filters)
sles = frappe.get_all("Stock Ledger Entry", fields=["*"], filters=filters,
@@ -694,7 +694,6 @@ class TestStockLedgerEntry(FrappeTestCase):
transfer = make_stock_entry(item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10)
for rate in rates[1:]:
row = frappe.copy_doc(transfer.items[0], ignore_no_copy=False)
- row.basic_rate = rate
transfer.append("items", row)
transfer.save()
@@ -703,6 +702,43 @@ class TestStockLedgerEntry(FrappeTestCase):
# same exact queue should be transferred
self.assertSLEs(transfer, expected_queues, sle_filters={"warehouse": target})
+ def test_fifo_multi_item_repack_consumption(self):
+ rm = make_item("_TestFifoRepackRM")
+ packed = make_item("_TestFifoRepackFinished")
+ warehouse = "_Test Warehouse - _TC"
+
+ rates = [10 * i for i in range(1, 5)]
+
+ receipt = make_stock_entry(item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10)
+ for rate in rates[1:]:
+ row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False)
+ row.basic_rate = rate
+ receipt.append("items", row)
+
+ receipt.save()
+ receipt.submit()
+
+ repack = make_stock_entry(item_code=rm.name, source=warehouse, qty=10,
+ do_not_save=True, rate=10, purpose="Repack")
+ for rate in rates[1:]:
+ row = frappe.copy_doc(repack.items[0], ignore_no_copy=False)
+ repack.append("items", row)
+
+ repack.append("items", {
+ "item_code": packed.name,
+ "t_warehouse": warehouse,
+ "qty": 1,
+ "transfer_qty": 1,
+ })
+
+ repack.save()
+ repack.submit()
+
+ # same exact queue should be transferred
+ self.assertSLEs(repack, [
+ {"incoming_rate": sum(rates) * 10}
+ ], sle_filters={"item_code": packed.name})
+
def create_repack_entry(**args):
args = frappe._dict(args)
From 701878f60b4a9b60035295f23ae65973680b03e5 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 1 Mar 2022 18:08:29 +0530
Subject: [PATCH 213/447] revert "fix: FIFO valuation in case of multi-item
entries"
This reverts commit b8ee193d1a124668691b3d8181ce4e3bf612afe0.
This is huge performance regression for large docs.
---
erpnext/stock/stock_ledger.py | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 97d54f8984..69755529e5 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -448,8 +448,8 @@ class update_entries_after(object):
return
# Get dynamic incoming/outgoing rate
- # XXX: performance regression
- self.get_dynamic_incoming_outgoing_rate(sle)
+ if not self.args.get("sle_id"):
+ self.get_dynamic_incoming_outgoing_rate(sle)
if get_serial_nos(sle.serial_no):
self.get_serialized_values(sle)
@@ -492,8 +492,8 @@ class update_entries_after(object):
sle.doctype="Stock Ledger Entry"
frappe.get_doc(sle).db_update()
- # XXX: performance regression
- self.update_outgoing_rate_on_transaction(sle)
+ if not self.args.get("sle_id"):
+ self.update_outgoing_rate_on_transaction(sle)
def validate_negative_stock(self, sle):
@@ -576,8 +576,9 @@ class update_entries_after(object):
def update_rate_on_stock_entry(self, sle, outgoing_rate):
frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
- # XXX: performance regression
- self.recalculate_amounts_in_stock_entry(sle.voucher_no)
+ # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
+ if not sle.dependant_sle_voucher_detail_no:
+ self.recalculate_amounts_in_stock_entry(sle.voucher_no)
def recalculate_amounts_in_stock_entry(self, voucher_no):
stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
From 3638fbf06bfc492515096e4b4a065a52a495420a Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 1 Mar 2022 18:17:14 +0530
Subject: [PATCH 214/447] fix: repost items with repeating item-warehouses
---
erpnext/controllers/stock_controller.py | 30 ++++++++++++++++++++++++-
1 file changed, 29 insertions(+), 1 deletion(-)
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index c8e5eddfea..8972c32879 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -507,13 +507,41 @@ class StockController(AccountsController):
"voucher_no": self.name,
"company": self.company
})
- if future_sle_exists(args):
+
+ if future_sle_exists(args) or repost_required_for_queue(self):
item_based_reposting = cint(frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"))
if item_based_reposting:
create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name)
else:
create_repost_item_valuation_entry(args)
+def repost_required_for_queue(doc: StockController) -> bool:
+ """check if stock document contains repeated item-warehouse with queue based valuation.
+
+ if queue exists for repeated items then SLEs need to reprocessed in background again.
+ """
+
+ consuming_sles = frappe.db.get_all("Stock Ledger Entry",
+ filters={
+ "voucher_type": doc.doctype,
+ "voucher_no": doc.name,
+ "actual_qty": ("<", 0),
+ "is_cancelled": 0
+ },
+ fields=["item_code", "warehouse", "stock_queue"]
+ )
+ item_warehouses = [(sle.item_code, sle.warehouse) for sle in consuming_sles]
+
+ unique_item_warehouses = set(item_warehouses)
+
+ if len(unique_item_warehouses) == len(item_warehouses):
+ return False
+
+ for sle in consuming_sles:
+ if sle.stock_queue != "[]": # using FIFO/LIFO valuation
+ return True
+ return False
+
@frappe.whitelist()
def make_quality_inspections(doctype, docname, items):
From b988046082b97e41c8b07eeaae35cfc2829e2bf3 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Wed, 2 Mar 2022 14:15:22 +0530
Subject: [PATCH 215/447] fix(Timesheet): fetch exchange rate only if currency
is set (#30057)
---
erpnext/projects/doctype/timesheet/timesheet.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js
index f615f051f0..453d46c7c4 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.js
+++ b/erpnext/projects/doctype/timesheet/timesheet.js
@@ -116,7 +116,7 @@ frappe.ui.form.on("Timesheet", {
currency: function(frm) {
let base_currency = frappe.defaults.get_global_default('currency');
- if (base_currency != frm.doc.currency) {
+ if (frm.doc.currency && (base_currency != frm.doc.currency)) {
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
From 55a966ec4137a5d60ef4351ea401e0fcbc5dff91 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 2 Mar 2022 14:49:45 +0530
Subject: [PATCH 216/447] chore: get stock reco qty from SR instead of SLE
(#30059)
[skip ci]
---
.../stock_ledger_invariant_check.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
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 7826d34422..1ba2482935 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
@@ -21,6 +21,7 @@ SLE_FIELDS = (
"stock_value",
"stock_value_difference",
"valuation_rate",
+ "voucher_detail_no",
)
@@ -66,7 +67,9 @@ def add_invariant_check_fields(sles):
balance_qty += sle.actual_qty
balance_stock_value += sle.stock_value_difference
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
- balance_qty = sle.qty_after_transaction
+ balance_qty = frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "qty")
+ if balance_qty is None:
+ balance_qty = sle.qty_after_transaction
sle.fifo_queue_qty = fifo_qty
sle.fifo_stock_value = fifo_value
From f8ac4c082a8512349187cecf058c8945231e7d52 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 2 Mar 2022 16:25:18 +0530
Subject: [PATCH 217/447] fix: remove dead dashboard links
---
.../manufacturing/doctype/operation/operation_dashboard.py | 2 +-
.../doctype/workstation/workstation_dashboard.py | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/erpnext/manufacturing/doctype/operation/operation_dashboard.py b/erpnext/manufacturing/doctype/operation/operation_dashboard.py
index 4fbcf4954e..9f7efa2b38 100644
--- a/erpnext/manufacturing/doctype/operation/operation_dashboard.py
+++ b/erpnext/manufacturing/doctype/operation/operation_dashboard.py
@@ -7,7 +7,7 @@ def get_data():
'transactions': [
{
'label': _('Manufacture'),
- 'items': ['BOM', 'Work Order', 'Job Card', 'Timesheet']
+ 'items': ['BOM', 'Work Order', 'Job Card']
}
]
}
diff --git a/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py b/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py
index bc481ca192..1fa14940a4 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py
+++ b/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py
@@ -11,9 +11,9 @@ def get_data():
},
{
'label': _('Transaction'),
- 'items': ['Work Order', 'Job Card', 'Timesheet']
+ 'items': ['Work Order', 'Job Card',]
}
],
'disable_create_buttons': ['BOM', 'Routing', 'Operation',
- 'Work Order', 'Job Card', 'Timesheet']
+ 'Work Order', 'Job Card',]
}
From 9ef35ef7735683041d412f25eed9d7b59e2e8dd7 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 2 Mar 2022 16:32:48 +0530
Subject: [PATCH 218/447] fix: dont hardcode precision in routing
Co-Authored-By: Suraj Shetty
---
erpnext/manufacturing/doctype/routing/routing.js | 2 +-
erpnext/manufacturing/doctype/routing/routing.py | 3 ++-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/erpnext/manufacturing/doctype/routing/routing.js b/erpnext/manufacturing/doctype/routing/routing.js
index 33a313e32f..b480c70ad5 100644
--- a/erpnext/manufacturing/doctype/routing/routing.js
+++ b/erpnext/manufacturing/doctype/routing/routing.js
@@ -17,7 +17,7 @@ frappe.ui.form.on('Routing', {
},
calculate_operating_cost: function(frm, child) {
- const operating_cost = flt(flt(child.hour_rate) * flt(child.time_in_mins) / 60, 2);
+ const operating_cost = flt(flt(child.hour_rate) * flt(child.time_in_mins) / 60, precision("operating_cost", child));
frappe.model.set_value(child.doctype, child.name, "operating_cost", operating_cost);
}
});
diff --git a/erpnext/manufacturing/doctype/routing/routing.py b/erpnext/manufacturing/doctype/routing/routing.py
index 1c76634646..b207906c5e 100644
--- a/erpnext/manufacturing/doctype/routing/routing.py
+++ b/erpnext/manufacturing/doctype/routing/routing.py
@@ -20,7 +20,8 @@ class Routing(Document):
for operation in self.operations:
if not operation.hour_rate:
operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, 'hour_rate')
- operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, 2)
+ operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60,
+ operation.precision("operating_cost"))
def set_routing_id(self):
sequence_id = 0
From e2c144e9e3e6bd71f6040dc81e698ffe2f0f37d2 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Sun, 6 Mar 2022 19:05:02 +0530
Subject: [PATCH 219/447] fix: Item-wise sales history report
---
.../item_wise_sales_history.py | 36 +++++++++----------
1 file changed, 18 insertions(+), 18 deletions(-)
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 4a245e1f77..56e1eb57b8 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
@@ -156,24 +156,24 @@ def get_data(filters):
customer_record = customer_details.get(record.customer)
item_record = item_details.get(record.item_code)
row = {
- "item_code": record.item_code,
- "item_name": item_record.item_name,
- "item_group": item_record.item_group,
- "description": record.description,
- "quantity": record.qty,
- "uom": record.uom,
- "rate": record.base_rate,
- "amount": record.base_amount,
- "sales_order": record.name,
- "transaction_date": record.transaction_date,
- "customer": record.customer,
- "customer_name": customer_record.customer_name,
- "customer_group": customer_record.customer_group,
- "territory": record.territory,
- "project": record.project,
- "delivered_quantity": flt(record.delivered_qty),
- "billed_amount": flt(record.billed_amt),
- "company": record.company
+ "item_code": record.get('item_code'),
+ "item_name": item_record.get('item_name'),
+ "item_group": item_record.get('item_group'),
+ "description": record.get('description'),
+ "quantity": record.get('qty'),
+ "uom": record.get('uom'),
+ "rate": record.get('base_rate'),
+ "amount": record.get('base_amount'),
+ "sales_order": record.get('name'),
+ "transaction_date": record.get('transaction_date'),
+ "customer": record.get('customer'),
+ "customer_name": customer_record.get('customer_name'),
+ "customer_group": customer_record.get('customer_group'),
+ "territory": record.get('territory'),
+ "project": record.get('project'),
+ "delivered_quantity": flt(record.get('delivered_qty')),
+ "billed_amount": flt(record.get('billed_amt')),
+ "company": record.get('company')
}
data.append(row)
From 031a0dd7035aced02c0a943ce3bff7dba49d3a7f Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Mon, 7 Mar 2022 11:40:01 +0530
Subject: [PATCH 220/447] fix(e-invoicing): remove batch no from e-invoices
---
erpnext/regional/india/e_invoice/einv_item_template.json | 6 +-----
erpnext/regional/india/e_invoice/utils.py | 2 --
2 files changed, 1 insertion(+), 7 deletions(-)
diff --git a/erpnext/regional/india/e_invoice/einv_item_template.json b/erpnext/regional/india/e_invoice/einv_item_template.json
index 78e56518df..2c04c6dcf4 100644
--- a/erpnext/regional/india/e_invoice/einv_item_template.json
+++ b/erpnext/regional/india/e_invoice/einv_item_template.json
@@ -23,9 +23,5 @@
"StateCesAmt": "{item.state_cess_amount}",
"StateCesNonAdvlAmt": "{item.state_cess_nadv_amount}",
"OthChrg": "{item.other_charges}",
- "TotItemVal": "{item.total_value}",
- "BchDtls": {{
- "Nm": "{item.batch_no}",
- "ExpDt": "{item.batch_expiry_date}"
- }}
+ "TotItemVal": "{item.total_value}"
}}
\ No newline at end of file
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index e3f7e90ff3..64c75c4a75 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -214,8 +214,6 @@ def get_item_list(invoice):
item.taxable_value = abs(item.taxable_value)
item.discount_amount = 0
- item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
- item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
item.is_service_item = 'Y' if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else 'N'
item.serial_no = ""
From b2755f6fdddd3e1b0a305b57c18651c98fee8f7e Mon Sep 17 00:00:00 2001
From: marination
Date: Mon, 7 Mar 2022 13:02:08 +0530
Subject: [PATCH 221/447] feat: Include child item group products in Item Group
Page & cleanup
- Added 'Include descendants' checkbox, which will pull child item group products too
- Build item group filters in query engine file
- Include logic in filter engine
- Clean up Website section of Item Group page (UX)
- Add util to fetch child item groups including self
---
erpnext/e_commerce/api.py | 1 -
.../e_commerce/product_data_engine/filters.py | 22 ++++++---
.../e_commerce/product_data_engine/query.py | 48 ++++++++-----------
.../setup/doctype/item_group/item_group.json | 31 +++++++++---
.../setup/doctype/item_group/item_group.py | 8 +++-
5 files changed, 69 insertions(+), 41 deletions(-)
diff --git a/erpnext/e_commerce/api.py b/erpnext/e_commerce/api.py
index 43cb36ca2e..84554ae7d0 100644
--- a/erpnext/e_commerce/api.py
+++ b/erpnext/e_commerce/api.py
@@ -48,7 +48,6 @@ def get_product_filter_data(query_args=None):
sub_categories = []
if item_group:
- field_filters['item_group'] = item_group
sub_categories = get_child_groups_for_website(item_group, immediate=True)
engine = ProductQuery()
diff --git a/erpnext/e_commerce/product_data_engine/filters.py b/erpnext/e_commerce/product_data_engine/filters.py
index c4a3cb9fbe..89d4ffdff4 100644
--- a/erpnext/e_commerce/product_data_engine/filters.py
+++ b/erpnext/e_commerce/product_data_engine/filters.py
@@ -14,6 +14,8 @@ class ProductFiltersBuilder:
self.item_group = item_group
def get_field_filters(self):
+ from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website
+
if not self.item_group and not self.doc.enable_field_filters:
return
@@ -25,18 +27,26 @@ class ProductFiltersBuilder:
fields = [item_meta.get_field(field) for field in filter_fields if item_meta.has_field(field)]
for df in fields:
- item_filters, item_or_filters = {}, []
+ item_filters, item_or_filters = {"published_in_website": 1}, []
link_doctype_values = self.get_filtered_link_doctype_records(df)
if df.fieldtype == "Link":
if self.item_group:
- item_or_filters.extend([
- ["item_group", "=", self.item_group],
- ["Website Item Group", "item_group", "=", self.item_group] # consider website item groups
- ])
+ include_child = frappe.db.get_value("Item Group", self.item_group, "include_descendants")
+ if include_child:
+ include_groups = get_child_groups_for_website(self.item_group, include_self=True)
+ include_groups = [x.name for x in include_groups]
+ item_or_filters.extend([
+ ["item_group", "in", include_groups],
+ ["Website Item Group", "item_group", "=", self.item_group] # consider website item groups
+ ])
+ else:
+ item_or_filters.extend([
+ ["item_group", "=", self.item_group],
+ ["Website Item Group", "item_group", "=", self.item_group] # consider website item groups
+ ])
# Get link field values attached to published items
- item_filters['published_in_website'] = 1
item_values = frappe.get_all(
"Item",
fields=[df.fieldname],
diff --git a/erpnext/e_commerce/product_data_engine/query.py b/erpnext/e_commerce/product_data_engine/query.py
index cfc3c7b357..5c92e3d98a 100644
--- a/erpnext/e_commerce/product_data_engine/query.py
+++ b/erpnext/e_commerce/product_data_engine/query.py
@@ -46,10 +46,10 @@ class ProductQuery:
self.filter_with_discount = bool(fields.get("discount"))
result, discount_list, website_item_groups, cart_items, count = [], [], [], [], 0
- website_item_groups = self.get_website_item_group_results(item_group, website_item_groups)
-
if fields:
self.build_fields_filters(fields)
+ if item_group:
+ self.build_item_group_filters(item_group)
if search_term:
self.build_search_filters(search_term)
if self.settings.hide_variants:
@@ -61,8 +61,6 @@ class ProductQuery:
else:
result, count = self.query_items(start=start)
- result = self.combine_web_item_group_results(item_group, result, website_item_groups)
-
# sort combined results by ranking
result = sorted(result, key=lambda x: x.get("ranking"), reverse=True)
@@ -167,6 +165,25 @@ class ProductQuery:
# `=` will be faster than `IN` for most cases
self.filters.append([field, "=", values])
+ def build_item_group_filters(self, item_group):
+ "Add filters for Item group page and include Website Item Groups."
+ from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website
+ item_group_filters = []
+
+ item_group_filters.append(["Website Item", "item_group", "=", item_group])
+ # Consider Website Item Groups
+ item_group_filters.append(["Website Item Group", "item_group", "=", item_group])
+
+ if frappe.db.get_value("Item Group", item_group, "include_descendants"):
+ # include child item group's items as well
+ # eg. Group Node A, will show items of child 1 and child 2 as well
+ # on it's web page
+ include_groups = get_child_groups_for_website(item_group, include_self=True)
+ include_groups = [x.name for x in include_groups]
+ item_group_filters.append(["Website Item", "item_group", "in", include_groups])
+
+ self.or_filters.extend(item_group_filters)
+
def build_search_filters(self, search_term):
"""Query search term in specified fields
@@ -190,19 +207,6 @@ class ProductQuery:
for field in search_fields:
self.or_filters.append([field, "like", search])
- def get_website_item_group_results(self, item_group, website_item_groups):
- """Get Web Items for Item Group Page via Website Item Groups."""
- if item_group:
- website_item_groups = frappe.db.get_all(
- "Website Item",
- fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
- filters=[
- ["Website Item Group", "item_group", "=", item_group],
- ["published", "=", 1]
- ]
- )
- return website_item_groups
-
def add_display_details(self, result, discount_list, cart_items):
"""Add price and availability details in result."""
for item in result:
@@ -278,16 +282,6 @@ class ProductQuery:
return []
- def combine_web_item_group_results(self, item_group, result, website_item_groups):
- """Combine results with context of website item groups into item results."""
- if item_group and website_item_groups:
- items_list = {row.name for row in result}
- for row in website_item_groups:
- if row.wig_parent not in items_list:
- result.append(row)
-
- return result
-
def filter_results_by_discount(self, fields, result):
if fields and fields.get("discount"):
discount_percent = frappe.utils.flt(fields["discount"][0])
diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json
index 3e0680f4f5..a090c8d76c 100644
--- a/erpnext/setup/doctype/item_group/item_group.json
+++ b/erpnext/setup/doctype/item_group/item_group.json
@@ -20,12 +20,14 @@
"sec_break_taxes",
"taxes",
"sb9",
- "show_in_website",
"route",
- "weightage",
- "slideshow",
"website_title",
"description",
+ "show_in_website",
+ "include_descendants",
+ "column_break_16",
+ "weightage",
+ "slideshow",
"website_specifications",
"website_filters_section",
"filter_fields",
@@ -111,7 +113,7 @@
},
{
"default": "0",
- "description": "Check this if you want to show in website",
+ "description": "Make Item Group visible in website",
"fieldname": "show_in_website",
"fieldtype": "Check",
"label": "Show in Website"
@@ -124,6 +126,7 @@
"unique": 1
},
{
+ "depends_on": "show_in_website",
"fieldname": "weightage",
"fieldtype": "Int",
"label": "Weightage"
@@ -186,6 +189,8 @@
"report_hide": 1
},
{
+ "collapsible": 1,
+ "depends_on": "show_in_website",
"fieldname": "website_filters_section",
"fieldtype": "Section Break",
"label": "Website Filters"
@@ -203,9 +208,21 @@
"options": "Website Attribute"
},
{
+ "depends_on": "show_in_website",
"fieldname": "website_title",
"fieldtype": "Data",
"label": "Title"
+ },
+ {
+ "fieldname": "column_break_16",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "description": "Include Website Items belonging to child Item Groups",
+ "fieldname": "include_descendants",
+ "fieldtype": "Check",
+ "label": "Include Descendants"
}
],
"icon": "fa fa-sitemap",
@@ -214,11 +231,12 @@
"is_tree": 1,
"links": [],
"max_attachments": 3,
- "modified": "2021-02-18 13:40:30.049650",
+ "modified": "2022-03-07 09:44:47.561532",
"modified_by": "Administrator",
"module": "Setup",
"name": "Item Group",
"name_case": "Title Case",
+ "naming_rule": "By fieldname",
"nsm_parent_field": "parent_item_group",
"owner": "Administrator",
"permissions": [
@@ -285,5 +303,6 @@
"search_fields": "parent_item_group",
"show_name_in_global_search": 1,
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index 4f92240c84..5c7194baf6 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -111,7 +111,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
from erpnext.stock.doctype.item.item import validate_item_default_company_links
validate_item_default_company_links(self.item_group_defaults)
-def get_child_groups_for_website(item_group_name, immediate=False):
+def get_child_groups_for_website(item_group_name, immediate=False, include_self=False):
"""Returns child item groups *excluding* passed group."""
item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1)
filters = {
@@ -123,6 +123,12 @@ def get_child_groups_for_website(item_group_name, immediate=False):
if immediate:
filters["parent_item_group"] = item_group_name
+ if include_self:
+ filters.update({
+ "lft": [">=", item_group.lft],
+ "rgt": ["<=", item_group.rgt]
+ })
+
return frappe.get_all(
"Item Group",
filters=filters,
From f3a95d3c27a5430c4a9176b685da20489a7e2429 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Mon, 7 Mar 2022 16:10:26 +0530
Subject: [PATCH 222/447] fix(ux): Improve label for better understanding
---
.../doctype/sales_invoice/sales_invoice.json | 18 +++++++++++++-----
1 file changed, 13 insertions(+), 5 deletions(-)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 5062c1c807..973c8371ea 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -2,13 +2,14 @@
"actions": [],
"allow_import": 1,
"autoname": "naming_series:",
- "creation": "2013-05-24 19:29:05",
+ "creation": "2022-01-25 10:29:57.771398",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"customer_section",
"title",
"naming_series",
+ "tax_invoice_number",
"customer",
"customer_name",
"tax_id",
@@ -651,7 +652,6 @@
"hide_seconds": 1,
"label": "Ignore Pricing Rule",
"no_copy": 1,
- "permlevel": 0,
"print_hide": 1
},
{
@@ -1974,9 +1974,10 @@
},
{
"default": "0",
+ "description": "Issue a debit note with 0 qty against an existing Sales Invoice",
"fieldname": "is_debit_note",
"fieldtype": "Check",
- "label": "Is Debit Note"
+ "label": "Is Rate Adjustment Entry (Debit Note)"
},
{
"default": "0",
@@ -2026,6 +2027,12 @@
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"read_only": 1
+ },
+ {
+ "fieldname": "tax_invoice_number",
+ "fieldtype": "Data",
+ "label": "Tax Invoice Number",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
@@ -2038,7 +2045,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2021-12-23 20:19:38.667508",
+ "modified": "2022-03-07 16:08:53.517903",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
@@ -2089,8 +2096,9 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"timeline_field": "customer",
"title_field": "title",
"track_changes": 1,
"track_seen": 1
-}
+}
\ No newline at end of file
From 17445c7e04ff88cc5db727cb9f769647bcbebfdf Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Mon, 7 Mar 2022 18:01:07 +0530
Subject: [PATCH 223/447] fix(pos): multiple pos round off cases
---
.../pos_invoice_merge_log.py | 9 --
.../test_pos_invoice_merge_log.py | 98 +++++++++++++++++++
.../doctype/sales_invoice/sales_invoice.py | 3 +
erpnext/controllers/taxes_and_totals.py | 29 +++---
4 files changed, 117 insertions(+), 22 deletions(-)
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index ddca68a57b..d4513c6a68 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -84,20 +84,12 @@ class POSInvoiceMergeLog(Document):
sales_invoice.set_posting_time = 1
sales_invoice.posting_date = getdate(self.posting_date)
sales_invoice.save()
- self.write_off_fractional_amount(sales_invoice, data)
sales_invoice.submit()
self.consolidated_invoice = sales_invoice.name
return sales_invoice.name
- def write_off_fractional_amount(self, invoice, data):
- pos_invoice_grand_total = sum(d.grand_total for d in data)
-
- if abs(pos_invoice_grand_total - invoice.grand_total) < 1:
- invoice.write_off_amount += -1 * (pos_invoice_grand_total - invoice.grand_total)
- invoice.save()
-
def process_merging_into_credit_note(self, data):
credit_note = self.get_new_sales_invoice()
credit_note.is_return = 1
@@ -110,7 +102,6 @@ class POSInvoiceMergeLog(Document):
# TODO: return could be against multiple sales invoice which could also have been consolidated?
# credit_note.return_against = self.consolidated_invoice
credit_note.save()
- self.write_off_fractional_amount(credit_note, data)
credit_note.submit()
self.consolidated_credit_note = credit_note.name
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
index 5930aa097f..89f7f18b42 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
@@ -5,6 +5,7 @@ import json
import unittest
import frappe
+from frappe.tests.utils import change_settings
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
@@ -280,3 +281,100 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")
+
+ @change_settings("System Settings", {"number_format": "#,###.###", "currency_precision": 3, "float_precision": 3})
+ def test_consolidation_round_off_error_3(self):
+ frappe.db.sql("delete from `tabPOS Invoice`")
+
+ try:
+ make_stock_entry(
+ to_warehouse="_Test Warehouse - _TC",
+ item_code="_Test Item",
+ rate=8000,
+ qty=10,
+ )
+ init_user_and_profile()
+
+ item_rates = [69, 59, 29]
+ for i in [1, 2]:
+ inv = create_pos_invoice(is_return=1, do_not_save=1)
+ inv.items = []
+ for rate in item_rates:
+ inv.append("items", {
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": -1,
+ "rate": rate,
+ "income_account": "Sales - _TC",
+ "expense_account": "Cost of Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ })
+ inv.append("taxes", {
+ "account_head": "_Test Account VAT - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "VAT",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 15,
+ "included_in_print_rate": 1
+ })
+ inv.payments = []
+ inv.append('payments', {
+ 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -157
+ })
+ inv.paid_amount = -157
+ inv.save()
+ inv.submit()
+
+ consolidate_pos_invoices()
+
+ inv.load_from_db()
+ consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
+ self.assertEqual(consolidated_invoice.status, 'Return')
+ self.assertEqual(consolidated_invoice.rounding_adjustment, -0.001)
+
+ finally:
+ frappe.set_user("Administrator")
+ frappe.db.sql("delete from `tabPOS Profile`")
+ frappe.db.sql("delete from `tabPOS Invoice`")
+
+ def test_consolidation_rounding_adjustment(self):
+ '''
+ Test if the rounding adjustment is calculated correctly
+ '''
+ frappe.db.sql("delete from `tabPOS Invoice`")
+
+ try:
+ make_stock_entry(
+ to_warehouse="_Test Warehouse - _TC",
+ item_code="_Test Item",
+ rate=8000,
+ qty=10,
+ )
+
+ init_user_and_profile()
+
+ inv = create_pos_invoice(qty=1, rate=69.5, do_not_save=True)
+ inv.append('payments', {
+ 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 70
+ })
+ inv.insert()
+ inv.submit()
+
+ inv2 = create_pos_invoice(qty=1, rate=59.5, do_not_save=True)
+ inv2.append('payments', {
+ 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60
+ })
+ inv2.insert()
+ inv2.submit()
+
+ consolidate_pos_invoices()
+
+ inv.load_from_db()
+ consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
+ self.assertEqual(consolidated_invoice.rounding_adjustment, 1)
+
+ finally:
+ frappe.set_user("Administrator")
+ frappe.db.sql("delete from `tabPOS Profile`")
+ frappe.db.sql("delete from `tabPOS Invoice`")
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index b894f90c7e..573da276a2 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -263,6 +263,9 @@ class SalesInvoice(SellingController):
self.process_common_party_accounting()
def validate_pos_return(self):
+ if self.is_consolidated:
+ # pos return is already validated in pos invoice
+ return
if self.is_pos and self.is_return:
total_amount_in_payments = 0
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 2776628227..42400bb807 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -270,7 +270,8 @@ class calculate_taxes_and_totals(object):
shipping_rule.apply(self.doc)
def calculate_taxes(self):
- if not self.doc.get('is_consolidated'):
+ rounding_adjustment_computed = self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment')
+ if not rounding_adjustment_computed:
self.doc.rounding_adjustment = 0
# maintain actual tax rate based on idx
@@ -326,7 +327,7 @@ class calculate_taxes_and_totals(object):
if i == (len(self.doc.get("taxes")) - 1) and self.discount_amount_applied \
and self.doc.discount_amount \
and self.doc.apply_discount_on == "Grand Total" \
- and not self.doc.get('is_consolidated'):
+ and not rounding_adjustment_computed:
self.doc.rounding_adjustment = flt(self.doc.grand_total
- flt(self.doc.discount_amount) - tax.total,
self.doc.precision("rounding_adjustment"))
@@ -465,20 +466,22 @@ class calculate_taxes_and_totals(object):
self.doc.total_net_weight += d.total_weight
def set_rounded_total(self):
- if not self.doc.get('is_consolidated'):
- if self.doc.meta.get_field("rounded_total"):
- if self.doc.is_rounded_total_disabled():
- self.doc.rounded_total = self.doc.base_rounded_total = 0
- return
+ if self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment'):
+ return
- self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total,
- self.doc.currency, self.doc.precision("rounded_total"))
+ if self.doc.meta.get_field("rounded_total"):
+ if self.doc.is_rounded_total_disabled():
+ self.doc.rounded_total = self.doc.base_rounded_total = 0
+ return
- #if print_in_rate is set, we would have already calculated rounding adjustment
- self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total,
- self.doc.precision("rounding_adjustment"))
+ self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total,
+ self.doc.currency, self.doc.precision("rounded_total"))
- self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
+ #if print_in_rate is set, we would have already calculated rounding adjustment
+ self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total,
+ self.doc.precision("rounding_adjustment"))
+
+ self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
def _cleanup(self):
if not self.doc.get('is_consolidated'):
From 748d5c4873c3e09a37c7466b70c3bc0e329f12c5 Mon Sep 17 00:00:00 2001
From: Dany Robert
Date: Mon, 7 Mar 2022 19:57:57 +0530
Subject: [PATCH 224/447] fix: wrong payment days in salary slip for employees
joining/leaving during mid payroll dates (#29082)
Co-authored-by: Rucha Mahabal
---
erpnext/hr/doctype/attendance/attendance.py | 18 ++-
.../hr/doctype/attendance/test_attendance.py | 93 +++++++++++++-
.../doctype/salary_slip/salary_slip.py | 55 ++++++--
.../doctype/salary_slip/test_salary_slip.py | 120 ++++++++++++++----
4 files changed, 239 insertions(+), 47 deletions(-)
diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py
index b1eaaf8b58..b1e373e218 100644
--- a/erpnext/hr/doctype/attendance/attendance.py
+++ b/erpnext/hr/doctype/attendance/attendance.py
@@ -174,16 +174,22 @@ def get_month_map():
def get_unmarked_days(employee, month, exclude_holidays=0):
import calendar
month_map = get_month_map()
-
today = get_datetime()
- dates_of_month = ['{}-{}-{}'.format(today.year, month_map[month], r) for r in range(1, calendar.monthrange(today.year, month_map[month])[1] + 1)]
+ joining_date, relieving_date = frappe.get_cached_value("Employee", employee, ["date_of_joining", "relieving_date"])
+ start_day = 1
+ end_day = calendar.monthrange(today.year, month_map[month])[1] + 1
- length = len(dates_of_month)
- month_start, month_end = dates_of_month[0], dates_of_month[length-1]
+ if joining_date and joining_date.month == month_map[month]:
+ start_day = joining_date.day
+ if relieving_date and relieving_date.month == month_map[month]:
+ end_day = relieving_date.day + 1
- records = frappe.get_all("Attendance", fields = ['attendance_date', 'employee'] , filters = [
+ dates_of_month = ['{}-{}-{}'.format(today.year, month_map[month], r) for r in range(start_day, end_day)]
+ month_start, month_end = dates_of_month[0], dates_of_month[-1]
+
+ records = frappe.get_all("Attendance", fields=['attendance_date', 'employee'], filters=[
["attendance_date", ">=", month_start],
["attendance_date", "<=", month_end],
["employee", "=", employee],
@@ -200,7 +206,7 @@ def get_unmarked_days(employee, month, exclude_holidays=0):
for date in dates_of_month:
date_time = get_datetime(date)
- if today.day == date_time.day and today.month == date_time.month:
+ if today.day <= date_time.day and today.month <= date_time.month:
break
if date_time not in marked_days:
unmarked_days.append(date)
diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py
index a770d70ffa..118cc987ef 100644
--- a/erpnext/hr/doctype/attendance/test_attendance.py
+++ b/erpnext/hr/doctype/attendance/test_attendance.py
@@ -4,17 +4,104 @@
import unittest
import frappe
-from frappe.utils import nowdate
+from frappe.utils import add_days, get_first_day, getdate, nowdate
+
+from erpnext.hr.doctype.attendance.attendance import (
+ get_month_map,
+ get_unmarked_days,
+ mark_attendance,
+)
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday
test_records = frappe.get_test_records('Attendance')
class TestAttendance(unittest.TestCase):
def test_mark_absent(self):
- from erpnext.hr.doctype.employee.test_employee import make_employee
employee = make_employee("test_mark_absent@example.com")
date = nowdate()
frappe.db.delete('Attendance', {'employee':employee, 'attendance_date':date})
- from erpnext.hr.doctype.attendance.attendance import mark_attendance
attendance = mark_attendance(employee, date, 'Absent')
fetch_attendance = frappe.get_value('Attendance', {'employee':employee, 'attendance_date':date, 'status':'Absent'})
self.assertEqual(attendance, fetch_attendance)
+
+ def test_unmarked_days(self):
+ first_day = get_first_day(getdate())
+
+ employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1))
+ frappe.db.delete('Attendance', {'employee': employee})
+
+ from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
+ holiday_list = make_holiday_list()
+ frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
+
+ first_sunday = get_first_sunday(holiday_list)
+ mark_attendance(employee, first_day, 'Present')
+ month_name = get_month_name(first_day)
+
+ unmarked_days = get_unmarked_days(employee, month_name)
+ unmarked_days = [getdate(date) for date in unmarked_days]
+
+ # attendance already marked for the day
+ self.assertNotIn(first_day, unmarked_days)
+ # attendance unmarked
+ self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
+ # holiday considered in unmarked days
+ self.assertIn(first_sunday, unmarked_days)
+
+ def test_unmarked_days_excluding_holidays(self):
+ first_day = get_first_day(getdate())
+
+ employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1))
+ frappe.db.delete('Attendance', {'employee': employee})
+
+ from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
+ holiday_list = make_holiday_list()
+ frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
+
+ first_sunday = get_first_sunday(holiday_list)
+ mark_attendance(employee, first_day, 'Present')
+ month_name = get_month_name(first_day)
+
+ unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True)
+ unmarked_days = [getdate(date) for date in unmarked_days]
+
+ # attendance already marked for the day
+ self.assertNotIn(first_day, unmarked_days)
+ # attendance unmarked
+ self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
+ # holidays not considered in unmarked days
+ self.assertNotIn(first_sunday, unmarked_days)
+
+ def test_unmarked_days_as_per_joining_and_relieving_dates(self):
+ first_day = get_first_day(getdate())
+
+ doj = add_days(first_day, 1)
+ relieving_date = add_days(first_day, 5)
+ employee = make_employee('test_unmarked_days_as_per_doj@example.com', date_of_joining=doj,
+ date_of_relieving=relieving_date)
+ frappe.db.delete('Attendance', {'employee': employee})
+
+ attendance_date = add_days(first_day, 2)
+ mark_attendance(employee, attendance_date, 'Present')
+ month_name = get_month_name(first_day)
+
+ unmarked_days = get_unmarked_days(employee, month_name)
+ unmarked_days = [getdate(date) for date in unmarked_days]
+
+ # attendance already marked for the day
+ self.assertNotIn(attendance_date, unmarked_days)
+ # date before doj not in unmarked days
+ self.assertNotIn(add_days(doj, -1), unmarked_days)
+ # date after relieving not in unmarked days
+ self.assertNotIn(add_days(relieving_date, 1), unmarked_days)
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+
+def get_month_name(date):
+ month_number = date.month
+ for month, number in get_month_map().items():
+ if number == month_number:
+ return month
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index d2a39989a6..b44dbb926d 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -307,28 +307,59 @@ class SalarySlip(TransactionBase):
if payroll_based_on == "Attendance":
self.payment_days -= flt(absent)
- unmarked_days = self.get_unmarked_days()
consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, "consider_unmarked_attendance_as") or "Present"
if payroll_based_on == "Attendance" and consider_unmarked_attendance_as =="Absent":
+ unmarked_days = self.get_unmarked_days(include_holidays_in_total_working_days)
self.absent_days += unmarked_days #will be treated as absent
self.payment_days -= unmarked_days
- if include_holidays_in_total_working_days:
- for holiday in holidays:
- if not frappe.db.exists("Attendance", {"employee": self.employee, "attendance_date": holiday, "docstatus": 1 }):
- self.payment_days += 1
else:
self.payment_days = 0
- def get_unmarked_days(self):
- marked_days = frappe.get_all("Attendance", filters = {
- "attendance_date": ["between", [self.start_date, self.end_date]],
- "employee": self.employee,
- "docstatus": 1
- }, fields = ["COUNT(*) as marked_days"])[0].marked_days
+ def get_unmarked_days(self, include_holidays_in_total_working_days):
+ unmarked_days = self.total_working_days
+ joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
+ ["date_of_joining", "relieving_date"])
+ start_date = self.start_date
+ end_date = self.end_date
- return self.total_working_days - marked_days
+ if joining_date and (getdate(self.start_date) < joining_date <= getdate(self.end_date)):
+ start_date = joining_date
+ unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving(unmarked_days,
+ include_holidays_in_total_working_days, self.start_date, joining_date)
+ if relieving_date and (getdate(self.start_date) <= relieving_date < getdate(self.end_date)):
+ end_date = relieving_date
+ unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving(unmarked_days,
+ include_holidays_in_total_working_days, relieving_date, self.end_date)
+
+ # exclude days for which attendance has been marked
+ unmarked_days -= frappe.get_all("Attendance", filters = {
+ "attendance_date": ["between", [start_date, end_date]],
+ "employee": self.employee,
+ "docstatus": 1
+ }, fields = ["COUNT(*) as marked_days"])[0].marked_days
+
+ return unmarked_days
+
+ def get_unmarked_days_based_on_doj_or_relieving(self, unmarked_days,
+ include_holidays_in_total_working_days, start_date, end_date):
+ """
+ Exclude days before DOJ or after
+ Relieving Date from unmarked days
+ """
+ from erpnext.hr.doctype.employee.employee import is_holiday
+
+ if include_holidays_in_total_working_days:
+ unmarked_days -= date_diff(end_date, start_date)
+ else:
+ # exclude only if not holidays
+ for days in range(date_diff(end_date, start_date)):
+ date = add_days(end_date, -days)
+ if not is_holiday(self.employee, date):
+ unmarked_days -= 1
+
+ return unmarked_days
def get_payment_days(self, joining_date, relieving_date, include_holidays_in_total_working_days):
if not joining_date:
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index 6a5debf998..fe15f2d3fa 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -7,10 +7,12 @@ import unittest
import frappe
from frappe.model.document import Document
+from frappe.tests.utils import change_settings
from frappe.utils import (
add_days,
add_months,
cstr,
+ date_diff,
flt,
get_first_day,
get_last_day,
@@ -21,6 +23,7 @@ from frappe.utils.make_random import get_random
import erpnext
from erpnext.accounts.utils import get_fiscal_year
+from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
@@ -37,17 +40,17 @@ class TestSalarySlip(unittest.TestCase):
setup_test()
def tearDown(self):
+ frappe.db.rollback()
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
frappe.set_user("Administrator")
+ @change_settings("Payroll Settings", {
+ "payroll_based_on": "Attendance",
+ "daily_wages_fraction_for_half_day": 0.75
+ })
def test_payment_days_based_on_attendance(self):
- from erpnext.hr.doctype.attendance.attendance import mark_attendance
no_of_days = self.get_no_of_days()
- # Payroll based on attendance
- frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
- frappe.db.set_value("Payroll Settings", None, "daily_wages_fraction_for_half_day", 0.75)
-
emp_id = make_employee("test_payment_days_based_on_attendance@salary.com")
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
@@ -85,14 +88,78 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(ss.gross_pay, gross_pay)
- frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
+ @change_settings("Payroll Settings", {
+ "payroll_based_on": "Attendance",
+ "consider_unmarked_attendance_as": "Absent",
+ "include_holidays_in_total_working_days": True
+ })
+ def test_payment_days_for_mid_joinee_including_holidays(self):
+ from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
+ no_of_days = self.get_no_of_days()
+ month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
+
+ new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
+ joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5)
+ frappe.db.set_value("Employee", new_emp_id, {
+ "date_of_joining": joining_date,
+ "relieving_date": relieving_date,
+ "status": "Left"
+ })
+
+ holidays = 0
+
+ for days in range(date_diff(relieving_date, joining_date) + 1):
+ date = add_days(joining_date, days)
+ if not is_holiday("Salary Slip Test Holiday List", date):
+ mark_attendance(new_emp_id, date, 'Present', ignore_validate=True)
+ else:
+ holidays += 1
+
+ new_ss = make_employee_salary_slip("test_payment_days_based_on_joining_date@salary.com", "Monthly", "Test Payment Based On Attendence")
+
+ self.assertEqual(new_ss.total_working_days, no_of_days[0])
+ self.assertEqual(new_ss.payment_days, no_of_days[0] - holidays - 8)
+
+ @change_settings("Payroll Settings", {
+ "payroll_based_on": "Attendance",
+ "consider_unmarked_attendance_as": "Absent",
+ "include_holidays_in_total_working_days": False
+ })
+ def test_payment_days_for_mid_joinee_excluding_holidays(self):
+ from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
+
+ no_of_days = self.get_no_of_days()
+ month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
+
+ new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
+ joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5)
+ frappe.db.set_value("Employee", new_emp_id, {
+ "date_of_joining": joining_date,
+ "relieving_date": relieving_date,
+ "status": "Left"
+ })
+
+ holidays = 0
+
+ for days in range(date_diff(relieving_date, joining_date) + 1):
+ date = add_days(joining_date, days)
+ if not is_holiday("Salary Slip Test Holiday List", date):
+ mark_attendance(new_emp_id, date, 'Present', ignore_validate=True)
+ else:
+ holidays += 1
+
+ new_ss = make_employee_salary_slip("test_payment_days_based_on_joining_date@salary.com", "Monthly", "Test Payment Based On Attendence")
+
+ self.assertEqual(new_ss.total_working_days, no_of_days[0] - no_of_days[1])
+ self.assertEqual(new_ss.payment_days, no_of_days[0] - holidays - 8)
+
+ @change_settings("Payroll Settings", {
+ "payroll_based_on": "Leave"
+ })
def test_payment_days_based_on_leave_application(self):
no_of_days = self.get_no_of_days()
- # Payroll based on attendance
- frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
-
emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com")
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
@@ -133,8 +200,9 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 4)
- frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
-
+ @change_settings("Payroll Settings", {
+ "payroll_based_on": "Attendance"
+ })
def test_payment_days_in_salary_slip_based_on_timesheet(self):
from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.projects.doctype.timesheet.test_timesheet import (
@@ -145,9 +213,6 @@ class TestSalarySlip(unittest.TestCase):
make_salary_slip as make_salary_slip_for_timesheet,
)
- # Payroll based on attendance
- frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
-
emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company", holiday_list="Salary Slip Test Holiday List")
frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"})
@@ -185,17 +250,15 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2))
- frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
-
+ @change_settings("Payroll Settings", {
+ "payroll_based_on": "Attendance"
+ })
def test_component_amount_dependent_on_another_payment_days_based_component(self):
from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
create_salary_structure_assignment,
)
- # Payroll based on attendance
- frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
-
salary_structure = make_salary_structure_for_payment_days_based_component_dependency()
employee = make_employee("test_payment_days_based_component@salary.com", company="_Test Company")
@@ -238,11 +301,12 @@ class TestSalarySlip(unittest.TestCase):
expected_amount = flt((flt(ss.gross_pay) - payment_days_based_comp_amount) * 0.12, precision)
self.assertEqual(actual_amount, expected_amount)
- frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
+ @change_settings("Payroll Settings", {
+ "include_holidays_in_total_working_days": 1
+ })
def test_salary_slip_with_holidays_included(self):
no_of_days = self.get_no_of_days()
- frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1)
make_employee("test_salary_slip_with_holidays_included@salary.com")
frappe.db.set_value("Employee", frappe.get_value("Employee",
{"employee_name":"test_salary_slip_with_holidays_included@salary.com"}, "name"), "relieving_date", None)
@@ -256,9 +320,11 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(ss.earnings[1].amount, 3000)
self.assertEqual(ss.gross_pay, 78000)
+ @change_settings("Payroll Settings", {
+ "include_holidays_in_total_working_days": 0
+ })
def test_salary_slip_with_holidays_excluded(self):
no_of_days = self.get_no_of_days()
- frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
make_employee("test_salary_slip_with_holidays_excluded@salary.com")
frappe.db.set_value("Employee", frappe.get_value("Employee",
{"employee_name":"test_salary_slip_with_holidays_excluded@salary.com"}, "name"), "relieving_date", None)
@@ -273,14 +339,15 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(ss.earnings[1].amount, 3000)
self.assertEqual(ss.gross_pay, 78000)
+ @change_settings("Payroll Settings", {
+ "include_holidays_in_total_working_days": 1
+ })
def test_payment_days(self):
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
create_salary_structure_assignment,
)
no_of_days = self.get_no_of_days()
- # Holidays not included in working days
- frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1)
# set joinng date in the same month
employee = make_employee("test_payment_days@salary.com")
@@ -338,11 +405,12 @@ class TestSalarySlip(unittest.TestCase):
frappe.set_user("test_employee_salary_slip_read_permission@salary.com")
self.assertTrue(salary_slip_test_employee.has_permission("read"))
+ @change_settings("Payroll Settings", {
+ "email_salary_slip_to_employee": 1
+ })
def test_email_salary_slip(self):
frappe.db.sql("delete from `tabEmail Queue`")
- frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 1)
-
make_employee("test_email_salary_slip@salary.com")
ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email")
ss.company = "_Test Company"
From 517fbf1d1f0a7d44e817b3f22ae30142e7bdf4c8 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Mon, 7 Mar 2022 22:31:42 +0530
Subject: [PATCH 225/447] fix: Ambigous column in picklist query
---
erpnext/stock/doctype/pick_list/pick_list.py | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 5484a117dd..86f24c2427 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -9,6 +9,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import map_child_doc
from frappe.utils import cint, floor, flt, today
+from six import iteritems
from erpnext.selling.doctype.sales_order.sales_order import (
make_delivery_note as create_delivery_note_from_sales_order,
@@ -245,7 +246,7 @@ def get_available_item_locations_for_serialized_item(item_code, from_warehouses,
warehouse_serial_nos_map.setdefault(warehouse, []).append(serial_no)
locations = []
- for warehouse, serial_nos in warehouse_serial_nos_map.items():
+ for warehouse, serial_nos in iteritems(warehouse_serial_nos_map):
locations.append({
'qty': len(serial_nos),
'warehouse': warehouse,
@@ -272,9 +273,9 @@ def get_available_item_locations_for_batched_item(item_code, from_warehouses, re
and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s
{warehouse_condition}
GROUP BY
- `warehouse`,
- `batch_no`,
- `item_code`
+ sle.`warehouse`,
+ sle.`batch_no`,
+ sle.`item_code`
HAVING `qty` > 0
ORDER BY IFNULL(batch.`expiry_date`, '2200-01-01'), batch.`creation`
""".format(warehouse_condition=warehouse_condition), { #nosec
From d9d4c2ce792304a528a7b8a27d50259a04bdd7ca Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Tue, 8 Mar 2022 07:56:20 +0530
Subject: [PATCH 226/447] fix: Remove unintentional changes
---
erpnext/stock/doctype/pick_list/pick_list.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 86f24c2427..b2eaecb586 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -9,7 +9,6 @@ from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import map_child_doc
from frappe.utils import cint, floor, flt, today
-from six import iteritems
from erpnext.selling.doctype.sales_order.sales_order import (
make_delivery_note as create_delivery_note_from_sales_order,
@@ -246,7 +245,7 @@ def get_available_item_locations_for_serialized_item(item_code, from_warehouses,
warehouse_serial_nos_map.setdefault(warehouse, []).append(serial_no)
locations = []
- for warehouse, serial_nos in iteritems(warehouse_serial_nos_map):
+ for warehouse, serial_nos in warehouse_serial_nos_map.items():
locations.append({
'qty': len(serial_nos),
'warehouse': warehouse,
From 9ace7d606cb5a63da8434200e9811d550de5cb5a Mon Sep 17 00:00:00 2001
From: ChillarAnand
Date: Mon, 7 Mar 2022 16:53:59 +0530
Subject: [PATCH 227/447] fix: Ignore missing customer group while fetching
price list
---
erpnext/accounts/party.py | 11 +++++------
erpnext/accounts/test_party.py | 16 ++++++++++++++++
2 files changed, 21 insertions(+), 6 deletions(-)
create mode 100644 erpnext/accounts/test_party.py
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index d6f6c5bcb6..b443852715 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -151,7 +151,7 @@ def set_contact_details(party_details, party, party_type):
def set_other_values(party_details, party, party_type):
# copy
- if party_type=="Customer":
+ if party_type == "Customer":
to_copy = ["customer_name", "customer_group", "territory", "language"]
else:
to_copy = ["supplier_name", "supplier_group", "language"]
@@ -170,12 +170,11 @@ def get_default_price_list(party):
return party.default_price_list
if party.doctype == "Customer":
- price_list = frappe.get_cached_value("Customer Group",
- party.customer_group, "default_price_list")
- if price_list:
- return price_list
+ try:
+ return frappe.get_cached_value("Customer Group", party.customer_group, "default_price_list")
+ except frappe.exceptions.DoesNotExistError:
+ return
- return None
def set_price_list(party_details, party, party_type, given_price_list, pos=None):
# price list
diff --git a/erpnext/accounts/test_party.py b/erpnext/accounts/test_party.py
new file mode 100644
index 0000000000..f7a1a858ab
--- /dev/null
+++ b/erpnext/accounts/test_party.py
@@ -0,0 +1,16 @@
+import frappe
+from frappe.tests.utils import FrappeTestCase
+
+from erpnext.accounts.party import get_default_price_list
+
+
+class PartyTestCase(FrappeTestCase):
+ def test_get_default_price_list_should_return_none_for_invalid_group(self):
+ customer = frappe.get_doc({
+ 'doctype': 'Customer',
+ 'customer_name': 'test customer',
+ }).insert(ignore_permissions=True, ignore_mandatory=True)
+ customer.customer_group = None
+ customer.save()
+ price_list = get_default_price_list(customer)
+ assert price_list is None
From 7a3d30161fae1b1d2eea9f4a6fda9ea01dd61e19 Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Tue, 8 Mar 2022 10:42:30 +0530
Subject: [PATCH 228/447] fix: translate error message titles (#30105)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
(cherry picked from commit 456ebc32f00be56cc9972cc76ac412776581060a)
Co-authored-by: Türker Tunalı
---
erpnext/stock/stock_ledger.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 69755529e5..ba1081f4dc 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -829,7 +829,7 @@ class update_entries_after(object):
if msg_list:
message = "\n\n".join(msg_list)
if self.verbose:
- frappe.throw(message, NegativeStockError, title='Insufficient Stock')
+ frappe.throw(message, NegativeStockError, title=_('Insufficient Stock'))
else:
raise NegativeStockError(message)
@@ -1157,7 +1157,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
neg_sle[0]["posting_date"], neg_sle[0]["posting_time"],
frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]))
- frappe.throw(message, NegativeStockError, title='Insufficient Stock')
+ frappe.throw(message, NegativeStockError, title=_('Insufficient Stock'))
if not args.batch_no:
@@ -1171,7 +1171,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
frappe.get_desk_link('Warehouse', args.warehouse),
neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"],
frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]))
- frappe.throw(message, NegativeStockError, title="Insufficient Stock for Batch")
+ frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
def get_future_sle_with_negative_qty(args):
From 430bf004588d4c5b759ad4ef8e95377f4473b4a4 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Tue, 8 Mar 2022 13:36:08 +0530
Subject: [PATCH 229/447] fix: add type hints for employee leave balance report
---
.../employee_leave_balance.py | 28 +++++++++----------
.../test_employee_leave_balance.py | 24 ++++++++--------
2 files changed, 26 insertions(+), 26 deletions(-)
diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
index 5c18d11721..3324ede1dd 100644
--- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
@@ -3,6 +3,7 @@
from itertools import groupby
+from typing import Dict, List, Tuple
import frappe
from frappe import _
@@ -14,10 +15,9 @@ from erpnext.hr.doctype.leave_application.leave_application import (
get_leaves_for_period,
)
+Filters = frappe._dict
-def execute(filters=None):
- filters = frappe._dict(filters or {})
-
+def execute(filters: Filters = None) -> Tuple:
if filters.to_date <= filters.from_date:
frappe.throw(_('"From Date" can not be greater than or equal to "To Date"'))
@@ -26,8 +26,9 @@ def execute(filters=None):
charts = get_chart_data(data)
return columns, data, None, charts
-def get_columns():
- columns = [{
+
+def get_columns() -> List[Dict]:
+ return [{
'label': _('Leave Type'),
'fieldtype': 'Link',
'fieldname': 'leave_type',
@@ -72,9 +73,8 @@ def get_columns():
'width': 150,
}]
- return columns
-def get_data(filters):
+def get_data(filters: Filters) -> List:
leave_types = frappe.db.get_list('Leave Type', pluck='name', order_by='name')
conditions = get_conditions(filters)
@@ -128,7 +128,7 @@ def get_data(filters):
return data
-def get_opening_balance(employee, leave_type, filters, carry_forwarded_leaves):
+def get_opening_balance(employee: str, leave_type: str, filters: Filters, carry_forwarded_leaves: float) -> float:
# allocation boundary condition
# opening balance is the closing leave balance 1 day before the filter start date
opening_balance_date = add_days(filters.from_date, -1)
@@ -146,7 +146,7 @@ def get_opening_balance(employee, leave_type, filters, carry_forwarded_leaves):
return opening_balance
-def get_conditions(filters):
+def get_conditions(filters: Filters) -> Dict:
conditions={
'status': 'Active',
}
@@ -162,7 +162,7 @@ def get_conditions(filters):
return conditions
-def get_department_leave_approver_map(department=None):
+def get_department_leave_approver_map(department: str = None):
# get current department and all its child
department_list = frappe.get_list('Department',
filters={'disabled': 0},
@@ -190,7 +190,7 @@ def get_department_leave_approver_map(department=None):
return approvers
-def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type):
+def get_allocated_and_expired_leaves(from_date: str, to_date: str, employee: str, leave_type: str) -> Tuple[float, float, float]:
new_allocation = 0
expired_leaves = 0
carry_forwarded_leaves = 0
@@ -219,7 +219,7 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type):
return new_allocation, expired_leaves, carry_forwarded_leaves
-def get_leave_ledger_entries(from_date, to_date, employee, leave_type):
+def get_leave_ledger_entries(from_date: str, to_date: str, employee: str, leave_type: str) -> List[Dict]:
ledger = frappe.qb.DocType('Leave Ledger Entry')
records = (
frappe.qb.from_(ledger)
@@ -243,7 +243,7 @@ def get_leave_ledger_entries(from_date, to_date, employee, leave_type):
return records
-def get_chart_data(data):
+def get_chart_data(data: List) -> Dict:
labels = []
datasets = []
employee_data = data
@@ -263,7 +263,7 @@ def get_chart_data(data):
return chart
-def get_dataset_for_chart(employee_data, datasets, labels):
+def get_dataset_for_chart(employee_data: List, datasets: List, labels: List) -> List:
leaves = []
employee_data = sorted(employee_data, key=lambda k: k['employee_name'])
diff --git a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py
index aecf0a4d4e..b2ed72c04d 100644
--- a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py
@@ -59,11 +59,11 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
leave_application = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type')
leave_application.reload()
- filters = {
+ filters = frappe._dict({
'from_date': allocation1.from_date,
'to_date': allocation2.to_date,
'employee': self.employee_id
- }
+ })
report = execute(filters)
@@ -93,30 +93,30 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
leave_application.reload()
# Case 1: opening balance for first alloc boundary
- filters = {
+ filters = frappe._dict({
'from_date': self.year_start,
'to_date': self.year_end,
'employee': self.employee_id
- }
+ })
report = execute(filters)
self.assertEqual(report[1][0].opening_balance, 0)
# Case 2: opening balance after leave application date
- filters = {
+ filters = frappe._dict({
'from_date': add_days(leave_application.to_date, 1),
'to_date': self.year_end,
'employee': self.employee_id
- }
+ })
report = execute(filters)
self.assertEqual(report[1][0].opening_balance, (allocation1.new_leaves_allocated - leave_application.total_leave_days))
# Case 3: leave balance shows actual balance and not consumption balance as per remaining days near alloc end date
# eg: 3 days left for alloc to end, leave balance should still be 26 and not 3
- filters = {
+ filters = frappe._dict({
'from_date': add_days(self.year_end, -3),
'to_date': self.year_end,
'employee': self.employee_id
- }
+ })
report = execute(filters)
self.assertEqual(report[1][0].opening_balance, (allocation1.new_leaves_allocated - leave_application.total_leave_days))
@@ -139,22 +139,22 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
carry_forward=True, leave_type=leave_type.name)
# Case 1: carry forwarded leaves considered in opening balance for second alloc
- filters = {
+ filters = frappe._dict({
'from_date': add_days(self.mid_year, 1),
'to_date': self.year_end,
'employee': self.employee_id
- }
+ })
report = execute(filters)
# available leaves from old alloc
opening_balance = allocation1.new_leaves_allocated - leave_application.total_leave_days
self.assertEqual(report[1][0].opening_balance, opening_balance)
# Case 2: opening balance one day after alloc boundary = carry forwarded leaves + new leaves alloc
- filters = {
+ filters = frappe._dict({
'from_date': add_days(self.mid_year, 2),
'to_date': self.year_end,
'employee': self.employee_id
- }
+ })
report = execute(filters)
# available leaves from old alloc
opening_balance = allocation2.new_leaves_allocated + (allocation1.new_leaves_allocated - leave_application.total_leave_days)
From a0dc79332df5b2a567a13a0b052bfc9bdf4e3153 Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Tue, 8 Mar 2022 14:28:34 +0530
Subject: [PATCH 230/447] fix: customer credit limit validation on update
---
erpnext/selling/doctype/customer/customer.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 7742f26ad1..634c4819e6 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -218,7 +218,9 @@ class Customer(TransactionBase):
else:
company_record.append(limit.company)
- outstanding_amt = get_customer_outstanding(self.name, limit.company)
+ outstanding_amt = get_customer_outstanding(
+ self.name, limit.company, ignore_outstanding_sales_order=limit.bypass_credit_limit_check
+ )
if flt(limit.credit_limit) < outstanding_amt:
frappe.throw(_("""New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0}""").format(outstanding_amt))
From 66f20209f657ae1825bc9971dbd19f186e454d50 Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Tue, 8 Mar 2022 15:57:54 +0530
Subject: [PATCH 231/447] perf(asset): fetch only distinct depreciable assets
---
erpnext/assets/doctype/asset/depreciation.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index 874fb630f8..6e04242210 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -23,7 +23,7 @@ def post_depreciation_entries(date=None):
frappe.db.commit()
def get_depreciable_assets(date):
- return frappe.db.sql_list("""select a.name
+ return frappe.db.sql_list("""select distinct a.name
from tabAsset a, `tabDepreciation Schedule` ds
where a.name = ds.parent and a.docstatus=1 and ds.schedule_date<=%s and a.calculate_depreciation = 1
and a.status in ('Submitted', 'Partially Depreciated')
From ce27cdb121e523c00ee0846a9715d72e3efed694 Mon Sep 17 00:00:00 2001
From: Chillar Anand
Date: Tue, 8 Mar 2022 23:07:23 +0530
Subject: [PATCH 232/447] test: Fix flaky tests (#30107)
---
erpnext/hr/doctype/attendance/test_attendance.py | 15 ++++++++-------
.../leave_application/test_leave_application.py | 2 +-
.../test_project_profitability.py | 8 ++------
3 files changed, 11 insertions(+), 14 deletions(-)
diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py
index 118cc987ef..c74967d213 100644
--- a/erpnext/hr/doctype/attendance/test_attendance.py
+++ b/erpnext/hr/doctype/attendance/test_attendance.py
@@ -1,10 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt
-import unittest
-
import frappe
-from frappe.utils import add_days, get_first_day, getdate, nowdate
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_days, get_first_day, getdate, now_datetime, nowdate
from erpnext.hr.doctype.attendance.attendance import (
get_month_map,
@@ -16,7 +15,7 @@ from erpnext.hr.doctype.leave_application.test_leave_application import get_firs
test_records = frappe.get_test_records('Attendance')
-class TestAttendance(unittest.TestCase):
+class TestAttendance(FrappeTestCase):
def test_mark_absent(self):
employee = make_employee("test_mark_absent@example.com")
date = nowdate()
@@ -74,12 +73,14 @@ class TestAttendance(unittest.TestCase):
self.assertNotIn(first_sunday, unmarked_days)
def test_unmarked_days_as_per_joining_and_relieving_dates(self):
- first_day = get_first_day(getdate())
+ now = now_datetime()
+ previous_month = now.month - 1
+ first_day = now.replace(day=1).replace(month=previous_month).date()
doj = add_days(first_day, 1)
relieving_date = add_days(first_day, 5)
employee = make_employee('test_unmarked_days_as_per_doj@example.com', date_of_joining=doj,
- date_of_relieving=relieving_date)
+ relieving_date=relieving_date)
frappe.db.delete('Attendance', {'employee': employee})
attendance_date = add_days(first_day, 2)
@@ -104,4 +105,4 @@ def get_month_name(date):
month_number = date.month
for month, number in get_month_map().items():
if number == month_number:
- return month
\ No newline at end of file
+ return month
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 6d27f4abef..7b3aa49729 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -792,4 +792,4 @@ def get_first_sunday(holiday_list):
order by holiday_date
""", (holiday_list, month_start_date, month_end_date))[0][0]
- return first_sunday
\ No newline at end of file
+ return first_sunday
diff --git a/erpnext/projects/report/project_profitability/test_project_profitability.py b/erpnext/projects/report/project_profitability/test_project_profitability.py
index 1eb3d0d717..3ca28c1ce0 100644
--- a/erpnext/projects/report/project_profitability/test_project_profitability.py
+++ b/erpnext/projects/report/project_profitability/test_project_profitability.py
@@ -1,6 +1,5 @@
-import unittest
-
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
@@ -12,7 +11,7 @@ from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_
from erpnext.projects.report.project_profitability.project_profitability import execute
-class TestProjectProfitability(unittest.TestCase):
+class TestProjectProfitability(FrappeTestCase):
def setUp(self):
frappe.db.sql('delete from `tabTimesheet`')
emp = make_employee('test_employee_9@salary.com', company='_Test Company')
@@ -67,6 +66,3 @@ class TestProjectProfitability(unittest.TestCase):
fractional_cost = self.salary_slip.base_gross_pay * utilization
self.assertEqual(fractional_cost, row.fractional_cost)
-
- def tearDown(self):
- frappe.db.rollback()
From 096092f1731d5a8025462317384c7d19772f024c Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Tue, 8 Mar 2022 23:16:40 +0530
Subject: [PATCH 233/447] fix: leave allocation records query (#30118)
---
.../leave_application/leave_application.py | 54 +++++++++++--------
1 file changed, 33 insertions(+), 21 deletions(-)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 70250f5bcf..ef5f4bcb0f 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -4,6 +4,7 @@
import frappe
from frappe import _
+from frappe.query_builder.functions import Max, Min, Sum
from frappe.utils import (
add_days,
cint,
@@ -567,28 +568,39 @@ def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_
return get_remaining_leaves(allocation, leaves_taken, date, expiry)
def get_leave_allocation_records(employee, date, leave_type=None):
- ''' returns the total allocated leaves and carry forwarded leaves based on ledger entries '''
+ """Returns the total allocated leaves and carry forwarded leaves based on ledger entries"""
+ Ledger = frappe.qb.DocType("Leave Ledger Entry")
- conditions = ("and leave_type='%s'" % leave_type) if leave_type else ""
- allocation_details = frappe.db.sql("""
- SELECT
- SUM(CASE WHEN is_carry_forward = 1 THEN leaves ELSE 0 END) as cf_leaves,
- SUM(CASE WHEN is_carry_forward = 0 THEN leaves ELSE 0 END) as new_leaves,
- MIN(from_date) as from_date,
- MAX(to_date) as to_date,
- leave_type
- FROM `tabLeave Ledger Entry`
- WHERE
- from_date <= %(date)s
- AND to_date >= %(date)s
- AND docstatus=1
- AND transaction_type="Leave Allocation"
- AND employee=%(employee)s
- AND is_expired=0
- AND is_lwp=0
- {0}
- GROUP BY employee, leave_type
- """.format(conditions), dict(date=date, employee=employee), as_dict=1) #nosec
+ cf_leave_case = frappe.qb.terms.Case().when(Ledger.is_carry_forward == "1", Ledger.leaves).else_(0)
+ sum_cf_leaves = Sum(cf_leave_case).as_("cf_leaves")
+
+ new_leaves_case = frappe.qb.terms.Case().when(Ledger.is_carry_forward == "0", Ledger.leaves).else_(0)
+ sum_new_leaves = Sum(new_leaves_case).as_("new_leaves")
+
+ query = (
+ frappe.qb.from_(Ledger)
+ .select(
+ sum_cf_leaves,
+ sum_new_leaves,
+ Min(Ledger.from_date).as_("from_date"),
+ Max(Ledger.to_date).as_("to_date"),
+ Ledger.leave_type
+ ).where(
+ (Ledger.from_date <= date)
+ & (Ledger.to_date >= date)
+ & (Ledger.docstatus == 1)
+ & (Ledger.transaction_type == "Leave Allocation")
+ & (Ledger.employee == employee)
+ & (Ledger.is_expired == 0)
+ & (Ledger.is_lwp == 0)
+ )
+ )
+
+ if leave_type:
+ query = query.where((Ledger.leave_type == leave_type))
+ query = query.groupby(Ledger.employee, Ledger.leave_type)
+
+ allocation_details = query.run(as_dict=True)
allocated_leaves = frappe._dict()
for d in allocation_details:
From c0f1e269e4982052b265cb209f5b9cea7ca5eded Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Wed, 9 Mar 2022 10:25:49 +0530
Subject: [PATCH 234/447] feat: split ledger entries for applications created
across allocations
- fix: ledger entry for expiring cf leaves not considering holidays
---
.../leave_application/leave_application.py | 139 +++++++++++++-----
1 file changed, 101 insertions(+), 38 deletions(-)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index f98994511a..4c0945665c 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -137,21 +137,36 @@ class LeaveApplication(Document):
def validate_dates_across_allocation(self):
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
return
- def _get_leave_allocation_record(date):
- allocation = frappe.db.sql("""select name from `tabLeave Allocation`
- where employee=%s and leave_type=%s and docstatus=1
- and %s between from_date and to_date""", (self.employee, self.leave_type, date))
- return allocation and allocation[0][0]
+ alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates()
+
+ if not (alloc_on_from_date or alloc_on_to_date):
+ frappe.throw(_("Application period cannot be outside leave allocation period"))
+
+ elif alloc_on_from_date.name != alloc_on_to_date.name:
+ frappe.throw(_("Application period cannot be across two allocation records"))
+
+ def get_allocation_based_on_application_dates(self):
+ """Returns allocation name, from and to dates for application dates"""
+ def _get_leave_allocation_record(date):
+ LeaveAllocation = frappe.qb.DocType("Leave Allocation")
+ allocation = (
+ frappe.qb.from_(LeaveAllocation)
+ .select(LeaveAllocation.name, LeaveAllocation.from_date, LeaveAllocation.to_date)
+ .where(
+ (LeaveAllocation.employee == self.employee)
+ & (LeaveAllocation.leave_type == self.leave_type)
+ & (LeaveAllocation.docstatus == 1)
+ & ((date >= LeaveAllocation.from_date) & (date <= LeaveAllocation.to_date))
+ )
+ ).run(as_dict=True)
+
+ return allocation and allocation[0]
allocation_based_on_from_date = _get_leave_allocation_record(self.from_date)
allocation_based_on_to_date = _get_leave_allocation_record(self.to_date)
- if not (allocation_based_on_from_date or allocation_based_on_to_date):
- frappe.throw(_("Application period cannot be outside leave allocation period"))
-
- elif allocation_based_on_from_date != allocation_based_on_to_date:
- frappe.throw(_("Application period cannot be across two allocation records"))
+ return allocation_based_on_from_date, allocation_based_on_to_date
def validate_back_dated_application(self):
future_allocation = frappe.db.sql("""select name, from_date from `tabLeave Allocation`
@@ -433,49 +448,97 @@ class LeaveApplication(Document):
expiry_date = get_allocation_expiry_for_cf_leaves(self.employee, self.leave_type,
self.to_date, self.from_date)
-
lwp = frappe.db.get_value("Leave Type", self.leave_type, "is_lwp")
if expiry_date:
self.create_ledger_entry_for_intermediate_allocation_expiry(expiry_date, submit, lwp)
else:
- raise_exception = True
- if frappe.flags.in_patch:
- raise_exception=False
+ alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates()
+ if self.is_separate_ledger_entry_required(alloc_on_from_date, alloc_on_to_date):
+ # required only if negative balance is allowed for leave type
+ # else will be stopped in validation itself
+ self.create_separate_ledger_entries(alloc_on_from_date, alloc_on_to_date, submit, lwp)
+ else:
+ raise_exception = False if frappe.flags.in_patch else True
+ args = dict(
+ leaves=self.total_leave_days * -1,
+ from_date=self.from_date,
+ to_date=self.to_date,
+ is_lwp=lwp,
+ holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
+ )
+ create_leave_ledger_entry(self, args, submit)
- args = dict(
- leaves=self.total_leave_days * -1,
+ def is_separate_ledger_entry_required(self, alloc_on_from_date=None, alloc_on_to_date=None) -> bool:
+ if ((alloc_on_from_date and not alloc_on_to_date)
+ or (not alloc_on_from_date and alloc_on_to_date)
+ or (alloc_on_from_date and alloc_on_to_date and alloc_on_from_date.name != alloc_on_to_date.name)):
+ return True
+ return False
+
+ def create_separate_ledger_entries(self, alloc_on_from_date, alloc_on_to_date, submit, lwp):
+ """Creates separate ledger entries for application period falling into separate allocations"""
+ # for creating separate ledger entries existing allocation periods should be consecutive
+ if submit and alloc_on_from_date and alloc_on_to_date and add_days(alloc_on_from_date.to_date, 1) != alloc_on_to_date.from_date:
+ frappe.throw(_("Leave Application period cannot be across two non-consecutive leave allocations {0} and {1}.").format(
+ get_link_to_form("Leave Allocation", alloc_on_from_date.name), get_link_to_form("Leave Allocation", alloc_on_to_date)))
+
+ raise_exception = False if frappe.flags.in_patch else True
+ leaves_in_first_alloc = get_number_of_leave_days(self.employee, self.leave_type,
+ self.from_date, alloc_on_from_date.to_date, self.half_day, self.half_day_date)
+ leaves_in_second_alloc = get_number_of_leave_days(self.employee, self.leave_type,
+ add_days(alloc_on_from_date.to_date, 1), self.to_date, self.half_day, self.half_day_date)
+
+ args = dict(
+ is_lwp=lwp,
+ holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
+ )
+
+ if leaves_in_first_alloc:
+ args.update(dict(
from_date=self.from_date,
+ to_date=alloc_on_from_date.to_date,
+ leaves=leaves_in_first_alloc * -1
+ ))
+ create_leave_ledger_entry(self, args, submit)
+
+ if leaves_in_second_alloc:
+ args.update(dict(
+ from_date=add_days(alloc_on_from_date.to_date, 1),
to_date=self.to_date,
+ leaves=leaves_in_second_alloc * -1
+ ))
+ create_leave_ledger_entry(self, args, submit)
+
+ def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, submit, lwp):
+ """Splits leave application into two ledger entries to consider expiry of allocation"""
+ raise_exception = False if frappe.flags.in_patch else True
+
+ leaves = get_number_of_leave_days(self.employee, self.leave_type,
+ self.from_date, expiry_date, self.half_day, self.half_day_date)
+
+ if leaves:
+ args = dict(
+ from_date=self.from_date,
+ to_date=expiry_date,
+ leaves=leaves * -1,
is_lwp=lwp,
holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
)
create_leave_ledger_entry(self, args, submit)
- def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, submit, lwp):
- ''' splits leave application into two ledger entries to consider expiry of allocation '''
-
- raise_exception = True
- if frappe.flags.in_patch:
- raise_exception=False
-
- args = dict(
- from_date=self.from_date,
- to_date=expiry_date,
- leaves=(date_diff(expiry_date, self.from_date) + 1) * -1,
- is_lwp=lwp,
- holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
- )
- create_leave_ledger_entry(self, args, submit)
-
if getdate(expiry_date) != getdate(self.to_date):
start_date = add_days(expiry_date, 1)
- args.update(dict(
- from_date=start_date,
- to_date=self.to_date,
- leaves=date_diff(self.to_date, expiry_date) * -1
- ))
- create_leave_ledger_entry(self, args, submit)
+ leaves = get_number_of_leave_days(self.employee, self.leave_type,
+ start_date, self.to_date, self.half_day, self.half_day_date)
+
+ if leaves:
+ args.update(dict(
+ from_date=start_date,
+ to_date=self.to_date,
+ leaves=leaves * -1
+ ))
+ create_leave_ledger_entry(self, args, submit)
def get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, from_date):
From aadca02018f7c3bc0011c4f31b72fa6a7bedf459 Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Wed, 9 Mar 2022 11:27:15 +0530
Subject: [PATCH 235/447] fix: add filters in default_discount_account (#30095)
(#30125)
(cherry picked from commit 3d8eaa5392fcc420820ac86eb01a6cf67cec1cc7)
Co-authored-by: Mohamed-D-Ouf <65343412+Mohamed-D-Ouf@users.noreply.github.com>
---
erpnext/setup/doctype/item_group/item_group.js | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/erpnext/setup/doctype/item_group/item_group.js b/erpnext/setup/doctype/item_group/item_group.js
index 885d874720..f570c2faec 100644
--- a/erpnext/setup/doctype/item_group/item_group.js
+++ b/erpnext/setup/doctype/item_group/item_group.js
@@ -14,6 +14,16 @@ frappe.ui.form.on("Item Group", {
]
}
}
+ frm.fields_dict['item_group_defaults'].grid.get_field("default_discount_account").get_query = function(doc, cdt, cdn) {
+ const row = locals[cdt][cdn];
+ return {
+ filters: {
+ 'report_type': 'Profit and Loss',
+ 'company': row.company,
+ "is_group": 0
+ }
+ };
+ }
frm.fields_dict["item_group_defaults"].grid.get_field("expense_account").get_query = function(doc, cdt, cdn) {
const row = locals[cdt][cdn];
return {
From 700e65959a45aabd91be63d49e4b9aaf13af81f4 Mon Sep 17 00:00:00 2001
From: Mohammed Yusuf Shaikh
<49878143+mohammedyusufshaikh@users.noreply.github.com>
Date: Wed, 9 Mar 2022 11:42:50 +0530
Subject: [PATCH 236/447] fix: default log date needed to query records
(#29951)
---
.../bulk_transaction_log/bulk_transaction_log.js | 10 +++-------
.../bulk_transaction_log/bulk_transaction_log.py | 6 +++---
2 files changed, 6 insertions(+), 10 deletions(-)
diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js
index a739cc3730..0073170a85 100644
--- a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js
+++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js
@@ -3,15 +3,11 @@
frappe.ui.form.on('Bulk Transaction Log', {
- before_load: function(frm) {
- query(frm);
- },
-
refresh: function(frm) {
frm.disable_save();
frm.add_custom_button(__('Retry Failed Transactions'), ()=>{
frappe.confirm(__("Retry Failing Transactions ?"), ()=>{
- query(frm);
+ query(frm, 1);
}
);
});
@@ -25,8 +21,8 @@ function query(frm) {
log_date: frm.doc.log_date
}
}).then((r) => {
- if (r.message) {
- frm.remove_custom_button("Retry Failed Transactions");
+ if (r.message === "No Failed Records") {
+ frappe.show_alert(__(r.message), 5);
} else {
frappe.show_alert(__("Retrying Failed Transactions"), 5);
}
diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py
index de7cde5a6d..92f37f5667 100644
--- a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py
+++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py
@@ -15,6 +15,8 @@ class BulkTransactionLog(Document):
@frappe.whitelist()
def retry_failing_transaction(log_date=None):
+ if not log_date:
+ log_date = str(date.today())
btp = frappe.qb.DocType("Bulk Transaction Log Detail")
data = (
frappe.qb.from_(btp)
@@ -25,9 +27,7 @@ def retry_failing_transaction(log_date=None):
.where(btp.date == log_date)
).run(as_dict=True)
- if data:
- if not log_date:
- log_date = str(date.today())
+ if data :
if len(data) > 10:
frappe.enqueue(job, queue="long", job_name="bulk_retry", data=data, log_date=log_date)
else:
From a504ffcc4ccc1a445814c45f52a234238a8538e5 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Wed, 9 Mar 2022 13:49:07 +0530
Subject: [PATCH 237/447] fix: clearer validation/warning messages for
insufficient balance in leave application
---
.../leave_application/leave_application.py | 32 ++++++++++++++-----
1 file changed, 24 insertions(+), 8 deletions(-)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 4c0945665c..369847fd2f 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -142,8 +142,7 @@ class LeaveApplication(Document):
if not (alloc_on_from_date or alloc_on_to_date):
frappe.throw(_("Application period cannot be outside leave allocation period"))
-
- elif alloc_on_from_date.name != alloc_on_to_date.name:
+ elif self.is_separate_ledger_entry_required(alloc_on_from_date, alloc_on_to_date):
frappe.throw(_("Application period cannot be across two allocation records"))
def get_allocation_based_on_application_dates(self):
@@ -284,12 +283,28 @@ class LeaveApplication(Document):
leave_balance_for_consumption = leave_balance.get("leave_balance_for_consumption")
if self.status != "Rejected" and (leave_balance_for_consumption < self.total_leave_days or not leave_balance_for_consumption):
- if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
- frappe.msgprint(_("Insufficient leave balance for Leave Type {0}")
- .format(frappe.bold(self.leave_type)), title=_("Warning"), indicator="orange")
- else:
- frappe.throw(_("Insufficient leave balance for Leave Type {0}")
- .format(self.leave_type), InsufficientLeaveBalanceError, title=_("Insufficient Balance"))
+ self.show_insufficient_balance_message(leave_balance_for_consumption)
+
+ def show_insufficient_balance_message(self, leave_balance_for_consumption):
+ alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates()
+
+ if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
+ if leave_balance_for_consumption != self.leave_balance:
+ msg = _("Warning: Insufficient leave balance for Leave Type {0} in this allocation.").format(frappe.bold(self.leave_type))
+ msg += " "
+ msg += _("Actual leave balance is {0} but only {1} leave(s) can be consumed between {2} (Application Date) and {3} (Allocation Expiry).").format(
+ frappe.bold(self.leave_balance), frappe.bold(leave_balance_for_consumption),
+ frappe.bold(formatdate(self.from_date)),
+ frappe.bold(formatdate(alloc_on_from_date.to_date)))
+ msg += " "
+ msg += _("Remaining leaves would be compensated in the next allocation.")
+ else:
+ msg = _("Warning: Insufficient leave balance for Leave Type {0}.").format(frappe.bold(self.leave_type))
+
+ frappe.msgprint(msg, title=_("Warning"), indicator="orange")
+ else:
+ frappe.throw(_("Insufficient leave balance for Leave Type {0}").format(frappe.bold(self.leave_type)),
+ exc=InsufficientLeaveBalanceError, title=_("Insufficient Balance"))
def validate_leave_overlap(self):
if not self.name:
@@ -470,6 +485,7 @@ class LeaveApplication(Document):
create_leave_ledger_entry(self, args, submit)
def is_separate_ledger_entry_required(self, alloc_on_from_date=None, alloc_on_to_date=None) -> bool:
+ """Checks if application dates fall in separate allocations"""
if ((alloc_on_from_date and not alloc_on_to_date)
or (not alloc_on_from_date and alloc_on_to_date)
or (alloc_on_from_date and alloc_on_to_date and alloc_on_from_date.name != alloc_on_to_date.name)):
From 3507cf59852c6d6814f6650b4b1a6e6584e69aa6 Mon Sep 17 00:00:00 2001
From: marination
Date: Wed, 9 Mar 2022 12:24:57 +0530
Subject: [PATCH 238/447] test: Test include_descendants in Item Group Product
Listing
- Also made include_descendants field's visibility dependant on show_in_website
---
.../test_item_group_product_data_engine.py | 53 ++++++++++++++++---
.../setup/doctype/item_group/item_group.json | 3 +-
.../templates/pages/non_profit/__init__.py | 0
3 files changed, 49 insertions(+), 7 deletions(-)
create mode 100644 erpnext/templates/pages/non_profit/__init__.py
diff --git a/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py b/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py
index f0f7918d00..6549ba692a 100644
--- a/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py
+++ b/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py
@@ -13,8 +13,7 @@ test_dependencies = ["Item", "Item Group"]
class TestItemGroupProductDataEngine(unittest.TestCase):
"Test Products & Sub-Category Querying for Product Listing on Item Group Page."
- @classmethod
- def setUpClass(cls):
+ def setUp(self):
item_codes = [
("Test Mobile A", "_Test Item Group B"),
("Test Mobile B", "_Test Item Group B"),
@@ -28,8 +27,10 @@ class TestItemGroupProductDataEngine(unittest.TestCase):
if not frappe.db.exists("Website Item", {"item_code": item_code}):
create_regular_web_item(item_code, item_args=item_args)
- @classmethod
- def tearDownClass(cls):
+ frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1)
+ frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 1)
+
+ def tearDown(self):
frappe.db.rollback()
def test_product_listing_in_item_group(self):
@@ -87,7 +88,6 @@ class TestItemGroupProductDataEngine(unittest.TestCase):
def test_item_group_with_sub_groups(self):
"Test Valid Sub Item Groups in Item Group Page."
- frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1)
frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 0)
result = get_product_filter_data(query_args={
@@ -114,4 +114,45 @@ class TestItemGroupProductDataEngine(unittest.TestCase):
# check if child group is fetched if shown in website
self.assertIn("_Test Item Group B - 1", child_groups)
- self.assertIn("_Test Item Group B - 2", child_groups)
\ No newline at end of file
+ self.assertIn("_Test Item Group B - 2", child_groups)
+
+ def test_item_group_page_with_descendants_included(self):
+ """
+ Test if 'include_descendants' pulls Items belonging to descendant Item Groups (Level 2 & 3).
+ > _Test Item Group B [Level 1]
+ > _Test Item Group B - 1 [Level 2]
+ > _Test Item Group B - 1 - 1 [Level 3]
+ """
+ frappe.get_doc({ # create Level 3 nested child group
+ "doctype": "Item Group",
+ "is_group": 1,
+ "item_group_name": "_Test Item Group B - 1 - 1",
+ "parent_item_group": "_Test Item Group B - 1"
+ }).insert()
+
+ create_regular_web_item( # create an item belonging to level 3 item group
+ "Test Mobile F",
+ item_args={"item_group": "_Test Item Group B - 1 - 1"}
+ )
+
+ frappe.db.set_value("Item Group", "_Test Item Group B - 1 - 1", "show_in_website", 1)
+
+ # enable 'include descendants' in Level 1
+ frappe.db.set_value("Item Group", "_Test Item Group B", "include_descendants", 1)
+
+ result = get_product_filter_data(query_args={
+ "field_filters": {},
+ "attribute_filters": {},
+ "start": 0,
+ "item_group": "_Test Item Group B"
+ })
+
+ items = result.get("items")
+ item_codes = [item.get("item_code") for item in items]
+
+ # check if all sub groups' items are pulled
+ self.assertEqual(len(items), 6)
+ self.assertIn("Test Mobile A", item_codes)
+ self.assertIn("Test Mobile C", item_codes)
+ self.assertIn("Test Mobile E", item_codes)
+ self.assertIn("Test Mobile F", item_codes)
\ No newline at end of file
diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json
index a090c8d76c..50f923d87e 100644
--- a/erpnext/setup/doctype/item_group/item_group.json
+++ b/erpnext/setup/doctype/item_group/item_group.json
@@ -219,6 +219,7 @@
},
{
"default": "0",
+ "depends_on": "show_in_website",
"description": "Include Website Items belonging to child Item Groups",
"fieldname": "include_descendants",
"fieldtype": "Check",
@@ -231,7 +232,7 @@
"is_tree": 1,
"links": [],
"max_attachments": 3,
- "modified": "2022-03-07 09:44:47.561532",
+ "modified": "2022-03-09 12:27:11.055782",
"modified_by": "Administrator",
"module": "Setup",
"name": "Item Group",
diff --git a/erpnext/templates/pages/non_profit/__init__.py b/erpnext/templates/pages/non_profit/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
From 64905188c46b90452e30927323f892d8b210f14a Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 9 Mar 2022 15:37:14 +0530
Subject: [PATCH 239/447] fix: dont fetch entire barcode table in
get_item_details (#30131)
---
erpnext/stock/get_item_details.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 9bec5f7494..9bb41b9dbb 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -367,7 +367,7 @@ def get_basic_details(args, item, overwrite_warehouse=True):
if not out[d[1]]:
out[d[1]] = frappe.get_cached_value('Company', args.company, d[2]) if d[2] else None
- for fieldname in ("item_name", "item_group", "barcodes", "brand", "stock_uom"):
+ for fieldname in ("item_name", "item_group", "brand", "stock_uom"):
out[fieldname] = item.get(fieldname)
if args.get("manufacturer"):
From 8a2fe7a2e39c28ccb52238651b439eba17d153ab Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Wed, 9 Mar 2022 15:42:06 +0530
Subject: [PATCH 240/447] fix: Remove tax invoice no field
---
erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 7 -------
1 file changed, 7 deletions(-)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 973c8371ea..82854ba2a6 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -9,7 +9,6 @@
"customer_section",
"title",
"naming_series",
- "tax_invoice_number",
"customer",
"customer_name",
"tax_id",
@@ -2027,12 +2026,6 @@
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"read_only": 1
- },
- {
- "fieldname": "tax_invoice_number",
- "fieldtype": "Data",
- "label": "Tax Invoice Number",
- "read_only": 1
}
],
"icon": "fa fa-file-text",
From 9b8258479c6e71a06303d4774df5ab3a749d9de9 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Wed, 9 Mar 2022 15:43:26 +0530
Subject: [PATCH 241/447] fix: Update timestamp
---
erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 82854ba2a6..80b95db886 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -2038,7 +2038,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2022-03-07 16:08:53.517903",
+ "modified": "2022-03-08 16:08:53.517903",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
From 6755d6e6f5bb17d5cad58f3bf064796239879211 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Wed, 9 Mar 2022 15:48:57 +0530
Subject: [PATCH 242/447] test: leave application validations
---
.../leave_application/leave_application.py | 4 +-
.../test_leave_application.py | 94 ++++++++++++++++++-
2 files changed, 96 insertions(+), 2 deletions(-)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 369847fd2f..d4062c7640 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -37,6 +37,8 @@ class AttendanceAlreadyMarkedError(frappe.ValidationError): pass
class NotAnOptionalHoliday(frappe.ValidationError): pass
class InsufficientLeaveBalanceError(frappe.ValidationError):
pass
+class LeaveAcrossAllocationsError(frappe.ValidationError):
+ pass
from frappe.model.document import Document
@@ -143,7 +145,7 @@ class LeaveApplication(Document):
if not (alloc_on_from_date or alloc_on_to_date):
frappe.throw(_("Application period cannot be outside leave allocation period"))
elif self.is_separate_ledger_entry_required(alloc_on_from_date, alloc_on_to_date):
- frappe.throw(_("Application period cannot be across two allocation records"))
+ frappe.throw(_("Application period cannot be across two allocation records"), exc=LeaveAcrossAllocationsError)
def get_allocation_based_on_application_dates(self):
"""Returns allocation name, from and to dates for application dates"""
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 287cb5cf94..7cc919e61c 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -20,6 +20,8 @@ from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
from erpnext.hr.doctype.leave_application.leave_application import (
+ InsufficientLeaveBalanceError,
+ LeaveAcrossAllocationsError,
LeaveDayBlockedError,
NotAnOptionalHoliday,
OverlapError,
@@ -82,8 +84,16 @@ class TestLeaveApplication(unittest.TestCase):
frappe.db.delete("Attendance", {"employee": "_T-Employee-00001"})
self.holiday_list = make_holiday_list()
+ if not frappe.db.exists("Leave Type", "_Test Leave Type"):
+ frappe.get_doc(dict(
+ leave_type_name="_Test Leave Type",
+ doctype="Leave Type",
+ include_holiday=True
+ )).insert()
+
def tearDown(self):
frappe.db.rollback()
+ frappe.set_user("Administrator")
def _clear_roles(self):
frappe.db.sql("""delete from `tabHas Role` where parent in
@@ -98,6 +108,81 @@ class TestLeaveApplication(unittest.TestCase):
application.to_date = "2013-01-05"
return application
+ @set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
+ def test_validate_application_across_allocations(self):
+ # Test validation for application dates when negative balance is disabled
+ frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1)
+ leave_type = frappe.get_doc(dict(
+ leave_type_name="Test Leave Validation",
+ doctype="Leave Type",
+ allow_negative=False
+ )).insert()
+
+ employee = get_employee()
+ date = getdate()
+ first_sunday = get_first_sunday(self.holiday_list, for_date=get_year_start(date))
+
+ leave_application = frappe.get_doc(dict(
+ doctype='Leave Application',
+ employee=employee.name,
+ leave_type=leave_type.name,
+ from_date=add_days(first_sunday, 1),
+ to_date=add_days(first_sunday, 4),
+ company="_Test Company",
+ status="Approved",
+ leave_approver = 'test@example.com'
+ ))
+ # Application period cannot be outside leave allocation period
+ self.assertRaises(frappe.ValidationError, leave_application.insert)
+
+ make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
+
+ leave_application = frappe.get_doc(dict(
+ doctype='Leave Application',
+ employee=employee.name,
+ leave_type=leave_type.name,
+ from_date=add_days(first_sunday, -10),
+ to_date=add_days(first_sunday, 1),
+ company="_Test Company",
+ status="Approved",
+ leave_approver = 'test@example.com'
+ ))
+
+ # Application period cannot be across two allocation records
+ self.assertRaises(LeaveAcrossAllocationsError, leave_application.insert)
+
+ @set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
+ def test_insufficient_leave_balance_validation(self):
+ # CASE 1: Validation when allow negative is disabled
+ frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1)
+ leave_type = frappe.get_doc(dict(
+ leave_type_name="Test Leave Validation",
+ doctype="Leave Type",
+ allow_negative=False
+ )).insert()
+
+ employee = get_employee()
+ date = getdate()
+ first_sunday = get_first_sunday(self.holiday_list, for_date=get_year_start(date))
+
+ # allocate 2 leaves, apply for more
+ make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date), leaves=2)
+ leave_application = frappe.get_doc(dict(
+ doctype='Leave Application',
+ employee=employee.name,
+ leave_type=leave_type.name,
+ from_date=add_days(first_sunday, 1),
+ to_date=add_days(first_sunday, 3),
+ company="_Test Company",
+ status="Approved",
+ leave_approver = 'test@example.com'
+ ))
+ self.assertRaises(InsufficientLeaveBalanceError, leave_application.insert)
+
+ # CASE 2: Allows creating application with a warning message when allow negative is enabled
+ frappe.db.set_value("Leave Type", "Test Leave Validation", "allow_negative", True)
+ make_leave_application(employee.name, add_days(first_sunday, 1), add_days(first_sunday, 3), leave_type.name)
+
def test_overwrite_attendance(self):
'''check attendance is automatically created on leave approval'''
make_allocation_record()
@@ -562,7 +647,14 @@ class TestLeaveApplication(unittest.TestCase):
# test to not consider current leave in leave balance while submitting
def test_current_leave_on_submit(self):
employee = get_employee()
- leave_type = 'Sick leave'
+
+ leave_type = 'Sick Leave'
+ if not frappe.db.exists('Leave Type', leave_type):
+ frappe.get_doc(dict(
+ leave_type_name=leave_type,
+ doctype='Leave Type'
+ )).insert()
+
allocation = frappe.get_doc(dict(
doctype = 'Leave Allocation',
employee = employee.name,
From 7b37a74023b088b8dcc5114b954c716ebf7f6eae Mon Sep 17 00:00:00 2001
From: marination
Date: Wed, 9 Mar 2022 16:04:12 +0530
Subject: [PATCH 243/447] fix: Linter
---
erpnext/assets/doctype/asset/asset_dashboard.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/erpnext/assets/doctype/asset/asset_dashboard.py b/erpnext/assets/doctype/asset/asset_dashboard.py
index 1833b0e716..c81b611a41 100644
--- a/erpnext/assets/doctype/asset/asset_dashboard.py
+++ b/erpnext/assets/doctype/asset/asset_dashboard.py
@@ -1,5 +1,6 @@
from frappe import _
+
def get_data():
return {
'non_standard_fieldnames': {
From 70239158b976ede73e55cc2d34f622752c020666 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Wed, 9 Mar 2022 16:18:23 +0530
Subject: [PATCH 244/447] fix: boundary determination for separate ledger
entries
---
.../leave_application/leave_application.py | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index d4062c7640..144dcc68e2 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -502,10 +502,18 @@ class LeaveApplication(Document):
get_link_to_form("Leave Allocation", alloc_on_from_date.name), get_link_to_form("Leave Allocation", alloc_on_to_date)))
raise_exception = False if frappe.flags.in_patch else True
+
+ if alloc_on_from_date:
+ first_alloc_end = alloc_on_from_date.to_date
+ second_alloc_start = add_days(alloc_on_from_date.to_date, 1)
+ else:
+ first_alloc_end = add_days(alloc_on_to_date.from_date, -1)
+ second_alloc_start = alloc_on_to_date.from_date
+
leaves_in_first_alloc = get_number_of_leave_days(self.employee, self.leave_type,
- self.from_date, alloc_on_from_date.to_date, self.half_day, self.half_day_date)
+ self.from_date, first_alloc_end, self.half_day, self.half_day_date)
leaves_in_second_alloc = get_number_of_leave_days(self.employee, self.leave_type,
- add_days(alloc_on_from_date.to_date, 1), self.to_date, self.half_day, self.half_day_date)
+ second_alloc_start, self.to_date, self.half_day, self.half_day_date)
args = dict(
is_lwp=lwp,
@@ -515,14 +523,14 @@ class LeaveApplication(Document):
if leaves_in_first_alloc:
args.update(dict(
from_date=self.from_date,
- to_date=alloc_on_from_date.to_date,
+ to_date=first_alloc_end,
leaves=leaves_in_first_alloc * -1
))
create_leave_ledger_entry(self, args, submit)
if leaves_in_second_alloc:
args.update(dict(
- from_date=add_days(alloc_on_from_date.to_date, 1),
+ from_date=second_alloc_start,
to_date=self.to_date,
leaves=leaves_in_second_alloc * -1
))
From 97b7b5012e911f7095810f29bfc328c02e6e34a1 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Wed, 9 Mar 2022 16:35:25 +0530
Subject: [PATCH 245/447] test: separate leave ledger entries for leave
applications across allocations
---
.../test_leave_application.py | 51 +++++++++++++++++++
1 file changed, 51 insertions(+)
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 7cc919e61c..17a83eb75e 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -183,6 +183,57 @@ class TestLeaveApplication(unittest.TestCase):
frappe.db.set_value("Leave Type", "Test Leave Validation", "allow_negative", True)
make_leave_application(employee.name, add_days(first_sunday, 1), add_days(first_sunday, 3), leave_type.name)
+ @set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
+ def test_separate_leave_ledger_entry_for_boundary_applications(self):
+ # When application falls in 2 different allocations and Allow Negative is enabled
+ # creates separate leave ledger entries
+ frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1)
+ leave_type = frappe.get_doc(dict(
+ leave_type_name="Test Leave Validation",
+ doctype="Leave Type",
+ allow_negative=True
+ )).insert()
+
+ employee = get_employee()
+ date = getdate()
+ year_start = getdate(get_year_start(date))
+ year_end = getdate(get_year_ending(date))
+
+ make_allocation_record(leave_type=leave_type.name, from_date=year_start, to_date=year_end)
+ # application across allocations
+
+ # CASE 1: from date has no allocation, to date has an allocation / both dates have allocation
+ application = make_leave_application(employee.name, add_days(year_start, -10), add_days(year_start, 3), leave_type.name)
+
+ # 2 separate leave ledger entries
+ ledgers = frappe.db.get_all("Leave Ledger Entry", {
+ "transaction_type": "Leave Application",
+ "transaction_name": application.name
+ }, ["leaves", "from_date", "to_date"], order_by="from_date")
+ self.assertEqual(len(ledgers), 2)
+
+ self.assertEqual(ledgers[0].from_date, application.from_date)
+ self.assertEqual(ledgers[0].to_date, add_days(year_start, -1))
+
+ self.assertEqual(ledgers[1].from_date, year_start)
+ self.assertEqual(ledgers[1].to_date, application.to_date)
+
+ # CASE 2: from date has an allocation, to date has no allocation
+ application = make_leave_application(employee.name, add_days(year_end, -3), add_days(year_end, 5), leave_type.name)
+
+ # 2 separate leave ledger entries
+ ledgers = frappe.db.get_all("Leave Ledger Entry", {
+ "transaction_type": "Leave Application",
+ "transaction_name": application.name
+ }, ["leaves", "from_date", "to_date"], order_by="from_date")
+ self.assertEqual(len(ledgers), 2)
+
+ self.assertEqual(ledgers[0].from_date, application.from_date)
+ self.assertEqual(ledgers[0].to_date, year_end)
+
+ self.assertEqual(ledgers[1].from_date, add_days(year_end, 1))
+ self.assertEqual(ledgers[1].to_date, application.to_date)
+
def test_overwrite_attendance(self):
'''check attendance is automatically created on leave approval'''
make_allocation_record()
From 4d8798b0ead57eb3ae3a0b27755aad60a84989b9 Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Wed, 9 Mar 2022 16:41:54 +0530
Subject: [PATCH 246/447] fix: ignore non-unique swift numbers while migrating
(#30132)
---
.../v12_0/move_bank_account_swift_number_to_bank.py | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py b/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py
index b3ee340464..7ae4c42cec 100644
--- a/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py
+++ b/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py
@@ -5,10 +5,13 @@ def execute():
frappe.reload_doc('accounts', 'doctype', 'bank', force=1)
if frappe.db.table_exists('Bank') and frappe.db.table_exists('Bank Account') and frappe.db.has_column('Bank Account', 'swift_number'):
- frappe.db.sql("""
- UPDATE `tabBank` b, `tabBank Account` ba
- SET b.swift_number = ba.swift_number WHERE b.name = ba.bank
- """)
+ try:
+ frappe.db.sql("""
+ UPDATE `tabBank` b, `tabBank Account` ba
+ SET b.swift_number = ba.swift_number WHERE b.name = ba.bank
+ """)
+ except Exception as e:
+ frappe.log_error(e, title="Patch Migration Failed")
frappe.reload_doc('accounts', 'doctype', 'bank_account')
frappe.reload_doc('accounts', 'doctype', 'payment_request')
From 921d6b25d7bf974f3591bde6e6d9fa50a9791111 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Wed, 9 Mar 2022 16:58:15 +0530
Subject: [PATCH 247/447] chore: linter issue
---
erpnext/assets/doctype/asset/asset_dashboard.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/erpnext/assets/doctype/asset/asset_dashboard.py b/erpnext/assets/doctype/asset/asset_dashboard.py
index 1833b0e716..c81b611a41 100644
--- a/erpnext/assets/doctype/asset/asset_dashboard.py
+++ b/erpnext/assets/doctype/asset/asset_dashboard.py
@@ -1,5 +1,6 @@
from frappe import _
+
def get_data():
return {
'non_standard_fieldnames': {
From b1c8a4543d226a1b6422a6778182cc20eacfaa56 Mon Sep 17 00:00:00 2001
From: Chillar Anand
Date: Wed, 9 Mar 2022 17:05:00 +0530
Subject: [PATCH 248/447] test: Added test for monthly attendance report
(#29989)
Co-authored-by: Rucha Mahabal
---
.../test_monthly_attendance_sheet.py | 45 +++++++++++++++++++
1 file changed, 45 insertions(+)
create mode 100644 erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py
diff --git a/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py
new file mode 100644
index 0000000000..b196fb5b98
--- /dev/null
+++ b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py
@@ -0,0 +1,45 @@
+import frappe
+from dateutil.relativedelta import relativedelta
+
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import now_datetime
+
+from erpnext.hr.doctype.attendance.attendance import mark_attendance
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.report.monthly_attendance_sheet.monthly_attendance_sheet import execute
+
+
+class TestMonthlyAttendanceSheet(FrappeTestCase):
+ def setUp(self):
+ self.employee = make_employee("test_employee@example.com")
+ frappe.db.delete('Attendance', {'employee': self.employee})
+
+ def test_monthly_attendance_sheet_report(self):
+ now = now_datetime()
+ previous_month = now.month - 1
+ previous_month_first = now.replace(day=1).replace(month=previous_month).date()
+
+ company = frappe.db.get_value('Employee', self.employee, 'company')
+
+ # mark different attendance status on first 3 days of previous month
+ mark_attendance(self.employee, previous_month_first, 'Absent')
+ mark_attendance(self.employee, previous_month_first + relativedelta(days=1), 'Present')
+ mark_attendance(self.employee, previous_month_first + relativedelta(days=2), 'On Leave')
+
+ filters = frappe._dict({
+ 'month': previous_month,
+ 'year': now.year,
+ 'company': company,
+ })
+ report = execute(filters=filters)
+ employees = report[1][0]
+ datasets = report[3]['data']['datasets']
+ absent = datasets[0]['values']
+ present = datasets[1]['values']
+ leaves = datasets[2]['values']
+
+ # ensure correct attendance is reflect on the report
+ self.assertIn(self.employee, employees)
+ self.assertEqual(absent[0], 1)
+ self.assertEqual(present[1], 1)
+ self.assertEqual(leaves[2], 1)
From fe4b6771b5fd935ed278cf553c864a18e3356a33 Mon Sep 17 00:00:00 2001
From: Chillar Anand
Date: Wed, 9 Mar 2022 17:16:05 +0530
Subject: [PATCH 249/447] refactor: Remove dead code (#30140)
---
erpnext/hr/doctype/leave_application/leave_application.py | 1 -
.../report/project_profitability/project_profitability.py | 2 --
erpnext/regional/report/irs_1099/irs_1099.py | 1 -
erpnext/regional/report/uae_vat_201/test_uae_vat_201.py | 3 +--
erpnext/stock/report/stock_balance/stock_balance.py | 1 -
5 files changed, 1 insertion(+), 7 deletions(-)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index ef5f4bcb0f..345d8dc370 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -495,7 +495,6 @@ def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day
number_of_days = date_diff(to_date, from_date) + .5
else:
number_of_days = date_diff(to_date, from_date) + 1
-
else:
number_of_days = date_diff(to_date, from_date) + 1
diff --git a/erpnext/projects/report/project_profitability/project_profitability.py b/erpnext/projects/report/project_profitability/project_profitability.py
index 9520cd17be..23c3b82c6d 100644
--- a/erpnext/projects/report/project_profitability/project_profitability.py
+++ b/erpnext/projects/report/project_profitability/project_profitability.py
@@ -1,14 +1,12 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-
import frappe
from frappe import _
from frappe.utils import flt
def execute(filters=None):
- columns, data = [], []
data = get_data(filters)
columns = get_columns()
charts = get_chart_data(data)
diff --git a/erpnext/regional/report/irs_1099/irs_1099.py b/erpnext/regional/report/irs_1099/irs_1099.py
index b1a5d10962..147a59fb01 100644
--- a/erpnext/regional/report/irs_1099/irs_1099.py
+++ b/erpnext/regional/report/irs_1099/irs_1099.py
@@ -30,7 +30,6 @@ def execute(filters=None):
if region != 'United States':
return [], []
- data = []
columns = get_columns()
conditions = ""
if filters.supplier_group:
diff --git a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py
index 41336873ac..464939f39e 100644
--- a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py
+++ b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py
@@ -118,8 +118,7 @@ def make_customer():
"customer_type": "Company",
})
customer.insert()
- else:
- customer = frappe.get_doc("Customer", "_Test UAE Customer")
+
def make_supplier():
if not frappe.db.exists("Supplier", "_Test UAE Supplier"):
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index b4f43a7fef..24f47c1946 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -18,7 +18,6 @@ def execute(filters=None):
is_reposting_item_valuation_in_progress()
if not filters: filters = {}
- from_date = filters.get('from_date')
to_date = filters.get('to_date')
if filters.get("company"):
From fc42041f8fff7bd9f3b374992565bf3eccfaf43d Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Wed, 9 Mar 2022 18:01:10 +0530
Subject: [PATCH 250/447] fix(psoa): add company filter to account
---
.../process_statement_of_accounts.js | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
index 088c190f45..29f2e98e77 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
@@ -51,6 +51,13 @@ frappe.ui.form.on('Process Statement Of Accounts', {
}
}
});
+ frm.set_query("account", function() {
+ return {
+ filters: {
+ 'company': frm.doc.company
+ }
+ };
+ });
if(frm.doc.__islocal){
frm.set_value('from_date', frappe.datetime.add_months(frappe.datetime.get_today(), -1));
frm.set_value('to_date', frappe.datetime.get_today());
From f0664279130edea2b9ba1233f5fcbd0a8a001649 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Thu, 10 Mar 2022 10:06:07 +0530
Subject: [PATCH 251/447] fix: program enrollment button labels (#30148)
---
erpnext/www/lms/macros/hero.html | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/erpnext/www/lms/macros/hero.html b/erpnext/www/lms/macros/hero.html
index 95ba8f7df2..dd3c23a014 100644
--- a/erpnext/www/lms/macros/hero.html
+++ b/erpnext/www/lms/macros/hero.html
@@ -39,16 +39,13 @@
frappe.call(opts).then(res => {
let success_dialog = new frappe.ui.Dialog({
title: __('Success'),
- primary_action_label: __('View Program Content'),
+ primary_action_label: __('OK'),
primary_action: function() {
window.location.reload();
- },
- secondary_action: function() {
- window.location.reload();
}
})
success_dialog.show();
- success_dialog.set_message(__('You have successfully enrolled for the program '));
+ success_dialog.set_message(__('You have successfully enrolled for the program.'));
})
}
From 395b15058caedbcd6bc461d9d397a563bd2e981b Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 10 Mar 2022 10:50:03 +0530
Subject: [PATCH 252/447] fix: Sales and Purchase retrun optimization
---
erpnext/controllers/sales_and_purchase_return.py | 13 ++++++++-----
.../stock/doctype/delivery_note/delivery_note.py | 3 +++
.../doctype/purchase_receipt/purchase_receipt.py | 3 +++
3 files changed, 14 insertions(+), 5 deletions(-)
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 8c3aab442b..2b094e3626 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -208,7 +208,7 @@ def get_already_returned_items(doc):
return items
-def get_returned_qty_map_for_row(row_name, doctype):
+def get_returned_qty_map_for_row(return_against, supplier, row_name, doctype):
child_doctype = doctype + " Item"
reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype)
@@ -226,9 +226,12 @@ def get_returned_qty_map_for_row(row_name, doctype):
if doctype == "Purchase Receipt":
fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)]
+ # Used retrun against and supplier and is_retrun because there is an index added for it
data = frappe.db.get_list(doctype,
fields = fields,
filters = [
+ [doctype, "return_against", "=", return_against],
+ [doctype, "supplier", "=", supplier],
[doctype, "docstatus", "=", 1],
[doctype, "is_return", "=", 1],
[child_doctype, reference_field, "=", row_name]
@@ -307,7 +310,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.serial_no = '\n'.join(serial_nos)
if doctype == "Purchase Receipt":
- returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
+ returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.supplier, source_doc.name, doctype)
target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0))
target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get('rejected_qty') or 0))
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
@@ -321,7 +324,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.purchase_receipt_item = source_doc.name
elif doctype == "Purchase Invoice":
- returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
+ returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.supplier, source_doc.name, doctype)
target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0))
target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get('rejected_qty') or 0))
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
@@ -335,7 +338,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.purchase_invoice_item = source_doc.name
elif doctype == "Delivery Note":
- returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
+ returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.supplier, source_doc.name, doctype)
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
@@ -348,7 +351,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
elif doctype == "Sales Invoice" or doctype == "POS Invoice":
- returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
+ returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.supplier, source_doc.name, doctype)
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 2a4d63954a..06439b64e4 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -798,3 +798,6 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
}, target_doc, set_missing_values)
return doclist
+
+def on_doctype_update():
+ frappe.db.add_index("Delivery Note", ["customer", "is_return", "return_against"])
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 33e40c85f1..52f10ea9c8 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -926,3 +926,6 @@ def get_item_account_wise_additional_cost(purchase_document):
account.base_amount * item.get(based_on_field) / total_item_cost
return item_account_wise_cost
+
+def on_doctype_update():
+ frappe.db.add_index("Purchase Receipt", ["supplier", "is_return", "return_against"])
From 5d5ae16fe574e2b701731e1f084d652c0959b1e8 Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Thu, 10 Mar 2022 11:12:33 +0530
Subject: [PATCH 253/447] fix(psoa): no such element: dict object['account']
---
.../process_statement_of_accounts.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
index f8d191cc3f..82705a9cea 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
@@ -64,10 +64,10 @@
{{ frappe.format(row.account, {fieldtype: "Link"}) or " " }}
- {{ row.account and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}
+ {{ row.get('account', '') and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}
- {{ row.account and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}
+ {{ row.get('account', '') and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}
{% endif %}
From 84568ac3410f53976c76aa3e583b739df5f2a7a6 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 10 Mar 2022 11:20:23 +0530
Subject: [PATCH 254/447] chore: imports
---
.../monthly_attendance_sheet/test_monthly_attendance_sheet.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py
index b196fb5b98..952af8117e 100644
--- a/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py
+++ b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py
@@ -1,6 +1,5 @@
import frappe
from dateutil.relativedelta import relativedelta
-
from frappe.tests.utils import FrappeTestCase
from frappe.utils import now_datetime
From 5d66cc4c4a3a91af0d306f24c0b35f69a9b2966e Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 10 Mar 2022 12:22:37 +0530
Subject: [PATCH 255/447] fix: Add cost center in loan document
---
erpnext/loan_management/doctype/loan/loan.json | 15 ++++++++++++++-
erpnext/loan_management/doctype/loan/loan.py | 8 ++++++++
.../loan_interest_accrual.py | 7 ++++---
erpnext/patches.txt | 3 ++-
.../patches/v13_0/add_cost_center_in_loans.py | 16 ++++++++++++++++
5 files changed, 44 insertions(+), 5 deletions(-)
create mode 100644 erpnext/patches/v13_0/add_cost_center_in_loans.py
diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json
index 196f36f0f4..ef78a640aa 100644
--- a/erpnext/loan_management/doctype/loan/loan.json
+++ b/erpnext/loan_management/doctype/loan/loan.json
@@ -32,6 +32,8 @@
"monthly_repayment_amount",
"repayment_start_date",
"is_term_loan",
+ "accounting_dimensions_section",
+ "cost_center",
"account_info",
"mode_of_payment",
"disbursement_account",
@@ -366,12 +368,23 @@
"options": "Account",
"read_only": 1,
"reqd": 1
+ },
+ {
+ "fieldname": "accounting_dimensions_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Dimensions"
+ },
+ {
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "options": "Cost Center"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-01-25 16:29:16.325501",
+ "modified": "2022-03-10 11:50:31.957360",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
index b798e088b4..0fe9947472 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -25,6 +25,7 @@ class Loan(AccountsController):
self.set_loan_amount()
self.validate_loan_amount()
self.set_missing_fields()
+ self.validate_cost_center()
self.validate_accounts()
self.check_sanctioned_amount_limit()
self.validate_repay_from_salary()
@@ -45,6 +46,13 @@ class Loan(AccountsController):
frappe.throw(_("Account {0} does not belongs to company {1}").format(frappe.bold(self.get(fieldname)),
frappe.bold(self.company)))
+ def validate_cost_center(self):
+ if not self.cost_center and self.rate_of_interest != 0:
+ self.cost_center = frappe.db.get_value('Company', self.company, 'cost_center')
+
+ if not self.cost_center:
+ frappe.throw(_('Cost center is mandatory for loans having rate of interest greater than 0'))
+
def on_submit(self):
self.link_loan_security_pledge()
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
index 1c800a06da..f6a3ededcb 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
@@ -6,7 +6,6 @@ import frappe
from frappe import _
from frappe.utils import add_days, cint, date_diff, flt, get_datetime, getdate, nowdate
-import erpnext
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.controllers.accounts_controller import AccountsController
@@ -41,6 +40,8 @@ class LoanInterestAccrual(AccountsController):
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
+ cost_center = frappe.db.get_value('Loan', self.loan, 'cost_center')
+
if self.interest_amount:
gle_map.append(
self.get_gl_dict({
@@ -54,7 +55,7 @@ class LoanInterestAccrual(AccountsController):
"against_voucher": self.loan,
"remarks": _("Interest accrued from {0} to {1} against loan: {2}").format(
self.last_accrual_date, self.posting_date, self.loan),
- "cost_center": erpnext.get_default_cost_center(self.company),
+ "cost_center": cost_center,
"posting_date": self.posting_date
})
)
@@ -69,7 +70,7 @@ class LoanInterestAccrual(AccountsController):
"against_voucher": self.loan,
"remarks": ("Interest accrued from {0} to {1} against loan: {2}").format(
self.last_accrual_date, self.posting_date, self.loan),
- "cost_center": erpnext.get_default_cost_center(self.company),
+ "cost_center": cost_center,
"posting_date": self.posting_date
})
)
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 13f0e7b872..ebda8058e0 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -357,4 +357,5 @@ erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
erpnext.patches.v13_0.update_accounts_in_loan_docs
erpnext.patches.v14_0.update_batch_valuation_flag
erpnext.patches.v14_0.delete_non_profit_doctypes
-erpnext.patches.v14_0.update_employee_advance_status
\ No newline at end of file
+erpnext.patches.v14_0.update_employee_advance_status
+erpnext.patches.v13_0.add_cost_center_in_loans
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/add_cost_center_in_loans.py b/erpnext/patches/v13_0/add_cost_center_in_loans.py
new file mode 100644
index 0000000000..25e1722a4f
--- /dev/null
+++ b/erpnext/patches/v13_0/add_cost_center_in_loans.py
@@ -0,0 +1,16 @@
+import frappe
+
+
+def execute():
+ frappe.reload_doc('loan_management', 'doctype', 'loan')
+ loan = frappe.qb.DocType('Loan')
+
+ for company in frappe.get_all('Company', pluck='name'):
+ default_cost_center = frappe.db.get_value('Company', company, 'cost_center')
+ frappe.qb.update(
+ loan
+ ).set(
+ loan.cost_center, default_cost_center
+ ).where(
+ loan.company == company
+ ).run()
\ No newline at end of file
From e9d458b822e7436632d0d53fdc4a068267876a0a Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 10 Mar 2022 12:29:17 +0530
Subject: [PATCH 256/447] fix: Update party type
---
erpnext/controllers/sales_and_purchase_return.py | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 2b094e3626..21666336e0 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -208,10 +208,15 @@ def get_already_returned_items(doc):
return items
-def get_returned_qty_map_for_row(return_against, supplier, row_name, doctype):
+def get_returned_qty_map_for_row(return_against, party, row_name, doctype):
child_doctype = doctype + " Item"
reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype)
+ if doctype in ('Purchase Receipt', 'Purchase Invoice'):
+ party_type = 'supplier'
+ else:
+ party_type = 'customer'
+
fields = [
"sum(abs(`tab{0}`.qty)) as qty".format(child_doctype),
"sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype)
@@ -231,7 +236,7 @@ def get_returned_qty_map_for_row(return_against, supplier, row_name, doctype):
fields = fields,
filters = [
[doctype, "return_against", "=", return_against],
- [doctype, "supplier", "=", supplier],
+ [doctype, party_type, "=", party],
[doctype, "docstatus", "=", 1],
[doctype, "is_return", "=", 1],
[child_doctype, reference_field, "=", row_name]
@@ -338,7 +343,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.purchase_invoice_item = source_doc.name
elif doctype == "Delivery Note":
- returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.supplier, source_doc.name, doctype)
+ returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.customer, source_doc.name, doctype)
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
@@ -351,7 +356,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
elif doctype == "Sales Invoice" or doctype == "POS Invoice":
- returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.supplier, source_doc.name, doctype)
+ returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.customer, source_doc.name, doctype)
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
From 5193a637810268c7730e2ce386b2c234924cc28f Mon Sep 17 00:00:00 2001
From: Himanshu
Date: Thu, 10 Mar 2022 08:13:35 +0000
Subject: [PATCH 257/447] fix: do not reset asset_category (#29696)
---
erpnext/stock/doctype/item/item.js | 16 ++++++++--------
erpnext/stock/doctype/item/item.py | 26 +++++++++++++-------------
2 files changed, 21 insertions(+), 21 deletions(-)
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index ffea9c2d6e..9e8b3bd463 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -165,21 +165,21 @@ frappe.ui.form.on("Item", {
frm.set_value('has_batch_no', 0);
frm.toggle_enable(['has_serial_no', 'serial_no_series'], !frm.doc.is_fixed_asset);
- frm.call({
- method: "set_asset_naming_series",
- doc: frm.doc,
- callback: function() {
+ frappe.call({
+ method: "erpnext.stock.doctype.item.item.get_asset_naming_series",
+ callback: function(r) {
frm.set_value("is_stock_item", frm.doc.is_fixed_asset ? 0 : 1);
- frm.trigger("set_asset_naming_series");
+ frm.events.set_asset_naming_series(frm, r.message);
}
});
frm.trigger('auto_create_assets');
},
- set_asset_naming_series: function(frm) {
- if (frm.doc.__onload && frm.doc.__onload.asset_naming_series) {
- frm.set_df_property("asset_naming_series", "options", frm.doc.__onload.asset_naming_series);
+ set_asset_naming_series: function(frm, asset_naming_series) {
+ if ((frm.doc.__onload && frm.doc.__onload.asset_naming_series) || asset_naming_series) {
+ let naming_series = (frm.doc.__onload && frm.doc.__onload.asset_naming_series) || asset_naming_series;
+ frm.set_df_property("asset_naming_series", "options", naming_series);
}
},
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 494fb3b8bb..32c72fd2f6 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -50,15 +50,7 @@ class DataValidationError(frappe.ValidationError):
class Item(Document):
def onload(self):
self.set_onload('stock_exists', self.stock_ledger_created())
- self.set_asset_naming_series()
-
- @frappe.whitelist()
- def set_asset_naming_series(self):
- if not hasattr(self, '_asset_naming_series'):
- from erpnext.assets.doctype.asset.asset import get_asset_naming_series
- self._asset_naming_series = get_asset_naming_series()
-
- self.set_onload('asset_naming_series', self._asset_naming_series)
+ self.set_onload('asset_naming_series', get_asset_naming_series())
def autoname(self):
if frappe.db.get_default("item_naming_by") == "Naming Series":
@@ -999,7 +991,7 @@ def get_uom_conv_factor(uom, stock_uom):
if uom == stock_uom:
return 1.0
- from_uom, to_uom = uom, stock_uom # renaming for readability
+ from_uom, to_uom = uom, stock_uom # renaming for readability
exact_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": to_uom, "from_uom": from_uom}, ["value"], as_dict=1)
if exact_match:
@@ -1011,9 +1003,9 @@ def get_uom_conv_factor(uom, stock_uom):
# This attempts to try and get conversion from intermediate UOM.
# case:
- # g -> mg = 1000
- # g -> kg = 0.001
- # therefore kg -> mg = 1000 / 0.001 = 1,000,000
+ # g -> mg = 1000
+ # g -> kg = 0.001
+ # therefore kg -> mg = 1000 / 0.001 = 1,000,000
intermediate_match = frappe.db.sql("""
select (first.value / second.value) as value
from `tabUOM Conversion Factor` first
@@ -1072,3 +1064,11 @@ def validate_item_default_company_links(item_defaults: List[ItemDefault]) -> Non
frappe.bold(item_default.company),
frappe.bold(frappe.unscrub(field))
), title=_("Invalid Item Defaults"))
+
+
+@frappe.whitelist()
+def get_asset_naming_series():
+ from erpnext.assets.doctype.asset.asset import get_asset_naming_series
+
+ return get_asset_naming_series()
+
From a13e06156b3c195d2340dafcebe0f12d2c95dba8 Mon Sep 17 00:00:00 2001
From: marination
Date: Thu, 10 Mar 2022 13:54:00 +0530
Subject: [PATCH 258/447] fix: 'save_quotations_as_draft' checkbox not honoured
- Make sure `request_for_quotation` considers `save_quotations_as_draft`
- Added test for checkout disabled quote
---
erpnext/e_commerce/shopping_cart/cart.py | 6 +++-
.../shopping_cart/test_shopping_cart.py | 35 +++++++++++++++----
2 files changed, 33 insertions(+), 8 deletions(-)
diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py
index 372aed0b95..0f3f69d18d 100644
--- a/erpnext/e_commerce/shopping_cart/cart.py
+++ b/erpnext/e_commerce/shopping_cart/cart.py
@@ -120,7 +120,11 @@ def place_order():
def request_for_quotation():
quotation = _get_cart_quotation()
quotation.flags.ignore_permissions = True
- quotation.submit()
+
+ if get_shopping_cart_settings().save_quotations_as_draft:
+ quotation.save()
+ else:
+ quotation.submit()
return quotation.name
@frappe.whitelist()
diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
index 9c389d0d0b..4a3787a313 100644
--- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
+++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
@@ -6,7 +6,7 @@ import unittest
import frappe
from frappe.tests.utils import change_settings
-from frappe.utils import add_months, nowdate
+from frappe.utils import add_months, cint, nowdate
from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
@@ -14,22 +14,17 @@ from erpnext.e_commerce.shopping_cart.cart import (
_get_cart_quotation,
get_cart_quotation,
get_party,
+ request_for_quotation,
update_cart,
)
from erpnext.tests.utils import create_test_contact_and_address
-# test_dependencies = ['Payment Terms Template']
class TestShoppingCart(unittest.TestCase):
"""
Note:
Shopping Cart == Quotation
"""
-
- @classmethod
- def tearDownClass(cls):
- frappe.db.sql("delete from `tabTax Rule`")
-
def setUp(self):
frappe.set_user("Administrator")
create_test_contact_and_address()
@@ -45,6 +40,10 @@ class TestShoppingCart(unittest.TestCase):
frappe.set_user("Administrator")
self.disable_shopping_cart()
+ @classmethod
+ def tearDownClass(cls):
+ frappe.db.sql("delete from `tabTax Rule`")
+
def test_get_cart_new_user(self):
self.login_as_new_user()
@@ -179,6 +178,28 @@ class TestShoppingCart(unittest.TestCase):
# test if items are rendered without error
frappe.render_template("templates/includes/cart/cart_items.html", cart)
+ @change_settings("E Commerce Settings",{
+ "save_quotations_as_draft": 1
+ })
+ def test_cart_without_checkout_and_draft_quotation(self):
+ "Test impact of 'save_quotations_as_draft' checkbox."
+ frappe.local.shopping_cart_settings = None
+
+ # add item to cart
+ update_cart("_Test Item", 1)
+ quote_name = request_for_quotation() # Request for Quote
+ quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus"))
+
+ self.assertEqual(quote_doctstatus, 0)
+
+ frappe.db.set_value("E Commerce Settings", None, "save_quotations_as_draft", 0)
+ frappe.local.shopping_cart_settings = None
+ update_cart("_Test Item", 1)
+ quote_name = request_for_quotation() # Request for Quote
+ quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus"))
+
+ self.assertEqual(quote_doctstatus, 1)
+
def create_tax_rule(self):
tax_rule = frappe.get_test_records("Tax Rule")[0]
try:
From e5fb871ef4a9e738e3a5ddc6d0e8eb2dfdd2ea16 Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Thu, 10 Mar 2022 13:54:43 +0530
Subject: [PATCH 259/447] fix: Ignore missing customer group while fetching
price list
---
erpnext/accounts/party.py | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index b443852715..791cd1d5e9 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -170,10 +170,7 @@ def get_default_price_list(party):
return party.default_price_list
if party.doctype == "Customer":
- try:
- return frappe.get_cached_value("Customer Group", party.customer_group, "default_price_list")
- except frappe.exceptions.DoesNotExistError:
- return
+ return frappe.db.get_value("Customer Group", party.customer_group, "default_price_list")
def set_price_list(party_details, party, party_type, given_price_list, pos=None):
From a5befb6bf82546deaf9ea21da2eb828aca1bb51e Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 10 Mar 2022 14:18:54 +0530
Subject: [PATCH 260/447] fix: Update modified timestamp
---
erpnext/stock/doctype/delivery_note/delivery_note.json | 2 +-
erpnext/stock/doctype/purchase_receipt/purchase_receipt.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index 55a4c956a6..7ebc4eed75 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -1315,7 +1315,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2021-10-09 14:29:13.428984",
+ "modified": "2022-03-10 14:29:13.428984",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index b54a90eed3..6d4b4a19bd 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -1165,7 +1165,7 @@
"idx": 261,
"is_submittable": 1,
"links": [],
- "modified": "2022-02-01 11:40:52.690984",
+ "modified": "2022-03-10 11:40:52.690984",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",
From 9fae89ff6105622c592b9efddb0ef4f8e0dcf942 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Thu, 10 Mar 2022 14:22:12 +0530
Subject: [PATCH 261/447] fix: flaky tests (#30154)
---
.../hr/doctype/attendance/test_attendance.py | 16 +++++++++---
.../test_leave_application.py | 25 +++++++++++++------
2 files changed, 30 insertions(+), 11 deletions(-)
diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py
index c74967d213..6095413771 100644
--- a/erpnext/hr/doctype/attendance/test_attendance.py
+++ b/erpnext/hr/doctype/attendance/test_attendance.py
@@ -25,7 +25,9 @@ class TestAttendance(FrappeTestCase):
self.assertEqual(attendance, fetch_attendance)
def test_unmarked_days(self):
- first_day = get_first_day(getdate())
+ now = now_datetime()
+ previous_month = now.month - 1
+ first_day = now.replace(day=1).replace(month=previous_month).date()
employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1))
frappe.db.delete('Attendance', {'employee': employee})
@@ -34,7 +36,7 @@ class TestAttendance(FrappeTestCase):
holiday_list = make_holiday_list()
frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
- first_sunday = get_first_sunday(holiday_list)
+ first_sunday = get_first_sunday(holiday_list, for_date=first_day)
mark_attendance(employee, first_day, 'Present')
month_name = get_month_name(first_day)
@@ -49,7 +51,9 @@ class TestAttendance(FrappeTestCase):
self.assertIn(first_sunday, unmarked_days)
def test_unmarked_days_excluding_holidays(self):
- first_day = get_first_day(getdate())
+ now = now_datetime()
+ previous_month = now.month - 1
+ first_day = now.replace(day=1).replace(month=previous_month).date()
employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1))
frappe.db.delete('Attendance', {'employee': employee})
@@ -58,7 +62,7 @@ class TestAttendance(FrappeTestCase):
holiday_list = make_holiday_list()
frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
- first_sunday = get_first_sunday(holiday_list)
+ first_sunday = get_first_sunday(holiday_list, for_date=first_day)
mark_attendance(employee, first_day, 'Present')
month_name = get_month_name(first_day)
@@ -83,6 +87,10 @@ class TestAttendance(FrappeTestCase):
relieving_date=relieving_date)
frappe.db.delete('Attendance', {'employee': employee})
+ from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
+ holiday_list = make_holiday_list()
+ frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
+
attendance_date = add_days(first_day, 2)
mark_attendance(employee, attendance_date, 'Present')
month_name = get_month_name(first_day)
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 7b3aa49729..01e0ca045b 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -133,7 +133,9 @@ class TestLeaveApplication(unittest.TestCase):
holiday_list = make_holiday_list()
employee = get_employee()
- frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list)
+ original_holiday_list = employee.holiday_list
+ frappe.db.set_value("Employee", employee.name, "holiday_list", holiday_list)
+
first_sunday = get_first_sunday(holiday_list)
leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name)
@@ -143,6 +145,8 @@ class TestLeaveApplication(unittest.TestCase):
leave_application.cancel()
+ frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list)
+
def test_attendance_update_for_exclude_holidays(self):
# Case 2: leave type with 'Include holidays within leaves as leaves' disabled
frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1)
@@ -157,7 +161,8 @@ class TestLeaveApplication(unittest.TestCase):
holiday_list = make_holiday_list()
employee = get_employee()
- frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list)
+ original_holiday_list = employee.holiday_list
+ frappe.db.set_value("Employee", employee.name, "holiday_list", holiday_list)
first_sunday = get_first_sunday(holiday_list)
# already marked attendance on a holiday should be deleted in this case
@@ -177,7 +182,7 @@ class TestLeaveApplication(unittest.TestCase):
attendance.flags.ignore_validate = True
attendance.save()
- leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name)
+ leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name, employee.company)
leave_application.reload()
# holiday should be excluded while marking attendance
self.assertEqual(leave_application.total_leave_days, 3)
@@ -189,6 +194,8 @@ class TestLeaveApplication(unittest.TestCase):
# attendance on non-holiday updated
self.assertEqual(frappe.db.get_value("Attendance", attendance.name, "status"), "On Leave")
+ frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list)
+
def test_block_list(self):
self._clear_roles()
@@ -327,7 +334,8 @@ class TestLeaveApplication(unittest.TestCase):
employee = get_employee()
default_holiday_list = make_holiday_list()
- frappe.db.set_value("Company", employee.company, "default_holiday_list", default_holiday_list)
+ original_holiday_list = employee.holiday_list
+ frappe.db.set_value("Employee", employee.name, "holiday_list", default_holiday_list)
first_sunday = get_first_sunday(default_holiday_list)
optional_leave_date = add_days(first_sunday, 1)
@@ -378,6 +386,8 @@ class TestLeaveApplication(unittest.TestCase):
# check leave balance is reduced
self.assertEqual(get_leave_balance_on(employee.name, leave_type, optional_leave_date), 9)
+ frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list)
+
def test_leaves_allowed(self):
employee = get_employee()
leave_period = get_leave_period()
@@ -782,9 +792,10 @@ def allocate_leaves(employee, leave_period, leave_type, new_leaves_allocated, el
allocate_leave.submit()
-def get_first_sunday(holiday_list):
- month_start_date = get_first_day(nowdate())
- month_end_date = get_last_day(nowdate())
+def get_first_sunday(holiday_list, for_date=None):
+ date = for_date or getdate()
+ month_start_date = get_first_day(date)
+ month_end_date = get_last_day(date)
first_sunday = frappe.db.sql("""
select holiday_date from `tabHoliday`
where parent = %s
From 6794148c04d6cfdb3ac9b4df38e0957f35334c5a Mon Sep 17 00:00:00 2001
From: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
Date: Thu, 10 Mar 2022 10:17:29 +0100
Subject: [PATCH 262/447] fix(lead): reload contact before updating links
(#29966)
* fix(lead): reload contact before updading links
Contact might have changed since it was created.
* refactor: reload contact after insert
---
erpnext/crm/doctype/lead/lead.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index c31b068a43..33ec552152 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -214,6 +214,7 @@ class Lead(SellingController):
})
contact.insert(ignore_permissions=True)
+ contact.reload() # load changes by hooks on contact
return contact
From b2c549a464a86d6f693156c87bf9674a02429a0e Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Thu, 10 Mar 2022 15:41:14 +0530
Subject: [PATCH 263/447] fix: conflicts
---
.../hr/doctype/leave_application/test_leave_application.py | 4 ----
1 file changed, 4 deletions(-)
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 08743aaa96..3e75578d18 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -326,8 +326,6 @@ class TestLeaveApplication(unittest.TestCase):
# attendance on non-holiday updated
self.assertEqual(frappe.db.get_value("Attendance", attendance.name, "status"), "On Leave")
- frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list)
-
def test_block_list(self):
self._clear_roles()
@@ -515,8 +513,6 @@ class TestLeaveApplication(unittest.TestCase):
# check leave balance is reduced
self.assertEqual(get_leave_balance_on(employee.name, leave_type, optional_leave_date), 9)
- frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list)
-
def test_leaves_allowed(self):
employee = get_employee()
leave_period = get_leave_period()
From 412645597567df133dd91a6cf44db93207575052 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 10 Mar 2022 15:43:09 +0530
Subject: [PATCH 264/447] fix: dont reset UOM in MR on every get_item_detail
call (#30164)
---
erpnext/stock/doctype/material_request/material_request.js | 2 ++
1 file changed, 2 insertions(+)
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index 5f53be0869..e68b0abfb9 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -214,6 +214,7 @@ frappe.ui.form.on('Material Request', {
material_request_type: frm.doc.material_request_type,
plc_conversion_rate: 1,
rate: item.rate,
+ uom: item.uom,
conversion_factor: item.conversion_factor
},
overwrite_warehouse: overwrite_warehouse
@@ -392,6 +393,7 @@ frappe.ui.form.on("Material Request Item", {
item_code: function(frm, doctype, name) {
const item = locals[doctype][name];
item.rate = 0;
+ item.uom = '';
set_schedule_date(frm);
frm.events.get_item_data(frm, item, true);
},
From b4d4ae6aa3f258357a974e0a76247b3e752bdbf2 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 10 Mar 2022 16:09:26 +0530
Subject: [PATCH 265/447] test: refactor item merge test and disable commits
---
erpnext/stock/doctype/item/test_item.py | 19 ++++++++++---------
.../repost_item_valuation.py | 3 ++-
2 files changed, 12 insertions(+), 10 deletions(-)
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index d7671b1d71..d57308b2af 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -371,23 +371,24 @@ class TestItem(FrappeTestCase):
variant.save()
def test_item_merging(self):
- create_item("Test Item for Merging 1")
- create_item("Test Item for Merging 2")
+ old = create_item(frappe.generate_hash(length=20)).name
+ new = create_item(frappe.generate_hash(length=20)).name
- make_stock_entry(item_code="Test Item for Merging 1", target="_Test Warehouse - _TC",
+ make_stock_entry(item_code=old, target="_Test Warehouse - _TC",
qty=1, rate=100)
- make_stock_entry(item_code="Test Item for Merging 2", target="_Test Warehouse 1 - _TC",
+ make_stock_entry(item_code=old, target="_Test Warehouse 1 - _TC",
+ qty=1, rate=100)
+ make_stock_entry(item_code=new, target="_Test Warehouse 1 - _TC",
qty=1, rate=100)
- frappe.rename_doc("Item", "Test Item for Merging 1", "Test Item for Merging 2", merge=True)
+ frappe.rename_doc("Item", old, new, merge=True)
- self.assertFalse(frappe.db.exists("Item", "Test Item for Merging 1"))
+ self.assertFalse(frappe.db.exists("Item", old))
self.assertTrue(frappe.db.get_value("Bin",
- {"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse - _TC"}))
-
+ {"item_code": new, "warehouse": "_Test Warehouse - _TC"}))
self.assertTrue(frappe.db.get_value("Bin",
- {"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"}))
+ {"item_code": new, "warehouse": "_Test Warehouse 1 - _TC"}))
def test_item_merging_with_product_bundle(self):
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index 977d470995..f4d52ad73d 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -118,7 +118,8 @@ def repost(doc):
doc.set_status('Failed')
raise
finally:
- frappe.db.commit()
+ if not frappe.flags.in_test:
+ frappe.db.commit()
def repost_sl_entries(doc):
if doc.based_on == 'Transaction':
From 73901aad6f88c06cfb6dab8da133fba4175bd692 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 10 Mar 2022 16:30:29 +0530
Subject: [PATCH 266/447] fix: handle duplicate bins during item merge renames
---
erpnext/stock/doctype/item/item.py | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 32c72fd2f6..8ede95539b 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -392,6 +392,7 @@ class Item(Document):
self.validate_properties_before_merge(new_name)
self.validate_duplicate_product_bundles_before_merge(old_name, new_name)
self.validate_duplicate_website_item_before_merge(old_name, new_name)
+ self.delete_old_bins(old_name)
def after_rename(self, old_name, new_name, merge):
if merge:
@@ -420,6 +421,9 @@ class Item(Document):
frappe.db.set_value(dt, d.name, "item_wise_tax_detail",
json.dumps(item_wise_tax_detail), update_modified=False)
+ def delete_old_bins(self, old_name):
+ frappe.db.delete("Bin", {"item_code": old_name})
+
def validate_duplicate_item_in_stock_reconciliation(self, old_name, new_name):
records = frappe.db.sql(""" SELECT parent, COUNT(*) as records
FROM `tabStock Reconciliation Item`
@@ -500,11 +504,11 @@ class Item(Document):
existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
- repost_stock_for_warehouses = frappe.db.sql_list("""select distinct warehouse
- from tabBin where item_code=%s""", new_name)
+ repost_stock_for_warehouses = frappe.get_all("Stock Ledger Entry",
+ "warehouse", filters={"item_code": new_name}, pluck="warehouse", distinct=True)
# Delete all existing bins to avoid duplicate bins for the same item and warehouse
- frappe.db.sql("delete from `tabBin` where item_code=%s", new_name)
+ frappe.db.delete("Bin", {"item_code": new_name})
for warehouse in repost_stock_for_warehouses:
repost_stock(new_name, warehouse)
From 18e2a33a9be4d4efea13ce5711343413b31358b8 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 10 Mar 2022 16:44:05 +0530
Subject: [PATCH 267/447] fix: fetch new fields in bom from routing
---
erpnext/manufacturing/doctype/bom/bom.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 37d2b9ff97..c0fb63f828 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -192,7 +192,8 @@ class BOM(WebsiteGenerator):
if self.routing:
self.set("operations", [])
fields = ["sequence_id", "operation", "workstation", "description",
- "time_in_mins", "batch_size", "operating_cost", "idx", "hour_rate"]
+ "time_in_mins", "batch_size", "operating_cost", "idx", "hour_rate",
+ "set_cost_based_on_bom_qty", "fixed_time"]
for row in frappe.get_all("BOM Operation", fields = fields,
filters = {'parenttype': 'Routing', 'parent': self.routing}, order_by="sequence_id, idx"):
From 362102e802bb501312a39bb21810336de26696b9 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 10 Mar 2022 16:49:29 +0530
Subject: [PATCH 268/447] fix: dont hardcode hour rate precision
---
erpnext/manufacturing/doctype/bom/bom.py | 2 +-
.../manufacturing/doctype/bom_operation/bom_operation.json | 5 +++--
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index c0fb63f828..a8ce1d7642 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -198,7 +198,7 @@ class BOM(WebsiteGenerator):
for row in frappe.get_all("BOM Operation", fields = fields,
filters = {'parenttype': 'Routing', 'parent': self.routing}, order_by="sequence_id, idx"):
child = self.append('operations', row)
- child.hour_rate = flt(row.hour_rate / self.conversion_rate, 2)
+ child.hour_rate = flt(row.hour_rate / self.conversion_rate, child.precision("hour_rate"))
def set_bom_material_details(self):
for item in self.get("items"):
diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
index c7be7efc9e..341f9692c4 100644
--- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
+++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
@@ -66,7 +66,8 @@
"label": "Hour Rate",
"oldfieldname": "hour_rate",
"oldfieldtype": "Currency",
- "options": "currency"
+ "options": "currency",
+ "precision": "2"
},
{
"description": "In minutes",
@@ -186,7 +187,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-12-15 03:00:00.473173",
+ "modified": "2022-03-10 06:19:08.462027",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operation",
From 7dd10367f49b9f67def80aa0daa612848f00092c Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 10 Mar 2022 17:07:57 +0530
Subject: [PATCH 269/447] fix: only update valuation rate if not None
---
erpnext/stock/stock_ledger.py | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index ba1081f4dc..353bfa452b 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -838,11 +838,13 @@ class update_entries_after(object):
for warehouse, data in self.data.items():
bin_name = get_or_make_bin(self.item_code, warehouse)
- frappe.db.set_value('Bin', bin_name, {
- "valuation_rate": data.valuation_rate,
+ updated_values = {
"actual_qty": data.qty_after_transaction,
"stock_value": data.stock_value
- })
+ }
+ if data.valuation_rate is not None:
+ updated_values["valuation_rate"] = data.valuation_rate
+ frappe.db.set_value('Bin', bin_name, updated_values)
def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
From 6c54be0dcd83cc021844017f2ceeed58a88ca920 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 10 Mar 2022 17:37:29 +0530
Subject: [PATCH 270/447] test: flaky MR report test
all test records are on same day so sorting was random in report rows.
---
...test_requested_items_to_order_and_receive.py | 17 ++++++++---------
1 file changed, 8 insertions(+), 9 deletions(-)
diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py
index f3c751c5c3..a533da00e3 100644
--- a/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py
+++ b/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py
@@ -17,8 +17,8 @@ class TestRequestedItemsToOrderAndReceive(FrappeTestCase):
def setUp(self) -> None:
create_item("Test MR Report Item")
self.setup_material_request() # to order and receive
- self.setup_material_request(order=True) # to receive (ordered)
- self.setup_material_request(order=True, receive=True) # complete (ordered & received)
+ self.setup_material_request(order=True, days=1) # to receive (ordered)
+ self.setup_material_request(order=True, receive=True, days=2) # complete (ordered & received)
self.filters = frappe._dict(
company="_Test Company", from_date=today(), to_date=add_days(today(), 30),
@@ -32,9 +32,8 @@ class TestRequestedItemsToOrderAndReceive(FrappeTestCase):
data = get_data(self.filters)
self.assertEqual(len(data), 2) # MRs today should be fetched
- self.filters.from_date = add_days(today(), 1)
- data = get_data(self.filters)
- self.assertEqual(len(data), 0) # MRs today should not be fetched as from date is tomorrow
+ data = get_data(self.filters.update({"from_date": add_days(today(), 10)}))
+ self.assertEqual(len(data), 0) # MRs today should not be fetched as from date is in future
def test_ordered_received_material_requests(self):
data = get_data(self.filters)
@@ -44,19 +43,19 @@ class TestRequestedItemsToOrderAndReceive(FrappeTestCase):
self.assertEqual(data[0].ordered_qty, 0.0)
self.assertEqual(data[1].ordered_qty, 57.0)
- def setup_material_request(self, order=False, receive=False):
+ def setup_material_request(self, order=False, receive=False, days=0):
po = None
test_records = frappe.get_test_records('Material Request')
mr = frappe.copy_doc(test_records[0])
- mr.transaction_date = today()
- mr.schedule_date = add_days(today(), 1)
+ mr.transaction_date = add_days(today(), days)
+ mr.schedule_date = add_days(mr.transaction_date, 1)
for row in mr.items:
row.item_code = "Test MR Report Item"
row.item_name = "Test MR Report Item"
row.description = "Test MR Report Item"
row.uom = "Nos"
- row.schedule_date = add_days(today(), 1)
+ row.schedule_date = mr.schedule_date
mr.submit()
if order or receive:
From 472fe8e3191764d281b54c0c49c83a88b26deed9 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 10 Mar 2022 17:39:55 +0530
Subject: [PATCH 271/447] test: submit PR directly
---
.../stock/doctype/purchase_receipt/test_purchase_receipt.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index fa28f2252d..0017fa7ee1 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -769,8 +769,7 @@ class TestPurchaseReceipt(FrappeTestCase):
update_purchase_receipt_status,
)
- pr = make_purchase_receipt(do_not_submit=True)
- pr.submit()
+ pr = make_purchase_receipt()
update_purchase_receipt_status(pr.name, "Closed")
self.assertEqual(
From d596e0e4dff86cf8cde640bd18a54ee159276a4c Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 10 Mar 2022 20:56:36 +0530
Subject: [PATCH 272/447] fix: Shipping rule application fixes
---
.../doctype/shipping_rule/shipping_rule.py | 3 +--
erpnext/controllers/taxes_and_totals.py | 5 ++++-
.../public/js/controllers/taxes_and_totals.js | 4 +++-
.../doctype/sales_order/test_sales_order.py | 22 +++++++++++++++++++
4 files changed, 30 insertions(+), 4 deletions(-)
diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py
index 792e7d21a7..7e5129911e 100644
--- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py
+++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py
@@ -71,8 +71,7 @@ class ShippingRule(Document):
if doc.currency != doc.company_currency:
shipping_amount = flt(shipping_amount / doc.conversion_rate, 2)
- if shipping_amount:
- self.add_shipping_rule_to_tax_table(doc, shipping_amount)
+ self.add_shipping_rule_to_tax_table(doc, shipping_amount)
def get_shipping_amount_from_rules(self, value):
for condition in self.get("conditions"):
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index a1bb6670c4..d362cdde11 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -37,6 +37,8 @@ class calculate_taxes_and_totals(object):
self.set_discount_amount()
self.apply_discount_amount()
+ self.calculate_shipping_charges()
+
if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
self.calculate_total_advance()
@@ -50,7 +52,6 @@ class calculate_taxes_and_totals(object):
self.initialize_taxes()
self.determine_exclusive_rate()
self.calculate_net_total()
- self.calculate_shipping_charges()
self.calculate_taxes()
self.manipulate_grand_total_for_inclusive_tax()
self.calculate_totals()
@@ -276,6 +277,8 @@ class calculate_taxes_and_totals(object):
shipping_rule = frappe.get_doc("Shipping Rule", self.doc.shipping_rule)
shipping_rule.apply(self.doc)
+ self._calculate()
+
def calculate_taxes(self):
rounding_adjustment_computed = self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment')
if not rounding_adjustment_computed:
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index ae0e2a3f6f..9fec43b74e 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -39,6 +39,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
this._calculate_taxes_and_totals();
this.calculate_discount_amount();
+ this.calculate_shipping_charges();
+
// Advance calculation applicable to Sales /Purchase Invoice
if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype)
&& this.frm.doc.docstatus < 2 && !this.frm.doc.is_return) {
@@ -81,7 +83,6 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
this.initialize_taxes();
this.determine_exclusive_rate();
this.calculate_net_total();
- this.calculate_shipping_charges();
this.calculate_taxes();
this.manipulate_grand_total_for_inclusive_tax();
this.calculate_totals();
@@ -275,6 +276,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]);
if (frappe.meta.get_docfield(this.frm.doc.doctype, "shipping_rule", this.frm.doc.name)) {
this.shipping_rule();
+ this._calculate_taxes_and_totals();
}
}
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index b5284793e1..86a08828b2 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -1477,6 +1477,28 @@ class TestSalesOrder(FrappeTestCase):
self.assertEqual(so.items[0].work_order_qty, wo.produced_qty)
self.assertEqual(mr.status, "Manufactured")
+ def test_sales_order_with_shipping_rule(self):
+ from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule
+ shipping_rule = create_shipping_rule(shipping_rule_type = "Selling", shipping_rule_name = "Shipping Rule - Sales Invoice Test")
+ sales_order = make_sales_order(do_not_save=True)
+ sales_order.shipping_rule = shipping_rule.name
+
+ sales_order.items[0].qty = 1
+ sales_order.save()
+ self.assertEqual(sales_order.taxes[0].tax_amount, 50)
+
+ sales_order.items[0].qty = 2
+ sales_order.save()
+ self.assertEqual(sales_order.taxes[0].tax_amount, 100)
+
+ sales_order.items[0].qty = 3
+ sales_order.save()
+ self.assertEqual(sales_order.taxes[0].tax_amount, 200)
+
+ sales_order.items[0].qty = 21
+ sales_order.save()
+ self.assertEqual(sales_order.taxes[0].tax_amount, 0)
+
def automatically_fetch_payment_terms(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")
accounts_settings.automatically_fetch_payment_terms = enable
From 8173e6a8ea8b2659fca6acfcd19a06bde6b89869 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Fri, 11 Mar 2022 15:30:01 +0530
Subject: [PATCH 273/447] fix: simplify insufficient leave balance message
---
erpnext/hr/doctype/leave_application/leave_application.py | 7 +------
.../employee_leave_balance/employee_leave_balance.py | 6 +++---
2 files changed, 4 insertions(+), 9 deletions(-)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 040803d010..86c0d0d570 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -295,12 +295,7 @@ class LeaveApplication(Document):
if leave_balance_for_consumption != self.leave_balance:
msg = _("Warning: Insufficient leave balance for Leave Type {0} in this allocation.").format(frappe.bold(self.leave_type))
msg += " "
- msg += _("Actual leave balance is {0} but only {1} leave(s) can be consumed between {2} (Application Date) and {3} (Allocation Expiry).").format(
- frappe.bold(self.leave_balance), frappe.bold(leave_balance_for_consumption),
- frappe.bold(formatdate(self.from_date)),
- frappe.bold(formatdate(alloc_on_from_date.to_date)))
- msg += " "
- msg += _("Remaining leaves would be compensated in the next allocation.")
+ msg += _("Actual balances aren't available because the leave application spans over different leave allocations. You can still apply for leaves which would be compensated during the next allocation.")
else:
msg = _("Warning: Insufficient leave balance for Leave Type {0}.").format(frappe.bold(self.leave_type))
diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
index 3324ede1dd..66c1d25d59 100644
--- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
@@ -3,7 +3,7 @@
from itertools import groupby
-from typing import Dict, List, Tuple
+from typing import Dict, List, Optional, Tuple
import frappe
from frappe import _
@@ -17,7 +17,7 @@ from erpnext.hr.doctype.leave_application.leave_application import (
Filters = frappe._dict
-def execute(filters: Filters = None) -> Tuple:
+def execute(filters: Optional[Filters] = None) -> Tuple:
if filters.to_date <= filters.from_date:
frappe.throw(_('"From Date" can not be greater than or equal to "To Date"'))
@@ -162,7 +162,7 @@ def get_conditions(filters: Filters) -> Dict:
return conditions
-def get_department_leave_approver_map(department: str = None):
+def get_department_leave_approver_map(department: Optional[str] = None):
# get current department and all its child
department_list = frappe.get_list('Department',
filters={'disabled': 0},
From d9c91748f4e8eb37ec1c76ee5718c38de8ce9f65 Mon Sep 17 00:00:00 2001
From: Rohan Bansal
Date: Tue, 15 Feb 2022 16:35:04 +0530
Subject: [PATCH 274/447] fix: if an item code is too long, truncate before
setting BOM name
---
erpnext/manufacturing/doctype/bom/bom.py | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index a8ce1d7642..3c325037e5 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -121,7 +121,21 @@ class BOM(WebsiteGenerator):
else:
idx = 1
- name = 'BOM-' + self.item + ('-%.3i' % idx)
+ prefix = self.doctype
+ suffix = "%.3i" % idx
+ bom_name = prefix + "-" + self.item + "-" + suffix
+
+ if len(bom_name) <= 140:
+ name = bom_name
+ else:
+ # since max characters for name is 140, remove enough characters from the
+ # item name to fit the prefix, suffix and the separators
+ truncated_length = 140 - (len(prefix) + len(suffix) + 2)
+ truncated_item_name = self.item[:truncated_length]
+ # if a partial word is found after truncate, remove the extra characters
+ truncated_item_name = truncated_item_name.rsplit(" ", 1)[0]
+ name = prefix + "-" + truncated_item_name + "-" + suffix
+
if frappe.db.exists("BOM", name):
conflicting_bom = frappe.get_doc("BOM", name)
From e2c99e02a95a87021786a0666e97e174a3f65a44 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 16 Feb 2022 12:35:34 +0530
Subject: [PATCH 275/447] test: bom for item_code that is >VARCHAR_LEN
---
erpnext/manufacturing/doctype/bom/test_bom.py | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 3cc91b341c..e472388d36 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -432,6 +432,15 @@ class TestBOM(FrappeTestCase):
self.assertEqual(bom.transfer_material_against, "Work Order")
bom.delete()
+ def test_bom_name_length(self):
+ """ test >140 char names"""
+ bom_tree = {
+ "x" * 140 : {
+ " ".join(["abc"] * 35): {}
+ }
+ }
+ create_nested_bom(bom_tree, prefix="")
+
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
From 7f2670941ca7f2e41a37a8ea3ed186a0fa04b57c Mon Sep 17 00:00:00 2001
From: Rohan Bansal
Date: Wed, 16 Feb 2022 15:55:25 +0530
Subject: [PATCH 276/447] fix: improve bom autoname logic
---
erpnext/manufacturing/doctype/bom/bom.py | 38 ++++++++++++++----------
1 file changed, 23 insertions(+), 15 deletions(-)
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 3c325037e5..2c342c0d69 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
import functools
+import re
from collections import deque
from operator import itemgetter
from typing import List
@@ -103,27 +104,34 @@ class BOM(WebsiteGenerator):
)
def autoname(self):
- names = frappe.db.sql_list("""select name from `tabBOM` where item=%s""", self.item)
+ existing_boms = frappe.get_all("BOM", filters={"item": self.item})
+ if existing_boms:
+ existing_bom_names = [bom.name for bom in existing_boms]
- if names:
- # name can be BOM/ITEM/001, BOM/ITEM/001-1, BOM-ITEM-001, BOM-ITEM-001-1
+ # split by "/" and "-"
+ delimiters = ["/", "-"]
+ pattern = "|".join(map(re.escape, delimiters))
+ bom_parts = [re.split(pattern, bom_name) for bom_name in existing_bom_names]
- # split by item
- names = [name.split(self.item, 1) for name in names]
- names = [d[-1][1:] for d in filter(lambda x: len(x) > 1 and x[-1], names)]
+ # filter out BOMs that do not follow the following formats:
+ # - BOM/ITEM/001
+ # - BOM/ITEM/001-1
+ # - BOM-ITEM-001
+ # - BOM-ITEM-001-1
+ valid_bom_parts = list(filter(lambda x: len(x) > 1 and x[-1], bom_parts))
- # split by (-) if cancelled
- if names:
- names = [cint(name.split('-')[-1]) for name in names]
- idx = max(names) + 1
+ # extract the current index from the BOM parts
+ if valid_bom_parts:
+ indexes = [cint(part[-1]) for part in valid_bom_parts]
+ index = max(indexes) + 1
else:
- idx = 1
+ index = 1
else:
- idx = 1
+ index = 1
prefix = self.doctype
- suffix = "%.3i" % idx
- bom_name = prefix + "-" + self.item + "-" + suffix
+ suffix = "%.3i" % index # convert index to string (1 -> "001")
+ bom_name = f"{prefix}-{self.item}-{suffix}"
if len(bom_name) <= 140:
name = bom_name
@@ -134,7 +142,7 @@ class BOM(WebsiteGenerator):
truncated_item_name = self.item[:truncated_length]
# if a partial word is found after truncate, remove the extra characters
truncated_item_name = truncated_item_name.rsplit(" ", 1)[0]
- name = prefix + "-" + truncated_item_name + "-" + suffix
+ name = f"{prefix}-{truncated_item_name}-{suffix}"
if frappe.db.exists("BOM", name):
conflicting_bom = frappe.get_doc("BOM", name)
From 6b58d534030df956f9983c003741ad69263aa287 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 26 Feb 2022 11:36:44 +0530
Subject: [PATCH 277/447] refactor: split versioning code for testability
---
erpnext/manufacturing/doctype/bom/bom.py | 46 ++++++++++---------
erpnext/manufacturing/doctype/bom/test_bom.py | 21 +++++++++
2 files changed, 46 insertions(+), 21 deletions(-)
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 2c342c0d69..817b8e986e 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -104,28 +104,9 @@ class BOM(WebsiteGenerator):
)
def autoname(self):
- existing_boms = frappe.get_all("BOM", filters={"item": self.item})
+ existing_boms = frappe.get_all("BOM", filters={"item": self.item}, pluck="name")
if existing_boms:
- existing_bom_names = [bom.name for bom in existing_boms]
-
- # split by "/" and "-"
- delimiters = ["/", "-"]
- pattern = "|".join(map(re.escape, delimiters))
- bom_parts = [re.split(pattern, bom_name) for bom_name in existing_bom_names]
-
- # filter out BOMs that do not follow the following formats:
- # - BOM/ITEM/001
- # - BOM/ITEM/001-1
- # - BOM-ITEM-001
- # - BOM-ITEM-001-1
- valid_bom_parts = list(filter(lambda x: len(x) > 1 and x[-1], bom_parts))
-
- # extract the current index from the BOM parts
- if valid_bom_parts:
- indexes = [cint(part[-1]) for part in valid_bom_parts]
- index = max(indexes) + 1
- else:
- index = 1
+ index = self.get_next_version_index(existing_boms)
else:
index = 1
@@ -156,6 +137,29 @@ class BOM(WebsiteGenerator):
self.name = name
+ @staticmethod
+ def get_next_version_index(existing_boms: List[str]) -> int:
+ # split by "/" and "-"
+ delimiters = ["/", "-"]
+ pattern = "|".join(map(re.escape, delimiters))
+ bom_parts = [re.split(pattern, bom_name) for bom_name in existing_boms]
+
+ # filter out BOMs that do not follow the following formats:
+ # - BOM/ITEM/001
+ # - BOM/ITEM/001-1
+ # - BOM-ITEM-001
+ # - BOM-ITEM-001-1
+ valid_bom_parts = list(filter(lambda x: len(x) > 1 and x[-1], bom_parts))
+
+ # extract the current index from the BOM parts
+ if valid_bom_parts:
+ indexes = [cint(part[-1]) for part in valid_bom_parts]
+ index = max(indexes) + 1
+ else:
+ index = 1
+
+ return index
+
def validate(self):
self.route = frappe.scrub(self.name).replace('_', '-')
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index e472388d36..346870d1d1 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -441,6 +441,27 @@ class TestBOM(FrappeTestCase):
}
create_nested_bom(bom_tree, prefix="")
+ def test_version_index(self):
+
+ bom = frappe.new_doc("BOM")
+
+ version_index_test_cases = [
+ (1, []),
+ (1, ["BOM#XYZ"]),
+ (2, ["BOM/ITEM/001"]),
+ (2, ["BOM/ITEM/001", "BOM/ITEM/001-1"]),
+ (2, ["BOM-ITEM-001",]),
+ (2, ["BOM-ITEM-001", "BOM-ITEM-001-1"]),
+ (3, ["BOM-ITEM-001", "BOM-ITEM-002", "BOM-ITEM-001-1"]),
+ (4, ["BOM-ITEM-001", "BOM-ITEM-002", "BOM-ITEM-003"]),
+ (2, ["BOM-ITEM-001", "BOM-ITEM-001-1", "BOM-ITEM-001-2"]),
+ ]
+
+ for expected_index, existing_boms in version_index_test_cases:
+ with self.subTest():
+ self.assertEqual(expected_index, bom.get_next_version_index(existing_boms),
+ msg=f"Incorrect index for {existing_boms}")
+
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
From 67d8a7ba86c3131b19b4077055524f69d334314e Mon Sep 17 00:00:00 2001
From: Rohan Bansal
Date: Wed, 2 Mar 2022 15:25:06 +0530
Subject: [PATCH 278/447] fix: cancelled document check
---
erpnext/manufacturing/doctype/bom/bom.py | 15 +++++++++------
erpnext/manufacturing/doctype/bom/test_bom.py | 7 ++-----
2 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 817b8e986e..a025ff740c 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -104,7 +104,13 @@ class BOM(WebsiteGenerator):
)
def autoname(self):
- existing_boms = frappe.get_all("BOM", filters={"item": self.item}, pluck="name")
+ # ignore amended documents while calculating current index
+ existing_boms = frappe.get_all(
+ "BOM",
+ filters={"item": self.item, "amended_from": ["is", "not set"]},
+ pluck="name"
+ )
+
if existing_boms:
index = self.get_next_version_index(existing_boms)
else:
@@ -144,15 +150,12 @@ class BOM(WebsiteGenerator):
pattern = "|".join(map(re.escape, delimiters))
bom_parts = [re.split(pattern, bom_name) for bom_name in existing_boms]
- # filter out BOMs that do not follow the following formats:
- # - BOM/ITEM/001
- # - BOM/ITEM/001-1
- # - BOM-ITEM-001
- # - BOM-ITEM-001-1
+ # filter out BOMs that do not follow the following formats: BOM/ITEM/001, BOM-ITEM-001
valid_bom_parts = list(filter(lambda x: len(x) > 1 and x[-1], bom_parts))
# extract the current index from the BOM parts
if valid_bom_parts:
+ # handle cancelled and submitted documents
indexes = [cint(part[-1]) for part in valid_bom_parts]
index = max(indexes) + 1
else:
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 346870d1d1..6f9dff454e 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -449,12 +449,9 @@ class TestBOM(FrappeTestCase):
(1, []),
(1, ["BOM#XYZ"]),
(2, ["BOM/ITEM/001"]),
- (2, ["BOM/ITEM/001", "BOM/ITEM/001-1"]),
- (2, ["BOM-ITEM-001",]),
- (2, ["BOM-ITEM-001", "BOM-ITEM-001-1"]),
- (3, ["BOM-ITEM-001", "BOM-ITEM-002", "BOM-ITEM-001-1"]),
+ (2, ["BOM-ITEM-001"]),
+ (3, ["BOM-ITEM-001", "BOM-ITEM-002"]),
(4, ["BOM-ITEM-001", "BOM-ITEM-002", "BOM-ITEM-003"]),
- (2, ["BOM-ITEM-001", "BOM-ITEM-001-1", "BOM-ITEM-001-2"]),
]
for expected_index, existing_boms in version_index_test_cases:
From 94d0f8d4e79aa87d66f73bc7056cf5faf9114588 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Fri, 11 Mar 2022 16:48:53 +0530
Subject: [PATCH 279/447] test: actual bom naming test
---
erpnext/manufacturing/doctype/bom/test_bom.py | 36 +++++++++++++++++++
1 file changed, 36 insertions(+)
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 6f9dff454e..4417123178 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -459,6 +459,42 @@ class TestBOM(FrappeTestCase):
self.assertEqual(expected_index, bom.get_next_version_index(existing_boms),
msg=f"Incorrect index for {existing_boms}")
+ def test_bom_versioning(self):
+ bom_tree = {
+ frappe.generate_hash(length=10) : {
+ frappe.generate_hash(length=10): {}
+ }
+ }
+ bom = create_nested_bom(bom_tree, prefix="")
+ self.assertEqual(int(bom.name.split("-")[-1]), 1)
+ original_bom_name = bom.name
+
+ bom.cancel()
+ bom.reload()
+ self.assertEqual(bom.name, original_bom_name)
+
+ # create a new amendment
+ amendment = frappe.copy_doc(bom)
+ amendment.docstatus = 0
+ amendment.amended_from = bom.name
+
+ amendment.save()
+ amendment.submit()
+ amendment.reload()
+
+ self.assertNotEqual(amendment.name, bom.name)
+ # `origname-001-1` version
+ self.assertEqual(int(amendment.name.split("-")[-1]), 1)
+ self.assertEqual(int(amendment.name.split("-")[-2]), 1)
+
+ # create a new version
+ version = frappe.copy_doc(amendment)
+ version.docstatus = 0
+ version.amended_from = None
+ version.save()
+ self.assertNotEqual(amendment.name, version.name)
+ self.assertEqual(int(version.name.split("-")[-1]), 2)
+
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
From b085e96a1265aebddb895ea4a5e8df6b5f9e72bc Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Fri, 11 Mar 2022 18:28:50 +0530
Subject: [PATCH 280/447] revert: "Merge pull request #29290 from
s-aga-r/fix/delivery-note/billed-amount" (#29782) (#29807)
* Revert "Merge pull request #29290 from s-aga-r/fix/delivery-note/billed-amount"
This reverts commit 038f94955006c88209f9df28e3a785c59a4ddb28, reversing
changes made to c7b491843476bca89be02851ccafb7e409876609.
* fix: linter
(cherry picked from commit 7fa46f77e0bdbc516b3c0cb0fb20594ee7fa398b)
# Conflicts:
# erpnext/patches.txt
Co-authored-by: Sagar Sharma
---
.../doctype/sales_invoice/sales_invoice.py | 6 +++---
.../stock/doctype/delivery_note/delivery_note.py | 16 ++++------------
2 files changed, 7 insertions(+), 15 deletions(-)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 573da276a2..862ac81ff3 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -1255,14 +1255,14 @@ class SalesInvoice(SellingController):
def update_billing_status_in_dn(self, update_modified=True):
updated_delivery_notes = []
for d in self.get("items"):
- if d.so_detail:
- updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
- elif d.dn_detail:
+ if d.dn_detail:
billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item`
where dn_detail=%s and docstatus=1""", d.dn_detail)
billed_amt = billed_amt and billed_amt[0][0] or 0
frappe.db.set_value("Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified)
updated_delivery_notes.append(d.delivery_note)
+ elif d.so_detail:
+ updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
for dn in set(updated_delivery_notes):
frappe.get_doc("Delivery Note", dn).update_billing_percentage(update_modified=update_modified)
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 2a4d63954a..fbcc8038aa 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -342,25 +342,21 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True):
from frappe.query_builder.functions import Sum
# Billed against Sales Order directly
- si = frappe.qb.DocType("Sales Invoice").as_("si")
si_item = frappe.qb.DocType("Sales Invoice Item").as_("si_item")
sum_amount = Sum(si_item.amount).as_("amount")
- billed_against_so = frappe.qb.from_(si).from_(si_item).select(sum_amount).where(
- (si_item.parent == si.name) &
+ billed_against_so = frappe.qb.from_(si_item).select(sum_amount).where(
(si_item.so_detail == so_detail) &
((si_item.dn_detail.isnull()) | (si_item.dn_detail == '')) &
- (si_item.docstatus == 1) &
- (si.update_stock == 0)
+ (si_item.docstatus == 1)
).run()
billed_against_so = billed_against_so and billed_against_so[0][0] or 0
# Get all Delivery Note Item rows against the Sales Order Item row
-
dn = frappe.qb.DocType("Delivery Note").as_("dn")
dn_item = frappe.qb.DocType("Delivery Note Item").as_("dn_item")
- dn_details = frappe.qb.from_(dn).from_(dn_item).select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent, dn_item.stock_qty, dn_item.returned_qty).where(
+ dn_details = frappe.qb.from_(dn).from_(dn_item).select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent).where(
(dn.name == dn_item.parent) &
(dn_item.so_detail == so_detail) &
(dn.docstatus == 1) &
@@ -385,11 +381,7 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True):
# Distribute billed amount directly against SO between DNs based on FIFO
if billed_against_so and billed_amt_agianst_dn < dnd.amount:
- if dnd.returned_qty:
- pending_to_bill = flt(dnd.amount) * (dnd.stock_qty - dnd.returned_qty) / dnd.stock_qty
- else:
- pending_to_bill = flt(dnd.amount)
- pending_to_bill -= billed_amt_agianst_dn
+ pending_to_bill = flt(dnd.amount) - billed_amt_agianst_dn
if pending_to_bill <= billed_against_so:
billed_amt_agianst_dn += pending_to_bill
billed_against_so -= pending_to_bill
From e8a7a54d5ab5f326091915056d4bc86d55ac4572 Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Sat, 12 Mar 2022 19:20:48 +0530
Subject: [PATCH 281/447] fix(pos): do not reset mode of payments in case of
consolidation
---
erpnext/controllers/taxes_and_totals.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index a1bb6670c4..a19699c743 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -633,7 +633,12 @@ class calculate_taxes_and_totals(object):
self.doc.outstanding_amount = flt(total_amount_to_pay - flt(paid_amount) + flt(change_amount),
self.doc.precision("outstanding_amount"))
- if self.doc.doctype == 'Sales Invoice' and self.doc.get('is_pos') and self.doc.get('is_return'):
+ if (
+ self.doc.doctype == 'Sales Invoice'
+ and self.doc.get('is_pos')
+ and self.doc.get('is_return')
+ and not self.doc.get('is_consolidated')
+ ):
self.set_total_amount_to_default_mop(total_amount_to_pay)
self.calculate_paid_amount()
From 96b5cedcf8fdf4242daffd95ca3f40fea3a02199 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Sun, 13 Mar 2022 17:48:37 +0530
Subject: [PATCH 282/447] fix: Do not update ignore prcing rule check
implicitly
---
erpnext/buying/doctype/purchase_order/purchase_order.py | 1 -
erpnext/selling/doctype/sales_order/sales_order.py | 1 -
erpnext/stock/doctype/delivery_note/delivery_note.py | 1 -
erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 1 -
4 files changed, 4 deletions(-)
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 2e7d3063cc..2c6654285f 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -402,7 +402,6 @@ def close_or_unclose_purchase_orders(names, status):
frappe.local.message_log = []
def set_missing_values(source, target):
- target.ignore_pricing_rule = 1
target.run_method("set_missing_values")
target.run_method("calculate_taxes_and_totals")
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index abbb3c9b90..94581b6b1d 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -520,7 +520,6 @@ def make_project(source_name, target_doc=None):
@frappe.whitelist()
def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False):
def set_missing_values(source, target):
- target.ignore_pricing_rule = 1
target.run_method("set_missing_values")
target.run_method("set_po_nos")
target.run_method("calculate_taxes_and_totals")
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 2a4d63954a..e41979ecac 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -447,7 +447,6 @@ def make_sales_invoice(source_name, target_doc=None):
invoiced_qty_map = get_invoiced_qty_map(source_name)
def set_missing_values(source, target):
- target.ignore_pricing_rule = 1
target.run_method("set_missing_values")
target.run_method("set_po_nos")
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 33e40c85f1..25070afaf3 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -762,7 +762,6 @@ def make_purchase_invoice(source_name, target_doc=None):
frappe.throw(_("All items have already been Invoiced/Returned"))
doc = frappe.get_doc(target)
- doc.ignore_pricing_rule = 1
doc.payment_terms_template = get_payment_terms_template(source.supplier, "Supplier", source.company)
doc.run_method("onload")
doc.run_method("set_missing_values")
From 6308e1be91fef76023d17c232519d51069ce31a1 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Sun, 13 Mar 2022 18:13:12 +0530
Subject: [PATCH 283/447] fix: Do not consider cancelled entries
---
.../report/trial_balance_for_party/trial_balance_for_party.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py
index d843dfd3ce..70effce568 100644
--- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py
+++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py
@@ -107,6 +107,7 @@ def get_opening_balances(filters):
select party, sum(debit) as opening_debit, sum(credit) as opening_credit
from `tabGL Entry`
where company=%(company)s
+ and is_cancelled=0
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes')
{account_filter}
@@ -133,6 +134,7 @@ def get_balances_within_period(filters):
select party, sum(debit) as debit, sum(credit) as credit
from `tabGL Entry`
where company=%(company)s
+ and is_cancelled = 0
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
and posting_date >= %(from_date)s and posting_date <= %(to_date)s
and ifnull(is_opening, 'No') = 'No'
From 1c37d2711abebe7db43c89c49869ce63e533e2a7 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Fri, 11 Mar 2022 19:01:26 +0530
Subject: [PATCH 284/447] test: standalone SI creates and attaches serial nos
---
.../accounts/doctype/sales_invoice/test_sales_invoice.py | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 6d929e4386..271e02a50c 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -2615,6 +2615,14 @@ class TestSalesInvoice(unittest.TestCase):
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
+ def test_standalone_serial_no_return(self):
+ si = create_sales_invoice(item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1)
+ si.submit()
+ self.assertTrue(si.items[0].serial_no)
+
+ return si
+
+
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'
From 1a256c62c422c518ba074cec6b275482d8de7d38 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Fri, 11 Mar 2022 19:06:07 +0530
Subject: [PATCH 285/447] fix: attach sr no si standalone credit note
---
erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 9 ++++++++-
.../accounts/doctype/sales_invoice/test_sales_invoice.py | 4 +---
erpnext/stock/doctype/delivery_note/delivery_note.py | 8 +++++++-
.../stock/doctype/delivery_note/test_delivery_note.py | 5 +++++
erpnext/stock/doctype/serial_no/serial_no.py | 7 +++++--
5 files changed, 26 insertions(+), 7 deletions(-)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 862ac81ff3..54217fba83 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -42,7 +42,11 @@ from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timeshe
from erpnext.setup.doctype.company.company import update_company_current_month_sales
from erpnext.stock.doctype.batch.batch import set_batch_nos
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
-from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos
+from erpnext.stock.doctype.serial_no.serial_no import (
+ get_delivery_note_serial_no,
+ get_serial_nos,
+ update_serial_nos_after_submit,
+)
from erpnext.stock.utils import calculate_mapped_packed_items_return
form_grid_templates = {
@@ -226,6 +230,9 @@ class SalesInvoice(SellingController):
# because updating reserved qty in bin depends upon updated delivered qty in SO
if self.update_stock == 1:
self.update_stock_ledger()
+ if self.is_return and self.update_stock:
+ update_serial_nos_after_submit(self, "items")
+
# this sequence because outstanding may get -ve
self.make_gl_entries()
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 271e02a50c..62c3508ab0 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -2617,11 +2617,9 @@ class TestSalesInvoice(unittest.TestCase):
def test_standalone_serial_no_return(self):
si = create_sales_invoice(item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1)
- si.submit()
+ si.reload()
self.assertTrue(si.items[0].serial_no)
- return si
-
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 314e4ba433..75ccd86153 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -13,7 +13,10 @@ from frappe.utils import cint, flt
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.selling_controller import SellingController
from erpnext.stock.doctype.batch.batch import set_batch_nos
-from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no
+from erpnext.stock.doctype.serial_no.serial_no import (
+ get_delivery_note_serial_no,
+ update_serial_nos_after_submit,
+)
from erpnext.stock.utils import calculate_mapped_packed_items_return
form_grid_templates = {
@@ -220,6 +223,9 @@ class DeliveryNote(SellingController):
# Updating stock ledger should always be called after updating prevdoc status,
# because updating reserved qty in bin depends upon updated delivered qty in SO
self.update_stock_ledger()
+ if self.is_return:
+ update_serial_nos_after_submit(self, "items")
+
self.make_gl_entries()
self.repost_future_sle_and_gle()
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 16c892128a..fc3dce1ee9 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -822,6 +822,11 @@ class TestDeliveryNote(FrappeTestCase):
automatically_fetch_payment_terms(enable=0)
+ def test_standalone_serial_no_return(self):
+ dn = create_delivery_note(item_code="_Test Serialized Item With Series", is_return=True, qty=-1)
+ dn.reload()
+ self.assertTrue(dn.items[0].serial_no)
+
def create_return_delivery_note(**args):
args = frappe._dict(args)
from erpnext.controllers.sales_and_purchase_return import make_return_doc
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index ee08e38f33..bf62f50c97 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -394,7 +394,7 @@ def update_serial_nos(sle, item_det):
if not sle.is_cancelled and not sle.serial_no and cint(sle.actual_qty) > 0 \
and item_det.has_serial_no == 1 and item_det.serial_no_series:
serial_nos = get_auto_serial_nos(item_det.serial_no_series, sle.actual_qty)
- frappe.db.set(sle, "serial_no", serial_nos)
+ sle.db_set("serial_no", serial_nos)
validate_serial_no(sle, item_det)
if sle.serial_no:
auto_make_serial_nos(sle)
@@ -516,13 +516,16 @@ def update_serial_nos_after_submit(controller, parentfield):
if controller.doctype == "Stock Entry":
warehouse = d.t_warehouse
qty = d.transfer_qty
+ elif controller.doctype in ("Sales Invoice", "Delivery Note"):
+ warehouse = d.warehouse
+ qty = d.stock_qty
else:
warehouse = d.warehouse
qty = (d.qty if controller.doctype == "Stock Reconciliation"
else d.stock_qty)
for sle in stock_ledger_entries:
if sle.voucher_detail_no==d.name:
- if not accepted_serial_nos_updated and qty and abs(sle.actual_qty)==qty \
+ if not accepted_serial_nos_updated and qty and abs(sle.actual_qty) == abs(qty) \
and sle.warehouse == warehouse and sle.serial_no != d.serial_no:
d.serial_no = sle.serial_no
frappe.db.set_value(d.doctype, d.name, "serial_no", sle.serial_no)
From 558650bc3ac7293818202b307996831a416d7554 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Sun, 13 Mar 2022 20:02:51 +0530
Subject: [PATCH 286/447] fix: add more type hints
---
.../leave_application/leave_application.py | 41 +++++++++++++------
.../employee_leave_balance_summary.py | 1 -
.../test_employee_leave_balance_summary.py | 8 ++--
3 files changed, 32 insertions(+), 18 deletions(-)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 86c0d0d570..2987c1ef0e 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -1,7 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-from typing import Dict
+from typing import Dict, Optional, Tuple
import frappe
from frappe import _
@@ -148,7 +148,7 @@ class LeaveApplication(Document):
elif self.is_separate_ledger_entry_required(alloc_on_from_date, alloc_on_to_date):
frappe.throw(_("Application period cannot be across two allocation records"), exc=LeaveAcrossAllocationsError)
- def get_allocation_based_on_application_dates(self):
+ def get_allocation_based_on_application_dates(self) -> Tuple[Dict, Dict]:
"""Returns allocation name, from and to dates for application dates"""
def _get_leave_allocation_record(date):
LeaveAllocation = frappe.qb.DocType("Leave Allocation")
@@ -288,7 +288,7 @@ class LeaveApplication(Document):
if self.status != "Rejected" and (leave_balance_for_consumption < self.total_leave_days or not leave_balance_for_consumption):
self.show_insufficient_balance_message(leave_balance_for_consumption)
- def show_insufficient_balance_message(self, leave_balance_for_consumption):
+ def show_insufficient_balance_message(self, leave_balance_for_consumption: float) -> None:
alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates()
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
@@ -482,7 +482,7 @@ class LeaveApplication(Document):
)
create_leave_ledger_entry(self, args, submit)
- def is_separate_ledger_entry_required(self, alloc_on_from_date=None, alloc_on_to_date=None) -> bool:
+ def is_separate_ledger_entry_required(self, alloc_on_from_date: Optional[Dict] = None, alloc_on_to_date: Optional[Dict] = None) -> bool:
"""Checks if application dates fall in separate allocations"""
if ((alloc_on_from_date and not alloc_on_to_date)
or (not alloc_on_from_date and alloc_on_to_date)
@@ -563,7 +563,7 @@ class LeaveApplication(Document):
create_leave_ledger_entry(self, args, submit)
-def get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, from_date):
+def get_allocation_expiry_for_cf_leaves(employee: str, leave_type: str, to_date: str, from_date: str) -> str:
''' Returns expiry of carry forward allocation in leave ledger entry '''
expiry = frappe.get_all("Leave Ledger Entry",
filters={
@@ -574,10 +574,14 @@ def get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, from_date
'to_date': ['between', (from_date, to_date)],
'docstatus': 1
},fields=['to_date'])
- return expiry[0]['to_date'] if expiry else None
+ return expiry[0]['to_date'] if expiry else ''
+
@frappe.whitelist()
-def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day = None, half_day_date = None, holiday_list = None):
+def get_number_of_leave_days(employee: str, leave_type: str, from_date: str, to_date: str, half_day: Optional[int] = None,
+ half_day_date: Optional[str] = None, holiday_list: Optional[str] = None) -> float:
+ """Returns number of leave days between 2 dates after considering half day and holidays
+ (Based on the include_holiday setting in Leave Type)"""
number_of_days = 0
if cint(half_day) == 1:
if from_date == to_date:
@@ -593,6 +597,7 @@ def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day
number_of_days = flt(number_of_days) - flt(get_holidays(employee, from_date, to_date, holiday_list=holiday_list))
return number_of_days
+
@frappe.whitelist()
def get_leave_details(employee, date):
allocation_records = get_leave_allocation_records(employee, date)
@@ -633,8 +638,8 @@ def get_leave_details(employee, date):
@frappe.whitelist()
-def get_leave_balance_on(employee, leave_type, date, to_date=None,
- consider_all_leaves_in_the_allocation_period=False, for_consumption=False):
+def get_leave_balance_on(employee: str, leave_type: str, date: str, to_date: str = None,
+ consider_all_leaves_in_the_allocation_period: bool = False, for_consumption: bool = False):
'''
Returns leave balance till date
:param employee: employee name
@@ -715,8 +720,9 @@ def get_leave_allocation_records(employee, date, leave_type=None):
}))
return allocated_leaves
-def get_leaves_pending_approval_for_period(employee, leave_type, from_date, to_date):
- ''' Returns leaves that are pending approval '''
+
+def get_leaves_pending_approval_for_period(employee: str, leave_type: str, from_date: str, to_date: str) -> float:
+ ''' Returns leaves that are pending for approval '''
leaves = frappe.get_all("Leave Application",
filters={
"employee": employee,
@@ -729,7 +735,8 @@ def get_leaves_pending_approval_for_period(employee, leave_type, from_date, to_d
}, fields=['SUM(total_leave_days) as leaves'])[0]
return leaves['leaves'] if leaves['leaves'] else 0.0
-def get_remaining_leaves(allocation, leaves_taken, date, cf_expiry) -> Dict[str, float]:
+
+def get_remaining_leaves(allocation: Dict, leaves_taken: float, date: str, cf_expiry: str) -> Dict[str, float]:
'''Returns a dict of leave_balance and leave_balance_for_consumption
leave_balance returns the available leave balance
leave_balance_for_consumption returns the minimum leaves remaining after comparing with remaining days for allocation expiry
@@ -755,7 +762,8 @@ def get_remaining_leaves(allocation, leaves_taken, date, cf_expiry) -> Dict[str,
remaining_leaves = _get_remaining_leaves(leave_balance_for_consumption, allocation.to_date)
return frappe._dict(leave_balance=leave_balance, leave_balance_for_consumption=remaining_leaves)
-def get_leaves_for_period(employee, leave_type, from_date, to_date, skip_expired_leaves=True):
+
+def get_leaves_for_period(employee: str, leave_type: str, from_date: str, to_date: str, skip_expired_leaves: bool = True) -> float:
leave_entries = get_leave_entries(employee, leave_type, from_date, to_date)
leave_days = 0
@@ -810,6 +818,7 @@ def get_leave_entries(employee, leave_type, from_date, to_date):
"leave_type": leave_type
}, as_dict=1)
+
@frappe.whitelist()
def get_holidays(employee, from_date, to_date, holiday_list = None):
'''get holidays between two dates for the given employee'''
@@ -826,6 +835,7 @@ def is_lwp(leave_type):
lwp = frappe.db.sql("select is_lwp from `tabLeave Type` where name = %s", leave_type)
return lwp and cint(lwp[0][0]) or 0
+
@frappe.whitelist()
def get_events(start, end, filters=None):
from frappe.desk.reportview import get_filters_cond
@@ -854,6 +864,7 @@ def get_events(start, end, filters=None):
return events
+
def add_department_leaves(events, start, end, employee, company):
department = frappe.db.get_value("Employee", employee, "department")
@@ -934,6 +945,7 @@ def add_block_dates(events, start, end, employee, company):
})
cnt+=1
+
def add_holidays(events, start, end, employee, company):
applicable_holiday_list = get_holiday_list_for_employee(employee, company)
if not applicable_holiday_list:
@@ -950,6 +962,7 @@ def add_holidays(events, start, end, employee, company):
"name": holiday.name
})
+
@frappe.whitelist()
def get_mandatory_approval(doctype):
mandatory = ""
@@ -962,6 +975,7 @@ def get_mandatory_approval(doctype):
return mandatory
+
def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
query = """
select employee, leave_type, from_date, to_date, total_leave_days
@@ -997,6 +1011,7 @@ def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
return leave_days
+
@frappe.whitelist()
def get_leave_approver(employee):
leave_approver, department = frappe.db.get_value("Employee",
diff --git a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py
index eee2eb816c..71c18bb51f 100644
--- a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py
+++ b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py
@@ -12,7 +12,6 @@ from erpnext.hr.report.employee_leave_balance.employee_leave_balance import (
def execute(filters=None):
- filters = frappe._dict(filters or {})
leave_types = frappe.db.sql_list("select name from `tabLeave Type` order by name asc")
columns = get_columns(leave_types)
diff --git a/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py
index 9b953de0dc..b76858d843 100644
--- a/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py
+++ b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py
@@ -64,11 +64,11 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
leave_application2 = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type')
leave_application2.reload()
- filters = {
+ filters = frappe._dict({
'date': self.date,
'company': '_Test Company',
'employee': self.employee_id
- }
+ })
report = execute(filters)
@@ -100,11 +100,11 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
# Leave balance should show actual balance, and not "consumption balance as per remaining days", near alloc end date
# eg: 3 days left for alloc to end, leave balance should still be 26 and not 3
- filters = {
+ frappe._dict({
'date': add_days(self.year_end, -3),
'company': '_Test Company',
'employee': self.employee_id
- }
+ })
report = execute(filters)
expected_data = [[
From d61c43758893895943e36242e3add3eb2502ac82 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Sun, 13 Mar 2022 20:30:18 +0530
Subject: [PATCH 287/447] fix: flaky tests
---
.../hr/doctype/leave_application/test_leave_application.py | 2 +-
.../test_employee_leave_balance_summary.py | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 3e75578d18..27f98a2659 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -274,7 +274,7 @@ class TestLeaveApplication(unittest.TestCase):
employee = get_employee()
first_sunday = get_first_sunday(self.holiday_list)
- leave_application = make_leave_application(employee.name, add_days(first_sunday, 1), add_days(first_sunday, 4), leave_type.name)
+ leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name)
leave_application.reload()
self.assertEqual(leave_application.total_leave_days, 4)
self.assertEqual(frappe.db.count('Attendance', {'leave_application': leave_application.name}), 4)
diff --git a/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py
index b76858d843..6f16a8d58c 100644
--- a/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py
+++ b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py
@@ -65,7 +65,7 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
leave_application2.reload()
filters = frappe._dict({
- 'date': self.date,
+ 'date': add_days(leave_application2.to_date, 1),
'company': '_Test Company',
'employee': self.employee_id
})
@@ -100,7 +100,7 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
# Leave balance should show actual balance, and not "consumption balance as per remaining days", near alloc end date
# eg: 3 days left for alloc to end, leave balance should still be 26 and not 3
- frappe._dict({
+ filters = frappe._dict({
'date': add_days(self.year_end, -3),
'company': '_Test Company',
'employee': self.employee_id
From 91fd9d917aa7ef0f7ec23c956f7511094dd900ee Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sun, 13 Mar 2022 13:16:45 +0530
Subject: [PATCH 288/447] test: negative fifo test
---
erpnext/stock/doctype/item/test_item.py | 5 +++-
.../test_stock_ledger_entry.py | 24 +++++++++++++++++++
2 files changed, 28 insertions(+), 1 deletion(-)
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index d57308b2af..669cabc901 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -30,7 +30,10 @@ from erpnext.stock.get_item_details import get_item_details
test_ignore = ["BOM"]
test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"]
-def make_item(item_code, properties=None):
+def make_item(item_code=None, properties=None):
+ if not item_code:
+ item_code = frappe.generate_hash(length=16)
+
if frappe.db.exists("Item", item_code):
return frappe.get_doc("Item", item_code)
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 684a8d4d7c..6a2e7fb9f5 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
@@ -739,6 +739,30 @@ class TestStockLedgerEntry(FrappeTestCase):
{"incoming_rate": sum(rates) * 10}
], sle_filters={"item_code": packed.name})
+ def test_negative_fifo_valuation(self):
+ """
+ When stock goes negative discard FIFO queue.
+ Only pervailing valuation rate should be used for making transactions in such cases.
+ """
+ item = make_item(properties={"allow_negative_stock": 1}).name
+ warehouse = "_Test Warehouse - _TC"
+
+ receipt = make_stock_entry(item_code=item, target=warehouse, qty=10, rate=10)
+ consume1 = make_stock_entry(item_code=item, source=warehouse, qty=15)
+
+ self.assertSLEs(consume1, [
+ {"stock_value": -5 * 10, "stock_queue": [[-5, 10]]}
+ ])
+
+ consume2 = make_stock_entry(item_code=item, source=warehouse, qty=5)
+ self.assertSLEs(consume2, [
+ {"stock_value": -10 * 10, "stock_queue": [[-10, 10]]}
+ ])
+
+ receipt2 = make_stock_entry(item_code=item, target=warehouse, qty=15, rate=15)
+ self.assertSLEs(receipt2, [
+ {"stock_queue": [[5, 15]], "stock_value_difference": 175}
+ ])
def create_repack_entry(**args):
args = frappe._dict(args)
From 941ea1ec74af347ceb186352cc5d301a9af63e48 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sun, 13 Mar 2022 19:39:39 +0530
Subject: [PATCH 289/447] fix(ux): skip items without batch series
---
.../doctype/work_order/test_work_order.py | 24 ++++++++++++++++++-
.../doctype/work_order/work_order.py | 8 +++++++
2 files changed, 31 insertions(+), 1 deletion(-)
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index bc07d22e83..eaf4de716d 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -2,7 +2,7 @@
# License: GNU General Public License v3. See license.txt
import frappe
-from frappe.tests.utils import FrappeTestCase, timeout
+from frappe.tests.utils import FrappeTestCase, change_settings, timeout
from frappe.utils import add_days, add_months, cint, flt, now, today
from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
@@ -976,6 +976,28 @@ class TestWorkOrder(FrappeTestCase):
frappe.db.set_value("Manufacturing Settings", None,
"backflush_raw_materials_based_on", "BOM")
+ @change_settings("Manufacturing Settings", {"make_serial_no_batch_from_work_order": 1})
+ def test_auto_batch_creation(self):
+ from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
+
+ fg_item = frappe.generate_hash(length=20)
+ child_item = frappe.generate_hash(length=20)
+
+ bom_tree = {fg_item: {child_item: {}}}
+
+ create_nested_bom(bom_tree, prefix="")
+
+ item = frappe.get_doc("Item", fg_item)
+ item.has_batch_no = 1
+ item.create_new_batch = 0
+ item.save()
+
+ try:
+ make_wo_order_test_record(item=fg_item)
+ except frappe.MandatoryError:
+ self.fail("Batch generation causing failing in Work Order")
+
+
def update_job_card(job_card, jc_qty=None):
employee = frappe.db.get_value('Employee', {'status': 'Active'}, 'name')
job_card_doc = frappe.get_doc('Job Card', job_card)
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 374ab86cad..8ec80ad0c6 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -333,6 +333,14 @@ class WorkOrder(Document):
if not self.batch_size:
self.batch_size = total_qty
+ batch_auto_creation = frappe.get_cached_value("Item", self.production_item, "create_new_batch")
+ if not batch_auto_creation:
+ frappe.msgprint(
+ _("Batch not created for item {} since it does not have a batch series.")
+ .format(frappe.bold(self.production_item)),
+ alert=True, indicator="orange")
+ return
+
while total_qty > 0:
qty = self.batch_size
if self.batch_size >= total_qty:
From 2e265d9bf6bd187e05a5f177d24251fa013f0edc Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Mon, 14 Mar 2022 11:02:33 +0530
Subject: [PATCH 290/447] test: mode of payments in case of consolidation
---
.../test_pos_invoice_merge_log.py | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
index 89f7f18b42..8909da96fc 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
@@ -83,7 +83,10 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
pos_inv_cn = make_sales_return(pos_inv.name)
pos_inv_cn.set("payments", [])
pos_inv_cn.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -300
+ 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -100
+ })
+ pos_inv_cn.append('payments', {
+ 'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': -200
})
pos_inv_cn.paid_amount = -300
pos_inv_cn.submit()
@@ -98,7 +101,12 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
pos_inv_cn.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice))
- self.assertTrue(frappe.db.get_value("Sales Invoice", pos_inv_cn.consolidated_invoice, "is_return"))
+ consolidated_credit_note = frappe.get_doc("Sales Invoice", pos_inv_cn.consolidated_invoice)
+ self.assertEqual(consolidated_credit_note.is_return, 1)
+ self.assertEqual(consolidated_credit_note.payments[0].mode_of_payment, 'Cash')
+ self.assertEqual(consolidated_credit_note.payments[0].amount, -100)
+ self.assertEqual(consolidated_credit_note.payments[1].mode_of_payment, 'Bank Draft')
+ self.assertEqual(consolidated_credit_note.payments[1].amount, -200)
finally:
frappe.set_user("Administrator")
From 58804b8436e782b520d39ec9e3633a724bf8e75f Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Mon, 14 Mar 2022 12:31:13 +0530
Subject: [PATCH 291/447] fix: cannot create purchase order from sales order
---
erpnext/selling/doctype/sales_order/sales_order.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index f80eaf2757..87f277f65c 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -693,12 +693,12 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
get_ordered_qty(item, so) {
let ordered_qty = item.ordered_qty;
- if (so.packed_items) {
+ if (so.packed_items && so.packed_items.length) {
// calculate ordered qty based on packed items in case of product bundle
let packed_items = so.packed_items.filter(
(pi) => pi.parent_detail_docname == item.name
);
- if (packed_items) {
+ if (packed_items && packed_items.length) {
ordered_qty = packed_items.reduce(
(sum, pi) => sum + flt(pi.ordered_qty),
0
From c1740ced97a1680d2ecd4d310af08074584e8adf Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Mon, 14 Mar 2022 13:43:55 +0530
Subject: [PATCH 292/447] fix: cannot create multicurrency sales order with
product bundles (#30166)
---
erpnext/stock/doctype/packed_item/packed_item.json | 3 ++-
erpnext/stock/doctype/packed_item/packed_item.py | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json
index d6e2e9ce2d..e94c34d7ad 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.json
+++ b/erpnext/stock/doctype/packed_item/packed_item.json
@@ -223,6 +223,7 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Rate",
+ "options": "currency",
"print_hide": 1,
"read_only": 1
},
@@ -239,7 +240,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2022-02-22 12:57:45.325488",
+ "modified": "2022-03-10 15:42:00.265915",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packed Item",
diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py
index 07c2f1f0dd..f9c00c59ba 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.py
+++ b/erpnext/stock/doctype/packed_item/packed_item.py
@@ -185,7 +185,8 @@ def update_packed_item_price_data(pi_row, item_data, doc):
row_data.update({
"company": doc.get("company"),
"price_list": doc.get("selling_price_list"),
- "currency": doc.get("currency")
+ "currency": doc.get("currency"),
+ "conversion_rate": doc.get("conversion_rate"),
})
rate = get_price_list_rate(row_data, item_doc).get("price_list_rate")
From 1af13ca4bfe99def6800c477ab79a01946ee8aa2 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 14 Mar 2022 13:27:44 +0530
Subject: [PATCH 293/447] fix(ux): remove get item buttons from submitted
production plan
---
.../doctype/production_plan/production_plan.json | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json
index 3bfb764ba5..339c6af64a 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.json
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json
@@ -190,7 +190,7 @@
"label": "Select Items to Manufacture"
},
{
- "depends_on": "get_items_from",
+ "depends_on": "eval:doc.get_items_from && doc.docstatus == 0",
"fieldname": "get_items",
"fieldtype": "Button",
"label": "Get Finished Goods for Manufacture"
@@ -198,6 +198,7 @@
{
"fieldname": "po_items",
"fieldtype": "Table",
+ "label": "Assembly Items",
"no_copy": 1,
"options": "Production Plan Item",
"reqd": 1
@@ -350,6 +351,7 @@
"hide_border": 1
},
{
+ "depends_on": "get_items_from",
"fieldname": "sub_assembly_items",
"fieldtype": "Table",
"label": "Sub Assembly Items",
@@ -357,6 +359,7 @@
"options": "Production Plan Sub Assembly Item"
},
{
+ "depends_on": "eval:doc.po_items && doc.po_items.length && doc.docstatus == 0",
"fieldname": "get_sub_assembly_items",
"fieldtype": "Button",
"label": "Get Sub Assembly Items"
@@ -382,7 +385,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-02-23 17:16:10.629378",
+ "modified": "2022-03-14 03:56:23.209247",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",
@@ -404,5 +407,6 @@
}
],
"sort_field": "modified",
- "sort_order": "ASC"
+ "sort_order": "ASC",
+ "states": []
}
\ No newline at end of file
From d3e90ed8c81c347040a9e225ab829b3359afd621 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 14 Mar 2022 13:47:46 +0530
Subject: [PATCH 294/447] fix(patch): remove dead links to ProdPlan Item
---
erpnext/patches.txt | 3 +-
...remove_unknown_links_to_prod_plan_items.py | 34 +++++++++++++++++++
2 files changed, 36 insertions(+), 1 deletion(-)
create mode 100644 erpnext/patches/v13_0/remove_unknown_links_to_prod_plan_items.py
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index ebda8058e0..16d8c730a1 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -358,4 +358,5 @@ erpnext.patches.v13_0.update_accounts_in_loan_docs
erpnext.patches.v14_0.update_batch_valuation_flag
erpnext.patches.v14_0.delete_non_profit_doctypes
erpnext.patches.v14_0.update_employee_advance_status
-erpnext.patches.v13_0.add_cost_center_in_loans
\ No newline at end of file
+erpnext.patches.v13_0.add_cost_center_in_loans
+erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items
diff --git a/erpnext/patches/v13_0/remove_unknown_links_to_prod_plan_items.py b/erpnext/patches/v13_0/remove_unknown_links_to_prod_plan_items.py
new file mode 100644
index 0000000000..317e85e63d
--- /dev/null
+++ b/erpnext/patches/v13_0/remove_unknown_links_to_prod_plan_items.py
@@ -0,0 +1,34 @@
+import frappe
+
+
+def execute():
+ """
+ Remove "production_plan_item" field where linked field doesn't exist in tha table.
+ """
+
+ work_order = frappe.qb.DocType("Work Order")
+ pp_item = frappe.qb.DocType("Production Plan Item")
+
+ broken_work_orders = (
+ frappe.qb
+ .from_(work_order)
+ .left_join(pp_item).on(work_order.production_plan_item == pp_item.name)
+ .select(work_order.name)
+ .where(
+ (work_order.docstatus == 0)
+ & (work_order.production_plan_item.notnull())
+ & (work_order.production_plan_item.like("new-production-plan%"))
+ & (pp_item.name.isnull())
+ )
+ ).run(pluck=True)
+
+ if not broken_work_orders:
+ return
+
+ (frappe.qb
+ .update(work_order)
+ .set(work_order.production_plan_item, None)
+ .where(work_order.name.isin(broken_work_orders))
+ ).run()
+
+
From 12696ffeb444f4284f4a41d5f2de8b143ccba742 Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Mon, 14 Mar 2022 14:44:02 +0530
Subject: [PATCH 295/447] fix: Search query of payroll entry reference in
Journal Entry (#30225) (#30226)
(cherry picked from commit 98a67967a38c4e5b7b1977c4257bd0b23c57eded)
Co-authored-by: Nabin Hait
---
erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index a634dfe8c1..32b0f0f20c 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -719,7 +719,7 @@ def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filte
where reference_type="Payroll Entry")
order by name limit %(start)s, %(page_len)s"""
.format(key=searchfield), {
- 'txt': "%%%s%%" % frappe.db.escape(txt),
+ 'txt': "%%%s%%" % txt,
'start': start, 'page_len': page_len
})
From 6ee904d6411b1a3ba340cd38b6ff1e42b9f0c5f8 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 14 Mar 2022 15:12:57 +0530
Subject: [PATCH 296/447] test: dont resubmit work order
---
erpnext/manufacturing/doctype/work_order/test_work_order.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index eaf4de716d..6a8136de32 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -355,7 +355,6 @@ class TestWorkOrder(FrappeTestCase):
wo_order = make_wo_order_test_record(planned_start_date=now(),
sales_order=so.name, qty=3)
- wo_order.submit()
self.assertEqual(wo_order.docstatus, 1)
allow_overproduction("overproduction_percentage_for_sales_order", 0)
From e8ba1f4e74d86f5e2e7bad13a013c5abdf064463 Mon Sep 17 00:00:00 2001
From: Noah Jacob
Date: Tue, 1 Mar 2022 18:15:02 +0530
Subject: [PATCH 297/447] fix: incorrect balance serial no in stock ledger
report
---
erpnext/stock/report/stock_ledger/stock_ledger.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index 81fa0458f2..57149da359 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -7,6 +7,7 @@ from frappe import _
from frappe.utils import cint, flt
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import get_stock_balance_for
from erpnext.stock.utils import (
is_reposting_item_valuation_in_progress,
update_included_uom_in_report,
@@ -70,7 +71,10 @@ def update_available_serial_nos(available_serial_nos, sle):
serial_nos = get_serial_nos(sle.serial_no)
key = (sle.item_code, sle.warehouse)
if key not in available_serial_nos:
- available_serial_nos.setdefault(key, [])
+ stock_balance = get_stock_balance_for(sle.item_code, sle.warehouse, sle.date.split(' ')[0],
+ sle.date.split(' ')[1], batch_no=sle.batch_no)
+ serials = get_serial_nos(stock_balance['serial_nos']) if stock_balance['serial_nos'] else []
+ available_serial_nos.setdefault(key, serials)
existing_serial_no = available_serial_nos[key]
for sn in serial_nos:
From 00797fa02dba9051c2b67f1476e66c632aa70af2 Mon Sep 17 00:00:00 2001
From: Noah Jacob
Date: Thu, 10 Mar 2022 15:23:46 +0530
Subject: [PATCH 298/447] test: checking balance serial nos in stock ledger
report
---
.../stock_ledger/test_stock_ledger_report.py | 41 +++++++++++++++++++
1 file changed, 41 insertions(+)
create mode 100644 erpnext/stock/report/stock_ledger/test_stock_ledger_report.py
diff --git a/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py b/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py
new file mode 100644
index 0000000000..163b2057c9
--- /dev/null
+++ b/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py
@@ -0,0 +1,41 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_days, today
+
+from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
+ make_serial_item_with_serial,
+)
+from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+from erpnext.stock.report.stock_ledger.stock_ledger import execute
+
+
+class TestStockLedgerReeport(FrappeTestCase):
+ def setUp(self) -> None:
+ make_serial_item_with_serial("_Test Stock Report Serial Item")
+ self.filters = frappe._dict(
+ company="_Test Company", from_date=today(), to_date=add_days(today(), 30),
+ item_code="_Test Stock Report Serial Item"
+ )
+
+ def tearDown(self) -> None:
+ frappe.db.rollback()
+
+ def test_serial_balance(self):
+ item_code = "_Test Stock Report Serial Item"
+ # Checks serials which were added through stock in entry.
+ columns, data = execute(self.filters)
+ self.assertEqual(data[0].in_qty, 2)
+ serials_added = get_serial_nos(data[0].serial_no)
+ self.assertEqual(len(serials_added), 2)
+ # Stock out entry for one of the serials.
+ dn = create_delivery_note(item=item_code, serial_no=serials_added[1])
+ self.filters.voucher_no = dn.name
+ columns, data = execute(self.filters)
+ self.assertEqual(data[0].out_qty, -1)
+ self.assertEqual(data[0].serial_no, serials_added[1])
+ self.assertEqual(data[0].balance_serial_no, serials_added[0])
+
From 60c4593f7bd83e5e559f6679156cd1fdecd8facb Mon Sep 17 00:00:00 2001
From: Noah Jacob
Date: Mon, 14 Mar 2022 10:11:38 +0530
Subject: [PATCH 299/447] chore: removed unrequired batch_no parameter while
fetching stock_balance
---
erpnext/stock/report/stock_ledger/stock_ledger.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index 57149da359..9fde47e061 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -72,7 +72,7 @@ def update_available_serial_nos(available_serial_nos, sle):
key = (sle.item_code, sle.warehouse)
if key not in available_serial_nos:
stock_balance = get_stock_balance_for(sle.item_code, sle.warehouse, sle.date.split(' ')[0],
- sle.date.split(' ')[1], batch_no=sle.batch_no)
+ sle.date.split(' ')[1])
serials = get_serial_nos(stock_balance['serial_nos']) if stock_balance['serial_nos'] else []
available_serial_nos.setdefault(key, serials)
From dcd88ddc877f3e11afa3129cd6fa81c78f06b36d Mon Sep 17 00:00:00 2001
From: marination
Date: Mon, 14 Mar 2022 15:57:24 +0530
Subject: [PATCH 300/447] fix: Add missing currency option in Supplier
Quotation's `rounded_total` field
---
.../buying/doctype/supplier_quotation/supplier_quotation.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
index 023c95d697..933949f516 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
@@ -635,6 +635,7 @@
"fieldname": "rounded_total",
"fieldtype": "Currency",
"label": "Rounded Total",
+ "options": "currency",
"read_only": 1
},
{
@@ -810,7 +811,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-12-11 06:43:20.924080",
+ "modified": "2022-03-14 15:53:02.550969",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",
@@ -875,6 +876,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"timeline_field": "supplier",
"title_field": "title"
}
\ No newline at end of file
From b37559c535ce5cc4589a08a1af95311b360ce810 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Mon, 14 Mar 2022 16:13:35 +0530
Subject: [PATCH 301/447] fix: KSA E-Invoice QR Code showing wrong VAT amount
---
erpnext/regional/saudi_arabia/utils.py | 18 +++++++++++++++++-
1 file changed, 17 insertions(+), 1 deletion(-)
diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py
index a03c3f0994..edcc3ef203 100644
--- a/erpnext/regional/saudi_arabia/utils.py
+++ b/erpnext/regional/saudi_arabia/utils.py
@@ -90,7 +90,7 @@ def create_qr_code(doc, method=None):
tlv_array.append(''.join([tag, length, value]))
# VAT Amount
- vat_amount = str(doc.total_taxes_and_charges)
+ vat_amount = str(get_vat_amount(doc))
tag = bytes([5]).hex()
length = bytes([len(vat_amount)]).hex()
@@ -127,6 +127,22 @@ def create_qr_code(doc, method=None):
doc.db_set('ksa_einv_qr', _file.file_url)
doc.notify_update()
+def get_vat_amount(doc):
+ vat_settings = frappe.db.get_value('KSA VAT Setting', {'company': doc.company})
+ vat_accounts = []
+ vat_amount = 0
+
+ if vat_settings:
+ vat_settings_doc = frappe.get_doc('KSA VAT Setting', vat_settings)
+
+ for row in vat_settings_doc.get('ksa_vat_sales_accounts'):
+ vat_accounts.append(row.account)
+
+ for tax in doc.get('taxes'):
+ if tax.account_head in vat_accounts:
+ vat_amount += tax.tax_amount
+
+ return vat_amount
def delete_qr_code_file(doc, method=None):
region = get_region(doc.company)
From a579a211fd2ad10ce08157c447d0e81988a68dce Mon Sep 17 00:00:00 2001
From: marination
Date: Mon, 14 Mar 2022 16:15:12 +0530
Subject: [PATCH 302/447] chore: Re-arrange fields for consistency with base
currency LHS
---
.../buying/doctype/supplier_quotation/supplier_quotation.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
index 933949f516..567e41fb61 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
@@ -72,8 +72,8 @@
"section_break_46",
"base_grand_total",
"base_rounding_adjustment",
- "base_in_words",
"base_rounded_total",
+ "base_in_words",
"column_break4",
"grand_total",
"rounding_adjustment",
@@ -811,7 +811,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-03-14 15:53:02.550969",
+ "modified": "2022-03-14 16:13:20.284572",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",
From 3cc2e53b0822a221a76b900406eaff4b160da532 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Mon, 14 Mar 2022 16:27:04 +0530
Subject: [PATCH 303/447] fix: Linting Issue
---
erpnext/regional/saudi_arabia/utils.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py
index edcc3ef203..d46f642962 100644
--- a/erpnext/regional/saudi_arabia/utils.py
+++ b/erpnext/regional/saudi_arabia/utils.py
@@ -130,7 +130,7 @@ def create_qr_code(doc, method=None):
def get_vat_amount(doc):
vat_settings = frappe.db.get_value('KSA VAT Setting', {'company': doc.company})
vat_accounts = []
- vat_amount = 0
+ vat_amount = 0
if vat_settings:
vat_settings_doc = frappe.get_doc('KSA VAT Setting', vat_settings)
From 3b86d6c73cf85e64155a177250011614d518a53f Mon Sep 17 00:00:00 2001
From: Sagar Sharma
Date: Mon, 14 Mar 2022 12:05:34 +0530
Subject: [PATCH 304/447] fix: max_qty validation condition
(cherry picked from commit d198c488a4071d13d0891d87f790562febd44b3e)
---
erpnext/manufacturing/doctype/work_order/work_order.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 8ec80ad0c6..7eb40ec660 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -647,12 +647,12 @@ class WorkOrder(Document):
if self.production_plan and self.production_plan_item:
qty_dict = frappe.db.get_value("Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1)
- allowance_qty =flt(frappe.db.get_single_value("Manufacturing Settings",
+ allowance_qty = flt(frappe.db.get_single_value("Manufacturing Settings",
"overproduction_percentage_for_work_order"))/100 * qty_dict.get("planned_qty", 0)
max_qty = qty_dict.get("planned_qty", 0) + allowance_qty - qty_dict.get("ordered_qty", 0)
- if max_qty < 1:
+ if not max_qty > 0:
frappe.throw(_("Cannot produce more item for {0}")
.format(self.production_item), OverProductionError)
elif self.qty > max_qty:
From 884cd814bc7240d841b02af3c66a0a1847024d6f Mon Sep 17 00:00:00 2001
From: Sagar Sharma
Date: Mon, 14 Mar 2022 16:28:23 +0530
Subject: [PATCH 305/447] test: add test for planned_qty
(cherry picked from commit b22bdc5ff762d41242c742bb3f6fda64eb98fd2e)
---
.../doctype/production_plan/test_production_plan.py | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index eeab788d5c..6425374b1b 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -655,6 +655,17 @@ class TestProductionPlan(FrappeTestCase):
]
self.assertFalse(pp.all_items_completed())
+ def test_production_plan_planned_qty(self):
+ pln = create_production_plan(item_code="_Test FG Item", planned_qty=0.55)
+ pln.make_work_order()
+ work_order = frappe.db.get_value('Work Order', {'production_plan': pln.name}, 'name')
+ wo_doc = frappe.get_doc('Work Order', work_order)
+ wo_doc.update({
+ 'wip_warehouse': 'Work In Progress - _TC',
+ 'fg_warehouse': 'Finished Goods - _TC'
+ })
+ wo_doc.submit()
+ self.assertEqual(wo_doc.qty, 0.55)
def create_production_plan(**args):
"""
From 02d64a32c231715e251363d0ec2c6bd52b559c1c Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 14 Mar 2022 16:29:00 +0530
Subject: [PATCH 306/447] fix(ux): negative stock warning
---
.../doctype/stock_settings/stock_settings.js | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.js b/erpnext/stock/doctype/stock_settings/stock_settings.js
index cc0e2cfc42..89ac4b5fc9 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.js
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.js
@@ -13,5 +13,24 @@ frappe.ui.form.on('Stock Settings', {
frm.set_query("default_warehouse", filters);
frm.set_query("sample_retention_warehouse", filters);
+ },
+ allow_negative_stock: function(frm) {
+ if (!frm.doc.allow_negative_stock) {
+ return;
+ }
+
+ let msg = __("Using negative stock disables FIFO/Moving average valuation when inventory is negative.");
+ msg += " ";
+ msg += __("This is considered dangerous from accounting point of view.")
+ msg += " ";
+ msg += ("Do you still want to enable negative inventory?");
+
+ frappe.confirm(
+ msg,
+ () => {},
+ () => {
+ frm.set_value("allow_negative_stock", 0);
+ }
+ );
}
});
From be56efad262cc8233d7bc462aa08d7f02e35ddcb Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Mon, 14 Mar 2022 17:17:01 +0530
Subject: [PATCH 307/447] fix: Itemised tax rate updation
---
erpnext/regional/united_arab_emirates/utils.py | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py
index f350ec4ec2..bdede846fe 100644
--- a/erpnext/regional/united_arab_emirates/utils.py
+++ b/erpnext/regional/united_arab_emirates/utils.py
@@ -26,9 +26,12 @@ def update_itemised_tax_data(doc):
elif row.item_code and itemised_tax.get(row.item_code):
tax_rate = sum([tax.get('tax_rate', 0) for d, tax in itemised_tax.get(row.item_code).items()])
- row.tax_rate = flt(tax_rate, row.precision("tax_rate"))
- row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount"))
- row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount"))
+ meta = frappe.get_meta(row.doctype)
+
+ if meta.has_field('tax_rate'):
+ row.tax_rate = flt(tax_rate, row.precision("tax_rate"))
+ row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount"))
+ row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount"))
def get_account_currency(account):
"""Helper function to get account currency."""
From 61eb9b6c6848253da4108fbe1483c94ea4ec8a26 Mon Sep 17 00:00:00 2001
From: Krithi Ramani
Date: Mon, 14 Mar 2022 17:20:49 +0530
Subject: [PATCH 308/447] feat: Create single PL/DN from several SO. New PR
from latest develop to avoid rebase
---
erpnext/stock/doctype/pick_list/pick_list.py | 168 +++++++++++++-----
.../stock/doctype/pick_list/test_pick_list.py | 98 +++++++++-
2 files changed, 214 insertions(+), 52 deletions(-)
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index b2eaecb586..3ec0324a11 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -24,8 +24,21 @@ class PickList(Document):
def before_save(self):
self.set_item_locations()
+ # set percentage picked in SO
+ for i in self.get('locations'):
+ if i.sales_order and frappe.db.get_value("Sales Order",i.sales_order,"per_picked") == 100:
+ frappe.throw("Row " + str(i.idx) + " has been picked already!")
+
def before_submit(self):
for item in self.locations:
+ # if the user has not entered any picked qty, set it to stock_qty, before submit
+ if item.picked_qty == 0:
+ item.picked_qty = item.stock_qty
+
+ if item.sales_order_item:
+ # update the picked_qty in SO Item
+ self.update_so(item.sales_order_item,item.picked_qty,item.item_code)
+
if not frappe.get_cached_value('Item', item.item_code, 'has_serial_no'):
continue
if not item.serial_no:
@@ -37,6 +50,28 @@ class PickList(Document):
frappe.throw(_('For item {0} at row {1}, count of serial numbers does not match with the picked quantity')
.format(frappe.bold(item.item_code), frappe.bold(item.idx)), title=_("Quantity Mismatch"))
+ def before_cancel(self):
+ #update picked_qty in SO Item on cancel of PL
+ for i in self.get('locations'):
+ if i.sales_order_item:
+ self.update_so(i.sales_order_item,0,i.item_code)
+
+ def update_so(self,so_item,picked_qty,item_code):
+ so_doc = frappe.get_doc("Sales Order",frappe.db.get_value("Sales Order Item",so_item,"parent"))
+ already_picked,actual_qty = frappe.db.get_value("Sales Order Item",so_item,["picked_qty","qty"])
+
+ if self.docstatus == 1:
+ if (((already_picked + picked_qty)/ actual_qty)*100) > (100 + flt(frappe.db.get_single_value('Stock Settings', 'over_delivery_receipt_allowance'))):
+ frappe.throw('You are picking more than required quantity for ' + item_code + '. Check if there is any other pick list created for '+so_doc.name)
+
+ frappe.db.set_value("Sales Order Item",so_item,"picked_qty",already_picked+picked_qty)
+
+ total_picked_qty = sum(flt(d.picked_qty) for d in so_doc.get('items')) + picked_qty
+ total_so_qty = sum(flt(d.stock_qty) for d in so_doc.get('items'))
+ per_picked = total_picked_qty/total_so_qty * 100
+
+ so_doc.db_set("per_picked", flt(per_picked) ,update_modified=False)
+
@frappe.whitelist()
def set_item_locations(self, save=False):
self.validate_for_qty()
@@ -64,10 +99,6 @@ class PickList(Document):
item_doc.name = None
for row in locations:
- row.update({
- 'picked_qty': row.stock_qty
- })
-
location = item_doc.as_dict()
location.update(row)
self.append('locations', location)
@@ -340,63 +371,106 @@ def get_available_item_locations_for_other_item(item_code, from_warehouses, requ
def create_delivery_note(source_name, target_doc=None):
pick_list = frappe.get_doc('Pick List', source_name)
validate_item_locations(pick_list)
-
- sales_orders = [d.sales_order for d in pick_list.locations if d.sales_order]
- sales_orders = set(sales_orders)
-
+ sales_dict = dict()
+ sales_orders = []
delivery_note = None
- for sales_order in sales_orders:
- delivery_note = create_delivery_note_from_sales_order(sales_order,
- delivery_note, skip_item_mapping=True)
+ for d in pick_list.locations:
+ if d.sales_order:
+ sales_orders.append([frappe.db.get_value("Sales Order",d.sales_order,'customer'),d.sales_order])
+
+ from itertools import groupby
+ from operator import itemgetter
- # map rows without sales orders as well
- if not delivery_note:
+ # Group sales orders by customer
+ for key,keydata in groupby(sales_orders,key=itemgetter(0)):
+ sales_dict[key] = set([d[1] for d in keydata])
+
+ if sales_dict:
+ delivery_note = create_dn_with_so(sales_dict,pick_list)
+
+ is_item_wo_so = 0
+ for n in pick_list.locations :
+ if not n.sales_order:
+ is_item_wo_so = 1
+ break;
+ if is_item_wo_so == 1:
+ # Create a DN for items without sales orders as well
+ delivery_note = create_dn_wo_so(pick_list)
+
+ frappe.msgprint(_('Delivery Note(s) created for the Pick List'))
+ return delivery_note
+
+def create_dn_wo_so(pick_list):
delivery_note = frappe.new_doc("Delivery Note")
- item_table_mapper = {
- 'doctype': 'Delivery Note Item',
- 'field_map': {
- 'rate': 'rate',
- 'name': 'so_detail',
- 'parent': 'against_sales_order',
- },
- 'condition': lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
- }
-
- item_table_mapper_without_so = {
- 'doctype': 'Delivery Note Item',
- 'field_map': {
- 'rate': 'rate',
- 'name': 'name',
- 'parent': '',
+ item_table_mapper_without_so = {
+ 'doctype': 'Delivery Note Item',
+ 'field_map': {
+ 'rate': 'rate',
+ 'name': 'name',
+ 'parent': '',
+ }
}
- }
+ map_pl_locations(pick_list,item_table_mapper_without_so,delivery_note)
+ delivery_note.insert(ignore_mandatory = True)
+
+ return delivery_note
+
+
+def create_dn_with_so(sales_dict,pick_list):
+ delivery_note = None
+
+ for customer in sales_dict:
+ for so in sales_dict[customer]:
+ delivery_note = None
+ delivery_note = create_delivery_note_from_sales_order(so,
+ delivery_note, skip_item_mapping=True)
+
+ item_table_mapper = {
+ 'doctype': 'Delivery Note Item',
+ 'field_map': {
+ 'rate': 'rate',
+ 'name': 'so_detail',
+ 'parent': 'against_sales_order',
+ },
+ 'condition': lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
+ }
+ break;
+ if delivery_note:
+ # map all items of all sales orders of that customer
+ for so in sales_dict[customer]:
+ map_pl_locations(pick_list,item_table_mapper,delivery_note,so)
+ delivery_note.insert(ignore_mandatory = True)
+
+ return delivery_note
+
+def map_pl_locations(pick_list,item_mapper,delivery_note,sales_order = None):
for location in pick_list.locations:
- if location.sales_order_item:
- sales_order_item = frappe.get_cached_doc('Sales Order Item', {'name':location.sales_order_item})
- else:
- sales_order_item = None
+ if location.sales_order == sales_order:
+ if location.sales_order_item:
+ sales_order_item = frappe.get_cached_doc('Sales Order Item', {'name':location.sales_order_item})
+ else:
+ sales_order_item = None
- source_doc, table_mapper = [sales_order_item, item_table_mapper] if sales_order_item \
- else [location, item_table_mapper_without_so]
+ source_doc, table_mapper = [sales_order_item, item_mapper] if sales_order_item \
+ else [location, item_mapper]
- dn_item = map_child_doc(source_doc, delivery_note, table_mapper)
+ dn_item = map_child_doc(source_doc, delivery_note, table_mapper)
- if dn_item:
- dn_item.warehouse = location.warehouse
- dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
- dn_item.batch_no = location.batch_no
- dn_item.serial_no = location.serial_no
-
- update_delivery_note_item(source_doc, dn_item, delivery_note)
+ if dn_item:
+ dn_item.warehouse = location.warehouse
+ dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
+ dn_item.batch_no = location.batch_no
+ dn_item.serial_no = location.serial_no
+ update_delivery_note_item(source_doc, dn_item, delivery_note)
set_delivery_note_missing_values(delivery_note)
delivery_note.pick_list = pick_list.name
- delivery_note.customer = pick_list.customer if pick_list.customer else None
+ delivery_note.company = pick_list.company
+ delivery_note.customer = frappe.get_value("Sales Order",sales_order,"customer")
- return delivery_note
@frappe.whitelist()
def create_stock_entry(pick_list):
@@ -561,4 +635,4 @@ def update_common_item_properties(item, location):
item.material_request = location.material_request
item.serial_no = location.serial_no
item.batch_no = location.batch_no
- item.material_request_item = location.material_request_item
+ item.material_request_item = location.material_request_item
\ No newline at end of file
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index f3b6b89784..82d8fe9011 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -7,7 +7,6 @@ from frappe import _dict
test_dependencies = ['Item', 'Sales Invoice', 'Stock Entry', 'Batch']
from frappe.tests.utils import FrappeTestCase
-
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
@@ -188,7 +187,6 @@ class TestPickList(FrappeTestCase):
}]
})
pick_list.set_item_locations()
-
self.assertEqual(pick_list.locations[0].batch_no, oldest_batch_no)
pr1.cancel()
@@ -311,6 +309,7 @@ class TestPickList(FrappeTestCase):
'item_code': '_Test Item',
'qty': 1,
'conversion_factor': 5,
+ 'stock_qty':5,
'delivery_date': frappe.utils.today()
}, {
'item_code': '_Test Item',
@@ -329,9 +328,9 @@ class TestPickList(FrappeTestCase):
'purpose': 'Delivery',
'locations': [{
'item_code': '_Test Item',
- 'qty': 1,
- 'stock_qty': 5,
- 'conversion_factor': 5,
+ 'qty': 2,
+ 'stock_qty': 1,
+ 'conversion_factor': 0.5,
'sales_order': sales_order.name,
'sales_order_item': sales_order.items[0].name ,
}, {
@@ -389,6 +388,95 @@ class TestPickList(FrappeTestCase):
for expected_item, created_item in zip(expected_items, pl.locations):
_compare_dicts(expected_item, created_item)
+ def test_multiple_dn_creation(self):
+ sales_order_1 = frappe.get_doc({
+ 'doctype': 'Sales Order',
+ 'customer': '_Test Customer',
+ 'company': '_Test Company',
+ 'items': [{
+ 'item_code': '_Test Item',
+ 'qty': 1,
+ 'conversion_factor': 1,
+ 'delivery_date': frappe.utils.today()
+ }],
+ }).insert()
+ sales_order_1.submit()
+ sales_order_2 = frappe.get_doc({
+ 'doctype': 'Sales Order',
+ 'customer': '_Test Customer 1',
+ 'company': '_Test Company',
+ 'items': [{
+ 'item_code': '_Test Item 2',
+ 'qty': 1,
+ 'conversion_factor': 1,
+ 'delivery_date': frappe.utils.today()
+ },
+ ],
+ }).insert()
+ sales_order_2.submit()
+ pick_list = frappe.get_doc({
+ 'doctype': 'Pick List',
+ 'company': '_Test Company',
+ 'items_based_on': 'Sales Order',
+ 'purpose': 'Delivery',
+ 'picker':'P001',
+ 'locations': [{
+ 'item_code': '_Test Item ',
+ 'qty': 1,
+ 'stock_qty': 1,
+ 'conversion_factor': 1,
+ 'sales_order': sales_order_1.name,
+ 'sales_order_item': sales_order_1.items[0].name ,
+ }, {
+ 'item_code': '_Test Item 2',
+ 'qty': 1,
+ 'stock_qty': 1,
+ 'conversion_factor': 1,
+ 'sales_order': sales_order_2.name,
+ 'sales_order_item': sales_order_2.items[0].name ,
+ }
+ ]
+ })
+ pick_list.set_item_locations()
+ pick_list.submit()
+ create_delivery_note(pick_list.name)
+ for dn in frappe.get_all("Delivery Note",filters={"pick_list":pick_list.name,"customer":"_Test Customer"},fields={"name"}):
+ for dn_item in frappe.get_doc("Delivery Note",dn.name).get("items"):
+ self.assertEqual(dn_item.item_code, '_Test Item')
+ self.assertEqual(dn_item.against_sales_order,sales_order_1.name)
+ for dn in frappe.get_all("Delivery Note",filters={"pick_list":pick_list.name,"customer":"_Test Customer 1"},fields={"name"}):
+ for dn_item in frappe.get_doc("Delivery Note",dn.name).get("items"):
+ self.assertEqual(dn_item.item_code, '_Test Item 2')
+ self.assertEqual(dn_item.against_sales_order,sales_order_2.name)
+ #test DN creation without so
+ pick_list_1 = frappe.get_doc({
+ 'doctype': 'Pick List',
+ 'company': '_Test Company',
+ 'purpose': 'Delivery',
+ 'picker':'P001',
+ 'locations': [{
+ 'item_code': '_Test Item ',
+ 'qty': 1,
+ 'stock_qty': 1,
+ 'conversion_factor': 1,
+ }, {
+ 'item_code': '_Test Item 2',
+ 'qty': 2,
+ 'stock_qty': 2,
+ 'conversion_factor': 1,
+ }
+ ]
+ })
+ pick_list_1.set_item_locations()
+ pick_list_1.submit()
+ create_delivery_note(pick_list_1.name)
+ for dn in frappe.get_all("Delivery Note",filters={"pick_list":pick_list_1.name},fields={"name"}):
+ for dn_item in frappe.get_doc("Delivery Note",dn.name).get("items"):
+ if dn_item.item_code == '_Test Item':
+ self.assertEqual(dn_item.qty,1)
+ if dn_item.item_code == '_Test Item 2':
+ self.assertEqual(dn_item.qty,2)
+
# def test_pick_list_skips_items_in_expired_batch(self):
# pass
From 466df6bdb76cbd7da88f4d092e608b4c39889929 Mon Sep 17 00:00:00 2001
From: Krithi Ramani
Date: Mon, 14 Mar 2022 17:29:02 +0530
Subject: [PATCH 309/447] to enable selection of SO irrespective of
customer,removed validation for customer
---
erpnext/stock/doctype/pick_list/pick_list.js | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js
index 730fd7a829..ababf3dc42 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.js
+++ b/erpnext/stock/doctype/pick_list/pick_list.js
@@ -146,10 +146,7 @@ frappe.ui.form.on('Pick List', {
customer: frm.doc.customer
};
frm.get_items_btn = frm.add_custom_button(__('Get Items'), () => {
- if (!frm.doc.customer) {
- frappe.msgprint(__('Please select Customer first'));
- return;
- }
+
erpnext.utils.map_current_doc({
method: 'erpnext.selling.doctype.sales_order.sales_order.create_pick_list',
source_doctype: 'Sales Order',
From 7537dac2a4cfa3af092acc01946b3b053274e5b2 Mon Sep 17 00:00:00 2001
From: Wolfram Schmidt
Date: Mon, 14 Mar 2022 13:32:54 +0100
Subject: [PATCH 310/447] fix: translation for accounting
"Aufwand" rather than "Auslage". This is commonly used in accounting.
---
erpnext/translations/de.csv | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index b882b9d3c1..b53e5a1716 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -987,15 +987,15 @@ Expected Delivery Date should be after Sales Order Date,Voraussichtlicher Liefer
Expected End Date,Voraussichtliches Enddatum,
Expected Hrs,Erwartete Stunden,
Expected Start Date,Voraussichtliches Startdatum,
-Expense,Auslage,
+Expense,Aufwand,
Expense / Difference account ({0}) must be a 'Profit or Loss' account,"Aufwands-/Differenz-Konto ({0}) muss ein ""Gewinn oder Verlust""-Konto sein",
Expense Account,Aufwandskonto,
-Expense Claim,Aufwandsabrechnung,
+Expense Claim,Auslagenabrechnung,
Expense Claim for Vehicle Log {0},Auslagenabrechnung für Fahrtenbuch {0},
Expense Claim {0} already exists for the Vehicle Log,Auslagenabrechnung {0} existiert bereits für das Fahrzeug Log,
Expense Claims,Aufwandsabrechnungen,
Expense account is mandatory for item {0},Aufwandskonto ist zwingend für Artikel {0},
-Expenses,Ausgaben,
+Expenses,Aufwendungen,
Expenses Included In Asset Valuation,"Aufwendungen, die in der Vermögensbewertung enthalten sind",
Expenses Included In Valuation,In der Bewertung enthaltene Aufwendungen,
Expired Batches,Abgelaufene Chargen,
From f1a7e3b9ada142453659c805da64182e90803840 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 14 Mar 2022 18:19:52 +0530
Subject: [PATCH 311/447] ci: skip tests on CSV
[skip ci]
---
.github/workflows/patch.yml | 3 +++
.github/workflows/server-tests-mariadb.yml | 2 ++
2 files changed, 5 insertions(+)
diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml
index d05bbbec50..afabe43fec 100644
--- a/.github/workflows/patch.yml
+++ b/.github/workflows/patch.yml
@@ -4,7 +4,10 @@ on:
pull_request:
paths-ignore:
- '**.js'
+ - '**.css'
- '**.md'
+ - '**.html'
+ - '**.csv'
workflow_dispatch:
concurrency:
diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml
index 40f93651f4..69be7656a6 100644
--- a/.github/workflows/server-tests-mariadb.yml
+++ b/.github/workflows/server-tests-mariadb.yml
@@ -4,8 +4,10 @@ on:
pull_request:
paths-ignore:
- '**.js'
+ - '**.css'
- '**.md'
- '**.html'
+ - '**.csv'
push:
branches: [ develop ]
paths-ignore:
From 9415229e8d1faa2112db52d1f322db8be292a44a Mon Sep 17 00:00:00 2001
From: Florian HENRY
Date: Mon, 14 Mar 2022 17:46:28 +0530
Subject: [PATCH 312/447] fix: BOM - clear Quality Inspection Template
according to Inspection Quality Required
Squashed commit of the following:
commit b73fa210b234d2c8067db2c32f94f362b89afe5a
Author: Florian HENRY
Date: Mon Mar 14 08:30:24 2022 +0100
add json tes
commit 984d015a7d9aceb6ea64be7ed9f1cc0caa356714
Author: Florian HENRY
Date: Mon Mar 14 08:30:06 2022 +0100
better test
commit 42061146658598da02eda7cb781bd2cf44c8ca34
Author: Florian HENRY
Date: Fri Mar 11 16:12:57 2022 +0100
update test
commit 6259c0957566600b044fcd4a5e14e69cdff71020
Author: Florian HENRY
Date: Fri Mar 11 13:59:13 2022 +0100
update test
commit cbc4ad9566f6f7f01dd622651458fea309d80954
Merge: faa44b3fdd 94d0f8d4e7
Author: Florian HENRY
Date: Fri Mar 11 12:46:48 2022 +0100
Merge branch 'develop' of https://github.com/frappe/erpnext into dev_fix_30190
commit faa44b3fdd983adfc04bbf04a7ebfef114501bb3
Author: Florian HENRY
Date: Fri Mar 11 12:03:35 2022 +0100
fix: clear Quality Inspection Template according to Inspection Quality Required
---
erpnext/manufacturing/doctype/bom/bom.py | 5 +++++
erpnext/manufacturing/doctype/bom/test_bom.py | 19 +++++++++++++++++++
.../test_records.json | 12 ++++++++++++
3 files changed, 36 insertions(+)
create mode 100644 erpnext/stock/doctype/quality_inspection_template/test_records.json
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index a025ff740c..d3acd5f788 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -170,6 +170,7 @@ class BOM(WebsiteGenerator):
frappe.throw(_("Please select a Company first."), title=_("Mandatory"))
self.clear_operations()
+ self.clear_inspection()
self.validate_main_item()
self.validate_currency()
self.set_conversion_rate()
@@ -425,6 +426,10 @@ class BOM(WebsiteGenerator):
if not self.with_operations:
self.set('operations', [])
+ def clear_inspection(self):
+ if not self.inspection_required:
+ self.set('quality_inspection_template', None)
+
def validate_main_item(self):
""" Validate main FG item"""
item = self.get_item_det(self.item)
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 4417123178..fe6cb0b9c9 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -26,6 +26,9 @@ class TestBOM(FrappeTestCase):
if not frappe.get_value('Item', '_Test Item'):
make_test_records('Item')
+ if not frappe.get_value('Quality Inspection Template', '_Test Quality Inspection Template'):
+ make_test_records('Quality Inspection_Template')
+
def test_get_items(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
items_dict = get_bom_items_as_dict(bom=get_default_bom(),
@@ -495,6 +498,22 @@ class TestBOM(FrappeTestCase):
self.assertNotEqual(amendment.name, version.name)
self.assertEqual(int(version.name.split("-")[-1]), 2)
+ def test_clear_inpection_quality(self):
+
+ bom = frappe.copy_doc(test_records[2])
+ bom.is_active = 0
+ bom.quality_inspection_template = "_Test Quality Inspection Template"
+ bom.inspection_required = 1
+ bom.save()
+
+ self.assertEqual(bom.quality_inspection_template, '_Test Quality Inspection Template')
+
+ bom.inspection_required = 0
+ bom.save()
+ bom.load_from_db()
+
+ self.assertEqual(bom.quality_inspection_template, None)
+
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
diff --git a/erpnext/stock/doctype/quality_inspection_template/test_records.json b/erpnext/stock/doctype/quality_inspection_template/test_records.json
new file mode 100644
index 0000000000..a1b6415b54
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_template/test_records.json
@@ -0,0 +1,12 @@
+[
+ {
+ "quality_inspection_template_name" : "_Test Quality Inspection Template",
+ "item_quality_inspection_parameter" : [
+ {
+ "specification": "_Test Param",
+ "doctype": "Item Quality Inspection Parameter",
+ "parentfield": "item_quality_inspection_parameter"
+ }
+ ]
+ }
+]
From dc257665f19e762b271f0931b5796e8afe4942ba Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 14 Mar 2022 18:24:18 +0530
Subject: [PATCH 313/447] ci: allow stable branch merge for release
[skip ci]
---
.mergify.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.mergify.yml b/.mergify.yml
index b7d1df4524..315d90febc 100644
--- a/.mergify.yml
+++ b/.mergify.yml
@@ -7,6 +7,8 @@ pull_request_rules:
- author!=gavindsouza
- author!=rohitwaghchaure
- author!=nabinhait
+ - author!=ankush
+ - author!=deepeshgarg007
- or:
- base=version-13
- base=version-12
From 8e559f01c85b0bf0383b29cf776964b49bb0e6c9 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 14 Mar 2022 17:43:28 +0530
Subject: [PATCH 314/447] test: refactor BOM quality template test
---
erpnext/manufacturing/doctype/bom/bom.py | 2 +-
erpnext/manufacturing/doctype/bom/test_bom.py | 17 ++++++-----------
.../test_records.json | 1 +
3 files changed, 8 insertions(+), 12 deletions(-)
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index d3acd5f788..15fa67bca5 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -428,7 +428,7 @@ class BOM(WebsiteGenerator):
def clear_inspection(self):
if not self.inspection_required:
- self.set('quality_inspection_template', None)
+ self.quality_inspection_template = None
def validate_main_item(self):
""" Validate main FG item"""
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index fe6cb0b9c9..2f9b9deb00 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -6,7 +6,6 @@ from collections import deque
from functools import partial
import frappe
-from frappe.test_runner import make_test_records
from frappe.tests.utils import FrappeTestCase
from frappe.utils import cstr, flt
@@ -20,15 +19,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
from erpnext.tests.test_subcontracting import set_backflush_based_on
test_records = frappe.get_test_records('BOM')
+test_dependencies = ["Item", "Quality Inspection Template"]
class TestBOM(FrappeTestCase):
- def setUp(self):
- if not frappe.get_value('Item', '_Test Item'):
- make_test_records('Item')
-
- if not frappe.get_value('Quality Inspection Template', '_Test Quality Inspection Template'):
- make_test_records('Quality Inspection_Template')
-
def test_get_items(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
items_dict = get_bom_items_as_dict(bom=get_default_bom(),
@@ -500,17 +493,19 @@ class TestBOM(FrappeTestCase):
def test_clear_inpection_quality(self):
- bom = frappe.copy_doc(test_records[2])
- bom.is_active = 0
+ bom = frappe.copy_doc(test_records[2], ignore_no_copy=True)
+ bom.docstatus = 0
+ bom.is_default = 0
bom.quality_inspection_template = "_Test Quality Inspection Template"
bom.inspection_required = 1
bom.save()
+ bom.reload()
self.assertEqual(bom.quality_inspection_template, '_Test Quality Inspection Template')
bom.inspection_required = 0
bom.save()
- bom.load_from_db()
+ bom.reload()
self.assertEqual(bom.quality_inspection_template, None)
diff --git a/erpnext/stock/doctype/quality_inspection_template/test_records.json b/erpnext/stock/doctype/quality_inspection_template/test_records.json
index a1b6415b54..980f49a80a 100644
--- a/erpnext/stock/doctype/quality_inspection_template/test_records.json
+++ b/erpnext/stock/doctype/quality_inspection_template/test_records.json
@@ -1,6 +1,7 @@
[
{
"quality_inspection_template_name" : "_Test Quality Inspection Template",
+ "doctype": "Quality Inspection Template",
"item_quality_inspection_parameter" : [
{
"specification": "_Test Param",
From e33d4713cda199f4d74b0d2fdb540dd77b81a381 Mon Sep 17 00:00:00 2001
From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com>
Date: Mon, 14 Mar 2022 18:35:49 +0530
Subject: [PATCH 315/447] Update erpnext/regional/saudi_arabia/utils.py
Co-authored-by: Saqib Ansari
---
erpnext/regional/saudi_arabia/utils.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py
index d46f642962..515862d06a 100644
--- a/erpnext/regional/saudi_arabia/utils.py
+++ b/erpnext/regional/saudi_arabia/utils.py
@@ -133,7 +133,7 @@ def get_vat_amount(doc):
vat_amount = 0
if vat_settings:
- vat_settings_doc = frappe.get_doc('KSA VAT Setting', vat_settings)
+ vat_settings_doc = frappe.get_cached_doc('KSA VAT Setting', vat_settings)
for row in vat_settings_doc.get('ksa_vat_sales_accounts'):
vat_accounts.append(row.account)
From 83a5fae5913bb05d949c3a3f5fb24bd6b1d19d95 Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Mon, 14 Mar 2022 20:14:46 +0530
Subject: [PATCH 316/447] fix: incorrect debit credit amount in presentation
currency
---
erpnext/accounts/report/utils.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py
index c38e4b8c7f..9e721fb2ee 100644
--- a/erpnext/accounts/report/utils.py
+++ b/erpnext/accounts/report/utils.py
@@ -92,10 +92,10 @@ def convert_to_presentation_currency(gl_entries, currency_info, company):
account_currency = entry['account_currency']
if len(account_currencies) == 1 and account_currency == presentation_currency:
- if entry.get('debit'):
+ if debit_in_account_currency:
entry['debit'] = debit_in_account_currency
- if entry.get('credit'):
+ if credit_in_account_currency:
entry['credit'] = credit_in_account_currency
else:
date = currency_info['report_date']
From b03b9ac99f95ed813e15c43ff1e16ed2e0d4e2fa Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Tue, 15 Mar 2022 11:39:15 +0530
Subject: [PATCH 317/447] fix: salary slip amount rounding errors (#30248)
---
erpnext/payroll/doctype/salary_slip/salary_slip.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 181a2b5309..caaed1f59c 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -1002,7 +1002,7 @@ class SalarySlip(TransactionBase):
# apply rounding
if frappe.get_cached_value("Salary Component", row.salary_component, "round_to_the_nearest_integer"):
- amount, additional_amount = rounded(amount), rounded(additional_amount)
+ amount, additional_amount = rounded(amount or 0), rounded(additional_amount or 0)
return amount, additional_amount
@@ -1279,9 +1279,9 @@ class SalarySlip(TransactionBase):
def set_base_totals(self):
self.base_gross_pay = flt(self.gross_pay) * flt(self.exchange_rate)
self.base_total_deduction = flt(self.total_deduction) * flt(self.exchange_rate)
- self.rounded_total = rounded(self.net_pay)
+ self.rounded_total = rounded(self.net_pay or 0)
self.base_net_pay = flt(self.net_pay) * flt(self.exchange_rate)
- self.base_rounded_total = rounded(self.base_net_pay)
+ self.base_rounded_total = rounded(self.base_net_pay or 0)
self.set_net_total_in_words()
#calculate total working hours, earnings based on hourly wages and totals
From 3218ccbb96e3a8c62d9afefee4dae02fb37fca50 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Tue, 15 Mar 2022 11:43:27 +0530
Subject: [PATCH 318/447] fix: Leave Policy Assignment creation patch (#30215)
---
...nt_based_on_employee_current_leave_policy.py | 17 +++++++----------
1 file changed, 7 insertions(+), 10 deletions(-)
diff --git a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
index 55125431b5..8cf1037223 100644
--- a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
+++ b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
@@ -6,14 +6,13 @@ import frappe
def execute():
+ frappe.reload_doc('hr', 'doctype', 'leave_policy_assignment')
+ employee_with_assignment = []
+ leave_policy = []
+
if "leave_policy" in frappe.db.get_table_columns("Employee"):
employees_with_leave_policy = frappe.db.sql("SELECT name, leave_policy FROM `tabEmployee` WHERE leave_policy IS NOT NULL and leave_policy != ''", as_dict = 1)
- employee_with_assignment = []
- leave_policy =[]
-
- #for employee
-
for employee in employees_with_leave_policy:
alloc = frappe.db.exists("Leave Allocation", {"employee":employee.name, "leave_policy": employee.leave_policy, "docstatus": 1})
if not alloc:
@@ -22,12 +21,10 @@ def execute():
employee_with_assignment.append(employee.name)
leave_policy.append(employee.leave_policy)
-
- if "default_leave_policy" in frappe.db.get_table_columns("Employee"):
+ if "default_leave_policy" in frappe.db.get_table_columns("Employee Grade"):
employee_grade_with_leave_policy = frappe.db.sql("SELECT name, default_leave_policy FROM `tabEmployee Grade` WHERE default_leave_policy IS NOT NULL and default_leave_policy!=''", as_dict = 1)
#for whole employee Grade
-
for grade in employee_grade_with_leave_policy:
employees = get_employee_with_grade(grade.name)
for employee in employees:
@@ -47,13 +44,13 @@ def execute():
allocation_exists=True)
def create_assignment(employee, leave_policy, leave_period=None, allocation_exists = False):
+ if frappe.db.get_value("Leave Policy", leave_policy, "docstatus") == 2:
+ return
filters = {"employee":employee, "leave_policy": leave_policy}
if leave_period:
filters["leave_period"] = leave_period
- frappe.reload_doc('hr', 'doctype', 'leave_policy_assignment')
-
if not frappe.db.exists("Leave Policy Assignment" , filters):
lpa = frappe.new_doc("Leave Policy Assignment")
lpa.employee = employee
From f33a725a9e137d5b9d7290baa072152e26740328 Mon Sep 17 00:00:00 2001
From: Krithi Ramani
Date: Tue, 15 Mar 2022 15:35:51 +0530
Subject: [PATCH 319/447] fixed spacings
---
erpnext/stock/doctype/pick_list/pick_list.js | 1 -
erpnext/stock/doctype/pick_list/pick_list.py | 6 ++----
erpnext/stock/doctype/pick_list/test_pick_list.py | 2 +-
3 files changed, 3 insertions(+), 6 deletions(-)
diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js
index ababf3dc42..13b74b5eb1 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.js
+++ b/erpnext/stock/doctype/pick_list/pick_list.js
@@ -146,7 +146,6 @@ frappe.ui.form.on('Pick List', {
customer: frm.doc.customer
};
frm.get_items_btn = frm.add_custom_button(__('Get Items'), () => {
-
erpnext.utils.map_current_doc({
method: 'erpnext.selling.doctype.sales_order.sales_order.create_pick_list',
source_doctype: 'Sales Order',
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 3ec0324a11..915400aa64 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -3,6 +3,8 @@
import json
from collections import OrderedDict, defaultdict
+from itertools import groupby
+from operator import itemgetter
import frappe
from frappe import _
@@ -377,10 +379,6 @@ def create_delivery_note(source_name, target_doc=None):
for d in pick_list.locations:
if d.sales_order:
sales_orders.append([frappe.db.get_value("Sales Order",d.sales_order,'customer'),d.sales_order])
-
- from itertools import groupby
- from operator import itemgetter
-
# Group sales orders by customer
for key,keydata in groupby(sales_orders,key=itemgetter(0)):
sales_dict[key] = set([d[1] for d in keydata])
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index 82d8fe9011..f60104c09a 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -7,6 +7,7 @@ from frappe import _dict
test_dependencies = ['Item', 'Sales Invoice', 'Stock Entry', 'Batch']
from frappe.tests.utils import FrappeTestCase
+
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
@@ -16,7 +17,6 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
class TestPickList(FrappeTestCase):
-
def test_pick_list_picks_warehouse_for_each_item(self):
try:
frappe.get_doc({
From e8310c6dec637b42f75d2e27a7bbd7c3e201de8d Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Tue, 15 Mar 2022 15:48:15 +0530
Subject: [PATCH 320/447] fix: Job Card sub operations status and list view
(backport #30243) (#30256)
* fix: show status in job card list view in Draft mode
(cherry picked from commit fa32fc3c832196004154055ac1b2334d6202c261)
* fix: job card - sub operations table status misbehaviour on pause / resume Job Card
(cherry picked from commit 7b8723445ee695e88590bb833f60a0c4731106fa)
Co-authored-by: Anoop Kurungadam
---
erpnext/manufacturing/doctype/job_card/job_card.py | 2 +-
erpnext/manufacturing/doctype/job_card/job_card_list.js | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 9f4ace296e..5f492d7cf2 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -48,7 +48,7 @@ class JobCard(Document):
self.validate_work_order()
def set_sub_operations(self):
- if self.operation:
+ if not self.sub_operations and self.operation:
self.sub_operations = []
for row in frappe.get_all('Sub Operation',
filters = {'parent': self.operation}, fields=['operation', 'idx'], order_by='idx'):
diff --git a/erpnext/manufacturing/doctype/job_card/job_card_list.js b/erpnext/manufacturing/doctype/job_card/job_card_list.js
index 8017209e7d..7f60bdc6d9 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card_list.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card_list.js
@@ -1,4 +1,5 @@
frappe.listview_settings['Job Card'] = {
+ has_indicator_for_draft: true,
get_indicator: function(doc) {
if (doc.status === "Work In Progress") {
return [__("Work In Progress"), "orange", "status,=,Work In Progress"];
From 7b0a97d679851a102342a4ca23c8c498881eeac1 Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Tue, 15 Mar 2022 15:54:37 +0530
Subject: [PATCH 321/447] fix(pos): loyalty points in case of returned pos
invoice (#30242)
---
.../doctype/pos_invoice/pos_invoice.py | 4 ++--
.../doctype/sales_invoice/sales_invoice.py | 19 +++++++++++++------
2 files changed, 15 insertions(+), 8 deletions(-)
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 9b3b3aa414..91c07ade7f 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -53,7 +53,7 @@ class POSInvoice(SalesInvoice):
def on_submit(self):
# create the loyalty point ledger entry if the customer is enrolled in any loyalty program
- if self.loyalty_program:
+ if not self.is_return and self.loyalty_program:
self.make_loyalty_point_entry()
elif self.is_return and self.return_against and self.loyalty_program:
against_psi_doc = frappe.get_doc("POS Invoice", self.return_against)
@@ -87,7 +87,7 @@ class POSInvoice(SalesInvoice):
def on_cancel(self):
# run on cancel method of selling controller
super(SalesInvoice, self).on_cancel()
- if self.loyalty_program:
+ if not self.is_return and self.loyalty_program:
self.delete_loyalty_point_entry()
elif self.is_return and self.return_against and self.loyalty_program:
against_psi_doc = frappe.get_doc("POS Invoice", self.return_against)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 54217fba83..bfe72dccab 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -1411,12 +1411,19 @@ class SalesInvoice(SellingController):
frappe.db.set_value("Customer", self.customer, "loyalty_program_tier", lp_details.tier_name)
def get_returned_amount(self):
- returned_amount = frappe.db.sql("""
- select sum(grand_total)
- from `tabSales Invoice`
- where docstatus=1 and is_return=1 and ifnull(return_against, '')=%s
- """, self.name)
- return abs(flt(returned_amount[0][0])) if returned_amount else 0
+ from frappe.query_builder.functions import Coalesce, Sum
+ doc = frappe.qb.DocType(self.doctype)
+ returned_amount = (
+ frappe.qb.from_(doc)
+ .select(Sum(doc.grand_total))
+ .where(
+ (doc.docstatus == 1)
+ & (doc.is_return == 1)
+ & (Coalesce(doc.return_against, '') == self.name)
+ )
+ ).run()
+
+ return abs(returned_amount[0][0]) if returned_amount[0][0] else 0
# redeem the loyalty points.
def apply_loyalty_points(self):
From 8264d6b0bcef5d9c7900f652c4554f95572aa202 Mon Sep 17 00:00:00 2001
From: marination
Date: Tue, 15 Mar 2022 16:27:52 +0530
Subject: [PATCH 322/447] fix: Sub-Categpry Routing in Item Group Page Listing
pills
- Use absolute route even 3-4 sub-category levels down
- Remove scroll from category pills due to accessibility issues
- Arrange sub-category pills alphabetically
---
erpnext/e_commerce/product_ui/views.js | 2 +-
erpnext/public/scss/shopping_cart.scss | 9 +++------
erpnext/setup/doctype/item_group/item_group.py | 3 ++-
3 files changed, 6 insertions(+), 8 deletions(-)
diff --git a/erpnext/e_commerce/product_ui/views.js b/erpnext/e_commerce/product_ui/views.js
index 1b5c44038f..6dce79dd72 100644
--- a/erpnext/e_commerce/product_ui/views.js
+++ b/erpnext/e_commerce/product_ui/views.js
@@ -495,7 +495,7 @@ erpnext.ProductView = class {
categories.forEach(category => {
sub_group_html += `
-
+
${ category.name }
diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
index 666043b219..019496d295 100644
--- a/erpnext/public/scss/shopping_cart.scss
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -569,15 +569,12 @@ body.product-page {
}
.scroll-categories {
- white-space: nowrap;
- overflow-x: auto;
-
.category-pill {
- margin: 0px 4px;
display: inline-block;
- padding: 6px 12px;
- background-color: #ecf5fe;
width: fit-content;
+ padding: 6px 12px;
+ margin-bottom: 8px;
+ background-color: #ecf5fe;
font-size: 14px;
border-radius: 18px;
color: var(--blue-500);
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index 5c7194baf6..2c53246c62 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -132,7 +132,8 @@ def get_child_groups_for_website(item_group_name, immediate=False, include_self=
return frappe.get_all(
"Item Group",
filters=filters,
- fields=["name", "route"]
+ fields=["name", "route"],
+ order_by="name"
)
def get_child_item_groups(item_group_name):
From 12d99ed69a8a71cf92c38cd0e251965b2be9fe0a Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Tue, 15 Mar 2022 17:56:42 +0530
Subject: [PATCH 323/447] test:
foreign_account_balance_after_exchange_rate_revaluation
---
.../general_ledger/test_general_ledger.py | 134 ++++++++++++++++++
1 file changed, 134 insertions(+)
create mode 100644 erpnext/accounts/report/general_ledger/test_general_ledger.py
diff --git a/erpnext/accounts/report/general_ledger/test_general_ledger.py b/erpnext/accounts/report/general_ledger/test_general_ledger.py
new file mode 100644
index 0000000000..2373c8c2a6
--- /dev/null
+++ b/erpnext/accounts/report/general_ledger/test_general_ledger.py
@@ -0,0 +1,134 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import today
+
+from erpnext.accounts.report.general_ledger.general_ledger import execute
+
+
+class TestGeneralLedger(FrappeTestCase):
+
+ def test_foreign_account_balance_after_exchange_rate_revaluation(self):
+ """
+ Checks the correctness of balance after exchange rate revaluation
+ """
+ # create a new account with USD currency
+ account_name = "Test USD Account for Revalutation"
+ company = "_Test Company"
+ account = frappe.get_doc({
+ "account_name": account_name,
+ "is_group": 0,
+ "company": company,
+ "root_type": "Asset",
+ "report_type": "Balance Sheet",
+ "account_currency": "USD",
+ "inter_company_account": 0,
+ "parent_account": "Bank Accounts - _TC",
+ "account_type": "Bank",
+ "doctype": "Account"
+ })
+ account.insert(ignore_if_duplicate=True)
+ # create a JV to debit 1000 USD at 75 exchange rate
+ jv = frappe.new_doc("Journal Entry")
+ jv.posting_date = today()
+ jv.company = company
+ jv.multi_currency = 1
+ jv.cost_center = "_Test Cost Center - _TC"
+ jv.set("accounts", [
+ {
+ "account": account.name,
+ "debit_in_account_currency": 1000,
+ "credit_in_account_currency": 0,
+ "exchange_rate": 75,
+ "cost_center": "_Test Cost Center - _TC",
+ },
+ {
+ "account": "Cash - _TC",
+ "debit_in_account_currency": 0,
+ "credit_in_account_currency": 75000,
+ "cost_center": "_Test Cost Center - _TC",
+ },
+ ])
+ jv.save()
+ jv.submit()
+ # create a JV to credit 900 USD at 100 exchange rate
+ jv = frappe.new_doc("Journal Entry")
+ jv.posting_date = today()
+ jv.company = company
+ jv.multi_currency = 1
+ jv.cost_center = "_Test Cost Center - _TC"
+ jv.set("accounts", [
+ {
+ "account": account.name,
+ "debit_in_account_currency": 0,
+ "credit_in_account_currency": 900,
+ "exchange_rate": 100,
+ "cost_center": "_Test Cost Center - _TC",
+ },
+ {
+ "account": "Cash - _TC",
+ "debit_in_account_currency": 90000,
+ "credit_in_account_currency": 0,
+ "cost_center": "_Test Cost Center - _TC",
+ },
+ ])
+ jv.save()
+ jv.submit()
+
+ # create an exchange rate revaluation entry at 77 exchange rate
+ revaluation = frappe.new_doc("Exchange Rate Revaluation")
+ revaluation.posting_date = today()
+ revaluation.company = company
+ revaluation.set("accounts", [
+ {
+ "account": account.name,
+ "account_currency": "USD",
+ "new_exchange_rate": 77,
+ "new_balance_in_base_currency": 7700,
+ "balance_in_base_currency": -15000,
+ "balance_in_account_currency": 100,
+ "current_exchange_rate": -150
+ }
+ ])
+ revaluation.save()
+ revaluation.submit()
+
+ # post journal entry to revaluate
+ frappe.db.set_value('Company', company, "unrealized_exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC")
+ revaluation_jv = revaluation.make_jv_entry()
+ revaluation_jv = frappe.get_doc(revaluation_jv)
+ revaluation_jv.cost_center = "_Test Cost Center - _TC"
+ for acc in revaluation_jv.get("accounts"):
+ acc.cost_center = "_Test Cost Center - _TC"
+ revaluation_jv.save()
+ revaluation_jv.submit()
+
+ # check the balance of the account
+ balance = frappe.db.sql(
+ """
+ select sum(debit_in_account_currency) - sum(credit_in_account_currency)
+ from `tabGL Entry`
+ where account = %s
+ group by account
+ """, account.name)
+
+ self.assertEqual(balance[0][0], 100)
+
+ # check if general ledger shows correct balance
+ columns, data = execute(frappe._dict({
+ "company": company,
+ "from_date": today(),
+ "to_date": today(),
+ "account": [account.name],
+ "group_by": "Group by Voucher (Consolidated)",
+ }))
+
+ self.assertEqual(data[1]["account"], account.name)
+ self.assertEqual(data[1]["debit"], 1000)
+ self.assertEqual(data[1]["credit"], 0)
+ self.assertEqual(data[2]["debit"], 0)
+ self.assertEqual(data[2]["credit"], 900)
+ self.assertEqual(data[3]["debit"], 100)
+ self.assertEqual(data[3]["credit"], 100)
\ No newline at end of file
From fbcb413d9689222442f56197ab14bc17bd9b72bb Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Tue, 15 Mar 2022 18:20:11 +0530
Subject: [PATCH 324/447] fix: Error in bank reco statement
---
.../bank_reconciliation_statement.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py
index b72d266977..fe644ed356 100644
--- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py
+++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py
@@ -267,7 +267,7 @@ def get_loan_amount(filters):
total_amount += flt(amount)
- return amount
+ return total_amount
def get_balance_row(label, amount, account_currency):
if amount > 0:
From a68213d82e7a98e289f04cfc5ee58c2f6d5730d9 Mon Sep 17 00:00:00 2001
From: Krithi Ramani
Date: Tue, 15 Mar 2022 20:18:23 +0530
Subject: [PATCH 325/447] added new field - Picked Qty, in Sales Order Item
---
.../doctype/sales_order_item/sales_order_item.json | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
index 7e55499533..195e96486b 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -23,6 +23,7 @@
"quantity_and_rate",
"qty",
"stock_uom",
+ "picked_qty",
"col_break2",
"uom",
"conversion_factor",
@@ -798,12 +799,17 @@
"fieldtype": "Check",
"label": "Grant Commission",
"read_only": 1
+ },
+ {
+ "fieldname": "picked_qty",
+ "fieldtype": "Float",
+ "label": "Picked Qty"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2022-02-24 14:41:57.325799",
+ "modified": "2022-03-15 20:17:33.984799",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",
From bbe5739547b221816adb05a0b24c397dd00fedfd Mon Sep 17 00:00:00 2001
From: Samuel Danieli <23150094+scdanieli@users.noreply.github.com>
Date: Tue, 15 Mar 2022 17:03:30 +0100
Subject: [PATCH 326/447] feat: add German translations
---
erpnext/translations/de.csv | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index b53e5a1716..1a24f2b05f 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -1541,7 +1541,7 @@ Marketing,Marketing,
Marketing Expenses,Marketingkosten,
Marketplace,Marktplatz,
Marketplace Error,Marktplatzfehler,
-Masters,Stämme,
+Masters,Stammdaten,
Match Payments with Invoices,Zahlungen und Rechnungen abgleichen,
Match non-linked Invoices and Payments.,Nicht verknüpfte Rechnungen und Zahlungen verknüpfen,
Material,Material,
@@ -1621,7 +1621,7 @@ More Information,Mehr Informationen,
More than one selection for {0} not allowed,Mehr als eine Auswahl für {0} ist nicht zulässig,
More...,Mehr...,
Motion Picture & Video,Film & Fernsehen,
-Move,Bewegen,
+Move,Verschieben,
Move Item,Element verschieben,
Multi Currency,Unterschiedliche Währungen,
Multiple Item prices.,Mehrere verschiedene Artikelpreise,
@@ -1943,7 +1943,7 @@ Pharmaceutical,Arzneimittel,
Pharmaceuticals,Pharmaprodukte,
Physician,Arzt,
Piecework,Akkordarbeit,
-Pincode,Postleitzahl (PLZ),
+Pincode,Postleitzahl,
Place Of Supply (State/UT),Ort der Lieferung (Staat / UT),
Place Order,Bestellung aufgeben,
Plan Name,Planname,
@@ -2539,7 +2539,7 @@ Sales Orders,Aufträge,
Sales Partner,Vertriebspartner,
Sales Pipeline,Vertriebspipeline,
Sales Price List,Verkaufspreisliste,
-Sales Return,Rücklieferung,
+Sales Return,Retoure,
Sales Summary,Verkaufszusammenfassung,
Sales Tax Template,Umsatzsteuer-Vorlage,
Sales Team,Verkaufsteam,
@@ -9845,3 +9845,8 @@ Enable European Access,Ermöglichen Sie den europäischen Zugang,
Creating Purchase Order ...,Bestellung anlegen ...,
"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Wählen Sie einen Lieferanten aus den Standardlieferanten der folgenden Artikel aus. Bei der Auswahl erfolgt eine Bestellung nur für Artikel, die dem ausgewählten Lieferanten gehören.",
Row #{}: You must select {} serial numbers for item {}.,Zeile # {}: Sie müssen {} Seriennummern für Artikel {} auswählen.,
+{} To Deliver,{} Zu liefern,
+{} To Receive,{} Zu erhalten,
+{} Available,{} Verfügbar,
+Report an Issue,Ein Problem melden,
+User Forum,Anwenderforum,
From 9f7fee7a4f601a455003a2e7b2deddeb7acfbcab Mon Sep 17 00:00:00 2001
From: Krithi Ramani
Date: Tue, 15 Mar 2022 21:40:51 +0530
Subject: [PATCH 327/447] Added new field in SO - % Picked
---
erpnext/selling/doctype/sales_order/sales_order.json | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index 7e99a06243..fe2f14e19a 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -130,6 +130,7 @@
"per_delivered",
"column_break_81",
"per_billed",
+ "per_picked",
"billing_status",
"sales_team_section_break",
"sales_partner",
@@ -1514,13 +1515,19 @@
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"read_only": 1
+ },
+ {
+ "fieldname": "per_picked",
+ "fieldtype": "Percent",
+ "label": "% Picked",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2021-10-05 12:16:40.775704",
+ "modified": "2022-03-15 21:38:31.437586",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",
@@ -1594,6 +1601,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"timeline_field": "customer",
"title_field": "customer_name",
"track_changes": 1,
From e970616b511e8aaa1a07fd96030029992ed2e6f6 Mon Sep 17 00:00:00 2001
From: Krithi Ramani
Date: Tue, 15 Mar 2022 21:52:34 +0530
Subject: [PATCH 328/447] removed semicolon for break statement
---
erpnext/stock/doctype/pick_list/pick_list.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 915400aa64..598378a95c 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -390,7 +390,7 @@ def create_delivery_note(source_name, target_doc=None):
for n in pick_list.locations :
if not n.sales_order:
is_item_wo_so = 1
- break;
+ break
if is_item_wo_so == 1:
# Create a DN for items without sales orders as well
delivery_note = create_dn_wo_so(pick_list)
@@ -433,7 +433,7 @@ def create_dn_with_so(sales_dict,pick_list):
},
'condition': lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
}
- break;
+ break
if delivery_note:
# map all items of all sales orders of that customer
for so in sales_dict[customer]:
From 06936cf1c024ffa4ed471790b8859aeadc5c5575 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Wed, 16 Mar 2022 09:02:04 +0530
Subject: [PATCH 329/447] fix: Validate income/expense account in sales and
purchase invoice
---
.../accounts/doctype/purchase_invoice/purchase_invoice.py | 6 ++++++
erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 7 +++++++
erpnext/controllers/accounts_controller.py | 6 +++---
3 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 2c3156175f..7654aa44dc 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -32,6 +32,7 @@ from erpnext.accounts.utils import get_account_currency, get_fiscal_year
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.buying.utils import check_on_hold_or_closed_status
+from erpnext.controllers.accounts_controller import validate_account_head
from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
@@ -107,6 +108,7 @@ class PurchaseInvoice(BuyingController):
self.validate_uom_is_integer("uom", "qty")
self.validate_uom_is_integer("stock_uom", "stock_qty")
self.set_expense_account(for_validate=True)
+ self.validate_expense_account()
self.set_against_expense_account()
self.validate_write_off_account()
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items")
@@ -311,6 +313,10 @@ class PurchaseInvoice(BuyingController):
elif not item.expense_account and for_validate:
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
+ def validate_expense_account(self):
+ for item in self.get('items'):
+ validate_account_head(item.idx, item.expense_account, self.company, 'Expense')
+
def set_against_expense_account(self):
against_accounts = []
for item in self.get("items"):
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 862ac81ff3..5aabc21c14 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -37,6 +37,7 @@ from erpnext.assets.doctype.asset.depreciation import (
get_gl_entries_on_asset_regain,
make_depreciation_entry,
)
+from erpnext.controllers.accounts_controller import validate_account_head
from erpnext.controllers.selling_controller import SellingController
from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
from erpnext.setup.doctype.company.company import update_company_current_month_sales
@@ -108,6 +109,8 @@ class SalesInvoice(SellingController):
self.validate_fixed_asset()
self.set_income_account_for_fixed_assets()
self.validate_item_cost_centers()
+ self.validate_income_account()
+
validate_inter_company_party(self.doctype, self.customer, self.company, self.inter_company_invoice_reference)
if cint(self.is_pos):
@@ -175,6 +178,10 @@ class SalesInvoice(SellingController):
if cost_center_company != self.company:
frappe.throw(_("Row #{0}: Cost Center {1} does not belong to company {2}").format(frappe.bold(item.idx), frappe.bold(item.cost_center), frappe.bold(self.company)))
+ def validate_income_account(self):
+ for item in self.get('items'):
+ validate_account_head(item.idx, item.income_account, self.company, 'Income')
+
def set_tax_withholding(self):
tax_withholding_details = get_party_tax_withholding_details(self)
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index a94af10cde..34ff45708b 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1566,12 +1566,12 @@ def validate_taxes_and_charges(tax):
tax.rate = None
-def validate_account_head(idx, account, company):
+def validate_account_head(idx, account, company, context=''):
account_company = frappe.get_cached_value('Account', account, 'company')
if account_company != company:
- frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}')
- .format(idx, frappe.bold(account), frappe.bold(company)), title=_('Invalid Account'))
+ frappe.throw(_('Row {0}: {3} Account {1} does not belong to Company {2}')
+ .format(idx, frappe.bold(account), frappe.bold(company), context), title=_('Invalid Account'))
def validate_cost_center(tax, doc):
From 597bb2c7e827f16f6b63c6c0cd4e702b2b8509d2 Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Wed, 16 Mar 2022 10:53:39 +0530
Subject: [PATCH 330/447] feat: safely parse exception
---
erpnext/utilities/doctype/rename_tool/rename_tool.js | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/erpnext/utilities/doctype/rename_tool/rename_tool.js b/erpnext/utilities/doctype/rename_tool/rename_tool.js
index 5a2d249b42..b8203f4226 100644
--- a/erpnext/utilities/doctype/rename_tool/rename_tool.js
+++ b/erpnext/utilities/doctype/rename_tool/rename_tool.js
@@ -33,8 +33,10 @@ frappe.ui.form.on("Rename Tool", {
let html = r.message.join(" ");
if (r.exc) {
- r.exc = JSON.parse(r.exc);
- html += " " + r.exc.join(" ");
+ r.exc = frappe.utils.parse_json(r.exc);
+ if (Array.isArray(r.exc)) {
+ html += " " + r.exc.join(" ");
+ }
}
frm.get_field("rename_log").$wrapper.html(html);
From 1f79b47a1797762cd57972f6f3b28688d558a0cc Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Wed, 16 Mar 2022 11:55:43 +0530
Subject: [PATCH 331/447] fix: Do not update ignore prcing rule check
implicitly
---
erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 1 -
erpnext/buying/doctype/purchase_order/purchase_order.py | 1 -
.../buying/doctype/supplier_quotation/supplier_quotation.py | 1 -
erpnext/selling/doctype/quotation/quotation.py | 3 +--
erpnext/selling/doctype/sales_order/sales_order.py | 2 --
erpnext/stock/doctype/delivery_note/delivery_note.py | 1 -
6 files changed, 1 insertion(+), 8 deletions(-)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 862ac81ff3..ff8b6537dc 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -1650,7 +1650,6 @@ def make_maintenance_schedule(source_name, target_doc=None):
@frappe.whitelist()
def make_delivery_note(source_name, target_doc=None):
def set_missing_values(source, target):
- target.ignore_pricing_rule = 1
target.run_method("set_missing_values")
target.run_method("set_po_nos")
target.run_method("calculate_taxes_and_totals")
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 2e7d3063cc..2c6654285f 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -402,7 +402,6 @@ def close_or_unclose_purchase_orders(names, status):
frappe.local.message_log = []
def set_missing_values(source, target):
- target.ignore_pricing_rule = 1
target.run_method("set_missing_values")
target.run_method("calculate_taxes_and_totals")
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py
index 171de7882d..81fc32424e 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py
@@ -109,7 +109,6 @@ def get_list_context(context=None):
@frappe.whitelist()
def make_purchase_order(source_name, target_doc=None):
def set_missing_values(source, target):
- target.ignore_pricing_rule = 1
target.run_method("set_missing_values")
target.run_method("get_schedule_dates")
target.run_method("calculate_taxes_and_totals")
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index eebde766d3..06b4ff18bf 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -165,7 +165,6 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
if source.referral_sales_partner:
target.sales_partner=source.referral_sales_partner
target.commission_rate=frappe.get_value('Sales Partner', source.referral_sales_partner, 'commission_rate')
- target.ignore_pricing_rule = 1
target.flags.ignore_permissions = ignore_permissions
target.run_method("set_missing_values")
target.run_method("calculate_taxes_and_totals")
@@ -240,7 +239,7 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
if customer:
target.customer = customer.name
target.customer_name = customer.customer_name
- target.ignore_pricing_rule = 1
+
target.flags.ignore_permissions = ignore_permissions
target.run_method("set_missing_values")
target.run_method("calculate_taxes_and_totals")
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index abbb3c9b90..73e3d193d0 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -520,7 +520,6 @@ def make_project(source_name, target_doc=None):
@frappe.whitelist()
def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False):
def set_missing_values(source, target):
- target.ignore_pricing_rule = 1
target.run_method("set_missing_values")
target.run_method("set_po_nos")
target.run_method("calculate_taxes_and_totals")
@@ -596,7 +595,6 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
target.set_advances()
def set_missing_values(source, target):
- target.ignore_pricing_rule = 1
target.flags.ignore_permissions = True
target.run_method("set_missing_values")
target.run_method("set_po_nos")
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 314e4ba433..4037989533 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -439,7 +439,6 @@ def make_sales_invoice(source_name, target_doc=None):
invoiced_qty_map = get_invoiced_qty_map(source_name)
def set_missing_values(source, target):
- target.ignore_pricing_rule = 1
target.run_method("set_missing_values")
target.run_method("set_po_nos")
From 0211f27e83255bc3c40b8866a3a4fc49909b8279 Mon Sep 17 00:00:00 2001
From: Krithi Ramani
Date: Wed, 16 Mar 2022 12:28:30 +0530
Subject: [PATCH 332/447] as per review comments - changed for loop
---
erpnext/stock/doctype/pick_list/pick_list.py | 30 +++++++++++---------
1 file changed, 17 insertions(+), 13 deletions(-)
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 598378a95c..373dfac85d 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -27,9 +27,9 @@ class PickList(Document):
self.set_item_locations()
# set percentage picked in SO
- for i in self.get('locations'):
- if i.sales_order and frappe.db.get_value("Sales Order",i.sales_order,"per_picked") == 100:
- frappe.throw("Row " + str(i.idx) + " has been picked already!")
+ for location in self.get('locations'):
+ if location.sales_order and frappe.db.get_value("Sales Order",location.sales_order,"per_picked") == 100:
+ frappe.throw("Row " + str(location.idx) + " has been picked already!")
def before_submit(self):
for item in self.locations:
@@ -54,9 +54,9 @@ class PickList(Document):
def before_cancel(self):
#update picked_qty in SO Item on cancel of PL
- for i in self.get('locations'):
- if i.sales_order_item:
- self.update_so(i.sales_order_item,0,i.item_code)
+ for location in self.get('locations'):
+ if location.sales_order_item:
+ self.update_so(location.sales_order_item,0,location.item_code)
def update_so(self,so_item,picked_qty,item_code):
so_doc = frappe.get_doc("Sales Order",frappe.db.get_value("Sales Order Item",so_item,"parent"))
@@ -68,8 +68,12 @@ class PickList(Document):
frappe.db.set_value("Sales Order Item",so_item,"picked_qty",already_picked+picked_qty)
- total_picked_qty = sum(flt(d.picked_qty) for d in so_doc.get('items')) + picked_qty
- total_so_qty = sum(flt(d.stock_qty) for d in so_doc.get('items'))
+ total_picked_qty = 0
+ total_so_qty = 0
+ for item in so_doc.get('items'):
+ total_picked_qty += flt(item.picked_qty)
+ total_so_qty += flt(item.stock_qty)
+ total_picked_qty = total_picked_qty + picked_qty
per_picked = total_picked_qty/total_so_qty * 100
so_doc.db_set("per_picked", flt(per_picked) ,update_modified=False)
@@ -376,9 +380,9 @@ def create_delivery_note(source_name, target_doc=None):
sales_dict = dict()
sales_orders = []
delivery_note = None
- for d in pick_list.locations:
- if d.sales_order:
- sales_orders.append([frappe.db.get_value("Sales Order",d.sales_order,'customer'),d.sales_order])
+ for location in pick_list.locations:
+ if location.sales_order:
+ sales_orders.append([frappe.db.get_value("Sales Order",location.sales_order,'customer'),location.sales_order])
# Group sales orders by customer
for key,keydata in groupby(sales_orders,key=itemgetter(0)):
sales_dict[key] = set([d[1] for d in keydata])
@@ -387,8 +391,8 @@ def create_delivery_note(source_name, target_doc=None):
delivery_note = create_dn_with_so(sales_dict,pick_list)
is_item_wo_so = 0
- for n in pick_list.locations :
- if not n.sales_order:
+ for location in pick_list.locations :
+ if not location.sales_order:
is_item_wo_so = 1
break
if is_item_wo_so == 1:
From b781e8b7d18f41db986b3126b98aa484db4fc851 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 15 Mar 2022 19:09:26 +0530
Subject: [PATCH 333/447] revert: "fix: updated packed_items getting fetched on
Sales Return / Credit Note (#28607)"
This reverts commit 20216fa9f1e98e612c36a89e497de3e92238db7b.
---
.../accounts/doctype/sales_invoice/sales_invoice.py | 8 ++------
.../stock/doctype/delivery_note/delivery_note.py | 9 ++-------
.../doctype/delivery_note/test_delivery_note.py | 12 ++----------
.../stock_ledger_entry/test_stock_ledger_entry.py | 8 +++-----
erpnext/stock/utils.py | 13 -------------
5 files changed, 9 insertions(+), 41 deletions(-)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 96f6eeb10a..3afd72eabb 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -47,7 +47,6 @@ from erpnext.stock.doctype.serial_no.serial_no import (
get_serial_nos,
update_serial_nos_after_submit,
)
-from erpnext.stock.utils import calculate_mapped_packed_items_return
form_grid_templates = {
"items": "templates/form_grid/item_grid.html"
@@ -744,11 +743,8 @@ class SalesInvoice(SellingController):
def update_packing_list(self):
if cint(self.update_stock) == 1:
- if cint(self.is_return) and self.return_against:
- calculate_mapped_packed_items_return(self)
- else:
- from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
- make_packing_list(self)
+ from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
+ make_packing_list(self)
else:
self.set('packed_items', [])
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index e9ef331f66..7951b8d29f 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -17,7 +17,6 @@ from erpnext.stock.doctype.serial_no.serial_no import (
get_delivery_note_serial_no,
update_serial_nos_after_submit,
)
-from erpnext.stock.utils import calculate_mapped_packed_items_return
form_grid_templates = {
"items": "templates/form_grid/item_grid.html"
@@ -132,12 +131,8 @@ class DeliveryNote(SellingController):
self.validate_uom_is_integer("uom", "qty")
self.validate_with_previous_doc()
- # Keeps mapped packed_items in case product bundle is updated.
- if self.is_return and self.return_against:
- calculate_mapped_packed_items_return(self)
- else:
- from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
- make_packing_list(self)
+ from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
+ make_packing_list(self)
if self._action != 'submit' and not self.is_return:
set_batch_nos(self, 'warehouse', throw=True)
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index fc3dce1ee9..9dc112fda8 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -386,7 +386,8 @@ class TestDeliveryNote(FrappeTestCase):
self.assertEqual(actual_qty, 25)
# return bundled item
- dn1 = create_return_delivery_note(source_name=dn.name, rate=500, qty=-2)
+ dn1 = create_delivery_note(item_code='_Test Product Bundle Item', is_return=1,
+ return_against=dn.name, qty=-2, rate=500, company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1")
# qty after return
actual_qty = get_qty_after_transaction(warehouse="Stores - TCP1")
@@ -827,15 +828,6 @@ class TestDeliveryNote(FrappeTestCase):
dn.reload()
self.assertTrue(dn.items[0].serial_no)
-def create_return_delivery_note(**args):
- args = frappe._dict(args)
- from erpnext.controllers.sales_and_purchase_return import make_return_doc
- doc = make_return_doc("Delivery Note", args.source_name, None)
- doc.items[0].rate = args.rate
- doc.items[0].qty = args.qty
- doc.submit()
- return doc
-
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")
args = frappe._dict(args)
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 6a2e7fb9f5..2352235cb3 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
@@ -10,10 +10,7 @@ from frappe.core.page.permission_manager.permission_manager import reset
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, today
-from erpnext.stock.doctype.delivery_note.test_delivery_note import (
- create_delivery_note,
- create_return_delivery_note,
-)
+from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
create_landed_cost_voucher,
@@ -239,7 +236,8 @@ class TestStockLedgerEntry(FrappeTestCase):
self.assertEqual(outgoing_rate, 100)
# Return Entry: Qty = -2, Rate = 150
- return_dn = create_return_delivery_note(source_name=dn.name, rate=150, qty=-2)
+ return_dn = create_delivery_note(is_return=1, return_against=dn.name, item_code=bundled_item, qty=-2, rate=150,
+ company=company, warehouse="Stores - _TC", expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
# check incoming rate for Return entry
incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index f85a04f944..e20538928e 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -422,19 +422,6 @@ def is_reposting_item_valuation_in_progress():
if reposting_in_progress:
frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1)
-
-def calculate_mapped_packed_items_return(return_doc):
- parent_items = set([item.parent_item for item in return_doc.packed_items])
- against_doc = frappe.get_doc(return_doc.doctype, return_doc.return_against)
-
- for original_bundle, returned_bundle in zip(against_doc.items, return_doc.items):
- if original_bundle.item_code in parent_items:
- for returned_packed_item, original_packed_item in zip(return_doc.packed_items, against_doc.packed_items):
- if returned_packed_item.parent_item == original_bundle.item_code:
- returned_packed_item.parent_detail_docname = returned_bundle.name
- returned_packed_item.qty = (original_packed_item.qty / original_bundle.qty) * returned_bundle.qty
-
-
def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool:
"""Check if there are pending reposting job till the specified posting date."""
From b46d6e3c058ad546e7c87740f8dcb094ed1e8269 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 16 Mar 2022 12:55:35 +0530
Subject: [PATCH 334/447] test: packed item return scenarios
---
.../doctype/packed_item/test_packed_item.py | 113 +++++++++++++++++-
1 file changed, 112 insertions(+), 1 deletion(-)
diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py
index 94268a8ef3..5f1b9542d6 100644
--- a/erpnext/stock/doctype/packed_item/test_packed_item.py
+++ b/erpnext/stock/doctype/packed_item/test_packed_item.py
@@ -17,15 +17,25 @@ class TestPackedItem(FrappeTestCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
+ cls.warehouse = "_Test Warehouse - _TC"
cls.bundle = "_Test Product Bundle X"
cls.bundle_items = ["_Test Bundle Item 1", "_Test Bundle Item 2"]
+
+ cls.bundle2 = "_Test Product Bundle Y"
+ cls.bundle2_items = ["_Test Bundle Item 3", "_Test Bundle Item 4"]
+
make_item(cls.bundle, {"is_stock_item": 0})
- for item in cls.bundle_items:
+ make_item(cls.bundle2, {"is_stock_item": 0})
+ for item in cls.bundle_items + cls.bundle2_items:
make_item(item, {"is_stock_item": 1})
make_item("_Test Normal Stock Item", {"is_stock_item": 1})
make_product_bundle(cls.bundle, cls.bundle_items, qty=2)
+ make_product_bundle(cls.bundle2, cls.bundle2_items, qty=2)
+
+ for item in cls.bundle_items + cls.bundle2_items:
+ make_stock_entry(item_code=item, to_warehouse=cls.warehouse, qty=100, rate=100)
def test_adding_bundle_item(self):
"Test impact on packed items if bundle item row is added."
@@ -156,3 +166,104 @@ class TestPackedItem(FrappeTestCase):
credit_after_reposting = sum(gle.credit for gle in gles)
self.assertNotEqual(credit_before_repost, credit_after_reposting)
self.assertAlmostEqual(credit_after_reposting, 2 * credit_before_repost)
+
+ def assertReturns(self, original, returned):
+ self.assertEqual(len(original), len(returned))
+
+ sort_function = lambda p: (p.parent_item, p.item_code, p.qty)
+
+ for sent, returned in zip(
+ sorted(original, key=sort_function),
+ sorted(returned, key=sort_function)
+ ):
+ self.assertEqual(sent.item_code, returned.item_code)
+ self.assertEqual(sent.parent_item, returned.parent_item)
+ self.assertEqual(sent.qty, -1 * returned.qty)
+
+ def test_returning_full_bundles(self):
+ from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+
+ item_list = [
+ {
+ "item_code": self.bundle,
+ "warehouse": self.warehouse,
+ "qty": 1,
+ "rate": 100,
+ },
+ {
+ "item_code": self.bundle2,
+ "warehouse": self.warehouse,
+ "qty": 1,
+ "rate": 100,
+ }
+ ]
+ so = make_sales_order(item_list=item_list, warehouse=self.warehouse)
+
+ dn = make_delivery_note(so.name)
+ dn.save()
+ dn.submit()
+
+ # create return
+ dn_ret = make_sales_return(dn.name)
+ dn_ret.save()
+ dn_ret.submit()
+ self.assertReturns(dn.packed_items, dn_ret.packed_items)
+
+ def test_returning_partial_bundles(self):
+ from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+
+ item_list = [
+ {
+ "item_code": self.bundle,
+ "warehouse": self.warehouse,
+ "qty": 1,
+ "rate": 100,
+ },
+ {
+ "item_code": self.bundle2,
+ "warehouse": self.warehouse,
+ "qty": 1,
+ "rate": 100,
+ }
+ ]
+ so = make_sales_order(item_list=item_list, warehouse=self.warehouse)
+
+ dn = make_delivery_note(so.name)
+ dn.save()
+ dn.submit()
+
+ # create return
+ dn_ret = make_sales_return(dn.name)
+ # remove bundle 2
+ dn_ret.items.pop()
+
+ dn_ret.save()
+ dn_ret.submit()
+ dn_ret.reload()
+
+ self.assertTrue(all(d.parent_item == self.bundle for d in dn_ret.packed_items))
+
+ expected_returns = [d for d in dn.packed_items if d.parent_item == self.bundle]
+ self.assertReturns(expected_returns, dn_ret.packed_items)
+
+
+ def test_returning_partial_bundle_qty(self):
+ from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+
+ so = make_sales_order(item_code=self.bundle, warehouse=self.warehouse, qty = 2)
+
+ dn = make_delivery_note(so.name)
+ dn.save()
+ dn.submit()
+
+ # create return
+ dn_ret = make_sales_return(dn.name)
+ # halve the qty
+ dn_ret.items[0].qty = -1
+ dn_ret.save()
+ dn_ret.submit()
+
+ expected_returns = dn.packed_items
+ for d in expected_returns:
+ d.qty /= 2
+ self.assertReturns(expected_returns, dn_ret.packed_items)
From a12895ec035af45cacb04fd9459df6eb153024a4 Mon Sep 17 00:00:00 2001
From: Krithi Ramani
Date: Wed, 16 Mar 2022 13:56:54 +0530
Subject: [PATCH 335/447] corrected spacing
---
erpnext/stock/doctype/pick_list/pick_list.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 373dfac85d..3a496866cf 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -73,7 +73,7 @@ class PickList(Document):
for item in so_doc.get('items'):
total_picked_qty += flt(item.picked_qty)
total_so_qty += flt(item.stock_qty)
- total_picked_qty = total_picked_qty + picked_qty
+ total_picked_qty=total_picked_qty + picked_qty
per_picked = total_picked_qty/total_so_qty * 100
so_doc.db_set("per_picked", flt(per_picked) ,update_modified=False)
From b93ce78df15ec15ec395b2ed1ee3768eb38ca116 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 16 Mar 2022 17:50:34 +0530
Subject: [PATCH 336/447] chore: dont supress errors server side
not required after https://github.com/frappe/frappe/pull/16284
---
erpnext/stock/stock_ledger.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 353bfa452b..1aaaad2dff 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -1040,7 +1040,6 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
if not allow_zero_rate and not valuation_rate and raise_error_if_no_rate \
and cint(erpnext.is_perpetual_inventory_enabled(company)):
- frappe.local.message_log = []
form_link = get_link_to_form("Item", item_code)
message = _("Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}.").format(form_link, voucher_type, voucher_no)
From 2a0ca7c91a8f788e765d7465b82e9f6238960d4a Mon Sep 17 00:00:00 2001
From: Noah Jacob
Date: Wed, 16 Mar 2022 19:27:47 +0530
Subject: [PATCH 337/447] refactor: removed unrequired code and test for
standalone delivery note serial return (#30276)
---
erpnext/stock/doctype/delivery_note/delivery_note.py | 8 +-------
erpnext/stock/doctype/delivery_note/test_delivery_note.py | 5 -----
2 files changed, 1 insertion(+), 12 deletions(-)
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 7951b8d29f..ffa2f93b96 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -13,10 +13,7 @@ from frappe.utils import cint, flt
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.selling_controller import SellingController
from erpnext.stock.doctype.batch.batch import set_batch_nos
-from erpnext.stock.doctype.serial_no.serial_no import (
- get_delivery_note_serial_no,
- update_serial_nos_after_submit,
-)
+from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no
form_grid_templates = {
"items": "templates/form_grid/item_grid.html"
@@ -218,9 +215,6 @@ class DeliveryNote(SellingController):
# Updating stock ledger should always be called after updating prevdoc status,
# because updating reserved qty in bin depends upon updated delivered qty in SO
self.update_stock_ledger()
- if self.is_return:
- update_serial_nos_after_submit(self, "items")
-
self.make_gl_entries()
self.repost_future_sle_and_gle()
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 9dc112fda8..82f4e7dd29 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -823,11 +823,6 @@ class TestDeliveryNote(FrappeTestCase):
automatically_fetch_payment_terms(enable=0)
- def test_standalone_serial_no_return(self):
- dn = create_delivery_note(item_code="_Test Serialized Item With Series", is_return=True, qty=-1)
- dn.reload()
- self.assertTrue(dn.items[0].serial_no)
-
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")
args = frappe._dict(args)
From 76187d175f67bb36e4a433b58b62373bb0ae3104 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 16 Mar 2022 19:50:56 +0530
Subject: [PATCH 338/447] refactor: call repost directly during tests (#30277)
enqueue(now=frappe.flags.in_test) is always true in test, this change avoids
confusion.
---
.../repost_item_valuation.py | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index f4d52ad73d..f8ec784697 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -45,11 +45,21 @@ class RepostItemValuation(Document):
self.db_set('status', self.status)
def on_submit(self):
- if not frappe.flags.in_test or self.flags.dont_run_in_test or frappe.flags.dont_execute_stock_reposts:
+ """During tests reposts are executed immediately.
+
+ Exceptions:
+ 1. "Repost Item Valuation" document has self.flags.dont_run_in_test
+ 2. global flag frappe.flags.dont_execute_stock_reposts is set
+
+ These flags are useful for asserting real time behaviour like quantity updates.
+ """
+
+ if not frappe.flags.in_test:
+ return
+ if self.flags.dont_run_in_test or frappe.flags.dont_execute_stock_reposts:
return
- frappe.enqueue(repost, timeout=1800, queue='long',
- job_name='repost_sle', now=frappe.flags.in_test, doc=self)
+ repost(self)
@frappe.whitelist()
def restart_reposting(self):
From 119273e633ec8e56c7d5c4649ef81c3deeb5f7d2 Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Thu, 17 Mar 2022 11:56:10 +0530
Subject: [PATCH 339/447] fix: custom cash flow mapper doesn't show any data
---
.../report/cash_flow/custom_cash_flow.py | 26 +++++++++----------
1 file changed, 13 insertions(+), 13 deletions(-)
diff --git a/erpnext/accounts/report/cash_flow/custom_cash_flow.py b/erpnext/accounts/report/cash_flow/custom_cash_flow.py
index 45d147e7a2..ec0c9a7c7e 100644
--- a/erpnext/accounts/report/cash_flow/custom_cash_flow.py
+++ b/erpnext/accounts/report/cash_flow/custom_cash_flow.py
@@ -30,12 +30,12 @@ def get_mappers_from_db():
def get_accounts_in_mappers(mapping_names):
return frappe.db.sql('''
select cfma.name, cfm.label, cfm.is_working_capital, cfm.is_income_tax_liability,
- cfm.is_income_tax_expense, cfm.is_finance_cost, cfm.is_finance_cost_adjustment
+ cfm.is_income_tax_expense, cfm.is_finance_cost, cfm.is_finance_cost_adjustment, cfma.account
from `tabCash Flow Mapping Accounts` cfma
join `tabCash Flow Mapping` cfm on cfma.parent=cfm.name
where cfma.parent in (%s)
order by cfm.is_working_capital
- ''', (', '.join('"%s"' % d for d in mapping_names)))
+ ''', (', '.join('%s' % d for d in mapping_names)))
def setup_mappers(mappers):
@@ -57,31 +57,31 @@ def setup_mappers(mappers):
account_types = [
dict(
- name=account[0], label=account[1], is_working_capital=account[2],
+ name=account[0], account_name=account[7], label=account[1], is_working_capital=account[2],
is_income_tax_liability=account[3], is_income_tax_expense=account[4]
) for account in accounts if not account[3]]
finance_costs_adjustments = [
dict(
- name=account[0], label=account[1], is_finance_cost=account[5],
+ name=account[0], account_name=account[7], label=account[1], is_finance_cost=account[5],
is_finance_cost_adjustment=account[6]
) for account in accounts if account[6]]
tax_liabilities = [
dict(
- name=account[0], label=account[1], is_income_tax_liability=account[3],
+ name=account[0], account_name=account[7], label=account[1], is_income_tax_liability=account[3],
is_income_tax_expense=account[4]
) for account in accounts if account[3]]
tax_expenses = [
dict(
- name=account[0], label=account[1], is_income_tax_liability=account[3],
+ name=account[0], account_name=account[7], label=account[1], is_income_tax_liability=account[3],
is_income_tax_expense=account[4]
) for account in accounts if account[4]]
finance_costs = [
dict(
- name=account[0], label=account[1], is_finance_cost=account[5])
+ name=account[0], account_name=account[7], label=account[1], is_finance_cost=account[5])
for account in accounts if account[5]]
account_types_labels = sorted(
@@ -124,27 +124,27 @@ def setup_mappers(mappers):
)
for label in account_types_labels:
- names = [d['name'] for d in account_types if d['label'] == label[0]]
+ names = [d['account_name'] for d in account_types if d['label'] == label[0]]
m = dict(label=label[0], names=names, is_working_capital=label[1])
mapping['account_types'].append(m)
for label in fc_adjustment_labels:
- names = [d['name'] for d in finance_costs_adjustments if d['label'] == label[0]]
+ names = [d['account_name'] for d in finance_costs_adjustments if d['label'] == label[0]]
m = dict(label=label[0], names=names)
mapping['finance_costs_adjustments'].append(m)
for label in unique_liability_labels:
- names = [d['name'] for d in tax_liabilities if d['label'] == label[0]]
+ names = [d['account_name'] for d in tax_liabilities if d['label'] == label[0]]
m = dict(label=label[0], names=names, tax_liability=label[1], tax_expense=label[2])
mapping['tax_liabilities'].append(m)
for label in unique_expense_labels:
- names = [d['name'] for d in tax_expenses if d['label'] == label[0]]
+ names = [d['account_name'] for d in tax_expenses if d['label'] == label[0]]
m = dict(label=label[0], names=names, tax_liability=label[1], tax_expense=label[2])
mapping['tax_expenses'].append(m)
for label in unique_finance_costs_labels:
- names = [d['name'] for d in finance_costs if d['label'] == label[0]]
+ names = [d['account_name'] for d in finance_costs if d['label'] == label[0]]
m = dict(label=label[0], names=names, is_finance_cost=label[1])
mapping['finance_costs'].append(m)
@@ -378,7 +378,7 @@ def _get_account_type_based_data(filters, account_names, period_list, accumulate
total = 0
for period in period_list:
start_date = get_start_date(period, accumulated_values, company)
- accounts = ', '.join('"%s"' % d for d in account_names)
+ accounts = ', '.join('%s' % d for d in account_names)
if opening_balances:
date_info = dict(date=start_date)
From f090e63e544aebf6b936be4e27701a26c3c51f26 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Thu, 17 Mar 2022 15:21:56 +0530
Subject: [PATCH 340/447] test: fix holiday list creation causing flaky tests
(#30260)
---
.../hr/doctype/attendance/test_attendance.py | 26 +++++++++---------
.../test_leave_application.py | 7 ++++-
..._based_on_employee_current_leave_policy.py | 1 +
.../doctype/salary_slip/test_salary_slip.py | 27 ++++++++++---------
4 files changed, 34 insertions(+), 27 deletions(-)
diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py
index 6095413771..585059ff47 100644
--- a/erpnext/hr/doctype/attendance/test_attendance.py
+++ b/erpnext/hr/doctype/attendance/test_attendance.py
@@ -3,7 +3,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase
-from frappe.utils import add_days, get_first_day, getdate, now_datetime, nowdate
+from frappe.utils import add_days, get_year_ending, get_year_start, getdate, now_datetime, nowdate
from erpnext.hr.doctype.attendance.attendance import (
get_month_map,
@@ -16,6 +16,13 @@ from erpnext.hr.doctype.leave_application.test_leave_application import get_firs
test_records = frappe.get_test_records('Attendance')
class TestAttendance(FrappeTestCase):
+ def setUp(self):
+ from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
+
+ from_date = get_year_start(getdate())
+ to_date = get_year_ending(getdate())
+ self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
+
def test_mark_absent(self):
employee = make_employee("test_mark_absent@example.com")
date = nowdate()
@@ -31,12 +38,9 @@ class TestAttendance(FrappeTestCase):
employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1))
frappe.db.delete('Attendance', {'employee': employee})
+ frappe.db.set_value('Employee', employee, 'holiday_list', self.holiday_list)
- from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
- holiday_list = make_holiday_list()
- frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
-
- first_sunday = get_first_sunday(holiday_list, for_date=first_day)
+ first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
mark_attendance(employee, first_day, 'Present')
month_name = get_month_name(first_day)
@@ -58,11 +62,9 @@ class TestAttendance(FrappeTestCase):
employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1))
frappe.db.delete('Attendance', {'employee': employee})
- from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
- holiday_list = make_holiday_list()
- frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
+ frappe.db.set_value('Employee', employee, 'holiday_list', self.holiday_list)
- first_sunday = get_first_sunday(holiday_list, for_date=first_day)
+ first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
mark_attendance(employee, first_day, 'Present')
month_name = get_month_name(first_day)
@@ -87,9 +89,7 @@ class TestAttendance(FrappeTestCase):
relieving_date=relieving_date)
frappe.db.delete('Attendance', {'employee': employee})
- from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
- holiday_list = make_holiday_list()
- frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
+ frappe.db.set_value('Employee', employee, 'holiday_list', self.holiday_list)
attendance_date = add_days(first_day, 2)
mark_attendance(employee, attendance_date, 'Present')
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 27f98a2659..7d32fd8865 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -82,7 +82,11 @@ class TestLeaveApplication(unittest.TestCase):
set_leave_approver()
frappe.db.delete("Attendance", {"employee": "_T-Employee-00001"})
- self.holiday_list = make_holiday_list()
+ frappe.db.set_value("Employee", "_T-Employee-00001", "holiday_list", "")
+
+ from_date = get_year_start(getdate())
+ to_date = get_year_ending(getdate())
+ self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
if not frappe.db.exists("Leave Type", "_Test Leave Type"):
frappe.get_doc(dict(
@@ -316,6 +320,7 @@ class TestLeaveApplication(unittest.TestCase):
leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name, employee.company)
leave_application.reload()
+
# holiday should be excluded while marking attendance
self.assertEqual(leave_application.total_leave_days, 3)
self.assertEqual(frappe.db.count("Attendance", {"leave_application": leave_application.name}), 3)
diff --git a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
index 8cf1037223..6f9031fc50 100644
--- a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
+++ b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
@@ -7,6 +7,7 @@ import frappe
def execute():
frappe.reload_doc('hr', 'doctype', 'leave_policy_assignment')
+ frappe.reload_doc('hr', 'doctype', 'employee_grade')
employee_with_assignment = []
leave_policy = []
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index ebad3cbf94..5e41b661f8 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -411,7 +411,7 @@ class TestSalarySlip(unittest.TestCase):
def test_email_salary_slip(self):
frappe.db.sql("delete from `tabEmail Queue`")
- make_employee("test_email_salary_slip@salary.com")
+ make_employee("test_email_salary_slip@salary.com", company="_Test Company")
ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email")
ss.company = "_Test Company"
ss.save()
@@ -1091,18 +1091,19 @@ def setup_test():
def make_holiday_list(list_name=None, from_date=None, to_date=None):
fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
name = list_name or "Salary Slip Test Holiday List"
- holiday_list = frappe.db.exists("Holiday List", name)
- if not holiday_list:
- holiday_list = frappe.get_doc({
- "doctype": "Holiday List",
- "holiday_list_name": name,
- "from_date": from_date or fiscal_year[1],
- "to_date": to_date or fiscal_year[2],
- "weekly_off": "Sunday"
- }).insert()
- holiday_list.get_weekly_off_dates()
- holiday_list.save()
- holiday_list = holiday_list.name
+
+ frappe.delete_doc_if_exists("Holiday List", name, force=True)
+
+ holiday_list = frappe.get_doc({
+ "doctype": "Holiday List",
+ "holiday_list_name": name,
+ "from_date": from_date or fiscal_year[1],
+ "to_date": to_date or fiscal_year[2],
+ "weekly_off": "Sunday"
+ }).insert()
+ holiday_list.get_weekly_off_dates()
+ holiday_list.save()
+ holiday_list = holiday_list.name
return holiday_list
From 2dd0e20003f9da13d51ced0da5ee5c492e621230 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 17 Mar 2022 12:56:43 +0530
Subject: [PATCH 341/447] fix: Clean and fixes in Dimension-wise Accounts
Balance Report
(cherry picked from commit 08a06ce5c68023c41f53b584082b5e1c4ddb1f59)
---
.../dimension_wise_accounts_balance_report.js | 5 +-
.../dimension_wise_accounts_balance_report.py | 154 ++++++++----------
2 files changed, 76 insertions(+), 83 deletions(-)
diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js
index 6a0394861b..ea05a35b25 100644
--- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js
+++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js
@@ -39,12 +39,14 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"label": __("From Date"),
"fieldtype": "Date",
"default": frappe.defaults.get_user_default("year_start_date"),
+ "reqd": 1
},
{
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
"default": frappe.defaults.get_user_default("year_end_date"),
+ "reqd": 1
},
{
"fieldname": "finance_book",
@@ -56,6 +58,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"fieldname": "dimension",
"label": __("Select Dimension"),
"fieldtype": "Select",
+ "default": "Cost Center",
"options": get_accounting_dimension_options(),
"reqd": 1,
},
@@ -70,7 +73,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
});
function get_accounting_dimension_options() {
- let options =["", "Cost Center", "Project"];
+ let options =["Cost Center", "Project"];
frappe.db.get_list('Accounting Dimension',
{fields:['document_type']}).then((res) => {
res.forEach((dimension) => {
diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py
index d547470bf3..d39e320f3c 100644
--- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py
+++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py
@@ -15,20 +15,22 @@ from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
def execute(filters=None):
- validate_filters(filters)
- dimension_items_list = get_dimension_items_list(filters.dimension, filters.company)
- if not dimension_items_list:
+ validate_filters(filters)
+ dimension_list = get_dimensions(filters)
+
+ if not dimension_list:
return [], []
- dimension_items_list = [''.join(d) for d in dimension_items_list]
- columns = get_columns(dimension_items_list)
- data = get_data(filters, dimension_items_list)
+ # dimension_items_list = [''.join(d) for d in dimension_items_list]
+ columns = get_columns(dimension_list)
+ data = get_data(filters, dimension_list)
return columns, data
-def get_data(filters, dimension_items_list):
+def get_data(filters, dimension_list):
company_currency = erpnext.get_company_currency(filters.company)
+
acc = frappe.db.sql("""
select
name, account_number, parent_account, lft, rgt, root_type,
@@ -51,60 +53,54 @@ def get_data(filters, dimension_items_list):
where lft >= %s and rgt <= %s and company = %s""", (min_lft, max_rgt, filters.company))
gl_entries_by_account = {}
- set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account)
- format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list)
- accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list)
- out = prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list)
+ set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_account)
+ format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_list,
+ frappe.scrub(filters.get('dimension')))
+ accumulate_values_into_parents(accounts, accounts_by_name, dimension_list)
+ out = prepare_data(accounts, filters, company_currency, dimension_list)
out = filter_out_zero_value_rows(out, parent_children_map)
return out
-def set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account):
- for item in dimension_items_list:
- condition = get_condition(filters.from_date, item, filters.dimension)
- if account:
- condition += " and account in ({})"\
- .format(", ".join([frappe.db.escape(d) for d in account]))
+def set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_account):
+ condition = get_condition(filters.get('dimension'))
- gl_filters = {
- "company": filters.get("company"),
- "from_date": filters.get("from_date"),
- "to_date": filters.get("to_date"),
- "finance_book": cstr(filters.get("finance_book"))
- }
+ if account:
+ condition += " and account in ({})"\
+ .format(", ".join([frappe.db.escape(d) for d in account]))
- gl_filters['item'] = ''.join(item)
+ gl_filters = {
+ "company": filters.get("company"),
+ "from_date": filters.get("from_date"),
+ "to_date": filters.get("to_date"),
+ "finance_book": cstr(filters.get("finance_book"))
+ }
- if filters.get("include_default_book_entries"):
- gl_filters["company_fb"] = frappe.db.get_value("Company",
- filters.company, 'default_finance_book')
+ gl_filters['dimensions'] = set(dimension_list)
- for key, value in filters.items():
- if value:
- gl_filters.update({
- key: value
- })
+ if filters.get("include_default_book_entries"):
+ gl_filters["company_fb"] = frappe.db.get_value("Company",
+ filters.company, 'default_finance_book')
- gl_entries = frappe.db.sql("""
+ gl_entries = frappe.db.sql("""
select
- posting_date, account, debit, credit, is_opening, fiscal_year,
+ posting_date, account, {dimension}, debit, credit, is_opening, fiscal_year,
debit_in_account_currency, credit_in_account_currency, account_currency
from
`tabGL Entry`
where
company=%(company)s
{condition}
+ and posting_date >= %(from_date)s
and posting_date <= %(to_date)s
and is_cancelled = 0
order by account, posting_date""".format(
- condition=condition),
- gl_filters, as_dict=True) #nosec
+ dimension = frappe.scrub(filters.get('dimension')), condition=condition), gl_filters, as_dict=True) #nosec
- for entry in gl_entries:
- entry['dimension_item'] = ''.join(item)
- gl_entries_by_account.setdefault(entry.account, []).append(entry)
+ for entry in gl_entries:
+ gl_entries_by_account.setdefault(entry.account, []).append(entry)
-def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list):
+def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_list, dimension_type):
for entries in gl_entries_by_account.values():
for entry in entries:
@@ -114,11 +110,12 @@ def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_l
_("Could not retrieve information for {0}.").format(entry.account), title="Error",
raise_exception=1
)
- for item in dimension_items_list:
- if item == entry.dimension_item:
- d[frappe.scrub(item)] = d.get(frappe.scrub(item), 0.0) + flt(entry.debit) - flt(entry.credit)
-def prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list):
+ for dimension in dimension_list:
+ if dimension == entry.get(dimension_type):
+ d[frappe.scrub(dimension)] = d.get(frappe.scrub(dimension), 0.0) + flt(entry.debit) - flt(entry.credit)
+
+def prepare_data(accounts, filters, company_currency, dimension_list):
data = []
for d in accounts:
@@ -135,13 +132,13 @@ def prepare_data(accounts, filters, parent_children_map, company_currency, dimen
if d.account_number else d.account_name)
}
- for item in dimension_items_list:
- row[frappe.scrub(item)] = flt(d.get(frappe.scrub(item), 0.0), 3)
+ for dimension in dimension_list:
+ row[frappe.scrub(dimension)] = flt(d.get(frappe.scrub(dimension), 0.0), 3)
- if abs(row[frappe.scrub(item)]) >= 0.005:
+ if abs(row[frappe.scrub(dimension)]) >= 0.005:
# ignore zero values
has_value = True
- total += flt(d.get(frappe.scrub(item), 0.0), 3)
+ total += flt(d.get(frappe.scrub(dimension), 0.0), 3)
row["has_value"] = has_value
row["total"] = total
@@ -149,62 +146,55 @@ def prepare_data(accounts, filters, parent_children_map, company_currency, dimen
return data
-def accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list):
+def accumulate_values_into_parents(accounts, accounts_by_name, dimension_list):
"""accumulate children's values in parent accounts"""
for d in reversed(accounts):
if d.parent_account:
- for item in dimension_items_list:
- accounts_by_name[d.parent_account][frappe.scrub(item)] = \
- accounts_by_name[d.parent_account].get(frappe.scrub(item), 0.0) + d.get(frappe.scrub(item), 0.0)
+ for dimension in dimension_list:
+ accounts_by_name[d.parent_account][frappe.scrub(dimension)] = \
+ accounts_by_name[d.parent_account].get(frappe.scrub(dimension), 0.0) + d.get(frappe.scrub(dimension), 0.0)
-def get_condition(from_date, item, dimension):
+def get_condition(dimension):
conditions = []
- if from_date:
- conditions.append("posting_date >= %(from_date)s")
- if dimension:
- if dimension not in ['Cost Center', 'Project']:
- if dimension in ['Customer', 'Supplier']:
- dimension = 'Party'
- else:
- dimension = 'Voucher No'
- txt = "{0} = %(item)s".format(frappe.scrub(dimension))
- conditions.append(txt)
+ conditions.append("{0} in %(dimensions)s".format(frappe.scrub(dimension)))
return " and {}".format(" and ".join(conditions)) if conditions else ""
-def get_dimension_items_list(dimension, company):
- meta = frappe.get_meta(dimension, cached=False)
- fieldnames = [d.fieldname for d in meta.get("fields")]
- filters = {}
- if 'company' in fieldnames:
- filters['company'] = company
- return frappe.get_all(dimension, filters, as_list=True)
+def get_dimensions(filters):
+ meta = frappe.get_meta(filters.get('dimension'), cached=False)
+ query_filters = {}
-def get_columns(dimension_items_list, accumulated_values=1, company=None):
+ if meta.has_field('company'):
+ query_filters = {'company': filters.get('company')}
+
+ return frappe.get_all(filters.get('dimension'), filters=query_filters, pluck='name')
+
+def get_columns(dimension_list):
columns = [{
"fieldname": "account",
"label": _("Account"),
"fieldtype": "Link",
"options": "Account",
"width": 300
+ },
+ {
+ "fieldname": "currency",
+ "label": _("Currency"),
+ "fieldtype": "Link",
+ "options": "Currency",
+ "hidden": 1
}]
- if company:
+
+ for dimension in dimension_list:
columns.append({
- "fieldname": "currency",
- "label": _("Currency"),
- "fieldtype": "Link",
- "options": "Currency",
- "hidden": 1
- })
- for item in dimension_items_list:
- columns.append({
- "fieldname": frappe.scrub(item),
- "label": item,
+ "fieldname": frappe.scrub(dimension),
+ "label": dimension,
"fieldtype": "Currency",
"options": "currency",
"width": 150
})
+
columns.append({
"fieldname": "total",
"label": "Total",
From 5f89cb2ccaf7656233ff6298422e333817dac82c Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 17 Mar 2022 12:57:12 +0530
Subject: [PATCH 342/447] fix: Remove comments
(cherry picked from commit cab69fe1f298b44613be21c06591e868c24ee249)
---
.../dimension_wise_accounts_balance_report.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py
index d39e320f3c..0c2b6cb4cb 100644
--- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py
+++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py
@@ -22,7 +22,6 @@ def execute(filters=None):
if not dimension_list:
return [], []
- # dimension_items_list = [''.join(d) for d in dimension_items_list]
columns = get_columns(dimension_list)
data = get_data(filters, dimension_list)
From 4237e5d9283023bc41e6efb147140750d1075b17 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 17 Mar 2022 18:00:39 +0530
Subject: [PATCH 343/447] fix: Test case
---
.../test_deferred_revenue_and_expense.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
index 86eb2134fe..17475a77db 100644
--- a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
+++ b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
@@ -88,10 +88,12 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
posting_date="2021-05-01",
parent_cost_center="Main - _CD",
cost_center="Main - _CD",
- do_not_submit=True,
+ do_not_save=True,
rate=300,
price_list_rate=300,
)
+
+ si.items[0].income_account = "Sales - _CD"
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2021-05-01"
si.items[0].service_end_date = "2021-08-01"
@@ -269,11 +271,13 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
posting_date="2021-05-01",
parent_cost_center="Main - _CD",
cost_center="Main - _CD",
- do_not_submit=True,
+ do_not_save=True,
rate=300,
price_list_rate=300,
)
+
si.items[0].enable_deferred_revenue = 1
+ si.items[0].income_account = "Sales - _CD"
si.items[0].deferred_revenue_account = deferred_revenue_account
si.items[0].income_account = "Sales - _CD"
si.save()
From 00bfee97c766e771a1ab0b57d223ba9e87b70e9a Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Fri, 18 Mar 2022 14:26:33 +0530
Subject: [PATCH 344/447] refactor: convert raw sql to frappe.qb
---
.../report/cash_flow/custom_cash_flow.py | 89 ++++++++++++-------
1 file changed, 56 insertions(+), 33 deletions(-)
diff --git a/erpnext/accounts/report/cash_flow/custom_cash_flow.py b/erpnext/accounts/report/cash_flow/custom_cash_flow.py
index ec0c9a7c7e..20f7fcfb1b 100644
--- a/erpnext/accounts/report/cash_flow/custom_cash_flow.py
+++ b/erpnext/accounts/report/cash_flow/custom_cash_flow.py
@@ -4,7 +4,8 @@
import frappe
from frappe import _
-from frappe.utils import add_to_date
+from frappe.query_builder.functions import Sum
+from frappe.utils import add_to_date, get_date_str
from erpnext.accounts.report.financial_statements import get_columns, get_data, get_period_list
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import (
@@ -28,15 +29,22 @@ def get_mappers_from_db():
def get_accounts_in_mappers(mapping_names):
- return frappe.db.sql('''
- select cfma.name, cfm.label, cfm.is_working_capital, cfm.is_income_tax_liability,
- cfm.is_income_tax_expense, cfm.is_finance_cost, cfm.is_finance_cost_adjustment, cfma.account
- from `tabCash Flow Mapping Accounts` cfma
- join `tabCash Flow Mapping` cfm on cfma.parent=cfm.name
- where cfma.parent in (%s)
- order by cfm.is_working_capital
- ''', (', '.join('%s' % d for d in mapping_names)))
+ cfm = frappe.qb.DocType('Cash Flow Mapping')
+ cfma = frappe.qb.DocType('Cash Flow Mapping Accounts')
+ result = (
+ frappe.qb
+ .select(
+ cfma.name, cfm.label, cfm.is_working_capital,
+ cfm.is_income_tax_liability, cfm.is_income_tax_expense,
+ cfm.is_finance_cost, cfm.is_finance_cost_adjustment, cfma.account
+ )
+ .from_(cfm)
+ .join(cfma)
+ .on(cfm.name == cfma.parent)
+ .where(cfma.parent.isin(mapping_names))
+ ).run()
+ return result
def setup_mappers(mappers):
cash_flow_accounts = []
@@ -371,14 +379,30 @@ def execute(filters=None):
def _get_account_type_based_data(filters, account_names, period_list, accumulated_values, opening_balances=0):
+ if not account_names or not account_names[0] or not type(account_names[0]) == str:
+ # only proceed if account_names is a list of account names
+ return {}
+
from erpnext.accounts.report.cash_flow.cash_flow import get_start_date
company = filters.company
data = {}
total = 0
+ GLEntry = frappe.qb.DocType('GL Entry')
+ Account = frappe.qb.DocType('Account')
+
for period in period_list:
start_date = get_start_date(period, accumulated_values, company)
- accounts = ', '.join('%s' % d for d in account_names)
+
+ account_subquery = (
+ frappe.qb.from_(Account)
+ .where(
+ (Account.name.isin(account_names)) |
+ (Account.parent_account.isin(account_names))
+ )
+ .select(Account.name)
+ .as_("account_subquery")
+ )
if opening_balances:
date_info = dict(date=start_date)
@@ -395,32 +419,31 @@ def _get_account_type_based_data(filters, account_names, period_list, accumulate
else:
start, end = add_to_date(**date_info), add_to_date(**date_info)
- gl_sum = frappe.db.sql_list("""
- select sum(credit) - sum(debit)
- from `tabGL Entry`
- where company=%s and posting_date >= %s and posting_date <= %s
- and voucher_type != 'Period Closing Voucher'
- and account in ( SELECT name FROM tabAccount WHERE name IN (%s)
- OR parent_account IN (%s))
- """, (company, start, end, accounts, accounts))
- else:
- gl_sum = frappe.db.sql_list("""
- select sum(credit) - sum(debit)
- from `tabGL Entry`
- where company=%s and posting_date >= %s and posting_date <= %s
- and voucher_type != 'Period Closing Voucher'
- and account in ( SELECT name FROM tabAccount WHERE name IN (%s)
- OR parent_account IN (%s))
- """, (company, start_date if accumulated_values else period['from_date'],
- period['to_date'], accounts, accounts))
+ start, end = get_date_str(start), get_date_str(end)
- if gl_sum and gl_sum[0]:
- amount = gl_sum[0]
else:
- amount = 0
+ start, end = start_date if accumulated_values else period['from_date'], period['to_date']
+ start, end = get_date_str(start), get_date_str(end)
- total += amount
- data.setdefault(period["key"], amount)
+ result = (
+ frappe.qb.from_(GLEntry)
+ .select(Sum(GLEntry.credit) - Sum(GLEntry.debit))
+ .where(
+ (GLEntry.company == company) &
+ (GLEntry.posting_date >= start) &
+ (GLEntry.posting_date <= end) &
+ (GLEntry.voucher_type != 'Period Closing Voucher') &
+ (GLEntry.account.isin(account_subquery))
+ )
+ ).run()
+
+ if result and result[0]:
+ gl_sum = result[0][0]
+ else:
+ gl_sum = 0
+
+ total += gl_sum
+ data.setdefault(period["key"], gl_sum)
data["total"] = total
return data
From 5a9bf9ffd64ffba14cb4bff63431fff319a82ed7 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Fri, 18 Mar 2022 17:29:09 +0530
Subject: [PATCH 345/447] fix: respect db multi_tenancy while fetching
precision (#30301)
[skip ci]
---
erpnext/stock/report/stock_ageing/stock_ageing.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index 97a740e184..7ca40033ed 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -12,7 +12,7 @@ from frappe.utils import cint, date_diff, flt
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Filters = frappe._dict
-precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
+
def execute(filters: Filters = None) -> Tuple:
to_date = filters["to_date"]
@@ -30,6 +30,8 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li
_func = itemgetter(1)
data = []
+ precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True))
+
for item, item_dict in item_details.items():
earliest_age, latest_age = 0, 0
details = item_dict["details"]
@@ -76,6 +78,9 @@ def get_average_age(fifo_queue: List, to_date: str) -> float:
return flt(age_qty / total_qty, 2) if total_qty else 0.0
def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: Dict) -> Tuple:
+
+ precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True))
+
range1 = range2 = range3 = above_range3 = 0.0
for item in fifo_queue:
From 972d06555aea8a8e4aec3186f3844b942fa54d38 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Fri, 18 Mar 2022 17:32:46 +0530
Subject: [PATCH 346/447] fix: Add permission for KSA Vat documents
---
erpnext/patches.txt | 1 +
erpnext/patches/v13_0/enable_ksa_vat_docs.py | 12 ++++++++++++
2 files changed, 13 insertions(+)
create mode 100644 erpnext/patches/v13_0/enable_ksa_vat_docs.py
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 16d8c730a1..3199912621 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -333,6 +333,7 @@ erpnext.patches.v13_0.update_asset_quantity_field
erpnext.patches.v13_0.delete_bank_reconciliation_detail
erpnext.patches.v13_0.enable_provisional_accounting
erpnext.patches.v13_0.non_profit_deprecation_warning
+erpnext.patches.v13_0.enable_ksa_vat_docs #1
[post_model_sync]
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
diff --git a/erpnext/patches/v13_0/enable_ksa_vat_docs.py b/erpnext/patches/v13_0/enable_ksa_vat_docs.py
new file mode 100644
index 0000000000..3f482620e1
--- /dev/null
+++ b/erpnext/patches/v13_0/enable_ksa_vat_docs.py
@@ -0,0 +1,12 @@
+import frappe
+
+from erpnext.regional.saudi_arabia.setup import add_permissions, add_print_formats
+
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'})
+ if not company:
+ return
+
+ add_print_formats()
+ add_permissions()
\ No newline at end of file
From c2cbd407c14d0d9763627adefc5f81ab264eccac Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Sat, 19 Mar 2022 20:31:06 +0530
Subject: [PATCH 347/447] fix: Allow on Submit for Material Request Item
Required Date (#30174) (#30308)
* fix: Allow on Submit for Material Request Item Required Date
* chore: whitespace
(cherry picked from commit cca9668309711bf3ca517726a94df547c5a16bbb)
Co-authored-by: Devarsh Bhatt <58166671+bhattdevarsh@users.noreply.github.com>
Co-authored-by: Ankush Menat
---
erpnext/stock/doctype/material_request/material_request.py | 3 +++
.../doctype/material_request_item/material_request_item.json | 3 ++-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 51209acb27..49fefae550 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -82,6 +82,9 @@ class MaterialRequest(BuyingController):
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
+ def before_update_after_submit(self):
+ self.validate_schedule_date()
+
def validate_material_request_type(self):
""" Validate fields in accordance with selected type """
diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json
index 2bad42a0eb..dd66cfff8b 100644
--- a/erpnext/stock/doctype/material_request_item/material_request_item.json
+++ b/erpnext/stock/doctype/material_request_item/material_request_item.json
@@ -177,6 +177,7 @@
"fieldtype": "Column Break"
},
{
+ "allow_on_submit": 1,
"bold": 1,
"columns": 2,
"fieldname": "schedule_date",
@@ -459,7 +460,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-11-03 14:40:24.409826",
+ "modified": "2022-03-10 18:42:42.705190",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request Item",
From f4c213379e03f09c59dac1c16efbafe00ae37e3f Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Mar 2022 17:47:18 +0530
Subject: [PATCH 348/447] fix: set current qty as default qty for stock entry
---
erpnext/public/js/utils/serial_no_batch_selector.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index 08270bdea1..79864b717c 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -75,7 +75,7 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector {
fieldtype:'Float',
read_only: me.has_batch && !me.has_serial_no,
label: __(me.has_batch && !me.has_serial_no ? 'Selected Qty' : 'Qty'),
- default: flt(me.item.stock_qty),
+ default: flt(me.item.stock_qty) || flt(me.item.transfer_qty),
},
...get_pending_qty_fields(me),
{
From 5ec27c90552ab5c95ef734bb620a0e1bdd5256ee Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Mar 2022 17:48:43 +0530
Subject: [PATCH 349/447] fix: filter serial nos by selected batch number
---
erpnext/public/js/utils/serial_no_batch_selector.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index 79864b717c..ae8d3d455c 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -100,7 +100,7 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector {
qty: qty,
item_code: me.item_code,
warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '',
- batch_no: me.item.batch_no || null,
+ batch_nos: me.item.batch_no || null,
posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date
}
});
From 0a533d6ccc7356ef8b7c10e1c581cf630f7c3121 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Mar 2022 17:49:19 +0530
Subject: [PATCH 350/447] fix: skip already selected serials in sr selector
---
.../js/utils/serial_no_batch_selector.js | 28 +++++++++++++++----
erpnext/stock/doctype/serial_no/serial_no.py | 15 ++++++----
2 files changed, 32 insertions(+), 11 deletions(-)
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index ae8d3d455c..81ff351d37 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -94,6 +94,7 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector {
description: __('Fetch Serial Numbers based on FIFO'),
click: () => {
let qty = this.dialog.fields_dict.qty.get_value();
+ let already_selected_serial_nos = get_selected_serial_nos(me);
let numbers = frappe.call({
method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number",
args: {
@@ -101,7 +102,8 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector {
item_code: me.item_code,
warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '',
batch_nos: me.item.batch_no || null,
- posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date
+ posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date,
+ exclude_sr_nos: already_selected_serial_nos
}
});
@@ -577,15 +579,29 @@ function get_pending_qty_fields(me) {
return pending_qty_fields;
}
-function calc_total_selected_qty(me) {
+// get all items with same item code except row for which selector is open.
+function get_rows_with_same_item_code(me) {
const { frm: { doc: { items }}, item: { name, item_code }} = me;
- const totalSelectedQty = items
- .filter( item => ( item.name !== name ) && ( item.item_code === item_code ) )
- .map( item => flt(item.qty) )
- .reduce( (i, j) => i + j, 0);
+ return items.filter(item => (item.name !== name) && (item.item_code === item_code))
+}
+
+function calc_total_selected_qty(me) {
+ const totalSelectedQty = get_rows_with_same_item_code(me)
+ .map(item => flt(item.qty))
+ .reduce((i, j) => i + j, 0);
return totalSelectedQty;
}
+function get_selected_serial_nos(me) {
+ const selected_serial_nos = get_rows_with_same_item_code(me)
+ .map(item => item.serial_no)
+ .filter(serial => serial)
+ .map(sr_no_string => sr_no_string.split('\n'))
+ .reduce((acc, arr) => acc.concat(arr), [])
+ .filter(serial => serial);
+ return selected_serial_nos;
+};
+
function check_can_calculate_pending_qty(me) {
const { frm: { doc }, item } = me;
const docChecks = doc.bom_no
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index bf62f50c97..3cb97558c2 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -564,9 +564,15 @@ def get_delivery_note_serial_no(item_code, qty, delivery_note):
return serial_nos
@frappe.whitelist()
-def auto_fetch_serial_number(qty, item_code, warehouse, posting_date=None, batch_nos=None, for_doctype=None):
+def auto_fetch_serial_number(qty, item_code, warehouse,
+ posting_date=None, batch_nos=None, for_doctype=None, exclude_sr_nos=None):
filters = { "item_code": item_code, "warehouse": warehouse }
+ if exclude_sr_nos is None:
+ exclude_sr_nos = []
+ else:
+ exclude_sr_nos = get_serial_nos(clean_serial_no_string("\n".join(exclude_sr_nos)))
+
if batch_nos:
try:
filters["batch_no"] = json.loads(batch_nos) if (type(json.loads(batch_nos)) == list) else [json.loads(batch_nos)]
@@ -578,10 +584,9 @@ def auto_fetch_serial_number(qty, item_code, warehouse, posting_date=None, batch
serial_numbers = []
if for_doctype == 'POS Invoice':
- reserved_sr_nos = get_pos_reserved_serial_nos(filters)
- serial_numbers = fetch_serial_numbers(filters, qty, do_not_include=reserved_sr_nos)
- else:
- serial_numbers = fetch_serial_numbers(filters, qty)
+ exclude_sr_nos.extend(get_pos_reserved_serial_nos(filters))
+
+ serial_numbers = fetch_serial_numbers(filters, qty, do_not_include=exclude_sr_nos)
return [d.get('name') for d in serial_numbers]
From 4f8bb91eae3ac6d3a3837a21be7f635a4044ccd0 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Mar 2022 18:39:42 +0530
Subject: [PATCH 351/447] fix: sort serial nos before sending
---
erpnext/stock/doctype/serial_no/serial_no.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 3cb97558c2..884cb1e996 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -588,7 +588,7 @@ def auto_fetch_serial_number(qty, item_code, warehouse,
serial_numbers = fetch_serial_numbers(filters, qty, do_not_include=exclude_sr_nos)
- return [d.get('name') for d in serial_numbers]
+ return sorted([d.get('name') for d in serial_numbers])
@frappe.whitelist()
def get_pos_reserved_serial_nos(filters):
From b9eec331e3b884a2da5ee6995cca1a7135b7c3b0 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Mar 2022 18:40:14 +0530
Subject: [PATCH 352/447] test: auto serial fetching
---
.../stock/doctype/serial_no/test_serial_no.py | 50 +++++++++++++++++--
1 file changed, 47 insertions(+), 3 deletions(-)
diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py
index 057a7d4c01..0d362d9024 100644
--- a/erpnext/stock/doctype/serial_no/test_serial_no.py
+++ b/erpnext/stock/doctype/serial_no/test_serial_no.py
@@ -6,10 +6,12 @@
import frappe
+from frappe.tests.utils import FrappeTestCase
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+from erpnext.stock.doctype.serial_no.serial_no import *
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
@@ -18,9 +20,6 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
test_dependencies = ["Item"]
test_records = frappe.get_test_records('Serial No')
-from frappe.tests.utils import FrappeTestCase
-
-from erpnext.stock.doctype.serial_no.serial_no import *
class TestSerialNo(FrappeTestCase):
@@ -242,3 +241,48 @@ class TestSerialNo(FrappeTestCase):
)
self.assertEqual(value_diff, -113)
+ def test_auto_fetch(self):
+ item_code = make_item(properties={
+ "has_serial_no": 1,
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "serial_no_series": "TEST.#######"
+ }).name
+ warehouse = "_Test Warehouse - _TC"
+
+ in1 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=5)
+ in2 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=5)
+
+ in1.reload()
+ in2.reload()
+
+ batch_wise_serials = {
+ in1.items[0].batch_no: get_serial_nos(in1.items[0].serial_no),
+ in2.items[0].batch_no: get_serial_nos(in2.items[0].serial_no)
+ }
+
+ # Test FIFO
+ first_fetch = auto_fetch_serial_number(5, item_code, warehouse)
+ self.assertEqual(first_fetch, batch_wise_serials[in1.items[0].batch_no])
+
+ # partial FIFO
+ partial_fetch = auto_fetch_serial_number(2, item_code, warehouse)
+ self.assertTrue(set(partial_fetch).issubset(set(first_fetch)),
+ msg=f"{partial_fetch} should be subset of {first_fetch}")
+
+ # exclusion
+ remaining = auto_fetch_serial_number(3, item_code, warehouse, exclude_sr_nos=partial_fetch)
+ self.assertEqual(sorted(remaining + partial_fetch), first_fetch)
+
+ # batchwise
+ for batch, expected_serials in batch_wise_serials.items():
+ fetched_sr = auto_fetch_serial_number(5, item_code, warehouse, batch_nos=batch)
+ self.assertEqual(fetched_sr, sorted(expected_serials))
+
+ # non existing warehouse
+ self.assertEqual(auto_fetch_serial_number(10, item_code, warehouse="Nonexisting"), [])
+
+ # multi batch
+ all_serials = [sr for sr_list in batch_wise_serials.values() for sr in sr_list]
+ fetched_serials = auto_fetch_serial_number(10, item_code, warehouse, batch_nos=list(batch_wise_serials.keys()))
+ self.assertEqual(sorted(all_serials), fetched_serials)
From a585dff6fd05c885c6b4c35892f48d1fdf644487 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Mar 2022 18:50:05 +0530
Subject: [PATCH 353/447] refactor: batch no filter handling
---
erpnext/stock/doctype/serial_no/serial_no.py | 25 ++++++++++++++------
1 file changed, 18 insertions(+), 7 deletions(-)
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 884cb1e996..244f3e2f02 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -7,7 +7,16 @@ import json
import frappe
from frappe import ValidationError, _
from frappe.model.naming import make_autoname
-from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, nowdate
+from frappe.utils import (
+ add_days,
+ cint,
+ cstr,
+ flt,
+ get_link_to_form,
+ getdate,
+ nowdate,
+ safe_json_loads,
+)
from erpnext.controllers.stock_controller import StockController
from erpnext.stock.get_item_details import get_reserved_qty_for_so
@@ -566,7 +575,8 @@ def get_delivery_note_serial_no(item_code, qty, delivery_note):
@frappe.whitelist()
def auto_fetch_serial_number(qty, item_code, warehouse,
posting_date=None, batch_nos=None, for_doctype=None, exclude_sr_nos=None):
- filters = { "item_code": item_code, "warehouse": warehouse }
+
+ filters = frappe._dict({"item_code": item_code, "warehouse": warehouse})
if exclude_sr_nos is None:
exclude_sr_nos = []
@@ -574,13 +584,14 @@ def auto_fetch_serial_number(qty, item_code, warehouse,
exclude_sr_nos = get_serial_nos(clean_serial_no_string("\n".join(exclude_sr_nos)))
if batch_nos:
- try:
- filters["batch_no"] = json.loads(batch_nos) if (type(json.loads(batch_nos)) == list) else [json.loads(batch_nos)]
- except Exception:
- filters["batch_no"] = [batch_nos]
+ batch_nos = safe_json_loads(batch_nos)
+ if isinstance(batch_nos, list):
+ filters.batch_no = batch_nos
+ elif isinstance(batch_nos, str):
+ filters.batch_no = [batch_nos]
if posting_date:
- filters["expiry_date"] = posting_date
+ filters.expiry_date = posting_date
serial_numbers = []
if for_doctype == 'POS Invoice':
From 4b695915f458a7acb5257d1208d91f23998727eb Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Mar 2022 19:10:57 +0530
Subject: [PATCH 354/447] refactor: Use QB for serial fetching query
---
erpnext/stock/doctype/serial_no/serial_no.py | 55 ++++++++++---------
.../stock/doctype/serial_no/test_serial_no.py | 14 ++++-
2 files changed, 39 insertions(+), 30 deletions(-)
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 244f3e2f02..c77c6c3ba9 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -7,6 +7,7 @@ import json
import frappe
from frappe import ValidationError, _
from frappe.model.naming import make_autoname
+from frappe.query_builder.functions import Coalesce
from frappe.utils import (
add_days,
cint,
@@ -626,37 +627,37 @@ def get_pos_reserved_serial_nos(filters):
def fetch_serial_numbers(filters, qty, do_not_include=None):
if do_not_include is None:
do_not_include = []
- batch_join_selection = ""
- batch_no_condition = ""
+
batch_nos = filters.get("batch_no")
expiry_date = filters.get("expiry_date")
+ serial_no = frappe.qb.DocType("Serial No")
+
+ query = (
+ frappe.qb
+ .from_(serial_no)
+ .select(serial_no.name)
+ .where(
+ (serial_no.item_code == filters["item_code"])
+ & (serial_no.warehouse == filters["warehouse"])
+ & (Coalesce(serial_no.sales_invoice, "") == "")
+ & (Coalesce(serial_no.delivery_document_no, "") == "")
+ )
+ .orderby(serial_no.creation)
+ .limit(qty or 1)
+ )
+
+ if do_not_include:
+ query = query.where(serial_no.name.notin(do_not_include))
+
if batch_nos:
- batch_no_condition = """and sr.batch_no in ({}) """.format(', '.join("'%s'" % d for d in batch_nos))
+ query = query.where(serial_no.batch_no.isin(batch_nos))
if expiry_date:
- batch_join_selection = "LEFT JOIN `tabBatch` batch on sr.batch_no = batch.name "
- expiry_date_cond = "AND ifnull(batch.expiry_date, '2500-12-31') >= %(expiry_date)s "
- batch_no_condition += expiry_date_cond
-
- excluded_sr_nos = ", ".join(["" + frappe.db.escape(sr) + "" for sr in do_not_include]) or "''"
- serial_numbers = frappe.db.sql("""
- SELECT sr.name FROM `tabSerial No` sr {batch_join_selection}
- WHERE
- sr.name not in ({excluded_sr_nos}) AND
- sr.item_code = %(item_code)s AND
- sr.warehouse = %(warehouse)s AND
- ifnull(sr.sales_invoice,'') = '' AND
- ifnull(sr.delivery_document_no, '') = ''
- {batch_no_condition}
- ORDER BY
- sr.creation
- LIMIT
- {qty}
- """.format(
- excluded_sr_nos=excluded_sr_nos,
- qty=qty or 1,
- batch_join_selection=batch_join_selection,
- batch_no_condition=batch_no_condition
- ), filters, as_dict=1)
+ batch = frappe.qb.DocType("Batch")
+ query = (query
+ .left_join(batch).on(serial_no.batch_no == batch.name)
+ .where(Coalesce(batch.expiry_date, "4000-12-31") >= expiry_date)
+ )
+ serial_numbers = query.run(as_dict=True)
return serial_numbers
diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py
index 0d362d9024..cca6307840 100644
--- a/erpnext/stock/doctype/serial_no/test_serial_no.py
+++ b/erpnext/stock/doctype/serial_no/test_serial_no.py
@@ -256,14 +256,17 @@ class TestSerialNo(FrappeTestCase):
in1.reload()
in2.reload()
+ batch1 = in1.items[0].batch_no
+ batch2 = in2.items[0].batch_no
+
batch_wise_serials = {
- in1.items[0].batch_no: get_serial_nos(in1.items[0].serial_no),
- in2.items[0].batch_no: get_serial_nos(in2.items[0].serial_no)
+ batch1 : get_serial_nos(in1.items[0].serial_no),
+ batch2: get_serial_nos(in2.items[0].serial_no)
}
# Test FIFO
first_fetch = auto_fetch_serial_number(5, item_code, warehouse)
- self.assertEqual(first_fetch, batch_wise_serials[in1.items[0].batch_no])
+ self.assertEqual(first_fetch, batch_wise_serials[batch1])
# partial FIFO
partial_fetch = auto_fetch_serial_number(2, item_code, warehouse)
@@ -286,3 +289,8 @@ class TestSerialNo(FrappeTestCase):
all_serials = [sr for sr_list in batch_wise_serials.values() for sr in sr_list]
fetched_serials = auto_fetch_serial_number(10, item_code, warehouse, batch_nos=list(batch_wise_serials.keys()))
self.assertEqual(sorted(all_serials), fetched_serials)
+
+ # expiry date
+ frappe.db.set_value("Batch", batch1, "expiry_date", "1980-01-01")
+ non_expired_serials = auto_fetch_serial_number(5, item_code, warehouse, posting_date="2021-01-01", batch_nos=batch1)
+ self.assertEqual(non_expired_serials, [])
From 953afda01bc74f930d844d426d00f9f8cadea897 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Mar 2022 19:35:22 +0530
Subject: [PATCH 355/447] refactor(warehouse): raw query to ORM
---
erpnext/stock/doctype/warehouse/warehouse.py | 22 +++++++-------------
1 file changed, 8 insertions(+), 14 deletions(-)
diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py
index 9cfad86f14..fbca7a0394 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.py
+++ b/erpnext/stock/doctype/warehouse/warehouse.py
@@ -41,14 +41,13 @@ class Warehouse(NestedSet):
def on_trash(self):
# delete bin
- bins = frappe.db.sql("select * from `tabBin` where warehouse = %s",
- self.name, as_dict=1)
+ bins = frappe.get_all("Bin", fields="*", filters={"warehouse": self.name})
for d in bins:
if d['actual_qty'] or d['reserved_qty'] or d['ordered_qty'] or \
d['indented_qty'] or d['projected_qty'] or d['planned_qty']:
throw(_("Warehouse {0} can not be deleted as quantity exists for Item {1}").format(self.name, d['item_code']))
else:
- frappe.db.sql("delete from `tabBin` where name = %s", d['name'])
+ frappe.db.delete("Bin", filters={"name": d["name"]})
if self.check_if_sle_exists():
throw(_("Warehouse can not be deleted as stock ledger entry exists for this warehouse."))
@@ -60,12 +59,10 @@ class Warehouse(NestedSet):
self.unlink_from_items()
def check_if_sle_exists(self):
- return frappe.db.sql("""select name from `tabStock Ledger Entry`
- where warehouse = %s limit 1""", self.name)
+ return frappe.db.exists("Stock Ledger Entry", {"warehouse": self.name})
def check_if_child_exists(self):
- return frappe.db.sql("""select name from `tabWarehouse`
- where parent_warehouse = %s limit 1""", self.name)
+ return frappe.db.exists("Warehouse", {"parent_warehouse": self.name})
def convert_to_group_or_ledger(self):
if self.is_group:
@@ -92,10 +89,7 @@ class Warehouse(NestedSet):
return 1
def unlink_from_items(self):
- frappe.db.sql("""
- update `tabItem Default`
- set default_warehouse=NULL
- where default_warehouse=%s""", self.name)
+ frappe.db.set_value("Item Default", {"default_warehouse": self.name}, "default_warehouse", None)
@frappe.whitelist()
def get_children(doctype, parent=None, company=None, is_root=False):
@@ -169,10 +163,10 @@ def convert_to_group_or_ledger():
return frappe.get_doc("Warehouse", args.docname).convert_to_group_or_ledger()
def get_child_warehouses(warehouse):
- lft, rgt = frappe.get_cached_value("Warehouse", warehouse, ["lft", "rgt"])
+ from frappe.utils.nestedset import get_descendants_of
- return frappe.db.sql_list("""select name from `tabWarehouse`
- where lft >= %s and rgt <= %s""", (lft, rgt))
+ children = get_descendants_of("Warehouse", warehouse, ignore_permissions=True, order_by="lft")
+ return children + [warehouse] # append self for backward compatibility
def get_warehouses_based_on_account(account, company=None):
warehouses = []
From 684d9d66d1bf98a010516f125507712a3d59b2c8 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Mar 2022 19:53:10 +0530
Subject: [PATCH 356/447] test: warehouse conversion and treeview test
---
.../stock/doctype/warehouse/test_warehouse.py | 31 +++++++++++++++++--
erpnext/stock/doctype/warehouse/warehouse.py | 7 +++--
2 files changed, 33 insertions(+), 5 deletions(-)
diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py
index cdb771935b..08d7c99352 100644
--- a/erpnext/stock/doctype/warehouse/test_warehouse.py
+++ b/erpnext/stock/doctype/warehouse/test_warehouse.py
@@ -4,12 +4,12 @@
import frappe
from frappe.test_runner import make_test_records
from frappe.tests.utils import FrappeTestCase
-from frappe.utils import cint
import erpnext
-from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
+from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+from erpnext.stock.doctype.warehouse.warehouse import convert_to_group_or_ledger, get_children
test_records = frappe.get_test_records('Warehouse')
@@ -65,6 +65,33 @@ class TestWarehouse(FrappeTestCase):
f"{item} linked to {item_default.default_warehouse} in {warehouse_ids}."
)
+ def test_group_non_group_conversion(self):
+
+ warehouse = frappe.get_doc("Warehouse", create_warehouse("TestGroupConversion"))
+
+ convert_to_group_or_ledger(warehouse.name)
+ warehouse.reload()
+ self.assertEqual(warehouse.is_group, 1)
+
+ child = create_warehouse("GroupWHChild", {"parent_warehouse": warehouse.name})
+ # chid exists
+ self.assertRaises(frappe.ValidationError, convert_to_group_or_ledger, warehouse.name)
+ frappe.delete_doc("Warehouse", child)
+
+ convert_to_group_or_ledger(warehouse.name)
+ warehouse.reload()
+ self.assertEqual(warehouse.is_group, 0)
+
+ make_stock_entry(item_code="_Test Item", target=warehouse.name, qty=1)
+ # SLE exists
+ self.assertRaises(frappe.ValidationError, convert_to_group_or_ledger, warehouse.name)
+
+ def test_get_children(self):
+ company = "_Test Company"
+
+ children = get_children("Warehouse", parent=company, company=company, is_root=True)
+ self.assertTrue(any(wh['value'] == "_Test Warehouse - _TC" for wh in children))
+
def create_warehouse(warehouse_name, properties=None, company=None):
if not company:
diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py
index fbca7a0394..e2a5b4bc4a 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.py
+++ b/erpnext/stock/doctype/warehouse/warehouse.py
@@ -158,9 +158,10 @@ def add_node():
frappe.get_doc(args).insert()
@frappe.whitelist()
-def convert_to_group_or_ledger():
- args = frappe.form_dict
- return frappe.get_doc("Warehouse", args.docname).convert_to_group_or_ledger()
+def convert_to_group_or_ledger(docname=None):
+ if not docname:
+ docname = frappe.form_dict.docname
+ return frappe.get_doc("Warehouse", docname).convert_to_group_or_ledger()
def get_child_warehouses(warehouse):
from frappe.utils.nestedset import get_descendants_of
From 48595742338a5ca2cd429164a643ae70a8c63caf Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 19 Mar 2022 20:05:19 +0530
Subject: [PATCH 357/447] perf: Single query to delete bins instead of `N`
---
erpnext/stock/doctype/warehouse/warehouse.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py
index e2a5b4bc4a..4c7f41dcb5 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.py
+++ b/erpnext/stock/doctype/warehouse/warehouse.py
@@ -46,8 +46,6 @@ class Warehouse(NestedSet):
if d['actual_qty'] or d['reserved_qty'] or d['ordered_qty'] or \
d['indented_qty'] or d['projected_qty'] or d['planned_qty']:
throw(_("Warehouse {0} can not be deleted as quantity exists for Item {1}").format(self.name, d['item_code']))
- else:
- frappe.db.delete("Bin", filters={"name": d["name"]})
if self.check_if_sle_exists():
throw(_("Warehouse can not be deleted as stock ledger entry exists for this warehouse."))
@@ -55,6 +53,7 @@ class Warehouse(NestedSet):
if self.check_if_child_exists():
throw(_("Child warehouse exists for this warehouse. You can not delete this warehouse."))
+ frappe.db.delete("Bin", filters={"warehouse": self.name})
self.update_nsm_model()
self.unlink_from_items()
From 36df21067cd13eb7ffd8bc88b6eb144d016f0934 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Sun, 20 Mar 2022 03:58:24 +0100
Subject: [PATCH 358/447] refactor: sales analytics
---
.../report/sales_analytics/sales_analytics.js | 66 +++++++------------
1 file changed, 23 insertions(+), 43 deletions(-)
diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.js b/erpnext/selling/report/sales_analytics/sales_analytics.js
index 6b03c7d92f..d527e42ea4 100644
--- a/erpnext/selling/report/sales_analytics/sales_analytics.js
+++ b/erpnext/selling/report/sales_analytics/sales_analytics.js
@@ -82,62 +82,42 @@ frappe.query_reports["Sales Analytics"] = {
const tree_type = frappe.query_report.filters[0].value;
if (data_doctype != tree_type) return;
- row_name = data[2].content;
- length = data.length;
-
- if (tree_type == "Customer") {
- row_values = data
- .slice(4, length - 1)
- .map(function (column) {
- return column.content;
- });
- } else if (tree_type == "Item") {
- row_values = data
- .slice(5, length - 1)
- .map(function (column) {
- return column.content;
- });
- } else {
- row_values = data
- .slice(3, length - 1)
- .map(function (column) {
- return column.content;
- });
- }
-
- entry = {
- name: row_name,
- values: row_values,
- };
-
- let raw_data = frappe.query_report.chart.data;
- let new_datasets = raw_data.datasets;
-
- let element_found = new_datasets.some((element, index, array)=>{
- if(element.name == row_name){
- array.splice(index, 1)
- return true
+ const row_name = data[2].content;
+ const raw_data = frappe.query_report.chart.data;
+ const new_datasets = raw_data.datasets;
+ const element_found = new_datasets.some(
+ (element, index, array) => {
+ if (element.name == row_name) {
+ array.splice(index, 1);
+ return true;
+ }
+ return false;
}
- return false
- })
+ );
+ const slice_at = { Customer: 4, Item: 5 }[tree_type] || 3;
if (!element_found) {
- new_datasets.push(entry);
+ new_datasets.push({
+ name: row_name,
+ values: data
+ .slice(slice_at, data.length - 1)
+ .map(column => column.content),
+ });
}
- let new_data = {
+ const new_data = {
labels: raw_data.labels,
datasets: new_datasets,
};
- chart_options = {
+
+ frappe.query_report.render_chart({
data: new_data,
type: "line",
- };
- frappe.query_report.render_chart(chart_options);
+ });
frappe.query_report.raw_chart_data = new_data;
},
},
});
},
-}
+};
From ca8d7576911c49139c556a00352aa6b5deda1d5b Mon Sep 17 00:00:00 2001
From: HENRY Florian
Date: Sun, 20 Mar 2022 10:57:00 +0100
Subject: [PATCH 359/447] fix: clear "Retain Sample" and "Max Sample Quantity"
in Item card if Has Batch No is uncheck (#30307)
---
erpnext/stock/doctype/item/item.py | 8 ++++++++
erpnext/stock/doctype/item/test_item.py | 14 ++++++++++++++
erpnext/stock/doctype/warehouse/test_records.json | 7 +++++++
3 files changed, 29 insertions(+)
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 8ede95539b..3abeecd742 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -107,6 +107,7 @@ class Item(Document):
self.validate_variant_attributes()
self.validate_variant_based_on_change()
self.validate_fixed_asset()
+ self.clear_retain_sample()
self.validate_retain_sample()
self.validate_uom_conversion_factor()
self.validate_customer_provided_part()
@@ -209,6 +210,13 @@ class Item(Document):
frappe.throw(_("{0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format(
self.item_code))
+ def clear_retain_sample(self):
+ if not self.has_batch_no:
+ self.retain_sample = None
+
+ if not self.retain_sample:
+ self.sample_quantity = None
+
def add_default_uom_in_conversion_factor_table(self):
if not self.is_new() and self.has_value_changed("stock_uom"):
self.uoms = []
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 669cabc901..c12200b750 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -656,6 +656,20 @@ class TestItem(FrappeTestCase):
make_stock_entry(qty=1, item_code=item.name, target="_Test Warehouse - _TC", posting_date = add_days(today(), 5))
self.consume_item_code_with_differet_stock_transactions(item_code=item.name)
+ @change_settings("Stock Settings", {"sample_retention_warehouse": 0})
+ def test_retain_sample(self):
+ frappe.db.set_single_value('Stock Settings', 'sample_retention_warehouse', '_Test Retain Sample Warehouse')
+ item = make_item("_TestRetainSample", {'has_batch_no': 1, 'retain_sample': 1, 'sample_quantity': 1})
+
+ self.assertEqual(item.has_batch_no, 1)
+ self.assertEqual(item.retain_sample, 1)
+ self.assertEqual(item.sample_quantity, 1)
+
+ item.has_batch_no = None
+ item.save()
+ self.assertEqual(item.retain_sample, None)
+ self.assertEqual(item.sample_quantity, None)
+ item.delete()
def consume_item_code_with_differet_stock_transactions(self, item_code, warehouse="_Test Warehouse - _TC"):
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
diff --git a/erpnext/stock/doctype/warehouse/test_records.json b/erpnext/stock/doctype/warehouse/test_records.json
index e128558ed3..1b1e905850 100644
--- a/erpnext/stock/doctype/warehouse/test_records.json
+++ b/erpnext/stock/doctype/warehouse/test_records.json
@@ -60,5 +60,12 @@
"warehouse_name": "_Test Warehouse Group-C2",
"is_group": 0,
"parent_warehouse": "_Test Warehouse Group - _TC"
+ },
+ {
+ "company": "_Test Company",
+ "doctype": "Warehouse",
+ "warehouse_name": "_Test Retain Sample Warehouse",
+ "parent_warehouse": "_Test Warehouse Group - _TC",
+ "is_group": 0
}
]
From c2aad115c19338998be81b4c47f1cd2f695b96cf Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sun, 20 Mar 2022 19:26:34 +0530
Subject: [PATCH 360/447] fix: disable deferred naming on SLE/GLE if hash
method is used. (#30286)
* fix: dont rename GLE/SLE that dont have naming series
* test: tests for deferred naming of ledgers
---
erpnext/accounts/doctype/gl_entry/gl_entry.py | 2 +
.../stock_ledger_entry/stock_ledger_entry.py | 2 +
.../test_stock_ledger_entry.py | 61 +++++++++++++++++++
3 files changed, 65 insertions(+)
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index 9d1452b1b3..192099b0d1 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -32,6 +32,8 @@ class GLEntry(Document):
name will be changed using autoname options (in a scheduled job)
"""
self.name = frappe.generate_hash(txt="", length=10)
+ if self.meta.autoname == "hash":
+ self.to_rename = 0
def validate(self):
self.flags.ignore_submit_comment = True
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index c53830799d..2f593041bf 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -26,6 +26,8 @@ class StockLedgerEntry(Document):
name will be changed using autoname options (in a scheduled job)
"""
self.name = frappe.generate_hash(txt="", length=10)
+ if self.meta.autoname == "hash":
+ self.to_rename = 0
def validate(self):
self.flags.ignore_submit_comment = True
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 2352235cb3..fc579958be 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
@@ -7,9 +7,11 @@ from uuid import uuid4
import frappe
from frappe.core.page.permission_manager.permission_manager import reset
+from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, today
+from erpnext.accounts.doctype.gl_entry.gl_entry import rename_gle_sle_docs
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
@@ -939,3 +941,62 @@ def get_unique_suffix():
# Used to isolate valuation sensitive
# tests to prevent future tests from failing.
return str(uuid4())[:8].upper()
+
+
+class TestDeferredNaming(FrappeTestCase):
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ super().setUpClass()
+ cls.gle_autoname = frappe.get_meta("GL Entry").autoname
+ cls.sle_autoname = frappe.get_meta("Stock Ledger Entry").autoname
+
+ def setUp(self) -> None:
+ self.item = make_item().name
+ self.warehouse = "Stores - TCP1"
+ self.company = "_Test Company with perpetual inventory"
+
+ def tearDown(self) -> None:
+ make_property_setter(doctype="GL Entry", for_doctype=True,
+ property="autoname", value=self.gle_autoname, property_type="Data", fieldname=None)
+ make_property_setter(doctype="Stock Ledger Entry", for_doctype=True,
+ property="autoname", value=self.sle_autoname, property_type="Data", fieldname=None)
+
+ # since deferred naming autocommits, commit all changes to avoid flake
+ frappe.db.commit() # nosemgrep
+
+ @staticmethod
+ def get_gle_sles(se):
+ filters = {"voucher_type": se.doctype, "voucher_no": se.name}
+ gle = set(frappe.get_list("GL Entry", filters, pluck="name"))
+ sle = set(frappe.get_list("Stock Ledger Entry", filters, pluck="name"))
+ return gle, sle
+
+ def test_deferred_naming(self):
+ se = make_stock_entry(item_code=self.item, to_warehouse=self.warehouse,
+ qty=10, rate=100, company=self.company)
+
+ gle, sle = self.get_gle_sles(se)
+ rename_gle_sle_docs()
+ renamed_gle, renamed_sle = self.get_gle_sles(se)
+
+ self.assertFalse(gle & renamed_gle, msg="GLEs not renamed")
+ self.assertFalse(sle & renamed_sle, msg="SLEs not renamed")
+ se.cancel()
+
+ def test_hash_naming(self):
+ # disable naming series
+ for doctype in ("GL Entry", "Stock Ledger Entry"):
+ make_property_setter(doctype=doctype, for_doctype=True,
+ property="autoname", value="hash", property_type="Data", fieldname=None)
+
+ se = make_stock_entry(item_code=self.item, to_warehouse=self.warehouse,
+ qty=10, rate=100, company=self.company)
+
+ gle, sle = self.get_gle_sles(se)
+ rename_gle_sle_docs()
+ renamed_gle, renamed_sle = self.get_gle_sles(se)
+
+ self.assertEqual(gle, renamed_gle, msg="GLEs are renamed while using hash naming")
+ self.assertEqual(sle, renamed_sle, msg="SLEs are renamed while using hash naming")
+ se.cancel()
From 0a015b7f70039d8f0aeeb5aad2473d0d8846b0db Mon Sep 17 00:00:00 2001
From: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
Date: Sun, 20 Mar 2022 14:59:41 +0100
Subject: [PATCH 361/447] refactor: remove redundant if-statement (#30311)
---
.../production_plan/production_plan.py | 20 +++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 48cd753d75..2b6e6968bd 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -1018,21 +1018,21 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
required_qty = item.get("quantity")
# get available material by transferring to production warehouse
for d in locations:
- if required_qty <=0: return
+ if required_qty <= 0:
+ return
new_dict = copy.deepcopy(item)
quantity = required_qty if d.get("qty") > required_qty else d.get("qty")
- if required_qty > 0:
- new_dict.update({
- "quantity": quantity,
- "material_request_type": "Material Transfer",
- "uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM
- "from_warehouse": d.get("warehouse")
- })
+ new_dict.update({
+ "quantity": quantity,
+ "material_request_type": "Material Transfer",
+ "uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM
+ "from_warehouse": d.get("warehouse")
+ })
- required_qty -= quantity
- new_mr_items.append(new_dict)
+ required_qty -= quantity
+ new_mr_items.append(new_dict)
# raise purchase request for remaining qty
if required_qty:
From 2ff67902833283e120237435be3d53f8da8df2c4 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Mon, 21 Mar 2022 09:42:28 +0530
Subject: [PATCH 362/447] fix: P&L account validation on cancellation
---
erpnext/accounts/doctype/gl_entry/gl_entry.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index 9d1452b1b3..b2ba485ab8 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -134,7 +134,7 @@ class GLEntry(Document):
def check_pl_account(self):
if self.is_opening=='Yes' and \
- frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss":
+ frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and not self.is_cancelled:
frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry")
.format(self.voucher_type, self.voucher_no, self.account))
From d5fd8e0ba6f2b6297339dd12bb7cbf1dc0c3155e Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Mon, 21 Mar 2022 11:36:30 +0530
Subject: [PATCH 363/447] fix: prevent multiple save on applying coupon code
---
.../selling/page/point_of_sale/pos_payment.js | 32 +++++++++++--------
1 file changed, 18 insertions(+), 14 deletions(-)
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index 1e9f6d7d92..326ee59d11 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -170,20 +170,24 @@ erpnext.PointOfSale.Payment = class {
});
frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => {
- if (!frm.doc.ignore_pricing_rule && frm.doc.coupon_code) {
- frappe.run_serially([
- () => frm.doc.ignore_pricing_rule=1,
- () => frm.trigger('ignore_pricing_rule'),
- () => frm.doc.ignore_pricing_rule=0,
- () => frm.trigger('apply_pricing_rule'),
- () => frm.save(),
- () => this.update_totals_section(frm.doc)
- ]);
- } else if (frm.doc.ignore_pricing_rule && frm.doc.coupon_code) {
- frappe.show_alert({
- message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."),
- indicator: "orange"
- });
+ if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) {
+ if (!frm.doc.ignore_pricing_rule) {
+ frm.applying_pos_coupon_code = true
+ frappe.run_serially([
+ () => frm.doc.ignore_pricing_rule=1,
+ () => frm.trigger('ignore_pricing_rule'),
+ () => frm.doc.ignore_pricing_rule=0,
+ () => frm.trigger('apply_pricing_rule'),
+ () => frm.save(),
+ () => this.update_totals_section(frm.doc),
+ () => (frm.applying_pos_coupon_code = false)
+ ]);
+ } else if (frm.doc.ignore_pricing_rule) {
+ frappe.show_alert({
+ message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."),
+ indicator: "orange"
+ });
+ }
}
});
From 3a7776ea7f01e36f3380dae1abab3b3a5ef4ea83 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Mon, 21 Mar 2022 12:04:18 +0530
Subject: [PATCH 364/447] fix: Contribution amount against invoices in Sales
Person Dashboard
---
.../setup/doctype/sales_person/sales_person.js | 8 ++++++--
.../setup/doctype/sales_person/sales_person.py | 15 +++++++++------
2 files changed, 15 insertions(+), 8 deletions(-)
diff --git a/erpnext/setup/doctype/sales_person/sales_person.js b/erpnext/setup/doctype/sales_person/sales_person.js
index b71a92f8a9..d86a8f3d98 100644
--- a/erpnext/setup/doctype/sales_person/sales_person.js
+++ b/erpnext/setup/doctype/sales_person/sales_person.js
@@ -4,8 +4,12 @@
frappe.ui.form.on('Sales Person', {
refresh: function(frm) {
if(frm.doc.__onload && frm.doc.__onload.dashboard_info) {
- var info = frm.doc.__onload.dashboard_info;
- frm.dashboard.add_indicator(__('Total Contribution Amount: {0}', [format_currency(info.allocated_amount, info.currency)]), 'blue');
+ let info = frm.doc.__onload.dashboard_info;
+ frm.dashboard.add_indicator(__('Total Contribution Amount Against Orders: {0}',
+ [format_currency(info.allocated_amount_against_order, info.currency)]), 'blue');
+
+ frm.dashboard.add_indicator(__('Total Contribution Amount Against Invoices: {0}',
+ [format_currency(info.allocated_amount_against_invoice, info.currency)]), 'blue');
}
},
diff --git a/erpnext/setup/doctype/sales_person/sales_person.py b/erpnext/setup/doctype/sales_person/sales_person.py
index b79a566578..6af1b312bd 100644
--- a/erpnext/setup/doctype/sales_person/sales_person.py
+++ b/erpnext/setup/doctype/sales_person/sales_person.py
@@ -28,14 +28,17 @@ class SalesPerson(NestedSet):
def load_dashboard_info(self):
company_default_currency = get_default_currency()
- allocated_amount = frappe.db.sql("""
- select sum(allocated_amount)
- from `tabSales Team`
- where sales_person = %s and docstatus=1 and parenttype = 'Sales Order'
- """,(self.sales_person_name))
+ allocated_amount_against_order = flt(frappe.db.get_value('Sales Team',
+ {'docstatus': 1, 'parenttype': 'Sales Order', 'sales_person': self.sales_person_name},
+ 'sum(allocated_amount)'))
+
+ allocated_amount_against_invoice = flt(frappe.db.get_value('Sales Team',
+ {'docstatus': 1, 'parenttype': 'Sales Invoice', 'sales_person': self.sales_person_name},
+ 'sum(allocated_amount)'))
info = {}
- info["allocated_amount"] = flt(allocated_amount[0][0]) if allocated_amount else 0
+ info["allocated_amount_against_order"] = allocated_amount_against_order
+ info["allocated_amount_against_invoice"] = allocated_amount_against_invoice
info["currency"] = company_default_currency
self.set_onload('dashboard_info', info)
From 2f82e237ef650ab49d08dfd3eeaf56f1f299c84a Mon Sep 17 00:00:00 2001
From: Saqib Ansari