From 3503598735de13b8a87984ca66f187432770dab8 Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Tue, 23 Feb 2021 18:57:52 +0000 Subject: [PATCH 01/22] Initial commit of Tax Detail report and report builder --- .../accounts/report/tax_detail/__init__.py | 0 .../accounts/report/tax_detail/tax_detail.js | 165 +++++++++++++++++ .../report/tax_detail/tax_detail.json | 32 ++++ .../accounts/report/tax_detail/tax_detail.py | 169 ++++++++++++++++++ 4 files changed, 366 insertions(+) create mode 100644 erpnext/accounts/report/tax_detail/__init__.py create mode 100644 erpnext/accounts/report/tax_detail/tax_detail.js create mode 100644 erpnext/accounts/report/tax_detail/tax_detail.json create mode 100644 erpnext/accounts/report/tax_detail/tax_detail.py diff --git a/erpnext/accounts/report/tax_detail/__init__.py b/erpnext/accounts/report/tax_detail/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/report/tax_detail/tax_detail.js b/erpnext/accounts/report/tax_detail/tax_detail.js new file mode 100644 index 0000000000..1ac11409e3 --- /dev/null +++ b/erpnext/accounts/report/tax_detail/tax_detail.js @@ -0,0 +1,165 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +// Contributed by Case Solved and sponsored by Nulight Studios +/* eslint-disable */ + +frappe.query_reports["Tax Detail"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("company"), + "reqd": 1 + }, + { + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "default": frappe.datetime.month_start(frappe.datetime.get_today()), + "reqd": 1, + "width": "60px" + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.month_end(frappe.datetime.get_today()), + "reqd": 1, + "width": "60px" + }, + ], + onload: function(report) { + report.page.add_inner_button(__("New Report"), () => new_report(), __("Custom Report")); + report.page.add_inner_button(__("Load Report"), () => load_report(), __("Custom Report")); + load_page_report(); + } +}; + +class TaxReport { + constructor() { + this.report = frappe.query_reports["Tax Detail"] + this.qr = frappe.query_report + this.page = frappe.query_report.page + this.create_controls() + } + save_report() { + frappe.call({ + method:'erpnext.accounts.report.tax_detail.tax_detail.new_custom_report', + args: {'name': values.report_name}, + freeze: true + }).then((r) => { + frappe.set_route("query-report", values.report_name); + }); + } + create_controls() { + this.section_name = this.page.add_field({ + label: 'Section', + fieldtype: 'Select', + fieldname: 'section_name', + change() { + this.taxreport.set_section() + } + }); + this.new_section = this.page.add_field({ + label: 'New Section', + fieldtype: 'Button', + fieldname: 'new_section' + }); + this.delete_section = this.page.add_field({ + label: 'Delete Section', + fieldtype: 'Button', + fieldname: 'delete_section' + }); + this.page.add_field({ + label: 'Filter', + fieldtype: 'Select', + fieldname: 'filter_index' + }); + this.page.add_field({ + label: 'Add Filter', + fieldtype: 'Button', + fieldname: 'add_filter' + }); + this.page.add_field({ + label: 'Delete Filter', + fieldtype: 'Button', + fieldname: 'delete_filter' + }); + this.page.add_field({ + label: 'Value Column', + fieldtype: 'Select', + fieldname: 'value_field', + }); + this.page.add_field({ + label: 'Save', + fieldtype: 'Button', + fieldname: 'save' + }); + } +} + +function get_reports(cb) { + frappe.call({ + method: 'erpnext.accounts.report.tax_detail.tax_detail.get_custom_reports', + freeze: true + }).then((r) => { + cb(r.message); + }) +} + +function new_report() { + const dialog = new frappe.ui.Dialog({ + title: __("New Report"), + fields: [ + { + fieldname: 'report_name', + label: 'Report Name', + fieldtype: 'Data', + default: 'VAT Return' + } + ], + primary_action_label: __('Create'), + primary_action: function new_report_pa(values) { + frappe.call({ + method:'erpnext.accounts.report.tax_detail.tax_detail.new_custom_report', + args: {'name': values.report_name}, + freeze: true + }).then((r) => { + frappe.set_route("query-report", values.report_name); + }); + dialog.hide(); + } + }); + dialog.show(); +} + +function load_page_report() { + if (frappe.query_report.report_name === 'Tax Detail') { + return; + } + this.taxreport = new TaxReport(); +} + +function load_report() { + get_reports(function load_report_cb(reports) { + const dialog = new frappe.ui.Dialog({ + title: __("Load Report"), + fields: [ + { + fieldname: 'report_name', + label: 'Report Name', + fieldtype: 'Select', + options: Object.keys(reports) + } + ], + primary_action_label: __('Load'), + primary_action: function load_report_pa(values) { + dialog.hide(); + frappe.set_route("query-report", values.report_name); + } + }); + dialog.show(); + }); +} diff --git a/erpnext/accounts/report/tax_detail/tax_detail.json b/erpnext/accounts/report/tax_detail/tax_detail.json new file mode 100644 index 0000000000..d52ffd05ac --- /dev/null +++ b/erpnext/accounts/report/tax_detail/tax_detail.json @@ -0,0 +1,32 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-02-19 16:44:21.175113", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-02-19 16:44:21.175113", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Tax Detail", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "GL Entry", + "report_name": "Tax Detail", + "report_type": "Script Report", + "roles": [ + { + "role": "Accounts User" + }, + { + "role": "Accounts Manager" + }, + { + "role": "Auditor" + } + ] +} \ No newline at end of file diff --git a/erpnext/accounts/report/tax_detail/tax_detail.py b/erpnext/accounts/report/tax_detail/tax_detail.py new file mode 100644 index 0000000000..46e7ae08e9 --- /dev/null +++ b/erpnext/accounts/report/tax_detail/tax_detail.py @@ -0,0 +1,169 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt +# Contributed by Case Solved and sponsored by Nulight Studios + +from __future__ import unicode_literals +import frappe +from frappe import _ + +# field lists in multiple doctypes will be coalesced +required_sql_fields = { + "GL Entry": ["posting_date", "voucher_type", "voucher_no", "account", "account_currency", "debit", "credit"], + "Account": ["account_type"], + ("Purchase Invoice", "Sales Invoice"): ["taxes_and_charges", "tax_category"], + ("Purchase Invoice Item", "Sales Invoice Item"): ["item_tax_template", "item_name", "base_net_amount", "item_tax_rate"], +# "Journal Entry": ["total_amount_currency"], +# "Journal Entry Account": ["debit_in_account_currency", "credit_in_account_currency"] +} + +@frappe.whitelist() +def get_required_fieldlist(): + """For overriding the fieldlist from the client""" + return required_sql_fields + +def execute(filters=None, fieldlist=required_sql_fields): + if not filters: + return [], [] + + fieldstr = get_fieldstr(fieldlist) + + gl_entries = frappe.db.sql(""" + select {fieldstr} + from `tabGL Entry` ge + inner join `tabAccount` a on + ge.account=a.name and ge.company=a.company + left join `tabSales Invoice` si on + a.account_type='Tax' and ge.company=si.company and ge.voucher_type='Sales Invoice' and ge.voucher_no=si.name + left join `tabSales Invoice Item` sii on + si.name=sii.parent + left join `tabPurchase Invoice` pi on + a.account_type='Tax' and ge.company=pi.company and ge.voucher_type='Purchase Invoice' and ge.voucher_no=pi.name + left join `tabPurchase Invoice Item` pii on + pi.name=pii.parent +/* left outer join `tabJournal Entry` je on + ge.voucher_no=je.name and ge.company=je.company + left outer join `tabJournal Entry Account` jea on + je.name=jea.parent and a.account_type='Tax' */ + where (ge.voucher_type, ge.voucher_no) in ( + select ge.voucher_type, ge.voucher_no + from `tabGL Entry` ge + join `tabAccount` a on ge.account=a.name and ge.company=a.company + where + a.account_type='Tax' and + ge.company=%(company)s and + ge.posting_date>=%(from_date)s and + ge.posting_date<=%(to_date)s + ) + order by ge.posting_date, ge.voucher_no + """.format(fieldstr=fieldstr), filters, as_dict=1) + + gl_entries = modify_report_data(gl_entries) + + return get_columns(fieldlist), gl_entries + + +abbrev = lambda dt: ''.join(l[0].lower() for l in dt.split(' ')) + '.' +doclist = lambda dt, dfs: [abbrev(dt) + f for f in dfs] +coalesce = lambda dts, dfs: ['coalesce(' + ', '.join(abbrev(dt) + f for dt in dts) + ') ' + f for f in dfs] + +def get_fieldstr(fieldlist): + fields = [] + for doctypes, docfields in fieldlist.items(): + if isinstance(doctypes, str): + fields += doclist(doctypes, docfields) + if isinstance(doctypes, tuple): + fields += coalesce(doctypes, docfields) + return ', '.join(fields) + +def get_columns(fieldlist): + columns = {} + for doctypes, docfields in fieldlist.items(): + if isinstance(doctypes, str): + doctypes = [doctypes] + for doctype in doctypes: + meta = frappe.get_meta(doctype) + # get column field metadata from the db + fieldmeta = {} + for field in meta.get('fields'): + if field.fieldname in docfields: + fieldmeta[field.fieldname] = { + "label": _(field.label), + "fieldname": field.fieldname, + "fieldtype": field.fieldtype, + "options": field.options + } + # edit the columns to match the modified data + for field in docfields: + col = modify_report_columns(doctype, field, fieldmeta[field]) + if col: + columns[col["fieldname"]] = col + # use of a dict ensures duplicate columns are removed + return list(columns.values()) + +def modify_report_columns(doctype, field, column): + "Because data is rearranged into other columns" + if doctype in ["Sales Invoice Item", "Purchase Invoice Item"] and field == "item_tax_rate": + return None + if doctype == "Sales Invoice Item" and field == "base_net_amount": + column.update({"label": _("Credit Net Amount"), "fieldname": "credit_net_amount"}) + if doctype == "Purchase Invoice Item" and field == "base_net_amount": + column.update({"label": _("Debit Net Amount"), "fieldname": "debit_net_amount"}) + if field == "taxes_and_charges": + column.update({"label": _("Taxes and Charges Template")}) + return column + +def modify_report_data(data): + import json + for line in data: + if line.account_type == "Tax" and line.item_tax_rate: + tax_rates = json.loads(line.item_tax_rate) + for account, rate in tax_rates.items(): + if account == line.account: + if line.voucher_type == "Sales Invoice": + line.credit = line.base_net_amount * (rate / 100) + line.credit_net_amount = line.base_net_amount + if line.voucher_type == "Purchase Invoice": + line.debit = line.base_net_amount * (rate / 100) + line.debit_net_amount = line.base_net_amount + return data + +####### JS client utilities + +custom_report_dict = { + 'ref_doctype': 'GL Entry', + 'report_type': 'Custom Report', + 'reference_report': 'Tax Detail' +} + +@frappe.whitelist() +def get_custom_reports(): + reports = frappe.get_list('Report', + filters = custom_report_dict, + fields = ['name', 'json'], + as_list=False + ) + reports_dict = {rep.pop('name'): rep for rep in reports} + # Prevent custom reports with the same name + reports_dict['Tax Detail'] = {'json': None} + return reports_dict + +@frappe.whitelist() +def new_custom_report(name=None): + if name == 'Tax Detail': + frappe.throw("The parent report cannot be overwritten.") + if not name: + frappe.throw("The report name must be supplied.") + doc = { + 'doctype': 'Report', + 'report_name': name, + 'is_standard': 'No', + 'module': 'Accounts' + } + doc.update(custom_report_dict) + doc = frappe.get_doc(doc) + doc.insert() + return True + +@frappe.whitelist() +def save_custom_report(data): + return None From a5d47f70b8864c67bddbb20e913116a01a901dfb Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Fri, 5 Mar 2021 06:46:38 +0000 Subject: [PATCH 02/22] Fleshed out report setup functionality --- .../accounts/report/tax_detail/tax_detail.js | 320 ++++++++++++++---- 1 file changed, 255 insertions(+), 65 deletions(-) diff --git a/erpnext/accounts/report/tax_detail/tax_detail.js b/erpnext/accounts/report/tax_detail/tax_detail.js index 1ac11409e3..0b0026028f 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.js +++ b/erpnext/accounts/report/tax_detail/tax_detail.js @@ -4,47 +4,79 @@ /* eslint-disable */ frappe.query_reports["Tax Detail"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("company"), + reqd: 1 }, { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": frappe.datetime.month_start(frappe.datetime.get_today()), - "reqd": 1, - "width": "60px" + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.month_start(frappe.datetime.get_today()), + reqd: 1, + width: "60px" }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.month_end(frappe.datetime.get_today()), - "reqd": 1, - "width": "60px" + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.month_end(frappe.datetime.get_today()), + reqd: 1, + width: "60px" }, ], - onload: function(report) { - report.page.add_inner_button(__("New Report"), () => new_report(), __("Custom Report")); - report.page.add_inner_button(__("Load Report"), () => load_report(), __("Custom Report")); - load_page_report(); + onload: function onload(report) { + // Remove Add Column and Save from menu + report.page.add_inner_button(__("New Report"), () => new_report, __("Custom Report")); + report.page.add_inner_button(__("Load Report"), () => load_report, __("Custom Report")); + }, + after_datatable_render: (datatable) => { + if (frappe.query_report.report_name == 'Tax Detail') { + return; + } + if (this.taxreport) { + this.taxreport.load_report(); + } else { + this.taxreport = new TaxReport(); + } } }; class TaxReport { + // construct after datatable is loaded constructor() { - this.report = frappe.query_reports["Tax Detail"] - this.qr = frappe.query_report - this.page = frappe.query_report.page - this.create_controls() + this.report = frappe.query_reports["Tax Detail"]; + this.qr = frappe.query_report; + this.page = frappe.query_report.page; + this.create_controls(); + this.sections = {}; + this.mode = 'run'; + this.load_report(); + } + load_report() { + // TODO + this.setup_menu(); + // this.qr.refresh_report() + } + setup_menu() { + this.qr.menu_items.forEach((item, idx) => { + if (item['label'] == __('Save')) { + delete this.qr.menu_items[idx]; + } + }) + this.qr.menu_items.push({ + label: __('Save'), + action: this.save_report + }) + this.qr.set_menu_items(); } save_report() { + // TODO frappe.call({ method:'erpnext.accounts.report.tax_detail.tax_detail.new_custom_report', args: {'name': values.report_name}, @@ -53,53 +85,218 @@ class TaxReport { frappe.set_route("query-report", values.report_name); }); } - create_controls() { - this.section_name = this.page.add_field({ - label: 'Section', - fieldtype: 'Select', - fieldname: 'section_name', - change() { - this.taxreport.set_section() + set_value_options() { + let curcols = []; + let options = []; + this.qr.columns.forEach((col, index) => { + if (col['fieldtype'] == "Currency") { + curcols.push(index); + options.push(col['label']); } }); - this.new_section = this.page.add_field({ - label: 'New Section', - fieldtype: 'Button', - fieldname: 'new_section' + this.currency_cols = curcols; + this.controls['value_field'].$wrapper.find("select").empty().add_options(options); + this.controls['value_field'].set_input(options[0]); + } + add_value_field_to_filters(filters) { + const curlabel = this.controls['value_field'].value; + this.currency_cols.forEach(index => { + if (this.qr.columns[index]['label'] == curlabel) { + filters['fieldname'] = this.qr.columns[index]['fieldname']; + } }); - this.delete_section = this.page.add_field({ - label: 'Delete Section', - fieldtype: 'Button', - fieldname: 'delete_section' + return filters; + } + new_section(label) { + const dialog = new frappe.ui.Dialog({ + title: label, + fields: [{ + fieldname: 'data', + label: label, + fieldtype: 'Data' + }], + primary_action_label: label, + primary_action: (values) => { + dialog.hide(); + this.set_section(values.data); + } }); - this.page.add_field({ - label: 'Filter', + dialog.show(); + } + set_section(name) { + this.mode = 'edit'; + if (name && !this.sections[name]) { + this.sections[name] = {}; + this.controls['section_name'].$wrapper.find("select").empty().add_options(Object.keys(this.sections)); + } + if (name) { + this.controls['section_name'].set_input(name); + } + this.reload(); + } + reload() { + if (this.mode == 'edit') { + const section_name = this.controls['section_name'].value; + let filters = {}; + if (section_name) { + let fidx = this.controls['filter_index'].value; + let section = this.sections[section_name]; + let fidxs = Object.keys(section); + fidxs.unshift(''); + this.controls['filter_index'].$wrapper.find("select").empty().add_options(fidxs); + this.controls['filter_index'].set_input(fidx); + if (fidx != '') { + filters = section[fidx]; + } + } else { + this.controls['filter_index'].$wrapper.find("select").empty(); + } + // Set filters + // reload datatable + } else { + this.controls['filter_index'].$wrapper.find("select").empty(); + // Query the result from the server & render + } + } + get_select(label, list, type) { + const dialog = new frappe.ui.Dialog({ + title: label, + fields: [{ + fieldname: 'select', + label: label, + fieldtype: 'Select', + options: list + }], + primary_action_label: label, + primary_action: (values) => { + dialog.hide(); + this.exec_select(values.select, type); + } + }); + dialog.show(); + } + delete(name, type) { + if (type === 'section') { + delete this.sections[name]; + this.controls['section_name'].$wrapper.find("select").empty().add_options(Object.keys(this.sections)); + this.controls['section_name'].set_input(Object.keys(this.sections)[0] || ''); + this.controls['filter_index'].set_input(''); + } + if (type === 'filter') { + let cur_section = this.controls['section_name'].value; + delete this.sections[cur_section][name]; + this.controls['filter_index'].set_input(''); + } + this.reload(); + } + create_controls() { + if (this.controls) { + return; + } + let controls = {}; + // SELECT in data.js + controls['section_name'] = this.page.add_field({ + label: __('Section'), fieldtype: 'Select', - fieldname: 'filter_index' + fieldname: 'section_name', + change: (e) => { + this.set_section(); + } }); - this.page.add_field({ - label: 'Add Filter', + // BUTTON in button.js + controls['new_section'] = this.page.add_field({ + label: __('New Section'), fieldtype: 'Button', - fieldname: 'add_filter' + fieldname: 'new_section', + click: () => { + this.new_section(__('New Section')); + } }); - this.page.add_field({ - label: 'Delete Filter', + controls['delete_section'] = this.page.add_field({ + label: __('Delete Section'), fieldtype: 'Button', - fieldname: 'delete_filter' + fieldname: 'delete_section', + click: () => { + let cur_section = this.controls['section_name'].value; + if (cur_section) { + frappe.confirm(__('Are you sure you want to delete section ') + cur_section + '?', + () => {this.delete(cur_section, 'section')}); + } + } }); - this.page.add_field({ - label: 'Value Column', + controls['filter_index'] = this.page.add_field({ + label: __('Filter'), + fieldtype: 'Select', + fieldname: 'filter_index', + change: (e) => { + // TODO + } + }); + controls['add_filter'] = this.page.add_field({ + label: __('Add Filter'), + fieldtype: 'Button', + fieldname: 'add_filter', + click: () => { + let section_name = this.controls['section_name'].value; + if (section_name) { + let prefix = 'Filter'; + let filters = this.qr.datatable.columnmanager.getAppliedFilters(); + filters = this.add_value_field_to_filters(filters); + const fidxs = Object.keys(this.sections[section_name]); + let new_idx = prefix + '0'; + if (fidxs.length > 0) { + const fiidxs = fidxs.map((key) => parseInt(key.replace(prefix, ''))); + new_idx = prefix + (Math.max(...fiidxs) + 1).toString(); + } + this.sections[section_name][new_idx] = filters; + this.controls['filter_index'].set_input(new_idx); + this.reload(); + } else { + frappe.throw(__('Please add or select the Section first')); + } + } + }); + controls['delete_filter'] = this.page.add_field({ + label: __('Delete Filter'), + fieldtype: 'Button', + fieldname: 'delete_filter', + click: () => { + let cur_filter = this.controls['filter_index'].value; + if (cur_filter) { + frappe.confirm(__('Are you sure you want to delete filter ') + cur_filter + '?', + () => {this.delete(cur_filter, 'filter')}); + } + } + }); + controls['value_field'] = this.page.add_field({ + label: __('Value Column'), fieldtype: 'Select', fieldname: 'value_field', + change: (e) => { + // TODO + } }); - this.page.add_field({ - label: 'Save', + controls['save'] = this.page.add_field({ + label: __('Save & Run'), fieldtype: 'Button', - fieldname: 'save' + fieldname: 'save', + click: () => { + // TODO: Save to db + this.mode = 'run'; + this.reload(); + } }); + this.controls = controls; + this.set_value_options(); + this.show_help(); + } + show_help() { + const help = __('You can add multiple sections to your custom report using the New Section button above. To specify what data goes in each section, specify column filters below, then save with Add Filter. Each section can have multiple filters added. You can specify which Currency column will be summed for each filter in the final report with the Value Column select box.'); + this.qr.show_status(help); } } + function get_reports(cb) { frappe.call({ method: 'erpnext.accounts.report.tax_detail.tax_detail.get_custom_reports', @@ -115,7 +312,7 @@ function new_report() { fields: [ { fieldname: 'report_name', - label: 'Report Name', + label: __('Report Name'), fieldtype: 'Data', default: 'VAT Return' } @@ -135,13 +332,6 @@ function new_report() { dialog.show(); } -function load_page_report() { - if (frappe.query_report.report_name === 'Tax Detail') { - return; - } - this.taxreport = new TaxReport(); -} - function load_report() { get_reports(function load_report_cb(reports) { const dialog = new frappe.ui.Dialog({ @@ -149,7 +339,7 @@ function load_report() { fields: [ { fieldname: 'report_name', - label: 'Report Name', + label: __('Report Name'), fieldtype: 'Select', options: Object.keys(reports) } From ef8ab135c9ebafaf3f5d9a8a7c4698935ef2eef8 Mon Sep 17 00:00:00 2001 From: Richard Case Date: Sun, 14 Mar 2021 06:05:02 +0000 Subject: [PATCH 03/22] develop: progress tax detail report --- .../accounts/report/tax_detail/tax_detail.js | 290 ++++++++++++------ .../accounts/report/tax_detail/tax_detail.py | 54 ++-- 2 files changed, 224 insertions(+), 120 deletions(-) diff --git a/erpnext/accounts/report/tax_detail/tax_detail.js b/erpnext/accounts/report/tax_detail/tax_detail.js index 0b0026028f..894db9e55c 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.js +++ b/erpnext/accounts/report/tax_detail/tax_detail.js @@ -29,11 +29,28 @@ frappe.query_reports["Tax Detail"] = { reqd: 1, width: "60px" }, + { + fieldname: "report_name", + label: __("Report Name"), + fieldtype: "Read Only", + default: frappe.query_report.report_name, + hidden: 1, + reqd: 1 + }, + { + fieldname: "mode", + label: __("Mode"), + fieldtype: "Read Only", + default: "run", + hidden: 1, + reqd: 1 + } ], onload: function onload(report) { // Remove Add Column and Save from menu - report.page.add_inner_button(__("New Report"), () => new_report, __("Custom Report")); - report.page.add_inner_button(__("Load Report"), () => load_report, __("Custom Report")); + report.page.add_inner_button(__("New Report"), () => new_report(), __("Custom Report")); + report.page.add_inner_button(__("Load Report"), () => load_report(), __("Custom Report")); + hide_filters(); }, after_datatable_render: (datatable) => { if (frappe.query_report.report_name == 'Tax Detail') { @@ -47,65 +64,83 @@ frappe.query_reports["Tax Detail"] = { } }; +function hide_filters() { + frappe.query_report.page.page_form[0].querySelectorAll('.form-group.frappe-control').forEach(function setHidden(field) { + if (field.dataset.fieldtype == "Read Only") { + field.classList.add("hidden"); + } + }); +} + class TaxReport { // construct after datatable is loaded constructor() { - this.report = frappe.query_reports["Tax Detail"]; this.qr = frappe.query_report; this.page = frappe.query_report.page; this.create_controls(); - this.sections = {}; - this.mode = 'run'; this.load_report(); } load_report() { - // TODO - this.setup_menu(); - // this.qr.refresh_report() - } - setup_menu() { - this.qr.menu_items.forEach((item, idx) => { - if (item['label'] == __('Save')) { - delete this.qr.menu_items[idx]; - } - }) - this.qr.menu_items.push({ - label: __('Save'), - action: this.save_report - }) - this.qr.set_menu_items(); - } - save_report() { - // TODO + if (this.loaded) { + return; + } + const report_name = this.qr.report_name; + this.report_name.value = report_name; frappe.call({ - method:'erpnext.accounts.report.tax_detail.tax_detail.new_custom_report', - args: {'name': values.report_name}, + method: 'erpnext.accounts.report.tax_detail.tax_detail.get_custom_reports', + args: {name: report_name}, freeze: true }).then((r) => { - frappe.set_route("query-report", values.report_name); + const data = JSON.parse(r.message[report_name]['json']); + if (data && data['sections']) { + this.sections = data['sections']; + } else { + this.sections = {}; + } + this.set_section(); + }) + this.loaded = 1; + } + save_report() { + frappe.call({ + method:'erpnext.accounts.report.tax_detail.tax_detail.save_custom_report', + args: { + reference_report: 'Tax Detail', + report_name: this.qr.report_name, + columns: this.qr.get_visible_columns(), + sections: this.sections + }, + freeze: true + }).then((r) => { + this.reload(); }); } set_value_options() { - let curcols = []; - let options = []; + this.fieldname_lookup = {}; + this.label_lookup = {}; this.qr.columns.forEach((col, index) => { if (col['fieldtype'] == "Currency") { - curcols.push(index); - options.push(col['label']); + this.fieldname_lookup[col['label']] = col['fieldname']; + this.label_lookup[col['fieldname']] = col['label']; } }); - this.currency_cols = curcols; + const options = Object.keys(this.fieldname_lookup); this.controls['value_field'].$wrapper.find("select").empty().add_options(options); this.controls['value_field'].set_input(options[0]); } - add_value_field_to_filters(filters) { + set_value_label_from_filter() { + const section_name = this.controls['section_name'].value; + const fidx = this.controls['filter_index'].value; + if (section_name && fidx) { + const fieldname = this.sections[section_name][fidx]['fieldname']; + this.controls['value_field'].set_input(this.label_lookup[fieldname]); + } else { + this.controls['value_field'].set_input(Object.keys(this.fieldname_lookup)[0]); + } + } + get_value_fieldname() { const curlabel = this.controls['value_field'].value; - this.currency_cols.forEach(index => { - if (this.qr.columns[index]['label'] == curlabel) { - filters['fieldname'] = this.qr.columns[index]['fieldname']; - } - }); - return filters; + return this.fieldname_lookup[curlabel]; } new_section(label) { const dialog = new frappe.ui.Dialog({ @@ -123,57 +158,87 @@ class TaxReport { }); dialog.show(); } - set_section(name) { - this.mode = 'edit'; - if (name && !this.sections[name]) { - this.sections[name] = {}; - this.controls['section_name'].$wrapper.find("select").empty().add_options(Object.keys(this.sections)); - } - if (name) { - this.controls['section_name'].set_input(name); - } - this.reload(); - } - reload() { - if (this.mode == 'edit') { - const section_name = this.controls['section_name'].value; - let filters = {}; - if (section_name) { - let fidx = this.controls['filter_index'].value; - let section = this.sections[section_name]; - let fidxs = Object.keys(section); - fidxs.unshift(''); - this.controls['filter_index'].$wrapper.find("select").empty().add_options(fidxs); - this.controls['filter_index'].set_input(fidx); - if (fidx != '') { - filters = section[fidx]; - } - } else { - this.controls['filter_index'].$wrapper.find("select").empty(); + get_filter_controls() { + this.qr.filters.forEach(filter => { + if (filter['fieldname'] == 'mode') { + this.mode = filter; } - // Set filters - // reload datatable - } else { - this.controls['filter_index'].$wrapper.find("select").empty(); - // Query the result from the server & render - } - } - get_select(label, list, type) { - const dialog = new frappe.ui.Dialog({ - title: label, - fields: [{ - fieldname: 'select', - label: label, - fieldtype: 'Select', - options: list - }], - primary_action_label: label, - primary_action: (values) => { - dialog.hide(); - this.exec_select(values.select, type); + if (filter['fieldname'] == 'report_name') { + this.report_name = filter; } }); - dialog.show(); + } + set_mode(mode) { + this.mode.value = mode; + } + edit_mode() { + return this.mode.value == 'edit'; + } + set_section(name) { + if (name && !this.sections[name]) { + this.sections[name] = {}; + } + let options = Object.keys(this.sections); + options.unshift(''); + this.controls['section_name'].$wrapper.find("select").empty().add_options(options); + if (name) { + this.controls['section_name'].set_input(name); + } else { + this.controls['section_name'].set_input(''); + } + if (this.controls['section_name'].value) { + this.set_mode('edit'); + } else { + this.set_mode('run'); + } + this.controls['filter_index'].set_input(''); + this.reload(); + } + reload_filter() { + const section_name = this.controls['section_name'].value; + if (section_name) { + let fidx = this.controls['filter_index'].value; + let section = this.sections[section_name]; + let fidxs = Object.keys(section); + fidxs.unshift(''); + this.controls['filter_index'].$wrapper.find("select").empty().add_options(fidxs); + this.controls['filter_index'].set_input(fidx); + } else { + this.controls['filter_index'].$wrapper.find("select").empty(); + this.controls['filter_index'].set_input(''); + } + this.set_filters(); + } + set_filters() { + let filters = {}; + const section_name = this.controls['section_name'].value; + const fidx = this.controls['filter_index'].value; + if (section_name && fidx) { + filters = this.sections[section_name][fidx]['filters']; + } + this.setAppliedFilters(filters); + this.qr.datatable.columnmanager.applyFilter(filters); + this.set_value_label_from_filter(); + } + setAppliedFilters(filters) { + Array.from(this.qr.datatable.header.querySelectorAll('.dt-filter')).map(function setFilters(input) { + let idx = input.dataset.colIndex; + if (filters[idx]) { + input.value = filters[idx]; + } else { + input.value = null; + } + }); + } + reload() { + // Reloads the data. When the datatable is reloaded, load_report() + // will be run by the after_datatable_render event. + this.qr.refresh(); + if (this.edit_mode()) { + this.reload_filter(); + } else { + this.controls['filter_index'].$wrapper.find("select").empty(); + } } delete(name, type) { if (type === 'section') { @@ -200,7 +265,7 @@ class TaxReport { fieldtype: 'Select', fieldname: 'section_name', change: (e) => { - this.set_section(); + this.set_section(this.controls['section_name'].get_input_value()); } }); // BUTTON in button.js @@ -229,7 +294,8 @@ class TaxReport { fieldtype: 'Select', fieldname: 'filter_index', change: (e) => { - // TODO + this.controls['filter_index'].set_input(this.controls['filter_index'].get_input_value()); + this.set_filters(); } }); controls['add_filter'] = this.page.add_field({ @@ -240,17 +306,19 @@ class TaxReport { let section_name = this.controls['section_name'].value; if (section_name) { let prefix = 'Filter'; - let filters = this.qr.datatable.columnmanager.getAppliedFilters(); - filters = this.add_value_field_to_filters(filters); + let data = { + filters: this.qr.datatable.columnmanager.getAppliedFilters(), + fieldname: this.get_value_fieldname() + } const fidxs = Object.keys(this.sections[section_name]); let new_idx = prefix + '0'; if (fidxs.length > 0) { const fiidxs = fidxs.map((key) => parseInt(key.replace(prefix, ''))); new_idx = prefix + (Math.max(...fiidxs) + 1).toString(); } - this.sections[section_name][new_idx] = filters; + this.sections[section_name][new_idx] = data; this.controls['filter_index'].set_input(new_idx); - this.reload(); + this.reload_filter(); } else { frappe.throw(__('Please add or select the Section first')); } @@ -273,7 +341,7 @@ class TaxReport { fieldtype: 'Select', fieldname: 'value_field', change: (e) => { - // TODO + this.controls['value_field'].set_input(this.controls['value_field'].get_input_value()); } }); controls['save'] = this.page.add_field({ @@ -281,17 +349,22 @@ class TaxReport { fieldtype: 'Button', fieldname: 'save', click: () => { - // TODO: Save to db - this.mode = 'run'; - this.reload(); + this.controls['section_name'].set_input(''); + this.set_mode('run'); + this.save_report(); } }); this.controls = controls; this.set_value_options(); + this.get_filter_controls(); this.show_help(); } show_help() { - const help = __('You can add multiple sections to your custom report using the New Section button above. To specify what data goes in each section, specify column filters below, then save with Add Filter. Each section can have multiple filters added. You can specify which Currency column will be summed for each filter in the final report with the Value Column select box.'); + const help = __(`You can add multiple sections to your custom report using the New Section button above. + To specify what data goes in each section, specify column filters below, then save with Add Filter. + Each section can have multiple filters added. + You can specify which Currency column will be summed for each filter in the final report with the Value Column select box. + Once you're done, hit Save & Run.`); this.qr.show_status(help); } } @@ -306,6 +379,20 @@ function get_reports(cb) { }) } +function override_menu() { + //TODO: Replace save button + this.qr.menu_items.forEach((item, idx) => { + if (item['label'] == __('Save')) { + delete this.qr.menu_items[idx]; + } + }) + this.qr.menu_items.push({ + label: __('Save'), + action: this.save_report + }) + this.qr.set_menu_items(); +} + function new_report() { const dialog = new frappe.ui.Dialog({ title: __("New Report"), @@ -320,8 +407,13 @@ function new_report() { primary_action_label: __('Create'), primary_action: function new_report_pa(values) { frappe.call({ - method:'erpnext.accounts.report.tax_detail.tax_detail.new_custom_report', - args: {'name': values.report_name}, + method:'erpnext.accounts.report.tax_detail.tax_detail.save_custom_report', + args: { + reference_report: 'Tax Detail', + report_name: values.report_name, + columns: frappe.query_report.get_visible_columns(), + sections: {} + }, freeze: true }).then((r) => { frappe.set_route("query-report", values.report_name); diff --git a/erpnext/accounts/report/tax_detail/tax_detail.py b/erpnext/accounts/report/tax_detail/tax_detail.py index 46e7ae08e9..2ea782eb7a 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.py +++ b/erpnext/accounts/report/tax_detail/tax_detail.py @@ -3,7 +3,7 @@ # Contributed by Case Solved and sponsored by Nulight Studios from __future__ import unicode_literals -import frappe +import frappe, json from frappe import _ # field lists in multiple doctypes will be coalesced @@ -16,15 +16,12 @@ required_sql_fields = { # "Journal Entry Account": ["debit_in_account_currency", "credit_in_account_currency"] } -@frappe.whitelist() -def get_required_fieldlist(): - """For overriding the fieldlist from the client""" - return required_sql_fields -def execute(filters=None, fieldlist=required_sql_fields): +def execute(filters=None): if not filters: return [], [] + fieldlist = required_sql_fields fieldstr = get_fieldstr(fieldlist) gl_entries = frappe.db.sql(""" @@ -136,9 +133,12 @@ custom_report_dict = { } @frappe.whitelist() -def get_custom_reports(): +def get_custom_reports(name=None): + filters = custom_report_dict.copy() + if name: + filters['name'] = name reports = frappe.get_list('Report', - filters = custom_report_dict, + filters = filters, fields = ['name', 'json'], as_list=False ) @@ -148,22 +148,34 @@ def get_custom_reports(): return reports_dict @frappe.whitelist() -def new_custom_report(name=None): - if name == 'Tax Detail': - frappe.throw("The parent report cannot be overwritten.") - if not name: - frappe.throw("The report name must be supplied.") +def save_custom_report(reference_report, report_name, columns, sections): + import pymysql + if reference_report != 'Tax Detail': + frappe.throw(_("The wrong report is referenced.")) + if report_name == 'Tax Detail': + frappe.throw(_("The parent report cannot be overwritten.")) + + data = { + 'columns': json.loads(columns), + 'sections': json.loads(sections) + } + doc = { 'doctype': 'Report', - 'report_name': name, + 'report_name': report_name, 'is_standard': 'No', - 'module': 'Accounts' + 'module': 'Accounts', + 'json': json.dumps(data, separators=(',', ':')) } doc.update(custom_report_dict) - doc = frappe.get_doc(doc) - doc.insert() - return True -@frappe.whitelist() -def save_custom_report(data): - return None + try: + newdoc = frappe.get_doc(doc) + newdoc.insert() + frappe.msgprint(_("Report created successfully")) + except (frappe.exceptions.DuplicateEntryError, pymysql.err.IntegrityError): + dbdoc = frappe.get_doc('Report', report_name) + dbdoc.update(doc) + dbdoc.save() + frappe.msgprint(_("Report updated successfully")) + return report_name From dba4b3cd13e12e3db1233cbf707744773fff62e1 Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Fri, 19 Mar 2021 23:05:19 +0000 Subject: [PATCH 04/22] feat: add run mode, add tests, various fixes --- .../accounts/report/tax_detail/tax_detail.js | 22 +++-- .../accounts/report/tax_detail/tax_detail.py | 95 ++++++++++++++++--- .../report/tax_detail/test_tax_detail.py | 67 +++++++++++++ 3 files changed, 166 insertions(+), 18 deletions(-) create mode 100644 erpnext/accounts/report/tax_detail/test_tax_detail.py diff --git a/erpnext/accounts/report/tax_detail/tax_detail.js b/erpnext/accounts/report/tax_detail/tax_detail.js index 894db9e55c..8cdce54852 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.js +++ b/erpnext/accounts/report/tax_detail/tax_detail.js @@ -92,11 +92,8 @@ class TaxReport { freeze: true }).then((r) => { const data = JSON.parse(r.message[report_name]['json']); - if (data && data['sections']) { - this.sections = data['sections']; - } else { - this.sections = {}; - } + this.sections = data.sections || {}; + this.controls['show_detail'].set_input(data.show_detail); this.set_section(); }) this.loaded = 1; @@ -107,8 +104,11 @@ class TaxReport { args: { reference_report: 'Tax Detail', report_name: this.qr.report_name, - columns: this.qr.get_visible_columns(), - sections: this.sections + data: { + columns: this.qr.get_visible_columns(), + sections: this.sections, + show_detail: this.controls['show_detail'].get_input_value() + } }, freeze: true }).then((r) => { @@ -233,7 +233,9 @@ class TaxReport { reload() { // Reloads the data. When the datatable is reloaded, load_report() // will be run by the after_datatable_render event. + // TODO: why does this trigger multiple reloads? this.qr.refresh(); + this.show_help(); if (this.edit_mode()) { this.reload_filter(); } else { @@ -354,6 +356,12 @@ class TaxReport { this.save_report(); } }); + controls['show_detail'] = this.page.add_field({ + label: __('Show Detail'), + fieldtype: 'Check', + fieldname: 'show_detail', + default: 1 + }); this.controls = controls; this.set_value_options(); this.get_filter_controls(); diff --git a/erpnext/accounts/report/tax_detail/tax_detail.py b/erpnext/accounts/report/tax_detail/tax_detail.py index 2ea782eb7a..6bed89841c 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.py +++ b/erpnext/accounts/report/tax_detail/tax_detail.py @@ -54,10 +54,89 @@ def execute(filters=None): order by ge.posting_date, ge.voucher_no """.format(fieldstr=fieldstr), filters, as_dict=1) - gl_entries = modify_report_data(gl_entries) + report_data = modify_report_data(gl_entries) + summary = None + if filters['mode'] == 'run' and filters['report_name'] != 'Tax Detail': + report_data, summary = run_report(filters['report_name'], report_data) - return get_columns(fieldlist), gl_entries + # return columns, data, message, chart, report_summary + return get_columns(fieldlist), report_data, None, None, summary +def run_report(report_name, data): + "Applies the sections and filters saved in the custom report" + report_config = json.loads(frappe.get_doc('Report', report_name).json) + # Columns indexed from 1 wrt colno + columns = report_config.get('columns') + sections = report_config.get('sections', {}) + show_detail = report_config.get('show_detail', 1) + new_data = [] + summary = [] + for section_name, section in sections.items(): + section_total = 0.0 + for filt_name, filt in section.items(): + value_field = filt['fieldname'] + rmidxs = [] + for colno, filter_string in filt['filters'].items(): + filter_field = columns[int(colno) - 1]['fieldname'] + for i, row in enumerate(data): + if not filter_match(row[filter_field], filter_string): + rmidxs += [i] + rows = [row for i, row in enumerate(data) if i not in rmidxs] + section_total += subtotal(rows, value_field) + if show_detail: new_data += rows + new_data += [ {columns[1]['fieldname']: section_name, columns[2]['fieldname']: section_total} ] + summary += [ {'label': section_name, 'datatype': 'Currency', 'value': section_total} ] + if show_detail: new_data += [ {} ] + return new_data if new_data else data, summary + +def filter_match(value, string): + "Approximation to datatable filters" + import datetime + if string == '': return True + if value is None: value = -999999999999999 + elif isinstance(value, datetime.date): return True + + if isinstance(value, str): + value = value.lower() + string = string.lower() + if string[0] == '<': return True if string[1:].strip() else False + elif string[0] == '>': return False if string[1:].strip() else True + elif string[0] == '=': return string[1:] in value if string[1:] else False + elif string[0:2] == '!=': return string[2:] not in value + elif len(string.split(':')) == 2: + pre, post = string.split(':') + return (True if not pre.strip() and post.strip() in value else False) + else: + return string in value + else: + if string[0] in ['<', '>', '=']: + operator = string[0] + if operator == '=': operator = '==' + string = string[1:].strip() + elif string[0:2] == '!=': + operator = '!=' + string = string[2:].strip() + elif len(string.split(':')) == 2: + pre, post = string.split(':') + try: + return (True if float(pre) <= value and float(post) >= value else False) + except ValueError: + return (False if pre.strip() else True) + else: + return string in str(value) + + try: + num = float(string) if string.strip() else 0 + return eval(f'{value} {operator} {num}') + except ValueError: + if operator == '<': return True + return False + +def subtotal(data, field): + subtotal = 0.0 + for row in data: + subtotal += row[field] + return subtotal abbrev = lambda dt: ''.join(l[0].lower() for l in dt.split(' ')) + '.' doclist = lambda dt, dfs: [abbrev(dt) + f for f in dfs] @@ -148,24 +227,18 @@ def get_custom_reports(name=None): return reports_dict @frappe.whitelist() -def save_custom_report(reference_report, report_name, columns, sections): - import pymysql +def save_custom_report(reference_report, report_name, data): if reference_report != 'Tax Detail': frappe.throw(_("The wrong report is referenced.")) if report_name == 'Tax Detail': frappe.throw(_("The parent report cannot be overwritten.")) - data = { - 'columns': json.loads(columns), - 'sections': json.loads(sections) - } - doc = { 'doctype': 'Report', 'report_name': report_name, 'is_standard': 'No', 'module': 'Accounts', - 'json': json.dumps(data, separators=(',', ':')) + 'json': data } doc.update(custom_report_dict) @@ -173,7 +246,7 @@ def save_custom_report(reference_report, report_name, columns, sections): newdoc = frappe.get_doc(doc) newdoc.insert() frappe.msgprint(_("Report created successfully")) - except (frappe.exceptions.DuplicateEntryError, pymysql.err.IntegrityError): + except frappe.exceptions.DuplicateEntryError: dbdoc = frappe.get_doc('Report', report_name) dbdoc.update(doc) dbdoc.save() diff --git a/erpnext/accounts/report/tax_detail/test_tax_detail.py b/erpnext/accounts/report/tax_detail/test_tax_detail.py new file mode 100644 index 0000000000..dfd8d9e121 --- /dev/null +++ b/erpnext/accounts/report/tax_detail/test_tax_detail.py @@ -0,0 +1,67 @@ +from __future__ import unicode_literals + +import frappe, unittest, datetime +from frappe.utils import getdate +from .tax_detail import execute, filter_match + +class TestTaxDetail(unittest.TestCase): + def setup(self): + pass + + def test_filter_match(self): + # None - treated as -inf number except range + self.assertTrue(filter_match(None, '!=')) + self.assertTrue(filter_match(None, '<')) + self.assertTrue(filter_match(None, '3.4')) + self.assertFalse(filter_match(None, ' <')) + self.assertFalse(filter_match(None, 'ew')) + self.assertFalse(filter_match(None, ' ')) + self.assertFalse(filter_match(None, ' f :')) + + # Numbers + self.assertTrue(filter_match(3.4, '3.4')) + self.assertTrue(filter_match(3.4, '.4')) + self.assertTrue(filter_match(3.4, '3')) + self.assertTrue(filter_match(-3.4, '< -3')) + self.assertTrue(filter_match(-3.4, '> -4')) + self.assertTrue(filter_match(3.4, '= 3.4 ')) + self.assertTrue(filter_match(3.4, '!=4.5')) + self.assertTrue(filter_match(3.4, ' 3 : 4 ')) + self.assertTrue(filter_match(0.0, ' : ')) + self.assertFalse(filter_match(3.4, '=4.5')) + self.assertFalse(filter_match(3.4, ' = 3.4 ')) + self.assertFalse(filter_match(3.4, '!=3.4')) + self.assertFalse(filter_match(3.4, '>6')) + self.assertFalse(filter_match(3.4, '<-4.5')) + self.assertFalse(filter_match(3.4, '4.5')) + self.assertFalse(filter_match(3.4, '5:9')) + + # Strings + self.assertTrue(filter_match('ACC-SINV-2021-00001', 'SINV')) + self.assertTrue(filter_match('ACC-SINV-2021-00001', 'sinv')) + self.assertTrue(filter_match('ACC-SINV-2021-00001', '-2021')) + self.assertTrue(filter_match(' ACC-SINV-2021-00001', ' acc')) + self.assertTrue(filter_match('ACC-SINV-2021-00001', '=2021')) + self.assertTrue(filter_match('ACC-SINV-2021-00001', '!=zz')) + self.assertTrue(filter_match('ACC-SINV-2021-00001', '< zzz ')) + self.assertTrue(filter_match('ACC-SINV-2021-00001', ' : sinv ')) + self.assertFalse(filter_match('ACC-SINV-2021-00001', ' sinv :')) + self.assertFalse(filter_match('ACC-SINV-2021-00001', ' acc')) + self.assertFalse(filter_match('ACC-SINV-2021-00001', '= 2021 ')) + self.assertFalse(filter_match('ACC-SINV-2021-00001', '!=sinv')) + self.assertFalse(filter_match('ACC-SINV-2021-00001', ' >')) + self.assertFalse(filter_match('ACC-SINV-2021-00001', '>aa')) + self.assertFalse(filter_match('ACC-SINV-2021-00001', ' <')) + self.assertFalse(filter_match('ACC-SINV-2021-00001', '< ')) + self.assertFalse(filter_match('ACC-SINV-2021-00001', ' =')) + self.assertFalse(filter_match('ACC-SINV-2021-00001', '=')) + + # Date - always match + self.assertTrue(filter_match(datetime.date(2021, 3, 19), ' kdsjkldfs ')) From 8e413651c2b4cfc9c7754a482f353996393cd507 Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Wed, 24 Mar 2021 02:56:30 +0000 Subject: [PATCH 05/22] fix: major refactor to monkey-patch into the QueryReport class --- .../accounts/report/tax_detail/tax_detail.js | 300 +++++++++--------- 1 file changed, 153 insertions(+), 147 deletions(-) diff --git a/erpnext/accounts/report/tax_detail/tax_detail.js b/erpnext/accounts/report/tax_detail/tax_detail.js index 8cdce54852..6049000404 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.js +++ b/erpnext/accounts/report/tax_detail/tax_detail.js @@ -3,6 +3,8 @@ // Contributed by Case Solved and sponsored by Nulight Studios /* eslint-disable */ +frappe.provide('frappe.query_reports'); + frappe.query_reports["Tax Detail"] = { filters: [ { @@ -50,83 +52,124 @@ frappe.query_reports["Tax Detail"] = { // Remove Add Column and Save from menu report.page.add_inner_button(__("New Report"), () => new_report(), __("Custom Report")); report.page.add_inner_button(__("Load Report"), () => load_report(), __("Custom Report")); - hide_filters(); - }, - after_datatable_render: (datatable) => { - if (frappe.query_report.report_name == 'Tax Detail') { - return; - } - if (this.taxreport) { - this.taxreport.load_report(); - } else { - this.taxreport = new TaxReport(); - } + hide_filters(report); } }; -function hide_filters() { - frappe.query_report.page.page_form[0].querySelectorAll('.form-group.frappe-control').forEach(function setHidden(field) { +function hide_filters(report) { + report.page.page_form[0].querySelectorAll('.form-group.frappe-control').forEach(function setHidden(field) { if (field.dataset.fieldtype == "Read Only") { field.classList.add("hidden"); } }); } -class TaxReport { - // construct after datatable is loaded +erpnext.TaxDetail = class TaxDetail { constructor() { - this.qr = frappe.query_report; - this.page = frappe.query_report.page; - this.create_controls(); + this.patch(); this.load_report(); } - load_report() { - if (this.loaded) { - return; + // Monkey patch the QueryReport class + patch() { + this.qr = frappe.query_report; + this.super = { + refresh_report: this.qr.refresh_report, + show_footer_message: this.qr.show_footer_message } - const report_name = this.qr.report_name; - this.report_name.value = report_name; - frappe.call({ - method: 'erpnext.accounts.report.tax_detail.tax_detail.get_custom_reports', - args: {name: report_name}, - freeze: true - }).then((r) => { - const data = JSON.parse(r.message[report_name]['json']); - this.sections = data.sections || {}; - this.controls['show_detail'].set_input(data.show_detail); - this.set_section(); - }) - this.loaded = 1; + this.qr.refresh_report = () => this.refresh_report(); + this.qr.show_footer_message = () => this.show_footer_message(); + } + show_footer_message() { + // The last thing to run after datatable_render in refresh() + console.log('show_footer_message'); + this.super.show_footer_message.apply(this.qr); + if (this.qr.report_name !== 'Tax Detail') { + this.set_value_options(); + this.show_help(); + if (this.loading) { + this.set_section(''); + } + this.reload_filter(); + } + this.loading = false; + } + refresh_report() { + // Infrequent report build (onload), load filters & data + // super function runs a refresh() serially + // already run within frappe.run_serially + console.log('refresh_report'); + this.loading = true; + this.super.refresh_report.apply(this.qr); + if (this.qr.report_name !== 'Tax Detail') { + frappe.call({ + method: 'erpnext.accounts.report.tax_detail.tax_detail.get_custom_reports', + args: {name: this.qr.report_name} + }).then((r) => { + const data = JSON.parse(r.message[this.qr.report_name]['json']); + this.create_controls(); + this.sections = data.sections || {}; + this.controls['show_detail'].set_input(data.show_detail); + }); + } + } + load_report() { + // One-off report build like titles, menu, etc + // Run when this object is created which happens in qr.load_report + console.log('load_report'); + this.qr.menu_items = this.get_menu_items(); + } + get_menu_items() { + // Replace save button + let new_items = []; + const label = __('Save'); + + for (let item of this.qr.menu_items) { + if (item.label === label) { + new_items.push({ + label: label, + action: this.save_report, + standard: false + }); + } else { + new_items.push(item); + } + } + return new_items; } save_report() { - frappe.call({ - method:'erpnext.accounts.report.tax_detail.tax_detail.save_custom_report', - args: { - reference_report: 'Tax Detail', - report_name: this.qr.report_name, - data: { - columns: this.qr.get_visible_columns(), - sections: this.sections, - show_detail: this.controls['show_detail'].get_input_value() - } - }, - freeze: true - }).then((r) => { - this.reload(); - }); + if (this.qr.report_name !== 'Tax Detail') { + frappe.call({ + method:'erpnext.accounts.report.tax_detail.tax_detail.save_custom_report', + args: { + reference_report: 'Tax Detail', + report_name: this.qr.report_name, + data: { + columns: this.qr.get_visible_columns(), + sections: this.sections, + show_detail: this.controls['show_detail'].get_input_value() + } + }, + freeze: true + }).then((r) => { + this.set_section(''); + }); + } } set_value_options() { - this.fieldname_lookup = {}; - this.label_lookup = {}; - this.qr.columns.forEach((col, index) => { - if (col['fieldtype'] == "Currency") { - this.fieldname_lookup[col['label']] = col['fieldname']; - this.label_lookup[col['fieldname']] = col['label']; - } - }); - const options = Object.keys(this.fieldname_lookup); - this.controls['value_field'].$wrapper.find("select").empty().add_options(options); - this.controls['value_field'].set_input(options[0]); + // May be run with no columns or data + if (this.qr.columns) { + this.fieldname_lookup = {}; + this.label_lookup = {}; + this.qr.columns.forEach((col, index) => { + if (col['fieldtype'] == "Currency") { + this.fieldname_lookup[col['label']] = col['fieldname']; + this.label_lookup[col['fieldname']] = col['label']; + } + }); + const options = Object.keys(this.fieldname_lookup); + this.controls['value_field'].$wrapper.find("select").empty().add_options(options); + this.controls['value_field'].set_input(options[0]); + } } set_value_label_from_filter() { const section_name = this.controls['section_name'].value; @@ -158,46 +201,38 @@ class TaxReport { }); dialog.show(); } - get_filter_controls() { - this.qr.filters.forEach(filter => { - if (filter['fieldname'] == 'mode') { - this.mode = filter; - } - if (filter['fieldname'] == 'report_name') { - this.report_name = filter; - } - }); - } - set_mode(mode) { - this.mode.value = mode; - } - edit_mode() { - return this.mode.value == 'edit'; - } set_section(name) { + // Sets the given section name and then reloads the data if (name && !this.sections[name]) { this.sections[name] = {}; } let options = Object.keys(this.sections); options.unshift(''); this.controls['section_name'].$wrapper.find("select").empty().add_options(options); + const org_mode = this.qr.get_filter_value('mode'); + let refresh = false; if (name) { this.controls['section_name'].set_input(name); + this.qr.set_filter_value('mode', 'edit'); + if (org_mode === 'run') { + refresh = true; + } } else { this.controls['section_name'].set_input(''); + this.qr.set_filter_value('mode', 'run'); + if (org_mode === 'edit') { + refresh = true; + } } - if (this.controls['section_name'].value) { - this.set_mode('edit'); - } else { - this.set_mode('run'); + this.reload_filter(); + if (refresh) { + this.qr.refresh(); } - this.controls['filter_index'].set_input(''); - this.reload(); } reload_filter() { - const section_name = this.controls['section_name'].value; + const section_name = this.controls['section_name'].get_input_value(); if (section_name) { - let fidx = this.controls['filter_index'].value; + let fidx = this.controls['filter_index'].get_input_value(); let section = this.sections[section_name]; let fidxs = Object.keys(section); fidxs.unshift(''); @@ -207,17 +242,16 @@ class TaxReport { this.controls['filter_index'].$wrapper.find("select").empty(); this.controls['filter_index'].set_input(''); } - this.set_filters(); + this.set_table_filters(); } - set_filters() { + set_table_filters() { let filters = {}; - const section_name = this.controls['section_name'].value; - const fidx = this.controls['filter_index'].value; + const section_name = this.controls['section_name'].get_input_value(); + const fidx = this.controls['filter_index'].get_input_value(); if (section_name && fidx) { filters = this.sections[section_name][fidx]['filters']; } this.setAppliedFilters(filters); - this.qr.datatable.columnmanager.applyFilter(filters); this.set_value_label_from_filter(); } setAppliedFilters(filters) { @@ -229,32 +263,20 @@ class TaxReport { input.value = null; } }); - } - reload() { - // Reloads the data. When the datatable is reloaded, load_report() - // will be run by the after_datatable_render event. - // TODO: why does this trigger multiple reloads? - this.qr.refresh(); - this.show_help(); - if (this.edit_mode()) { - this.reload_filter(); - } else { - this.controls['filter_index'].$wrapper.find("select").empty(); - } + this.qr.datatable.columnmanager.applyFilter(filters); } delete(name, type) { if (type === 'section') { delete this.sections[name]; - this.controls['section_name'].$wrapper.find("select").empty().add_options(Object.keys(this.sections)); - this.controls['section_name'].set_input(Object.keys(this.sections)[0] || ''); - this.controls['filter_index'].set_input(''); + const new_section = Object.keys(this.sections)[0] || ''; + this.set_section(new_section); } if (type === 'filter') { - let cur_section = this.controls['section_name'].value; + const cur_section = this.controls['section_name'].get_input_value(); delete this.sections[cur_section][name]; this.controls['filter_index'].set_input(''); + this.reload_filter(); } - this.reload(); } create_controls() { if (this.controls) { @@ -262,7 +284,7 @@ class TaxReport { } let controls = {}; // SELECT in data.js - controls['section_name'] = this.page.add_field({ + controls['section_name'] = this.qr.page.add_field({ label: __('Section'), fieldtype: 'Select', fieldname: 'section_name', @@ -271,7 +293,7 @@ class TaxReport { } }); // BUTTON in button.js - controls['new_section'] = this.page.add_field({ + controls['new_section'] = this.qr.page.add_field({ label: __('New Section'), fieldtype: 'Button', fieldname: 'new_section', @@ -279,33 +301,33 @@ class TaxReport { this.new_section(__('New Section')); } }); - controls['delete_section'] = this.page.add_field({ + controls['delete_section'] = this.qr.page.add_field({ label: __('Delete Section'), fieldtype: 'Button', fieldname: 'delete_section', click: () => { - let cur_section = this.controls['section_name'].value; + let cur_section = this.controls['section_name'].get_input_value(); if (cur_section) { frappe.confirm(__('Are you sure you want to delete section ') + cur_section + '?', () => {this.delete(cur_section, 'section')}); } } }); - controls['filter_index'] = this.page.add_field({ + controls['filter_index'] = this.qr.page.add_field({ label: __('Filter'), fieldtype: 'Select', fieldname: 'filter_index', change: (e) => { this.controls['filter_index'].set_input(this.controls['filter_index'].get_input_value()); - this.set_filters(); + this.set_table_filters(); } }); - controls['add_filter'] = this.page.add_field({ + controls['add_filter'] = this.qr.page.add_field({ label: __('Add Filter'), fieldtype: 'Button', fieldname: 'add_filter', click: () => { - let section_name = this.controls['section_name'].value; + let section_name = this.controls['section_name'].get_input_value(); if (section_name) { let prefix = 'Filter'; let data = { @@ -326,19 +348,19 @@ class TaxReport { } } }); - controls['delete_filter'] = this.page.add_field({ + controls['delete_filter'] = this.qr.page.add_field({ label: __('Delete Filter'), fieldtype: 'Button', fieldname: 'delete_filter', click: () => { - let cur_filter = this.controls['filter_index'].value; + let cur_filter = this.controls['filter_index'].get_input_value(); if (cur_filter) { frappe.confirm(__('Are you sure you want to delete filter ') + cur_filter + '?', () => {this.delete(cur_filter, 'filter')}); } } }); - controls['value_field'] = this.page.add_field({ + controls['value_field'] = this.qr.page.add_field({ label: __('Value Column'), fieldtype: 'Select', fieldname: 'value_field', @@ -346,37 +368,35 @@ class TaxReport { this.controls['value_field'].set_input(this.controls['value_field'].get_input_value()); } }); - controls['save'] = this.page.add_field({ + controls['save'] = this.qr.page.add_field({ label: __('Save & Run'), fieldtype: 'Button', fieldname: 'save', click: () => { - this.controls['section_name'].set_input(''); - this.set_mode('run'); this.save_report(); } }); - controls['show_detail'] = this.page.add_field({ + controls['show_detail'] = this.qr.page.add_field({ label: __('Show Detail'), fieldtype: 'Check', fieldname: 'show_detail', default: 1 }); this.controls = controls; - this.set_value_options(); - this.get_filter_controls(); - this.show_help(); } show_help() { const help = __(`You can add multiple sections to your custom report using the New Section button above. - To specify what data goes in each section, specify column filters below, then save with Add Filter. - Each section can have multiple filters added. - You can specify which Currency column will be summed for each filter in the final report with the Value Column select box. - Once you're done, hit Save & Run.`); - this.qr.show_status(help); + To specify what data goes in each section, specify column filters in the data table, then save with Add Filter. + Each section can have multiple filters added but be careful with the duplicated data rows. + You can specify which Currency column will be summed for each filter in the final report with the Value Column + select box. Once you're done, hit Save & Run.`); + this.qr.$report_footer.append(`
${help}
`); } } +if (!window.taxdetail) { + window.taxdetail = new erpnext.TaxDetail(); +} function get_reports(cb) { frappe.call({ @@ -387,23 +407,9 @@ function get_reports(cb) { }) } -function override_menu() { - //TODO: Replace save button - this.qr.menu_items.forEach((item, idx) => { - if (item['label'] == __('Save')) { - delete this.qr.menu_items[idx]; - } - }) - this.qr.menu_items.push({ - label: __('Save'), - action: this.save_report - }) - this.qr.set_menu_items(); -} - function new_report() { const dialog = new frappe.ui.Dialog({ - title: __("New Report"), + title: __('New Report'), fields: [ { fieldname: 'report_name', @@ -424,7 +430,7 @@ function new_report() { }, freeze: true }).then((r) => { - frappe.set_route("query-report", values.report_name); + frappe.set_route('query-report', values.report_name); }); dialog.hide(); } @@ -435,7 +441,7 @@ function new_report() { function load_report() { get_reports(function load_report_cb(reports) { const dialog = new frappe.ui.Dialog({ - title: __("Load Report"), + title: __('Load Report'), fields: [ { fieldname: 'report_name', @@ -447,7 +453,7 @@ function load_report() { primary_action_label: __('Load'), primary_action: function load_report_pa(values) { dialog.hide(); - frappe.set_route("query-report", values.report_name); + frappe.set_route('query-report', values.report_name); } }); dialog.show(); From 5d9217ab29d2f335b862a06f17f07151c8684051 Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Wed, 24 Mar 2021 04:01:18 +0000 Subject: [PATCH 06/22] fix: minor bugs and improvements --- .../accounts/report/tax_detail/tax_detail.js | 22 +++++++++---------- .../accounts/report/tax_detail/tax_detail.py | 4 +++- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/report/tax_detail/tax_detail.js b/erpnext/accounts/report/tax_detail/tax_detail.js index 6049000404..5da63dec57 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.js +++ b/erpnext/accounts/report/tax_detail/tax_detail.js @@ -81,7 +81,6 @@ erpnext.TaxDetail = class TaxDetail { } show_footer_message() { // The last thing to run after datatable_render in refresh() - console.log('show_footer_message'); this.super.show_footer_message.apply(this.qr); if (this.qr.report_name !== 'Tax Detail') { this.set_value_options(); @@ -97,7 +96,6 @@ erpnext.TaxDetail = class TaxDetail { // Infrequent report build (onload), load filters & data // super function runs a refresh() serially // already run within frappe.run_serially - console.log('refresh_report'); this.loading = true; this.super.refresh_report.apply(this.qr); if (this.qr.report_name !== 'Tax Detail') { @@ -115,21 +113,23 @@ erpnext.TaxDetail = class TaxDetail { load_report() { // One-off report build like titles, menu, etc // Run when this object is created which happens in qr.load_report - console.log('load_report'); this.qr.menu_items = this.get_menu_items(); } get_menu_items() { - // Replace save button + // Replace Save, remove Add Column let new_items = []; - const label = __('Save'); + const save = __('Save'); + const addColumn = __('Add Column'); for (let item of this.qr.menu_items) { - if (item.label === label) { + if (item.label === save) { new_items.push({ - label: label, - action: this.save_report, + label: save, + action: () => this.save_report(), standard: false }); + } else if (item.label === addColumn) { + // Don't add } else { new_items.push(item); } @@ -279,9 +279,6 @@ erpnext.TaxDetail = class TaxDetail { } } create_controls() { - if (this.controls) { - return; - } let controls = {}; // SELECT in data.js controls['section_name'] = this.qr.page.add_field({ @@ -389,7 +386,8 @@ erpnext.TaxDetail = class TaxDetail { To specify what data goes in each section, specify column filters in the data table, then save with Add Filter. Each section can have multiple filters added but be careful with the duplicated data rows. You can specify which Currency column will be summed for each filter in the final report with the Value Column - select box. Once you're done, hit Save & Run.`); + select box. Use the Show Detail box to see the data rows included in each section in the final report. + Once you're done, hit Save & Run.`); this.qr.$report_footer.append(`
${help}
`); } } diff --git a/erpnext/accounts/report/tax_detail/tax_detail.py b/erpnext/accounts/report/tax_detail/tax_detail.py index 6bed89841c..db1bf5b678 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.py +++ b/erpnext/accounts/report/tax_detail/tax_detail.py @@ -6,6 +6,8 @@ from __future__ import unicode_literals import frappe, json from frappe import _ +# NOTE: Not compatible with the frappe custom report feature of adding arbitrary doctype columns to the report + # field lists in multiple doctypes will be coalesced required_sql_fields = { "GL Entry": ["posting_date", "voucher_type", "voucher_no", "account", "account_currency", "debit", "credit"], @@ -87,7 +89,7 @@ def run_report(report_name, data): new_data += [ {columns[1]['fieldname']: section_name, columns[2]['fieldname']: section_total} ] summary += [ {'label': section_name, 'datatype': 'Currency', 'value': section_total} ] if show_detail: new_data += [ {} ] - return new_data if new_data else data, summary + return new_data or data, summary or None def filter_match(value, string): "Approximation to datatable filters" From 3027cc7da61c81b8b99ea672917766e5c0530fc7 Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Wed, 24 Mar 2021 04:30:28 +0000 Subject: [PATCH 07/22] fix: minor bug and tidy --- erpnext/accounts/report/tax_detail/tax_detail.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/tax_detail/tax_detail.js b/erpnext/accounts/report/tax_detail/tax_detail.js index 5da63dec57..391aacf391 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.js +++ b/erpnext/accounts/report/tax_detail/tax_detail.js @@ -172,8 +172,8 @@ erpnext.TaxDetail = class TaxDetail { } } set_value_label_from_filter() { - const section_name = this.controls['section_name'].value; - const fidx = this.controls['filter_index'].value; + const section_name = this.controls['section_name'].get_input_value(); + const fidx = this.controls['filter_index'].get_input_value(); if (section_name && fidx) { const fieldname = this.sections[section_name][fidx]['fieldname']; this.controls['value_field'].set_input(this.label_lookup[fieldname]); @@ -182,7 +182,7 @@ erpnext.TaxDetail = class TaxDetail { } } get_value_fieldname() { - const curlabel = this.controls['value_field'].value; + const curlabel = this.controls['value_field'].get_input_value(); return this.fieldname_lookup[curlabel]; } new_section(label) { @@ -203,6 +203,7 @@ erpnext.TaxDetail = class TaxDetail { } set_section(name) { // Sets the given section name and then reloads the data + this.controls['filter_index'].set_input(''); if (name && !this.sections[name]) { this.sections[name] = {}; } @@ -224,10 +225,10 @@ erpnext.TaxDetail = class TaxDetail { refresh = true; } } - this.reload_filter(); if (refresh) { this.qr.refresh(); } + this.reload_filter(); } reload_filter() { const section_name = this.controls['section_name'].get_input_value(); From ece00287eadf326e68945c4221ee20429306cb9a Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Sat, 27 Mar 2021 03:02:30 +0000 Subject: [PATCH 08/22] Refactor for Journal Entries (payroll) --- .../accounts/report/tax_detail/tax_detail.py | 90 ++++++++++++++----- 1 file changed, 69 insertions(+), 21 deletions(-) diff --git a/erpnext/accounts/report/tax_detail/tax_detail.py b/erpnext/accounts/report/tax_detail/tax_detail.py index db1bf5b678..b08e796807 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.py +++ b/erpnext/accounts/report/tax_detail/tax_detail.py @@ -7,15 +7,18 @@ import frappe, json from frappe import _ # NOTE: Not compatible with the frappe custom report feature of adding arbitrary doctype columns to the report +# NOTE: Payroll is implemented using Journal Entries # field lists in multiple doctypes will be coalesced required_sql_fields = { - "GL Entry": ["posting_date", "voucher_type", "voucher_no", "account", "account_currency", "debit", "credit"], - "Account": ["account_type"], + "GL Entry": ["posting_date", "voucher_type", "voucher_no", "account as tax_account", "account_currency", "debit", "credit"], +# "Account": ["account_type"], + "Journal Entry Account": ["account_type", "account", "debit_in_account_currency", "credit_in_account_currency"], + ("Purchase Invoice Item", "Sales Invoice Item"): ["base_net_amount", "item_tax_rate", "item_tax_template", "item_name"], ("Purchase Invoice", "Sales Invoice"): ["taxes_and_charges", "tax_category"], - ("Purchase Invoice Item", "Sales Invoice Item"): ["item_tax_template", "item_name", "base_net_amount", "item_tax_rate"], # "Journal Entry": ["total_amount_currency"], -# "Journal Entry Account": ["debit_in_account_currency", "credit_in_account_currency"] + "Purchase Invoice Item": ["expense_account"], + "Sales Invoice Item": ["income_account"] } @@ -40,9 +43,9 @@ def execute(filters=None): left join `tabPurchase Invoice Item` pii on pi.name=pii.parent /* left outer join `tabJournal Entry` je on - ge.voucher_no=je.name and ge.company=je.company + ge.voucher_no=je.name and ge.company=je.company */ left outer join `tabJournal Entry Account` jea on - je.name=jea.parent and a.account_type='Tax' */ + ge.voucher_type=jea.parenttype and ge.voucher_no=jea.parent where (ge.voucher_type, ge.voucher_no) in ( select ge.voucher_type, ge.voucher_no from `tabGL Entry` ge @@ -142,7 +145,18 @@ def subtotal(data, field): abbrev = lambda dt: ''.join(l[0].lower() for l in dt.split(' ')) + '.' doclist = lambda dt, dfs: [abbrev(dt) + f for f in dfs] -coalesce = lambda dts, dfs: ['coalesce(' + ', '.join(abbrev(dt) + f for dt in dts) + ') ' + f for f in dfs] + +def as_split(fields): + for field in fields: + split = field.split(' as ') + yield (split[0], split[1] if len(split) > 1 else split[0]) + +def coalesce(doctypes, fields): + coalesce = [] + for name, new_name in as_split(fields): + sharedfields = ', '.join(abbrev(dt) + name for dt in doctypes) + coalesce += [f'coalesce({sharedfields}) as {new_name}'] + return coalesce def get_fieldstr(fieldlist): fields = [] @@ -158,20 +172,22 @@ def get_columns(fieldlist): for doctypes, docfields in fieldlist.items(): if isinstance(doctypes, str): doctypes = [doctypes] + fieldmap = {name: new_name for name, new_name in as_split(docfields)} for doctype in doctypes: meta = frappe.get_meta(doctype) # get column field metadata from the db fieldmeta = {} for field in meta.get('fields'): - if field.fieldname in docfields: - fieldmeta[field.fieldname] = { + if field.fieldname in fieldmap.keys(): + new_name = fieldmap[field.fieldname] + fieldmeta[new_name] = { "label": _(field.label), - "fieldname": field.fieldname, + "fieldname": new_name, "fieldtype": field.fieldtype, "options": field.options } # edit the columns to match the modified data - for field in docfields: + for field in fieldmap.values(): col = modify_report_columns(doctype, field, fieldmeta[field]) if col: columns[col["fieldname"]] = col @@ -182,10 +198,28 @@ def modify_report_columns(doctype, field, column): "Because data is rearranged into other columns" if doctype in ["Sales Invoice Item", "Purchase Invoice Item"] and field == "item_tax_rate": return None + if doctype == "GL Entry" and field == "tax_account": + column.update({"label": _("Tax Account")}) + if doctype == "GL Entry" and field == "debit": + column.update({"label": _("Tax Debit")}) + if doctype == "GL Entry" and field == "credit": + column.update({"label": _("Tax Credit")}) + + if doctype == "Journal Entry Account" and field == "debit_in_account_currency": + column.update({"label": _("Debit Net Amount"), "fieldname": "debit_net_amount"}) + if doctype == "Journal Entry Account" and field == "credit_in_account_currency": + column.update({"label": _("Credit Net Amount"), "fieldname": "credit_net_amount"}) + if doctype == "Sales Invoice Item" and field == "base_net_amount": column.update({"label": _("Credit Net Amount"), "fieldname": "credit_net_amount"}) + if doctype == "Sales Invoice Item" and field == "income_account": + column.update({"label": _("Account"), "fieldname": "account"}) + if doctype == "Purchase Invoice Item" and field == "base_net_amount": column.update({"label": _("Debit Net Amount"), "fieldname": "debit_net_amount"}) + if doctype == "Purchase Invoice Item" and field == "expense_account": + column.update({"label": _("Account"), "fieldname": "account"}) + if field == "taxes_and_charges": column.update({"label": _("Taxes and Charges Template")}) return column @@ -193,16 +227,30 @@ def modify_report_columns(doctype, field, column): def modify_report_data(data): import json for line in data: - if line.account_type == "Tax" and line.item_tax_rate: - tax_rates = json.loads(line.item_tax_rate) - for account, rate in tax_rates.items(): - if account == line.account: - if line.voucher_type == "Sales Invoice": - line.credit = line.base_net_amount * (rate / 100) - line.credit_net_amount = line.base_net_amount - if line.voucher_type == "Purchase Invoice": - line.debit = line.base_net_amount * (rate / 100) - line.debit_net_amount = line.base_net_amount + # Transform Invoice lines + if "Invoice" in line.voucher_type: + if line.income_account: + line.account = line.income_account + line.account_type = "Income Account" + if line.expense_account: + line.account = line.expense_account + line.account_type = "Expense Account" + if line.item_tax_rate: + tax_rates = json.loads(line.item_tax_rate) + for account, rate in tax_rates.items(): + if account == line.account: + if line.voucher_type == "Sales Invoice": + line.credit = line.base_net_amount * (rate / 100) + line.credit_net_amount = line.base_net_amount + if line.voucher_type == "Purchase Invoice": + line.debit = line.base_net_amount * (rate / 100) + line.debit_net_amount = line.base_net_amount + # Transform Journal Entry lines + if "Journal" in line.voucher_type: + if line.debit_in_account_currency: + line.debit_net_amount = line.debit_in_account_currency + if line.credit_in_account_currency: + line.credit_net_amount = line.credit_in_account_currency return data ####### JS client utilities From 442a0de09496a843c33f24e98c59ffa0ec5138df Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Sat, 27 Mar 2021 04:02:59 +0000 Subject: [PATCH 09/22] fix: finalise query, fix bugs, put Add Columns back --- .../accounts/report/tax_detail/tax_detail.js | 12 +++---- .../accounts/report/tax_detail/tax_detail.py | 31 +++++++------------ 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/erpnext/accounts/report/tax_detail/tax_detail.js b/erpnext/accounts/report/tax_detail/tax_detail.js index 391aacf391..56694fbec2 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.js +++ b/erpnext/accounts/report/tax_detail/tax_detail.js @@ -116,10 +116,9 @@ erpnext.TaxDetail = class TaxDetail { this.qr.menu_items = this.get_menu_items(); } get_menu_items() { - // Replace Save, remove Add Column + // Replace Save action let new_items = []; const save = __('Save'); - const addColumn = __('Add Column'); for (let item of this.qr.menu_items) { if (item.label === save) { @@ -128,8 +127,6 @@ erpnext.TaxDetail = class TaxDetail { action: () => this.save_report(), standard: false }); - } else if (item.label === addColumn) { - // Don't add } else { new_items.push(item); } @@ -424,8 +421,11 @@ function new_report() { args: { reference_report: 'Tax Detail', report_name: values.report_name, - columns: frappe.query_report.get_visible_columns(), - sections: {} + data: { + columns: [], + sections: {}, + show_detail: 1 + } }, freeze: true }).then((r) => { diff --git a/erpnext/accounts/report/tax_detail/tax_detail.py b/erpnext/accounts/report/tax_detail/tax_detail.py index b08e796807..c4ec1374ce 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.py +++ b/erpnext/accounts/report/tax_detail/tax_detail.py @@ -6,17 +6,14 @@ from __future__ import unicode_literals import frappe, json from frappe import _ -# NOTE: Not compatible with the frappe custom report feature of adding arbitrary doctype columns to the report # NOTE: Payroll is implemented using Journal Entries # field lists in multiple doctypes will be coalesced required_sql_fields = { "GL Entry": ["posting_date", "voucher_type", "voucher_no", "account as tax_account", "account_currency", "debit", "credit"], -# "Account": ["account_type"], "Journal Entry Account": ["account_type", "account", "debit_in_account_currency", "credit_in_account_currency"], ("Purchase Invoice Item", "Sales Invoice Item"): ["base_net_amount", "item_tax_rate", "item_tax_template", "item_name"], ("Purchase Invoice", "Sales Invoice"): ["taxes_and_charges", "tax_category"], -# "Journal Entry": ["total_amount_currency"], "Purchase Invoice Item": ["expense_account"], "Sales Invoice Item": ["income_account"] } @@ -35,27 +32,20 @@ def execute(filters=None): inner join `tabAccount` a on ge.account=a.name and ge.company=a.company left join `tabSales Invoice` si on - a.account_type='Tax' and ge.company=si.company and ge.voucher_type='Sales Invoice' and ge.voucher_no=si.name + ge.company=si.company and ge.voucher_type='Sales Invoice' and ge.voucher_no=si.name left join `tabSales Invoice Item` sii on si.name=sii.parent left join `tabPurchase Invoice` pi on - a.account_type='Tax' and ge.company=pi.company and ge.voucher_type='Purchase Invoice' and ge.voucher_no=pi.name + ge.company=pi.company and ge.voucher_type='Purchase Invoice' and ge.voucher_no=pi.name left join `tabPurchase Invoice Item` pii on pi.name=pii.parent -/* left outer join `tabJournal Entry` je on - ge.voucher_no=je.name and ge.company=je.company */ - left outer join `tabJournal Entry Account` jea on + left join `tabJournal Entry Account` jea on ge.voucher_type=jea.parenttype and ge.voucher_no=jea.parent - where (ge.voucher_type, ge.voucher_no) in ( - select ge.voucher_type, ge.voucher_no - from `tabGL Entry` ge - join `tabAccount` a on ge.account=a.name and ge.company=a.company - where - a.account_type='Tax' and - ge.company=%(company)s and - ge.posting_date>=%(from_date)s and - ge.posting_date<=%(to_date)s - ) + where + a.account_type='Tax' and + ge.company=%(company)s and + ge.posting_date>=%(from_date)s and + ge.posting_date<=%(to_date)s order by ge.posting_date, ge.voucher_no """.format(fieldstr=fieldstr), filters, as_dict=1) @@ -238,7 +228,7 @@ def modify_report_data(data): if line.item_tax_rate: tax_rates = json.loads(line.item_tax_rate) for account, rate in tax_rates.items(): - if account == line.account: + if account == line.tax_account: if line.voucher_type == "Sales Invoice": line.credit = line.base_net_amount * (rate / 100) line.credit_net_amount = line.base_net_amount @@ -247,6 +237,9 @@ def modify_report_data(data): line.debit_net_amount = line.base_net_amount # Transform Journal Entry lines if "Journal" in line.voucher_type: + if line.account_type != 'Tax': + line.debit = 0.0 + line.credit = 0.0 if line.debit_in_account_currency: line.debit_net_amount = line.debit_in_account_currency if line.credit_in_account_currency: From 1c37390899724d152a2152d50e2e0c543368471e Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Tue, 30 Mar 2021 17:03:16 +0000 Subject: [PATCH 10/22] fix: Change & simplify query to cater for zero rate tax entries --- .../accounts/report/tax_detail/tax_detail.py | 94 ++++++------------- 1 file changed, 31 insertions(+), 63 deletions(-) diff --git a/erpnext/accounts/report/tax_detail/tax_detail.py b/erpnext/accounts/report/tax_detail/tax_detail.py index c4ec1374ce..1f4d1ba8a0 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.py +++ b/erpnext/accounts/report/tax_detail/tax_detail.py @@ -6,16 +6,15 @@ from __future__ import unicode_literals import frappe, json from frappe import _ -# NOTE: Payroll is implemented using Journal Entries +# NOTE: Payroll is implemented using Journal Entries which translate directly to GL Entries # field lists in multiple doctypes will be coalesced required_sql_fields = { - "GL Entry": ["posting_date", "voucher_type", "voucher_no", "account as tax_account", "account_currency", "debit", "credit"], - "Journal Entry Account": ["account_type", "account", "debit_in_account_currency", "credit_in_account_currency"], - ("Purchase Invoice Item", "Sales Invoice Item"): ["base_net_amount", "item_tax_rate", "item_tax_template", "item_name"], + ("GL Entry", 1): ["posting_date"], + ("Account",): ["account_type"], + ("GL Entry", 2): ["account", "voucher_type", "voucher_no", "debit", "credit"], + ("Purchase Invoice Item", "Sales Invoice Item"): ["base_net_amount", "item_tax_rate", "item_tax_template", "item_group", "item_name"], ("Purchase Invoice", "Sales Invoice"): ["taxes_and_charges", "tax_category"], - "Purchase Invoice Item": ["expense_account"], - "Sales Invoice Item": ["income_account"] } @@ -34,15 +33,12 @@ def execute(filters=None): left join `tabSales Invoice` si on ge.company=si.company and ge.voucher_type='Sales Invoice' and ge.voucher_no=si.name left join `tabSales Invoice Item` sii on - si.name=sii.parent + a.root_type='Income' and si.name=sii.parent left join `tabPurchase Invoice` pi on ge.company=pi.company and ge.voucher_type='Purchase Invoice' and ge.voucher_no=pi.name left join `tabPurchase Invoice Item` pii on - pi.name=pii.parent - left join `tabJournal Entry Account` jea on - ge.voucher_type=jea.parenttype and ge.voucher_no=jea.parent + a.root_type='Expense' and pi.name=pii.parent where - a.account_type='Tax' and ge.company=%(company)s and ge.posting_date>=%(from_date)s and ge.posting_date<=%(to_date)s @@ -151,19 +147,18 @@ def coalesce(doctypes, fields): def get_fieldstr(fieldlist): fields = [] for doctypes, docfields in fieldlist.items(): - if isinstance(doctypes, str): - fields += doclist(doctypes, docfields) - if isinstance(doctypes, tuple): + if len(doctypes) == 1 or isinstance(doctypes[1], int): + fields += doclist(doctypes[0], docfields) + else: fields += coalesce(doctypes, docfields) return ', '.join(fields) def get_columns(fieldlist): columns = {} for doctypes, docfields in fieldlist.items(): - if isinstance(doctypes, str): - doctypes = [doctypes] fieldmap = {name: new_name for name, new_name in as_split(docfields)} for doctype in doctypes: + if isinstance(doctype, int): break meta = frappe.get_meta(doctype) # get column field metadata from the db fieldmeta = {} @@ -186,29 +181,9 @@ def get_columns(fieldlist): def modify_report_columns(doctype, field, column): "Because data is rearranged into other columns" - if doctype in ["Sales Invoice Item", "Purchase Invoice Item"] and field == "item_tax_rate": - return None - if doctype == "GL Entry" and field == "tax_account": - column.update({"label": _("Tax Account")}) - if doctype == "GL Entry" and field == "debit": - column.update({"label": _("Tax Debit")}) - if doctype == "GL Entry" and field == "credit": - column.update({"label": _("Tax Credit")}) - - if doctype == "Journal Entry Account" and field == "debit_in_account_currency": - column.update({"label": _("Debit Net Amount"), "fieldname": "debit_net_amount"}) - if doctype == "Journal Entry Account" and field == "credit_in_account_currency": - column.update({"label": _("Credit Net Amount"), "fieldname": "credit_net_amount"}) - - if doctype == "Sales Invoice Item" and field == "base_net_amount": - column.update({"label": _("Credit Net Amount"), "fieldname": "credit_net_amount"}) - if doctype == "Sales Invoice Item" and field == "income_account": - column.update({"label": _("Account"), "fieldname": "account"}) - - if doctype == "Purchase Invoice Item" and field == "base_net_amount": - column.update({"label": _("Debit Net Amount"), "fieldname": "debit_net_amount"}) - if doctype == "Purchase Invoice Item" and field == "expense_account": - column.update({"label": _("Account"), "fieldname": "account"}) + if doctype in ["Sales Invoice Item", "Purchase Invoice Item"]: + if field in ["item_tax_rate", "base_net_amount"]: + return None if field == "taxes_and_charges": column.update({"label": _("Taxes and Charges Template")}) @@ -216,35 +191,28 @@ def modify_report_columns(doctype, field, column): def modify_report_data(data): import json + new_data = [] for line in data: - # Transform Invoice lines + # Remove Invoice GL Tax Entries and generate Tax entries from the invoice lines if "Invoice" in line.voucher_type: - if line.income_account: - line.account = line.income_account - line.account_type = "Income Account" - if line.expense_account: - line.account = line.expense_account - line.account_type = "Expense Account" + if line.account_type != "Tax": + new_data += [line] if line.item_tax_rate: tax_rates = json.loads(line.item_tax_rate) for account, rate in tax_rates.items(): - if account == line.tax_account: - if line.voucher_type == "Sales Invoice": - line.credit = line.base_net_amount * (rate / 100) - line.credit_net_amount = line.base_net_amount - if line.voucher_type == "Purchase Invoice": - line.debit = line.base_net_amount * (rate / 100) - line.debit_net_amount = line.base_net_amount - # Transform Journal Entry lines - if "Journal" in line.voucher_type: - if line.account_type != 'Tax': - line.debit = 0.0 - line.credit = 0.0 - if line.debit_in_account_currency: - line.debit_net_amount = line.debit_in_account_currency - if line.credit_in_account_currency: - line.credit_net_amount = line.credit_in_account_currency - return data + tax_line = line.copy() + tax_line.account_type = "Tax" + tax_line.account = account + if line.voucher_type == "Sales Invoice": + line.credit = line.base_net_amount + tax_line.credit = line.base_net_amount * (rate / 100) + if line.voucher_type == "Purchase Invoice": + line.debit = line.base_net_amount + tax_line.debit = line.base_net_amount * (rate / 100) + new_data += [tax_line] + else: + new_data += [line] + return new_data ####### JS client utilities From 2cb0da8780cedb4b13d76a40766427f9f2632e8d Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Thu, 1 Apr 2021 22:31:24 +0000 Subject: [PATCH 11/22] fix: rewrite to allow referring to existing sections and reduce to single amount column --- .../accounts/report/tax_detail/tax_detail.js | 235 +++++++++--------- .../accounts/report/tax_detail/tax_detail.py | 60 +++-- 2 files changed, 153 insertions(+), 142 deletions(-) diff --git a/erpnext/accounts/report/tax_detail/tax_detail.js b/erpnext/accounts/report/tax_detail/tax_detail.js index 56694fbec2..0c0397ab04 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.js +++ b/erpnext/accounts/report/tax_detail/tax_detail.js @@ -43,7 +43,7 @@ frappe.query_reports["Tax Detail"] = { fieldname: "mode", label: __("Mode"), fieldtype: "Read Only", - default: "run", + default: "edit", hidden: 1, reqd: 1 } @@ -83,12 +83,12 @@ erpnext.TaxDetail = class TaxDetail { // The last thing to run after datatable_render in refresh() this.super.show_footer_message.apply(this.qr); if (this.qr.report_name !== 'Tax Detail') { - this.set_value_options(); this.show_help(); if (this.loading) { this.set_section(''); + } else { + this.reload_component(''); } - this.reload_filter(); } this.loading = false; } @@ -134,6 +134,7 @@ erpnext.TaxDetail = class TaxDetail { return new_items; } save_report() { + this.check_datatable(); if (this.qr.report_name !== 'Tax Detail') { frappe.call({ method:'erpnext.accounts.report.tax_detail.tax_detail.save_custom_report', @@ -152,55 +153,13 @@ erpnext.TaxDetail = class TaxDetail { }); } } - set_value_options() { - // May be run with no columns or data - if (this.qr.columns) { - this.fieldname_lookup = {}; - this.label_lookup = {}; - this.qr.columns.forEach((col, index) => { - if (col['fieldtype'] == "Currency") { - this.fieldname_lookup[col['label']] = col['fieldname']; - this.label_lookup[col['fieldname']] = col['label']; - } - }); - const options = Object.keys(this.fieldname_lookup); - this.controls['value_field'].$wrapper.find("select").empty().add_options(options); - this.controls['value_field'].set_input(options[0]); + check_datatable() { + if (!this.qr.datatable) { + frappe.throw(__('Please change the date range to load data first')); } } - set_value_label_from_filter() { - const section_name = this.controls['section_name'].get_input_value(); - const fidx = this.controls['filter_index'].get_input_value(); - if (section_name && fidx) { - const fieldname = this.sections[section_name][fidx]['fieldname']; - this.controls['value_field'].set_input(this.label_lookup[fieldname]); - } else { - this.controls['value_field'].set_input(Object.keys(this.fieldname_lookup)[0]); - } - } - get_value_fieldname() { - const curlabel = this.controls['value_field'].get_input_value(); - return this.fieldname_lookup[curlabel]; - } - new_section(label) { - const dialog = new frappe.ui.Dialog({ - title: label, - fields: [{ - fieldname: 'data', - label: label, - fieldtype: 'Data' - }], - primary_action_label: label, - primary_action: (values) => { - dialog.hide(); - this.set_section(values.data); - } - }); - dialog.show(); - } set_section(name) { // Sets the given section name and then reloads the data - this.controls['filter_index'].set_input(''); if (name && !this.sections[name]) { this.sections[name] = {}; } @@ -225,43 +184,49 @@ erpnext.TaxDetail = class TaxDetail { if (refresh) { this.qr.refresh(); } - this.reload_filter(); + this.reload_component(''); } - reload_filter() { + reload_component(component_name) { const section_name = this.controls['section_name'].get_input_value(); if (section_name) { - let fidx = this.controls['filter_index'].get_input_value(); - let section = this.sections[section_name]; - let fidxs = Object.keys(section); - fidxs.unshift(''); - this.controls['filter_index'].$wrapper.find("select").empty().add_options(fidxs); - this.controls['filter_index'].set_input(fidx); + const section = this.sections[section_name]; + const component_names = Object.keys(section); + component_names.unshift(''); + this.controls['component'].$wrapper.find("select").empty().add_options(component_names); + this.controls['component'].set_input(component_name); + if (component_name) { + this.controls['component_type'].set_input(section[component_name].type); + } } else { - this.controls['filter_index'].$wrapper.find("select").empty(); - this.controls['filter_index'].set_input(''); + this.controls['component'].$wrapper.find("select").empty(); + this.controls['component'].set_input(''); } this.set_table_filters(); } set_table_filters() { let filters = {}; const section_name = this.controls['section_name'].get_input_value(); - const fidx = this.controls['filter_index'].get_input_value(); - if (section_name && fidx) { - filters = this.sections[section_name][fidx]['filters']; + const component_name = this.controls['component'].get_input_value(); + if (section_name && component_name) { + const component_type = this.sections[section_name][component_name].type; + if (component_type === 'filter') { + filters = this.sections[section_name][component_name]['filters']; + } } this.setAppliedFilters(filters); - this.set_value_label_from_filter(); } setAppliedFilters(filters) { - Array.from(this.qr.datatable.header.querySelectorAll('.dt-filter')).map(function setFilters(input) { - let idx = input.dataset.colIndex; - if (filters[idx]) { - input.value = filters[idx]; - } else { - input.value = null; - } - }); - this.qr.datatable.columnmanager.applyFilter(filters); + if (this.qr.datatable) { + Array.from(this.qr.datatable.header.querySelectorAll('.dt-filter')).map(function setFilters(input) { + let idx = input.dataset.colIndex; + if (filters[idx]) { + input.value = filters[idx]; + } else { + input.value = null; + } + }); + this.qr.datatable.columnmanager.applyFilter(filters); + } } delete(name, type) { if (type === 'section') { @@ -269,11 +234,10 @@ erpnext.TaxDetail = class TaxDetail { const new_section = Object.keys(this.sections)[0] || ''; this.set_section(new_section); } - if (type === 'filter') { + if (type === 'component') { const cur_section = this.controls['section_name'].get_input_value(); delete this.sections[cur_section][name]; - this.controls['filter_index'].set_input(''); - this.reload_filter(); + this.reload_component(''); } } create_controls() { @@ -293,7 +257,13 @@ erpnext.TaxDetail = class TaxDetail { fieldtype: 'Button', fieldname: 'new_section', click: () => { - this.new_section(__('New Section')); + frappe.prompt({ + label: __('Section Name'), + fieldname: 'name', + fieldtype: 'Data' + }, (values) => { + this.set_section(values.name); + }); } }); controls['delete_section'] = this.qr.page.add_field({ @@ -308,61 +278,87 @@ erpnext.TaxDetail = class TaxDetail { } } }); - controls['filter_index'] = this.qr.page.add_field({ - label: __('Filter'), + controls['component'] = this.qr.page.add_field({ + label: __('Component'), fieldtype: 'Select', - fieldname: 'filter_index', + fieldname: 'component', change: (e) => { - this.controls['filter_index'].set_input(this.controls['filter_index'].get_input_value()); - this.set_table_filters(); + this.reload_component(this.controls['component'].get_input_value()); } }); - controls['add_filter'] = this.qr.page.add_field({ - label: __('Add Filter'), + controls['component_type'] = this.qr.page.add_field({ + label: __('Component Type'), + fieldtype: 'Select', + fieldname: 'component_type', + default: 'filter', + options: [ + {label: __('Filtered Row Subtotal'), value: 'filter'}, + {label: __('Section Subtotal'), value: 'section'} + ] + }); + controls['add_component'] = this.qr.page.add_field({ + label: __('Add Component'), fieldtype: 'Button', - fieldname: 'add_filter', + fieldname: 'add_component', click: () => { + this.check_datatable(); let section_name = this.controls['section_name'].get_input_value(); if (section_name) { - let prefix = 'Filter'; - let data = { - filters: this.qr.datatable.columnmanager.getAppliedFilters(), - fieldname: this.get_value_fieldname() + const component_type = this.controls['component_type'].get_input_value(); + let idx = 0; + const names = Object.keys(this.sections[section_name]); + if (names.length > 0) { + const idxs = names.map((key) => parseInt(key.match(/\d+$/)) || 0); + idx = Math.max(...idxs) + 1; } - const fidxs = Object.keys(this.sections[section_name]); - let new_idx = prefix + '0'; - if (fidxs.length > 0) { - const fiidxs = fidxs.map((key) => parseInt(key.replace(prefix, ''))); - new_idx = prefix + (Math.max(...fiidxs) + 1).toString(); + const filters = this.qr.datatable.columnmanager.getAppliedFilters(); + if (component_type === 'filter') { + const name = 'Filter' + idx.toString(); + let data = { + type: component_type, + filters: filters + } + this.sections[section_name][name] = data; + this.reload_component(name); + } else if (component_type === 'section') { + if (filters && Object.keys(filters).length !== 0) { + frappe.show_alert({ + message: __('Column filters ignored'), + indicator: 'yellow' + }); + } + let data = { + type: component_type + } + frappe.prompt({ + label: __('Section'), + fieldname: 'section', + fieldtype: 'Select', + options: Object.keys(this.sections) + }, (values) => { + this.sections[section_name][values.section] = data; + this.reload_component(values.section); + }); + } else { + frappe.throw(__('Please select the Component Type first')); } - this.sections[section_name][new_idx] = data; - this.controls['filter_index'].set_input(new_idx); - this.reload_filter(); } else { - frappe.throw(__('Please add or select the Section first')); + frappe.throw(__('Please select the Section first')); } } }); - controls['delete_filter'] = this.qr.page.add_field({ - label: __('Delete Filter'), + controls['delete_component'] = this.qr.page.add_field({ + label: __('Delete Component'), fieldtype: 'Button', - fieldname: 'delete_filter', + fieldname: 'delete_component', click: () => { - let cur_filter = this.controls['filter_index'].get_input_value(); - if (cur_filter) { - frappe.confirm(__('Are you sure you want to delete filter ') + cur_filter + '?', - () => {this.delete(cur_filter, 'filter')}); + const component = this.controls['component'].get_input_value(); + if (component) { + frappe.confirm(__('Are you sure you want to delete component ') + component + '?', + () => {this.delete(component, 'component')}); } } }); - controls['value_field'] = this.qr.page.add_field({ - label: __('Value Column'), - fieldtype: 'Select', - fieldname: 'value_field', - change: (e) => { - this.controls['value_field'].set_input(this.controls['value_field'].get_input_value()); - } - }); controls['save'] = this.qr.page.add_field({ label: __('Save & Run'), fieldtype: 'Button', @@ -380,13 +376,16 @@ erpnext.TaxDetail = class TaxDetail { this.controls = controls; } show_help() { - const help = __(`You can add multiple sections to your custom report using the New Section button above. - To specify what data goes in each section, specify column filters in the data table, then save with Add Filter. - Each section can have multiple filters added but be careful with the duplicated data rows. - You can specify which Currency column will be summed for each filter in the final report with the Value Column - select box. Use the Show Detail box to see the data rows included in each section in the final report. - Once you're done, hit Save & Run.`); - this.qr.$report_footer.append(`
${help}
`); + const help = __(`Help: Your custom report is built from General Ledger Entries within the date range. + You can add multiple sections to the report using the New Section button. + Each component added to a section adds a subset of the data into the specified section. + Beware of duplicated data rows. + The Filtered Row component type saves the datatable column filters to specify the added data. + The Section component type refers to the data in a previously defined section, but it cannot refer to its parent section. + The Amount column is summed to give the section subtotal. + Use the Show Detail box to see the data rows included in each section in the final report. + Once finished, hit Save & Run. Report contributed by`); + this.qr.$report_footer.append(``); } } diff --git a/erpnext/accounts/report/tax_detail/tax_detail.py b/erpnext/accounts/report/tax_detail/tax_detail.py index 1f4d1ba8a0..426e8d4aec 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.py +++ b/erpnext/accounts/report/tax_detail/tax_detail.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, json from frappe import _ -# NOTE: Payroll is implemented using Journal Entries which translate directly to GL Entries +# NOTE: Payroll is implemented using Journal Entries which are included as GL Entries # field lists in multiple doctypes will be coalesced required_sql_fields = { @@ -60,23 +60,35 @@ def run_report(report_name, data): columns = report_config.get('columns') sections = report_config.get('sections', {}) show_detail = report_config.get('show_detail', 1) + report = {} new_data = [] summary = [] for section_name, section in sections.items(): - section_total = 0.0 - for filt_name, filt in section.items(): - value_field = filt['fieldname'] - rmidxs = [] - for colno, filter_string in filt['filters'].items(): - filter_field = columns[int(colno) - 1]['fieldname'] - for i, row in enumerate(data): - if not filter_match(row[filter_field], filter_string): - rmidxs += [i] - rows = [row for i, row in enumerate(data) if i not in rmidxs] - section_total += subtotal(rows, value_field) - if show_detail: new_data += rows - new_data += [ {columns[1]['fieldname']: section_name, columns[2]['fieldname']: section_total} ] - summary += [ {'label': section_name, 'datatype': 'Currency', 'value': section_total} ] + report[section_name] = {'rows': [], 'subtotal': 0.0} + for component_name, component in section.items(): + if component['type'] == 'filter': + for row in data: + matched = True + for colno, filter_string in component['filters'].items(): + filter_field = columns[int(colno) - 1]['fieldname'] + if not filter_match(row[filter_field], filter_string): + matched = False + break + if matched: + report[section_name]['rows'] += [row] + report[section_name]['subtotal'] += row['amount'] + if component['type'] == 'section': + if component_name == section_name: + frappe.throw(_("A report component cannot refer to its parent section: ") + section_name) + try: + report[section_name]['rows'] += report[component_name]['rows'] + report[section_name]['subtotal'] += report[component_name]['subtotal'] + except KeyError: + frappe.throw(_("A report component can only refer to an earlier section: ") + section_name) + + if show_detail: new_data += report[section_name]['rows'] + new_data += [ {'voucher_no': section_name, 'amount': report[section_name]['subtotal']} ] + summary += [ {'label': section_name, 'datatype': 'Currency', 'value': report[section_name]['subtotal']} ] if show_detail: new_data += [ {} ] return new_data or data, summary or None @@ -123,11 +135,6 @@ def filter_match(value, string): if operator == '<': return True return False -def subtotal(data, field): - subtotal = 0.0 - for row in data: - subtotal += row[field] - return subtotal abbrev = lambda dt: ''.join(l[0].lower() for l in dt.split(' ')) + '.' doclist = lambda dt, dfs: [abbrev(dt) + f for f in dfs] @@ -185,6 +192,9 @@ def modify_report_columns(doctype, field, column): if field in ["item_tax_rate", "base_net_amount"]: return None + if doctype == "GL Entry" and field in ["debit", "credit"]: + column.update({"label": _("Amount"), "fieldname": "amount"}) + if field == "taxes_and_charges": column.update({"label": _("Taxes and Charges Template")}) return column @@ -193,6 +203,8 @@ def modify_report_data(data): import json new_data = [] for line in data: + if line.debit: line.amount = -line.debit + else: line.amount = line.credit # Remove Invoice GL Tax Entries and generate Tax entries from the invoice lines if "Invoice" in line.voucher_type: if line.account_type != "Tax": @@ -204,11 +216,11 @@ def modify_report_data(data): tax_line.account_type = "Tax" tax_line.account = account if line.voucher_type == "Sales Invoice": - line.credit = line.base_net_amount - tax_line.credit = line.base_net_amount * (rate / 100) + line.amount = line.base_net_amount + tax_line.amount = line.base_net_amount * (rate / 100) if line.voucher_type == "Purchase Invoice": - line.debit = line.base_net_amount - tax_line.debit = line.base_net_amount * (rate / 100) + line.amount = -line.base_net_amount + tax_line.amount = -line.base_net_amount * (rate / 100) new_data += [tax_line] else: new_data += [line] From 77ffa6b1f67a4ee69066f749ce14b4e1ff29711a Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Thu, 8 Apr 2021 22:19:31 +0000 Subject: [PATCH 12/22] feat: add test case for report output --- .../accounts/report/tax_detail/tax_detail.py | 2 +- .../report/tax_detail/test_tax_detail.json | 755 ++++++++++++++++++ .../report/tax_detail/test_tax_detail.py | 96 ++- 3 files changed, 847 insertions(+), 6 deletions(-) create mode 100644 erpnext/accounts/report/tax_detail/test_tax_detail.json diff --git a/erpnext/accounts/report/tax_detail/tax_detail.py b/erpnext/accounts/report/tax_detail/tax_detail.py index 426e8d4aec..fb7791f7e1 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.py +++ b/erpnext/accounts/report/tax_detail/tax_detail.py @@ -11,7 +11,7 @@ from frappe import _ # field lists in multiple doctypes will be coalesced required_sql_fields = { ("GL Entry", 1): ["posting_date"], - ("Account",): ["account_type"], + ("Account",): ["root_type", "account_type"], ("GL Entry", 2): ["account", "voucher_type", "voucher_no", "debit", "credit"], ("Purchase Invoice Item", "Sales Invoice Item"): ["base_net_amount", "item_tax_rate", "item_tax_template", "item_group", "item_name"], ("Purchase Invoice", "Sales Invoice"): ["taxes_and_charges", "tax_category"], diff --git a/erpnext/accounts/report/tax_detail/test_tax_detail.json b/erpnext/accounts/report/tax_detail/test_tax_detail.json new file mode 100644 index 0000000000..17248d0320 --- /dev/null +++ b/erpnext/accounts/report/tax_detail/test_tax_detail.json @@ -0,0 +1,755 @@ +[ + { + "abbr": "_T", + "company_name": "_T", + "country": "United Kingdom", + "default_currency": "GBP", + "doctype": "Company", + "name": "_T" + },{ + "account_currency": "GBP", + "account_name": "Debtors", + "account_number": "", + "account_type": "Receivable", + "balance_must_be": "", + "company": "_T", + "disabled": 0, + "docstatus": 0, + "doctype": "Account", + "freeze_account": "No", + "include_in_gross": 0, + "inter_company_account": 0, + "is_group": 0, + "lft": 58, + "modified": "2021-03-26 04:44:19.955468", + "name": "Debtors - _T", + "old_parent": null, + "parent": null, + "parent_account": "Application of Funds (Assets) - _T", + "parentfield": null, + "parenttype": null, + "report_type": "Balance Sheet", + "rgt": 59, + "root_type": "Asset", + "tax_rate": 0.0 + },{ + "account_currency": "GBP", + "account_name": "Sales", + "account_number": "", + "account_type": "Income Account", + "balance_must_be": "", + "company": "_T", + "disabled": 0, + "docstatus": 0, + "doctype": "Account", + "freeze_account": "No", + "include_in_gross": 0, + "inter_company_account": 0, + "is_group": 0, + "lft": 291, + "modified": "2021-03-26 04:50:21.697703", + "name": "Sales - _T", + "old_parent": null, + "parent": null, + "parent_account": "Income - _T", + "parentfield": null, + "parenttype": null, + "report_type": "Profit and Loss", + "rgt": 292, + "root_type": "Income", + "tax_rate": 0.0 + },{ + "account_currency": "GBP", + "account_name": "VAT on Sales", + "account_number": "", + "account_type": "Tax", + "balance_must_be": "", + "company": "_T", + "disabled": 0, + "docstatus": 0, + "doctype": "Account", + "freeze_account": "No", + "include_in_gross": 0, + "inter_company_account": 0, + "is_group": 0, + "lft": 317, + "modified": "2021-03-26 04:50:21.697703", + "name": "VAT on Sales - _T", + "old_parent": null, + "parent": null, + "parent_account": "Source of Funds (Liabilities) - _T", + "parentfield": null, + "parenttype": null, + "report_type": "Balance Sheet", + "rgt": 318, + "root_type": "Liability", + "tax_rate": 0.0 + },{ + "account_currency": "GBP", + "account_name": "Cost of Goods Sold", + "account_number": "", + "account_type": "Cost of Goods Sold", + "balance_must_be": "", + "company": "_T", + "disabled": 0, + "docstatus": 0, + "doctype": "Account", + "freeze_account": "No", + "include_in_gross": 0, + "inter_company_account": 0, + "is_group": 0, + "lft": 171, + "modified": "2021-03-26 04:44:19.994857", + "name": "Cost of Goods Sold - _T", + "old_parent": null, + "parent": null, + "parent_account": "Expenses - _T", + "parentfield": null, + "parenttype": null, + "report_type": "Profit and Loss", + "rgt": 172, + "root_type": "Expense", + "tax_rate": 0.0 + },{ + "account_currency": "GBP", + "account_name": "VAT on Purchases", + "account_number": "", + "account_type": "Tax", + "balance_must_be": "", + "company": "_T", + "disabled": 0, + "docstatus": 0, + "doctype": "Account", + "freeze_account": "No", + "include_in_gross": 0, + "inter_company_account": 0, + "is_group": 0, + "lft": 80, + "modified": "2021-03-26 04:44:19.961983", + "name": "VAT on Purchases - _T", + "old_parent": null, + "parent": null, + "parent_account": "Application of Funds (Assets) - _T", + "parentfield": null, + "parenttype": null, + "report_type": "Balance Sheet", + "rgt": 81, + "root_type": "Asset", + "tax_rate": 0.0 + },{ + "account_currency": "GBP", + "account_name": "Creditors", + "account_number": "", + "account_type": "Payable", + "balance_must_be": "", + "company": "_T", + "disabled": 0, + "docstatus": 0, + "doctype": "Account", + "freeze_account": "No", + "include_in_gross": 0, + "inter_company_account": 0, + "is_group": 0, + "lft": 302, + "modified": "2021-03-26 04:50:21.697703", + "name": "Creditors - _T", + "old_parent": null, + "parent": null, + "parent_account": "Source of Funds (Liabilities) - _T", + "parentfield": null, + "parenttype": null, + "report_type": "Balance Sheet", + "rgt": 303, + "root_type": "Liability", + "tax_rate": 0.0 + },{ + "additional_discount_percentage": 0.0, + "address_display": null, + "adjust_advance_taxes": 0, + "advances": [], + "against_expense_account": "Cost of Goods Sold - _T", + "allocate_advances_automatically": 0, + "amended_from": null, + "apply_discount_on": "Grand Total", + "apply_tds": 0, + "auto_repeat": null, + "base_discount_amount": 0.0, + "base_grand_total": 511.68, + "base_in_words": "GBP Five Hundred And Eleven and Sixty Eight Pence only.", + "base_net_total": 426.4, + "base_paid_amount": 0.0, + "base_rounded_total": 511.68, + "base_rounding_adjustment": 0.0, + "base_taxes_and_charges_added": 85.28, + "base_taxes_and_charges_deducted": 0.0, + "base_total": 426.4, + "base_total_taxes_and_charges": 85.28, + "base_write_off_amount": 0.0, + "bill_date": null, + "bill_no": null, + "billing_address": null, + "billing_address_display": null, + "buying_price_list": "Standard Buying", + "cash_bank_account": null, + "clearance_date": null, + "company": "_T", + "contact_display": null, + "contact_email": null, + "contact_mobile": null, + "contact_person": null, + "conversion_rate": 1.0, + "cost_center": null, + "credit_to": "Creditors - _T", + "currency": "GBP", + "disable_rounded_total": 0, + "discount_amount": 0.0, + "docstatus": 0, + "doctype": "Purchase Invoice", + "due_date": "2021-04-30", + "from_date": null, + "grand_total": 511.68, + "group_same_items": 0, + "hold_comment": null, + "ignore_pricing_rule": 0, + "in_words": "GBP Five Hundred And Eleven and Sixty Eight Pence only.", + "inter_company_invoice_reference": null, + "is_internal_supplier": 0, + "is_opening": "No", + "is_paid": 0, + "is_return": 0, + "is_subcontracted": "No", + "items": [ + { + "allow_zero_valuation_rate": 0, + "amount": 426.4, + "asset_category": null, + "asset_location": null, + "base_amount": 426.4, + "base_net_amount": 426.4, + "base_net_rate": 5.33, + "base_price_list_rate": 5.33, + "base_rate": 5.33, + "base_rate_with_margin": 0.0, + "batch_no": null, + "bom": null, + "brand": null, + "conversion_factor": 0.0, + "cost_center": "Main - _T", + "deferred_expense_account": null, + "description": "

Fluid to make widgets

", + "discount_amount": 0.0, + "discount_percentage": 0.0, + "enable_deferred_expense": 0, + "expense_account": "Cost of Goods Sold - _T", + "from_warehouse": null, + "image": null, + "include_exploded_items": 0, + "is_fixed_asset": 0, + "is_free_item": 0, + "item_code": null, + "item_group": null, + "item_name": "Widget Fluid 1Litre", + "item_tax_amount": 0.0, + "item_tax_rate": "{\"VAT on Purchases - _T\": 20.0}", + "item_tax_template": "Purchase - Standard VAT", + "landed_cost_voucher_amount": 0.0, + "manufacturer": null, + "manufacturer_part_no": null, + "margin_rate_or_amount": 0.0, + "margin_type": "", + "net_amount": 426.4, + "net_rate": 5.33, + "page_break": 0, + "parent": null, + "parentfield": "items", + "parenttype": "Purchase Invoice", + "po_detail": null, + "pr_detail": null, + "price_list_rate": 5.33, + "pricing_rules": null, + "project": null, + "purchase_invoice_item": null, + "purchase_order": null, + "purchase_receipt": null, + "qty": 80.0, + "quality_inspection": null, + "rate": 5.33, + "rate_with_margin": 0.0, + "received_qty": 0.0, + "rejected_qty": 0.0, + "rejected_serial_no": null, + "rejected_warehouse": null, + "rm_supp_cost": 0.0, + "sales_invoice_item": null, + "serial_no": null, + "service_end_date": null, + "service_start_date": null, + "service_stop_date": null, + "stock_qty": 0.0, + "stock_uom": "Nos", + "stock_uom_rate": 0.0, + "total_weight": 0.0, + "uom": "Nos", + "valuation_rate": 0.0, + "warehouse": null, + "weight_per_unit": 0.0, + "weight_uom": null + } + ], + "language": "en", + "letter_head": null, + "mode_of_payment": null, + "modified": "2021-04-03 03:33:09.180453", + "name": null, + "naming_series": "ACC-PINV-.YYYY.-", + "net_total": 426.4, + "on_hold": 0, + "other_charges_calculation": "
\n\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\n\t
ItemTaxable AmountVAT on Purchases
Widget Fluid 1Litre\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\u00a3 426.40\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(20.0%)\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\u00a3 85.28\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
\n
", + "outstanding_amount": 511.68, + "paid_amount": 0.0, + "parent": null, + "parentfield": null, + "parenttype": null, + "party_account_currency": "GBP", + "payment_schedule": [], + "payment_terms_template": null, + "plc_conversion_rate": 1.0, + "posting_date": null, + "posting_time": "16:59:56.789522", + "price_list_currency": "GBP", + "pricing_rules": [], + "project": null, + "rejected_warehouse": null, + "release_date": null, + "remarks": "No Remarks", + "represents_company": null, + "return_against": null, + "rounded_total": 511.68, + "rounding_adjustment": 0.0, + "scan_barcode": null, + "select_print_heading": null, + "set_from_warehouse": null, + "set_posting_time": 0, + "set_warehouse": null, + "shipping_address": null, + "shipping_address_display": "", + "shipping_rule": null, + "status": "Unpaid", + "supplied_items": [], + "supplier": "Raw Materials Inc", + "supplier_address": null, + "supplier_name": "Raw Materials Inc", + "supplier_warehouse": "Stores - _T", + "tax_category": "Other Supplier", + "tax_id": null, + "tax_withholding_category": null, + "taxes": [ + { + "account_head": "VAT on Purchases - _T", + "add_deduct_tax": "Add", + "base_tax_amount": 85.28, + "base_tax_amount_after_discount_amount": 85.28, + "base_total": 511.68, + "category": "Total", + "charge_type": "On Net Total", + "cost_center": "Main - _T", + "description": "VAT on Purchases", + "included_in_print_rate": 0, + "item_wise_tax_detail": "{\"Widget Fluid 1Litre\":[20.0,85.28]}", + "parent": null, + "parentfield": "taxes", + "parenttype": "Purchase Invoice", + "rate": 0.0, + "row_id": null, + "tax_amount": 85.28, + "tax_amount_after_discount_amount": 85.28, + "total": 511.68 + } + ], + "taxes_and_charges": null, + "taxes_and_charges_added": 85.28, + "taxes_and_charges_deducted": 0.0, + "tc_name": null, + "terms": null, + "title": "Raw Materials Inc", + "to_date": null, + "total": 426.4, + "total_advance": 0.0, + "total_net_weight": 0.0, + "total_qty": 80.0, + "total_taxes_and_charges": 85.28, + "unrealized_profit_loss_account": null, + "update_stock": 0, + "write_off_account": null, + "write_off_amount": 0.0, + "write_off_cost_center": null + },{ + "account_for_change_amount": null, + "additional_discount_percentage": 0.0, + "address_display": null, + "advances": [], + "against_income_account": "Sales - _T", + "allocate_advances_automatically": 0, + "amended_from": null, + "apply_discount_on": "Grand Total", + "auto_repeat": null, + "base_change_amount": 0.0, + "base_discount_amount": 0.0, + "base_grand_total": 868.25, + "base_in_words": "GBP Eight Hundred And Sixty Eight and Twenty Five Pence only.", + "base_net_total": 825.0, + "base_paid_amount": 0.0, + "base_rounded_total": 868.25, + "base_rounding_adjustment": 0.0, + "base_total": 825.0, + "base_total_taxes_and_charges": 43.25, + "base_write_off_amount": 0.0, + "c_form_applicable": "No", + "c_form_no": null, + "campaign": null, + "cash_bank_account": null, + "change_amount": 0.0, + "commission_rate": 0.0, + "company": "_T", + "company_address": null, + "company_address_display": null, + "company_tax_id": null, + "contact_display": null, + "contact_email": null, + "contact_mobile": null, + "contact_person": null, + "conversion_rate": 1.0, + "cost_center": null, + "currency": "GBP", + "customer": "ABC Tyres", + "customer_address": null, + "customer_group": "All Customer Groups", + "customer_name": "ABC Tyres", + "debit_to": "Debtors - _T", + "discount_amount": 0.0, + "docstatus": 0, + "doctype": "Sales Invoice", + "due_date": "2021-03-31", + "from_date": null, + "grand_total": 868.25, + "group_same_items": 0, + "ignore_pricing_rule": 0, + "in_words": "GBP Eight Hundred And Sixty Eight and Twenty Five Pence only.", + "inter_company_invoice_reference": null, + "is_consolidated": 0, + "is_discounted": 0, + "is_internal_customer": 0, + "is_opening": "No", + "is_pos": 0, + "is_return": 0, + "items": [ + { + "actual_batch_qty": 0.0, + "actual_qty": 0.0, + "allow_zero_valuation_rate": 0, + "amount": 200.0, + "asset": null, + "barcode": null, + "base_amount": 200.0, + "base_net_amount": 200.0, + "base_net_rate": 50.0, + "base_price_list_rate": 0.0, + "base_rate": 50.0, + "base_rate_with_margin": 0.0, + "batch_no": null, + "brand": null, + "conversion_factor": 1.0, + "cost_center": "Main - _T", + "customer_item_code": null, + "deferred_revenue_account": null, + "delivered_by_supplier": 0, + "delivered_qty": 0.0, + "delivery_note": null, + "description": "

Used

", + "discount_amount": 0.0, + "discount_percentage": 0.0, + "dn_detail": null, + "enable_deferred_revenue": 0, + "expense_account": null, + "finance_book": null, + "image": null, + "income_account": "Sales - _T", + "incoming_rate": 0.0, + "is_fixed_asset": 0, + "is_free_item": 0, + "item_code": null, + "item_group": null, + "item_name": "Dunlop tyres", + "item_tax_rate": "{\"VAT on Sales - _T\": 20.0}", + "item_tax_template": "Sale - Standard VAT", + "margin_rate_or_amount": 0.0, + "margin_type": "", + "net_amount": 200.0, + "net_rate": 50.0, + "page_break": 0, + "parent": null, + "parentfield": "items", + "parenttype": "Sales Invoice", + "price_list_rate": 0.0, + "pricing_rules": null, + "project": null, + "qty": 4.0, + "quality_inspection": null, + "rate": 50.0, + "rate_with_margin": 0.0, + "sales_invoice_item": null, + "sales_order": null, + "serial_no": null, + "service_end_date": null, + "service_start_date": null, + "service_stop_date": null, + "so_detail": null, + "stock_qty": 4.0, + "stock_uom": "Nos", + "stock_uom_rate": 50.0, + "target_warehouse": null, + "total_weight": 0.0, + "uom": "Nos", + "warehouse": null, + "weight_per_unit": 0.0, + "weight_uom": null + }, + { + "actual_batch_qty": 0.0, + "actual_qty": 0.0, + "allow_zero_valuation_rate": 0, + "amount": 65.0, + "asset": null, + "barcode": null, + "base_amount": 65.0, + "base_net_amount": 65.0, + "base_net_rate": 65.0, + "base_price_list_rate": 0.0, + "base_rate": 65.0, + "base_rate_with_margin": 0.0, + "batch_no": null, + "brand": null, + "conversion_factor": 1.0, + "cost_center": "Main - _T", + "customer_item_code": null, + "deferred_revenue_account": null, + "delivered_by_supplier": 0, + "delivered_qty": 0.0, + "delivery_note": null, + "description": "

Used

", + "discount_amount": 0.0, + "discount_percentage": 0.0, + "dn_detail": null, + "enable_deferred_revenue": 0, + "expense_account": null, + "finance_book": null, + "image": null, + "income_account": "Sales - _T", + "incoming_rate": 0.0, + "is_fixed_asset": 0, + "is_free_item": 0, + "item_code": "", + "item_group": null, + "item_name": "Continental tyres", + "item_tax_rate": "{\"VAT on Sales - _T\": 5.0}", + "item_tax_template": "Sale - Reduced VAT", + "margin_rate_or_amount": 0.0, + "margin_type": "", + "net_amount": 65.0, + "net_rate": 65.0, + "page_break": 0, + "parent": null, + "parentfield": "items", + "parenttype": "Sales Invoice", + "price_list_rate": 0.0, + "pricing_rules": null, + "project": null, + "qty": 1.0, + "quality_inspection": null, + "rate": 65.0, + "rate_with_margin": 0.0, + "sales_invoice_item": null, + "sales_order": null, + "serial_no": null, + "service_end_date": null, + "service_start_date": null, + "service_stop_date": null, + "so_detail": null, + "stock_qty": 1.0, + "stock_uom": null, + "stock_uom_rate": 65.0, + "target_warehouse": null, + "total_weight": 0.0, + "uom": "Nos", + "warehouse": null, + "weight_per_unit": 0.0, + "weight_uom": null + }, + { + "actual_batch_qty": 0.0, + "actual_qty": 0.0, + "allow_zero_valuation_rate": 0, + "amount": 560.0, + "asset": null, + "barcode": null, + "base_amount": 560.0, + "base_net_amount": 560.0, + "base_net_rate": 70.0, + "base_price_list_rate": 0.0, + "base_rate": 70.0, + "base_rate_with_margin": 0.0, + "batch_no": null, + "brand": null, + "conversion_factor": 1.0, + "cost_center": "Main - _T", + "customer_item_code": null, + "deferred_revenue_account": null, + "delivered_by_supplier": 0, + "delivered_qty": 0.0, + "delivery_note": null, + "description": "

New

", + "discount_amount": 0.0, + "discount_percentage": 0.0, + "dn_detail": null, + "enable_deferred_revenue": 0, + "expense_account": null, + "finance_book": null, + "image": null, + "income_account": "Sales - _T", + "incoming_rate": 0.0, + "is_fixed_asset": 0, + "is_free_item": 0, + "item_code": null, + "item_group": null, + "item_name": "Toyo tyres", + "item_tax_rate": "{\"VAT on Sales - _T\": 0.0}", + "item_tax_template": "Sale - Zero VAT", + "margin_rate_or_amount": 0.0, + "margin_type": "", + "net_amount": 560.0, + "net_rate": 70.0, + "page_break": 0, + "parent": null, + "parentfield": "items", + "parenttype": "Sales Invoice", + "price_list_rate": 0.0, + "pricing_rules": null, + "project": null, + "qty": 8.0, + "quality_inspection": null, + "rate": 70.0, + "rate_with_margin": 0.0, + "sales_invoice_item": null, + "sales_order": null, + "serial_no": null, + "service_end_date": null, + "service_start_date": null, + "service_stop_date": null, + "so_detail": null, + "stock_qty": 8.0, + "stock_uom": null, + "stock_uom_rate": 70.0, + "target_warehouse": null, + "total_weight": 0.0, + "uom": "Nos", + "warehouse": null, + "weight_per_unit": 0.0, + "weight_uom": null + } + ], + "language": "en", + "letter_head": null, + "loyalty_amount": 0.0, + "loyalty_points": 0, + "loyalty_program": null, + "loyalty_redemption_account": null, + "loyalty_redemption_cost_center": null, + "modified": "2021-02-16 05:18:59.755144", + "name": null, + "naming_series": "ACC-SINV-.YYYY.-", + "net_total": 825.0, + "other_charges_calculation": "
\n\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\n\t
ItemTaxable AmountVAT on Sales
Dunlop tyres\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\u00a3 200.00\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(20.0%)\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\u00a3 40.00\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
Continental tyres\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\u00a3 65.00\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(5.0%)\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\u00a3 3.25\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
Toyo tyres\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\u00a3 560.00\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(0.0%)\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\u00a3 0.00\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
\n
", + "outstanding_amount": 868.25, + "packed_items": [], + "paid_amount": 0.0, + "parent": null, + "parentfield": null, + "parenttype": null, + "party_account_currency": "GBP", + "payment_schedule": [], + "payment_terms_template": null, + "payments": [], + "plc_conversion_rate": 1.0, + "po_date": null, + "po_no": "", + "pos_profile": null, + "posting_date": null, + "posting_time": "5:19:02.994077", + "price_list_currency": "GBP", + "pricing_rules": [], + "project": null, + "redeem_loyalty_points": 0, + "remarks": "No Remarks", + "represents_company": "", + "return_against": null, + "rounded_total": 868.25, + "rounding_adjustment": 0.0, + "sales_partner": null, + "sales_team": [], + "scan_barcode": null, + "select_print_heading": null, + "selling_price_list": "Standard Selling", + "set_posting_time": 0, + "set_target_warehouse": null, + "set_warehouse": null, + "shipping_address": null, + "shipping_address_name": "", + "shipping_rule": null, + "source": null, + "status": "Overdue", + "tax_category": "", + "tax_id": null, + "taxes": [ + { + "account_head": "VAT on Sales - _T", + "base_tax_amount": 43.25, + "base_tax_amount_after_discount_amount": 43.25, + "base_total": 868.25, + "charge_type": "On Net Total", + "cost_center": "Main - _T", + "description": "VAT on Sales", + "included_in_print_rate": 0, + "item_wise_tax_detail": "{\"Dunlop tyres\":[20.0,40.0],\"Continental tyres\":[5.0,3.25],\"Toyo tyres\":[0.0,0.0]}", + "parent": null, + "parentfield": "taxes", + "parenttype": "Sales Invoice", + "rate": 0.0, + "row_id": null, + "tax_amount": 43.25, + "tax_amount_after_discount_amount": 43.25, + "total": 868.25 + } + ], + "taxes_and_charges": null, + "tc_name": null, + "terms": null, + "territory": "All Territories", + "timesheets": [], + "title": "ABC Tyres", + "to_date": null, + "total": 825.0, + "total_advance": 0.0, + "total_billing_amount": 0.0, + "total_commission": 0.0, + "total_net_weight": 0.0, + "total_qty": 13.0, + "total_taxes_and_charges": 43.25, + "unrealized_profit_loss_account": null, + "update_billed_amount_in_sales_order": 0, + "update_stock": 0, + "write_off_account": null, + "write_off_amount": 0.0, + "write_off_cost_center": null, + "write_off_outstanding_amount_automatically": 0 + } +] \ No newline at end of file diff --git a/erpnext/accounts/report/tax_detail/test_tax_detail.py b/erpnext/accounts/report/tax_detail/test_tax_detail.py index dfd8d9e121..c9b8e209e4 100644 --- a/erpnext/accounts/report/tax_detail/test_tax_detail.py +++ b/erpnext/accounts/report/tax_detail/test_tax_detail.py @@ -1,12 +1,98 @@ from __future__ import unicode_literals -import frappe, unittest, datetime -from frappe.utils import getdate -from .tax_detail import execute, filter_match +import frappe, unittest, datetime, json, os +from frappe.utils import getdate, add_to_date, get_first_day, get_last_day +from .tax_detail import filter_match, save_custom_report class TestTaxDetail(unittest.TestCase): - def setup(self): - pass + def load_testdocs(self): + datapath, _ = os.path.splitext(os.path.realpath(__file__)) + with open(datapath + '.json', 'r') as fp: + self.docs = json.load(fp) + + def load_defcols(self): + custom_report = frappe.get_doc('Report', 'Tax Detail') + self.default_columns, _ = custom_report.run_query_report( + filters={ + 'from_date': '2021-03-01', + 'to_date': '2021-03-31', + 'company': '_T', + 'mode': 'run', + 'report_name': 'Tax Detail' + }, user=frappe.session.user) + + def setUp(self): + "Add Transactions in 01-03-2021 - 31-03-2021" + self.load_testdocs() + now = getdate() + self.from_date = get_first_day(now) + self.to_date = get_last_day(now) + + for doc in self.docs: + try: + db_doc = frappe.get_doc(doc) + if 'Invoice' in db_doc.doctype: + db_doc.due_date = add_to_date(now, days=1) + db_doc.insert() + # Create GL Entries: + db_doc.submit() + else: + db_doc.insert() + except frappe.exceptions.DuplicateEntryError as e: + pass + #print(f'Duplicate Entry: {e}') + except: + print(f'\nError importing {doc["doctype"]}: {doc["name"]}') + raise + + self.load_defcols() + + def tearDown(self): + "Remove the Company and all data" + from erpnext.setup.doctype.company.delete_company_transactions import delete_company_transactions + for co in filter(lambda doc: doc['doctype'] == 'Company', self.docs): + delete_company_transactions(co['name']) + db_co = frappe.get_doc('Company', co['name']) + db_co.delete() + + def test_report(self): + report_name = save_custom_report( + 'Tax Detail', + '_Test Tax Detail', + json.dumps({ + 'columns': self.default_columns, + 'sections': { + 'Box1':{'Filter0':{'type':'filter','filters':{'4':'VAT on Sales'}}}, + 'Box2':{'Filter0':{'type':'filter','filters':{'4':'Acquisition'}}}, + 'Box3':{'Box1':{'type':'section'},'Box2':{'type':'section'}}, + 'Box4':{'Filter0':{'type':'filter','filters':{'4':'VAT on Purchases'}}}, + 'Box5':{'Box3':{'type':'section'},'Box4':{'type':'section'}}, + 'Box6':{'Filter0':{'type':'filter','filters':{'3':'!=Tax','4':'Sales'}}}, + 'Box7':{'Filter0':{'type':'filter','filters':{'2':'Expense','3':'!=Tax'}}}, + 'Box8':{'Filter0':{'type':'filter','filters':{'3':'!=Tax','4':'Sales','12':'EU'}}}, + 'Box9':{'Filter0':{'type':'filter','filters':{'2':'Expense','3':'!=Tax','12':'EU'}}} + }, + 'show_detail': 1 + })) + data = frappe.desk.query_report.run(report_name, + filters={ + 'from_date': self.from_date, + 'to_date': self.to_date, + 'company': '_T', + 'mode': 'run', + 'report_name': report_name + }, user=frappe.session.user) + + self.assertListEqual(data.get('columns'), self.default_columns) + expected = (('Box1', 43.25), ('Box2', 0.0), ('Box3', 43.25), ('Box4', -85.28), ('Box5', -42.03), + ('Box6', 825.0), ('Box7', -426.40), ('Box8', 0.0), ('Box9', 0.0)) + exrow = iter(expected) + for row in data.get('result'): + if row.get('voucher_no') and not row.get('posting_date'): + label, value = next(exrow) + self.assertDictEqual(row, {'voucher_no': label, 'amount': value}) + self.assertListEqual(data.get('report_summary'), + [{'label': label, 'datatype': 'Currency', 'value': value} for label, value in expected]) def test_filter_match(self): # None - treated as -inf number except range From 7555f5f6130c20bfb9d608810a0215752dd7914c Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Thu, 8 Apr 2021 23:54:34 +0000 Subject: [PATCH 13/22] fix: add to workspace and fix lint --- erpnext/accounts/report/tax_detail/tax_detail.js | 12 ++---------- .../accounts/workspace/accounting/accounting.json | 10 ++++++++++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/report/tax_detail/tax_detail.js b/erpnext/accounts/report/tax_detail/tax_detail.js index 0c0397ab04..098096ce0b 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.js +++ b/erpnext/accounts/report/tax_detail/tax_detail.js @@ -376,16 +376,8 @@ erpnext.TaxDetail = class TaxDetail { this.controls = controls; } show_help() { - const help = __(`Help: Your custom report is built from General Ledger Entries within the date range. - You can add multiple sections to the report using the New Section button. - Each component added to a section adds a subset of the data into the specified section. - Beware of duplicated data rows. - The Filtered Row component type saves the datatable column filters to specify the added data. - The Section component type refers to the data in a previously defined section, but it cannot refer to its parent section. - The Amount column is summed to give the section subtotal. - Use the Show Detail box to see the data rows included in each section in the final report. - Once finished, hit Save & Run. Report contributed by`); - this.qr.$report_footer.append(``); + const help = __('Your custom report is built from General Ledger Entries within the date range. You can add multiple sections to the report using the New Section button. Each component added to a section adds a subset of the data into the specified section. Beware of duplicated data rows. The Filtered Row component type saves the datatable column filters to specify the added data. The Section component type refers to the data in a previously defined section, but it cannot refer to its parent section. The Amount column is summed to give the section subtotal. Use the Show Detail box to see the data rows included in each section in the final report. Once finished, hit Save & Run. Report contributed by'); + this.qr.$report_footer.append('
' + __('Help') + `: ${help} Case Solved
`); } } diff --git a/erpnext/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json index df68318052..cbfba7e31c 100644 --- a/erpnext/accounts/workspace/accounting/accounting.json +++ b/erpnext/accounts/workspace/accounting/accounting.json @@ -434,6 +434,16 @@ "onboard": 0, "type": "Link" }, + { + "dependencies": "GL Entry", + "hidden": 0, + "is_query_report": 1, + "label": "Tax Detail", + "link_to": "Tax Detail", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, { "dependencies": "GL Entry", "hidden": 0, From 391dc45964913d79c3baec434a6692a370d570d3 Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Fri, 9 Apr 2021 00:23:08 +0000 Subject: [PATCH 14/22] chore: fix sider --- .../accounts/report/tax_detail/tax_detail.py | 59 ++++++++++++------- .../report/tax_detail/test_tax_detail.py | 12 ++-- 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/erpnext/accounts/report/tax_detail/tax_detail.py b/erpnext/accounts/report/tax_detail/tax_detail.py index fb7791f7e1..aafcf1297e 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.py +++ b/erpnext/accounts/report/tax_detail/tax_detail.py @@ -3,7 +3,8 @@ # Contributed by Case Solved and sponsored by Nulight Studios from __future__ import unicode_literals -import frappe, json +import frappe +import json from frappe import _ # NOTE: Payroll is implemented using Journal Entries which are included as GL Entries @@ -86,26 +87,35 @@ def run_report(report_name, data): except KeyError: frappe.throw(_("A report component can only refer to an earlier section: ") + section_name) - if show_detail: new_data += report[section_name]['rows'] - new_data += [ {'voucher_no': section_name, 'amount': report[section_name]['subtotal']} ] - summary += [ {'label': section_name, 'datatype': 'Currency', 'value': report[section_name]['subtotal']} ] - if show_detail: new_data += [ {} ] + if show_detail: + new_data += report[section_name]['rows'] + new_data += [{'voucher_no': section_name, 'amount': report[section_name]['subtotal']}] + summary += [{'label': section_name, 'datatype': 'Currency', 'value': report[section_name]['subtotal']}] + if show_detail: + new_data += [{}] return new_data or data, summary or None def filter_match(value, string): "Approximation to datatable filters" import datetime - if string == '': return True - if value is None: value = -999999999999999 - elif isinstance(value, datetime.date): return True + if string == '': + return True + if value is None: + value = -999999999999999 + elif isinstance(value, datetime.date): + return True if isinstance(value, str): value = value.lower() string = string.lower() - if string[0] == '<': return True if string[1:].strip() else False - elif string[0] == '>': return False if string[1:].strip() else True - elif string[0] == '=': return string[1:] in value if string[1:] else False - elif string[0:2] == '!=': return string[2:] not in value + if string[0] == '<': + return True if string[1:].strip() else False + elif string[0] == '>': + return False if string[1:].strip() else True + elif string[0] == '=': + return string[1:] in value if string[1:] else False + elif string[0:2] == '!=': + return string[2:] not in value elif len(string.split(':')) == 2: pre, post = string.split(':') return (True if not pre.strip() and post.strip() in value else False) @@ -114,7 +124,8 @@ def filter_match(value, string): else: if string[0] in ['<', '>', '=']: operator = string[0] - if operator == '=': operator = '==' + if operator == '=': + operator = '==' string = string[1:].strip() elif string[0:2] == '!=': operator = '!=' @@ -132,12 +143,16 @@ def filter_match(value, string): num = float(string) if string.strip() else 0 return eval(f'{value} {operator} {num}') except ValueError: - if operator == '<': return True + if operator == '<': + return True return False -abbrev = lambda dt: ''.join(l[0].lower() for l in dt.split(' ')) + '.' -doclist = lambda dt, dfs: [abbrev(dt) + f for f in dfs] +def abbrev(dt): + return ''.join(l[0].lower() for l in dt.split(' ')) + '.' + +def doclist(dt, dfs): + return [abbrev(dt) + f for f in dfs] def as_split(fields): for field in fields: @@ -165,7 +180,8 @@ def get_columns(fieldlist): for doctypes, docfields in fieldlist.items(): fieldmap = {name: new_name for name, new_name in as_split(docfields)} for doctype in doctypes: - if isinstance(doctype, int): break + if isinstance(doctype, int): + break meta = frappe.get_meta(doctype) # get column field metadata from the db fieldmeta = {} @@ -203,8 +219,10 @@ def modify_report_data(data): import json new_data = [] for line in data: - if line.debit: line.amount = -line.debit - else: line.amount = line.credit + if line.debit: + line.amount = -line.debit + else: + line.amount = line.credit # Remove Invoice GL Tax Entries and generate Tax entries from the invoice lines if "Invoice" in line.voucher_type: if line.account_type != "Tax": @@ -226,7 +244,8 @@ def modify_report_data(data): new_data += [line] return new_data -####### JS client utilities + +# JS client utilities custom_report_dict = { 'ref_doctype': 'GL Entry', diff --git a/erpnext/accounts/report/tax_detail/test_tax_detail.py b/erpnext/accounts/report/tax_detail/test_tax_detail.py index c9b8e209e4..614ef8d234 100644 --- a/erpnext/accounts/report/tax_detail/test_tax_detail.py +++ b/erpnext/accounts/report/tax_detail/test_tax_detail.py @@ -1,6 +1,10 @@ from __future__ import unicode_literals -import frappe, unittest, datetime, json, os +import frappe +import unittest +import datetime +import json +import os from frappe.utils import getdate, add_to_date, get_first_day, get_last_day from .tax_detail import filter_match, save_custom_report @@ -38,12 +42,8 @@ class TestTaxDetail(unittest.TestCase): db_doc.submit() else: db_doc.insert() - except frappe.exceptions.DuplicateEntryError as e: + except frappe.exceptions.DuplicateEntryError: pass - #print(f'Duplicate Entry: {e}') - except: - print(f'\nError importing {doc["doctype"]}: {doc["name"]}') - raise self.load_defcols() From 06e31e5d7dd4d453f65b728743a62d782b4a133b Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Sat, 10 Apr 2021 00:31:16 +0000 Subject: [PATCH 15/22] fix: Test data for empty db --- .../report/tax_detail/test_tax_detail.json | 116 ++++++++++++++++-- .../report/tax_detail/test_tax_detail.py | 50 ++++---- 2 files changed, 132 insertions(+), 34 deletions(-) diff --git a/erpnext/accounts/report/tax_detail/test_tax_detail.json b/erpnext/accounts/report/tax_detail/test_tax_detail.json index 17248d0320..977920a231 100644 --- a/erpnext/accounts/report/tax_detail/test_tax_detail.json +++ b/erpnext/accounts/report/tax_detail/test_tax_detail.json @@ -6,6 +6,98 @@ "default_currency": "GBP", "doctype": "Company", "name": "_T" + },{ + "account_manager": null, + "accounts": [], + "companies": [], + "credit_limits": [], + "customer_details": null, + "customer_group": "All Customer Groups", + "customer_name": "_Test Customer", + "customer_pos_id": null, + "customer_primary_address": null, + "customer_primary_contact": null, + "customer_type": "Company", + "default_bank_account": null, + "default_commission_rate": 0.0, + "default_currency": null, + "default_price_list": null, + "default_sales_partner": null, + "disabled": 0, + "dn_required": 0, + "docstatus": 0, + "doctype": "Customer", + "email_id": null, + "gender": null, + "image": null, + "industry": null, + "is_frozen": 0, + "is_internal_customer": 0, + "language": "en", + "lead_name": null, + "loyalty_program": null, + "loyalty_program_tier": null, + "market_segment": null, + "mobile_no": null, + "modified": "2021-02-15 05:18:03.624724", + "name": "_Test Customer", + "naming_series": "CUST-.YYYY.-", + "pan": null, + "parent": null, + "parentfield": null, + "parenttype": null, + "payment_terms": null, + "primary_address": null, + "represents_company": "", + "sales_team": [], + "salutation": null, + "so_required": 0, + "tax_category": null, + "tax_id": null, + "tax_withholding_category": null, + "territory": "All Territories", + "website": null + },{ + "accounts": [], + "allow_purchase_invoice_creation_without_purchase_order": 0, + "allow_purchase_invoice_creation_without_purchase_receipt": 0, + "companies": [], + "country": "United Kingdom", + "default_bank_account": null, + "default_currency": null, + "default_price_list": null, + "disabled": 0, + "docstatus": 0, + "doctype": "Supplier", + "hold_type": "", + "image": null, + "is_frozen": 0, + "is_internal_supplier": 0, + "is_transporter": 0, + "language": "en", + "modified": "2021-03-31 16:47:10.109316", + "name": "_Test Supplier", + "naming_series": "SUP-.YYYY.-", + "on_hold": 0, + "pan": null, + "parent": null, + "parentfield": null, + "parenttype": null, + "payment_terms": null, + "prevent_pos": 0, + "prevent_rfqs": 0, + "release_date": null, + "represents_company": null, + "supplier_details": null, + "supplier_group": "Raw Material", + "supplier_name": "_Test Supplier", + "supplier_type": "Company", + "tax_category": null, + "tax_id": null, + "tax_withholding_category": null, + "warn_pos": 0, + "warn_rfqs": 0, + "website": null },{ "account_currency": "GBP", "account_name": "Debtors", @@ -251,7 +343,7 @@ "item_name": "Widget Fluid 1Litre", "item_tax_amount": 0.0, "item_tax_rate": "{\"VAT on Purchases - _T\": 20.0}", - "item_tax_template": "Purchase - Standard VAT", + "item_tax_template": null, "landed_cost_voucher_amount": 0.0, "manufacturer": null, "manufacturer_part_no": null, @@ -336,11 +428,11 @@ "shipping_rule": null, "status": "Unpaid", "supplied_items": [], - "supplier": "Raw Materials Inc", + "supplier": "_Test Supplier", "supplier_address": null, - "supplier_name": "Raw Materials Inc", + "supplier_name": "_Test Supplier", "supplier_warehouse": "Stores - _T", - "tax_category": "Other Supplier", + "tax_category": null, "tax_id": null, "tax_withholding_category": null, "taxes": [ @@ -371,7 +463,7 @@ "taxes_and_charges_deducted": 0.0, "tc_name": null, "terms": null, - "title": "Raw Materials Inc", + "title": "_Purchase Invoice", "to_date": null, "total": 426.4, "total_advance": 0.0, @@ -421,10 +513,10 @@ "conversion_rate": 1.0, "cost_center": null, "currency": "GBP", - "customer": "ABC Tyres", + "customer": "_Test Customer", "customer_address": null, "customer_group": "All Customer Groups", - "customer_name": "ABC Tyres", + "customer_name": "_Test Customer", "debit_to": "Debtors - _T", "discount_amount": 0.0, "docstatus": 0, @@ -481,7 +573,7 @@ "item_group": null, "item_name": "Dunlop tyres", "item_tax_rate": "{\"VAT on Sales - _T\": 20.0}", - "item_tax_template": "Sale - Standard VAT", + "item_tax_template": null, "margin_rate_or_amount": 0.0, "margin_type": "", "net_amount": 200.0, @@ -552,7 +644,7 @@ "item_group": null, "item_name": "Continental tyres", "item_tax_rate": "{\"VAT on Sales - _T\": 5.0}", - "item_tax_template": "Sale - Reduced VAT", + "item_tax_template": null, "margin_rate_or_amount": 0.0, "margin_type": "", "net_amount": 65.0, @@ -623,7 +715,7 @@ "item_group": null, "item_name": "Toyo tyres", "item_tax_rate": "{\"VAT on Sales - _T\": 0.0}", - "item_tax_template": "Sale - Zero VAT", + "item_tax_template": null, "margin_rate_or_amount": 0.0, "margin_type": "", "net_amount": 560.0, @@ -735,7 +827,7 @@ "terms": null, "territory": "All Territories", "timesheets": [], - "title": "ABC Tyres", + "title": "_Sales Invoice", "to_date": null, "total": 825.0, "total_advance": 0.0, @@ -752,4 +844,4 @@ "write_off_cost_center": null, "write_off_outstanding_amount_automatically": 0 } -] \ No newline at end of file +] diff --git a/erpnext/accounts/report/tax_detail/test_tax_detail.py b/erpnext/accounts/report/tax_detail/test_tax_detail.py index 614ef8d234..21732b9dfd 100644 --- a/erpnext/accounts/report/tax_detail/test_tax_detail.py +++ b/erpnext/accounts/report/tax_detail/test_tax_detail.py @@ -5,34 +5,27 @@ import unittest import datetime import json import os -from frappe.utils import getdate, add_to_date, get_first_day, get_last_day +from frappe.utils import getdate, add_to_date, get_first_day, get_last_day, get_year_start, get_year_ending from .tax_detail import filter_match, save_custom_report class TestTaxDetail(unittest.TestCase): def load_testdocs(self): datapath, _ = os.path.splitext(os.path.realpath(__file__)) with open(datapath + '.json', 'r') as fp: - self.docs = json.load(fp) + docs = json.load(fp) - def load_defcols(self): - custom_report = frappe.get_doc('Report', 'Tax Detail') - self.default_columns, _ = custom_report.run_query_report( - filters={ - 'from_date': '2021-03-01', - 'to_date': '2021-03-31', - 'company': '_T', - 'mode': 'run', - 'report_name': 'Tax Detail' - }, user=frappe.session.user) - - def setUp(self): - "Add Transactions in 01-03-2021 - 31-03-2021" - self.load_testdocs() now = getdate() self.from_date = get_first_day(now) self.to_date = get_last_day(now) - for doc in self.docs: + docs = [{ + "doctype": "Fiscal Year", + "year": "_Test Fiscal", + "year_end_date": get_year_ending(now), + "year_start_date": get_year_start(now) + }] + docs + + for doc in docs: try: db_doc = frappe.get_doc(doc) if 'Invoice' in db_doc.doctype: @@ -45,15 +38,28 @@ class TestTaxDetail(unittest.TestCase): except frappe.exceptions.DuplicateEntryError: pass + def load_defcols(self): + self.company = frappe.get_doc('Company', '_T') + custom_report = frappe.get_doc('Report', 'Tax Detail') + self.default_columns, _ = custom_report.run_query_report( + filters={ + 'from_date': '2021-03-01', + 'to_date': '2021-03-31', + 'company': self.company.name, + 'mode': 'run', + 'report_name': 'Tax Detail' + }, user=frappe.session.user) + + def setUp(self): + self.load_testdocs() self.load_defcols() def tearDown(self): "Remove the Company and all data" from erpnext.setup.doctype.company.delete_company_transactions import delete_company_transactions - for co in filter(lambda doc: doc['doctype'] == 'Company', self.docs): - delete_company_transactions(co['name']) - db_co = frappe.get_doc('Company', co['name']) - db_co.delete() + delete_company_transactions(self.company.name) + self.company.delete() + def test_report(self): report_name = save_custom_report( @@ -78,7 +84,7 @@ class TestTaxDetail(unittest.TestCase): filters={ 'from_date': self.from_date, 'to_date': self.to_date, - 'company': '_T', + 'company': self.company.name, 'mode': 'run', 'report_name': report_name }, user=frappe.session.user) From 89fcdf32263e2baaa704fb97f121759e34d87701 Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Sat, 10 Apr 2021 00:32:11 +0000 Subject: [PATCH 16/22] fix: exclude rounding GL Entries from invoice tax lines --- .../accounts/report/tax_detail/tax_detail.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/report/tax_detail/tax_detail.py b/erpnext/accounts/report/tax_detail/tax_detail.py index aafcf1297e..fdecd269eb 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.py +++ b/erpnext/accounts/report/tax_detail/tax_detail.py @@ -225,21 +225,21 @@ def modify_report_data(data): line.amount = line.credit # Remove Invoice GL Tax Entries and generate Tax entries from the invoice lines if "Invoice" in line.voucher_type: - if line.account_type != "Tax": + if line.account_type not in ("Tax", "Round Off"): new_data += [line] - if line.item_tax_rate: - tax_rates = json.loads(line.item_tax_rate) - for account, rate in tax_rates.items(): - tax_line = line.copy() - tax_line.account_type = "Tax" - tax_line.account = account - if line.voucher_type == "Sales Invoice": - line.amount = line.base_net_amount - tax_line.amount = line.base_net_amount * (rate / 100) - if line.voucher_type == "Purchase Invoice": - line.amount = -line.base_net_amount - tax_line.amount = -line.base_net_amount * (rate / 100) - new_data += [tax_line] + if line.item_tax_rate: + tax_rates = json.loads(line.item_tax_rate) + for account, rate in tax_rates.items(): + tax_line = line.copy() + tax_line.account_type = "Tax" + tax_line.account = account + if line.voucher_type == "Sales Invoice": + line.amount = line.base_net_amount + tax_line.amount = line.base_net_amount * (rate / 100) + if line.voucher_type == "Purchase Invoice": + line.amount = -line.base_net_amount + tax_line.amount = -line.base_net_amount * (rate / 100) + new_data += [tax_line] else: new_data += [line] return new_data From 75ebfbdaf392cbb98100291a52d2623a7337618d Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Sat, 10 Apr 2021 02:27:00 +0000 Subject: [PATCH 17/22] fix: fiscal year test case issue --- .../accounts/report/tax_detail/test_tax_detail.json | 11 ++--------- .../accounts/report/tax_detail/test_tax_detail.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/report/tax_detail/test_tax_detail.json b/erpnext/accounts/report/tax_detail/test_tax_detail.json index 977920a231..3a4b175455 100644 --- a/erpnext/accounts/report/tax_detail/test_tax_detail.json +++ b/erpnext/accounts/report/tax_detail/test_tax_detail.json @@ -1,12 +1,5 @@ [ { - "abbr": "_T", - "company_name": "_T", - "country": "United Kingdom", - "default_currency": "GBP", - "doctype": "Company", - "name": "_T" - },{ "account_manager": null, "accounts": [], "companies": [], @@ -297,7 +290,7 @@ "discount_amount": 0.0, "docstatus": 0, "doctype": "Purchase Invoice", - "due_date": "2021-04-30", + "due_date": null, "from_date": null, "grand_total": 511.68, "group_same_items": 0, @@ -521,7 +514,7 @@ "discount_amount": 0.0, "docstatus": 0, "doctype": "Sales Invoice", - "due_date": "2021-03-31", + "due_date": null, "from_date": null, "grand_total": 868.25, "group_same_items": 0, diff --git a/erpnext/accounts/report/tax_detail/test_tax_detail.py b/erpnext/accounts/report/tax_detail/test_tax_detail.py index 21732b9dfd..dcf0e79064 100644 --- a/erpnext/accounts/report/tax_detail/test_tax_detail.py +++ b/erpnext/accounts/report/tax_detail/test_tax_detail.py @@ -19,6 +19,19 @@ class TestTaxDetail(unittest.TestCase): self.to_date = get_last_day(now) docs = [{ + "abbr": "_T", + "company_name": "_T", + "country": "United Kingdom", + "default_currency": "GBP", + "doctype": "Company", + "name": "_T" + },{ + "companies": [{ + "company": "_T", + "parent": "_Test Fiscal", + "parentfield": "companies", + "parenttype": "Fiscal Year" + }], "doctype": "Fiscal Year", "year": "_Test Fiscal", "year_end_date": get_year_ending(now), From 68a31d3d0d799608fbf5593f5f7d732d2971eb03 Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Sat, 10 Apr 2021 18:59:57 +0000 Subject: [PATCH 18/22] fix: fiscal year error --- .../report/tax_detail/test_tax_detail.py | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/report/tax_detail/test_tax_detail.py b/erpnext/accounts/report/tax_detail/test_tax_detail.py index dcf0e79064..772b9a468a 100644 --- a/erpnext/accounts/report/tax_detail/test_tax_detail.py +++ b/erpnext/accounts/report/tax_detail/test_tax_detail.py @@ -18,6 +18,23 @@ class TestTaxDetail(unittest.TestCase): self.from_date = get_first_day(now) self.to_date = get_last_day(now) + for fy in frappe.get_list('Fiscal Year', fields=('year', 'year_start_date', 'year_end_date')): + if now >= fy['year_start_date'] and now <= fy['year_end_date']: + break + else: + docs = [{ + "companies": [{ + "company": "_T", + "parent": "_Test Fiscal", + "parentfield": "companies", + "parenttype": "Fiscal Year" + }], + "doctype": "Fiscal Year", + "year": "_Test Fiscal", + "year_end_date": get_year_ending(now), + "year_start_date": get_year_start(now) + }] + docs + docs = [{ "abbr": "_T", "company_name": "_T", @@ -25,17 +42,6 @@ class TestTaxDetail(unittest.TestCase): "default_currency": "GBP", "doctype": "Company", "name": "_T" - },{ - "companies": [{ - "company": "_T", - "parent": "_Test Fiscal", - "parentfield": "companies", - "parenttype": "Fiscal Year" - }], - "doctype": "Fiscal Year", - "year": "_Test Fiscal", - "year_end_date": get_year_ending(now), - "year_start_date": get_year_start(now) }] + docs for doc in docs: From cf5b57bfacff6ffe813c7fcfef1b74d80b545337 Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Sat, 10 Apr 2021 20:32:22 +0000 Subject: [PATCH 19/22] fix: only load data for tests that need it --- erpnext/accounts/report/tax_detail/test_tax_detail.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/tax_detail/test_tax_detail.py b/erpnext/accounts/report/tax_detail/test_tax_detail.py index 772b9a468a..78b15b11c5 100644 --- a/erpnext/accounts/report/tax_detail/test_tax_detail.py +++ b/erpnext/accounts/report/tax_detail/test_tax_detail.py @@ -69,11 +69,7 @@ class TestTaxDetail(unittest.TestCase): 'report_name': 'Tax Detail' }, user=frappe.session.user) - def setUp(self): - self.load_testdocs() - self.load_defcols() - - def tearDown(self): + def rm_testdocs(self): "Remove the Company and all data" from erpnext.setup.doctype.company.delete_company_transactions import delete_company_transactions delete_company_transactions(self.company.name) @@ -81,6 +77,8 @@ class TestTaxDetail(unittest.TestCase): def test_report(self): + self.load_testdocs() + self.load_defcols() report_name = save_custom_report( 'Tax Detail', '_Test Tax Detail', @@ -119,6 +117,8 @@ class TestTaxDetail(unittest.TestCase): self.assertListEqual(data.get('report_summary'), [{'label': label, 'datatype': 'Currency', 'value': value} for label, value in expected]) + self.rm_testdocs() + def test_filter_match(self): # None - treated as -inf number except range self.assertTrue(filter_match(None, '!=')) From 699878605531f588d9f1afd836cf4b0c6621658f Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Mon, 12 Apr 2021 13:03:09 +0000 Subject: [PATCH 20/22] fix: use correct fiscal year function in testing --- erpnext/accounts/report/tax_detail/test_tax_detail.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/tax_detail/test_tax_detail.py b/erpnext/accounts/report/tax_detail/test_tax_detail.py index 78b15b11c5..d3b8de5d19 100644 --- a/erpnext/accounts/report/tax_detail/test_tax_detail.py +++ b/erpnext/accounts/report/tax_detail/test_tax_detail.py @@ -10,6 +10,7 @@ from .tax_detail import filter_match, save_custom_report class TestTaxDetail(unittest.TestCase): def load_testdocs(self): + from erpnext.accounts.utils import get_fiscal_year, FiscalYearError datapath, _ = os.path.splitext(os.path.realpath(__file__)) with open(datapath + '.json', 'r') as fp: docs = json.load(fp) @@ -18,10 +19,9 @@ class TestTaxDetail(unittest.TestCase): self.from_date = get_first_day(now) self.to_date = get_last_day(now) - for fy in frappe.get_list('Fiscal Year', fields=('year', 'year_start_date', 'year_end_date')): - if now >= fy['year_start_date'] and now <= fy['year_end_date']: - break - else: + try: + get_fiscal_year(now, company="_T") + except FiscalYearError: docs = [{ "companies": [{ "company": "_T", From d5256b60d48806e5e10442663623959fda7fb570 Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Sun, 18 Apr 2021 22:13:39 +0000 Subject: [PATCH 21/22] fix: lint --- erpnext/accounts/report/tax_detail/tax_detail.js | 4 ++-- erpnext/accounts/report/tax_detail/tax_detail.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/tax_detail/tax_detail.js b/erpnext/accounts/report/tax_detail/tax_detail.js index 098096ce0b..ed6fac4e60 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.js +++ b/erpnext/accounts/report/tax_detail/tax_detail.js @@ -273,7 +273,7 @@ erpnext.TaxDetail = class TaxDetail { click: () => { let cur_section = this.controls['section_name'].get_input_value(); if (cur_section) { - frappe.confirm(__('Are you sure you want to delete section ') + cur_section + '?', + frappe.confirm(__('Are you sure you want to delete section') + ' ' + cur_section + '?', () => {this.delete(cur_section, 'section')}); } } @@ -354,7 +354,7 @@ erpnext.TaxDetail = class TaxDetail { click: () => { const component = this.controls['component'].get_input_value(); if (component) { - frappe.confirm(__('Are you sure you want to delete component ') + component + '?', + frappe.confirm(__('Are you sure you want to delete component') + ' ' + component + '?', () => {this.delete(component, 'component')}); } } diff --git a/erpnext/accounts/report/tax_detail/tax_detail.py b/erpnext/accounts/report/tax_detail/tax_detail.py index fdecd269eb..18436de3d8 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.py +++ b/erpnext/accounts/report/tax_detail/tax_detail.py @@ -80,12 +80,12 @@ def run_report(report_name, data): report[section_name]['subtotal'] += row['amount'] if component['type'] == 'section': if component_name == section_name: - frappe.throw(_("A report component cannot refer to its parent section: ") + section_name) + frappe.throw(_("A report component cannot refer to its parent section") + ": " + section_name) try: report[section_name]['rows'] += report[component_name]['rows'] report[section_name]['subtotal'] += report[component_name]['subtotal'] except KeyError: - frappe.throw(_("A report component can only refer to an earlier section: ") + section_name) + frappe.throw(_("A report component can only refer to an earlier section") + ": " + section_name) if show_detail: new_data += report[section_name]['rows'] @@ -141,7 +141,7 @@ def filter_match(value, string): try: num = float(string) if string.strip() else 0 - return eval(f'{value} {operator} {num}') + return frappe.safe_eval(f'{value} {operator} {num}') except ValueError: if operator == '<': return True From 6ab46e288f15e77815c0bd3a67fc0d6ba8e28e03 Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Thu, 13 May 2021 12:56:02 +0000 Subject: [PATCH 22/22] fix: workspace formatting due to manual edit and filter tweaks --- .../workspace/accounting/accounting.json | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json index cbfba7e31c..148436edaa 100644 --- a/erpnext/accounts/workspace/accounting/accounting.json +++ b/erpnext/accounts/workspace/accounting/accounting.json @@ -452,18 +452,20 @@ "link_to": "DATEV", "link_type": "Report", "onboard": 0, + "only_for": "Germany", "type": "Link" }, { - "dependencies": "GL Entry", - "hidden": 0, - "is_query_report": 1, - "label": "UAE VAT 201", - "link_to": "UAE VAT 201", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, + "dependencies": "GL Entry", + "hidden": 0, + "is_query_report": 1, + "label": "UAE VAT 201", + "link_to": "UAE VAT 201", + "link_type": "Report", + "onboard": 0, + "only_for": "United Arab Emirates", + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, @@ -1062,7 +1064,7 @@ "type": "Link" } ], - "modified": "2021-05-12 11:48:01.905144", + "modified": "2021-05-13 13:44:56.249888", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting",