Merge branch 'develop' into multiple-shifts
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 7315ae8..403e2bd 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -224,10 +224,7 @@
(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 c45b069..2438f4b 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
@@ -35,10 +35,11 @@
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 c359959..a73c72c 100644
--- a/erpnext/accounts/report/purchase_register/purchase_register.py
+++ b/erpnext/accounts/report/purchase_register/purchase_register.py
@@ -124,11 +124,10 @@
_("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 @@
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 03ff269..96c730c 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -126,7 +126,8 @@
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 77e6ae2..3a46fb0 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 @@
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 b0c174b..d23a22a 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 @@
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 9742387..aef4412 100755
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.js
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.js
@@ -34,6 +34,15 @@
});
}
}
+
+ // 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 9ecbe01..9d1db9b 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 fd69a9b..40ab805 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -353,6 +353,17 @@
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 304d1a7..2535180 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 @@
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 d85b8a6..b2824e1 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -28,12 +28,12 @@
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 b567823..398dd70 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 @@
"""
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 ba919a7..9b07ba8 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 @@
"""
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 f484545..64c5ee5 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 @@
&& 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 2e5cbb8..8889a5f 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -100,7 +100,8 @@
@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 4027d2e..36ca2b2 100644
--- a/erpnext/selling/doctype/customer/test_customer.py
+++ b/erpnext/selling/doctype/customer/test_customer.py
@@ -45,7 +45,8 @@
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 c626f5b..6b33a71 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 @@
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"] = []
+ 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})
- 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
-
+ result = {"datasets": pivot_table, "labels": list(summary.keys())}
return result
else:
@@ -148,20 +142,14 @@
for x in opportunities
]
- df = (
- pd.DataFrame(cp_opportunities)
- .groupby(["sales_stage"], as_index=True)
- .agg({"compound_amount": "sum"})
- .to_dict()
- )
+ 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 = {}
- result["labels"] = df["compound_amount"].keys()
- result["datasets"] = []
- result["datasets"].append(
- {"name": _("Total Amount"), "values": df["compound_amount"].values(), "chartType": "bar"}
- )
-
+ 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 0e36b3f..c068ae3 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 @@
"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]
+ ]
}
- },
- on_change: function(){
- frappe.query_report.refresh();
+ }
+ },
+ {
+ "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]
+ ]
+ }
}
}
]
-
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 7f797f6..cb22fb6 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():
@@ -15,6 +15,12 @@
"options": "Sales Order",
},
{
+ "label": _("Customer"),
+ "fieldname": "customer",
+ "fieldtype": "Link",
+ "options": "Customer",
+ },
+ {
"label": _("Posting Date"),
"fieldname": "submitted",
"fieldtype": "Date",
@@ -67,6 +73,55 @@
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 @@
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 @@
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 @@
& (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 89940a6..9d542f5 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.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 @@
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 @@
"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 @@
},
{
"name": so.name,
+ "customer": so.customer,
"submitted": datetime.date(2021, 6, 15),
"status": "Partly Paid",
"payment_term": None,
@@ -132,11 +137,11 @@
)
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 @@
"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 @@
expected_value = [
{
"name": so.name,
+ "customer": so.customer,
"submitted": datetime.date(2021, 6, 15),
"status": "Completed",
"payment_term": None,
@@ -187,6 +193,7 @@
},
{
"name": so.name,
+ "customer": so.customer,
"submitted": datetime.date(2021, 6, 15),
"status": "Partly Paid",
"payment_term": None,
@@ -200,3 +207,134 @@
},
]
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 535f565..b2f5fb7 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 @@
now_datetime,
nowtime,
strip,
+ strip_html,
)
from frappe.utils.html_utils import clean_html
@@ -69,10 +70,6 @@
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 @@
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 @@
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, fields, as_dict=True)
+ values = frappe.db.get_value("Item", self.name, restricted_fields, as_dict=True)
+ if not values:
+ return
+
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 += "<br>"
+ 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 @@
# 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 @@
"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 3caed02..897acb7 100644
--- a/erpnext/stock/doctype/item/item_dashboard.py
+++ b/erpnext/stock/doctype/item/item_dashboard.py
@@ -31,7 +31,7 @@
},
{"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 328d937..aa0a549 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -744,6 +744,40 @@
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": "<p></p>"})
+ self.assertEqual(item.description, item.item_name)
+ item.description = ""
+ item.save()
+ self.assertEqual(item.description, item.item_name)
+
+ def test_item_type_field_change(self):
+ """Check if critical fields like `is_stock_item`, `has_batch_no` are not changed if transactions exist."""
+ from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
+ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+
+ transaction_creators = [
+ lambda i: make_purchase_receipt(item_code=i),
+ lambda i: make_purchase_invoice(item_code=i, update_stock=1),
+ lambda i: make_stock_entry(item_code=i, qty=1, target="_Test Warehouse - _TC"),
+ lambda i: create_delivery_note(item_code=i),
+ ]
+
+ properties = {"has_batch_no": 0, "allow_negative_stock": 1, "valuation_rate": 10}
+ for transaction_creator in transaction_creators:
+ item = make_item(properties=properties)
+ transaction = transaction_creator(item.name)
+ item.has_batch_no = 1
+ self.assertRaises(frappe.ValidationError, item.save)
+
+ transaction.cancel()
+ # should be allowed now
+ item.reload()
+ item.has_batch_no = 1
+ item.save()
+
def set_item_variant_settings(fields):
doc = frappe.get_doc("Item Variant Settings")
diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py
index 1e9d01a..5a7228a 100644
--- a/erpnext/stock/doctype/warehouse/test_warehouse.py
+++ b/erpnext/stock/doctype/warehouse/test_warehouse.py
@@ -38,6 +38,16 @@
self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse)
self.assertEqual(child_warehouse.is_group, 0)
+ def test_naming(self):
+ company = "Wind Power LLC"
+ warehouse_name = "Named Warehouse - WP"
+ wh = frappe.get_doc(doctype="Warehouse", warehouse_name=warehouse_name, company=company).insert()
+ self.assertEqual(wh.name, warehouse_name)
+
+ warehouse_name = "Unnamed Warehouse"
+ wh = frappe.get_doc(doctype="Warehouse", warehouse_name=warehouse_name, company=company).insert()
+ self.assertIn(warehouse_name, wh.name)
+
def test_unlinking_warehouse_from_item_defaults(self):
company = "_Test Company"
diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py
index c892ba3..3b18a9a 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.py
+++ b/erpnext/stock/doctype/warehouse/warehouse.py
@@ -21,8 +21,9 @@
suffix = " - " + frappe.get_cached_value("Company", self.company, "abbr")
if not self.warehouse_name.endswith(suffix):
self.name = self.warehouse_name + suffix
- else:
- self.name = self.warehouse_name
+ return
+
+ self.name = self.warehouse_name
def onload(self):
"""load account name for General Ledger Report"""
diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv
index 3cdae45..db454de 100644
--- a/erpnext/translations/fr.csv
+++ b/erpnext/translations/fr.csv
@@ -285,7 +285,7 @@
"Asset {0} cannot be scrapped, as it is already {1}","L'actif {0} ne peut pas être mis au rebut, car il est déjà {1}",
Asset {0} does not belong to company {1},L'actif {0} ne fait pas partie à la société {1},
Asset {0} must be submitted,L'actif {0} doit être soumis,
-Assets,Les atouts,
+Assets,Actifs - Immo.,
Assign,Assigner,
Assign Salary Structure,Affecter la structure salariale,
Assign To,Attribuer À,
@@ -1211,7 +1211,7 @@
Help Results for,Aide Résultats pour,
High,Haut,
High Sensitivity,Haute sensibilité,
-Hold,Tenir,
+Hold,Mettre en attente,
Hold Invoice,Facture en attente,
Holiday,Vacances,
Holiday List,Liste de vacances,
@@ -4240,7 +4240,7 @@
From date cannot be greater than To date,La Date Initiale ne peut pas être postérieure à la Date Finale,
Group by,Grouper Par,
In stock,En stock,
-Item name,Nom de l'article,
+Item name,Libellé de l'article,
Loan amount is mandatory,Le montant du prêt est obligatoire,
Minimum Qty,Quantité minimum,
More details,Plus de détails,
@@ -5473,7 +5473,7 @@
PUR-ORD-.YYYY.-,PUR-ORD-.YYYY.-,
Get Items from Open Material Requests,Obtenir des Articles de Demandes Matérielles Ouvertes,
Fetch items based on Default Supplier.,Récupérez les articles en fonction du fournisseur par défaut.,
-Required By,Requis Par,
+Required By,Requis pour le,
Order Confirmation No,No de confirmation de commande,
Order Confirmation Date,Date de confirmation de la commande,
Customer Mobile No,N° de Portable du Client,
@@ -7223,8 +7223,8 @@
Scrap %,% de Rebut,
Original Item,Article original,
BOM Operation,Opération LDM,
-Operation Time ,Moment de l'opération,
-In minutes,En quelques minutes,
+Operation Time ,Durée de l'opération,
+In minutes,En minutes,
Batch Size,Taille du lot,
Base Hour Rate(Company Currency),Taux Horaire de Base (Devise de la Société),
Operating Cost(Company Currency),Coût d'Exploitation (Devise Société),
@@ -9267,7 +9267,7 @@
Amount Delivered,Montant livré,
Delay (in Days),Retard (en jours),
Group by Sales Order,Regrouper par commande client,
- Sales Value,La valeur des ventes,
+Sales Value,La valeur des ventes,
Stock Qty vs Serial No Count,Quantité de stock vs numéro de série,
Serial No Count,Numéro de série,
Work Order Summary,Résumé de l'ordre de travail,
@@ -9647,7 +9647,7 @@
Validate Selling Price for Item Against Purchase Rate or Valuation Rate,Valider le prix de vente de l'article par rapport au taux d'achat ou au taux de valorisation,
Hide Customer's Tax ID from Sales Transactions,Masquer le numéro d'identification fiscale du client dans les transactions de vente,
"The percentage you are allowed to receive or deliver more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed to receive 110 units.","Le pourcentage que vous êtes autorisé à recevoir ou à livrer plus par rapport à la quantité commandée. Par exemple, si vous avez commandé 100 unités et que votre allocation est de 10%, vous êtes autorisé à recevoir 110 unités.",
-Action If Quality Inspection Is Not Submitted,Action si l'inspection de la qualité n'est pas soumise,
+Action If Quality Inspection Is Not Submitted,Action si l'inspection qualité n'est pas soumise,
Auto Insert Price List Rate If Missing,Taux de liste de prix d'insertion automatique s'il est manquant,
Automatically Set Serial Nos Based on FIFO,Définir automatiquement les numéros de série en fonction de FIFO,
Set Qty in Transactions Based on Serial No Input,Définir la quantité dans les transactions en fonction du numéro de série,
@@ -9838,3 +9838,35 @@
Creating Purchase Order ...,Création d'une commande d'achat ...,
"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.","Sélectionnez un fournisseur parmi les fournisseurs par défaut des articles ci-dessous. Lors de la sélection, un bon de commande sera effectué contre des articles appartenant uniquement au fournisseur sélectionné.",
Row #{}: You must select {} serial numbers for item {}.,Ligne n ° {}: vous devez sélectionner {} numéros de série pour l'article {}.,
+Update Rate as per Last Purchase,Mettre à jour avec les derniers prix d'achats
+Company Shipping Address,Adresse d'expédition
+Shipping Address Details,Détail d'adresse d'expédition
+Company Billing Address,Adresse de la société de facturation
+Supplier Address Details,
+Bank Reconciliation Tool,Outil de réconcialiation d'écritures bancaires
+Supplier Contact,Contact fournisseur
+Subcontracting,Sous traitance
+Order Status,Statut de la commande
+Build,Personnalisations avancées
+Dispatch Address Name,Adresse de livraison intermédiaire
+Amount Eligible for Commission,Montant éligible à comission
+Grant Commission,Eligible aux commissions
+Stock Transactions Settings, Paramétre des transactions
+Role Allowed to Over Deliver/Receive, Rôle autorisé à dépasser cette limite
+Users with this role are allowed to over deliver/receive against orders above the allowance percentage,Rôle Utilisateur qui sont autorisé à livrée/commandé au-delà de la limite
+Over Transfer Allowance,Autorisation de limite de transfert
+Quality Inspection Settings,Paramétre de l'inspection qualité
+Action If Quality Inspection Is Rejected,Action si l'inspection qualité est rejetée
+Disable Serial No And Batch Selector,Désactiver le sélecteur de numéro de lot/série
+Is Rate Adjustment Entry (Debit Note),Est un justement du prix de la note de débit
+Issue a debit note with 0 qty against an existing Sales Invoice,Creer une note de débit avec une quatité à O pour la facture
+Control Historical Stock Transactions,Controle de l'historique des stransaction de stock
+No stock transactions can be created or modified before this date.,Aucune transaction ne peux être créée ou modifié avant cette date.
+Stock transactions that are older than the mentioned days cannot be modified.,Les transactions de stock plus ancienne que le nombre de jours ci-dessus ne peuvent être modifiées
+Role Allowed to Create/Edit Back-dated Transactions,Rôle autorisé à créer et modifier des transactions anti-datée
+"If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.","LEs utilisateur de ce role pourront creer et modifier des transactions dans le passé. Si vide tout les utilisateurs pourrons le faire"
+Auto Insert Item Price If Missing,Création du prix de l'article dans les listes de prix si abscent
+Update Existing Price List Rate,Mise a jour automatique du prix dans les listes de prix
+Show Barcode Field in Stock Transactions,Afficher le champ Code Barre dans les transactions de stock
+Convert Item Description to Clean HTML in Transactions,Convertir les descriptions d'articles en HTML valide lors des transactions
+Have Default Naming Series for Batch ID?,Nom de série par défaut pour les Lots ou Séries