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