From dba4b3cd13e12e3db1233cbf707744773fff62e1 Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Fri, 19 Mar 2021 23:05:19 +0000 Subject: [PATCH] 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 '))