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",
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(`<div class="col-md-12">${help}</div>`);
const help = __(`<strong>Help:</strong> 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(`<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
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]