From 4d8340d7b996561e4e871510644355d34c5513b7 Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 13 Aug 2020 11:34:05 +0530 Subject: [PATCH 01/71] feat: enable total row in Gross Profit Report --- .../report/gross_profit/gross_profit.json | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.json b/erpnext/accounts/report/gross_profit/gross_profit.json index 9cfb0627d30..cd6bac2d77d 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.json +++ b/erpnext/accounts/report/gross_profit/gross_profit.json @@ -1,24 +1,23 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2013-02-25 17:03:34", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2017-02-24 20:12:22.464240", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Gross Profit", - "owner": "Administrator", - "ref_doctype": "Sales Invoice", - "report_name": "Gross Profit", - "report_type": "Script Report", + "add_total_row": 1, + "creation": "2013-02-25 17:03:34", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 3, + "is_standard": "Yes", + "modified": "2020-08-13 11:26:39.112352", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Gross Profit", + "owner": "Administrator", + "ref_doctype": "Sales Invoice", + "report_name": "Gross Profit", + "report_type": "Script Report", "roles": [ { "role": "Accounts Manager" - }, + }, { "role": "Accounts User" } From c28e37781a424793eec33a65b5ae1fa1389927e2 Mon Sep 17 00:00:00 2001 From: prssanna Date: Tue, 11 Aug 2020 16:50:31 +0530 Subject: [PATCH 02/71] fix: add company in list fields to fetch for Expense Claim --- erpnext/hr/doctype/expense_claim/expense_claim_list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/expense_claim/expense_claim_list.js b/erpnext/hr/doctype/expense_claim/expense_claim_list.js index 6195ad414a1..9bafc185628 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim_list.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim_list.js @@ -1,5 +1,5 @@ frappe.listview_settings['Expense Claim'] = { - add_fields: ["total_claimed_amount", "docstatus"], + add_fields: ["total_claimed_amount", "docstatus", "company"], get_indicator: function(doc) { if(doc.status == "Paid") { return [__("Paid"), "green", "status,=,Paid"]; From 2cdc0c3f2d2c3e89759c1c0f75fda2be288c9599 Mon Sep 17 00:00:00 2001 From: aakvatech <35020381+aakvatech@users.noreply.github.com> Date: Fri, 14 Aug 2020 10:26:54 +0300 Subject: [PATCH 03/71] fix: AttributeError: 'ProgramFee' object has no attribute 'course' Error occurs while program enrollment on new student. This happens on version 12 and not on version 13. Version 12 uses course_list = [course.course for course in program.get_all_children()] where as version 13 uses course_list = [course.course for course in program.courses] erpnext/erpnext/education/doctype/program_enrollment/program_enrollment.py --- .../education/doctype/program_enrollment/program_enrollment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment.py b/erpnext/education/doctype/program_enrollment/program_enrollment.py index d5348ffd067..75361728917 100644 --- a/erpnext/education/doctype/program_enrollment/program_enrollment.py +++ b/erpnext/education/doctype/program_enrollment/program_enrollment.py @@ -71,7 +71,7 @@ class ProgramEnrollment(Document): def create_course_enrollments(self): student = frappe.get_doc("Student", self.student) program = frappe.get_doc("Program", self.program) - course_list = [course.course for course in program.get_all_children()] + course_list = [course.course for course in program.courses] for course_name in course_list: student.enroll_in_course(course_name=course_name, program_enrollment=self.name) From 4b6d5ef6aa2fb9d54eb40f6ba452c764f7d0286a Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Mon, 17 Aug 2020 12:25:34 +0530 Subject: [PATCH 04/71] fix: the JSON object must be str, bytes or bytearray, not "list" (#23053) --- erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py index f28a07431fe..88e1055beb4 100644 --- a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py +++ b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py @@ -27,4 +27,4 @@ def get_vouchar_detials(column_list, doctype, docname): for col in column_list: sanitize_searchfield(col) return frappe.db.sql(''' select {columns} from `tab{doctype}` where name=%s''' - .format(columns=", ".join(json.loads(column_list)), doctype=doctype), docname, as_dict=1)[0] + .format(columns=", ".join(column_list), doctype=doctype), docname, as_dict=1)[0] From df42df52467ee0e1f062b3d8071b8e6ca4c92bda Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Mon, 17 Aug 2020 13:48:07 +0530 Subject: [PATCH 05/71] fix: handled condition if staffing isn't created (#23057) Co-authored-by: Marica --- erpnext/hr/doctype/job_offer/job_offer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/job_offer/job_offer.py b/erpnext/hr/doctype/job_offer/job_offer.py index cfb275b1f75..5b2640f727f 100644 --- a/erpnext/hr/doctype/job_offer/job_offer.py +++ b/erpnext/hr/doctype/job_offer/job_offer.py @@ -21,8 +21,12 @@ class JobOffer(Document): check_vacancies = frappe.get_single("HR Settings").check_vacancies if staffing_plan and check_vacancies: job_offers = self.get_job_offer(staffing_plan.from_date, staffing_plan.to_date) - if staffing_plan.vacancies - len(job_offers) <= 0: - frappe.throw(_("There are no vacancies under staffing plan {0}").format(frappe.bold(get_link_to_form("Staffing Plan", staffing_plan.parent)))) + if not staffing_plan.get("vacancies") or staffing_plan.vacancies - len(job_offers) <= 0: + error_variable = 'for ' + frappe.bold(self.designation) + if staffing_plan.get("parent"): + error_variable = frappe.bold(get_link_to_form("Staffing Plan", staffing_plan.parent)) + + frappe.throw(_("There are no vacancies under staffing plan {0}").format(error_variable)) def on_change(self): update_job_applicant(self.status, self.job_applicant) From 09b6628e536321cd139052554cfadf6a16807b74 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Tue, 18 Aug 2020 14:43:34 +0530 Subject: [PATCH 06/71] fix: Leave application status (#23043) * fix: leave application status update fix * fix:adding patch --- .../doctype/leave_application/leave_application.json | 7 +++---- erpnext/patches.txt | 1 + .../patches/v12_0/update_leave_application_status.py | 10 ++++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 erpnext/patches/v12_0/update_leave_application_status.py diff --git a/erpnext/hr/doctype/leave_application/leave_application.json b/erpnext/hr/doctype/leave_application/leave_application.json index 460be514b59..9cc9c87f7f8 100644 --- a/erpnext/hr/doctype/leave_application/leave_application.json +++ b/erpnext/hr/doctype/leave_application/leave_application.json @@ -1,5 +1,4 @@ { - "actions": [], "allow_import": 1, "autoname": "naming_series:", "creation": "2013-02-20 11:18:11", @@ -167,6 +166,7 @@ "fieldtype": "Column Break" }, { + "allow_on_submit": 1, "default": "Open", "fieldname": "status", "fieldtype": "Select", @@ -246,9 +246,8 @@ "icon": "fa fa-calendar", "idx": 1, "is_submittable": 1, - "links": [], "max_attachments": 3, - "modified": "2020-03-10 22:40:43.487721", + "modified": "2020-08-13 17:22:44.832397", "modified_by": "Administrator", "module": "HR", "name": "Leave Application", @@ -333,4 +332,4 @@ "sort_order": "DESC", "timeline_field": "employee", "title_field": "employee_name" -} +} \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 51d18e83e99..831541ccb1f 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -676,3 +676,4 @@ erpnext.patches.v12_0.move_due_advance_amount_to_pending_amount erpnext.patches.v12_0.set_multi_uom_in_rfq erpnext.patches.v12_0.update_state_code_for_daman_and_diu erpnext.patches.v12_0.rename_lost_reason_detail +erpnext.patches.v12_0.update_leave_application_status diff --git a/erpnext/patches/v12_0/update_leave_application_status.py b/erpnext/patches/v12_0/update_leave_application_status.py new file mode 100644 index 00000000000..261a3a5c0cb --- /dev/null +++ b/erpnext/patches/v12_0/update_leave_application_status.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + # frappe.reload_doc('HR', 'doctype', 'leave_application') + frappe.db.sql(""" + UPDATE `tabLeave Application` SET + status = 'Cancelled' + WHERE status = 'Approved' and docstatus = 2 + """) \ No newline at end of file From dae1fad1a6a4788cf34e8094925c78848d2a16fb Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 18 Aug 2020 19:32:38 +0530 Subject: [PATCH 07/71] fix: Total calculations for multicurrency RCM invoices (#23070) --- erpnext/regional/india/utils.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 66fe3f63881..086dc9dcb6c 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import frappe, re, json from frappe import _ +import erpnext from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words from erpnext.regional.india import states, state_numbers from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount @@ -663,20 +664,26 @@ def update_grand_total_for_rcm(doc, method): gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ + gst_accounts.get('igst_account') + base_gst_tax = 0 gst_tax = 0 + for tax in doc.get('taxes'): if tax.category not in ("Total", "Valuation and Total"): continue if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list: - gst_tax += tax.base_tax_amount_after_discount_amount + base_gst_tax += tax.base_tax_amount_after_discount_amount + gst_tax += tax.tax_amount_after_discount_amount doc.taxes_and_charges_added -= gst_tax doc.total_taxes_and_charges -= gst_tax + doc.base_taxes_and_charges_added -= base_gst_tax + doc.base_total_taxes_and_charges -= base_gst_tax - update_totals(gst_tax, doc) + update_totals(gst_tax, base_gst_tax, doc) -def update_totals(gst_tax, doc): +def update_totals(gst_tax, base_gst_tax, doc): + doc.base_grand_total -= base_gst_tax doc.grand_total -= gst_tax if doc.meta.get_field("rounded_total"): @@ -692,6 +699,7 @@ def update_totals(gst_tax, doc): doc.outstanding_amount = doc.rounded_total or doc.grand_total doc.in_words = money_in_words(doc.grand_total, doc.currency) + doc.base_in_words = money_in_words(doc.base_grand_total, erpnext.get_company_currency(doc.company)) doc.set_payment_schedule() def make_regional_gl_entries(gl_entries, doc): From 1bf9de1527597a92da44f506d3938ac63bca3902 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 19 Aug 2020 15:13:30 +0530 Subject: [PATCH 08/71] feat: JSON download for HSN wise outward summary --- .../hsn_wise_summary_of_outward_supplies.js | 23 +++++ .../hsn_wise_summary_of_outward_supplies.py | 95 +++++++++++++++++-- 2 files changed, 108 insertions(+), 10 deletions(-) diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.js b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.js index dfdf9dc0958..b757d53aa23 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.js +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.js @@ -46,5 +46,28 @@ frappe.query_reports["HSN-wise-summary of outward supplies"] = { ], onload: (report) => { fetch_gstins(report); + + report.page.add_inner_button(__("Download JSON"), function () { + var filters = report.get_values(); + + frappe.call({ + method: 'erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies.get_json', + args: { + data: report.data, + report_name: report.report_name, + filters: filters + }, + callback: function(r) { + if (r.message) { + const args = { + cmd: 'erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies.download_json_file', + data: r.message.data, + report_name: r.message.report_name + }; + open_url_post(frappe.request.url, args); + } + } + }); + }); } }; diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index a3ed4cebb12..6f3fff29323 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -4,11 +4,13 @@ from __future__ import unicode_literals import frappe, erpnext from frappe import _ -from frappe.utils import flt +from frappe.utils import flt, getdate, cstr from frappe.model.meta import get_field_precision from frappe.utils.xlsxutils import handle_html from six import iteritems import json +from erpnext.regional.india.utils import get_gst_accounts +from erpnext.regional.report.gstr_1.gstr_1 import get_company_gstin_number def execute(filters=None): return _execute(filters) @@ -141,7 +143,7 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic tax_details = frappe.db.sql(""" select - parent, description, item_wise_tax_detail, + parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount from `tab%s` where @@ -153,11 +155,11 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic """ % (tax_doctype, '%s', ', '.join(['%s']*len(invoice_item_row)), conditions), tuple([doctype] + list(invoice_item_row))) - for parent, description, item_wise_tax_detail, tax_amount in tax_details: - description = handle_html(description) - if description not in tax_columns and tax_amount: + for parent, account_head, item_wise_tax_detail, tax_amount in tax_details: + + if account_head not in tax_columns and tax_amount: # as description is text editor earlier and markup can break the column convention in reports - tax_columns.append(description) + tax_columns.append(account_head) if item_wise_tax_detail: try: @@ -175,17 +177,17 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic for d in item_row_map.get(parent, {}).get(item_code, []): item_tax_amount = tax_amount if item_tax_amount: - itemised_tax.setdefault((parent, item_code), {})[description] = frappe._dict({ + itemised_tax.setdefault((parent, item_code), {})[account_head] = frappe._dict({ "tax_amount": flt(item_tax_amount, tax_amount_precision) }) except ValueError: continue tax_columns.sort() - for desc in tax_columns: + for account_head in tax_columns: columns.append({ - "label": desc, - "fieldname": frappe.scrub(desc), + "label": account_head, + "fieldname": frappe.scrub(account_head), "fieldtype": "Float", "width": 110 }) @@ -212,3 +214,76 @@ def get_merged_data(columns, data): return result +@frappe.whitelist() +def get_json(filters, report_name, data): + filters = json.loads(filters) + report_data = json.loads(data) + gstin = filters.get('company_gstin') or get_company_gstin_number(filters["company"]) + + if not filters.get('from_date') or not filters.get('to_date'): + frappe.throw(_("Please enter From Date and To Date to generate JSON")) + + fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) + + gst_json = {"gstin": "", "version": "GST2.3.4", + "hash": "hash", "gstin": gstin, "fp": fp} + + gst_json["hsn"] = { + "data": get_hsn_wise_json_data(filters, report_data) + } + + return { + 'report_name': report_name, + 'data': gst_json + } + +@frappe.whitelist() +def download_json_file(): + ''' download json content in a file ''' + data = frappe._dict(frappe.local.form_dict) + frappe.response['filename'] = frappe.scrub("{0}".format(data['report_name'])) + '.json' + frappe.response['filecontent'] = data['data'] + frappe.response['content_type'] = 'application/json' + frappe.response['type'] = 'download' + +def get_hsn_wise_json_data(filters, report_data): + + filters = frappe._dict(filters) + gst_accounts = get_gst_accounts(filters.company) + data = [] + count = 1 + + for hsn in report_data: + row = { + "num": count, + "hsn_sc": hsn.get("gst_hsn_code"), + "desc": hsn.get("description"), + "uqc": hsn.get("stock_uom").upper(), + "qty": hsn.get("stock_qty"), + "val": flt(hsn.get("total_amount"), 2), + "txval": flt(hsn.get("taxable_amount", 2)), + "iamt": 0.0, + "camt": 0.0, + "samt": 0.0, + "csamt": 0.0 + + } + + for account in gst_accounts.get('igst_account'): + row['iamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + for account in gst_accounts.get('cgst_account'): + row['camt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + for account in gst_accounts.get('sgst_account'): + row['samt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + for account in gst_accounts.get('cess_account'): + row['csamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + data.append(row) + count +=1 + + return data + + From f61a8bc8898e361d10e7ec54165eb30e11d3c2a6 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 19 Aug 2020 16:18:06 +0530 Subject: [PATCH 09/71] fix: Print Language for Customer not set for POS Invoice --- .../page/point_of_sale/point_of_sale.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) 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 0b93324b19d..30790f0a987 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -286,7 +286,7 @@ erpnext.pos.PointOfSale = class PointOfSale { if (in_list(['serial_no', 'batch_no'], field)) { args[field] = value; } - + // add to cur_frm const item = this.frm.add_child('items', args); frappe.flags.hide_serial_batch_dialog = true; @@ -436,7 +436,7 @@ erpnext.pos.PointOfSale = class PointOfSale { set_primary_action_in_modal() { if (!this.frm.msgbox) { this.frm.msgbox = frappe.msgprint( - ` + ` ${__('Print')} ${__('New')}` @@ -445,7 +445,15 @@ erpnext.pos.PointOfSale = class PointOfSale { $(this.frm.msgbox.body).find('.btn-default').on('click', () => { this.frm.msgbox.hide(); this.make_new_invoice(); - }) + }); + + $(this.frm.msgbox.body).find('.btn-primary').on('click', () => { + this.frm.msgbox.hide(); + const frm = this.events.get_frm(); + frm.doc = this.doc; + frm.print_preview.lang_code = frm.doc.language; + frm.print_preview.printit(true); + }); } } @@ -680,7 +688,10 @@ erpnext.pos.PointOfSale = class PointOfSale { if(this.frm.doc.docstatus != 1 ){ await this.frm.save(); } - this.frm.print_preview.printit(true); + const frm = this.events.get_frm(); + frm.doc = this.doc; + frm.print_preview.lang_code = frm.doc.language; + frm.print_preview.printit(true); }); } if(this.frm.doc.items.length == 0){ From 1c29da32fc695974e06f60be67dd93ade8d2ab0b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 19 Aug 2020 18:30:18 +0530 Subject: [PATCH 10/71] fix: Do not update total for RCM invvoices if net taxes are zero --- erpnext/regional/india/utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 086dc9dcb6c..1968076c53d 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -659,6 +659,9 @@ def update_grand_total_for_rcm(doc, method): if country != 'India': return + if not doc.total_taxes_and_charges: + return + if doc.reverse_charge == 'Y': gst_accounts = get_gst_accounts(doc.company) gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ @@ -706,7 +709,10 @@ def make_regional_gl_entries(gl_entries, doc): country = frappe.get_cached_value('Company', doc.company, 'country') if country != 'India': - return + return gl_entries + + if not doc.total_taxes_and_charges: + return gl_entries if doc.reverse_charge == 'Y': gst_accounts = get_gst_accounts(doc.company) From bf7adb8b38d8f9bd10592a4fad45e6ccaadd7a58 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Thu, 20 Aug 2020 11:17:30 +0530 Subject: [PATCH 11/71] refactor: Format and sanitise user inputs to search queries. (#23064) * refactor: Sanitize whitelisted method inputs Co-authored-by: Prssanna Desai Co-authored-by: Shivam Mishra * refactor: Format and sanitize tax_account_query inputs Co-authored-by: Nabin Hait Co-authored-by: Prssanna Desai Co-authored-by: Shivam Mishra * refactor: Validate and sanitize search inputs via decorator Co-authored-by: Nabin Hait Co-authored-by: Prssanna Desai Co-authored-by: Shivam Mishra * style: Minor formatting fix * refactor: Validate and sanitize search inputs using decorator * fix: Typo * fix: Remove unwanted import statement * refactor: Repalce validate_and_sanitize_search_inputs() with validate_and_sanitize_search_inputs Co-authored-by: Prssanna Desai Co-authored-by: Shivam Mishra Co-authored-by: Prssanna Desai Co-authored-by: Shivam Mishra Co-authored-by: Nabin Hait --- erpnext/accounts/doctype/account/account.py | 2 + .../doctype/journal_entry/journal_entry.py | 32 +++++++-- .../doctype/payment_order/payment_order.py | 2 + .../doctype/pos_profile/pos_profile.py | 1 + .../doctype/pricing_rule/pricing_rule.py | 12 ++-- .../bank_reconciliation.py | 3 + .../asset_maintenance/asset_maintenance.py | 1 + .../asset_maintenance_log.py | 1 + .../request_for_quotation.py | 1 + erpnext/controllers/queries.py | 72 ++++++++++++++----- .../program_enrollment/program_enrollment.py | 2 + .../doctype/student_group/student_group.py | 1 + .../healthcare_practitioner.py | 1 + .../inpatient_record/inpatient_record.py | 1 + .../department_approver.py | 1 + .../employee_benefit_application.py | 1 + .../hr/doctype/payroll_entry/payroll_entry.py | 1 + .../doctype/work_order/work_order.py | 1 + .../bom_variance_report.py | 5 +- erpnext/projects/doctype/project/project.py | 1 + erpnext/projects/doctype/task/task.py | 1 + .../projects/doctype/timesheet/timesheet.py | 1 + erpnext/projects/utils.py | 1 + erpnext/selling/doctype/customer/customer.py | 2 + .../doctype/product_bundle/product_bundle.py | 1 + .../doctype/sales_order/sales_order.py | 1 + .../page/point_of_sale/point_of_sale.py | 1 + .../setup/doctype/party_type/party_type.py | 1 + .../item_alternative/item_alternative.py | 1 + .../material_request/material_request.py | 1 + .../doctype/packing_slip/packing_slip.py | 1 + .../quality_inspection/quality_inspection.py | 2 + 32 files changed, 123 insertions(+), 33 deletions(-) diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index c6de6410ebc..164f120067f 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -244,6 +244,8 @@ class Account(NestedSet): super(Account, self).on_trash(True) +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_parent_account(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql("""select name from tabAccount where is_group = 1 and docstatus != 2 and company = %s diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 5d0c67f277a..d8a045b4db7 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -837,13 +837,33 @@ def get_opening_accounts(company): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_against_jv(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql("""select jv.name, jv.posting_date, jv.user_remark - from `tabJournal Entry` jv, `tabJournal Entry Account` jv_detail - where jv_detail.parent = jv.name and jv_detail.account = %s and ifnull(jv_detail.party, '') = %s - and (jv_detail.reference_type is null or jv_detail.reference_type = '') - and jv.docstatus = 1 and jv.`{0}` like %s order by jv.name desc limit %s, %s""".format(searchfield), - (filters.get("account"), cstr(filters.get("party")), "%{0}%".format(txt), start, page_len)) + if not frappe.db.has_column('Journal Entry', searchfield): + return [] + + return frappe.db.sql(""" + SELECT jv.name, jv.posting_date, jv.user_remark + FROM `tabJournal Entry` jv, `tabJournal Entry Account` jv_detail + WHERE jv_detail.parent = jv.name + AND jv_detail.account = %(account)s + AND IFNULL(jv_detail.party, '') = %(party)s + AND ( + jv_detail.reference_type IS NULL + OR jv_detail.reference_type = '' + ) + AND jv.docstatus = 1 + AND jv.`{0}` LIKE %(txt)s + ORDER BY jv.name DESC + LIMIT %(offset)s, %(limit)s + """.format(searchfield), dict( + account=filters.get("account"), + party=cstr(filters.get("party")), + txt="%{0}%".format(txt), + offset=start, + limit=page_len + ) + ) @frappe.whitelist() diff --git a/erpnext/accounts/doctype/payment_order/payment_order.py b/erpnext/accounts/doctype/payment_order/payment_order.py index 4702e58cef1..e5880aa67a8 100644 --- a/erpnext/accounts/doctype/payment_order/payment_order.py +++ b/erpnext/accounts/doctype/payment_order/payment_order.py @@ -27,6 +27,7 @@ class PaymentOrder(Document): frappe.db.set_value(self.payment_order_type, d.get(frappe.scrub(self.payment_order_type)), ref_field, status) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_mop_query(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(""" select mode_of_payment from `tabPayment Order Reference` where parent = %(parent)s and mode_of_payment like %(txt)s @@ -38,6 +39,7 @@ def get_mop_query(doctype, txt, searchfield, start, page_len, filters): }) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_supplier_query(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(""" select supplier from `tabPayment Order Reference` where parent = %(parent)s and supplier like %(txt)s and diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index f1869671ae9..ed1e09e31b6 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -116,6 +116,7 @@ def get_series(): return frappe.get_meta("Sales Invoice").get_field("naming_series").options or "" @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def pos_profile_query(doctype, txt, searchfield, start, page_len, filters): user = frappe.session['user'] company = filters.get('company') or frappe.defaults.get_user_default('company') diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index bb4f5bc602c..0c2b5475cb8 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -437,14 +437,14 @@ def make_pricing_rule(doctype, docname): return doc @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_item_uoms(doctype, txt, searchfield, start, page_len, filters): items = [filters.get('value')] if filters.get('apply_on') != 'Item Code': field = frappe.scrub(filters.get('apply_on')) + items = [d.name for d in frappe.db.get_all("Item", filters={field: filters.get('value')})] - items = frappe.db.sql_list("""select name - from `tabItem` where {0} = %s""".format(field), filters.get('value')) - - return frappe.get_all('UOM Conversion Detail', - filters = {'parent': ('in', items), 'uom': ("like", "{0}%".format(txt))}, - fields = ["distinct uom"], as_list=1) + return frappe.get_all('UOM Conversion Detail', filters={ + 'parent': ('in', items), + 'uom': ("like", "{0}%".format(txt)) + }, fields = ["distinct uom"], as_list=1) diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py index 2a6384a3fcd..b4fffec7d40 100644 --- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py +++ b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py @@ -286,6 +286,7 @@ def get_matching_transactions_payments(description_matching): return [] @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def payment_entry_query(doctype, txt, searchfield, start, page_len, filters): account = frappe.db.get_value("Bank Account", filters.get("bank_account"), "account") if not account: @@ -315,6 +316,7 @@ def payment_entry_query(doctype, txt, searchfield, start, page_len, filters): ) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def journal_entry_query(doctype, txt, searchfield, start, page_len, filters): account = frappe.db.get_value("Bank Account", filters.get("bank_account"), "account") @@ -351,6 +353,7 @@ def journal_entry_query(doctype, txt, searchfield, start, page_len, filters): ) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def sales_invoices_query(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(""" SELECT diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index d6adde6a371..8a954b94d1e 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -106,6 +106,7 @@ def update_maintenance_log(asset_maintenance, item_code, item_name, task): maintenance_log.save() @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_team_members(doctype, txt, searchfield, start, page_len, filters): return frappe.db.get_values('Maintenance Team Member', { 'parent': filters.get("maintenance_team") }) diff --git a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py index dab4e468680..34facd8d050 100644 --- a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py +++ b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py @@ -41,6 +41,7 @@ class AssetMaintenanceLog(Document): asset_maintenance_doc.save() @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_maintenance_tasks(doctype, txt, searchfield, start, page_len, filters): asset_maintenance_tasks = frappe.db.get_values('Asset Maintenance Task', {'parent':filters.get("asset_maintenance")}, 'maintenance_task') return asset_maintenance_tasks diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 4b852300e5f..b54a585b97f 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -207,6 +207,7 @@ def get_list_context(context=None): return list_context @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_supplier_contacts(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql("""select `tabContact`.name from `tabContact`, `tabDynamic Link` where `tabDynamic Link`.link_doctype = 'Supplier' and (`tabDynamic Link`.link_name=%(name)s diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 8d7779c42bb..b49198579b8 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -12,6 +12,7 @@ from frappe.utils import unique # searches for active employees @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def employee_query(doctype, txt, searchfield, start, page_len, filters): conditions = [] fields = get_fields("Employee", ["name", "employee_name"]) @@ -42,6 +43,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): # searches for leads which are not converted @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def lead_query(doctype, txt, searchfield, start, page_len, filters): fields = get_fields("Lead", ["name", "lead_name", "company_name"]) @@ -72,6 +74,7 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters): # searches for customer @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def customer_query(doctype, txt, searchfield, start, page_len, filters): conditions = [] cust_master_name = frappe.defaults.get_user_default("cust_master_name") @@ -110,8 +113,10 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): # searches for supplier @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def supplier_query(doctype, txt, searchfield, start, page_len, filters): supp_master_name = frappe.defaults.get_user_default("supp_master_name") + if supp_master_name == "Supplier Name": fields = ["name", "supplier_group"] else: @@ -142,32 +147,49 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def tax_account_query(doctype, txt, searchfield, start, page_len, filters): company_currency = erpnext.get_company_currency(filters.get('company')) - tax_accounts = frappe.db.sql("""select name, parent_account from tabAccount - where tabAccount.docstatus!=2 - and account_type in (%s) - and is_group = 0 - and company = %s - and account_currency = %s - and `%s` LIKE %s - order by idx desc, name - limit %s, %s""" % - (", ".join(['%s']*len(filters.get("account_type"))), "%s", "%s", searchfield, "%s", "%s", "%s"), - tuple(filters.get("account_type") + [filters.get("company"), company_currency, "%%%s%%" % txt, - start, page_len])) + def get_accounts(with_account_type_filter): + account_type_condition = '' + if with_account_type_filter: + account_type_condition = "AND account_type in %(account_types)s" + + accounts = frappe.db.sql(""" + SELECT name, parent_account + FROM `tabAccount` + WHERE `tabAccount`.docstatus!=2 + {account_type_condition} + AND is_group = 0 + AND company = %(company)s + AND account_currency = %(currency)s + AND `{searchfield}` LIKE %(txt)s + ORDER BY idx DESC, name + LIMIT %(offset)s, %(limit)s + """.format(account_type_condition=account_type_condition, searchfield=searchfield), + dict( + account_types=filters.get("account_type"), + company=filters.get("company"), + currency=company_currency, + txt="%{}%".format(txt), + offset=start, + limit=page_len + ) + ) + + return accounts + + tax_accounts = get_accounts(True) + if not tax_accounts: - tax_accounts = frappe.db.sql("""select name, parent_account from tabAccount - where tabAccount.docstatus!=2 and is_group = 0 - and company = %s and account_currency = %s and `%s` LIKE %s limit %s, %s""" #nosec - % ("%s", "%s", searchfield, "%s", "%s", "%s"), - (filters.get("company"), company_currency, "%%%s%%" % txt, start, page_len)) + tax_accounts = get_accounts(False) return tax_accounts @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): conditions = [] @@ -215,7 +237,6 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals idx desc, name, item_name limit %(start)s, %(page_len)s """.format( - key=searchfield, columns=columns, scond=searchfields, fcond=get_filters_cond(doctype, filters, conditions).replace('%', '%%'), @@ -231,6 +252,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def bom(doctype, txt, searchfield, start, page_len, filters): conditions = [] fields = get_fields("BOM", ["name", "item"]) @@ -258,6 +280,7 @@ def bom(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_project_name(doctype, txt, searchfield, start, page_len, filters): cond = '' if filters.get('customer'): @@ -285,6 +308,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters, as_dict): fields = get_fields("Delivery Note", ["name", "customer", "posting_date"]) @@ -315,6 +339,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_batch_no(doctype, txt, searchfield, start, page_len, filters): cond = "" if filters.get("posting_date"): @@ -373,6 +398,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_account_list(doctype, txt, searchfield, start, page_len, filters): filter_list = [] @@ -395,8 +421,8 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters): fields = ["name", "parent_account"], limit_start=start, limit_page_length=page_len, as_list=True) - @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql("""select distinct bo.name, bo.blanket_order_type, bo.to_date from `tabBlanket Order` bo, `tabBlanket Order Item` boi @@ -413,6 +439,7 @@ def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_income_account(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond @@ -439,6 +466,7 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_expense_account(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond @@ -463,6 +491,7 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def warehouse_query(doctype, txt, searchfield, start, page_len, filters): # Should be used when item code is passed in filters. conditions, bin_conditions = [], [] @@ -506,6 +535,7 @@ def get_doctype_wise_filters(filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_batch_numbers(doctype, txt, searchfield, start, page_len, filters): query = """select batch_id from `tabBatch` where disabled = 0 @@ -519,6 +549,7 @@ def get_batch_numbers(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def item_manufacturer_query(doctype, txt, searchfield, start, page_len, filters): item_filters = [ ['manufacturer', 'like', '%' + txt + '%'], @@ -537,6 +568,7 @@ def item_manufacturer_query(doctype, txt, searchfield, start, page_len, filters) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_purchase_receipts(doctype, txt, searchfield, start, page_len, filters): query = """ select pr.name @@ -551,6 +583,7 @@ def get_purchase_receipts(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters): query = """ select pi.name @@ -565,6 +598,7 @@ def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_tax_template(doctype, txt, searchfield, start, page_len, filters): item_doc = frappe.get_cached_doc('Item', filters.get('item_code')) diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment.py b/erpnext/education/doctype/program_enrollment/program_enrollment.py index 75361728917..3e27670d05d 100644 --- a/erpnext/education/doctype/program_enrollment/program_enrollment.py +++ b/erpnext/education/doctype/program_enrollment/program_enrollment.py @@ -97,6 +97,7 @@ class ProgramEnrollment(Document): return quiz_progress @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_program_courses(doctype, txt, searchfield, start, page_len, filters): if filters.get('program'): return frappe.db.sql("""select course, course_name from `tabProgram Course` @@ -115,6 +116,7 @@ def get_program_courses(doctype, txt, searchfield, start, page_len, filters): }) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_students(doctype, txt, searchfield, start, page_len, filters): if not filters.get("academic_term"): filters["academic_term"] = frappe.defaults.get_defaults().academic_term diff --git a/erpnext/education/doctype/student_group/student_group.py b/erpnext/education/doctype/student_group/student_group.py index aba1b5ff5fd..54b32a843f8 100644 --- a/erpnext/education/doctype/student_group/student_group.py +++ b/erpnext/education/doctype/student_group/student_group.py @@ -106,6 +106,7 @@ def get_program_enrollment(academic_year, academic_term=None, program=None, batc @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def fetch_students(doctype, txt, searchfield, start, page_len, filters): if filters.get("group_based_on") != "Activity": enrolled_students = get_program_enrollment(filters.get('academic_year'), filters.get('academic_term'), diff --git a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py index 40f31016bc4..d07c7962b2f 100644 --- a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py +++ b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py @@ -68,6 +68,7 @@ def validate_service_item(item, msg): frappe.throw(_(msg)) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_practitioner_list(doctype, txt, searchfield, start, page_len, filters=None): fields = ["name", "first_name", "mobile_phone"] diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py index c107cd73350..52c31120bb3 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py @@ -168,6 +168,7 @@ def patient_leave_service_unit(inpatient_record, check_out, leave_from): inpatient_record.save(ignore_permissions = True) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_leave_from(doctype, txt, searchfield, start, page_len, filters): docname = filters['docname'] diff --git a/erpnext/hr/doctype/department_approver/department_approver.py b/erpnext/hr/doctype/department_approver/department_approver.py index df0f75a18c3..70a0aa217f7 100644 --- a/erpnext/hr/doctype/department_approver/department_approver.py +++ b/erpnext/hr/doctype/department_approver/department_approver.py @@ -11,6 +11,7 @@ class DepartmentApprover(Document): pass @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_approvers(doctype, txt, searchfield, start, page_len, filters): if not filters.get("employee"): diff --git a/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.py index fb2fc46cfde..1322e25063f 100644 --- a/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.py +++ b/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.py @@ -224,6 +224,7 @@ def get_benefit_amount_based_on_pro_rata(sal_struct, component_max_benefit): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_earning_components(doctype, txt, searchfield, start, page_len, filters): if len(filters) < 2: return {} diff --git a/erpnext/hr/doctype/payroll_entry/payroll_entry.py b/erpnext/hr/doctype/payroll_entry/payroll_entry.py index 5050f3b3d8c..2a81b32aa9a 100644 --- a/erpnext/hr/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/hr/doctype/payroll_entry/payroll_entry.py @@ -574,6 +574,7 @@ def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progr frappe.msgprint(_("Could not submit some Salary Slips")) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(""" select name from `tabPayroll Entry` diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index d5ca612e485..b0585e5d734 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -563,6 +563,7 @@ class WorkOrder(Document): return bom @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_bom_operations(doctype, txt, searchfield, start, page_len, filters): if txt: filters['operation'] = ('like', '%%%s%%' % txt) diff --git a/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py b/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py index c5627e0c087..982266c3ebd 100644 --- a/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py +++ b/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py @@ -30,7 +30,7 @@ def get_columns(filters): "width": 180 } ]) - + columns.extend([ { "label": _("Finished Good"), @@ -73,7 +73,7 @@ def get_columns(filters): ]) return columns - + def get_data(filters): cond = "1=1" @@ -95,6 +95,7 @@ def get_data(filters): return results @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_work_orders(doctype, txt, searchfield, start, page_len, filters): cond = "1=1" if filters.get('bom_no'): diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 731fece3462..cf5c0fb400f 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -239,6 +239,7 @@ def get_list_context(context=None): } @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_users_for_project(doctype, txt, searchfield, start, page_len, filters): conditions = [] return frappe.db.sql("""select name, concat_ws(' ', first_name, middle_name, last_name) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 1e3a57f40b2..0d403b193f3 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -190,6 +190,7 @@ def check_if_child_exists(name): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_project(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond return frappe.db.sql(""" select name from `tabProject` diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index e90821689bd..2ffec339d7f 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -226,6 +226,7 @@ def get_projectwise_timesheet_data(project, parent=None): and sales_invoice is null""".format(cond), {'project': project, 'parent': parent}, as_dict=1) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_timesheet(doctype, txt, searchfield, start, page_len, filters): if not filters: filters = {} diff --git a/erpnext/projects/utils.py b/erpnext/projects/utils.py index d0d88ebdf06..c39f908e43e 100644 --- a/erpnext/projects/utils.py +++ b/erpnext/projects/utils.py @@ -7,6 +7,7 @@ from __future__ import unicode_literals import frappe @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def query_task(doctype, txt, searchfield, start, page_len, filters): from frappe.desk.reportview import build_match_conditions diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 97c3e20bb8a..1484f6b2290 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -308,6 +308,7 @@ def get_loyalty_programs(doc): return lp_details @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): from erpnext.controllers.queries import get_fields @@ -477,6 +478,7 @@ def make_address(args, is_primary_address=1): return address @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, filters): customer = filters.get('customer') return frappe.db.sql(""" diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index f6ac40927ed..273bf784fad 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -30,6 +30,7 @@ class ProductBundle(Document): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_new_item_code(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index ffb66354fa0..f88289871e9 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -888,6 +888,7 @@ def make_purchase_order(source_name, for_supplier=None, selected_items=[], targe @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_supplier(doctype, txt, searchfield, start, page_len, filters): supp_master_name = frappe.defaults.get_user_default("supp_master_name") if supp_master_name == "Supplier Name": diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index d2a63ae3db3..8e130ba4246 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -168,6 +168,7 @@ def get_item_group_condition(pos_profile): return cond % tuple(item_groups) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def item_group_query(doctype, txt, searchfield, start, page_len, filters): item_groups = [] cond = "1=1" diff --git a/erpnext/setup/doctype/party_type/party_type.py b/erpnext/setup/doctype/party_type/party_type.py index b29c305ee7f..96e60936a4b 100644 --- a/erpnext/setup/doctype/party_type/party_type.py +++ b/erpnext/setup/doctype/party_type/party_type.py @@ -10,6 +10,7 @@ class PartyType(Document): pass @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_party_type(doctype, txt, searchfield, start, page_len, filters): cond = '' if filters and filters.get('account'): diff --git a/erpnext/stock/doctype/item_alternative/item_alternative.py b/erpnext/stock/doctype/item_alternative/item_alternative.py index 2b7be1ce985..204f71a2bb4 100644 --- a/erpnext/stock/doctype/item_alternative/item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/item_alternative.py @@ -43,6 +43,7 @@ class ItemAlternative(Document): frappe.throw(_("Already record exists for the item {0}".format(self.item_code))) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_alternative_items(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(""" (select alternative_item_code from `tabItem Alternative` where item_code = %(item_code)s and alternative_item_code like %(txt)s) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 42a362867b8..1c7cdad48bc 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -386,6 +386,7 @@ def get_material_requests_based_on_supplier(supplier): return material_requests, supplier_items @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_default_supplier_query(doctype, txt, searchfield, start, page_len, filters): doc = frappe.get_doc("Material Request", filters.get("doc")) item_list = [] diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index 4f831d7a858..a7a29cca7f8 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -176,6 +176,7 @@ class PackingSlip(Document): self.update_item_details() @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def item_details(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond return frappe.db.sql("""select name, item_name, description from `tabItem` diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 568e7428765..c3bb5141849 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -59,6 +59,7 @@ class QualityInspection(Document): (quality_inspection, self.modified, self.reference_name, self.item_code)) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def item_query(doctype, txt, searchfield, start, page_len, filters): if filters.get("from"): from frappe.desk.reportview import get_match_cond @@ -88,6 +89,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): {'parent': filters.get('parent'), 'txt': "%%%s%%" % txt}) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def quality_inspection_query(doctype, txt, searchfield, start, page_len, filters): return frappe.get_all('Quality Inspection', limit_start=start, From 5e32ee8457f0f907caa4bdfd2b04c488c67337f5 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 20 Aug 2020 12:11:28 +0530 Subject: [PATCH 12/71] fix: not able to submit delivery note --- .../accounts/doctype/purchase_invoice/purchase_invoice.json | 2 +- .../doctype/sales_invoice_item/sales_invoice_item.json | 2 +- erpnext/controllers/stock_controller.py | 4 ++-- .../stock/doctype/delivery_note_item/delivery_note_item.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index a990e8a9977..fbd4dee4d66 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1307,7 +1307,7 @@ "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, - "modified": "2020-08-03 13:08:19.611710", + "modified": "2020-08-20 11:08:19.611710", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 67c882153d5..7e285113b1c 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -805,7 +805,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-07-18 12:24:41.749986", + "modified": "2020-08-20 11:24:41.749986", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 2272bca49ac..2f275bb3c91 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -94,7 +94,7 @@ class StockController(AccountsController): "account": warehouse_account[sle.warehouse]["account"], "against": item_row.expense_account, "cost_center": item_row.cost_center, - "project": item_row.project or self.get('project'), + "project": item_row.get("project") or self.get("project"), "remarks": self.get("remarks") or "Accounting Entry for Stock", "debit": flt(sle.stock_value_difference, precision), "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No", @@ -105,7 +105,7 @@ class StockController(AccountsController): "account": item_row.expense_account, "against": warehouse_account[sle.warehouse]["account"], "cost_center": item_row.cost_center, - "project": item_row.project or self.get('project'), + "project": item_row.get("project") or self.get("project"), "remarks": self.get("remarks") or "Accounting Entry for Stock", "credit": flt(sle.stock_value_difference, precision), "project": item_row.get("project") or self.get("project"), diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 3e7e05a301f..542d198c946 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -736,7 +736,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-04-28 14:18:33.131672", + "modified": "2020-08-20 11:18:33.131672", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", From 2cc0ad51f5b84bf6d1722c4f113b529252d27e0a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 20 Aug 2020 12:53:43 +0530 Subject: [PATCH 13/71] fix: val is not defined --- erpnext/public/js/utils/serial_no_batch_selector.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index f29e42f8940..7bd0a72e311 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -348,9 +348,9 @@ erpnext.SerialNoBatchSelector = Class.extend({ return row.on_grid_fields_dict.batch_no.get_value(); } }); - if (selected_batches.includes(val)) { + if (selected_batches.includes(batch_no)) { this.set_value(""); - frappe.throw(__(`Batch ${val} already selected.`)); + frappe.throw(__(`Batch ${batch_no} already selected.`)); return; } From 8c5bc5631acd1fbe230a6f9496c54f7058587dcb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 20 Aug 2020 16:31:38 +0530 Subject: [PATCH 14/71] fix: Unable to submit reverse charge invoice --- erpnext/regional/india/utils.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 1968076c53d..88637bb4ec5 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -711,9 +711,6 @@ def make_regional_gl_entries(gl_entries, doc): if country != 'India': return gl_entries - if not doc.total_taxes_and_charges: - return gl_entries - if doc.reverse_charge == 'Y': gst_accounts = get_gst_accounts(doc.company) gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ @@ -723,6 +720,7 @@ def make_regional_gl_entries(gl_entries, doc): if tax.category not in ("Total", "Valuation and Total"): continue + dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list: account_currency = get_account_currency(tax.account_head) @@ -732,8 +730,8 @@ def make_regional_gl_entries(gl_entries, doc): "cost_center": tax.cost_center, "posting_date": doc.posting_date, "against": doc.supplier, - "credit": tax.base_tax_amount_after_discount_amount, - "credits_in_account_currency": tax.base_tax_amount_after_discount_amount \ + dr_or_cr: tax.base_tax_amount_after_discount_amount, + dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount \ if account_currency==doc.company_currency \ else tax.tax_amount_after_discount_amount }, account_currency, item=tax) From 685ca83b5e7601f7d70faabca67a321e8ff116c9 Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 20 Aug 2020 19:11:40 +0530 Subject: [PATCH 15/71] fix: Create Opoortunity without Default Company from Email (#23098) * fix: Create Opoortunity without Default Company from Email * fix: Add Prompt to Select Company * Update communication.js Co-authored-by: Nabin Hait --- .../crm/doctype/opportunity/opportunity.py | 3 +- erpnext/public/js/communication.js | 45 +++++++++++++------ 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 7d3f4028662..8302978e1c1 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -321,7 +321,7 @@ def auto_close_opportunity(): doc.save() @frappe.whitelist() -def make_opportunity_from_communication(communication, ignore_communication_links=False): +def make_opportunity_from_communication(communication, company, ignore_communication_links=False): from erpnext.crm.doctype.lead.lead import make_lead_from_communication doc = frappe.get_doc("Communication", communication) @@ -333,6 +333,7 @@ def make_opportunity_from_communication(communication, ignore_communication_link opportunity = frappe.get_doc({ "doctype": "Opportunity", + "company": company, "opportunity_from": opportunity_from, "party_name": lead }).insert(ignore_permissions=True) diff --git a/erpnext/public/js/communication.js b/erpnext/public/js/communication.js index 9432d421752..26e5ab8b322 100644 --- a/erpnext/public/js/communication.js +++ b/erpnext/public/js/communication.js @@ -7,7 +7,7 @@ frappe.ui.form.on("Communication", { }, setup_custom_buttons: (frm) => { - let confirm_msg = "Are you sure you want to create {0} from this email"; + let confirm_msg = "Are you sure you want to create {0} from this email?"; if(frm.doc.reference_doctype !== "Issue") { frm.add_custom_button(__("Issue"), () => { frappe.confirm(__(confirm_msg, [__("Issue")]), () => { @@ -62,17 +62,36 @@ frappe.ui.form.on("Communication", { }, make_opportunity_from_communication: (frm) => { - return frappe.call({ - method: "erpnext.crm.doctype.opportunity.opportunity.make_opportunity_from_communication", - args: { - communication: frm.doc.name - }, - freeze: true, - callback: (r) => { - if(r.message) { - frm.reload_doc() + const fields = [{ + fieldtype: 'Link', + label: __('Select a Company'), + fieldname: 'company', + options: 'Company', + reqd: 1, + default: frappe.defaults.get_user_default("Company") + }]; + + frappe.prompt(fields, data => { + frappe.call({ + method: "erpnext.crm.doctype.opportunity.opportunity.make_opportunity_from_communication", + args: { + communication: frm.doc.name, + company: data.company + }, + freeze: true, + callback: (r) => { + if(r.message) { + frm.reload_doc(); + frappe.show_alert({ + message: __("Opportunity {0} created", + ['' + r.message + '']), + indicator: 'green' + }); + } } - } - }) + }); + }, + 'Create an Opportunity', + 'Create'); } -}); \ No newline at end of file +}); From fa36a061886da909a96aded0bf0aa0db6f96c654 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 20 Aug 2020 19:38:55 +0530 Subject: [PATCH 16/71] fix:Validate Job offer against vacancies (#23107) --- erpnext/hr/doctype/job_offer/job_offer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/job_offer/job_offer.py b/erpnext/hr/doctype/job_offer/job_offer.py index 5b2640f727f..fc6400d6447 100644 --- a/erpnext/hr/doctype/job_offer/job_offer.py +++ b/erpnext/hr/doctype/job_offer/job_offer.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import frappe +from frappe.utils import cint from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc from frappe import _ @@ -21,7 +22,7 @@ class JobOffer(Document): check_vacancies = frappe.get_single("HR Settings").check_vacancies if staffing_plan and check_vacancies: job_offers = self.get_job_offer(staffing_plan.from_date, staffing_plan.to_date) - if not staffing_plan.get("vacancies") or staffing_plan.vacancies - len(job_offers) <= 0: + if not staffing_plan.get("vacancies") or cint(staffing_plan.vacancies) - len(job_offers) <= 0: error_variable = 'for ' + frappe.bold(self.designation) if staffing_plan.get("parent"): error_variable = frappe.bold(get_link_to_form("Staffing Plan", staffing_plan.parent)) @@ -60,7 +61,7 @@ def get_staffing_plan_detail(designation, company, offer_date): AND %s between sp.from_date and sp.to_date """, (designation, company, offer_date), as_dict=1) - return frappe._dict(detail[0]) if detail else None + return frappe._dict(detail[0]) if (detail and detail[0].parent) else None @frappe.whitelist() def make_employee(source_name, target_doc=None): From 6c6175200cbf5de0f199280914843e94a2fa7c7f Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 21 Aug 2020 15:40:29 +0530 Subject: [PATCH 17/71] fix: cannot search items in offline pos (#23083) --- erpnext/accounts/page/pos/pos.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js index 279ae25bf71..1ed3f2341eb 100755 --- a/erpnext/accounts/page/pos/pos.js +++ b/erpnext/accounts/page/pos/pos.js @@ -1098,8 +1098,8 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ get_items: function (item_code) { // To search item as per the key enter - item_code = unescape(item_code); + item_code = item_code === "undefined" ? undefined : item_code; var me = this; this.item_serial_no = {}; this.item_batch_no = {}; From 572d1509253b56ee14bf0d97010bf7a112727389 Mon Sep 17 00:00:00 2001 From: Wolfram Schmidt Date: Fri, 21 Aug 2020 20:02:29 +0200 Subject: [PATCH 18/71] Update lead_source.json as requested in https://github.com/frappe/erpnext/pull/22872 --- erpnext/selling/doctype/lead_source/lead_source.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/doctype/lead_source/lead_source.json b/erpnext/selling/doctype/lead_source/lead_source.json index 868f6d11d04..373e83af9cf 100644 --- a/erpnext/selling/doctype/lead_source/lead_source.json +++ b/erpnext/selling/doctype/lead_source/lead_source.json @@ -1,7 +1,7 @@ { "allow_copy": 0, "allow_import": 0, - "allow_rename": 0, + "allow_rename": 1, "autoname": "field:source_name", "beta": 0, "creation": "2016-09-16 01:47:47.382372", @@ -74,7 +74,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2016-09-16 02:03:01.441622", + "modified": "2020-09-16 02:03:01.441622", "modified_by": "Administrator", "module": "Selling", "name": "Lead Source", @@ -128,4 +128,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_seen": 0 -} \ No newline at end of file +} From 98fda4b8c303b637cbbcafce3a66923e1fcb56f1 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sat, 22 Aug 2020 19:17:18 +0530 Subject: [PATCH 19/71] fix: GLE for subcontracted PR is fg item rate is zero --- .../purchase_receipt/purchase_receipt.py | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index a150e097d42..be2453373e4 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -223,6 +223,15 @@ class PurchaseReceipt(BuyingController): if not stock_value_diff: continue + + # If PR is sub-contracted and fg item rate is zero + # in that case if account for shource and target warehouse are same, + # then GL entries should not be posted + if flt(stock_value_diff) == flt(d.rm_supp_cost) \ + and warehouse_account.get(self.supplier_warehouse) \ + and warehouse_account[d.warehouse]["account"] == warehouse_account[self.supplier_warehouse]["account"]: + continue + gl_entries.append(self.get_gl_dict({ "account": warehouse_account[d.warehouse]["account"], "against": stock_rbnb, @@ -232,16 +241,17 @@ class PurchaseReceipt(BuyingController): }, warehouse_account[d.warehouse]["account_currency"], item=d)) # stock received but not billed - stock_rbnb_currency = get_account_currency(stock_rbnb) - gl_entries.append(self.get_gl_dict({ - "account": stock_rbnb, - "against": warehouse_account[d.warehouse]["account"], - "cost_center": d.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(d.base_net_amount, d.precision("base_net_amount")), - "credit_in_account_currency": flt(d.base_net_amount, d.precision("base_net_amount")) \ - if stock_rbnb_currency==self.company_currency else flt(d.net_amount, d.precision("net_amount")) - }, stock_rbnb_currency, item=d)) + if d.base_net_amount: + stock_rbnb_currency = get_account_currency(stock_rbnb) + gl_entries.append(self.get_gl_dict({ + "account": stock_rbnb, + "against": warehouse_account[d.warehouse]["account"], + "cost_center": d.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": flt(d.base_net_amount, d.precision("base_net_amount")), + "credit_in_account_currency": flt(d.base_net_amount, d.precision("base_net_amount")) \ + if stock_rbnb_currency==self.company_currency else flt(d.net_amount, d.precision("net_amount")) + }, stock_rbnb_currency, item=d)) negative_expense_to_be_booked += flt(d.item_tax_amount) From a82d9d3182b6da35ab0b1972a2911c77123e421d Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sat, 22 Aug 2020 20:02:27 +0530 Subject: [PATCH 20/71] test: Test case for GLE in subcontracted PR if FG item rate is zero --- .../purchase_receipt/test_purchase_receipt.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 74019c7b3cb..e2b636eaaa6 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -121,6 +121,22 @@ class TestPurchaseReceipt(unittest.TestCase): rm_supp_cost = sum([d.amount for d in pr.get("supplied_items")]) self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2)) + def test_subcontracting_gle_fg_item_rate_zero(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + set_perpetual_inventory() + frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM") + make_stock_entry(item_code="_Test Item", target="Work In Progress - TCP1", qty=100, basic_rate=100, company="_Test Company with perpetual inventory") + make_stock_entry(item_code="_Test Item Home Desktop 100", target="Work In Progress - TCP1", + qty=100, basic_rate=100, company="_Test Company with perpetual inventory") + pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=0, is_subcontracted="Yes", + company="_Test Company with perpetual inventory", warehouse='Stores - TCP1', supplier_warehouse='Work In Progress - TCP1') + + gl_entries = get_gl_entries("Purchase Receipt", pr.name) + + self.assertFalse(gl_entries) + + set_perpetual_inventory(0) + def test_serial_no_supplier(self): pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"), @@ -607,7 +623,7 @@ def make_purchase_receipt(**args): "received_qty": received_qty, "rejected_qty": rejected_qty, "rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "", - "rate": args.rate or 50, + "rate": args.rate if args.rate != None else 50, "conversion_factor": args.conversion_factor or 1.0, "serial_no": args.serial_no, "stock_uom": args.stock_uom or "_Test UOM", From 9b4cc0b6e9a1dcdea21a7884261d9629cf3f7174 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 25 Feb 2020 13:21:16 +0530 Subject: [PATCH 21/71] fix: Update paid amount for pos return (#20543) * fix: Paid amount updation for pos return * fix: Remove console * fix: Styling * fix: get default mode of payment from POS profile * fix: Add test cases * fix: Codacy --- .../doctype/pos_profile/test_pos_profile.py | 28 ++++----- .../sales_invoice/test_sales_invoice.py | 58 +++++++++++++++++++ erpnext/controllers/taxes_and_totals.py | 28 ++++++++- .../public/js/controllers/taxes_and_totals.js | 50 ++++++++++++++-- 4 files changed, 144 insertions(+), 20 deletions(-) diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py index f8d52a78336..83a4e921743 100644 --- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py @@ -29,27 +29,29 @@ class TestPOSProfile(unittest.TestCase): frappe.db.sql("delete from `tabPOS Profile`") -def make_pos_profile(): +def make_pos_profile(**args): frappe.db.sql("delete from `tabPOS Profile`") + args = frappe._dict(args) + pos_profile = frappe.get_doc({ - "company": "_Test Company", - "cost_center": "_Test Cost Center - _TC", - "currency": "INR", + "company": args.company or "_Test Company", + "cost_center": args.cost_center or "_Test Cost Center - _TC", + "currency": args.currency or "INR", "doctype": "POS Profile", - "expense_account": "_Test Account Cost for Goods Sold - _TC", - "income_account": "Sales - _TC", - "name": "_Test POS Profile", + "expense_account": args.expense_account or "_Test Account Cost for Goods Sold - _TC", + "income_account": args.income_account or "Sales - _TC", + "name": args.name or "_Test POS Profile", "naming_series": "_T-POS Profile-", - "selling_price_list": "_Test Price List", - "territory": "_Test Territory", + "selling_price_list": args.selling_price_list or "_Test Price List", + "territory": args.territory or "_Test Territory", "customer_group": frappe.db.get_value('Customer Group', {'is_group': 0}, 'name'), - "warehouse": "_Test Warehouse - _TC", - "write_off_account": "_Test Write Off - _TC", - "write_off_cost_center": "_Test Write Off Cost Center - _TC" + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "write_off_account": args.write_off_account or "_Test Write Off - _TC", + "write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC" }) - if not frappe.db.exists("POS Profile", "_Test POS Profile"): + if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"): pos_profile.insert() return pos_profile diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index f1a2bf7aa0e..26f488d8c9a 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -714,6 +714,64 @@ class TestSalesInvoice(unittest.TestCase): self.pos_gl_entry(si, pos, 50) + def test_pos_returns_without_repayment(self): + pos_profile = make_pos_profile() + + pos = create_sales_invoice(qty = 10, do_not_save=True) + pos.is_pos = 1 + pos.pos_profile = pos_profile.name + + pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500}) + pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500}) + pos.insert() + pos.submit() + + pos_return = create_sales_invoice(is_return=1, + return_against=pos.name, qty=-5, do_not_save=True) + + pos_return.is_pos = 1 + pos_return.pos_profile = pos_profile.name + + pos_return.insert() + pos_return.submit() + + self.assertFalse(pos_return.is_pos) + self.assertFalse(pos_return.get('payments')) + + def test_pos_returns_with_repayment(self): + pos_profile = make_pos_profile() + + pos_profile.append('payments', { + 'default': 1, + 'mode_of_payment': 'Cash', + 'amount': 0.0 + }) + + pos_profile.save() + + pos = create_sales_invoice(qty = 10, do_not_save=True) + + pos.is_pos = 1 + pos.pos_profile = pos_profile.name + + pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500}) + pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500}) + pos.insert() + pos.submit() + + pos_return = create_sales_invoice(is_return=1, + return_against=pos.name, qty=-5, do_not_save=True) + + pos_return.is_pos = 1 + pos_return.pos_profile = pos_profile.name + pos_return.insert() + pos_return.submit() + + self.assertEqual(pos_return.get('payments')[0].amount, -500) + pos_profile.payments = [] + pos_profile.save() + + def test_pos_change_amount(self): make_pos_profile() diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index d50461766b0..085f14ab8e1 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -524,7 +524,7 @@ class calculate_taxes_and_totals(object): if self.doc.doctype == "Sales Invoice": self.calculate_paid_amount() - if self.doc.is_return and self.doc.return_against: return + if self.doc.is_return and self.doc.return_against and not self.doc.get('is_pos'): return self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"]) self._set_in_company_currency(self.doc, ['write_off_amount']) @@ -542,7 +542,7 @@ class calculate_taxes_and_totals(object): self.doc.round_floats_in(self.doc, ["paid_amount"]) change_amount = 0 - if self.doc.doctype == "Sales Invoice": + if self.doc.doctype == "Sales Invoice" and not self.doc.get('is_return'): self.calculate_write_off_amount() self.calculate_change_amount() change_amount = self.doc.change_amount \ @@ -554,6 +554,9 @@ class calculate_taxes_and_totals(object): self.doc.outstanding_amount = flt(total_amount_to_pay - flt(paid_amount) + flt(change_amount), self.doc.precision("outstanding_amount")) + if self.doc.doctype == 'Sales Invoice' and self.doc.get('is_pos') and self.doc.get('is_return'): + self.update_paid_amount_for_return(total_amount_to_pay) + def calculate_paid_amount(self): paid_amount = base_paid_amount = 0.0 @@ -624,6 +627,27 @@ class calculate_taxes_and_totals(object): def set_item_wise_tax_breakup(self): self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc) + def update_paid_amount_for_return(self, total_amount_to_pay): + default_mode_of_payment = frappe.db.get_value('Sales Invoice Payment', + {'parent': self.doc.pos_profile, 'default': 1}, + ['mode_of_payment', 'type', 'account'], as_dict=1) + + self.doc.payments = [] + + if default_mode_of_payment: + self.doc.append('payments', { + 'mode_of_payment': default_mode_of_payment.mode_of_payment, + 'type': default_mode_of_payment.type, + 'account': default_mode_of_payment.account, + 'amount': total_amount_to_pay + }) + else: + self.doc.is_pos = 0 + self.doc.pos_profile = '' + + self.calculate_paid_amount() + + def get_itemised_tax_breakup_html(doc): if not doc.taxes: return diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 2947e880311..1e99aa3bf0b 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -38,6 +38,11 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ this.calculate_total_advance(update_paid_amount); } + if (this.frm.doc.doctype == "Sales Invoice" && this.frm.doc.is_pos && + this.frm.doc.is_return) { + this.update_paid_amount_for_return(); + } + // Sales person's commission if(in_list(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"], this.frm.doc.doctype)) { this.calculate_commission(); @@ -653,23 +658,58 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } }, - set_default_payment: function(total_amount_to_pay, update_paid_amount){ + update_paid_amount_for_return: function() { + var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; + + if(this.frm.doc.party_account_currency == this.frm.doc.currency) { + var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance + - this.frm.doc.write_off_amount), precision("grand_total")); + } else { + var total_amount_to_pay = flt( + (flt(grand_total*this.frm.doc.conversion_rate, precision("grand_total")) + - this.frm.doc.total_advance - this.frm.doc.base_write_off_amount), + precision("base_grand_total") + ); + } + + frappe.db.get_value('Sales Invoice Payment', {'parent': this.frm.doc.pos_profile, 'default': 1}, + ['mode_of_payment', 'account', 'type'], (value) => { + if (this.frm.is_dirty()) { + frappe.model.clear_table(this.frm.doc, 'payments'); + if (value) { + let row = frappe.model.add_child(this.frm.doc, 'Sales Invoice Payment', 'payments'); + row.mode_of_payment = value.mode_of_payment; + row.type = value.type; + row.account = value.account; + row.default = 1; + row.amount = total_amount_to_pay; + } else { + this.frm.set_value('is_pos', 1); + } + this.frm.refresh_fields(); + } + }, 'Sales Invoice'); + + this.calculate_paid_amount(); + }, + + set_default_payment: function(total_amount_to_pay, update_paid_amount) { var me = this; var payment_status = true; - if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)){ - $.each(this.frm.doc['payments'] || [], function(index, data){ + if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) { + $.each(this.frm.doc['payments'] || [], function(index, data) { if(data.default && payment_status && total_amount_to_pay > 0) { data.base_amount = flt(total_amount_to_pay, precision("base_amount")); data.amount = flt(total_amount_to_pay / me.frm.doc.conversion_rate, precision("amount")); payment_status = false; - }else if(me.frm.doc.paid_amount){ + } else if(me.frm.doc.paid_amount) { data.amount = 0.0; } }); } }, - calculate_paid_amount: function(){ + calculate_paid_amount: function() { var me = this; var paid_amount = 0.0; var base_paid_amount = 0.0; From 69bb85d1d09ec71997aebfaf19bf025b45b2b98d Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 25 Aug 2020 06:55:02 +0530 Subject: [PATCH 22/71] fix: Form dashboard showing wrong balance --- erpnext/hr/doctype/leave_application/leave_application.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 1622fb38eec..95fedde420f 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -433,6 +433,7 @@ def get_leave_details(employee, date): 'from_date': ('<=', date), 'to_date': ('>=', date), 'leave_type': allocation.leave_type, + 'employee': employee }, 'SUM(total_leaves_allocated)') or 0 remaining_leaves = get_leave_balance_on(employee, d, date, to_date = allocation.to_date, @@ -597,7 +598,7 @@ def get_leave_entries(employee, leave_type, from_date, to_date): is_carry_forward, is_expired FROM `tabLeave Ledger Entry` WHERE employee=%(employee)s AND leave_type=%(leave_type)s - AND docstatus=1 + AND docstatus=1 AND (leaves<0 OR is_expired=1) AND (from_date between %(from_date)s AND %(to_date)s From d223d4b82999b04866099cfe92f57225e0f3dc4d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 25 Aug 2020 16:18:44 +0530 Subject: [PATCH 23/71] fix(hot): Pricing Rule encoding fixed --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 8 +++++--- erpnext/accounts/doctype/pricing_rule/utils.py | 11 ++++++++--- erpnext/controllers/accounts_controller.py | 2 +- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 0c2b5475cb8..6219f68ffe5 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -241,7 +241,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa update_args_for_pricing_rule(args) - pricing_rules = (get_applied_pricing_rules(args) + pricing_rules = (get_applied_pricing_rules(args.get('pricing_rules')) if for_validate and args.get("pricing_rules") else get_pricing_rules(args, doc)) if pricing_rules: @@ -369,8 +369,10 @@ def set_discount_amount(rate, item_details): item_details.rate = rate def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): - from erpnext.accounts.doctype.pricing_rule.utils import get_pricing_rule_items - for d in json.loads(pricing_rules): + from erpnext.accounts.doctype.pricing_rule.utils import (get_applied_pricing_rules, + get_pricing_rule_items) + + for d in get_applied_pricing_rules(pricing_rules): if not d or not frappe.db.exists("Pricing Rule", d): continue pricing_rule = frappe.get_cached_doc('Pricing Rule', d) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 7d20208fc79..09feb7d97bc 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -470,9 +470,14 @@ def apply_pricing_rule_on_transaction(doc): apply_pricing_rule_for_free_items(doc, item_details.free_item_data) doc.set_missing_values() -def get_applied_pricing_rules(item_row): - return (json.loads(item_row.get("pricing_rules")) - if item_row.get("pricing_rules") else []) +def get_applied_pricing_rules(pricing_rules): + if pricing_rules: + if pricing_rules.startswith('['): + return json.loads(pricing_rules) + else: + return pricing_rules.split(',') + + return [] def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): free_item = pricing_rule.free_item diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 56b872bcfba..4f34f7f3ce4 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -325,7 +325,7 @@ class AccountsController(TransactionBase): apply_pricing_rule_for_free_items(self, pricing_rule_args.get('free_item_data')) elif pricing_rule_args.get("validate_applied_rule"): - for pricing_rule in get_applied_pricing_rules(item): + for pricing_rule in get_applied_pricing_rules(item.get('pricing_rules')): pricing_rule_doc = frappe.get_cached_doc("Pricing Rule", pricing_rule) for field in ['discount_percentage', 'discount_amount', 'rate']: if item.get(field) < pricing_rule_doc.get(field): From 3fc00a5bfef297734f1b386f6dd4622cfded6380 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 25 Aug 2020 18:09:14 +0530 Subject: [PATCH 24/71] fix: conversion factor for BOM exploded item rate --- erpnext/manufacturing/doctype/bom/bom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 68f796ba1bc..14bcbaac6ff 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -539,7 +539,7 @@ class BOM(WebsiteGenerator): 'image' : d.image, 'stock_uom' : d.stock_uom, 'stock_qty' : flt(d.stock_qty), - 'rate' : flt(d.base_rate) / flt(d.conversion_factor), + 'rate' : flt(d.base_rate) / (flt(d.conversion_factor) or 1.0), 'include_item_in_manufacturing': d.include_item_in_manufacturing })) From 62876c708bf9cf3ea6be2a34f804aef38afd2830 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 25 Aug 2020 18:32:46 +0530 Subject: [PATCH 25/71] fix: mixed condition pricing rule not working on js side --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 3 ++- erpnext/public/js/controllers/transaction.js | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 6219f68ffe5..c5571970595 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -395,7 +395,8 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): items = get_pricing_rule_items(pricing_rule) item_details.apply_on = (frappe.scrub(pricing_rule.apply_rule_on_other) if pricing_rule.apply_rule_on_other else frappe.scrub(pricing_rule.get('apply_on'))) - item_details.applied_on_items = ','.join(items) + item_details.applied_on_items = json.dumps(items) + item_details.price_or_product_discount = pricing_rule.price_or_product_discount item_details.pricing_rules = '' diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 2c4277674cf..3c71a45309c 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1412,9 +1412,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ 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])) { + if (in_list(JSON.parse(data.apply_rule_on_other_items), d[data.apply_rule_on])) { for(var k in data) { - if (in_list(fields, k) && data[k] && (data.price_or_product_discount === 'price' || k === 'pricing_rules')) { + if (in_list(fields, k) && data[k] && (data.price_or_product_discount === 'Price' || k === 'pricing_rules')) { frappe.model.set_value(d.doctype, d.name, k, data[k]); } } @@ -1498,9 +1498,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ me.frm.doc.items = items; refresh_field('items'); } else if(item.applied_on_items && item.apply_on) { - const applied_on_items = item.applied_on_items.split(','); + const applied_on_items = JSON.parse(item.applied_on_items); me.frm.doc.items.forEach(row => { - if(applied_on_items.includes(row[item.apply_on])) { + if(in_list(applied_on_items, row[item.apply_on])) { fields.forEach(f => { row[f] = 0; }); From deb741bea5240012374dc4b196fcd223b08aee8c Mon Sep 17 00:00:00 2001 From: Michelle Alva <50285544+michellealva@users.noreply.github.com> Date: Tue, 25 Aug 2020 21:00:41 +0530 Subject: [PATCH 26/71] fix:Add Delivery Note link in Sales Invoice Dashboard --- .../doctype/sales_invoice/sales_invoice_dashboard.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py index 4a8fcc03fd1..b35e32c5ca5 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py @@ -13,7 +13,8 @@ def get_data(): 'Auto Repeat': 'reference_document', }, 'internal_links': { - 'Sales Order': ['items', 'sales_order'] + 'Sales Order': ['items', 'sales_order'], + 'Delivery Note': ['items', 'delivery_note'] }, 'transactions': [ { @@ -33,4 +34,4 @@ def get_data(): 'items': ['Auto Repeat'] }, ] - } \ No newline at end of file + } From 45d615887a464b613cab5f7418959ca32d3e3287 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 26 Aug 2020 12:10:12 +0530 Subject: [PATCH 27/71] fix: Codacy fixes --- .../hsn_wise_summary_of_outward_supplies.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index 6f3fff29323..59389ce3269 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -225,7 +225,7 @@ def get_json(filters, report_name, data): fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) - gst_json = {"gstin": "", "version": "GST2.3.4", + gst_json = {"version": "GST2.3.4", "hash": "hash", "gstin": gstin, "fp": fp} gst_json["hsn"] = { @@ -239,7 +239,7 @@ def get_json(filters, report_name, data): @frappe.whitelist() def download_json_file(): - ''' download json content in a file ''' + '''download json content in a file''' data = frappe._dict(frappe.local.form_dict) frappe.response['filename'] = frappe.scrub("{0}".format(data['report_name'])) + '.json' frappe.response['filecontent'] = data['data'] From 759282651b6ce4a946630167e37d5d329aaf5492 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Wed, 26 Aug 2020 14:23:56 +0530 Subject: [PATCH 28/71] feat: Added phone field in product Inquiry --- erpnext/templates/generators/item/item_inquiry.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/templates/generators/item/item_inquiry.js b/erpnext/templates/generators/item/item_inquiry.js index 52ddae2624c..e7db3a368df 100644 --- a/erpnext/templates/generators/item/item_inquiry.js +++ b/erpnext/templates/generators/item/item_inquiry.js @@ -20,6 +20,13 @@ frappe.ready(() => { options: 'Email', reqd: 1 }, + { + fieldtype: 'Data', + label: __('Phone Number'), + fieldname: 'phone', + options: 'Phone', + reqd: 1 + }, { fieldtype: 'Data', label: __('Subject'), From a3907e1329fe10b5d190d53c8ab6802389436da7 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 26 Aug 2020 15:15:05 +0530 Subject: [PATCH 29/71] fix: don't overwrite appointment duration if already specified --- .../doctype/patient_appointment/patient_appointment.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js index 087f1628404..0a208128564 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js @@ -149,7 +149,9 @@ var check_and_set_availability = function(frm) { primary_action: function() { frm.set_value('appointment_time', selected_slot); frm.set_value('service_unit', service_unit || ''); - frm.set_value('duration', duration); + if (!frm.doc.duration) { + frm.set_value('duration', duration); + } frm.set_value('practitioner', d.get_value('practitioner')); frm.set_value('department', d.get_value('department')); frm.set_value('appointment_date', d.get_value('appointment_date')); From 4cce4a771271fd99fe5a1cb0c3bd05f72ccda2af Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 26 Aug 2020 18:23:12 +0530 Subject: [PATCH 30/71] fix: get_applied_pricing_rule in taxes_and_totals --- erpnext/controllers/taxes_and_totals.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index d50461766b0..638500e1240 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -9,6 +9,7 @@ from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction from erpnext.controllers.accounts_controller import validate_conversion_rate, \ validate_taxes_and_charges, validate_inclusive_tax from erpnext.stock.get_item_details import _get_item_tax_template +from erpnext.accounts.doctype.pricing_rule.utils import get_applied_pricing_rules class calculate_taxes_and_totals(object): def __init__(self, doc): @@ -208,7 +209,7 @@ class calculate_taxes_and_totals(object): elif tax.charge_type == "On Previous Row Total": current_tax_fraction = (tax_rate / 100.0) * \ self.doc.get("taxes")[cint(tax.row_id) - 1].grand_total_fraction_for_current_item - + elif tax.charge_type == "On Item Quantity": inclusive_tax_amount_per_qty = flt(tax_rate) @@ -603,7 +604,7 @@ class calculate_taxes_and_totals(object): base_rate_with_margin = 0.0 if item.price_list_rate: if item.pricing_rules and not self.doc.ignore_pricing_rule: - for d in json.loads(item.pricing_rules): + for d in get_applied_pricing_rules(item.pricing_rules): pricing_rule = frappe.get_cached_doc('Pricing Rule', d) if (pricing_rule.margin_type == 'Amount' and pricing_rule.currency == self.doc.currency)\ From dcd80a3f4879325d218d5e9b9733ef9dd393b39c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 27 Aug 2020 13:24:33 +0530 Subject: [PATCH 31/71] fix: incorrect stock balance issue --- .../stock_reconciliation.py | 155 +++++++++--------- 1 file changed, 73 insertions(+), 82 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 0dc87767dde..fbb7d9e6aeb 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -183,17 +183,11 @@ class StockReconciliation(StockController): from erpnext.stock.stock_ledger import get_previous_sle sl_entries = [] - has_serial_no = False - has_batch_no = False - for row in self.items: - item = frappe.get_doc("Item", row.item_code) - if item.has_batch_no: - has_batch_no = True - if item.has_serial_no or item.has_batch_no: - has_serial_no = True - self.get_sle_for_serialized_items(row, sl_entries) - else: + serialized_items = False + for row in self.items: + item = frappe.get_cached_doc("Item", row.item_code) + if not (item.has_serial_no or item.has_batch_no): if row.serial_no or row.batch_no: frappe.throw(_("Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.") \ .format(row.idx, frappe.bold(row.item_code))) @@ -221,88 +215,93 @@ class StockReconciliation(StockController): sl_entries.append(self.get_sle_for_items(row)) + else: + serialized_items = True + + if serialized_items: + self.get_sle_for_serialized_items(sl_entries) + if sl_entries: - if has_serial_no: - sl_entries = self.merge_similar_item_serial_nos(sl_entries) - - allow_negative_stock = False - if has_batch_no: - allow_negative_stock = True - + allow_negative_stock = frappe.get_cached_value("Stock Settings", None, "allow_negative_stock") self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock) - if has_serial_no and sl_entries: - self.update_valuation_rate_for_serial_no() + def get_sle_for_serialized_items(self, sl_entries): + self.issue_existing_serial_and_batch(sl_entries) + self.add_new_serial_and_batch(sl_entries) + self.update_valuation_rate_for_serial_no() - def get_sle_for_serialized_items(self, row, sl_entries): + if sl_entries: + sl_entries = self.merge_similar_item_serial_nos(sl_entries) + + def issue_existing_serial_and_batch(self, sl_entries): from erpnext.stock.stock_ledger import get_previous_sle - serial_nos = get_serial_nos(row.serial_no) + for row in self.items: + serial_nos = get_serial_nos(row.serial_no) or [] - - # To issue existing serial nos - if row.current_qty and (row.current_serial_no or row.batch_no): - args = self.get_sle_for_items(row) - args.update({ - 'actual_qty': -1 * row.current_qty, - 'serial_no': row.current_serial_no, - 'batch_no': row.batch_no, - 'valuation_rate': row.current_valuation_rate - }) - - if row.current_serial_no: + # To issue existing serial nos + if row.current_qty and (row.current_serial_no or row.batch_no): + args = self.get_sle_for_items(row) args.update({ - 'qty_after_transaction': 0, + 'actual_qty': -1 * row.current_qty, + 'serial_no': row.current_serial_no, + 'batch_no': row.batch_no, + 'valuation_rate': row.current_valuation_rate }) - sl_entries.append(args) + if row.current_serial_no: + args.update({ + 'qty_after_transaction': 0, + }) - qty_after_transaction = 0 - for serial_no in serial_nos: - args = self.get_sle_for_items(row, [serial_no]) + sl_entries.append(args) - previous_sle = get_previous_sle({ - "item_code": row.item_code, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "serial_no": serial_no - }) + qty_after_transaction = 0 + for serial_no in serial_nos: + args = self.get_sle_for_items(row, [serial_no]) - if previous_sle and row.warehouse != previous_sle.get("warehouse"): - # If serial no exists in different warehouse - - warehouse = previous_sle.get("warehouse", '') or row.warehouse - - if not qty_after_transaction: - qty_after_transaction = get_stock_balance(row.item_code, - warehouse, self.posting_date, self.posting_time) - - qty_after_transaction -= 1 - - new_args = args.copy() - new_args.update({ - 'actual_qty': -1, - 'qty_after_transaction': qty_after_transaction, - 'warehouse': warehouse, - 'valuation_rate': previous_sle.get("valuation_rate") + previous_sle = get_previous_sle({ + "item_code": row.item_code, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "serial_no": serial_no }) - sl_entries.append(new_args) + if previous_sle and row.warehouse != previous_sle.get("warehouse"): + # If serial no exists in different warehouse - if row.qty: - args = self.get_sle_for_items(row) + warehouse = previous_sle.get("warehouse", '') or row.warehouse - args.update({ - 'actual_qty': row.qty, - 'incoming_rate': row.valuation_rate, - 'valuation_rate': row.valuation_rate - }) + if not qty_after_transaction: + qty_after_transaction = get_stock_balance(row.item_code, + warehouse, self.posting_date, self.posting_time) - sl_entries.append(args) + qty_after_transaction -= 1 - if serial_nos == get_serial_nos(row.current_serial_no): - # update valuation rate - self.update_valuation_rate_for_serial_nos(row, serial_nos) + new_args = args.copy() + new_args.update({ + 'actual_qty': -1, + 'qty_after_transaction': qty_after_transaction, + 'warehouse': warehouse, + 'valuation_rate': previous_sle.get("valuation_rate") + }) + + sl_entries.append(new_args) + + def add_new_serial_and_batch(self, sl_entries): + for row in self.items: + if row.qty: + serial_nos = get_serial_nos(row.serial_no) or [] + + args = self.get_sle_for_items(row) + + args.update({ + 'actual_qty': row.qty, + 'incoming_rate': row.valuation_rate, + 'valuation_rate': row.valuation_rate + }) + + sl_entries.append(args) def update_valuation_rate_for_serial_no(self): for d in self.items: @@ -360,17 +359,9 @@ class StockReconciliation(StockController): where voucher_type=%s and voucher_no=%s""", (self.doctype, self.name)) sl_entries = [] - - has_serial_no = False - for row in self.items: - if row.serial_no or row.batch_no or row.current_serial_no: - has_serial_no = True - self.get_sle_for_serialized_items(row, sl_entries) + self.get_sle_for_serialized_items(sl_entries) if sl_entries: - if has_serial_no: - sl_entries = self.merge_similar_item_serial_nos(sl_entries) - sl_entries.reverse() allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock) From 09cb0f33b2edcb920ed32d39c1345fe1ded2c232 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 27 Aug 2020 14:43:06 +0530 Subject: [PATCH 32/71] added test cases --- .../stock_reconciliation.py | 2 - .../test_stock_reconciliation.py | 81 +++++++++++++++++-- 2 files changed, 73 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index fbb7d9e6aeb..5002426fbdd 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -291,8 +291,6 @@ class StockReconciliation(StockController): def add_new_serial_and_batch(self, sl_entries): for row in self.items: if row.qty: - serial_nos = get_serial_nos(row.serial_no) or [] - args = self.get_sle_for_items(row) args.update({ diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 51d027f22ef..e027a6f3808 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -204,6 +204,58 @@ class TestStockReconciliation(unittest.TestCase): stock_doc = frappe.get_doc("Stock Reconciliation", d) stock_doc.cancel() + def test_stock_reco_for_same_item_with_multiple_batches(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + set_perpetual_inventory() + + item_code = "Stock-Reco-batch-Item-2" + warehouse = "_Test Warehouse for Stock Reco3 - _TC" + + create_warehouse("_Test Warehouse for Stock Reco3", {"is_group": 0, + "parent_warehouse": "_Test Warehouse Group - _TC", "company": "_Test Company"}) + + batch_item_doc = create_item(item_code, is_stock_item=1) + if not batch_item_doc.has_batch_no: + frappe.db.set_value("Item", item_code, { + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "Test-C.####" + }) + + # inward entries with different batch and valuation rate + ste1=make_stock_entry(posting_date="2012-12-15", posting_time="02:00", item_code=item_code, + target=warehouse, qty=6, basic_rate=700) + ste2=make_stock_entry(posting_date="2012-12-16", posting_time="02:00", item_code=item_code, + target=warehouse, qty=3, basic_rate=200) + ste3=make_stock_entry(posting_date="2012-12-17", posting_time="02:00", item_code=item_code, + target=warehouse, qty=2, basic_rate=500) + ste4=make_stock_entry(posting_date="2012-12-17", posting_time="02:00", item_code=item_code, + target=warehouse, qty=4, basic_rate=100) + + batchwise_item_details = {} + for stock_doc in [ste1, ste2, ste3, ste4]: + self.assertEqual(item_code, stock_doc.items[0].item_code) + batchwise_item_details[stock_doc.items[0].batch_no] = [stock_doc.items[0].qty, 0.01] + + stock_balance = frappe.get_all("Stock Ledger Entry", + filters = {"item_code": item_code, "warehouse": warehouse}, + fields=["sum(stock_value_difference)"], as_list=1) + + self.assertEqual(flt(stock_balance[0][0]), 6200.00) + + sr = create_stock_reconciliation(item_code=item_code, + warehouse = warehouse, batch_details = batchwise_item_details) + + stock_balance = frappe.get_all("Stock Ledger Entry", + filters = {"item_code": item_code, "warehouse": warehouse}, + fields=["sum(stock_value_difference)"], as_list=1) + + self.assertEqual(flt(stock_balance[0][0]), 0.15) + + for doc in [sr, ste1, ste2, ste3, ste4]: + doc.cancel() + frappe.delete_doc(doc.doctype, doc.name) def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -251,16 +303,29 @@ def create_stock_reconciliation(**args): or frappe.get_cached_value("Company", sr.company, "cost_center") \ or "_Test Cost Center - _TC" - sr.append("items", { - "item_code": args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty, - "valuation_rate": args.rate, - "serial_no": args.serial_no, - "batch_no": args.batch_no - }) + if not args.batch_details: + sr.append("items", { + "item_code": args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty, + "valuation_rate": args.rate, + "serial_no": args.serial_no, + "batch_no": args.batch_no + }) + elif args.batch_details: + for batch, data in args.batch_details.items(): + sr.append("items", { + "item_code": args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": data[0], + "valuation_rate": data[1], + "batch_no": batch + }) try: + if args.do_not_save: + return sr + if not args.do_not_submit: sr.submit() except EmptyStockReconciliationItemsError: From 00dbbc9f5cc6fc63ba458f9f24b64c20434328db Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 27 Aug 2020 19:00:07 +0530 Subject: [PATCH 33/71] fix: BOM Update Tool failing due to Too Many Writes error --- .../manufacturing/doctype/bom_update_tool/bom_update_tool.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index e6c10ad12b0..742d18c4cda 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -90,6 +90,7 @@ def update_latest_price_in_all_boms(): update_cost() def replace_bom(args): + frappe.db.auto_commit_on_many_writes = 1 args = frappe._dict(args) doc = frappe.get_doc("BOM Update Tool") @@ -97,6 +98,8 @@ def replace_bom(args): doc.new_bom = args.new_bom doc.replace_bom() + frappe.db.auto_commit_on_many_writes = 0 + def update_cost(): frappe.db.auto_commit_on_many_writes = 1 bom_list = get_boms_in_bottom_up_order() From eab2d25c6aeef71c1f149cb60a846ab25c0b6555 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 27 Aug 2020 20:34:51 +0530 Subject: [PATCH 34/71] fix: Unlink and delete batch created from stock reco on cancel --- erpnext/controllers/stock_controller.py | 3 ++- .../stock/doctype/stock_reconciliation/stock_reconciliation.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 2f275bb3c91..8cad82c3e25 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -242,10 +242,11 @@ class StockController(AccountsController): _(self.doctype), self.name, item.get("item_code"))) def delete_auto_created_batches(self): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos for d in self.items: if not d.batch_no: continue - serial_nos = [sr.name for sr in frappe.get_all("Serial No", {'batch_no': d.batch_no})] + serial_nos = get_serial_nos(d.serial_no) if serial_nos: frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 0dc87767dde..00dc7725039 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -45,6 +45,7 @@ class StockReconciliation(StockController): def on_cancel(self): self.delete_and_repost_sle() self.make_gl_entries_on_cancel() + self.delete_auto_created_batches() def remove_items_with_no_change(self): """Remove items if qty or rate is not changed""" From e352706330a1eb23f27a30a992dbf66001f8a996 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 24 Aug 2020 17:55:23 +0530 Subject: [PATCH 35/71] fix: user created manual job card not linking job card operations with work order operations --- .../doctype/job_card/job_card.js | 61 ++++++++++++++++++- .../doctype/job_card/job_card.json | 9 ++- .../doctype/job_card/job_card.py | 34 +++++++++++ .../doctype/job_card/test_job_card.py | 24 +++++++- 4 files changed, 124 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index bbce6f55d81..895ef83c4d7 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -2,6 +2,17 @@ // For license information, please see license.txt frappe.ui.form.on('Job Card', { + setup: function(frm) { + frm.set_query('operation', function() { + return { + query: 'erpnext.manufacturing.doctype.job_card.job_card.get_operations', + filters: { + 'work_order': frm.doc.work_order + } + }; + }); + }, + refresh: function(frm) { if(frm.doc.docstatus == 0) { @@ -24,12 +35,60 @@ frappe.ui.form.on('Job Card', { } } + frm.trigger("toggle_operation_number"); + if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) - && (!frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { + && (!frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { frm.trigger("prepare_timer_buttons"); } }, + operation: function(frm) { + frm.trigger("toggle_operation_number"); + + if (frm.doc.operation && frm.doc.work_order) { + frappe.call({ + method: "erpnext.manufacturing.doctype.job_card.job_card.get_operation_details", + args: { + "work_order":frm.doc.work_order, + "operation":frm.doc.operation + }, + callback: function (r) { + if (r.message) { + if (r.message.length == 1) { + frm.set_value("operation_id", r.message[0].name); + } else { + let args = []; + + r.message.forEach((row) => { + args.push({ "label": row.idx, "value": row.name }); + }); + + let description = __("Operation {0} added multiple times in the work order {1}", + [frm.doc.operation, frm.doc.work_order]); + + frm.set_df_property("operation_row_number", "options", args); + frm.set_df_property("operation_row_number", "description", description); + } + + frm.trigger("toggle_operation_number"); + } + } + }) + } + }, + + operation_row_number(frm) { + if (frm.doc.operation_row_number) { + frm.set_value("operation_id", frm.doc.operation_row_number); + } + }, + + toggle_operation_number(frm) { + frm.toggle_display("operation_row_number", !frm.doc.operation_id && frm.doc.operation); + frm.toggle_reqd("operation_row_number", !frm.doc.operation_id && frm.doc.operation); + }, + prepare_timer_buttons: function(frm) { frm.trigger("make_dashboard"); if (!frm.doc.job_started) { diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 7661fffa864..b5d17803ee9 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -10,6 +10,7 @@ "bom_no", "workstation", "operation", + "operation_row_number", "column_break_4", "posting_date", "company", @@ -287,10 +288,15 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "operation_row_number", + "fieldtype": "Select", + "label": "Operation Row Number" } ], "is_submittable": 1, - "modified": "2020-03-27 13:36:35.417502", + "modified": "2020-08-24 15:21:21.398267", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", @@ -342,7 +348,6 @@ "write": 1 } ], - "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", "title_field": "operation", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 88b20eb6942..d34bed586cc 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -9,10 +9,13 @@ from frappe.utils import flt, time_diff_in_hours, get_datetime, time_diff, get_l from frappe.model.mapper import get_mapped_doc from frappe.model.document import Document +class OperationMismatchError(frappe.ValidationError): pass + class JobCard(Document): def validate(self): self.validate_time_logs() self.set_status() + self.validate_operation_id() def validate_time_logs(self): self.total_completed_qty = 0.0 @@ -204,6 +207,37 @@ class JobCard(Document): if update_status: self.db_set('status', self.status) + def validate_operation_id(self): + if (self.get("operation_id") and self.get("operation_row_number") and self.operation and self.work_order and + frappe.get_cached_value("Work Order Operation", self.operation_row_number, "name") != self.operation_id): + work_order = frappe.bold(get_link_to_form("Work Order", self.work_order)) + frappe.throw(_("Operation {0} does not belong to the work order {1}") + .format(frappe.bold(self.operation), work_order), OperationMismatchError) + +@frappe.whitelist() +def get_operation_details(work_order, operation): + if work_order and operation: + return frappe.get_all("Work Order Operation", fields = ["name", "idx"], + filters = { + "parent": work_order, + "operation": operation + } + ) + +@frappe.whitelist() +def get_operations(doctype, txt, searchfield, start, page_len, filters): + if filters.get("work_order"): + args = {"parent": filters.get("work_order")} + if txt: + args["operation"] = ("like", "%{0}%".format(txt)) + + return frappe.get_all("Work Order Operation", + filters = args, + fields = ["distinct operation as operation"], + limit_start = start, + limit_page_length = page_len, + order_by="idx asc", as_list=1) + @frappe.whitelist() def make_material_request(source_name, target_doc=None): def update_item(obj, target, source_parent): diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index ca05fea0f6f..2a6c35fc04c 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -4,6 +4,28 @@ from __future__ import unicode_literals import unittest +import frappe +from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record +from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError class TestJobCard(unittest.TestCase): - pass + def test_job_card(self): + data = frappe.get_cached_value('BOM', + {'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) + + if data: + bom, bom_item = data + + work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom) + + job_cards = frappe.get_all('Job Card', + filters = {'work_order': work_order.name}, fields = ["operation_id", "name"]) + + if job_cards: + job_card = job_cards[0] + frappe.db.set_value("Job Card", job_card.name, "operation_row_number", job_card.operation_id) + + doc = frappe.get_doc("Job Card", job_card.name) + doc.operation_id = "Test Data" + self.assertRaises(OperationMismatchError, doc.save) + From 17a3af47d931edd341c828b0be2a8f91646b61ad Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 3 Jul 2020 22:03:25 +0530 Subject: [PATCH 36/71] fix: Check for Company before trying to fetch party details --- erpnext/accounts/party.py | 2 +- erpnext/public/js/utils/party.js | 80 ++++++++++++++++++-------------- 2 files changed, 45 insertions(+), 37 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index b0b64c8b56f..12e7b8b8c37 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -391,7 +391,7 @@ def set_taxes(party, party_type, posting_date, company, customer_group=None, sup from erpnext.accounts.doctype.tax_rule.tax_rule import get_tax_template, get_party_details args = { party_type.lower(): party, - "company": company + "company": company } if tax_category: diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index 99c1b8ae8f3..065326744c2 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -4,7 +4,7 @@ frappe.provide("erpnext.utils"); erpnext.utils.get_party_details = function(frm, method, args, callback) { - if(!method) { + if (!method) { method = "erpnext.accounts.party.get_party_details"; } @@ -22,12 +22,12 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { } } - if(!args) { - if((frm.doctype != "Purchase Order" && frm.doc.customer) + if (!args) { + if ((frm.doctype != "Purchase Order" && frm.doc.customer) || (frm.doc.party_name && in_list(['Quotation', 'Opportunity'], frm.doc.doctype))) { let party_type = "Customer"; - if(frm.doc.quotation_to && frm.doc.quotation_to === "Lead") { + if (frm.doc.quotation_to && frm.doc.quotation_to === "Lead") { party_type = "Lead"; } @@ -36,7 +36,7 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { party_type: party_type, price_list: frm.doc.selling_price_list }; - } else if(frm.doc.supplier) { + } else if (frm.doc.supplier) { args = { party: frm.doc.supplier, party_type: "Supplier", @@ -78,13 +78,17 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { args.posting_date = frm.doc.posting_date || frm.doc.transaction_date; } } - if(!args || !args.party) return; + if (!args || !args.party) return; - if(frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { - if(!erpnext.utils.validate_mandatory(frm, "Posting/Transaction Date", + if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { + if (!erpnext.utils.validate_mandatory(frm, "Posting / Transaction Date", args.posting_date, args.party_type=="Customer" ? "customer": "supplier")) return; } + if (!erpnext.utils.validate_mandatory(frm, "Company", frm.doc.company, args.party_type=="Customer" ? "customer": "supplier")) { + return; + } + args.currency = frm.doc.currency; args.company = frm.doc.company; args.doctype = frm.doc.doctype; @@ -92,14 +96,14 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { method: method, args: args, callback: function(r) { - if(r.message) { + if (r.message) { frm.supplier_tds = r.message.supplier_tds; frm.updating_party_details = true; frappe.run_serially([ () => frm.set_value(r.message), () => { frm.updating_party_details = false; - if(callback) callback(); + if (callback) callback(); frm.refresh(); erpnext.utils.add_item(frm); } @@ -110,9 +114,9 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { } erpnext.utils.add_item = function(frm) { - if(frm.is_new()) { + if (frm.is_new()) { var prev_route = frappe.get_prev_route(); - if(prev_route[1]==='Item' && !(frm.doc.items && frm.doc.items.length)) { + if (prev_route[1]==='Item' && !(frm.doc.items && frm.doc.items.length)) { // add row var item = frm.add_child('items'); frm.refresh_field('items'); @@ -124,23 +128,23 @@ erpnext.utils.add_item = function(frm) { } erpnext.utils.get_address_display = function(frm, address_field, display_field, is_your_company_address) { - if(frm.updating_party_details) return; + if (frm.updating_party_details) return; - if(!address_field) { - if(frm.doctype != "Purchase Order" && frm.doc.customer) { + if (!address_field) { + if (frm.doctype != "Purchase Order" && frm.doc.customer) { address_field = "customer_address"; - } else if(frm.doc.supplier) { + } else if (frm.doc.supplier) { address_field = "supplier_address"; } else return; } - if(!display_field) display_field = "address_display"; - if(frm.doc[address_field]) { + if (!display_field) display_field = "address_display"; + if (frm.doc[address_field]) { frappe.call({ method: "frappe.contacts.doctype.address.address.get_address_display", args: {"address_dict": frm.doc[address_field] }, callback: function(r) { - if(r.message) { + if (r.message) { frm.set_value(display_field, r.message) } } @@ -151,15 +155,15 @@ erpnext.utils.get_address_display = function(frm, address_field, display_field, }; erpnext.utils.set_taxes_from_address = function(frm, triggered_from_field, billing_address_field, shipping_address_field) { - if(frm.updating_party_details) return; + if (frm.updating_party_details) return; - if(frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { - if(!erpnext.utils.validate_mandatory(frm, "Lead/Customer/Supplier", + if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { + if (!erpnext.utils.validate_mandatory(frm, "Lead / Customer / Supplier", frm.doc.customer || frm.doc.supplier || frm.doc.lead || frm.doc.party_name, triggered_from_field)) { return; } - if(!erpnext.utils.validate_mandatory(frm, "Posting/Transaction Date", + if (!erpnext.utils.validate_mandatory(frm, "Posting / Transaction Date", frm.doc.posting_date || frm.doc.transaction_date, triggered_from_field)) { return; } @@ -175,8 +179,8 @@ erpnext.utils.set_taxes_from_address = function(frm, triggered_from_field, billi "shipping_address": frm.doc[shipping_address_field] }, callback: function(r) { - if(!r.exc){ - if(frm.doc.tax_category != r.message) { + if (!r.exc){ + if (frm.doc.tax_category != r.message) { frm.set_value("tax_category", r.message); } else { erpnext.utils.set_taxes(frm, triggered_from_field); @@ -187,13 +191,17 @@ erpnext.utils.set_taxes_from_address = function(frm, triggered_from_field, billi }; erpnext.utils.set_taxes = function(frm, triggered_from_field) { - if(frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { - if(!erpnext.utils.validate_mandatory(frm, "Lead/Customer/Supplier", + if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { + if (!erpnext.utils.validate_mandatory(frm, "Company", frm.doc.company, triggered_from_field)) { + return; + } + + if (!erpnext.utils.validate_mandatory(frm, "Lead / Customer / Supplier", frm.doc.customer || frm.doc.supplier || frm.doc.lead || frm.doc.party_name, triggered_from_field)) { return; } - if(!erpnext.utils.validate_mandatory(frm, "Posting/Transaction Date", + if (!erpnext.utils.validate_mandatory(frm, "Posting / Transaction Date", frm.doc.posting_date || frm.doc.transaction_date, triggered_from_field)) { return; } @@ -230,7 +238,7 @@ erpnext.utils.set_taxes = function(frm, triggered_from_field) { "shipping_address": frm.doc.shipping_address_name }, callback: function(r) { - if(r.message){ + if (r.message){ frm.set_value("taxes_and_charges", r.message) } } @@ -238,14 +246,14 @@ erpnext.utils.set_taxes = function(frm, triggered_from_field) { }; erpnext.utils.get_contact_details = function(frm) { - if(frm.updating_party_details) return; + if (frm.updating_party_details) return; - if(frm.doc["contact_person"]) { + if (frm.doc["contact_person"]) { frappe.call({ method: "frappe.contacts.doctype.contact.contact.get_contact_details", args: {contact: frm.doc.contact_person }, callback: function(r) { - if(r.message) + if (r.message) frm.set_value(r.message); } }) @@ -253,10 +261,10 @@ erpnext.utils.get_contact_details = function(frm) { } erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) { - if(!value) { + if (!value) { frm.doc[trigger_on] = ""; refresh_field(trigger_on); - frappe.msgprint(__("Please enter {0} first", [label])); + frappe.throw({message:__("Please enter {0} first", [label]), title:__("Mandatory")}); return false; } return true; @@ -271,12 +279,12 @@ erpnext.utils.get_shipping_address = function(frm, callback){ address: frm.doc.shipping_address }, callback: function(r){ - if(r.message){ + if (r.message){ frm.set_value("shipping_address", r.message[0]) //Address title or name frm.set_value("shipping_address_display", r.message[1]) //Address to be displayed on the page } - if(callback){ + if (callback){ return callback(); } } From b73879056f24889ce6c87e78d866694ee50f8a04 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 31 Aug 2020 11:57:57 +0530 Subject: [PATCH 37/71] fix: incorrect completed qty against operation in work order if workstation is different in job card --- .../doctype/job_card/job_card.py | 12 +++---- .../doctype/job_card/test_job_card.py | 36 +++++++++++++++++++ .../doctype/workstation/test_workstation.py | 15 ++++++++ 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index d34bed586cc..c7443d911ce 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -110,11 +110,10 @@ class JobCard(Document): for_quantity, time_in_mins = 0, 0 from_time_list, to_time_list = [], [] - field = "operation_id" if self.operation_id else "operation" + field = "operation_id" data = frappe.get_all('Job Card', fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], - filters = {"docstatus": 1, "work_order": self.work_order, - "workstation": self.workstation, field: self.get(field)}) + filters = {"docstatus": 1, "work_order": self.work_order, field: self.get(field)}) if data and len(data) > 0: for_quantity = data[0].completed_qty @@ -127,14 +126,13 @@ class JobCard(Document): FROM `tabJob Card` jc, `tabJob Card Time Log` jctl WHERE jctl.parent = jc.name and jc.work_order = %s - and jc.workstation = %s and jc.{0} = %s and jc.docstatus = 1 - """.format(field), (self.work_order, self.workstation, self.get(field)), as_dict=1) + and jc.{0} = %s and jc.docstatus = 1 + """.format(field), (self.work_order, self.get(field)), as_dict=1) wo = frappe.get_doc('Work Order', self.work_order) - work_order_field = "name" if field == "operation_id" else field for data in wo.operations: - if data.get(work_order_field) == self.get(field): + if data.get("name") == self.get(field): data.completed_qty = for_quantity data.actual_operation_time = time_in_mins data.actual_start_time = time_data[0].start_time if time_data else None diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 2a6c35fc04c..353f6d281a8 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -5,6 +5,8 @@ from __future__ import unicode_literals import unittest import frappe +from frappe.utils import random_string +from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError @@ -29,3 +31,37 @@ class TestJobCard(unittest.TestCase): doc.operation_id = "Test Data" self.assertRaises(OperationMismatchError, doc.save) + def test_job_card_with_different_work_station(self): + data = frappe.get_cached_value('BOM', + {'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) + + if data: + bom, bom_item = data + + work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom) + + job_card = frappe.get_all('Job Card', + filters = {'work_order': work_order.name}, + fields = ["operation_id", "workstation", "name", "for_quantity"])[0] + + if job_card: + workstation = frappe.db.get_value("Workstation", + {"name": ("not in", [job_card.workstation])}, "name") + + if not workstation or job_card.workstation == workstation: + workstation = make_workstation(workstation_name=random_string(5)).name + + doc = frappe.get_doc("Job Card", job_card.name) + doc.workstation = workstation + doc.append("time_logs", { + "from_time": "2009-01-01 12:06:25", + "to_time": "2009-01-01 12:37:25", + "time_in_mins": "31.00002", + "completed_qty": job_card.for_quantity + }) + doc.submit() + + completed_qty = frappe.db.get_value("Work Order Operation", job_card.operation_id, "completed_qty") + self.assertEqual(completed_qty, job_card.for_quantity) + + doc.cancel() \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py index 21692608548..8266cf7b779 100644 --- a/erpnext/manufacturing/doctype/workstation/test_workstation.py +++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py @@ -20,3 +20,18 @@ class TestWorkstation(unittest.TestCase): "_Test Workstation 1", "Operation 1", "2013-02-02 05:00:00", "2013-02-02 20:00:00") self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours, "_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00") + +def make_workstation(**args): + args = frappe._dict(args) + + try: + doc = frappe.get_doc({ + "doctype": "Workstation", + "workstation_name": args.workstation_name + }) + + doc.insert() + + return doc + except frappe.DuplicateEntryError: + return frappe.get_doc("Workstation", args.workstation_name) \ No newline at end of file From 0ff376fa4a3ca28a71080495d779fcbde668aba2 Mon Sep 17 00:00:00 2001 From: Marica Date: Mon, 31 Aug 2020 13:54:10 +0530 Subject: [PATCH 38/71] fix: Better error feedback on creating SO from Quotation (#23214) --- erpnext/selling/doctype/quotation/quotation.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 3c8ba467680..1748ee353f4 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -262,9 +262,17 @@ def _make_customer(source_name, ignore_permissions=False): return customer else: raise - except frappe.MandatoryError: + except frappe.MandatoryError as e: + mandatory_fields = e.args[0].split(':')[1].split(',') + mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields] + frappe.local.message_log = [] - frappe.throw(_("Please create Customer from Lead {0}").format(lead_name)) + lead_link = frappe.utils.get_link_to_form("Lead", lead_name) + message = _("Could not auto create Customer due to the following missing mandatory field(s):") + "
" + message += "
  • " + "
  • ".join(mandatory_fields) + "
" + message += _("Please create Customer from Lead {0}.").format(lead_link) + + frappe.throw(message, title=_("Mandatory Missing")) else: return customer_name else: From b574353c00a48d6971446135c7a8b5aba2cddeb0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 31 Aug 2020 20:09:48 +0530 Subject: [PATCH 39/71] fix(Payroll Entry): Set cost center for payroll payable account (#23223) --- erpnext/hr/doctype/payroll_entry/payroll_entry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/hr/doctype/payroll_entry/payroll_entry.py b/erpnext/hr/doctype/payroll_entry/payroll_entry.py index 2a81b32aa9a..f8aad316ed9 100644 --- a/erpnext/hr/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/hr/doctype/payroll_entry/payroll_entry.py @@ -290,6 +290,7 @@ class PayrollEntry(Document): "account": default_payroll_payable_account, "credit_in_account_currency": flt(payable_amount, precision), "party_type": '', + "cost_center": self.cost_center }) journal_entry.set("accounts", accounts) From ab6f76175345d6bfaea73a650fb333ada44bc5d7 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Mon, 31 Aug 2020 22:01:54 +0530 Subject: [PATCH 40/71] fix:reverse journal entry (#23224) --- .../doctype/journal_entry/journal_entry.js | 20 +++------ .../doctype/journal_entry/journal_entry.py | 31 +++++++++++++ .../journal_entry/test_journal_entry.py | 43 +++++++++++++++++++ 3 files changed, 80 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 3604b60b751..d37d76f8880 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -619,20 +619,12 @@ $.extend(erpnext.journal_entry, { return { filters: filters }; }, - reverse_journal_entry: function(frm) { - var me = frm.doc; - for(var i=0; i Date: Tue, 1 Sep 2020 14:02:34 +0530 Subject: [PATCH 41/71] fix: incorrect job card timer issue --- erpnext/manufacturing/doctype/job_card/job_card.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 895ef83c4d7..596206fb63c 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -38,7 +38,7 @@ frappe.ui.form.on('Job Card', { frm.trigger("toggle_operation_number"); if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) - && (!frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { + && (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { frm.trigger("prepare_timer_buttons"); } }, @@ -98,9 +98,9 @@ frappe.ui.form.on('Job Card', { fieldname: 'employee'}, d => { if (d.employee) { frm.set_value("employee", d.employee); + } else { + frm.events.start_job(frm); } - - frm.events.start_job(frm); }, __("Enter Value"), __("Start")); } else { frm.events.start_job(frm); @@ -145,9 +145,7 @@ frappe.ui.form.on('Job Card', { frm.set_value('current_time' , 0); } - frm.save("Save", () => {}, "", () => { - frm.doc.time_logs.pop(-1); - }); + frm.save(); }, complete_job: function(frm, completed_time, completed_qty) { @@ -179,6 +177,8 @@ frappe.ui.form.on('Job Card', { employee: function(frm) { if (frm.doc.job_started && !frm.doc.current_time) { frm.trigger("reset_timer"); + } else { + frm.events.start_job(frm); } }, From 05a90baddd977128e414338297fb25e9c2baf42d Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 1 Sep 2020 15:29:27 +0530 Subject: [PATCH 42/71] fix: Don't overwrite doctype while setting attributes in child row (#23228) --- erpnext/public/js/controllers/buying.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 802cc056c6a..b45efa22656 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -508,7 +508,7 @@ erpnext.buying.get_items_from_product_bundle = function(frm) { var d = frm.add_child("items"); var item = r.message[i]; for ( var key in item) { - if ( !is_null(item[key]) ) { + if ( !is_null(item[key]) && key !== "doctype" ) { d[key] = item[key]; } } From 457a909784f8c0a485d3fe0af3e0cc326399bd24 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 1 Sep 2020 16:30:34 +0530 Subject: [PATCH 43/71] fix: events not deleted on cancelling maintenance schedule (#23219) * feat: add participant to event_participant child table * feat: add tests * chore: update function name Co-authored-by: Marica Co-authored-by: Marica --- .../maintenance_schedule.py | 12 +++--- .../test_maintenance_schedule.py | 38 ++++++++++++++++++- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index c5bc92dcc17..c6e89f63bf0 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -54,7 +54,7 @@ class MaintenanceSchedule(TransactionBase): email_map[d.sales_person] = sp.get_email_id() except frappe.ValidationError: no_email_sp.append(d.sales_person) - + if no_email_sp: frappe.msgprint( frappe._("Setting Events to {0}, since the Employee attached to the below Sales Persons does not have a User ID{1}").format( @@ -66,17 +66,17 @@ class MaintenanceSchedule(TransactionBase): parent=%s""", (d.sales_person, d.item_code, self.name), as_dict=1) for key in scheduled_date: - description =frappe._("Reference: {0}, Item Code: {1} and Customer: {2}").format(self.name, d.item_code, self.customer) - frappe.get_doc({ + description =frappe._("Reference: {0}, Item Code: {1} and Customer: {2}").format(self.name, d.item_code, self.customer) + event = frappe.get_doc({ "doctype": "Event", "owner": email_map.get(d.sales_person, self.owner), "subject": description, "description": description, "starts_on": cstr(key["scheduled_date"]) + " 10:00:00", "event_type": "Private", - "ref_type": self.doctype, - "ref_name": self.name - }).insert(ignore_permissions=1) + }) + event.add_participant(self.doctype, self.name) + event.insert(ignore_permissions=1) frappe.db.set(self, 'status', 'Submitted') diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py index d8ae17b4c7f..3c307e920fc 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py @@ -2,6 +2,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals +from frappe.utils.data import get_datetime, add_days import frappe import unittest @@ -9,4 +10,39 @@ import unittest # test_records = frappe.get_test_records('Maintenance Schedule') class TestMaintenanceSchedule(unittest.TestCase): - pass + def test_events_should_be_created_and_deleted(self): + ms = make_maintenance_schedule() + ms.generate_schedule() + ms.submit() + + all_events = get_events(ms) + self.assertTrue(len(all_events) > 0) + + ms.cancel() + events_after_cancel = get_events(ms) + self.assertTrue(len(events_after_cancel) == 0) + +def get_events(ms): + return frappe.get_all("Event Participants", filters={ + "reference_doctype": ms.doctype, + "reference_docname": ms.name, + "parenttype": "Event" + }) + +def make_maintenance_schedule(): + ms = frappe.new_doc("Maintenance Schedule") + ms.company = "_Test Company" + ms.customer = "_Test Customer" + ms.transaction_date = get_datetime() + + ms.append("items", { + "item_code": "_Test Item", + "start_date": get_datetime(), + "end_date": add_days(get_datetime(), 32), + "periodicity": "Weekly", + "no_of_visits": 4, + "sales_person": "Sales Team", + }) + ms.insert(ignore_permissions=True) + + return ms From 3992ddedd074d1796e9b1523833c93cc06d8c80a Mon Sep 17 00:00:00 2001 From: Marica Date: Wed, 2 Sep 2020 11:10:46 +0530 Subject: [PATCH 44/71] fix: General Ledger filter validation (#23230) --- erpnext/accounts/report/general_ledger/general_ledger.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index bb9cfcd886f..ec007061bfd 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -43,8 +43,11 @@ def execute(filters=None): def validate_filters(filters, account_details): - if not filters.get('company'): - frappe.throw(_('{0} is mandatory').format(_('Company'))) + if not filters.get("company"): + frappe.throw(_("{0} is mandatory").format(_("Company"))) + + if not filters.get("from_date") and not filters.get("to_date"): + frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date")))) if filters.get("account") and not account_details.get(filters.account): frappe.throw(_("Account {0} does not exists").format(filters.account)) From 6b785cf52202ed639527efda16ebaac37a6a4ab4 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 2 Sep 2020 15:45:02 +0530 Subject: [PATCH 45/71] chore: Tests for Stock Entry --- .../doctype/stock_entry/test_stock_entry.py | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 2afabe1480d..6098038e74e 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -483,6 +483,100 @@ class TestStockEntry(unittest.TestCase): serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] self.assertFalse(frappe.db.get_value("Serial No", serial_no, "warehouse")) + def test_serial_batch_item_stock_entry(self): + """ + Behaviour: 1) Submit Stock Entry (Receipt) with Serial & Batched Item + 2) Cancel same Stock Entry + Expected Result: 1) Batch is created with Reference in Serial No + 2) Batch is deleted and Serial No is Inactive + """ + from erpnext.stock.doctype.batch.batch import get_batch_qty + + item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'}) + if not item: + item = create_item("Batched and Serialised Item") + item.has_batch_no = 1 + item.create_new_batch = 1 + item.has_serial_no = 1 + item.batch_number_series = "B-BATCH-.##" + item.serial_no_series = "S-.####" + item.save() + else: + item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'}) + + se = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100) + batch_no = se.items[0].batch_no + serial_no = get_serial_nos(se.items[0].serial_no)[0] + batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code) + + batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no") + self.assertEqual(batch_in_serial_no, batch_no) + + self.assertEqual(batch_qty, 1) + + se.cancel() + + batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no") + self.assertEqual(batch_in_serial_no, None) + + self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Inactive") + self.assertEqual(frappe.db.exists("Batch", batch_no), None) + + def test_serial_batch_item_qty_deduction(self): + """ + Behaviour: Create 2 Stock Entries, both adding Serial Nos to same batch + Expected Result: 1) Cancelling first Stock Entry (origin transaction of created batch) + should throw a Link Exists Error + 2) Cancelling second Stock Entry should make Serial Nos that are, linked to mentioned batch + and in that transaction only, Inactive. + """ + from erpnext.stock.doctype.batch.batch import get_batch_qty + + item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'}) + if not item: + item = create_item("Batched and Serialised Item") + item.has_batch_no = 1 + item.create_new_batch = 1 + item.has_serial_no = 1 + item.batch_number_series = "B-BATCH-.##" + item.serial_no_series = "S-.####" + item.save() + else: + item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'}) + + se1 = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100) + batch_no = se1.items[0].batch_no + serial_no1 = get_serial_nos(se1.items[0].serial_no)[0] + + # Check Source (Origin) Document of Batch + self.assertEqual(frappe.db.get_value("Batch", batch_no, "reference_name"), se1.name) + + se2 = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100, + batch_no=batch_no) + serial_no2 = get_serial_nos(se2.items[0].serial_no)[0] + + batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code) + self.assertEqual(batch_qty, 2) + frappe.db.commit() + + # Cancelling Origin Document + self.assertRaises(frappe.LinkExistsError, se1.cancel) + frappe.db.rollback() + + se2.cancel() + + # Check decrease in Batch Qty + batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code) + self.assertEqual(batch_qty, 1) + + # Check if Serial No from Stock Entry 1 is intact + self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "batch_no"), batch_no) + self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "status"), "Active") + + # Check id Serial No from Stock Entry 2 is Unlinked and Inactive + self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "batch_no"), None) + self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "status"), "Inactive") + def test_warehouse_company_validation(self): company = frappe.db.get_value('Warehouse', '_Test Warehouse 2 - _TC1', 'company') set_perpetual_inventory(0, company) From f30181527fc8ffb8647b97055b4a440d974d4849 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 2 Sep 2020 17:08:28 +0530 Subject: [PATCH 46/71] fix: pos item name special character issue --- erpnext/accounts/page/pos/pos.js | 2 ++ erpnext/public/js/pos/pos_bill_item_new.html | 2 +- erpnext/public/js/pos/pos_item.html | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js index 1ed3f2341eb..cecf7f5e6fc 100755 --- a/erpnext/accounts/page/pos/pos.js +++ b/erpnext/accounts/page/pos/pos.js @@ -1064,6 +1064,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ $(frappe.render_template("pos_item", { item_code: escape(obj.name), item_price: item_price, + title: obj.name || obj.item_name, item_name: obj.name === obj.item_name ? "" : obj.item_name, item_image: obj.image, item_stock: __('Stock Qty') + ": " + me.get_actual_qty(obj), @@ -1545,6 +1546,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ $.each(this.frm.doc.items || [], function (i, d) { $(frappe.render_template("pos_bill_item_new", { item_code: escape(d.item_code), + title: d.item_code || d.item_name, item_name: (d.item_name === d.item_code || !d.item_name) ? "" : ("
" + d.item_name), qty: d.qty, discount_percentage: d.discount_percentage || 0.0, diff --git a/erpnext/public/js/pos/pos_bill_item_new.html b/erpnext/public/js/pos/pos_bill_item_new.html index cb626cefcea..b365845d518 100644 --- a/erpnext/public/js/pos/pos_bill_item_new.html +++ b/erpnext/public/js/pos/pos_bill_item_new.html @@ -1,7 +1,7 @@
{%= qty %}
{%= discount_percentage %}
diff --git a/erpnext/public/js/pos/pos_item.html b/erpnext/public/js/pos/pos_item.html index 52f3cf698ae..5b1bb3c167b 100755 --- a/erpnext/public/js/pos/pos_item.html +++ b/erpnext/public/js/pos/pos_item.html @@ -1,12 +1,12 @@
Date: Thu, 3 Sep 2020 14:41:05 +0530 Subject: [PATCH 48/71] fix: Item Alternative Test --- .../doctype/item_alternative/test_item_alternative.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py index f045e4f9114..61d90392364 100644 --- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py @@ -13,6 +13,7 @@ from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_ord from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt, make_rm_stock_entry import unittest from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory +from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError class TestItemAlternative(unittest.TestCase): def setUp(self): @@ -110,8 +111,11 @@ def make_items(): if not frappe.db.exists('Item', item_code): create_item(item_code) - create_stock_reconciliation(item_code="Test FG A RW 1", - warehouse='_Test Warehouse - _TC', qty=10, rate=2000) + try: + create_stock_reconciliation(item_code="Test FG A RW 1", + warehouse='_Test Warehouse - _TC', qty=10, rate=2000) + except EmptyStockReconciliationItemsError: + pass if frappe.db.exists('Item', 'Test FG A RW 1'): doc = frappe.get_doc('Item', 'Test FG A RW 1') From 5c1f11e5615eced97a3a110a9219ac642cb3a510 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 28 Aug 2020 12:21:10 +0530 Subject: [PATCH 49/71] fix: Raise Error on over receipt/consumption for sub-contrcated PR --- erpnext/controllers/buying_controller.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index c7e6bcf4d65..aef7bcd156b 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -267,6 +267,9 @@ class BuyingController(StockController): qty_to_be_received_map = get_qty_to_be_received(purchase_orders) for item in self.get('items'): + if not item.purchase_order: + continue + # reset raw_material cost item.rm_supp_cost = 0 @@ -279,6 +282,12 @@ class BuyingController(StockController): fg_yet_to_be_received = qty_to_be_received_map.get(item_key) + if not fg_yet_to_be_received: + frappe.throw(_("Row #{0}: Item {1} is already fully received in Purchase Order {2}") + .format(item.idx, frappe.bold(item.item_code), + frappe.utils.get_link_to_form("Purchase Order", item.purchase_order)), + title=_("Limit Crossed")) + transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code) backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code) From 8781e771328c8354958faa49bca1586e37ce9111 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 28 Aug 2020 14:09:02 +0530 Subject: [PATCH 50/71] fix: Test for Over Receipt via PRs on a PO --- .../purchase_receipt/test_purchase_receipt.py | 68 ++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index e2b636eaaa6..8008b48850c 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import unittest +import json import frappe, erpnext import frappe.defaults from frappe.utils import cint, flt, cstr, today, random_string @@ -130,13 +131,78 @@ class TestPurchaseReceipt(unittest.TestCase): qty=100, basic_rate=100, company="_Test Company with perpetual inventory") pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=0, is_subcontracted="Yes", company="_Test Company with perpetual inventory", warehouse='Stores - TCP1', supplier_warehouse='Work In Progress - TCP1') - + gl_entries = get_gl_entries("Purchase Receipt", pr.name) self.assertFalse(gl_entries) set_perpetual_inventory(0) + def test_subcontracting_over_receipt(self): + """ + Behaviour: Raise multiple PRs against one PO that in total + receive more than the required qty in the PO. + Expected Result: Error Raised for Over Receipt against PO. + """ + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + from erpnext.buying.doctype.purchase_order.test_purchase_order import (update_backflush_based_on, + make_subcontracted_item, create_purchase_order) + from erpnext.buying.doctype.purchase_order.purchase_order import (make_purchase_receipt, + make_rm_stock_entry as make_subcontract_transfer_entry) + + update_backflush_based_on("Material Transferred for Subcontract") + item_code = "_Test Subcontracted FG Item 1" + make_subcontracted_item(item_code) + + po = create_purchase_order(item_code=item_code, qty=1, + is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") + + #stock raw materials in a warehouse before transfer + make_stock_entry(target="_Test Warehouse - _TC", + item_code="_Test Item Home Desktop 100", qty=1, basic_rate=100) + make_stock_entry(target="_Test Warehouse - _TC", + item_code = "Test Extra Item 1", qty=1, basic_rate=100) + make_stock_entry(target="_Test Warehouse - _TC", + item_code = "_Test Item", qty=1, basic_rate=100) + + rm_items = [ + { + "item_code": item_code, + "rm_item_code": po.supplied_items[0].rm_item_code, + "item_name": "_Test Item", + "qty": po.supplied_items[0].required_qty, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos" + }, + { + "item_code": item_code, + "rm_item_code": po.supplied_items[1].rm_item_code, + "item_name": "Test Extra Item 1", + "qty": po.supplied_items[1].required_qty, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos" + }, + { + "item_code": item_code, + "rm_item_code": po.supplied_items[2].rm_item_code, + "item_name": "_Test Item Home Desktop 100", + "qty": po.supplied_items[2].required_qty, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos" + } + ] + rm_item_string = json.dumps(rm_items) + se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) + se.to_warehouse = "_Test Warehouse 1 - _TC" + se.save() + se.submit() + + pr1 = make_purchase_receipt(po.name) + pr2 = make_purchase_receipt(po.name) + + pr1.submit() + self.assertRaises(frappe.ValidationError, pr2.submit) + def test_serial_no_supplier(self): pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"), From c407bb2385729844888fa436cd514ab1ad4386e4 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 5 Aug 2020 15:04:48 +0530 Subject: [PATCH 51/71] fix: Misleading filters on Item tax Template Link field --- erpnext/controllers/queries.py | 5 ++++- erpnext/public/js/controllers/transaction.js | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index b49198579b8..afc63fc8f51 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -613,9 +613,12 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters): if not taxes: return frappe.db.sql(""" SELECT name FROM `tabItem Tax Template` """) else: + valid_from = filters.get('valid_from') + valid_from = valid_from[1] if isinstance(valid_from, list) else valid_from + args = { 'item_code': filters.get('item_code'), - 'posting_date': filters.get('valid_from'), + 'posting_date': valid_from, 'tax_category': filters.get('tax_category') } diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 3c71a45309c..58fb8e17996 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1788,7 +1788,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }, set_query_for_item_tax_template: function(doc, cdt, cdn) { - var item = frappe.get_doc(cdt, cdn); if(!item.item_code) { frappe.throw(__("Please enter Item Code to get item taxes")); @@ -1796,7 +1795,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ let filters = { 'item_code': item.item_code, - 'valid_from': doc.transaction_date || doc.bill_date || doc.posting_date, + 'valid_from': ["<=", doc.transaction_date || doc.bill_date || doc.posting_date], 'item_group': item.item_group, } From 053fdd91da6dfee753f34618b0f18fc938e3be93 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 3 Sep 2020 17:11:59 +0530 Subject: [PATCH 52/71] fix: data was not properly maped --- erpnext/hr/doctype/attendance/attendance.py | 3 ++- erpnext/hr/doctype/attendance/attendance_calendar.js | 6 ------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index f23b4220849..73056f16ed1 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -82,7 +82,8 @@ def add_attendance(events, start, end, conditions=None): e = { "name": d.name, "doctype": "Attendance", - "date": d.attendance_date, + "start": d.attendance_date, + "end": d.attendance_date, "title": cstr(d.status), "docstatus": d.docstatus } diff --git a/erpnext/hr/doctype/attendance/attendance_calendar.js b/erpnext/hr/doctype/attendance/attendance_calendar.js index 104f09d69ff..45664896965 100644 --- a/erpnext/hr/doctype/attendance/attendance_calendar.js +++ b/erpnext/hr/doctype/attendance/attendance_calendar.js @@ -1,12 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt frappe.views.calendar["Attendance"] = { - field_map: { - "start": "attendance_date", - "end": "attendance_date", - "id": "name", - "docstatus": 1 - }, options: { header: { left: 'prev,next today', From b78d35ae1d9c8aa1c56686a8931acd252d566fc8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 3 Sep 2020 21:16:31 +0530 Subject: [PATCH 53/71] fix: Stock qty in HSN wise outward summary --- .../hsn_wise_summary_of_outward_supplies.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index 59389ce3269..4060a553bd8 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -43,6 +43,12 @@ def _execute(filters=None): data.append(row) added_item.append((d.parent, d.item_code)) + # gst is already added, just add qty and taxable value + else: + row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty, d.base_net_amount, d.base_net_amount] + for tax in tax_columns: + row += [0] + data.append(row) if data: data = get_merged_data(columns, data) # merge same hsn code data return columns, data From 551982441441768d5a026f002a89926450e29c09 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Fri, 4 Sep 2020 11:09:26 +0530 Subject: [PATCH 54/71] fix: profit and loss report not working --- erpnext/accounts/report/financial_statements.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 58117b68c52..9e3f3b739c2 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -14,7 +14,7 @@ import frappe, erpnext from erpnext.accounts.report.utils import get_currency, convert_to_presentation_currency from erpnext.accounts.utils import get_fiscal_year from frappe import _ -from frappe.utils import (flt, getdate, get_first_day, add_months, add_days, formatdate, cstr) +from frappe.utils import (flt, getdate, get_first_day, add_months, add_days, formatdate, cstr, cint) from six import itervalues from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions, get_dimension_with_children @@ -43,7 +43,7 @@ def get_period_list(from_fiscal_year, to_fiscal_year, periodicity, accumulated_v start_date = year_start_date months = get_months(year_start_date, year_end_date) - for i in range(math.ceil(months / months_to_add)): + for i in range(cint(math.ceil(months / months_to_add))): period = frappe._dict({ "from_date": start_date }) From 9260cf11b6bf53e897336dd651b8b4648e9db639 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Fri, 4 Sep 2020 15:10:50 +0530 Subject: [PATCH 55/71] fix: SE quantity data type issue --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 0174858dc36..7b632415e18 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -500,7 +500,7 @@ class StockEntry(StockController): d.basic_amount = flt((raw_material_cost - scrap_material_cost), d.precision("basic_amount")) elif self.purpose == "Repack" and total_fg_qty and not d.set_basic_rate_manually: d.basic_rate = flt(raw_material_cost) / flt(total_fg_qty) - d.basic_amount = d.basic_rate * d.qty + d.basic_amount = d.basic_rate * flt(d.qty) def distribute_additional_costs(self): if self.purpose == "Material Issue": From 4f3eb3fcebbffb5b62ec5adf53f9bdeaef84b062 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 4 Sep 2020 15:20:37 +0530 Subject: [PATCH 56/71] fix: not able to make material request from SO --- .../doctype/sales_order/sales_order.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index f88289871e9..ed3a96446a0 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import json import frappe.utils -from frappe.utils import cstr, flt, getdate, cint, nowdate, add_days, get_link_to_form +from frappe.utils import cstr, flt, getdate, cint, nowdate, add_days, get_link_to_form, strip_html from frappe import _ from six import string_types from frappe.model.utils import get_fetch_values @@ -994,15 +994,20 @@ def make_raw_material_request(items, company, sales_order, project=None): )) for item in raw_materials: item_doc = frappe.get_cached_doc('Item', item.get('item_code')) + schedule_date = add_days(nowdate(), cint(item_doc.lead_time_days)) - material_request.append('items', { - 'item_code': item.get('item_code'), - 'qty': item.get('quantity'), - 'schedule_date': schedule_date, - 'warehouse': item.get('warehouse'), - 'sales_order': sales_order, - 'project': project + row = material_request.append('items', { + 'item_code': item.get('item_code'), + 'qty': item.get('quantity'), + 'schedule_date': schedule_date, + 'warehouse': item.get('warehouse'), + 'sales_order': sales_order, + 'project': project }) + + if not (strip_html(item.get("description")) and strip_html(item_doc.description)): + row.description = item_doc.item_name or item.get('item_code') + material_request.insert() material_request.flags.ignore_permissions = 1 material_request.run_method("set_missing_values") From 785b7b3359dd81064835b1356e0531704fcfc3eb Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 7 Sep 2020 21:47:39 +0530 Subject: [PATCH 57/71] fix: incorrect payment entry status --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 9 ++++++--- erpnext/patches.txt | 1 + erpnext/patches/v12_0/update_payment_entry_status.py | 7 +++++++ 3 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 erpnext/patches/v12_0/update_payment_entry_status.py diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index c17f775a4e7..d1630f3ea69 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -84,7 +84,7 @@ class PaymentEntry(AccountsController): self.delink_advance_entry_references() self.update_payment_schedule(cancel=1) self.set_payment_req_status() - self.set_status() + self.set_status(update=True) def set_payment_req_status(self): from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status @@ -279,7 +279,7 @@ class PaymentEntry(AccountsController): outstanding_amount, is_return = frappe.get_cached_value(d.reference_doctype, d.reference_name, ["outstanding_amount", "is_return"]) if outstanding_amount <= 0 and not is_return: no_oustanding_refs.setdefault(d.reference_doctype, []).append(d) - + for k, v in no_oustanding_refs.items(): frappe.msgprint(_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.

\ If this is undesirable please cancel the corresponding Payment Entry.") @@ -340,7 +340,7 @@ class PaymentEntry(AccountsController): frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` + %s WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0])) - def set_status(self): + def set_status(self, update=False): if self.docstatus == 2: self.status = 'Cancelled' elif self.docstatus == 1: @@ -348,6 +348,9 @@ class PaymentEntry(AccountsController): else: self.status = 'Draft' + if update: + self.db_set('status', self.status) + def set_amounts(self): self.set_amounts_in_company_currency() self.set_total_allocated_amount() diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 831541ccb1f..afb6db35f27 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -677,3 +677,4 @@ erpnext.patches.v12_0.set_multi_uom_in_rfq erpnext.patches.v12_0.update_state_code_for_daman_and_diu erpnext.patches.v12_0.rename_lost_reason_detail erpnext.patches.v12_0.update_leave_application_status +erpnext.patches.v12_0.update_payment_entry_status \ No newline at end of file diff --git a/erpnext/patches/v12_0/update_payment_entry_status.py b/erpnext/patches/v12_0/update_payment_entry_status.py new file mode 100644 index 00000000000..8d033800af3 --- /dev/null +++ b/erpnext/patches/v12_0/update_payment_entry_status.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc("accounts", "doctype", "payment_entry") + + frappe.db.sql(""" UPDATE `tabPayment Entry` set status = 'Cancelled' WHERE docstatus = 2 """) \ No newline at end of file From 47c535399c09cb45f9acafe7626f39bf9d9cded4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 8 Sep 2020 10:30:02 +0530 Subject: [PATCH 58/71] fix: Check if subscription is already created for current invoicing period (#23143) * fix: Check if subscription is already created for current invoicing period * fix: Test for duplicate subscription * fix: invoice generation at the begining of period fix * fix: invoice generation at the begining of period fix * fix: Remove unwanted file * fix: Generate new invoices even though current invoices are unpaid * fix: Make invoices table read-only * fix: Update test cases --- .../doctype/subscription/subscription.json | 7 ++--- .../doctype/subscription/subscription.py | 23 +++++++++++---- .../doctype/subscription/test_subscription.py | 29 +++++++++++++++---- 3 files changed, 44 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/subscription/subscription.json b/erpnext/accounts/doctype/subscription/subscription.json index 32b97ba80b5..c17f3aeb846 100644 --- a/erpnext/accounts/doctype/subscription/subscription.json +++ b/erpnext/accounts/doctype/subscription/subscription.json @@ -1,5 +1,4 @@ { - "actions": [], "autoname": "ACC-SUB-.YYYY.-.#####", "creation": "2017-07-18 17:50:43.967266", "doctype": "DocType", @@ -184,7 +183,8 @@ "fieldname": "invoices", "fieldtype": "Table", "label": "Invoices", - "options": "Subscription Invoice" + "options": "Subscription Invoice", + "read_only": 1 }, { "collapsible": 1, @@ -197,8 +197,7 @@ "fieldtype": "Column Break" } ], - "links": [], - "modified": "2020-01-27 14:37:32.845173", + "modified": "2020-08-27 23:30:02.504042", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription", diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 98d07c71a3a..bc34816e375 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -326,8 +326,7 @@ class Subscription(Document): def is_postpaid_to_invoice(self): return getdate(nowdate()) > getdate(self.current_invoice_end) or \ - (getdate(nowdate()) >= getdate(self.current_invoice_end) and getdate(self.current_invoice_end) == getdate(self.current_invoice_start)) and \ - not self.has_outstanding_invoice() + (getdate(nowdate()) >= getdate(self.current_invoice_end) and getdate(self.current_invoice_end) == getdate(self.current_invoice_start)) def is_prepaid_to_invoice(self): if not self.generate_invoice_at_period_start: @@ -337,8 +336,16 @@ class Subscription(Document): return True # Check invoice dates and make sure it doesn't have outstanding invoices - return getdate(nowdate()) >= getdate(self.current_invoice_start) and not self.has_outstanding_invoice() - + return getdate(nowdate()) >= getdate(self.current_invoice_start) + + def is_current_invoice_generated(self): + invoice = self.get_current_invoice() + + if invoice and getdate(self.current_invoice_start) <= getdate(invoice.posting_date) <= getdate(self.current_invoice_end): + return True + + return False + def is_current_invoice_paid(self): if self.is_new_subscription(): return False @@ -346,7 +353,7 @@ class Subscription(Document): last_invoice = frappe.get_doc('Sales Invoice', self.invoices[-1].invoice) if getdate(last_invoice.posting_date) == getdate(self.current_invoice_start) and last_invoice.status == 'Paid': return True - + return False def process_for_active(self): @@ -358,7 +365,8 @@ class Subscription(Document): 2. Change the `Subscription` status to 'Past Due Date' 3. Change the `Subscription` status to 'Cancelled' """ - if not self.is_current_invoice_paid() and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()): + if not self.is_current_invoice_generated() and not self.is_current_invoice_paid() and \ + (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()): self.generate_invoice() if self.current_invoice_is_past_due(): self.status = 'Past Due Date' @@ -369,6 +377,9 @@ class Subscription(Document): if self.cancel_at_period_end and getdate(nowdate()) > getdate(self.current_invoice_end): self.cancel_subscription_at_period_end() + if self.is_current_invoice_generated() and getdate() > getdate(self.current_invoice_end): + self.update_subscription_period(add_days(self.current_invoice_end, 1)) + def cancel_subscription_at_period_end(self): """ Called when `Subscription.cancel_at_period_end` is truthy diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index 3d96f233b40..e38de252699 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -101,19 +101,19 @@ class TestSubscription(unittest.TestCase): subscription.delete() def test_invoice_is_generated_at_end_of_billing_period(self): + start_date = add_to_date(nowdate(), months=-1) subscription = frappe.new_doc('Subscription') subscription.customer = '_Test Customer' - subscription.start = '2018-01-01' + subscription.start = start_date subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) subscription.insert() self.assertEqual(subscription.status, 'Active') - self.assertEqual(subscription.current_invoice_start, '2018-01-01') - self.assertEqual(subscription.current_invoice_end, '2018-01-31') + self.assertEqual(subscription.current_invoice_start, start_date) + self.assertEqual(subscription.current_invoice_end, add_days(nowdate(), -1)) subscription.process() self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.current_invoice_start, '2018-01-01') self.assertEqual(subscription.status, 'Past Due Date') subscription.delete() @@ -137,7 +137,6 @@ class TestSubscription(unittest.TestCase): subscription.process() self.assertEqual(subscription.status, 'Active') - self.assertEqual(subscription.current_invoice_start, add_months(subscription.start, 1)) self.assertEqual(len(subscription.invoices), 1) subscription.delete() @@ -538,3 +537,23 @@ class TestSubscription(unittest.TestCase): settings.save() subscription.delete() + + def test_duplicate_invoice_check(self): + subscription = frappe.new_doc('Subscription') + subscription.customer = '_Test Customer' + subscription.generate_invoice_at_period_start = True + subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription.start = nowdate() + subscription.save() + + # Generate invoice for the current invoicing period + subscription.process() + subscription.load_from_db() + self.assertEqual(len(subscription.invoices), 1) + + # Proccess subscription again for the same period + subscription.process() + subscription.load_from_db() + + # No new invoice should be created for current period + self.assertEqual(len(subscription.invoices), 1) From 5096639591ae055336ee7ff736bb21286da7245f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 8 Sep 2020 11:58:07 +0530 Subject: [PATCH 59/71] fix: Lock row in subquery while setting delivered qty (#23101) --- erpnext/controllers/status_updater.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index b465a106f0e..da99f1267f6 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -249,7 +249,7 @@ class StatusUpdater(Document): args['second_source_condition'] = """ + ifnull((select sum(%(second_source_field)s) from `tab%(second_source_dt)s` where `%(second_join_field)s`="%(detail_id)s" - and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s), 0) """ % args + and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s FOR UPDATE), 0) """ % args if args['detail_id']: if not args.get("extra_cond"): args["extra_cond"] = "" From d44052441b4ecd6f98c47450e55065eb2f199069 Mon Sep 17 00:00:00 2001 From: Afshan Date: Tue, 8 Sep 2020 13:03:37 +0530 Subject: [PATCH 60/71] fix: returned empty list if non US based company --- erpnext/regional/report/irs_1099/irs_1099.py | 5 +++++ erpnext/regional/united_states/test_united_states.py | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/report/irs_1099/irs_1099.py b/erpnext/regional/report/irs_1099/irs_1099.py index 67834d12210..ceea8460bea 100644 --- a/erpnext/regional/report/irs_1099/irs_1099.py +++ b/erpnext/regional/report/irs_1099/irs_1099.py @@ -19,6 +19,11 @@ def execute(filters=None): if not filters: filters.setdefault('fiscal_year', get_fiscal_year(nowdate())[0]) filters.setdefault('company', frappe.db.get_default("company")) + + region = frappe.db.get_value("Company", fieldname = ["country"], filters = { "name": filters.company }) + if region != 'United States': + return [],[] + data = [] columns = get_columns() data = frappe.db.sql(""" diff --git a/erpnext/regional/united_states/test_united_states.py b/erpnext/regional/united_states/test_united_states.py index 688f14576c8..ad95010a9ac 100644 --- a/erpnext/regional/united_states/test_united_states.py +++ b/erpnext/regional/united_states/test_united_states.py @@ -24,7 +24,7 @@ class TestUnitedStates(unittest.TestCase): def test_irs_1099_report(self): make_payment_entry_to_irs_1099_supplier() - filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company"}) + filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company 1"}) columns, data = execute_1099_report(filters) print(columns, data) expected_row = {'supplier': '_US 1099 Test Supplier', @@ -42,10 +42,10 @@ def make_payment_entry_to_irs_1099_supplier(): pe = frappe.new_doc("Payment Entry") pe.payment_type = "Pay" - pe.company = "_Test Company" + pe.company = "_Test Company 1" pe.posting_date = "2016-01-10" - pe.paid_from = "_Test Bank USD - _TC" - pe.paid_to = "_Test Payable USD - _TC" + pe.paid_from = "_Test Bank USD - _TC1" + pe.paid_to = "_Test Payable USD - _TC1" pe.paid_amount = 100 pe.received_amount = 100 pe.reference_no = "For IRS 1099 testing" From f1471ecbd4727ebb14e5e8882b819e382f9b08c5 Mon Sep 17 00:00:00 2001 From: Afshan Date: Tue, 8 Sep 2020 13:50:54 +0530 Subject: [PATCH 61/71] fix: removed ignore permission flag --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index b854f27968f..4d19a7c4537 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -1019,7 +1019,7 @@ def make_inter_company_journal_entry(name, voucher_type, company): return journal_entry.as_dict() @frappe.whitelist() -def make_reverse_journal_entry(source_name, target_doc=None, ignore_permissions=False): +def make_reverse_journal_entry(source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc def update_accounts(source, target, source_parent): @@ -1045,6 +1045,6 @@ def make_reverse_journal_entry(source_name, target_doc=None, ignore_permissions= }, "postprocess": update_accounts, }, - }, target_doc, ignore_permissions=ignore_permissions) + }, target_doc) return doclist \ No newline at end of file From e5fbebf94658971549eae600ba503ae775a616a7 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Tue, 8 Sep 2020 15:55:07 +0530 Subject: [PATCH 62/71] fix: update filters --- erpnext/hr/doctype/leave_application/leave_application.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 95fedde420f..6c42c4752b7 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -433,7 +433,8 @@ def get_leave_details(employee, date): 'from_date': ('<=', date), 'to_date': ('>=', date), 'leave_type': allocation.leave_type, - 'employee': employee + 'employee': employee, + 'docstatus': 1 }, 'SUM(total_leaves_allocated)') or 0 remaining_leaves = get_leave_balance_on(employee, d, date, to_date = allocation.to_date, @@ -791,4 +792,4 @@ def get_leave_approver(employee): leave_approver = frappe.db.get_value('Department Approver', {'parent': department, 'parentfield': 'leave_approvers', 'idx': 1}, 'approver') - return leave_approver \ No newline at end of file + return leave_approver From d409f914694d225508041c7f537f0db4d6743a7e Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 9 Sep 2020 10:53:48 +0530 Subject: [PATCH 63/71] fix: asset movement date for backdated asset entry (#23300) --- erpnext/assets/doctype/asset/asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index bfb44e5c30a..2a9e4db32b9 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -160,7 +160,7 @@ class Asset(AccountsController): 'assets': assets, 'purpose': 'Receipt', 'company': self.company, - 'transaction_date': getdate(nowdate()), + 'transaction_date': getdate(self.purchase_date), 'reference_doctype': reference_doctype, 'reference_name': reference_docname }).insert() From 815416adc582109a6b3b736ab652e74cec367cca Mon Sep 17 00:00:00 2001 From: marination Date: Sat, 12 Sep 2020 16:17:13 +0530 Subject: [PATCH 64/71] fix: Make Reference fields mandatory in Quality Inspection --- .../doctype/quality_inspection/quality_inspection.json | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json index a9f3cd09ef5..b99e98b65c1 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json @@ -72,7 +72,8 @@ "fieldname": "reference_type", "fieldtype": "Select", "label": "Reference Type", - "options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry" + "options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry", + "reqd": 1 }, { "fieldname": "reference_name", @@ -83,7 +84,8 @@ "label": "Reference Name", "oldfieldname": "purchase_receipt_no", "oldfieldtype": "Link", - "options": "reference_type" + "options": "reference_type", + "reqd": 1 }, { "fieldname": "section_break_7", @@ -230,8 +232,10 @@ ], "icon": "fa fa-search", "idx": 1, + "index_web_pages_for_search": 1, "is_submittable": 1, - "modified": "2019-07-12 12:07:23.153698", + "links": [], + "modified": "2020-09-12 16:11:31.910508", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection", From a34a490e0cbbb4690bfb8ec01adc32c19c92b78a Mon Sep 17 00:00:00 2001 From: marination Date: Sat, 12 Sep 2020 15:39:27 +0530 Subject: [PATCH 65/71] fix: Make sure Supplier/Customer is selected before fetching Items. --- erpnext/stock/doctype/delivery_note/delivery_note.js | 8 +++++++- .../stock/doctype/purchase_receipt/purchase_receipt.js | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 62aebbaf504..6be20a2e71d 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -121,12 +121,18 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( if (this.frm.doc.docstatus===0) { this.frm.add_custom_button(__('Sales Order'), function() { + if (!me.frm.doc.customer) { + frappe.throw({ + title: __("Mandatory"), + message: __("Please Select a Customer") + }); + } erpnext.utils.map_current_doc({ method: "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note", source_doctype: "Sales Order", target: me.frm, setters: { - customer: me.frm.doc.customer || undefined, + customer: me.frm.doc.customer, }, get_query_filters: { docstatus: 1, diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 89ca1bef856..27946586eaa 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -101,12 +101,18 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend if (this.frm.doc.docstatus == 0) { this.frm.add_custom_button(__('Purchase Order'), function () { + if (!me.frm.doc.supplier) { + frappe.throw({ + title: __("Mandatory"), + message: __("Please Select a Supplier") + }); + } erpnext.utils.map_current_doc({ method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt", source_doctype: "Purchase Order", target: me.frm, setters: { - supplier: me.frm.doc.supplier || undefined, + supplier: me.frm.doc.supplier, }, get_query_filters: { docstatus: 1, From 1e981b50d5f6d3cdbb00ae46a2004034ef5104c7 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 14 Sep 2020 19:53:52 +0530 Subject: [PATCH 66/71] Update items with workflow v12 (#23324) * feat: validate workflow before so/po update items * fix: incorrect workflow validation on so/po update items --- erpnext/controllers/accounts_controller.py | 50 ++++++++++---- .../doctype/sales_order/test_sales_order.py | 69 +++++++++++++++++++ 2 files changed, 106 insertions(+), 13 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 4f34f7f3ce4..fc535074005 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -7,6 +7,7 @@ import json from frappe import _, throw from frappe.utils import (today, flt, cint, fmt_money, formatdate, getdate, add_days, add_months, get_last_day, nowdate, get_link_to_form) +from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied, WorkflowPermissionError from erpnext.stock.get_item_details import get_conversion_factor, get_item_details from erpnext.setup.utils import get_exchange_rate from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_account_currency @@ -1163,7 +1164,7 @@ 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): +def validate_and_delete_children(parent, data): deleted_children = [] updated_item_names = [d.get("docname") for d in data] for item in parent.items: @@ -1190,18 +1191,37 @@ def check_and_delete_children(parent, data): @frappe.whitelist() def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): - def check_permissions(doc, perm_type='create'): + def check_doc_permissions(doc, perm_type='create'): try: doc.check_permission(perm_type) - except: - action = "add" if perm_type == 'create' else "update" - frappe.throw(_("You do not have permissions to {} items in a Sales Order.").format(action), title=_("Insufficient Permissions")) + except frappe.PermissionError: + actions = { 'create': 'add', 'write': 'update', 'cancel': 'remove' } + + frappe.throw(_("You do not have permissions to {} items in a {}.") + .format(actions[perm_type], parent_doctype), title=_("Insufficient Permissions")) + + def validate_workflow_conditions(doc): + workflow = get_workflow_name(doc.doctype) + if not workflow: + return + + workflow_doc = frappe.get_doc("Workflow", workflow) + current_state = doc.get(workflow_doc.workflow_state_field) + roles = frappe.get_roles() + + transitions = [] + for transition in workflow_doc.transitions: + if transition.next_state == current_state and transition.allowed in roles: + if not is_transition_condition_satisfied(transition, doc): + continue + transitions.append(transition.as_dict()) + + if not transitions: + frappe.throw(_("You do not have workflow access to update this document."), title=_("Insufficient Workflow Permissions")) def get_new_child_item(item_row): - if parent_doctype == "Sales Order": - return set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_row) - if parent_doctype == "Purchase Order": - return set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_row) + new_child_function = set_sales_order_defaults if parent_doctype == "Sales Order" else set_purchase_order_defaults + return new_child_function(parent_doctype, parent_doctype_name, child_docname, item_row) def validate_quantity(child_item, d): if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty): @@ -1214,17 +1234,18 @@ 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) + + check_doc_permissions(parent, 'cancel') + validate_and_delete_children(parent, data) for d in data: new_child_flag = False if not d.get("docname"): new_child_flag = True - check_permissions(parent, 'create') + check_doc_permissions(parent, 'create') child_item = get_new_child_item(d) else: - check_permissions(parent, 'write') + check_doc_permissions(parent, 'write') child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname")) prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate")) @@ -1331,6 +1352,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.update_prevdoc_status('submit') parent.update_delivery_status() + parent.reload() + validate_workflow_conditions(parent) + parent.update_blanket_order() parent.update_billing_percentage() parent.set_status() diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 5e360902e83..254f3c1993a 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -416,7 +416,42 @@ class TestSalesOrder(unittest.TestCase): # add new item trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 100, 'qty' : 2}]) self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) + test_user.remove_roles("Accounts User") frappe.set_user("Administrator") + + def test_update_child_qty_rate_with_workflow(self): + from frappe.model.workflow import apply_workflow + + workflow = make_sales_order_workflow() + so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1) + apply_workflow(so, 'Approve') + + frappe.set_user("Administrator") + user = 'test@example.com' + test_user = frappe.get_doc('User', user) + test_user.add_roles("Sales User", "Test Junior Approver") + frappe.set_user(user) + + # user shouldn't be able to edit since grand_total will become > 200 if qty is doubled + trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 150, 'qty' : 2, 'docname': so.items[0].name}]) + self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Sales Order', trans_item, so.name) + + frappe.set_user("Administrator") + user2 = 'test2@example.com' + test_user2 = frappe.get_doc('User', user2) + test_user2.add_roles("Sales User", "Test Approver") + frappe.set_user(user2) + + # Test Approver is allowed to edit with grand_total > 200 + update_child_qty_rate("Sales Order", trans_item, so.name) + so.reload() + self.assertEqual(so.items[0].qty, 2) + + frappe.set_user("Administrator") + test_user.remove_roles("Sales User", "Test Junior Approver", "Test Approver") + test_user2.remove_roles("Sales User", "Test Junior Approver", "Test Approver") + workflow.is_active = 0 + workflow.save() def test_update_child_qty_rate_product_bundle(self): # test Update Items with product bundle @@ -953,3 +988,37 @@ def get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"): "reserved_qty")) test_dependencies = ["Currency Exchange"] + +def make_sales_order_workflow(): + if frappe.db.exists('Workflow', 'SO Test Workflow'): + doc = frappe.get_doc("Workflow", "SO Test Workflow") + doc.set("is_active", 1) + doc.save() + return doc + + frappe.get_doc(dict(doctype='Role', role_name='Test Junior Approver')).insert(ignore_if_duplicate=True) + frappe.get_doc(dict(doctype='Role', role_name='Test Approver')).insert(ignore_if_duplicate=True) + frappe.db.commit() + frappe.cache().hdel('roles', frappe.session.user) + + workflow = frappe.get_doc({ + "doctype": "Workflow", + "workflow_name": "SO Test Workflow", + "document_type": "Sales Order", + "workflow_state_field": "workflow_state", + "is_active": 1, + "send_email_alert": 0, + }) + workflow.append('states', dict( state = 'Pending', allow_edit = 'All' )) + workflow.append('states', dict( state = 'Approved', allow_edit = 'Test Approver', doc_status = 1 )) + workflow.append('transitions', dict( + state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Junior Approver', allow_self_approval = 1, + condition = 'doc.grand_total < 200' + )) + workflow.append('transitions', dict( + state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Approver', allow_self_approval = 1, + condition = 'doc.grand_total > 200' + )) + workflow.insert(ignore_permissions=True) + + return workflow \ No newline at end of file From 444f657d25591678aea15e562594d8f9bf623b73 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 14 Sep 2020 20:51:20 +0530 Subject: [PATCH 67/71] fix: production plan incorrect work order qty (#23264) * fix: production plan incorrect work order qty * added test case * Update test_production_plan.py --- .../production_plan/production_plan.py | 11 ++--- .../production_plan/test_production_plan.py | 44 ++++++++++++++++++- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 8ff691cc300..02dfabe6f70 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -322,12 +322,13 @@ class ProductionPlan(Document): work_orders = [] bom_data = {} - get_sub_assembly_items(item.get("bom_no"), bom_data) + get_sub_assembly_items(item.get("bom_no"), bom_data, item.get("qty")) for key, data in bom_data.items(): data.update({ - 'qty': data.get("stock_qty") * item.get("qty"), + 'qty': data.get("stock_qty"), 'production_plan': self.name, + 'use_multi_level_bom': item.get("use_multi_level_bom"), 'company': self.company, 'fg_warehouse': item.get("fg_warehouse"), 'update_consumed_material_cost_in_project': 0 @@ -724,7 +725,7 @@ def get_item_data(item_code): # "description": item_details.get("description") } -def get_sub_assembly_items(bom_no, bom_data): +def get_sub_assembly_items(bom_no, bom_data, to_produce_qty): data = get_children('BOM', parent = bom_no) for d in data: if d.expandable: @@ -741,6 +742,6 @@ def get_sub_assembly_items(bom_no, bom_data): }) bom_item = bom_data.get(key) - bom_item["stock_qty"] += d.stock_qty / d.parent_bom_qty + bom_item["stock_qty"] += (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) - get_sub_assembly_items(bom_item.get("bom_no"), bom_data) + get_sub_assembly_items(bom_item.get("bom_no"), bom_data, bom_item["stock_qty"]) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 26f580db339..c67330ad45f 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -158,6 +158,46 @@ class TestProductionPlan(unittest.TestCase): self.assertTrue(mr.material_request_type, 'Customer Provided') self.assertTrue(mr.customer, '_Test Customer') + def test_production_plan_with_multi_level_bom(self): + #|Item Code | Qty | + #|Test BOM 1 | 1 | + #| Test BOM 2 | 2 | + #| Test BOM 3 | 3 | + + for item_code in ["Test BOM 1", "Test BOM 2", "Test BOM 3", "Test RM BOM 1"]: + create_item(item_code, is_stock_item=1) + + # created bom upto 3 level + if not frappe.db.get_value('BOM', {'item': "Test BOM 3"}): + make_bom(item = "Test BOM 3", raw_materials = ["Test RM BOM 1"], rm_qty=3) + + if not frappe.db.get_value('BOM', {'item': "Test BOM 2"}): + make_bom(item = "Test BOM 2", raw_materials = ["Test BOM 3"], rm_qty=3) + + if not frappe.db.get_value('BOM', {'item': "Test BOM 1"}): + make_bom(item = "Test BOM 1", raw_materials = ["Test BOM 2"], rm_qty=2) + + item_code = "Test BOM 1" + pln = frappe.new_doc('Production Plan') + pln.company = "_Test Company" + pln.append("po_items", { + "item_code": item_code, + "bom_no": frappe.db.get_value('BOM', {'item': "Test BOM 1"}), + "planned_qty": 3, + "make_work_order_for_sub_assembly_items": 1 + }) + + pln.submit() + pln.make_work_order() + + #last level sub-assembly work order produce qty + to_produce_qty = frappe.db.get_value("Work Order", + {"production_plan": pln.name, "production_item": "Test BOM 3"}, "qty") + + self.assertEqual(to_produce_qty, 18.0) + pln.cancel() + frappe.delete_doc("Production Plan", pln.name) + def create_production_plan(**args): args = frappe._dict(args) @@ -205,7 +245,7 @@ def make_bom(**args): bom.append('items', { 'item_code': item, - 'qty': 1, + 'qty': args.rm_qty or 1.0, 'uom': item_doc.stock_uom, 'stock_uom': item_doc.stock_uom, 'rate': item_doc.valuation_rate or args.rate, @@ -213,4 +253,4 @@ def make_bom(**args): bom.insert(ignore_permissions=True) bom.submit() - return bom \ No newline at end of file + return bom From a951a59094eaa11dd52486c62bf30ea44cfb90dd Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 14 Sep 2020 21:05:54 +0530 Subject: [PATCH 68/71] fix: get_transaction_entries function arguments in Bank Statement Transaction Entry (#23052) --- .../bank_statement_transaction_entry.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py b/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py index 1318cf18d76..b8f1b85907f 100644 --- a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py +++ b/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py @@ -55,7 +55,7 @@ class BankStatementTransactionEntry(Document): def populate_payment_entries(self): if self.bank_statement is None: return - filename = self.bank_statement.split("/")[-1] + file_url = self.bank_statement if (len(self.new_transaction_items + self.reconciled_transaction_items) > 0): frappe.throw(_("Transactions already retreived from the statement")) @@ -65,7 +65,7 @@ class BankStatementTransactionEntry(Document): if self.bank_settings: mapped_items = frappe.get_doc("Bank Statement Settings", self.bank_settings).mapped_items statement_headers = self.get_statement_headers() - transactions = get_transaction_entries(filename, statement_headers) + transactions = get_transaction_entries(file_url, statement_headers) for entry in transactions: date = entry[statement_headers["Date"]].strip() #print("Processing entry DESC:{0}-W:{1}-D:{2}-DT:{3}".format(entry["Particulars"], entry["Withdrawals"], entry["Deposits"], entry["Date"])) @@ -398,20 +398,21 @@ def get_transaction_info(headers, header_index, row): transaction[header] = "" return transaction -def get_transaction_entries(filename, headers): +def get_transaction_entries(file_url, headers): header_index = {} rows, transactions = [], [] - if (filename.lower().endswith("xlsx")): + if (file_url.lower().endswith("xlsx")): from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file - rows = read_xlsx_file_from_attached_file(file_id=filename) - elif (filename.lower().endswith("csv")): + rows = read_xlsx_file_from_attached_file(file_url=file_url) + elif (file_url.lower().endswith("csv")): from frappe.utils.csvutils import read_csv_content - _file = frappe.get_doc("File", {"file_name": filename}) + _file = frappe.get_doc("File", {"file_url": file_url}) filepath = _file.get_full_path() with open(filepath,'rb') as csvfile: rows = read_csv_content(csvfile.read()) - elif (filename.lower().endswith("xls")): + elif (file_url.lower().endswith("xls")): + filename = file_url.split("/")[-1] rows = get_rows_from_xls_file(filename) else: frappe.throw(_("Only .csv and .xlsx files are supported currently")) From bb905ca4b082632db20e1b324d61c509fe1135de Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 14 Sep 2020 21:12:16 +0530 Subject: [PATCH 69/71] fix: incorrect calculation for consumed qty for subcontract item (#23257) * fix: incorrect calculation for consumed qty for subcontract item * added test case --- erpnext/controllers/buying_controller.py | 23 +++-- .../purchase_receipt/test_purchase_receipt.py | 88 +++++++++++++++++++ .../stock/doctype/stock_entry/stock_entry.py | 29 ++++-- .../stock_entry_detail.json | 11 +-- 4 files changed, 132 insertions(+), 19 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index aef7bcd156b..64bca44a608 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -322,7 +322,7 @@ class BuyingController(StockController): if raw_material.batch_nos: batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code, - qty, transferred_batch_qty_map, backflushed_batch_qty_map) + qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order) for batch_data in batches_qty: qty = batch_data['qty'] raw_material.batch_no = batch_data['batch'] @@ -334,6 +334,9 @@ class BuyingController(StockController): rm = self.append('supplied_items', {}) rm.update(raw_material_data) + if not rm.main_item_code: + rm.main_item_code = fg_item_doc.item_code + rm.required_qty = qty rm.consumed_qty = qty @@ -844,7 +847,7 @@ def get_subcontracted_raw_materials_from_se(purchase_order, fg_item): AND se.purpose='Send to Subcontractor' AND se.purchase_order = %s AND IFNULL(sed.t_warehouse, '') != '' - AND sed.subcontracted_item = %s + AND IFNULL(sed.subcontracted_item, '') in ('', %s) GROUP BY sed.item_code, sed.subcontracted_item """ raw_materials = frappe.db.multisql({ @@ -975,14 +978,15 @@ def get_transferred_batch_qty_map(purchase_order, fg_item): SELECT sed.batch_no, SUM(sed.qty) AS qty, - sed.item_code + sed.item_code, + sed.subcontracted_item FROM `tabStock Entry` se,`tabStock Entry Detail` sed WHERE se.name = sed.parent AND se.docstatus=1 AND se.purpose='Send to Subcontractor' AND se.purchase_order = %s - AND sed.subcontracted_item = %s + AND ifnull(sed.subcontracted_item, '') in ('', %s) AND sed.batch_no IS NOT NULL GROUP BY sed.batch_no, @@ -990,8 +994,10 @@ def get_transferred_batch_qty_map(purchase_order, fg_item): """, (purchase_order, fg_item), as_dict=1) for batch_data in transferred_batches: - transferred_batch_qty_map.setdefault((batch_data.item_code, fg_item), {}) - transferred_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty + key = ((batch_data.item_code, fg_item) + if batch_data.subcontracted_item else (batch_data.item_code, purchase_order)) + transferred_batch_qty_map.setdefault(key, {}) + transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty return transferred_batch_qty_map @@ -1028,9 +1034,12 @@ def get_backflushed_batch_qty_map(purchase_order, fg_item): return backflushed_batch_qty_map -def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batch_qty_map): +def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batch_qty_map, po): # Returns available batches to be backflushed based on requirements transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {}) + if not transferred_batches: + transferred_batches = transferred_batch_qty_map.get((item_code, po), {}) + backflushed_batches = backflushed_batch_qty_map.get((item_code, fg_item), {}) available_batches = [] diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 8008b48850c..d5e39786e65 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -579,6 +579,67 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEquals(pi2.items[0].qty, 2) self.assertEquals(pi2.items[1].qty, 1) + def test_subcontracted_pr_for_multi_transfer_batches(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + from erpnext.buying.doctype.purchase_order.purchase_order import make_rm_stock_entry, make_purchase_receipt + from erpnext.buying.doctype.purchase_order.test_purchase_order import (update_backflush_based_on, + create_purchase_order) + + update_backflush_based_on("Material Transferred for Subcontract") + item_code = "_Test Subcontracted FG Item 3" + + make_item('Sub Contracted Raw Material 3', { + 'is_stock_item': 1, + 'is_sub_contracted_item': 1, + 'has_batch_no': 1, + 'create_new_batch': 1 + }) + + create_subcontracted_item(item_code=item_code, has_batch_no=1, + raw_materials=["Sub Contracted Raw Material 3"]) + + order_qty = 500 + po = create_purchase_order(item_code=item_code, qty=order_qty, + is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") + + ste1=make_stock_entry(target="_Test Warehouse - _TC", + item_code = "Sub Contracted Raw Material 3", qty=300, basic_rate=100) + ste2=make_stock_entry(target="_Test Warehouse - _TC", + item_code = "Sub Contracted Raw Material 3", qty=200, basic_rate=100) + + transferred_batch = { + ste1.items[0].batch_no : 300, + ste2.items[0].batch_no : 200 + } + + rm_items = [ + {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item", + "qty":300,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}, + {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item", + "qty":200,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"} + ] + + rm_item_string = json.dumps(rm_items) + se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string)) + self.assertEqual(len(se.items), 2) + se.items[0].batch_no = ste1.items[0].batch_no + se.items[1].batch_no = ste2.items[0].batch_no + se.submit() + + supplied_qty = frappe.db.get_value("Purchase Order Item Supplied", + {"parent": po.name, "rm_item_code": "Sub Contracted Raw Material 3"}, "supplied_qty") + + self.assertEqual(supplied_qty, 500.00) + + pr = make_purchase_receipt(po.name) + pr.save() + self.assertEqual(len(pr.supplied_items), 2) + + for row in pr.supplied_items: + self.assertEqual(transferred_batch.get(row.batch_no), row.consumed_qty) + + update_backflush_based_on("BOM") + def get_gl_entries(voucher_type, voucher_no): return frappe.db.sql("""select account, debit, credit, cost_center from `tabGL Entry` where voucher_type=%s and voucher_no=%s @@ -714,6 +775,33 @@ def make_purchase_receipt(**args): pr.submit() return pr +def create_subcontracted_item(**args): + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + args = frappe._dict(args) + + if not frappe.db.exists('Item', args.item_code): + make_item(args.item_code, { + 'is_stock_item': 1, + 'is_sub_contracted_item': 1, + 'has_batch_no': args.get("has_batch_no") or 0 + }) + + if not args.raw_materials: + if not frappe.db.exists('Item', "Test Extra Item 1"): + make_item("Test Extra Item 1", { + 'is_stock_item': 1, + }) + + if not frappe.db.exists('Item', "Test Extra Item 2"): + make_item("Test Extra Item 2", { + 'is_stock_item': 1, + }) + + args.raw_materials = ['_Test FG Item', 'Test Extra Item 1'] + + if not frappe.db.get_value('BOM', {'item': args.item_code}, 'name'): + make_bom(item = args.item_code, raw_materials = args.get("raw_materials")) test_dependencies = ["BOM", "Item Price", "Location"] test_records = frappe.get_test_records('Purchase Receipt') diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 7b632415e18..961f5f45325 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -556,8 +556,9 @@ class StockEntry(StockController): qty_allowance = flt(frappe.db.get_single_value("Buying Settings", "over_transfer_allowance")) - if (self.purpose == "Send to Subcontractor" and self.purchase_order and - backflush_raw_materials_based_on == 'BOM'): + if not (self.purpose == "Send to Subcontractor" and self.purchase_order): return + + if (backflush_raw_materials_based_on == 'BOM'): purchase_order = frappe.get_doc("Purchase Order", self.purchase_order) for se_item in self.items: item_code = se_item.original_item or se_item.item_code @@ -594,6 +595,11 @@ class StockEntry(StockController): if flt(total_supplied, precision) > flt(total_allowed, precision): frappe.throw(_("Row {0}# Item {1} cannot be transferred more than {2} against Purchase Order {3}") .format(se_item.idx, se_item.item_code, total_allowed, self.purchase_order)) + elif backflush_raw_materials_based_on == "Material Transferred for Subcontract": + for row in self.items: + if not row.subcontracted_item: + frappe.throw(_("Row {0}: Subcontracted Item is mandatory for the raw material {1}") + .format(row.idx, frappe.bold(row.item_code))) def validate_bom(self): for d in self.get('items'): @@ -797,6 +803,13 @@ class StockEntry(StockController): ret.get('has_batch_no') and not args.get('batch_no')): args.batch_no = get_batch_no(args['item_code'], args['s_warehouse'], args['qty']) + if self.purpose == "Send to Subcontractor" and self.get("purchase_order") and args.get('item_code'): + subcontract_items = frappe.get_all("Purchase Order Item Supplied", + {"parent": self.purchase_order, "rm_item_code": args.get('item_code')}, "main_item_code") + + if subcontract_items and len(subcontract_items) == 1: + ret["subcontracted_item"] = subcontract_items[0].main_item_code + return ret def set_items_for_stock_in(self): @@ -1237,9 +1250,15 @@ class StockEntry(StockController): #Update Supplied Qty in PO Supplied Items frappe.db.sql("""UPDATE `tabPurchase Order Item Supplied` pos - SET pos.supplied_qty = (SELECT ifnull(sum(transfer_qty), 0) FROM `tabStock Entry Detail` sed - WHERE pos.name = sed.po_detail and sed.docstatus = 1) - WHERE pos.docstatus = 1 and pos.parent = %s""", self.purchase_order) + SET + pos.supplied_qty = IFNULL((SELECT ifnull(sum(transfer_qty), 0) + FROM + `tabStock Entry Detail` sed, `tabStock Entry` se + WHERE + (pos.name = sed.po_detail OR sed.subcontracted_item = pos.main_item_code) + AND sed.docstatus = 1 AND se.name = sed.parent and se.purchase_order = %(po)s + ), 0) + WHERE pos.docstatus = 1 and pos.parent = %(po)s""", {"po": self.purchase_order}) #Update reserved sub contracted quantity in bin based on Supplied Item Details and for d in self.get("items"): diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 9992d10febe..9d397df8bcd 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -1,5 +1,4 @@ { - "actions": [], "autoname": "hash", "creation": "2013-03-29 18:22:12", "doctype": "DocType", @@ -17,6 +16,7 @@ "item_group", "col_break2", "item_name", + "subcontracted_item", "section_break_8", "description", "column_break_10", @@ -57,7 +57,6 @@ "material_request", "material_request_item", "original_item", - "subcontracted_item", "reference_section", "against_stock_entry", "ste_detail", @@ -415,6 +414,7 @@ "read_only": 1 }, { + "depends_on": "eval:parent.purpose == 'Send to Subcontractor'", "fieldname": "subcontracted_item", "fieldtype": "Link", "label": "Subcontracted Item", @@ -497,15 +497,12 @@ "depends_on": "eval:parent.purpose===\"Repack\" && doc.t_warehouse", "fieldname": "set_basic_rate_manually", "fieldtype": "Check", - "label": "Set Basic Rate Manually", - "show_days": 1, - "show_seconds": 1 + "label": "Set Basic Rate Manually" } ], "idx": 1, "istable": 1, - "links": [], - "modified": "2020-06-08 12:57:03.172887", + "modified": "2020-09-04 12:12:35.668198", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", From 812033b78245e5fa6c37ce06df47edeffaa2b0b3 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 15 Sep 2020 11:21:16 +0530 Subject: [PATCH 70/71] fix: purchase order updates are not tracked (#23325) --- erpnext/buying/doctype/purchase_order/purchase_order.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 5dacfb0e02e..ee07892fe43 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -1055,7 +1055,8 @@ "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, - "modified": "2020-07-01 12:40:45.240948", + "links": [], + "modified": "2020-09-14 14:36:12.418690", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", @@ -1112,5 +1113,6 @@ "sort_field": "modified", "sort_order": "DESC", "timeline_field": "supplier", - "title_field": "title" + "title_field": "supplier", + "track_changes": 1 } \ No newline at end of file From bea5e5b1bc6855f827d6dcd07c050e4c2d8c42db Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 15 Sep 2020 19:40:37 +0530 Subject: [PATCH 71/71] fix: consumed qty logic for subcontracted raw materials (#23314) * fix: consumed qty logic for subcontracted raw materials * added test case * fix: sales order workflow test case --- .../purchase_order/test_purchase_order.py | 110 +++++++++++++++--- erpnext/controllers/buying_controller.py | 62 +++++----- .../doctype/sales_order/test_sales_order.py | 3 +- erpnext/stock/doctype/batch/test_batch.py | 15 +++ .../purchase_receipt/test_purchase_receipt.py | 2 +- erpnext/stock/doctype/serial_no/serial_no.py | 3 + 6 files changed, 148 insertions(+), 47 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 075db6e46ba..86e1e973abb 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -17,6 +17,8 @@ from erpnext.stock.doctype.material_request.material_request import make_purchas from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.controllers.accounts_controller import update_child_qty_rate from erpnext.controllers.status_updater import OverAllowanceError +from erpnext.stock.doctype.batch.test_batch import make_new_batch +from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials class TestPurchaseOrder(unittest.TestCase): def test_make_purchase_receipt(self): @@ -580,7 +582,7 @@ class TestPurchaseOrder(unittest.TestCase): def test_exploded_items_in_subcontracted(self): item_code = "_Test Subcontracted FG Item 1" - make_subcontracted_item(item_code) + make_subcontracted_item(item_code=item_code) po = create_purchase_order(item_code=item_code, qty=1, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") @@ -602,7 +604,7 @@ class TestPurchaseOrder(unittest.TestCase): def test_backflush_based_on_stock_entry(self): item_code = "_Test Subcontracted FG Item 1" - make_subcontracted_item(item_code) + make_subcontracted_item(item_code=item_code) make_item('Sub Contracted Raw Material 1', { 'is_stock_item': 1, 'is_sub_contracted_item': 1 @@ -661,6 +663,76 @@ class TestPurchaseOrder(unittest.TestCase): update_backflush_based_on("BOM") + def test_backflushed_based_on_for_multiple_batches(self): + item_code = "_Test Subcontracted FG Item 2" + make_item('Sub Contracted Raw Material 2', { + 'is_stock_item': 1, + 'is_sub_contracted_item': 1 + }) + + make_subcontracted_item(item_code=item_code, has_batch_no=1, create_new_batch=1, + raw_materials=["Sub Contracted Raw Material 2"]) + + update_backflush_based_on("Material Transferred for Subcontract") + + order_qty = 500 + po = create_purchase_order(item_code=item_code, qty=order_qty, + is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") + + make_stock_entry(target="_Test Warehouse - _TC", + item_code = "Sub Contracted Raw Material 2", qty=552, basic_rate=100) + + rm_items = [ + {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 2","item_name":"_Test Item", + "qty":552,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}] + + rm_item_string = json.dumps(rm_items) + se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) + se.submit() + + for batch in ["ABCD1", "ABCD2", "ABCD3", "ABCD4"]: + make_new_batch(batch_id=batch, item_code=item_code) + + pr = make_purchase_receipt(po.name) + + # partial receipt + pr.get('items')[0].qty = 30 + pr.get('items')[0].batch_no = "ABCD1" + + purchase_order = po.name + purchase_order_item = po.items[0].name + + for batch_no, qty in {"ABCD2": 60, "ABCD3": 70, "ABCD4":40}.items(): + pr.append("items", { + "item_code": pr.get('items')[0].item_code, + "item_name": pr.get('items')[0].item_name, + "uom": pr.get('items')[0].uom, + "stock_uom": pr.get('items')[0].stock_uom, + "warehouse": pr.get('items')[0].warehouse, + "conversion_factor": pr.get('items')[0].conversion_factor, + "cost_center": pr.get('items')[0].cost_center, + "rate": pr.get('items')[0].rate, + "qty": qty, + "batch_no": batch_no, + "purchase_order": purchase_order, + "purchase_order_item": purchase_order_item + }) + + pr.submit() + + pr1 = make_purchase_receipt(po.name) + pr1.get('items')[0].qty = 300 + pr1.get('items')[0].batch_no = "ABCD1" + pr1.save() + + pr_key = ("Sub Contracted Raw Material 2", po.name) + consumed_qty = get_backflushed_subcontracted_raw_materials([po.name]).get(pr_key) + + self.assertTrue(pr1.supplied_items[0].consumed_qty > 0) + self.assertTrue(pr1.supplied_items[0].consumed_qty, flt(552.0) - flt(consumed_qty)) + + update_backflush_based_on("BOM") + def test_advance_payment_entry_unlink_against_purchase_order(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry frappe.db.set_value("Accounts Settings", "Accounts Settings", @@ -712,27 +784,33 @@ def make_pr_against_po(po, received_qty=0): pr.submit() return pr -def make_subcontracted_item(item_code): +def make_subcontracted_item(**args): from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom - if not frappe.db.exists('Item', item_code): - make_item(item_code, { + args = frappe._dict(args) + + if not frappe.db.exists('Item', args.item_code): + make_item(args.item_code, { 'is_stock_item': 1, - 'is_sub_contracted_item': 1 + 'is_sub_contracted_item': 1, + 'has_batch_no': args.get("has_batch_no") or 0 }) - if not frappe.db.exists('Item', "Test Extra Item 1"): - make_item("Test Extra Item 1", { - 'is_stock_item': 1, - }) + if not args.raw_materials: + if not frappe.db.exists('Item', "Test Extra Item 1"): + make_item("Test Extra Item 1", { + 'is_stock_item': 1, + }) - if not frappe.db.exists('Item', "Test Extra Item 2"): - make_item("Test Extra Item 2", { - 'is_stock_item': 1, - }) + if not frappe.db.exists('Item', "Test Extra Item 2"): + make_item("Test Extra Item 2", { + 'is_stock_item': 1, + }) - if not frappe.db.get_value('BOM', {'item': item_code}, 'name'): - make_bom(item = item_code, raw_materials = ['_Test FG Item', 'Test Extra Item 1']) + args.raw_materials = ['_Test FG Item', 'Test Extra Item 1'] + + if not frappe.db.get_value('BOM', {'item': args.item_code}, 'name'): + make_bom(item = args.item_code, raw_materials = args.get("raw_materials")) def update_backflush_based_on(based_on): doc = frappe.get_doc('Buying Settings') diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 64bca44a608..8ed76421894 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _, msgprint from frappe.utils import flt,cint, cstr, getdate - +from six import iteritems from erpnext.accounts.party import get_party_details from erpnext.stock.get_item_details import get_conversion_factor from erpnext.buying.utils import validate_for_items, update_last_purchase_rate @@ -292,7 +292,7 @@ class BuyingController(StockController): backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code) for raw_material in transferred_raw_materials + non_stock_items: - rm_item_key = '{}{}'.format(raw_material.rm_item_code, item.purchase_order) + rm_item_key = (raw_material.rm_item_code, item.purchase_order) raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {}) consumed_qty = raw_material_data.get('qty', 0) @@ -337,6 +337,7 @@ class BuyingController(StockController): if not rm.main_item_code: rm.main_item_code = fg_item_doc.item_code + rm.reference_name = fg_item_doc.name rm.required_qty = qty rm.consumed_qty = qty @@ -864,39 +865,42 @@ def get_subcontracted_raw_materials_from_se(purchase_order, fg_item): return raw_materials def get_backflushed_subcontracted_raw_materials(purchase_orders): - common_query = """ - SELECT - CONCAT(prsi.rm_item_code, pri.purchase_order) AS item_key, - SUM(prsi.consumed_qty) AS qty, - {serial_no_concat_syntax} AS serial_nos, - {batch_no_concat_syntax} AS batch_nos - FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` prsi - WHERE - pr.name = pri.parent - AND pr.name = prsi.parent - AND pri.purchase_order IN %s - AND pri.item_code = prsi.main_item_code - AND pr.docstatus = 1 - GROUP BY prsi.rm_item_code, pri.purchase_order - """ + purchase_receipts = frappe.get_all("Purchase Receipt Item", + fields = ["purchase_order", "item_code", "name", "parent"], + filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))}) - backflushed_raw_materials = frappe.db.multisql({ - 'mariadb': common_query.format( - serial_no_concat_syntax="GROUP_CONCAT(prsi.serial_no)", - batch_no_concat_syntax="GROUP_CONCAT(prsi.batch_no)" - ), - 'postgres': common_query.format( - serial_no_concat_syntax="STRING_AGG(prsi.serial_no, ',')", - batch_no_concat_syntax="STRING_AGG(prsi.batch_no, ',')" - ) - }, (purchase_orders, ), as_dict=1) + distinct_purchase_receipts = {} + for pr in purchase_receipts: + key = (pr.purchase_order, pr.item_code, pr.parent) + distinct_purchase_receipts.setdefault(key, []).append(pr.name) backflushed_raw_materials_map = frappe._dict() - for item in backflushed_raw_materials: - backflushed_raw_materials_map.setdefault(item.item_key, item) + for args, references in iteritems(distinct_purchase_receipts): + purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references) + + for data in purchase_receipt_supplied_items: + pr_key = (data.rm_item_code, args[0]) + if pr_key not in backflushed_raw_materials_map: + backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({ + "qty": 0.0, + "serial_no": [], + "batch_no": [] + })) + + row = backflushed_raw_materials_map.get(pr_key) + row.qty += data.consumed_qty + + for field in ["serial_no", "batch_no"]: + if data.get(field): + row[field].append(data.get(field)) return backflushed_raw_materials_map +def get_supplied_items(item_code, purchase_receipt, references): + return frappe.get_all("Purchase Receipt Item Supplied", + fields=["rm_item_code", "consumed_qty", "serial_no", "batch_no"], + filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)}) + def get_asset_item_details(asset_items): asset_items_data = {} for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"], diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 254f3c1993a..ae0faf2128b 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -418,10 +418,11 @@ class TestSalesOrder(unittest.TestCase): self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) test_user.remove_roles("Accounts User") frappe.set_user("Administrator") - + def test_update_child_qty_rate_with_workflow(self): from frappe.model.workflow import apply_workflow + frappe.set_user("Administrator") workflow = make_sales_order_workflow() so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1) apply_workflow(so, 'Approve') diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 32445a618d1..0cc0fd487d3 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -241,3 +241,18 @@ class TestBatch(unittest.TestCase): batch.insert() return batch + +def make_new_batch(**args): + args = frappe._dict(args) + + try: + batch = frappe.get_doc({ + "doctype": "Batch", + "batch_id": args.batch_id, + "item": args.item_code, + }).insert() + + except frappe.DuplicateEntryError: + batch = frappe.get_doc("Batch", args.batch_id) + + return batch \ No newline at end of file diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index d5e39786e65..c9cda37c40b 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -152,7 +152,7 @@ class TestPurchaseReceipt(unittest.TestCase): update_backflush_based_on("Material Transferred for Subcontract") item_code = "_Test Subcontracted FG Item 1" - make_subcontracted_item(item_code) + make_subcontracted_item(item_code=item_code) po = create_purchase_order(item_code=item_code, qty=1, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index bbdac992b58..f8885a91edc 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -420,6 +420,9 @@ def get_item_details(item_code): from tabItem where name=%s""", item_code, as_dict=True)[0] def get_serial_nos(serial_no): + if isinstance(serial_no, list): + return serial_no + return [s.strip() for s in cstr(serial_no).strip().upper().replace(',', '\n').split('\n') if s.strip()]