fix: rewrite to allow referring to existing sections and reduce to single amount column

This commit is contained in:
casesolved-co-uk 2021-04-01 22:31:24 +00:00
parent 1c37390899
commit 2cb0da8780
2 changed files with 153 additions and 142 deletions

View File

@ -43,7 +43,7 @@ frappe.query_reports["Tax Detail"] = {
fieldname: "mode", fieldname: "mode",
label: __("Mode"), label: __("Mode"),
fieldtype: "Read Only", fieldtype: "Read Only",
default: "run", default: "edit",
hidden: 1, hidden: 1,
reqd: 1 reqd: 1
} }
@ -83,12 +83,12 @@ erpnext.TaxDetail = class TaxDetail {
// The last thing to run after datatable_render in refresh() // The last thing to run after datatable_render in refresh()
this.super.show_footer_message.apply(this.qr); this.super.show_footer_message.apply(this.qr);
if (this.qr.report_name !== 'Tax Detail') { if (this.qr.report_name !== 'Tax Detail') {
this.set_value_options();
this.show_help(); this.show_help();
if (this.loading) { if (this.loading) {
this.set_section(''); this.set_section('');
} else {
this.reload_component('');
} }
this.reload_filter();
} }
this.loading = false; this.loading = false;
} }
@ -134,6 +134,7 @@ erpnext.TaxDetail = class TaxDetail {
return new_items; return new_items;
} }
save_report() { save_report() {
this.check_datatable();
if (this.qr.report_name !== 'Tax Detail') { if (this.qr.report_name !== 'Tax Detail') {
frappe.call({ frappe.call({
method:'erpnext.accounts.report.tax_detail.tax_detail.save_custom_report', method:'erpnext.accounts.report.tax_detail.tax_detail.save_custom_report',
@ -152,55 +153,13 @@ erpnext.TaxDetail = class TaxDetail {
}); });
} }
} }
set_value_options() { check_datatable() {
// May be run with no columns or data if (!this.qr.datatable) {
if (this.qr.columns) { frappe.throw(__('Please change the date range to load data first'));
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'].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) { set_section(name) {
// Sets the given section name and then reloads the data // Sets the given section name and then reloads the data
this.controls['filter_index'].set_input('');
if (name && !this.sections[name]) { if (name && !this.sections[name]) {
this.sections[name] = {}; this.sections[name] = {};
} }
@ -225,43 +184,49 @@ erpnext.TaxDetail = class TaxDetail {
if (refresh) { if (refresh) {
this.qr.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(); const section_name = this.controls['section_name'].get_input_value();
if (section_name) { if (section_name) {
let fidx = this.controls['filter_index'].get_input_value(); const section = this.sections[section_name];
let section = this.sections[section_name]; const component_names = Object.keys(section);
let fidxs = Object.keys(section); component_names.unshift('');
fidxs.unshift(''); this.controls['component'].$wrapper.find("select").empty().add_options(component_names);
this.controls['filter_index'].$wrapper.find("select").empty().add_options(fidxs); this.controls['component'].set_input(component_name);
this.controls['filter_index'].set_input(fidx); if (component_name) {
this.controls['component_type'].set_input(section[component_name].type);
}
} else { } else {
this.controls['filter_index'].$wrapper.find("select").empty(); this.controls['component'].$wrapper.find("select").empty();
this.controls['filter_index'].set_input(''); this.controls['component'].set_input('');
} }
this.set_table_filters(); this.set_table_filters();
} }
set_table_filters() { set_table_filters() {
let filters = {}; let filters = {};
const section_name = this.controls['section_name'].get_input_value(); const section_name = this.controls['section_name'].get_input_value();
const fidx = this.controls['filter_index'].get_input_value(); const component_name = this.controls['component'].get_input_value();
if (section_name && fidx) { if (section_name && component_name) {
filters = this.sections[section_name][fidx]['filters']; 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.setAppliedFilters(filters);
this.set_value_label_from_filter();
} }
setAppliedFilters(filters) { setAppliedFilters(filters) {
Array.from(this.qr.datatable.header.querySelectorAll('.dt-filter')).map(function setFilters(input) { if (this.qr.datatable) {
let idx = input.dataset.colIndex; Array.from(this.qr.datatable.header.querySelectorAll('.dt-filter')).map(function setFilters(input) {
if (filters[idx]) { let idx = input.dataset.colIndex;
input.value = filters[idx]; if (filters[idx]) {
} else { input.value = filters[idx];
input.value = null; } else {
} input.value = null;
}); }
this.qr.datatable.columnmanager.applyFilter(filters); });
this.qr.datatable.columnmanager.applyFilter(filters);
}
} }
delete(name, type) { delete(name, type) {
if (type === 'section') { if (type === 'section') {
@ -269,11 +234,10 @@ erpnext.TaxDetail = class TaxDetail {
const new_section = Object.keys(this.sections)[0] || ''; const new_section = Object.keys(this.sections)[0] || '';
this.set_section(new_section); this.set_section(new_section);
} }
if (type === 'filter') { if (type === 'component') {
const cur_section = this.controls['section_name'].get_input_value(); const cur_section = this.controls['section_name'].get_input_value();
delete this.sections[cur_section][name]; delete this.sections[cur_section][name];
this.controls['filter_index'].set_input(''); this.reload_component('');
this.reload_filter();
} }
} }
create_controls() { create_controls() {
@ -293,7 +257,13 @@ erpnext.TaxDetail = class TaxDetail {
fieldtype: 'Button', fieldtype: 'Button',
fieldname: 'new_section', fieldname: 'new_section',
click: () => { 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({ controls['delete_section'] = this.qr.page.add_field({
@ -308,61 +278,87 @@ erpnext.TaxDetail = class TaxDetail {
} }
} }
}); });
controls['filter_index'] = this.qr.page.add_field({ controls['component'] = this.qr.page.add_field({
label: __('Filter'), label: __('Component'),
fieldtype: 'Select', fieldtype: 'Select',
fieldname: 'filter_index', fieldname: 'component',
change: (e) => { change: (e) => {
this.controls['filter_index'].set_input(this.controls['filter_index'].get_input_value()); this.reload_component(this.controls['component'].get_input_value());
this.set_table_filters();
} }
}); });
controls['add_filter'] = this.qr.page.add_field({ controls['component_type'] = this.qr.page.add_field({
label: __('Add Filter'), 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', fieldtype: 'Button',
fieldname: 'add_filter', fieldname: 'add_component',
click: () => { click: () => {
this.check_datatable();
let section_name = this.controls['section_name'].get_input_value(); let section_name = this.controls['section_name'].get_input_value();
if (section_name) { if (section_name) {
let prefix = 'Filter'; const component_type = this.controls['component_type'].get_input_value();
let data = { let idx = 0;
filters: this.qr.datatable.columnmanager.getAppliedFilters(), const names = Object.keys(this.sections[section_name]);
fieldname: this.get_value_fieldname() 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]); const filters = this.qr.datatable.columnmanager.getAppliedFilters();
let new_idx = prefix + '0'; if (component_type === 'filter') {
if (fidxs.length > 0) { const name = 'Filter' + idx.toString();
const fiidxs = fidxs.map((key) => parseInt(key.replace(prefix, ''))); let data = {
new_idx = prefix + (Math.max(...fiidxs) + 1).toString(); 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 { } 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({ controls['delete_component'] = this.qr.page.add_field({
label: __('Delete Filter'), label: __('Delete Component'),
fieldtype: 'Button', fieldtype: 'Button',
fieldname: 'delete_filter', fieldname: 'delete_component',
click: () => { click: () => {
let cur_filter = this.controls['filter_index'].get_input_value(); const component = this.controls['component'].get_input_value();
if (cur_filter) { if (component) {
frappe.confirm(__('Are you sure you want to delete filter ') + cur_filter + '?', frappe.confirm(__('Are you sure you want to delete component ') + component + '?',
() => {this.delete(cur_filter, 'filter')}); () => {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({ controls['save'] = this.qr.page.add_field({
label: __('Save & Run'), label: __('Save & Run'),
fieldtype: 'Button', fieldtype: 'Button',
@ -380,13 +376,16 @@ erpnext.TaxDetail = class TaxDetail {
this.controls = controls; this.controls = controls;
} }
show_help() { show_help() {
const help = __(`You can add multiple sections to your custom report using the New Section button above. const help = __(`<strong>Help:</strong> Your custom report is built from General Ledger Entries within the date range.
To specify what data goes in each section, specify column filters in the data table, then save with Add Filter. You can add multiple sections to the report using the New Section button.
Each section can have multiple filters added but be careful with the duplicated data rows. Each component added to a section adds a subset of the data into the specified section.
You can specify which Currency column will be summed for each filter in the final report with the Value Column Beware of duplicated data rows.
select box. Use the Show Detail box to see the data rows included in each section in the final report. The Filtered Row component type saves the datatable column filters to specify the added data.
Once you're done, hit Save & Run.`); The Section component type refers to the data in a previously defined section, but it cannot refer to its parent section.
this.qr.$report_footer.append(`<div class="col-md-12">${help}</div>`); 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(`<div class="col-md-12">${help}<a href="https://www.casesolved.co.uk"> Case Solved</a></div>`);
} }
} }

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe, json import frappe, json
from frappe import _ 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 # field lists in multiple doctypes will be coalesced
required_sql_fields = { required_sql_fields = {
@ -60,23 +60,35 @@ def run_report(report_name, data):
columns = report_config.get('columns') columns = report_config.get('columns')
sections = report_config.get('sections', {}) sections = report_config.get('sections', {})
show_detail = report_config.get('show_detail', 1) show_detail = report_config.get('show_detail', 1)
report = {}
new_data = [] new_data = []
summary = [] summary = []
for section_name, section in sections.items(): for section_name, section in sections.items():
section_total = 0.0 report[section_name] = {'rows': [], 'subtotal': 0.0}
for filt_name, filt in section.items(): for component_name, component in section.items():
value_field = filt['fieldname'] if component['type'] == 'filter':
rmidxs = [] for row in data:
for colno, filter_string in filt['filters'].items(): matched = True
filter_field = columns[int(colno) - 1]['fieldname'] for colno, filter_string in component['filters'].items():
for i, row in enumerate(data): filter_field = columns[int(colno) - 1]['fieldname']
if not filter_match(row[filter_field], filter_string): if not filter_match(row[filter_field], filter_string):
rmidxs += [i] matched = False
rows = [row for i, row in enumerate(data) if i not in rmidxs] break
section_total += subtotal(rows, value_field) if matched:
if show_detail: new_data += rows report[section_name]['rows'] += [row]
new_data += [ {columns[1]['fieldname']: section_name, columns[2]['fieldname']: section_total} ] report[section_name]['subtotal'] += row['amount']
summary += [ {'label': section_name, 'datatype': 'Currency', 'value': section_total} ] 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 += [ {} ] if show_detail: new_data += [ {} ]
return new_data or data, summary or None return new_data or data, summary or None
@ -123,11 +135,6 @@ def filter_match(value, string):
if operator == '<': return True if operator == '<': return True
return False 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(' ')) + '.' abbrev = lambda dt: ''.join(l[0].lower() for l in dt.split(' ')) + '.'
doclist = lambda dt, dfs: [abbrev(dt) + f for f in dfs] 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"]: if field in ["item_tax_rate", "base_net_amount"]:
return None return None
if doctype == "GL Entry" and field in ["debit", "credit"]:
column.update({"label": _("Amount"), "fieldname": "amount"})
if field == "taxes_and_charges": if field == "taxes_and_charges":
column.update({"label": _("Taxes and Charges Template")}) column.update({"label": _("Taxes and Charges Template")})
return column return column
@ -193,6 +203,8 @@ def modify_report_data(data):
import json import json
new_data = [] new_data = []
for line in 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 # Remove Invoice GL Tax Entries and generate Tax entries from the invoice lines
if "Invoice" in line.voucher_type: if "Invoice" in line.voucher_type:
if line.account_type != "Tax": if line.account_type != "Tax":
@ -204,11 +216,11 @@ def modify_report_data(data):
tax_line.account_type = "Tax" tax_line.account_type = "Tax"
tax_line.account = account tax_line.account = account
if line.voucher_type == "Sales Invoice": if line.voucher_type == "Sales Invoice":
line.credit = line.base_net_amount line.amount = line.base_net_amount
tax_line.credit = line.base_net_amount * (rate / 100) tax_line.amount = line.base_net_amount * (rate / 100)
if line.voucher_type == "Purchase Invoice": if line.voucher_type == "Purchase Invoice":
line.debit = line.base_net_amount line.amount = -line.base_net_amount
tax_line.debit = line.base_net_amount * (rate / 100) tax_line.amount = -line.base_net_amount * (rate / 100)
new_data += [tax_line] new_data += [tax_line]
else: else:
new_data += [line] new_data += [line]