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 01/59] 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 02/59] 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 03/59] 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 d4817c8685f3b31e72c8d5acca297cbbb6d6877b Mon Sep 17 00:00:00 2001 From: marination Date: Sun, 2 Aug 2020 05:35:45 +0530 Subject: [PATCH 04/59] 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 05/59] 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 06/59] 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 07/59] 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 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 08/59] 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 698d983eefb6cb068b9fffaaa5bfac1718b1ce0b Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 19 Aug 2020 14:59:46 +0530 Subject: [PATCH 09/59] 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 22d3729427671e5d412c5634163413ae0ba507c1 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 19 Aug 2020 15:42:40 +0530 Subject: [PATCH 10/59] 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 41f421d4003fb80c62f2dc872b266a543e1b87e9 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 2 Sep 2020 13:47:18 +0530 Subject: [PATCH 11/59] 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 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 12/59] 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 9ce38de439ebd2a240539542a8de40f8ae2d2417 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 7 Sep 2020 13:27:17 +0530 Subject: [PATCH 13/59] 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 06ee0ea00bab3793ef7fcb48348ba4a3ef501c0b Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 7 Sep 2020 18:49:06 +0530 Subject: [PATCH 14/59] 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 15/59] 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 53cd60c217fc2442dc8d1db23c2595446efc08bb Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 7 Sep 2020 20:45:50 +0530 Subject: [PATCH 16/59] 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 0a0e258d8b43f88dc51fc1ed1aac48b55d4ca22a Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 10 Sep 2020 13:12:35 +0530 Subject: [PATCH 17/59] 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 18/59] 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 19/59] 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 37b99d202d6f4d7c5ffdffb1739d91aa74965757 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 10 Sep 2020 18:27:20 +0530 Subject: [PATCH 20/59] 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 ed08e593c37c6cb213007f32578cfe8db9621a77 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Fri, 11 Sep 2020 13:59:07 +0530 Subject: [PATCH 21/59] 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 22/59] 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 23/59] 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 b7cd84196d79f4a728a9a3aaf1504e62601aac5e Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Sep 2020 09:42:51 +0530 Subject: [PATCH 24/59] 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 4bb8d0c193ab9e967ad224a0e37743f1ef2cd648 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Sep 2020 11:18:02 +0530 Subject: [PATCH 25/59] 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 26/59] 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 27/59] 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 28/59] 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 29/59] 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 f995f517c95a9995e5d16333b341d9a79072f836 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Sep 2020 17:06:27 +0530 Subject: [PATCH 30/59] 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 31/59] 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 32/59] 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 33/59] 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 34/59] 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 35/59] 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 36/59] 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 37/59] 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 38/59] 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 39/59] 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 40/59] 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 41/59] 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 42/59] 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 43/59] 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 44/59] 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 45/59] 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 46/59] 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 47/59] 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 48/59] 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 49/59] 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 50/59] 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 51/59] 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 52/59] 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 53/59] 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 54/59] 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 55/59] 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 56/59] 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 57/59] 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 58/59] 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 59/59] 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) {