From 9c923ae418ec3d7d1a7ac91ba3f48d18d63d0965 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Thu, 25 Apr 2019 01:09:56 +0200 Subject: [PATCH 01/30] feat(regional): Report for german tax consultants (DATEV) --- erpnext/regional/report/datev/__init__.py | 0 erpnext/regional/report/datev/datev.js | 26 ++++ erpnext/regional/report/datev/datev.json | 29 +++++ erpnext/regional/report/datev/datev.py | 137 ++++++++++++++++++++++ 4 files changed, 192 insertions(+) create mode 100644 erpnext/regional/report/datev/__init__.py create mode 100644 erpnext/regional/report/datev/datev.js create mode 100644 erpnext/regional/report/datev/datev.json create mode 100644 erpnext/regional/report/datev/datev.py diff --git a/erpnext/regional/report/datev/__init__.py b/erpnext/regional/report/datev/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/report/datev/datev.js b/erpnext/regional/report/datev/datev.js new file mode 100644 index 00000000000..737a84aef24 --- /dev/null +++ b/erpnext/regional/report/datev/datev.js @@ -0,0 +1,26 @@ +frappe.query_reports["DATEV"] = { + "filters": [ + { + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, + { + "fieldname": "fiscal_year", + "label": __("Fiscal Year"), + "fieldtype": "Link", + "options": "Fiscal Year", + "default": frappe.defaults.get_user_default("fiscal_year"), + "reqd": 1 + } + ], + onload: function(query_report) { + query_report.export_report = function() { + const filters = JSON.stringify(query_report.get_values()); + window.open(`/api/method/erpnext.regional.report.datev.datev.download_datev_csv?filters=${filters}`); + }; + } +}; diff --git a/erpnext/regional/report/datev/datev.json b/erpnext/regional/report/datev/datev.json new file mode 100644 index 00000000000..d4f44b6fbb6 --- /dev/null +++ b/erpnext/regional/report/datev/datev.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 0, + "apply_user_permissions": 0, + "creation": "2019-04-24 08:45:16.650129", + "disabled": 0, + "icon": "octicon octicon-repo-pull", + "color": "#96cf41", + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "module": "Regional", + "name": "DATEV", + "owner": "Administrator", + "ref_doctype": "GL Entry", + "report_name": "DATEV", + "report_type": "Script Report", + "roles": [ + { + "role": "Accounts User" + }, + { + "role": "Accounts Manager" + }, + { + "role": "Auditor" + } + ] +} diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py new file mode 100644 index 00000000000..268de2874d7 --- /dev/null +++ b/erpnext/regional/report/datev/datev.py @@ -0,0 +1,137 @@ +# coding: utf-8 +from __future__ import unicode_literals +import json +from six import string_types +import frappe +from frappe.utils import format_datetime +from frappe import _ + + +def execute(filters=None): + validate_filters(filters) + result = get_gl_entries(filters, as_dict=0) + columns = get_columns() + + return columns, result + + +def validate_filters(filters): + if not filters.get('company'): + frappe.throw(_('{0} is mandatory').format(_('Company'))) + + if not filters.get('fiscal_year'): + frappe.throw(_('{0} is mandatory').format(_('Fiscal Year'))) + + +def get_columns(): + columns = [ + { + "label": "Umsatz (ohne Soll/Haben-Kz)", + "fieldname": "umsatz", + "fieldtype": "Currency", + }, + { + "label": "Soll/Haben-Kennzeichen", + "fieldname": "soll_haben_kennzeichen", + "fieldtype": "Data", + }, + { + "label": "Kontonummer", + "fieldname": "kontonummer", + "fieldtype": "Data", + }, + { + "label": "Gegenkonto (ohne BU-Schlüssel)", + "fieldname": "gegenkonto_nummer", + "fieldtype": "Data", + }, + { + "label": "Belegdatum", + "fieldname": "belegdatum", + "fieldtype": "Date", + } + ] + + return columns + + +def get_gl_entries(filters, as_dict): + gl_entries = frappe.db.sql(""" + select + + case gl.debit when 0 then gl.credit else gl.debit end as Umsatz, + case gl.debit when 0 then 'H' else 'S' end as Kennzeichen, + coalesce(acc.account_number, acc_pa.account_number) as Kontonummer, + coalesce(acc_against.account_number, acc_against_pa.account_number) as Gegenkonto, + gl.posting_date as Belegdatum + + from `tabGL Entry` gl + + /* Statistisches Konto (Debitoren/Kreditoren) */ + left join `tabParty Account` pa + on gl.against = pa.parent + + /* Kontonummer */ + left join `tabAccount` acc + on gl.account = acc.name + + /* Gegenkonto-Nummer */ + left join `tabAccount` acc_against + on gl.against = acc_against.name + + /* Statistische Kontonummer */ + left join `tabAccount` acc_pa + on pa.account = acc_pa.name + + /* Statistische Gegenkonto-Nummer */ + left join `tabAccount` acc_against_pa + on pa.account = acc_against_pa.name + + where gl.company=%(company)s and gl.fiscal_year=%(fiscal_year)s + order by 'Belegdatum:Date', voucher_no""", filters, as_dict=as_dict) + + return gl_entries + + +def get_datev_csv(data): + title_row = [ + "Umsatz (ohne Soll/Haben-Kz)", + "Soll/Haben-Kennzeichen", + "Kontonummer", + "Gegenkonto (ohne BU-Schlüssel)", + "Belegdatum" + ] + + result = ['"' + '";"'.join(title_row) + '"'] + result += [ + ';'.join( + [ + '{:.2f}'.format(d.get('Umsatz')).replace('.', ','), + '"{}"'.format(d.get('Kennzeichen')), + d.get('Kontonummer'), + # Can be empty, if there are no debtor / creditor accounts + d.get('Gegenkonto') or '', + format_datetime(d.get('Belegdatum'), 'ddMMyyyy') + ] + ) for d in data + ] + + return b'\r\n'.join(result).encode(encoding='latin_1') + + +@frappe.whitelist() +def download_datev_csv(filters=None): + if isinstance(filters, string_types): + filters = json.loads(filters) + + validate_filters(filters) + data = get_gl_entries(filters, as_dict=1) + + filename = 'DATEV_Buchungsstapel_{}-{}'.format( + filters.get('company'), + filters.get('fiscal_year') + ) + + frappe.response['result'] = get_datev_csv(data) + frappe.response['doctype'] = filename + frappe.response['type'] = 'csv' From e5f7af9e9f1703b53159f046526470a2439f49f0 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Thu, 9 May 2019 03:54:49 +0200 Subject: [PATCH 02/30] Use pandas, more columns, filter by date --- erpnext/regional/report/datev/datev.js | 14 ++- erpnext/regional/report/datev/datev.py | 149 ++++++++++++++++++++----- 2 files changed, 128 insertions(+), 35 deletions(-) diff --git a/erpnext/regional/report/datev/datev.js b/erpnext/regional/report/datev/datev.js index 737a84aef24..a556199a046 100644 --- a/erpnext/regional/report/datev/datev.js +++ b/erpnext/regional/report/datev/datev.js @@ -9,11 +9,15 @@ frappe.query_reports["DATEV"] = { "reqd": 1 }, { - "fieldname": "fiscal_year", - "label": __("Fiscal Year"), - "fieldtype": "Link", - "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "fieldname": "from_date", + "label": __("From Date"), + "fieldtype": "Date", + "reqd": 1 + }, + { + "fieldname": "to_date", + "label": __("To Date"), + "fieldtype": "Date", "reqd": 1 } ], diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 268de2874d7..5209f34528e 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -5,6 +5,7 @@ from six import string_types import frappe from frappe.utils import format_datetime from frappe import _ +import pandas as pd def execute(filters=None): @@ -19,35 +20,38 @@ def validate_filters(filters): if not filters.get('company'): frappe.throw(_('{0} is mandatory').format(_('Company'))) - if not filters.get('fiscal_year'): - frappe.throw(_('{0} is mandatory').format(_('Fiscal Year'))) + if not filters.get('from_date'): + frappe.throw(_('{0} is mandatory').format(_('From Date'))) + + if not filters.get('to_date'): + frappe.throw(_('{0} is mandatory').format(_('To Date'))) def get_columns(): columns = [ { "label": "Umsatz (ohne Soll/Haben-Kz)", - "fieldname": "umsatz", + "fieldname": "Umsatz (ohne Soll/Haben-Kz)", "fieldtype": "Currency", }, { "label": "Soll/Haben-Kennzeichen", - "fieldname": "soll_haben_kennzeichen", + "fieldname": "Soll/Haben-Kennzeichen", "fieldtype": "Data", }, { "label": "Kontonummer", - "fieldname": "kontonummer", + "fieldname": "Kontonummer", "fieldtype": "Data", }, { "label": "Gegenkonto (ohne BU-Schlüssel)", - "fieldname": "gegenkonto_nummer", + "fieldname": "Gegenkonto (ohne BU-Schlüssel)", "fieldtype": "Data", }, { "label": "Belegdatum", - "fieldname": "belegdatum", + "fieldname": "Belegdatum", "fieldtype": "Date", } ] @@ -59,11 +63,11 @@ def get_gl_entries(filters, as_dict): gl_entries = frappe.db.sql(""" select - case gl.debit when 0 then gl.credit else gl.debit end as Umsatz, - case gl.debit when 0 then 'H' else 'S' end as Kennzeichen, - coalesce(acc.account_number, acc_pa.account_number) as Kontonummer, - coalesce(acc_against.account_number, acc_against_pa.account_number) as Gegenkonto, - gl.posting_date as Belegdatum + case gl.debit when 0 then gl.credit else gl.debit end as 'Umsatz (ohne Soll/Haben-Kz)', + case gl.debit when 0 then 'H' else 'S' end as 'Soll/Haben-Kennzeichen', + coalesce(acc.account_number, acc_pa.account_number) as 'Kontonummer', + coalesce(acc_against.account_number, acc_against_pa.account_number) as 'Gegenkonto (ohne BU-Schlüssel)', + gl.posting_date as 'Belegdatum' from `tabGL Entry` gl @@ -87,36 +91,121 @@ def get_gl_entries(filters, as_dict): left join `tabAccount` acc_against_pa on pa.account = acc_against_pa.name - where gl.company=%(company)s and gl.fiscal_year=%(fiscal_year)s - order by 'Belegdatum:Date', voucher_no""", filters, as_dict=as_dict) + where gl.company = %(company)s + and DATE(gl.posting_date) >= %(from_date)s + and DATE(gl.posting_date) <= %(to_date)s + order by 'Belegdatum', gl.voucher_no""", filters, as_dict=as_dict) return gl_entries def get_datev_csv(data): - title_row = [ + columns = [ + # Umsatz "Umsatz (ohne Soll/Haben-Kz)", "Soll/Haben-Kennzeichen", + "WKZ Umsatz", + "Kurs", + "Basis-Umsatz", + "WKZ Basis-Umsatz", + # Konto/Gegenkonto "Kontonummer", "Gegenkonto (ohne BU-Schlüssel)", - "Belegdatum" + "BU-Schlüssel", + # Datum + "Belegdatum", + # Belegfelder + "Belegfeld 1", + "Belegfeld 2", + # Weitere Felder + "Skonto", + "Buchungstext", + # OPOS-Informationen + "Postensperre", + "Diverse Adressnummer", + "Geschäftspartnerbank", + "Sachverhalt", + "Zinssperre", + # Digitaler Beleg + "Beleglink", + # Kostenrechnung + "Kost 1 - Kostenstelle", + "Kost 2 - Kostenstelle", + "Kost-Menge", + # Steuerrechnung + "EU-Land u. UStID", + "EU-Steuersatz", + "Abw. Versteuerungsart", + # L+L Sachverhalt + "Sachverhalt L+L", + "FunktionsergänzungL+L", + # Mengenfelder LuF + "Stück", + "Gewicht", + # Forderungsart + "Zahlweise", + "Forderungsart", + "Veranlagungsjahr", + "Zugeordnete Fälligkeit", + # Weitere Felder + "Skontotyp", + # Anzahlungen + "Auftragsnummer", + "Buchungstyp", + "USt-Schlüssel (Anzahlungen)", + "EU-Land (Anzahlungen)", + "Sachverhalt L+L (Anzahlungen)", + "EU-Steuersatz (Anzahlungen)", + "Erlöskonto (Anzahlungen)", + # Stapelinformationen + "Herkunft-Kz", + # Technische Identifikation + "Buchungs GUID", + # Kostenrechnung + "Kost-Datum", + # OPOS-Informationen + "SEPA-Mandatsreferenz", + "Skontosperre", + # Gesellschafter und Sonderbilanzsachverhalt + "Gesellschaftername", + "Beteiligtennummer", + "Identifikationsnummer", + "Zeichnernummer", + # OPOS-Informationen + "Postensperre bis", + # Gesellschafter und Sonderbilanzsachverhalt + "Bezeichnung SoBil-Sachverhalt", + "Kennzeichen SoBil-Buchung", + # Stapelinformationen + "Festschreibung", + # Datum + "Leistungsdatum", + "Datum Zuord. Steuerperiode", + # OPOS-Informationen + "Fälligkeit", + # Konto/Gegenkonto + "Generalumkehr (GU)", + # Steuersatz für Steuerschlüssel + "Steuersatz", + "Land" ] - result = ['"' + '";"'.join(title_row) + '"'] - result += [ - ';'.join( - [ - '{:.2f}'.format(d.get('Umsatz')).replace('.', ','), - '"{}"'.format(d.get('Kennzeichen')), - d.get('Kontonummer'), - # Can be empty, if there are no debtor / creditor accounts - d.get('Gegenkonto') or '', - format_datetime(d.get('Belegdatum'), 'ddMMyyyy') - ] - ) for d in data - ] + empty_df = pd.DataFrame(columns=columns) - return b'\r\n'.join(result).encode(encoding='latin_1') + data_df = pd.DataFrame.from_records(data) + data_df["Belegdatum"] = pd.to_datetime(data_df["Belegdatum"]) + + result = empty_df.append(data_df) + + return result.to_csv( + sep=b';', + decimal=',', + encoding='latin_1', + date_format='%d%m', + line_terminator=b'\r\n', + index=False, + columns=columns + ) @frappe.whitelist() From 4b2901704cf4fa613c7b050fb0c18f32b001b0c8 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Thu, 9 May 2019 04:03:23 +0200 Subject: [PATCH 03/30] Add download button --- erpnext/regional/report/datev/datev.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/report/datev/datev.js b/erpnext/regional/report/datev/datev.js index a556199a046..781862c2a6a 100644 --- a/erpnext/regional/report/datev/datev.js +++ b/erpnext/regional/report/datev/datev.js @@ -22,9 +22,9 @@ frappe.query_reports["DATEV"] = { } ], onload: function(query_report) { - query_report.export_report = function() { + query_report.page.add_inner_button("Download DATEV Export", () => { const filters = JSON.stringify(query_report.get_values()); window.open(`/api/method/erpnext.regional.report.datev.datev.download_datev_csv?filters=${filters}`); - }; + }); } }; From 9eef60754da9c27527a21bbd627b0e1bbe56fc2d Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Sun, 12 May 2019 02:24:39 +0200 Subject: [PATCH 04/30] remove unused import --- erpnext/regional/report/datev/datev.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 5209f34528e..e6d1cbfbf38 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import json from six import string_types import frappe -from frappe.utils import format_datetime from frappe import _ import pandas as pd From 2701f944911d98b2a6e829392814f98928d10980 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Tue, 21 May 2019 21:58:35 +0200 Subject: [PATCH 05/30] add remarks and voucher info --- erpnext/regional/report/datev/datev.py | 37 +++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index e6d1cbfbf38..4a6e0ef27e1 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -52,6 +52,31 @@ def get_columns(): "label": "Belegdatum", "fieldname": "Belegdatum", "fieldtype": "Date", + }, + { + "label": "Buchungstext", + "fieldname": "Buchungstext", + "fieldtype": "Text", + }, + { + "label": "Beleginfo - Art 1", + "fieldname": "Beleginfo - Art 1", + "fieldtype": "Data", + }, + { + "label": "Beleginfo - Inhalt 1", + "fieldname": "Beleginfo - Inhalt 1", + "fieldtype": "Data", + }, + { + "label": "Beleginfo - Art 2", + "fieldname": "Beleginfo - Art 2", + "fieldtype": "Data", + }, + { + "label": "Beleginfo - Inhalt 2", + "fieldname": "Beleginfo - Inhalt 2", + "fieldtype": "Data", } ] @@ -66,7 +91,12 @@ def get_gl_entries(filters, as_dict): case gl.debit when 0 then 'H' else 'S' end as 'Soll/Haben-Kennzeichen', coalesce(acc.account_number, acc_pa.account_number) as 'Kontonummer', coalesce(acc_against.account_number, acc_against_pa.account_number) as 'Gegenkonto (ohne BU-Schlüssel)', - gl.posting_date as 'Belegdatum' + gl.posting_date as 'Belegdatum', + gl.remarks as 'Buchungstext', + gl.voucher_type as 'Beleginfo - Art 1', + gl.voucher_no as 'Beleginfo - Inhalt 1', + gl.against_voucher_type as 'Beleginfo - Art 2', + gl.against_voucher as 'Beleginfo - Inhalt 2' from `tabGL Entry` gl @@ -127,6 +157,11 @@ def get_datev_csv(data): "Zinssperre", # Digitaler Beleg "Beleglink", + # Beleginfo + "Beleginfo - Art 1", + "Beleginfo - Inhalt 1", + "Beleginfo - Art 2", + "Beleginfo - Inhalt 2", # Kostenrechnung "Kost 1 - Kostenstelle", "Kost 2 - Kostenstelle", From 50de88417c1ce6dd08fee257a5144c043aa160b4 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Tue, 21 May 2019 21:59:04 +0200 Subject: [PATCH 06/30] fix filename --- erpnext/regional/report/datev/datev.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 4a6e0ef27e1..2012fa21bff 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -250,9 +250,10 @@ def download_datev_csv(filters=None): validate_filters(filters) data = get_gl_entries(filters, as_dict=1) - filename = 'DATEV_Buchungsstapel_{}-{}'.format( + filename = 'DATEV_Buchungsstapel_{}-{}_bis_{}'.format( filters.get('company'), - filters.get('fiscal_year') + filters.get('from_date'), + filters.get('to_date') ) frappe.response['result'] = get_datev_csv(data) From ce9239af83b519b7aad810b407acac587f022b02 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Tue, 21 May 2019 22:49:59 +0200 Subject: [PATCH 07/30] fix error when downloading empty report --- erpnext/regional/report/datev/datev.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 2012fa21bff..8b05ad0878c 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -225,11 +225,10 @@ def get_datev_csv(data): ] empty_df = pd.DataFrame(columns=columns) - data_df = pd.DataFrame.from_records(data) - data_df["Belegdatum"] = pd.to_datetime(data_df["Belegdatum"]) result = empty_df.append(data_df) + result["Belegdatum"] = pd.to_datetime(result["Belegdatum"]) return result.to_csv( sep=b';', From 31f505528705f7b1a40749cacb273a7341340ec6 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Wed, 22 May 2019 00:03:16 +0200 Subject: [PATCH 08/30] fix sql for multi-company case --- erpnext/regional/report/datev/datev.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 8b05ad0878c..84bd52926b8 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -103,6 +103,7 @@ def get_gl_entries(filters, as_dict): /* Statistisches Konto (Debitoren/Kreditoren) */ left join `tabParty Account` pa on gl.against = pa.parent + and gl.company = pa.company /* Kontonummer */ left join `tabAccount` acc From 9638f0ef2b1234bf84207b2c892cb49ffde2a7a0 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Mon, 27 May 2019 14:42:52 +0200 Subject: [PATCH 09/30] Add columns to CSV, add comments and docstrings --- erpnext/regional/report/datev/datev.py | 122 ++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 5 deletions(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 84bd52926b8..6ca1fe54c85 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -1,4 +1,12 @@ # coding: utf-8 +""" +Provide a report and downloadable CSV according to the German DATEV format. + +- Query report showing only the columns that contain data, formatted nicely for + dispay to the user. +- CSV download functionality `download_datev_csv` that provides a CSV file with + all required columns. Used to import the data into the DATEV Software. +""" from __future__ import unicode_literals import json from six import string_types @@ -8,6 +16,7 @@ import pandas as pd def execute(filters=None): + """Entry point for frappe.""" validate_filters(filters) result = get_gl_entries(filters, as_dict=0) columns = get_columns() @@ -16,6 +25,7 @@ def execute(filters=None): def validate_filters(filters): + """Make sure all mandatory filters are present.""" if not filters.get('company'): frappe.throw(_('{0} is mandatory').format(_('Company'))) @@ -27,6 +37,7 @@ def validate_filters(filters): def get_columns(): + """Return the list of columns that will be shown in query report.""" columns = [ { "label": "Umsatz (ohne Soll/Haben-Kz)", @@ -84,13 +95,31 @@ def get_columns(): def get_gl_entries(filters, as_dict): + """ + Get a list of accounting entries. + + Select GL Entries joined with Account and Party Account in order to get the + account numbers. Returns a list of accounting entries. + + Arguments: + filters -- dict of filters to be passed to the sql query + as_dict -- return as list of dicts [0,1] + """ gl_entries = frappe.db.sql(""" select + /* either debit or credit amount; always positive */ case gl.debit when 0 then gl.credit else gl.debit end as 'Umsatz (ohne Soll/Haben-Kz)', + + /* 'H' when credit, 'S' when debit */ case gl.debit when 0 then 'H' else 'S' end as 'Soll/Haben-Kennzeichen', + + /* account number or, if empty, party account number */ coalesce(acc.account_number, acc_pa.account_number) as 'Kontonummer', + + /* against number or, if empty, party against number */ coalesce(acc_against.account_number, acc_against_pa.account_number) as 'Gegenkonto (ohne BU-Schlüssel)', + gl.posting_date as 'Belegdatum', gl.remarks as 'Buchungstext', gl.voucher_type as 'Beleginfo - Art 1', @@ -130,7 +159,16 @@ def get_gl_entries(filters, as_dict): def get_datev_csv(data): + """ + Fill in missing columns and return a CSV in DATEV Format. + + Arguments: + data -- array of dictionaries + """ columns = [ + # All possible columns must tbe listed here, because DATEV requires them to + # be present in the CSV. + # --- # Umsatz "Umsatz (ohne Soll/Haben-Kz)", "Soll/Haben-Kennzeichen", @@ -159,10 +197,22 @@ def get_datev_csv(data): # Digitaler Beleg "Beleglink", # Beleginfo - "Beleginfo - Art 1", - "Beleginfo - Inhalt 1", - "Beleginfo - Art 2", - "Beleginfo - Inhalt 2", + "Beleginfo – Art 1", + "Beleginfo – Inhalt 1", + "Beleginfo – Art 2", + "Beleginfo – Inhalt 2", + "Beleginfo – Art 3", + "Beleginfo – Inhalt 3", + "Beleginfo – Art 4", + "Beleginfo – Inhalt 4", + "Beleginfo – Art 5", + "Beleginfo – Inhalt 5", + "Beleginfo – Art 6", + "Beleginfo – Inhalt 6", + "Beleginfo – Art 7", + "Beleginfo – Inhalt 7", + "Beleginfo – Art 8", + "Beleginfo – Inhalt 8", # Kostenrechnung "Kost 1 - Kostenstelle", "Kost 2 - Kostenstelle", @@ -173,7 +223,52 @@ def get_datev_csv(data): "Abw. Versteuerungsart", # L+L Sachverhalt "Sachverhalt L+L", - "FunktionsergänzungL+L", + "Funktionsergänzung L+L", + # Funktion Steuerschlüssel 49 + "BU 49 Hauptfunktionstyp", + "BU 49 Hauptfunktionsnummer", + "BU 49 Funktionsergänzung", + # Zusatzinformationen + "Zusatzinformation – Art 1", + "Zusatzinformation – Inhalt 1", + "Zusatzinformation – Art 2", + "Zusatzinformation – Inhalt 2", + "Zusatzinformation – Art 3", + "Zusatzinformation – Inhalt 3", + "Zusatzinformation – Art 4", + "Zusatzinformation – Inhalt 4", + "Zusatzinformation – Art 5", + "Zusatzinformation – Inhalt 5", + "Zusatzinformation – Art 6", + "Zusatzinformation – Inhalt 6", + "Zusatzinformation – Art 7", + "Zusatzinformation – Inhalt 7", + "Zusatzinformation – Art 8", + "Zusatzinformation – Inhalt 8", + "Zusatzinformation – Art 9", + "Zusatzinformation – Inhalt 9", + "Zusatzinformation – Art 10", + "Zusatzinformation – Inhalt 10", + "Zusatzinformation – Art 11", + "Zusatzinformation – Inhalt 11", + "Zusatzinformation – Art 12", + "Zusatzinformation – Inhalt 12", + "Zusatzinformation – Art 13", + "Zusatzinformation – Inhalt 13", + "Zusatzinformation – Art 14", + "Zusatzinformation – Inhalt 14", + "Zusatzinformation – Art 15", + "Zusatzinformation – Inhalt 15", + "Zusatzinformation – Art 16", + "Zusatzinformation – Inhalt 16", + "Zusatzinformation – Art 17", + "Zusatzinformation – Inhalt 17", + "Zusatzinformation – Art 18", + "Zusatzinformation – Inhalt 18", + "Zusatzinformation – Art 19", + "Zusatzinformation – Inhalt 19", + "Zusatzinformation – Art 20", + "Zusatzinformation – Inhalt 20", # Mengenfelder LuF "Stück", "Gewicht", @@ -233,17 +328,34 @@ def get_datev_csv(data): return result.to_csv( sep=b';', + # European decimal seperator decimal=',', + # Windows "ANSI" encoding encoding='latin_1', + # format date as DDMM date_format='%d%m', + # Windows line terminator line_terminator=b'\r\n', + # Do not number rows index=False, + # Use all columns defined above columns=columns ) @frappe.whitelist() def download_datev_csv(filters=None): + """ + Provide accounting entries for download in DATEV format. + + Validate the filters, get the data, produce the CSV file and provide it for + download. Can be called like this: + + GET /api/method/erpnext.regional.report.datev.datev.download_datev_csv + + Arguments / Params: + filters -- dict of filters to be passed to the sql query + """ if isinstance(filters, string_types): filters = json.loads(filters) From b7b5bcbd855a8b23eda9929f8c3ab66215fe15ab Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Mon, 27 May 2019 15:05:08 +0200 Subject: [PATCH 10/30] fix: em dash cannot be encoded in latin-1 --- erpnext/regional/report/datev/datev.py | 112 ++++++++++++------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 6ca1fe54c85..50aed084aba 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -197,22 +197,22 @@ def get_datev_csv(data): # Digitaler Beleg "Beleglink", # Beleginfo - "Beleginfo – Art 1", - "Beleginfo – Inhalt 1", - "Beleginfo – Art 2", - "Beleginfo – Inhalt 2", - "Beleginfo – Art 3", - "Beleginfo – Inhalt 3", - "Beleginfo – Art 4", - "Beleginfo – Inhalt 4", - "Beleginfo – Art 5", - "Beleginfo – Inhalt 5", - "Beleginfo – Art 6", - "Beleginfo – Inhalt 6", - "Beleginfo – Art 7", - "Beleginfo – Inhalt 7", - "Beleginfo – Art 8", - "Beleginfo – Inhalt 8", + "Beleginfo - Art 1", + "Beleginfo - Inhalt 1", + "Beleginfo - Art 2", + "Beleginfo - Inhalt 2", + "Beleginfo - Art 3", + "Beleginfo - Inhalt 3", + "Beleginfo - Art 4", + "Beleginfo - Inhalt 4", + "Beleginfo - Art 5", + "Beleginfo - Inhalt 5", + "Beleginfo - Art 6", + "Beleginfo - Inhalt 6", + "Beleginfo - Art 7", + "Beleginfo - Inhalt 7", + "Beleginfo - Art 8", + "Beleginfo - Inhalt 8", # Kostenrechnung "Kost 1 - Kostenstelle", "Kost 2 - Kostenstelle", @@ -229,46 +229,46 @@ def get_datev_csv(data): "BU 49 Hauptfunktionsnummer", "BU 49 Funktionsergänzung", # Zusatzinformationen - "Zusatzinformation – Art 1", - "Zusatzinformation – Inhalt 1", - "Zusatzinformation – Art 2", - "Zusatzinformation – Inhalt 2", - "Zusatzinformation – Art 3", - "Zusatzinformation – Inhalt 3", - "Zusatzinformation – Art 4", - "Zusatzinformation – Inhalt 4", - "Zusatzinformation – Art 5", - "Zusatzinformation – Inhalt 5", - "Zusatzinformation – Art 6", - "Zusatzinformation – Inhalt 6", - "Zusatzinformation – Art 7", - "Zusatzinformation – Inhalt 7", - "Zusatzinformation – Art 8", - "Zusatzinformation – Inhalt 8", - "Zusatzinformation – Art 9", - "Zusatzinformation – Inhalt 9", - "Zusatzinformation – Art 10", - "Zusatzinformation – Inhalt 10", - "Zusatzinformation – Art 11", - "Zusatzinformation – Inhalt 11", - "Zusatzinformation – Art 12", - "Zusatzinformation – Inhalt 12", - "Zusatzinformation – Art 13", - "Zusatzinformation – Inhalt 13", - "Zusatzinformation – Art 14", - "Zusatzinformation – Inhalt 14", - "Zusatzinformation – Art 15", - "Zusatzinformation – Inhalt 15", - "Zusatzinformation – Art 16", - "Zusatzinformation – Inhalt 16", - "Zusatzinformation – Art 17", - "Zusatzinformation – Inhalt 17", - "Zusatzinformation – Art 18", - "Zusatzinformation – Inhalt 18", - "Zusatzinformation – Art 19", - "Zusatzinformation – Inhalt 19", - "Zusatzinformation – Art 20", - "Zusatzinformation – Inhalt 20", + "Zusatzinformation - Art 1", + "Zusatzinformation - Inhalt 1", + "Zusatzinformation - Art 2", + "Zusatzinformation - Inhalt 2", + "Zusatzinformation - Art 3", + "Zusatzinformation - Inhalt 3", + "Zusatzinformation - Art 4", + "Zusatzinformation - Inhalt 4", + "Zusatzinformation - Art 5", + "Zusatzinformation - Inhalt 5", + "Zusatzinformation - Art 6", + "Zusatzinformation - Inhalt 6", + "Zusatzinformation - Art 7", + "Zusatzinformation - Inhalt 7", + "Zusatzinformation - Art 8", + "Zusatzinformation - Inhalt 8", + "Zusatzinformation - Art 9", + "Zusatzinformation - Inhalt 9", + "Zusatzinformation - Art 10", + "Zusatzinformation - Inhalt 10", + "Zusatzinformation - Art 11", + "Zusatzinformation - Inhalt 11", + "Zusatzinformation - Art 12", + "Zusatzinformation - Inhalt 12", + "Zusatzinformation - Art 13", + "Zusatzinformation - Inhalt 13", + "Zusatzinformation - Art 14", + "Zusatzinformation - Inhalt 14", + "Zusatzinformation - Art 15", + "Zusatzinformation - Inhalt 15", + "Zusatzinformation - Art 16", + "Zusatzinformation - Inhalt 16", + "Zusatzinformation - Art 17", + "Zusatzinformation - Inhalt 17", + "Zusatzinformation - Art 18", + "Zusatzinformation - Inhalt 18", + "Zusatzinformation - Art 19", + "Zusatzinformation - Inhalt 19", + "Zusatzinformation - Art 20", + "Zusatzinformation - Inhalt 20", # Mengenfelder LuF "Stück", "Gewicht", From 1c1a5958bc4ed65ceeb746e43177de285cb2bed4 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Mon, 27 May 2019 15:05:32 +0200 Subject: [PATCH 11/30] more pleasant icon color --- erpnext/regional/report/datev/datev.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/report/datev/datev.json b/erpnext/regional/report/datev/datev.json index d4f44b6fbb6..80a866cbf5c 100644 --- a/erpnext/regional/report/datev/datev.json +++ b/erpnext/regional/report/datev/datev.json @@ -4,7 +4,7 @@ "creation": "2019-04-24 08:45:16.650129", "disabled": 0, "icon": "octicon octicon-repo-pull", - "color": "#96cf41", + "color": "#4CB944", "docstatus": 0, "doctype": "Report", "idx": 0, From 0261472ecd305ac405dd7604913cf2b359e7bbbd Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Mon, 27 May 2019 16:01:05 +0200 Subject: [PATCH 12/30] add default filters --- erpnext/regional/report/datev/datev.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/report/datev/datev.js b/erpnext/regional/report/datev/datev.js index 781862c2a6a..1e000b673e6 100644 --- a/erpnext/regional/report/datev/datev.js +++ b/erpnext/regional/report/datev/datev.js @@ -5,18 +5,20 @@ frappe.query_reports["DATEV"] = { "label": __("Company"), "fieldtype": "Link", "options": "Company", - "default": frappe.defaults.get_user_default("Company"), + "default": frappe.defaults.get_user_default("Company") || frappe.defaults.get_global_default("Company"), "reqd": 1 }, { "fieldname": "from_date", "label": __("From Date"), + "default": frappe.datetime.month_start(), "fieldtype": "Date", "reqd": 1 }, { "fieldname": "to_date", "label": __("To Date"), + "default": frappe.datetime.now_date(), "fieldtype": "Date", "reqd": 1 } From 1d11ddc23524201b81d8b815a83612b6a621ff13 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Mon, 10 Jun 2019 17:33:36 +0530 Subject: [PATCH 13/30] fix: Available qty not shown in item batch selector for batch --- .../js/utils/serial_no_batch_selector.js | 65 ++++++++++--------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index df50884ce71..d9c84f5c463 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -233,30 +233,32 @@ erpnext.SerialNoBatchSelector = Class.extend({ get_batch_fields: function() { var me = this; return [ - {fieldtype:'Section Break', label: __('Batches')}, - {fieldname: 'batches', fieldtype: 'Table', + { fieldtype: 'Section Break', label: __('Batches') }, + { + fieldname: 'batches', fieldtype: 'Table', fields: [ { - fieldtype:'Link', - fieldname:'batch_no', - options: 'Batch', - label: __('Select Batch'), - in_list_view:1, - get_query: function() { + 'fieldtype': 'Link', + 'read_only': 0, + 'fieldname': 'batch_no', + 'options': 'Batch', + 'label': __('Select Batch'), + 'in_list_view': 1, + get_query: function () { return { - filters: {item: me.item_code }, - query: 'erpnext.controllers.queries.get_batch_numbers' - }; + filters: { item: me.item_code }, + query: 'erpnext.controllers.queries.get_batch_numbers' + }; }, - onchange: function(e) { + change: function (e) { let val = this.get_value(); - if(val.length === 0) { + if (val.length === 0) { this.grid_row.on_grid_fields_dict .available_qty.set_value(0); return; } let selected_batches = this.grid.grid_rows.map((row) => { - if(row === this.grid_row) { + if (row === this.grid_row) { return ""; } @@ -264,12 +266,12 @@ erpnext.SerialNoBatchSelector = Class.extend({ return row.on_grid_fields_dict.batch_no.get_value(); } }); - if(selected_batches.includes(val)) { + if (selected_batches.includes(val)) { this.set_value(""); frappe.throw(__(`Batch ${val} already selected.`)); return; } - if(me.warehouse_details.name) { + if (me.warehouse_details.name) { frappe.call({ method: 'erpnext.stock.doctype.batch.batch.get_batch_qty', args: { @@ -292,31 +294,32 @@ erpnext.SerialNoBatchSelector = Class.extend({ } }, { - fieldtype:'Float', - read_only:1, - fieldname:'available_qty', - label: __('Available'), - in_list_view:1, - default: 0, - onchange: function() { + 'fieldtype': 'Float', + 'read_only': 1, + 'fieldname': 'available_qty', + 'label': __('Available'), + 'in_list_view': 1, + 'default': 0, + change: function () { this.grid_row.on_grid_fields_dict.selected_qty.set_value('0'); } }, { - fieldtype:'Float', - fieldname:'selected_qty', - label: __('Qty'), - in_list_view:1, + 'fieldtype': 'Float', + 'read_only': 0, + 'fieldname': 'selected_qty', + 'label': __('Qty'), + 'in_list_view': 1, 'default': 0, - onchange: function(e) { + change: function (e) { var batch_no = this.grid_row.on_grid_fields_dict.batch_no.get_value(); var available_qty = this.grid_row.on_grid_fields_dict.available_qty.get_value(); var selected_qty = this.grid_row.on_grid_fields_dict.selected_qty.get_value(); - if(batch_no.length === 0 && parseInt(selected_qty)!==0) { + if (batch_no.length === 0 && parseInt(selected_qty) !== 0) { frappe.throw(__("Please select a batch")); } - if(me.warehouse_details.type === 'Source Warehouse' && + if (me.warehouse_details.type === 'Source Warehouse' && parseFloat(available_qty) < parseFloat(selected_qty)) { this.set_value('0'); @@ -332,7 +335,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ ], in_place_edit: true, data: this.data, - get_data: function() { + get_data: function () { return this.data; }, } From 2de80fdc3ecc1ed5a4ba782056783bad9be28ffa Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Mon, 10 Jun 2019 17:51:47 +0530 Subject: [PATCH 14/30] fix: Codacy --- erpnext/public/js/utils/serial_no_batch_selector.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index d9c84f5c463..0202bbb872a 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -250,7 +250,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ query: 'erpnext.controllers.queries.get_batch_numbers' }; }, - change: function (e) { + change: function () { let val = this.get_value(); if (val.length === 0) { this.grid_row.on_grid_fields_dict @@ -311,7 +311,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ 'label': __('Qty'), 'in_list_view': 1, 'default': 0, - change: function (e) { + change: function () { var batch_no = this.grid_row.on_grid_fields_dict.batch_no.get_value(); var available_qty = this.grid_row.on_grid_fields_dict.available_qty.get_value(); var selected_qty = this.grid_row.on_grid_fields_dict.selected_qty.get_value(); From 860b6bac75bad1fffd09ef296426fe1f123abbe1 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Thu, 13 Jun 2019 14:28:46 +0530 Subject: [PATCH 15/30] fix: get bom item when company is not passed --- erpnext/stock/doctype/material_request/material_request.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index fe1319103a8..a0fc3f34e20 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -186,6 +186,7 @@ frappe.ui.form.on('Material Request', { var values = d.get_values(); if(!values) return; values["company"] = frm.doc.company; + if(!frm.doc.company) frappe.throw(__("Company field is required")); frappe.call({ method: "erpnext.manufacturing.doctype.bom.bom.get_bom_items", args: values, From ffd6b27f4b6b367dd2c5f4cbb77441ba8dda0d31 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 13 Jun 2019 14:33:47 +0530 Subject: [PATCH 16/30] fix: Fixed setters for getting items from quotation/opportunity --- .../selling/doctype/quotation/quotation.js | 27 ++++++++++++------- .../doctype/sales_order/sales_order.js | 14 +++++++--- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 1a31db92cb3..02d293d3236 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -90,22 +90,29 @@ erpnext.selling.QuotationController = erpnext.selling.SellingController.extend({ if (this.frm.doc.docstatus===0) { this.frm.add_custom_button(__('Opportunity'), function() { - var setters = {}; - if(me.frm.doc.quotation_to == "Customer" && me.frm.doc.party_name) { - setters.customer = me.frm.doc.party_name || undefined; - } else if (me.frm.doc.quotation_to == "Lead" && me.frm.doc.party_name) { - setters.lead = me.frm.doc.party_name || undefined; - } erpnext.utils.map_current_doc({ method: "erpnext.crm.doctype.opportunity.opportunity.make_quotation", source_doctype: "Opportunity", target: me.frm, - setters: setters, + setters: [ + { + label: "Party", + fieldname: "party_name", + fieldtype: "Link", + options: me.frm.doc.quotation_to, + default: me.frm.doc.party_name || undefined + }, + { + label: "Opportunity Type", + fieldname: "opportunity_type", + fieldtype: "Link", + options: "Opportunity Type", + default: me.frm.doc.order_type || undefined + } + ], get_query_filters: { status: ["not in", ["Lost", "Closed"]], - company: me.frm.doc.company, - // cannot set opportunity_type as setter, as the fieldname is order_type - opportunity_type: me.frm.doc.order_type, + company: me.frm.doc.company } }) }, __("Get items from"), "btn-default"); diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 6cdb4f88840..53b3e73f5e2 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -219,13 +219,19 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( method: "erpnext.selling.doctype.quotation.quotation.make_sales_order", source_doctype: "Quotation", target: me.frm, - setters: { - customer: me.frm.doc.customer || undefined - }, + setters: [ + { + label: "Customer", + fieldname: "party_name", + fieldtype: "Link", + options: "Customer", + default: me.frm.doc.customer || undefined + } + ], get_query_filters: { company: me.frm.doc.company, docstatus: 1, - status: ["!=", "Lost"], + status: ["!=", "Lost"] } }) }, __("Get items from")); From 49c6c909f8e23f2fbe98bd6df71a617149f5c507 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 13 Jun 2019 15:42:13 +0530 Subject: [PATCH 17/30] fix: taxes are not overriding after changing the taxes template --- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index a836a1813e6..5393f5d37fb 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -371,6 +371,10 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte me.frm.pos_print_format = r.message.print_format; } me.frm.script_manager.trigger("update_stock"); + if(me.frm.doc.taxes_and_charges) { + me.frm.script_manager.trigger("taxes_and_charges"); + } + frappe.model.set_default_values(me.frm.doc); me.set_dynamic_labels(); me.calculate_taxes_and_totals(); From 2fc6d1d210b0e07b98a2dff446d84f168b793602 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 13 Jun 2019 16:09:30 +0530 Subject: [PATCH 18/30] fix: column sequence and width --- erpnext/projects/report/billing_summary.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/projects/report/billing_summary.py b/erpnext/projects/report/billing_summary.py index 929a13f6683..76379f1de2e 100644 --- a/erpnext/projects/report/billing_summary.py +++ b/erpnext/projects/report/billing_summary.py @@ -30,23 +30,23 @@ def get_columns(): "options": "Timesheet", "width": 150 }, - { - "label": _("Billable Hours"), - "fieldtype": "Float", - "fieldname": "total_billable_hours", - "width": 50 - }, { "label": _("Working Hours"), "fieldtype": "Float", "fieldname": "total_hours", - "width": 50 + "width": 150 + }, + { + "label": _("Billable Hours"), + "fieldtype": "Float", + "fieldname": "total_billable_hours", + "width": 150 }, { "label": _("Billing Amount"), "fieldtype": "Currency", "fieldname": "amount", - "width": 100 + "width": 150 } ] From a59aaf48f2476a681181ea8a8666c9c8321827b1 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 13 Jun 2019 19:43:47 +0530 Subject: [PATCH 19/30] fix: salary slip amount calculation based on formula --- erpnext/hr/doctype/salary_slip/salary_slip.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index ffd786836a5..d3b960d29eb 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -445,6 +445,8 @@ class SalarySlip(TransactionBase): if not overwrite and component_row.default_amount: amount += component_row.default_amount + else: + component_row.default_amount = amount component_row.amount = amount component_row.deduct_full_tax_on_selected_payroll_date = struct_row.deduct_full_tax_on_selected_payroll_date From f7af9b87457564506c89564165c0cce137ae3fca Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 14 Jun 2019 11:22:23 +0530 Subject: [PATCH 20/30] fix: trial balance opening balance not showing in the debit side for the liability account (#17786) --- .../report/trial_balance/trial_balance.py | 57 +++++++++---------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 6b18c5d8731..b5f0186d4d2 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -180,20 +180,28 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, if d["root_type"] == "Asset" or d["root_type"] == "Equity" or d["root_type"] == "Expense": d["opening_debit"] -= d["opening_credit"] - d["opening_credit"] = 0.0 - total_row["opening_debit"] += d["opening_debit"] + d["closing_debit"] -= d["closing_credit"] + + # For opening + check_opening_closing_has_negative_value(d, "opening_debit", "opening_credit") + + # For closing + check_opening_closing_has_negative_value(d, "closing_debit", "closing_credit") + if d["root_type"] == "Liability" or d["root_type"] == "Income": d["opening_credit"] -= d["opening_debit"] - d["opening_debit"] = 0.0 - total_row["opening_credit"] += d["opening_credit"] - if d["root_type"] == "Asset" or d["root_type"] == "Equity" or d["root_type"] == "Expense": - d["closing_debit"] -= d["closing_credit"] - d["closing_credit"] = 0.0 - total_row["closing_debit"] += d["closing_debit"] - if d["root_type"] == "Liability" or d["root_type"] == "Income": d["closing_credit"] -= d["closing_debit"] - d["closing_debit"] = 0.0 - total_row["closing_credit"] += d["closing_credit"] + + # For opening + check_opening_closing_has_negative_value(d, "opening_credit", "opening_debit") + + # For closing + check_opening_closing_has_negative_value(d, "closing_credit", "closing_debit") + + total_row["opening_debit"] += d["opening_debit"] + total_row["closing_debit"] += d["closing_debit"] + total_row["opening_credit"] += d["opening_credit"] + total_row["closing_credit"] += d["closing_credit"] return total_row @@ -219,8 +227,6 @@ def prepare_data(accounts, filters, total_row, parent_children_map, company_curr if d.account_number else d.account_name) } - prepare_opening_and_closing(d) - for key in value_fields: row[key] = flt(d.get(key, 0.0), 3) @@ -295,22 +301,11 @@ def get_columns(): } ] -def prepare_opening_and_closing(d): - d["closing_debit"] = d["opening_debit"] + d["debit"] - d["closing_credit"] = d["opening_credit"] + d["credit"] +def check_opening_closing_has_negative_value(d, dr_or_cr, switch_to_column): + # If opening debit has negetive value then move it to opening credit and vice versa. - if d["root_type"] == "Asset" or d["root_type"] == "Equity" or d["root_type"] == "Expense": - d["opening_debit"] -= d["opening_credit"] - d["opening_credit"] = 0.0 - - if d["root_type"] == "Liability" or d["root_type"] == "Income": - d["opening_credit"] -= d["opening_debit"] - d["opening_debit"] = 0.0 - - if d["root_type"] == "Asset" or d["root_type"] == "Equity" or d["root_type"] == "Expense": - d["closing_debit"] -= d["closing_credit"] - d["closing_credit"] = 0.0 - - if d["root_type"] == "Liability" or d["root_type"] == "Income": - d["closing_credit"] -= d["closing_debit"] - d["closing_debit"] = 0.0 + if d[dr_or_cr] < 0: + d[switch_to_column] = abs(d[dr_or_cr]) + d[dr_or_cr] = 0.0 + else: + d[switch_to_column] = 0.0 From 1450a7b40822d487c6940dde465f60c97fa4b74c Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Fri, 14 Jun 2019 11:29:29 +0530 Subject: [PATCH 21/30] fix column width (#17936) --- .../budget_variance_report.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index b292bd33b9b..0f802d88cc4 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -45,8 +45,8 @@ def execute(filters=None): if(filters.get("show_cumulative")): last_total = period_data[0] - period_data[1] - - period_data[2] = period_data[0] - period_data[1] + + period_data[2] = period_data[0] - period_data[1] row += period_data totals[2] = totals[0] - totals[1] if filters["period"] != "Yearly" : @@ -60,7 +60,7 @@ def validate_filters(filters): frappe.throw(_("Filter based on Cost Center is only applicable if Budget Against is selected as Cost Center")) def get_columns(filters): - columns = [_(filters.get("budget_against")) + ":Link/%s:80"%(filters.get("budget_against")), _("Account") + ":Link/Account:80"] + columns = [_(filters.get("budget_against")) + ":Link/%s:150"%(filters.get("budget_against")), _("Account") + ":Link/Account:150"] group_months = False if filters["period"] == "Monthly" else True @@ -71,7 +71,7 @@ def get_columns(filters): if filters["period"] == "Yearly": labels = [_("Budget") + " " + str(year[0]), _("Actual ") + " " + str(year[0]), _("Varaiance ") + " " + str(year[0])] for label in labels: - columns.append(label+":Float:80") + columns.append(label+":Float:150") else: for label in [_("Budget") + " (%s)" + " " + str(year[0]), _("Actual") + " (%s)" + " " + str(year[0]), _("Variance") + " (%s)" + " " + str(year[0])]: if group_months: @@ -79,20 +79,20 @@ def get_columns(filters): else: label = label % formatdate(from_date, format_string="MMM") - columns.append(label+":Float:80") + columns.append(label+":Float:150") if filters["period"] != "Yearly" : - return columns + [_("Total Budget") + ":Float:80", _("Total Actual") + ":Float:80", - _("Total Variance") + ":Float:80"] + return columns + [_("Total Budget") + ":Float:150", _("Total Actual") + ":Float:150", + _("Total Variance") + ":Float:150"] else: return columns - + def get_cost_centers(filters): cond = "and 1=1" if filters.get("budget_against") == "Cost Center": cond = "order by lft" - return frappe.db.sql_list("""select name from `tab{tab}` where company=%s + return frappe.db.sql_list("""select name from `tab{tab}` where company=%s {cond}""".format(tab=filters.get("budget_against"), cond=cond), filters.get("company")) #Get cost center & target details @@ -109,7 +109,7 @@ def get_cost_center_target_details(filters): """.format(budget_against=filters.get("budget_against").replace(" ", "_").lower(), cond=cond), (filters.from_fiscal_year,filters.to_fiscal_year,filters.budget_against, filters.company), as_dict=True) - + #Get target distribution details of accounts of cost center def get_target_distribution_details(filters): @@ -118,7 +118,7 @@ def get_target_distribution_details(filters): from `tabMonthly Distribution Percentage` mdp, `tabMonthly Distribution` md where mdp.parent=md.name and md.fiscal_year between %s and %s order by md.fiscal_year""",(filters.from_fiscal_year, filters.to_fiscal_year), as_dict=1): target_details.setdefault(d.name, {}).setdefault(d.month, flt(d.percentage_allocation)) - + return target_details #Get actual details from gl entry @@ -129,7 +129,7 @@ def get_actual_details(name, filters): if filters.get("budget_against") == "Cost Center": cc_lft, cc_rgt = frappe.db.get_value("Cost Center", name, ["lft", "rgt"]) cond = "lft>='{lft}' and rgt<='{rgt}'".format(lft = cc_lft, rgt=cc_rgt) - + ac_details = frappe.db.sql("""select gl.account, gl.debit, gl.credit,gl.fiscal_year, MONTHNAME(gl.posting_date) as month_name, b.{budget_against} as budget_against from `tabGL Entry` gl, `tabBudget Account` ba, `tabBudget` b @@ -159,7 +159,7 @@ def get_cost_center_account_month_map(filters): for ccd in cost_center_target_details: actual_details = get_actual_details(ccd.budget_against, filters) - + for month_id in range(1, 13): month = datetime.date(2013, month_id, 1).strftime('%B') cam_map.setdefault(ccd.budget_against, {}).setdefault(ccd.account, {}).setdefault(ccd.fiscal_year,{})\ @@ -172,7 +172,7 @@ def get_cost_center_account_month_map(filters): if ccd.monthly_distribution else 100.0/12 tav_dict.target = flt(ccd.budget_amount) * month_percentage / 100 - + for ad in actual_details.get(ccd.account, []): if ad.month_name == month: tav_dict.actual += flt(ad.debit) - flt(ad.credit) From dc24fe60be702ad2e0b95ef6867a871179e0a0fe Mon Sep 17 00:00:00 2001 From: Khadija Tul Kubra Zaki Date: Fri, 14 Jun 2019 11:29:45 +0500 Subject: [PATCH 22/30] fix: leave without pay spelling in salary register (#17938) * leave without pay spelling in salary register * fixed typo --- erpnext/hr/report/salary_register/salary_register.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/report/salary_register/salary_register.py b/erpnext/hr/report/salary_register/salary_register.py index 3326ac7a6c8..9c45a628a3f 100644 --- a/erpnext/hr/report/salary_register/salary_register.py +++ b/erpnext/hr/report/salary_register/salary_register.py @@ -19,12 +19,12 @@ def execute(filters=None): data = [] for ss in salary_slips: row = [ss.name, ss.employee, ss.employee_name, ss.branch, ss.department, ss.designation, - ss.company, ss.start_date, ss.end_date, ss.leave_withut_pay, ss.payment_days] + ss.company, ss.start_date, ss.end_date, ss.leave_without_pay, ss.payment_days] if not ss.branch == None:columns[3] = columns[3].replace('-1','120') if not ss.department == None: columns[4] = columns[4].replace('-1','120') if not ss.designation == None: columns[5] = columns[5].replace('-1','120') - if not ss.leave_withut_pay == None: columns[9] = columns[9].replace('-1','130') + if not ss.leave_without_pay == None: columns[9] = columns[9].replace('-1','130') for e in earning_types: @@ -117,4 +117,4 @@ def get_ss_ded_map(salary_slips): ss_ded_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, []) ss_ded_map[d.parent][d.salary_component] = flt(d.amount) - return ss_ded_map \ No newline at end of file + return ss_ded_map From 740c9546790ccc9f2ad5b2fb71aaed4b904d3ee3 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 17 Jun 2019 07:40:21 +0530 Subject: [PATCH 23/30] fix: revert group by voucher general ledger changes --- erpnext/accounts/report/general_ledger/general_ledger.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 4bea0978025..ccb427665b2 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -127,8 +127,7 @@ def get_gl_entries(filters): order_by_statement = "order by posting_date, voucher_type, voucher_no" if filters.get("group_by") == _("Group by Voucher (Consolidated)"): - group_by_statement = """group by voucher_type, voucher_no, account, - cost_center, against_voucher_type, against_voucher, posting_date""" + group_by_statement = "group by voucher_type, voucher_no, account, cost_center" select_fields = """, sum(debit) as debit, sum(credit) as credit, sum(debit_in_account_currency) as debit_in_account_currency, From 6b5c365cb6640dd67899e2afa6cb66bf67325785 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 17 Jun 2019 13:32:36 +0530 Subject: [PATCH 24/30] fix: not able to save asset if depreciation method is manual --- erpnext/assets/doctype/asset/asset.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 72f5c627a71..45f7b30ae8a 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -291,16 +291,19 @@ class Asset(AccountsController): def validate_expected_value_after_useful_life(self): for row in self.get('finance_books'): - accumulated_depreciation_after_full_schedule = max([d.accumulated_depreciation_amount - for d in self.get("schedules") if cint(d.finance_book_id) == row.idx]) + accumulated_depreciation_after_full_schedule = [d.accumulated_depreciation_amount + for d in self.get("schedules") if cint(d.finance_book_id) == row.idx] - asset_value_after_full_schedule = flt(flt(self.gross_purchase_amount) - - flt(accumulated_depreciation_after_full_schedule), - self.precision('gross_purchase_amount')) + if accumulated_depreciation_after_full_schedule: + accumulated_depreciation_after_full_schedule = max(accumulated_depreciation_after_full_schedule) - if row.expected_value_after_useful_life < asset_value_after_full_schedule: - frappe.throw(_("Depreciation Row {0}: Expected value after useful life must be greater than or equal to {1}") - .format(row.idx, asset_value_after_full_schedule)) + asset_value_after_full_schedule = flt(flt(self.gross_purchase_amount) - + flt(accumulated_depreciation_after_full_schedule), + self.precision('gross_purchase_amount')) + + if row.expected_value_after_useful_life < asset_value_after_full_schedule: + frappe.throw(_("Depreciation Row {0}: Expected value after useful life must be greater than or equal to {1}") + .format(row.idx, asset_value_after_full_schedule)) def validate_cancellation(self): if self.status not in ("Submitted", "Partially Depreciated", "Fully Depreciated"): From 8fb018de47e553ffc29cc9f4602fa852f36c4d2d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 17 Jun 2019 16:19:23 +0530 Subject: [PATCH 25/30] fix: allow edit is enabled in the pos profile still user not able to edit the rates --- .../page/point_of_sale/point_of_sale.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 3e6ee86df00..a16a77f1340 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -737,6 +737,17 @@ class POSCart { const customer = this.frm.doc.customer; this.customer_field.set_value(customer); + + if (this.numpad) { + const disable_btns = this.disable_numpad_control() + const enable_btns = [__('Rate'), __('Disc')] + + if (disable_btns) { + enable_btns.filter(btn => !disable_btns.includes(btn)) + } + + this.numpad.enable_buttons(enable_btns); + } } get_grand_total() { @@ -1507,6 +1518,16 @@ class NumberPad { } } + enable_buttons(btns) { + btns.forEach((btn) => { + const $btn = this.get_btn(btn); + $btn.prop("disabled", false) + $btn.hover(() => { + $btn.css('cursor','pointer'); + }) + }) + } + set_class() { for (const btn in this.add_class) { const class_name = this.add_class[btn]; From 2493d5e0d3a3ef405ceadbfa03c798396c1551f0 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 17 Jun 2019 14:57:05 +0530 Subject: [PATCH 26/30] fix: bank guarantee, not able to select the purchase order --- erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js index 2a44cb3b52f..0acbe2009f5 100644 --- a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js +++ b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js @@ -43,8 +43,13 @@ frappe.ui.form.on('Bank Guarantee', { reference_docname: function(frm) { if (frm.doc.reference_docname && frm.doc.reference_doctype) { - let fields_to_fetch = ["project", "grand_total"]; + let fields_to_fetch = ["grand_total"]; let party_field = frm.doc.reference_doctype == "Sales Order" ? "customer" : "supplier"; + + if (frm.doc.reference_doctype == "Sales Order") { + fields_to_fetch.push("project"); + } + fields_to_fetch.push(party_field); frappe.call({ method: "erpnext.accounts.doctype.bank_guarantee.bank_guarantee.get_vouchar_detials", From acbdeb833f79b24e3e89f1ea4710c94c4227bf59 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Mon, 17 Jun 2019 18:05:41 +0530 Subject: [PATCH 27/30] fix(employee-benefit-application): remove query from setup --- .../employee_benefit_application.js | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.js b/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.js index e71ce1276b5..c55c46db993 100644 --- a/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.js +++ b/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.js @@ -2,20 +2,8 @@ // For license information, please see license.txt frappe.ui.form.on('Employee Benefit Application', { - setup: function(frm) { - if(!frm.doc.employee || !frm.doc.date) { - frappe.throw(__("Please select Employee and Date first")); - } else { - frm.set_query("earning_component", "employee_benefits", function() { - return { - query : "erpnext.hr.doctype.employee_benefit_application.employee_benefit_application.get_earning_components", - filters: {date: frm.doc.date, employee: frm.doc.employee} - }; - }); - } - }, - employee: function(frm) { + frm.trigger('set_earning_component'); var method, args; if(frm.doc.employee && frm.doc.date && frm.doc.payroll_period){ method = "erpnext.hr.doctype.employee_benefit_application.employee_benefit_application.get_max_benefits_remaining"; @@ -35,6 +23,21 @@ frappe.ui.form.on('Employee Benefit Application', { get_max_benefits(frm, method, args); } }, + + date: function(frm) { + frm.trigger('set_earning_component'); + }, + + set_earning_component: function(frm) { + if(!frm.doc.employee && !frm.doc.date) return; + frm.set_query("earning_component", "employee_benefits", function() { + return { + query : "erpnext.hr.doctype.employee_benefit_application.employee_benefit_application.get_earning_components", + filters: {date: frm.doc.date, employee: frm.doc.employee} + } + }); + }, + payroll_period: function(frm) { var method, args; if(frm.doc.employee && frm.doc.date && frm.doc.payroll_period){ From 77ed46242f0953eb33dcc18bb056882171e97a4a Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Mon, 17 Jun 2019 19:03:46 +0530 Subject: [PATCH 28/30] fix: change formatting --- .../employee_benefit_application.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.js b/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.js index c55c46db993..b73dcf8ac36 100644 --- a/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.js +++ b/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.js @@ -34,7 +34,7 @@ frappe.ui.form.on('Employee Benefit Application', { return { query : "erpnext.hr.doctype.employee_benefit_application.employee_benefit_application.get_earning_components", filters: {date: frm.doc.date, employee: frm.doc.employee} - } + }; }); }, From 0c7afb389d2db9bf18461439431e0a3cf2c8745d Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Mon, 17 Jun 2019 20:48:11 +0530 Subject: [PATCH 29/30] fix: Company Filter in Total Stock Summary Report (#17973) --- .../total_stock_summary.js | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/report/total_stock_summary/total_stock_summary.js b/erpnext/stock/report/total_stock_summary/total_stock_summary.js index b7461c485f6..264642856da 100644 --- a/erpnext/stock/report/total_stock_summary/total_stock_summary.js +++ b/erpnext/stock/report/total_stock_summary/total_stock_summary.js @@ -10,8 +10,23 @@ frappe.query_reports["Total Stock Summary"] = { "fieldtype": "Select", "width": "80", "reqd": 1, - "options": ["","Warehouse", "Company"], - "default": "Warehouse" + "options": ["", "Warehouse", "Company"], + "change": function() { + let group_by = frappe.query_report.get_filter_value("group_by") + let company_filter = frappe.query_report.get_filter("company") + if (group_by == "Company") { + company_filter.df.reqd = 0; + company_filter.df.hidden = 1; + frappe.query_report.set_filter_value("company", ""); + company_filter.refresh(); + } + else { + company_filter.df.reqd = 1; + company_filter.df.hidden = 0; + company_filter.refresh(); + frappe.query_report.refresh(); + } + } }, { "fieldname": "company", @@ -23,4 +38,4 @@ frappe.query_reports["Total Stock Summary"] = { "reqd": 1 }, ] -} +} \ No newline at end of file From e5647f744185afd4ed0308e2a8e1d6a19c54e1b7 Mon Sep 17 00:00:00 2001 From: Sahil Khan Date: Tue, 18 Jun 2019 15:01:56 +0550 Subject: [PATCH 30/30] bumped to version 11.1.39 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 0fdf7031ab3..a06efa0ff75 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '11.1.38' +__version__ = '11.1.39' def get_default_company(user=None): '''Get default company for user'''