From 5d5f026a0d4ac2d088b722f254f7b58db27b8a4f Mon Sep 17 00:00:00 2001 From: Anupam K Date: Fri, 24 Jul 2020 02:32:45 +0530 Subject: [PATCH 001/192] fix: Target variance report signs --- .../item_group_wise_sales_target_variance.py | 20 +++++++++---------- ...ner_target_variance_based_on_item_group.js | 17 +++++++++++++++- ...son_target_variance_based_on_item_group.js | 17 +++++++++++++++- ...ory_target_variance_based_on_item_group.js | 17 +++++++++++++++- 4 files changed, 58 insertions(+), 13 deletions(-) diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py index 857b9823e0..ae216ca5d6 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py @@ -63,13 +63,13 @@ def get_columns(filters, period_list, partner_doctype): "label": _(partner_doctype), "fieldtype": "Link", "options": partner_doctype, - "width": 100 + "width": 150 }, { "fieldname": "item_group", "label": _("Item Group"), "fieldtype": "Link", "options": "Item Group", - "width": 100 + "width": 150 }] for period in period_list: @@ -81,19 +81,19 @@ def get_columns(filters, period_list, partner_doctype): "label": _("Target ({})").format(period.label), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }, { "fieldname": period.key, "label": _("Achieved ({})").format(period.label), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }, { "fieldname": variance_key, "label": _("Variance ({})").format(period.label), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }]) columns.extend([{ @@ -101,19 +101,19 @@ def get_columns(filters, period_list, partner_doctype): "label": _("Total Target"), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }, { "fieldname": "total_achieved", "label": _("Total Achieved"), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }, { "fieldname": "total_variance", "label": _("Total Variance"), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }]) return columns @@ -154,10 +154,10 @@ def prepare_data(filters, sales_users_data, actual_data, date_field, period_list if (r.get(sales_field) == d.parent and r.item_group == d.item_group and period.from_date <= r.get(date_field) and r.get(date_field) <= period.to_date): details[p_key] += r.get(qty_or_amount_field, 0) - details[variance_key] = details.get(target_key) - details.get(p_key) + details[variance_key] = details.get(p_key) - details.get(target_key) details["total_achieved"] += details.get(p_key) - details["total_variance"] = details.get("total_target") - details.get("total_achieved") + details["total_variance"] = details.get("total_achieved") - details.get("total_target") return rows diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js index f99f68c524..38bb127e23 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js @@ -44,5 +44,20 @@ frappe.query_reports["Sales Partner Target Variance based on Item Group"] = { options: "Quantity\nAmount", default: "Quantity" }, - ] + ], + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname.includes('variance')) { + + if (data[column.fieldname] < 0) { + value = "" + value + ""; + } + else if (data[column.fieldname] > 0) { + value = "" + value + ""; + } + } + + return value; + } } diff --git a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js index 9f6bfc41df..a8e2fad373 100644 --- a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js +++ b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js @@ -44,5 +44,20 @@ frappe.query_reports["Sales Person Target Variance Based On Item Group"] = { options: "Quantity\nAmount", default: "Quantity" }, - ] + ], + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname.includes('variance')) { + + if (data[column.fieldname] < 0) { + value = "" + value + ""; + } + else if (data[column.fieldname] > 0) { + value = "" + value + ""; + } + } + + return value; + } } diff --git a/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js b/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js index dd9607ffbd..263391a7f7 100644 --- a/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js +++ b/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js @@ -44,5 +44,20 @@ frappe.query_reports["Territory Target Variance Based On Item Group"] = { options: "Quantity\nAmount", default: "Quantity" }, - ] + ], + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname.includes('variance')) { + + if (data[column.fieldname] < 0) { + value = "" + value + ""; + } + else if (data[column.fieldname] > 0) { + value = "" + value + ""; + } + } + + return value; + } } From 8718daa11e11c17ec64d550df661b3f3ac7c2eac Mon Sep 17 00:00:00 2001 From: Anupam K Date: Sat, 25 Jul 2020 23:56:11 +0530 Subject: [PATCH 002/192] Adding formatter in budget variance report --- .../budget_variance_report.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js index 3ec4d306c3..30415d1e65 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js @@ -71,7 +71,22 @@ frappe.query_reports["Budget Variance Report"] = { fieldtype: "Check", default: 0, }, - ] + ], + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname.includes('variance')) { + + if (data[column.fieldname] < 0) { + value = "" + value + ""; + } + else if (data[column.fieldname] > 0) { + value = "" + value + ""; + } + } + + return value; + } } erpnext.dimension_filters.forEach((dimension) => { From dec2c90866a2f6df3fd95f9ad5c5923704a83bb8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 29 Jul 2020 17:14:23 +0200 Subject: [PATCH 003/192] refactor: move datev-specific stuff to utils --- erpnext/regional/germany/utils/__init__.py | 0 .../regional/germany/utils/datev/__init__.py | 0 .../utils}/datev/datev_constants.py | 73 ----- .../regional/germany/utils/datev/datev_csv.py | 176 +++++++++++ erpnext/regional/report/datev/datev.py | 279 ++++++------------ erpnext/regional/report/datev/test_datev.py | 18 +- 6 files changed, 275 insertions(+), 271 deletions(-) create mode 100644 erpnext/regional/germany/utils/__init__.py create mode 100644 erpnext/regional/germany/utils/datev/__init__.py rename erpnext/regional/{report => germany/utils}/datev/datev_constants.py (89%) create mode 100644 erpnext/regional/germany/utils/datev/datev_csv.py diff --git a/erpnext/regional/germany/utils/__init__.py b/erpnext/regional/germany/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/regional/germany/utils/datev/__init__.py b/erpnext/regional/germany/utils/datev/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/regional/report/datev/datev_constants.py b/erpnext/regional/germany/utils/datev/datev_constants.py similarity index 89% rename from erpnext/regional/report/datev/datev_constants.py rename to erpnext/regional/germany/utils/datev/datev_constants.py index e063703005..88dc02dfca 100644 --- a/erpnext/regional/report/datev/datev_constants.py +++ b/erpnext/regional/germany/utils/datev/datev_constants.py @@ -460,79 +460,6 @@ ACCOUNT_NAME_COLUMNS = [ "Sprach-ID" ] -QUERY_REPORT_COLUMNS = [ - { - "label": "Umsatz (ohne Soll/Haben-Kz)", - "fieldname": "Umsatz (ohne Soll/Haben-Kz)", - "fieldtype": "Currency", - "width": 100 - }, - { - "label": "Soll/Haben-Kennzeichen", - "fieldname": "Soll/Haben-Kennzeichen", - "fieldtype": "Data", - "width": 100 - }, - { - "label": "Konto", - "fieldname": "Konto", - "fieldtype": "Data", - "width": 100 - }, - { - "label": "Gegenkonto (ohne BU-Schlüssel)", - "fieldname": "Gegenkonto (ohne BU-Schlüssel)", - "fieldtype": "Data", - "width": 100 - }, - { - "label": "Belegdatum", - "fieldname": "Belegdatum", - "fieldtype": "Date", - "width": 100 - }, - { - "label": "Belegfeld 1", - "fieldname": "Belegfeld 1", - "fieldtype": "Data", - "width": 150 - }, - { - "label": "Buchungstext", - "fieldname": "Buchungstext", - "fieldtype": "Text", - "width": 300 - }, - { - "label": "Beleginfo - Art 1", - "fieldname": "Beleginfo - Art 1", - "fieldtype": "Link", - "options": "DocType", - "width": 100 - }, - { - "label": "Beleginfo - Inhalt 1", - "fieldname": "Beleginfo - Inhalt 1", - "fieldtype": "Dynamic Link", - "options": "Beleginfo - Art 1", - "width": 150 - }, - { - "label": "Beleginfo - Art 2", - "fieldname": "Beleginfo - Art 2", - "fieldtype": "Link", - "options": "DocType", - "width": 100 - }, - { - "label": "Beleginfo - Inhalt 2", - "fieldname": "Beleginfo - Inhalt 2", - "fieldtype": "Dynamic Link", - "options": "Beleginfo - Art 2", - "width": 150 - } -] - class DataCategory(): """Field of the CSV Header.""" diff --git a/erpnext/regional/germany/utils/datev/datev_csv.py b/erpnext/regional/germany/utils/datev/datev_csv.py new file mode 100644 index 0000000000..be41a4ab45 --- /dev/null +++ b/erpnext/regional/germany/utils/datev/datev_csv.py @@ -0,0 +1,176 @@ +# coding: utf-8 +from __future__ import unicode_literals + +import datetime +import zipfile +import six +import frappe +import pandas as pd +from frappe import _ +from csv import QUOTE_NONNUMERIC +from six import BytesIO +from six import string_types +from .datev_constants import DataCategory +from .datev_constants import Transactions +from .datev_constants import AccountNames + + +def get_datev_csv(data, filters, csv_class): + """ + Fill in missing columns and return a CSV in DATEV Format. + + For automatic processing, DATEV requires the first line of the CSV file to + hold meta data such as the length of account numbers oder the category of + the data. + + Arguments: + data -- array of dictionaries + filters -- dict + csv_class -- defines DATA_CATEGORY, FORMAT_NAME and COLUMNS + """ + empty_df = pd.DataFrame(columns=csv_class.COLUMNS) + data_df = pd.DataFrame.from_records(data) + result = empty_df.append(data_df, sort=True) + + if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS: + result['Belegdatum'] = pd.to_datetime(result['Belegdatum']) + + if csv_class.DATA_CATEGORY == DataCategory.ACCOUNT_NAMES: + result['Sprach-ID'] = 'de-DE' + + data = result.to_csv( + # Reason for str(';'): https://github.com/pandas-dev/pandas/issues/6035 + sep=str(';'), + # European decimal seperator + decimal=',', + # Windows "ANSI" encoding + encoding='latin_1', + # format date as DDMM + date_format='%d%m', + # Windows line terminator + line_terminator='\r\n', + # Do not number rows + index=False, + # Use all columns defined above + columns=csv_class.COLUMNS, + # Quote most fields, even currency values with "," separator + quoting=QUOTE_NONNUMERIC + ) + + if not six.PY2: + data = data.encode('latin_1') + + header = get_header(filters, csv_class) + header = ';'.join(header).encode('latin_1') + + # 1st Row: Header with meta data + # 2nd Row: Data heading (Überschrift der Nutzdaten), included in `data` here. + # 3rd - nth Row: Data (Nutzdaten) + return header + b'\r\n' + data + + +def get_header(filters, csv_class): + description = filters.get('voucher_type', csv_class.FORMAT_NAME) + company = filters.get('company') + datev_settings = frappe.get_doc('DATEV Settings', {'client': company}) + default_currency = frappe.get_value('Company', company, 'default_currency') + coa = frappe.get_value('Company', company, 'chart_of_accounts') + coa_short_code = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '') + + header = [ + # DATEV format + # "DTVF" = created by DATEV software, + # "EXTF" = created by other software + '"EXTF"', + # version of the DATEV format + # 141 = 1.41, + # 510 = 5.10, + # 720 = 7.20 + '700', + csv_class.DATA_CATEGORY, + '"%s"' % csv_class.FORMAT_NAME, + # Format version (regarding format name) + csv_class.FORMAT_VERSION, + # Generated on + datetime.datetime.now().strftime("%Y%m%d%H%M%S") + '000', + # Imported on -- stays empty + '', + # Origin. Any two symbols, will be replaced by "SV" on import. + '"EN"', + # I = Exported by + '"%s"' % frappe.session.user, + # J = Imported by -- stays empty + '', + # K = Tax consultant number (Beraternummer) + datev_settings.get('consultant_number', '0000000'), + # L = Tax client number (Mandantennummer) + datev_settings.get('client_number', '00000'), + # M = Start of the fiscal year (Wirtschaftsjahresbeginn) + frappe.utils.formatdate(frappe.defaults.get_user_default('year_start_date'), 'yyyyMMdd'), + # N = Length of account numbers (Sachkontenlänge) + datev_settings.get('account_number_length', '4'), + # O = Transaction batch start date (YYYYMMDD) + frappe.utils.formatdate(filters.get('from_date'), 'yyyyMMdd') if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + # P = Transaction batch end date (YYYYMMDD) + frappe.utils.formatdate(filters.get('to_date'), 'yyyyMMdd') if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + # Q = Description (for example, "Sales Invoice") Max. 30 chars + '"{}"'.format(_(description)) if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + # R = Diktatkürzel + '', + # S = Buchungstyp + # 1 = Transaction batch (Finanzbuchführung), + # 2 = Annual financial statement (Jahresabschluss) + '1' if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + # T = Rechnungslegungszweck + # 0 oder leer = vom Rechnungslegungszweck unabhängig + # 50 = Handelsrecht + # 30 = Steuerrecht + # 64 = IFRS + # 40 = Kalkulatorik + # 11 = Reserviert + # 12 = Reserviert + '0' if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + # U = Festschreibung + # TODO: Filter by Accounting Period. In export for closed Accounting Period, this will be "1" + '0', + # V = Default currency, for example, "EUR" + '"%s"' % default_currency if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + # reserviert + '', + # Derivatskennzeichen + '', + # reserviert + '', + # reserviert + '', + # SKR + '"%s"' % coa_short_code, + # Branchen-Lösungs-ID + '', + # reserviert + '', + # reserviert + '', + # Anwendungsinformation (Verarbeitungskennzeichen der abgebenden Anwendung) + '' + ] + return header + + +def download_csv_files_as_zip(csv_data_list): + """ + Put CSV files in a zip archive and send that to the client. + + Params: + csv_data_list -- list of dicts [{'file_name': 'EXTF_Buchunsstapel.zip', 'csv_data': get_datev_csv()}] + """ + zip_buffer = BytesIO() + + datev_zip = zipfile.ZipFile(zip_buffer, mode='w', compression=zipfile.ZIP_DEFLATED) + for csv_file in csv_data_list: + datev_zip.writestr(csv_file.get('file_name'), csv_file.get('csv_data')) + datev_zip.close() + + frappe.response['filecontent'] = zip_buffer.getvalue() + frappe.response['filename'] = 'DATEV.zip' + frappe.response['type'] = 'binary' diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 7fec94e740..97d93d3b80 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -9,31 +9,92 @@ Provide a report and downloadable CSV according to the German DATEV format. """ from __future__ import unicode_literals -import datetime import json -import zipfile import six import frappe -import pandas as pd - from frappe import _ -from csv import QUOTE_NONNUMERIC -from six import BytesIO from six import string_types -from .datev_constants import DataCategory -from .datev_constants import Transactions -from .datev_constants import DebtorsCreditors -from .datev_constants import AccountNames -from .datev_constants import QUERY_REPORT_COLUMNS +from erpnext.regional.germany.utils.datev.datev_csv import download_csv_files_as_zip, get_datev_csv +from erpnext.regional.germany.utils.datev.datev_constants import Transactions, DebtorsCreditors, AccountNames + +COLUMNS = [ + { + "label": "Umsatz (ohne Soll/Haben-Kz)", + "fieldname": "Umsatz (ohne Soll/Haben-Kz)", + "fieldtype": "Currency", + "width": 100 + }, + { + "label": "Soll/Haben-Kennzeichen", + "fieldname": "Soll/Haben-Kennzeichen", + "fieldtype": "Data", + "width": 100 + }, + { + "label": "Konto", + "fieldname": "Konto", + "fieldtype": "Data", + "width": 100 + }, + { + "label": "Gegenkonto (ohne BU-Schlüssel)", + "fieldname": "Gegenkonto (ohne BU-Schlüssel)", + "fieldtype": "Data", + "width": 100 + }, + { + "label": "Belegdatum", + "fieldname": "Belegdatum", + "fieldtype": "Date", + "width": 100 + }, + { + "label": "Belegfeld 1", + "fieldname": "Belegfeld 1", + "fieldtype": "Data", + "width": 150 + }, + { + "label": "Buchungstext", + "fieldname": "Buchungstext", + "fieldtype": "Text", + "width": 300 + }, + { + "label": "Beleginfo - Art 1", + "fieldname": "Beleginfo - Art 1", + "fieldtype": "Link", + "options": "DocType", + "width": 100 + }, + { + "label": "Beleginfo - Inhalt 1", + "fieldname": "Beleginfo - Inhalt 1", + "fieldtype": "Dynamic Link", + "options": "Beleginfo - Art 1", + "width": 150 + }, + { + "label": "Beleginfo - Art 2", + "fieldname": "Beleginfo - Art 2", + "fieldtype": "Link", + "options": "DocType", + "width": 100 + }, + { + "label": "Beleginfo - Inhalt 2", + "fieldname": "Beleginfo - Inhalt 2", + "fieldtype": "Dynamic Link", + "options": "Beleginfo - Art 2", + "width": 150 + } +] def execute(filters=None): """Entry point for frappe.""" validate(filters) - result = get_transactions(filters, as_dict=0) - columns = QUERY_REPORT_COLUMNS - - return columns, result + return COLUMNS, get_transactions(filters, as_dict=0) def validate(filters): @@ -240,146 +301,8 @@ def get_account_names(filters): """, filters, as_dict=1) -def get_datev_csv(data, filters, csv_class): - """ - Fill in missing columns and return a CSV in DATEV Format. - - For automatic processing, DATEV requires the first line of the CSV file to - hold meta data such as the length of account numbers oder the category of - the data. - - Arguments: - data -- array of dictionaries - filters -- dict - csv_class -- defines DATA_CATEGORY, FORMAT_NAME and COLUMNS - """ - empty_df = pd.DataFrame(columns=csv_class.COLUMNS) - data_df = pd.DataFrame.from_records(data) - - result = empty_df.append(data_df, sort=True) - - if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS: - result['Belegdatum'] = pd.to_datetime(result['Belegdatum']) - - if csv_class.DATA_CATEGORY == DataCategory.ACCOUNT_NAMES: - result['Sprach-ID'] = 'de-DE' - - data = result.to_csv( - # Reason for str(';'): https://github.com/pandas-dev/pandas/issues/6035 - sep=str(';'), - # European decimal seperator - decimal=',', - # Windows "ANSI" encoding - encoding='latin_1', - # format date as DDMM - date_format='%d%m', - # Windows line terminator - line_terminator='\r\n', - # Do not number rows - index=False, - # Use all columns defined above - columns=csv_class.COLUMNS, - # Quote most fields, even currency values with "," separator - quoting=QUOTE_NONNUMERIC - ) - - if not six.PY2: - data = data.encode('latin_1') - - header = get_header(filters, csv_class) - header = ';'.join(header).encode('latin_1') - - # 1st Row: Header with meta data - # 2nd Row: Data heading (Überschrift der Nutzdaten), included in `data` here. - # 3rd - nth Row: Data (Nutzdaten) - return header + b'\r\n' + data - - -def get_header(filters, csv_class): - description = filters.get('voucher_type', csv_class.FORMAT_NAME) - - header = [ - # DATEV format - # "DTVF" = created by DATEV software, - # "EXTF" = created by other software - '"EXTF"', - # version of the DATEV format - # 141 = 1.41, - # 510 = 5.10, - # 720 = 7.20 - '700', - csv_class.DATA_CATEGORY, - '"%s"' % csv_class.FORMAT_NAME, - # Format version (regarding format name) - csv_class.FORMAT_VERSION, - # Generated on - datetime.datetime.now().strftime("%Y%m%d%H%M%S") + '000', - # Imported on -- stays empty - '', - # Origin. Any two symbols, will be replaced by "SV" on import. - '"EN"', - # I = Exported by - '"%s"' % frappe.session.user, - # J = Imported by -- stays empty - '', - # K = Tax consultant number (Beraternummer) - filters.get('consultant_number', '0000000'), - # L = Tax client number (Mandantennummer) - filters.get('client_number', '00000'), - # M = Start of the fiscal year (Wirtschaftsjahresbeginn) - frappe.utils.formatdate(frappe.defaults.get_user_default("year_start_date"), "yyyyMMdd"), - # N = Length of account numbers (Sachkontenlänge) - '%d' % filters.get('acc_len', 4), - # O = Transaction batch start date (YYYYMMDD) - frappe.utils.formatdate(filters.get('from_date'), "yyyyMMdd") if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', - # P = Transaction batch end date (YYYYMMDD) - frappe.utils.formatdate(filters.get('to_date'), "yyyyMMdd") if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', - # Q = Description (for example, "Sales Invoice") Max. 30 chars - '"{}"'.format(_(description)) if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', - # R = Diktatkürzel - '', - # S = Buchungstyp - # 1 = Transaction batch (Finanzbuchführung), - # 2 = Annual financial statement (Jahresabschluss) - '1' if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', - # T = Rechnungslegungszweck - # 0 oder leer = vom Rechnungslegungszweck unabhängig - # 50 = Handelsrecht - # 30 = Steuerrecht - # 64 = IFRS - # 40 = Kalkulatorik - # 11 = Reserviert - # 12 = Reserviert - '0' if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', - # U = Festschreibung - # TODO: Filter by Accounting Period. In export for closed Accounting Period, this will be "1" - '0', - # V = Default currency, for example, "EUR" - '"%s"' % filters.get('default_currency', 'EUR') if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', - # reserviert - '', - # Derivatskennzeichen - '', - # reserviert - '', - # reserviert - '', - # SKR - '"%s"' % filters.get('skr', '04'), - # Branchen-Lösungs-ID - '', - # reserviert - '', - # reserviert - '', - # Anwendungsinformation (Verarbeitungskennzeichen der abgebenden Anwendung) - '' - ] - return header - - @frappe.whitelist() -def download_datev_csv(filters=None): +def download_datev_csv(filters): """ Provide accounting entries for download in DATEV format. @@ -400,38 +323,26 @@ def download_datev_csv(filters=None): coa = frappe.get_value('Company', filters.get('company'), 'chart_of_accounts') filters['skr'] = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '') - # set account number length - account_numbers = frappe.get_list('Account', fields=['account_number'], filters={'is_group': 0, 'account_number': ('!=', '')}) - filters['acc_len'] = max([len(a.account_number) for a in account_numbers]) - - filters['consultant_number'] = frappe.get_value('DATEV Settings', filters.get('company'), 'consultant_number') - filters['client_number'] = frappe.get_value('DATEV Settings', filters.get('company'), 'client_number') - filters['default_currency'] = frappe.get_value('Company', filters.get('company'), 'default_currency') - - # This is where my zip will be written - zip_buffer = BytesIO() - # This is my zip file - datev_zip = zipfile.ZipFile(zip_buffer, mode='w', compression=zipfile.ZIP_DEFLATED) - transactions = get_transactions(filters) - transactions_csv = get_datev_csv(transactions, filters, csv_class=Transactions) - datev_zip.writestr('EXTF_Buchungsstapel.csv', transactions_csv) - account_names = get_account_names(filters) - account_names_csv = get_datev_csv(account_names, filters, csv_class=AccountNames) - datev_zip.writestr('EXTF_Kontenbeschriftungen.csv', account_names_csv) - customers = get_customers(filters) - customers_csv = get_datev_csv(customers, filters, csv_class=DebtorsCreditors) - datev_zip.writestr('EXTF_Kunden.csv', customers_csv) - suppliers = get_suppliers(filters) - suppliers_csv = get_datev_csv(suppliers, filters, csv_class=DebtorsCreditors) - datev_zip.writestr('EXTF_Lieferanten.csv', suppliers_csv) - - # You must call close() before exiting your program or essential records will not be written. - datev_zip.close() - frappe.response['filecontent'] = zip_buffer.getvalue() - frappe.response['filename'] = 'DATEV.zip' - frappe.response['type'] = 'binary' + download_csv_files_as_zip([ + { + 'file_name': 'EXTF_Buchungsstapel.csv', + 'csv_data': get_datev_csv(transactions, filters, csv_class=Transactions) + }, + { + 'file_name': 'EXTF_Kontenbeschriftungen.csv', + 'csv_data': get_datev_csv(account_names, filters, csv_class=AccountNames) + }, + { + 'file_name': 'EXTF_Kunden.csv', + 'csv_data': get_datev_csv(customers, filters, csv_class=DebtorsCreditors) + }, + { + 'file_name': 'EXTF_Lieferanten.csv', + 'csv_data': get_datev_csv(suppliers, filters, csv_class=DebtorsCreditors) + }, + ]) diff --git a/erpnext/regional/report/datev/test_datev.py b/erpnext/regional/report/datev/test_datev.py index eed62a8690..9529923a73 100644 --- a/erpnext/regional/report/datev/test_datev.py +++ b/erpnext/regional/report/datev/test_datev.py @@ -1,32 +1,22 @@ # coding=utf-8 from __future__ import unicode_literals -import os -import json import zipfile +import frappe from six import BytesIO from unittest import TestCase - -import frappe -from frappe.utils import getdate, today, now_datetime, cstr -from frappe.test_runner import make_test_objects +from frappe.utils import today, now_datetime, cstr from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice -from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts from erpnext.regional.report.datev.datev import validate from erpnext.regional.report.datev.datev import get_transactions from erpnext.regional.report.datev.datev import get_customers from erpnext.regional.report.datev.datev import get_suppliers from erpnext.regional.report.datev.datev import get_account_names -from erpnext.regional.report.datev.datev import get_datev_csv -from erpnext.regional.report.datev.datev import get_header from erpnext.regional.report.datev.datev import download_datev_csv -from erpnext.regional.report.datev.datev_constants import DataCategory -from erpnext.regional.report.datev.datev_constants import Transactions -from erpnext.regional.report.datev.datev_constants import DebtorsCreditors -from erpnext.regional.report.datev.datev_constants import AccountNames -from erpnext.regional.report.datev.datev_constants import QUERY_REPORT_COLUMNS +from erpnext.regional.germany.utils.datev.datev_csv import get_datev_csv, get_header +from erpnext.regional.germany.utils.datev.datev_constants import Transactions, DebtorsCreditors, AccountNames def make_company(company_name, abbr): if not frappe.db.exists("Company", company_name): From b719378ee5e2d1d8701db82650d14f0536d0b4cd Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 29 Jul 2020 18:11:04 +0200 Subject: [PATCH 004/192] fix: codacy --- erpnext/regional/germany/utils/datev/datev_csv.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/germany/utils/datev/datev_csv.py b/erpnext/regional/germany/utils/datev/datev_csv.py index be41a4ab45..df95a5b95a 100644 --- a/erpnext/regional/germany/utils/datev/datev_csv.py +++ b/erpnext/regional/germany/utils/datev/datev_csv.py @@ -9,15 +9,12 @@ import pandas as pd from frappe import _ from csv import QUOTE_NONNUMERIC from six import BytesIO -from six import string_types from .datev_constants import DataCategory from .datev_constants import Transactions -from .datev_constants import AccountNames def get_datev_csv(data, filters, csv_class): - """ - Fill in missing columns and return a CSV in DATEV Format. + """Fill in missing columns and return a CSV in DATEV Format. For automatic processing, DATEV requires the first line of the CSV file to hold meta data such as the length of account numbers oder the category of @@ -158,8 +155,7 @@ def get_header(filters, csv_class): def download_csv_files_as_zip(csv_data_list): - """ - Put CSV files in a zip archive and send that to the client. + """Put CSV files in a zip archive and send that to the client. Params: csv_data_list -- list of dicts [{'file_name': 'EXTF_Buchunsstapel.zip', 'csv_data': get_datev_csv()}] From 517df5fb0eba9e06c3623c80b05bfba42b75b470 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 29 Jul 2020 18:16:49 +0200 Subject: [PATCH 005/192] fix: codacy (again) --- erpnext/regional/germany/utils/datev/datev_constants.py | 2 ++ erpnext/regional/report/datev/datev.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/germany/utils/datev/datev_constants.py b/erpnext/regional/germany/utils/datev/datev_constants.py index 88dc02dfca..63f9a777bb 100644 --- a/erpnext/regional/germany/utils/datev/datev_constants.py +++ b/erpnext/regional/germany/utils/datev/datev_constants.py @@ -461,6 +461,7 @@ ACCOUNT_NAME_COLUMNS = [ ] class DataCategory(): + """Field of the CSV Header.""" DEBTORS_CREDITORS = "16" @@ -469,6 +470,7 @@ class DataCategory(): POSTING_TEXT_CONSTANTS = "67" class FormatName(): + """Field of the CSV Header, corresponds to DataCategory.""" DEBTORS_CREDITORS = "Debitoren/Kreditoren" diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 97d93d3b80..dd818e6054 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -10,7 +10,6 @@ Provide a report and downloadable CSV according to the German DATEV format. from __future__ import unicode_literals import json -import six import frappe from frappe import _ from six import string_types From 440c7525292a804995d9decc48b76310a793a480 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 30 Jul 2020 12:16:18 +0530 Subject: [PATCH 006/192] chore: Lab Test Template form clean-up --- .../lab_test_template/lab_test_template.json | 8 ++-- .../lab_test_template/lab_test_template.py | 37 ++++++++++--------- .../lab_test_template_list.js | 2 +- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json index db64297269..fc6a1e1790 100644 --- a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json @@ -112,7 +112,7 @@ { "default": "1", "depends_on": "eval:doc.lab_test_template_type != 'Grouped'", - "description": "If unchecked, the item wont be appear in Sales Invoice, but can be used in group test creation. ", + "description": "If unchecked, the item will not be available in Sales Invoices for billing but can be used in group test creation. ", "fieldname": "is_billable", "fieldtype": "Check", "label": "Is Billable", @@ -184,7 +184,7 @@ "depends_on": "eval:doc.lab_test_template_type == 'Descriptive'", "fieldname": "section_break_special", "fieldtype": "Section Break", - "label": "Descriptive" + "label": "Descriptive Test" }, { "default": "0", @@ -196,7 +196,7 @@ "depends_on": "eval:doc.lab_test_template_type == 'Grouped'", "fieldname": "section_break_group", "fieldtype": "Section Break", - "label": "Group" + "label": "Group Tests" }, { "fieldname": "lab_test_groups", @@ -314,7 +314,7 @@ } ], "links": [], - "modified": "2020-07-13 12:57:09.925436", + "modified": "2020-07-30 11:55:43.093828", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Test Template", diff --git a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py index 6f0d08cf85..e4fbdd9d86 100644 --- a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py @@ -15,7 +15,8 @@ class LabTestTemplate(Document): def validate(self): if self.is_billable and (not self.lab_test_rate or self.lab_test_rate <= 0.0): - frappe.throw(_("Standard Selling Rate should be greater than zero.")) + frappe.throw(_('Standard Selling Rate should be greater than zero.')) + self.validate_conversion_factor() self.enable_disable_item() @@ -42,7 +43,9 @@ class LabTestTemplate(Document): # Remove template reference from item and disable item if self.item: try: - frappe.delete_doc('Item', self.item) + item = self.item + self.db_set('item', '') + frappe.delete_doc('Item', item) except Exception: frappe.throw(_('Not permitted. Please disable the Lab Test Template')) @@ -63,26 +66,26 @@ class LabTestTemplate(Document): 'standard_rate': self.lab_test_rate, 'description': self.lab_test_description }) - item.save() + item.flags.ignore_mandatory = True + item.save(ignore_permissions=True) def item_price_exists(self): item_price = frappe.db.exists({'doctype': 'Item Price', 'item_code': self.lab_test_code}) if item_price: return item_price[0][0] - else: - return False + return False def validate_conversion_factor(self): - if self.lab_test_template_type == "Single" and self.secondary_uom and not self.conversion_factor: - frappe.throw(_("Conversion Factor is mandatory")) - if self.lab_test_template_type == "Compound": + if self.lab_test_template_type == 'Single' and self.secondary_uom and not self.conversion_factor: + frappe.throw(_('Conversion Factor is mandatory')) + if self.lab_test_template_type == 'Compound': for item in self.normal_test_templates: if item.secondary_uom and not item.conversion_factor: - frappe.throw(_("Conversion Factor is mandatory")) - if self.lab_test_template_type == "Grouped": + frappe.throw(_('Conversion Factor is mandatory')) + if self.lab_test_template_type == 'Grouped': for group in self.lab_test_groups: - if group.template_or_new_line == "Add New Line" and group.secondary_uom and not group.conversion_factor: - frappe.throw(_("Conversion Factor is mandatory")) + if group.template_or_new_line == 'Add New Line' and group.secondary_uom and not group.conversion_factor: + frappe.throw(_('Conversion Factor is mandatory')) def create_item_from_template(doc): @@ -101,9 +104,9 @@ def create_item_from_template(doc): 'include_item_in_manufacturing': 0, 'show_in_website': 0, 'is_pro_applicable': 0, - 'disabled': 0 if doc.is_billable and not doc.disabled else doc.disabled, + 'disabled': 0 if doc.is_billable and not doc.disabled else doc.disabled, 'stock_uom': uom - }).insert(ignore_permissions = True, ignore_mandatory = True) + }).insert(ignore_permissions=True, ignore_mandatory=True) # Insert item price if doc.is_billable and doc.lab_test_rate != 0.0: @@ -123,7 +126,7 @@ def make_item_price(item, price_list_name, item_price): 'price_list': price_list_name, 'item_code': item, 'price_list_rate': item_price - }).insert(ignore_permissions = True, ignore_mandatory = True) + }).insert(ignore_permissions=True, ignore_mandatory=True) @frappe.whitelist() def change_test_code_from_template(lab_test_code, doc): @@ -132,8 +135,8 @@ def change_test_code_from_template(lab_test_code, doc): if frappe.db.exists({'doctype': 'Item', 'item_code': lab_test_code}): frappe.throw(_('Lab Test Item {0} already exist').format(lab_test_code)) else: - rename_doc('Item', doc.name, lab_test_code, ignore_permissions = True) + rename_doc('Item', doc.name, lab_test_code, ignore_permissions=True) frappe.db.set_value('Lab Test Template', doc.name, 'lab_test_code', lab_test_code) frappe.db.set_value('Lab Test Template', doc.name, 'lab_test_name', lab_test_code) - rename_doc('Lab Test Template', doc.name, lab_test_code, ignore_permissions = True) + rename_doc('Lab Test Template', doc.name, lab_test_code, ignore_permissions=True) return lab_test_code diff --git a/erpnext/healthcare/doctype/lab_test_template/lab_test_template_list.js b/erpnext/healthcare/doctype/lab_test_template/lab_test_template_list.js index a3417ebdfc..08fc2cddda 100644 --- a/erpnext/healthcare/doctype/lab_test_template/lab_test_template_list.js +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template_list.js @@ -3,5 +3,5 @@ */ frappe.listview_settings['Lab Test Template'] = { add_fields: ['lab_test_name', 'lab_test_code', 'lab_test_rate'], - filters: [['disabled', '=', 0]] + filters: [['disabled', '=', 'No']] }; From ff0b9bffcf373a8781ed76008aec551b2648cba1 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 30 Jul 2020 12:16:56 +0530 Subject: [PATCH 007/192] feat: added dashboard for Lab Test Template --- .../lab_test_template_dashboard.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 erpnext/healthcare/doctype/lab_test_template/lab_test_template_dashboard.py diff --git a/erpnext/healthcare/doctype/lab_test_template/lab_test_template_dashboard.py b/erpnext/healthcare/doctype/lab_test_template/lab_test_template_dashboard.py new file mode 100644 index 0000000000..94dfeea7a4 --- /dev/null +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template_dashboard.py @@ -0,0 +1,13 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'template', + 'transactions': [ + { + 'label': _('Lab Tests'), + 'items': ['Lab Test'] + } + ] + } From b00f870c77c2fcd0e6ee829a5c858550fe8c9e88 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 30 Jul 2020 13:49:13 +0530 Subject: [PATCH 008/192] fix: Lab Test Completed status not visible in list view --- .../healthcare/doctype/lab_test/lab_test_list.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/erpnext/healthcare/doctype/lab_test/lab_test_list.js b/erpnext/healthcare/doctype/lab_test/lab_test_list.js index b7f157c38b..0a6ed20f3e 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test_list.js +++ b/erpnext/healthcare/doctype/lab_test/lab_test_list.js @@ -3,13 +3,16 @@ */ frappe.listview_settings['Lab Test'] = { add_fields: ['name', 'status', 'invoiced'], - filters: [['docstatus', '=', '0']], + filters: [['docstatus', '=', '1']], get_indicator: function (doc) { - if (doc.status == 'Approved') { - return [__('Approved'), 'green', 'status, = ,Approved']; - } - if (doc.status == 'Rejected') { + if (doc.status === 'Approved') { + return [__('Approved'), 'green', 'status, =, Approved']; + } else if (doc.status === 'Rejected') { return [__('Rejected'), 'orange', 'status, =, Rejected']; + } else if (doc.status === 'Completed') { + return [__('Completed'), 'green', 'status, =, Completed']; + } else if (doc.status === 'Cancelled') { + return [__('Cancelled'), 'red', 'status, =, Cancelled']; } }, onload: function (listview) { From c33703d54c3294793d4d644d7a541216c6b34338 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 30 Jul 2020 14:25:40 +0530 Subject: [PATCH 009/192] fix: move result value validations to server side --- .../healthcare/doctype/lab_test/lab_test.js | 17 -------- .../healthcare/doctype/lab_test/lab_test.json | 23 ++++++----- .../healthcare/doctype/lab_test/lab_test.py | 40 +++++++++++++------ .../lab_test_group_template.json | 5 ++- .../lab_test_template/lab_test_template.py | 4 +- 5 files changed, 46 insertions(+), 43 deletions(-) diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.js b/erpnext/healthcare/doctype/lab_test/lab_test.js index 8036c7dc13..87d9c83763 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.js +++ b/erpnext/healthcare/doctype/lab_test/lab_test.js @@ -179,23 +179,6 @@ var show_lab_tests = function (frm, lab_test_list) { d.show(); }; -cur_frm.cscript.custom_before_submit = function (doc) { - if (doc.normal_test_items) { - for (let result in doc.normal_test_items) { - if (!doc.normal_test_items[result].result_value && !doc.normal_test_items[result].allow_blank && doc.normal_test_items[result].require_result_value) { - frappe.throw(__('Please input all required result values')); - } - } - } - if (doc.descriptive_test_items) { - for (let result in doc.descriptive_test_items) { - if (!doc.descriptive_test_items[result].result_value && !doc.descriptive_test_items[result].allow_blank && doc.descriptive_test_items[result].require_result_value) { - frappe.throw(__('Please input all required result values')); - } - } - } -}; - var make_dialog = function (frm, emailed, printed) { var number = frm.doc.mobile; diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.json b/erpnext/healthcare/doctype/lab_test/lab_test.json index 2eb8014b7e..575a2659db 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.json +++ b/erpnext/healthcare/doctype/lab_test/lab_test.json @@ -84,7 +84,7 @@ "fieldname": "naming_series", "fieldtype": "Select", "label": "Series", - "options": "LP-", + "options": "HLC-LAB-.YYYY.-", "print_hide": 1, "report_hide": 1, "reqd": 1 @@ -197,11 +197,10 @@ { "fieldname": "status", "fieldtype": "Select", + "in_list_view": 1, "label": "Status", "options": "Draft\nCompleted\nApproved\nRejected\nCancelled", - "print_hide": 1, "read_only": 1, - "report_hide": 1, "search_index": 1 }, { @@ -354,7 +353,8 @@ }, { "fieldname": "sb_normal", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Compound Test Result" }, { "fieldname": "normal_test_items", @@ -369,11 +369,13 @@ { "depends_on": "descriptive_toggle", "fieldname": "organisms_section", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Organism Test Result" }, { "fieldname": "sb_sensitivity", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Sensitivity Test Result" }, { "fieldname": "sensitivity_test_items", @@ -383,8 +385,10 @@ "report_hide": 1 }, { + "collapsible": 1, "fieldname": "sb_comments", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Comments" }, { "fieldname": "lab_test_comment", @@ -531,7 +535,8 @@ }, { "fieldname": "sb_descriptive", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Descriptive Test Result" }, { "default": "0", @@ -550,7 +555,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-07-16 13:35:24.811062", + "modified": "2020-07-30 14:03:00.166003", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Test", diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.py b/erpnext/healthcare/doctype/lab_test/lab_test.py index 865f4a14e3..c676dfb8a8 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/lab_test.py @@ -9,20 +9,21 @@ from frappe.model.document import Document from frappe.utils import getdate, cstr class LabTest(Document): + def validate(self): + if not self.is_new(): + self.set_secondary_uom_result() + def on_submit(self): + self.validate_result_values() self.db_set('submitted_date', getdate()) self.db_set('status', 'Completed') insert_lab_test_to_medical_record(self) def on_cancel(self): - delete_lab_test_from_medical_record(self) self.db_set('status', 'Cancelled') + delete_lab_test_from_medical_record(self) self.reload() - def validate(self): - if not self.is_new(): - self.set_secondary_uom_result() - def on_update(self): if self.sensitivity_test_items: sensitivity = sorted(self.sensitivity_test_items, key=lambda x: x.antibiotic_sensitivity) @@ -51,7 +52,20 @@ class LabTest(Document): item.secondary_uom_result = float(item.result_value) * float(item.conversion_factor) except: item.secondary_uom_result = '' - frappe.msgprint(_('Result for Secondary UOM not calculated for row #{0}'.format(item.idx)), title = _('Warning')) + frappe.msgprint(_('Row #{0}: Result for Secondary UOM not calculated'.format(item.idx)), title = _('Warning')) + + def validate_result_values(self): + if self.normal_test_items: + for item in self.normal_test_items: + if not item.result_value and not item.allow_blank and item.require_result_value: + frappe.throw(_('Row #{0}: Please enter the result value for {1}').format( + item.idx, frappe.bold(item.lab_test_name)), title=_('Mandatory Results')) + + if self.descriptive_test_items: + for item in self.descriptive_test_items: + if not item.result_value and not item.allow_blank and item.require_result_value: + frappe.throw(_('Row #{0}: Please enter the result value {1}').format( + item.idx, frappe.bold(item.lab_test_name)), title=_('Mandatory Results')) def create_test_from_template(lab_test): @@ -263,8 +277,7 @@ def load_result_format(lab_test, template, prescription, invoice): for lab_test_group in template.lab_test_groups: # Template_in_group = None if lab_test_group.lab_test_template: - template_in_group = frappe.get_doc('Lab Test Template', - lab_test_group.lab_test_template) + template_in_group = frappe.get_doc('Lab Test Template', lab_test_group.lab_test_template) if template_in_group: if template_in_group.lab_test_template_type == 'Single': create_normals(template_in_group, lab_test) @@ -302,9 +315,10 @@ def load_result_format(lab_test, template, prescription, invoice): @frappe.whitelist() def get_employee_by_user_id(user_id): - emp_id = frappe.db.get_value('Employee', { 'user_id': user_id }) - employee = frappe.get_doc('Employee', emp_id) - return employee + emp_id = frappe.db.exists('Employee', { 'user_id': user_id }) + if emp_id: + return frappe.get_doc('Employee', emp_id) + return None def insert_lab_test_to_medical_record(doc): table_row = False @@ -325,7 +339,7 @@ def insert_lab_test_to_medical_record(doc): table_row += ' ' + frappe.bold(_('Lab Test Result: ')) + item.result_value if item.normal_range: - table_row += ' ' + _('Normal Range:') + item.normal_range + table_row += ' ' + _('Normal Range: ') + item.normal_range table_row += ' ' + comment elif doc.descriptive_test_items: @@ -356,7 +370,7 @@ def insert_lab_test_to_medical_record(doc): medical_record.save(ignore_permissions = True) def delete_lab_test_from_medical_record(self): - medical_record_id = frappe.db.sql('select name from `tabPatient Medical Record` where reference_name= %s', (self.name)) + medical_record_id = frappe.db.sql('select name from `tabPatient Medical Record` where reference_name=%s', (self.name)) if medical_record_id and medical_record_id[0][0]: frappe.delete_doc('Patient Medical Record', medical_record_id[0][0]) diff --git a/erpnext/healthcare/doctype/lab_test_group_template/lab_test_group_template.json b/erpnext/healthcare/doctype/lab_test_group_template/lab_test_group_template.json index beea7a357e..2767f7ec77 100644 --- a/erpnext/healthcare/doctype/lab_test_group_template/lab_test_group_template.json +++ b/erpnext/healthcare/doctype/lab_test_group_template/lab_test_group_template.json @@ -93,7 +93,8 @@ "depends_on": "secondary_uom", "fieldname": "conversion_factor", "fieldtype": "Float", - "label": "Conversion Factor" + "label": "Conversion Factor", + "mandatory_depends_on": "secondary_uom" }, { "default": "0", @@ -106,7 +107,7 @@ ], "istable": 1, "links": [], - "modified": "2020-06-24 10:59:01.921924", + "modified": "2020-07-30 12:36:03.082391", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Test Group Template", diff --git a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py index e4fbdd9d86..543dee27eb 100644 --- a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py @@ -81,11 +81,11 @@ class LabTestTemplate(Document): if self.lab_test_template_type == 'Compound': for item in self.normal_test_templates: if item.secondary_uom and not item.conversion_factor: - frappe.throw(_('Conversion Factor is mandatory')) + frappe.throw(_('Row #{0}: Conversion Factor is mandatory').format(item.idx)) if self.lab_test_template_type == 'Grouped': for group in self.lab_test_groups: if group.template_or_new_line == 'Add New Line' and group.secondary_uom and not group.conversion_factor: - frappe.throw(_('Conversion Factor is mandatory')) + frappe.throw(_('Row #{0}: Conversion Factor is mandatory').format(group.idx)) def create_item_from_template(doc): From 73edb12ab3a579794782ae63173517f1c01a20ba Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 30 Jul 2020 14:50:30 +0530 Subject: [PATCH 010/192] fix: msgprint for sample collection doc created on Lab Test creation --- erpnext/healthcare/doctype/lab_test/lab_test.py | 6 ++++-- .../lab_test_template/lab_test_template.json | 15 ++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.py b/erpnext/healthcare/doctype/lab_test/lab_test.py index c676dfb8a8..2bf4a3a7db 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/lab_test.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import getdate, cstr +from frappe.utils import getdate, cstr, get_link_to_form class LabTest(Document): def validate(self): @@ -262,7 +262,9 @@ def create_sample_collection(lab_test, template, patient, invoice): sample_collection = create_sample_doc(template, patient, invoice, lab_test.company) if sample_collection: lab_test.sample = sample_collection.name - + sample_collection_doc = get_link_to_form('Sample Collection', sample_collection.name) + frappe.msgprint(_('Sample Collection {0} has been created').format(sample_collection_doc), + title=_('Sample Collection'), indicator='green') return lab_test def load_result_format(lab_test, template, prescription, invoice): diff --git a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json index fc6a1e1790..c3fc842047 100644 --- a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json @@ -34,14 +34,15 @@ "descriptive_test_templates", "section_break_group", "lab_test_groups", - "medical_coding_section", - "medical_code_standard", - "medical_code", "sb_sample_collection", "sample", "sample_uom", "sample_qty", + "column_break_33", "sample_details", + "medical_coding_section", + "medical_code", + "medical_code_standard", "worksheet_section", "worksheet_instructions", "result_legend_section", @@ -128,6 +129,7 @@ "mandatory_depends_on": "eval:doc.is_billable == 1" }, { + "collapsible": 1, "fieldname": "medical_coding_section", "fieldtype": "Section Break", "label": "Medical Coding" @@ -217,7 +219,6 @@ "no_copy": 1 }, { - "collapsible": 1, "fieldname": "sb_sample_collection", "fieldtype": "Section Break", "label": "Sample Collection" @@ -311,10 +312,14 @@ "fieldname": "descriptive_test_templates", "fieldtype": "Table", "options": "Descriptive Test Template" + }, + { + "fieldname": "column_break_33", + "fieldtype": "Column Break" } ], "links": [], - "modified": "2020-07-30 11:55:43.093828", + "modified": "2020-07-30 14:32:40.449818", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Test Template", From afa001a1d1f6a756b22e362535ac79b0e61f9a7d Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 30 Jul 2020 15:25:04 +0530 Subject: [PATCH 011/192] fix: code clean-up --- erpnext/healthcare/doctype/lab_test/lab_test.js | 16 ++++++++-------- erpnext/healthcare/doctype/lab_test/lab_test.py | 14 +++++++++++--- .../healthcare/doctype/lab_test/lab_test_list.js | 4 ++-- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.js b/erpnext/healthcare/doctype/lab_test/lab_test.js index 87d9c83763..f1634c1294 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.js +++ b/erpnext/healthcare/doctype/lab_test/lab_test.js @@ -34,10 +34,10 @@ frappe.ui.form.on('Lab Test', { if (frm.doc.docstatus === 1 && frm.doc.status !== 'Approved' && frm.doc.status !== 'Rejected') { frm.add_custom_button(__('Approve'), function () { status_update(1, frm); - }); + }, __('Actions')); frm.add_custom_button(__('Reject'), function () { status_update(0, frm); - }); + }, __('Actions')); } } @@ -186,7 +186,7 @@ var make_dialog = function (frm, emailed, printed) { title: 'Send SMS', width: 400, fields: [ - { fieldname: 'sms_type', fieldtype: 'Select', label: 'Type', options: ['Emailed', 'Printed'] }, + { fieldname: 'result_format', fieldtype: 'Select', label: 'Result Format', options: ['Emailed', 'Printed'] }, { fieldname: 'number', fieldtype: 'Data', label: 'Mobile Number', reqd: 1 }, { fieldname: 'message', fieldtype: 'Small Text', label: 'Message', reqd: 1 } ], @@ -200,22 +200,22 @@ var make_dialog = function (frm, emailed, printed) { dialog.hide(); } }); - if (frm.doc.report_preference == 'Print') { + if (frm.doc.report_preference === 'Print') { dialog.set_values({ - 'sms_type': 'Printed', + 'result_format': 'Printed', 'number': number, 'message': printed }); } else { dialog.set_values({ - 'sms_type': 'Emailed', + 'result_format': 'Emailed', 'number': number, 'message': emailed }); } var fd = dialog.fields_dict; - $(fd.sms_type.input).change(function () { - if (dialog.get_value('sms_type') == 'Emailed') { + $(fd.result_format.input).change(function () { + if (dialog.get_value('result_format') === 'Emailed') { dialog.set_values({ 'number': number, 'message': emailed diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.py b/erpnext/healthcare/doctype/lab_test/lab_test.py index 2bf4a3a7db..8dc26b09bc 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/lab_test.py @@ -103,7 +103,7 @@ def create_multiple(doctype, docname): lab_test_created = create_lab_test_from_encounter(docname) if lab_test_created: - frappe.msgprint(_('Lab Test(s) {0} created'.format(lab_test_created))) + frappe.msgprint(_('Lab Test(s) {0} created'.format(lab_test_created)), indicator='green') else: frappe.msgprint(_('No Lab Tests created')) @@ -225,8 +225,9 @@ def create_sample_doc(template, patient, invoice, company = None): 'docstatus': 0, 'sample': template.sample }) + if sample_exists: - # Update Sample Collection by adding quantity + # update sample collection by adding quantity sample_collection = frappe.get_doc('Sample Collection', sample_exists[0][0]) quantity = int(sample_collection.sample_qty) + int(template.sample_qty) if template.sample_details: @@ -252,7 +253,7 @@ def create_sample_doc(template, patient, invoice, company = None): sample_collection.company = company if template.sample_details: - sample_collection.sample_details = 'Test :' + (template.get('lab_test_name') or template.get('template')) + '\n' + 'Collection Detials:\n\t' + template.sample_details + sample_collection.sample_details = _('Test :') + (template.get('lab_test_name') or template.get('template')) + '\n' + 'Collection Detials:\n\t' + template.sample_details sample_collection.save(ignore_permissions=True) return sample_collection @@ -270,10 +271,13 @@ def create_sample_collection(lab_test, template, patient, invoice): def load_result_format(lab_test, template, prescription, invoice): if template.lab_test_template_type == 'Single': create_normals(template, lab_test) + elif template.lab_test_template_type == 'Compound': create_compounds(template, lab_test, False) + elif template.lab_test_template_type == 'Descriptive': create_descriptives(template, lab_test) + elif template.lab_test_template_type == 'Grouped': # Iterate for each template in the group and create one result for all. for lab_test_group in template.lab_test_groups: @@ -283,6 +287,7 @@ def load_result_format(lab_test, template, prescription, invoice): if template_in_group: if template_in_group.lab_test_template_type == 'Single': create_normals(template_in_group, lab_test) + elif template_in_group.lab_test_template_type == 'Compound': normal_heading = lab_test.append('normal_test_items') normal_heading.lab_test_name = template_in_group.lab_test_name @@ -290,6 +295,7 @@ def load_result_format(lab_test, template, prescription, invoice): normal_heading.allow_blank = 1 normal_heading.template = template_in_group.name create_compounds(template_in_group, lab_test, True) + elif template_in_group.lab_test_template_type == 'Descriptive': descriptive_heading = lab_test.append('descriptive_test_items') descriptive_heading.lab_test_name = template_in_group.lab_test_name @@ -297,6 +303,7 @@ def load_result_format(lab_test, template, prescription, invoice): descriptive_heading.allow_blank = 1 descriptive_heading.template = template_in_group.name create_descriptives(template_in_group, lab_test) + else: # Lab Test Group - Add New Line normal = lab_test.append('normal_test_items') normal.lab_test_name = lab_test_group.group_event @@ -307,6 +314,7 @@ def load_result_format(lab_test, template, prescription, invoice): normal.allow_blank = lab_test_group.allow_blank normal.require_result_value = 1 normal.template = template.name + if template.lab_test_template_type != 'No Result': if prescription: lab_test.prescription = prescription diff --git a/erpnext/healthcare/doctype/lab_test/lab_test_list.js b/erpnext/healthcare/doctype/lab_test/lab_test_list.js index 0a6ed20f3e..7b5b9d922a 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test_list.js +++ b/erpnext/healthcare/doctype/lab_test/lab_test_list.js @@ -24,7 +24,7 @@ frappe.listview_settings['Lab Test'] = { var create_multiple_dialog = function (listview) { var dialog = new frappe.ui.Dialog({ - title: 'Create Multiple Lab Test', + title: 'Create Multiple Lab Tests', width: 100, fields: [ { fieldtype: 'Link', label: 'Patient', fieldname: 'patient', options: 'Patient', reqd: 1 }, @@ -44,7 +44,7 @@ var create_multiple_dialog = function (listview) { } } ], - primary_action_label: __('Create Lab Test'), + primary_action_label: __('Create'), primary_action: function () { frappe.call({ method: 'erpnext.healthcare.doctype.lab_test.lab_test.create_multiple', From fa2c20ea9e9f127adc871b7ba188f7ce44b1a155 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 30 Jul 2020 16:56:11 +0530 Subject: [PATCH 012/192] chore: Sample Collection Form clean-up --- .../healthcare/doctype/lab_test/lab_test.py | 6 +- .../sample_collection/sample_collection.js | 20 ++--- .../sample_collection/sample_collection.json | 85 ++++++++++++++++--- 3 files changed, 85 insertions(+), 26 deletions(-) diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.py b/erpnext/healthcare/doctype/lab_test/lab_test.py index 8dc26b09bc..2db7743865 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/lab_test.py @@ -64,8 +64,8 @@ class LabTest(Document): if self.descriptive_test_items: for item in self.descriptive_test_items: if not item.result_value and not item.allow_blank and item.require_result_value: - frappe.throw(_('Row #{0}: Please enter the result value {1}').format( - item.idx, frappe.bold(item.lab_test_name)), title=_('Mandatory Results')) + frappe.throw(_('Row #{0}: Please enter the result value for {1}').format( + item.idx, frappe.bold(item.lab_test_particulars)), title=_('Mandatory Results')) def create_test_from_template(lab_test): @@ -103,7 +103,7 @@ def create_multiple(doctype, docname): lab_test_created = create_lab_test_from_encounter(docname) if lab_test_created: - frappe.msgprint(_('Lab Test(s) {0} created'.format(lab_test_created)), indicator='green') + frappe.msgprint(_('Lab Test(s) {0} created successfully').format(lab_test_created), indicator='green') else: frappe.msgprint(_('No Lab Tests created')) diff --git a/erpnext/healthcare/doctype/sample_collection/sample_collection.js b/erpnext/healthcare/doctype/sample_collection/sample_collection.js index 2f5278b2d5..0390391235 100644 --- a/erpnext/healthcare/doctype/sample_collection/sample_collection.js +++ b/erpnext/healthcare/doctype/sample_collection/sample_collection.js @@ -3,29 +3,29 @@ frappe.ui.form.on('Sample Collection', { refresh: function(frm) { - if(frappe.defaults.get_default("create_sample_collection_for_lab_test")){ - frm.add_custom_button(__("View Lab Tests"), function() { - frappe.route_options = {"sample": frm.doc.name}; - frappe.set_route("List", "Lab Test"); + if (frappe.defaults.get_default('create_sample_collection_for_lab_test')) { + frm.add_custom_button(__('View Lab Tests'), function() { + frappe.route_options = {'sample': frm.doc.name}; + frappe.set_route('List', 'Lab Test'); }); } } }); -frappe.ui.form.on("Sample Collection", "patient", function(frm) { +frappe.ui.form.on('Sample Collection', 'patient', function(frm) { if(frm.doc.patient){ frappe.call({ - "method": "erpnext.healthcare.doctype.patient.patient.get_patient_detail", + 'method': 'erpnext.healthcare.doctype.patient.patient.get_patient_detail', args: { patient: frm.doc.patient }, callback: function (data) { var age = null; - if(data.message.dob){ + if (data.message.dob){ age = calculate_age(data.message.dob); } - frappe.model.set_value(frm.doctype,frm.docname, "patient_age", age); - frappe.model.set_value(frm.doctype,frm.docname, "patient_sex", data.message.sex); + frappe.model.set_value(frm.doctype,frm.docname, 'patient_age', age); + frappe.model.set_value(frm.doctype,frm.docname, 'patient_sex', data.message.sex); } }); } @@ -36,5 +36,5 @@ var calculate_age = function(birth) { var age = new Date(); age.setTime(ageMS); var years = age.getFullYear() - 1970; - return years + " Year(s) " + age.getMonth() + " Month(s) " + age.getDate() + " Day(s)"; + return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)'; }; diff --git a/erpnext/healthcare/doctype/sample_collection/sample_collection.json b/erpnext/healthcare/doctype/sample_collection/sample_collection.json index 016cfbc3ae..83383e3445 100644 --- a/erpnext/healthcare/doctype/sample_collection/sample_collection.json +++ b/erpnext/healthcare/doctype/sample_collection/sample_collection.json @@ -9,8 +9,10 @@ "document_type": "Document", "engine": "InnoDB", "field_order": [ + "patient_details_section", "naming_series", "patient", + "patient_name", "patient_age", "patient_sex", "column_break_4", @@ -25,15 +27,17 @@ "collected_by", "collected_time", "num_print", - "amended_from", "section_break_15", - "sample_details" + "sample_details", + "amended_from" ], "fields": [ { "fetch_from": "patient.inpatient_record", "fieldname": "inpatient_record", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Inpatient Record", "options": "Inpatient Record", "read_only": 1 @@ -42,6 +46,8 @@ "bold": 1, "fieldname": "naming_series", "fieldtype": "Select", + "hide_days": 1, + "hide_seconds": 1, "label": "Series", "no_copy": 1, "options": "HLC-SC-.YYYY.-", @@ -52,6 +58,8 @@ "default": "0", "fieldname": "invoiced", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Invoiced", "no_copy": 1, "read_only": 1, @@ -61,41 +69,60 @@ "fetch_from": "inpatient_record.patient", "fieldname": "patient", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "ignore_user_permissions": 1, "in_standard_filter": 1, "label": "Patient", "options": "Patient", + "reqd": 1, "search_index": 1 }, { "fieldname": "column_break_4", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "patient_age", "fieldtype": "Data", - "label": "Age" + "hide_days": 1, + "hide_seconds": 1, + "label": "Age", + "read_only": 1 }, { "fetch_from": "patient.sex", "fieldname": "patient_sex", - "fieldtype": "Data", - "label": "Gender" + "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, + "label": "Gender", + "options": "Gender", + "read_only": 1 }, { "fieldname": "company", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "in_standard_filter": 1, "label": "Company", "options": "Company" }, { "fieldname": "section_break_6", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, + "label": "Sample Details" }, { "fieldname": "sample", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "ignore_user_permissions": 1, "in_list_view": 1, "in_standard_filter": 1, @@ -108,16 +135,23 @@ "fetch_from": "sample.sample_uom", "fieldname": "sample_uom", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, - "label": "UOM" + "label": "UOM", + "read_only": 1 }, { "fieldname": "column_break_10", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "collected_by", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "ignore_user_permissions": 1, "label": "Collected By", "options": "User" @@ -125,20 +159,27 @@ { "fieldname": "collected_time", "fieldtype": "Datetime", - "label": "Collected Time" + "hide_days": 1, + "hide_seconds": 1, + "label": "Collected On" }, { "allow_on_submit": 1, "default": "1", + "description": "Number of prints required for labelling the samples", "fieldname": "num_print", "fieldtype": "Int", - "label": "No. of print", + "hide_days": 1, + "hide_seconds": 1, + "label": "No. of prints", "print_hide": 1, "report_hide": 1 }, { "fieldname": "amended_from", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Amended From", "no_copy": 1, "options": "Sample Collection", @@ -147,25 +188,43 @@ }, { "fieldname": "section_break_15", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1 }, { "default": "0", "fieldname": "sample_qty", "fieldtype": "Float", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, "label": "Quantity" }, { "fieldname": "sample_details", "fieldtype": "Long Text", + "hide_days": 1, + "hide_seconds": 1, "ignore_xss_filter": 1, "label": "Collection Details" + }, + { + "fieldname": "patient_details_section", + "fieldtype": "Section Break", + "label": "Patient Details" + }, + { + "fetch_from": "patient.patient_name", + "fieldname": "patient_name", + "fieldtype": "Data", + "label": "Patient Name", + "read_only": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-05-25 14:36:46.990469", + "modified": "2020-07-30 16:53:13.076104", "modified_by": "Administrator", "module": "Healthcare", "name": "Sample Collection", From ca1b389c99529eeccf892fbfde8a2b34ce7a0153 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 30 Jul 2020 16:58:19 +0530 Subject: [PATCH 013/192] fix: validate negative quantity for sample collection --- .../doctype/sample_collection/sample_collection.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/healthcare/doctype/sample_collection/sample_collection.py b/erpnext/healthcare/doctype/sample_collection/sample_collection.py index 2c64320fac..461f809507 100644 --- a/erpnext/healthcare/doctype/sample_collection/sample_collection.py +++ b/erpnext/healthcare/doctype/sample_collection/sample_collection.py @@ -3,7 +3,12 @@ # For license information, please see license.txt from __future__ import unicode_literals +import frappe from frappe.model.document import Document +from frappe.utils import flt +from frappe import _ class SampleCollection(Document): - pass + def validate(self): + if flt(self.sample_qty) <= 0: + frappe.throw(_('Sample Quantity cannot be negative or 0'), title=_('Invalid Quantity')) From c5ccf38cdf849431f27d3601756159391c237cd4 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 30 Jul 2020 18:56:16 +0530 Subject: [PATCH 014/192] refactor: Lab Test Report --- .../healthcare/doctype/lab_test/lab_test.json | 4 +- .../report/lab_test_report/lab_test_report.js | 39 ++++- .../lab_test_report/lab_test_report.json | 39 ++--- .../report/lab_test_report/lab_test_report.py | 139 ++++++++++++++---- 4 files changed, 165 insertions(+), 56 deletions(-) diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.json b/erpnext/healthcare/doctype/lab_test/lab_test.json index 575a2659db..edf1d911aa 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.json +++ b/erpnext/healthcare/doctype/lab_test/lab_test.json @@ -248,8 +248,8 @@ { "fieldname": "result_date", "fieldtype": "Date", - "hidden": 1, "label": "Result Date", + "read_only": 1, "search_index": 1 }, { @@ -555,7 +555,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-07-30 14:03:00.166003", + "modified": "2020-07-30 18:18:38.516215", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Test", diff --git a/erpnext/healthcare/report/lab_test_report/lab_test_report.js b/erpnext/healthcare/report/lab_test_report/lab_test_report.js index 3128f819bb..7754e2e196 100644 --- a/erpnext/healthcare/report/lab_test_report/lab_test_report.js +++ b/erpnext/healthcare/report/lab_test_report/lab_test_report.js @@ -4,29 +4,54 @@ frappe.query_reports["Lab Test Report"] = { "filters": [ { - "fieldname":"from_date", + "fieldname": "from_date", "label": __("From Date"), "fieldtype": "Date", - "default": frappe.datetime.now_date(), - "width": "80" + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + "reqd": 1 }, { - "fieldname":"to_date", + "fieldname": "to_date", "label": __("To Date"), "fieldtype": "Date", - "default": frappe.datetime.now_date() + "default": frappe.datetime.now_date(), + "reqd": 1 }, { - "fieldname":"patient", + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "default": frappe.defaults.get_default("Company"), + "options": "Company" + }, + { + "fieldname": "template", + "label": __("Lab Test Template"), + "fieldtype": "Link", + "options": "Lab Test Template" + }, + { + "fieldname": "patient", "label": __("Patient"), "fieldtype": "Link", "options": "Patient" }, { - "fieldname":"department", + "fieldname": "department", "label": __("Medical Department"), "fieldtype": "Link", "options": "Medical Department" + }, + { + "fieldname": "status", + "label": __("Status"), + "fieldtype": "Select", + "options": "\nCompleted\nApproved\nRejected" + }, + { + "fieldname": "invoiced", + "label": __("Invoiced"), + "fieldtype": "Check" } ] }; diff --git a/erpnext/healthcare/report/lab_test_report/lab_test_report.json b/erpnext/healthcare/report/lab_test_report/lab_test_report.json index 30e5a5fd56..aeb42897b8 100644 --- a/erpnext/healthcare/report/lab_test_report/lab_test_report.json +++ b/erpnext/healthcare/report/lab_test_report/lab_test_report.json @@ -1,30 +1,31 @@ { - "add_total_row": 1, - "creation": "2013-04-23 18:15:29", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 1, - "is_standard": "Yes", - "modified": "2018-08-06 11:41:50.218737", - "modified_by": "Administrator", - "module": "Healthcare", - "name": "Lab Test Report", - "owner": "Administrator", - "prepared_report": 0, - "ref_doctype": "Lab Test", - "report_name": "Lab Test Report", - "report_type": "Script Report", + "add_total_row": 0, + "creation": "2013-04-23 18:15:29", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 1, + "is_standard": "Yes", + "modified": "2020-07-30 18:53:20.102873", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Lab Test Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Lab Test", + "report_name": "Lab Test Report", + "report_type": "Script Report", "roles": [ { "role": "Laboratory User" - }, + }, { "role": "Nursing User" - }, + }, { "role": "LabTest Approver" - }, + }, { "role": "Healthcare Administrator" } diff --git a/erpnext/healthcare/report/lab_test_report/lab_test_report.py b/erpnext/healthcare/report/lab_test_report/lab_test_report.py index 17f25fa7a7..be2d06193e 100644 --- a/erpnext/healthcare/report/lab_test_report/lab_test_report.py +++ b/erpnext/healthcare/report/lab_test_report/lab_test_report.py @@ -8,51 +8,134 @@ from frappe import msgprint, _ def execute(filters=None): if not filters: filters = {} - lab_test_list = get_lab_test(filters) + data, columns = [], [] + columns = get_columns() + lab_test_list = get_lab_tests(filters) if not lab_test_list: - msgprint(_("No record found")) + msgprint(_("No records found")) return columns, lab_test_list data = [] for lab_test in lab_test_list: - row = [ lab_test.lab_test_name, lab_test.patient, lab_test.practitioner, lab_test.invoiced, lab_test.status, lab_test.result_date, lab_test.department] + row = frappe._dict({ + 'test': lab_test.name, + 'template': lab_test.template, + 'company': lab_test.company, + 'patient': lab_test.patient, + 'patient_name': lab_test.patient_name, + 'practitioner': lab_test.practitioner, + 'employee': lab_test.employee, + 'status': lab_test.status, + 'invoiced': lab_test.invoiced, + 'result_date': lab_test.result_date, + 'department': lab_test.department + }) data.append(row) return columns, data def get_columns(): - columns = [ - _("Test") + ":Data:120", - _("Patient") + ":Link/Patient:180", - _("Healthcare Practitioner") + ":Link/Healthcare Practitioner:120", - _("Invoiced") + ":Check:100", - _("Status") + ":Data:120", - _("Result Date") + ":Date:120", - _("Department") + ":Data:120", + return [ + { + "fieldname": "test", + "label": _("Lab Test"), + "fieldtype": "Link", + "options": "Lab Test", + "width": "120" + }, + { + "fieldname": "template", + "label": _("Lab Test Template"), + "fieldtype": "Link", + "options": "Lab Test Template", + "width": "120" + }, + { + "fieldname": "company", + "label": _("Company"), + "fieldtype": "Link", + "options": "Company", + "width": "120" + }, + { + "fieldname": "patient", + "label": _("Patient"), + "fieldtype": "Link", + "options": "Patient", + "width": "120" + }, + { + "fieldname": "patient_name", + "label": _("Patient Name"), + "fieldtype": "Data", + "width": "120" + }, + { + "fieldname": "practitioner", + "label": _("Requesting Practitioner"), + "fieldtype": "Link", + "options": "Healthcare Practitioner", + "width": "120" + }, + { + "fieldname": "employee", + "label": _("Lab Technician"), + "fieldtype": "Link", + "options": "Employee", + "width": "120" + }, + { + "fieldname": "status", + "label": _("Status"), + "fieldtype": "Data", + "width": "100" + }, + { + "fieldname": "invoiced", + "label": _("Invoiced"), + "fieldtype": "Check", + "width": "100" + }, + { + "fieldname": "result_date", + "label": _("Result Date"), + "fieldtype": "Date", + "width": "100" + }, + { + "fieldname": "department", + "label": _("Medical Department"), + "fieldtype": "Link", + "options": "Medical Department", + "width": "100" + } ] - return columns +def get_lab_tests(filters): + conditions = get_conditions(filters) + data = frappe.get_all( + doctype='Lab Test', + fields=['name', 'template', 'company', 'patient', 'patient_name', 'practitioner', 'employee', 'status', 'invoiced', 'result_date', 'department'], + filters=conditions, + order_by='submitted_date desc' + ) + return data def get_conditions(filters): - conditions = "" + conditions = { + 'docstatus': ('=', 1) + } - if filters.get("patient"): - conditions += "and patient = %(patient)s" - if filters.get("from_date"): - conditions += "and result_date >= %(from_date)s" - if filters.get("to_date"): - conditions += " and result_date <= %(to_date)s" - if filters.get("department"): - conditions += " and department = %(department)s" + if filters.get('from_date') and filters.get('to_date'): + conditions['result_date'] = ('between', (filters.get('from_date'), filters.get('to_date'))) + filters.pop('from_date') + filters.pop('to_date') - return conditions + for key, value in filters.items(): + if filters.get(key): + conditions[key] = value -def get_lab_test(filters): - conditions = get_conditions(filters) - return frappe.db.sql("""select name, patient, lab_test_name, patient_name, status, result_date, practitioner, invoiced, department - from `tabLab Test` - where docstatus<2 %s order by submitted_date desc, name desc""" % - conditions, filters, as_dict=1) + return conditions \ No newline at end of file From b6675f8dd4f3c70bfbe8d1cedbc5590809041dc7 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 30 Jul 2020 19:33:08 +0530 Subject: [PATCH 015/192] feat: Lab Test Report Summary and Chart --- .../report/lab_test_report/lab_test_report.py | 178 ++++++++++++------ 1 file changed, 124 insertions(+), 54 deletions(-) diff --git a/erpnext/healthcare/report/lab_test_report/lab_test_report.py b/erpnext/healthcare/report/lab_test_report/lab_test_report.py index be2d06193e..2e59bed037 100644 --- a/erpnext/healthcare/report/lab_test_report/lab_test_report.py +++ b/erpnext/healthcare/report/lab_test_report/lab_test_report.py @@ -14,7 +14,7 @@ def execute(filters=None): lab_test_list = get_lab_tests(filters) if not lab_test_list: - msgprint(_("No records found")) + msgprint(_('No records found')) return columns, lab_test_list data = [] @@ -34,83 +34,85 @@ def execute(filters=None): }) data.append(row) - return columns, data + chart = get_chart_data(data) + report_summary = get_report_summary(data) + return columns, data, None, chart, report_summary def get_columns(): return [ { - "fieldname": "test", - "label": _("Lab Test"), - "fieldtype": "Link", - "options": "Lab Test", - "width": "120" + 'fieldname': 'test', + 'label': _('Lab Test'), + 'fieldtype': 'Link', + 'options': 'Lab Test', + 'width': '120' }, { - "fieldname": "template", - "label": _("Lab Test Template"), - "fieldtype": "Link", - "options": "Lab Test Template", - "width": "120" + 'fieldname': 'template', + 'label': _('Lab Test Template'), + 'fieldtype': 'Link', + 'options': 'Lab Test Template', + 'width': '120' }, { - "fieldname": "company", - "label": _("Company"), - "fieldtype": "Link", - "options": "Company", - "width": "120" + 'fieldname': 'company', + 'label': _('Company'), + 'fieldtype': 'Link', + 'options': 'Company', + 'width': '120' }, { - "fieldname": "patient", - "label": _("Patient"), - "fieldtype": "Link", - "options": "Patient", - "width": "120" + 'fieldname': 'patient', + 'label': _('Patient'), + 'fieldtype': 'Link', + 'options': 'Patient', + 'width': '120' }, { - "fieldname": "patient_name", - "label": _("Patient Name"), - "fieldtype": "Data", - "width": "120" + 'fieldname': 'patient_name', + 'label': _('Patient Name'), + 'fieldtype': 'Data', + 'width': '120' }, { - "fieldname": "practitioner", - "label": _("Requesting Practitioner"), - "fieldtype": "Link", - "options": "Healthcare Practitioner", - "width": "120" + 'fieldname': 'employee', + 'label': _('Lab Technician'), + 'fieldtype': 'Link', + 'options': 'Employee', + 'width': '120' }, { - "fieldname": "employee", - "label": _("Lab Technician"), - "fieldtype": "Link", - "options": "Employee", - "width": "120" + 'fieldname': 'status', + 'label': _('Status'), + 'fieldtype': 'Data', + 'width': '100' }, { - "fieldname": "status", - "label": _("Status"), - "fieldtype": "Data", - "width": "100" + 'fieldname': 'invoiced', + 'label': _('Invoiced'), + 'fieldtype': 'Check', + 'width': '100' }, { - "fieldname": "invoiced", - "label": _("Invoiced"), - "fieldtype": "Check", - "width": "100" + 'fieldname': 'result_date', + 'label': _('Result Date'), + 'fieldtype': 'Date', + 'width': '100' }, { - "fieldname": "result_date", - "label": _("Result Date"), - "fieldtype": "Date", - "width": "100" + 'fieldname': 'practitioner', + 'label': _('Requesting Practitioner'), + 'fieldtype': 'Link', + 'options': 'Healthcare Practitioner', + 'width': '120' }, { - "fieldname": "department", - "label": _("Medical Department"), - "fieldtype": "Link", - "options": "Medical Department", - "width": "100" + 'fieldname': 'department', + 'label': _('Medical Department'), + 'fieldtype': 'Link', + 'options': 'Medical Department', + 'width': '100' } ] @@ -138,4 +140,72 @@ def get_conditions(filters): if filters.get(key): conditions[key] = value - return conditions \ No newline at end of file + return conditions + +def get_chart_data(data): + if not data: + return None + + labels = ['Completed', 'Approved', 'Rejected'] + + status_wise_data = { + 'Completed': 0, + 'Approved': 0, + 'Rejected': 0 + } + + datasets = [] + + for entry in data: + status_wise_data[entry.status] += 1 + + datasets.append({ + 'name': 'Lab Test Status', + 'values': [status_wise_data.get('Completed'), status_wise_data.get('Approved'), status_wise_data.get('Rejected')] + }) + + chart = { + 'data': { + 'labels': labels, + 'datasets': datasets + }, + 'type': 'donut', + 'height': 300, + } + + return chart + + +def get_report_summary(data): + if not data: + return None + + total_lab_tests = len(data) + invoiced_lab_tests, unbilled_lab_tests = 0, 0 + + for entry in data: + if entry.invoiced: + invoiced_lab_tests += 1 + else: + unbilled_lab_tests += 1 + + return [ + { + 'value': total_lab_tests, + 'indicator': 'Blue', + 'label': 'Total Lab Tests', + 'datatype': 'Int', + }, + { + 'value': invoiced_lab_tests, + 'indicator': 'Green', + 'label': 'Invoiced Lab Tests', + 'datatype': 'Int', + }, + { + 'value': unbilled_lab_tests, + 'indicator': 'Red', + 'label': 'Unbilled Lab Tests', + 'datatype': 'Int', + } + ] From d4817c8685f3b31e72c8d5acca297cbbb6d6877b Mon Sep 17 00:00:00 2001 From: marination Date: Sun, 2 Aug 2020 05:35:45 +0530 Subject: [PATCH 016/192] feat: Track Youtube interactions via Video DocType --- erpnext/utilities/doctype/video/video.js | 34 ++++++++- erpnext/utilities/doctype/video/video.json | 64 +++++++++++++++-- erpnext/utilities/doctype/video/video.py | 20 +++++- .../doctype/video_settings/__init__.py | 0 .../video_settings/test_video_settings.py | 10 +++ .../doctype/video_settings/video_settings.js | 8 +++ .../video_settings/video_settings.json | 49 +++++++++++++ .../doctype/video_settings/video_settings.py | 10 +++ erpnext/utilities/report/__init__.py | 0 .../report/youtube_interactions/__init__.py | 0 .../youtube_interactions.js | 9 +++ .../youtube_interactions.json | 27 +++++++ .../youtube_interactions.py | 72 +++++++++++++++++++ requirements.txt | 1 + 14 files changed, 296 insertions(+), 8 deletions(-) create mode 100644 erpnext/utilities/doctype/video_settings/__init__.py create mode 100644 erpnext/utilities/doctype/video_settings/test_video_settings.py create mode 100644 erpnext/utilities/doctype/video_settings/video_settings.js create mode 100644 erpnext/utilities/doctype/video_settings/video_settings.json create mode 100644 erpnext/utilities/doctype/video_settings/video_settings.py create mode 100644 erpnext/utilities/report/__init__.py create mode 100644 erpnext/utilities/report/youtube_interactions/__init__.py create mode 100644 erpnext/utilities/report/youtube_interactions/youtube_interactions.js create mode 100644 erpnext/utilities/report/youtube_interactions/youtube_interactions.json create mode 100644 erpnext/utilities/report/youtube_interactions/youtube_interactions.py diff --git a/erpnext/utilities/doctype/video/video.js b/erpnext/utilities/doctype/video/video.js index 056bd3ccd6..4dd4e67a7f 100644 --- a/erpnext/utilities/doctype/video/video.js +++ b/erpnext/utilities/doctype/video/video.js @@ -2,7 +2,37 @@ // For license information, please see license.txt frappe.ui.form.on('Video', { - // refresh: function(frm) { + refresh: function (frm) { + if (frm.doc.provider === "YouTube") { + frappe.db.get_single_value("Video Settings", "enable_youtube_tracking").then(value => { + if (value) { + frm.events.get_video_stats(frm); + } else { + frm.set_df_property('youtube_tracking_section', 'hidden', true); + } + }); + } - // } + frm.add_custom_button("Watch Video", () => frappe.help.show_video(frm.doc.url, frm.doc.title)); + }, + + get_video_stats: (frm) => { + const expression = '(?:youtube.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu.be/)([^"&?\\s]{11})'; + var youtube_id = frm.doc.url.match(expression)[1]; + + frappe.call({ + method: "erpnext.utilities.doctype.video.video.update_video_stats", + args: { + youtube_id: youtube_id + }, + callback: (r) => { + var result = r.message; + var fields = ['like_count', 'view_count', 'dislike_count', 'comment_count']; + fields.forEach((field) => { + frm.doc[field] = result[field]; + }) + frm.refresh_fields(); + } + }); + } }); diff --git a/erpnext/utilities/doctype/video/video.json b/erpnext/utilities/doctype/video/video.json index 5d2cc13348..a6c0f3f82a 100644 --- a/erpnext/utilities/doctype/video/video.json +++ b/erpnext/utilities/doctype/video/video.json @@ -15,7 +15,14 @@ "publish_date", "duration", "section_break_7", - "description" + "description", + "image", + "youtube_tracking_section", + "like_count", + "view_count", + "col_break", + "dislike_count", + "comment_count" ], "fields": [ { @@ -37,7 +44,6 @@ { "fieldname": "url", "fieldtype": "Data", - "in_list_view": 1, "label": "URL", "reqd": 1 }, @@ -48,11 +54,12 @@ { "fieldname": "publish_date", "fieldtype": "Date", + "in_list_view": 1, "label": "Publish Date" }, { "fieldname": "duration", - "fieldtype": "Data", + "fieldtype": "Duration", "label": "Duration" }, { @@ -62,13 +69,60 @@ { "fieldname": "description", "fieldtype": "Text Editor", - "in_list_view": 1, "label": "Description", "reqd": 1 + }, + { + "fieldname": "like_count", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Likes", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "view_count", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Views", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "col_break", + "fieldtype": "Column Break" + }, + { + "fieldname": "dislike_count", + "fieldtype": "Float", + "label": "Dislikes", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "comment_count", + "fieldtype": "Float", + "label": "Comments", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "image", + "fieldtype": "Attach Image", + "hidden": 1, + "label": "Image", + "no_copy": 1 + }, + { + "depends_on": "eval:doc.provider==\"YouTube\"", + "fieldname": "youtube_tracking_section", + "fieldtype": "Section Break", + "label": "Youtube Statistics" } ], + "image_field": "image", "links": [], - "modified": "2020-07-21 19:29:46.603734", + "modified": "2020-08-02 04:26:16.345569", "modified_by": "Administrator", "module": "Utilities", "name": "Video", diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py index 3c17b560f3..263884a93d 100644 --- a/erpnext/utilities/doctype/video/video.py +++ b/erpnext/utilities/doctype/video/video.py @@ -3,8 +3,26 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe from frappe.model.document import Document +from pyyoutube import Api class Video(Document): pass + +@frappe.whitelist() +def update_video_stats(youtube_id): + ''' + :param youtube_id: Unique ID from URL + ''' + api_key = frappe.db.get_single_value("Video Settings", "api_key") + api = Api(api_key=api_key) + + video = api.get_video_by_id(video_id=youtube_id) + video_stats = video.items[0].to_dict().get('statistics') + return { + 'like_count' : video_stats.get('likeCount'), + 'view_count' : video_stats.get('viewCount'), + 'dislike_count' : video_stats.get('dislikeCount'), + 'comment_count' : video_stats.get('commentCount') + } \ No newline at end of file diff --git a/erpnext/utilities/doctype/video_settings/__init__.py b/erpnext/utilities/doctype/video_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/utilities/doctype/video_settings/test_video_settings.py b/erpnext/utilities/doctype/video_settings/test_video_settings.py new file mode 100644 index 0000000000..b217afe3d8 --- /dev/null +++ b/erpnext/utilities/doctype/video_settings/test_video_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestVideoSettings(unittest.TestCase): + pass diff --git a/erpnext/utilities/doctype/video_settings/video_settings.js b/erpnext/utilities/doctype/video_settings/video_settings.js new file mode 100644 index 0000000000..9ac8b9ec16 --- /dev/null +++ b/erpnext/utilities/doctype/video_settings/video_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Video Settings', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/utilities/doctype/video_settings/video_settings.json b/erpnext/utilities/doctype/video_settings/video_settings.json new file mode 100644 index 0000000000..0a0efd9a53 --- /dev/null +++ b/erpnext/utilities/doctype/video_settings/video_settings.json @@ -0,0 +1,49 @@ +{ + "actions": [], + "creation": "2020-08-02 03:50:21.339609", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enable_youtube_tracking", + "api_key" + ], + "fields": [ + { + "default": "0", + "fieldname": "enable_youtube_tracking", + "fieldtype": "Check", + "label": "Enable YouTube Tracking" + }, + { + "depends_on": "eval:doc.enable_youtube_tracking", + "fieldname": "api_key", + "fieldtype": "Data", + "label": "API Key", + "mandatory_depends_on": "eval:doc.enable_youtube_tracking" + } + ], + "issingle": 1, + "links": [], + "modified": "2020-08-02 03:56:49.673870", + "modified_by": "Administrator", + "module": "Utilities", + "name": "Video Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/utilities/doctype/video_settings/video_settings.py b/erpnext/utilities/doctype/video_settings/video_settings.py new file mode 100644 index 0000000000..7008066909 --- /dev/null +++ b/erpnext/utilities/doctype/video_settings/video_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class VideoSettings(Document): + pass diff --git a/erpnext/utilities/report/__init__.py b/erpnext/utilities/report/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/utilities/report/youtube_interactions/__init__.py b/erpnext/utilities/report/youtube_interactions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/utilities/report/youtube_interactions/youtube_interactions.js b/erpnext/utilities/report/youtube_interactions/youtube_interactions.js new file mode 100644 index 0000000000..f194cca834 --- /dev/null +++ b/erpnext/utilities/report/youtube_interactions/youtube_interactions.js @@ -0,0 +1,9 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["YouTube Interactions"] = { + "filters": [ + + ] +}; diff --git a/erpnext/utilities/report/youtube_interactions/youtube_interactions.json b/erpnext/utilities/report/youtube_interactions/youtube_interactions.json new file mode 100644 index 0000000000..a40247b6df --- /dev/null +++ b/erpnext/utilities/report/youtube_interactions/youtube_interactions.json @@ -0,0 +1,27 @@ +{ + "add_total_row": 0, + "creation": "2020-08-02 05:05:00.457093", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-08-02 05:05:00.457093", + "modified_by": "Administrator", + "module": "Utilities", + "name": "YouTube Interactions", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Video", + "report_name": "YouTube Interactions", + "report_type": "Script Report", + "roles": [ + { + "role": "All" + }, + { + "role": "System Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/utilities/report/youtube_interactions/youtube_interactions.py b/erpnext/utilities/report/youtube_interactions/youtube_interactions.py new file mode 100644 index 0000000000..169d0716b0 --- /dev/null +++ b/erpnext/utilities/report/youtube_interactions/youtube_interactions.py @@ -0,0 +1,72 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +def execute(filters=None): + columns = get_columns() + data = get_data() + return columns, data + +def get_columns(): + return [ + { + "label": _("Published Date"), + "fieldname": "publish_date", + "fieldtype": "Date", + "width": 100 + }, + { + "label": _("Title"), + "fieldname": "title", + "fieldtype": "Data", + "width": 100 + }, + { + "label": _("Provider"), + "fieldname": "provider", + "fieldtype": "Data", + "width": 100 + }, + { + "label": _("Views"), + "fieldname": "view_count", + "fieldtype": "Float", + "width": 100 + }, + { + "label": _("Likes"), + "fieldname": "like_count", + "fieldtype": "Float", + "width": 100 + }, + { + "label": _("Dislikes"), + "fieldname": "dislike_count", + "fieldtype": "Float", + "width": 100 + }, + { + "label": _("Views"), + "fieldname": "view_count", + "fieldtype": "Float", + "width": 100 + }, + { + "label": _("Like:Dislike Ratio"), + "fieldname": "ratio", + "fieldtype": "Data", + "width": 100 + } + ] + +def get_data(): + return frappe.db.sql(""" + SELECT + publish_date, title, provider, + view_count, like_count, dislike_count, comment_count + FROM `tabVideo` + WHERE view_count is not null + ORDER BY view_count desc""") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 912d61f7a6..872d78caa3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ plaid-python==3.4.0 pycountry==19.8.18 PyGithub==1.44.1 python-stdnum==1.12 +python-youtube==0.6.0 taxjar==1.9.0 tweepy==3.8.0 Unidecode==1.1.1 From ccf4ab9f852f7af8cf669abd1bd664d9e15dfc58 Mon Sep 17 00:00:00 2001 From: marination Date: Sun, 2 Aug 2020 16:26:36 +0530 Subject: [PATCH 017/192] chore: Added Interactions Report and behaviour fixes - Youtube Interactions Report with Chart and Summary - Statistics change in doc on refresh and get updated in db as well --- erpnext/utilities/doctype/video/video.js | 11 +-- erpnext/utilities/doctype/video/video.py | 27 ++++++- .../youtube_interactions.js | 13 ++- .../youtube_interactions.py | 79 ++++++++++++++----- 4 files changed, 97 insertions(+), 33 deletions(-) diff --git a/erpnext/utilities/doctype/video/video.js b/erpnext/utilities/doctype/video/video.js index 4dd4e67a7f..c2994ecc96 100644 --- a/erpnext/utilities/doctype/video/video.js +++ b/erpnext/utilities/doctype/video/video.js @@ -21,17 +21,10 @@ frappe.ui.form.on('Video', { var youtube_id = frm.doc.url.match(expression)[1]; frappe.call({ - method: "erpnext.utilities.doctype.video.video.update_video_stats", + method: "erpnext.utilities.doctype.video.video.get_video_stats", args: { + docname: frm.doc.name, youtube_id: youtube_id - }, - callback: (r) => { - var result = r.message; - var fields = ['like_count', 'view_count', 'dislike_count', 'comment_count']; - fields.forEach((field) => { - frm.doc[field] = result[field]; - }) - frm.refresh_fields(); } }); } diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py index 263884a93d..bea7904609 100644 --- a/erpnext/utilities/doctype/video/video.py +++ b/erpnext/utilities/doctype/video/video.py @@ -5,24 +5,43 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document +from six import string_types from pyyoutube import Api class Video(Document): pass @frappe.whitelist() -def update_video_stats(youtube_id): - ''' +def get_video_stats(docname, youtube_id, update=True): + '''Returns/Sets video statistics + :param docname: Name of Video :param youtube_id: Unique ID from URL + :param update: Updates db stats value if True, else returns statistics ''' + if isinstance(update, string_types): + update = json.loads(update) + api_key = frappe.db.get_single_value("Video Settings", "api_key") api = Api(api_key=api_key) video = api.get_video_by_id(video_id=youtube_id) video_stats = video.items[0].to_dict().get('statistics') - return { + stats = { 'like_count' : video_stats.get('likeCount'), 'view_count' : video_stats.get('viewCount'), 'dislike_count' : video_stats.get('dislikeCount'), 'comment_count' : video_stats.get('commentCount') - } \ No newline at end of file + } + + if not update: + return stats + + frappe.db.sql(""" + UPDATE `tabVideo` + SET + like_count = %(like_count)s, + view_count = %(view_count)s, + dislike_count = %(dislike_count)s, + comment_count = %(comment_count)s + WHERE name = {0}""".format(frappe.db.escape(docname)), stats) + frappe.db.commit() \ No newline at end of file diff --git a/erpnext/utilities/report/youtube_interactions/youtube_interactions.js b/erpnext/utilities/report/youtube_interactions/youtube_interactions.js index f194cca834..6e3e4e6980 100644 --- a/erpnext/utilities/report/youtube_interactions/youtube_interactions.js +++ b/erpnext/utilities/report/youtube_interactions/youtube_interactions.js @@ -4,6 +4,17 @@ frappe.query_reports["YouTube Interactions"] = { "filters": [ - + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.now_date(), -12), + }, + { + fieldname:"to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.now_date(), + } ] }; diff --git a/erpnext/utilities/report/youtube_interactions/youtube_interactions.py b/erpnext/utilities/report/youtube_interactions/youtube_interactions.py index 169d0716b0..3516a35097 100644 --- a/erpnext/utilities/report/youtube_interactions/youtube_interactions.py +++ b/erpnext/utilities/report/youtube_interactions/youtube_interactions.py @@ -4,11 +4,16 @@ from __future__ import unicode_literals import frappe from frappe import _ +from frappe.utils import flt def execute(filters=None): + if not frappe.db.get_single_value("Video Settings", "enable_youtube_tracking") or not filters: + return [], [] + columns = get_columns() - data = get_data() - return columns, data + data = get_data(filters) + chart_data, summary = get_chart_summary_data(data) + return columns, data, None, chart_data, summary def get_columns(): return [ @@ -22,25 +27,25 @@ def get_columns(): "label": _("Title"), "fieldname": "title", "fieldtype": "Data", - "width": 100 + "width": 200 }, { - "label": _("Provider"), - "fieldname": "provider", - "fieldtype": "Data", + "label": _("Duration"), + "fieldname": "duration", + "fieldtype": "Duration", "width": 100 }, { "label": _("Views"), "fieldname": "view_count", "fieldtype": "Float", - "width": 100 + "width": 200 }, { "label": _("Likes"), "fieldname": "like_count", "fieldtype": "Float", - "width": 100 + "width": 200 }, { "label": _("Dislikes"), @@ -49,24 +54,60 @@ def get_columns(): "width": 100 }, { - "label": _("Views"), - "fieldname": "view_count", + "label": _("Comments"), + "fieldname": "comment_count", "fieldtype": "Float", "width": 100 - }, - { - "label": _("Like:Dislike Ratio"), - "fieldname": "ratio", - "fieldtype": "Data", - "width": 100 } ] -def get_data(): +def get_data(filters): return frappe.db.sql(""" SELECT - publish_date, title, provider, + publish_date, title, provider, duration, view_count, like_count, dislike_count, comment_count FROM `tabVideo` WHERE view_count is not null - ORDER BY view_count desc""") \ No newline at end of file + and publish_date between %(from_date)s and %(to_date)s + ORDER BY view_count desc""", filters, as_dict=1) + +def get_chart_summary_data(data): + labels, likes, views = [], [], [] + total_views = 0 + + for row in data: + labels.append(row.get('title')) + likes.append(row.get('like_count')) + views.append(row.get('view_count')) + total_views += flt(row.get('view_count')) + + + chart_data = { + "data" : { + "labels" : labels, + "datasets" : [ + { + "name" : "Likes", + "values" : likes + }, + { + "name" : "Views", + "values" : views + } + ] + }, + "type": "bar", + "barOptions": { + "stacked": 1 + }, + } + + summary = [ + { + "value": total_views, + "indicator": "Blue", + "label": "Total Views", + "datatype": "Float", + } + ] + return chart_data, summary \ No newline at end of file From c16ace6732ba27061da55d9f20171fe4693972d7 Mon Sep 17 00:00:00 2001 From: marination Date: Sun, 2 Aug 2020 16:43:15 +0530 Subject: [PATCH 018/192] fix: Codacy --- erpnext/utilities/doctype/video/video.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py index bea7904609..002ee681ed 100644 --- a/erpnext/utilities/doctype/video/video.py +++ b/erpnext/utilities/doctype/video/video.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe +import json from frappe.model.document import Document from six import string_types from pyyoutube import Api @@ -14,6 +15,7 @@ class Video(Document): @frappe.whitelist() def get_video_stats(docname, youtube_id, update=True): '''Returns/Sets video statistics + :param docname: Name of Video :param youtube_id: Unique ID from URL :param update: Updates db stats value if True, else returns statistics @@ -43,5 +45,5 @@ def get_video_stats(docname, youtube_id, update=True): view_count = %(view_count)s, dislike_count = %(dislike_count)s, comment_count = %(comment_count)s - WHERE name = {0}""".format(frappe.db.escape(docname)), stats) + WHERE name = {0}""".format(frappe.db.escape(docname)), stats) #nosec frappe.db.commit() \ No newline at end of file From e3495116dd959bccbfaa39157e62b151c688c47e Mon Sep 17 00:00:00 2001 From: marination Date: Sun, 2 Aug 2020 17:13:34 +0530 Subject: [PATCH 019/192] chore: Error Logging and exception hnadling on connection failure --- erpnext/utilities/doctype/video/video.py | 42 +++++++++++++----------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py index 002ee681ed..a2a4a7b745 100644 --- a/erpnext/utilities/doctype/video/video.py +++ b/erpnext/utilities/doctype/video/video.py @@ -26,24 +26,28 @@ def get_video_stats(docname, youtube_id, update=True): api_key = frappe.db.get_single_value("Video Settings", "api_key") api = Api(api_key=api_key) - video = api.get_video_by_id(video_id=youtube_id) - video_stats = video.items[0].to_dict().get('statistics') - stats = { - 'like_count' : video_stats.get('likeCount'), - 'view_count' : video_stats.get('viewCount'), - 'dislike_count' : video_stats.get('dislikeCount'), - 'comment_count' : video_stats.get('commentCount') - } + try: + video = api.get_video_by_id(video_id=youtube_id) + video_stats = video.items[0].to_dict().get('statistics') + stats = { + 'like_count' : video_stats.get('likeCount'), + 'view_count' : video_stats.get('viewCount'), + 'dislike_count' : video_stats.get('dislikeCount'), + 'comment_count' : video_stats.get('commentCount') + } - if not update: - return stats + if not update: + return stats - frappe.db.sql(""" - UPDATE `tabVideo` - SET - like_count = %(like_count)s, - view_count = %(view_count)s, - dislike_count = %(dislike_count)s, - comment_count = %(comment_count)s - WHERE name = {0}""".format(frappe.db.escape(docname)), stats) #nosec - frappe.db.commit() \ No newline at end of file + frappe.db.sql(""" + UPDATE `tabVideo` + SET + like_count = %(like_count)s, + view_count = %(view_count)s, + dislike_count = %(dislike_count)s, + comment_count = %(comment_count)s + WHERE name = {0}""".format(frappe.db.escape(docname)), stats) #nosec + frappe.db.commit() + except: + message = "Please make sure you are connected to the Internet" + frappe.log_error(message + "\n\n" + frappe.get_traceback(), "Failed to Update YouTube Statistics for Video: {0}".format(docname)) \ No newline at end of file From af1f46f2d97dbdcfbd1ee020ac79072edcc375fa Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 7 Aug 2020 19:44:20 +0530 Subject: [PATCH 020/192] fix: Add default billing address for purchase documents --- erpnext/public/js/controllers/transaction.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 4e50f3d7f6..862b6fbf9b 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -781,10 +781,23 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ else var date = this.frm.doc.transaction_date; if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && - in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)){ + in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) { erpnext.utils.get_shipping_address(this.frm, function(){ set_party_account(set_pricing); }) + + // Get default company billing address in Purchase Invoice, Order and Receipt + frappe.call({ + 'method': 'frappe.contacts.doctype.address.address.get_default_address', + 'args': { + 'doctype': 'Company', + 'name': this.frm.doc.company + }, + 'callback': function(r) { + me.frm.set_value('billing_address', r.message); + } + }); + } else { set_party_account(set_pricing); } From 821eeb9852bd586fc4b98fa9ab37c4075c787e73 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Sun, 9 Aug 2020 14:07:32 +0200 Subject: [PATCH 021/192] fix(membership): currency should be a link --- erpnext/non_profit/doctype/membership/membership.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json index 238f4c31fd..b95ae9738c 100644 --- a/erpnext/non_profit/doctype/membership/membership.json +++ b/erpnext/non_profit/doctype/membership/membership.json @@ -90,9 +90,9 @@ }, { "fieldname": "currency", - "fieldtype": "Select", + "fieldtype": "Link", "label": "Currency", - "options": "USD\nINR" + "options": "Currency" }, { "fieldname": "amount", @@ -163,4 +163,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} From 069a54e5c38c10f8e97d16e4e279119adea69423 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 10 Aug 2020 16:01:01 +0530 Subject: [PATCH 022/192] fix: Cancellation of accounting transactions within closed accounting period --- erpnext/accounts/general_ledger.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index cf3deb828f..01d3903d28 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -45,8 +45,8 @@ def validate_accounting_period(gl_map): }, as_dict=1) if accounting_periods: - frappe.throw(_("You can't create accounting entries in the closed accounting period {0}") - .format(accounting_periods[0].name), ClosedAccountingPeriod) + frappe.throw(_("You cannot create or cancel any accounting entries with in the closed Accounting Period {0}") + .format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod) def process_gl_map(gl_map, merge_entries=True): if merge_entries: @@ -301,8 +301,9 @@ def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None, }) if gl_entries: - set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no']) + validate_accounting_period(gl_entries) check_freezing_date(gl_entries[0]["posting_date"], adv_adj) + set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no']) for entry in gl_entries: entry['name'] = None @@ -342,7 +343,7 @@ def set_as_cancel(voucher_type, voucher_no): """ Set is_cancelled=1 in all original gl entries for the voucher """ - frappe.db.sql("""update `tabGL Entry` set is_cancelled = 1, + frappe.db.sql("""UPDATE `tabGL Entry` SET is_cancelled = 1, modified=%s, modified_by=%s where voucher_type=%s and voucher_no=%s and is_cancelled = 0""", (now(), frappe.session.user, voucher_type, voucher_no)) From bfb219612e143139b535d273556898b7d0ecaeaf Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 13 Aug 2020 11:34:05 +0530 Subject: [PATCH 023/192] 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 9cfb0627d3..cd6bac2d77 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 b3bd780d46cd9d9d48033d9bb8b074a25942e598 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Thu, 13 Aug 2020 17:07:15 +0530 Subject: [PATCH 024/192] fix: codasy dix --- .../report/budget_variance_report/budget_variance_report.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js index 30415d1e65..f547ca619b 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js @@ -74,9 +74,9 @@ frappe.query_reports["Budget Variance Report"] = { ], "formatter": function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); - + if (column.fieldname.includes('variance')) { - + if (data[column.fieldname] < 0) { value = "" + value + ""; } From a16b24d50a7580862e1610a95838de2545bd1a58 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 28 Jul 2020 11:06:42 +0530 Subject: [PATCH 025/192] feat: add enable or disable invoicing and print format field --- .../membership_settings.js | 9 +++++++ .../membership_settings.json | 26 +++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index 8c0e3a4fa7..bbfece31d4 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js @@ -10,6 +10,15 @@ frappe.ui.form.on("Membership Settings", { }) }); } + + frm.set_query('print_format', function(doc) { + return { + filters: { + "doc_type": "Sales Invoice" + } + }; + }); + frm.trigger("add_generate_button"); }, diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 52b9d01088..29013fafdc 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -9,7 +9,10 @@ "razorpay_settings_section", "billing_cycle", "billing_frequency", - "webhook_secret" + "webhook_secret", + "column_break_6", + "enable_auto_invoicing", + "print_format" ], "fields": [ { @@ -41,11 +44,30 @@ "fieldtype": "Password", "label": "Webhook Secret", "read_only": 1 + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "enable_auto_invoicing", + "fieldtype": "Check", + "label": "Enable Auto Invoicing" + }, + { + "depends_on": "eval:doc.enable_auto_invoicing", + "fieldname": "print_format", + "fieldtype": "Link", + "label": "Print Format", + "mandatory_depends_on": "eval:doc.enable_auto_invoicing", + "options": "Print Format" } ], + "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-05-22 12:38:27.103759", + "modified": "2020-07-28 11:01:40.125896", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", From 3a1868dae813a41ee9e124ae05f20374214c35db Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 28 Jul 2020 11:07:35 +0530 Subject: [PATCH 026/192] feat: add item field --- .../doctype/membership_type/membership_type.js | 6 +++++- .../doctype/membership_type/membership_type.json | 12 ++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.js b/erpnext/non_profit/doctype/membership_type/membership_type.js index 226981dc78..43311a2c96 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.js +++ b/erpnext/non_profit/doctype/membership_type/membership_type.js @@ -5,6 +5,10 @@ frappe.ui.form.on('Membership Type', { refresh: function(frm) { frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false); - }) + }); + + frappe.db.get_single_value("Membership Settings", "enable_auto_invoicing").then(val => { + if (val) frm.set_df_property('linked_item', 'hidden', false); + }); } }); diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.json b/erpnext/non_profit/doctype/membership_type/membership_type.json index 319078fd6c..a163568bb9 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.json +++ b/erpnext/non_profit/doctype/membership_type/membership_type.json @@ -8,7 +8,8 @@ "field_order": [ "membership_type", "amount", - "razorpay_plan_id" + "razorpay_plan_id", + "linked_item" ], "fields": [ { @@ -33,10 +34,17 @@ "hidden": 1, "label": "Razorpay Plan ID", "unique": 1 + }, + { + "fieldname": "linked_item", + "fieldtype": "Link", + "label": "Linked Item", + "options": "Item" } ], + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-03-30 12:54:07.850857", + "modified": "2020-07-28 10:57:50.821375", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Type", From 96dc67c35edf360721c83ac4ae87e8d76d3851f0 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 28 Jul 2020 11:43:54 +0530 Subject: [PATCH 027/192] feat: add copy button and docs link --- .../membership_settings/membership_settings.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index bbfece31d4..f5e0274c3a 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js @@ -19,7 +19,12 @@ frappe.ui.form.on("Membership Settings", { }; }); + let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership"; + + frm.set_intro(__("You can learn more about memberships in the manual. ") + `${__('ERPNext Docs')}`, true); + frm.trigger("add_generate_button"); + frm.trigger("add_copy_buttonn"); }, add_generate_button: function(frm) { @@ -36,4 +41,12 @@ frappe.ui.form.on("Membership Settings", { }); }); }, + + add_copy_buttonn: function(frm) { + if (frm.doc.webhook_secret) { + frm.add_custom_button(__("Copy Webhook URL"), () => { + frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.membership.membership.trigger_razorpay_subscription`); + }); + } + } }); From 811ac909f93d543cd537241fa8522393281c9bcf Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 28 Jul 2020 12:27:37 +0530 Subject: [PATCH 028/192] feat: add auto invoice creation fields and controllers --- .../doctype/membership/membership.py | 29 ++++++++++ .../membership_settings.js | 10 ++++ .../membership_settings.json | 57 +++++++++++++++++-- 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 729e111e57..eea18393f1 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -57,6 +57,35 @@ class Membership(Document): self.load_from_db() self.db_set('paid', 1) + def generate_and_send_invoice(self): + if not self.paid: + frappe.throw(_("The payment for this membership is not paid. To generate invoice mark the paid check")) + + member = frappe.get_doc("Member", self.member) + plan = frappe.get_doc("Membership Type", self.membership_type) + settings = frappe.get_doc("Membership Settings") + + invoice = make_invoice(self, member, plan, settings) + +def make_invoice(membership, member, plan, settings): + invoice = frappe.get_doc({ + 'doctype': 'Sales Invoice', + 'customer': member.customer, + 'debit_to': settings.debit_account, + 'currency': membership.currency, + 'is_pos': 0, + 'items': [ + { + 'item_code': plan.linked_item, + 'rate': membership.amount, + 'qty': 1 + } + ] + }) + + invoice.insert(ignore_permissions=True) + invoice.submit() + def get_member_based_on_subscription(subscription_id, email): members = frappe.get_all("Member", filters={ 'subscription_id': subscription_id, diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index f5e0274c3a..02ef0292ea 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js @@ -19,6 +19,16 @@ frappe.ui.form.on("Membership Settings", { }; }); + frm.set_query('debit_account', function(doc) { + return { + filters: { + 'account_type': 'Receivable', + 'is_group': 0, + 'company': frm.doc.company + } + }; + }); + let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership"; frm.set_intro(__("You can learn more about memberships in the manual. ") + `${__('ERPNext Docs')}`, true); diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 29013fafdc..37bea49826 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -11,8 +11,15 @@ "billing_frequency", "webhook_secret", "column_break_6", + "print_format", + "company", + "debit_account", + "column_break_9", "enable_auto_invoicing", - "print_format" + "send_invoice", + "section_break_10", + "new_member", + "renewal" ], "fields": [ { @@ -47,7 +54,8 @@ }, { "fieldname": "column_break_6", - "fieldtype": "Column Break" + "fieldtype": "Section Break", + "label": "Invoicing" }, { "default": "0", @@ -56,18 +64,57 @@ "label": "Enable Auto Invoicing" }, { - "depends_on": "eval:doc.enable_auto_invoicing", "fieldname": "print_format", "fieldtype": "Link", "label": "Print Format", - "mandatory_depends_on": "eval:doc.enable_auto_invoicing", "options": "Print Format" + }, + { + "fieldname": "new_member", + "fieldtype": "Text Editor", + "label": "Message for New Member", + "mandatory_depends_on": "eval:doc.send_invoice" + }, + { + "fieldname": "renewal", + "fieldtype": "Text Editor", + "label": "Message for Renewal", + "mandatory_depends_on": "eval:doc.send_invoice" + }, + { + "depends_on": "eval:doc.company", + "fieldname": "debit_account", + "fieldtype": "Link", + "label": "Debit Account", + "options": "Account" + }, + { + "depends_on": "eval:doc.send_invoice", + "fieldname": "section_break_10", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "default": "0", + "fieldname": "send_invoice", + "fieldtype": "Check", + "label": "Send Email with Invoice" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-07-28 11:01:40.125896", + "modified": "2020-07-28 12:18:35.289893", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", From 15340e0c7f7c21da75403a54b66b502650f04a07 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 28 Jul 2020 12:56:14 +0530 Subject: [PATCH 029/192] feat: send invoice via email --- .../doctype/membership/membership.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index eea18393f1..82b3145a07 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -67,6 +67,25 @@ class Membership(Document): invoice = make_invoice(self, member, plan, settings) + if invoice and settings.send_invoice and self.membership_status in ["New", "Current"]: + print("Sending") + message = settings.new_message if self.membership_status == "New" else settings.renewal + email = member.email_id if member.email_id else member.email + + email_args = { + "recipients": [email], + "message": message, + "subject": _('Here is your invoice'), + "attachments": [frappe.attach_print("Sales Invoice", invoice.name, print_format=settings.print_format)], + "reference_doctype": self.doctype, + "reference_name": self.name + } + if not frappe.flags.in_test: + frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args) + else: + frappe.sendmail(**email_args) + + def make_invoice(membership, member, plan, settings): invoice = frappe.get_doc({ 'doctype': 'Sales Invoice', @@ -86,6 +105,8 @@ def make_invoice(membership, member, plan, settings): invoice.insert(ignore_permissions=True) invoice.submit() + return invoice + def get_member_based_on_subscription(subscription_id, email): members = frappe.get_all("Member", filters={ 'subscription_id': subscription_id, From 62e344188a2f82f70eca1910081dc6218b1655c0 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 13:46:50 +0530 Subject: [PATCH 030/192] feat: update settings and link filters --- .../membership_settings.js | 10 ++- .../membership_settings.json | 83 ++++++++++++------- 2 files changed, 60 insertions(+), 33 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index 02ef0292ea..1d894027b0 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js @@ -11,7 +11,7 @@ frappe.ui.form.on("Membership Settings", { }); } - frm.set_query('print_format', function(doc) { + frm.set_query('inv_print_format', function(doc) { return { filters: { "doc_type": "Sales Invoice" @@ -19,6 +19,14 @@ frappe.ui.form.on("Membership Settings", { }; }); + frm.set_query('membership_print_format', function(doc) { + return { + filters: { + "doc_type": "Membership" + } + }; + }); + frm.set_query('debit_account', function(doc) { return { filters: { diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 37bea49826..7eeb7fbde2 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -11,15 +11,14 @@ "billing_frequency", "webhook_secret", "column_break_6", - "print_format", + "enable_auto_invoicing", "company", "debit_account", "column_break_9", - "enable_auto_invoicing", + "send_email", "send_invoice", - "section_break_10", - "new_member", - "renewal" + "membership_print_format", + "inv_print_format" ], "fields": [ { @@ -61,60 +60,63 @@ "default": "0", "fieldname": "enable_auto_invoicing", "fieldtype": "Check", - "label": "Enable Auto Invoicing" - }, - { - "fieldname": "print_format", - "fieldtype": "Link", - "label": "Print Format", - "options": "Print Format" - }, - { - "fieldname": "new_member", - "fieldtype": "Text Editor", - "label": "Message for New Member", + "label": "Enable Auto Invoicing", "mandatory_depends_on": "eval:doc.send_invoice" }, { - "fieldname": "renewal", - "fieldtype": "Text Editor", - "label": "Message for Renewal", - "mandatory_depends_on": "eval:doc.send_invoice" - }, - { - "depends_on": "eval:doc.company", + "depends_on": "eval:doc.enable_auto_invoicing", "fieldname": "debit_account", "fieldtype": "Link", "label": "Debit Account", + "mandatory_depends_on": "eval:doc.enable_auto_invoicing", "options": "Account" }, - { - "depends_on": "eval:doc.send_invoice", - "fieldname": "section_break_10", - "fieldtype": "Section Break", - "hide_border": 1 - }, { "fieldname": "column_break_9", "fieldtype": "Column Break" }, { + "depends_on": "eval:doc.enable_auto_invoicing", "fieldname": "company", "fieldtype": "Link", "label": "Company", + "mandatory_depends_on": "eval:doc.enable_auto_invoicing", "options": "Company" }, { "default": "0", + "depends_on": "eval:doc.enable_auto_invoicing && doc.send_email", "fieldname": "send_invoice", "fieldtype": "Check", - "label": "Send Email with Invoice" + "label": "Send Invoice with Email" + }, + { + "default": "0", + "fieldname": "send_email", + "fieldtype": "Check", + "label": "Send Acknowledge Email" + }, + { + "depends_on": "eval: doc.send_invoice", + "fieldname": "inv_print_format", + "fieldtype": "Link", + "label": "Invoice Print Format", + "mandatory_depends_on": "eval: doc.send_invoice", + "options": "Print Format" + }, + { + "depends_on": "eval:doc.send_email", + "fieldname": "membership_print_format", + "fieldtype": "Link", + "label": "Membership Print Format", + "mandatory_depends_on": "eval:doc.send_email", + "options": "Print Format" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-07-28 12:18:35.289893", + "modified": "2020-07-31 13:45:28.868235", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", @@ -129,6 +131,23 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "Non Profit Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "print": 1, + "read": 1, + "role": "Non Profit Member", + "share": 1 } ], "quick_entry": 1, From 1678eb9b506af6a3f4a1e20c1e91136c17493a13 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 14:00:22 +0530 Subject: [PATCH 031/192] feat: drop payload column --- .../doctype/membership/membership.json | 16 +++++++--------- erpnext/patches.txt | 1 + .../v13_0/drop_razorpay_payload_column.py | 7 +++++++ 3 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 erpnext/patches/v13_0/drop_razorpay_payload_column.py diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json index 238f4c31fd..95bb3a5d84 100644 --- a/erpnext/non_profit/doctype/membership/membership.json +++ b/erpnext/non_profit/doctype/membership/membership.json @@ -19,10 +19,10 @@ "paid", "currency", "amount", + "invoice", "razorpay_details_section", "subscription_id", - "payment_id", - "webhook_payload" + "payment_id" ], "fields": [ { @@ -118,17 +118,15 @@ "read_only": 1 }, { - "fieldname": "webhook_payload", - "fieldtype": "Code", - "hidden": 1, - "label": "Webhook Payload", - "options": "JSON", - "read_only": 1 + "fieldname": "invoice", + "fieldtype": "Link", + "label": "Invoice", + "options": "Sales Invoice" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-07-27 14:28:11.532696", + "modified": "2020-07-31 13:57:02.328995", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 361fe8352a..17b46ab649 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -721,3 +721,4 @@ erpnext.patches.v13_0.healthcare_lab_module_rename_doctypes erpnext.patches.v13_0.stock_entry_enhancements erpnext.patches.v12_0.update_state_code_for_daman_and_diu erpnext.patches.v12_0.rename_lost_reason_detail +erpnext.patches.v13_0.drop_razorpay_payload_column diff --git a/erpnext/patches/v13_0/drop_razorpay_payload_column.py b/erpnext/patches/v13_0/drop_razorpay_payload_column.py new file mode 100644 index 0000000000..62f0373008 --- /dev/null +++ b/erpnext/patches/v13_0/drop_razorpay_payload_column.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + if frappe.db.exists("DocType", "Membership Settings"): + if 'webhook_payload' in frappe.db.get_table_columns("Membership Settings"): + frappe.db.sql("alter table `tabMembership Settings` drop column webhook_payload") \ No newline at end of file From c8d9e7f77bb82284eeb322290f28273a6c2d46e6 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 14:31:24 +0530 Subject: [PATCH 032/192] feat: add message field --- .../membership_settings.json | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 7eeb7fbde2..2452a763ce 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -18,7 +18,9 @@ "send_email", "send_invoice", "membership_print_format", - "inv_print_format" + "inv_print_format", + "section_break_15", + "message" ], "fields": [ { @@ -111,12 +113,24 @@ "label": "Membership Print Format", "mandatory_depends_on": "eval:doc.send_email", "options": "Print Format" + }, + { + "depends_on": "eval:doc.send_email", + "fieldname": "section_break_15", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "depends_on": "eval:doc.send_email", + "fieldname": "message", + "fieldtype": "Text Editor", + "label": "Message" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-07-31 13:45:28.868235", + "modified": "2020-07-31 14:30:15.701767", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", From 8e0314e7f684f0ae52963d4daf5a692ccd0308fa Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 14:31:39 +0530 Subject: [PATCH 033/192] feat: separate invoice generation and email --- .../doctype/membership/membership.py | 69 ++++++++++++++----- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 82b3145a07..c960daa1d3 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -57,34 +57,65 @@ class Membership(Document): self.load_from_db() self.db_set('paid', 1) - def generate_and_send_invoice(self): - if not self.paid: - frappe.throw(_("The payment for this membership is not paid. To generate invoice mark the paid check")) + def generate_invoice(self, save=True): + if not (self.paid or self.currency or self.amount): + frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details")) + + if self.invoice: + frappe.throw(_("An invoice is already linked to this document")) member = frappe.get_doc("Member", self.member) plan = frappe.get_doc("Membership Type", self.membership_type) settings = frappe.get_doc("Membership Settings") + attachments = [] + + if not member.customer: + frappe.throw(_("No customer linked to member {}", [member.name])) + + if not settings.debit_account: + frappe.throw(_("You need to set Debit Account in Membership Settings")) + + if not settings.company: + frappe.throw(_("You need to set Default Company for invoicing in Membership Settings")) invoice = make_invoice(self, member, plan, settings) + self.invoice = invoice.name - if invoice and settings.send_invoice and self.membership_status in ["New", "Current"]: - print("Sending") - message = settings.new_message if self.membership_status == "New" else settings.renewal - email = member.email_id if member.email_id else member.email + if save: + self.save() - email_args = { - "recipients": [email], - "message": message, - "subject": _('Here is your invoice'), - "attachments": [frappe.attach_print("Sales Invoice", invoice.name, print_format=settings.print_format)], - "reference_doctype": self.doctype, - "reference_name": self.name - } - if not frappe.flags.in_test: - frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args) - else: - frappe.sendmail(**email_args) + return invoice + def send_acknowlement(self): + settings = frappe.get_doc("Membership Settings") + if not settings.send_email: + frappe.throw(_("You need to enable Send Acknowledge Email in Membership Settings")) + + member = frappe.get_doc("Member", self.member) + plan = frappe.get_doc("Membership Type", self.membership_type) + email = member.email_id if member.email_id else member.email + attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)] + + if self.invoice and settings.send_invoice: + attachments.append(frappe.attach_print("Sales Invoice", self.invoice, print_format=settings.inv_print_format)) + + email_args = { + "recipients": [email], + "message": settings.message, + "subject": _('Here is your invoice'), + "attachments": [frappe.attach_print("Sales Invoice", invoice.name, print_format=settings.inv_print_format)], + "reference_doctype": self.doctype, + "reference_name": self.name + } + + if not frappe.flags.in_test: + frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args) + else: + frappe.sendmail(**email_args) + + def generate_and_send_invoice(self): + invoice = self.generate_invoice(False) + self.send_acknowlement() def make_invoice(membership, member, plan, settings): invoice = frappe.get_doc({ From 3a67a78ece8ad5a102e0d9623e3666c607532df4 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 14:54:06 +0530 Subject: [PATCH 034/192] feat: add custom buttons for invoice and email --- .../non_profit/doctype/membership/membership.js | 16 ++++++++++++++++ .../non_profit/doctype/membership/membership.py | 1 - 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js index 554549a0bd..8408a6a1f0 100644 --- a/erpnext/non_profit/doctype/membership/membership.js +++ b/erpnext/non_profit/doctype/membership/membership.js @@ -8,6 +8,22 @@ frappe.ui.form.on('Membership', { }) }, + refresh: function(frm) { + !frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => { + frm.call("generate_invoice", { + save: true + }).then(() => { + frm.reload_doc(); + }); + }); + + frm.add_custom_button("Send Acknowledgement", () => { + frm.call("send_acknowlement").then(() => { + frm.reload_doc(); + }); + }); + }, + onload: function(frm) { frm.add_fetch('membership_type', 'amount', 'amount'); } diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index c960daa1d3..8d1e44da5d 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -67,7 +67,6 @@ class Membership(Document): member = frappe.get_doc("Member", self.member) plan = frappe.get_doc("Membership Type", self.membership_type) settings = frappe.get_doc("Membership Settings") - attachments = [] if not member.customer: frappe.throw(_("No customer linked to member {}", [member.name])) From 733bde31c9e3de279a77de361c4392fa649777ec Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 15:06:51 +0530 Subject: [PATCH 035/192] fix: doctype name in patch --- erpnext/patches/v13_0/drop_razorpay_payload_column.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v13_0/drop_razorpay_payload_column.py b/erpnext/patches/v13_0/drop_razorpay_payload_column.py index 62f0373008..8980fd0039 100644 --- a/erpnext/patches/v13_0/drop_razorpay_payload_column.py +++ b/erpnext/patches/v13_0/drop_razorpay_payload_column.py @@ -2,6 +2,6 @@ from __future__ import unicode_literals import frappe def execute(): - if frappe.db.exists("DocType", "Membership Settings"): - if 'webhook_payload' in frappe.db.get_table_columns("Membership Settings"): - frappe.db.sql("alter table `tabMembership Settings` drop column webhook_payload") \ No newline at end of file + if frappe.db.exists("DocType", "Membership"): + if 'webhook_payload' in frappe.db.get_table_columns("Membership"): + frappe.db.sql("alter table `tabMembership` drop column webhook_payload") \ No newline at end of file From 6fbe9b54041b1325284774b60e9e475f8b2208a3 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sat, 1 Aug 2020 13:58:21 +0530 Subject: [PATCH 036/192] fix: remove payload --- erpnext/non_profit/doctype/membership/membership.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 8d1e44da5d..bfc2661a8f 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -200,7 +200,6 @@ def trigger_razorpay_subscription(*args, **kwargs): "currency": "INR", "paid": 1, "payment_id": payment.id, - "webhook_payload": data_json, "from_date": datetime.fromtimestamp(subscription.current_start), "to_date": datetime.fromtimestamp(subscription.current_end), "amount": payment.amount / 100 # Convert to rupees from paise From 87ddec069e2da7d624554696eeeba7f9d7db3e56 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sat, 1 Aug 2020 14:01:41 +0530 Subject: [PATCH 037/192] refactor: try block across the function --- erpnext/non_profit/doctype/membership/membership.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index bfc2661a8f..a3a1b5b275 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -157,9 +157,7 @@ def verify_signature(data): controller.verify_signature(data, signature, key) - -@frappe.whitelist(allow_guest=True) -def trigger_razorpay_subscription(*args, **kwargs): +def make_membership_entry(*args, **kwargs): data = frappe.request.get_data(as_text=True) try: verify_signature(data) @@ -218,6 +216,14 @@ def trigger_razorpay_subscription(*args, **kwargs): return { status: 'Success' } +@frappe.whitelist(allow_guest=True) +def trigger_razorpay_subscription(*args, **kwargs): + try: + return make_membership_entry(*args, **kwargs) + except Exception as e: + log = frappe.log_error(e, "Webhook Failed") + return { status: 'Failed' } + def notify_failure(log): try: From d7139bbd437f8dc4f58e64ae36e6733539b66952 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sun, 2 Aug 2020 12:03:01 +0530 Subject: [PATCH 038/192] fix: type error --- erpnext/non_profit/doctype/membership/membership.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index a3a1b5b275..dc243d8c75 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -182,10 +182,10 @@ def make_membership_entry(*args, **kwargs): except Exception as e: error_log = frappe.log_error(frappe.get_traceback() + '\n' + data_json , _("Membership Webhook Failed")) notify_failure(error_log) - return { status: 'Failed' } + return { 'status': 'Failed' } if not member: - return { status: 'Failed' } + return { 'status': 'Failed' } try: if data.event == "subscription.activated": member.customer_id = payment.customer_id @@ -212,9 +212,9 @@ def make_membership_entry(*args, **kwargs): except Exception as e: log = frappe.log_error(e, "Error creating membership entry") notify_failure(log) - return { status: 'Failed' } + return { 'status': 'Failed' } - return { status: 'Success' } + return { 'status': 'Success' } @frappe.whitelist(allow_guest=True) def trigger_razorpay_subscription(*args, **kwargs): @@ -222,7 +222,7 @@ def trigger_razorpay_subscription(*args, **kwargs): return make_membership_entry(*args, **kwargs) except Exception as e: log = frappe.log_error(e, "Webhook Failed") - return { status: 'Failed' } + return { 'status': 'Failed' } def notify_failure(log): From 98bcedd09c510c52fe823342e849bc707d9b20e5 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sun, 2 Aug 2020 14:50:27 +0530 Subject: [PATCH 039/192] feat: create member if not already exists --- erpnext/non_profit/doctype/member/member.py | 2 +- .../doctype/membership/membership.py | 85 +++++++++++-------- .../membership_type/membership_type.py | 2 +- 3 files changed, 50 insertions(+), 39 deletions(-) diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index c52082ca23..6c2da07bb7 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -78,7 +78,7 @@ def create_member(user_details): member.update({ "member_name": user_details.fullname, "email_id": user_details.email, - "pan_number": user_details.pan, + "pan_number": user_details.pan or None, "membership_type": user_details.plan_id, "customer": create_customer(user_details) }) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index dc243d8c75..fc89396fa8 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -10,6 +10,7 @@ from datetime import datetime from frappe.model.document import Document from frappe.email import sendmail_to_system_managers from frappe.utils import add_days, add_years, nowdate, getdate, add_months, get_link_to_form +from erpnext.non_profit.doctype.member.member import create_member from frappe import _ import erpnext @@ -142,6 +143,7 @@ def get_member_based_on_subscription(subscription_id, email): 'subscription_id': subscription_id, 'email_id': email }, order_by="creation desc") + try: return frappe.get_doc("Member", members[0]['name']) except: @@ -157,14 +159,15 @@ def verify_signature(data): controller.verify_signature(data, signature, key) -def make_membership_entry(*args, **kwargs): +@frappe.whitelist(allow_guest=True) +def trigger_razorpay_subscription(*args, **kwargs): data = frappe.request.get_data(as_text=True) try: verify_signature(data) except Exception as e: - signature = frappe.request.headers.get('X-Razorpay-Signature') - log = "{0} \n\n {1} \n\n {2} \n\n {3}".format(e, frappe.get_traceback(), signature, data) - frappe.log_error(e, "Webhook Verification Error") + log = frappe.log_error(e, "Webhook Verification Error") + notify_failure(log) + return { 'status': 'Failed', 'reason': e} if isinstance(data, six.string_types): data = json.loads(data) @@ -177,34 +180,42 @@ def make_membership_entry(*args, **kwargs): payment = frappe._dict(payment) try: - data_json = json.dumps(data, indent=4, sort_keys=True) + if not data.event == "subscription.charged": + return + member = get_member_based_on_subscription(subscription.id, payment.email) - except Exception as e: - error_log = frappe.log_error(frappe.get_traceback() + '\n' + data_json , _("Membership Webhook Failed")) - notify_failure(error_log) - return { 'status': 'Failed' } + if not member: + member = create_member(frappe._dict({ + 'fullname': payment.email, + 'email': payment.email, + 'plan_id': get_plan_from_razorpay_id(subscription.plan_id) + })) - if not member: - return { 'status': 'Failed' } - try: - if data.event == "subscription.activated": + member.subscription_id = subscription.id member.customer_id = payment.customer_id - elif data.event == "subscription.charged": - membership = frappe.new_doc("Membership") - membership.update({ - "member": member.name, - "membership_status": "Current", - "membership_type": member.membership_type, - "currency": "INR", - "paid": 1, - "payment_id": payment.id, - "from_date": datetime.fromtimestamp(subscription.current_start), - "to_date": datetime.fromtimestamp(subscription.current_end), - "amount": payment.amount / 100 # Convert to rupees from paise - }) - membership.insert(ignore_permissions=True) + if subscription.notes and type(subscription.notes) == dict: + notes = '\n'.join("{}: {}".format(k, v) for k, v in subscription.notes.items()) + member.add_comment("Comment", notes) + elif subscription.notes and type(subscription.notes) == str: + member.add_comment("Comment", subscription.notes) - # Update these values anyway + + # Update Membership + membership = frappe.new_doc("Membership") + membership.update({ + "member": member.name, + "membership_status": "Current", + "membership_type": member.membership_type, + "currency": "INR", + "paid": 1, + "payment_id": payment.id, + "from_date": datetime.fromtimestamp(subscription.current_start), + "to_date": datetime.fromtimestamp(subscription.current_end), + "amount": payment.amount / 100 # Convert to rupees from paise + }) + membership.insert(ignore_permissions=True) + + # Update membership values member.subscription_start = datetime.fromtimestamp(subscription.start_at) member.subscription_end = datetime.fromtimestamp(subscription.end_at) member.subscription_activated = 1 @@ -212,18 +223,10 @@ def make_membership_entry(*args, **kwargs): except Exception as e: log = frappe.log_error(e, "Error creating membership entry") notify_failure(log) - return { 'status': 'Failed' } + return { 'status': 'Failed', 'reason': e} return { 'status': 'Success' } -@frappe.whitelist(allow_guest=True) -def trigger_razorpay_subscription(*args, **kwargs): - try: - return make_membership_entry(*args, **kwargs) - except Exception as e: - log = frappe.log_error(e, "Webhook Failed") - return { 'status': 'Failed' } - def notify_failure(log): try: @@ -237,3 +240,11 @@ Administrator""".format(get_link_to_form("Error Log", log.name)) sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content) except: pass + +def get_plan_from_razorpay_id(plan_id): + plan = frappe.get_all("Membership Type", filters={'razorpay_plan_id': plan_id}, order_by="creation desc") + + try: + return plan[0]['name'] + except: + return None \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.py b/erpnext/non_profit/doctype/membership_type/membership_type.py index ed6b549600..7fa98a9481 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.py +++ b/erpnext/non_profit/doctype/membership_type/membership_type.py @@ -6,4 +6,4 @@ from __future__ import unicode_literals from frappe.model.document import Document class MembershipType(Document): - pass + pass \ No newline at end of file From e4c58c37c84c65f3f6eaac8ad4d2287e760df8a0 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 3 Aug 2020 14:56:01 +0530 Subject: [PATCH 040/192] feat: allow member creation via API --- erpnext/non_profit/doctype/member/member.py | 42 ++++++++++++++++--- .../membership_type/membership_type.json | 6 +-- .../membership_type/membership_type.py | 6 ++- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 6c2da07bb7..797736a3db 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -9,6 +9,7 @@ from frappe.model.document import Document from frappe.contacts.address_and_contact import load_address_and_contact from frappe.utils import cint from frappe.integrations.utils import get_payment_gateway_controller +from erpnext.non_profit.doctype.membership_type.membership_type import get_membership_type class Member(Document): def onload(self): @@ -74,19 +75,23 @@ def get_or_create_member(user_details): return create_member(user_details) def create_member(user_details): + user_details = frappe._dict(user_details) member = frappe.new_doc("Member") member.update({ "member_name": user_details.fullname, "email_id": user_details.email, "pan_number": user_details.pan or None, "membership_type": user_details.plan_id, - "customer": create_customer(user_details) + "subscription_id": user_details.subscription_id or None }) member.insert(ignore_permissions=True) + member.customer = create_customer(user_details, member.name) + member.save(ignore_permissions=True) + return member -def create_customer(user_details): +def create_customer(user_details, member=None): customer = frappe.new_doc("Customer") customer.customer_name = user_details.fullname customer.customer_type = "Individual" @@ -107,7 +112,13 @@ def create_customer(user_details): "link_name": customer.name }) - contact.save() + if member: + contact.append("links", { + "link_doctype": "Member", + "link_name": member + }) + + contact.save(ignore_permissions=True) except frappe.DuplicateEntryError: return customer.name @@ -139,12 +150,31 @@ def create_member_subscription_order(user_details): user_details = frappe._dict(user_details) member = get_or_create_member(user_details) - if not member: - member = create_member(user_details) subscription = member.setup_subscription() member.subscription_id = subscription.get('subscription_id') member.save(ignore_permissions=True) - return subscription \ No newline at end of file + return subscription + +@frappe.whitelist(allow_guest=True) +def register_member(fullname, email, rzpay_plan_id, subscription_id, pan=None, mobile=None): + plan = get_membership_type(rzpay_plan_id) + if not plan: + raise frappe.DoesNotExistError + + member = frappe.db.exists("Member", {'email': email, 'subscription_id': subscription_id }) + if member: + return member + else: + member = create_member(dict( + fullname=fullname, + email=email, + plan_id=plan, + subscription_id=subscription_id, + pan=pan, + mobile=mobile + )) + + return member.name \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.json b/erpnext/non_profit/doctype/membership_type/membership_type.json index a163568bb9..6ce1ecde12 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.json +++ b/erpnext/non_profit/doctype/membership_type/membership_type.json @@ -39,12 +39,12 @@ "fieldname": "linked_item", "fieldtype": "Link", "label": "Linked Item", - "options": "Item" + "options": "Item", + "unique": 1 } ], - "index_web_pages_for_search": 1, "links": [], - "modified": "2020-07-28 10:57:50.821375", + "modified": "2020-08-05 15:21:43.595745", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Type", diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.py b/erpnext/non_profit/doctype/membership_type/membership_type.py index 7fa98a9481..b95b04316f 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.py +++ b/erpnext/non_profit/doctype/membership_type/membership_type.py @@ -4,6 +4,10 @@ from __future__ import unicode_literals from frappe.model.document import Document +import frappe class MembershipType(Document): - pass \ No newline at end of file + pass + +def get_membership_type(razorpay_id): + return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id}) \ No newline at end of file From b4d3666e69f8a469d358ecb2cd082c45bce9f065 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 5 Aug 2020 17:33:18 +0530 Subject: [PATCH 041/192] feat: show send acknowledgement button if enabled in settings --- erpnext/non_profit/doctype/membership/membership.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js index 8408a6a1f0..ee8a8c0a7b 100644 --- a/erpnext/non_profit/doctype/membership/membership.js +++ b/erpnext/non_profit/doctype/membership/membership.js @@ -17,11 +17,13 @@ frappe.ui.form.on('Membership', { }); }); - frm.add_custom_button("Send Acknowledgement", () => { - frm.call("send_acknowlement").then(() => { - frm.reload_doc(); + frappe.db.get_single_value("Membership Settings", "send_email").then(val => { + if (val) frm.add_custom_button("Send Acknowledgement", () => { + frm.call("send_acknowlement").then(() => { + frm.reload_doc(); + }); }); - }); + }) }, onload: function(frm) { From 6b68eaad1ec1eb770e3cb2ae0b73fe4f43d1b219 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 5 Aug 2020 17:33:33 +0530 Subject: [PATCH 042/192] feat: use email template for membership acknowledgement --- .../doctype/membership/membership.py | 9 +++++--- .../membership_settings.json | 23 +++++++------------ 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index fc89396fa8..f058004ff9 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -99,11 +99,14 @@ class Membership(Document): if self.invoice and settings.send_invoice: attachments.append(frappe.attach_print("Sales Invoice", self.invoice, print_format=settings.inv_print_format)) + email_template = frappe.get_doc("Email Template", settings.email_template) + context = { "doc": self, "member": member} + email_args = { "recipients": [email], - "message": settings.message, - "subject": _('Here is your invoice'), - "attachments": [frappe.attach_print("Sales Invoice", invoice.name, print_format=settings.inv_print_format)], + "message": frappe.render_template(email_template.get("response"), context), + "subject": frappe.render_template(email_template.get("subject"), context), + "attachments": attachments, "reference_doctype": self.doctype, "reference_name": self.name } diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 2452a763ce..5b6bab5b0a 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -19,8 +19,7 @@ "send_invoice", "membership_print_format", "inv_print_format", - "section_break_15", - "message" + "email_template" ], "fields": [ { @@ -96,7 +95,7 @@ "default": "0", "fieldname": "send_email", "fieldtype": "Check", - "label": "Send Acknowledge Email" + "label": "Send Membership Acknowledgement" }, { "depends_on": "eval: doc.send_invoice", @@ -111,26 +110,20 @@ "fieldname": "membership_print_format", "fieldtype": "Link", "label": "Membership Print Format", - "mandatory_depends_on": "eval:doc.send_email", "options": "Print Format" }, { "depends_on": "eval:doc.send_email", - "fieldname": "section_break_15", - "fieldtype": "Section Break", - "hide_border": 1 - }, - { - "depends_on": "eval:doc.send_email", - "fieldname": "message", - "fieldtype": "Text Editor", - "label": "Message" + "fieldname": "email_template", + "fieldtype": "Link", + "label": "Email Template", + "mandatory_depends_on": "eval:doc.send_email", + "options": "Email Template" } ], - "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-07-31 14:30:15.701767", + "modified": "2020-08-05 17:26:37.287395", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", From 3daa9de998e7acbbd440643258b131b863a5b363 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 6 Aug 2020 10:06:23 +0530 Subject: [PATCH 043/192] feat: add email option to field email_id --- erpnext/non_profit/doctype/member/member.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json index bb73a843ee..77cdb94b3d 100644 --- a/erpnext/non_profit/doctype/member/member.json +++ b/erpnext/non_profit/doctype/member/member.json @@ -133,7 +133,8 @@ { "fieldname": "email_id", "fieldtype": "Data", - "label": "Email Address" + "label": "Email Address", + "options": "Email" }, { "fieldname": "subscription_id", @@ -176,7 +177,7 @@ ], "image_field": "image", "links": [], - "modified": "2020-04-07 14:20:33.215700", + "modified": "2020-08-06 10:06:01.153564", "modified_by": "Administrator", "module": "Non Profit", "name": "Member", From d7b69e57aeb62cd5ea5dc83a9702b8072e755313 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Mon, 17 Aug 2020 11:20:56 +0530 Subject: [PATCH 044/192] fix: deleting unused Stock Entry Type --- erpnext/patches/v13_0/stock_entry_enhancements.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v13_0/stock_entry_enhancements.py b/erpnext/patches/v13_0/stock_entry_enhancements.py index dcc4f956f7..0bdcc9c0e8 100644 --- a/erpnext/patches/v13_0/stock_entry_enhancements.py +++ b/erpnext/patches/v13_0/stock_entry_enhancements.py @@ -24,4 +24,8 @@ def execute(): if not frappe.db.exists('Warehouse Type', 'Transit'): doc = frappe.new_doc('Warehouse Type') doc.name = 'Transit' - doc.insert() \ No newline at end of file + doc.insert() + + frappe.reload_doc("stock", "doctype", "stock_entry_type") + frappe.delete_doc_if_exists("Stock Entry Type", "Send to Warehouse") + frappe.delete_doc_if_exists("Stock Entry Type", "Receive at Warehouse") \ No newline at end of file From eb241c6c424e7bbdd50c33edf31a8883c1126686 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 17 Aug 2020 12:44:52 +0530 Subject: [PATCH 045/192] fix: General Ledger filter validation --- .../accounts/report/general_ledger/general_ledger.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index fcd36e4e6e..779cdbde9b 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)) @@ -132,7 +135,7 @@ def get_gl_entries(filters): if filters and filters.get('cost_center'): select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit, credit*(DCC_allocation.percentage_allocation/100) as credit, debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency, credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency """ - + distributed_cost_center_query = """ UNION ALL SELECT name as gl_entry, @@ -146,7 +149,7 @@ def get_gl_entries(filters): against_voucher_type, against_voucher, account_currency, - remarks, against, + remarks, against, is_opening, `tabGL Entry`.creation {select_fields_with_percentage} FROM `tabGL Entry`, ( From 205df14a480fb305504eca16957c891c99f079e2 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 17 Aug 2020 16:23:44 +0530 Subject: [PATCH 046/192] fix: Credit Limit Email not working --- erpnext/selling/doctype/customer/customer.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 93d4832173..1f955fcd52 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -184,10 +184,10 @@ class Customer(TransactionBase): def validate_credit_limit_on_change(self): if self.get("__islocal") or not self.credit_limits: return - + past_credit_limits = [d.credit_limit for d in frappe.db.get_all("Customer Credit Limit", filters={'parent': self.name}, fields=["credit_limit"], order_by="company")] - + current_credit_limits = [d.credit_limit for d in sorted(self.credit_limits, key=lambda k: k.company)] if past_credit_limits == current_credit_limits: @@ -396,13 +396,12 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, credit_controller_users = get_users_with_role(credit_controller_role or "Sales Master Manager") # form a list of emails and names to show to the user - credit_controller_users = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users] - - if not credit_controller_users: + credit_controller_users_formatted = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users] + if not credit_controller_users_formatted: frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.".format(customer))) message = """Please contact any of the following users to extend the credit limits for {0}: -

  • {1}
""".format(customer, '
  • '.join(credit_controller_users)) +

    • {1}
    """.format(customer, '
  • '.join(credit_controller_users_formatted)) # if the current user does not have permissions to override credit limit, # prompt them to send out an email to the controller users @@ -427,7 +426,7 @@ def send_emails(args): subject = (_("Credit limit reached for customer {0}").format(args.get('customer'))) message = (_("Credit limit has been crossed for customer {0} ({1}/{2})") .format(args.get('customer'), args.get('customer_outstanding'), args.get('credit_limit'))) - frappe.sendmail(recipients=[args.get('credit_controller_users_list')], subject=subject, message=message) + frappe.sendmail(recipients=args.get('credit_controller_users_list'), subject=subject, message=message) def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=False, cost_center=None): # Outstanding based on GL Entries From 698d983eefb6cb068b9fffaaa5bfac1718b1ce0b Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 19 Aug 2020 14:59:46 +0530 Subject: [PATCH 047/192] fix: Item Tax Updation via Update Items in SO/PO --- erpnext/controllers/accounts_controller.py | 16 +++++++++++++++- erpnext/controllers/taxes_and_totals.py | 12 ++++++++---- .../public/js/controllers/taxes_and_totals.js | 11 ++++++++--- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 3091193b8d..791b932695 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -19,7 +19,7 @@ from erpnext.accounts.doctype.pricing_rule.utils import (apply_pricing_rule_on_t from erpnext.exceptions import InvalidCurrency from six import text_type from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions -from erpnext.stock.get_item_details import get_item_warehouse +from erpnext.stock.get_item_details import get_item_warehouse, _get_item_tax_template, get_item_tax_map from erpnext.stock.doctype.packed_item.packed_item import make_packing_list force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules") @@ -1157,6 +1157,18 @@ def get_supplier_block_status(party_name): } return info +def set_child_tax_template_and_map(item, child_item, parent_doc): + args = { + 'item_code': item.item_code, + 'posting_date': parent_doc.transaction_date, + 'tax_category': parent_doc.get('tax_category'), + 'company': parent_doc.get('company') + } + + child_item.item_tax_template = _get_item_tax_template(args, item.taxes) + if child_item.get("item_tax_template"): + child_item.item_tax_rate = get_item_tax_map(parent_doc.get('company'), child_item.item_tax_template, as_json=True) + def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item): """ Returns a Sales Order Item child item containing the default values @@ -1170,6 +1182,7 @@ def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0 child_item.uom = item.stock_uom + set_child_tax_template_and_map(item, child_item, p_doc) child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) if not child_item.warehouse: frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.") @@ -1192,6 +1205,7 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna child_item.uom = item.stock_uom child_item.base_rate = 1 # Initiallize value will update in parent validation child_item.base_amount = 1 # Initiallize value will update in parent validation + set_child_tax_template_and_map(item, child_item, p_doc) return child_item def check_and_delete_children(parent, data): diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 2a14be8532..59c60f7fdc 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -209,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) @@ -220,10 +220,14 @@ class calculate_taxes_and_totals(object): return current_tax_fraction, inclusive_tax_amount_per_qty def _get_tax_rate(self, tax, item_tax_map): - if tax.account_head in item_tax_map: - return flt(item_tax_map.get(tax.account_head), self.doc.precision("rate", tax)) + if item_tax_map: + if tax.account_head in item_tax_map: + return flt(item_tax_map.get(tax.account_head), self.doc.precision("rate", tax)) + else: + return tax.rate else: - return tax.rate + # If no item tax template against item dont calculate tax against it + return 0 def calculate_net_total(self): self.doc.total_qty = self.doc.total = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0 diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 6951539026..9659124960 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -223,8 +223,13 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ }, _get_tax_rate: function(tax, item_tax_map) { - return (Object.keys(item_tax_map).indexOf(tax.account_head) != -1) ? - flt(item_tax_map[tax.account_head], precision("rate", tax)) : tax.rate; + if(!$.isEmptyObject(item_tax_map)) { + return (Object.keys(item_tax_map).indexOf(tax.account_head) != -1) ? + flt(item_tax_map[tax.account_head], precision("rate", tax)) : tax.rate; + } else { + // If no item tax template against item dont calculate tax against it + return 0 + } }, calculate_net_total: function() { @@ -595,7 +600,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ $.each(actual_taxes_dict, function(key, value) { if (value) total_actual_tax += value; }); - + return flt(this.frm.doc.grand_total - total_actual_tax, precision("grand_total")); } }, From aaeb3980bcdc0df493c52d20c4440b71f4ad4af2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 19 Aug 2020 15:13:30 +0530 Subject: [PATCH 048/192] 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 dfdf9dc095..b757d53aa2 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 a3ed4cebb1..6f3fff2932 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 22d3729427671e5d412c5634163413ae0ba507c1 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 19 Aug 2020 15:42:40 +0530 Subject: [PATCH 049/192] fix: Codacy --- erpnext/public/js/controllers/taxes_and_totals.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 9659124960..87f6d6a947 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -228,7 +228,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ flt(item_tax_map[tax.account_head], precision("rate", tax)) : tax.rate; } else { // If no item tax template against item dont calculate tax against it - return 0 + return 0; } }, From e292fd6c5877c346c74f3c7cde02e4b7677ca912 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Mon, 24 Aug 2020 00:45:13 +0530 Subject: [PATCH 050/192] feat: add integrations desk page --- .../erpnext_integrations.json | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 erpnext/erpnext_integrations/desk_page/erpnext_integrations/erpnext_integrations.json diff --git a/erpnext/erpnext_integrations/desk_page/erpnext_integrations/erpnext_integrations.json b/erpnext/erpnext_integrations/desk_page/erpnext_integrations/erpnext_integrations.json new file mode 100644 index 0000000000..8dcc77d174 --- /dev/null +++ b/erpnext/erpnext_integrations/desk_page/erpnext_integrations/erpnext_integrations.json @@ -0,0 +1,40 @@ +{ + "cards": [ + { + "hidden": 0, + "label": "Marketplace", + "links": "[\n {\n \"description\": \"Woocommerce marketplace settings\",\n \"label\": \"Woocommerce Settings\",\n \"name\": \"Woocommerce Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Amazon MWS settings\",\n \"label\": \"Amazon MWS Settings\",\n \"name\": \"Amazon MWS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Shopify settings\",\n \"label\": \"Shopify Settings\",\n \"name\": \"Shopify Settings\",\n \"type\": \"doctype\"\n }\n]" + }, + { + "hidden": 0, + "label": "Payments", + "links": "[\n {\n \"description\": \"GoCardless payment gateway settings\",\n \"label\": \"GoCardless Settings\",\n \"name\": \"GoCardless Settings\",\n \"type\": \"doctype\"\n }\n]" + }, + { + "hidden": 0, + "label": "Settings", + "links": "[\n {\n \"description\": \"Plaid settings\",\n \"label\": \"Plaid Settings\",\n \"name\": \"Plaid Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Exotel settings\",\n \"label\": \"Exotel Settings\",\n \"name\": \"Exotel Settings\",\n \"type\": \"doctype\"\n }\n]" + } + ], + "category": "Modules", + "charts": [], + "creation": "2020-08-20 19:30:48.138801", + "developer_mode_only": 0, + "disable_user_customization": 0, + "docstatus": 0, + "doctype": "Desk Page", + "extends": "Integrations", + "extends_another_page": 1, + "hide_custom": 1, + "idx": 0, + "is_standard": 1, + "label": "ERPNext Integrations", + "modified": "2020-08-23 16:30:51.494655", + "modified_by": "Administrator", + "module": "ERPNext Integrations", + "name": "ERPNext Integrations", + "owner": "Administrator", + "pin_to_bottom": 0, + "pin_to_top": 0, + "shortcuts": [] +} \ No newline at end of file From f49665077c59716db6a985639dfb4388a43db976 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 24 Aug 2020 18:26:48 +0530 Subject: [PATCH 051/192] feat: Quoted Item Comparison Report Enhancements v2 --- .../quoted_item_comparison.js | 58 +++++++++++-- .../quoted_item_comparison.py | 83 +++++++++++++------ 2 files changed, 109 insertions(+), 32 deletions(-) diff --git a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js index a76ffeec2e..8718e4e2ec 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js @@ -12,7 +12,22 @@ frappe.query_reports["Quoted Item Comparison"] = { "reqd": 1 }, { - reqd: 1, + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "width": "80", + "reqd": 1, + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "width": "80", + "reqd": 1, + "default": frappe.datetime.get_today() + }, + { default: "", options: "Item", label: __("Item"), @@ -45,13 +60,12 @@ frappe.query_reports["Quoted Item Comparison"] = { } }, { - fieldtype: "Link", + fieldtype: "MultiSelectList", label: __("Supplier Quotation"), - options: "Supplier Quotation", fieldname: "supplier_quotation", default: "", - get_query: () => { - return { filters: { "docstatus": ["<", 2] } } + get_data: function(txt) { + return frappe.db.get_link_options('Supplier Quotation', txt); } }, { @@ -63,9 +77,30 @@ frappe.query_reports["Quoted Item Comparison"] = { get_query: () => { return { filters: { "docstatus": ["<", 2] } } } + }, + { + fieldtype: "Check", + label: __("Include Expired"), + fieldname: "include_expired", + default: 0 } ], + formatter: (value, row, column, data, default_formatter) => { + value = default_formatter(value, row, column, data); + + if(column.fieldname === "valid_till" && data.valid_till){ + if(frappe.datetime.get_diff(data.valid_till, frappe.datetime.nowdate()) <= 1){ + value = `
    ${value}
    `; + } + else if (frappe.datetime.get_diff(data.valid_till, frappe.datetime.nowdate()) <= 7){ + value = `
    ${value}
    `; + } + } + + return value; + }, + onload: (report) => { // Create a button for setting the default supplier report.page.add_inner_button(__("Select Default Supplier"), () => { @@ -75,6 +110,19 @@ frappe.query_reports["Quoted Item Comparison"] = { reporter.make_default_supplier_dialog(report); }, 'Tools'); + const status_message = ` + + Valid till :    + + + Expires in a week + +      + + Expires today / Already Expired + ` + report.$status.html(status_message).show(); + }, make_default_supplier_dialog: (report) => { // Get the name of the item to change diff --git a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py index a33867a525..ffa138f1e9 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py @@ -16,44 +16,48 @@ def execute(filters=None): supplier_quotation_data = get_data(filters, conditions) columns = get_columns() - data, chart_data = prepare_data(supplier_quotation_data) + data, chart_data = prepare_data(supplier_quotation_data, filters) return columns, data, None, chart_data def get_conditions(filters): conditions = "" + if filters.get("item_code"): + conditions += " AND sqi.item_code = %(item_code)s" + if filters.get("supplier_quotation"): - conditions += " AND sqi.parent = %(supplier_quotation)s" + conditions += " AND sqi.parent in %(supplier_quotation)s" if filters.get("request_for_quotation"): conditions += " AND sqi.request_for_quotation = %(request_for_quotation)s" if filters.get("supplier"): conditions += " AND sq.supplier in %(supplier)s" + + if not filters.get("include_expired"): + conditions += " AND sq.status != 'Expired'" + return conditions def get_data(filters, conditions): - if not filters.get("item_code"): - return [] - supplier_quotation_data = frappe.db.sql("""SELECT - sqi.parent, sqi.qty, sqi.rate, sqi.uom, sqi.request_for_quotation, - sq.supplier + sqi.parent, sqi.item_code, sqi.qty, sqi.rate, sqi.uom, sqi.request_for_quotation, + sqi.lead_time_days, sq.supplier, sq.valid_till FROM `tabSupplier Quotation Item` sqi, `tabSupplier Quotation` sq WHERE - sqi.item_code = %(item_code)s - AND sqi.parent = sq.name + sqi.parent = sq.name AND sqi.docstatus < 2 AND sq.company = %(company)s - AND sq.status != 'Expired' - {0}""".format(conditions), filters, as_dict=1) + AND sq.transaction_date between %(from_date)s and %(to_date)s + {0} + order by sq.transaction_date, sqi.item_code""".format(conditions), filters, as_dict=1) return supplier_quotation_data -def prepare_data(supplier_quotation_data): - out, suppliers, qty_list = [], [], [] +def prepare_data(supplier_quotation_data, filters): + out, suppliers, qty_list, chart_data = [], [], [], [] supplier_wise_map = defaultdict(list) supplier_qty_price_map = {} @@ -70,20 +74,24 @@ def prepare_data(supplier_quotation_data): exchange_rate = 1 row = { + "item_code": data.get('item_code'), "quotation": data.get("parent"), "qty": data.get("qty"), "price": flt(data.get("rate") * exchange_rate, float_precision), "uom": data.get("uom"), "request_for_quotation": data.get("request_for_quotation"), + "valid_till": data.get('valid_till'), + "lead_time_days": data.get('lead_time_days') } # map for report view of form {'supplier1':[{},{},...]} supplier_wise_map[supplier].append(row) # map for chart preparation of the form {'supplier1': {'qty': 'price'}} - if not supplier in supplier_qty_price_map: - supplier_qty_price_map[supplier] = {} - supplier_qty_price_map[supplier][row["qty"]] = row["price"] + if filters.get("item_code"): + if not supplier in supplier_qty_price_map: + supplier_qty_price_map[supplier] = {} + supplier_qty_price_map[supplier][row["qty"]] = row["price"] suppliers.append(supplier) qty_list.append(data.get("qty")) @@ -97,7 +105,8 @@ def prepare_data(supplier_quotation_data): for entry in supplier_wise_map[supplier]: out.append(entry) - chart_data = prepare_chart_data(suppliers, qty_list, supplier_qty_price_map) + if filters.get("item_code"): + chart_data = prepare_chart_data(suppliers, qty_list, supplier_qty_price_map) return out, chart_data @@ -117,9 +126,10 @@ def prepare_chart_data(suppliers, qty_list, supplier_qty_price_map): data_points_map[qty].append(None) dataset = [] + currency_symbol = frappe.db.get_value("Currency", frappe.db.get_default("currency"), "symbol") for qty in qty_list: datapoints = { - "name": _("Price for Qty ") + str(qty), + "name": currency_symbol + " (Qty " + str(qty) + " )", "values": data_points_map[qty] } dataset.append(datapoints) @@ -140,14 +150,21 @@ def get_columns(): "label": _("Supplier"), "fieldtype": "Link", "options": "Supplier", + "width": 150 + }, + { + "fieldname": "item_code", + "label": _("Item"), + "fieldtype": "Link", + "options": "Item", "width": 200 }, { - "fieldname": "quotation", - "label": _("Supplier Quotation"), + "fieldname": "uom", + "label": _("UOM"), "fieldtype": "Link", - "options": "Supplier Quotation", - "width": 200 + "options": "UOM", + "width": 90 }, { "fieldname": "qty", @@ -163,18 +180,30 @@ def get_columns(): "width": 110 }, { - "fieldname": "uom", - "label": _("UOM"), + "fieldname": "quotation", + "label": _("Supplier Quotation"), "fieldtype": "Link", - "options": "UOM", - "width": 90 + "options": "Supplier Quotation", + "width": 200 + }, + { + "fieldname": "valid_till", + "label": _("Valid Till"), + "fieldtype": "Date", + "width": 100 + }, + { + "fieldname": "lead_time_days", + "label": _("Lead Time (Days)"), + "fieldtype": "Int", + "width": 100 }, { "fieldname": "request_for_quotation", "label": _("Request for Quotation"), "fieldtype": "Link", "options": "Request for Quotation", - "width": 200 + "width": 150 } ] From 8f452b86630b306dd47c6360d074a9d1ec2ad040 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 24 Aug 2020 18:57:07 +0530 Subject: [PATCH 052/192] fix: Codacy and indicator message --- .../report/quoted_item_comparison/quoted_item_comparison.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js index 8718e4e2ec..ad390c446b 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js @@ -115,12 +115,12 @@ frappe.query_reports["Quoted Item Comparison"] = { Valid till :    - Expires in a week + Expires in a week or less      Expires today / Already Expired - ` + `; report.$status.html(status_message).show(); }, From 28f4417fc526eef80d3f24911ed1a0917d1359ea Mon Sep 17 00:00:00 2001 From: Anupam K Date: Tue, 25 Aug 2020 08:59:37 +0530 Subject: [PATCH 053/192] fix: added filter show in website for filtering product --- erpnext/portal/product_configurator/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index 9eef16bed3..9ba4cdc514 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -13,13 +13,15 @@ def get_field_filter_data(): for f in fields: doctype = f.get_link_doctype() - # apply enable/disable filter + # apply enable/disable/show_in_website filter meta = frappe.get_meta(doctype) filters = {} if meta.has_field('enabled'): filters['enabled'] = 1 if meta.has_field('disabled'): filters['disabled'] = 0 + if meta.has_field('show_in_website'): + filters['show_in_website'] = 1 values = [d.name for d in frappe.get_all(doctype, filters)] filter_data.append([f, values]) From 2030b66fc6ca831c21e6a8d0a1dab82e0601f2b3 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 25 Aug 2020 17:09:53 +0530 Subject: [PATCH 054/192] feat: add customer link to call log --- .../communication/doctype/call_log/call_log.json | 16 ++++++++++++++-- .../communication/doctype/call_log/call_log.py | 3 +++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/erpnext/communication/doctype/call_log/call_log.json b/erpnext/communication/doctype/call_log/call_log.json index cfc08eb084..31e79f17cd 100644 --- a/erpnext/communication/doctype/call_log/call_log.json +++ b/erpnext/communication/doctype/call_log/call_log.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "field:id", "creation": "2019-06-05 12:07:02.634534", "doctype": "DocType", @@ -14,6 +15,7 @@ "contact", "contact_name", "column_break_10", + "customer", "lead", "lead_name", "section_break_5", @@ -28,7 +30,8 @@ }, { "fieldname": "section_break_5", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Call Details" }, { "fieldname": "id", @@ -125,10 +128,19 @@ "in_list_view": 1, "label": "Lead Name", "read_only": 1 + }, + { + "fieldname": "customer", + "fieldtype": "Link", + "label": "Customer", + "options": "Customer", + "read_only": 1 } ], "in_create": 1, - "modified": "2019-08-06 05:46:53.144683", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-08-25 17:08:34.085731", "modified_by": "Administrator", "module": "Communication", "name": "Call Log", diff --git a/erpnext/communication/doctype/call_log/call_log.py b/erpnext/communication/doctype/call_log/call_log.py index 5fe3c4edbb..b31b757a37 100644 --- a/erpnext/communication/doctype/call_log/call_log.py +++ b/erpnext/communication/doctype/call_log/call_log.py @@ -16,6 +16,9 @@ class CallLog(Document): self.contact = get_contact_with_phone_number(number) self.lead = get_lead_with_phone_number(number) + contact = frappe.get_doc("Contact", self.contact) + self.customer = contact.get_link_for("Customer") + def after_insert(self): self.trigger_call_popup() From de4ac0c9052aede26eee0208d3b66013a7fd908e Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 25 Aug 2020 17:10:05 +0530 Subject: [PATCH 055/192] chore: remove stray tabs --- erpnext/selling/doctype/customer/customer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 93d4832173..911fe511ae 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -184,10 +184,10 @@ class Customer(TransactionBase): def validate_credit_limit_on_change(self): if self.get("__islocal") or not self.credit_limits: return - + past_credit_limits = [d.credit_limit for d in frappe.db.get_all("Customer Credit Limit", filters={'parent': self.name}, fields=["credit_limit"], order_by="company")] - + current_credit_limits = [d.credit_limit for d in sorted(self.credit_limits, key=lambda k: k.company)] if past_credit_limits == current_credit_limits: From f4b939754c84111e9417734ea248e1c85a16ce1d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 26 Aug 2020 12:10:12 +0530 Subject: [PATCH 056/192] 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 6f3fff2932..59389ce326 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 7600960d2b8b2b39c54b6176c9ff9e2331033e36 Mon Sep 17 00:00:00 2001 From: Afshan Date: Wed, 26 Aug 2020 18:40:11 +0530 Subject: [PATCH 057/192] fix: returned empty list if non US based company --- erpnext/regional/report/irs_1099/irs_1099.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/report/irs_1099/irs_1099.py b/erpnext/regional/report/irs_1099/irs_1099.py index 67834d1221..a06efc89c6 100644 --- a/erpnext/regional/report/irs_1099/irs_1099.py +++ b/erpnext/regional/report/irs_1099/irs_1099.py @@ -16,9 +16,14 @@ from frappe.utils.jinja import render_template def execute(filters=None): filters = filters if isinstance(filters, _dict) else _dict(filters) + company = frappe.db.get_default("company") if not filters: filters.setdefault('fiscal_year', get_fiscal_year(nowdate())[0]) - filters.setdefault('company', frappe.db.get_default("company")) + filters.setdefault('company', company) + + region = frappe.db.get_value("Company", fieldname = ["country"], filters = { "name": company }) + if region != 'United States': + return [],[] data = [] columns = get_columns() data = frappe.db.sql(""" From b719620d0cdcb68c08b9805fd1066b7db47c9059 Mon Sep 17 00:00:00 2001 From: Afshan Date: Wed, 26 Aug 2020 20:55:16 +0530 Subject: [PATCH 058/192] fix: handleling condition if default company doesn't exist --- erpnext/regional/report/irs_1099/irs_1099.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/report/irs_1099/irs_1099.py b/erpnext/regional/report/irs_1099/irs_1099.py index a06efc89c6..d3509e500f 100644 --- a/erpnext/regional/report/irs_1099/irs_1099.py +++ b/erpnext/regional/report/irs_1099/irs_1099.py @@ -16,14 +16,15 @@ from frappe.utils.jinja import render_template def execute(filters=None): filters = filters if isinstance(filters, _dict) else _dict(filters) - company = frappe.db.get_default("company") + if not filters: filters.setdefault('fiscal_year', get_fiscal_year(nowdate())[0]) - filters.setdefault('company', company) + filters.setdefault('company', frappe.db.get_default("company")) - region = frappe.db.get_value("Company", fieldname = ["country"], filters = { "name": 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(""" From 7586c3408e3fa55b4277ce5616709649bc91a88c Mon Sep 17 00:00:00 2001 From: Afshan Date: Fri, 28 Aug 2020 12:56:37 +0530 Subject: [PATCH 059/192] fix: test case --- erpnext/regional/united_states/test_united_states.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/united_states/test_united_states.py b/erpnext/regional/united_states/test_united_states.py index 688f14576c..af16f973bf 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,7 +42,7 @@ 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" From d659774190c60cb43816cf31a163e692360d5915 Mon Sep 17 00:00:00 2001 From: Afshan Date: Fri, 28 Aug 2020 13:40:48 +0530 Subject: [PATCH 060/192] fix:tests --- erpnext/regional/united_states/test_united_states.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/united_states/test_united_states.py b/erpnext/regional/united_states/test_united_states.py index af16f973bf..ad95010a9a 100644 --- a/erpnext/regional/united_states/test_united_states.py +++ b/erpnext/regional/united_states/test_united_states.py @@ -44,8 +44,8 @@ def make_payment_entry_to_irs_1099_supplier(): pe.payment_type = "Pay" 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 a787882a8f55333d7f41a4df9c09e01d05b540df Mon Sep 17 00:00:00 2001 From: Frappe ERPnext Date: Fri, 28 Aug 2020 13:28:52 +0200 Subject: [PATCH 061/192] feat: Option to print UOM after quantity Standard for ERPNext prints was UOM before quantity. Now user has a choice. --- erpnext/controllers/print_settings.py | 12 +++++++++--- .../includes/item_table_qty_swapped.html | 5 +++++ 2 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 erpnext/templates/print_formats/includes/item_table_qty_swapped.html diff --git a/erpnext/controllers/print_settings.py b/erpnext/controllers/print_settings.py index c41db25253..d24b69162b 100644 --- a/erpnext/controllers/print_settings.py +++ b/erpnext/controllers/print_settings.py @@ -7,9 +7,15 @@ from frappe.utils import cint def print_settings_for_item_table(doc): - doc.print_templates = { - "qty": "templates/print_formats/includes/item_table_qty.html" - } + if frappe.db.get_single_value("Print Settings", "print_uom_after_quantity") != 1: + doc.print_templates = { + "qty": "templates/print_formats/includes/item_table_qty.html" + } + else: + doc.print_templates = { + "qty": "templates/print_formats/includes/item_table_qty_swapped.html" + } + doc.hide_in_print_layout = ["uom", "stock_uom"] doc.flags.compact_item_print = cint(frappe.db.get_single_value("Print Settings", "compact_item_print")) diff --git a/erpnext/templates/print_formats/includes/item_table_qty_swapped.html b/erpnext/templates/print_formats/includes/item_table_qty_swapped.html new file mode 100644 index 0000000000..c6c70f2cf7 --- /dev/null +++ b/erpnext/templates/print_formats/includes/item_table_qty_swapped.html @@ -0,0 +1,5 @@ +{{ doc.get_formatted("qty", doc) }} +{% if (doc.uom and not doc.is_print_hide("uom")) %}{{ _(doc.uom) }} +{% elif (doc.stock_uom and not doc.is_print_hide("stock_uom")) %}{{ _(doc.stock_uom) }} +{%- endif %} + From b23840bf7b668971364ac05cb2425d396b617dd5 Mon Sep 17 00:00:00 2001 From: Syed Mujeer Hashmi Date: Sat, 29 Aug 2020 12:48:48 +0530 Subject: [PATCH 062/192] fix: Filter out cancelled entries in customer ledger summary Signed-off-by: Syed Mujeer Hashmi --- .../report/customer_ledger_summary/customer_ledger_summary.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index 2cb10b11e1..10b32fea56 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -173,7 +173,7 @@ class PartyLedgerSummaryReport(object): from `tabGL Entry` gle {join} where - gle.docstatus < 2 and gle.party_type=%(party_type)s and ifnull(gle.party, '') != '' + gle.docstatus < 2 and gle.is_cancelled = 0 and gle.party_type=%(party_type)s and ifnull(gle.party, '') != '' and gle.posting_date <= %(to_date)s {conditions} order by gle.posting_date """.format(join=join, join_field=join_field, conditions=conditions), self.filters, as_dict=True) @@ -248,7 +248,7 @@ class PartyLedgerSummaryReport(object): from `tabGL Entry` where - docstatus < 2 + docstatus < 2 and is_cancelled = 0 and (voucher_type, voucher_no) in ( select voucher_type, voucher_no from `tabGL Entry` gle, `tabAccount` acc where acc.name = gle.account and acc.account_type = '{income_or_expense}' From c71e37c988c79ed126d38ffe59c3b1b74f7718a0 Mon Sep 17 00:00:00 2001 From: michellealva Date: Sun, 30 Aug 2020 19:42:24 +0530 Subject: [PATCH 063/192] feat: Allow Rename for Tax Category --- .../doctype/tax_category/tax_category.json | 90 +++---------------- 1 file changed, 11 insertions(+), 79 deletions(-) diff --git a/erpnext/accounts/doctype/tax_category/tax_category.json b/erpnext/accounts/doctype/tax_category/tax_category.json index 1e3ae455b3..6f682a0466 100644 --- a/erpnext/accounts/doctype/tax_category/tax_category.json +++ b/erpnext/accounts/doctype/tax_category/tax_category.json @@ -1,134 +1,66 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, + "actions": [], + "allow_rename": 1, "autoname": "field:title", - "beta": 0, "creation": "2018-11-22 23:38:39.668804", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "title" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "title", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Title", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, "unique": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2020-01-15 17:14:28.951793", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-08-30 19:41:25.783852", "modified_by": "Administrator", "module": "Accounts", "name": "Tax Category", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Accounts Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Accounts User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 0 + "share": 1 } ], "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} + "track_changes": 1 +} \ No newline at end of file From e268d294b3390daad2a6031db22bdc264a3fda53 Mon Sep 17 00:00:00 2001 From: marination Date: Sun, 30 Aug 2020 21:01:34 +0530 Subject: [PATCH 064/192] fix: Better error feedback on creating SO from Quotation --- 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 ab095ebfe0..20ae19f5db 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -285,9 +285,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 a4259208e7482df727d8efc1beace9eb4906467c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 30 Aug 2020 23:09:23 +0530 Subject: [PATCH 065/192] feat: Utility function to get possible loan disbursal amount --- .../loan_management/doctype/loan/test_loan.py | 51 ++++++++++++++++++ .../loan_application/loan_application.js | 10 ++-- .../loan_disbursement/loan_disbursement.py | 53 +++++++++++-------- .../loan_interest_accrual.py | 14 +++-- .../doctype/loan_repayment/loan_repayment.py | 7 ++- .../loan_security_shortfall.py | 12 +++-- 6 files changed, 111 insertions(+), 36 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 23815d5982..b75f7bdd75 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -17,6 +17,7 @@ from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loa from erpnext.loan_management.doctype.loan.loan import create_loan_security_unpledge from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge +from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount class TestLoan(unittest.TestCase): def setUp(self): @@ -323,6 +324,56 @@ class TestLoan(unittest.TestCase): self.assertEqual(loan.status, 'Closed') self.assertEquals(sum(pledged_qty.values()), 0) + def test_disbursal_check_with_shortfall(self): + pledges = [{ + "loan_security": "Test Security 2", + "qty": 8000.00, + "haircut": 50, + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, + 'Stock Loan', pledges, "Repay Over Number of Periods", 12) + + create_pledge(loan_application) + + loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application) + loan.submit() + + #Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge + make_loan_disbursement_entry(loan.name, 700000) + + frappe.db.sql("""UPDATE `tabLoan Security Price` SET loan_security_price = 100 + where loan_security='Test Security 2'""") + + create_process_loan_security_shortfall() + loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name}) + self.assertTrue(loan_security_shortfall) + + self.assertEqual(get_disbursal_amount(loan.name), 0) + + frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 250 + where loan_security='Test Security 2'""") + + def test_disbursal_check_without_shortfall(self): + pledges = [{ + "loan_security": "Test Security 2", + "qty": 8000.00, + "haircut": 50, + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, + 'Stock Loan', pledges, "Repay Over Number of Periods", 12) + + create_pledge(loan_application) + + loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application) + loan.submit() + + #Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge + make_loan_disbursement_entry(loan.name, 700000) + + self.assertEqual(get_disbursal_amount(loan.name), 300000) + def create_loan_accounts(): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.js b/erpnext/loan_management/doctype/loan_application/loan_application.js index b56fce1d7c..1365274971 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.js +++ b/erpnext/loan_management/doctype/loan_application/loan_application.js @@ -33,18 +33,18 @@ frappe.ui.form.on('Loan Application', { if (frm.doc.is_secured_loan) { frappe.db.get_value("Loan Security Pledge", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { - if (!r) { + if (Object.keys(r).length === 0) { frm.add_custom_button(__('Loan Security Pledge'), function() { - frm.trigger('create_loan_security_pledge') + frm.trigger('create_loan_security_pledge'); },__('Create')) } }); } frappe.db.get_value("Loan", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { - if (!r) { + if (Object.keys(r).length === 0) { frm.add_custom_button(__('Loan'), function() { - frm.trigger('create_loan') + frm.trigger('create_loan'); },__('Create')) } else { frm.set_df_property('status', 'read_only', 1); @@ -54,7 +54,7 @@ frappe.ui.form.on('Loan Application', { }, create_loan: function(frm) { if (frm.doc.status != "Approved") { - frappe.throw(__("Cannot create loan until application is approved")) + frappe.throw(__("Cannot create loan until application is approved")); } frappe.model.open_mapped_doc({ diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index 6c27e12134..260fada893 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -67,28 +67,10 @@ class LoanDisbursement(AccountsController): disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount total_payment = loan_details.total_payment - if disbursed_amount > loan_details.loan_amount and loan_details.is_term_loan: - frappe.throw(_("Disbursed Amount cannot be greater than loan amount")) + possible_disbursal_amount = get_disbursal_amount(self.against_loan) - if loan_details.status == 'Disbursed': - pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \ - - flt(loan_details.total_principal_paid) - else: - pending_principal_amount = loan_details.disbursed_amount - - security_value = 0.0 - if loan_details.is_secured_loan: - security_value = get_total_pledged_security_value(self.against_loan) - - if not security_value: - security_value = loan_details.loan_amount - - if pending_principal_amount + self.disbursed_amount > flt(security_value): - allowed_amount = security_value - pending_principal_amount - if allowed_amount < 0: - allowed_amount = 0 - - frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(allowed_amount)) + if self.disbursed_amount > possible_disbursal_amount: + frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount)) if loan_details.status == "Disbursed" and not loan_details.is_term_loan: process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1), @@ -176,3 +158,32 @@ def get_total_pledged_security_value(loan): security_value += (loan_security_price_map.get(security) * qty * hair_cut_map.get(security))/100 return security_value + +@frappe.whitelist() +def get_disbursal_amount(loan): + loan_details = frappe.get_all("Loan", fields = ["loan_amount", "disbursed_amount", "total_payment", + "total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan"], + filters= { "name": loan })[0] + + if loan_details.is_secured_loan and frappe.get_all('Loan Security Shortfall', filters={'loan': loan, + 'status': 'Pending'}): + return 0 + + if loan_details.status == 'Disbursed': + pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \ + - flt(loan_details.total_principal_paid) + else: + pending_principal_amount = flt(loan_details.disbursed_amount) + + security_value = 0.0 + if loan_details.is_secured_loan: + security_value = get_total_pledged_security_value(loan) + + if not security_value and not loan_details.is_secured_loan: + security_value = flt(loan_details.loan_amount) + + disbursal_amount = flt(security_value) - flt(pending_principal_amount) + + return disbursal_amount + + diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index c5111fdc93..1d3fa71068 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -85,8 +85,11 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i if no_of_days <= 0: return - pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ - - flt(loan.total_principal_paid) + if loan.status == 'Disbursed': + pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ + - flt(loan.total_principal_paid) + else: + pending_principal_amount = loan.disbursed_amount interest_per_day = (pending_principal_amount * loan.rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100) payable_interest = interest_per_day * no_of_days @@ -107,7 +110,7 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None): query_filters = { - "status": "Disbursed", + "status": ('in', ['Disbursed', 'Partially Disbursed']), "docstatus": 1 } @@ -118,8 +121,9 @@ def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_inte if not open_loans: open_loans = frappe.get_all("Loan", - fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", "is_term_loan", - "disbursement_date", "applicant_type", "applicant", "rate_of_interest", "total_interest_payable", "repayment_start_date"], + fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", + "is_term_loan", "status", "disbursement_date", "disbursed_amount", "applicant_type", "applicant", + "rate_of_interest", "total_interest_payable", "total_principal_paid", "repayment_start_date"], filters=query_filters) for loan in open_loans: diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 9605045777..451ae85afb 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -281,7 +281,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): due_date = add_days(entry.posting_date, 1) no_of_late_days = date_diff(posting_date, - add_days(due_date, loan_type_details.grace_period_in_days)) + add_days(due_date, loan_type_details.grace_period_in_days)) if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary): penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)/365 @@ -297,7 +297,10 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): if not final_due_date: final_due_date = add_days(due_date, loan_type_details.grace_period_in_days) - pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable + if against_loan_doc.status == 'Disbursed': + pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable + else: + pending_principal_amount = against_loan_doc.disbursed_amount if payment_type == "Loan Closure": if due_date: diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py index 02efe240bd..c3ea882809 100644 --- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -51,13 +51,19 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): "valid_upto": (">=", update_time) }, as_list=1)) - loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid'], - filters={'status': 'Disbursed', 'is_secured_loan': 1}) + loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid', 'total_payment', + 'total_interest_payable', 'disbursed_amount'], + filters={'status': ('in',['Disbursed','Partially Disbursed']), 'is_secured_loan': 1}) loan_security_map = {} for loan in loans: - outstanding_amount = loan.loan_amount - loan.total_principal_paid + if loan.status == 'Disbursed': + outstanding_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ + - flt(loan.total_principal_paid) + else: + outstanding_amount = loan.disbursed_amount + pledged_securities = get_pledged_security_qty(loan.name) ltv_ratio = '' security_value = 0.0 From ce29757bff235030d49bc1caf3d2a17d4fd2b941 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 31 Aug 2020 11:26:51 +0530 Subject: [PATCH 066/192] fix: Import flt --- .../doctype/loan_security_shortfall/loan_security_shortfall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py index c3ea882809..71e741ccf0 100644 --- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import get_datetime +from frappe.utils import get_datetime, flt from frappe.model.document import Document from six import iteritems from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty From e8b121c2c2dc4fd00f78f32c8d7a26089c9ecddb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 31 Aug 2020 11:31:48 +0530 Subject: [PATCH 067/192] fix: Add status in field list --- .../doctype/loan_security_shortfall/loan_security_shortfall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py index 71e741ccf0..0f42bde3c4 100644 --- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -52,7 +52,7 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): }, as_list=1)) loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid', 'total_payment', - 'total_interest_payable', 'disbursed_amount'], + 'total_interest_payable', 'disbursed_amount', 'status'], filters={'status': ('in',['Disbursed','Partially Disbursed']), 'is_secured_loan': 1}) loan_security_map = {} From d70e711aea9221b4a0d42f56081eb28d6b059b2e Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 31 Aug 2020 13:14:32 +0530 Subject: [PATCH 068/192] fix: events not deleted on cancelling maintenance schedule (#22954) * 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 | 8 ++-- .../test_maintenance_schedule.py | 38 ++++++++++++++++++- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index add7bbfa57..cba6a2d014 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -67,16 +67,16 @@ class MaintenanceSchedule(TransactionBase): 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({ + 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 d8ae17b4c7..3c307e920f 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 e7ab53a711a822e801f4d9c2c8b1006a5e4c3c9c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 22 Jun 2020 12:36:23 +0530 Subject: [PATCH 069/192] fix: completed qty not updated in work order --- erpnext/manufacturing/doctype/job_card/job_card.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index c29d4ba3d5..8dd90e612b 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -233,7 +233,7 @@ class JobCard(Document): work_order_field = "name" if field == "operation_id" else field for data in wo.operations: - if data.get(work_order_field) == self.get(field) and data.workstation == self.workstation: + if data.get(work_order_field) == 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 From a5963e1b2c94cb7f028f49cb2957154c76cff9a4 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 24 Aug 2020 17:55:23 +0530 Subject: [PATCH 070/192] 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 | 10 ++- .../doctype/job_card/job_card.py | 34 +++++++++++ .../doctype/job_card/test_job_card.py | 24 +++++++- 4 files changed, 124 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index bab0dfb6b4..7ac0b11ecb 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) { frappe.flags.pause_job = 0; frappe.flags.resume_job = 0; @@ -20,12 +31,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 fba670c1c1..087ab6b484 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -11,6 +11,7 @@ "bom_no", "workstation", "operation", + "operation_row_number", "column_break_4", "posting_date", "company", @@ -291,11 +292,15 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "operation_row_number", + "fieldtype": "Select", + "label": "Operation Row Number" } ], "is_submittable": 1, - "links": [], - "modified": "2020-04-20 15:14:00.273441", + "modified": "2020-08-24 15:21:21.398267", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", @@ -347,7 +352,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 8dd90e612b..a87e6e5037 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -15,10 +15,13 @@ from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings class OverlapError(frappe.ValidationError): pass +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 @@ -306,6 +309,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 ca05fea0f6..2a6c35fc04 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 d0aafece32bc779963cb28951cf73617f1d36ef4 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 31 Aug 2020 11:57:57 +0530 Subject: [PATCH 071/192] 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 a87e6e5037..8855e0acf5 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -212,11 +212,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 @@ -229,14 +228,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 2a6c35fc04..353f6d281a 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 2169260854..8266cf7b77 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 f9f26d2fa419aaec5c27b6c51086c979bb78907a Mon Sep 17 00:00:00 2001 From: Anupam K Date: Mon, 31 Aug 2020 14:00:27 +0530 Subject: [PATCH 072/192] fix: contact us button issue --- .../generators/item/item_add_to_cart.html | 31 ++++++++++--------- .../generators/item/item_configure.html | 7 ++--- .../generators/item/item_inquiry.html | 11 +++++++ 3 files changed, 30 insertions(+), 19 deletions(-) create mode 100644 erpnext/templates/generators/item/item_inquiry.html diff --git a/erpnext/templates/generators/item/item_add_to_cart.html b/erpnext/templates/generators/item/item_add_to_cart.html index 2a70d8dbe9..40bc0c749b 100644 --- a/erpnext/templates/generators/item/item_add_to_cart.html +++ b/erpnext/templates/generators/item/item_add_to_cart.html @@ -27,22 +27,25 @@ {% endif %} {% endif %} - {% if product_info.price and (cart_settings.allow_items_not_in_stock or product_info.in_stock) %}
    - - {{ _("View in Cart") }} - - + {% if product_info.price and (cart_settings.allow_items_not_in_stock or product_info.in_stock) %} + + {{ _("View in Cart") }} + + + {% endif %} + {% if cart_settings.show_contact_us_button %} + {% include "templates/generators/item/item_inquiry.html" %} + {% endif %}
    - {% endif %} diff --git a/erpnext/templates/generators/item/item_configure.html b/erpnext/templates/generators/item/item_configure.html index b8b0d98bdc..73f9ec99b3 100644 --- a/erpnext/templates/generators/item/item_configure.html +++ b/erpnext/templates/generators/item/item_configure.html @@ -10,14 +10,11 @@ {{ _('Configure') }} {% endif %} - {% if cart_settings.show_contact_us_button | int %} - + {% if cart_settings.show_contact_us_button %} + {% include "templates/generators/item/item_inquiry.html" %} {% endif %} {% endif %} diff --git a/erpnext/templates/generators/item/item_inquiry.html b/erpnext/templates/generators/item/item_inquiry.html new file mode 100644 index 0000000000..83653b6821 --- /dev/null +++ b/erpnext/templates/generators/item/item_inquiry.html @@ -0,0 +1,11 @@ +{% if shopping_cart and shopping_cart.cart_settings.enabled %} +{% set cart_settings = shopping_cart.cart_settings %} + {% if cart_settings.show_contact_us_button | int %} + + {% endif %} + +{% endif %} From 25042a22eb407486bb80099b8e8b416aec022f50 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 31 Aug 2020 19:17:29 +0530 Subject: [PATCH 073/192] fix: Pending amount after loan closure request --- .../loan_management/doctype/loan_repayment/loan_repayment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 451ae85afb..d1bb6ccedf 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -297,7 +297,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): if not final_due_date: final_due_date = add_days(due_date, loan_type_details.grace_period_in_days) - if against_loan_doc.status == 'Disbursed': + if against_loan_doc.status in ('Disbursed', 'Loan Closure Requested'): pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable else: pending_principal_amount = against_loan_doc.disbursed_amount From bc6c4e864dfe36835bffc7e0af03a27b32d0eb38 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 31 Aug 2020 20:32:17 +0530 Subject: [PATCH 074/192] fix: Status in Report and filter query --- .../quoted_item_comparison.js | 15 +-------------- .../quoted_item_comparison.py | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js index ad390c446b..518d665e7e 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js @@ -65,7 +65,7 @@ frappe.query_reports["Quoted Item Comparison"] = { fieldname: "supplier_quotation", default: "", get_data: function(txt) { - return frappe.db.get_link_options('Supplier Quotation', txt); + return frappe.db.get_link_options('Supplier Quotation', txt, {'docstatus': ["<", 2]}); } }, { @@ -110,19 +110,6 @@ frappe.query_reports["Quoted Item Comparison"] = { reporter.make_default_supplier_dialog(report); }, 'Tools'); - const status_message = ` - - Valid till :    - - - Expires in a week or less - -      - - Expires today / Already Expired - `; - report.$status.html(status_message).show(); - }, make_default_supplier_dialog: (report) => { // Get the name of the item to change diff --git a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py index ffa138f1e9..4426560c16 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py @@ -17,8 +17,9 @@ def execute(filters=None): columns = get_columns() data, chart_data = prepare_data(supplier_quotation_data, filters) + message = get_message() - return columns, data, None, chart_data + return columns, data, message, chart_data def get_conditions(filters): conditions = "" @@ -207,4 +208,16 @@ def get_columns(): } ] - return columns \ No newline at end of file + return columns + +def get_message(): + return """ + Valid till :    + + + Expires in a week or less + +    + + Expires today / Already Expired + """ \ No newline at end of file From 1dc9a9b669cb502e905318952bda9d8aa2234b22 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Mon, 31 Aug 2020 20:35:05 +0530 Subject: [PATCH 075/192] fix: reverse journal entry for multi-currency (#23165) * fix: reverse journal entry for multi-currency * fix: test case for reverse journal entry --- .../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 a09face791..409c15f75c 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -638,20 +638,12 @@ $.extend(erpnext.journal_entry, { return { filters: filters }; }, - reverse_journal_entry: function(frm) { - var me = frm.doc; - for(var i=0; i Date: Mon, 31 Aug 2020 21:21:05 +0530 Subject: [PATCH 076/192] fix: Add unit test for pending principal amount --- .../loan_management/doctype/loan/test_loan.py | 53 +++++++++++++++---- .../doctype/loan_repayment/loan_repayment.py | 8 +++ .../loan_security_unpledge.py | 8 +-- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index b75f7bdd75..2f6cd25a36 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -18,6 +18,7 @@ from erpnext.loan_management.doctype.loan.loan import create_loan_security_unple from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount +from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts class TestLoan(unittest.TestCase): def setUp(self): @@ -194,18 +195,14 @@ class TestLoan(unittest.TestCase): make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date = last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) repayment_entry.submit() amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', 'paid_principal_amount']) - unaccrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * 6) \ - / (days_in_year(get_datetime(first_date).year) * 100) - - self.assertEquals(flt(amounts[0] + unaccrued_interest_amount, 3), - flt(accrued_interest_amount, 3)) + self.assertEquals(flt(amounts[0], 2),flt(accrued_interest_amount, 2)) self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) loan.load_from_db() @@ -307,9 +304,6 @@ class TestLoan(unittest.TestCase): "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) repayment_entry.submit() - amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', - 'paid_principal_amount']) - loan.load_from_db() self.assertEquals(loan.status, "Loan Closure Requested") @@ -374,6 +368,47 @@ class TestLoan(unittest.TestCase): self.assertEqual(get_disbursal_amount(loan.name), 300000) + def test_pending_loan_amount_after_closure_request(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + create_pledge(loan_application) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + + no_of_days += 6 + + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime(first_date).year) * 100) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + + amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry.submit() + + amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', + 'paid_principal_amount']) + + loan.load_from_db() + self.assertEquals(loan.status, "Loan Closure Requested") + + amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") + self.assertEquals(amounts['pending_principal_amount'], 0.0) def create_loan_accounts(): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index d1bb6ccedf..7d83e32213 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -116,6 +116,7 @@ class LoanRepayment(AccountsController): def allocate_amounts(self, paid_entries): self.set('repayment_details', []) self.principal_amount_paid = 0 + total_interest_paid = 0 interest_paid = self.amount_paid - self.penalty_amount if self.amount_paid - self.penalty_amount > 0 and paid_entries: @@ -137,12 +138,19 @@ class LoanRepayment(AccountsController): interest_paid = 0 paid_principal=0 + total_interest_paid += interest_amount self.append('repayment_details', { 'loan_interest_accrual': lia, 'paid_interest_amount': interest_amount, 'paid_principal_amount': paid_principal }) + if self.payment_type == 'Loan Closure' and total_interest_paid < self.interest_payable: + unaccrued_interest = self.interest_payable - total_interest_paid + interest_paid -= unaccrued_interest + if self.repayment_details: + self.repayment_details[-1].paid_interest_amount += unaccrued_interest + if interest_paid: self.principal_amount_paid += interest_paid diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index 5e9d82aa91..f6b28dae75 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -43,8 +43,10 @@ class LoanSecurityUnpledge(Document): "valid_upto": (">=", get_datetime()) }, as_list=1)) - loan_amount, principal_paid = frappe.get_value("Loan", self.loan, ['loan_amount', 'total_principal_paid']) - pending_principal_amount = loan_amount - principal_paid + total_payment, principal_paid, interest_payable = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid', + 'total_interest_payable']) + + pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) security_value = 0 for security in self.securities: @@ -60,7 +62,7 @@ class LoanSecurityUnpledge(Document): security_value += qty_after_unpledge * loan_security_price_map.get(security.loan_security) - if not security_value and pending_principal_amount > 0: + if not security_value and flt(pending_principal_amount, 2) > 0: frappe.throw("Cannot Unpledge, loan to value ratio is breaching") if security_value and (pending_principal_amount/security_value) * 100 > ltv_ratio: From eb4b2aa14252161433bb3f3f5e2be45476f0af8b Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 1 Sep 2020 15:01:32 +0530 Subject: [PATCH 077/192] fix: get_items from product bundle for purchase order (#22821) * fix: get_items from product bundle for purchase order * fix: Don't overwrite doctype while setting attributes in child row Co-authored-by: Marica --- erpnext/public/js/controllers/buying.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index a4cc68b3e2..cb76c87b62 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -503,11 +503,11 @@ erpnext.buying.get_items_from_product_bundle = function(frm) { if(!r.exc && r.message) { remove_empty_first_row(frm); - for ( var i=0; i< r.message.length; i++ ) { + for (var i=0; i< r.message.length; i++) { var d = frm.add_child("items"); var item = r.message[i]; - for ( var key in item) { - if ( !is_null(item[key]) ) { + for (var key in item) { + if (!is_null(item[key]) && key !== "doctype") { d[key] = item[key]; } } From cebf8034552821a4c54104ac3170b54ffc7a8504 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 1 Sep 2020 16:15:28 +0530 Subject: [PATCH 078/192] fix: Update tax template on supplier address change --- erpnext/regional/india/taxes.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js index fbccc6b078..3b6a28f52c 100644 --- a/erpnext/regional/india/taxes.js +++ b/erpnext/regional/india/taxes.js @@ -6,6 +6,9 @@ erpnext.setup_auto_gst_taxation = (doctype) => { shipping_address: function(frm) { frm.trigger('get_tax_template'); }, + supplier_address: function(frm) { + frm.trigger('get_tax_template'); + }, tax_category: function(frm) { frm.trigger('get_tax_template'); }, From 86a190652e4643a59862b9663f7dfdd10343d1ef Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 1 Sep 2020 14:02:34 +0530 Subject: [PATCH 079/192] fix: incorrect job card timer issue --- erpnext/manufacturing/doctype/job_card/job_card.js | 12 ++++++------ .../doctype/job_card/test_job_card.py | 14 +++++++++++--- .../doctype/work_order/test_work_order.py | 2 +- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 7ac0b11ecb..b051b3243f 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -34,7 +34,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"); } }, @@ -94,9 +94,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); @@ -141,9 +141,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) { @@ -175,6 +173,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); } }, diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 353f6d281a..b6a6c33d37 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -31,6 +31,9 @@ class TestJobCard(unittest.TestCase): doc.operation_id = "Test Data" self.assertRaises(OperationMismatchError, doc.save) + for d in job_cards: + frappe.delete_doc("Job Card", d.name) + 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']) @@ -40,9 +43,11 @@ class TestJobCard(unittest.TestCase): work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom) - job_card = frappe.get_all('Job Card', + job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}, - fields = ["operation_id", "workstation", "name", "for_quantity"])[0] + fields = ["operation_id", "workstation", "name", "for_quantity"]) + + job_card = job_cards[0] if job_card: workstation = frappe.db.get_value("Workstation", @@ -64,4 +69,7 @@ class TestJobCard(unittest.TestCase): 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 + doc.cancel() + + for d in job_cards: + frappe.delete_doc("Job Card", d.name) \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 2260befb3f..b7c7c32869 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -7,7 +7,7 @@ import unittest import frappe from frappe.utils import flt, time_diff_in_hours, now, add_months, cint, today from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory -from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry, +from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry, ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError, CapacityError) from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.utils import get_bin From d2881ba4dde0eb3157b52ef905517996de17191f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 2 Sep 2020 11:52:59 +0530 Subject: [PATCH 080/192] fix: set conversion factor while creating RFQ from Opportunity --- erpnext/crm/doctype/opportunity/opportunity.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 6096053136..ad12941fda 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -267,6 +267,12 @@ def make_quotation(source_name, target_doc=None): @frappe.whitelist() def make_request_for_quotation(source_name, target_doc=None): + def set_missing_values(source, target): + rfq = frappe.get_doc(target) + for item in rfq.items: + # opportunity item is not multi-uom + item.conversion_factor = 1.0 + doclist = get_mapped_doc("Opportunity", source_name, { "Opportunity": { "doctype": "Request for Quotation" @@ -279,7 +285,7 @@ def make_request_for_quotation(source_name, target_doc=None): ["uom", "uom"] ] } - }, target_doc) + }, target_doc, set_missing_values) return doclist From a1b5d570768d5b71c4a199a02ea7038c7ac409de Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 2 Sep 2020 13:14:01 +0530 Subject: [PATCH 081/192] test: make rfq from opportunity --- .../test_request_for_quotation.py | 19 +++++++++++++++++++ .../doctype/opportunity/test_opportunity.py | 3 ++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index 3de9526c4f..019cefc0bd 100644 --- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py @@ -11,6 +11,8 @@ from erpnext.stock.doctype.item.test_item import make_item from erpnext.templates.pages.rfq import check_supplier_has_docname_access from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation from erpnext.buying.doctype.request_for_quotation.request_for_quotation import create_supplier_quotation +from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity +from erpnext.crm.doctype.opportunity.opportunity import make_request_for_quotation as make_rfq class TestRequestforQuotation(unittest.TestCase): def test_quote_status(self): @@ -110,6 +112,23 @@ class TestRequestforQuotation(unittest.TestCase): self.assertEqual(supplier_quotation.items[0].qty, 5) self.assertEqual(supplier_quotation.items[0].stock_qty, 10) + def test_make_rfq_from_opportunity(self): + opportunity = make_opportunity(with_items=1) + supplier_data = get_supplier_data() + rfq = make_rfq(opportunity.name) + + self.assertEqual(len(rfq.get("items")), len(opportunity.get("items"))) + rfq.message_for_supplier = 'Please supply the specified items at the best possible rates.' + + for item in rfq.items: + item.warehouse = "_Test Warehouse - _TC" + + for data in supplier_data: + rfq.append('suppliers', data) + + rfq.status = 'Draft' + rfq.submit() + def make_request_for_quotation(**args): """ :param supplier_data: List containing supplier data diff --git a/erpnext/crm/doctype/opportunity/test_opportunity.py b/erpnext/crm/doctype/opportunity/test_opportunity.py index 33d90076c4..04cd8a26ca 100644 --- a/erpnext/crm/doctype/opportunity/test_opportunity.py +++ b/erpnext/crm/doctype/opportunity/test_opportunity.py @@ -82,7 +82,8 @@ def make_opportunity(**args): if args.with_items: opp_doc.append('items', { "item_code": args.item_code or "_Test Item", - "qty": args.qty or 1 + "qty": args.qty or 1, + "uom": "_Test UOM" }) opp_doc.insert() From 41f421d4003fb80c62f2dc872b266a543e1b87e9 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 2 Sep 2020 13:47:18 +0530 Subject: [PATCH 082/192] feat: added option to add custom remarks in payment entry --- .../doctype/payment_entry/payment_entry.json | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 997937738b..72149a665d 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "autoname": "naming_series:", "creation": "2016-06-01 14:38:51.012597", @@ -63,6 +64,7 @@ "cost_center", "section_break_12", "status", + "custom_remarks", "remarks", "column_break_16", "letter_head", @@ -462,7 +464,8 @@ "fieldname": "remarks", "fieldtype": "Small Text", "label": "Remarks", - "no_copy": 1 + "no_copy": 1, + "read_only_depends_on": "eval:doc.custom_remarks == 0" }, { "fieldname": "column_break_16", @@ -573,10 +576,18 @@ "label": "Status", "options": "\nDraft\nSubmitted\nCancelled", "read_only": 1 + }, + { + "default": "0", + "fieldname": "custom_remarks", + "fieldtype": "Check", + "label": "Custom Remarks" } ], + "index_web_pages_for_search": 1, "is_submittable": 1, - "modified": "2019-12-08 13:02:30.016610", + "links": [], + "modified": "2020-09-02 13:39:43.383705", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", From c3984691b39cd1209a323f5e83e8f50576b1b4f8 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Wed, 2 Sep 2020 20:01:14 +0530 Subject: [PATCH 083/192] fix: Sales funnel data is inconsistent (#23110) * fix: Sales funnel data is inconsistent * fix: data inconsistency * fix: Converted Count Co-authored-by: Marica --- .../selling/page/sales_funnel/sales_funnel.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.py b/erpnext/selling/page/sales_funnel/sales_funnel.py index dba24ef5b0..b613718c7e 100644 --- a/erpnext/selling/page/sales_funnel/sales_funnel.py +++ b/erpnext/selling/page/sales_funnel/sales_funnel.py @@ -20,29 +20,28 @@ def get_funnel_data(from_date, to_date, company): validate_filters(from_date, to_date, company) active_leads = frappe.db.sql("""select count(*) from `tabLead` - where (date(`modified`) between %s and %s) - and status != "Do Not Contact" and company=%s""", (from_date, to_date, company))[0][0] - - active_leads += frappe.db.sql("""select count(distinct contact.name) from `tabContact` contact - left join `tabDynamic Link` dl on (dl.parent=contact.name) where dl.link_doctype='Customer' - and (date(contact.modified) between %s and %s) and status != "Passive" """, (from_date, to_date))[0][0] + where (date(`creation`) between %s and %s) + and company=%s""", (from_date, to_date, company))[0][0] opportunities = frappe.db.sql("""select count(*) from `tabOpportunity` where (date(`creation`) between %s and %s) - and status != "Lost" and company=%s""", (from_date, to_date, company))[0][0] + and opportunity_from='Lead' and company=%s""", (from_date, to_date, company))[0][0] quotations = frappe.db.sql("""select count(*) from `tabQuotation` where docstatus = 1 and (date(`creation`) between %s and %s) - and status != "Lost" and company=%s""", (from_date, to_date, company))[0][0] + and (opportunity!="" or quotation_to="Lead") and company=%s""", (from_date, to_date, company))[0][0] + + converted = frappe.db.sql("""select count(*) from `tabCustomer` + JOIN `tabLead` ON `tabLead`.name = `tabCustomer`.lead_name + WHERE (date(`tabCustomer`.creation) between %s and %s) + and `tabLead`.company=%s""", (from_date, to_date, company))[0][0] - sales_orders = frappe.db.sql("""select count(*) from `tabSales Order` - where docstatus = 1 and (date(`creation`) between %s and %s) and company=%s""", (from_date, to_date, company))[0][0] return [ - { "title": _("Active Leads / Customers"), "value": active_leads, "color": "#B03B46" }, + { "title": _("Active Leads"), "value": active_leads, "color": "#B03B46" }, { "title": _("Opportunities"), "value": opportunities, "color": "#F09C00" }, { "title": _("Quotations"), "value": quotations, "color": "#006685" }, - { "title": _("Sales Orders"), "value": sales_orders, "color": "#00AD65" } + { "title": _("Converted"), "value": converted, "color": "#00AD65" } ] @frappe.whitelist() From b872035fd9bbe362847c12604a7d8021ce82491d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2020 17:22:19 +0000 Subject: [PATCH 084/192] chore(deps): bump bl from 3.0.0 to 3.0.1 Bumps [bl](https://github.com/rvagg/bl) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/rvagg/bl/releases) - [Commits](https://github.com/rvagg/bl/compare/v3.0.0...v3.0.1) Signed-off-by: dependabot[bot] --- yarn.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/yarn.lock b/yarn.lock index b19f566fd0..97a063597d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -282,9 +282,9 @@ balanced-match@^1.0.0: integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= bl@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88" - integrity sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A== + version "3.0.1" + resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.1.tgz#1cbb439299609e419b5a74d7fce2f8b37d8e5c6f" + integrity sha512-jrCW5ZhfQ/Vt07WX1Ngs+yn9BDqPL/gw28S7s9H6QK/gupnizNzJAss5akW20ISgOrbLTlXOOCTJeNUQqruAWQ== dependencies: readable-stream "^3.0.1" @@ -866,12 +866,12 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@~2.0.3: +inherits@2, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -inherits@2.0.4, inherits@~2.0.1: +inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.1: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1447,9 +1447,9 @@ readable-stream@2, readable-stream@~2.3.6: util-deprecate "~1.0.1" readable-stream@^3.0.1, readable-stream@^3.1.1: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.5.0.tgz#465d70e6d1087f6162d079cd0b5db7fbebfd1606" - integrity sha512-gSz026xs2LfxBPudDuI41V1lka8cxg64E66SGe78zJlsUofOg/yqwezdIcdfwik6B4h8LFmWPA9ef9X3FiNFLA== + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" @@ -1505,9 +1505,9 @@ safe-buffer@^5.0.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== safe-buffer@~5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" - integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== "safer-buffer@>= 2.1.2 < 3": version "2.1.2" From f28bef868db05c1482f6534ef9950a90caf7fc51 Mon Sep 17 00:00:00 2001 From: Frappe ERPnext Date: Wed, 2 Sep 2020 21:42:52 +0200 Subject: [PATCH 085/192] feat: Checkbox for activation of printing UOM after quantity Checkbox will be added to Frappe after installation of ERPNext. (Print Settings docType is part of frappe app, not erpnext app) --- erpnext/setup/install.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 4f0f5721c2..2225fe169f 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -20,6 +20,7 @@ def after_install(): frappe.get_doc({'doctype': "Role", "role_name": "Analytics"}).insert() set_single_defaults() create_compact_item_print_custom_field() + create_print_uom_after_qty_custom_field() create_print_zero_amount_taxes_custom_field() add_all_roles_to("Administrator") create_default_cash_flow_mapper_templates() @@ -66,6 +67,16 @@ def create_compact_item_print_custom_field(): }) +def create_print_uom_after_qty_custom_field(): + create_custom_field('Print Settings', { + 'label': _('Print UOM after Quantity'), + 'fieldname': 'print_uom_after_quantity', + 'fieldtype': 'Check', + 'default': 0, + 'insert_after': 'compact_item_print' + }) + + def create_print_zero_amount_taxes_custom_field(): create_custom_field('Print Settings', { 'label': _('Print taxes with zero amount'), From 0857e6d6699570443afaf52ff55ed0ae53b60b21 Mon Sep 17 00:00:00 2001 From: michellealva Date: Thu, 3 Sep 2020 10:37:43 +0530 Subject: [PATCH 086/192] fix: Add "Bank Clearance" and "Bank Reconciliation" in desk --- erpnext/accounts/desk_page/accounting/accounting.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json index a2497838ee..2c5231491c 100644 --- a/erpnext/accounts/desk_page/accounting/accounting.json +++ b/erpnext/accounts/desk_page/accounting/accounting.json @@ -43,7 +43,7 @@ { "hidden": 0, "label": "Bank Statement", - "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"label\": \"Bank Clearance\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -98,7 +98,7 @@ "idx": 0, "is_standard": 1, "label": "Accounting", - "modified": "2020-06-19 12:42:44.054598", + "modified": "2020-09-03 10:37:07.865801", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", @@ -158,4 +158,4 @@ "type": "Dashboard" } ] -} +} \ No newline at end of file From 979ad180fa640536f8c8c831517aecb7ba2a5e2f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 3 Sep 2020 11:03:22 +0530 Subject: [PATCH 087/192] fix: update rfq item in postprocess --- erpnext/crm/doctype/opportunity/opportunity.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index ad12941fda..47b05f306b 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -267,11 +267,8 @@ def make_quotation(source_name, target_doc=None): @frappe.whitelist() def make_request_for_quotation(source_name, target_doc=None): - def set_missing_values(source, target): - rfq = frappe.get_doc(target) - for item in rfq.items: - # opportunity item is not multi-uom - item.conversion_factor = 1.0 + def update_item(obj, target, source_parent): + target.conversion_factor = 1.0 doclist = get_mapped_doc("Opportunity", source_name, { "Opportunity": { @@ -283,9 +280,10 @@ def make_request_for_quotation(source_name, target_doc=None): ["name", "opportunity_item"], ["parent", "opportunity"], ["uom", "uom"] - ] + ], + "postprocess": update_item } - }, target_doc, set_missing_values) + }, target_doc) return doclist From 54cd194b4127dfc7b767c96ddd108a5466f9f176 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 3 Sep 2020 13:51:26 +0530 Subject: [PATCH 088/192] feat: removed roles for some reginal report and added via setup --- erpnext/patches.txt | 1 + ..._custom_roles_for_some_regional_reports.py | 10 +++++ erpnext/regional/india/setup.py | 13 ++++++ erpnext/regional/report/gstr_1/gstr_1.json | 14 +----- erpnext/regional/report/gstr_2/gstr_2.json | 44 +++++++------------ .../hsn_wise_summary_of_outward_supplies.json | 42 +++++++----------- 6 files changed, 59 insertions(+), 65 deletions(-) create mode 100644 erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 4b9c566f04..771babef6a 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -724,3 +724,4 @@ erpnext.patches.v12_0.update_state_code_for_daman_and_diu erpnext.patches.v12_0.rename_lost_reason_detail erpnext.patches.v13_0.drop_razorpay_payload_column erpnext.patches.v13_0.update_start_end_date_for_old_shift_assignment +erpnext.patches.v13_0.setting_custom_roles_for_some_regional_reports diff --git a/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py b/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py new file mode 100644 index 0000000000..ecc7822e1d --- /dev/null +++ b/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +import frappe +from erpnext.regional.india.setup import add_custom_roles_for_reports + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + add_custom_roles_for_reports() \ No newline at end of file diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 290694a789..cbcd6e3203 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -73,6 +73,19 @@ def add_custom_roles_for_reports(): ] )).insert() + for report_name in ('HSN-wise-summary of outward supplies', 'GSTR-1', 'GSTR-2'): + + if not frappe.db.get_value('Custom Role', dict(report=report_name)): + frappe.get_doc(dict( + doctype='Custom Role', + report=report_name, + roles= [ + dict(role='Accounts User'), + dict(role='Accounts Manager'), + dict(role='Auditor') + ] + )).insert() + def add_permissions(): for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'): add_permission(doctype, 'All', 0) diff --git a/erpnext/regional/report/gstr_1/gstr_1.json b/erpnext/regional/report/gstr_1/gstr_1.json index 2012bb8840..75aed8cffc 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.json +++ b/erpnext/regional/report/gstr_1/gstr_1.json @@ -7,7 +7,7 @@ "doctype": "Report", "idx": 0, "is_standard": "Yes", - "modified": "2019-06-30 19:33:59.769385", + "modified": "2019-09-03 19:33:59.769385", "modified_by": "Administrator", "module": "Regional", "name": "GSTR-1", @@ -16,15 +16,5 @@ "ref_doctype": "GL Entry", "report_name": "GSTR-1", "report_type": "Script Report", - "roles": [ - { - "role": "Accounts User" - }, - { - "role": "Accounts Manager" - }, - { - "role": "Auditor" - } - ] + "roles": [] } \ No newline at end of file diff --git a/erpnext/regional/report/gstr_2/gstr_2.json b/erpnext/regional/report/gstr_2/gstr_2.json index 929ed914d8..b70d0f9416 100644 --- a/erpnext/regional/report/gstr_2/gstr_2.json +++ b/erpnext/regional/report/gstr_2/gstr_2.json @@ -1,29 +1,19 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2018-01-29 12:59:55.650445", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "modified": "2018-01-29 12:59:55.650445", - "modified_by": "Administrator", - "module": "Regional", - "name": "GSTR-2", - "owner": "Administrator", - "ref_doctype": "GL Entry", - "report_name": "GSTR-2", - "report_type": "Script Report", - "roles": [ - { - "role": "Accounts User" - }, - { - "role": "Accounts Manager" - }, - { - "role": "Auditor" - } - ] + "add_total_row": 0, + "apply_user_permissions": 1, + "creation": "2018-01-29 12:59:55.650445", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2018-09-03 12:59:55.650445", + "modified_by": "Administrator", + "module": "Regional", + "name": "GSTR-2", + "owner": "Administrator", + "ref_doctype": "GL Entry", + "report_name": "GSTR-2", + "report_type": "Script Report", + "roles": [] } \ No newline at end of file diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.json b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.json index 124a720134..cc6ad574af 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.json +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.json @@ -1,28 +1,18 @@ { - "add_total_row": 0, - "creation": "2018-04-26 10:49:29.159400", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "modified": "2019-04-26 12:59:38.603649", - "modified_by": "Administrator", - "module": "Regional", - "name": "HSN-wise-summary of outward supplies", - "owner": "Administrator", - "ref_doctype": "Sales Invoice", - "report_name": "HSN-wise-summary of outward supplies", - "report_type": "Script Report", - "roles": [ - { - "role": "Accounts User" - }, - { - "role": "Accounts Manager" - }, - { - "role": "Auditor" - } - ] + "add_total_row": 0, + "creation": "2018-04-26 10:49:29.159400", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2019-09-03 12:59:38.603649", + "modified_by": "Administrator", + "module": "Regional", + "name": "HSN-wise-summary of outward supplies", + "owner": "Administrator", + "ref_doctype": "Sales Invoice", + "report_name": "HSN-wise-summary of outward supplies", + "report_type": "Script Report", + "roles": [] } \ No newline at end of file From 68b0108301cc8ecab255e6f2c3afc3edbf145723 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 10 Jul 2020 14:23:12 +0530 Subject: [PATCH 089/192] fix: incorrect qty after transaction in stock ledger entry --- .../stock_reconciliation/stock_reconciliation.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 43fbc00466..b81f8a086d 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -258,6 +258,7 @@ class StockReconciliation(StockController): sl_entries.append(args) + qty_after_transaction = 0 for serial_no in serial_nos: args = self.get_sle_for_items(row, [serial_no]) @@ -271,11 +272,19 @@ class StockReconciliation(StockController): 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': cint(previous_sle.get('qty_after_transaction')) - 1, - 'warehouse': previous_sle.get("warehouse", '') or row.warehouse, + 'qty_after_transaction': qty_after_transaction, + 'warehouse': warehouse, 'valuation_rate': previous_sle.get("valuation_rate") }) From 7952f89596c9f05e749494c8b51fcd88a7b0672a Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 3 Sep 2020 15:21:35 +0530 Subject: [PATCH 090/192] feat: Overlap validation additional salary --- .../additional_salary/additional_salary.py | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index ef174bdea2..cf2bff0405 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -5,8 +5,8 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document -from frappe import _ -from frappe.utils import getdate, date_diff +from frappe import _, bold +from frappe.utils import getdate, date_diff, comma_and, formatdate class AdditionalSalary(Document): @@ -22,9 +22,37 @@ class AdditionalSalary(Document): def validate(self): self.validate_dates() + self.validate_recurring_additional_salary_overlap() if self.amount < 0: frappe.throw(_("Amount should not be less than zero.")) + def validate_recurring_additional_salary_overlap(self): + if self.is_recurring: + additional_salaries = frappe.db.sql(""" + SELECT + name + FROM `tabAdditional Salary` + WHERE + employee=%s + AND name <> %s + AND docstatus=1 + AND is_recurring=1 + AND salary_component = %s + AND to_date >= %s + AND from_date <= %s""", + (self.employee, self.name, self.salary_component, self.from_date, self.to_date), as_dict = 1) + + additional_salaries = [salary.name for salary in additional_salaries] + + if len(additional_salaries): + frappe.throw(_("Additional Salary: {0} already exist for Salary Component: {1} for period {2} and {3}").format( + bold(comma_and(additional_salaries)), + bold(self.salary_component), + bold(formatdate(self.from_date)), + bold(formatdate(self.to_date) + ))) + + def validate_dates(self): date_of_joining, relieving_date = frappe.db.get_value("Employee", self.employee, ["date_of_joining", "relieving_date"]) From e5b7cb0c15a3278469c775d5ff4fece8ca5137ef Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 3 Sep 2020 16:59:21 +0530 Subject: [PATCH 091/192] fix: data was not properly maped --- erpnext/hr/doctype/attendance/attendance.py | 3 ++- erpnext/hr/doctype/attendance/attendance_calendar.js | 6 ------ erpnext/hr/doctype/shift_assignment/shift_assignment.py | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index 45b7060610..373b94008e 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -98,7 +98,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 104f09d69f..4566489696 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', diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index f8b73349c1..b1f9fd58e3 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -103,7 +103,7 @@ def add_assignments(events, start, end, conditions=None): "doctype": "Shift Assignment", "start_date": d.start_date, "end_date": d.end_date if d.end_date else nowdate(), - "title": cstr(d.employee_name) + \ + "title": cstr(d.employee_name) + ":"+ \ cstr(d.shift_type), "docstatus": d.docstatus } From 9b681770d06ed09213da30603a9b2524bc2137a4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 3 Sep 2020 18:09:02 +0530 Subject: [PATCH 092/192] fix: Account filter in Process Deferred Accounting doctype --- .../process_deferred_accounting.js | 6 ++++-- .../process_deferred_accounting.json | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.js b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.js index 2800c195ce..1ec6805ae0 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.js +++ b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.js @@ -10,13 +10,15 @@ frappe.ui.form.on('Process Deferred Accounting', { } }; }); + }, - if (frm.doc.company) { + type: function(frm) { + if (frm.doc.company && frm.doc.type) { frm.set_query("account", function() { return { filters: { 'company': frm.doc.company, - 'root_type': 'Liability', + 'root_type': frm.doc.type === 'Income' ? 'Liability' : 'Asset', 'is_group': 0 } }; diff --git a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json index 4daafef3ec..457e98ca54 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json +++ b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json @@ -60,6 +60,7 @@ "reqd": 1 }, { + "depends_on": "eval: doc.type", "fieldname": "account", "fieldtype": "Link", "label": "Account", @@ -73,9 +74,10 @@ "reqd": 1 } ], + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-02-06 18:18:09.852844", + "modified": "2020-09-03 18:07:02.463754", "modified_by": "Administrator", "module": "Accounts", "name": "Process Deferred Accounting", From 2a56000460da2f8c3c12d1da18cd77b6f9a92fdc Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 3 Sep 2020 19:45:04 +0530 Subject: [PATCH 093/192] fix: Supplier Leaderboard fix --- erpnext/startup/leaderboard.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/startup/leaderboard.py b/erpnext/startup/leaderboard.py index 5545f13e8c..ef238f1165 100644 --- a/erpnext/startup/leaderboard.py +++ b/erpnext/startup/leaderboard.py @@ -123,7 +123,8 @@ def get_all_suppliers(date_range, company, field, limit = None): if field == "outstanding_amount": filters = [['docstatus', '=', '1'], ['company', '=', company]] if date_range: - filters.append(['posting_date', 'between' [date_range[0], date_range[1]]]) + date_range = frappe.parse_json(date_range) + filters.append(['posting_date', 'between', [date_range[0], date_range[1]]]) return frappe.db.get_all('Purchase Invoice', fields = ['supplier as name', 'sum(outstanding_amount) as value'], filters = filters, From a5d5e70bf4f256e70064fc710c8597b32077e68f Mon Sep 17 00:00:00 2001 From: Frappe ERPnext Date: Thu, 3 Sep 2020 17:30:25 +0200 Subject: [PATCH 094/192] Revert "feat: Option to print UOM after quantity" This reverts commit a787882a8f55333d7f41a4df9c09e01d05b540df. --- erpnext/controllers/print_settings.py | 12 +++--------- .../includes/item_table_qty_swapped.html | 5 ----- 2 files changed, 3 insertions(+), 14 deletions(-) delete mode 100644 erpnext/templates/print_formats/includes/item_table_qty_swapped.html diff --git a/erpnext/controllers/print_settings.py b/erpnext/controllers/print_settings.py index d24b69162b..c41db25253 100644 --- a/erpnext/controllers/print_settings.py +++ b/erpnext/controllers/print_settings.py @@ -7,15 +7,9 @@ from frappe.utils import cint def print_settings_for_item_table(doc): - if frappe.db.get_single_value("Print Settings", "print_uom_after_quantity") != 1: - doc.print_templates = { - "qty": "templates/print_formats/includes/item_table_qty.html" - } - else: - doc.print_templates = { - "qty": "templates/print_formats/includes/item_table_qty_swapped.html" - } - + doc.print_templates = { + "qty": "templates/print_formats/includes/item_table_qty.html" + } doc.hide_in_print_layout = ["uom", "stock_uom"] doc.flags.compact_item_print = cint(frappe.db.get_single_value("Print Settings", "compact_item_print")) diff --git a/erpnext/templates/print_formats/includes/item_table_qty_swapped.html b/erpnext/templates/print_formats/includes/item_table_qty_swapped.html deleted file mode 100644 index c6c70f2cf7..0000000000 --- a/erpnext/templates/print_formats/includes/item_table_qty_swapped.html +++ /dev/null @@ -1,5 +0,0 @@ -{{ doc.get_formatted("qty", doc) }} -{% if (doc.uom and not doc.is_print_hide("uom")) %}{{ _(doc.uom) }} -{% elif (doc.stock_uom and not doc.is_print_hide("stock_uom")) %}{{ _(doc.stock_uom) }} -{%- endif %} - From 9355641c49835327a0efcbb03e7a3cded7104b15 Mon Sep 17 00:00:00 2001 From: Frappe ERPnext Date: Thu, 3 Sep 2020 17:31:21 +0200 Subject: [PATCH 095/192] refactor: logic moved to standard template Previously print_settings.py controller decided which template to use. Now standard template decides what to print according to settings. --- .../includes/item_table_qty.html | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/erpnext/templates/print_formats/includes/item_table_qty.html b/erpnext/templates/print_formats/includes/item_table_qty.html index 239859eea1..ecaaef42b5 100644 --- a/erpnext/templates/print_formats/includes/item_table_qty.html +++ b/erpnext/templates/print_formats/includes/item_table_qty.html @@ -1,6 +1,15 @@ -{% if (doc.uom and not doc.is_print_hide("uom")) %} - {{ _(doc.uom) }} -{% elif (doc.stock_uom and not doc.is_print_hide("stock_uom")) %} - {{ _(doc.stock_uom) }} +{% set qty_first=frappe.db.get_single_value("Print Settings", "print_uom_after_quantity") %} +{% if qty_first %} + {{ doc.get_formatted("qty", doc) }} + {% if (doc.uom and not doc.is_print_hide("uom")) %} {{ _(doc.uom) }} + {% elif (doc.stock_uom and not doc.is_print_hide("stock_uom")) %} {{ _(doc.stock_uom) }} + {%- endif %} +{% else %} + {% if (doc.uom and not doc.is_print_hide("uom")) %} + {{ _(doc.uom) }} + {% elif (doc.stock_uom and not doc.is_print_hide("stock_uom")) %} + {{ _(doc.stock_uom) }} + {%- endif %} + {{ doc.get_formatted("qty", doc) }} {%- endif %} -{{ doc.get_formatted("qty", doc) }} + From 8d8af941823ab1f054c3e59229c962f034f78227 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 4 Sep 2020 12:17:56 +0530 Subject: [PATCH 096/192] fix: capture advance payments in payment order --- .../doctype/payment_entry/payment_entry.py | 29 +++++++------------ .../payment_order_reference.json | 13 +++------ 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 842c64fdbe..bb312bf72e 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1172,30 +1172,23 @@ def make_payment_order(source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc def set_missing_values(source, target): target.payment_order_type = "Payment Entry" + target.append('references', dict( + reference_doctype="Payment Entry", + reference_name=source.name, + bank_account=source.party_bank_account, + amount=source.paid_amount, + account=source.paid_to, + supplier=source.party, + mode_of_payment=source.mode_of_payment, + )) - def update_item(source_doc, target_doc, source_parent): - target_doc.bank_account = source_parent.party_bank_account - target_doc.amount = source_doc.allocated_amount - target_doc.account = source_parent.paid_to - target_doc.payment_entry = source_parent.name - target_doc.supplier = source_parent.party - target_doc.mode_of_payment = source_parent.mode_of_payment - - - doclist = get_mapped_doc("Payment Entry", source_name, { + doclist = get_mapped_doc("Payment Entry", source_name, { "Payment Entry": { "doctype": "Payment Order", - "validation": { - "docstatus": ["=", 1] - } - }, - "Payment Entry Reference": { - "doctype": "Payment Order Reference", "validation": { "docstatus": ["=", 1] }, - "postprocess": update_item - }, + } }, target_doc, set_missing_values) diff --git a/erpnext/accounts/doctype/payment_order_reference/payment_order_reference.json b/erpnext/accounts/doctype/payment_order_reference/payment_order_reference.json index db0b76135d..d94ba74c8c 100644 --- a/erpnext/accounts/doctype/payment_order_reference/payment_order_reference.json +++ b/erpnext/accounts/doctype/payment_order_reference/payment_order_reference.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2018-07-20 16:38:06.630813", "doctype": "DocType", "editable_grid": 1, @@ -10,7 +11,6 @@ "column_break_4", "supplier", "payment_request", - "payment_entry", "mode_of_payment", "bank_account_details", "bank_account", @@ -103,17 +103,12 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 - }, - { - "fieldname": "payment_entry", - "fieldtype": "Link", - "label": "Payment Entry", - "options": "Payment Entry", - "read_only": 1 } ], + "index_web_pages_for_search": 1, "istable": 1, - "modified": "2019-05-08 13:56:25.724557", + "links": [], + "modified": "2020-09-04 08:29:51.014390", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Order Reference", From e1889d0c1b213df492f56f37cac0361fef3ed486 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 4 Sep 2020 14:45:19 +0530 Subject: [PATCH 097/192] fix(payment entry): update payment order status --- erpnext/accounts/doctype/payment_order/payment_order.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_order/payment_order.py b/erpnext/accounts/doctype/payment_order/payment_order.py index e5880aa67a..8d29ae70ae 100644 --- a/erpnext/accounts/doctype/payment_order/payment_order.py +++ b/erpnext/accounts/doctype/payment_order/payment_order.py @@ -21,10 +21,15 @@ class PaymentOrder(Document): if cancel: status = 'Initiated' - ref_field = "status" if self.payment_order_type == "Payment Request" else "payment_order_status" + if self.payment_order_type == "Payment Request": + ref_field = "status" + ref_doc_field = frappe.scrub(self.payment_order_type) + else: + ref_field = "payment_order_status" + ref_doc_field = "reference_name" for d in self.references: - frappe.db.set_value(self.payment_order_type, d.get(frappe.scrub(self.payment_order_type)), ref_field, status) + frappe.db.set_value(self.payment_order_type, d.get(ref_doc_field), ref_field, status) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs From faebef21b747992f91b5afa68da60390e5b93644 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Fri, 4 Sep 2020 15:09:16 +0530 Subject: [PATCH 098/192] 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 30bcccdda6..a92d04ff8c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -513,7 +513,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 4a238a2093da2b802b933a46f3b71070ca0ead0e Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 4 Sep 2020 15:18:16 +0530 Subject: [PATCH 099/192] feat: don't allow guests --- erpnext/non_profit/doctype/member/member.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 797736a3db..44b975e9e9 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -158,7 +158,7 @@ def create_member_subscription_order(user_details): return subscription -@frappe.whitelist(allow_guest=True) +@frappe.whitelist() def register_member(fullname, email, rzpay_plan_id, subscription_id, pan=None, mobile=None): plan = get_membership_type(rzpay_plan_id) if not plan: From d9e48833926bca7ad0eb8700e5d4d46719eaabe7 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 4 Sep 2020 18:55:14 +0530 Subject: [PATCH 100/192] test: check payment order creation against payment entry --- .../bank_transaction/test_bank_transaction.py | 37 ++++++++++--------- .../payment_order/test_payment_order.py | 35 +++++++++++++++++- 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index 0fe57c3239..47d2de1816 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -91,28 +91,31 @@ class TestBankTransaction(unittest.TestCase): self.assertEqual(frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0) self.assertTrue(frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date") is not None) +def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"): + try: + frappe.get_doc({ + "doctype": "Bank", + "bank_name":bank_name, + }).insert() + except frappe.DuplicateEntryError: + pass + + try: + doc = frappe.get_doc({ + "doctype": "Bank Account", + "account_name":"Checking Account", + "bank": bank_name, + "account": account_name + }).insert() + except frappe.DuplicateEntryError: + pass + def add_transactions(): if frappe.flags.test_bank_transactions_created: return frappe.set_user("Administrator") - try: - frappe.get_doc({ - "doctype": "Bank", - "bank_name":"Citi Bank", - }).insert() - except frappe.DuplicateEntryError: - pass - - try: - frappe.get_doc({ - "doctype": "Bank Account", - "account_name":"Checking Account", - "bank": "Citi Bank", - "account": "_Test Bank - _TC" - }).insert() - except frappe.DuplicateEntryError: - pass + create_bank_account() doc = frappe.get_doc({ "doctype": "Bank Transaction", diff --git a/erpnext/accounts/doctype/payment_order/test_payment_order.py b/erpnext/accounts/doctype/payment_order/test_payment_order.py index 711c4cc1df..d871e3b71f 100644 --- a/erpnext/accounts/doctype/payment_order/test_payment_order.py +++ b/erpnext/accounts/doctype/payment_order/test_payment_order.py @@ -5,6 +5,39 @@ from __future__ import unicode_literals import frappe import unittest +from frappe.utils import getdate +from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry, InvalidPaymentEntry, make_payment_order +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice, make_purchase_invoice_against_cost_center class TestPaymentOrder(unittest.TestCase): - pass + def setUp(self): + create_bank_account() + + def test_payment_order_creation_against_payment_entry(self): + purchase_invoice = make_purchase_invoice() + payment_entry = get_payment_entry("Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC") + payment_entry.reference_no = "_Test_Payment_Order" + payment_entry.reference_date = getdate() + payment_entry.party_bank_account = "Checking Account - Citi Bank" + payment_entry.insert() + payment_entry.submit() + + doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry") + reference_doc = doc.get("references")[0] + self.assertEquals(reference_doc.reference_name, payment_entry.name) + self.assertEquals(reference_doc.reference_doctype, "Payment Entry") + self.assertEquals(reference_doc.supplier, "_Test Supplier") + self.assertEquals(reference_doc.amount, 250) + +def create_payment_order_against_payment_entry(ref_doc, order_type): + payment_order = frappe.get_doc(dict( + doctype="Payment Order", + company="_Test Company", + payment_order_type=order_type, + company_bank_account="Checking Account - Citi Bank" + )) + doc = make_payment_order(ref_doc.name, payment_order) + doc.save() + doc.submit() + return doc \ No newline at end of file From a76bf4d7af53dfe96274bc2c75a6c31b10ac8add Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 4 Sep 2020 19:08:08 +0530 Subject: [PATCH 101/192] fix: remove unused imports --- .../doctype/bank_transaction/test_bank_transaction.py | 2 +- erpnext/accounts/doctype/payment_order/test_payment_order.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index 47d2de1816..27546335c9 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -101,7 +101,7 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"): pass try: - doc = frappe.get_doc({ + frappe.get_doc({ "doctype": "Bank Account", "account_name":"Checking Account", "bank": bank_name, diff --git a/erpnext/accounts/doctype/payment_order/test_payment_order.py b/erpnext/accounts/doctype/payment_order/test_payment_order.py index d871e3b71f..1093a9b60a 100644 --- a/erpnext/accounts/doctype/payment_order/test_payment_order.py +++ b/erpnext/accounts/doctype/payment_order/test_payment_order.py @@ -7,8 +7,8 @@ import frappe import unittest from frappe.utils import getdate from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account -from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry, InvalidPaymentEntry, make_payment_order -from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice, make_purchase_invoice_against_cost_center +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry, make_payment_order +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice class TestPaymentOrder(unittest.TestCase): def setUp(self): From 7456ffca0f89ed608733887b26f7f68524d7d3f4 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 4 Sep 2020 19:59:01 +0530 Subject: [PATCH 102/192] fix: teardown created payment order --- .../accounts/doctype/payment_order/test_payment_order.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/accounts/doctype/payment_order/test_payment_order.py b/erpnext/accounts/doctype/payment_order/test_payment_order.py index 1093a9b60a..1c23e2a0ec 100644 --- a/erpnext/accounts/doctype/payment_order/test_payment_order.py +++ b/erpnext/accounts/doctype/payment_order/test_payment_order.py @@ -14,6 +14,12 @@ class TestPaymentOrder(unittest.TestCase): def setUp(self): create_bank_account() + def tearDown(self): + for bt in frappe.get_all("Payment Order"): + doc = frappe.get_doc("Payment Order", bt.name) + doc.cancel() + doc.delete() + def test_payment_order_creation_against_payment_entry(self): purchase_invoice = make_purchase_invoice() payment_entry = get_payment_entry("Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC") From 4d636fde3af438aee340bcc0c942f163a46550bb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 4 Sep 2020 22:23:30 +0530 Subject: [PATCH 103/192] fix: API changes in loan security unpledge utility --- erpnext/loan_management/doctype/loan/loan.js | 6 +- erpnext/loan_management/doctype/loan/loan.py | 56 +++++++++++++------ .../loan_management/doctype/loan/test_loan.py | 4 +- .../loan_security_unpledge.py | 21 +++++-- 4 files changed, 58 insertions(+), 29 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index ffef60b6b0..6696386950 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -119,12 +119,10 @@ frappe.ui.form.on('Loan', { create_loan_security_unpledge: function(frm) { frappe.call({ - method: "erpnext.loan_management.doctype.loan.loan.create_loan_security_unpledge", + method: "erpnext.loan_management.doctype.loan.loan.unpledge_security", args : { "loan": frm.doc.name, - "applicant_type": frm.doc.applicant_type, - "applicant": frm.doc.applicant, - "company": frm.doc.company + "as_dict": 1 }, callback: function(r) { if (r.message) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index e20b484fc0..e2e27dd45d 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -7,7 +7,7 @@ import frappe, math, json import erpnext from frappe import _ from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime - +from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.controllers.accounts_controller import AccountsController class Loan(AccountsController): @@ -223,30 +223,52 @@ def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as return repayment_entry @frappe.whitelist() -def create_loan_security_unpledge(loan, applicant_type, applicant, company, as_dict=1): - loan_security_pledge_details = frappe.db.sql(""" - SELECT p.loan_security, sum(p.qty) as qty - FROM `tabLoan Security Pledge` lsp , `tabPledge` p - WHERE p.parent = lsp.name AND lsp.loan = %s AND lsp.docstatus = 1 - GROUP BY p.loan_security - """,(loan), as_dict=1) +def unpledge_security(loan=None, loan_security_pledge=None, as_dict=0, save=0, submit=0, approve=0): + # if loan is passed it will be considered as full unpledge + if loan: + pledge_qty_map = get_pledged_security_qty(loan) + loan_doc = frappe.get_doc('Loan', loan) + unpledge_request = create_loan_security_unpledge(pledge_qty_map, loan_doc.name, loan_doc.company, + loan_doc.applicant_type, loan_doc.applicant) + # will unpledge qty based on loan security pledge + elif loan_security_pledge: + security_map = {} + pledge_doc = frappe.get_doc('Loan Security Pledge', loan_security_pledge) + for security in pledge_doc.securities: + security_map.setdefault(security.loan_security, security.qty) + unpledge_request = create_loan_security_unpledge(security_map, pledge_doc.loan, + pledge_doc.company, pledge_doc.applicant_type, pledge_doc.applicant) + + if approve: + unpledge_request.status = 'Approved' + + if save: + unpledge_request.save() + + if submit: + unpledge_request.submit() + + if as_dict: + return unpledge_request + else: + return unpledge_request + +def create_loan_security_unpledge(unpledge_map, loan, company, applicant_type, applicant): unpledge_request = frappe.new_doc("Loan Security Unpledge") unpledge_request.applicant_type = applicant_type unpledge_request.applicant = applicant unpledge_request.loan = loan unpledge_request.company = company - for loan_security in loan_security_pledge_details: - unpledge_request.append('securities', { - "loan_security": loan_security.loan_security, - "qty": loan_security.qty - }) + for security, qty in unpledge_map.items(): + if qty: + unpledge_request.append('securities', { + "loan_security": security, + "qty": qty + }) - if as_dict: - return unpledge_request.as_dict() - else: - return unpledge_request + return unpledge_request diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 2f6cd25a36..5faf80e625 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -14,7 +14,7 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_ process_loan_interest_accrual_for_term_loans) from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall import create_process_loan_security_shortfall -from erpnext.loan_management.doctype.loan.loan import create_loan_security_unpledge +from erpnext.loan_management.doctype.loan.loan import unpledge_security from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount @@ -307,7 +307,7 @@ class TestLoan(unittest.TestCase): loan.load_from_db() self.assertEquals(loan.status, "Loan Closure Requested") - unpledge_request = create_loan_security_unpledge(loan.name, loan.applicant_type, loan.applicant, loan.company, as_dict=0) + unpledge_request = unpledge_security(loan=loan.name, save=1) unpledge_request.submit() unpledge_request.status = 'Approved' unpledge_request.save() diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index f6b28dae75..4cd3dad369 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -17,10 +17,12 @@ class LoanSecurityUnpledge(Document): self.validate_unpledge_qty() def on_cancel(self): - self.update_loan_security_pledge(cancel=1) self.update_loan_status(cancel=1) self.db_set('status', 'Requested') + def on_submit(self): + self.approve() + def validate_duplicate_securities(self): security_list = [] for d in self.securities: @@ -32,6 +34,7 @@ class LoanSecurityUnpledge(Document): def validate_unpledge_qty(self): pledge_qty_map = get_pledged_security_qty(self.loan) + print(pledge_qty_map, "$$$$$$$$") ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type", fields=["name", "loan_to_value_ratio"], as_list=1)) @@ -50,8 +53,7 @@ class LoanSecurityUnpledge(Document): security_value = 0 for security in self.securities: - pledged_qty = pledge_qty_map.get(security.loan_security) - + pledged_qty = pledge_qty_map.get(security.loan_security, 0) if security.qty > pledged_qty: frappe.throw(_("""Row {0}: {1} {2} of {3} is pledged against Loan {4}. You are trying to unpledge more""").format(security.idx, pledged_qty, security.uom, @@ -60,16 +62,23 @@ class LoanSecurityUnpledge(Document): qty_after_unpledge = pledged_qty - security.qty ltv_ratio = ltv_ratio_map.get(security.loan_security_type) - security_value += qty_after_unpledge * loan_security_price_map.get(security.loan_security) + current_price = loan_security_price_map.get(security.loan_security) + if not current_price: + frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(security.loan_security))) + + security_value += qty_after_unpledge * current_price if not security_value and flt(pending_principal_amount, 2) > 0: frappe.throw("Cannot Unpledge, loan to value ratio is breaching") - if security_value and (pending_principal_amount/security_value) * 100 > ltv_ratio: + if security_value and flt(pending_principal_amount/security_value) * 100 > ltv_ratio: frappe.throw("Cannot Unpledge, loan to value ratio is breaching") def on_update_after_submit(self): - if self.status == "Approved": + self.approve() + + def approve(self): + if self.status == "Approved" and not self.unpledge_time: self.update_loan_status() self.db_set('unpledge_time', get_datetime()) From e4c38e5fe5139f5f7a43fd324a89444afdb6e8b5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 4 Sep 2020 22:30:13 +0530 Subject: [PATCH 104/192] fix: Remove print statement --- .../doctype/loan_security_unpledge/loan_security_unpledge.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index 4cd3dad369..a87d832b1c 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -34,7 +34,6 @@ class LoanSecurityUnpledge(Document): def validate_unpledge_qty(self): pledge_qty_map = get_pledged_security_qty(self.loan) - print(pledge_qty_map, "$$$$$$$$") ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type", fields=["name", "loan_to_value_ratio"], as_list=1)) From 1b4ed454d296145b4be458ba32a9ec180c1c2024 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 4 Sep 2020 22:42:41 +0530 Subject: [PATCH 105/192] fix: Add reference no and descripiton field in Loan Security Pledge and Unpledge --- .../loan_security_pledge.json | 29 ++++++++++++++++++- .../loan_security_unpledge.json | 29 ++++++++++++++++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json index 4572e99299..7dd5725e2e 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json @@ -21,6 +21,10 @@ "total_security_value", "column_break_11", "maximum_loan_value", + "more_information_section", + "reference_no", + "column_break_18", + "description", "amended_from" ], "fields": [ @@ -129,11 +133,34 @@ "label": "Applicant Type", "options": "Employee\nMember\nCustomer", "reqd": 1 + }, + { + "collapsible": 1, + "fieldname": "more_information_section", + "fieldtype": "Section Break", + "label": "More Information" + }, + { + "allow_on_submit": 1, + "fieldname": "reference_no", + "fieldtype": "Data", + "label": "Reference No" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "description", + "fieldtype": "Text", + "label": "Description" } ], + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-07-02 23:38:24.002382", + "modified": "2020-09-04 22:38:19.894488", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Pledge", diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json index aece46ffda..2e2b2518d2 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json @@ -16,6 +16,10 @@ "status", "loan_security_details_section", "securities", + "more_information_section", + "reference_no", + "column_break_13", + "description", "amended_from" ], "fields": [ @@ -95,11 +99,34 @@ "label": "Applicant Type", "options": "Employee\nMember\nCustomer", "reqd": 1 + }, + { + "collapsible": 1, + "fieldname": "more_information_section", + "fieldtype": "Section Break", + "label": "More Information" + }, + { + "allow_on_submit": 1, + "fieldname": "reference_no", + "fieldtype": "Data", + "label": "Reference No" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "description", + "fieldtype": "Text", + "label": "Description" } ], + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-05-05 07:23:18.440058", + "modified": "2020-09-04 22:39:57.756146", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Unpledge", From a59f8640d15006481ab1864c392ffe51877b7780 Mon Sep 17 00:00:00 2001 From: kasgel Date: Sat, 5 Sep 2020 12:15:49 +0200 Subject: [PATCH 106/192] fix: pass stock_qty to get_pricing_rule_for_item get_pricing_rule_for_item uses the "stock_qty" and "qty" arguments to filter pricing rules. Not passing stock_qty prevents for example the shopping cart from filtering pricing rules appropriately based on Quantity. --- erpnext/utilities/product.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py index c23c1f7096..66d6cd3888 100644 --- a/erpnext/utilities/product.py +++ b/erpnext/utilities/product.py @@ -82,6 +82,7 @@ def get_price(item_code, price_list, customer_group, company, qty=1): pricing_rule = get_pricing_rule_for_item(frappe._dict({ "item_code": item_code, "qty": qty, + "stock_qty": qty, "transaction_type": "selling", "price_list": price_list, "customer_group": customer_group, From 5158d0fbbaf5eb61b3fa05a55bb815308f936bfa Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 5 Sep 2020 21:16:57 +0530 Subject: [PATCH 107/192] fix: Repayment Method not visible correctly --- erpnext/loan_management/doctype/loan/loan.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index 6696386950..9b4c21770e 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -73,8 +73,8 @@ frappe.ui.form.on('Loan', { loan_type: function(frm) { frm.toggle_reqd("repayment_method", frm.doc.is_term_loan); - frm.toggle_display("repayment_method", 1 - frm.doc.is_term_loan); - frm.toggle_display("repayment_periods", s1 - frm.doc.is_term_loan); + frm.toggle_display("repayment_method", frm.doc.is_term_loan); + frm.toggle_display("repayment_periods", frm.doc.is_term_loan); }, From fa98e81f0b4041e9496cb7f90c852203e7d39355 Mon Sep 17 00:00:00 2001 From: barry86m <65860880+barry86m@users.noreply.github.com> Date: Mon, 7 Sep 2020 01:36:15 +0100 Subject: [PATCH 108/192] fix: sql error on saving sales invoice without a sales order --- erpnext/controllers/selling_controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 17f3ae53e7..ad06f97b7d 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -81,6 +81,7 @@ class SellingController(StockController): party_details = _get_party_details(customer, ignore_permissions=self.flags.ignore_permissions, doctype=self.doctype, company=self.company, + posting_date=self.posting_date if hasattr(self, 'posting_date') else None, fetch_payment_terms_template=fetch_payment_terms_template, party_address=self.customer_address, shipping_address=self.shipping_address_name) if not self.meta.get_field("sales_team"): From 209f7c76c926ce4efc8ea84df8249347cedbf9fa Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Mon, 7 Sep 2020 11:50:24 +0530 Subject: [PATCH 109/192] feat: added transaction_type in leave ledger enter (#23258) --- .../leave_ledger_entry/leave_ledger_entry.json | 6 +++--- .../leave_ledger_entry/leave_ledger_entry_list.js | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry_list.js diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json index a5ac3f3d47..4abba5f2d4 100644 --- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json @@ -1,5 +1,4 @@ { - "actions": [], "creation": "2019-05-09 15:47:39.760406", "doctype": "DocType", "engine": "InnoDB", @@ -54,6 +53,7 @@ { "fieldname": "transaction_type", "fieldtype": "Link", + "in_standard_filter": 1, "label": "Transaction Type", "options": "DocType" }, @@ -109,9 +109,9 @@ } ], "in_create": 1, + "index_web_pages_for_search": 1, "is_submittable": 1, - "links": [], - "modified": "2020-02-27 14:40:10.502605", + "modified": "2020-09-04 12:16:36.569066", "modified_by": "Administrator", "module": "HR", "name": "Leave Ledger Entry", diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry_list.js b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry_list.js new file mode 100644 index 0000000000..889325bf2b --- /dev/null +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry_list.js @@ -0,0 +1,13 @@ +frappe.listview_settings['Leave Ledger Entry'] = { + onload: function(listview) { + if(listview.page.fields_dict.transaction_type) { + listview.page.fields_dict.transaction_type.get_query = function() { + return { + "filters": { + "name": ["in", ["Leave Allocation", "Leave Application", "Leave Encashment"]], + } + }; + }; + } + } +}; From 06130ee56bf6b5e6ba2b09b0279d7c7ad6776658 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Mon, 7 Sep 2020 11:53:50 +0530 Subject: [PATCH 110/192] fix: leave ledger patch (#23182) --- erpnext/patches.txt | 2 +- .../v12_0/generate_leave_ledger_entries.py | 27 ++++++------------- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 771babef6a..aa7996e3e1 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -632,7 +632,7 @@ execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_source') execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart') execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_field') erpnext.patches.v12_0.remove_bank_remittance_custom_fields -erpnext.patches.v12_0.generate_leave_ledger_entries +erpnext.patches.v12_0.generate_leave_ledger_entries #27-08-2020 execute:frappe.delete_doc_if_exists("Report", "Loan Repayment") erpnext.patches.v12_0.move_credit_limit_to_customer_credit_limit erpnext.patches.v12_0.add_variant_of_in_item_attribute_table diff --git a/erpnext/patches/v12_0/generate_leave_ledger_entries.py b/erpnext/patches/v12_0/generate_leave_ledger_entries.py index c5bec19fed..342c12996d 100644 --- a/erpnext/patches/v12_0/generate_leave_ledger_entries.py +++ b/erpnext/patches/v12_0/generate_leave_ledger_entries.py @@ -36,8 +36,7 @@ def generate_allocation_ledger_entries(): for allocation in allocation_list: if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name}): - allocation.update(dict(doctype="Leave Allocation")) - allocation_obj = frappe.get_doc(allocation) + allocation_obj = frappe.get_doc("Leave Allocation", allocation) allocation_obj.create_leave_ledger_entry() def generate_application_leave_ledger_entries(): @@ -46,8 +45,7 @@ def generate_application_leave_ledger_entries(): for application in leave_applications: if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Application', 'transaction_name': application.name}): - application.update(dict(doctype="Leave Application")) - frappe.get_doc(application).create_leave_ledger_entry() + frappe.get_doc("Leave Application", application.name).create_leave_ledger_entry() def generate_encashment_leave_ledger_entries(): ''' fix ledger entries for missing leave encashment transaction ''' @@ -55,8 +53,7 @@ def generate_encashment_leave_ledger_entries(): for encashment in leave_encashments: if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Encashment', 'transaction_name': encashment.name}): - encashment.update(dict(doctype="Leave Encashment")) - frappe.get_doc(encashment).create_leave_ledger_entry() + frappe.get_doc("Leave Enchashment", encashment).create_leave_ledger_entry() def generate_expiry_allocation_ledger_entries(): ''' fix ledger entries for missing leave allocation transaction ''' @@ -65,24 +62,16 @@ def generate_expiry_allocation_ledger_entries(): for allocation in allocation_list: if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name, 'is_expired': 1}): - allocation.update(dict(doctype="Leave Allocation")) - allocation_obj = frappe.get_doc(allocation) + allocation_obj = frappe.get_doc("Leave Allocation", allocation) if allocation_obj.to_date <= getdate(today()): expire_allocation(allocation_obj) def get_allocation_records(): - return frappe.get_all("Leave Allocation", filters={ - "docstatus": 1 - }, fields=['name', 'employee', 'leave_type', 'new_leaves_allocated', - 'unused_leaves', 'from_date', 'to_date', 'carry_forward' - ], order_by='to_date ASC') + return frappe.get_all("Leave Allocation", filters={"docstatus": 1}, + fields=['name'], order_by='to_date ASC') def get_leaves_application_records(): - return frappe.get_all("Leave Application", filters={ - "docstatus": 1 - }, fields=['name', 'employee', 'leave_type', 'total_leave_days', 'from_date', 'to_date']) + return frappe.get_all("Leave Application", filters={"docstatus": 1}, fields=['name']) def get_leave_encashment_records(): - return frappe.get_all("Leave Encashment", filters={ - "docstatus": 1 - }, fields=['name', 'employee', 'leave_type', 'encashable_days', 'encashment_date']) + return frappe.get_all("Leave Encashment", filters={"docstatus": 1}, fields=['name']) From 9ce38de439ebd2a240539542a8de40f8ae2d2417 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 7 Sep 2020 13:27:17 +0530 Subject: [PATCH 111/192] chore: Key Validation and restructure - Added API Key validation in Video Settings - Moved Statistics section above description in Video - Moved id retrieving from URL to py side - Removed js side calls, added section toggling in js - Restructured Code --- erpnext/utilities/doctype/video/video.js | 28 ++------ erpnext/utilities/doctype/video/video.json | 10 +-- erpnext/utilities/doctype/video/video.py | 71 +++++++++---------- .../doctype/video_settings/video_settings.py | 16 ++++- 4 files changed, 61 insertions(+), 64 deletions(-) diff --git a/erpnext/utilities/doctype/video/video.js b/erpnext/utilities/doctype/video/video.js index c2994ecc96..9cb5a155ad 100644 --- a/erpnext/utilities/doctype/video/video.js +++ b/erpnext/utilities/doctype/video/video.js @@ -3,29 +3,15 @@ frappe.ui.form.on('Video', { refresh: function (frm) { - if (frm.doc.provider === "YouTube") { - frappe.db.get_single_value("Video Settings", "enable_youtube_tracking").then(value => { - if (value) { - frm.events.get_video_stats(frm); - } else { - frm.set_df_property('youtube_tracking_section', 'hidden', true); - } - }); - } - + frm.events.toggle_youtube_statistics_section(frm); frm.add_custom_button("Watch Video", () => frappe.help.show_video(frm.doc.url, frm.doc.title)); }, - get_video_stats: (frm) => { - const expression = '(?:youtube.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu.be/)([^"&?\\s]{11})'; - var youtube_id = frm.doc.url.match(expression)[1]; - - frappe.call({ - method: "erpnext.utilities.doctype.video.video.get_video_stats", - args: { - docname: frm.doc.name, - youtube_id: youtube_id - } - }); + toggle_youtube_statistics_section: (frm) => { + if (frm.doc.provider === "YouTube") { + frappe.db.get_single_value("Video Settings", "enable_youtube_tracking").then( val => { + frm.toggle_display("youtube_tracking_section", val); + }); + } } }); diff --git a/erpnext/utilities/doctype/video/video.json b/erpnext/utilities/doctype/video/video.json index a6c0f3f82a..11df56c77d 100644 --- a/erpnext/utilities/doctype/video/video.json +++ b/erpnext/utilities/doctype/video/video.json @@ -14,15 +14,15 @@ "column_break_4", "publish_date", "duration", - "section_break_7", - "description", - "image", "youtube_tracking_section", "like_count", "view_count", "col_break", "dislike_count", - "comment_count" + "comment_count", + "section_break_7", + "description", + "image" ], "fields": [ { @@ -122,7 +122,7 @@ ], "image_field": "image", "links": [], - "modified": "2020-08-02 04:26:16.345569", + "modified": "2020-09-04 12:59:28.283622", "modified_by": "Administrator", "module": "Utilities", "name": "Video", diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py index a2a4a7b745..2299f95f76 100644 --- a/erpnext/utilities/doctype/video/video.py +++ b/erpnext/utilities/doctype/video/video.py @@ -5,49 +5,48 @@ from __future__ import unicode_literals import frappe import json +import re from frappe.model.document import Document +from frappe import _ from six import string_types from pyyoutube import Api class Video(Document): - pass + def validate(self): + self.set_youtube_statistics() + + def set_youtube_statistics(self): + tracking_enabled = frappe.db.get_single_value("Video Settings", "enable_youtube_tracking") + if self.provider == "YouTube" and not tracking_enabled: + return + + api_key = frappe.db.get_single_value("Video Settings", "api_key") + youtube_id = get_id_from_url(self.url) + api = Api(api_key=api_key) + + try: + video = api.get_video_by_id(video_id=youtube_id) + video_stats = video.items[0].to_dict().get('statistics') + + self.like_count = video_stats.get('likeCount') + self.view_count = video_stats.get('viewCount') + self.dislike_count = video_stats.get('dislikeCount') + self.comment_count = video_stats.get('commentCount') + + except Exception: + title = "Failed to Update YouTube Statistics for Video: {0}".format(self.name) + frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) @frappe.whitelist() -def get_video_stats(docname, youtube_id, update=True): - '''Returns/Sets video statistics - - :param docname: Name of Video - :param youtube_id: Unique ID from URL - :param update: Updates db stats value if True, else returns statistics +def get_id_from_url(url): ''' - if isinstance(update, string_types): - update = json.loads(update) + Returns video id from url - api_key = frappe.db.get_single_value("Video Settings", "api_key") - api = Api(api_key=api_key) + :param youtube url: String URL + ''' + if not isinstance(url, string_types): + frappe.throw(_("URL can only be a string"), title=_("Invalid URL")) - try: - video = api.get_video_by_id(video_id=youtube_id) - video_stats = video.items[0].to_dict().get('statistics') - stats = { - 'like_count' : video_stats.get('likeCount'), - 'view_count' : video_stats.get('viewCount'), - 'dislike_count' : video_stats.get('dislikeCount'), - 'comment_count' : video_stats.get('commentCount') - } - - if not update: - return stats - - frappe.db.sql(""" - UPDATE `tabVideo` - SET - like_count = %(like_count)s, - view_count = %(view_count)s, - dislike_count = %(dislike_count)s, - comment_count = %(comment_count)s - WHERE name = {0}""".format(frappe.db.escape(docname)), stats) #nosec - frappe.db.commit() - except: - message = "Please make sure you are connected to the Internet" - frappe.log_error(message + "\n\n" + frappe.get_traceback(), "Failed to Update YouTube Statistics for Video: {0}".format(docname)) \ No newline at end of file + pattern = re.compile(r'[a-z\:\//\.]+(youtube|youtu)\.(com|be)/(watch\?v=|embed/|.+\?v=)?([^"&?\s]{11})?') + id = pattern.match(url) + return id.groups()[-1] \ No newline at end of file diff --git a/erpnext/utilities/doctype/video_settings/video_settings.py b/erpnext/utilities/doctype/video_settings/video_settings.py index 7008066909..36fb54f015 100644 --- a/erpnext/utilities/doctype/video_settings/video_settings.py +++ b/erpnext/utilities/doctype/video_settings/video_settings.py @@ -3,8 +3,20 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe +from frappe import _ from frappe.model.document import Document +from apiclient.discovery import build class VideoSettings(Document): - pass + def validate(self): + self.validate_youtube_api_key() + + def validate_youtube_api_key(self): + if self.enable_youtube_tracking and self.api_key: + try: + build("youtube", "v3", developerKey=self.api_key) + except Exception: + title = _("Failed to Authenticate the API key.") + frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) + frappe.throw(title + " Please check the error logs.", title=_("Invalid Credentials")) \ No newline at end of file From da6074a84c029b73f1508de31689689956f495dc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 7 Sep 2020 14:55:04 +0530 Subject: [PATCH 112/192] fix: Pending loan interest accrual on loan closure --- .../loan_management/doctype/loan/test_loan.py | 5 ++-- .../loan_interest_accrual.py | 3 ++- .../doctype/loan_repayment/loan_repayment.py | 26 +++++++++++++++++-- .../process_loan_interest_accrual.py | 4 +++ 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 5faf80e625..f225409f62 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -199,10 +199,9 @@ class TestLoan(unittest.TestCase): "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) repayment_entry.submit() - amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', - 'paid_principal_amount']) + amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)']) - self.assertEquals(flt(amounts[0], 2),flt(accrued_interest_amount, 2)) + self.assertEquals(flt(amount, 2),flt(accrued_interest_amount, 2)) self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) loan.load_from_db() diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index 1d3fa71068..2d959bf3be 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -213,7 +213,8 @@ def get_last_accural_date_in_current_month(loan): WHERE loan = %s""", (loan.name)) if last_posting_date[0][0]: - return last_posting_date[0][0] + # interest for last interest accrual date is already booked, so add 1 day + return add_days(last_posting_date[0][0], 1) else: return loan.disbursement_date diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 7d83e32213..a970b4eb34 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -13,6 +13,7 @@ from frappe.utils import date_diff, add_days, getdate, add_months, get_first_day from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.general_ledger import make_gl_entries from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import update_shortfall_status +from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans class LoanRepayment(AccountsController): @@ -22,6 +23,9 @@ class LoanRepayment(AccountsController): self.validate_amount() self.allocate_amounts(amounts['pending_accrual_entries']) + def before_submit(self): + self.book_unaccrued_interest() + def on_submit(self): self.update_paid_amount() self.make_gl_entries() @@ -72,6 +76,26 @@ class LoanRepayment(AccountsController): msg = _("Amount of {0} is required for Loan closure").format(self.payable_amount) frappe.throw(msg) + def book_unaccrued_interest(self): + if self.payment_type == 'Loan Closure': + total_interest_paid = 0 + for payment in self.repayment_details: + total_interest_paid += payment.paid_interest_amount + + if total_interest_paid < self.interest_payable: + if not self.is_term_loan: + process = process_loan_interest_accrual_for_demand_loans(posting_date=self.posting_date, + loan=self.against_loan) + + lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual': + process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1) + + self.append('repayment_details', { + 'loan_interest_accrual': lia.name, + 'paid_interest_amount': lia.interest_amount, + 'paid_principal_amount': lia.payable_principal_amount + }) + def update_paid_amount(self): precision = cint(frappe.db.get_default("currency_precision")) or 2 @@ -148,8 +172,6 @@ class LoanRepayment(AccountsController): if self.payment_type == 'Loan Closure' and total_interest_paid < self.interest_payable: unaccrued_interest = self.interest_payable - total_interest_paid interest_paid -= unaccrued_interest - if self.repayment_details: - self.repayment_details[-1].paid_interest_amount += unaccrued_interest if interest_paid: self.principal_amount_paid += interest_paid diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py index cd3cf7ec96..0fa96860d0 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py @@ -36,6 +36,8 @@ def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type= loan_process.submit() + return loan_process.name + def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=None, loan=None): if not term_loan_accrual_pending(posting_date or nowdate()): @@ -49,6 +51,8 @@ def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=No loan_process.submit() + return loan_process.name + def term_loan_accrual_pending(date): pending_accrual = frappe.db.get_value('Repayment Schedule', { 'payment_date': ('<=', date), From afefa878b04781f7d6b005e2110824bebce9ec28 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 7 Sep 2020 14:57:44 +0530 Subject: [PATCH 113/192] fix: Pending loan interest accrual on loan closure --- .../doctype/loan_repayment/loan_repayment.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index a970b4eb34..47fb885f8a 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -87,14 +87,14 @@ class LoanRepayment(AccountsController): process = process_loan_interest_accrual_for_demand_loans(posting_date=self.posting_date, loan=self.against_loan) - lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual': - process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1) + lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual': + process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1) - self.append('repayment_details', { - 'loan_interest_accrual': lia.name, - 'paid_interest_amount': lia.interest_amount, - 'paid_principal_amount': lia.payable_principal_amount - }) + self.append('repayment_details', { + 'loan_interest_accrual': lia.name, + 'paid_interest_amount': lia.interest_amount, + 'paid_principal_amount': lia.payable_principal_amount + }) def update_paid_amount(self): precision = cint(frappe.db.get_default("currency_precision")) or 2 From 74ab1084b31e8c38129aeb61b6820a01386f3a76 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 7 Sep 2020 17:55:10 +0530 Subject: [PATCH 114/192] fix: Apply TDS on Purchase Invoice creation from Purchase Order and Purchase Receipt --- .../accounts/doctype/purchase_invoice/purchase_invoice.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 4f6be59c65..b5c1cd7e1e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -25,6 +25,12 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ this.frm.set_df_property("credit_to", "print_hide", 0); } } + + // Trigger supplier event on load if supplier is available + // The reason for this is PI can be created from PR or PO and supplier is pre populated + if (this.frm.doc.supplier) { + this.frm.trigger('supplier'); + } }, refresh: function(doc) { From 06ee0ea00bab3793ef7fcb48348ba4a3ef501c0b Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 7 Sep 2020 18:49:06 +0530 Subject: [PATCH 115/192] feat: Added Scheduler Job to auto update statistics - Added frequency in Video Settings - Cron job to check half hourly job - Hourly job to run job based on frequency - Patch to set youtube id in video for ease in computing --- erpnext/hooks.py | 6 + erpnext/patches.txt | 1 + erpnext/patches/v13_0/set_youtube_video_id.py | 8 ++ erpnext/utilities/doctype/video/video.json | 10 +- erpnext/utilities/doctype/video/video.py | 124 ++++++++++++++++-- .../video_settings/video_settings.json | 15 ++- 6 files changed, 150 insertions(+), 14 deletions(-) create mode 100644 erpnext/patches/v13_0/set_youtube_video_id.py diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 95a836fe65..db1fd2f3fc 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -282,6 +282,11 @@ auto_cancel_exempted_doctypes= [ ] scheduler_events = { + "cron": { + "0/30 * * * *": [ + "erpnext.utilities.doctype.video.video.update_youtube_data_half_hourly", + ] + }, "all": [ "erpnext.projects.doctype.project.project.project_status_update_reminder", "erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder", @@ -297,6 +302,7 @@ scheduler_events = { "erpnext.projects.doctype.project.project.collect_project_status", "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts", "erpnext.support.doctype.issue.issue.set_service_level_agreement_variance", + "erpnext.utilities.doctype.video.video.update_youtube_data" ], "daily": [ "erpnext.stock.reorder_item.reorder_item", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 3bd416952f..2a52ff67e5 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -718,3 +718,4 @@ erpnext.patches.v13_0.delete_report_requested_items_to_order erpnext.patches.v12_0.update_item_tax_template_company erpnext.patches.v13_0.move_branch_code_to_bank_account erpnext.patches.v13_0.healthcare_lab_module_rename_doctypes +erpnext.patches.v13_0.set_youtube_video_id diff --git a/erpnext/patches/v13_0/set_youtube_video_id.py b/erpnext/patches/v13_0/set_youtube_video_id.py new file mode 100644 index 0000000000..8e5dd306a9 --- /dev/null +++ b/erpnext/patches/v13_0/set_youtube_video_id.py @@ -0,0 +1,8 @@ +from __future__ import unicode_literals +import frappe +from erpnext.utilities.doctype.video.video import get_id_from_url + +def execute(): + for video in frappe.get_all("Video", fields=["name", "url", "youtube_video_id"]): + if video.url and not video.youtube_video_id: + frappe.db.set_value("Video", video.name, "youtube_video_id", get_id_from_url(video.url)) \ No newline at end of file diff --git a/erpnext/utilities/doctype/video/video.json b/erpnext/utilities/doctype/video/video.json index 11df56c77d..2a82db2514 100644 --- a/erpnext/utilities/doctype/video/video.json +++ b/erpnext/utilities/doctype/video/video.json @@ -11,6 +11,7 @@ "title", "provider", "url", + "youtube_video_id", "column_break_4", "publish_date", "duration", @@ -118,11 +119,18 @@ "fieldname": "youtube_tracking_section", "fieldtype": "Section Break", "label": "Youtube Statistics" + }, + { + "fieldname": "youtube_video_id", + "fieldtype": "Data", + "hidden": 1, + "label": "Youtube ID", + "read_only": 1 } ], "image_field": "image", "links": [], - "modified": "2020-09-04 12:59:28.283622", + "modified": "2020-09-07 17:02:20.185794", "modified_by": "Administrator", "module": "Utilities", "name": "Video", diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py index 2299f95f76..d8653f6446 100644 --- a/erpnext/utilities/doctype/video/video.py +++ b/erpnext/utilities/doctype/video/video.py @@ -4,8 +4,8 @@ from __future__ import unicode_literals import frappe -import json import re +import pytz from frappe.model.document import Document from frappe import _ from six import string_types @@ -13,20 +13,32 @@ from pyyoutube import Api class Video(Document): def validate(self): + self.set_video_id() self.set_youtube_statistics() - def set_youtube_statistics(self): - tracking_enabled = frappe.db.get_single_value("Video Settings", "enable_youtube_tracking") - if self.provider == "YouTube" and not tracking_enabled: + def set_video_id(self): + if self.provider == "YouTube" and self.url and not self.get("youtube_video_id"): + self.youtube_video_id = get_id_from_url(self.url) + + @classmethod + def set_youtube_statistics(self, video_ids=None, update=True): + if self.provider == "YouTube" and not is_tracking_enabled(): return api_key = frappe.db.get_single_value("Video Settings", "api_key") - youtube_id = get_id_from_url(self.url) api = Api(api_key=api_key) try: - video = api.get_video_by_id(video_id=youtube_id) - video_stats = video.items[0].to_dict().get('statistics') + video_id = video_ids or self.youtube_video_id + video = api.get_video_by_id(video_id=video_id) + + if video_ids: + video_stats = video.items + else: + video_stats = video.items[0].to_dict().get('statistics') + + if not update: + return video_stats self.like_count = video_stats.get('likeCount') self.view_count = video_stats.get('viewCount') @@ -37,16 +49,106 @@ class Video(Document): title = "Failed to Update YouTube Statistics for Video: {0}".format(self.name) frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) +def is_tracking_enabled(): + return frappe.db.get_single_value("Video Settings", "enable_youtube_tracking") + +def get_frequency(value): + if not value: + return None + + # Return frequency in hours + if value != "Daily": + return frappe.utils.cint(value[:2].strip()) + else: + # 24 hours for Daily + return 24 + + +def update_youtube_data_half_hourly(): + # Called every 30 mins via hooks + frequency = get_frequency(frappe.db.get_single_value("Video Settings", "frequency")) + if not is_tracking_enabled() or not frequency: + return + + if frequency == 30: + batch_update_data() + + +def update_youtube_data(): + # Called every hour via hooks + frequency = get_frequency(frappe.db.get_single_value("Video Settings", "frequency")) + + # if frequency is 30 mins dont proceed, as its handled in another method + if not is_tracking_enabled() or not frequency or frequency == 30: + return + + time = datetime.now() + timezone = pytz.timezone(frappe.utils.get_time_zone()) + site_time = time.astimezone(timezone) + + if site_time.hour % frequency == 0: + batch_update_youtube_data() + +def get_formatted_ids(video_list): + # format ids to comma separated string for bulk request + ids = [] + for video in video_list: + ids.append(video.youtube_video_id) + + return ','.join(ids) + @frappe.whitelist() def get_id_from_url(url): - ''' + """ Returns video id from url - :param youtube url: String URL - ''' + """ if not isinstance(url, string_types): frappe.throw(_("URL can only be a string"), title=_("Invalid URL")) pattern = re.compile(r'[a-z\:\//\.]+(youtube|youtu)\.(com|be)/(watch\?v=|embed/|.+\?v=)?([^"&?\s]{11})?') id = pattern.match(url) - return id.groups()[-1] \ No newline at end of file + return id.groups()[-1] + + +@frappe.whitelist() +def batch_update_youtube_data(): + def prepare_and_set_data(video_list): + video_ids = get_formatted_ids(video_list) + Video.provider = "YouTube" + stats = Video.set_youtube_statistics(video_ids=video_ids, update=False) + set_youtube_data(stats) + + def set_youtube_data(entries): + for entry in entries: + video_stats = entry.to_dict().get('statistics') + video_id = entry.to_dict().get('id') + stats = { + 'like_count' : video_stats.get('likeCount'), + 'view_count' : video_stats.get('viewCount'), + 'dislike_count' : video_stats.get('dislikeCount'), + 'comment_count' : video_stats.get('commentCount') + } + + frappe.db.sql(""" + UPDATE `tabVideo` + SET + like_count = %(like_count)s, + view_count = %(view_count)s, + dislike_count = %(dislike_count)s, + comment_count = %(comment_count)s + WHERE youtube_video_id = '{0}'""".format(video_id), stats) + + frappe.log_error("yooooooooo") + + video_list = frappe.get_all("Video", fields=["youtube_video_id"]) + if len(video_list) > 50: + # Update in batches of 50 + start, end = 0, 50 + while start < len(video_list): + batch = video_list[start:end] + prepare_and_set_data(batch) + start += 50 + end += 50 + else: + prepare_and_set_data(video_list) \ No newline at end of file diff --git a/erpnext/utilities/doctype/video_settings/video_settings.json b/erpnext/utilities/doctype/video_settings/video_settings.json index 0a0efd9a53..fb3274decd 100644 --- a/erpnext/utilities/doctype/video_settings/video_settings.json +++ b/erpnext/utilities/doctype/video_settings/video_settings.json @@ -6,7 +6,8 @@ "engine": "InnoDB", "field_order": [ "enable_youtube_tracking", - "api_key" + "api_key", + "frequency" ], "fields": [ { @@ -21,11 +22,21 @@ "fieldtype": "Data", "label": "API Key", "mandatory_depends_on": "eval:doc.enable_youtube_tracking" + }, + { + "default": "1 hr", + "depends_on": "eval:doc.enable_youtube_tracking", + "fieldname": "frequency", + "fieldtype": "Select", + "label": "Frequency", + "mandatory_depends_on": "eval:doc.enable_youtube_tracking", + "options": "30 mins\n1 hr\n6 hrs\nDaily" } ], + "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-08-02 03:56:49.673870", + "modified": "2020-09-07 16:09:00.360668", "modified_by": "Administrator", "module": "Utilities", "name": "Video Settings", From f18dd05c00c92033cd0f1e1db49f6e6b60d22e5f Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 7 Sep 2020 18:59:57 +0530 Subject: [PATCH 116/192] fix: Missing import and typo --- erpnext/utilities/doctype/video/video.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py index d8653f6446..3b6bd7e0df 100644 --- a/erpnext/utilities/doctype/video/video.py +++ b/erpnext/utilities/doctype/video/video.py @@ -8,6 +8,7 @@ import re import pytz from frappe.model.document import Document from frappe import _ +from datetime import datetime from six import string_types from pyyoutube import Api @@ -71,7 +72,7 @@ def update_youtube_data_half_hourly(): return if frequency == 30: - batch_update_data() + batch_update_youtube_data() def update_youtube_data(): From 06f89a1b94e98aeaa76c4745914a676d8654bb16 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 7 Sep 2020 19:46:53 +0530 Subject: [PATCH 117/192] fix: Set student email id mandatory only if skip user creation is disabled --- erpnext/education/doctype/student/student.js | 20 +- .../education/doctype/student/student.json | 3 +- .../student_applicant/student_applicant.js | 13 +- .../student_applicant/student_applicant.json | 1437 +++-------------- 4 files changed, 272 insertions(+), 1201 deletions(-) diff --git a/erpnext/education/doctype/student/student.js b/erpnext/education/doctype/student/student.js index 1936dcbd3e..9272d35543 100644 --- a/erpnext/education/doctype/student/student.js +++ b/erpnext/education/doctype/student/student.js @@ -3,15 +3,15 @@ frappe.ui.form.on('Student', { setup: function(frm) { - frm.add_fetch("guardian", "guardian_name", "guardian_name"); - frm.add_fetch("student", "title", "full_name"); - frm.add_fetch("student", "gender", "gender"); - frm.add_fetch("student", "date_of_birth", "date_of_birth"); + frm.add_fetch('guardian', 'guardian_name', 'guardian_name'); + frm.add_fetch('student', 'title', 'full_name'); + frm.add_fetch('student', 'gender', 'gender'); + frm.add_fetch('student', 'date_of_birth', 'date_of_birth'); - frm.set_query("student", "siblings", function(doc, cdt, cdn) { + frm.set_query('student', 'siblings', function(doc, cdt, cdn) { return { - "filters": { - "name": ["!=", doc.name] + 'filters': { + 'name': ['!=', doc.name] } }; }) @@ -25,6 +25,12 @@ frappe.ui.form.on('Student', { {party_type:'Student', party:frm.doc.name}); }); } + + frappe.db.get_value('Education Settings', {name: 'Education Settings'}, 'user_creation_skip', (r) => { + if (r.user_creation_skip === "0") { + frm.set_df_property('student_email_id', 'reqd', 1); + } + }); } }); diff --git a/erpnext/education/doctype/student/student.json b/erpnext/education/doctype/student/student.json index ac65c0cd7b..8ba9a7fa11 100644 --- a/erpnext/education/doctype/student/student.json +++ b/erpnext/education/doctype/student/student.json @@ -102,7 +102,6 @@ "fieldtype": "Data", "in_global_search": 1, "label": "Student Email Address", - "reqd": 1, "unique": 1 }, { @@ -255,7 +254,7 @@ ], "image_field": "image", "links": [], - "modified": "2020-07-23 18:14:06.366442", + "modified": "2020-09-07 19:28:08.914568", "modified_by": "Administrator", "module": "Education", "name": "Student", diff --git a/erpnext/education/doctype/student_applicant/student_applicant.js b/erpnext/education/doctype/student_applicant/student_applicant.js index 83621c5725..27b3377267 100644 --- a/erpnext/education/doctype/student_applicant/student_applicant.js +++ b/erpnext/education/doctype/student_applicant/student_applicant.js @@ -7,7 +7,7 @@ frappe.ui.form.on("Student Applicant", { }, refresh: function(frm) { - if(frm.doc.application_status== "Applied" && frm.doc.docstatus== 1 ) { + if (frm.doc.application_status==="Applied" && frm.doc.docstatus===1 ) { frm.add_custom_button(__("Approve"), function() { frm.set_value("application_status", "Approved"); frm.save_or_update(); @@ -20,10 +20,11 @@ frappe.ui.form.on("Student Applicant", { }, 'Actions'); } - if(frm.doc.application_status== "Approved" && frm.doc.docstatus== 1 ) { + if (frm.doc.application_status === "Approved" && frm.doc.docstatus === 1) { frm.add_custom_button(__("Enroll"), function() { frm.events.enroll(frm) }).addClass("btn-primary"); + frm.add_custom_button(__("Reject"), function() { frm.set_value("application_status", "Rejected"); frm.save_or_update(); @@ -35,7 +36,13 @@ frappe.ui.form.on("Student Applicant", { frappe.hide_msgprint(true); frappe.show_progress(__("Enrolling student"), data.progress[0],data.progress[1]); } - }) + }); + + frappe.db.get_value("Education Settings", {name: "Education Settings"}, "user_creation_skip", (r) => { + if (r.user_creation_skip === "0") { + frm.set_df_property("student_email_id", "reqd", 1); + } + }); }, enroll: function(frm) { diff --git a/erpnext/education/doctype/student_applicant/student_applicant.json b/erpnext/education/doctype/student_applicant/student_applicant.json index e5d0bd37de..bca38fb264 100644 --- a/erpnext/education/doctype/student_applicant/student_applicant.json +++ b/erpnext/education/doctype/student_applicant/student_applicant.json @@ -1,1233 +1,292 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "naming_series:", - "beta": 0, - "creation": "2015-09-11 11:50:09.740807", - "custom": 0, - "default_print_format": "", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 0, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "autoname": "naming_series:", + "creation": "2015-09-11 11:50:09.740807", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "first_name", + "middle_name", + "last_name", + "program", + "lms_only", + "paid", + "column_break_8", + "naming_series", + "application_status", + "application_date", + "academic_year", + "academic_term", + "student_admission", + "image", + "section_break_4", + "date_of_birth", + "gender", + "blood_group", + "column_break_12", + "student_email_id", + "student_mobile_number", + "nationality", + "home_address", + "address_line_1", + "address_line_2", + "pincode", + "column_break_23", + "city", + "state", + "section_break_20", + "guardians", + "section_break_21", + "siblings", + "section_break_23", + "title", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "first_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "First Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "first_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "First Name", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "middle_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Middle Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "middle_name", + "fieldtype": "Data", + "label": "Middle Name" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "last_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Last Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "last_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Last Name" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "program", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Program", - "length": 0, - "no_copy": 0, - "options": "Program", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "program", + "fieldtype": "Link", + "in_filter": 1, + "in_global_search": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Program", + "options": "Program", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fieldname": "lms_only", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "LMS Only", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "lms_only", + "fieldtype": "Check", + "label": "LMS Only" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "paid", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Paid", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "paid", + "fieldtype": "Check", + "label": "Paid" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_8", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "naming_series", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Naming Series", - "length": 0, - "no_copy": 1, - "options": "EDU-APP-.YYYY.-", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 1, - "translatable": 0, - "unique": 0 - }, + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "no_copy": 1, + "options": "EDU-APP-.YYYY.-", + "set_only_once": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.docstatus != 0", - "fieldname": "application_status", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Application Status", - "length": 0, - "no_copy": 1, - "options": "Applied\nApproved\nRejected\nAdmitted", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "allow_on_submit": 1, + "depends_on": "eval:doc.docstatus != 0", + "fieldname": "application_status", + "fieldtype": "Select", + "in_filter": 1, + "label": "Application Status", + "no_copy": 1, + "options": "Applied\nApproved\nRejected\nAdmitted" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fieldname": "application_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Application Date", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "Today", + "fieldname": "application_date", + "fieldtype": "Date", + "label": "Application Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "academic_year", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Academic Year", - "length": 0, - "no_copy": 0, - "options": "Academic Year", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "academic_year", + "fieldtype": "Link", + "label": "Academic Year", + "options": "Academic Year" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "academic_term", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Academic Term", - "length": 0, - "no_copy": 0, - "options": "Academic Term", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "academic_term", + "fieldtype": "Link", + "label": "Academic Term", + "options": "Academic Term" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "student_admission", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Student Admission", - "length": 0, - "no_copy": 0, - "options": "Student Admission", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "student_admission", + "fieldtype": "Link", + "label": "Student Admission", + "options": "Student Admission" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "image", - "fieldtype": "Attach Image", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Image", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "image", + "fieldtype": "Attach Image", + "hidden": 1, + "label": "Image" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": "", - "columns": 0, - "fieldname": "section_break_4", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Personal Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "label": "Personal Details" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "date_of_birth", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Date of Birth", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "date_of_birth", + "fieldtype": "Date", + "label": "Date of Birth" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gender", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Gender", - "length": 0, - "no_copy": 0, - "options": "\nMale\nFemale", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "gender", + "fieldtype": "Select", + "label": "Gender", + "options": "\nMale\nFemale" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "blood_group", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Blood Group", - "length": 0, - "no_copy": 0, - "options": "\nA+\nA-\nB+\nB-\nO+\nO-\nAB+\nAB-", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "blood_group", + "fieldtype": "Select", + "label": "Blood Group", + "options": "\nA+\nA-\nB+\nB-\nO+\nO-\nAB+\nAB-" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_12", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "student_email_id", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Student Email Address", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "fieldname": "student_email_id", + "fieldtype": "Data", + "label": "Student Email Address", "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "student_mobile_number", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Student Mobile Number", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "student_mobile_number", + "fieldtype": "Data", + "label": "Student Mobile Number" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "nationality", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Nationality", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "nationality", + "fieldtype": "Data", + "label": "Nationality" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "home_address", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Home Address", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "home_address", + "fieldtype": "Section Break", + "label": "Home Address" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address_line_1", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Address Line 1", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "address_line_1", + "fieldtype": "Data", + "label": "Address Line 1" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address_line_2", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Address Line 2", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "address_line_2", + "fieldtype": "Data", + "label": "Address Line 2" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "pincode", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Pincode", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "pincode", + "fieldtype": "Data", + "label": "Pincode" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_23", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_23", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "city", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "City", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "city", + "fieldtype": "Data", + "label": "City" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "state", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "State", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "state", + "fieldtype": "Data", + "label": "State" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "section_break_20", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Guardian Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "collapsible": 1, + "fieldname": "section_break_20", + "fieldtype": "Section Break", + "label": "Guardian Details" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "guardians", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Guardians", - "length": 0, - "no_copy": 0, - "options": "Student Guardian", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "guardians", + "fieldtype": "Table", + "label": "Guardians", + "options": "Student Guardian" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "section_break_21", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Sibling Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "collapsible": 1, + "fieldname": "section_break_21", + "fieldtype": "Section Break", + "label": "Sibling Details" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "siblings", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Siblings", - "length": 0, - "no_copy": 0, - "options": "Student Sibling", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "siblings", + "fieldtype": "Table", + "label": "Siblings", + "options": "Student Sibling" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_23", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_23", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "title", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Title", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amended_from", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Student Applicant", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Student Applicant", + "print_hide": 1, + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_field": "image", - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2018-10-05 13:15:59.283862", - "modified_by": "Administrator", - "module": "Education", - "name": "Student Applicant", - "name_case": "", - "owner": "Administrator", + ], + "image_field": "image", + "is_submittable": 1, + "links": [], + "modified": "2020-09-07 19:31:30.063563", + "modified_by": "Administrator", + "module": "Education", + "name": "Student Applicant", + "owner": "Administrator", "permissions": [ { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Academics User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Academics User", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Education", - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "title", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 -} + ], + "restrict_to_domain": "Education", + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "title" +} \ No newline at end of file From 4673bed67ed79e6e95f39da67dc547b2cfd4a0f3 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 7 Sep 2020 20:12:29 +0530 Subject: [PATCH 118/192] fix: codacy issues --- erpnext/education/doctype/student/student.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/education/doctype/student/student.js b/erpnext/education/doctype/student/student.js index 9272d35543..5b05bc4f9a 100644 --- a/erpnext/education/doctype/student/student.js +++ b/erpnext/education/doctype/student/student.js @@ -8,7 +8,7 @@ frappe.ui.form.on('Student', { frm.add_fetch('student', 'gender', 'gender'); frm.add_fetch('student', 'date_of_birth', 'date_of_birth'); - frm.set_query('student', 'siblings', function(doc, cdt, cdn) { + frm.set_query('student', 'siblings', function(doc) { return { 'filters': { 'name': ['!=', doc.name] @@ -27,7 +27,7 @@ frappe.ui.form.on('Student', { } frappe.db.get_value('Education Settings', {name: 'Education Settings'}, 'user_creation_skip', (r) => { - if (r.user_creation_skip === "0") { + if (r.user_creation_skip === "0") { frm.set_df_property('student_email_id', 'reqd', 1); } }); From 03b0ad4f98aaf0b6555f8df7899fa14f30ffe0bc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 7 Sep 2020 23:22:21 +0530 Subject: [PATCH 119/192] fix: Only submitted Loan security pledges should be approved --- erpnext/loan_management/doctype/loan/loan.py | 10 +++++++--- .../loan_security_unpledge/loan_security_unpledge.py | 3 --- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index e2e27dd45d..d1b7589a17 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -240,15 +240,19 @@ def unpledge_security(loan=None, loan_security_pledge=None, as_dict=0, save=0, s unpledge_request = create_loan_security_unpledge(security_map, pledge_doc.loan, pledge_doc.company, pledge_doc.applicant_type, pledge_doc.applicant) - if approve: - unpledge_request.status = 'Approved' - if save: unpledge_request.save() if submit: unpledge_request.submit() + if approve: + if unpledge_request.docstatus == 1: + unpledge_request.status = 'Approved' + unpledge_request.save() + else: + frappe.throw(_('Only submittted unpledge requests can be approved')) + if as_dict: return unpledge_request else: diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index a87d832b1c..b3eb6001e4 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -20,9 +20,6 @@ class LoanSecurityUnpledge(Document): self.update_loan_status(cancel=1) self.db_set('status', 'Requested') - def on_submit(self): - self.approve() - def validate_duplicate_securities(self): security_list = [] for d in self.securities: From 5b381ac5b782030d0bc9aff0777c27d440efa199 Mon Sep 17 00:00:00 2001 From: barry86m <65860880+barry86m@users.noreply.github.com> Date: Tue, 8 Sep 2020 00:27:57 +0100 Subject: [PATCH 120/192] fix: consolidated financial statement sums values into wrong parent fix proposed by Andriesvn in bug report closes #22180 --- .../consolidated_financial_statement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 219871b1d6..d0116890b6 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -256,7 +256,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies): """accumulate children's values in parent accounts""" for d in reversed(accounts): if d.parent_account: - account = d.parent_account.split('-')[0].strip() + account = d.parent_account.split(' - ')[0].strip() if not accounts_by_name.get(account): continue From 842cb6d1d638d3ca946d6dcdc5ce51524e87e8a7 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 8 Sep 2020 10:14:04 +0530 Subject: [PATCH 121/192] Update shift_assignment.py --- erpnext/hr/doctype/shift_assignment/shift_assignment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index b1f9fd58e3..2c385e80f4 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -103,7 +103,7 @@ def add_assignments(events, start, end, conditions=None): "doctype": "Shift Assignment", "start_date": d.start_date, "end_date": d.end_date if d.end_date else nowdate(), - "title": cstr(d.employee_name) + ":"+ \ + "title": cstr(d.employee_name) + ": "+ \ cstr(d.shift_type), "docstatus": d.docstatus } From 7d7fa79efab8da0bfa6adfa3ece9c5dcbd3c73a6 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 8 Sep 2020 10:18:18 +0530 Subject: [PATCH 122/192] Update additional_salary.py --- erpnext/payroll/doctype/additional_salary/additional_salary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index cf2bff0405..e3dc9070ec 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -44,7 +44,7 @@ class AdditionalSalary(Document): additional_salaries = [salary.name for salary in additional_salaries] - if len(additional_salaries): + if additional_salaries and len(additional_salaries): frappe.throw(_("Additional Salary: {0} already exist for Salary Component: {1} for period {2} and {3}").format( bold(comma_and(additional_salaries)), bold(self.salary_component), From 9691422fb766b9d0f628fd9a5a1c712376896ddf Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 8 Sep 2020 11:57:15 +0530 Subject: [PATCH 123/192] feat: added report for mismatch in serial nos and stock quantity in warehouse (#22669) * feat: added report for mismatch in serial nos and stock quantity in warehouse * style: removed print statement * fix: rename of reports and refactor of code * fix: handled null condition for actual qty and added report link in stock desk page * style: breaking code in multiple lines --- erpnext/stock/desk_page/stock/stock.json | 4 +- .../stock_qty_vs_serial_no_count/__init__.py | 0 .../stock_qty_vs_serial_no_count.js | 42 ++++++++++ .../stock_qty_vs_serial_no_count.json | 27 +++++++ .../stock_qty_vs_serial_no_count.py | 80 +++++++++++++++++++ 5 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 erpnext/stock/report/stock_qty_vs_serial_no_count/__init__.py create mode 100644 erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js create mode 100644 erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.json create mode 100644 erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py diff --git a/erpnext/stock/desk_page/stock/stock.json b/erpnext/stock/desk_page/stock/stock.json index 1bf81f7f0e..2fba5fa804 100644 --- a/erpnext/stock/desk_page/stock/stock.json +++ b/erpnext/stock/desk_page/stock/stock.json @@ -33,7 +33,7 @@ { "hidden": 0, "label": "Key Reports", - "links": "[\n {\n \"dependencies\": [\n \"Item Price\"\n ],\n \"doctype\": \"Item Price\",\n \"is_query_report\": false,\n \"label\": \"Item-wise Price List Rate\",\n \"name\": \"Item-wise Price List Rate\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Stock Entry\"\n ],\n \"doctype\": \"Stock Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Analytics\",\n \"name\": \"Stock Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Delivery Note\"\n ],\n \"doctype\": \"Delivery Note\",\n \"is_query_report\": true,\n \"label\": \"Delivery Note Trends\",\n \"name\": \"Delivery Note Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Receipt\"\n ],\n \"doctype\": \"Purchase Receipt\",\n \"is_query_report\": true,\n \"label\": \"Purchase Receipt Trends\",\n \"name\": \"Purchase Receipt Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Order\"\n ],\n \"doctype\": \"Purchase Order\",\n \"is_query_report\": true,\n \"label\": \"Purchase Order Analysis\",\n \"name\": \"Purchase Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Bin\"\n ],\n \"doctype\": \"Bin\",\n \"is_query_report\": true,\n \"label\": \"Item Shortage Report\",\n \"name\": \"Item Shortage Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Batch\"\n ],\n \"doctype\": \"Batch\",\n \"is_query_report\": true,\n \"label\": \"Batch-Wise Balance History\",\n \"name\": \"Batch-Wise Balance History\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Item Price\"\n ],\n \"doctype\": \"Item Price\",\n \"is_query_report\": false,\n \"label\": \"Item-wise Price List Rate\",\n \"name\": \"Item-wise Price List Rate\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Stock Entry\"\n ],\n \"doctype\": \"Stock Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Analytics\",\n \"name\": \"Stock Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Qty vs Serial No Count\",\n \"name\": \"Stock Qty vs Serial No Count\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Delivery Note\"\n ],\n \"doctype\": \"Delivery Note\",\n \"is_query_report\": true,\n \"label\": \"Delivery Note Trends\",\n \"name\": \"Delivery Note Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Receipt\"\n ],\n \"doctype\": \"Purchase Receipt\",\n \"is_query_report\": true,\n \"label\": \"Purchase Receipt Trends\",\n \"name\": \"Purchase Receipt Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Order\"\n ],\n \"doctype\": \"Purchase Order\",\n \"is_query_report\": true,\n \"label\": \"Purchase Order Analysis\",\n \"name\": \"Purchase Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Bin\"\n ],\n \"doctype\": \"Bin\",\n \"is_query_report\": true,\n \"label\": \"Item Shortage Report\",\n \"name\": \"Item Shortage Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Batch\"\n ],\n \"doctype\": \"Batch\",\n \"is_query_report\": true,\n \"label\": \"Batch-Wise Balance History\",\n \"name\": \"Batch-Wise Balance History\",\n \"type\": \"report\"\n }\n]" }, { "hidden": 0, @@ -58,7 +58,7 @@ "idx": 0, "is_standard": 1, "label": "Stock", - "modified": "2020-05-30 17:32:11.062681", + "modified": "2020-08-11 17:29:32.626067", "modified_by": "Administrator", "module": "Stock", "name": "Stock", diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/__init__.py b/erpnext/stock/report/stock_qty_vs_serial_no_count/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js new file mode 100644 index 0000000000..2a0fd4025c --- /dev/null +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js @@ -0,0 +1,42 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Stock Qty vs Serial No Count"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, + { + "fieldname":"warehouse", + "label": __("Warehouse"), + "fieldtype": "Link", + "options": "Warehouse", + "get_query": function() { + const company = frappe.query_report.get_filter_value('company'); + return { + filters: { 'company': company } + } + }, + "reqd": 1 + }, + ], + + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + if (column.fieldname == "difference" && data) { + if (data.difference > 0) { + value = "" + value + ""; + } + else if (data.difference < 0) { + value = "" + value + ""; + } + } + return value; + } +}; diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.json b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.json new file mode 100644 index 0000000000..c7108b513e --- /dev/null +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.json @@ -0,0 +1,27 @@ +{ + "add_total_row": 0, + "creation": "2020-07-23 19:31:32.395011", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-07-23 19:32:02.168185", + "modified_by": "Administrator", + "module": "Stock", + "name": "Stock Qty vs Serial No Count", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Item", + "report_name": "Stock Qty vs Serial No Count", + "report_type": "Script Report", + "roles": [ + { + "role": "Stock Manager" + }, + { + "role": "Stock User" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py new file mode 100644 index 0000000000..55f041c95c --- /dev/null +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py @@ -0,0 +1,80 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +def execute(filters=None): + columns = get_columns() + data = get_data(filters.warehouse) + return columns, data + +def get_columns(): + columns = [ + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 200 + }, + { + "label": _("Item Name"), + "fieldname": "item_name", + "fieldtype": "Data", + "width": 200 + }, + { + "label": _("Serial No Count"), + "fieldname": "total", + "fieldtype": "Float", + "width": 150 + }, + { + "label": _("Stock Qty"), + "fieldname": "stock_qty", + "fieldtype": "Float", + "width": 150 + }, + { + "label": _("Difference"), + "fieldname": "difference", + "fieldtype": "Float", + "width": 150 + }, + ] + + return columns + +def get_data(warehouse): + serial_item_list = frappe.get_all("Item", filters={ + 'has_serial_no': True, + }, fields=['item_code', 'item_name']) + + status_list = ['Active', 'Expired'] + data = [] + for item in serial_item_list: + total_serial_no = frappe.db.count("Serial No", + filters={"item_code": item.item_code, "status": ("in", status_list), "warehouse": warehouse}) + + actual_qty = frappe.db.get_value('Bin', fieldname=['actual_qty'], + filters={"warehouse": warehouse, "item_code": item.item_code}) + + # frappe.db.get_value returns null if no record exist. + if not actual_qty: + actual_qty = 0 + + difference = total_serial_no - actual_qty + + row = { + "item_code": item.item_code, + "item_name": item.item_name, + "total": total_serial_no, + "stock_qty": actual_qty, + "difference": difference, + } + + data.append(row) + + return data \ No newline at end of file From f7619df8b22e4579d27f1150f76b6b9e94823147 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 8 Sep 2020 11:57:45 +0530 Subject: [PATCH 124/192] fix: Lock row in subquery while setting delivered qty (#23100) --- 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 0dc9878afd..9feac78770 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -255,7 +255,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 12ab8ebc95d925815e856e360809694afb3e0f8d Mon Sep 17 00:00:00 2001 From: Afshan Date: Tue, 8 Sep 2020 12:55:42 +0530 Subject: [PATCH 125/192] 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 0a385d0af7..34c262e27f 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -1023,7 +1023,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): @@ -1049,6 +1049,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 f7731b0715a4b7ba07cc216998e9984f84c9f0ff Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 8 Sep 2020 13:41:04 +0530 Subject: [PATCH 126/192] fix: TDS applicable on creating PI from get items button --- .../accounts/doctype/purchase_invoice/purchase_invoice.js | 2 ++ .../accounts/doctype/purchase_invoice/purchase_invoice.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index b5c1cd7e1e..2bfa4a572e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -141,6 +141,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ } }); } + + this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1); }, unblock_invoice: function() { diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 7b1062f654..b4ee7c999e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -132,6 +132,11 @@ class PurchaseInvoice(BuyingController): if not self.due_date: self.due_date = get_due_date(self.posting_date, "Supplier", self.supplier, self.company, self.bill_date) + tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category") + if tds_category and not for_validate: + self.apply_tds = 1 + self.tax_withholding_category = tds_category + super(PurchaseInvoice, self).set_missing_values(for_validate) def check_conversion_rate(self): From 8f43cf2c9a6a2564d2173d7a4fc174f5d5ab6787 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 8 Sep 2020 17:44:47 +0530 Subject: [PATCH 127/192] fix: asset movement date for backdated asset entry (#23298) --- 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 0bd03a8dbe..9d08d9212d 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -146,7 +146,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 f247686ace6462ae43e3788161e96c8f9f2698ca Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 8 Sep 2020 21:26:06 +0530 Subject: [PATCH 128/192] fix: Amount for closed states --- .../loan_management/doctype/loan_repayment/loan_repayment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 7d83e32213..be221dfa7f 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -305,7 +305,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): if not final_due_date: final_due_date = add_days(due_date, loan_type_details.grace_period_in_days) - if against_loan_doc.status in ('Disbursed', 'Loan Closure Requested'): + if against_loan_doc.status in ('Disbursed', 'Loan Closure Requested', 'Closed'): pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable else: pending_principal_amount = against_loan_doc.disbursed_amount From 846e6d480321b5366264562ae11f8a95fa45d141 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 8 Sep 2020 21:32:06 +0530 Subject: [PATCH 129/192] fix: Add test --- erpnext/loan_management/doctype/loan/test_loan.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 5faf80e625..ba84921a2e 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -318,6 +318,11 @@ class TestLoan(unittest.TestCase): self.assertEqual(loan.status, 'Closed') self.assertEquals(sum(pledged_qty.values()), 0) + amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") + self.assertEqual(amounts['pending_principal_amount'], 0) + self.assertEqual(amounts['payable_principal_amount'], 0) + self.assertEqual(amounts['interest_amount'], 0) + def test_disbursal_check_with_shortfall(self): pledges = [{ "loan_security": "Test Security 2", From ec6a97fb6a0d6db5fc03b76fd27050db6e9a2c12 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 9 Sep 2020 10:54:14 +0530 Subject: [PATCH 130/192] fix: cannot delete pos page if linked with desk page (#22993) * fix: cannot delete pos page if linked with desk page * fix: replace pos page links with point-of-sale page Co-authored-by: gavin --- erpnext/patches.txt | 2 +- .../v13_0/replace_pos_page_with_point_of_sale_page.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index aa7996e3e1..6c58f2f452 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -697,7 +697,7 @@ erpnext.patches.v12_0.update_bom_in_so_mr execute:frappe.delete_doc("Report", "Department Analytics") execute:frappe.rename_doc("Desk Page", "Loan Management", "Loan", force=True) erpnext.patches.v12_0.update_uom_conversion_factor -execute:frappe.delete_doc_if_exists("Page", "pos") #29-05-2020 +erpnext.patches.v13_0.replace_pos_page_with_point_of_sale_page erpnext.patches.v13_0.delete_old_purchase_reports erpnext.patches.v12_0.set_italian_import_supplier_invoice_permissions erpnext.patches.v13_0.update_subscription diff --git a/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py b/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py new file mode 100644 index 0000000000..390e217cad --- /dev/null +++ b/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + if frappe.db.exists("Page", "point-of-sale"): + frappe.rename_doc("Page", "pos", "point-of-sale", 1, 1) \ No newline at end of file From 53cd60c217fc2442dc8d1db23c2595446efc08bb Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 7 Sep 2020 20:45:50 +0530 Subject: [PATCH 131/192] fix: Patch reload doctype --- erpnext/patches/v13_0/set_youtube_video_id.py | 2 ++ erpnext/utilities/doctype/video/video.py | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/patches/v13_0/set_youtube_video_id.py b/erpnext/patches/v13_0/set_youtube_video_id.py index 8e5dd306a9..c3b49eb4fe 100644 --- a/erpnext/patches/v13_0/set_youtube_video_id.py +++ b/erpnext/patches/v13_0/set_youtube_video_id.py @@ -3,6 +3,8 @@ import frappe from erpnext.utilities.doctype.video.video import get_id_from_url def execute(): + frappe.reload_doc("utilities", "doctype","video") + for video in frappe.get_all("Video", fields=["name", "url", "youtube_video_id"]): if video.url and not video.youtube_video_id: frappe.db.set_value("Video", video.name, "youtube_video_id", get_id_from_url(video.url)) \ No newline at end of file diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py index 3b6bd7e0df..6971beb5d7 100644 --- a/erpnext/utilities/doctype/video/video.py +++ b/erpnext/utilities/doctype/video/video.py @@ -140,8 +140,6 @@ def batch_update_youtube_data(): comment_count = %(comment_count)s WHERE youtube_video_id = '{0}'""".format(video_id), stats) - frappe.log_error("yooooooooo") - video_list = frappe.get_all("Video", fields=["youtube_video_id"]) if len(video_list) > 50: # Update in batches of 50 From 1915a608348d89f8dac040a8c81832fb502723dc Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 10 Sep 2020 12:13:58 +0530 Subject: [PATCH 132/192] fix: education settings check --- erpnext/education/doctype/student/student.js | 2 +- .../education/doctype/student_applicant/student_applicant.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/education/doctype/student/student.js b/erpnext/education/doctype/student/student.js index 5b05bc4f9a..fd23ae41ef 100644 --- a/erpnext/education/doctype/student/student.js +++ b/erpnext/education/doctype/student/student.js @@ -27,7 +27,7 @@ frappe.ui.form.on('Student', { } frappe.db.get_value('Education Settings', {name: 'Education Settings'}, 'user_creation_skip', (r) => { - if (r.user_creation_skip === "0") { + if (cint(r.user_creation_skip) !== 1) { frm.set_df_property('student_email_id', 'reqd', 1); } }); diff --git a/erpnext/education/doctype/student_applicant/student_applicant.js b/erpnext/education/doctype/student_applicant/student_applicant.js index 27b3377267..b4cfdf16e0 100644 --- a/erpnext/education/doctype/student_applicant/student_applicant.js +++ b/erpnext/education/doctype/student_applicant/student_applicant.js @@ -39,7 +39,7 @@ frappe.ui.form.on("Student Applicant", { }); frappe.db.get_value("Education Settings", {name: "Education Settings"}, "user_creation_skip", (r) => { - if (r.user_creation_skip === "0") { + if (cint(r.user_creation_skip) !== 1) { frm.set_df_property("student_email_id", "reqd", 1); } }); From 0a0e258d8b43f88dc51fc1ed1aac48b55d4ca22a Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 10 Sep 2020 13:12:35 +0530 Subject: [PATCH 133/192] feat: Add Desk Page for Utilities for Video group --- .../desk_page/utilities/utilities.json | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 erpnext/utilities/desk_page/utilities/utilities.json diff --git a/erpnext/utilities/desk_page/utilities/utilities.json b/erpnext/utilities/desk_page/utilities/utilities.json new file mode 100644 index 0000000000..591eab5ed4 --- /dev/null +++ b/erpnext/utilities/desk_page/utilities/utilities.json @@ -0,0 +1,29 @@ +{ + "cards": [ + { + "hidden": 0, + "label": "Video", + "links": "[\n {\n \"description\": \"Video\",\n \"label\": \"Video\",\n \"name\": \"Video\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Video settings\",\n \"label\": \"Video Settings\",\n \"name\": \"Video Settings\",\n \"type\": \"doctype\"\n }\n]" + } + ], + "category": "Modules", + "charts": [], + "creation": "2020-09-10 12:21:22.335307", + "developer_mode_only": 0, + "disable_user_customization": 0, + "docstatus": 0, + "doctype": "Desk Page", + "extends_another_page": 0, + "hide_custom": 0, + "idx": 0, + "is_standard": 1, + "label": "Utilities", + "modified": "2020-09-10 12:33:30.089853", + "modified_by": "user@erpnext.com", + "module": "Utilities", + "name": "Utilities", + "owner": "user@erpnext.com", + "pin_to_bottom": 1, + "pin_to_top": 0, + "shortcuts": [] +} \ No newline at end of file From 71ac39936977e6408d815a5ecb100fd51f8dadb8 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 10 Sep 2020 13:57:44 +0530 Subject: [PATCH 134/192] fix: Commonified function and updated scheduler events Co-authored-by: marination --- erpnext/hooks.py | 3 +- erpnext/utilities/doctype/video/video.py | 42 ++++++++++-------------- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 3beae3b6c8..f8b6be70ca 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -284,7 +284,7 @@ auto_cancel_exempted_doctypes= [ scheduler_events = { "cron": { "0/30 * * * *": [ - "erpnext.utilities.doctype.video.video.update_youtube_data_half_hourly", + "erpnext.utilities.doctype.video.video.update_youtube_data", ] }, "all": [ @@ -302,7 +302,6 @@ scheduler_events = { "erpnext.projects.doctype.project.project.collect_project_status", "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts", "erpnext.support.doctype.issue.issue.set_service_level_agreement_variance", - "erpnext.utilities.doctype.video.video.update_youtube_data" ], "daily": [ "erpnext.stock.reorder_item.reorder_item", diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py index 6971beb5d7..7918b828aa 100644 --- a/erpnext/utilities/doctype/video/video.py +++ b/erpnext/utilities/doctype/video/video.py @@ -50,45 +50,37 @@ class Video(Document): title = "Failed to Update YouTube Statistics for Video: {0}".format(self.name) frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) + def is_tracking_enabled(): return frappe.db.get_single_value("Video Settings", "enable_youtube_tracking") -def get_frequency(value): - if not value: - return None - # Return frequency in hours +def get_frequency(value): + # Return numeric value from frequency field, return 1 as fallback default value: 1 hour if value != "Daily": return frappe.utils.cint(value[:2].strip()) - else: - # 24 hours for Daily + elif value: return 24 - - -def update_youtube_data_half_hourly(): - # Called every 30 mins via hooks - frequency = get_frequency(frappe.db.get_single_value("Video Settings", "frequency")) - if not is_tracking_enabled() or not frequency: - return - - if frequency == 30: - batch_update_youtube_data() + return 1 def update_youtube_data(): - # Called every hour via hooks - frequency = get_frequency(frappe.db.get_single_value("Video Settings", "frequency")) + # Called every 30 minutes via hooks + enable_youtube_tracking, frequency = frappe.db.get_value("Video Settings", "Video Settings", ["enable_youtube_tracking", "frequency"]) - # if frequency is 30 mins dont proceed, as its handled in another method - if not is_tracking_enabled() or not frequency or frequency == 30: + if not enable_youtube_tracking: return + frequency = get_frequency(frequency) time = datetime.now() timezone = pytz.timezone(frappe.utils.get_time_zone()) site_time = time.astimezone(timezone) - if site_time.hour % frequency == 0: + if frequency == 30: batch_update_youtube_data() + elif site_time.hour % frequency == 0: + batch_update_youtube_data() + def get_formatted_ids(video_list): # format ids to comma separated string for bulk request @@ -98,6 +90,7 @@ def get_formatted_ids(video_list): return ','.join(ids) + @frappe.whitelist() def get_id_from_url(url): """ @@ -128,7 +121,8 @@ def batch_update_youtube_data(): 'like_count' : video_stats.get('likeCount'), 'view_count' : video_stats.get('viewCount'), 'dislike_count' : video_stats.get('dislikeCount'), - 'comment_count' : video_stats.get('commentCount') + 'comment_count' : video_stats.get('commentCount'), + 'video_id': video_id } frappe.db.sql(""" @@ -138,7 +132,7 @@ def batch_update_youtube_data(): view_count = %(view_count)s, dislike_count = %(dislike_count)s, comment_count = %(comment_count)s - WHERE youtube_video_id = '{0}'""".format(video_id), stats) + WHERE youtube_video_id = %(video_id)s""", stats) video_list = frappe.get_all("Video", fields=["youtube_video_id"]) if len(video_list) > 50: @@ -150,4 +144,4 @@ def batch_update_youtube_data(): start += 50 end += 50 else: - prepare_and_set_data(video_list) \ No newline at end of file + prepare_and_set_data(video_list) From 799d663398781f55af8e3e6b2f239b23f0e3eecf Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 10 Sep 2020 16:42:20 +0530 Subject: [PATCH 135/192] fix: Make set_youtube_statistics reusable and job logic fix - Make sure job runs only once every hour - set_youtube_statistics should be usable in and outside class --- erpnext/utilities/doctype/video/video.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py index 7918b828aa..f519146e6b 100644 --- a/erpnext/utilities/doctype/video/video.py +++ b/erpnext/utilities/doctype/video/video.py @@ -21,7 +21,6 @@ class Video(Document): if self.provider == "YouTube" and self.url and not self.get("youtube_video_id"): self.youtube_video_id = get_id_from_url(self.url) - @classmethod def set_youtube_statistics(self, video_ids=None, update=True): if self.provider == "YouTube" and not is_tracking_enabled(): return @@ -78,7 +77,8 @@ def update_youtube_data(): if frequency == 30: batch_update_youtube_data() - elif site_time.hour % frequency == 0: + elif site_time.hour % frequency == 0 and site_time.minute < 15: + # make sure it runs within the first 15 mins of the hour batch_update_youtube_data() @@ -109,8 +109,8 @@ def get_id_from_url(url): def batch_update_youtube_data(): def prepare_and_set_data(video_list): video_ids = get_formatted_ids(video_list) - Video.provider = "YouTube" - stats = Video.set_youtube_statistics(video_ids=video_ids, update=False) + video_doc = frappe.new_doc("Video") + stats = video_doc.set_youtube_statistics(video_ids=video_ids, update=False) set_youtube_data(stats) def set_youtube_data(entries): From c0f9d1b65a46b92d97e76a4f0a3341302bc252f6 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 10 Sep 2020 17:33:52 +0530 Subject: [PATCH 136/192] test: Lab Module --- .../doctype/lab_test/test_lab_test.py | 202 +++++++++++++++++- .../test_patient_medical_record.py | 6 +- 2 files changed, 202 insertions(+), 6 deletions(-) diff --git a/erpnext/healthcare/doctype/lab_test/test_lab_test.py b/erpnext/healthcare/doctype/lab_test/test_lab_test.py index 4131ad99b7..d949c46d16 100644 --- a/erpnext/healthcare/doctype/lab_test/test_lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/test_lab_test.py @@ -3,8 +3,204 @@ # See license.txt from __future__ import unicode_literals import unittest - -# test_records = frappe.get_test_records('Lab Test') +import frappe +from frappe.utils import getdate, nowtime +from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient +from erpnext.healthcare.doctype.lab_test.lab_test import create_multiple +from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account, get_income_account +from erpnext.healthcare.doctype.patient_medical_record.test_patient_medical_record import create_lab_test_template as create_blood_test_template class TestLabTest(unittest.TestCase): - pass + def test_lab_test_item(self): + lab_template = create_lab_test_template() + self.assertTrue(frappe.db.exists('Item', lab_template.item)) + self.assertEqual(frappe.db.get_value('Item Price', {'item_code':lab_template.item}, 'price_list_rate'), lab_template.lab_test_rate) + + lab_template.disabled = 1 + lab_template.save() + self.assertEquals(frappe.db.get_value('Item', lab_template.item, 'disabled'), 1) + + lab_template.reload() + + lab_template.disabled = 0 + lab_template.save() + + def test_descriptive_lab_test(self): + lab_template = create_lab_test_template() + + # blank result value not allowed as per template + lab_test = create_lab_test(lab_template) + lab_test.descriptive_test_items[0].result_value = 12 + lab_test.descriptive_test_items[2].result_value = 1 + lab_test.save() + self.assertRaises(frappe.ValidationError, lab_test.submit) + + def test_sample_collection(self): + frappe.db.set_value('Healthcare Settings', 'Healthcare Settings', 'create_sample_collection_for_lab_test', 1) + lab_template = create_lab_test_template() + + lab_test = create_lab_test(lab_template) + lab_test.descriptive_test_items[0].result_value = 12 + lab_test.descriptive_test_items[1].result_value = 1 + lab_test.descriptive_test_items[2].result_value = 2.3 + lab_test.save() + + # check sample collection created + self.assertTrue(frappe.db.exists('Sample Collection', {'sample': lab_template.sample})) + + frappe.db.set_value('Healthcare Settings', 'Healthcare Settings', 'create_sample_collection_for_lab_test', 0) + lab_test = create_lab_test(lab_template) + lab_test.descriptive_test_items[0].result_value = 12 + lab_test.descriptive_test_items[1].result_value = 1 + lab_test.descriptive_test_items[2].result_value = 2.3 + lab_test.save() + + # sample collection should not be created + lab_test.reload() + self.assertEquals(lab_test.sample, None) + + def test_create_lab_tests_from_sales_invoice(self): + sales_invoice = create_sales_invoice() + create_multiple('Sales Invoice', sales_invoice.name) + sales_invoice.reload() + self.assertIsNotNone(sales_invoice.items[0].reference_dn) + self.assertIsNotNone(sales_invoice.items[1].reference_dn) + + def test_create_lab_tests_from_patient_encounter(self): + patient_encounter = create_patient_encounter() + create_multiple('Patient Encounter', patient_encounter.name) + patient_encounter.reload() + self.assertTrue(patient_encounter.lab_test_prescription[0].lab_test_created) + self.assertTrue(patient_encounter.lab_test_prescription[0].lab_test_created) + + +def create_lab_test_template(test_sensitivity=0, sample_collection=1): + medical_department = create_medical_department() + if frappe.db.exists('Lab Test Template', 'Insulin Resistance'): + return frappe.get_doc('Lab Test Template', 'Insulin Resistance') + template = frappe.new_doc('Lab Test Template') + template.lab_test_name = 'Insulin Resistance' + template.lab_test_template_type = 'Descriptive' + template.lab_test_code = 'Insulin Resistance' + template.lab_test_group = 'Services' + template.department = medical_department + template.is_billable = 1 + template.lab_test_description = 'Insulin Resistance' + template.lab_test_rate = 2000 + + for entry in ['FBS', 'Insulin', 'IR']: + template.append('descriptive_test_templates', { + 'particulars': entry, + 'allow_blank': 1 if entry=='IR' else 0 + }) + + if test_sensitivity: + template.sensitivity = 1 + + if sample_collection: + template.sample = create_lab_test_sample() + template.sample_qty = 5.0 + + template.save() + return template + +def create_medical_department(): + medical_department = frappe.db.exists('Medical Department', '_Test Medical Department') + if not medical_department: + medical_department = frappe.new_doc('Medical Department') + medical_department.department = '_Test Medical Department' + medical_department.save() + medical_department = medical_department.name + + return medical_department + +def create_lab_test(lab_template): + patient = create_patient() + lab_test = frappe.new_doc('Lab Test') + lab_test.template = lab_template.name + lab_test.patient = patient + lab_test.patient_sex = 'Female' + lab_test.save() + + return lab_test + +def create_lab_test_sample(): + blood_sample = frappe.db.exists('Lab Test Sample', 'Blood Sample') + if blood_sample: + return blood_sample + + sample = frappe.new_doc('Lab Test Sample') + sample.sample = 'Blood Sample' + sample.sample_uom = 'U/ml' + sample.save() + + return sample.name + +def create_sales_invoice(): + patient = create_patient() + medical_department = create_medical_department() + insulin_resistance_template = create_lab_test_template() + blood_test_template = create_blood_test_template(medical_department) + + sales_invoice = frappe.new_doc('Sales Invoice') + sales_invoice.patient = patient + sales_invoice.customer = frappe.db.get_value('Patient', patient, 'customer') + sales_invoice.due_date = getdate() + sales_invoice.company = '_Test Company' + sales_invoice.debit_to = get_receivable_account('_Test Company') + + tests = [insulin_resistance_template, blood_test_template] + for entry in tests: + item_line = sales_invoice.append('items', { + 'item_code': entry.item, + 'item_name': entry.lab_test_name, + 'description': entry.lab_test_description, + 'qty': 1, + 'uom': 'Nos', + 'conversion_factor': 1, + 'income_account': get_income_account(None, '_Test Company'), + 'rate': entry.lab_test_rate, + 'amount': entry.lab_test_rate + }) + + sales_invoice.set_missing_values() + + sales_invoice.submit() + return sales_invoice + +def create_patient_encounter(): + patient = create_patient() + medical_department = create_medical_department() + insulin_resistance_template = create_lab_test_template() + blood_test_template = create_blood_test_template(medical_department) + + patient_encounter = frappe.new_doc('Patient Encounter') + patient_encounter.patient = patient + patient_encounter.practitioner = create_practitioner() + patient_encounter.encounter_date = getdate() + patient_encounter.encounter_time = nowtime() + + tests = [insulin_resistance_template, blood_test_template] + for entry in tests: + patient_encounter.append('lab_test_prescription', { + 'lab_test_code': entry.item, + 'lab_test_name': entry.lab_test_name + }) + + patient_encounter.submit() + return patient_encounter + + +def create_practitioner(): + practitioner = frappe.db.exists('Healthcare Practitioner', '_Test Healthcare Practitioner') + + if not practitioner: + practitioner = frappe.new_doc('Healthcare Practitioner') + practitioner.first_name = '_Test Healthcare Practitioner' + practitioner.gender = 'Female' + practitioner.op_consulting_charge = 500 + practitioner.inpatient_visit_charge = 500 + practitioner.save(ignore_permissions=True) + practitioner = practitioner.name + + return practitioner diff --git a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py index e5a5e4c010..aa85a23113 100644 --- a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py +++ b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py @@ -34,7 +34,7 @@ class TestPatientMedicalRecord(unittest.TestCase): self.assertTrue(medical_rec) template = create_lab_test_template(medical_department) - lab_test = create_lab_test(template, patient) + lab_test = create_lab_test(template.name, patient) # check for lab test medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': lab_test.name}) self.assertTrue(medical_rec) @@ -66,7 +66,7 @@ def create_vital_signs(appointment): def create_lab_test_template(medical_department): if frappe.db.exists('Lab Test Template', 'Blood Test'): - return 'Blood Test' + return frappe.get_doc('Lab Test Template', 'Blood Test') template = frappe.new_doc('Lab Test Template') template.lab_test_name = 'Blood Test' @@ -76,7 +76,7 @@ def create_lab_test_template(medical_department): template.is_billable = 1 template.lab_test_rate = 2000 template.save() - return template.name + return template def create_lab_test(template, patient): lab_test = frappe.new_doc('Lab Test') From a29436dc91f75abe619d53d6b53689faef109a07 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 10 Sep 2020 17:45:17 +0530 Subject: [PATCH 137/192] fix: codacy --- erpnext/healthcare/doctype/lab_test/test_lab_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/healthcare/doctype/lab_test/test_lab_test.py b/erpnext/healthcare/doctype/lab_test/test_lab_test.py index d949c46d16..79ab8a4d7f 100644 --- a/erpnext/healthcare/doctype/lab_test/test_lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/test_lab_test.py @@ -151,7 +151,7 @@ def create_sales_invoice(): tests = [insulin_resistance_template, blood_test_template] for entry in tests: - item_line = sales_invoice.append('items', { + sales_invoice.append('items', { 'item_code': entry.item, 'item_name': entry.lab_test_name, 'description': entry.lab_test_description, From 37b99d202d6f4d7c5ffdffb1739d91aa74965757 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 10 Sep 2020 18:27:20 +0530 Subject: [PATCH 138/192] fix: Separate function to get bulk and single youtube stats --- erpnext/utilities/doctype/video/video.py | 39 ++++++++++++------------ 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py index f519146e6b..c2e414eef8 100644 --- a/erpnext/utilities/doctype/video/video.py +++ b/erpnext/utilities/doctype/video/video.py @@ -14,31 +14,21 @@ from pyyoutube import Api class Video(Document): def validate(self): - self.set_video_id() - self.set_youtube_statistics() + if self.provider == "YouTube" and is_tracking_enabled(): + self.set_video_id() + self.set_youtube_statistics() def set_video_id(self): - if self.provider == "YouTube" and self.url and not self.get("youtube_video_id"): + if self.url and not self.get("youtube_video_id"): self.youtube_video_id = get_id_from_url(self.url) - def set_youtube_statistics(self, video_ids=None, update=True): - if self.provider == "YouTube" and not is_tracking_enabled(): - return - + def set_youtube_statistics(self): api_key = frappe.db.get_single_value("Video Settings", "api_key") api = Api(api_key=api_key) try: - video_id = video_ids or self.youtube_video_id - video = api.get_video_by_id(video_id=video_id) - - if video_ids: - video_stats = video.items - else: - video_stats = video.items[0].to_dict().get('statistics') - - if not update: - return video_stats + video = api.get_video_by_id(video_id=self.youtube_video_id) + video_stats = video.items[0].to_dict().get('statistics') self.like_count = video_stats.get('likeCount') self.view_count = video_stats.get('viewCount') @@ -49,7 +39,6 @@ class Video(Document): title = "Failed to Update YouTube Statistics for Video: {0}".format(self.name) frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) - def is_tracking_enabled(): return frappe.db.get_single_value("Video Settings", "enable_youtube_tracking") @@ -107,10 +96,20 @@ def get_id_from_url(url): @frappe.whitelist() def batch_update_youtube_data(): + def get_youtube_statistics(video_ids): + api_key = frappe.db.get_single_value("Video Settings", "api_key") + api = Api(api_key=api_key) + try: + video = api.get_video_by_id(video_id=video_ids) + video_stats = video.items + return video_stats + except Exception: + title = "Failed to Update YouTube Statistics" + frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) + def prepare_and_set_data(video_list): video_ids = get_formatted_ids(video_list) - video_doc = frappe.new_doc("Video") - stats = video_doc.set_youtube_statistics(video_ids=video_ids, update=False) + stats = get_youtube_statistics(video_ids) set_youtube_data(stats) def set_youtube_data(entries): From cd89994b33bdb120111f9fa9cdf5b145b2c04d91 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 10 Sep 2020 19:28:46 +0530 Subject: [PATCH 139/192] fix: multiple pos issues (#23297) * fix: returns can be made against unconsolidated invoices * fix: indentation * fix: mode of payment not fetching for pos returns * patch: default pos profile print format * fix: tests * chore: clean up retail desk page --- .../desk_page/accounting/accounting.json | 9 +- .../pos_closing_entry/pos_closing_entry.js | 54 ++- .../doctype/pos_invoice/pos_invoice.json | 6 +- .../doctype/pos_invoice/test_pos_invoice.py | 6 +- .../pos_invoice_merge_log.py | 21 +- .../controllers/sales_and_purchase_return.py | 3 +- erpnext/patches.txt | 1 + .../v13_0/change_default_pos_print_format.py | 8 + .../public/js/controllers/taxes_and_totals.js | 25 +- erpnext/selling/desk_page/retail/retail.json | 33 +- .../page/point_of_sale/point_of_sale.js | 2 +- .../page/point_of_sale/pos_controller.js | 31 +- .../page/point_of_sale/pos_item_cart.js | 248 ++++++------ .../page/point_of_sale/pos_item_details.js | 114 +++--- .../page/point_of_sale/pos_item_selector.js | 374 +++++++++--------- .../page/point_of_sale/pos_number_pad.js | 81 ++-- 16 files changed, 521 insertions(+), 495 deletions(-) create mode 100644 erpnext/patches/v13_0/change_default_pos_print_format.py diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json index 2c5231491c..3f23ba9019 100644 --- a/erpnext/accounts/desk_page/accounting/accounting.json +++ b/erpnext/accounts/desk_page/accounting/accounting.json @@ -43,7 +43,7 @@ { "hidden": 0, "label": "Bank Statement", - "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"label\": \"Bank Clearance\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -98,7 +98,7 @@ "idx": 0, "is_standard": 1, "label": "Accounting", - "modified": "2020-09-03 10:37:07.865801", + "modified": "2020-09-09 11:45:33.766400", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", @@ -147,11 +147,6 @@ "link_to": "Trial Balance", "type": "Report" }, - { - "label": "Point of Sale", - "link_to": "point-of-sale", - "type": "Page" - }, { "label": "Dashboard", "link_to": "Accounts", diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js index 8dcd2e4a72..9336fc3706 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js @@ -55,14 +55,48 @@ frappe.ui.form.on('POS Closing Entry', { }, callback: (r) => { let pos_docs = r.message; - set_form_data(pos_docs, frm) - refresh_fields(frm) - set_html_data(frm) + set_form_data(pos_docs, frm); + refresh_fields(frm); + set_html_data(frm); } }) } }); +cur_frm.cscript.before_pos_transactions_remove = function(doc, cdt, cdn) { + const removed_row = locals[cdt][cdn]; + + if (!removed_row.pos_invoice) return; + + frappe.db.get_doc("POS Invoice", removed_row.pos_invoice).then(doc => { + cur_frm.doc.grand_total -= flt(doc.grand_total); + cur_frm.doc.net_total -= flt(doc.net_total); + cur_frm.doc.total_quantity -= flt(doc.total_qty); + refresh_payments(doc, cur_frm, 1); + refresh_taxes(doc, cur_frm, 1); + refresh_fields(cur_frm); + set_html_data(cur_frm); + }); +} + +frappe.ui.form.on('POS Invoice Reference', { + pos_invoice(frm, cdt, cdn) { + const added_row = locals[cdt][cdn]; + + if (!added_row.pos_invoice) return; + + frappe.db.get_doc("POS Invoice", added_row.pos_invoice).then(doc => { + frm.doc.grand_total += flt(doc.grand_total); + frm.doc.net_total += flt(doc.net_total); + frm.doc.total_quantity += flt(doc.total_qty); + refresh_payments(doc, frm); + refresh_taxes(doc, frm); + refresh_fields(frm); + set_html_data(frm); + }); + } +}) + frappe.ui.form.on('POS Closing Entry Detail', { closing_amount: (frm, cdt, cdn) => { const row = locals[cdt][cdn]; @@ -76,8 +110,8 @@ function set_form_data(data, frm) { frm.doc.grand_total += flt(d.grand_total); frm.doc.net_total += flt(d.net_total); frm.doc.total_quantity += flt(d.total_qty); - add_to_payments(d, frm); - add_to_taxes(d, frm); + refresh_payments(d, frm); + refresh_taxes(d, frm); }); } @@ -90,11 +124,12 @@ function add_to_pos_transaction(d, frm) { }) } -function add_to_payments(d, frm) { +function refresh_payments(d, frm, remove) { d.payments.forEach(p => { const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment); if (payment) { - payment.expected_amount += flt(p.amount); + if (!remove) payment.expected_amount += flt(p.amount); + else payment.expected_amount -= flt(p.amount); } else { frm.add_child("payment_reconciliation", { mode_of_payment: p.mode_of_payment, @@ -105,11 +140,12 @@ function add_to_payments(d, frm) { }) } -function add_to_taxes(d, frm) { +function refresh_taxes(d, frm, remove) { d.taxes.forEach(t => { const tax = frm.doc.taxes.find(tx => tx.account_head === t.account_head && tx.rate === t.rate); if (tax) { - tax.amount += flt(t.tax_amount); + if (!remove) tax.amount += flt(t.tax_amount); + else tax.amount -= flt(t.tax_amount); } else { frm.add_child("taxes", { account_head: t.account_head, diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index 2a2e3df8ae..4780688471 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -279,7 +279,8 @@ "fieldtype": "Check", "label": "Is Return (Credit Note)", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "set_only_once": 1 }, { "fieldname": "column_break1", @@ -1578,9 +1579,10 @@ } ], "icon": "fa fa-file-text", + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-05-29 15:08:39.337385", + "modified": "2020-09-07 12:43:09.138720", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 9c62a87677..514a2acd8c 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -182,8 +182,9 @@ class TestPOSInvoice(unittest.TestCase): def test_pos_returns_with_repayment(self): pos = create_pos_invoice(qty = 10, do_not_save=True) + pos.set('payments', []) 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.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500, 'default': 1}) pos.insert() pos.submit() @@ -200,8 +201,9 @@ class TestPOSInvoice(unittest.TestCase): income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105, cost_center = "Main - _TC", do_not_save=True) + pos.set('payments', []) pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 50}) - pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60}) + pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60, 'default': 1}) pos.insert() pos.submit() diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 00dbad5fa0..11b9d2509e 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -24,11 +24,20 @@ class POSInvoiceMergeLog(Document): def validate_pos_invoice_status(self): for d in self.pos_invoices: - status, docstatus = frappe.db.get_value('POS Invoice', d.pos_invoice, ['status', 'docstatus']) + status, docstatus, is_return, return_against = frappe.db.get_value( + 'POS Invoice', d.pos_invoice, ['status', 'docstatus', 'is_return', 'return_against']) + if docstatus != 1: frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, d.pos_invoice)) - if status in ['Consolidated']: + if status == "Consolidated": frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, d.pos_invoice, status)) + if is_return and return_against not in [d.pos_invoice for d in self.pos_invoices] and status != "Consolidated": + # if return entry is not getting merged in the current pos closing and if it is not consolidated + frappe.throw( + _("Row #{}: Return Invoice {} cannot be made against unconsolidated invoice. \ + You can add original invoice {} manually to proceed.") + .format(d.idx, frappe.bold(d.pos_invoice), frappe.bold(return_against)) + ) def on_submit(self): pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] @@ -36,12 +45,12 @@ class POSInvoiceMergeLog(Document): returns = [d for d in pos_invoice_docs if d.get('is_return') == 1] sales = [d for d in pos_invoice_docs if d.get('is_return') == 0] - sales_invoice = self.process_merging_into_sales_invoice(sales) + sales_invoice, credit_note = "", "" + if sales: + sales_invoice = self.process_merging_into_sales_invoice(sales) - if len(returns): + if returns: credit_note = self.process_merging_into_credit_note(returns) - else: - credit_note = "" self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index a03dee1174..afc5f8179f 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -242,7 +242,8 @@ def make_return_doc(doctype, source_name, target_doc=None): 'type': data.type, 'amount': -1 * paid_amount, 'base_amount': -1 * base_paid_amount, - 'account': data.account + 'account': data.account, + 'default': data.default }) if doc.is_pos: doc.paid_amount = -1 * source.paid_amount diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 6c58f2f452..9ce570e6d0 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -725,3 +725,4 @@ erpnext.patches.v12_0.rename_lost_reason_detail erpnext.patches.v13_0.drop_razorpay_payload_column erpnext.patches.v13_0.update_start_end_date_for_old_shift_assignment erpnext.patches.v13_0.setting_custom_roles_for_some_regional_reports +erpnext.patches.v13_0.change_default_pos_print_format diff --git a/erpnext/patches/v13_0/change_default_pos_print_format.py b/erpnext/patches/v13_0/change_default_pos_print_format.py new file mode 100644 index 0000000000..605a29e477 --- /dev/null +++ b/erpnext/patches/v13_0/change_default_pos_print_format.py @@ -0,0 +1,8 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.db.sql( + """UPDATE `tabPOS Profile` profile + SET profile.`print_format` = 'POS Invoice' + WHERE profile.`print_format` = 'Point of Sale'""") \ No newline at end of file diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 6951539026..6d58fd2f3c 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -673,23 +673,14 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ ); } - 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.frm.doc.payments.find(pay => { + if (pay.default) { + pay.amount = total_amount_to_pay; + } else { + pay.amount = 0.0 + } + }); + this.frm.refresh_fields(); this.calculate_paid_amount(); }, diff --git a/erpnext/selling/desk_page/retail/retail.json b/erpnext/selling/desk_page/retail/retail.json index 581e14cf81..c4ddf26a90 100644 --- a/erpnext/selling/desk_page/retail/retail.json +++ b/erpnext/selling/desk_page/retail/retail.json @@ -2,8 +2,18 @@ "cards": [ { "hidden": 0, - "label": "Retail Operations", - "links": "[\n {\n \"description\": \"Setup default values for POS Invoices\",\n \"label\": \"Point of Sale Profile\",\n \"name\": \"POS Profile\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"POS Profile\"\n ],\n \"description\": \"Point of Sale\",\n \"label\": \"Point of Sale\",\n \"name\": \"point-of-sale\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"description\": \"Setup mode of POS (Online / Offline)\",\n \"label\": \"POS Settings\",\n \"name\": \"POS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Cashier Closing\",\n \"label\": \"Cashier Closing\",\n \"name\": \"Cashier Closing\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To make Customer based incentive schemes.\",\n \"label\": \"Loyalty Program\",\n \"name\": \"Loyalty Program\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To view logs of Loyalty Points assigned to a Customer.\",\n \"label\": \"Loyalty Point Entry\",\n \"name\": \"Loyalty Point Entry\",\n \"type\": \"doctype\"\n }\n]" + "label": "Settings & Configurations", + "links": "[\n {\n \"description\": \"Setup default values for POS Invoices\",\n \"label\": \"Point-of-Sale Profile\",\n \"name\": \"POS Profile\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"POS Settings\",\n \"name\": \"POS Settings\",\n \"type\": \"doctype\"\n }\n]" + }, + { + "hidden": 0, + "label": "Loyalty Program", + "links": "[\n {\n \"description\": \"To make Customer based incentive schemes.\",\n \"label\": \"Loyalty Program\",\n \"name\": \"Loyalty Program\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To view logs of Loyalty Points assigned to a Customer.\",\n \"label\": \"Loyalty Point Entry\",\n \"name\": \"Loyalty Point Entry\",\n \"type\": \"doctype\"\n }\n]" + }, + { + "hidden": 0, + "label": "Opening & Closing", + "links": "[\n {\n \"label\": \"POS Opening Entry\",\n \"name\": \"POS Opening Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"POS Closing Entry\",\n \"name\": \"POS Closing Entry\",\n \"type\": \"doctype\"\n }\n]" } ], "category": "Domains", @@ -18,7 +28,7 @@ "idx": 0, "is_standard": 1, "label": "Retail", - "modified": "2020-08-20 18:00:07.515691", + "modified": "2020-09-09 11:46:28.297435", "modified_by": "Administrator", "module": "Selling", "name": "Retail", @@ -28,25 +38,10 @@ "restrict_to_domain": "Retail", "shortcuts": [ { - "color": "#9deca2", "doc_view": "", - "format": "{} Active", - "label": "Point of Sale Profile", - "link_to": "POS Profile", - "stats_filter": "{\n \"disabled\": 0\n}", - "type": "DocType" - }, - { - "doc_view": "", - "label": "Point of Sale", + "label": "Point Of Sale", "link_to": "point-of-sale", "type": "Page" - }, - { - "doc_view": "", - "label": "POS Settings", - "link_to": "POS Settings", - "type": "DocType" } ] } \ No newline at end of file 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 2ce0b270f9..8d4ac78422 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -9,7 +9,7 @@ frappe.pages['point-of-sale'].on_page_load = function(wrapper) { title: __('Point of Sale'), single_column: true }); - // online + wrapper.pos = new erpnext.PointOfSale.Controller(wrapper); window.cur_pos = wrapper.pos; }; \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index ae5471b900..5018254b0a 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -8,7 +8,7 @@ {% include "erpnext/selling/page/point_of_sale/pos_past_order_summary.js" %} erpnext.PointOfSale.Controller = class { - constructor(wrapper) { + constructor(wrapper) { this.wrapper = $(wrapper).find('.layout-main-section'); this.page = wrapper.page; @@ -36,7 +36,7 @@ erpnext.PointOfSale.Controller = class { const table_fields = [ { fieldname: "mode_of_payment", fieldtype: "Link", in_list_view: 1, label: "Mode of Payment", options: "Mode of Payment", reqd: 1 }, { fieldname: "opening_amount", fieldtype: "Currency", default: 0, in_list_view: 1, label: "Opening Amount", - options: "company:company_currency", reqd: 1 } + options: "company:company_currency" } ]; const dialog = new frappe.ui.Dialog({ @@ -51,29 +51,16 @@ erpnext.PointOfSale.Controller = class { options: 'POS Profile', fieldname: 'pos_profile', reqd: 1, onchange: () => { const pos_profile = dialog.fields_dict.pos_profile.get_value(); - const company = dialog.fields_dict.company.get_value(); - const user = frappe.session.user - if (!pos_profile || !company || !user) return; + if (!pos_profile) return; - // auto fetch last closing entry's balance details - frappe.db.get_list("POS Closing Entry", { - filters: { company, pos_profile, user }, - limit: 1, - order_by: 'period_end_date desc' - }).then((res) => { - if (!res.length) return; - const pos_closing_entry = res[0]; - frappe.db.get_doc("POS Closing Entry", pos_closing_entry.name).then(({ payment_reconciliation }) => { - dialog.fields_dict.balance_details.df.data = []; - payment_reconciliation.forEach(pay => { - const { mode_of_payment } = pay; - dialog.fields_dict.balance_details.df.data.push({ - mode_of_payment: mode_of_payment - }); - }); - dialog.fields_dict.balance_details.grid.refresh(); + frappe.db.get_doc("POS Profile", pos_profile).then(doc => { + dialog.fields_dict.balance_details.df.data = []; + doc.payments.forEach(pay => { + const { mode_of_payment } = pay; + dialog.fields_dict.balance_details.df.data.push({ mode_of_payment }); }); + dialog.fields_dict.balance_details.grid.refresh(); }); } }, diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index eadeb8fde8..724b60b973 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -1,36 +1,36 @@ erpnext.PointOfSale.ItemCart = class { - constructor({ wrapper, events }) { + constructor({ wrapper, events }) { this.wrapper = wrapper; this.events = events; - this.customer_info = undefined; - - this.init_component(); - } - - init_component() { - this.prepare_dom(); - this.init_child_components(); + this.customer_info = undefined; + + this.init_component(); + } + + init_component() { + this.prepare_dom(); + this.init_child_components(); this.bind_events(); this.attach_shortcuts(); - } + } - prepare_dom() { + prepare_dom() { this.wrapper.append( - `
    ` - ) + `
    ` + ) - this.$component = this.wrapper.find('.item-cart'); - } + this.$component = this.wrapper.find('.item-cart'); + } - init_child_components() { - this.init_customer_selector(); - this.init_cart_components(); - } + init_child_components() { + this.init_customer_selector(); + this.init_cart_components(); + } - init_customer_selector() { + init_customer_selector() { this.$component.append( - `
    ` - ) + `
    ` + ) this.$customer_section = this.$component.find('.customer-section'); } @@ -41,9 +41,9 @@ erpnext.PointOfSale.ItemCart = class { this.make_customer_selector(); this.customer_field.set_focus(); } - - init_cart_components() { - this.$component.append( + + init_cart_components() { + this.$component.append( `
    @@ -55,23 +55,23 @@ erpnext.PointOfSale.ItemCart = class {
    -
    ` - ); +
    ` + ); this.$cart_container = this.$component.find('.cart-container'); this.make_cart_totals_section(); this.make_cart_items_section(); - this.make_cart_numpad(); - } + this.make_cart_numpad(); + } - make_cart_items_section() { - this.$cart_header = this.$component.find('.cart-header'); - this.$cart_items_wrapper = this.$component.find('.cart-items-section'); + make_cart_items_section() { + this.$cart_header = this.$component.find('.cart-header'); + this.$cart_items_wrapper = this.$component.find('.cart-items-section'); this.make_no_items_placeholder(); - } - - make_no_items_placeholder() { + } + + make_no_items_placeholder() { this.$cart_header.addClass('d-none'); this.$cart_items_wrapper.html( `
    @@ -81,8 +81,8 @@ erpnext.PointOfSale.ItemCart = class { this.$cart_items_wrapper.addClass('mt-4 border-grey border-dashed'); } - make_cart_totals_section() { - this.$totals_section = this.$component.find('.cart-totals-section'); + make_cart_totals_section() { + this.$totals_section = this.$component.find('.cart-totals-section'); this.$totals_section.append( `
    @@ -116,9 +116,9 @@ erpnext.PointOfSale.ItemCart = class { ) this.$add_discount_elem = this.$component.find(".add-discount"); - } - - make_cart_numpad() { + } + + make_cart_numpad() { this.$numpad_section = this.$component.find('.numpad-section'); this.number_pad = new erpnext.PointOfSale.NumberPad({ @@ -155,9 +155,9 @@ erpnext.PointOfSale.ItemCart = class { Checkout
    ` ) - } - - bind_events() { + } + + bind_events() { const me = this; this.$customer_section.on('click', '.add-remove-customer', function (e) { const customer_info_is_visible = me.$cart_container.hasClass('d-none'); @@ -381,8 +381,8 @@ erpnext.PointOfSale.ItemCart = class { ` ); } - - update_customer_section() { + + update_customer_section() { const { customer, email_id='', mobile_no='', image } = this.customer_info || {}; if (customer) { @@ -403,7 +403,7 @@ erpnext.PointOfSale.ItemCart = class {
    ` ); } else { - // reset customer selector + // reset customer selector this.reset_customer_selector(); } @@ -430,9 +430,9 @@ erpnext.PointOfSale.ItemCart = class { ` } } - } - - update_totals_section(frm) { + } + + update_totals_section(frm) { if (!frm) frm = this.events.get_frm(); this.render_net_total(frm.doc.base_net_total); @@ -440,9 +440,9 @@ erpnext.PointOfSale.ItemCart = class { const taxes = frm.doc.taxes.map(t => { return { description: t.description, rate: t.rate }}) this.render_taxes(frm.doc.base_total_taxes_and_charges, taxes); - } - - render_net_total(value) { + } + + render_net_total(value) { const currency = this.events.get_frm().doc.currency; this.$totals_section.find('.net-total').html( `
    @@ -454,9 +454,9 @@ erpnext.PointOfSale.ItemCart = class { ) this.$numpad_section.find('.numpad-net-total').html(`Net Total: ${format_currency(value, currency)}`) - } - - render_grand_total(value) { + } + + render_grand_total(value) { const currency = this.events.get_frm().doc.currency; this.$totals_section.find('.grand-total').html( `
    @@ -495,20 +495,20 @@ erpnext.PointOfSale.ItemCart = class { } else { this.$totals_section.find('.taxes').html('') } - } + } - get_cart_item({ item_code, batch_no, uom }) { + get_cart_item({ item_code, batch_no, uom }) { const batch_attr = `[data-batch-no="${escape(batch_no)}"]`; const item_code_attr = `[data-item-code="${escape(item_code)}"]`; const uom_attr = `[data-uom=${escape(uom)}]`; - const item_selector = batch_no ? - `.cart-item-wrapper${batch_attr}${uom_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}`; - - return this.$cart_items_wrapper.find(item_selector); - } - - update_item_html(item, remove_item) { + const item_selector = batch_no ? + `.cart-item-wrapper${batch_attr}${uom_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}`; + + return this.$cart_items_wrapper.find(item_selector); + } + + update_item_html(item, remove_item) { const $item = this.get_cart_item(item); if (remove_item) { @@ -524,33 +524,33 @@ erpnext.PointOfSale.ItemCart = class { const no_of_cart_items = this.$cart_items_wrapper.children().length; no_of_cart_items > 0 && this.highlight_checkout_btn(no_of_cart_items > 0); - + this.update_empty_cart_section(no_of_cart_items); } - - render_cart_item(item_data, $item_to_update) { + + render_cart_item(item_data, $item_to_update) { const currency = this.events.get_frm().doc.currency; const me = this; - if (!$item_to_update.length) { - this.$cart_items_wrapper.append( - `
    -
    ` - ) - $item_to_update = this.get_cart_item(item_data); - } +
    ` + ) + $item_to_update = this.get_cart_item(item_data); + } $item_to_update.html( `
    -
    - ${item_data.item_name} -
    - ${get_description_html()} -
    - ${get_rate_discount_html()} -
    ` +
    + ${item_data.item_name} +
    + ${get_description_html()} + + ${get_rate_discount_html()} + ` ) set_dynamic_rate_header_width(); @@ -572,7 +572,7 @@ erpnext.PointOfSale.ItemCart = class { me.$cart_header.find(".rate-list-header").css("width", max_width); me.$cart_items_wrapper.find(".rate-col").css("width", max_width); } - + function get_rate_discount_html() { if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) { return ` @@ -625,7 +625,7 @@ erpnext.PointOfSale.ItemCart = class { $item_to_update.attr(`data-${selector}`, value); } - toggle_checkout_btn(show_checkout) { + toggle_checkout_btn(show_checkout) { if (show_checkout) { this.$totals_section.find('.checkout-btn').removeClass('d-none'); this.$totals_section.find('.edit-cart-btn').addClass('d-none'); @@ -635,7 +635,7 @@ erpnext.PointOfSale.ItemCart = class { } } - highlight_checkout_btn(toggle) { + highlight_checkout_btn(toggle) { const has_primary_class = this.$totals_section.find('.checkout-btn').hasClass('bg-primary'); if (toggle && !has_primary_class) { this.$totals_section.find('.checkout-btn').addClass('bg-primary text-white text-lg'); @@ -643,8 +643,8 @@ erpnext.PointOfSale.ItemCart = class { this.$totals_section.find('.checkout-btn').removeClass('bg-primary text-white text-lg'); } } - - update_empty_cart_section(no_of_cart_items) { + + update_empty_cart_section(no_of_cart_items) { const $no_item_element = this.$cart_items_wrapper.find('.no-item-wrapper'); // if cart has items and no item is present @@ -652,27 +652,27 @@ erpnext.PointOfSale.ItemCart = class { && this.$cart_items_wrapper.removeClass('mt-4 border-grey border-dashed') && this.$cart_header.removeClass('d-none'); no_of_cart_items === 0 && !$no_item_element.length && this.make_no_items_placeholder(); - } - - on_numpad_event($btn) { + } + + on_numpad_event($btn) { const current_action = $btn.attr('data-button-value'); const action_is_field_edit = ['qty', 'discount_percentage', 'rate'].includes(current_action); this.highlight_numpad_btn($btn, current_action); - const action_is_pressed_twice = this.prev_action === current_action; - const first_click_event = !this.prev_action; - const field_to_edit_changed = this.prev_action && this.prev_action != current_action; + const action_is_pressed_twice = this.prev_action === current_action; + const first_click_event = !this.prev_action; + const field_to_edit_changed = this.prev_action && this.prev_action != current_action; if (action_is_field_edit) { if (first_click_event || field_to_edit_changed) { - this.prev_action = current_action; + this.prev_action = current_action; } else if (action_is_pressed_twice) { this.prev_action = undefined; } - this.numpad_value = ''; - + this.numpad_value = ''; + } else if (current_action === 'checkout') { this.prev_action = undefined; this.toggle_item_highlight(); @@ -688,7 +688,7 @@ erpnext.PointOfSale.ItemCart = class { this.numpad_value = this.numpad_value || 0; } - const first_click_event_is_not_field_edit = !action_is_field_edit && first_click_event; + const first_click_event_is_not_field_edit = !action_is_field_edit && first_click_event; if (first_click_event_is_not_field_edit) { frappe.show_alert({ @@ -708,34 +708,34 @@ erpnext.PointOfSale.ItemCart = class { this.numpad_value = current_action; } - this.events.numpad_event(this.numpad_value, this.prev_action); - } - - highlight_numpad_btn($btn, curr_action) { - const curr_action_is_highlighted = $btn.hasClass('shadow-inner'); - const curr_action_is_action = ['qty', 'discount_percentage', 'rate', 'done'].includes(curr_action); + this.events.numpad_event(this.numpad_value, this.prev_action); + } + + highlight_numpad_btn($btn, curr_action) { + const curr_action_is_highlighted = $btn.hasClass('shadow-inner'); + const curr_action_is_action = ['qty', 'discount_percentage', 'rate', 'done'].includes(curr_action); - if (!curr_action_is_highlighted) { - $btn.addClass('shadow-inner bg-selected'); - } - if (this.prev_action === curr_action && curr_action_is_highlighted) { - // if Qty is pressed twice - $btn.removeClass('shadow-inner bg-selected'); - } - if (this.prev_action && this.prev_action !== curr_action && curr_action_is_action) { - // Order: Qty -> Rate then remove Qty highlight - const prev_btn = $(`[data-button-value='${this.prev_action}']`); - prev_btn.removeClass('shadow-inner bg-selected'); - } - if (!curr_action_is_action || curr_action === 'done') { - // if numbers are clicked - setTimeout(() => { - $btn.removeClass('shadow-inner bg-selected'); - }, 100); - } - } + if (!curr_action_is_highlighted) { + $btn.addClass('shadow-inner bg-selected'); + } + if (this.prev_action === curr_action && curr_action_is_highlighted) { + // if Qty is pressed twice + $btn.removeClass('shadow-inner bg-selected'); + } + if (this.prev_action && this.prev_action !== curr_action && curr_action_is_action) { + // Order: Qty -> Rate then remove Qty highlight + const prev_btn = $(`[data-button-value='${this.prev_action}']`); + prev_btn.removeClass('shadow-inner bg-selected'); + } + if (!curr_action_is_action || curr_action === 'done') { + // if numbers are clicked + setTimeout(() => { + $btn.removeClass('shadow-inner bg-selected'); + }, 100); + } + } - toggle_numpad(show) { + toggle_numpad(show) { if (show) { this.$totals_section.addClass('d-none'); this.$numpad_section.removeClass('d-none'); @@ -946,6 +946,6 @@ erpnext.PointOfSale.ItemCart = class { toggle_component(show) { show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none'); - } - + } + } diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index 86a1be9faf..3a5f89ba93 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -1,28 +1,28 @@ erpnext.PointOfSale.ItemDetails = class { - constructor({ wrapper, events }) { + constructor({ wrapper, events }) { this.wrapper = wrapper; - this.events = events; - this.current_item = {}; + this.events = events; + this.current_item = {}; - this.init_component(); - } + this.init_component(); + } - init_component() { - this.prepare_dom(); - this.init_child_components(); + init_component() { + this.prepare_dom(); + this.init_child_components(); this.bind_events(); this.attach_shortcuts(); - } + } - prepare_dom() { - this.wrapper.append( - `
    ` - ) + prepare_dom() { + this.wrapper.append( + `
    ` + ) - this.$component = this.wrapper.find('.item-details'); - } + this.$component = this.wrapper.find('.item-details'); + } - init_child_components() { + init_child_components() { this.$component.html( `
    @@ -49,28 +49,28 @@ erpnext.PointOfSale.ItemDetails = class { this.$item_image = this.$component.find('.item-image'); this.$form_container = this.$component.find('.form-container'); this.$dicount_section = this.$component.find('.discount-section'); - } + } - toggle_item_details_section(item) { + toggle_item_details_section(item) { const { item_code, batch_no, uom } = this.current_item; const item_code_is_same = item && item_code === item.item_code; const batch_is_same = item && batch_no == item.batch_no; const uom_is_same = item && uom === item.uom; - this.item_has_changed = !item ? false : item_code_is_same && batch_is_same && uom_is_same ? false : true; + this.item_has_changed = !item ? false : item_code_is_same && batch_is_same && uom_is_same ? false : true; - this.events.toggle_item_selector(this.item_has_changed); + this.events.toggle_item_selector(this.item_has_changed); this.toggle_component(this.item_has_changed); - + if (this.item_has_changed) { - this.doctype = item.doctype; + this.doctype = item.doctype; this.item_meta = frappe.get_meta(this.doctype); this.name = item.name; this.item_row = item; - this.currency = this.events.get_frm().doc.currency; - - this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom }; - + this.currency = this.events.get_frm().doc.currency; + + this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom }; + this.render_dom(item); this.render_discount_dom(item); this.render_form(item); @@ -102,9 +102,9 @@ erpnext.PointOfSale.ItemDetails = class { this.events.remove_item_from_cart(); } } - - render_dom(item) { - let { item_code ,item_name, description, image, price_list_rate } = item; + + render_dom(item) { + let { item_code ,item_name, description, image, price_list_rate } = item; function get_description_html() { if (description) { @@ -112,8 +112,8 @@ erpnext.PointOfSale.ItemDetails = class { return description; } return ``; - } - + } + this.$item_name.html(item_name); this.$item_description.html(get_description_html()); this.$item_price.html(format_currency(price_list_rate, this.currency)); @@ -125,9 +125,9 @@ erpnext.PointOfSale.ItemDetails = class { this.$item_image.html(frappe.get_abbr(item_code)); } - } - - render_discount_dom(item) { + } + + render_discount_dom(item) { if (item.discount_percentage) { this.$dicount_section.html( `
    @@ -141,9 +141,9 @@ erpnext.PointOfSale.ItemDetails = class { } else { this.$dicount_section.html(``) } - } + } - render_form(item) { + render_form(item) { const fields_to_display = this.get_form_fields(item); this.$form_container.html(''); @@ -157,7 +157,7 @@ erpnext.PointOfSale.ItemDetails = class { const field_meta = this.item_meta.fields.find(df => df.fieldname === fieldname); fieldname === 'discount_percentage' ? (field_meta.label = __('Discount (%)')) : ''; const me = this; - + this[`${fieldname}_control`] = frappe.ui.form.make_control({ df: { ...field_meta, @@ -174,16 +174,16 @@ erpnext.PointOfSale.ItemDetails = class { this.make_auto_serial_selection_btn(item); this.bind_custom_control_change_event(); - } + } - get_form_fields(item) { + get_form_fields(item) { const fields = ['qty', 'uom', 'rate', 'price_list_rate', 'discount_percentage', 'warehouse', 'actual_qty']; if (item.has_serial_no) fields.push('serial_no'); if (item.has_batch_no) fields.push('batch_no'); return fields; } - make_auto_serial_selection_btn(item) { + make_auto_serial_selection_btn(item) { if (item.has_serial_no) { this.$form_container.append( `
    ` @@ -203,8 +203,8 @@ erpnext.PointOfSale.ItemDetails = class { this.$form_container.find('.serial_no-control').parent().addClass('row-span-2'); } } - - bind_custom_control_change_event() { + + bind_custom_control_change_event() { const me = this; if (this.rate_control) { this.rate_control.df.onchange = function() { @@ -276,8 +276,8 @@ erpnext.PointOfSale.ItemDetails = class { }; this.batch_no_control.df.onchange = function() { me.events.set_value_in_current_cart_item('batch-no', this.value); - me.events.form_updated(me.doctype, me.name, 'batch_no', this.value); - me.current_item.batch_no = this.value; + me.events.form_updated(me.doctype, me.name, 'batch_no', this.value); + me.current_item.batch_no = this.value; } this.batch_no_control.refresh(); } @@ -289,9 +289,9 @@ erpnext.PointOfSale.ItemDetails = class { me.current_item.uom = this.value; } } - } - - async auto_update_batch_no() { + } + + async auto_update_batch_no() { if (this.serial_no_control && this.batch_no_control) { const selected_serial_nos = this.serial_no_control.get_value().split(`\n`).filter(s => s); if (!selected_serial_nos.length) return; @@ -310,9 +310,9 @@ erpnext.PointOfSale.ItemDetails = class { const batch_no = Object.keys(batch_serial_map)[0]; const batch_serial_nos = batch_serial_map[batch_no].join(`\n`); // eg. 10 selected serial no. -> 5 belongs to first batch other 5 belongs to second batch - const serial_nos_belongs_to_other_batch = selected_serial_nos.length !== batch_serial_map[batch_no].length; - - const current_batch_no = this.batch_no_control.get_value(); + const serial_nos_belongs_to_other_batch = selected_serial_nos.length !== batch_serial_map[batch_no].length; + + const current_batch_no = this.batch_no_control.get_value(); current_batch_no != batch_no && await this.batch_no_control.set_value(batch_no); if (serial_nos_belongs_to_other_batch) { @@ -326,8 +326,8 @@ erpnext.PointOfSale.ItemDetails = class { this.events.clone_new_batch_item_in_frm(batch_serial_map, this.current_item); } } - - bind_events() { + + bind_events() { this.bind_auto_serial_fetch_event(); this.bind_fields_to_numpad_fields(); @@ -345,7 +345,7 @@ erpnext.PointOfSale.ItemDetails = class { }); } - bind_fields_to_numpad_fields() { + bind_fields_to_numpad_fields() { const me = this; this.$form_container.on('click', '.input-with-feedback', function() { const fieldname = $(this).attr('data-fieldname'); @@ -355,8 +355,8 @@ erpnext.PointOfSale.ItemDetails = class { } }); } - - bind_auto_serial_fetch_event() { + + bind_auto_serial_fetch_event() { this.$form_container.on('click', '.auto-fetch-btn', () => { this.batch_no_control.set_value(''); let qty = this.qty_control.get_value(); @@ -382,7 +382,7 @@ erpnext.PointOfSale.ItemDetails = class { frappe.msgprint(`Fetched only ${records_length} available serial numbers.`); this.qty_control.set_value(records_length); } - numbers = auto_fetched_serial_numbers.join(`\n`); + numbers = auto_fetched_serial_numbers.join(`\n`); this.serial_no_control.set_value(numbers); }); }) @@ -390,5 +390,5 @@ erpnext.PointOfSale.ItemDetails = class { toggle_component(show) { show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none'); - } + } } \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index ee0c06d45d..c87b845a41 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -1,115 +1,115 @@ erpnext.PointOfSale.ItemSelector = class { - constructor({ frm, wrapper, events, pos_profile }) { + constructor({ frm, wrapper, events, pos_profile }) { this.wrapper = wrapper; this.events = events; - this.pos_profile = pos_profile; - - this.inti_component(); - } - - inti_component() { - this.prepare_dom(); - this.make_search_bar(); - this.load_items_data(); - this.bind_events(); - this.attach_shortcuts(); - } + this.pos_profile = pos_profile; + + this.inti_component(); + } + + inti_component() { + this.prepare_dom(); + this.make_search_bar(); + this.load_items_data(); + this.bind_events(); + this.attach_shortcuts(); + } - prepare_dom() { + prepare_dom() { this.wrapper.append( - `
    -
    -
    -
    -
    -
    -
    -
    ALL ITEMS
    -
    -
    -
    -
    -
    ` - ); - - this.$component = this.wrapper.find('.items-selector'); - } + `
    +
    +
    +
    +
    +
    +
    +
    ALL ITEMS
    +
    +
    +
    +
    +
    ` + ); + + this.$component = this.wrapper.find('.items-selector'); + } - async load_items_data() { - if (!this.item_group) { - const res = await frappe.db.get_value("Item Group", {lft: 1, is_group: 1}, "name"); - this.parent_item_group = res.message.name; - }; - if (!this.price_list) { - const res = await frappe.db.get_value("POS Profile", this.pos_profile, "selling_price_list"); - this.price_list = res.message.selling_price_list; - } + async load_items_data() { + if (!this.item_group) { + const res = await frappe.db.get_value("Item Group", {lft: 1, is_group: 1}, "name"); + this.parent_item_group = res.message.name; + }; + if (!this.price_list) { + const res = await frappe.db.get_value("POS Profile", this.pos_profile, "selling_price_list"); + this.price_list = res.message.selling_price_list; + } - this.get_items({}).then(({message}) => { - this.render_item_list(message.items); - }); - } + this.get_items({}).then(({message}) => { + this.render_item_list(message.items); + }); + } - get_items({start = 0, page_length = 40, search_value=''}) { - const price_list = this.events.get_frm().doc?.selling_price_list || this.price_list; - let { item_group, pos_profile } = this; + get_items({start = 0, page_length = 40, search_value=''}) { + const price_list = this.events.get_frm().doc?.selling_price_list || this.price_list; + let { item_group, pos_profile } = this; - !item_group && (item_group = this.parent_item_group); - + !item_group && (item_group = this.parent_item_group); + return frappe.call({ method: "erpnext.selling.page.point_of_sale.point_of_sale.get_items", freeze: true, - args: { start, page_length, price_list, item_group, search_value, pos_profile }, - }); + args: { start, page_length, price_list, item_group, search_value, pos_profile }, + }); } render_item_list(items) { - this.$items_container = this.$component.find('.items-container'); - this.$items_container.html(''); + this.$items_container = this.$component.find('.items-container'); + this.$items_container.html(''); - items.forEach(item => { - const item_html = this.get_item_html(item); - this.$items_container.append(item_html); - }) - } + items.forEach(item => { + const item_html = this.get_item_html(item); + this.$items_container.append(item_html); + }) + } - get_item_html(item) { - const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom } = item; - const indicator_color = actual_qty > 10 ? "green" : actual_qty !== 0 ? "orange" : "red"; + get_item_html(item) { + const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom } = item; + const indicator_color = actual_qty > 10 ? "green" : actual_qty !== 0 ? "orange" : "red"; - function get_item_image_html() { - if (item_image) { - return `
    - ${item_image} -
    ` - } else { - return `
    - ${frappe.get_abbr(item.item_name)} -
    ` - } - } + function get_item_image_html() { + if (item_image) { + return `
    + ${item_image} +
    ` + } else { + return `
    + ${frappe.get_abbr(item.item_name)} +
    ` + } + } return ( - `
    - ${get_item_image_html()} -
    -
    - - ${frappe.ellipsis(item.item_name, 18)} -
    -
    ${format_currency(item.price_list_rate, item.currency, 0) || 0}
    -
    -
    ` - ) - } + `
    + ${get_item_image_html()} +
    +
    + + ${frappe.ellipsis(item.item_name, 18)} +
    +
    ${format_currency(item.price_list_rate, item.currency, 0) || 0}
    +
    +
    ` + ) + } - make_search_bar() { - const me = this; - this.$component.find('.search-field').html(''); - this.$component.find('.item-group-field').html(''); + make_search_bar() { + const me = this; + this.$component.find('.search-field').html(''); + this.$component.find('.item-group-field').html(''); this.search_field = frappe.ui.form.make_control({ df: { @@ -119,104 +119,104 @@ erpnext.PointOfSale.ItemSelector = class { }, parent: this.$component.find('.search-field'), render_input: true, - }); + }); this.item_group_field = frappe.ui.form.make_control({ df: { label: __('Item Group'), fieldtype: 'Link', options: 'Item Group', - placeholder: __('Select item group'), - onchange: function() { - me.item_group = this.value; - !me.item_group && (me.item_group = me.parent_item_group); - me.filter_items(); - }, - get_query: function () { - return { - query: 'erpnext.selling.page.point_of_sale.point_of_sale.item_group_query', - filters: { - pos_profile: me.events.get_frm().doc?.pos_profile - } - } - }, + placeholder: __('Select item group'), + onchange: function() { + me.item_group = this.value; + !me.item_group && (me.item_group = me.parent_item_group); + me.filter_items(); + }, + get_query: function () { + return { + query: 'erpnext.selling.page.point_of_sale.point_of_sale.item_group_query', + filters: { + pos_profile: me.events.get_frm().doc?.pos_profile + } + } + }, }, - parent: this.$component.find('.item-group-field'), + parent: this.$component.find('.item-group-field'), render_input: true, - }); - this.search_field.toggle_label(false); + }); + this.search_field.toggle_label(false); this.item_group_field.toggle_label(false); } - bind_events() { - const me = this; - onScan.attachTo(document, { - onScan: (sScancode) => { - if (this.search_field && this.$component.is(':visible')) { - this.search_field.set_focus(); - $(this.search_field.$input[0]).val(sScancode).trigger("input"); - this.barcode_scanned = true; - } - } - }); + bind_events() { + const me = this; + onScan.attachTo(document, { + onScan: (sScancode) => { + if (this.search_field && this.$component.is(':visible')) { + this.search_field.set_focus(); + $(this.search_field.$input[0]).val(sScancode).trigger("input"); + this.barcode_scanned = true; + } + } + }); this.$component.on('click', '.item-wrapper', function() { const $item = $(this); const item_code = unescape($item.attr('data-item-code')); - let batch_no = unescape($item.attr('data-batch-no')); - let serial_no = unescape($item.attr('data-serial-no')); - let uom = unescape($item.attr('data-uom')); - - // escape(undefined) returns "undefined" then unescape returns "undefined" - batch_no = batch_no === "undefined" ? undefined : batch_no; - serial_no = serial_no === "undefined" ? undefined : serial_no; - uom = uom === "undefined" ? undefined : uom; + let batch_no = unescape($item.attr('data-batch-no')); + let serial_no = unescape($item.attr('data-serial-no')); + let uom = unescape($item.attr('data-uom')); + + // escape(undefined) returns "undefined" then unescape returns "undefined" + batch_no = batch_no === "undefined" ? undefined : batch_no; + serial_no = serial_no === "undefined" ? undefined : serial_no; + uom = uom === "undefined" ? undefined : uom; - me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom }}); - }) + me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom }}); + }) - this.search_field.$input.on('input', (e) => { + this.search_field.$input.on('input', (e) => { clearTimeout(this.last_search); this.last_search = setTimeout(() => { const search_term = e.target.value; this.filter_items({ search_term }); }, 300); - }); - } + }); + } - attach_shortcuts() { - frappe.ui.keys.on("ctrl+i", () => { - const selector_is_visible = this.$component.is(':visible'); - if (!selector_is_visible) return; - this.search_field.set_focus(); - }); - frappe.ui.keys.on("ctrl+g", () => { - const selector_is_visible = this.$component.is(':visible'); - if (!selector_is_visible) return; - this.item_group_field.set_focus(); - }); - // for selecting the last filtered item on search - frappe.ui.keys.on("enter", () => { - const selector_is_visible = this.$component.is(':visible'); - if (!selector_is_visible || this.search_field.get_value() === "") return; + attach_shortcuts() { + frappe.ui.keys.on("ctrl+i", () => { + const selector_is_visible = this.$component.is(':visible'); + if (!selector_is_visible) return; + this.search_field.set_focus(); + }); + frappe.ui.keys.on("ctrl+g", () => { + const selector_is_visible = this.$component.is(':visible'); + if (!selector_is_visible) return; + this.item_group_field.set_focus(); + }); + // for selecting the last filtered item on search + frappe.ui.keys.on("enter", () => { + const selector_is_visible = this.$component.is(':visible'); + if (!selector_is_visible || this.search_field.get_value() === "") return; - if (this.items.length == 1) { - this.$items_container.find(".item-wrapper").click(); - frappe.utils.play_sound("submit"); - $(this.search_field.$input[0]).val("").trigger("input"); - } else if (this.items.length == 0 && this.barcode_scanned) { - // only show alert of barcode is scanned and enter is pressed - frappe.show_alert({ - message: __("No items found. Scan barcode again."), - indicator: 'orange' - }); - frappe.utils.play_sound("error"); - this.barcode_scanned = false; - $(this.search_field.$input[0]).val("").trigger("input"); - } - }); - } - - filter_items({ search_term='' }={}) { + if (this.items.length == 1) { + this.$items_container.find(".item-wrapper").click(); + frappe.utils.play_sound("submit"); + $(this.search_field.$input[0]).val("").trigger("input"); + } else if (this.items.length == 0 && this.barcode_scanned) { + // only show alert of barcode is scanned and enter is pressed + frappe.show_alert({ + message: __("No items found. Scan barcode again."), + indicator: 'orange' + }); + frappe.utils.play_sound("error"); + this.barcode_scanned = false; + $(this.search_field.$input[0]).val("").trigger("input"); + } + }); + } + + filter_items({ search_term='' }={}) { if (search_term) { search_term = search_term.toLowerCase(); @@ -227,39 +227,39 @@ erpnext.PointOfSale.ItemSelector = class { this.items = items; this.render_item_list(items); return; - } + } } this.get_items({ search_value: search_term }) - .then(({ message }) => { - const { items, serial_no, batch_no, barcode } = message; + .then(({ message }) => { + const { items, serial_no, batch_no, barcode } = message; if (search_term && !barcode) { this.search_index[search_term] = items; } this.items = items; - this.render_item_list(items); - }); + this.render_item_list(items); + }); } - - resize_selector(minimize) { - minimize ? - this.$component.find('.search-field').removeClass('mr-8') : - this.$component.find('.search-field').addClass('mr-8'); + + resize_selector(minimize) { + minimize ? + this.$component.find('.search-field').removeClass('mr-8') : + this.$component.find('.search-field').addClass('mr-8'); - minimize ? - this.$component.find('.filter-section').addClass('flex-col') : - this.$component.find('.filter-section').removeClass('flex-col'); + minimize ? + this.$component.find('.filter-section').addClass('flex-col') : + this.$component.find('.filter-section').removeClass('flex-col'); - minimize ? - this.$component.removeClass('col-span-6').addClass('col-span-2') : - this.$component.removeClass('col-span-2').addClass('col-span-6') + minimize ? + this.$component.removeClass('col-span-6').addClass('col-span-2') : + this.$component.removeClass('col-span-2').addClass('col-span-6') - minimize ? - this.$items_container.removeClass('grid-cols-4').addClass('grid-cols-1') : - this.$items_container.removeClass('grid-cols-1').addClass('grid-cols-4') - } + minimize ? + this.$items_container.removeClass('grid-cols-4').addClass('grid-cols-1') : + this.$items_container.removeClass('grid-cols-1').addClass('grid-cols-4') + } - toggle_component(show) { + toggle_component(show) { show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none'); - } + } } \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/pos_number_pad.js b/erpnext/selling/page/point_of_sale/pos_number_pad.js index 2ffc2c0229..4b8e841805 100644 --- a/erpnext/selling/page/point_of_sale/pos_number_pad.js +++ b/erpnext/selling/page/point_of_sale/pos_number_pad.js @@ -1,49 +1,48 @@ erpnext.PointOfSale.NumberPad = class { - constructor({ wrapper, events, cols, keys, css_classes, fieldnames_map }) { - this.wrapper = wrapper; - this.events = events; - this.cols = cols; - this.keys = keys; - this.css_classes = css_classes || []; - this.fieldnames = fieldnames_map || {}; + constructor({ wrapper, events, cols, keys, css_classes, fieldnames_map }) { + this.wrapper = wrapper; + this.events = events; + this.cols = cols; + this.keys = keys; + this.css_classes = css_classes || []; + this.fieldnames = fieldnames_map || {}; - this.init_component(); - } + this.init_component(); + } - init_component() { - this.prepare_dom(); - this.bind_events(); - } + init_component() { + this.prepare_dom(); + this.bind_events(); + } - prepare_dom() { - const { cols, keys, css_classes, fieldnames } = this; + prepare_dom() { + const { cols, keys, css_classes, fieldnames } = this; - function get_keys() { - return keys.reduce((a, row, i) => { - return a + row.reduce((a2, number, j) => { - const class_to_append = css_classes && css_classes[i] ? css_classes[i][j] : ''; - const fieldname = fieldnames && fieldnames[number] ? - fieldnames[number] : - typeof number === 'string' ? frappe.scrub(number) : number; - - return a2 + `
    ${number}
    ` - }, '') - }, ''); - } + function get_keys() { + return keys.reduce((a, row, i) => { + return a + row.reduce((a2, number, j) => { + const class_to_append = css_classes && css_classes[i] ? css_classes[i][j] : ''; + const fieldname = fieldnames && fieldnames[number] ? + fieldnames[number] : typeof number === 'string' ? frappe.scrub(number) : number; - this.wrapper.html( - `
    - ${get_keys()} -
    ` - ) - } + return a2 + `
    ${number}
    ` + }, '') + }, ''); + } - bind_events() { - const me = this; - this.wrapper.on('click', '.numpad-btn', function() { - const $btn = $(this); - me.events.numpad_event($btn); - }) - } + this.wrapper.html( + `
    + ${get_keys()} +
    ` + ) + } + + bind_events() { + const me = this; + this.wrapper.on('click', '.numpad-btn', function() { + const $btn = $(this); + me.events.numpad_event($btn); + }); + } } \ No newline at end of file From ed08e593c37c6cb213007f32578cfe8db9621a77 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Fri, 11 Sep 2020 13:59:07 +0530 Subject: [PATCH 140/192] feat: added project in Sales Analytics report --- .../report/sales_analytics/sales_analytics.js | 2 +- .../report/sales_analytics/sales_analytics.py | 28 ++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.js b/erpnext/selling/report/sales_analytics/sales_analytics.js index 80874c1deb..0e565a3fb6 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.js +++ b/erpnext/selling/report/sales_analytics/sales_analytics.js @@ -8,7 +8,7 @@ frappe.query_reports["Sales Analytics"] = { fieldname: "tree_type", label: __("Tree Type"), fieldtype: "Select", - options: ["Customer Group","Customer","Item Group","Item","Territory","Order Type"], + options: ["Customer Group", "Customer", "Item Group", "Item", "Territory", "Order Type", "Project"], default: "Customer", reqd: 1 }, diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index 4d113c8e9e..dbaf2acab9 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -34,7 +34,7 @@ class Analytics(object): def get_columns(self): self.columns = [{ - "label": _(self.filters.tree_type + " ID"), + "label": _(self.filters.tree_type), "options": self.filters.tree_type if self.filters.tree_type != "Order Type" else "", "fieldname": "entity", "fieldtype": "Link" if self.filters.tree_type != "Order Type" else "Data", @@ -97,6 +97,10 @@ class Analytics(object): self.get_sales_transactions_based_on_order_type() self.get_rows_by_group() + elif self.filters.tree_type == "Project": + self.get_sales_transactions_based_on_project() + self.get_rows() + def get_sales_transactions_based_on_order_type(self): if self.filters["value_quantity"] == 'Value': value_field = "base_net_total" @@ -198,6 +202,28 @@ class Analytics(object): self.get_groups() + def get_sales_transactions_based_on_project(self): + if self.filters["value_quantity"] == 'Value': + value_field = "base_net_total as value_field" + else: + value_field = "total_qty as value_field" + + entity = "project as entity" + + self.entries = frappe.get_all(self.filters.doc_type, + fields=[entity, value_field, self.date_field], + filters={ + "docstatus": 1, + "company": self.filters.company, + "project": ["!=", ""], + self.date_field: ('between', [self.filters.from_date, self.filters.to_date]) + }, debug =True + ) + + self.entity_names = {} + for d in self.entries: + self.entity_names.setdefault(d.entity, d.entity_name) + def get_rows(self): self.data = [] self.get_periodic_data() From 16f1435d630eff379ff809b975bec430fccc6c7e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 11 Sep 2020 18:57:58 +0200 Subject: [PATCH 141/192] fix: codacy --- erpnext/regional/germany/utils/datev/datev_csv.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/germany/utils/datev/datev_csv.py b/erpnext/regional/germany/utils/datev/datev_csv.py index df95a5b95a..aae734f8e2 100644 --- a/erpnext/regional/germany/utils/datev/datev_csv.py +++ b/erpnext/regional/germany/utils/datev/datev_csv.py @@ -3,18 +3,19 @@ from __future__ import unicode_literals import datetime import zipfile +from csv import QUOTE_NONNUMERIC +from six import BytesIO + import six import frappe import pandas as pd from frappe import _ -from csv import QUOTE_NONNUMERIC -from six import BytesIO from .datev_constants import DataCategory -from .datev_constants import Transactions def get_datev_csv(data, filters, csv_class): - """Fill in missing columns and return a CSV in DATEV Format. + """ + Fill in missing columns and return a CSV in DATEV Format. For automatic processing, DATEV requires the first line of the CSV file to hold meta data such as the length of account numbers oder the category of @@ -89,7 +90,7 @@ def get_header(filters, csv_class): # Format version (regarding format name) csv_class.FORMAT_VERSION, # Generated on - datetime.datetime.now().strftime("%Y%m%d%H%M%S") + '000', + datetime.datetime.now().strftime('%Y%m%d%H%M%S') + '000', # Imported on -- stays empty '', # Origin. Any two symbols, will be replaced by "SV" on import. @@ -155,7 +156,8 @@ def get_header(filters, csv_class): def download_csv_files_as_zip(csv_data_list): - """Put CSV files in a zip archive and send that to the client. + """ + Put CSV files in a zip archive and send that to the client. Params: csv_data_list -- list of dicts [{'file_name': 'EXTF_Buchunsstapel.zip', 'csv_data': get_datev_csv()}] From a91225f1522a877ab68fd95ac4a33831555cba06 Mon Sep 17 00:00:00 2001 From: Kwabena Adu-Darkwa Date: Fri, 11 Sep 2020 17:47:32 +0000 Subject: [PATCH 142/192] inventory management link updated --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 80ebdb6b2a..0f6a52142b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ERPNext as a monolith includes the following areas for managing businesses: 1. [Accounting](https://erpnext.com/open-source-accounting) -1. [Inventory](https://erpnext.com/distribution/inventory-management-system) +1. [Warehouse Management](https://erpnext.com/distribution/warehouse-management-system) 1. [CRM](https://erpnext.com/open-source-crm) 1. [Sales](https://erpnext.com/open-source-sales-purchase) 1. [Purchase](https://erpnext.com/open-source-sales-purchase) From 0a688005be30440ad0be86ca80911b3807281f33 Mon Sep 17 00:00:00 2001 From: marination Date: Sat, 12 Sep 2020 15:39:27 +0530 Subject: [PATCH 143/192] 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 60f6a68f49..19d0bec26c 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -130,12 +130,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 50c18f6282..c504e23677 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -116,12 +116,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 9205101ecffbfa358fcd9a0eefa2a297c3da29ce Mon Sep 17 00:00:00 2001 From: marination Date: Sat, 12 Sep 2020 16:17:13 +0530 Subject: [PATCH 144/192] fix: Make Reference fields mandatory in Quality Inspection --- .../doctype/quality_inspection/quality_inspection.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json index c951066aa8..3643174fb4 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json @@ -73,7 +73,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", @@ -84,7 +85,8 @@ "label": "Reference Name", "oldfieldname": "purchase_receipt_no", "oldfieldtype": "Link", - "options": "reference_type" + "options": "reference_type", + "reqd": 1 }, { "fieldname": "section_break_7", @@ -231,9 +233,10 @@ ], "icon": "fa fa-search", "idx": 1, + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-04-26 17:50:25.068222", + "modified": "2020-09-12 16:11:31.910508", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection", From 6db92fbb6a38b40818022c607a02ccf1f30e92f0 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 14 Sep 2020 19:54:17 +0530 Subject: [PATCH 145/192] feat: validate workflow before so/po update items (#23024) * feat: validate workflow before so/po update items * fix: incorrect workflow validation on so/po update items Co-authored-by: Marica --- 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 d61e44b53d..5244777558 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 @@ -1194,7 +1195,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: @@ -1221,18 +1222,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): @@ -1245,17 +1265,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")) @@ -1362,6 +1383,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 accaa9c3b3..34aaf08395 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -417,7 +417,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 @@ -973,3 +1008,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 fcb6ba6ec7fb245f201a22374d1c4a0c8227905e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 14 Sep 2020 21:12:36 +0530 Subject: [PATCH 146/192] fix: get_transaction_entries function arguments in Bank Statement Transaction Entry (#23051) --- .../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 5b6eb9dc24..27dd8e463f 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 b7cd84196d79f4a728a9a3aaf1504e62601aac5e Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Sep 2020 09:42:51 +0530 Subject: [PATCH 147/192] fix: Revert tax calculation changes --- erpnext/controllers/taxes_and_totals.py | 10 +++------- erpnext/public/js/controllers/taxes_and_totals.js | 9 ++------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 59c60f7fdc..f578e7eac4 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -220,14 +220,10 @@ class calculate_taxes_and_totals(object): return current_tax_fraction, inclusive_tax_amount_per_qty def _get_tax_rate(self, tax, item_tax_map): - if item_tax_map: - if tax.account_head in item_tax_map: - return flt(item_tax_map.get(tax.account_head), self.doc.precision("rate", tax)) - else: - return tax.rate + if tax.account_head in item_tax_map: + return flt(item_tax_map.get(tax.account_head), self.doc.precision("rate", tax)) else: - # If no item tax template against item dont calculate tax against it - return 0 + return tax.rate def calculate_net_total(self): self.doc.total_qty = self.doc.total = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0 diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 87f6d6a947..3bc70216db 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -223,13 +223,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ }, _get_tax_rate: function(tax, item_tax_map) { - if(!$.isEmptyObject(item_tax_map)) { - return (Object.keys(item_tax_map).indexOf(tax.account_head) != -1) ? - flt(item_tax_map[tax.account_head], precision("rate", tax)) : tax.rate; - } else { - // If no item tax template against item dont calculate tax against it - return 0; - } + return (Object.keys(item_tax_map).indexOf(tax.account_head) != -1) ? + flt(item_tax_map[tax.account_head], precision("rate", tax)) : tax.rate; }, calculate_net_total: function() { From 36bc0577a2a0c11fa10d669d76172cbb62a8ae90 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 15 Sep 2020 09:44:46 +0530 Subject: [PATCH 148/192] fix: Item GL entries for purchase invoice (#23027) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index b4ee7c999e..079f599706 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -405,8 +405,6 @@ class PurchaseInvoice(BuyingController): update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) def make_gl_entries(self, gl_entries=None): - if not self.grand_total: - return if not gl_entries: gl_entries = self.get_gl_entries() From 61314248bb733191f58cf033e2b0312bcf1de481 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 15 Sep 2020 11:14:31 +0530 Subject: [PATCH 149/192] feat: uom in update items for SO & PO (#22869) * feat: uom in update items for SO & PO * fix: supplied items doesn't updates on uom change * chore: add uom and conv factor change test * fix: test * fix: purchase order updates are not tracked * fix: fetch conversion factor on uom change * fix: codacy Co-authored-by: Marica --- .../purchase_order/purchase_order.json | 5 ++-- .../purchase_order/test_purchase_order.py | 27 +++++++++++++++--- erpnext/controllers/accounts_controller.py | 21 ++++++++++---- erpnext/public/js/utils.js | 28 +++++++++++++++++++ .../doctype/sales_order/test_sales_order.py | 24 ++++++++++++---- 5 files changed, 89 insertions(+), 16 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 4201e0b635..d1063b1503 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -1084,7 +1084,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2020-07-31 14:13:44.610190", + "modified": "2020-09-14 14:36:12.418690", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", @@ -1135,5 +1135,6 @@ "sort_field": "modified", "sort_order": "DESC", "timeline_field": "supplier", - "title_field": "supplier" + "title_field": "supplier", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 813286f7fa..69817a4675 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -89,7 +89,7 @@ class TestPurchaseOrder(unittest.TestCase): frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0) - def test_update_child_qty_rate(self): + def test_update_child(self): mr = make_material_request(qty=10) po = make_purchase_order(mr.name) po.supplier = "_Test Supplier" @@ -119,7 +119,7 @@ class TestPurchaseOrder(unittest.TestCase): self.assertEqual(get_ordered_qty(), existing_ordered_qty + 3) - def test_add_new_item_in_update_child_qty_rate(self): + def test_update_child_adding_new_item(self): po = create_purchase_order(do_not_save=1) po.items[0].qty = 4 po.save() @@ -145,7 +145,7 @@ class TestPurchaseOrder(unittest.TestCase): self.assertEqual(po.status, 'To Receive and Bill') - def test_remove_item_in_update_child_qty_rate(self): + def test_update_child_removing_item(self): po = create_purchase_order(do_not_save=1) po.items[0].qty = 4 po.save() @@ -185,7 +185,7 @@ class TestPurchaseOrder(unittest.TestCase): self.assertEquals(len(po.get('items')), 1) self.assertEqual(po.status, 'To Receive and Bill') - def test_update_child_qty_rate_perm(self): + def test_update_child_perm(self): po = create_purchase_order(item_code= "_Test Item", qty=4) user = 'test@example.com' @@ -202,6 +202,25 @@ class TestPurchaseOrder(unittest.TestCase): self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Purchase Order', trans_item, po.name) frappe.set_user("Administrator") + def test_update_child_uom_conv_factor_change(self): + po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes") + total_reqd_qty = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")]) + + trans_item = json.dumps([{ + 'item_code': po.get("items")[0].item_code, + 'rate': po.get("items")[0].rate, + 'qty': po.get("items")[0].qty, + 'uom': "_Test UOM 1", + 'conversion_factor': 2, + 'docname': po.get("items")[0].name + }]) + update_child_qty_rate('Purchase Order', trans_item, po.name) + po.reload() + + total_reqd_qty_after_change = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")]) + + self.assertEqual(total_reqd_qty_after_change, 2 * total_reqd_qty) + def test_update_qty(self): po = create_purchase_order() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 5244777558..9093cd570a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1169,8 +1169,9 @@ def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, child_item.item_name = item.item_name child_item.description = item.description child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date - child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0 - child_item.uom = item.stock_uom + child_item.uom = trans_item.get("uom") or item.stock_uom + conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor")) + child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) if not child_item.warehouse: frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.") @@ -1189,8 +1190,9 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna child_item.item_name = item.item_name child_item.description = item.description child_item.schedule_date = trans_item.get('schedule_date') or p_doc.schedule_date - child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0 - child_item.uom = item.stock_uom + child_item.uom = trans_item.get("uom") or item.stock_uom + conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor")) + child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor child_item.base_rate = 1 # Initiallize value will update in parent validation child_item.base_amount = 1 # Initiallize value will update in parent validation return child_item @@ -1282,6 +1284,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate")) prev_qty, new_qty = flt(child_item.get("qty")), flt(d.get("qty")) prev_con_fac, new_con_fac = flt(child_item.get("conversion_factor")), flt(d.get("conversion_factor")) + prev_uom, new_uom = child_item.get("uom"), d.get("uom") if parent_doctype == 'Sales Order': prev_date, new_date = child_item.get("delivery_date"), d.get("delivery_date") @@ -1290,9 +1293,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil rate_unchanged = prev_rate == new_rate qty_unchanged = prev_qty == new_qty + uom_unchanged = prev_uom == new_uom conversion_factor_unchanged = prev_con_fac == new_con_fac date_unchanged = prev_date == new_date if prev_date and new_date else False # in case of delivery note etc - if rate_unchanged and qty_unchanged and conversion_factor_unchanged and date_unchanged: + if rate_unchanged and qty_unchanged and conversion_factor_unchanged and uom_unchanged and date_unchanged: continue validate_quantity(child_item, d) @@ -1311,6 +1315,11 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil child_item.conversion_factor = 1 else: child_item.conversion_factor = flt(d.get('conversion_factor')) + + if d.get("uom"): + child_item.uom = d.get("uom") + conversion_factor = flt(get_conversion_factor(child_item.item_code, child_item.uom).get("conversion_factor")) + child_item.conversion_factor = flt(d.get('conversion_factor')) or conversion_factor if d.get("delivery_date") and parent_doctype == 'Sales Order': child_item.delivery_date = d.get('delivery_date') @@ -1377,6 +1386,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.update_receiving_percentage() if parent.is_subcontracted == "Yes": parent.update_reserved_qty_for_subcontract() + parent.create_raw_materials_supplied("supplied_items") + parent.save() else: parent.update_reserved_qty() parent.update_project() diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index bcab0d84c6..87982f14a6 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -466,6 +466,33 @@ erpnext.utils.update_child_items = function(opts) { read_only: 0, disabled: 0, label: __('Item Code') + }, { + fieldtype:'Link', + fieldname:'uom', + options: 'UOM', + read_only: 0, + label: __('UOM'), + reqd: 1, + onchange: function () { + frappe.call({ + method: "erpnext.stock.get_item_details.get_conversion_factor", + args: { item_code: this.doc.item_code, uom: this.value }, + callback: r => { + if(!r.exc) { + if (this.doc.conversion_factor == r.message.conversion_factor) return; + + const docname = this.doc.docname; + dialog.fields_dict.trans_items.df.data.some(doc => { + if (doc.docname == docname) { + doc.conversion_factor = r.message.conversion_factor; + dialog.fields_dict.trans_items.grid.refresh(); + return true; + } + }) + } + } + }); + } }, { fieldtype:'Float', fieldname:"qty", @@ -546,6 +573,7 @@ erpnext.utils.update_child_items = function(opts) { "conversion_factor": d.conversion_factor, "qty": d.qty, "rate": d.rate, + "uom": d.uom }); this.data = dialog.fields_dict.trans_items.df.data; dialog.fields_dict.trans_items.grid.refresh(); diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 34aaf08395..735b071f44 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -318,7 +318,7 @@ class TestSalesOrder(unittest.TestCase): self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20) - def test_add_new_item_in_update_child_qty_rate(self): + def test_update_child_adding_new_item(self): so = make_sales_order(item_code= "_Test Item", qty=4) create_dn_against_so(so.name, 4) make_sales_invoice(so.name) @@ -338,7 +338,7 @@ class TestSalesOrder(unittest.TestCase): self.assertEqual(so.get("items")[-1].amount, 1400) self.assertEqual(so.status, 'To Deliver and Bill') - def test_remove_item_in_update_child_qty_rate(self): + def test_update_child_removing_item(self): so = make_sales_order(**{ "item_list": [{ "item_code": '_Test Item', @@ -381,7 +381,7 @@ class TestSalesOrder(unittest.TestCase): self.assertEqual(so.status, 'To Deliver and Bill') - def test_update_child_qty_rate(self): + def test_update_child(self): so = make_sales_order(item_code= "_Test Item", qty=4) create_dn_against_so(so.name, 4) make_sales_invoice(so.name) @@ -402,7 +402,7 @@ class TestSalesOrder(unittest.TestCase): trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 2, 'docname': so.items[0].name}]) self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) - def test_update_child_qty_rate_perm(self): + def test_update_child_perm(self): so = make_sales_order(item_code= "_Test Item", qty=4) user = 'test@example.com' @@ -454,7 +454,7 @@ class TestSalesOrder(unittest.TestCase): workflow.is_active = 0 workflow.save() - def test_update_child_qty_rate_product_bundle(self): + def test_update_child_product_bundle(self): # test Update Items with product bundle if not frappe.db.exists("Item", "_Product Bundle Item"): bundle_item = make_item("_Product Bundle Item", {"is_stock_item": 0}) @@ -474,6 +474,20 @@ class TestSalesOrder(unittest.TestCase): so.reload() self.assertEqual(so.packed_items[0].qty, 4) + # test uom and conversion factor change + update_uom_conv_factor = json.dumps([{ + 'item_code': so.get("items")[0].item_code, + 'rate': so.get("items")[0].rate, + 'qty': so.get("items")[0].qty, + 'uom': "_Test UOM 1", + 'conversion_factor': 2, + 'docname': so.get("items")[0].name + }]) + update_child_qty_rate('Sales Order', update_uom_conv_factor, so.name) + + so.reload() + self.assertEqual(so.packed_items[0].qty, 8) + def test_warehouse_user(self): frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com") frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", "test2@example.com") From 4bb8d0c193ab9e967ad224a0e37743f1ef2cd648 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Sep 2020 11:18:02 +0530 Subject: [PATCH 150/192] chore: Test for PO --- .../purchase_order/test_purchase_order.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 813286f7fa..71d100dd05 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -202,6 +202,49 @@ class TestPurchaseOrder(unittest.TestCase): self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Purchase Order', trans_item, po.name) frappe.set_user("Administrator") + def test_update_child_with_tax_template(self): + tax_template = "_Test Account Excise Duty @ 10" + item = "_Test Item Home Desktop 100" + + if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}): + item_doc = frappe.get_doc("Item", item) + item_doc.append("taxes", { + "item_tax_template": tax_template, + "valid_from": nowdate() + }) + item_doc.save() + else: + # update valid from + frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = CURDATE() + where parent = %(item)s and item_tax_template = %(tax)s""", + {"item": item, "tax": tax_template}) + + po = create_purchase_order(item_code=item, qty=1, do_not_save=1) + + po.append("taxes", { + "account_head": "_Test Account Excise Duty - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Excise Duty", + "doctype": "Purchase Taxes and Charges", + "rate": 10 + }) + po.insert() + po.submit() + + self.assertEqual(po.taxes[0].tax_amount, 50) + self.assertEqual(po.taxes[0].total, 550) + + items = json.dumps([ + {'item_code' : item, 'rate' : 500, 'qty' : 1, 'docname': po.items[0].name}, + {'item_code' : item, 'rate' : 100, 'qty' : 1} # added item + ]) + update_child_qty_rate('Purchase Order', items, po.name) + + po.reload() + self.assertEqual(po.taxes[0].tax_amount, 60) + self.assertEqual(po.taxes[0].total, 660) + def test_update_qty(self): po = create_purchase_order() From 665c27af58dd2cdd581209eeecbcfd3bca1d2426 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Sep 2020 12:04:38 +0530 Subject: [PATCH 151/192] fix: Merge conflict missing line --- erpnext/controllers/accounts_controller.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 72e5df85e7..046fb2ca68 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1184,6 +1184,7 @@ def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, child_item.uom = trans_item.get("uom") or item.stock_uom conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor")) child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor + set_child_tax_template_and_map(item, child_item, p_doc) child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) if not child_item.warehouse: frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.") @@ -1245,7 +1246,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil 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: @@ -1280,7 +1281,7 @@ 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_doc_permissions(parent, 'cancel') validate_and_delete_children(parent, data) @@ -1328,7 +1329,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil child_item.conversion_factor = 1 else: child_item.conversion_factor = flt(d.get('conversion_factor')) - + if d.get("uom"): child_item.uom = d.get("uom") conversion_factor = flt(get_conversion_factor(child_item.item_code, child_item.uom).get("conversion_factor")) From bae2d5d485646ee39798eda865fe6b84158450e3 Mon Sep 17 00:00:00 2001 From: Anupam Date: Tue, 15 Sep 2020 12:12:31 +0530 Subject: [PATCH 152/192] fix: review changes --- erpnext/selling/report/sales_analytics/sales_analytics.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index dbaf2acab9..d036a1cb09 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -217,13 +217,9 @@ class Analytics(object): "company": self.filters.company, "project": ["!=", ""], self.date_field: ('between', [self.filters.from_date, self.filters.to_date]) - }, debug =True + } ) - self.entity_names = {} - for d in self.entries: - self.entity_names.setdefault(d.entity, d.entity_name) - def get_rows(self): self.data = [] self.get_periodic_data() @@ -231,7 +227,7 @@ class Analytics(object): for entity, period_data in iteritems(self.entity_periodic_data): row = { "entity": entity, - "entity_name": self.entity_names.get(entity) + "entity_name": self.entity_names.get(entity) if hasattr(self, 'entity_names') else None } total = 0 for end_date in self.periodic_daterange: From 9397b7f58bdb8f3c32170be3da9423f3bbc8910d Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 15 Sep 2020 12:36:01 +0530 Subject: [PATCH 153/192] fix: requested changes --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 842c64fdbe..b42efdfe8b 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -453,7 +453,7 @@ class PaymentEntry(AccountsController): frappe.throw(_("Reference No and Reference Date is mandatory for Bank transaction")) def set_remarks(self): - if self.remarks: return + if self.custom_remarks: return if self.payment_type=="Internal Transfer": remarks = [_("Amount {0} {1} transferred from {2} to {3}") From 4f3b7da9baea791d2f5bb2731989e730ba491c2f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 15 Sep 2020 14:47:36 +0530 Subject: [PATCH 154/192] fix: set_taxes() missing 1 required positional argument: 'company' --- erpnext/public/js/utils/party.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index 065326744c..af1f433514 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -224,6 +224,10 @@ erpnext.utils.set_taxes = function(frm, triggered_from_field) { party = frm.doc.party_name; } + if (!frm.doc.company) { + frappe.throw(_("Kindly select the company first")); + } + frappe.call({ method: "erpnext.accounts.party.set_taxes", args: { From 2f50cb3e58458a9679bfc6022c15f910b757aa5b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 15 Sep 2020 15:00:32 +0530 Subject: [PATCH 155/192] fix: Supplier trigger onload --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 2bfa4a572e..fe5301d5c8 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -28,7 +28,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ // Trigger supplier event on load if supplier is available // The reason for this is PI can be created from PR or PO and supplier is pre populated - if (this.frm.doc.supplier) { + if (this.frm.doc.supplier && this.frm.doc.__islocal) { this.frm.trigger('supplier'); } }, From f995f517c95a9995e5d16333b341d9a79072f836 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Sep 2020 17:06:27 +0530 Subject: [PATCH 156/192] fix: Test --- erpnext/buying/doctype/purchase_order/test_purchase_order.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 83f002e358..158799ce63 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -245,6 +245,10 @@ class TestPurchaseOrder(unittest.TestCase): self.assertEqual(po.taxes[0].tax_amount, 60) self.assertEqual(po.taxes[0].total, 660) + frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL + where parent = %(item)s and item_tax_template = %(tax)s""", + {"item": item, "tax": tax_template}) + def test_update_child_uom_conv_factor_change(self): po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes") total_reqd_qty = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")]) From c78ca36ee5a35c8bff6547037ec0a1120d521af4 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 16 Sep 2020 23:47:33 +0530 Subject: [PATCH 157/192] fix(Non Profit): Hide Address and Contact section for unsaved docs --- erpnext/non_profit/doctype/donor/donor.json | 407 +++-------- erpnext/non_profit/doctype/member/member.json | 3 +- .../doctype/volunteer/volunteer.json | 678 ++++-------------- 3 files changed, 213 insertions(+), 875 deletions(-) diff --git a/erpnext/non_profit/doctype/donor/donor.json b/erpnext/non_profit/doctype/donor/donor.json index 7e24dacbe1..96392658f1 100644 --- a/erpnext/non_profit/doctype/donor/donor.json +++ b/erpnext/non_profit/doctype/donor/donor.json @@ -1,336 +1,105 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 1, - "autoname": "field:email", - "beta": 0, - "creation": "2017-09-19 16:20:27.510196", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_rename": 1, + "autoname": "field:email", + "creation": "2017-09-19 16:20:27.510196", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "donor_name", + "column_break_5", + "donor_type", + "email", + "image", + "address_contacts", + "address_html", + "column_break_9", + "contact_html" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "donor_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Donor Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "donor_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Donor Name", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_5", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "donor_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Donor Type", - "length": 0, - "no_copy": 0, - "options": "Donor Type", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "donor_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Donor Type", + "options": "Donor Type", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "email", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Email", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "email", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Email", + "reqd": 1, + "unique": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "image", - "fieldtype": "Attach Image", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Image", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "image", + "fieldtype": "Attach Image", + "hidden": 1, + "label": "Image", + "no_copy": 1, + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address_contacts", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Address and Contact", - "length": 0, - "no_copy": 0, - "options": "fa fa-map-marker", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "depends_on": "eval:!doc.__islocal;", + "fieldname": "address_contacts", + "fieldtype": "Section Break", + "label": "Address and Contact", + "options": "fa fa-map-marker" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address_html", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Address HTML", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "address_html", + "fieldtype": "HTML", + "label": "Address HTML" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_9", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "contact_html", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Contact HTML", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "contact_html", + "fieldtype": "HTML", + "label": "Contact HTML" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_field": "image", - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-01-22 15:53:35.059946", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Donor", - "name_case": "", - "owner": "Administrator", + ], + "image_field": "image", + "links": [], + "modified": "2020-09-16 23:46:04.083274", + "modified_by": "Administrator", + "module": "Non Profit", + "name": "Donor", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Non Profit Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Non Profit", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "donor_name", - "track_changes": 1, - "track_seen": 0 + ], + "quick_entry": 1, + "restrict_to_domain": "Non Profit", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "donor_name", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json index 77cdb94b3d..992ef16d64 100644 --- a/erpnext/non_profit/doctype/member/member.json +++ b/erpnext/non_profit/doctype/member/member.json @@ -111,6 +111,7 @@ "options": "Supplier" }, { + "depends_on": "eval:!doc.__islocal;", "fieldname": "address_contacts", "fieldtype": "Section Break", "label": "Address and Contact", @@ -177,7 +178,7 @@ ], "image_field": "image", "links": [], - "modified": "2020-08-06 10:06:01.153564", + "modified": "2020-09-16 23:44:13.596948", "modified_by": "Administrator", "module": "Non Profit", "name": "Member", diff --git a/erpnext/non_profit/doctype/volunteer/volunteer.json b/erpnext/non_profit/doctype/volunteer/volunteer.json index 052b288089..08b7f87b2a 100644 --- a/erpnext/non_profit/doctype/volunteer/volunteer.json +++ b/erpnext/non_profit/doctype/volunteer/volunteer.json @@ -1,580 +1,148 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 1, - "autoname": "field:email", - "beta": 0, - "creation": "2017-09-19 16:16:45.676019", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_rename": 1, + "autoname": "field:email", + "creation": "2017-09-19 16:16:45.676019", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "volunteer_name", + "column_break_5", + "volunteer_type", + "email", + "image", + "address_contacts", + "address_html", + "column_break_9", + "contact_html", + "volunteer_availability_and_skills_details", + "availability", + "availability_timeslot", + "column_break_12", + "volunteer_skills", + "section_break_15", + "note" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "volunteer_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Volunteer Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "volunteer_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Volunteer Name", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_5", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "volunteer_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Volunteer Type", - "length": 0, - "no_copy": 0, - "options": "Volunteer Type", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "volunteer_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Volunteer Type", + "options": "Volunteer Type", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "email", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Email", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "fieldname": "email", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Email", + "reqd": 1, "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "image", - "fieldtype": "Attach Image", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Image", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "image", + "fieldtype": "Attach Image", + "hidden": 1, + "label": "Image", + "no_copy": 1, + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address_contacts", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Address and Contact", - "length": 0, - "no_copy": 0, - "options": "fa fa-map-marker", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:!doc.__islocal;", + "fieldname": "address_contacts", + "fieldtype": "Section Break", + "label": "Address and Contact", + "options": "fa fa-map-marker" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address_html", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Address HTML", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "address_html", + "fieldtype": "HTML", + "label": "Address HTML" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_9", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "contact_html", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Contact HTML", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "contact_html", + "fieldtype": "HTML", + "label": "Contact HTML" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "volunteer_availability_and_skills_details", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Availability and Skills", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "volunteer_availability_and_skills_details", + "fieldtype": "Section Break", + "label": "Availability and Skills" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "availability", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Availability", - "length": 0, - "no_copy": 0, - "options": "\nWeekly\nWeekdays\nWeekends", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "availability", + "fieldtype": "Select", + "label": "Availability", + "options": "\nWeekly\nWeekdays\nWeekends" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "availability_timeslot", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Availability Timeslot", - "length": 0, - "no_copy": 0, - "options": "\nMorning\nAfternoon\nEvening\nAnytime", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "availability_timeslot", + "fieldtype": "Select", + "label": "Availability Timeslot", + "options": "\nMorning\nAfternoon\nEvening\nAnytime" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_12", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "volunteer_skills", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Volunteer Skills", - "length": 0, - "no_copy": 0, - "options": "Volunteer Skill", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "volunteer_skills", + "fieldtype": "Table", + "label": "Volunteer Skills", + "options": "Volunteer Skill" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_15", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_15", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "note", - "fieldtype": "Long Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Note", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "note", + "fieldtype": "Long Text", + "label": "Note" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_field": "image", - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-11-04 03:36:25.776211", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Volunteer", - "name_case": "", - "owner": "Administrator", + ], + "image_field": "image", + "links": [], + "modified": "2020-09-16 23:45:15.595952", + "modified_by": "Administrator", + "module": "Non Profit", + "name": "Volunteer", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Non Profit Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Non Profit", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "volunteer_name", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "quick_entry": 1, + "restrict_to_domain": "Non Profit", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "volunteer_name", + "track_changes": 1 } \ No newline at end of file From e813684433f9165583e752e3335786e4b2e4cca5 Mon Sep 17 00:00:00 2001 From: Afshan Date: Thu, 17 Sep 2020 12:57:26 +0530 Subject: [PATCH 158/192] fix: warehouse address filtered based on warehouse --- .../stock/doctype/stock_entry/stock_entry.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 9845bc2f70..592ab5d2ed 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -23,6 +23,24 @@ frappe.ui.form.on('Stock Entry', { } }); + frm.set_query('source_warehouse_address', function() { + return { + filters: { + link_doctype: 'Warehouse', + link_name: frm.doc.from_warehouse + } + } + }); + + frm.set_query('target_warehouse_address', function() { + return { + filters: { + link_doctype: 'Warehouse', + link_name: frm.doc.to_warehouse + } + } + }); + frappe.db.get_value('Stock Settings', {name: 'Stock Settings'}, 'sample_retention_warehouse', (r) => { if (r.sample_retention_warehouse) { var filters = [ @@ -81,6 +99,9 @@ frappe.ui.form.on('Stock Entry', { frm.add_fetch("bom_no", "inspection_required", "inspection_required"); }, + + + setup_quality_inspection: function(frm) { if (!frm.doc.inspection_required) { return; From d5710b4d6f014e25358f7cd38e14b91013062f78 Mon Sep 17 00:00:00 2001 From: Afshan Date: Thu, 17 Sep 2020 13:01:47 +0530 Subject: [PATCH 159/192] style: removed extra spaces --- erpnext/stock/doctype/stock_entry/stock_entry.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 592ab5d2ed..1f95447951 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -99,9 +99,6 @@ frappe.ui.form.on('Stock Entry', { frm.add_fetch("bom_no", "inspection_required", "inspection_required"); }, - - - setup_quality_inspection: function(frm) { if (!frm.doc.inspection_required) { return; From 68672af8ab1b8e1f1061cbc43443fcc23eaec939 Mon Sep 17 00:00:00 2001 From: SDLyu Date: Fri, 18 Sep 2020 13:58:23 +0800 Subject: [PATCH 160/192] Fix missing tax autoload when frm.doc.taxes is [] --- erpnext/public/js/controllers/transaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 792235f7a3..33911793f6 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -417,7 +417,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ var taxes_and_charges_field = frappe.meta.get_docfield(me.frm.doc.doctype, "taxes_and_charges", me.frm.doc.name); - if (!this.frm.doc.taxes_and_charges && this.frm.doc.taxes) { + if (!this.frm.doc.taxes_and_charges && this.frm.doc.taxes && this.frm.doc.taxes.length > 0) { return; } From 170ecdc76e288a66c59e6337a9e712df9016ce5c Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Fri, 18 Sep 2020 13:26:46 +0530 Subject: [PATCH 161/192] fix: No handlefor Cost centers in Accounts Receivable --- .../report/accounts_receivable/accounts_receivable.js | 2 +- .../report/accounts_receivable/accounts_receivable.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index c999eb9b8e..29c4f7d394 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -34,7 +34,7 @@ frappe.query_reports["Accounts Receivable"] = { filters: { 'company': company } - } + }; } }, { diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 59117c8174..044fc1d3ab 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -617,9 +617,19 @@ class ReceivablePayableReport(object): elif party_type_field=="supplier": self.add_supplier_filters(conditions, values) + if self.filters.cost_center: + self.get_cost_center_conditions(conditions) + self.add_accounting_dimensions_filters(conditions, values) return " and ".join(conditions), values + def get_cost_center_conditions(self, conditions): + lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"]) + cost_center_list = [center.name for center in frappe.get_list("Cost Center", filters = {'lft': (">=", lft), 'rgt': ("<=", rgt)})] + + cost_center_string = '", "'.join(cost_center_list) + conditions.append('cost_center in ("{0}")'.format(cost_center_string)) + def get_order_by_condition(self): if self.filters.get('group_by_party'): return "order by party, posting_date" From c29ee691d4e7c5f738f0ce837c968d73599d337a Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 18 Sep 2020 15:27:17 +0530 Subject: [PATCH 162/192] fix: button click event not working in POS custom fields (#23358) --- .../doctype/pos_settings/pos_settings.js | 8 ++++---- .../selling/page/point_of_sale/pos_payment.js | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.js b/erpnext/accounts/doctype/pos_settings/pos_settings.js index 504941d8b6..05cb7f0b4b 100644 --- a/erpnext/accounts/doctype/pos_settings/pos_settings.js +++ b/erpnext/accounts/doctype/pos_settings/pos_settings.js @@ -7,10 +7,10 @@ frappe.ui.form.on('POS Settings', { }, get_invoice_fields: function(frm) { - frappe.model.with_doctype("Sales Invoice", () => { - var fields = $.map(frappe.get_doc("DocType", "Sales Invoice").fields, function(d) { + frappe.model.with_doctype("POS Invoice", () => { + var fields = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function(d) { if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 || - d.fieldtype === 'Table') { + ['Table', 'Button'].includes(d.fieldtype)) { return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname }; } else { return null; @@ -25,7 +25,7 @@ frappe.ui.form.on('POS Settings', { frappe.ui.form.on("POS Field", { fieldname: function(frm, doctype, name) { var doc = frappe.get_doc(doctype, name); - var df = $.map(frappe.get_doc("DocType", "Sales Invoice").fields, function(d) { + var df = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function(d) { return doc.fieldname == d.fieldname ? d : null; })[0]; diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index e1c54f64a7..7f0cabed8b 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -70,13 +70,23 @@ erpnext.PointOfSale.Payment = class { this.$invoice_fields.append( `
    ` ); + let df_events = { + onchange: function() { frm.set_value(this.df.fieldname, this.value); } + } + if (df.fieldtype == "Button") { + df_events = { + click: function() { + if (frm.script_manager.has_handlers(df.fieldname, frm.doc.doctype)) { + frm.script_manager.trigger(df.fieldname, frm.doc.doctype, frm.doc.docname); + } + } + } + } this[`${df.fieldname}_field`] = frappe.ui.form.make_control({ df: { ...df, - onchange: function() { - frm.set_value(this.df.fieldname, this.value); - } + ...df_events }, parent: this.$invoice_fields.find(`.${df.fieldname}-field`), render_input: true, From cc6ca97dc53fed52ba3e3ec5b47569c752a18aa7 Mon Sep 17 00:00:00 2001 From: Michelle Alva <50285544+michellealva@users.noreply.github.com> Date: Fri, 18 Sep 2020 15:50:29 +0530 Subject: [PATCH 163/192] fix: Item-wise Sales History Report Error (#23349) Co-authored-by: Rucha Mahabal --- .../report/item_wise_sales_history/item_wise_sales_history.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py index 0a70b97648..c716aa96e0 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py @@ -188,7 +188,7 @@ def get_conditions(filters): conditions += "AND so.transaction_date <= '%s'" %filters.to_date if filters.get("item_code"): - conditions += "AND so_item.item_code = '%s'" %frappe.db.escape(filters.item_code) + conditions += "AND so_item.item_code = %s" %frappe.db.escape(filters.item_code) if filters.get("customer"): conditions += "AND so.customer = %s" %frappe.db.escape(filters.customer) From 123eaea1a60f14fc6d464c8d816dcc9782db3773 Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Fri, 18 Sep 2020 16:30:50 +0530 Subject: [PATCH 164/192] fix: added new doctypes in get_level --- erpnext/utilities/activation.py | 35 +++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/erpnext/utilities/activation.py b/erpnext/utilities/activation.py index 63c36b35d1..7b17c8c464 100644 --- a/erpnext/utilities/activation.py +++ b/erpnext/utilities/activation.py @@ -11,8 +11,39 @@ def get_level(): activation_level = 0 sales_data = [] min_count = 0 - doctypes = {"Item": 5, "Customer": 5, "Sales Order": 2, "Sales Invoice": 2, "Purchase Order": 2, "Employee": 3, "Lead": 3, "Quotation": 3, - "Payment Entry": 2, "User": 5, "Student": 5, "Instructor": 5, "BOM": 3, "Journal Entry": 3, "Stock Entry": 3} + doctypes = { + "Asset": 5, + "BOM": 3, + "Customer": 5, + "Delivery Note": 5, + "Employee": 3, + "Instructor": 5, + "Instructor": 5, + "Issue": 5, + "Item": 5, + "Journal Entry": 3, + "Lead": 3, + "Leave Application": 5, + "Material Request": 5, + "Opportunity": 5, + "Payment Entry": 2, + "Project": 5, + "Purchase Order": 2, + "Purchase Invoice": 5, + "Purchase Receipt": 5, + "Quotation": 3, + "Salary Slip": 5, + "Salary Structure": 5, + "Sales Order": 2, + "Sales Invoice": 2, + "Stock Entry": 3, + "Student": 5, + "Supplier": 5, + "Task": 5, + "User": 5, + "Work Order": 5 + } + for doctype, min_count in iteritems(doctypes): count = frappe.db.count(doctype) if count > min_count: From bf166736b098574aecbbaa2329e58a5c75a938dd Mon Sep 17 00:00:00 2001 From: Anupam Date: Fri, 18 Sep 2020 17:15:49 +0530 Subject: [PATCH 165/192] fix: Batch-Wise Balance History filter validation --- .../batch_wise_balance_history/batch_wise_balance_history.js | 4 +++- .../batch_wise_balance_history/batch_wise_balance_history.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js index 23700c94ee..2499c801d2 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js @@ -9,13 +9,15 @@ frappe.query_reports["Batch-Wise Balance History"] = { "fieldtype": "Date", "width": "80", "default": frappe.sys_defaults.year_start_date, + "reqd": 1 }, { "fieldname":"to_date", "label": __("To Date"), "fieldtype": "Date", "width": "80", - "default": frappe.datetime.get_today() + "default": frappe.datetime.get_today(), + "reqd": 1 }, { "fieldname": "item", diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index 2c95084b81..8f3e246e7f 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -11,6 +11,9 @@ from frappe.utils import cint, flt, getdate def execute(filters=None): if not filters: filters = {} + if filters.from_date > filters.to_date: + frappe.throw(_("From Date must be before To Date")) + float_precision = cint(frappe.db.get_default("float_precision")) or 3 columns = get_columns(filters) From 4e6733293f7bbec2d265373ca8646a37856f8cb4 Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Fri, 18 Sep 2020 18:27:24 +0530 Subject: [PATCH 166/192] fix(DocTypes): Reset "owner" values in DocTypes to Administrator (#23350) * fix: reset owner values in doctype to Administrator * fix: updated modified value * fix: also fixed modified by * fix: change modified_by to Administrator --- .../doctype/item_tax_template/item_tax_template.json | 4 ++-- .../accounts/doctype/mode_of_payment/mode_of_payment.json | 6 +++--- .../period_closing_voucher/period_closing_voucher.json | 4 ++-- .../purchase_taxes_and_charges.json | 2 +- .../purchase_taxes_and_charges_template.json | 2 +- .../purchase_order_item_supplied.json | 4 ++-- .../purchase_receipt_item_supplied.json | 4 ++-- .../doctype/shopify_settings/shopify_settings.json | 4 ++-- .../healthcare_schedule_time_slot.json | 4 ++-- .../practitioner_schedule/practitioner_schedule.json | 4 ++-- erpnext/hr/doctype/appraisal/appraisal.json | 4 ++-- erpnext/hr/doctype/appraisal_goal/appraisal_goal.json | 4 ++-- .../hr/doctype/appraisal_template/appraisal_template.json | 4 ++-- .../appraisal_template_goal/appraisal_template_goal.json | 4 ++-- erpnext/hr/doctype/attendance/attendance.json | 4 ++-- erpnext/hr/doctype/expense_claim/expense_claim.json | 4 ++-- .../doctype/expense_claim_detail/expense_claim_detail.json | 4 ++-- .../hr/doctype/expense_claim_type/expense_claim_type.json | 4 ++-- erpnext/hr/doctype/upload_attendance/upload_attendance.json | 4 ++-- .../company_to_hub_company/company_to_hub_company.json | 4 ++-- .../hub_message_to_lead/hub_message_to_lead.json | 6 +++--- erpnext/hub_node/doctype/hub_user/hub_user.json | 4 ++-- erpnext/hub_node/doctype/hub_users/hub_users.json | 4 ++-- .../doctype/marketplace_settings/marketplace_settings.json | 4 ++-- .../doctype/maintenance_schedule/maintenance_schedule.json | 2 +- .../doctype/maintenance_visit/maintenance_visit.json | 4 ++-- .../maintenance_visit_purpose.json | 4 ++-- erpnext/projects/doctype/dependent_task/dependent_task.json | 4 ++-- erpnext/selling/doctype/industry_type/industry_type.json | 4 ++-- erpnext/selling/doctype/product_bundle/product_bundle.json | 4 ++-- erpnext/stock/doctype/batch/batch.json | 4 ++-- .../stock/doctype/landed_cost_item/landed_cost_item.json | 4 ++-- .../landed_cost_purchase_receipt.json | 4 ++-- erpnext/support/doctype/warranty_claim/warranty_claim.json | 4 ++-- 34 files changed, 67 insertions(+), 67 deletions(-) diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json index 856c371ecf..8915f79b92 100644 --- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json +++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json @@ -38,8 +38,8 @@ "reqd": 1 } ], - "modified": "2020-06-18 20:27:42.615842", - "modified_by": "ahmad@havenir.com", + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", "module": "Accounts", "name": "Item Tax Template", "owner": "Administrator", diff --git a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.json b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.json index f3df1f0bc9..50fc3bbab7 100644 --- a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.json +++ b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.json @@ -45,11 +45,11 @@ ], "icon": "fa fa-credit-card", "idx": 1, - "modified": "2019-08-14 14:58:42.079115", - "modified_by": "sammish.thundiyil@gmail.com", + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", "module": "Accounts", "name": "Mode of Payment", - "owner": "harshada@webnotestech.com", + "owner": "Administrator", "permissions": [ { "create": 1, diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json index d04f25b9ac..47546c07a4 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json @@ -291,11 +291,11 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-08-21 16:15:49.089450", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Accounts", "name": "Period Closing Voucher", - "owner": "jai@webnotestech.com", + "owner": "Administrator", "permissions": [ { "amend": 1, diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index 0e748f84bb..f9fdc4b605 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -210,7 +210,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-03-12 14:53:47.679439", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Taxes and Charges", diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json index a18fec61cf..b46d2e32f2 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json @@ -78,7 +78,7 @@ "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Taxes and Charges Template", - "owner": "wasim@webnotestech.com", + "owner": "Administrator", "permissions": [ { "email": 1, diff --git a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json index c3e1bf5303..d7ea9c1ccc 100644 --- a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json +++ b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json @@ -148,11 +148,11 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-03-12 15:43:53.862897", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item Supplied", - "owner": "dhanalekshmi@webnotestech.com", + "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC" diff --git a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json index ea5863020a..dc00bca5cc 100644 --- a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json +++ b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json @@ -188,11 +188,11 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-04-10 18:09:33.997618", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Receipt Item Supplied", - "owner": "wasim@webnotestech.com", + "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC", diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json index 5339c99155..2e10751f96 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json @@ -258,8 +258,8 @@ } ], "issingle": 1, - "modified": "2020-05-28 12:32:11.384757", - "modified_by": "umair@erpnext.com", + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "Shopify Settings", "owner": "Administrator", diff --git a/erpnext/healthcare/doctype/healthcare_schedule_time_slot/healthcare_schedule_time_slot.json b/erpnext/healthcare/doctype/healthcare_schedule_time_slot/healthcare_schedule_time_slot.json index 9faa5b2f03..cf54e82199 100644 --- a/erpnext/healthcare/doctype/healthcare_schedule_time_slot/healthcare_schedule_time_slot.json +++ b/erpnext/healthcare/doctype/healthcare_schedule_time_slot/healthcare_schedule_time_slot.json @@ -117,12 +117,12 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2018-05-08 13:45:57.226530", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare Schedule Time Slot", "name_case": "", - "owner": "rmehta@gmail.com", + "owner": "Administrator", "permissions": [], "quick_entry": 1, "read_only": 0, diff --git a/erpnext/healthcare/doctype/practitioner_schedule/practitioner_schedule.json b/erpnext/healthcare/doctype/practitioner_schedule/practitioner_schedule.json index cff100cc70..a21825ea8e 100644 --- a/erpnext/healthcare/doctype/practitioner_schedule/practitioner_schedule.json +++ b/erpnext/healthcare/doctype/practitioner_schedule/practitioner_schedule.json @@ -44,11 +44,11 @@ } ], "links": [], - "modified": "2020-01-31 12:21:45.975488", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Healthcare", "name": "Practitioner Schedule", - "owner": "rmehta@gmail.com", + "owner": "Administrator", "permissions": [ { "create": 1, diff --git a/erpnext/hr/doctype/appraisal/appraisal.json b/erpnext/hr/doctype/appraisal/appraisal.json index 4f6da975d5..91201d4b82 100644 --- a/erpnext/hr/doctype/appraisal/appraisal.json +++ b/erpnext/hr/doctype/appraisal/appraisal.json @@ -696,11 +696,11 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2019-01-30 11:28:08.401459", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "HR", "name": "Appraisal", - "owner": "ashwini@webnotestech.com", + "owner": "Administrator", "permissions": [ { "amend": 0, diff --git a/erpnext/hr/doctype/appraisal_goal/appraisal_goal.json b/erpnext/hr/doctype/appraisal_goal/appraisal_goal.json index f22969b7c1..b8ec5a8afb 100644 --- a/erpnext/hr/doctype/appraisal_goal/appraisal_goal.json +++ b/erpnext/hr/doctype/appraisal_goal/appraisal_goal.json @@ -207,11 +207,11 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2016-07-11 03:27:57.897071", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "HR", "name": "Appraisal Goal", - "owner": "ashwini@webnotestech.com", + "owner": "Administrator", "permissions": [], "quick_entry": 0, "read_only": 0, diff --git a/erpnext/hr/doctype/appraisal_template/appraisal_template.json b/erpnext/hr/doctype/appraisal_template/appraisal_template.json index ac6e400e09..7329c35dee 100644 --- a/erpnext/hr/doctype/appraisal_template/appraisal_template.json +++ b/erpnext/hr/doctype/appraisal_template/appraisal_template.json @@ -113,11 +113,11 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2016-12-13 12:37:56.937023", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "HR", "name": "Appraisal Template", - "owner": "ashwini@webnotestech.com", + "owner": "Administrator", "permissions": [ { "amend": 0, diff --git a/erpnext/hr/doctype/appraisal_template_goal/appraisal_template_goal.json b/erpnext/hr/doctype/appraisal_template_goal/appraisal_template_goal.json index 34ea5d8225..2858419f33 100644 --- a/erpnext/hr/doctype/appraisal_template_goal/appraisal_template_goal.json +++ b/erpnext/hr/doctype/appraisal_template_goal/appraisal_template_goal.json @@ -78,11 +78,11 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2016-07-11 03:27:57.979215", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "HR", "name": "Appraisal Template Goal", - "owner": "ashwini@webnotestech.com", + "owner": "Administrator", "permissions": [], "quick_entry": 0, "read_only": 0, diff --git a/erpnext/hr/doctype/attendance/attendance.json b/erpnext/hr/doctype/attendance/attendance.json index a656a7ea5f..134098f252 100644 --- a/erpnext/hr/doctype/attendance/attendance.json +++ b/erpnext/hr/doctype/attendance/attendance.json @@ -205,11 +205,11 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2020-05-29 13:51:37.177231", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "HR", "name": "Attendance", - "owner": "ashwini@webnotestech.com", + "owner": "Administrator", "permissions": [ { "cancel": 1, diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.json b/erpnext/hr/doctype/expense_claim/expense_claim.json index fa28470af8..e3e6e80616 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.json +++ b/erpnext/hr/doctype/expense_claim/expense_claim.json @@ -371,12 +371,12 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2020-06-15 12:43:04.099803", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim", "name_case": "Title Case", - "owner": "harshada@webnotestech.com", + "owner": "Administrator", "permissions": [ { "amend": 1, diff --git a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json index 3cce50e090..70a48f93b7 100644 --- a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json +++ b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json @@ -120,11 +120,11 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-05-11 18:54:35.601592", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim Detail", - "owner": "harshada@webnotestech.com", + "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC" diff --git a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.json b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.json index cf2cbb4308..02ab4cb6ac 100644 --- a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.json +++ b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.json @@ -48,11 +48,11 @@ "icon": "fa fa-flag", "idx": 1, "links": [], - "modified": "2019-12-11 13:38:59.534034", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim Type", - "owner": "harshada@webnotestech.com", + "owner": "Administrator", "permissions": [ { "create": 1, diff --git a/erpnext/hr/doctype/upload_attendance/upload_attendance.json b/erpnext/hr/doctype/upload_attendance/upload_attendance.json index 16b7a83173..a1451fde1d 100644 --- a/erpnext/hr/doctype/upload_attendance/upload_attendance.json +++ b/erpnext/hr/doctype/upload_attendance/upload_attendance.json @@ -229,11 +229,11 @@ "issingle": 1, "istable": 0, "max_attachments": 1, - "modified": "2017-11-14 12:51:34.980103", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "HR", "name": "Upload Attendance", - "owner": "harshada@webnotestech.com", + "owner": "Administrator", "permissions": [ { "amend": 0, diff --git a/erpnext/hub_node/data_migration_mapping/company_to_hub_company/company_to_hub_company.json b/erpnext/hub_node/data_migration_mapping/company_to_hub_company/company_to_hub_company.json index ae0d492d72..b1e421dada 100644 --- a/erpnext/hub_node/data_migration_mapping/company_to_hub_company/company_to_hub_company.json +++ b/erpnext/hub_node/data_migration_mapping/company_to_hub_company/company_to_hub_company.json @@ -40,8 +40,8 @@ "mapping_name": "Company to Hub Company", "mapping_type": "Push", "migration_id_field": "hub_sync_id", - "modified": "2018-02-14 15:57:05.571142", - "modified_by": "achilles@erpnext.com", + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", "name": "Company to Hub Company", "owner": "Administrator", "page_length": 10, diff --git a/erpnext/hub_node/data_migration_mapping/hub_message_to_lead/hub_message_to_lead.json b/erpnext/hub_node/data_migration_mapping/hub_message_to_lead/hub_message_to_lead.json index d2af755d04..d11abeb4b3 100644 --- a/erpnext/hub_node/data_migration_mapping/hub_message_to_lead/hub_message_to_lead.json +++ b/erpnext/hub_node/data_migration_mapping/hub_message_to_lead/hub_message_to_lead.json @@ -21,10 +21,10 @@ "mapping_name": "Hub Message to Lead", "mapping_type": "Pull", "migration_id_field": "hub_sync_id", - "modified": "2018-02-14 15:57:05.606597", - "modified_by": "achilles@erpnext.com", + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", "name": "Hub Message to Lead", - "owner": "frappetest@gmail.com", + "owner": "Administrator", "page_length": 10, "remote_objectname": "Hub Message", "remote_primary_key": "name" diff --git a/erpnext/hub_node/doctype/hub_user/hub_user.json b/erpnext/hub_node/doctype/hub_user/hub_user.json index a660022736..f51ffb4387 100644 --- a/erpnext/hub_node/doctype/hub_user/hub_user.json +++ b/erpnext/hub_node/doctype/hub_user/hub_user.json @@ -121,8 +121,8 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2018-09-01 13:56:07.816894", - "modified_by": "netchamp@rawcoderz.com", + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", "module": "Hub Node", "name": "Hub User", "name_case": "", diff --git a/erpnext/hub_node/doctype/hub_users/hub_users.json b/erpnext/hub_node/doctype/hub_users/hub_users.json index 2027e72fa0..d42f3fdf1b 100644 --- a/erpnext/hub_node/doctype/hub_users/hub_users.json +++ b/erpnext/hub_node/doctype/hub_users/hub_users.json @@ -54,12 +54,12 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2018-03-06 04:41:17.916243", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Hub Node", "name": "Hub Users", "name_case": "", - "owner": "test1@example.com", + "owner": "Administrator", "permissions": [], "quick_entry": 1, "read_only": 0, diff --git a/erpnext/hub_node/doctype/marketplace_settings/marketplace_settings.json b/erpnext/hub_node/doctype/marketplace_settings/marketplace_settings.json index 4b49f1978a..e784f68fcf 100644 --- a/erpnext/hub_node/doctype/marketplace_settings/marketplace_settings.json +++ b/erpnext/hub_node/doctype/marketplace_settings/marketplace_settings.json @@ -352,12 +352,12 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2019-02-01 14:21:16.729848", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Hub Node", "name": "Marketplace Settings", "name_case": "", - "owner": "netchamp@rawcoderz.com", + "owner": "Administrator", "permissions": [ { "amend": 0, diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.json b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.json index 3788bc3700..606d22f52b 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.json +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.json @@ -813,7 +813,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-08-21 14:44:33.670332", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Maintenance", "name": "Maintenance Schedule", diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json index 11925681df..32bfa0e324 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json @@ -1001,11 +1001,11 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2020-07-15 14:44:44.911402", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Maintenance", "name": "Maintenance Visit", - "owner": "ashwini@webnotestech.com", + "owner": "Administrator", "permissions": [ { "amend": 1, diff --git a/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json b/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json index 84dc72cd8a..467441d841 100644 --- a/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json +++ b/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json @@ -125,11 +125,11 @@ ], "idx": 1, "istable": 1, - "modified": "2019-10-03 14:55:52.786805", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Maintenance", "name": "Maintenance Visit Purpose", - "owner": "ashwini@webnotestech.com", + "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC", diff --git a/erpnext/projects/doctype/dependent_task/dependent_task.json b/erpnext/projects/doctype/dependent_task/dependent_task.json index e00a2b287a..858a554bad 100644 --- a/erpnext/projects/doctype/dependent_task/dependent_task.json +++ b/erpnext/projects/doctype/dependent_task/dependent_task.json @@ -48,8 +48,8 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2016-10-26 17:37:25.638190", - "modified_by": "rohitw1991@gmail.com", + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", "module": "Projects", "name": "Dependent Task", "name_case": "", diff --git a/erpnext/selling/doctype/industry_type/industry_type.json b/erpnext/selling/doctype/industry_type/industry_type.json index f4fcae428e..6c49f0f6dd 100644 --- a/erpnext/selling/doctype/industry_type/industry_type.json +++ b/erpnext/selling/doctype/industry_type/industry_type.json @@ -49,11 +49,11 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2016-07-25 05:24:25.881361", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Selling", "name": "Industry Type", - "owner": "harshada@webnotestech.com", + "owner": "Administrator", "permissions": [ { "amend": 0, diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.json b/erpnext/selling/doctype/product_bundle/product_bundle.json index b63fb4bdcf..56155fb750 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.json +++ b/erpnext/selling/doctype/product_bundle/product_bundle.json @@ -238,8 +238,8 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-10-18 14:23:06.538568", - "modified_by": "tundebabzy@gmail.com", + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", "module": "Selling", "name": "Product Bundle", "owner": "Administrator", diff --git a/erpnext/stock/doctype/batch/batch.json b/erpnext/stock/doctype/batch/batch.json index 1eb457734e..943cb3401f 100644 --- a/erpnext/stock/doctype/batch/batch.json +++ b/erpnext/stock/doctype/batch/batch.json @@ -166,11 +166,11 @@ "idx": 1, "image_field": "image", "max_attachments": 5, - "modified": "2019-10-03 22:38:45.104056", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Stock", "name": "Batch", - "owner": "harshada@webnotestech.com", + "owner": "Administrator", "permissions": [ { "create": 1, diff --git a/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json b/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json index 90a392c145..b24d621c31 100644 --- a/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json +++ b/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json @@ -133,11 +133,11 @@ ], "idx": 1, "istable": 1, - "modified": "2019-11-12 15:41:21.053462", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Item", - "owner": "wasim@webnotestech.com", + "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC" diff --git a/erpnext/stock/doctype/landed_cost_purchase_receipt/landed_cost_purchase_receipt.json b/erpnext/stock/doctype/landed_cost_purchase_receipt/landed_cost_purchase_receipt.json index f49c147682..9b2b5da9cb 100644 --- a/erpnext/stock/doctype/landed_cost_purchase_receipt/landed_cost_purchase_receipt.json +++ b/erpnext/stock/doctype/landed_cost_purchase_receipt/landed_cost_purchase_receipt.json @@ -173,11 +173,11 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2016-07-20 10:49:34.228751", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Purchase Receipt", - "owner": "wasim@webnotestech.com", + "owner": "Administrator", "permissions": [], "quick_entry": 0, "read_only": 0, diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.json b/erpnext/support/doctype/warranty_claim/warranty_claim.json index ae1a7a569d..88ee4a3beb 100644 --- a/erpnext/support/doctype/warranty_claim/warranty_claim.json +++ b/erpnext/support/doctype/warranty_claim/warranty_claim.json @@ -361,11 +361,11 @@ ], "icon": "fa fa-bug", "idx": 1, - "modified": "2019-05-24 10:56:30.626200", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Support", "name": "Warranty Claim", - "owner": "harshada@webnotestech.com", + "owner": "Administrator", "permissions": [ { "create": 1, From 0aff6d83af06f56bcbf30986b5659247754250f5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sat, 19 Sep 2020 18:05:58 +0530 Subject: [PATCH 167/192] Update erpnext/public/js/utils/party.js Co-authored-by: Sagar Vora --- erpnext/public/js/utils/party.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index af1f433514..44e75aee36 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -225,7 +225,7 @@ erpnext.utils.set_taxes = function(frm, triggered_from_field) { } if (!frm.doc.company) { - frappe.throw(_("Kindly select the company first")); + frappe.throw(__("Kindly select the company first")); } frappe.call({ @@ -296,4 +296,4 @@ erpnext.utils.get_shipping_address = function(frm, callback){ } else { frappe.msgprint(__("Select company first")); } -} \ No newline at end of file +} From c646a56d68a8ab6b13aaa289a214f3f3bed918ee Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sat, 19 Sep 2020 18:28:20 +0530 Subject: [PATCH 168/192] fix: Update modified timestamp --- erpnext/non_profit/doctype/membership/membership.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json index b95ae9738c..96dea84a92 100644 --- a/erpnext/non_profit/doctype/membership/membership.json +++ b/erpnext/non_profit/doctype/membership/membership.json @@ -128,7 +128,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-07-27 14:28:11.532696", + "modified": "2020-09-19 14:28:11.532696", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership", From 4b25eb4db48047ea3d9d8b85efe6b66a26fe160c Mon Sep 17 00:00:00 2001 From: Saqib Date: Sat, 19 Sep 2020 19:33:21 +0530 Subject: [PATCH 169/192] fix: mode of payment not fetched if no account is set (#23334) Co-authored-by: Sagar Vora --- .../pos_opening_entry/pos_opening_entry.py | 11 +++++++- .../doctype/pos_profile/pos_profile.py | 27 ++++++++++++------- .../doctype/sales_invoice/sales_invoice.py | 18 ++++++------- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py index 15f23b63dc..b9e07b8030 100644 --- a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py +++ b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py @@ -5,13 +5,14 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import cint +from frappe.utils import cint, get_link_to_form from frappe.model.document import Document from erpnext.controllers.status_updater import StatusUpdater class POSOpeningEntry(StatusUpdater): def validate(self): self.validate_pos_profile_and_cashier() + self.validate_payment_method_account() self.set_status() def validate_pos_profile_and_cashier(self): @@ -20,6 +21,14 @@ class POSOpeningEntry(StatusUpdater): if not cint(frappe.db.get_value("User", self.user, "enabled")): frappe.throw(_("User {} has been disabled. Please select valid user/cashier".format(self.user))) + + def validate_payment_method_account(self): + for d in self.balance_details: + account = frappe.db.get_value("Mode of Payment Account", + {"parent": d.mode_of_payment, "company": self.company}, "default_account") + if not account: + frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}") + .format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account")) def on_submit(self): self.set_status(update=True) \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index 789b4c3bd9..1386b70f55 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe from frappe import msgprint, _ -from frappe.utils import cint, now +from frappe.utils import cint, now, get_link_to_form from six import iteritems from frappe.model.document import Document @@ -13,7 +13,7 @@ class POSProfile(Document): self.validate_default_profile() self.validate_all_link_fields() self.validate_duplicate_groups() - self.check_default_payment() + self.validate_payment_methods() def validate_default_profile(self): for row in self.applicable_for_users: @@ -52,14 +52,23 @@ class POSProfile(Document): if len(customer_groups) != len(set(customer_groups)): frappe.throw(_("Duplicate customer group found in the cutomer group table"), title = "Duplicate Customer Group") - def check_default_payment(self): - if self.payments: - default_mode_of_payment = [d.default for d in self.payments if d.default] - if not default_mode_of_payment: - frappe.throw(_("Set default mode of payment")) + def validate_payment_methods(self): + if not self.payments: + frappe.throw(_("Payment methods are mandatory. Please add at least one payment method.")) - if len(default_mode_of_payment) > 1: - frappe.throw(_("Multiple default mode of payment is not allowed")) + default_mode_of_payment = [d.default for d in self.payments if d.default] + if not default_mode_of_payment: + frappe.throw(_("Please select a default mode of payment")) + + if len(default_mode_of_payment) > 1: + frappe.throw(_("You can only select one mode of payment as default")) + + for d in self.payments: + account = frappe.db.get_value("Mode of Payment Account", + {"parent": d.mode_of_payment, "company": self.company}, "default_account") + if not account: + frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}") + .format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account")) def on_update(self): self.set_defaults() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 71f2e120cc..92e49d59da 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe, erpnext import frappe.defaults -from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate +from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate, get_link_to_form from frappe import _, msgprint, throw from erpnext.accounts.party import get_party_account, get_due_date from frappe.model.mapper import get_mapped_doc @@ -1372,7 +1372,7 @@ def get_bank_cash_account(mode_of_payment, company): {"parent": mode_of_payment, "company": company}, "default_account") if not account: frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}") - .format(mode_of_payment)) + .format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account")) return { "account": account } @@ -1612,18 +1612,16 @@ def update_multi_mode_option(doc, pos_profile): payment.type = payment_mode.type doc.set('payments', []) - if not pos_profile or not pos_profile.get('payments'): - for payment_mode in get_all_mode_of_payments(doc): - append_payment(payment_mode) - return - for pos_payment_method in pos_profile.get('payments'): pos_payment_method = pos_payment_method.as_dict() payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company) - if payment_mode: - payment_mode[0].default = pos_payment_method.default - append_payment(payment_mode[0]) + if not payment_mode: + frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}") + .format(get_link_to_form("Mode of Payment", pos_payment_method.mode_of_payment)), title=_("Missing Account")) + + payment_mode[0].default = pos_payment_method.default + append_payment(payment_mode[0]) def get_all_mode_of_payments(doc): return frappe.db.sql(""" From f2ea3877c8f045d8fd7107f0c567748e13005628 Mon Sep 17 00:00:00 2001 From: Saqib Date: Sun, 20 Sep 2020 19:31:18 +0530 Subject: [PATCH 170/192] fix: depreciation start date ux fixes (#23339) --- erpnext/assets/doctype/asset/asset.js | 16 +- erpnext/assets/doctype/asset/asset.py | 9 +- erpnext/assets/doctype/asset/test_asset.py | 18 +- .../asset_finance_book.json | 420 ++++-------------- .../asset_movement/test_asset_movement.py | 6 +- .../purchase_receipt/test_purchase_receipt.py | 3 +- 6 files changed, 113 insertions(+), 359 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index fba20c0c87..7ad164a8b9 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -252,13 +252,6 @@ frappe.ui.form.on('Asset', { }) }, - available_for_use_date: function(frm) { - $.each(frm.doc.finance_books || [], function(i, d) { - if(!d.depreciation_start_date) d.depreciation_start_date = frm.doc.available_for_use_date; - }); - refresh_field("finance_books"); - }, - is_existing_asset: function(frm) { frm.trigger("toggle_reference_doc"); // frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation)); @@ -438,6 +431,15 @@ frappe.ui.form.on('Asset Finance Book', { } frappe.flags.dont_change_rate = false; + }, + + depreciation_start_date: function(frm, cdt, cdn) { + const book = locals[cdt][cdn]; + if (frm.doc.available_for_use_date && book.depreciation_start_date == frm.doc.available_for_use_date) { + frappe.msgprint(__(`Depreciation Posting Date should not be equal to Available for Use Date.`)); + book.depreciation_start_date = ""; + frm.refresh_field("finance_books"); + } } }); diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 9d08d9212d..72debb7eba 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, erpnext, math, json from frappe import _ from six import string_types -from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, month_diff, add_days +from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, month_diff, add_days, get_last_day from frappe.model.document import Document from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset.depreciation \ @@ -83,6 +83,11 @@ class Asset(AccountsController): if not self.available_for_use_date: frappe.throw(_("Available for use date is required")) + for d in self.finance_books: + if d.depreciation_start_date == self.available_for_use_date: + frappe.throw(_("Row #{}: Depreciation Posting Date should not be equal to Available for Use Date.").format(d.idx), + title=_("Incorrect Date")) + def set_missing_values(self): if not self.asset_category: self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") @@ -294,7 +299,7 @@ class Asset(AccountsController): if not row.depreciation_start_date: if not self.available_for_use_date: frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx)) - row.depreciation_start_date = self.available_for_use_date + row.depreciation_start_date = get_last_day(self.available_for_use_date) if not self.is_existing_asset: self.opening_accumulated_depreciation = 0 diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index aed78e7746..52039c183b 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -371,19 +371,18 @@ class TestAsset(unittest.TestCase): asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') asset = frappe.get_doc('Asset', asset_name) asset.calculate_depreciation = 1 - asset.available_for_use_date = nowdate() - asset.purchase_date = nowdate() + asset.available_for_use_date = '2020-01-01' + asset.purchase_date = '2020-01-01' asset.append("finance_books", { "expected_value_after_useful_life": 10000, "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": nowdate() + "total_number_of_depreciations": 10, + "frequency_of_depreciation": 1 }) asset.insert() asset.submit() - post_depreciation_entries(date=add_months(nowdate(), 10)) + post_depreciation_entries(date=add_months('2020-01-01', 4)) scrap_asset(asset.name) @@ -392,9 +391,9 @@ class TestAsset(unittest.TestCase): self.assertTrue(asset.journal_entry_for_scrap) expected_gle = ( - ("_Test Accumulated Depreciations - _TC", 30000.0, 0.0), + ("_Test Accumulated Depreciations - _TC", 36000.0, 0.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0), - ("_Test Gain/Loss on Asset Disposal - _TC", 70000.0, 0.0) + ("_Test Gain/Loss on Asset Disposal - _TC", 64000.0, 0.0) ) gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` @@ -466,8 +465,7 @@ class TestAsset(unittest.TestCase): "expected_value_after_useful_life": 10000, "depreciation_method": "Straight Line", "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": "2020-06-06" + "frequency_of_depreciation": 10 }) asset.insert() accumulated_depreciation_after_full_schedule = \ diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json index c80f95e155..79fcb957d4 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -1,347 +1,99 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2018-05-08 14:44:37.095570", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2018-05-08 14:44:37.095570", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "finance_book", + "depreciation_method", + "total_number_of_depreciations", + "column_break_5", + "frequency_of_depreciation", + "depreciation_start_date", + "expected_value_after_useful_life", + "value_after_depreciation", + "rate_of_depreciation" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, - "fieldname": "finance_book", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Finance Book", - "length": 0, - "no_copy": 0, - "options": "Finance Book", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "finance_book", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Finance Book", + "options": "Finance Book" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "depreciation_method", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Depreciation Method", - "length": 0, - "no_copy": 0, - "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "depreciation_method", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Depreciation Method", + "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "total_number_of_depreciations", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Total Number of Depreciations", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "total_number_of_depreciations", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Total Number of Depreciations", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_5", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "frequency_of_depreciation", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Frequency of Depreciation (Months)", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "frequency_of_depreciation", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Frequency of Depreciation (Months)", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:parent.doctype == 'Asset'", - "fetch_if_empty": 0, - "fieldname": "depreciation_start_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Depreciation Start Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:parent.doctype == 'Asset'", + "fieldname": "depreciation_start_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Depreciation Posting Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "depends_on": "eval:parent.doctype == 'Asset'", - "fetch_if_empty": 0, - "fieldname": "expected_value_after_useful_life", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Expected Value After Useful Life", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "depends_on": "eval:parent.doctype == 'Asset'", + "fieldname": "expected_value_after_useful_life", + "fieldtype": "Currency", + "label": "Expected Value After Useful Life", + "options": "Company:company:default_currency" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "value_after_depreciation", - "fieldtype": "Currency", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Value After Depreciation", - "length": 0, - "no_copy": 1, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "value_after_depreciation", + "fieldtype": "Currency", + "hidden": 1, + "label": "Value After Depreciation", + "no_copy": 1, + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.depreciation_method == 'Written Down Value'", - "description": "In Percentage", - "fetch_if_empty": 0, - "fieldname": "rate_of_depreciation", - "fieldtype": "Percent", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Rate of Depreciation", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "depends_on": "eval:doc.depreciation_method == 'Written Down Value'", + "description": "In Percentage", + "fieldname": "rate_of_depreciation", + "fieldtype": "Percent", + "label": "Rate of Depreciation" } - ], - "has_web_view": 0, - "hide_toolbar": 0, - "idx": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2019-04-09 19:45:14.523488", - "modified_by": "Administrator", - "module": "Assets", - "name": "Asset Finance Book", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-09-16 12:11:30.631788", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Finance Book", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_movement/test_asset_movement.py b/erpnext/assets/doctype/asset_movement/test_asset_movement.py index c3755a3fb9..cddee5fa0f 100644 --- a/erpnext/assets/doctype/asset_movement/test_asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/test_asset_movement.py @@ -32,8 +32,7 @@ class TestAssetMovement(unittest.TestCase): "next_depreciation_date": "2020-12-31", "depreciation_method": "Straight Line", "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": "2020-06-06" + "frequency_of_depreciation": 10 }) if asset.docstatus == 0: @@ -82,8 +81,7 @@ class TestAssetMovement(unittest.TestCase): "next_depreciation_date": "2020-12-31", "depreciation_method": "Straight Line", "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": "2020-06-06" + "frequency_of_depreciation": 10 }) if asset.docstatus == 0: asset.submit() diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 67161aa6dd..1e7153e774 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -494,8 +494,7 @@ class TestPurchaseReceipt(unittest.TestCase): "expected_value_after_useful_life": 10, "depreciation_method": "Straight Line", "total_number_of_depreciations": 3, - "frequency_of_depreciation": 1, - "depreciation_start_date": frappe.utils.nowdate() + "frequency_of_depreciation": 1 }) asset.submit() From c152b92ef4ffa5ffb2ac18dc879106f10f8f4a79 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sun, 20 Sep 2020 20:04:10 +0530 Subject: [PATCH 171/192] Update erpnext/controllers/selling_controller.py Co-authored-by: Sagar Vora --- erpnext/controllers/selling_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index ad06f97b7d..7f7aae31b1 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -81,7 +81,7 @@ class SellingController(StockController): party_details = _get_party_details(customer, ignore_permissions=self.flags.ignore_permissions, doctype=self.doctype, company=self.company, - posting_date=self.posting_date if hasattr(self, 'posting_date') else None, + posting_date=self.get('posting_date'), fetch_payment_terms_template=fetch_payment_terms_template, party_address=self.customer_address, shipping_address=self.shipping_address_name) if not self.meta.get_field("sales_team"): From 97f61d233e75a8b0eeb757a7c6085976cf9031ed Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Mon, 21 Sep 2020 13:00:43 +0530 Subject: [PATCH 172/192] fix: breadcrumbs for maintenance visit and schedule under support --- erpnext/public/js/conf.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/conf.js b/erpnext/public/js/conf.js index 2af9140f9e..eb709e5e85 100644 --- a/erpnext/public/js/conf.js +++ b/erpnext/public/js/conf.js @@ -11,7 +11,9 @@ $.extend(frappe.breadcrumbs.preferred, { "Territory": "Selling", "Sales Person": "Selling", "Sales Partner": "Selling", - "Brand": "Stock" + "Brand": "Stock", + "Maintenance Schedule": "Support", + "Maintenance Visit": "Support" }); $.extend(frappe.breadcrumbs.module_map, { From e42f08085b9f165422210e056e8c04820a619e09 Mon Sep 17 00:00:00 2001 From: Rohan Date: Mon, 21 Sep 2020 13:14:07 +0530 Subject: [PATCH 173/192] fix: use Plaid's new API (develop) (#23318) Co-authored-by: Mangesh-Khairnar --- .../bank_reconciliation.js | 38 ++++---- .../doctype/plaid_settings/plaid_connector.py | 97 ++++++++++--------- .../doctype/plaid_settings/plaid_settings.js | 57 +++++------ .../plaid_settings/plaid_settings.json | 11 +-- .../doctype/plaid_settings/plaid_settings.py | 50 ++++++---- .../plaid_settings/test_plaid_settings.py | 21 ++-- .../v12_0/move_plaid_settings_to_doctype.py | 5 +- requirements.txt | 2 +- 8 files changed, 149 insertions(+), 132 deletions(-) diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js index efc76f9158..9703527875 100644 --- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js +++ b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js @@ -72,7 +72,7 @@ erpnext.accounts.bankReconciliation = class BankReconciliation { check_plaid_status() { const me = this; frappe.db.get_value("Plaid Settings", "Plaid Settings", "enabled", (r) => { - if (r && r.enabled == "1") { + if (r && r.enabled === "1") { me.plaid_status = "active" } else { me.plaid_status = "inactive" @@ -139,7 +139,7 @@ erpnext.accounts.bankTransactionUpload = class bankTransactionUpload { } make() { - const me = this; + const me = this; new frappe.ui.FileUploader({ method: 'erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.upload_bank_statement', allow_multiple: 0, @@ -214,31 +214,35 @@ erpnext.accounts.bankTransactionSync = class bankTransactionSync { init_config() { const me = this; - frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.plaid_configuration') - .then(result => { - me.plaid_env = result.plaid_env; - me.plaid_public_key = result.plaid_public_key; - me.client_name = result.client_name; - me.sync_transactions() - }) + frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_plaid_configuration') + .then(result => { + me.plaid_env = result.plaid_env; + me.client_name = result.client_name; + me.link_token = result.link_token; + me.sync_transactions(); + }) } sync_transactions() { const me = this; - frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (v) => { + frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (r) => { frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions', { - bank: v['bank'], + bank: r.bank, bank_account: me.parent.bank_account, freeze: true }) .then((result) => { - let result_title = (result.length > 0) ? __("{0} bank transaction(s) created", [result.length]) : __("This bank account is already synchronized") + let result_title = (result && result.length > 0) + ? __("{0} bank transaction(s) created", [result.length]) + : __("This bank account is already synchronized"); + let result_msg = ` -
    -
    ${result_title}
    -
    ` +
    +
    ${result_title}
    +
    ` + this.parent.$main_section.append(result_msg) - frappe.show_alert({message:__("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator:'green'}); + frappe.show_alert({ message: __("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator: 'green' }); }) }) } @@ -384,7 +388,7 @@ erpnext.accounts.ReconciliationRow = class ReconciliationRow { }) frappe.xcall('erpnext.accounts.page.bank_reconciliation.bank_reconciliation.get_linked_payments', - {bank_transaction: data, freeze:true, freeze_message:__("Finding linked payments")} + { bank_transaction: data, freeze: true, freeze_message: __("Finding linked payments") } ).then((result) => { me.make_dialog(result) }) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py index 532e19cffd..f8154f2edd 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py @@ -2,81 +2,84 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -from frappe import _ -from frappe.utils.password import get_decrypted_password -from plaid import Client -from plaid.errors import APIError, ItemError +import plaid +import requests +from plaid.errors import APIError, ItemError, InvalidRequestError import frappe -import requests +from frappe import _ + class PlaidConnector(): def __init__(self, access_token=None): - - plaid_settings = frappe.get_single("Plaid Settings") - - self.config = { - "plaid_client_id": plaid_settings.plaid_client_id, - "plaid_secret": get_decrypted_password("Plaid Settings", "Plaid Settings", 'plaid_secret'), - "plaid_public_key": plaid_settings.plaid_public_key, - "plaid_env": plaid_settings.plaid_env - } - - self.client = Client(client_id=self.config.get("plaid_client_id"), - secret=self.config.get("plaid_secret"), - public_key=self.config.get("plaid_public_key"), - environment=self.config.get("plaid_env") - ) - self.access_token = access_token + self.settings = frappe.get_single("Plaid Settings") + self.products = ["auth", "transactions"] + self.client_name = frappe.local.site + self.client = plaid.Client( + client_id=self.settings.plaid_client_id, + secret=self.settings.get_password("plaid_secret"), + environment=self.settings.plaid_env, + api_version="2019-05-29" + ) def get_access_token(self, public_token): if public_token is None: frappe.log_error(_("Public token is missing for this bank"), _("Plaid public token error")) - response = self.client.Item.public_token.exchange(public_token) - access_token = response['access_token'] - + access_token = response["access_token"] return access_token + def get_link_token(self): + token_request = { + "client_name": self.client_name, + "client_id": self.settings.plaid_client_id, + "secret": self.settings.plaid_secret, + "products": self.products, + # only allow Plaid-supported languages and countries (LAST: Sep-19-2020) + "language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en", + "country_codes": ["US", "CA", "FR", "IE", "NL", "ES", "GB"], + "user": { + "client_user_id": frappe.generate_hash(frappe.session.user, length=32) + } + } + + try: + response = self.client.LinkToken.create(token_request) + except InvalidRequestError: + frappe.log_error(frappe.get_traceback(), _("Plaid invalid request error")) + frappe.msgprint(_("Please check your Plaid client ID and secret values")) + except APIError as e: + frappe.log_error(frappe.get_traceback(), _("Plaid authentication error")) + frappe.throw(_(str(e)), title=_("Authentication Failed")) + else: + return response["link_token"] + def auth(self): try: self.client.Auth.get(self.access_token) - print("Authentication successful.....") except ItemError as e: - if e.code == 'ITEM_LOGIN_REQUIRED': - pass - else: + if e.code == "ITEM_LOGIN_REQUIRED": pass except APIError as e: - if e.code == 'PLANNED_MAINTENANCE': - pass - else: + if e.code == "PLANNED_MAINTENANCE": pass except requests.Timeout: pass except Exception as e: - print(e) frappe.log_error(frappe.get_traceback(), _("Plaid authentication error")) - frappe.msgprint({"title": _("Authentication Failed"), "message":e, "raise_exception":1, "indicator":'red'}) + frappe.throw(_(str(e)), title=_("Authentication Failed")) def get_transactions(self, start_date, end_date, account_id=None): + self.auth() + account_ids = list(account_id) if account_id else None + try: - self.auth() - if account_id: - account_ids = [account_id] - - response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, account_ids=account_ids) - - else: - response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date) - - transactions = response['transactions'] - - while len(transactions) < response['total_transactions']: + response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, account_ids=account_ids) + transactions = response["transactions"] + while len(transactions) < response["total_transactions"]: response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions)) - transactions.extend(response['transactions']) + transactions.extend(response["transactions"]) return transactions except Exception: frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error")) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js index 0ffbb877ea..22a4004955 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js @@ -4,14 +4,14 @@ frappe.provide("erpnext.integrations"); frappe.ui.form.on('Plaid Settings', { - enabled: function(frm) { + enabled: function (frm) { frm.toggle_reqd('plaid_client_id', frm.doc.enabled); frm.toggle_reqd('plaid_secret', frm.doc.enabled); - frm.toggle_reqd('plaid_public_key', frm.doc.enabled); frm.toggle_reqd('plaid_env', frm.doc.enabled); }, - refresh: function(frm) { - if(frm.doc.enabled) { + + refresh: function (frm) { + if (frm.doc.enabled) { frm.add_custom_button('Link a new bank account', () => { new erpnext.integrations.plaidLink(frm); }); @@ -22,17 +22,16 @@ frappe.ui.form.on('Plaid Settings', { erpnext.integrations.plaidLink = class plaidLink { constructor(parent) { this.frm = parent; - this.product = ["transactions", "auth"]; this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js'; this.init_config(); } - init_config() { - const me = this; - me.plaid_env = me.frm.doc.plaid_env; - me.plaid_public_key = me.frm.doc.plaid_public_key; - me.client_name = frappe.boot.sitename; - me.init_plaid(); + async init_config() { + this.product = ["auth", "transactions"]; + this.plaid_env = this.frm.doc.plaid_env; + this.client_name = frappe.boot.sitename; + this.token = await this.frm.call("get_link_token").then(resp => resp.message); + this.init_plaid(); } init_plaid() { @@ -69,17 +68,17 @@ erpnext.integrations.plaidLink = class plaidLink { } onScriptLoaded(me) { - me.linkHandler = window.Plaid.create({ + me.linkHandler = Plaid.create({ clientName: me.client_name, + product: me.product, env: me.plaid_env, - key: me.plaid_public_key, - onSuccess: me.plaid_success, - product: me.product + token: me.token, + onSuccess: me.plaid_success }); } onScriptError(error) { - frappe.msgprint('There was an issue loading the link-initialize.js script'); + frappe.msgprint("There was an issue connecting to Plaid's authentication server"); frappe.msgprint(error); } @@ -87,21 +86,25 @@ erpnext.integrations.plaidLink = class plaidLink { const me = this; frappe.prompt({ - fieldtype:"Link", + fieldtype: "Link", options: "Company", - label:__("Company"), - fieldname:"company", - reqd:1 + label: __("Company"), + fieldname: "company", + reqd: 1 }, (data) => { me.company = data.company; - frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_institution', {token: token, response: response}) - .then((result) => { - frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_bank_accounts', {response: response, - bank: result, company: me.company}); - }) - .then(() => { - frappe.show_alert({message:__("Bank accounts added"), indicator:'green'}); + frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_institution', { + token: token, + response: response + }).then((result) => { + frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_bank_accounts', { + response: response, + bank: result, + company: me.company }); + }).then(() => { + frappe.show_alert({ message: __("Bank accounts added"), indicator: 'green' }); + }); }, __("Select a company"), __("Continue")); } }; diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json index d8203d7390..2706217223 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json @@ -1,5 +1,4 @@ { - "actions": [], "creation": "2018-10-25 10:02:48.656165", "doctype": "DocType", "editable_grid": 1, @@ -12,7 +11,6 @@ "plaid_client_id", "plaid_secret", "column_break_7", - "plaid_public_key", "plaid_env" ], "fields": [ @@ -41,12 +39,6 @@ "in_list_view": 1, "label": "Plaid Secret" }, - { - "fieldname": "plaid_public_key", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Plaid Public Key" - }, { "fieldname": "plaid_env", "fieldtype": "Select", @@ -69,8 +61,7 @@ } ], "issingle": 1, - "links": [], - "modified": "2020-02-07 15:21:11.616231", + "modified": "2020-09-12 02:31:44.542385", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "Plaid Settings", diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index c3371ed5df..e535e81bde 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -2,30 +2,36 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe import json -from frappe import _ -from frappe.model.document import Document + +import frappe from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_connector import PlaidConnector -from frappe.utils import getdate, formatdate, today, add_months +from frappe import _ from frappe.desk.doctype.tag.tag import add_tag +from frappe.model.document import Document +from frappe.utils import add_months, formatdate, getdate, today + class PlaidSettings(Document): - pass + @staticmethod + def get_link_token(): + plaid = PlaidConnector() + return plaid.get_link_token() + @frappe.whitelist() -def plaid_configuration(): +def get_plaid_configuration(): if frappe.db.get_single_value("Plaid Settings", "enabled"): plaid_settings = frappe.get_single("Plaid Settings") return { - "plaid_public_key": plaid_settings.plaid_public_key, "plaid_env": plaid_settings.plaid_env, + "link_token": plaid_settings.get_link_token(), "client_name": frappe.local.site } - else: - return "disabled" + + return "disabled" + @frappe.whitelist() def add_institution(token, response): @@ -33,6 +39,7 @@ def add_institution(token, response): plaid = PlaidConnector() access_token = plaid.get_access_token(token) + bank = None if not frappe.db.exists("Bank", response["institution"]["name"]): try: @@ -44,7 +51,6 @@ def add_institution(token, response): bank.insert() except Exception: frappe.throw(frappe.get_traceback()) - else: bank = frappe.get_doc("Bank", response["institution"]["name"]) bank.plaid_access_token = access_token @@ -52,6 +58,7 @@ def add_institution(token, response): return bank + @frappe.whitelist() def add_bank_accounts(response, bank, company): try: @@ -92,9 +99,8 @@ def add_bank_accounts(response, bank, company): new_account.insert() result.append(new_account.name) - except frappe.UniqueValidationError: - frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(new_account.account_name)) + frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(account["name"])) except Exception: frappe.throw(frappe.get_traceback()) @@ -103,6 +109,7 @@ def add_bank_accounts(response, bank, company): return result + def add_account_type(account_type): try: frappe.get_doc({ @@ -122,10 +129,11 @@ def add_account_subtype(account_subtype): except Exception: frappe.throw(frappe.get_traceback()) + @frappe.whitelist() def sync_transactions(bank, bank_account): - '''Sync transactions based on the last integration date as the start date, after the sync is completed - add the transaction date of the oldest transaction as the last integration date''' + """Sync transactions based on the last integration date as the start date, after sync is completed + add the transaction date of the oldest transaction as the last integration date.""" last_transaction_date = frappe.db.get_value("Bank Account", bank_account, "last_integration_date") if last_transaction_date: start_date = formatdate(last_transaction_date, "YYYY-MM-dd") @@ -147,10 +155,10 @@ def sync_transactions(bank, bank_account): len(result), bank_account, start_date, end_date)) frappe.db.set_value("Bank Account", bank_account, "last_integration_date", last_transaction_date) - except Exception: frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error")) + def get_transactions(bank, bank_account=None, start_date=None, end_date=None): access_token = None @@ -168,6 +176,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None): return transactions + def new_bank_transaction(transaction): result = [] @@ -182,8 +191,8 @@ def new_bank_transaction(transaction): status = "Pending" if transaction["pending"] == "True" else "Settled" + tags = [] try: - tags = [] tags += transaction["category"] tags += ["Plaid Cat. {}".format(transaction["category_id"])] except KeyError: @@ -216,6 +225,7 @@ def new_bank_transaction(transaction): return result + def automatic_synchronization(): settings = frappe.get_doc("Plaid Settings", "Plaid Settings") @@ -223,4 +233,8 @@ def automatic_synchronization(): plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"]) for plaid_account in plaid_accounts: - frappe.enqueue("erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions", bank=plaid_account.bank, bank_account=plaid_account.name) + frappe.enqueue( + "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions", + bank=plaid_account.bank, + bank_account=plaid_account.name + ) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py index 1a063d6b6f..3c906374c4 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py @@ -1,14 +1,17 @@ # -*- coding: utf-8 -*- # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -from __future__ import unicode_literals -import unittest -import frappe -from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import plaid_configuration, add_account_type, add_account_subtype, new_bank_transaction, add_bank_accounts import json -from frappe.utils.response import json_handler +import unittest + +import frappe from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account +from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import ( + add_account_subtype, add_account_type, add_bank_accounts, + new_bank_transaction, get_plaid_configuration) +from frappe.utils.response import json_handler + class TestPlaidSettings(unittest.TestCase): def setUp(self): @@ -31,7 +34,7 @@ class TestPlaidSettings(unittest.TestCase): def test_plaid_disabled(self): frappe.db.set_value("Plaid Settings", None, "enabled", 0) - self.assertTrue(plaid_configuration() == "disabled") + self.assertTrue(get_plaid_configuration() == "disabled") def test_add_account_type(self): add_account_type("brokerage") @@ -64,7 +67,7 @@ class TestPlaidSettings(unittest.TestCase): 'mask': '0000', 'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK', 'name': 'Plaid Checking' - }], + }], 'institution': { 'institution_id': 'ins_6', 'name': 'Citi' @@ -100,7 +103,7 @@ class TestPlaidSettings(unittest.TestCase): 'mask': '0000', 'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK', 'name': 'Plaid Checking' - }], + }], 'institution': { 'institution_id': 'ins_6', 'name': 'Citi' @@ -152,4 +155,4 @@ class TestPlaidSettings(unittest.TestCase): new_bank_transaction(transactions) - self.assertTrue(len(frappe.get_all("Bank Transaction")) == 1) \ No newline at end of file + self.assertTrue(len(frappe.get_all("Bank Transaction")) == 1) diff --git a/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py b/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py index 8e60d33f85..d2bcb12070 100644 --- a/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py +++ b/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py @@ -4,17 +4,16 @@ from __future__ import unicode_literals import frappe + def execute(): frappe.reload_doc("erpnext_integrations", "doctype", "plaid_settings") plaid_settings = frappe.get_single("Plaid Settings") if plaid_settings.enabled: - if not (frappe.conf.plaid_client_id and frappe.conf.plaid_env \ - and frappe.conf.plaid_public_key and frappe.conf.plaid_secret): + if not (frappe.conf.plaid_client_id and frappe.conf.plaid_env and frappe.conf.plaid_secret): plaid_settings.enabled = 0 else: plaid_settings.update({ "plaid_client_id": frappe.conf.plaid_client_id, - "plaid_public_key": frappe.conf.plaid_public_key, "plaid_env": frappe.conf.plaid_env, "plaid_secret": frappe.conf.plaid_secret }) diff --git a/requirements.txt b/requirements.txt index 912d61f7a6..b7ba412893 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ frappe gocardless-pro==1.11.0 googlemaps==3.1.1 pandas==1.0.5 -plaid-python==3.4.0 +plaid-python==6.0.0 pycountry==19.8.18 PyGithub==1.44.1 python-stdnum==1.12 From de7a2bc4be092133709133cb7c1fcb3ffd608201 Mon Sep 17 00:00:00 2001 From: prssanna Date: Mon, 21 Sep 2020 13:57:04 +0530 Subject: [PATCH 174/192] fix: apply user permissions in tax accounts query --- erpnext/controllers/queries.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index c88bf66411..10ff6ad8d9 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -165,9 +165,14 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters): AND company = %(company)s AND account_currency = %(currency)s AND `{searchfield}` LIKE %(txt)s + {mcond} ORDER BY idx DESC, name LIMIT %(offset)s, %(limit)s - """.format(account_type_condition=account_type_condition, searchfield=searchfield), + """.format( + account_type_condition=account_type_condition, + searchfield=searchfield, + mcond=get_match_cond(doctype) + ), dict( account_types=filters.get("account_type"), company=filters.get("company"), From e3c61286385c80f0bde88c03271702841fefc30f Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Mon, 21 Sep 2020 18:50:22 +0530 Subject: [PATCH 175/192] fix(plaid): do not send null list in account ids (#23376) --- .../doctype/plaid_settings/plaid_connector.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py index f8154f2edd..a033a2a722 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py @@ -72,10 +72,16 @@ class PlaidConnector(): def get_transactions(self, start_date, end_date, account_id=None): self.auth() - account_ids = list(account_id) if account_id else None + kwargs = dict( + access_token=self.access_token, + start_date=start_date, + end_date=end_date + ) + if account_id: + kwargs.update(dict(account_ids=[account_id])) try: - response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, account_ids=account_ids) + response = self.client.Transactions.get(**kwargs) transactions = response["transactions"] while len(transactions) < response["total_transactions"]: response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions)) From 2b53b98c1d438c6e43e1d5cdf1086001e7b1faec Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 22 Sep 2020 12:18:12 +0530 Subject: [PATCH 176/192] fix: reload dashboard (#23380) --- erpnext/patches.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9ce570e6d0..a919b670cb 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -690,6 +690,7 @@ erpnext.patches.v12_0.set_valid_till_date_in_supplier_quotation erpnext.patches.v13_0.update_old_loans erpnext.patches.v12_0.set_serial_no_status #2020-05-21 erpnext.patches.v12_0.update_price_list_currency_in_bom +execute:frappe.reload_doctype('Dashboard') execute:frappe.delete_doc_if_exists('Dashboard', 'Accounts') erpnext.patches.v13_0.update_actual_start_and_end_date_in_wo erpnext.patches.v13_0.set_company_field_in_healthcare_doctypes #2020-05-25 From a0d192eae4c00b6aeaadd5e621d55a2227207674 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 22 Sep 2020 13:54:07 +0530 Subject: [PATCH 177/192] feat: Show searchfields in batch query --- erpnext/controllers/queries.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index c88bf66411..e8fe62fd51 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -359,9 +359,21 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): if filters.get("is_return"): having_clause = "" + meta = frappe.get_meta("Batch", cached=True) + searchfields = meta.get_search_fields() + + search_columns = '' + if searchfields: + search_columns = ", " + ", ".join(searchfields) + if args.get('warehouse'): + searchfields = ['batch.' + field for field in searchfields] + if searchfields: + search_columns = ", " + ", ".join(searchfields) + batch_nos = frappe.db.sql("""select sle.batch_no, round(sum(sle.actual_qty),2), sle.stock_uom, concat('MFG-',batch.manufacturing_date), concat('EXP-',batch.expiry_date) + {search_columns} from `tabStock Ledger Entry` sle INNER JOIN `tabBatch` batch on sle.batch_no = batch.name where @@ -377,6 +389,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): group by batch_no {having_clause} order by batch.expiry_date, sle.batch_no desc limit %(start)s, %(page_len)s""".format( + search_columns = search_columns, cond=cond, match_conditions=get_match_cond(doctype), having_clause = having_clause @@ -384,7 +397,9 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): return batch_nos else: - return frappe.db.sql("""select name, concat('MFG-', manufacturing_date), concat('EXP-',expiry_date) from `tabBatch` batch + return frappe.db.sql("""select name, concat('MFG-', manufacturing_date), concat('EXP-',expiry_date) + {search_columns} + from `tabBatch` batch where batch.disabled = 0 and item = %(item_code)s and (name like %(txt)s @@ -394,7 +409,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): {0} {match_conditions} order by expiry_date, name desc - limit %(start)s, %(page_len)s""".format(cond, match_conditions=get_match_cond(doctype)), args) + limit %(start)s, %(page_len)s""".format(cond, search_columns = search_columns, match_conditions=get_match_cond(doctype)), args) @frappe.whitelist() From 6087fcaa9baeb4431b3ad22b073b4b6c351aaef1 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 22 Sep 2020 17:48:04 +0530 Subject: [PATCH 178/192] chore: Added video settings to List View Menu --- erpnext/utilities/doctype/video/video_list.js | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 erpnext/utilities/doctype/video/video_list.js diff --git a/erpnext/utilities/doctype/video/video_list.js b/erpnext/utilities/doctype/video/video_list.js new file mode 100644 index 0000000000..8273a4a781 --- /dev/null +++ b/erpnext/utilities/doctype/video/video_list.js @@ -0,0 +1,7 @@ +frappe.listview_settings["Video"] = { + onload: (listview) => { + listview.page.add_menu_item(__("Video Settings"), function() { + frappe.set_route("Form","Video Settings", "Video Settings"); + }); + } +} \ No newline at end of file From 1831893b774f0bbc9de09329a0b12fa6f58dc5e7 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 23 Sep 2020 13:01:49 +0530 Subject: [PATCH 179/192] fix: failed workflow condition error message in update items (#23393) --- erpnext/controllers/accounts_controller.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 046fb2ca68..90c466b631 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1264,7 +1264,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil transitions.append(transition.as_dict()) if not transitions: - frappe.throw(_("You do not have workflow access to update this document."), title=_("Insufficient Workflow Permissions")) + frappe.throw( + _("You are not allowed to update as per the conditions set in {} Workflow.").format(get_link_to_form("Workflow", workflow)), + title=_("Insufficient Permissions") + ) def get_new_child_item(item_row): new_child_function = set_sales_order_defaults if parent_doctype == "Sales Order" else set_purchase_order_defaults From 8be51e22c48c3ceaf09196e70f609db6e44a8c68 Mon Sep 17 00:00:00 2001 From: Afshan Date: Wed, 23 Sep 2020 14:52:36 +0530 Subject: [PATCH 180/192] fix: escape apostrophe in cost centre and project if exist --- erpnext/accounts/report/gross_profit/gross_profit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 2563b66d1c..84c74543da 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -268,9 +268,9 @@ class GrossProfitGenerator(object): def get_last_purchase_rate(self, item_code, row): condition = '' if row.project: - condition += " AND a.project='%s'" % (row.project) + condition += " AND a.project=%s" % (frappe.db.escape(row.project)) elif row.cost_center: - condition += " AND a.cost_center='%s'" % (row.cost_center) + condition += " AND a.cost_center=%s" % (frappe.db.escape(row.cost_center)) if self.filters.to_date: condition += " AND modified='%s'" % (self.filters.to_date) From 879a8ef26bdbea3d97812418e080c69638de02eb Mon Sep 17 00:00:00 2001 From: Michelle Alva <50285544+michellealva@users.noreply.github.com> Date: Wed, 23 Sep 2020 15:36:07 +0530 Subject: [PATCH 181/192] fix: Change button label in Item (#23410) --- erpnext/stock/doctype/item/item.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 38e5fe53a7..faeeb578fe 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -26,19 +26,19 @@ frappe.ui.form.on("Item", { refresh: function(frm) { if (frm.doc.is_stock_item) { - frm.add_custom_button(__("Balance"), function() { + frm.add_custom_button(__("Stock Balance"), function() { frappe.route_options = { "item_code": frm.doc.name } frappe.set_route("query-report", "Stock Balance"); }, __("View")); - frm.add_custom_button(__("Ledger"), function() { + frm.add_custom_button(__("Stock Ledger"), function() { frappe.route_options = { "item_code": frm.doc.name } frappe.set_route("query-report", "Stock Ledger"); }, __("View")); - frm.add_custom_button(__("Projected"), function() { + frm.add_custom_button(__("Stock Projected Qty"), function() { frappe.route_options = { "item_code": frm.doc.name } From 46d418038186e7b79e523616b41a80d50b341bec Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 23 Sep 2020 16:53:16 +0530 Subject: [PATCH 182/192] refactor: Issue form cleaned up and renamed Minutes to First Response field (#23066) * refactor: re-order fields in Issue DocType * refactor: rename Mins to First Response to First Response Time * refactor: First Response Time Reports for Issue and Opportunity * fix: added patch for renamed fields and setting durations * chore: update CRM and Support Desk Pages for Response Time reports * fix: first response time for opportunity report * fix: patch Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- erpnext/crm/desk_page/crm/crm.json | 4 +- .../crm/doctype/opportunity/opportunity.json | 18 ++--- .../__init__.py | 0 .../first_response_time_for_opportunity.js} | 25 +++++-- .../first_response_time_for_opportunity.json | 28 +++++++ .../first_response_time_for_opportunity.py | 35 +++++++++ ...tes_to_first_response_for_opportunity.json | 26 ------- ...nutes_to_first_response_for_opportunity.py | 28 ------- erpnext/patches.txt | 1 + .../v13_0/rename_issue_doctype_fields.py | 65 +++++++++++++++++ .../support/desk_page/support/support.json | 4 +- erpnext/support/doctype/issue/issue.js | 30 ++++---- erpnext/support/doctype/issue/issue.json | 73 +++++++++---------- erpnext/support/doctype/issue/issue.py | 42 +++++------ erpnext/support/doctype/issue/test_issue.py | 2 +- .../__init__.py | 0 .../first_response_time_for_issues.js | 44 +++++++++++ .../first_response_time_for_issues.json | 26 +++++++ .../first_response_time_for_issues.py | 35 +++++++++ .../minutes_to_first_response_for_issues.js | 30 -------- .../minutes_to_first_response_for_issues.json | 24 ------ .../minutes_to_first_response_for_issues.py | 28 ------- 22 files changed, 340 insertions(+), 228 deletions(-) rename erpnext/crm/report/{minutes_to_first_response_for_opportunity => first_response_time_for_opportunity}/__init__.py (100%) rename erpnext/crm/report/{minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.js => first_response_time_for_opportunity/first_response_time_for_opportunity.js} (55%) create mode 100644 erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.json create mode 100644 erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.py delete mode 100644 erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.json delete mode 100644 erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.py create mode 100644 erpnext/patches/v13_0/rename_issue_doctype_fields.py rename erpnext/support/report/{minutes_to_first_response_for_issues => first_response_time_for_issues}/__init__.py (100%) create mode 100644 erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js create mode 100644 erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.json create mode 100644 erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py delete mode 100644 erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.js delete mode 100644 erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.json delete mode 100644 erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.py diff --git a/erpnext/crm/desk_page/crm/crm.json b/erpnext/crm/desk_page/crm/crm.json index eb69dc06b6..d974beb2de 100644 --- a/erpnext/crm/desk_page/crm/crm.json +++ b/erpnext/crm/desk_page/crm/crm.json @@ -8,7 +8,7 @@ { "hidden": 0, "label": "Reports", - "links": "[\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Details\",\n \"name\": \"Lead Details\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Prospects Engaged But Not Converted\",\n \"name\": \"Prospects Engaged But Not Converted\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Opportunity\"\n ],\n \"doctype\": \"Opportunity\",\n \"is_query_report\": true,\n \"label\": \"Minutes to First Response for Opportunity\",\n \"name\": \"Minutes to First Response for Opportunity\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Campaign Efficiency\",\n \"name\": \"Campaign Efficiency\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Owner Efficiency\",\n \"name\": \"Lead Owner Efficiency\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Details\",\n \"name\": \"Lead Details\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Prospects Engaged But Not Converted\",\n \"name\": \"Prospects Engaged But Not Converted\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Opportunity\"\n ],\n \"doctype\": \"Opportunity\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Opportunity\",\n \"name\": \"First Response Time for Opportunity\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Campaign Efficiency\",\n \"name\": \"Campaign Efficiency\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Owner Efficiency\",\n \"name\": \"Lead Owner Efficiency\",\n \"type\": \"report\"\n }\n]" }, { "hidden": 0, @@ -42,7 +42,7 @@ "idx": 0, "is_standard": 1, "label": "CRM", - "modified": "2020-05-28 13:33:52.906750", + "modified": "2020-08-11 18:55:18.238900", "modified_by": "Administrator", "module": "CRM", "name": "CRM", diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json index b61cad3620..eee13f7e79 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -24,7 +24,7 @@ "converted_by", "sales_stage", "order_lost_reason", - "mins_to_first_response", + "first_response_time", "expected_closing", "next_contact", "contact_by", @@ -152,13 +152,6 @@ "no_copy": 1, "read_only": 1 }, - { - "bold": 1, - "fieldname": "mins_to_first_response", - "fieldtype": "Float", - "label": "Mins to first response", - "read_only": 1 - }, { "fieldname": "expected_closing", "fieldtype": "Date", @@ -419,12 +412,19 @@ "fieldtype": "Link", "label": "Converted By", "options": "User" + }, + { + "bold": 1, + "fieldname": "first_response_time", + "fieldtype": "Duration", + "label": "First Response Time", + "read_only": 1 } ], "icon": "fa fa-info-sign", "idx": 195, "links": [], - "modified": "2020-08-11 17:34:35.066961", + "modified": "2020-08-12 17:34:35.066961", "modified_by": "Administrator", "module": "CRM", "name": "Opportunity", diff --git a/erpnext/crm/report/minutes_to_first_response_for_opportunity/__init__.py b/erpnext/crm/report/first_response_time_for_opportunity/__init__.py similarity index 100% rename from erpnext/crm/report/minutes_to_first_response_for_opportunity/__init__.py rename to erpnext/crm/report/first_response_time_for_opportunity/__init__.py diff --git a/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.js b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js similarity index 55% rename from erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.js rename to erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js index 92d026a79c..3f5c95ab0a 100644 --- a/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.js +++ b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js @@ -1,33 +1,44 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +/* eslint-disable */ -frappe.query_reports["Minutes to First Response for Opportunity"] = { +frappe.query_reports["First Response Time for Opportunity"] = { "filters": [ { "fieldname": "from_date", "label": __("From Date"), "fieldtype": "Date", - 'reqd': 1, + "reqd": 1, "default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30) }, { "fieldname": "to_date", "label": __("To Date"), "fieldtype": "Date", - 'reqd': 1, + "reqd": 1, "default": frappe.datetime.nowdate() }, ], - get_chart_data: function (columns, result) { + get_chart_data: function (_columns, result) { return { data: { labels: result.map(d => d[0]), datasets: [{ - name: 'Mins to first response', + name: "First Response Time", values: result.map(d => d[1]) }] }, - type: 'line', + type: "line", + tooltipOptions: { + formatTooltipY: d => { + let duration_options = { + hide_days: 0, + hide_seconds: 0 + }; + value = frappe.utils.get_formatted_duration(d, duration_options); + return value; + } + } } } -} +}; diff --git a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.json b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.json new file mode 100644 index 0000000000..1b3184fe0a --- /dev/null +++ b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.json @@ -0,0 +1,28 @@ +{ + "add_total_row": 0, + "creation": "2020-08-10 18:34:19.083872", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "letter_head": "Test 2", + "modified": "2020-08-10 18:34:19.083872", + "modified_by": "Administrator", + "module": "CRM", + "name": "First Response Time for Opportunity", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Opportunity", + "report_name": "First Response Time for Opportunity", + "report_type": "Script Report", + "roles": [ + { + "role": "Sales User" + }, + { + "role": "Sales Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.py b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.py new file mode 100644 index 0000000000..2ffbc3e62a --- /dev/null +++ b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.py @@ -0,0 +1,35 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe + +def execute(filters=None): + columns = [ + { + 'fieldname': 'creation_date', + 'label': 'Date', + 'fieldtype': 'Date', + 'width': 300 + }, + { + 'fieldname': 'first_response_time', + 'fieldtype': 'Duration', + 'label': 'First Response Time', + 'width': 300 + }, + ] + + data = frappe.db.sql(''' + SELECT + date(creation) as creation_date, + avg(first_response_time) as avg_response_time + FROM tabOpportunity + WHERE + date(creation) between %s and %s + and first_response_time > 0 + GROUP BY creation_date + ORDER BY creation_date desc + ''', (filters.from_date, filters.to_date)) + + return columns, data diff --git a/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.json b/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.json deleted file mode 100644 index bcd092ba97..0000000000 --- a/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "add_total_row": 0, - "apply_user_permissions": 0, - "creation": "2016-06-17 11:28:25.867258", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 2, - "is_standard": "Yes", - "modified": "2017-02-24 20:06:08.801109", - "modified_by": "Administrator", - "module": "CRM", - "name": "Minutes to First Response for Opportunity", - "owner": "Administrator", - "ref_doctype": "Opportunity", - "report_name": "Minutes to First Response for Opportunity", - "report_type": "Script Report", - "roles": [ - { - "role": "Sales User" - }, - { - "role": "Sales Manager" - } - ] -} \ No newline at end of file diff --git a/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.py b/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.py deleted file mode 100644 index 54e3a60308..0000000000 --- a/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe - -def execute(filters=None): - columns = [ - { - 'fieldname': 'creation_date', - 'label': 'Date', - 'fieldtype': 'Date' - }, - { - 'fieldname': 'mins', - 'fieldtype': 'Float', - 'label': 'Mins to First Response' - }, - ] - - data = frappe.db.sql('''select date(creation) as creation_date, - avg(mins_to_first_response) as mins - from tabOpportunity - where date(creation) between %s and %s - and mins_to_first_response > 0 - group by creation_date order by creation_date desc''', (filters.from_date, filters.to_date)) - - return columns, data diff --git a/erpnext/patches.txt b/erpnext/patches.txt index c1aa4a632e..6087ce29aa 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -726,5 +726,6 @@ erpnext.patches.v12_0.rename_lost_reason_detail erpnext.patches.v13_0.drop_razorpay_payload_column erpnext.patches.v13_0.update_start_end_date_for_old_shift_assignment erpnext.patches.v13_0.setting_custom_roles_for_some_regional_reports +erpnext.patches.v13_0.rename_issue_doctype_fields erpnext.patches.v13_0.change_default_pos_print_format erpnext.patches.v13_0.set_youtube_video_id diff --git a/erpnext/patches/v13_0/rename_issue_doctype_fields.py b/erpnext/patches/v13_0/rename_issue_doctype_fields.py new file mode 100644 index 0000000000..5bd6596579 --- /dev/null +++ b/erpnext/patches/v13_0/rename_issue_doctype_fields.py @@ -0,0 +1,65 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.utils.rename_field import rename_field + +def execute(): + if frappe.db.exists('DocType', 'Issue'): + issues = frappe.db.get_all('Issue', fields=['name', 'response_by_variance', 'resolution_by_variance', 'mins_to_first_response'], + order_by='creation desc') + frappe.reload_doc('support', 'doctype', 'issue') + + # rename fields + rename_map = { + 'agreement_fulfilled': 'agreement_status', + 'mins_to_first_response': 'first_response_time' + } + for old, new in rename_map.items(): + rename_field('Issue', old, new) + + # change fieldtype to duration + count = 0 + for entry in issues: + response_by_variance = convert_to_seconds(entry.response_by_variance, 'Hours') + resolution_by_variance = convert_to_seconds(entry.resolution_by_variance, 'Hours') + mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, 'Minutes') + frappe.db.set_value('Issue', entry.name, { + 'response_by_variance': response_by_variance, + 'resolution_by_variance': resolution_by_variance, + 'first_response_time': mins_to_first_response + }) + # commit after every 100 updates + count += 1 + if count%100 == 0: + frappe.db.commit() + + if frappe.db.exists('DocType', 'Opportunity'): + opportunities = frappe.db.get_all('Opportunity', fields=['name', 'mins_to_first_response'], order_by='creation desc') + frappe.reload_doc('crm', 'doctype', 'opportunity') + rename_field('Opportunity', 'mins_to_first_response', 'first_response_time') + + # change fieldtype to duration + count = 0 + for entry in opportunities: + mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, 'Minutes') + frappe.db.set_value('Opportunity', entry.name, 'first_response_time', mins_to_first_response) + # commit after every 100 updates + count += 1 + if count%100 == 0: + frappe.db.commit() + + # renamed reports from "Minutes to First Response for Issues" to "First Response Time for Issues". Same for Opportunity + for report in ['Minutes to First Response for Issues', 'Minutes to First Response for Opportunity']: + if frappe.db.exists('Report', report): + frappe.delete_doc('Report', report) + + +def convert_to_seconds(value, unit): + seconds = 0 + if unit == 'Hours': + seconds = value * 3600 + if unit == 'Minutes': + seconds = value * 60 + return seconds diff --git a/erpnext/support/desk_page/support/support.json b/erpnext/support/desk_page/support/support.json index b1ad7c8aa0..28410f3a71 100644 --- a/erpnext/support/desk_page/support/support.json +++ b/erpnext/support/desk_page/support/support.json @@ -28,7 +28,7 @@ { "hidden": 0, "label": "Reports", - "links": "[\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"Minutes to First Response for Issues\",\n \"name\": \"Minutes to First Response for Issues\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Issues\",\n \"name\": \"First Response Time for Issues\",\n \"type\": \"report\"\n }\n]" } ], "category": "Modules", @@ -43,7 +43,7 @@ "idx": 0, "is_standard": 1, "label": "Support", - "modified": "2020-06-04 11:54:56.124219", + "modified": "2020-08-11 15:49:34.307341", "modified_by": "Administrator", "module": "Support", "name": "Support", diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index 858564a527..fe01d4b983 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -2,10 +2,14 @@ frappe.ui.form.on("Issue", { onload: function(frm) { frm.email_field = "raised_by"; - frappe.db.get_value("Support Settings", {name: "Support Settings"}, "allow_resetting_service_level_agreement", (r) => { - if (!r.allow_resetting_service_level_agreement) { - frm.set_df_property("reset_service_level_agreement", "hidden", 1) ; - } + frappe.db.get_value("Support Settings", {name: "Support Settings"}, + ["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => { + if (r && r.track_service_level_agreement == "0") { + frm.set_df_property("service_level_section", "hidden", 1); + } + if (r && r.allow_resetting_service_level_agreement == "0") { + frm.set_df_property("reset_service_level_agreement", "hidden", 1); + } }); if (frm.doc.service_level_agreement) { @@ -38,7 +42,7 @@ frappe.ui.form.on("Issue", { }, refresh: function (frm) { - if (frm.doc.status !== "Closed" && frm.doc.agreement_fulfilled === "Ongoing") { + if (frm.doc.status !== "Closed" && frm.doc.agreement_status === "Ongoing") { if (frm.doc.service_level_agreement) { frappe.call({ 'method': 'frappe.client.get', @@ -85,14 +89,14 @@ frappe.ui.form.on("Issue", { if (frm.doc.service_level_agreement) { frm.dashboard.clear_headline(); - let agreement_fulfilled = (frm.doc.agreement_fulfilled == "Fulfilled") ? + let agreement_status = (frm.doc.agreement_status == "Fulfilled") ? {"indicator": "green", "msg": "Service Level Agreement has been fulfilled"} : {"indicator": "red", "msg": "Service Level Agreement Failed"}; frm.dashboard.set_headline_alert( '
    ' + '
    ' + - ' ' + + ' ' + '
    ' + '
    ' ); @@ -198,13 +202,13 @@ function set_time_to_resolve_and_response(frm) { frm.dashboard.clear_headline(); var time_to_respond = get_status(frm.doc.response_by_variance); - if (!frm.doc.first_responded_on && frm.doc.agreement_fulfilled === "Ongoing") { - time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_fulfilled); + if (!frm.doc.first_responded_on && frm.doc.agreement_status === "Ongoing") { + time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_status); } var time_to_resolve = get_status(frm.doc.resolution_by_variance); - if (!frm.doc.resolution_date && frm.doc.agreement_fulfilled === "Ongoing") { - time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_fulfilled); + if (!frm.doc.resolution_date && frm.doc.agreement_status === "Ongoing") { + time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status); } frm.dashboard.set_headline_alert( @@ -219,10 +223,10 @@ function set_time_to_resolve_and_response(frm) { ); } -function get_time_left(timestamp, agreement_fulfilled) { +function get_time_left(timestamp, agreement_status) { const diff = moment(timestamp).diff(moment()); const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : "Failed"; - let indicator = (diff_display == 'Failed' && agreement_fulfilled != "Fulfilled") ? "red" : "green"; + let indicator = (diff_display == 'Failed' && agreement_status != "Fulfilled") ? "red" : "green"; return {"diff_display": diff_display, "indicator": indicator}; } diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 6525ab27d3..a43381c5c6 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -27,17 +27,25 @@ "response_by_variance", "reset_service_level_agreement", "cb", - "agreement_fulfilled", + "agreement_status", "resolution_by", "resolution_by_variance", "service_level_agreement_creation", "on_hold_since", "total_hold_time", "response", - "mins_to_first_response", + "first_response_time", "first_responded_on", "column_break_26", "avg_response_time", + "section_break_19", + "resolution_details", + "column_break1", + "opening_date", + "opening_time", + "resolution_date", + "resolution_time", + "user_resolution_time", "additional_info", "lead", "contact", @@ -46,23 +54,14 @@ "customer_name", "project", "company", - "section_break_19", - "resolution_details", - "column_break1", - "opening_date", - "opening_time", - "resolution_date", - "content_type", - "attachment", "via_customer_portal", - "resolution_time", - "user_resolution_time" + "attachment", + "content_type" ], "fields": [ { "fieldname": "subject_section", "fieldtype": "Section Break", - "label": "Subject", "options": "fa fa-flag" }, { @@ -158,7 +157,7 @@ "collapsible": 1, "fieldname": "service_level_section", "fieldtype": "Section Break", - "label": "Service Level" + "label": "Service Level Agreement Details" }, { "fieldname": "service_level_agreement", @@ -191,14 +190,7 @@ "collapsible": 1, "fieldname": "response", "fieldtype": "Section Break", - "label": "Response" - }, - { - "bold": 1, - "fieldname": "mins_to_first_response", - "fieldtype": "Float", - "label": "Mins to First Response", - "read_only": 1 + "label": "Response Details" }, { "fieldname": "first_responded_on", @@ -261,7 +253,7 @@ "collapsible": 1, "fieldname": "section_break_19", "fieldtype": "Section Break", - "label": "Resolution" + "label": "Resolution Details" }, { "depends_on": "eval:!doc.__islocal", @@ -326,28 +318,19 @@ "fieldtype": "Check", "label": "Via Customer Portal" }, - { - "default": "Ongoing", - "depends_on": "eval: doc.service_level_agreement", - "fieldname": "agreement_fulfilled", - "fieldtype": "Select", - "label": "Service Level Agreement Fulfilled", - "options": "Ongoing\nFulfilled\nFailed", - "read_only": 1 - }, { "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';", - "description": "in hours", "fieldname": "response_by_variance", - "fieldtype": "Float", + "fieldtype": "Duration", + "hide_seconds": 1, "label": "Response By Variance", "read_only": 1 }, { "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';", - "description": "in hours", "fieldname": "resolution_by_variance", - "fieldtype": "Float", + "fieldtype": "Duration", + "hide_seconds": 1, "label": "Resolution By Variance", "read_only": 1 }, @@ -406,12 +389,28 @@ "fieldtype": "Duration", "label": "Total Hold Time", "read_only": 1 + }, + { + "default": "Ongoing", + "depends_on": "eval: doc.service_level_agreement", + "fieldname": "agreement_status", + "fieldtype": "Select", + "label": "Service Level Agreement Status", + "options": "Ongoing\nFulfilled\nFailed", + "read_only": 1 + }, + { + "bold": 1, + "fieldname": "first_response_time", + "fieldtype": "Duration", + "label": "First Response Time", + "read_only": 1 } ], "icon": "fa fa-ticket", "idx": 7, "links": [], - "modified": "2020-06-10 12:47:37.146914", + "modified": "2020-08-11 18:49:07.574769", "modified_by": "Administrator", "module": "Support", "name": "Issue", diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 87168e151e..920c13c38d 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -61,7 +61,7 @@ class Issue(Document): if self.status in ["Closed", "Resolved"] and status not in ["Resolved", "Closed"]: self.resolution_date = frappe.flags.current_time or now_datetime() - if frappe.db.get_value("Issue", self.name, "agreement_fulfilled") == "Ongoing": + if frappe.db.get_value("Issue", self.name, "agreement_status") == "Ongoing": set_service_level_agreement_variance(issue=self.name) self.update_agreement_status() set_resolution_time(issue=self) @@ -72,7 +72,7 @@ class Issue(Document): self.resolution_date = None self.reset_issue_metrics() # enable SLA and variance on Reopen - self.agreement_fulfilled = "Ongoing" + self.agreement_status = "Ongoing" set_service_level_agreement_variance(issue=self.name) self.handle_hold_time(status) @@ -113,39 +113,39 @@ class Issue(Document): if not self.first_responded_on: response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) response_by = add_to_date(response_by, seconds=round(last_hold_time)) - response_by_variance = round(time_diff_in_hours(response_by, now_time)) + response_by_variance = round(time_diff_in_seconds(response_by, now_time)) update_values['response_by'] = response_by - update_values['response_by_variance'] = response_by_variance + (last_hold_time // 3600) + update_values['response_by_variance'] = response_by_variance + last_hold_time resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time)) - resolution_by_variance = round(time_diff_in_hours(resolution_by, now_time)) + resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time)) update_values['resolution_by'] = resolution_by - update_values['resolution_by_variance'] = resolution_by_variance + (last_hold_time // 3600) + update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time update_values['on_hold_since'] = None self.db_set(update_values) def update_agreement_status(self): - if self.service_level_agreement and self.agreement_fulfilled == "Ongoing": + if self.service_level_agreement and self.agreement_status == "Ongoing": if frappe.db.get_value("Issue", self.name, "response_by_variance") < 0 or \ frappe.db.get_value("Issue", self.name, "resolution_by_variance") < 0: - self.agreement_fulfilled = "Failed" + self.agreement_status = "Failed" else: - self.agreement_fulfilled = "Fulfilled" + self.agreement_status = "Fulfilled" - def update_agreement_fulfilled_on_custom_status(self): + def update_agreement_status_on_custom_status(self): """ Update Agreement Fulfilled status using Custom Scripts for Custom Issue Status """ if not self.first_responded_on: # first_responded_on set when first reply is sent to customer - self.response_by_variance = round(time_diff_in_hours(self.response_by, now_datetime()), 2) + self.response_by_variance = round(time_diff_in_seconds(self.response_by, now_datetime()), 2) if not self.resolution_date: # resolution_date set when issue has been closed - self.resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_datetime()), 2) + self.resolution_by_variance = round(time_diff_in_seconds(self.resolution_by, now_datetime()), 2) - self.agreement_fulfilled = "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed" + self.agreement_status = "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed" def create_communication(self): communication = frappe.new_doc("Communication") @@ -172,7 +172,7 @@ class Issue(Document): replicated_issue = deepcopy(self) replicated_issue.subject = subject replicated_issue.issue_split_from = self.name - replicated_issue.mins_to_first_response = 0 + replicated_issue.first_response_time = 0 replicated_issue.first_responded_on = None replicated_issue.creation = now_datetime() @@ -180,7 +180,7 @@ class Issue(Document): if replicated_issue.service_level_agreement: replicated_issue.service_level_agreement_creation = now_datetime() replicated_issue.service_level_agreement = None - replicated_issue.agreement_fulfilled = "Ongoing" + replicated_issue.agreement_status = "Ongoing" replicated_issue.response_by = None replicated_issue.response_by_variance = None replicated_issue.resolution_by = None @@ -241,8 +241,8 @@ class Issue(Document): self.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) self.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) - self.response_by_variance = round(time_diff_in_hours(self.response_by, now_datetime())) - self.resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_datetime())) + self.response_by_variance = round(time_diff_in_seconds(self.response_by, now_datetime())) + self.resolution_by_variance = round(time_diff_in_seconds(self.resolution_by, now_datetime())) def change_service_level_agreement_and_priority(self): if self.service_level_agreement and frappe.db.exists("Issue", self.name) and \ @@ -271,7 +271,7 @@ class Issue(Document): self.service_level_agreement_creation = now_datetime() self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) - self.agreement_fulfilled = "Ongoing" + self.agreement_status = "Ongoing" self.save() def reset_issue_metrics(self): @@ -347,7 +347,7 @@ def get_expected_time_for(parameter, service_level, start_date_time): def set_service_level_agreement_variance(issue=None): current_time = frappe.flags.current_time or now_datetime() - filters = {"status": "Open", "agreement_fulfilled": "Ongoing"} + filters = {"status": "Open", "agreement_status": "Ongoing"} if issue: filters = {"name": issue} @@ -358,13 +358,13 @@ def set_service_level_agreement_variance(issue=None): variance = round(time_diff_in_hours(doc.response_by, current_time), 2) frappe.db.set_value(dt="Issue", dn=doc.name, field="response_by_variance", val=variance, update_modified=False) if variance < 0: - frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False) + frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False) if not doc.resolution_date: # resolution_date set when issue has been closed variance = round(time_diff_in_hours(doc.resolution_by, current_time), 2) frappe.db.set_value(dt="Issue", dn=doc.name, field="resolution_by_variance", val=variance, update_modified=False) if variance < 0: - frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False) + frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False) def set_resolution_time(issue): diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index fb8ceb53b2..c962dc6b31 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -73,7 +73,7 @@ class TestIssue(unittest.TestCase): issue.status = 'Closed' issue.save() - self.assertEqual(issue.agreement_fulfilled, 'Fulfilled') + self.assertEqual(issue.agreement_status, 'Fulfilled') def test_issue_metrics(self): creation = datetime.datetime(2020, 3, 4, 4, 0) diff --git a/erpnext/support/report/minutes_to_first_response_for_issues/__init__.py b/erpnext/support/report/first_response_time_for_issues/__init__.py similarity index 100% rename from erpnext/support/report/minutes_to_first_response_for_issues/__init__.py rename to erpnext/support/report/first_response_time_for_issues/__init__.py diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js new file mode 100644 index 0000000000..576e0b76da --- /dev/null +++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js @@ -0,0 +1,44 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["First Response Time for Issues"] = { + "filters": [ + { + "fieldname": "from_date", + "label": __("From Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30) + }, + { + "fieldname": "to_date", + "label": __("To Date"), + "fieldtype": "Date", + "reqd": 1, + "default":frappe.datetime.nowdate() + } + ], + get_chart_data: function(_columns, result) { + return { + data: { + labels: result.map(d => d[0]), + datasets: [{ + name: 'First Response Time', + values: result.map(d => d[1]) + }] + }, + type: "line", + tooltipOptions: { + formatTooltipY: d => { + let duration_options = { + hide_days: 0, + hide_seconds: 0 + }; + value = frappe.utils.get_formatted_duration(d, duration_options); + return value; + } + } + } + } +}; diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.json b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.json new file mode 100644 index 0000000000..c4fe6f5193 --- /dev/null +++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.json @@ -0,0 +1,26 @@ +{ + "add_total_row": 0, + "creation": "2020-08-10 18:12:42.391224", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "letter_head": "Test 2", + "modified": "2020-08-10 18:12:42.391224", + "modified_by": "Administrator", + "module": "Support", + "name": "First Response Time for Issues", + "owner": "Administrator", + "prepared_report": 0, + "query": "select date(creation) as creation_date, avg(mins_to_first_response) from tabIssue where creation > '2016-05-01' group by date(creation) order by creation_date;", + "ref_doctype": "Issue", + "report_name": "First Response Time for Issues", + "report_type": "Script Report", + "roles": [ + { + "role": "Support Team" + } + ] +} \ No newline at end of file diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py new file mode 100644 index 0000000000..922da2b33d --- /dev/null +++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py @@ -0,0 +1,35 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe + +def execute(filters=None): + columns = [ + { + 'fieldname': 'creation_date', + 'label': 'Date', + 'fieldtype': 'Date', + 'width': 300 + }, + { + 'fieldname': 'first_response_time', + 'fieldtype': 'Duration', + 'label': 'First Response Time', + 'width': 300 + }, + ] + + data = frappe.db.sql(''' + SELECT + date(creation) as creation_date, + avg(first_response_time) as avg_response_time + FROM tabIssue + WHERE + date(creation) between %s and %s + and first_response_time > 0 + GROUP BY creation_date + ORDER BY creation_date desc + ''', (filters.from_date, filters.to_date)) + + return columns, data \ No newline at end of file diff --git a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.js b/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.js deleted file mode 100644 index 034e7779a6..0000000000 --- a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.js +++ /dev/null @@ -1,30 +0,0 @@ -frappe.query_reports["Minutes to First Response for Issues"] = { - "filters": [ - { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - 'reqd': 1, - "default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30) - }, - { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - 'reqd': 1, - "default":frappe.datetime.nowdate() - }, - ], - get_chart_data: function(columns, result) { - return { - data: { - labels: result.map(d => d[0]), - datasets: [{ - name: 'Mins to first response', - values: result.map(d => d[1]) - }] - }, - type: 'line', - } - } -} diff --git a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.json b/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.json deleted file mode 100644 index 539d3d941f..0000000000 --- a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2016-06-14 17:44:26.034112", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 2, - "is_standard": "Yes", - "modified": "2017-02-24 20:06:18.391100", - "modified_by": "Administrator", - "module": "Support", - "name": "Minutes to First Response for Issues", - "owner": "Administrator", - "query": "select date(creation) as creation_date, avg(mins_to_first_response) from tabIssue where creation > '2016-05-01' group by date(creation) order by creation_date;", - "ref_doctype": "Issue", - "report_name": "Minutes to First Response for Issues", - "report_type": "Script Report", - "roles": [ - { - "role": "Support Team" - } - ] -} \ No newline at end of file diff --git a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.py b/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.py deleted file mode 100644 index 57c2d442b2..0000000000 --- a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe - -def execute(filters=None): - columns = [ - { - 'fieldname': 'creation_date', - 'label': 'Date', - 'fieldtype': 'Date' - }, - { - 'fieldname': 'mins', - 'fieldtype': 'Float', - 'label': 'Mins to First Response' - }, - ] - - data = frappe.db.sql('''select date(creation) as creation_date, - avg(mins_to_first_response) as mins - from tabIssue - where date(creation) between %s and %s - and mins_to_first_response > 0 - group by creation_date order by creation_date desc''', (filters.from_date, filters.to_date)) - - return columns, data From fd42af5b917e101ef99b8dc8448840d61fc54a3c Mon Sep 17 00:00:00 2001 From: Prssanna Desai Date: Wed, 23 Sep 2020 17:14:37 +0530 Subject: [PATCH 183/192] fix: ignore permission while creating supplier scorecard period in supplier scorecard (#23406) --- erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py | 1 + .../supplier_scorecard_period/supplier_scorecard_period.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py index af109ba284..e956afdf74 100644 --- a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py +++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py @@ -178,6 +178,7 @@ def make_all_scorecards(docname): period_card = make_supplier_scorecard(docname, None) period_card.start_date = start_date period_card.end_date = end_date + period_card.insert(ignore_permissions=True) period_card.submit() scp_count = scp_count + 1 if start_date < first_start_date: diff --git a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py index 87f10336f4..9938710e6e 100644 --- a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py +++ b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py @@ -106,7 +106,7 @@ def make_supplier_scorecard(source_name, target_doc=None): "doctype": "Supplier Scorecard Scoring Criteria", "postprocess": update_criteria_fields, } - }, target_doc, post_process) + }, target_doc, post_process, ignore_permissions=True) return doc From cfc5e291274ef62f452cc9edfab686667d2da027 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Wed, 23 Sep 2020 17:19:11 +0530 Subject: [PATCH 184/192] fix: Post cancellation accounting entry on posting date instaed of current (#23361) --- erpnext/accounts/general_ledger.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 01d3903d28..c12e006d2b 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -320,7 +320,6 @@ def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None, entry['remarks'] = "On cancellation of " + entry['voucher_no'] entry['is_cancelled'] = 1 - entry['posting_date'] = today() if entry['debit'] or entry['credit']: make_entry(entry, adv_adj, "Yes") From 8370d9b997c4fb9368b29102a6fd2fa9ceb73a96 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 24 Sep 2020 11:51:03 +0530 Subject: [PATCH 185/192] fix: Check Company in Payment Entry before selecting values --- .../doctype/payment_entry/payment_entry.js | 18 ++++++++++++++++-- erpnext/public/js/controllers/accounts.js | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 9fc44bc1f0..e117471738 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -12,9 +12,10 @@ frappe.ui.form.on('Payment Entry', { setup: function(frm) { frm.set_query("paid_from", function() { + frm.events.validate_company(frm); + var account_types = in_list(["Pay", "Internal Transfer"], frm.doc.payment_type) ? ["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]]; - return { filters: { "account_type": ["in", account_types], @@ -23,13 +24,16 @@ frappe.ui.form.on('Payment Entry', { } } }); + frm.set_query("party_type", function() { + frm.events.validate_company(frm); return{ filters: { "name": ["in", Object.keys(frappe.boot.party_account_types)], } } }); + frm.set_query("party_bank_account", function() { return { filters: { @@ -39,6 +43,7 @@ frappe.ui.form.on('Payment Entry', { } } }); + frm.set_query("bank_account", function() { return { filters: { @@ -47,6 +52,7 @@ frappe.ui.form.on('Payment Entry', { } } }); + frm.set_query("contact_person", function() { if (frm.doc.party) { return { @@ -58,10 +64,12 @@ frappe.ui.form.on('Payment Entry', { }; } }); + frm.set_query("paid_to", function() { + frm.events.validate_company(frm); + var account_types = in_list(["Receive", "Internal Transfer"], frm.doc.payment_type) ? ["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]]; - return { filters: { "account_type": ["in", account_types], @@ -150,6 +158,12 @@ frappe.ui.form.on('Payment Entry', { frm.events.show_general_ledger(frm); }, + validate_company: (frm) => { + if (!frm.doc.company){ + frappe.throw({message:__("Please select a Company first."), title: __("Mandatory")}); + } + }, + company: function(frm) { frm.events.hide_unhide_fields(frm); frm.events.set_dynamic_labels(frm); diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index f4eaad58da..6e97d811fc 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -120,7 +120,7 @@ frappe.ui.form.on('Salary Structure', { var get_payment_mode_account = function(frm, mode_of_payment, callback) { if(!frm.doc.company) { - frappe.throw(__("Please select the Company first")); + frappe.throw({message:__("Please select a Company first."), title: __("Mandatory")}); } if(!mode_of_payment) { From 4f395cce54579e02804bc3678b35ff4cc0fcd8d2 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 24 Sep 2020 12:43:41 +0530 Subject: [PATCH 186/192] chore: Add Filter Group to dialogs --- erpnext/public/js/utils.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 87982f14a6..9ed500932f 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -480,7 +480,7 @@ erpnext.utils.update_child_items = function(opts) { callback: r => { if(!r.exc) { if (this.doc.conversion_factor == r.message.conversion_factor) return; - + const docname = this.doc.docname; dialog.fields_dict.trans_items.df.data.some(doc => { if (doc.docname == docname) { @@ -677,6 +677,7 @@ erpnext.utils.map_current_doc = function(opts) { date_field: opts.date_field || undefined, setters: opts.setters, get_query: opts.get_query, + add_filters_group: 1, action: function(selections, args) { let values = selections; if(values.length === 0){ From 2fa2a40b3f37a83380dc52a493c8a3a628fcb8c4 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 24 Sep 2020 15:07:29 +0530 Subject: [PATCH 187/192] chore: make asset movement transaction date match with purchase date & time (#23423) --- erpnext/assets/doctype/asset/asset.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 72debb7eba..efdbdb1fbf 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, erpnext, math, json from frappe import _ from six import string_types -from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, month_diff, add_days, get_last_day +from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, month_diff, add_days, get_last_day, get_datetime from frappe.model.document import Document from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset.depreciation \ @@ -140,6 +140,10 @@ class Asset(AccountsController): def make_asset_movement(self): reference_doctype = 'Purchase Receipt' if self.purchase_receipt else 'Purchase Invoice' reference_docname = self.purchase_receipt or self.purchase_invoice + transaction_date = getdate(self.purchase_date) + if reference_docname: + posting_date, posting_time = frappe.db.get_value(reference_doctype, reference_docname, ["posting_date", "posting_time"]) + transaction_date = get_datetime("{} {}".format(posting_date, posting_time)) assets = [{ 'asset': self.name, 'asset_name': self.asset_name, @@ -151,7 +155,7 @@ class Asset(AccountsController): 'assets': assets, 'purpose': 'Receipt', 'company': self.company, - 'transaction_date': getdate(self.purchase_date), + 'transaction_date': transaction_date, 'reference_doctype': reference_doctype, 'reference_name': reference_docname }).insert() From 751256518483252771bc47a4452800a0b2c5fb03 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 25 Sep 2020 12:45:47 +0530 Subject: [PATCH 188/192] fix: Patient Appointment not able to save (#23434) Co-authored-by: Rucha Mahabal --- .../doctype/patient_appointment/patient_appointment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index 512fb48360..e685b20a8c 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document import json -from frappe.utils import getdate, get_time +from frappe.utils import getdate, get_time, flt from frappe.model.mapper import get_mapped_doc from frappe import _ import datetime @@ -45,7 +45,7 @@ class PatientAppointment(Document): def validate_overlaps(self): end_time = datetime.datetime.combine(getdate(self.appointment_date), get_time(self.appointment_time)) \ - + datetime.timedelta(minutes=float(self.duration)) + + datetime.timedelta(minutes=flt(self.duration)) overlaps = frappe.db.sql(""" select From fec4ea56430eb7c27b6a6ac54ad23c42c7d2c8c5 Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 24 Sep 2020 17:08:22 +0530 Subject: [PATCH 189/192] fix: set stock uom in tem while creating material request from stock entry --- erpnext/stock/doctype/stock_entry/stock_entry.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 1f95447951..39fd029a89 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -156,6 +156,7 @@ frappe.ui.form.on('Stock Entry', { mr_item.item_code = item.item_code; mr_item.item_name = item.item_name; mr_item.uom = item.uom; + mr_item.stock_uom = item.stock_uom; mr_item.conversion_factor = item.conversion_factor; mr_item.item_group = item.item_group; mr_item.description = item.description; From 519a00ce44ebb2462d8ff2ffa9e1afa505d5c0ae Mon Sep 17 00:00:00 2001 From: Anupam Date: Mon, 28 Sep 2020 13:09:37 +0530 Subject: [PATCH 190/192] fix: flushing contact details, generating assignment rule issues --- erpnext/crm/doctype/lead/lead.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 315d298eb4..31db569a34 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -27,9 +27,6 @@ class Lead(SellingController): def after_insert(self): self.update_links() - # after the address and contact are created, flush the field values - # to avoid inconsistent reporting in case the documents are changed - self.flush_address_and_contact_fields() def validate(self): self.set_lead_name() @@ -210,14 +207,6 @@ class Lead(SellingController): }) self.contact_doc.save() - def flush_address_and_contact_fields(self): - fields = ['address_type', 'address_line1', 'address_line2', 'address_title', - 'city', 'county', 'country', 'fax', 'pincode', 'state'] - - for field in fields: - self.set(field, None) - - @frappe.whitelist() def make_customer(source_name, target_doc=None): return _make_customer(source_name, target_doc) From ce39323a0afb06ec3391659f4f3305f6c91bbdbf Mon Sep 17 00:00:00 2001 From: madar2020 <59777434+madar2020@users.noreply.github.com> Date: Mon, 28 Sep 2020 15:06:25 +0300 Subject: [PATCH 191/192] feat: Allow rich text edit for questions (#23402) * rich text * Update question.json As to make sense for rich text * Update quiz_question.json * Update question.json * Update quiz_question.json * Update macros.html fix: cards in second row of homepage section overlapps the row before. * Revert "Update macros.html" This reverts commit b649b507e5d546b5b4a78fbf68053493a71c7af2. Co-authored-by: Rucha Mahabal --- erpnext/education/doctype/question/question.json | 6 +++--- erpnext/education/doctype/quiz_question/quiz_question.json | 6 +++--- erpnext/public/js/education/lms/quiz.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/education/doctype/question/question.json b/erpnext/education/doctype/question/question.json index b3a161daa0..e396760616 100644 --- a/erpnext/education/doctype/question/question.json +++ b/erpnext/education/doctype/question/question.json @@ -13,7 +13,7 @@ "fields": [ { "fieldname": "question", - "fieldtype": "Small Text", + "fieldtype": "Text Editor", "in_list_view": 1, "label": "Question", "reqd": 1 @@ -34,7 +34,7 @@ "read_only": 1 } ], - "modified": "2019-05-30 18:39:21.880974", + "modified": "2020-09-24 18:39:21.880974", "modified_by": "Administrator", "module": "Education", "name": "Question", @@ -77,4 +77,4 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC" -} \ No newline at end of file +} diff --git a/erpnext/education/doctype/quiz_question/quiz_question.json b/erpnext/education/doctype/quiz_question/quiz_question.json index 0564482516..aab07a3e6b 100644 --- a/erpnext/education/doctype/quiz_question/quiz_question.json +++ b/erpnext/education/doctype/quiz_question/quiz_question.json @@ -20,14 +20,14 @@ { "fetch_from": "question_link.question", "fieldname": "question", - "fieldtype": "Data", + "fieldtype": "Text Editor", "in_list_view": 1, "label": "Question", "read_only": 1 } ], "istable": 1, - "modified": "2019-06-12 12:24:02.312577", + "modified": "2020-09-24 12:24:02.312577", "modified_by": "Administrator", "module": "Education", "name": "Quiz Question", @@ -37,4 +37,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/public/js/education/lms/quiz.js b/erpnext/public/js/education/lms/quiz.js index 91cbbf4d66..4a9d1e34e6 100644 --- a/erpnext/public/js/education/lms/quiz.js +++ b/erpnext/public/js/education/lms/quiz.js @@ -140,7 +140,7 @@ class Question { make_question() { let question_wrapper = document.createElement('h5'); question_wrapper.classList.add('mt-3'); - question_wrapper.innerText = this.question; + question_wrapper.innerHTML = this.question; this.wrapper.appendChild(question_wrapper); } From 77cf53201aec3f4f07af2a5270ea6f66aaf72b34 Mon Sep 17 00:00:00 2001 From: Anupam Date: Mon, 28 Sep 2020 18:38:41 +0530 Subject: [PATCH 192/192] feat: open lead status on next contact date --- erpnext/crm/doctype/lead/lead.py | 5 +++++ erpnext/hooks.py | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 31db569a34..99fa703fee 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -365,3 +365,8 @@ def get_lead_with_phone_number(number): lead = leads[0].name if leads else None return lead + +def daily_open_lead(): + leads = frappe.get_all("Lead", filters = [["contact_date", "Between", [nowdate(), nowdate()]]]) + for lead in leads: + frappe.db.set_value("Lead", lead.name, "status", "Open") \ No newline at end of file diff --git a/erpnext/hooks.py b/erpnext/hooks.py index f8b6be70ca..4e05076a3d 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -336,7 +336,8 @@ scheduler_events = { "erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation", "erpnext.hr.utils.generate_leave_encashment", "erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall.create_process_loan_security_shortfall", - "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_term_loans" + "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_term_loans", + "erpnext.crm.doctype.lead.lead.daily_open_lead" ], "monthly_long": [ "erpnext.accounts.deferred_revenue.process_deferred_accounting",