diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 0a72d4fa4e6..c6de6410ebc 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -89,7 +89,7 @@ class Account(NestedSet): throw(_("Root cannot be edited."), RootNotEditable) if not self.parent_account and not self.is_group: - frappe.throw(_("Root Account must be a group")) + frappe.throw(_("The root account {0} must be a group").format(frappe.bold(self.name))) def validate_root_company_and_sync_account_to_children(self): # ignore validation while creating new compnay or while syncing to child companies diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 2ffa27f6d99..100bb1d3e38 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -178,7 +178,8 @@ def filter_pricing_rules(args, pricing_rules, doc=None): if pricing_rules[0].mixed_conditions and doc: stock_qty, amount, items = get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args) - pricing_rules[0].apply_rule_on_other_items = items + for pricing_rule_args in pricing_rules: + pricing_rule_args.apply_rule_on_other_items = items elif pricing_rules[0].is_cumulative: items = [args.get(frappe.scrub(pr_doc.get('apply_on')))] @@ -329,9 +330,9 @@ def get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args): if pr_doc.mixed_conditions: amt = args.get('qty') * args.get("price_list_rate") if args.get("item_code") != row.get("item_code"): - amt = row.get('qty') * row.get("price_list_rate") + amt = row.get('qty') * (row.get("price_list_rate") or args.get("rate")) - sum_qty += row.get("stock_qty") or args.get("stock_qty") + sum_qty += row.get("stock_qty") or args.get("stock_qty") or args.get("qty") sum_amt += amt if pr_doc.is_cumulative: diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 9c974260a25..db3f72ada08 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -754,8 +754,7 @@ { "fieldname": "manufacturer_part_no", "fieldtype": "Data", - "label": "Manufacturer Part Number", - "read_only": 1 + "label": "Manufacturer Part Number" }, { "depends_on": "is_fixed_asset", @@ -777,7 +776,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-04-01 14:20:17.297284", + "modified": "2020-04-07 18:34:35.104178", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 82aedb62479..3c40112ae6f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -440,11 +440,12 @@ class SalesInvoice(SellingController): if pos.get("company_address"): self.company_address = pos.get("company_address") - customer_price_list, customer_group = frappe.get_value("Customer", self.customer, ['default_price_list', 'customer_group']) - - customer_group_price_list = frappe.get_value("Customer Group", customer_group, 'default_price_list') - - selling_price_list = customer_price_list or customer_group_price_list or pos.get('selling_price_list') + if self.customer: + customer_price_list, customer_group = frappe.get_value("Customer", self.customer, ['default_price_list', 'customer_group']) + customer_group_price_list = frappe.get_value("Customer Group", customer_group, 'default_price_list') + selling_price_list = customer_price_list or customer_group_price_list or pos.get('selling_price_list') + else: + selling_price_list = pos.get('selling_price_list') if selling_price_list: self.set('selling_price_list', selling_price_list) diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 6768dfabfea..e37e1dd99d8 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -702,8 +702,7 @@ { "fieldname": "manufacturer_part_no", "fieldtype": "Data", - "label": "Manufacturer Part Number", - "read_only": 1 + "label": "Manufacturer Part Number" }, { "default": "0", @@ -723,7 +722,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2019-12-06 13:17:12.142799", + "modified": "2020-04-07 18:35:17.558928", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json index 7d7d6f4d3db..b50e834ec73 100644 --- a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json +++ b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-05-22 12:43:10", "doctype": "DocType", @@ -522,8 +523,7 @@ { "fieldname": "manufacturer_part_no", "fieldtype": "Data", - "label": "Manufacturer Part Number", - "read_only": 1 + "label": "Manufacturer Part Number" }, { "fieldname": "column_break_15", @@ -532,7 +532,8 @@ ], "idx": 1, "istable": 1, - "modified": "2019-06-02 05:32:46.019237", + "links": [], + "modified": "2020-04-07 18:35:51.175947", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation Item", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index d95753df900..4045250c330 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1123,36 +1123,39 @@ def get_supplier_block_status(party_name): } return info -def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_code): +def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item): """ Returns a Sales Order Item child item containing the default values """ p_doc = frappe.get_doc(parent_doctype, parent_doctype_name) child_item = frappe.new_doc('Sales Order Item', p_doc, child_docname) - item = frappe.get_doc("Item", item_code) + item = frappe.get_doc("Item", trans_item.get('item_code')) child_item.item_code = item.item_code child_item.item_name = item.item_name child_item.description = item.description - child_item.reqd_by_date = p_doc.delivery_date + child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date child_item.uom = item.stock_uom - child_item.conversion_factor = get_conversion_factor(item_code, item.stock_uom).get("conversion_factor") or 1.0 + child_item.conversion_factor = get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0 child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) + if not child_item.warehouse: + frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.") + .format(frappe.bold("default warehouse"), frappe.bold(item.item_code))) return child_item -def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_code): +def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item): """ Returns a Purchase Order Item child item containing the default values """ p_doc = frappe.get_doc(parent_doctype, parent_doctype_name) child_item = frappe.new_doc('Purchase Order Item', p_doc, child_docname) - item = frappe.get_doc("Item", item_code) + item = frappe.get_doc("Item", trans_item.get('item_code')) child_item.item_code = item.item_code child_item.item_name = item.item_name child_item.description = item.description - child_item.schedule_date = p_doc.schedule_date + child_item.schedule_date = trans_item.get('schedule_date') or p_doc.schedule_date child_item.uom = item.stock_uom - child_item.conversion_factor = get_conversion_factor(item_code, item.stock_uom).get("conversion_factor") or 1.0 + child_item.conversion_factor = get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0 child_item.base_rate = 1 # Initiallize value will update in parent validation child_item.base_amount = 1 # Initiallize value will update in parent validation return child_item @@ -1196,9 +1199,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil if not d.get("docname"): new_child_flag = True if parent_doctype == "Sales Order": - child_item = set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, d.get("item_code")) + child_item = set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, d) if parent_doctype == "Purchase Order": - child_item = set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, d.get("item_code")) + child_item = set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, d) else: child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname")) if flt(child_item.get("rate")) == flt(d.get("rate")) and flt(child_item.get("qty")) == flt(d.get("qty")): @@ -1243,6 +1246,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil child_item.flags.ignore_validate_update_after_submit = True if new_child_flag: + parent.load_from_db() child_item.idx = len(parent.items) + 1 child_item.insert() else: diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 95e661aa221..4e568e2795a 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -667,8 +667,7 @@ def get_itemised_tax_breakup_html(doc): itemised_tax=itemised_tax, itemised_taxable_amount=itemised_taxable_amount, tax_accounts=tax_accounts, - conversion_rate=doc.conversion_rate, - currency=doc.currency + doc=doc ) ) diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 5e640e78c8a..1b071ea1b70 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -336,3 +336,27 @@ def make_opportunity_from_communication(communication, ignore_communication_link link_communication_to_document(doc, "Opportunity", opportunity.name, ignore_communication_links) return opportunity.name +@frappe.whitelist() +def get_events(start, end, filters=None): + """Returns events for Gantt / Calendar view rendering. + :param start: Start date-time. + :param end: End date-time. + :param filters: Filters (JSON). + """ + from frappe.desk.calendar import get_event_conditions + conditions = get_event_conditions("Opportunity", filters) + + data = frappe.db.sql(""" + select + distinct `tabOpportunity`.name, `tabOpportunity`.customer_name, `tabOpportunity`.opportunity_amount, + `tabOpportunity`.title, `tabOpportunity`.contact_date + from + `tabOpportunity` + where + (`tabOpportunity`.contact_date between %(start)s and %(end)s) + {conditions} + """.format(conditions=conditions), { + "start": start, + "end": end + }, as_dict=True, update={"allDay": 0}) + return data \ No newline at end of file diff --git a/erpnext/crm/doctype/opportunity/opportunity_calendar.js b/erpnext/crm/doctype/opportunity/opportunity_calendar.js new file mode 100644 index 00000000000..58fa2b8cd86 --- /dev/null +++ b/erpnext/crm/doctype/opportunity/opportunity_calendar.js @@ -0,0 +1,19 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +// License: GNU General Public License v3. See license.txt +frappe.views.calendar["Opportunity"] = { + field_map: { + "start": "contact_date", + "end": "contact_date", + "id": "name", + "title": "customer_name", + "allDay": "allDay" + }, + options: { + header: { + left: 'prev,next today', + center: 'title', + right: 'month' + } + }, + get_events_method: 'erpnext.crm.doctype.opportunity.opportunity.get_events' +} diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 2f07e15f1e3..6b6d1e22581 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -347,6 +347,8 @@ get_site_info = 'erpnext.utilities.get_site_info' payment_gateway_enabled = "erpnext.accounts.utils.create_payment_gateway_account" +communication_doctypes = ["Customer", "Supplier"] + regional_overrides = { 'France': { 'erpnext.tests.test_regional.test_method': 'erpnext.regional.france.utils.test_method' diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py index 189b2f5afff..0c1578ffef5 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py @@ -58,18 +58,16 @@ class TestLoanDisbursement(unittest.TestCase): process_loan_interest_accrual_for_demand_loans(posting_date=add_days(last_date, 1)) - # Paid 511095.89 amount includes 5,00,000 principal amount and 11095.89 interest amount + # Should not be able to create loan disbursement entry before repayment + self.assertRaises(frappe.ValidationError, make_loan_disbursement_entry, loan.name, + 500000, first_date) + repayment_entry = create_repayment_entry(loan.name, self.applicant, add_days(get_last_day(nowdate()), 5), "Regular Payment", 611095.89) - repayment_entry.submit() + repayment_entry.submit() loan.reload() + # After repayment loan disbursement entry should go through make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 16)) - total_principal_paid = loan.total_principal_paid - - loan.reload() - - # Loan Topup will result in decreasing the Total Principal Paid - self.assertEqual(flt(loan.total_principal_paid, 2), flt(total_principal_paid - 500000, 2)) diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json index a3525db9a56..51c5cb98a63 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type.json +++ b/erpnext/loan_management/doctype/loan_type/loan_type.json @@ -119,6 +119,7 @@ "label": "Penalty Interest Rate (%) Per Day" }, { + "description": "No. of days from due date until which penalty won't be charged in case of delay in loan repayment", "fieldname": "grace_period_in_days", "fieldtype": "Int", "label": "Grace Period in Days" @@ -142,7 +143,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-02-03 05:03:00.334813", + "modified": "2020-04-15 00:24:43.259963", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Type", diff --git a/erpnext/patches/v12_0/update_healthcare_refactored_changes.py b/erpnext/patches/v12_0/update_healthcare_refactored_changes.py index 02378e09223..d06c5713d23 100644 --- a/erpnext/patches/v12_0/update_healthcare_refactored_changes.py +++ b/erpnext/patches/v12_0/update_healthcare_refactored_changes.py @@ -100,7 +100,7 @@ def execute(): for entry in encounter_details: doc = frappe.get_doc('Patient Encounter', entry.name) - symptoms = entry.symptoms.split('\n') + symptoms = entry.symptoms.split('\n') if entry.symptoms else [] for symptom in symptoms: if not frappe.db.exists('Complaint', symptom): frappe.get_doc({ @@ -112,7 +112,7 @@ def execute(): }) row.db_update() - diagnosis = entry.diagnosis.split('\n') + diagnosis = entry.diagnosis.split('\n') if entry.diagnosis else [] for d in diagnosis: if not frappe.db.exists('Diagnosis', d): frappe.get_doc({ diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 27a9de95e0e..afbdbc661d3 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -379,7 +379,31 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ } }); } - } + }, + + manufacturer_part_no: function(doc, cdt, cdn) { + const row = locals[cdt][cdn]; + + if (row.manufacturer_part_no) { + frappe.model.get_value('Item Manufacturer', + { + 'item_code': row.item_code, + 'manufacturer': row.manufacturer, + 'manufacturer_part_no': row.manufacturer_part_no + }, + 'name', + function(data) { + if (!data) { + let msg = { + message: __("Manufacturer Part Number {0} is invalid", [row.manufacturer_part_no]), + title: __("Invalid Part Number") + } + frappe.throw(msg); + } + }); + + } + } }); cur_frm.add_fetch('project', 'cost_center', 'cost_center'); diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 0c63c33f7ab..8e8c48feb0d 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -559,7 +559,13 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }, () => me.conversion_factor(doc, cdt, cdn, true), - () => me.remove_pricing_rule(item) + () => me.remove_pricing_rule(item), + () => { + if (item.apply_rule_on_other_items) { + let key = item.name; + me.apply_rule_on_other_items({key: item}); + } + } ]); } } @@ -1394,20 +1400,22 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ apply_rule_on_other_items: function(args) { const me = this; - const fields = ["discount_percentage", "discount_amount", "rate"]; + const fields = ["discount_percentage", "pricing_rules", "discount_amount", "rate"]; for(var k in args) { let data = args[k]; - me.frm.doc.items.forEach(d => { - if (in_list(data.apply_rule_on_other_items, d[data.apply_rule_on])) { - for(var k in data) { - if (in_list(fields, k)) { - frappe.model.set_value(d.doctype, d.name, k, data[k]); + if (data && data.apply_rule_on_other_items) { + me.frm.doc.items.forEach(d => { + if (in_list(data.apply_rule_on_other_items, d[data.apply_rule_on])) { + for(var k in data) { + if (in_list(fields, k) && data[k]) { + frappe.model.set_value(d.doctype, d.name, k, data[k]); + } } } - } - }); + }); + } } }, diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 4d44eae086e..58969f2a9fd 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -436,6 +436,44 @@ erpnext.utils.update_child_items = function(opts) { const cannot_add_row = (typeof opts.cannot_add_row === 'undefined') ? true : opts.cannot_add_row; const child_docname = (typeof opts.cannot_add_row === 'undefined') ? "items" : opts.child_docname; this.data = []; + const fields = [{ + fieldtype:'Data', + fieldname:"docname", + read_only: 1, + hidden: 1, + }, { + fieldtype:'Link', + fieldname:"item_code", + options: 'Item', + in_list_view: 1, + read_only: 0, + disabled: 0, + label: __('Item Code') + }, { + fieldtype:'Float', + fieldname:"qty", + default: 0, + read_only: 0, + in_list_view: 1, + label: __('Qty') + }, { + fieldtype:'Currency', + fieldname:"rate", + default: 0, + read_only: 0, + in_list_view: 1, + label: __('Rate') + }]; + + if (frm.doc.doctype == 'Sales Order' || frm.doc.doctype == 'Purchase Order' ) { + fields.splice(2, 0, { + fieldtype: 'Date', + fieldname: frm.doc.doctype == 'Sales Order' ? "delivery_date" : "schedule_date", + in_list_view: 1, + label: frm.doc.doctype == 'Sales Order' ? __("Delivery Date") : __("Reqd by date") + }) + } + const dialog = new frappe.ui.Dialog({ title: __("Update Items"), fields: [ @@ -450,34 +488,7 @@ erpnext.utils.update_child_items = function(opts) { get_data: () => { return this.data; }, - fields: [{ - fieldtype:'Data', - fieldname:"docname", - read_only: 1, - hidden: 1, - }, { - fieldtype:'Link', - fieldname:"item_code", - options: 'Item', - in_list_view: 1, - read_only: 0, - disabled: 0, - label: __('Item Code') - }, { - fieldtype:'Float', - fieldname:"qty", - default: 0, - read_only: 0, - in_list_view: 1, - label: __('Qty') - }, { - fieldtype:'Currency', - fieldname:"rate", - default: 0, - read_only: 0, - in_list_view: 1, - label: __('Rate') - }] + fields: fields }, ], primary_action: function() { @@ -506,6 +517,8 @@ erpnext.utils.update_child_items = function(opts) { "docname": d.name, "name": d.name, "item_code": d.item_code, + "delivery_date": d.delivery_date, + "schedule_date": d.schedule_date, "qty": d.qty, "rate": d.rate, }); diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index df487833a8c..f175687f26c 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -1176,7 +1176,7 @@ class POSCart { return `