From c7cfc726d7ba28cc2d78668de7e019222a1882c9 Mon Sep 17 00:00:00 2001
From: Rohan Bansal
Date: Wed, 27 Nov 2019 16:58:06 +0530
Subject: [PATCH 01/30] feat: navigate to stock ledger from batch report
---
.../batch_wise_balance_history.js | 25 +++-
.../batch_wise_balance_history.py | 37 +++---
.../stock/report/stock_ledger/stock_ledger.js | 10 +-
.../stock/report/stock_ledger/stock_ledger.py | 110 +++++++++++++-----
4 files changed, 133 insertions(+), 49 deletions(-)
diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js
index b23c908e07a..23700c94ee5 100644
--- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js
+++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js
@@ -16,6 +16,29 @@ frappe.query_reports["Batch-Wise Balance History"] = {
"fieldtype": "Date",
"width": "80",
"default": frappe.datetime.get_today()
+ },
+ {
+ "fieldname": "item",
+ "label": __("Item"),
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": "80"
}
- ]
+ ],
+ "formatter": function (value, row, column, data, default_formatter) {
+ if (column.fieldname == "Batch" && data && !!data["Batch"]) {
+ value = data["Batch"];
+ column.link_onclick = "frappe.query_reports['Batch-Wise Balance History'].set_batch_route_to_stock_ledger(" + JSON.stringify(data) + ")";
+ }
+
+ value = default_formatter(value, row, column, data);
+ return value;
+ },
+ "set_batch_route_to_stock_ledger": function (data) {
+ frappe.route_options = {
+ "batch_no": data["Batch"]
+ };
+
+ frappe.set_route("query-report", "Stock Ledger");
+ }
}
\ No newline at end of file
diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
index 7f7835f74ee..2c95084b813 100644
--- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
+++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
@@ -2,9 +2,11 @@
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
+
import frappe
from frappe import _
-from frappe.utils import flt, cint, getdate
+from frappe.utils import cint, flt, getdate
+
def execute(filters=None):
if not filters: filters = {}
@@ -17,29 +19,31 @@ def execute(filters=None):
data = []
for item in sorted(iwb_map):
- for wh in sorted(iwb_map[item]):
- for batch in sorted(iwb_map[item][wh]):
- qty_dict = iwb_map[item][wh][batch]
- if qty_dict.opening_qty or qty_dict.in_qty or qty_dict.out_qty or qty_dict.bal_qty:
- data.append([item, item_map[item]["item_name"], item_map[item]["description"], wh, batch,
- flt(qty_dict.opening_qty, float_precision), flt(qty_dict.in_qty, float_precision),
- flt(qty_dict.out_qty, float_precision), flt(qty_dict.bal_qty, float_precision),
- item_map[item]["stock_uom"]
- ])
+ if not filters.get("item") or filters.get("item") == item:
+ for wh in sorted(iwb_map[item]):
+ for batch in sorted(iwb_map[item][wh]):
+ qty_dict = iwb_map[item][wh][batch]
+ if qty_dict.opening_qty or qty_dict.in_qty or qty_dict.out_qty or qty_dict.bal_qty:
+ data.append([item, item_map[item]["item_name"], item_map[item]["description"], wh, batch,
+ flt(qty_dict.opening_qty, float_precision), flt(qty_dict.in_qty, float_precision),
+ flt(qty_dict.out_qty, float_precision), flt(qty_dict.bal_qty, float_precision),
+ item_map[item]["stock_uom"]
+ ])
return columns, data
+
def get_columns(filters):
"""return columns based on filters"""
columns = [_("Item") + ":Link/Item:100"] + [_("Item Name") + "::150"] + [_("Description") + "::150"] + \
- [_("Warehouse") + ":Link/Warehouse:100"] + [_("Batch") + ":Link/Batch:100"] + [_("Opening Qty") + ":Float:90"] + \
- [_("In Qty") + ":Float:80"] + [_("Out Qty") + ":Float:80"] + [_("Balance Qty") + ":Float:90"] + \
- [_("UOM") + "::90"]
-
+ [_("Warehouse") + ":Link/Warehouse:100"] + [_("Batch") + ":Link/Batch:100"] + [_("Opening Qty") + ":Float:90"] + \
+ [_("In Qty") + ":Float:80"] + [_("Out Qty") + ":Float:80"] + [_("Balance Qty") + ":Float:90"] + \
+ [_("UOM") + "::90"]
return columns
+
def get_conditions(filters):
conditions = ""
if not filters.get("from_date"):
@@ -52,7 +56,8 @@ def get_conditions(filters):
return conditions
-#get all details
+
+# get all details
def get_stock_ledger_entries(filters):
conditions = get_conditions(filters)
return frappe.db.sql("""
@@ -63,6 +68,7 @@ def get_stock_ledger_entries(filters):
order by item_code, warehouse""" %
conditions, as_dict=1)
+
def get_item_warehouse_batch_map(filters, float_precision):
sle = get_stock_ledger_entries(filters)
iwb_map = {}
@@ -90,6 +96,7 @@ def get_item_warehouse_batch_map(filters, float_precision):
return iwb_map
+
def get_item_details(filters):
item_map = {}
for d in frappe.db.sql("select name, item_name, description, stock_uom from tabItem", as_dict=1):
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js
index 3fab3273b9e..df3bba5e406 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.js
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.js
@@ -77,7 +77,15 @@ frappe.query_reports["Stock Ledger"] = {
"fieldtype": "Link",
"options": "UOM"
}
- ]
+ ],
+ "formatter": function (value, row, column, data, default_formatter) {
+ value = default_formatter(value, row, column, data);
+ if (column.fieldname == "out_qty" && data.out_qty < 0) {
+ value = "" + value + "";
+ }
+
+ return value;
+ },
}
// $(function() {
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index db7f6ad1b9c..dd53a006b52 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -2,9 +2,11 @@
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
+
import frappe
-from frappe import _
from erpnext.stock.utils import update_included_uom_in_report
+from frappe import _
+
def execute(filters=None):
include_uom = filters.get("include_uom")
@@ -36,7 +38,22 @@ def execute(filters=None):
sle.update({
"qty_after_transaction": actual_qty,
- "stock_value": stock_value
+ "stock_value": stock_value,
+ "in_qty": max(sle.actual_qty, 0),
+ "out_qty": min(sle.actual_qty, 0)
+ })
+
+ # get the name of the item that was produced using this item
+ if sle.voucher_type == "Stock Entry":
+ purpose, work_order, fg_completed_qty = frappe.db.get_value(sle.voucher_type, sle.voucher_no, ["purpose", "work_order", "fg_completed_qty"])
+
+ if purpose == "Manufacture" and work_order:
+ finished_product = frappe.db.get_value("Work Order", work_order, "item_name")
+ finished_qty = fg_completed_qty
+
+ sle.update({
+ "finished_product": finished_product,
+ "finished_qty": finished_qty,
})
data.append(sle)
@@ -47,53 +64,74 @@ def execute(filters=None):
update_included_uom_in_report(columns, data, include_uom, conversion_factors)
return columns, data
+
def get_columns():
columns = [
- {"label": _("Date"), "fieldname": "date", "fieldtype": "Datetime", "width": 95},
- {"label": _("Item"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 130},
+ {"label": _("Date"), "fieldname": "date", "fieldtype": "Datetime", "width": 150},
+ {"label": _("Item"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 100},
{"label": _("Item Name"), "fieldname": "item_name", "width": 100},
+ {"label": _("Stock UOM"), "fieldname": "stock_uom", "fieldtype": "Link", "options": "UOM", "width": 90},
+ {"label": _("In Qty"), "fieldname": "in_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"},
+ {"label": _("Out Qty"), "fieldname": "out_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"},
+ {"label": _("Balance Qty"), "fieldname": "qty_after_transaction", "fieldtype": "Float", "width": 100, "convertible": "qty"},
+ {"label": _("Finished Product"), "fieldname": "finished_product", "width": 100},
+ {"label": _("Finished Qty"), "fieldname": "finished_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"},
+ {"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 150},
+ {"label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", "width": 150},
{"label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100},
{"label": _("Brand"), "fieldname": "brand", "fieldtype": "Link", "options": "Brand", "width": 100},
{"label": _("Description"), "fieldname": "description", "width": 200},
- {"label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", "width": 100},
- {"label": _("Stock UOM"), "fieldname": "stock_uom", "fieldtype": "Link", "options": "UOM", "width": 100},
- {"label": _("Qty"), "fieldname": "actual_qty", "fieldtype": "Float", "width": 50, "convertible": "qty"},
- {"label": _("Balance Qty"), "fieldname": "qty_after_transaction", "fieldtype": "Float", "width": 100, "convertible": "qty"},
- {"label": _("Incoming Rate"), "fieldname": "incoming_rate", "fieldtype": "Currency", "width": 110,
- "options": "Company:company:default_currency", "convertible": "rate"},
- {"label": _("Valuation Rate"), "fieldname": "valuation_rate", "fieldtype": "Currency", "width": 110,
- "options": "Company:company:default_currency", "convertible": "rate"},
- {"label": _("Balance Value"), "fieldname": "stock_value", "fieldtype": "Currency", "width": 110,
- "options": "Company:company:default_currency"},
+ {"label": _("Incoming Rate"), "fieldname": "incoming_rate", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency", "convertible": "rate"},
+ {"label": _("Valuation Rate"), "fieldname": "valuation_rate", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency", "convertible": "rate"},
+ {"label": _("Balance Value"), "fieldname": "stock_value", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency"},
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110},
{"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 100},
{"label": _("Batch"), "fieldname": "batch_no", "fieldtype": "Link", "options": "Batch", "width": 100},
- {"label": _("Serial #"), "fieldname": "serial_no", "width": 100},
+ {"label": _("Serial #"), "fieldname": "serial_no", "fieldtype": "Link", "options": "Serial No", "width": 100},
{"label": _("Project"), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100},
{"label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", "width": 110}
]
return columns
+
def get_stock_ledger_entries(filters, items):
item_conditions_sql = ''
if items:
item_conditions_sql = 'and sle.item_code in ({})'\
.format(', '.join([frappe.db.escape(i) for i in items]))
- return frappe.db.sql("""select concat_ws(" ", posting_date, posting_time) as date,
- item_code, warehouse, actual_qty, qty_after_transaction, incoming_rate, valuation_rate,
- stock_value, voucher_type, voucher_no, batch_no, serial_no, company, project, stock_value_difference
- from `tabStock Ledger Entry` sle
- where company = %(company)s and
- posting_date between %(from_date)s and %(to_date)s
- {sle_conditions}
- {item_conditions_sql}
- order by posting_date asc, posting_time asc, creation asc"""\
- .format(
- sle_conditions=get_sle_conditions(filters),
- item_conditions_sql = item_conditions_sql
- ), filters, as_dict=1)
+ sl_entries = frappe.db.sql("""
+ SELECT
+ concat_ws(" ", posting_date, posting_time) AS date,
+ item_code,
+ warehouse,
+ actual_qty,
+ qty_after_transaction,
+ incoming_rate,
+ valuation_rate,
+ stock_value,
+ voucher_type,
+ voucher_no,
+ batch_no,
+ serial_no,
+ company,
+ project,
+ stock_value_difference
+ FROM
+ `tabStock Ledger Entry` sle
+ WHERE
+ company = %(company)s
+ AND posting_date BETWEEN %(from_date)s AND %(to_date)s
+ {sle_conditions}
+ {item_conditions_sql}
+ ORDER BY
+ posting_date asc, posting_time asc, creation asc
+ """.format(sle_conditions=get_sle_conditions(filters), item_conditions_sql=item_conditions_sql),
+ filters, as_dict=1)
+
+ return sl_entries
+
def get_items(filters):
conditions = []
@@ -111,6 +149,7 @@ def get_items(filters):
.format(" and ".join(conditions)), filters)
return items
+
def get_item_details(items, sl_entries, include_uom):
item_details = {}
if not items:
@@ -140,6 +179,7 @@ def get_item_details(items, sl_entries, include_uom):
return item_details
+
def get_sle_conditions(filters):
conditions = []
if filters.get("warehouse"):
@@ -155,6 +195,7 @@ def get_sle_conditions(filters):
return "and {}".format(" and ".join(conditions)) if conditions else ""
+
def get_opening_balance(filters, columns):
if not (filters.item_code and filters.warehouse and filters.from_date):
return
@@ -166,13 +207,17 @@ def get_opening_balance(filters, columns):
"posting_date": filters.from_date,
"posting_time": "00:00:00"
})
- row = {}
- row["item_code"] = _("'Opening'")
- for dummy, v in ((9, 'qty_after_transaction'), (11, 'valuation_rate'), (12, 'stock_value')):
- row[v] = last_entry.get(v, 0)
+
+ row = {
+ "item_code": _("'Opening'"),
+ "qty_after_transaction": last_entry.get("qty_after_transaction", 0),
+ "valuation_rate": last_entry.get("valuation_rate", 0),
+ "stock_value": last_entry.get("stock_value", 0)
+ }
return row
+
def get_warehouse_condition(warehouse):
warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1)
if warehouse_details:
@@ -182,6 +227,7 @@ def get_warehouse_condition(warehouse):
return ''
+
def get_item_group_condition(item_group):
item_group_details = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"], as_dict=1)
if item_group_details:
From 312210c2b0bf2dff44c006b26ca1866ae2fe2b5b Mon Sep 17 00:00:00 2001
From: Rohan Bansal
Date: Tue, 13 Aug 2019 17:15:44 +0530
Subject: [PATCH 02/30] feat: create address and contact after lead creation
---
erpnext/crm/doctype/lead/lead.js | 55 +++++++-------
erpnext/crm/doctype/lead/lead.json | 95 +++++++++++++++++-------
erpnext/crm/doctype/lead/lead.py | 115 +++++++++++++++++++++++------
3 files changed, 187 insertions(+), 78 deletions(-)
diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js
index 122e2b4eee8..0c88d2826f7 100644
--- a/erpnext/crm/doctype/lead/lead.js
+++ b/erpnext/crm/doctype/lead/lead.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext");
@@ -7,57 +7,54 @@ cur_frm.email_field = "email_id";
erpnext.LeadController = frappe.ui.form.Controller.extend({
setup: function () {
this.frm.make_methods = {
+ 'Customer': this.make_customer,
'Quotation': this.make_quotation,
- 'Opportunity': this.create_opportunity
- }
-
- this.frm.fields_dict.customer.get_query = function (doc, cdt, cdn) {
- return { query: "erpnext.controllers.queries.customer_query" }
- }
+ 'Opportunity': this.make_opportunity
+ };
this.frm.toggle_reqd("lead_name", !this.frm.doc.organization_lead);
},
onload: function () {
- if (cur_frm.fields_dict.lead_owner.df.options.match(/^User/)) {
- cur_frm.fields_dict.lead_owner.get_query = function (doc, cdt, cdn) {
- return { query: "frappe.core.doctype.user.user.user_query" }
- }
- }
+ this.frm.set_query("customer", function (doc, cdt, cdn) {
+ return { query: "erpnext.controllers.queries.customer_query" }
+ });
- if (cur_frm.fields_dict.contact_by.df.options.match(/^User/)) {
- cur_frm.fields_dict.contact_by.get_query = function (doc, cdt, cdn) {
- return { query: "frappe.core.doctype.user.user.user_query" }
- }
- }
+ this.frm.set_query("lead_owner", function (doc, cdt, cdn) {
+ return { query: "frappe.core.doctype.user.user.user_query" }
+ });
+
+ this.frm.set_query("contact_by", function (doc, cdt, cdn) {
+ return { query: "frappe.core.doctype.user.user.user_query" }
+ });
},
refresh: function () {
- var doc = this.frm.doc;
+ let doc = this.frm.doc;
erpnext.toggle_naming_series();
frappe.dynamic_link = { doc: doc, fieldname: 'name', doctype: 'Lead' }
- if(!doc.__islocal && doc.__onload && !doc.__onload.is_customer) {
- this.frm.add_custom_button(__("Customer"), this.create_customer, __('Create'));
- this.frm.add_custom_button(__("Opportunity"), this.create_opportunity, __('Create'));
- this.frm.add_custom_button(__("Quotation"), this.make_quotation, __('Create'));
+ if (!this.frm.is_new() && doc.__onload && !doc.__onload.is_customer) {
+ this.frm.add_custom_button(__("Customer"), this.make_customer, __("Create"));
+ this.frm.add_custom_button(__("Opportunity"), this.make_opportunity, __("Create"));
+ this.frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
}
- if (!this.frm.doc.__islocal) {
- frappe.contacts.render_address_and_contact(cur_frm);
+ if (!this.frm.is_new()) {
+ frappe.contacts.render_address_and_contact(this.frm);
} else {
- frappe.contacts.clear_address_and_contact(cur_frm);
+ frappe.contacts.clear_address_and_contact(this.frm);
}
},
- create_customer: function () {
+ make_customer: function () {
frappe.model.open_mapped_doc({
method: "erpnext.crm.doctype.lead.lead.make_customer",
frm: cur_frm
})
},
- create_opportunity: function () {
+ make_opportunity: function () {
frappe.model.open_mapped_doc({
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
frm: cur_frm
@@ -77,7 +74,7 @@ erpnext.LeadController = frappe.ui.form.Controller.extend({
},
company_name: function () {
- if (this.frm.doc.organization_lead == 1) {
+ if (this.frm.doc.organization_lead && !this.frm.doc.lead_name) {
this.frm.set_value("lead_name", this.frm.doc.company_name);
}
},
@@ -85,7 +82,7 @@ erpnext.LeadController = frappe.ui.form.Controller.extend({
contact_date: function () {
if (this.frm.doc.contact_date) {
let d = moment(this.frm.doc.contact_date);
- d.add(1, "hours");
+ d.add(1, "day");
this.frm.set_value("ends_on", d.format(frappe.defaultDatetimeFormat));
}
}
diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json
index eb68c679ba5..d2a98b609ac 100644
--- a/erpnext/crm/doctype/lead/lead.json
+++ b/erpnext/crm/doctype/lead/lead.json
@@ -16,6 +16,7 @@
"col_break123",
"lead_owner",
"status",
+ "salutation",
"gender",
"source",
"customer",
@@ -29,16 +30,20 @@
"notes_section",
"notes",
"contact_info",
- "address_desc",
"address_html",
+ "address_title",
+ "address_line1",
+ "address_line2",
+ "city",
+ "county",
+ "state",
+ "country",
+ "pincode",
"column_break2",
"contact_html",
"phone",
- "salutation",
"mobile_no",
"fax",
- "website",
- "territory",
"more_info",
"type",
"market_segment",
@@ -46,6 +51,8 @@
"request_type",
"column_break3",
"company",
+ "website",
+ "territory",
"unsubscribed",
"blog_subscriber"
],
@@ -73,7 +80,6 @@
"set_only_once": 1
},
{
- "depends_on": "eval:!doc.organization_lead",
"fieldname": "lead_name",
"fieldtype": "Data",
"in_global_search": 1,
@@ -130,7 +136,6 @@
"search_index": 1
},
{
- "depends_on": "eval:!doc.organization_lead",
"fieldname": "gender",
"fieldtype": "Link",
"label": "Gender",
@@ -218,19 +223,13 @@
},
{
"collapsible": 1,
+ "collapsible_depends_on": "eval: doc.__islocal",
"fieldname": "contact_info",
"fieldtype": "Section Break",
"label": "Address & Contact",
"oldfieldtype": "Column Break",
"options": "fa fa-map-marker"
},
- {
- "depends_on": "eval:doc.__islocal",
- "fieldname": "address_desc",
- "fieldtype": "HTML",
- "label": "Address Desc",
- "print_hide": 1
- },
{
"fieldname": "address_html",
"fieldtype": "HTML",
@@ -242,14 +241,13 @@
"fieldtype": "Column Break"
},
{
- "depends_on": "eval:doc.organization_lead",
"fieldname": "contact_html",
"fieldtype": "HTML",
"label": "Contact HTML",
"read_only": 1
},
{
- "depends_on": "eval:!doc.organization_lead",
+ "depends_on": "eval: doc.__islocal",
"fieldname": "phone",
"fieldtype": "Data",
"label": "Phone",
@@ -257,14 +255,14 @@
"oldfieldtype": "Data"
},
{
- "depends_on": "eval:!doc.organization_lead",
+ "depends_on": "eval: doc.__islocal",
"fieldname": "salutation",
"fieldtype": "Link",
"label": "Salutation",
"options": "Salutation"
},
{
- "depends_on": "eval:!doc.organization_lead",
+ "depends_on": "eval: doc.__islocal",
"fieldname": "mobile_no",
"fieldtype": "Data",
"label": "Mobile No.",
@@ -272,7 +270,7 @@
"oldfieldtype": "Data"
},
{
- "depends_on": "eval:!doc.organization_lead",
+ "depends_on": "eval: doc.__islocal",
"fieldname": "fax",
"fieldtype": "Data",
"label": "Fax",
@@ -361,12 +359,62 @@
"fieldname": "blog_subscriber",
"fieldtype": "Check",
"label": "Blog Subscriber"
+ },
+ {
+ "depends_on": "eval: doc.__islocal",
+ "fieldname": "address_title",
+ "fieldtype": "Data",
+ "label": "Address Title"
+ },
+ {
+ "depends_on": "eval: doc.__islocal",
+ "fieldname": "address_line1",
+ "fieldtype": "Data",
+ "label": "Address Line 1"
+ },
+ {
+ "depends_on": "eval: doc.__islocal",
+ "fieldname": "address_line2",
+ "fieldtype": "Data",
+ "label": "Address Line 2"
+ },
+ {
+ "depends_on": "eval: doc.__islocal",
+ "fieldname": "city",
+ "fieldtype": "Data",
+ "label": "City/Town"
+ },
+ {
+ "depends_on": "eval: doc.__islocal",
+ "fieldname": "county",
+ "fieldtype": "Data",
+ "label": "County"
+ },
+ {
+ "depends_on": "eval: doc.__islocal",
+ "fieldname": "state",
+ "fieldtype": "Data",
+ "label": "State"
+ },
+ {
+ "depends_on": "eval: doc.__islocal",
+ "fieldname": "country",
+ "fieldtype": "Link",
+ "label": "Country",
+ "options": "Country"
+ },
+ {
+ "depends_on": "eval: doc.__islocal",
+ "fieldname": "pincode",
+ "fieldtype": "Data",
+ "label": "Postal Code",
+ "options": "Country"
}
],
"icon": "fa fa-user",
"idx": 5,
"image_field": "image",
- "modified": "2019-09-19 12:49:02.536647",
+ "modified": "2019-09-20 12:49:02.536647",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead",
@@ -423,15 +471,6 @@
"read": 1,
"report": 1,
"role": "Sales User"
- },
- {
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Guest",
- "share": 1
}
],
"search_fields": "lead_name,lead_owner,status",
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index 1dae4b9fc1c..cc2badf9848 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -2,18 +2,19 @@
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
-import frappe
-from frappe import _
-from frappe.utils import (cstr, validate_email_address, cint, comma_and, has_gravatar, now, getdate, nowdate)
-from frappe.model.mapper import get_mapped_doc
-from erpnext.controllers.selling_controller import SellingController
-from frappe.contacts.address_and_contact import load_address_and_contact
+import frappe
from erpnext.accounts.party import set_taxes
+from erpnext.controllers.selling_controller import SellingController
+from frappe import _
+from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.email.inbox import link_communication_to_document
+from frappe.model.mapper import get_mapped_doc
+from frappe.utils import cint, comma_and, cstr, getdate, has_gravatar, nowdate, validate_email_address
sender_field = "email_id"
+
class Lead(SellingController):
def get_feed(self):
return '{0}: {1}'.format(_(self.status), self.lead_name)
@@ -23,15 +24,22 @@ class Lead(SellingController):
self.get("__onload").is_customer = customer
load_address_and_contact(self)
+ def before_insert(self):
+ self.address_doc = self.create_address()
+ self.contact_doc = self.create_contact()
+
+ def after_insert(self):
+ self.update_links()
+ # after the address and contact are created, flush the field values
+ # to avoid inconsistent reporting in case the documents are changed
+ self.flush_address_and_contact_fields()
+
def validate(self):
self.set_lead_name()
self._prev = frappe._dict({
- "contact_date": frappe.db.get_value("Lead", self.name, "contact_date") if \
- (not cint(self.get("__islocal"))) else None,
- "ends_on": frappe.db.get_value("Lead", self.name, "ends_on") if \
- (not cint(self.get("__islocal"))) else None,
- "contact_by": frappe.db.get_value("Lead", self.name, "contact_by") if \
- (not cint(self.get("__islocal"))) else None,
+ "contact_date": frappe.db.get_value("Lead", self.name, "contact_date") if (not cint(self.is_new())) else None,
+ "ends_on": frappe.db.get_value("Lead", self.name, "ends_on") if (not cint(self.is_new())) else None,
+ "contact_by": frappe.db.get_value("Lead", self.name, "contact_by") if (not cint(self.is_new())) else None,
})
self.set_status()
@@ -39,7 +47,7 @@ class Lead(SellingController):
if self.email_id:
if not self.flags.ignore_email_validation:
- validate_email_address(self.email_id, True)
+ validate_email_address(self.email_id, throw=True)
if self.email_id == self.lead_owner:
frappe.throw(_("Lead Owner cannot be same as the Lead"))
@@ -53,8 +61,7 @@ class Lead(SellingController):
if self.contact_date and getdate(self.contact_date) < getdate(nowdate()):
frappe.throw(_("Next Contact Date cannot be in the past"))
- if self.ends_on and self.contact_date and\
- (self.ends_on < self.contact_date):
+ if self.ends_on and self.contact_date and (self.ends_on < self.contact_date):
frappe.throw(_("Ends On date cannot be before Next Contact Date."))
def on_update(self):
@@ -66,8 +73,7 @@ class Lead(SellingController):
"starts_on": self.contact_date,
"ends_on": self.ends_on or "",
"subject": ('Contact ' + cstr(self.lead_name)),
- "description": ('Contact ' + cstr(self.lead_name)) + \
- (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '')
+ "description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '')
}, force)
def check_email_id_is_unique(self):
@@ -81,8 +87,7 @@ class Lead(SellingController):
.format(comma_and(duplicate_leads)), frappe.DuplicateEntryError)
def on_trash(self):
- frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""",
- self.name)
+ frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name)
self.delete_events()
@@ -115,10 +120,74 @@ class Lead(SellingController):
self.lead_name = self.company_name
+ def create_address(self):
+ address_fields = ["address_title", "address_line1", "address_line2",
+ "city", "county", "state", "country", "pincode"]
+ info_fields = ["email_id", "phone", "fax"]
+
+ # do not create an address if no fields are available,
+ # skipping country since the system auto-sets it from system defaults
+ if not any([self.get(field) for field in address_fields if field != "country"]):
+ return
+
+ address = frappe.new_doc("Address")
+ address.update({addr_field: self.get(addr_field) for addr_field in address_fields})
+ address.update({info_field: self.get(info_field) for info_field in info_fields})
+ address.insert()
+
+ return address
+
+ def create_contact(self):
+ names = self.lead_name.split(" ")
+ if len(names) > 1:
+ first_name, last_name = names[0], " ".join(names[1:])
+ else:
+ first_name, last_name = self.lead_name, None
+
+ contact_fields = ["email_id", "salutation", "gender", "phone", "mobile_no"]
+
+ contact = frappe.new_doc("Contact")
+ contact.update({contact_field: self.get(contact_field) for contact_field in contact_fields})
+ contact.update({
+ "first_name": first_name,
+ "last_name": last_name
+ })
+ contact.insert()
+
+ return contact
+
+ def update_links(self):
+ # update address links
+ if self.address_doc:
+ self.address_doc.append("links", {
+ "link_doctype": "Lead",
+ "link_name": self.name,
+ "link_title": self.lead_name
+ })
+ self.address_doc.save()
+
+ # update contact links
+ if self.contact_doc:
+ self.contact_doc.append("links", {
+ "link_doctype": "Lead",
+ "link_name": self.name,
+ "link_title": self.lead_name
+ })
+ self.contact_doc.save()
+
+ def flush_address_and_contact_fields(self):
+ fields = ['address_line1', 'address_line2', 'address_title', 'city', 'country',
+ 'county', 'fax', 'mobile_no', 'phone', 'pincode', 'salutation', 'state']
+
+ for field in fields:
+ self.set(field, None)
+
+
@frappe.whitelist()
def make_customer(source_name, target_doc=None):
return _make_customer(source_name, target_doc)
+
def _make_customer(source_name, target_doc=None, ignore_permissions=False):
def set_missing_values(source, target):
if source.company_name:
@@ -143,6 +212,7 @@ def _make_customer(source_name, target_doc=None, ignore_permissions=False):
return doclist
+
@frappe.whitelist()
def make_opportunity(source_name, target_doc=None):
def set_missing_values(source, target):
@@ -164,6 +234,7 @@ def make_opportunity(source_name, target_doc=None):
return target_doc
+
@frappe.whitelist()
def make_quotation(source_name, target_doc=None):
def set_missing_values(source, target):
@@ -205,7 +276,8 @@ def _set_missing_values(source, target):
@frappe.whitelist()
def get_lead_details(lead, posting_date=None, company=None):
- if not lead: return {}
+ if not lead:
+ return {}
from erpnext.accounts.party import set_address_details
out = frappe._dict()
@@ -231,6 +303,7 @@ def get_lead_details(lead, posting_date=None, company=None):
return out
+
@frappe.whitelist()
def make_lead_from_communication(communication, ignore_communication_links=False):
""" raise a issue from email """
@@ -267,4 +340,4 @@ def get_lead_with_phone_number(number):
lead = leads[0].name if leads else None
- return lead
\ No newline at end of file
+ return lead
From f9e2bfcc2940c90868941cfd84548f8e301c4d49 Mon Sep 17 00:00:00 2001
From: Rohan Bansal
Date: Thu, 22 Aug 2019 15:03:34 +0530
Subject: [PATCH 03/30] fix: conditionally set lead title as organization or
person
---
erpnext/crm/doctype/lead/lead.json | 12 ++++++++++--
erpnext/crm/doctype/lead/lead.py | 7 +++++++
2 files changed, 17 insertions(+), 2 deletions(-)
diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json
index d2a98b609ac..88a562f720b 100644
--- a/erpnext/crm/doctype/lead/lead.json
+++ b/erpnext/crm/doctype/lead/lead.json
@@ -54,7 +54,8 @@
"website",
"territory",
"unsubscribed",
- "blog_subscriber"
+ "blog_subscriber",
+ "title"
],
"fields": [
{
@@ -409,6 +410,13 @@
"fieldtype": "Data",
"label": "Postal Code",
"options": "Country"
+ },
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Title",
+ "print_hide": 1
}
],
"icon": "fa fa-user",
@@ -477,5 +485,5 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
- "title_field": "lead_name"
+ "title_field": "title"
}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index cc2badf9848..9e5fdc0e120 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -36,6 +36,7 @@ class Lead(SellingController):
def validate(self):
self.set_lead_name()
+ self.set_title()
self._prev = frappe._dict({
"contact_date": frappe.db.get_value("Lead", self.name, "contact_date") if (not cint(self.is_new())) else None,
"ends_on": frappe.db.get_value("Lead", self.name, "ends_on") if (not cint(self.is_new())) else None,
@@ -120,6 +121,12 @@ class Lead(SellingController):
self.lead_name = self.company_name
+ def set_title(self):
+ if self.organization_lead:
+ self.title = self.company_name
+ else:
+ self.title = self.lead_name
+
def create_address(self):
address_fields = ["address_title", "address_line1", "address_line2",
"city", "county", "state", "country", "pincode"]
From 75da5af900cb49e509a63433e89830cbb58bd561 Mon Sep 17 00:00:00 2001
From: Rohan Bansal
Date: Thu, 22 Aug 2019 15:38:44 +0530
Subject: [PATCH 04/30] fix: set missing values
---
erpnext/crm/doctype/lead/lead.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index 9e5fdc0e120..bd0c742aa2d 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -145,6 +145,9 @@ class Lead(SellingController):
return address
def create_contact(self):
+ if not self.lead_name:
+ self.set_lead_name()
+
names = self.lead_name.split(" ")
if len(names) > 1:
first_name, last_name = names[0], " ".join(names[1:])
From 69e3868a9dfcaf02c61defac58df8d39cbf1cb51 Mon Sep 17 00:00:00 2001
From: Rohan Bansal
Date: Fri, 6 Sep 2019 13:38:15 +0530
Subject: [PATCH 05/30] patch: set title for old leads
---
erpnext/patches.txt | 3 ++-
erpnext/patches/v12_0/set_lead_title_field.py | 11 +++++++++++
2 files changed, 13 insertions(+), 1 deletion(-)
create mode 100644 erpnext/patches/v12_0/set_lead_title_field.py
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 07b646b0f82..0495c027523 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -646,4 +646,5 @@ erpnext.patches.v12_0.set_payment_entry_status
erpnext.patches.v12_0.update_owner_fields_in_acc_dimension_custom_fields
erpnext.patches.v12_0.set_default_for_add_taxes_from_item_tax_template
erpnext.patches.v12_0.remove_denied_leaves_from_leave_ledger
-erpnext.patches.v12_0.update_price_or_product_discount
\ No newline at end of file
+erpnext.patches.v12_0.update_price_or_product_discount
+erpnext.patches.v12_0.set_lead_title_field
diff --git a/erpnext/patches/v12_0/set_lead_title_field.py b/erpnext/patches/v12_0/set_lead_title_field.py
new file mode 100644
index 00000000000..86e00038f6c
--- /dev/null
+++ b/erpnext/patches/v12_0/set_lead_title_field.py
@@ -0,0 +1,11 @@
+import frappe
+
+
+def execute():
+ frappe.reload_doc("crm", "doctype", "lead")
+ frappe.db.sql("""
+ UPDATE
+ `tabLead`
+ SET
+ title = IF(organization_lead = 1, company_name, lead_name)
+ """)
From fd46fef857b578515db6d12245d8f01895dc00b1 Mon Sep 17 00:00:00 2001
From: Rohan Bansal
Date: Fri, 6 Sep 2019 14:10:20 +0530
Subject: [PATCH 06/30] fix: add designation to Lead
---
erpnext/crm/doctype/lead/lead.json | 153 +++++++++++++++--------------
erpnext/crm/doctype/lead/lead.py | 4 +-
2 files changed, 82 insertions(+), 75 deletions(-)
diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json
index 88a562f720b..c8e9fbc463e 100644
--- a/erpnext/crm/doctype/lead/lead.json
+++ b/erpnext/crm/doctype/lead/lead.json
@@ -17,6 +17,7 @@
"lead_owner",
"status",
"salutation",
+ "designation",
"gender",
"source",
"customer",
@@ -136,6 +137,13 @@
"reqd": 1,
"search_index": 1
},
+ {
+ "depends_on": "eval: doc.__islocal",
+ "fieldname": "salutation",
+ "fieldtype": "Link",
+ "label": "Salutation",
+ "options": "Salutation"
+ },
{
"fieldname": "gender",
"fieldtype": "Link",
@@ -237,6 +245,56 @@
"label": "Address HTML",
"read_only": 1
},
+ {
+ "depends_on": "eval: doc.__islocal",
+ "fieldname": "address_title",
+ "fieldtype": "Data",
+ "label": "Address Title"
+ },
+ {
+ "depends_on": "eval: doc.__islocal",
+ "fieldname": "address_line1",
+ "fieldtype": "Data",
+ "label": "Address Line 1"
+ },
+ {
+ "depends_on": "eval: doc.__islocal",
+ "fieldname": "address_line2",
+ "fieldtype": "Data",
+ "label": "Address Line 2"
+ },
+ {
+ "depends_on": "eval: doc.__islocal",
+ "fieldname": "city",
+ "fieldtype": "Data",
+ "label": "City/Town"
+ },
+ {
+ "depends_on": "eval: doc.__islocal",
+ "fieldname": "county",
+ "fieldtype": "Data",
+ "label": "County"
+ },
+ {
+ "depends_on": "eval: doc.__islocal",
+ "fieldname": "state",
+ "fieldtype": "Data",
+ "label": "State"
+ },
+ {
+ "depends_on": "eval: doc.__islocal",
+ "fieldname": "country",
+ "fieldtype": "Link",
+ "label": "Country",
+ "options": "Country"
+ },
+ {
+ "depends_on": "eval: doc.__islocal",
+ "fieldname": "pincode",
+ "fieldtype": "Data",
+ "label": "Postal Code",
+ "options": "Country"
+ },
{
"fieldname": "column_break2",
"fieldtype": "Column Break"
@@ -255,13 +313,6 @@
"oldfieldname": "contact_no",
"oldfieldtype": "Data"
},
- {
- "depends_on": "eval: doc.__islocal",
- "fieldname": "salutation",
- "fieldtype": "Link",
- "label": "Salutation",
- "options": "Salutation"
- },
{
"depends_on": "eval: doc.__islocal",
"fieldname": "mobile_no",
@@ -278,22 +329,6 @@
"oldfieldname": "fax",
"oldfieldtype": "Data"
},
- {
- "fieldname": "website",
- "fieldtype": "Data",
- "label": "Website",
- "oldfieldname": "website",
- "oldfieldtype": "Data"
- },
- {
- "fieldname": "territory",
- "fieldtype": "Link",
- "label": "Territory",
- "oldfieldname": "territory",
- "oldfieldtype": "Link",
- "options": "Territory",
- "print_hide": 1
- },
{
"collapsible": 1,
"fieldname": "more_info",
@@ -349,6 +384,22 @@
"options": "Company",
"remember_last_selected_value": 1
},
+ {
+ "fieldname": "website",
+ "fieldtype": "Data",
+ "label": "Website",
+ "oldfieldname": "website",
+ "oldfieldtype": "Data"
+ },
+ {
+ "fieldname": "territory",
+ "fieldtype": "Link",
+ "label": "Territory",
+ "oldfieldname": "territory",
+ "oldfieldtype": "Link",
+ "options": "Territory",
+ "print_hide": 1
+ },
{
"default": "0",
"fieldname": "unsubscribed",
@@ -361,62 +412,18 @@
"fieldtype": "Check",
"label": "Blog Subscriber"
},
- {
- "depends_on": "eval: doc.__islocal",
- "fieldname": "address_title",
- "fieldtype": "Data",
- "label": "Address Title"
- },
- {
- "depends_on": "eval: doc.__islocal",
- "fieldname": "address_line1",
- "fieldtype": "Data",
- "label": "Address Line 1"
- },
- {
- "depends_on": "eval: doc.__islocal",
- "fieldname": "address_line2",
- "fieldtype": "Data",
- "label": "Address Line 2"
- },
- {
- "depends_on": "eval: doc.__islocal",
- "fieldname": "city",
- "fieldtype": "Data",
- "label": "City/Town"
- },
- {
- "depends_on": "eval: doc.__islocal",
- "fieldname": "county",
- "fieldtype": "Data",
- "label": "County"
- },
- {
- "depends_on": "eval: doc.__islocal",
- "fieldname": "state",
- "fieldtype": "Data",
- "label": "State"
- },
- {
- "depends_on": "eval: doc.__islocal",
- "fieldname": "country",
- "fieldtype": "Link",
- "label": "Country",
- "options": "Country"
- },
- {
- "depends_on": "eval: doc.__islocal",
- "fieldname": "pincode",
- "fieldtype": "Data",
- "label": "Postal Code",
- "options": "Country"
- },
{
"fieldname": "title",
"fieldtype": "Data",
"hidden": 1,
"label": "Title",
"print_hide": 1
+ },
+ {
+ "fieldname": "designation",
+ "fieldtype": "Link",
+ "label": "Designation",
+ "options": "Designation"
}
],
"icon": "fa fa-user",
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index bd0c742aa2d..c0416092b1a 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -154,7 +154,7 @@ class Lead(SellingController):
else:
first_name, last_name = self.lead_name, None
- contact_fields = ["email_id", "salutation", "gender", "phone", "mobile_no"]
+ contact_fields = ["email_id", "salutation", "gender", "designation", "phone", "mobile_no"]
contact = frappe.new_doc("Contact")
contact.update({contact_field: self.get(contact_field) for contact_field in contact_fields})
@@ -187,7 +187,7 @@ class Lead(SellingController):
def flush_address_and_contact_fields(self):
fields = ['address_line1', 'address_line2', 'address_title', 'city', 'country',
- 'county', 'fax', 'mobile_no', 'phone', 'pincode', 'salutation', 'state']
+ 'county', 'fax', 'mobile_no', 'phone', 'pincode', 'state']
for field in fields:
self.set(field, None)
From c59ac36378fb720b6bd8562c004ae53fbed1beb7 Mon Sep 17 00:00:00 2001
From: Rohan Bansal
Date: Wed, 18 Sep 2019 13:46:22 +0530
Subject: [PATCH 07/30] fix: use new Contact schema
---
erpnext/crm/doctype/lead/lead.json | 9 +++++++++
erpnext/crm/doctype/lead/lead.py | 31 ++++++++++++++++++++++--------
2 files changed, 32 insertions(+), 8 deletions(-)
diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json
index c8e9fbc463e..2219307caf4 100644
--- a/erpnext/crm/doctype/lead/lead.json
+++ b/erpnext/crm/doctype/lead/lead.json
@@ -486,6 +486,15 @@
"read": 1,
"report": 1,
"role": "Sales User"
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Guest",
+ "share": 1
}
],
"search_fields": "lead_name,lead_owner,status",
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index c0416092b1a..6645c4d0019 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -80,8 +80,8 @@ class Lead(SellingController):
def check_email_id_is_unique(self):
if self.email_id:
# validate email is unique
- duplicate_leads = frappe.db.sql_list("""select name from tabLead
- where email_id=%s and name!=%s""", (self.email_id, self.name))
+ duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]})
+ duplicate_leads = [lead.name for lead in duplicate_leads]
if duplicate_leads:
frappe.throw(_("Email Address must be unique, already exists for {0}")
@@ -154,13 +154,28 @@ class Lead(SellingController):
else:
first_name, last_name = self.lead_name, None
- contact_fields = ["email_id", "salutation", "gender", "designation", "phone", "mobile_no"]
-
contact = frappe.new_doc("Contact")
- contact.update({contact_field: self.get(contact_field) for contact_field in contact_fields})
contact.update({
"first_name": first_name,
- "last_name": last_name
+ "last_name": last_name,
+ "salutation": self.salutation,
+ "gender": self.gender,
+ "designation": self.designation,
+ "email_ids": [
+ {
+ "email_id": self.email_id,
+ "is_primary": 1
+ }
+ ],
+ "phone_nos": [
+ {
+ "phone": self.phone,
+ "is_primary": 1
+ },
+ {
+ "phone": self.mobile_no,
+ }
+ ]
})
contact.insert()
@@ -186,8 +201,8 @@ class Lead(SellingController):
self.contact_doc.save()
def flush_address_and_contact_fields(self):
- fields = ['address_line1', 'address_line2', 'address_title', 'city', 'country',
- 'county', 'fax', 'mobile_no', 'phone', 'pincode', 'state']
+ fields = ['address_line1', 'address_line2', 'address_title',
+ 'city', 'county', 'country', 'fax', 'pincode', 'state']
for field in fields:
self.set(field, None)
From c131bd1b257ddd13fef98ef0527da63ae86e1255 Mon Sep 17 00:00:00 2001
From: Mangesh-Khairnar
Date: Mon, 2 Dec 2019 01:09:59 +0530
Subject: [PATCH 08/30] feat(marketplace): edit item dialog
---
.../js/hub/components/edit_details_dialog.js | 45 +++++++++++++++++++
erpnext/public/js/hub/pages/Item.vue | 37 ++++++++++++++-
2 files changed, 81 insertions(+), 1 deletion(-)
create mode 100644 erpnext/public/js/hub/components/edit_details_dialog.js
diff --git a/erpnext/public/js/hub/components/edit_details_dialog.js b/erpnext/public/js/hub/components/edit_details_dialog.js
new file mode 100644
index 00000000000..b341901fece
--- /dev/null
+++ b/erpnext/public/js/hub/components/edit_details_dialog.js
@@ -0,0 +1,45 @@
+function EditDetailsDialog(primary_action, defaults) {
+ let dialog = new frappe.ui.Dialog({
+ title: __('Update Details'),
+ fields: [
+ {
+ "label": "Item Name",
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "default": defaults.item_name,
+ "reqd": 1
+ },
+ {
+ "label": "Hub Category",
+ "fieldname": "hub_category",
+ "fieldtype": "Autocomplete",
+ "default": defaults.hub_category,
+ "options": [],
+ "reqd": 1
+ },
+ {
+ "label": "Description",
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "default": defaults.description,
+ "options": [],
+ "reqd": 1
+ }
+ ],
+ primary_action_label: primary_action.label || __('Update Details'),
+ primary_action: primary_action.fn,
+ });
+
+ hub.call('get_categories')
+ .then(categories => {
+ categories = categories.map(d => d.name);
+ dialog.fields_dict.hub_category.df.options = categories;
+ dialog.fields_dict.hub_category.set_options();
+ });
+
+ return dialog;
+}
+
+export {
+ EditDetailsDialog
+};
\ No newline at end of file
diff --git a/erpnext/public/js/hub/pages/Item.vue b/erpnext/public/js/hub/pages/Item.vue
index 841d0046db8..b8399e30d6d 100644
--- a/erpnext/public/js/hub/pages/Item.vue
+++ b/erpnext/public/js/hub/pages/Item.vue
@@ -35,6 +35,7 @@
From 088be37e64be70b47db8e8f6ce962875d79956dc Mon Sep 17 00:00:00 2001
From: Sun Howwrongbum
Date: Tue, 24 Dec 2019 12:29:25 +0530
Subject: [PATCH 15/30] feat: consider expiry_date during Batch queries
(#20065)
---
erpnext/controllers/queries.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 7b4a4c92ad1..5c319008a85 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -311,6 +311,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
and sle.item_code = %(item_code)s
and sle.warehouse = %(warehouse)s
and (sle.batch_no like %(txt)s
+ or batch.expiry_date like %(txt)s
or batch.manufacturing_date like %(txt)s)
and batch.docstatus < 2
{cond}
@@ -329,6 +330,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
where batch.disabled = 0
and item = %(item_code)s
and (name like %(txt)s
+ or expiry_date like %(txt)s
or manufacturing_date like %(txt)s)
and docstatus < 2
{0}
From 5af4c57ef7b1eee87dab16b39813e4d7f59f81a1 Mon Sep 17 00:00:00 2001
From: Himanshu
Date: Tue, 24 Dec 2019 12:31:38 +0530
Subject: [PATCH 16/30] fix: ambihious error (#20068)
---
erpnext/stock/doctype/batch/batch.py | 21 ++++++++++-----------
1 file changed, 10 insertions(+), 11 deletions(-)
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index 3e890b4dd4e..114925469b5 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -261,15 +261,14 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False):
def get_batches(item_code, warehouse, qty=1, throw=False):
- batches = frappe.db.sql(
- 'select batch_id, sum(actual_qty) as qty from `tabBatch` join `tabStock Ledger Entry` ignore index (item_code, warehouse) '
- 'on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no )'
- 'where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s '
- 'and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL)'
- 'group by batch_id '
- 'order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC',
- (item_code, warehouse),
- as_dict=True
- )
- return batches
+ return frappe.db.sql("""
+ select batch_id, sum(`tabStock Ledger Entry`.actual_qty) as qty
+ from `tabBatch`
+ join `tabStock Ledger Entry` ignore index (item_code, warehouse)
+ on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no )
+ where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s
+ and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL)
+ group by batch_id
+ order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC'
+ """, (item_code, warehouse), as_dict=True)
\ No newline at end of file
From 55bc26e300914954f16fd2fae76af038c10a8d34 Mon Sep 17 00:00:00 2001
From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com>
Date: Tue, 24 Dec 2019 12:55:01 +0530
Subject: [PATCH 17/30] fix: Allow creation of multiple landed cost voucher
against a Purchase Document (#20058)
---
.../purchase_receipt/purchase_receipt.py | 47 +++++++++++--------
1 file changed, 28 insertions(+), 19 deletions(-)
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 060175f9045..691f92ffa72 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -88,7 +88,7 @@ class PurchaseReceipt(BuyingController):
if getdate(self.posting_date) > getdate(nowdate()):
throw(_("Posting Date cannot be future date"))
-
+
def validate_cwip_accounts(self):
for item in self.get('items'):
if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category):
@@ -362,7 +362,7 @@ class PurchaseReceipt(BuyingController):
# valuation rate is total of net rate, raw mat supp cost, tax amount, lcv amount per item
self.update_assets(item, item.valuation_rate)
return gl_entries
-
+
def add_asset_gl_entries(self, item, gl_entries):
arbnb_account = self.get_company_default("asset_received_but_not_billed")
# This returns category's cwip account if not then fallback to company's default cwip account
@@ -395,7 +395,7 @@ class PurchaseReceipt(BuyingController):
"credit_in_account_currency": (base_asset_amount
if asset_rbnb_currency == self.company_currency else asset_amount)
}, item=item))
-
+
def add_lcv_gl_entries(self, item, gl_entries):
expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation")
if not is_cwip_accounting_enabled(item.asset_category):
@@ -404,7 +404,7 @@ class PurchaseReceipt(BuyingController):
else:
# This returns company's default cwip account
asset_account = get_asset_account("capital_work_in_progress_account", company=self.company)
-
+
gl_entries.append(self.get_gl_dict({
"account": expenses_included_in_asset_valuation,
"against": asset_account,
@@ -424,7 +424,7 @@ class PurchaseReceipt(BuyingController):
}, item=item))
def update_assets(self, item, valuation_rate):
- assets = frappe.db.get_all('Asset',
+ assets = frappe.db.get_all('Asset',
filters={ 'purchase_receipt': self.name, 'item_code': item.item_code }
)
@@ -610,27 +610,36 @@ def make_stock_entry(source_name,target_doc=None):
return doclist
def get_item_account_wise_additional_cost(purchase_document):
- landed_cost_voucher = frappe.get_value("Landed Cost Purchase Receipt",
- {"receipt_document": purchase_document, "docstatus": 1}, "parent")
+ landed_cost_vouchers = frappe.get_all("Landed Cost Purchase Receipt", fields=["parent"],
+ filters = {"receipt_document": purchase_document, "docstatus": 1})
- if not landed_cost_voucher:
+ if not landed_cost_vouchers:
return
total_item_cost = 0
item_account_wise_cost = {}
- landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", landed_cost_voucher)
- based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on)
+ item_cost_allocated = []
- for item in landed_cost_voucher_doc.items:
- total_item_cost += item.get(based_on_field)
+ for lcv in landed_cost_vouchers:
+ landed_cost_voucher_doc = frappe.get_cached_doc("Landed Cost Voucher", lcv.parent)
+ based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on)
- for item in landed_cost_voucher_doc.items:
- if item.receipt_document == purchase_document:
- for account in landed_cost_voucher_doc.taxes:
- item_account_wise_cost.setdefault((item.item_code, item.purchase_receipt_item), {})
- item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault(account.expense_account, 0.0)
- item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account] += \
- account.amount * item.get(based_on_field) / total_item_cost
+ for item in landed_cost_voucher_doc.items:
+ if item.purchase_receipt_item not in item_cost_allocated:
+ total_item_cost += item.get(based_on_field)
+ item_cost_allocated.append(item.purchase_receipt_item)
+
+ for lcv in landed_cost_vouchers:
+ landed_cost_voucher_doc = frappe.get_cached_doc("Landed Cost Voucher", lcv.parent)
+ based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on)
+
+ for item in landed_cost_voucher_doc.items:
+ if item.receipt_document == purchase_document:
+ for account in landed_cost_voucher_doc.taxes:
+ item_account_wise_cost.setdefault((item.item_code, item.purchase_receipt_item), {})
+ item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault(account.expense_account, 0.0)
+ item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account] += \
+ account.amount * item.get(based_on_field) / total_item_cost
return item_account_wise_cost
From 08433c2919160e2b099ad191318f3c52e2d0d789 Mon Sep 17 00:00:00 2001
From: rohitwaghchaure
Date: Tue, 24 Dec 2019 13:03:47 +0530
Subject: [PATCH 18/30] fix: supplier email id field not showing in the
notification for purchase cycle doctypes (#20071)
---
.../accounts/doctype/purchase_invoice/purchase_invoice.json | 5 ++++-
erpnext/buying/doctype/purchase_order/purchase_order.json | 3 ++-
erpnext/stock/doctype/purchase_receipt/purchase_receipt.json | 5 ++++-
3 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index 6fe18115c04..3715d774139 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-05-21 16:16:39",
@@ -417,6 +418,7 @@
"fieldname": "contact_email",
"fieldtype": "Small Text",
"label": "Contact Email",
+ "options": "Email",
"print_hide": 1,
"read_only": 1
},
@@ -1287,7 +1289,8 @@
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
- "modified": "2019-09-17 22:31:42.666601",
+ "links": [],
+ "modified": "2019-12-24 12:51:58.613538",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json
index 8cd44c789de..d4c5acee906 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.json
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.json
@@ -342,6 +342,7 @@
"fieldname": "contact_email",
"fieldtype": "Small Text",
"label": "Contact Email",
+ "options": "Email",
"print_hide": 1,
"read_only": 1
},
@@ -1054,7 +1055,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2019-12-18 13:13:22.852412",
+ "modified": "2019-12-24 12:44:13.137194",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index d6bc1a9b972..27cd997bcc2 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-05-21 16:16:39",
@@ -305,6 +306,7 @@
"fieldname": "contact_email",
"fieldtype": "Small Text",
"label": "Contact Email",
+ "options": "Email",
"print_hide": 1,
"read_only": 1
},
@@ -1056,7 +1058,8 @@
"icon": "fa fa-truck",
"idx": 261,
"is_submittable": 1,
- "modified": "2019-09-27 14:24:49.044505",
+ "links": [],
+ "modified": "2019-12-24 12:52:17.216304",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",
From 5557b16260093c0aa5e4a143421620ae677cdf4d Mon Sep 17 00:00:00 2001
From: Don-Leopardo <46027152+Don-Leopardo@users.noreply.github.com>
Date: Tue, 24 Dec 2019 08:09:34 -0300
Subject: [PATCH 19/30] fix: order_type validation restriction (#18096)
---
erpnext/controllers/selling_controller.py | 7 -------
erpnext/selling/doctype/quotation/quotation.py | 4 ----
erpnext/selling/doctype/sales_order/sales_order.py | 4 ----
3 files changed, 15 deletions(-)
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 9dbd5be9188..9a9f3d1d319 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -148,13 +148,6 @@ class SellingController(StockController):
if sales_team and total != 100.0:
throw(_("Total allocated percentage for sales team should be 100"))
- def validate_order_type(self):
- valid_types = ["Sales", "Maintenance", "Shopping Cart"]
- if not self.order_type:
- self.order_type = "Sales"
- elif self.order_type not in valid_types:
- throw(_("Order Type must be one of {0}").format(comma_or(valid_types)))
-
def validate_max_discount(self):
for d in self.get("items"):
if d.item_code:
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index 790b2f0804d..9ebef0d6bc7 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -26,7 +26,6 @@ class Quotation(SellingController):
super(Quotation, self).validate()
self.set_status()
self.update_opportunity()
- self.validate_order_type()
self.validate_uom_is_integer("stock_uom", "qty")
self.validate_valid_till()
self.set_customer_name()
@@ -40,9 +39,6 @@ class Quotation(SellingController):
def has_sales_order(self):
return frappe.db.get_value("Sales Order Item", {"prevdoc_docname": self.name, "docstatus": 1})
- def validate_order_type(self):
- super(Quotation, self).validate_order_type()
-
def update_lead(self):
if self.quotation_to == "Lead" and self.party_name:
frappe.get_doc("Lead", self.party_name).set_status(update=True)
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 2112a4174b1..94bbb793a3a 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -34,7 +34,6 @@ class SalesOrder(SellingController):
def validate(self):
super(SalesOrder, self).validate()
- self.validate_order_type()
self.validate_delivery_date()
self.validate_proj_cust()
self.validate_po()
@@ -100,9 +99,6 @@ class SalesOrder(SellingController):
frappe.msgprint(_("Quotation {0} not of type {1}")
.format(d.prevdoc_docname, self.order_type))
- def validate_order_type(self):
- super(SalesOrder, self).validate_order_type()
-
def validate_delivery_date(self):
if self.order_type == 'Sales' and not self.skip_delivery_note:
delivery_date_list = [d.delivery_date for d in self.get("items") if d.delivery_date]
From 0ad7449ae34cf9c78e8c31192f867000820b1237 Mon Sep 17 00:00:00 2001
From: Saqib
Date: Tue, 24 Dec 2019 16:42:30 +0530
Subject: [PATCH 20/30] feat: allow adding & deleting of items in submitted SO
& PO (#19911)
* feat: allow adding of items after quotation submission
* feat: allow deletion of items from submitted SO & PO
* fix: case when items are added and deleted at once
* fix: add test cases
* For deletion of items while Updating Items after submitting PO & SO
---
.../purchase_order/test_purchase_order.py | 67 +++++++++++++++++++
erpnext/controllers/accounts_controller.py | 21 ++++++
erpnext/public/js/utils.js | 3 +-
.../doctype/sales_order/test_sales_order.py | 49 +++++++++++++-
4 files changed, 138 insertions(+), 2 deletions(-)
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 08f5d8b4d00..1712369e60b 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -118,6 +118,73 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertEqual(po.get("items")[0].amount, 1400)
self.assertEqual(get_ordered_qty(), existing_ordered_qty + 3)
+
+ def test_add_new_item_in_update_child_qty_rate(self):
+ po = create_purchase_order(do_not_save=1)
+ po.items[0].qty = 4
+ po.save()
+ po.submit()
+ pr = make_pr_against_po(po.name, 2)
+
+ po.load_from_db()
+ first_item_of_po = po.get("items")[0]
+
+ trans_item = json.dumps([
+ {
+ 'item_code': first_item_of_po.item_code,
+ 'rate': first_item_of_po.rate,
+ 'qty': first_item_of_po.qty,
+ 'docname': first_item_of_po.name
+ },
+ {'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7}
+ ])
+ update_child_qty_rate('Purchase Order', trans_item, po.name)
+
+ po.reload()
+ self.assertEquals(len(po.get('items')), 2)
+ self.assertEqual(po.status, 'To Receive and Bill')
+
+
+ def test_remove_item_in_update_child_qty_rate(self):
+ po = create_purchase_order(do_not_save=1)
+ po.items[0].qty = 4
+ po.save()
+ po.submit()
+ pr = make_pr_against_po(po.name, 2)
+
+ po.reload()
+ first_item_of_po = po.get("items")[0]
+ # add an item
+ trans_item = json.dumps([
+ {
+ 'item_code': first_item_of_po.item_code,
+ 'rate': first_item_of_po.rate,
+ 'qty': first_item_of_po.qty,
+ 'docname': first_item_of_po.name
+ },
+ {'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7}])
+ update_child_qty_rate('Purchase Order', trans_item, po.name)
+
+ po.reload()
+ # check if can remove received item
+ trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': po.get("items")[1].name}])
+ self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Purchase Order', trans_item, po.name)
+
+ first_item_of_po = po.get("items")[0]
+ trans_item = json.dumps([
+ {
+ 'item_code': first_item_of_po.item_code,
+ 'rate': first_item_of_po.rate,
+ 'qty': first_item_of_po.qty,
+ 'docname': first_item_of_po.name
+ }
+ ])
+ update_child_qty_rate('Purchase Order', trans_item, po.name)
+
+ po.reload()
+ self.assertEquals(len(po.get('items')), 1)
+ self.assertEqual(po.status, 'To Receive and Bill')
+
def test_update_qty(self):
po = create_purchase_order()
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 6150516ac8c..86f5d53b33c 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1155,6 +1155,25 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna
child_item.base_amount = 1 # Initiallize value will update in parent validation
return child_item
+def check_and_delete_children(parent, data):
+ deleted_children = []
+ updated_item_names = [d.get("docname") for d in data]
+ for item in parent.items:
+ if item.name not in updated_item_names:
+ deleted_children.append(item)
+
+ for d in deleted_children:
+ if parent.doctype == "Sales Order" and flt(d.delivered_qty):
+ frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been delivered").format(d.idx, d.item_code))
+
+ if parent.doctype == "Purchase Order" and flt(d.received_qty):
+ frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been received").format(d.idx, d.item_code))
+
+ if flt(d.billed_amt):
+ frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been billed.").format(d.idx, d.item_code))
+
+ d.cancel()
+ d.delete()
@frappe.whitelist()
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
@@ -1163,6 +1182,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation']
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
+ check_and_delete_children(parent, data)
+
for d in data:
new_child_flag = False
if not d.get("docname"):
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index f363999ebe2..3f444f83879 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -458,7 +458,8 @@ erpnext.utils.update_child_items = function(opts) {
fieldname:"item_code",
options: 'Item',
in_list_view: 1,
- read_only: 1,
+ read_only: 0,
+ disabled: 0,
label: __('Item Code')
}, {
fieldtype:'Float',
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index feb6b76c4d3..d8e9a635b3a 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -321,7 +321,12 @@ class TestSalesOrder(unittest.TestCase):
create_dn_against_so(so.name, 4)
make_sales_invoice(so.name)
- trans_item = json.dumps([{'item_code' : '_Test Item 2', 'rate' : 200, 'qty' : 7}])
+ first_item_of_so = so.get("items")[0]
+ trans_item = json.dumps([
+ {'item_code' : first_item_of_so.item_code, 'rate' : first_item_of_so.rate, \
+ 'qty' : first_item_of_so.qty, 'docname': first_item_of_so.name},
+ {'item_code' : '_Test Item 2', 'rate' : 200, 'qty' : 7}
+ ])
update_child_qty_rate('Sales Order', trans_item, so.name)
so.reload()
@@ -330,6 +335,48 @@ class TestSalesOrder(unittest.TestCase):
self.assertEqual(so.get("items")[-1].qty, 7)
self.assertEqual(so.get("items")[-1].amount, 1400)
self.assertEqual(so.status, 'To Deliver and Bill')
+
+ def test_remove_item_in_update_child_qty_rate(self):
+ so = make_sales_order(**{
+ "item_list": [{
+ "item_code": '_Test Item',
+ "qty": 5,
+ "rate":1000
+ }]
+ })
+ create_dn_against_so(so.name, 2)
+ make_sales_invoice(so.name)
+
+ # add an item so as to try removing items
+ trans_item = json.dumps([
+ {"item_code": '_Test Item', "qty": 5, "rate":1000, "docname": so.get("items")[0].name},
+ {"item_code": '_Test Item 2', "qty": 2, "rate":500}
+ ])
+ update_child_qty_rate('Sales Order', trans_item, so.name)
+ so.reload()
+ self.assertEqual(len(so.get("items")), 2)
+
+ # check if delivered items can be removed
+ trans_item = json.dumps([{
+ "item_code": '_Test Item 2',
+ "qty": 2,
+ "rate":500,
+ "docname": so.get("items")[1].name
+ }])
+ self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Sales Order', trans_item, so.name)
+
+ #remove last added item
+ trans_item = json.dumps([{
+ "item_code": '_Test Item',
+ "qty": 5,
+ "rate":1000,
+ "docname": so.get("items")[0].name
+ }])
+ update_child_qty_rate('Sales Order', trans_item, so.name)
+
+ so.reload()
+ self.assertEqual(len(so.get("items")), 1)
+ self.assertEqual(so.status, 'To Deliver and Bill')
def test_update_child_qty_rate(self):
From d7427919a4d638359de9c7ba4be4832cf4d6011f Mon Sep 17 00:00:00 2001
From: deepeshgarg007
Date: Tue, 24 Dec 2019 16:45:09 +0530
Subject: [PATCH 21/30] fix: Address section rearrange and minor bug fixes
---
erpnext/crm/doctype/lead/lead.json | 36 +++++++++++++++++++-----------
erpnext/crm/doctype/lead/lead.py | 33 ++++++++++++++-------------
2 files changed, 41 insertions(+), 28 deletions(-)
diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json
index 2219307caf4..bc007b146f1 100644
--- a/erpnext/crm/doctype/lead/lead.json
+++ b/erpnext/crm/doctype/lead/lead.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_events_in_timeline": 1,
"allow_import": 1,
"autoname": "naming_series:",
@@ -30,18 +31,19 @@
"ends_on",
"notes_section",
"notes",
- "contact_info",
+ "address_info",
"address_html",
"address_title",
"address_line1",
"address_line2",
"city",
"county",
+ "column_break2",
+ "contact_html",
"state",
"country",
"pincode",
- "column_break2",
- "contact_html",
+ "contact_section",
"phone",
"mobile_no",
"fax",
@@ -230,15 +232,6 @@
"fieldtype": "Text Editor",
"label": "Notes"
},
- {
- "collapsible": 1,
- "collapsible_depends_on": "eval: doc.__islocal",
- "fieldname": "contact_info",
- "fieldtype": "Section Break",
- "label": "Address & Contact",
- "oldfieldtype": "Column Break",
- "options": "fa fa-map-marker"
- },
{
"fieldname": "address_html",
"fieldtype": "HTML",
@@ -424,12 +417,29 @@
"fieldtype": "Link",
"label": "Designation",
"options": "Designation"
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "eval: doc.__islocal",
+ "fieldname": "address_info",
+ "fieldtype": "Section Break",
+ "label": "Address & Contact",
+ "oldfieldtype": "Column Break",
+ "options": "fa fa-map-marker"
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "eval: doc.__islocal",
+ "fieldname": "contact_section",
+ "fieldtype": "Section Break",
+ "label": "Contact"
}
],
"icon": "fa fa-user",
"idx": 5,
"image_field": "image",
- "modified": "2019-09-20 12:49:02.536647",
+ "links": [],
+ "modified": "2019-12-24 16:00:44.239168",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead",
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index 6645c4d0019..6cab18dc1c3 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -161,22 +161,25 @@ class Lead(SellingController):
"salutation": self.salutation,
"gender": self.gender,
"designation": self.designation,
- "email_ids": [
- {
- "email_id": self.email_id,
- "is_primary": 1
- }
- ],
- "phone_nos": [
- {
- "phone": self.phone,
- "is_primary": 1
- },
- {
- "phone": self.mobile_no,
- }
- ]
})
+
+ if self.email_id:
+ contact.append("email_ids", {
+ "email_id": self.email_id,
+ "is_primary": 1
+ })
+
+ if self.phone:
+ contact.append("phone_nos", {
+ "phone": self.phone,
+ "is_primary": 1
+ })
+
+ if self.mobile_no:
+ contact.append("phone_nos", {
+ "phone": self.mobile_no
+ })
+
contact.insert()
return contact
From b972f763f30e2ab5b6e8f6aa5c59a103ccf22970 Mon Sep 17 00:00:00 2001
From: Pranav Nachnekar
Date: Tue, 24 Dec 2019 11:16:46 +0000
Subject: [PATCH 22/30] fix: added validation for cost center (#19932)
* fix: add validation for cost centers in sales invoice
* use `get_cached_value` for getting compnay, formatting of error message
---
erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 0f4d4451be9..9ea5a51e3f5 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -90,6 +90,7 @@ class SalesInvoice(SellingController):
self.validate_account_for_change_amount()
self.validate_fixed_asset()
self.set_income_account_for_fixed_assets()
+ self.validate_item_cost_centers()
validate_inter_company_party(self.doctype, self.customer, self.company, self.inter_company_invoice_reference)
if cint(self.is_pos):
@@ -147,6 +148,12 @@ class SalesInvoice(SellingController):
elif asset.status in ("Scrapped", "Cancelled", "Sold"):
frappe.throw(_("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format(d.idx, d.asset, asset.status))
+ def validate_item_cost_centers(self):
+ for item in self.items:
+ cost_center_company = frappe.get_cached_value("Cost Center", item.cost_center, "company")
+ 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 before_save(self):
set_account_for_mode_of_payment(self)
From ec258a43d9dba31d887379ef9b629db997c46a3a Mon Sep 17 00:00:00 2001
From: Don-Leopardo <46027152+Don-Leopardo@users.noreply.github.com>
Date: Tue, 24 Dec 2019 09:05:53 -0300
Subject: [PATCH 23/30] perf: Asset Depreciations and Balances report (#18022)
---
.../asset_depreciations_and_balances.py | 215 ++++++++++--------
1 file changed, 115 insertions(+), 100 deletions(-)
diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
index 0c99f1424cf..78546609adb 100644
--- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
+++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
@@ -4,126 +4,141 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-from frappe.utils import formatdate, getdate, flt, add_days
+from frappe.utils import formatdate, flt, add_days
+
def execute(filters=None):
filters.day_before_from_date = add_days(filters.from_date, -1)
columns, data = get_columns(filters), get_data(filters)
return columns, data
-
+
+
def get_data(filters):
data = []
-
+
asset_categories = get_asset_categories(filters)
assets = get_assets(filters)
- asset_costs = get_asset_costs(assets, filters)
- asset_depreciations = get_accumulated_depreciations(assets, filters)
-
+
for asset_category in asset_categories:
row = frappe._dict()
- row.asset_category = asset_category
- row.update(asset_costs.get(asset_category))
+ # row.asset_category = asset_category
+ row.update(asset_category)
+
+ row.cost_as_on_to_date = (flt(row.cost_as_on_from_date) + flt(row.cost_of_new_purchase) -
+ flt(row.cost_of_sold_asset) - flt(row.cost_of_scrapped_asset))
+
+ row.update(next(asset for asset in assets if asset["asset_category"] == asset_category.get("asset_category", "")))
+ row.accumulated_depreciation_as_on_to_date = (flt(row.accumulated_depreciation_as_on_from_date) +
+ flt(row.depreciation_amount_during_the_period) - flt(row.depreciation_eliminated))
+
+ row.net_asset_value_as_on_from_date = (flt(row.cost_as_on_from_date) -
+ flt(row.accumulated_depreciation_as_on_from_date))
+
+ row.net_asset_value_as_on_to_date = (flt(row.cost_as_on_to_date) -
+ flt(row.accumulated_depreciation_as_on_to_date))
- row.cost_as_on_to_date = (flt(row.cost_as_on_from_date) + flt(row.cost_of_new_purchase)
- - flt(row.cost_of_sold_asset) - flt(row.cost_of_scrapped_asset))
-
- row.update(asset_depreciations.get(asset_category))
- row.accumulated_depreciation_as_on_to_date = (flt(row.accumulated_depreciation_as_on_from_date) +
- flt(row.depreciation_amount_during_the_period) - flt(row.depreciation_eliminated))
-
- row.net_asset_value_as_on_from_date = (flt(row.cost_as_on_from_date) -
- flt(row.accumulated_depreciation_as_on_from_date))
-
- row.net_asset_value_as_on_to_date = (flt(row.cost_as_on_to_date) -
- flt(row.accumulated_depreciation_as_on_to_date))
-
data.append(row)
-
+
return data
-
+
+
def get_asset_categories(filters):
- return frappe.db.sql_list("""
- select distinct asset_category from `tabAsset`
- where docstatus=1 and company=%s and purchase_date <= %s
- """, (filters.company, filters.to_date))
-
+ return frappe.db.sql("""
+ SELECT asset_category,
+ ifnull(sum(case when purchase_date < %(from_date)s then
+ case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then
+ gross_purchase_amount
+ else
+ 0
+ end
+ else
+ 0
+ end), 0) as cost_as_on_from_date,
+ ifnull(sum(case when purchase_date >= %(from_date)s then
+ gross_purchase_amount
+ else
+ 0
+ end), 0) as cost_of_new_purchase,
+ ifnull(sum(case when ifnull(disposal_date, 0) != 0
+ and disposal_date >= %(from_date)s
+ and disposal_date <= %(to_date)s then
+ case when status = "Sold" then
+ gross_purchase_amount
+ else
+ 0
+ end
+ else
+ 0
+ end), 0) as cost_of_sold_asset,
+ ifnull(sum(case when ifnull(disposal_date, 0) != 0
+ and disposal_date >= %(from_date)s
+ and disposal_date <= %(to_date)s then
+ case when status = "Scrapped" then
+ gross_purchase_amount
+ else
+ 0
+ end
+ else
+ 0
+ end), 0) as cost_of_scrapped_asset
+ from `tabAsset`
+ where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s
+ group by asset_category
+ """, {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, as_dict=1)
+
+
def get_assets(filters):
return frappe.db.sql("""
- select name, asset_category, purchase_date, gross_purchase_amount, disposal_date, status
- from `tabAsset`
- where docstatus=1 and company=%s and purchase_date <= %s""",
- (filters.company, filters.to_date), as_dict=1)
-
-def get_asset_costs(assets, filters):
- asset_costs = frappe._dict()
- for d in assets:
- asset_costs.setdefault(d.asset_category, frappe._dict({
- "cost_as_on_from_date": 0,
- "cost_of_new_purchase": 0,
- "cost_of_sold_asset": 0,
- "cost_of_scrapped_asset": 0
- }))
-
- costs = asset_costs[d.asset_category]
-
- if getdate(d.purchase_date) < getdate(filters.from_date):
- if not d.disposal_date or getdate(d.disposal_date) >= getdate(filters.from_date):
- costs.cost_as_on_from_date += flt(d.gross_purchase_amount)
- else:
- costs.cost_of_new_purchase += flt(d.gross_purchase_amount)
-
- if d.disposal_date and getdate(d.disposal_date) >= getdate(filters.from_date) \
- and getdate(d.disposal_date) <= getdate(filters.to_date):
- if d.status == "Sold":
- costs.cost_of_sold_asset += flt(d.gross_purchase_amount)
- elif d.status == "Scrapped":
- costs.cost_of_scrapped_asset += flt(d.gross_purchase_amount)
-
- return asset_costs
-
-def get_accumulated_depreciations(assets, filters):
- asset_depreciations = frappe._dict()
- for d in assets:
- asset = frappe.get_doc("Asset", d.name)
-
- if d.asset_category in asset_depreciations:
- asset_depreciations[d.asset_category]['accumulated_depreciation_as_on_from_date'] += asset.opening_accumulated_depreciation
- else:
- asset_depreciations.setdefault(d.asset_category, frappe._dict({
- "accumulated_depreciation_as_on_from_date": asset.opening_accumulated_depreciation,
- "depreciation_amount_during_the_period": 0,
- "depreciation_eliminated_during_the_period": 0
- }))
+ SELECT results.asset_category,
+ sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
+ sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
+ sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
+ from (SELECT a.asset_category,
+ ifnull(sum(a.opening_accumulated_depreciation +
+ case when ds.schedule_date < %(from_date)s and
+ (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
+ ds.depreciation_amount
+ else
+ 0
+ end), 0) as accumulated_depreciation_as_on_from_date,
+ ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
+ and a.disposal_date <= %(to_date)s and ds.schedule_date <= a.disposal_date then
+ ds.depreciation_amount
+ else
+ 0
+ end), 0) as depreciation_eliminated_during_the_period,
- depr = asset_depreciations[d.asset_category]
+ ifnull(sum(case when ds.schedule_date >= %(from_date)s and ds.schedule_date <= %(to_date)s
+ and (ifnull(a.disposal_date, 0) = 0 or ds.schedule_date <= a.disposal_date) then
+ ds.depreciation_amount
+ else
+ 0
+ end), 0) as depreciation_amount_during_the_period
+ from `tabAsset` a, `tabDepreciation Schedule` ds
+ where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and a.name = ds.parent
+ group by a.asset_category
+ union
+ SELECT a.asset_category,
+ ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
+ and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
+ 0
+ else
+ a.opening_accumulated_depreciation
+ end), 0) as accumulated_depreciation_as_on_from_date,
+ ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
+ a.opening_accumulated_depreciation
+ else
+ 0
+ end), 0) as depreciation_eliminated_during_the_period,
+ 0 as depreciation_amount_during_the_period
+ from `tabAsset` a
+ where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s
+ and not exists(select * from `tabDepreciation Schedule` ds where a.name = ds.parent)
+ group by a.asset_category) as results
+ group by results.asset_category
+ """, {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, as_dict=1)
- if not asset.schedules: # if no schedule,
- if asset.disposal_date:
- # and disposal is NOT within the period, then opening accumulated depreciation not included
- if getdate(asset.disposal_date) < getdate(filters.from_date) or getdate(asset.disposal_date) > getdate(filters.to_date):
- asset_depreciations[d.asset_category]['accumulated_depreciation_as_on_from_date'] = 0
- # if no schedule, and disposal is within period, accumulated dep is the amount eliminated
- if getdate(asset.disposal_date) >= getdate(filters.from_date) and getdate(asset.disposal_date) <= getdate(filters.to_date):
- depr.depreciation_eliminated_during_the_period += asset.opening_accumulated_depreciation
-
- for schedule in asset.get("schedules"):
- if getdate(schedule.schedule_date) < getdate(filters.from_date):
- if not asset.disposal_date or getdate(asset.disposal_date) >= getdate(filters.from_date):
- depr.accumulated_depreciation_as_on_from_date += flt(schedule.depreciation_amount)
- elif getdate(schedule.schedule_date) <= getdate(filters.to_date):
- if not asset.disposal_date:
- depr.depreciation_amount_during_the_period += flt(schedule.depreciation_amount)
- else:
- if getdate(schedule.schedule_date) <= getdate(asset.disposal_date):
- depr.depreciation_amount_during_the_period += flt(schedule.depreciation_amount)
-
- if asset.disposal_date and getdate(asset.disposal_date) >= getdate(filters.from_date) and getdate(asset.disposal_date) <= getdate(filters.to_date):
- if getdate(schedule.schedule_date) <= getdate(asset.disposal_date):
- depr.depreciation_eliminated_during_the_period += flt(schedule.depreciation_amount)
-
- return asset_depreciations
-
def get_columns(filters):
return [
{
From fee8340f9896a4884f077445924ea1a17e475843 Mon Sep 17 00:00:00 2001
From: Mitchy25 <42224026+Mitchy25@users.noreply.github.com>
Date: Wed, 25 Dec 2019 01:32:18 +1300
Subject: [PATCH 24/30] fix: Bank Reconciliation Allows to Over Reconcile
(#19461)
* Update bank_reconciliation.py
* fix: improve error message while over reconciling
* fix: only check over-reconciliation when against payment entry
Co-authored-by: Saqib
---
.../page/bank_reconciliation/bank_reconciliation.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py
index bd4b4d7e0b1..69f9907a8d8 100644
--- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py
+++ b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py
@@ -18,6 +18,10 @@ def reconcile(bank_transaction, payment_doctype, payment_name):
account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
gl_entry = frappe.get_doc("GL Entry", dict(account=account, voucher_type=payment_doctype, voucher_no=payment_name))
+ if payment_doctype == "Payment Entry" and payment_entry.unallocated_amount > transaction.unallocated_amount:
+ frappe.throw(_("The unallocated amount of Payment Entry {0} \
+ is greater than the Bank Transaction's unallocated amount").format(payment_name))
+
if transaction.unallocated_amount == 0:
frappe.throw(_("This bank transaction is already fully reconciled"))
@@ -373,4 +377,4 @@ def sales_invoices_query(doctype, txt, searchfield, start, page_len, filters):
'start': start,
'page_len': page_len
}
- )
\ No newline at end of file
+ )
From dd42dbc6a34cec136d8a1f500bcc3f3807150784 Mon Sep 17 00:00:00 2001
From: Khushal Trivedi
Date: Tue, 24 Dec 2019 18:07:09 +0530
Subject: [PATCH 25/30] fix: date validation on student form, instructor
duplicate fix on student grp, instructor with same employee id fix (#20072)
* fix: date validation on inpatient record, else condition removing on clinical prcd templ which is not req
* fix:Pricing Rule error AttributeError: 'str' object has no attribute 'get' #19770
* fix:Pricing Rule error AttributeError: 'str' object has no attribute 'get' #19770
* fix: joining and relieving Date can be on same date as valid use case
* fix-education: date of birth validation
* fix:Sibling child table filtering for duplacacy on student form
* fix:Sibling child table filtering for duplacacy on student form
* fix:Sibling child table filtering for duplacacy on student form
* fix: date validation on student form, instructor duplicacy fix on student grp, instructor with same employee id fix
* fix: date validation on student form, instructor duplicacy fix on student grp, instructor with same employee id fix
* fix: Exclude current record while validating duplicate employee
Co-authored-by: Nabin Hait
---
erpnext/education/doctype/instructor/instructor.py | 9 +++++++++
erpnext/education/doctype/student/student.py | 3 +++
.../education/doctype/student_group/student_group.js | 12 ++++++++++++
3 files changed, 24 insertions(+)
diff --git a/erpnext/education/doctype/instructor/instructor.py b/erpnext/education/doctype/instructor/instructor.py
index 0756b5f01a4..28df2fcdc11 100644
--- a/erpnext/education/doctype/instructor/instructor.py
+++ b/erpnext/education/doctype/instructor/instructor.py
@@ -22,3 +22,12 @@ class Instructor(Document):
self.name = self.employee
elif naming_method == 'Full Name':
self.name = self.instructor_name
+
+ def validate(self):
+ self.validate_duplicate_employee()
+
+ def validate_duplicate_employee(self):
+ if self.employee and frappe.db.get_value("Instructor", {'employee': self.employee, 'name': ['!=', self.name]}, 'name'):
+ frappe.throw(_("Employee ID is linked with another instructor"))
+
+
diff --git a/erpnext/education/doctype/student/student.py b/erpnext/education/doctype/student/student.py
index 8e4b4e16f9a..99c4c0e9089 100644
--- a/erpnext/education/doctype/student/student.py
+++ b/erpnext/education/doctype/student/student.py
@@ -25,6 +25,9 @@ class Student(Document):
if self.date_of_birth and getdate(self.date_of_birth) >= getdate(today()):
frappe.throw(_("Date of Birth cannot be greater than today."))
+ if self.joining_date and self.date_of_leaving and getdate(self.joining_date) > getdate(self.date_of_leaving):
+ frappe.throw(_("Joining Date can not be greater than Leaving Date"))
+
def update_student_name_in_linked_doctype(self):
linked_doctypes = get_linked_doctypes("Student")
for d in linked_doctypes:
diff --git a/erpnext/education/doctype/student_group/student_group.js b/erpnext/education/doctype/student_group/student_group.js
index c29c134843e..4165ce0f2e7 100644
--- a/erpnext/education/doctype/student_group/student_group.js
+++ b/erpnext/education/doctype/student_group/student_group.js
@@ -122,3 +122,15 @@ frappe.ui.form.on("Student Group", {
}
}
});
+
+frappe.ui.form.on('Student Group Instructor', {
+ instructors_add: function(frm){
+ frm.fields_dict['instructors'].grid.get_field('instructor').get_query = function(doc){
+ let instructor_list = [];
+ $.each(doc.instructors, function(idx, val){
+ instructor_list.push(val.instructor);
+ });
+ return { filters: [['Instructor', 'name', 'not in', instructor_list]] };
+ };
+ }
+});
\ No newline at end of file
From df3b4e48fe32a96f0e7e710b87f0c42306c894ea Mon Sep 17 00:00:00 2001
From: DeeMysterio
Date: Tue, 24 Dec 2019 18:10:18 +0530
Subject: [PATCH 26/30] fix(adress): get the address title at the top and
mandatory in display (#20074)
---
erpnext/public/js/templates/address_list.html | 31 +++++++++----------
1 file changed, 15 insertions(+), 16 deletions(-)
diff --git a/erpnext/public/js/templates/address_list.html b/erpnext/public/js/templates/address_list.html
index 2379ef6b487..0f967b67a0f 100644
--- a/erpnext/public/js/templates/address_list.html
+++ b/erpnext/public/js/templates/address_list.html
@@ -1,23 +1,22 @@
{% for(var i=0, l=addr_list.length; i
-
- {%= i+1 %}. {%= addr_list[i].address_type!="Other" ? __(addr_list[i].address_type) : addr_list[i].address_title %}
- {% if(addr_list[i].is_primary_address) { %}
- ({%= __("Primary") %}){% } %}
- {% if(addr_list[i].is_shipping_address) { %}
- ({%= __("Shipping") %}){% } %}
+
+
+ {%= i+1 %}. {%= addr_list[i].address_title %}{% if(addr_list[i].address_type!="Other") { %}
+ ({%= __(addr_list[i].address_type) %}){% } %}
+ {% if(addr_list[i].is_primary_address) { %}
+ ({%= __("Primary") %}){% } %}
+ {% if(addr_list[i].is_shipping_address) { %}
+ ({%= __("Shipping") %}){% } %}
-
- {%= __("Edit") %}
-
-
{%= addr_list[i].display %}
-
+
+ {%= __("Edit") %}
+
+ {%= addr_list[i].display %}
+
{% } %}
{% if(!addr_list.length) { %}
{%= __("No address added yet.") %}
{% } %}
-
-
+
\ No newline at end of file
From 737267204430483db1bd138a69faa5398725f1c5 Mon Sep 17 00:00:00 2001
From: Rohan
Date: Tue, 24 Dec 2019 18:19:58 +0530
Subject: [PATCH 27/30] fix: pull serial numbers linked to batches + pull
warehouse correctly (develop) (#19022)
* fix: pull serial numbers according to set warehouses
* fix: handle purchase returns
---
erpnext/public/js/controllers/transaction.js | 34 +++++++++++++++++--
.../js/utils/serial_no_batch_selector.js | 2 ++
2 files changed, 34 insertions(+), 2 deletions(-)
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 6ca095869f7..3b907daa1b5 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1808,14 +1808,44 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
}
});
-erpnext.show_serial_batch_selector = function(frm, d, callback, on_close, show_dialog) {
+erpnext.show_serial_batch_selector = function (frm, d, callback, on_close, show_dialog) {
+ let warehouse, receiving_stock, existing_stock;
+ if (frm.doc.is_return) {
+ if (["Purchase Receipt", "Purchase Invoice"].includes(frm.doc.doctype)) {
+ existing_stock = true;
+ warehouse = d.warehouse;
+ } else if (["Delivery Note", "Sales Invoice"].includes(frm.doc.doctype)) {
+ receiving_stock = true;
+ }
+ } else {
+ if (frm.doc.doctype == "Stock Entry") {
+ if (frm.doc.purpose == "Material Receipt") {
+ receiving_stock = true;
+ } else {
+ existing_stock = true;
+ warehouse = d.s_warehouse;
+ }
+ } else {
+ existing_stock = true;
+ warehouse = d.warehouse;
+ }
+ }
+
+ if (!warehouse) {
+ if (receiving_stock) {
+ warehouse = ["like", ""];
+ } else if (existing_stock) {
+ warehouse = ["!=", ""];
+ }
+ }
+
frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", function() {
new erpnext.SerialNoBatchSelector({
frm: frm,
item: d,
warehouse_details: {
type: "Warehouse",
- name: d.warehouse
+ name: warehouse
},
callback: callback,
on_close: on_close
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index 41a59d0af5d..61a693933f6 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -389,12 +389,14 @@ erpnext.SerialNoBatchSelector = Class.extend({
let serial_no_filters = {
item_code: me.item_code,
+ batch_no: this.doc.batch_no || null,
delivery_document_no: ""
}
if (me.warehouse_details.name) {
serial_no_filters['warehouse'] = me.warehouse_details.name;
}
+
return [
{fieldtype: 'Section Break', label: __('Serial Numbers')},
{
From 6e8a9286c28aa32997df2ac7756a9f070dd208a9 Mon Sep 17 00:00:00 2001
From: rohitwaghchaure
Date: Tue, 24 Dec 2019 18:53:32 +0530
Subject: [PATCH 28/30] fix: multiple stock entry issues for the work order
(#18686)
* fix: multiple stock entry issues for the work order
* Update work_order.py
* Update work_order.py
Co-authored-by: Nabin Hait
---
.../doctype/work_order/work_order.py | 2 +-
.../stock/doctype/stock_entry/stock_entry.py | 31 ++++++++++---------
2 files changed, 17 insertions(+), 16 deletions(-)
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index c4238accac7..ff4ebfe8be5 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -856,4 +856,4 @@ def create_pick_list(source_name, target_doc=None, for_qty=None):
doc.set_item_locations()
- return doc
\ No newline at end of file
+ return doc
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 1b9660e6d2c..47f6cf64f53 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -808,24 +808,26 @@ class StockEntry(StockController):
if self.bom_no:
+ backflush_based_on = frappe.db.get_single_value("Manufacturing Settings",
+ "backflush_raw_materials_based_on")
+
if self.purpose in ["Material Issue", "Material Transfer", "Manufacture", "Repack",
"Send to Subcontractor", "Material Transfer for Manufacture", "Material Consumption for Manufacture"]:
if self.work_order and self.purpose == "Material Transfer for Manufacture":
- item_dict = self.get_pending_raw_materials()
+ item_dict = self.get_pending_raw_materials(backflush_based_on)
if self.to_warehouse and self.pro_doc:
for item in itervalues(item_dict):
item["to_warehouse"] = self.pro_doc.wip_warehouse
self.add_to_stock_entry_detail(item_dict)
elif (self.work_order and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture")
- and not self.pro_doc.skip_transfer and frappe.db.get_single_value("Manufacturing Settings",
- "backflush_raw_materials_based_on")== "Material Transferred for Manufacture"):
+ and not self.pro_doc.skip_transfer and backflush_based_on == "Material Transferred for Manufacture"):
self.get_transfered_raw_materials()
- elif self.work_order and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") and \
- frappe.db.get_single_value("Manufacturing Settings", "backflush_raw_materials_based_on")== "BOM" and \
- frappe.db.get_single_value("Manufacturing Settings", "material_consumption")== 1:
+ elif (self.work_order and backflush_based_on== "BOM" and
+ (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture")
+ and frappe.db.get_single_value("Manufacturing Settings", "material_consumption")== 1):
self.get_unconsumed_raw_materials()
else:
@@ -1034,10 +1036,6 @@ class StockEntry(StockController):
filters={'parent': self.work_order, 'item_code': item_code},
fields=["required_qty", "consumed_qty"]
)
- if not req_items:
- frappe.msgprint(_("Did not found transfered item {0} in Work Order {1}, the item not added in Stock Entry")
- .format(item_code, self.work_order))
- continue
req_qty = flt(req_items[0].required_qty)
req_qty_each = flt(req_qty / manufacturing_qty)
@@ -1085,18 +1083,20 @@ class StockEntry(StockController):
}
})
- def get_pending_raw_materials(self):
+ def get_pending_raw_materials(self, backflush_based_on=None):
"""
issue (item quantity) that is pending to issue or desire to transfer,
whichever is less
"""
- item_dict = self.get_pro_order_required_items()
+ item_dict = self.get_pro_order_required_items(backflush_based_on)
+
max_qty = flt(self.pro_doc.qty)
for item, item_details in iteritems(item_dict):
pending_to_issue = flt(item_details.required_qty) - flt(item_details.transferred_qty)
desire_to_transfer = flt(self.fg_completed_qty) * flt(item_details.required_qty) / max_qty
- if desire_to_transfer <= pending_to_issue:
+ if (desire_to_transfer <= pending_to_issue or
+ (desire_to_transfer > 0 and backflush_based_on == "Material Transferred for Manufacture")):
item_dict[item]["qty"] = desire_to_transfer
elif pending_to_issue > 0:
item_dict[item]["qty"] = pending_to_issue
@@ -1114,7 +1114,7 @@ class StockEntry(StockController):
return item_dict
- def get_pro_order_required_items(self):
+ def get_pro_order_required_items(self, backflush_based_on=None):
item_dict = frappe._dict()
pro_order = frappe.get_doc("Work Order", self.work_order)
if not frappe.db.get_value("Warehouse", pro_order.wip_warehouse, "is_group"):
@@ -1123,7 +1123,8 @@ class StockEntry(StockController):
wip_warehouse = None
for d in pro_order.get("required_items"):
- if (flt(d.required_qty) > flt(d.transferred_qty) and
+ if ( ((flt(d.required_qty) > flt(d.transferred_qty)) or
+ (backflush_based_on == "Material Transferred for Manufacture")) and
(d.include_item_in_manufacturing or self.purpose != "Material Transfer for Manufacture")):
item_row = d.as_dict()
if d.source_warehouse and not frappe.db.get_value("Warehouse", d.source_warehouse, "is_group"):
From 8c4cf12c933d09846fa470aa316684bbf7fd235a Mon Sep 17 00:00:00 2001
From: "Parth J. Kharwar"
Date: Wed, 25 Dec 2019 15:14:52 +0530
Subject: [PATCH 29/30] Setting preferred driver email in delivery trip
(#19832)
* fix: add driver's preferred contact email in delivery trip
* fix: modify driver's preferred email patch and blank field checks
* fix: patch file fix
* fix: patch changes to improve speed
* fix: removal of conflicts
Co-authored-by: Nabin Hait
---
erpnext/hr/doctype/employee/employee.py | 6 ++++++
erpnext/patches.txt | 1 +
.../v12_0/set_employee_preferred_emails.py | 16 ++++++++++++++++
.../doctype/delivery_trip/delivery_trip.js | 17 ++++++++++++++++-
.../doctype/delivery_trip/delivery_trip.json | 11 ++++++++++-
.../doctype/delivery_trip/delivery_trip.py | 6 ++++++
6 files changed, 55 insertions(+), 2 deletions(-)
create mode 100644 erpnext/patches/v12_0/set_employee_preferred_emails.py
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index 242531bd177..4d49503d2dc 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -164,6 +164,12 @@ class Employee(NestedSet):
if self.personal_email:
validate_email_address(self.personal_email, True)
+ def set_preferred_email(self):
+ preferred_email_field = frappe.scrub(self.prefered_contact_email)
+ if preferred_email_field:
+ preferred_email = self.get(preferred_email_field)
+ self.prefered_email = preferred_email
+
def validate_status(self):
if self.status == 'Left':
reports_to = frappe.db.get_all('Employee',
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index ab8e942ba79..89be499f6f3 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -649,6 +649,7 @@ erpnext.patches.v12_0.add_export_type_field_in_party_master
erpnext.patches.v12_0.remove_denied_leaves_from_leave_ledger
erpnext.patches.v12_0.update_price_or_product_discount
erpnext.patches.v12_0.set_production_capacity_in_workstation
+erpnext.patches.v12_0.set_employee_preferred_emails
erpnext.patches.v12_0.set_against_blanket_order_in_sales_and_purchase_order
erpnext.patches.v12_0.set_cost_center_in_child_table_of_expense_claim
erpnext.patches.v12_0.set_lead_title_field
diff --git a/erpnext/patches/v12_0/set_employee_preferred_emails.py b/erpnext/patches/v12_0/set_employee_preferred_emails.py
new file mode 100644
index 00000000000..27635612f98
--- /dev/null
+++ b/erpnext/patches/v12_0/set_employee_preferred_emails.py
@@ -0,0 +1,16 @@
+import frappe
+
+
+def execute():
+ employees = frappe.get_all("Employee",
+ filters={"prefered_email": ""},
+ fields=["name", "prefered_contact_email", "company_email", "personal_email", "user_id"])
+
+ for employee in employees:
+ preferred_email_field = frappe.scrub(employee.prefered_contact_email)
+
+ if not preferred_email_field:
+ continue
+
+ preferred_email = employee.get(preferred_email_field)
+ frappe.db.set_value("Employee", employee.name, "prefered_email", preferred_email, update_modified=False)
diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.js b/erpnext/stock/doctype/delivery_trip/delivery_trip.js
index 6a7eecfba63..a025f06711b 100755
--- a/erpnext/stock/doctype/delivery_trip/delivery_trip.js
+++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.js
@@ -79,6 +79,21 @@ frappe.ui.form.on('Delivery Trip', {
}, () => {
frm.reload_doc();
});
+ },
+
+ driver: function (frm) {
+ if (frm.doc.driver) {
+ frappe.call({
+ method: "erpnext.stock.doctype.delivery_trip.delivery_trip.get_driver_email",
+ args: {
+ driver: frm.doc.driver
+ },
+ callback: (data) => {
+ frm.set_value("driver_email", data.message.email);
+ }
+ });
+ };
+ },
},
@@ -196,4 +211,4 @@ frappe.ui.form.on('Delivery Stop', {
frappe.model.set_value(cdt, cdn, "customer_contact", "");
}
}
-});
\ No newline at end of file
+});
diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.json b/erpnext/stock/doctype/delivery_trip/delivery_trip.json
index 0a526243bc4..1bacf465fa0 100644
--- a/erpnext/stock/doctype/delivery_trip/delivery_trip.json
+++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "naming_series:",
"creation": "2017-10-16 16:45:48.293335",
"doctype": "DocType",
@@ -13,6 +14,7 @@
"section_break_3",
"driver",
"driver_name",
+ "driver_email",
"driver_address",
"total_distance",
"uom",
@@ -167,10 +169,17 @@
"fieldtype": "Link",
"label": "Driver Address",
"options": "Address"
+ },
+ {
+ "fieldname": "driver_email",
+ "fieldtype": "Data",
+ "label": "Driver Email",
+ "read_only": 1
}
],
"is_submittable": 1,
- "modified": "2019-09-27 15:43:01.975139",
+ "links": [],
+ "modified": "2019-12-06 17:06:59.681952",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Trip",
diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py
index 77d322ed28d..e2c5b91f514 100644
--- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py
+++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py
@@ -387,3 +387,9 @@ def get_attachments(delivery_stop):
file_name="Delivery Note", print_format=dispatch_attachment)
return [attachments]
+
+@frappe.whitelist()
+def get_driver_email(driver):
+ employee = frappe.db.get_value("Driver", driver, "employee")
+ email = frappe.db.get_value("Employee", employee, "prefered_email")
+ return {"email": email}
From e2ea11c43721f752c6d46f8e0bc48f2318f7c72b Mon Sep 17 00:00:00 2001
From: Himanshu
Date: Wed, 25 Dec 2019 15:15:35 +0530
Subject: [PATCH 30/30] fix: remove quote (#20076)
---
erpnext/stock/doctype/batch/batch.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index 114925469b5..0524eee2d53 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -270,5 +270,5 @@ def get_batches(item_code, warehouse, qty=1, throw=False):
where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s
and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL)
group by batch_id
- order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC'
+ order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC
""", (item_code, warehouse), as_dict=True)
\ No newline at end of file