diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 7315ae89365..403e2bdfe78 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -224,10 +224,7 @@ frappe.ui.form.on('Payment Entry', {
(frm.doc.total_allocated_amount > party_amount)));
frm.toggle_display("set_exchange_gain_loss",
- (frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount &&
- ((frm.doc.paid_from_account_currency != company_currency ||
- frm.doc.paid_to_account_currency != company_currency) &&
- frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency)));
+ frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount);
frm.refresh_fields();
},
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
index c45b069730e..2438f4b1ab8 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
@@ -35,10 +35,11 @@ class PricingRule(Document):
self.margin_rate_or_amount = 0.0
def validate_duplicate_apply_on(self):
- field = apply_on_dict.get(self.apply_on)
- values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field) if field]
- if len(values) != len(set(values)):
- frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
+ if self.apply_on != "Transaction":
+ field = apply_on_dict.get(self.apply_on)
+ values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field) if field]
+ if len(values) != len(set(values)):
+ frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
def validate_mandatory(self):
for apply_on, field in apply_on_dict.items():
diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py
index c3599593103..a73c72c6d82 100644
--- a/erpnext/accounts/report/purchase_register/purchase_register.py
+++ b/erpnext/accounts/report/purchase_register/purchase_register.py
@@ -124,11 +124,10 @@ def get_columns(invoice_list, additional_table_columns):
_("Purchase Receipt") + ":Link/Purchase Receipt:100",
{"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80},
]
- expense_accounts = (
- tax_accounts
- ) = (
- expense_columns
- ) = tax_columns = unrealized_profit_loss_accounts = unrealized_profit_loss_account_columns = []
+
+ expense_accounts = []
+ tax_accounts = []
+ unrealized_profit_loss_accounts = []
if invoice_list:
expense_accounts = frappe.db.sql_list(
@@ -163,10 +162,11 @@ def get_columns(invoice_list, additional_table_columns):
unrealized_profit_loss_account_columns = [
(account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts
]
-
- for account in tax_accounts:
- if account not in expense_accounts:
- tax_columns.append(account + ":Currency/currency:120")
+ tax_columns = [
+ (account + ":Currency/currency:120")
+ for account in tax_accounts
+ if account not in expense_accounts
+ ]
columns = (
columns
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index 03ff2691e7d..96c730c668c 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -126,7 +126,8 @@ class Opportunity(TransactionBase):
def declare_enquiry_lost(self, lost_reasons_list, competitors, detailed_reason=None):
if not self.has_active_quotation():
self.status = "Lost"
- self.lost_reasons = self.competitors = []
+ self.lost_reasons = []
+ self.competitors = []
if detailed_reason:
self.order_lost_reason = detailed_reason
diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py
index 77e6ae2e04f..3a46fb0879c 100644
--- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py
+++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py
@@ -1,9 +1,9 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import json
+from itertools import groupby
import frappe
-import pandas
from frappe import _
from frappe.utils import flt
@@ -101,18 +101,19 @@ class OpportunitySummaryBySalesStage(object):
self.convert_to_base_currency()
- dataframe = pandas.DataFrame.from_records(self.query_result)
- dataframe.replace(to_replace=[None], value="Not Assigned", inplace=True)
- result = dataframe.groupby(["sales_stage", based_on], as_index=False)["amount"].sum()
+ for row in self.query_result:
+ if not row.get(based_on):
+ row[based_on] = "Not Assigned"
self.grouped_data = []
- for i in range(len(result["amount"])):
+ grouping_key = lambda o: (o["sales_stage"], o[based_on]) # noqa
+ for (sales_stage, _based_on), rows in groupby(self.query_result, grouping_key):
self.grouped_data.append(
{
- "sales_stage": result["sales_stage"][i],
- based_on: result[based_on][i],
- "amount": result["amount"][i],
+ "sales_stage": sales_stage,
+ based_on: _based_on,
+ "amount": sum(flt(r["amount"]) for r in rows),
}
)
diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py
index b0c174be234..d23a22ac46d 100644
--- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py
+++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py
@@ -3,9 +3,9 @@
import json
from datetime import date
+from itertools import groupby
import frappe
-import pandas
from dateutil.relativedelta import relativedelta
from frappe import _
from frappe.utils import cint, flt
@@ -109,18 +109,15 @@ class SalesPipelineAnalytics(object):
self.convert_to_base_currency()
- dataframe = pandas.DataFrame.from_records(self.query_result)
- dataframe.replace(to_replace=[None], value="Not Assigned", inplace=True)
- result = dataframe.groupby([self.pipeline_by, self.period_by], as_index=False)["amount"].sum()
-
self.grouped_data = []
- for i in range(len(result["amount"])):
+ grouping_key = lambda o: (o.get(self.pipeline_by) or "Not Assigned", o[self.period_by]) # noqa
+ for (pipeline_by, period_by), rows in groupby(self.query_result, grouping_key):
self.grouped_data.append(
{
- self.pipeline_by: result[self.pipeline_by][i],
- self.period_by: result[self.period_by][i],
- "amount": result["amount"][i],
+ self.pipeline_by: pipeline_by,
+ self.period_by: period_by,
+ "amount": sum(flt(r["amount"]) for r in rows),
}
)
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.js b/erpnext/hr/doctype/leave_allocation/leave_allocation.js
index 9742387c16a..aef44122513 100755
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.js
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.js
@@ -34,6 +34,15 @@ frappe.ui.form.on("Leave Allocation", {
});
}
}
+
+ // make new leaves allocated field read only if allocation is created via leave policy assignment
+ // and leave type is earned leave, since these leaves would be allocated via the scheduler
+ if (frm.doc.leave_policy_assignment) {
+ frappe.db.get_value("Leave Type", frm.doc.leave_type, "is_earned_leave", (r) => {
+ if (r && cint(r.is_earned_leave))
+ frm.set_df_property("new_leaves_allocated", "read_only", 1);
+ });
+ }
},
expire_allocation: function(frm) {
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
index 9ecbe014b97..9d1db9b17f5 100644
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
@@ -237,7 +237,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-01-18 19:15:53.262536",
+ "modified": "2022-04-07 09:50:33.145825",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Allocation",
@@ -281,5 +281,6 @@
"sort_order": "DESC",
"states": [],
"timeline_field": "employee",
- "title_field": "employee_name"
+ "title_field": "employee_name",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index fd69a9b4f1d..40ab8053c9d 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -353,6 +353,17 @@ def update_previous_leave_allocation(
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
+ if e_leave_type.based_on_date_of_joining:
+ text = _("allocated {0} leave(s) via scheduler on {1} based on the date of joining").format(
+ frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
+ )
+ else:
+ text = _("allocated {0} leave(s) via scheduler on {1}").format(
+ frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
+ )
+
+ allocation.add_comment(comment_type="Info", text=text)
+
def get_monthly_earned_leave(annual_leaves, frequency, rounding):
earned_leaves = 0.0
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index 304d1a75c9a..25351800920 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -584,9 +584,10 @@ def regenerate_repayment_schedule(loan, cancel=0):
balance_amount / len(loan_doc.get("repayment_schedule")) - accrued_entries
)
else:
- if not cancel:
+ repayment_period = loan_doc.repayment_periods - accrued_entries
+ if not cancel and repayment_period > 0:
monthly_repayment_amount = get_monthly_repayment_amount(
- balance_amount, loan_doc.rate_of_interest, loan_doc.repayment_periods - accrued_entries
+ balance_amount, loan_doc.rate_of_interest, repayment_period
)
else:
monthly_repayment_amount = last_repayment_amount
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index d85b8a60d2f..b2824e139c8 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -28,12 +28,12 @@ frappe.ui.form.on('Job Card', {
frappe.flags.resume_job = 0;
let has_items = frm.doc.items && frm.doc.items.length;
- if (frm.doc.__onload.work_order_closed) {
+ if (!frm.is_new() && frm.doc.__onload.work_order_closed) {
frm.disable_save();
return;
}
- if (!frm.doc.__islocal && has_items && frm.doc.docstatus < 2) {
+ if (!frm.is_new() && has_items && frm.doc.docstatus < 2) {
let to_request = frm.doc.for_quantity > frm.doc.transferred_qty;
let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
diff --git a/erpnext/patches/v12_0/update_is_cancelled_field.py b/erpnext/patches/v12_0/update_is_cancelled_field.py
index b567823b062..398dd700eda 100644
--- a/erpnext/patches/v12_0/update_is_cancelled_field.py
+++ b/erpnext/patches/v12_0/update_is_cancelled_field.py
@@ -20,7 +20,7 @@ def execute():
"""
UPDATE `tab{doctype}`
SET is_cancelled = 0
- where is_cancelled in ('', NULL, 'No')""".format(
+ where is_cancelled in ('', 'No') or is_cancelled is NULL""".format(
doctype=doctype
)
)
diff --git a/erpnext/patches/v14_0/change_is_subcontracted_fieldtype.py b/erpnext/patches/v14_0/change_is_subcontracted_fieldtype.py
index ba919a756a8..9b07ba846f4 100644
--- a/erpnext/patches/v14_0/change_is_subcontracted_fieldtype.py
+++ b/erpnext/patches/v14_0/change_is_subcontracted_fieldtype.py
@@ -10,7 +10,7 @@ def execute():
"""
UPDATE `tab{doctype}`
SET is_subcontracted = 0
- where is_subcontracted in ('', NULL, 'No')""".format(
+ where is_subcontracted in ('', 'No') or is_subcontracted is null""".format(
doctype=doctype
)
)
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index f4845459839..64c5ee59dc8 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -609,8 +609,8 @@ function check_can_calculate_pending_qty(me) {
&& erpnext.stock.bom
&& erpnext.stock.bom.name === doc.bom_no;
const itemChecks = !!item
- && !item.allow_alternative_item
- && erpnext.stock.bom && erpnext.stock.items
+ && !item.original_item
+ && erpnext.stock.bom && erpnext.stock.bom.items
&& (item.item_code in erpnext.stock.bom.items);
return docChecks && itemChecks;
}
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 2e5cbb80cb6..8889a5f939a 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -100,7 +100,8 @@ class Customer(TransactionBase):
@frappe.whitelist()
def get_customer_group_details(self):
doc = frappe.get_doc("Customer Group", self.customer_group)
- self.accounts = self.credit_limits = []
+ self.accounts = []
+ self.credit_limits = []
self.payment_terms = self.default_price_list = ""
tables = [["accounts", "account"], ["credit_limits", "credit_limit"]]
diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py
index 4027d2ee146..36ca2b2fdc2 100644
--- a/erpnext/selling/doctype/customer/test_customer.py
+++ b/erpnext/selling/doctype/customer/test_customer.py
@@ -45,7 +45,8 @@ class TestCustomer(FrappeTestCase):
c_doc.customer_name = "Testing Customer"
c_doc.customer_group = "_Testing Customer Group"
c_doc.payment_terms = c_doc.default_price_list = ""
- c_doc.accounts = c_doc.credit_limits = []
+ c_doc.accounts = []
+ c_doc.credit_limits = []
c_doc.insert()
c_doc.get_customer_group_details()
self.assertEqual(c_doc.payment_terms, "_Test Payment Term Template 3")
diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.py b/erpnext/selling/page/sales_funnel/sales_funnel.py
index c626f5b05fc..6b33a717531 100644
--- a/erpnext/selling/page/sales_funnel/sales_funnel.py
+++ b/erpnext/selling/page/sales_funnel/sales_funnel.py
@@ -1,10 +1,11 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
+from itertools import groupby
import frappe
-import pandas as pd
from frappe import _
+from frappe.utils import flt
from erpnext.accounts.report.utils import convert
@@ -89,28 +90,21 @@ def get_opp_by_lead_source(from_date, to_date, company):
for x in opportunities
]
- df = (
- pd.DataFrame(cp_opportunities)
- .groupby(["source", "sales_stage"], as_index=False)
- .agg({"compound_amount": "sum"})
- )
+ summary = {}
+ sales_stages = set()
+ group_key = lambda o: (o["source"], o["sales_stage"]) # noqa
+ for (source, sales_stage), rows in groupby(cp_opportunities, group_key):
+ summary.setdefault(source, {})[sales_stage] = sum(r["compound_amount"] for r in rows)
+ sales_stages.add(sales_stage)
- result = {}
- result["labels"] = list(set(df.source.values))
- result["datasets"] = []
-
- for s in set(df.sales_stage.values):
- result["datasets"].append(
- {"name": s, "values": [0] * len(result["labels"]), "chartType": "bar"}
- )
-
- for row in df.itertuples():
- source_index = result["labels"].index(row.source)
-
- for dataset in result["datasets"]:
- if dataset["name"] == row.sales_stage:
- dataset["values"][source_index] = row.compound_amount
+ pivot_table = []
+ for sales_stage in sales_stages:
+ row = []
+ for source, sales_stage_values in summary.items():
+ row.append(flt(sales_stage_values.get(sales_stage)))
+ pivot_table.append({"chartType": "bar", "name": sales_stage, "values": row})
+ result = {"datasets": pivot_table, "labels": list(summary.keys())}
return result
else:
@@ -148,20 +142,14 @@ def get_pipeline_data(from_date, to_date, company):
for x in opportunities
]
- df = (
- pd.DataFrame(cp_opportunities)
- .groupby(["sales_stage"], as_index=True)
- .agg({"compound_amount": "sum"})
- .to_dict()
- )
-
- result = {}
- result["labels"] = df["compound_amount"].keys()
- result["datasets"] = []
- result["datasets"].append(
- {"name": _("Total Amount"), "values": df["compound_amount"].values(), "chartType": "bar"}
- )
+ summary = {}
+ for sales_stage, rows in groupby(cp_opportunities, lambda o: o["sales_stage"]):
+ summary[sales_stage] = sum(flt(r["compound_amount"]) for r in rows)
+ result = {
+ "labels": list(summary.keys()),
+ "datasets": [{"name": _("Total Amount"), "values": list(summary.values()), "chartType": "bar"}],
+ }
return result
else:
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 0e36b3fe3d2..c068ae3b5a4 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
@@ -27,28 +27,55 @@ function get_filters() {
"default": frappe.datetime.get_today()
},
{
- "fieldname":"sales_order",
- "label": __("Sales Order"),
- "fieldtype": "MultiSelectList",
+ "fieldname":"customer_group",
+ "label": __("Customer Group"),
+ "fieldtype": "Link",
"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")]]
+ "options": "Customer Group",
+ },
+ {
+ "fieldname":"customer",
+ "label": __("Customer"),
+ "fieldtype": "Link",
+ "width": 100,
+ "options": "Customer",
+ "get_query": () => {
+ var customer_group = frappe.query_report.get_filter_value('customer_group');
+ return{
+ "query": "erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order.get_customers_or_items",
+ "filters": [
+ ['Customer', 'disabled', '=', '0'],
+ ['Customer Group','name', '=', customer_group]
+ ]
+ }
+ }
+ },
+ {
+ "fieldname":"item_group",
+ "label": __("Item Group"),
+ "fieldtype": "Link",
+ "width": 100,
+ "options": "Item Group",
+
+ },
+ {
+ "fieldname":"item",
+ "label": __("Item"),
+ "fieldtype": "Link",
+ "width": 100,
+ "options": "Item",
+ "get_query": () => {
+ var item_group = frappe.query_report.get_filter_value('item_group');
+ return{
+ "query": "erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order.get_customers_or_items",
+ "filters": [
+ ['Item', 'disabled', '=', '0'],
+ ['Item Group','name', '=', item_group]
+ ]
}
- },
- on_change: function(){
- frappe.query_report.refresh();
}
}
]
-
return filters;
}
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 7f797f67eee..cb22fb6a80f 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
@@ -3,7 +3,7 @@
import frappe
from frappe import _, qb, query_builder
-from frappe.query_builder import functions
+from frappe.query_builder import Criterion, functions
def get_columns():
@@ -14,6 +14,12 @@ def get_columns():
"fieldtype": "Link",
"options": "Sales Order",
},
+ {
+ "label": _("Customer"),
+ "fieldname": "customer",
+ "fieldtype": "Link",
+ "options": "Customer",
+ },
{
"label": _("Posting Date"),
"fieldname": "submitted",
@@ -67,6 +73,55 @@ def get_columns():
return columns
+def get_descendants_of(doctype, group_name):
+ group_doc = qb.DocType(doctype)
+ # get lft and rgt of group node
+ lft, rgt = (
+ qb.from_(group_doc).select(group_doc.lft, group_doc.rgt).where(group_doc.name == group_name)
+ ).run()[0]
+
+ # get all children of group node
+ query = (
+ qb.from_(group_doc).select(group_doc.name).where((group_doc.lft >= lft) & (group_doc.rgt <= rgt))
+ )
+
+ child_nodes = []
+ for x in query.run():
+ child_nodes.append(x[0])
+
+ return child_nodes
+
+
+@frappe.whitelist()
+@frappe.validate_and_sanitize_search_inputs
+def get_customers_or_items(doctype, txt, searchfield, start, page_len, filters):
+ filter_list = []
+ if isinstance(filters, list):
+ for item in filters:
+ if item[0] == doctype:
+ filter_list.append(item)
+ elif item[0] == "Customer Group":
+ if item[3] != "":
+ filter_list.append(
+ [doctype, "customer_group", "in", get_descendants_of("Customer Group", item[3])]
+ )
+ elif item[0] == "Item Group":
+ if item[3] != "":
+ filter_list.append([doctype, "item_group", "in", get_descendants_of("Item Group", item[3])])
+
+ if searchfield and txt:
+ filter_list.append([doctype, searchfield, "like", "%%%s%%" % txt])
+
+ return frappe.desk.reportview.execute(
+ doctype,
+ filters=filter_list,
+ fields=["name", "customer_group"] if doctype == "Customer" else ["name", "item_group"],
+ limit_start=start,
+ limit_page_length=page_len,
+ as_list=True,
+ )
+
+
def get_conditions(filters):
"""
Convert filter options to conditions used in query
@@ -79,11 +134,37 @@ def get_conditions(filters):
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 build_filter_criterions(filters):
+ filters = frappe._dict(filters) if filters else frappe._dict({})
+ qb_criterions = []
+
+ if filters.customer_group:
+ qb_criterions.append(
+ qb.DocType("Sales Order").customer_group.isin(
+ get_descendants_of("Customer Group", filters.customer_group)
+ )
+ )
+
+ if filters.customer:
+ qb_criterions.append(qb.DocType("Sales Order").customer == filters.customer)
+
+ if filters.item_group:
+ qb_criterions.append(
+ qb.DocType("Sales Order Item").item_group.isin(
+ get_descendants_of("Item Group", filters.item_group)
+ )
+ )
+
+ if filters.item:
+ qb_criterions.append(qb.DocType("Sales Order Item").item_code == filters.item)
+
+ return qb_criterions
+
+
def get_so_with_invoices(filters):
"""
Get Sales Order with payment terms template with their associated Invoices
@@ -92,16 +173,23 @@ def get_so_with_invoices(filters):
so = qb.DocType("Sales Order")
ps = qb.DocType("Payment Schedule")
+ soi = qb.DocType("Sales Order Item")
+
+ conditions = get_conditions(filters)
+ filter_criterions = build_filter_criterions(filters)
+
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(soi)
+ .on(soi.parent == so.name)
.join(ps)
.on(ps.parent == so.name)
.select(
so.name,
+ so.customer,
so.transaction_date.as_("submitted"),
ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"),
ps.payment_term,
@@ -117,12 +205,10 @@ def get_so_with_invoices(filters):
& (so.company == conditions.company)
& (so.transaction_date[conditions.start_date : conditions.end_date])
)
+ .where(Criterion.all(filter_criterions))
.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 = []
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 89940a6e872..9d542f5079c 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
@@ -11,10 +11,13 @@ from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_s
)
from erpnext.stock.doctype.item.test_item import create_item
-test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template"]
+test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template", "Customer"]
class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
+ def tearDown(self):
+ frappe.db.rollback()
+
def create_payment_terms_template(self):
# create template for 50-50 payments
template = None
@@ -48,9 +51,9 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
template.insert()
self.template = template
- def test_payment_terms_status(self):
+ def test_01_payment_terms_status(self):
self.create_payment_terms_template()
- item = create_item(item_code="_Test Excavator", is_stock_item=0)
+ item = create_item(item_code="_Test Excavator 1", is_stock_item=0)
so = make_sales_order(
transaction_date="2021-06-15",
delivery_date=add_days("2021-06-15", -30),
@@ -78,13 +81,14 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
"company": "_Test Company",
"period_start_date": "2021-06-01",
"period_end_date": "2021-06-30",
- "sales_order": [so.name],
+ "item": item.item_code,
}
)
expected_value = [
{
"name": so.name,
+ "customer": so.customer,
"submitted": datetime.date(2021, 6, 15),
"status": "Completed",
"payment_term": None,
@@ -98,6 +102,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
},
{
"name": so.name,
+ "customer": so.customer,
"submitted": datetime.date(2021, 6, 15),
"status": "Partly Paid",
"payment_term": None,
@@ -132,11 +137,11 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
)
doc.insert()
- def test_alternate_currency(self):
+ def test_02_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)
+ item = create_item(item_code="_Test Excavator 2", is_stock_item=0)
so = make_sales_order(
transaction_date=transaction_date,
currency="USD",
@@ -166,7 +171,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
"company": "_Test Company",
"period_start_date": "2021-06-01",
"period_end_date": "2021-06-30",
- "sales_order": [so.name],
+ "item": item.item_code,
}
)
@@ -174,6 +179,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
expected_value = [
{
"name": so.name,
+ "customer": so.customer,
"submitted": datetime.date(2021, 6, 15),
"status": "Completed",
"payment_term": None,
@@ -187,6 +193,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
},
{
"name": so.name,
+ "customer": so.customer,
"submitted": datetime.date(2021, 6, 15),
"status": "Partly Paid",
"payment_term": None,
@@ -200,3 +207,134 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
},
]
self.assertEqual(data, expected_value)
+
+ def test_03_group_filters(self):
+ transaction_date = "2021-06-15"
+ self.create_payment_terms_template()
+ item1 = create_item(item_code="_Test Excavator 1", is_stock_item=0)
+ item1.item_group = "Products"
+ item1.save()
+
+ so1 = make_sales_order(
+ transaction_date=transaction_date,
+ delivery_date=add_days(transaction_date, -30),
+ item=item1.item_code,
+ qty=1,
+ rate=1000000,
+ do_not_save=True,
+ )
+ so1.po_no = ""
+ so1.taxes_and_charges = ""
+ so1.taxes = ""
+ so1.payment_terms_template = self.template.name
+ so1.save()
+ so1.submit()
+
+ item2 = create_item(item_code="_Test Steel", is_stock_item=0)
+ item2.item_group = "Raw Material"
+ item2.save()
+
+ so2 = make_sales_order(
+ customer="_Test Customer 1",
+ transaction_date=transaction_date,
+ delivery_date=add_days(transaction_date, -30),
+ item=item2.item_code,
+ qty=100,
+ rate=1000,
+ do_not_save=True,
+ )
+ so2.po_no = ""
+ so2.taxes_and_charges = ""
+ so2.taxes = ""
+ so2.payment_terms_template = self.template.name
+ so2.save()
+ so2.submit()
+
+ base_filters = {
+ "company": "_Test Company",
+ "period_start_date": "2021-06-01",
+ "period_end_date": "2021-06-30",
+ }
+
+ expected_value_so1 = [
+ {
+ "name": so1.name,
+ "customer": so1.customer,
+ "submitted": datetime.date(2021, 6, 15),
+ "status": "Overdue",
+ "payment_term": None,
+ "description": "_Test 50-50",
+ "due_date": datetime.date(2021, 6, 30),
+ "invoice_portion": 50.0,
+ "currency": "INR",
+ "base_payment_amount": 500000.0,
+ "paid_amount": 0.0,
+ "invoices": "",
+ },
+ {
+ "name": so1.name,
+ "customer": so1.customer,
+ "submitted": datetime.date(2021, 6, 15),
+ "status": "Overdue",
+ "payment_term": None,
+ "description": "_Test 50-50",
+ "due_date": datetime.date(2021, 7, 15),
+ "invoice_portion": 50.0,
+ "currency": "INR",
+ "base_payment_amount": 500000.0,
+ "paid_amount": 0.0,
+ "invoices": "",
+ },
+ ]
+
+ expected_value_so2 = [
+ {
+ "name": so2.name,
+ "customer": so2.customer,
+ "submitted": datetime.date(2021, 6, 15),
+ "status": "Overdue",
+ "payment_term": None,
+ "description": "_Test 50-50",
+ "due_date": datetime.date(2021, 6, 30),
+ "invoice_portion": 50.0,
+ "currency": "INR",
+ "base_payment_amount": 50000.0,
+ "paid_amount": 0.0,
+ "invoices": "",
+ },
+ {
+ "name": so2.name,
+ "customer": so2.customer,
+ "submitted": datetime.date(2021, 6, 15),
+ "status": "Overdue",
+ "payment_term": None,
+ "description": "_Test 50-50",
+ "due_date": datetime.date(2021, 7, 15),
+ "invoice_portion": 50.0,
+ "currency": "INR",
+ "base_payment_amount": 50000.0,
+ "paid_amount": 0.0,
+ "invoices": "",
+ },
+ ]
+
+ group_filters = [
+ {"customer_group": "All Customer Groups"},
+ {"item_group": "All Item Groups"},
+ {"item_group": "Products"},
+ {"item_group": "Raw Material"},
+ ]
+
+ expected_values_for_group_filters = [
+ expected_value_so1 + expected_value_so2,
+ expected_value_so1 + expected_value_so2,
+ expected_value_so1,
+ expected_value_so2,
+ ]
+
+ for idx, g in enumerate(group_filters, 0):
+ # build filter
+ filters = frappe._dict({}).update(base_filters).update(g)
+ with self.subTest(filters=filters):
+ columns, data, message, chart = execute(filters)
+ self.assertEqual(data, expected_values_for_group_filters[idx])
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 535f5652096..b2f5fb7d202 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -3,7 +3,7 @@
import copy
import json
-from typing import List
+from typing import Dict, List, Optional
import frappe
from frappe import _
@@ -18,6 +18,7 @@ from frappe.utils import (
now_datetime,
nowtime,
strip,
+ strip_html,
)
from frappe.utils.html_utils import clean_html
@@ -69,10 +70,6 @@ class Item(Document):
self.item_code = strip(self.item_code)
self.name = self.item_code
- def before_insert(self):
- if not self.description:
- self.description = self.item_name
-
def after_insert(self):
"""set opening stock and item price"""
if self.standard_rate:
@@ -86,7 +83,7 @@ class Item(Document):
if not self.item_name:
self.item_name = self.item_code
- if not self.description:
+ if not strip_html(cstr(self.description)).strip():
self.description = self.item_name
self.validate_uom()
@@ -890,25 +887,38 @@ class Item(Document):
if self.is_new():
return
- fields = ("has_serial_no", "is_stock_item", "valuation_method", "has_batch_no")
+ restricted_fields = ("has_serial_no", "is_stock_item", "valuation_method", "has_batch_no")
+
+ values = frappe.db.get_value("Item", self.name, restricted_fields, as_dict=True)
+ if not values:
+ return
- values = frappe.db.get_value("Item", self.name, fields, as_dict=True)
if not values.get("valuation_method") and self.get("valuation_method"):
values["valuation_method"] = (
frappe.db.get_single_value("Stock Settings", "valuation_method") or "FIFO"
)
- if values:
- for field in fields:
- if cstr(self.get(field)) != cstr(values.get(field)):
- if self.check_if_linked_document_exists(field):
- frappe.throw(
- _(
- "As there are existing transactions against item {0}, you can not change the value of {1}"
- ).format(self.name, frappe.bold(self.meta.get_label(field)))
- )
+ changed_fields = [
+ field for field in restricted_fields if cstr(self.get(field)) != cstr(values.get(field))
+ ]
+ if not changed_fields:
+ return
- def check_if_linked_document_exists(self, field):
+ if linked_doc := self._get_linked_submitted_documents(changed_fields):
+ changed_field_labels = [frappe.bold(self.meta.get_label(f)) for f in changed_fields]
+ msg = _(
+ "As there are existing submitted transactions against item {0}, you can not change the value of {1}."
+ ).format(self.name, ", ".join(changed_field_labels))
+
+ if linked_doc and isinstance(linked_doc, dict):
+ msg += "
"
+ msg += _("Example of a linked document: {0}").format(
+ frappe.get_desk_link(linked_doc.doctype, linked_doc.docname)
+ )
+
+ frappe.throw(msg, title=_("Linked with submitted documents"))
+
+ def _get_linked_submitted_documents(self, changed_fields: List[str]) -> Optional[Dict[str, str]]:
linked_doctypes = [
"Delivery Note Item",
"Sales Invoice Item",
@@ -921,7 +931,7 @@ class Item(Document):
# For "Is Stock Item", following doctypes is important
# because reserved_qty, ordered_qty and requested_qty updated from these doctypes
- if field == "is_stock_item":
+ if "is_stock_item" in changed_fields:
linked_doctypes += [
"Sales Order Item",
"Purchase Order Item",
@@ -940,11 +950,21 @@ class Item(Document):
"Sales Invoice Item",
):
# If Invoice has Stock impact, only then consider it.
- if self.stock_ledger_created():
- return True
+ if linked_doc := frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"item_code": self.name, "is_cancelled": 0},
+ ["voucher_no as docname", "voucher_type as doctype"],
+ as_dict=True,
+ ):
+ return linked_doc
- elif frappe.db.get_value(doctype, filters):
- return True
+ elif linked_doc := frappe.db.get_value(
+ doctype,
+ filters,
+ ["parent as docname", "parenttype as doctype"],
+ as_dict=True,
+ ):
+ return linked_doc
def validate_auto_reorder_enabled_in_stock_settings(self):
if self.reorder_levels:
diff --git a/erpnext/stock/doctype/item/item_dashboard.py b/erpnext/stock/doctype/item/item_dashboard.py
index 3caed02d69b..897acb74487 100644
--- a/erpnext/stock/doctype/item/item_dashboard.py
+++ b/erpnext/stock/doctype/item/item_dashboard.py
@@ -31,7 +31,7 @@ def get_data():
},
{"label": _("Manufacture"), "items": ["Production Plan", "Work Order", "Item Manufacturer"]},
{"label": _("Traceability"), "items": ["Serial No", "Batch"]},
- {"label": _("Move"), "items": ["Stock Entry"]},
+ {"label": _("Stock Movement"), "items": ["Stock Entry", "Stock Reconciliation"]},
{"label": _("E-commerce"), "items": ["Website Item"]},
],
}
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 328d937f318..aa0a5490b61 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -744,6 +744,40 @@ class TestItem(FrappeTestCase):
self.assertTrue(get_data(warehouse="_Test Warehouse - _TC"))
self.assertTrue(get_data(item_group="All Item Groups"))
+ def test_empty_description(self):
+ item = make_item(properties={"description": "