mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-26 14:57:06 +00:00
Compare commits
11 Commits
develop
...
consolidat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a588cfce96 | ||
|
|
29c3169d82 | ||
|
|
31e0f3b981 | ||
|
|
e49047984c | ||
|
|
4ebdff4ad2 | ||
|
|
385b9403e0 | ||
|
|
c5db01cdcd | ||
|
|
6227c63729 | ||
|
|
4f419384c4 | ||
|
|
268e7f2d5e | ||
|
|
9d000a0529 |
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.query_reports["Consolidated Accounts Payable"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "companies",
|
||||
label: __("Companies"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Company",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Company", txt);
|
||||
},
|
||||
description: __("Totals are dropped when the companies use different currencies"),
|
||||
},
|
||||
{
|
||||
fieldname: "report_date",
|
||||
label: __("Report Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
fieldname: "party_type",
|
||||
label: __("Party Type"),
|
||||
fieldtype: "Autocomplete",
|
||||
options: get_party_type_options(),
|
||||
on_change: function () {
|
||||
frappe.query_report.set_filter_value("party", "");
|
||||
frappe.query_report.toggle_filter_display(
|
||||
"supplier_group",
|
||||
frappe.query_report.get_filter_value("party_type") !== "Supplier"
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
let party_type = frappe.query_report.get_filter_value("party_type");
|
||||
if (!party_type) return;
|
||||
|
||||
return frappe.db.get_link_options(party_type, txt);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "ageing_based_on",
|
||||
label: __("Ageing Based On"),
|
||||
fieldtype: "Select",
|
||||
options: "Posting Date\nDue Date\nSupplier Invoice Date",
|
||||
default: "Due Date",
|
||||
},
|
||||
{
|
||||
fieldname: "age_as_on",
|
||||
label: __("Age as on"),
|
||||
fieldtype: "Select",
|
||||
options: "Report Date\nToday",
|
||||
default: "Report Date",
|
||||
},
|
||||
{
|
||||
fieldname: "range",
|
||||
label: __("Ageing Range"),
|
||||
fieldtype: "Data",
|
||||
default: "30, 60, 90, 120",
|
||||
},
|
||||
{
|
||||
fieldname: "supplier_group",
|
||||
label: __("Supplier Group"),
|
||||
fieldtype: "Link",
|
||||
options: "Supplier Group",
|
||||
},
|
||||
{
|
||||
fieldname: "group_by_party",
|
||||
label: __("Group By Supplier"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "group_by_company",
|
||||
label: __("Group By Company"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "ignore_accounts",
|
||||
label: __("Group by Voucher"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "based_on_payment_terms",
|
||||
label: __("Based On Payment Terms"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "show_future_payments",
|
||||
label: __("Show Future Payments"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "show_remarks",
|
||||
label: __("Show Remarks"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "for_revaluation_journals",
|
||||
label: __("Revaluation Journals"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "in_party_currency",
|
||||
label: __("In Party Currency"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
collapsible_filters: true,
|
||||
separate_check_filters: true,
|
||||
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
if (data && data.bold) {
|
||||
value = value.bold();
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
||||
onload: function (report) {
|
||||
report.page.add_inner_button(__("Consolidated Accounts Payable Summary"), function () {
|
||||
frappe.set_route("query-report", "Consolidated Accounts Payable Summary", report.get_values());
|
||||
});
|
||||
|
||||
const company = frappe.defaults.get_user_default("Company");
|
||||
if (company && !(report.get_filter_value("companies") || []).length) {
|
||||
report.set_filter_value("companies", [company]);
|
||||
}
|
||||
|
||||
if (frappe.boot.sysdefaults.default_ageing_range) {
|
||||
report.set_filter_value("range", frappe.boot.sysdefaults.default_ageing_range);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function get_party_type_options() {
|
||||
let options = [];
|
||||
frappe.db
|
||||
.get_list("Party Type", { filters: { account_type: "Payable" }, fields: ["name"] })
|
||||
.then((res) => {
|
||||
res.forEach((party_type) => {
|
||||
options.push(party_type.name);
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2026-09-11 10:00:00.000000",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-09-11 10:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Consolidated Accounts Payable",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Invoice",
|
||||
"report_name": "Consolidated Accounts Payable",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Purchase User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from erpnext.accounts.report.consolidated_accounts_receivable.consolidated_accounts_receivable import (
|
||||
ConsolidatedReceivablePayable,
|
||||
)
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
args = {
|
||||
"account_type": "Payable",
|
||||
"naming_by": ["Buying Settings", "supp_master_name"],
|
||||
}
|
||||
return ConsolidatedReceivablePayable(filters).run(args)
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.query_reports["Consolidated Accounts Payable Summary"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "companies",
|
||||
label: __("Companies"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Company",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Company", txt);
|
||||
},
|
||||
description: __("Totals are dropped when the companies use different currencies"),
|
||||
},
|
||||
{
|
||||
fieldname: "report_date",
|
||||
label: __("Report Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
fieldname: "ageing_based_on",
|
||||
label: __("Ageing Based On"),
|
||||
fieldtype: "Select",
|
||||
options: "Posting Date\nDue Date\nSupplier Invoice Date",
|
||||
default: "Due Date",
|
||||
},
|
||||
{
|
||||
fieldname: "age_as_on",
|
||||
label: __("Age as on"),
|
||||
fieldtype: "Select",
|
||||
options: "Report Date\nToday",
|
||||
default: "Report Date",
|
||||
},
|
||||
{
|
||||
fieldname: "range",
|
||||
label: __("Ageing Range"),
|
||||
fieldtype: "Data",
|
||||
default: "30, 60, 90, 120",
|
||||
},
|
||||
{
|
||||
fieldname: "party_type",
|
||||
label: __("Party Type"),
|
||||
fieldtype: "Autocomplete",
|
||||
options: get_party_type_options(),
|
||||
on_change: function () {
|
||||
frappe.query_report.set_filter_value("party", "");
|
||||
frappe.query_report.toggle_filter_display(
|
||||
"supplier_group",
|
||||
frappe.query_report.get_filter_value("party_type") !== "Supplier"
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
let party_type = frappe.query_report.get_filter_value("party_type");
|
||||
if (!party_type) return;
|
||||
|
||||
return frappe.db.get_link_options(party_type, txt);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "supplier_group",
|
||||
label: __("Supplier Group"),
|
||||
fieldtype: "Link",
|
||||
options: "Supplier Group",
|
||||
},
|
||||
{
|
||||
fieldname: "show_future_payments",
|
||||
label: __("Show Future Payments"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "show_gl_balance",
|
||||
label: __("Show GL Balance"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "based_on_payment_terms",
|
||||
label: __("Based On Payment Terms"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "for_revaluation_journals",
|
||||
label: __("Revaluation Journals"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
collapsible_filters: true,
|
||||
separate_check_filters: true,
|
||||
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
if (data && data.bold) {
|
||||
value = value.bold();
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
||||
onload: function (report) {
|
||||
report.page.add_inner_button(__("Consolidated Accounts Payable"), function () {
|
||||
frappe.set_route("query-report", "Consolidated Accounts Payable", report.get_values());
|
||||
});
|
||||
|
||||
const company = frappe.defaults.get_user_default("Company");
|
||||
if (company && !(report.get_filter_value("companies") || []).length) {
|
||||
report.set_filter_value("companies", [company]);
|
||||
}
|
||||
|
||||
if (frappe.boot.sysdefaults.default_ageing_range) {
|
||||
report.set_filter_value("range", frappe.boot.sysdefaults.default_ageing_range);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function get_party_type_options() {
|
||||
let options = [];
|
||||
frappe.db
|
||||
.get_list("Party Type", { filters: { account_type: "Payable" }, fields: ["name"] })
|
||||
.then((res) => {
|
||||
res.forEach((party_type) => {
|
||||
options.push(party_type.name);
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2026-08-07 10:00:00.000000",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-08-07 10:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Consolidated Accounts Payable Summary",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Invoice",
|
||||
"report_name": "Consolidated Accounts Payable Summary",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Purchase User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from erpnext.accounts.report.consolidated_accounts_receivable_summary.consolidated_accounts_receivable_summary import (
|
||||
ConsolidatedReceivablePayableSummary,
|
||||
)
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
args = {
|
||||
"account_type": "Payable",
|
||||
"naming_by": ["Buying Settings", "supp_master_name"],
|
||||
}
|
||||
return ConsolidatedReceivablePayableSummary(filters).run(args)
|
||||
@@ -0,0 +1,184 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.query_reports["Consolidated Accounts Receivable"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "companies",
|
||||
label: __("Companies"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Company",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Company", txt);
|
||||
},
|
||||
description: __("Totals are dropped when the companies use different currencies"),
|
||||
},
|
||||
{
|
||||
fieldname: "report_date",
|
||||
label: __("Report Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
fieldname: "party_type",
|
||||
label: __("Party Type"),
|
||||
fieldtype: "Autocomplete",
|
||||
options: get_party_type_options(),
|
||||
on_change: function () {
|
||||
frappe.query_report.set_filter_value("party", "");
|
||||
frappe.query_report.toggle_filter_display(
|
||||
"customer_group",
|
||||
frappe.query_report.get_filter_value("party_type") !== "Customer"
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
let party_type = frappe.query_report.get_filter_value("party_type");
|
||||
if (!party_type) return;
|
||||
|
||||
return frappe.db.get_link_options(party_type, txt);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "ageing_based_on",
|
||||
label: __("Ageing Based On"),
|
||||
fieldtype: "Select",
|
||||
options: "Posting Date\nDue Date",
|
||||
default: "Due Date",
|
||||
},
|
||||
{
|
||||
fieldname: "age_as_on",
|
||||
label: __("Age as on"),
|
||||
fieldtype: "Select",
|
||||
options: "Report Date\nToday",
|
||||
default: "Report Date",
|
||||
},
|
||||
{
|
||||
fieldname: "range",
|
||||
label: __("Ageing Range"),
|
||||
fieldtype: "Data",
|
||||
default: "30, 60, 90, 120",
|
||||
},
|
||||
{
|
||||
fieldname: "customer_group",
|
||||
label: __("Customer Group"),
|
||||
fieldtype: "Link",
|
||||
options: "Customer Group",
|
||||
},
|
||||
{
|
||||
fieldname: "territory",
|
||||
label: __("Territory"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Territory",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Territory", txt);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "sales_partner",
|
||||
label: __("Sales Partner"),
|
||||
fieldtype: "Link",
|
||||
options: "Sales Partner",
|
||||
},
|
||||
{
|
||||
fieldname: "sales_person",
|
||||
label: __("Sales Person"),
|
||||
fieldtype: "Link",
|
||||
options: "Sales Person",
|
||||
},
|
||||
{
|
||||
fieldname: "group_by_party",
|
||||
label: __("Group By Customer"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "group_by_company",
|
||||
label: __("Group By Company"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "ignore_accounts",
|
||||
label: __("Group by Voucher"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "based_on_payment_terms",
|
||||
label: __("Based On Payment Terms"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "show_future_payments",
|
||||
label: __("Show Future Payments"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "show_remarks",
|
||||
label: __("Show Remarks"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "show_sales_person",
|
||||
label: __("Show Sales Person"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "show_delivery_notes",
|
||||
label: __("Show Linked Delivery Notes"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "for_revaluation_journals",
|
||||
label: __("Revaluation Journals"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "in_party_currency",
|
||||
label: __("In Party Currency"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
collapsible_filters: true,
|
||||
separate_check_filters: true,
|
||||
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
if (data && data.bold) {
|
||||
value = value.bold();
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
||||
onload: function (report) {
|
||||
report.page.add_inner_button(__("Consolidated Accounts Receivable Summary"), function () {
|
||||
frappe.set_route("query-report", "Consolidated Accounts Receivable Summary", report.get_values());
|
||||
});
|
||||
|
||||
const company = frappe.defaults.get_user_default("Company");
|
||||
if (company && !(report.get_filter_value("companies") || []).length) {
|
||||
report.set_filter_value("companies", [company]);
|
||||
}
|
||||
|
||||
if (frappe.boot.sysdefaults.default_ageing_range) {
|
||||
report.set_filter_value("range", frappe.boot.sysdefaults.default_ageing_range);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function get_party_type_options() {
|
||||
let options = [];
|
||||
frappe.db
|
||||
.get_list("Party Type", { filters: { account_type: "Receivable" }, fields: ["name"] })
|
||||
.then((res) => {
|
||||
res.forEach((party_type) => {
|
||||
options.push(party_type.name);
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2026-09-11 10:00:00.000000",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-09-11 10:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Consolidated Accounts Receivable",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Sales Invoice",
|
||||
"report_name": "Consolidated Accounts Receivable",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model import numeric_fieldtypes
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
|
||||
from erpnext.accounts.report.consolidated_financial_statement.consolidated_financial_statement import (
|
||||
get_subsidiary_companies,
|
||||
)
|
||||
|
||||
# Outstanding invoices of a party across companies that need not be related to each other.
|
||||
# Every row carries the company it belongs to, and a party's companies sit together.
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
args = {
|
||||
"account_type": "Receivable",
|
||||
"naming_by": ["Selling Settings", "cust_master_name"],
|
||||
}
|
||||
return ConsolidatedReceivablePayable(filters).run(args)
|
||||
|
||||
|
||||
class ConsolidatedReceivablePayable(ReceivablePayableReport):
|
||||
def run(self, args):
|
||||
self.companies = get_consolidated_companies(self.filters)
|
||||
self.args = args # the engine's get_data() takes no arguments, so hand them over here
|
||||
columns, data, _message, chart, _report_summary, skip_total_row = super().run(args)
|
||||
|
||||
if self.filters.get("group_by_company") or len(row_currencies(data)) > 1:
|
||||
# a grand total would either double count the subtotals or add unlike currencies
|
||||
skip_total_row = 1
|
||||
|
||||
return columns, data, None, chart, None, skip_total_row
|
||||
|
||||
def get_columns(self):
|
||||
super().get_columns()
|
||||
add_company_columns(self.columns)
|
||||
|
||||
def get_data(self):
|
||||
# party wins when both are checked, its subtotal is the point of this report
|
||||
if self.filters.get("group_by_party"):
|
||||
group_by, subtotal_of = "party", self.party_subtotal
|
||||
elif self.filters.get("group_by_company"):
|
||||
group_by, subtotal_of = "company", self.company_subtotal
|
||||
else:
|
||||
group_by, subtotal_of = "party", None
|
||||
|
||||
self.data = []
|
||||
for rows in self.get_grouped_rows(group_by).values():
|
||||
self.data.extend(rows)
|
||||
if subtotal_of and len(row_currencies(rows)) <= 1:
|
||||
self.data.append(subtotal_of(rows))
|
||||
self.data.append({}) # blank separator, like the engine's own grouping
|
||||
|
||||
def get_grouped_rows(self, group_by):
|
||||
"""Rows of every company, regrouped so those sharing `group_by` sit together."""
|
||||
grouped = OrderedDict()
|
||||
for row in rows_per_company(self.companies, self.filters, self.args, ReceivablePayableReport):
|
||||
grouped.setdefault(row.get(group_by), []).append(row)
|
||||
|
||||
return grouped
|
||||
|
||||
def party_subtotal(self, rows):
|
||||
return self.subtotal(rows, party=rows[0].party)
|
||||
|
||||
def company_subtotal(self, rows):
|
||||
return self.subtotal(rows, company=rows[0].company)
|
||||
|
||||
def subtotal(self, rows, **label):
|
||||
# same shape as the engine's own group-by-party subtotal
|
||||
subtotal = frappe._dict(currency=rows[0].get("currency"), bold=1, **label)
|
||||
for field in self.get_currency_fields():
|
||||
subtotal[field] = sum(flt(row.get(field)) for row in rows)
|
||||
|
||||
return subtotal
|
||||
|
||||
|
||||
def rows_per_company(companies, filters, args, engine):
|
||||
"""Run `engine` once per company, tagging every row with the company it came from."""
|
||||
for company in companies:
|
||||
# subtotals are appended once per group by the caller, not once per company
|
||||
company_filters = frappe._dict(filters, company=company, group_by_party=0)
|
||||
company_filters.pop("companies", None)
|
||||
|
||||
parent = frappe.get_cached_value("Company", company, "parent_company")
|
||||
for row in engine(company_filters).run(args)[1]:
|
||||
row.company, row.parent_company = company, parent
|
||||
yield row
|
||||
|
||||
|
||||
def get_consolidated_companies(filters):
|
||||
"""Selected companies, a group company standing for the companies under it."""
|
||||
companies = []
|
||||
for selected in filters.get("companies") or []:
|
||||
for company in get_subsidiary_companies(selected):
|
||||
if company not in companies:
|
||||
companies.append(company)
|
||||
|
||||
return companies
|
||||
|
||||
|
||||
def row_currencies(rows):
|
||||
return {row.get("currency") for row in rows if row.get("currency")}
|
||||
|
||||
|
||||
def add_company_columns(columns):
|
||||
"""Company and its parent, right after the party columns, plus header alignment."""
|
||||
at = company_column_index(columns)
|
||||
columns.insert(
|
||||
at,
|
||||
dict(
|
||||
label=_("Company"),
|
||||
fieldname="company",
|
||||
fieldtype="Link",
|
||||
options="Company",
|
||||
width=180,
|
||||
sticky=True,
|
||||
),
|
||||
)
|
||||
columns.insert(
|
||||
at + 1,
|
||||
dict(
|
||||
label=_("Parent Company"),
|
||||
fieldname="parent_company",
|
||||
fieldtype="Link",
|
||||
options="Company",
|
||||
width=160,
|
||||
),
|
||||
)
|
||||
|
||||
# datatable guesses alignment from the first row, which misreads an empty column
|
||||
for column in columns:
|
||||
column["align"] = "right" if column["fieldtype"] in numeric_fieldtypes else "left"
|
||||
|
||||
|
||||
def company_column_index(columns):
|
||||
fieldnames = [column["fieldname"] for column in columns]
|
||||
for fieldname in ("party_name", "party"):
|
||||
if fieldname in fieldnames:
|
||||
return fieldnames.index(fieldname) + 1
|
||||
|
||||
return 0
|
||||
@@ -0,0 +1,133 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.consolidated_accounts_receivable.consolidated_accounts_receivable import (
|
||||
execute,
|
||||
)
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class ConsolidatedReportMixin(AccountsTestMixin):
|
||||
"""Companies, invoices and filters shared by the consolidated report tests."""
|
||||
|
||||
def filters(self, companies=None, **kwargs):
|
||||
return {
|
||||
# an explicit empty list means "nothing selected", so do not fall back on it
|
||||
"companies": [self.company_a, self.company_b] if companies is None else companies,
|
||||
"party_type": "Customer",
|
||||
"party": [self.customer],
|
||||
"report_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
**kwargs,
|
||||
}
|
||||
|
||||
def create_invoice(self, company, abbr, rate, customer=None, currency=None):
|
||||
return create_sales_invoice(
|
||||
item=self.item,
|
||||
company=company,
|
||||
customer=customer or self.customer,
|
||||
currency=currency,
|
||||
debit_to=f"Debtors - {abbr}",
|
||||
income_account=f"Sales - {abbr}",
|
||||
cost_center=f"Main - {abbr}",
|
||||
parent_cost_center=f"Main - {abbr}",
|
||||
warehouse=f"Stores - {abbr}",
|
||||
posting_date=today(),
|
||||
rate=rate,
|
||||
price_list_rate=rate,
|
||||
)
|
||||
|
||||
def create_test_company(self, company_name, abbr, currency="INR", is_group=0, parent=None):
|
||||
if frappe.db.exists("Company", company_name):
|
||||
return company_name
|
||||
|
||||
company = frappe.new_doc("Company")
|
||||
company.company_name = company_name
|
||||
company.abbr = abbr
|
||||
company.country = "India"
|
||||
company.default_currency = currency
|
||||
company.create_chart_of_accounts_based_on = "Standard Template"
|
||||
company.chart_of_accounts = "Standard"
|
||||
company.is_group = is_group
|
||||
company.parent_company = parent
|
||||
company.insert()
|
||||
|
||||
return company.name
|
||||
|
||||
|
||||
class TestConsolidatedAccountsReceivable(ERPNextTestSuite, ConsolidatedReportMixin):
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
# deliberately unrelated companies, there is no parent/child link between them
|
||||
self.company_a = self.create_test_company("_Test Unrelated A", "_TUNA")
|
||||
self.company_b = self.create_test_company("_Test Unrelated B", "_TUNB")
|
||||
self.create_customer("_Test Consolidation Customer")
|
||||
# the mixin would otherwise pass company=None, which the Item Default row rejects
|
||||
self.create_item("_Test Consolidation Item", company=self.company_a)
|
||||
|
||||
def test_rows_carry_the_company_they_came_from(self):
|
||||
self.create_invoice(self.company_a, "_TUNA", 200)
|
||||
self.create_invoice(self.company_b, "_TUNB", 300)
|
||||
|
||||
rows = execute(self.filters())[1]
|
||||
|
||||
self.assertEqual([r.company for r in rows], [self.company_a, self.company_b])
|
||||
self.assertFalse(any(r.parent_company for r in rows))
|
||||
|
||||
def test_group_by_party_adds_one_subtotal_for_all_companies(self):
|
||||
self.create_invoice(self.company_a, "_TUNA", 200)
|
||||
self.create_invoice(self.company_b, "_TUNB", 300)
|
||||
|
||||
result = execute(self.filters(group_by_party=1))
|
||||
subtotals = [row for row in result[1] if row.get("bold")]
|
||||
|
||||
self.assertEqual(len(subtotals), 1)
|
||||
self.assertEqual(subtotals[0].outstanding, 500.0)
|
||||
self.assertEqual(result[5], 1) # skip_total_row, else the grand total double counts
|
||||
|
||||
def test_group_by_company_subtotals_each_company(self):
|
||||
self.create_invoice(self.company_a, "_TUNA", 200)
|
||||
self.create_invoice(self.company_b, "_TUNB", 300)
|
||||
|
||||
result = execute(self.filters(group_by_company=1))
|
||||
subtotals = [row for row in result[1] if row.get("bold")]
|
||||
|
||||
self.assertEqual([s.company for s in subtotals], [self.company_a, self.company_b])
|
||||
self.assertEqual([s.outstanding for s in subtotals], [200.0, 300.0])
|
||||
self.assertEqual(result[5], 1)
|
||||
|
||||
def test_party_grouping_wins_when_both_are_selected(self):
|
||||
self.create_invoice(self.company_a, "_TUNA", 200)
|
||||
self.create_invoice(self.company_b, "_TUNB", 300)
|
||||
|
||||
subtotals = [
|
||||
row for row in execute(self.filters(group_by_party=1, group_by_company=1))[1] if row.get("bold")
|
||||
]
|
||||
|
||||
self.assertEqual(len(subtotals), 1)
|
||||
self.assertEqual(subtotals[0].party, self.customer)
|
||||
|
||||
def test_no_total_when_companies_use_different_currencies(self):
|
||||
usd = self.create_test_company("_Test Unrelated USD", "_TUNU", currency="USD")
|
||||
self.create_invoice(self.company_a, "_TUNA", 200)
|
||||
self.create_invoice(usd, "_TUNU", 300, currency="USD")
|
||||
|
||||
result = execute(self.filters(companies=[self.company_a, usd]))
|
||||
|
||||
self.assertEqual([r.company for r in result[1]], [self.company_a, usd])
|
||||
self.assertEqual(result[5], 1) # skip_total_row, the rows span currencies
|
||||
|
||||
def test_group_company_expands_to_its_subsidiaries(self):
|
||||
group = self.create_test_company("_Test Consolidation Group", "_TCGRP", is_group=1)
|
||||
child = self.create_test_company("_Test Consolidation Child", "_TCCLD", parent=group)
|
||||
self.create_invoice(child, "_TCCLD", 400)
|
||||
|
||||
rows = execute(self.filters(companies=[group]))[1]
|
||||
|
||||
self.assertEqual([r.company for r in rows], [child])
|
||||
self.assertEqual([r.parent_company for r in rows], [group])
|
||||
@@ -0,0 +1,159 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.query_reports["Consolidated Accounts Receivable Summary"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "companies",
|
||||
label: __("Companies"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Company",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Company", txt);
|
||||
},
|
||||
description: __("Totals are dropped when the companies use different currencies"),
|
||||
},
|
||||
{
|
||||
fieldname: "report_date",
|
||||
label: __("Report Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
fieldname: "ageing_based_on",
|
||||
label: __("Ageing Based On"),
|
||||
fieldtype: "Select",
|
||||
options: "Posting Date\nDue Date",
|
||||
default: "Due Date",
|
||||
},
|
||||
{
|
||||
fieldname: "age_as_on",
|
||||
label: __("Age as on"),
|
||||
fieldtype: "Select",
|
||||
options: "Report Date\nToday",
|
||||
default: "Report Date",
|
||||
},
|
||||
{
|
||||
fieldname: "range",
|
||||
label: __("Ageing Range"),
|
||||
fieldtype: "Data",
|
||||
default: "30, 60, 90, 120",
|
||||
},
|
||||
{
|
||||
fieldname: "party_type",
|
||||
label: __("Party Type"),
|
||||
fieldtype: "Autocomplete",
|
||||
options: get_party_type_options(),
|
||||
on_change: function () {
|
||||
frappe.query_report.set_filter_value("party", "");
|
||||
frappe.query_report.toggle_filter_display(
|
||||
"customer_group",
|
||||
frappe.query_report.get_filter_value("party_type") !== "Customer"
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
let party_type = frappe.query_report.get_filter_value("party_type");
|
||||
if (!party_type) return;
|
||||
|
||||
return frappe.db.get_link_options(party_type, txt);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "customer_group",
|
||||
label: __("Customer Group"),
|
||||
fieldtype: "Link",
|
||||
options: "Customer Group",
|
||||
},
|
||||
{
|
||||
fieldname: "territory",
|
||||
label: __("Territory"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Territory",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Territory", txt);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "show_future_payments",
|
||||
label: __("Show Future Payments"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "sales_partner",
|
||||
label: __("Sales Partner"),
|
||||
fieldtype: "Link",
|
||||
options: "Sales Partner",
|
||||
},
|
||||
{
|
||||
fieldname: "sales_person",
|
||||
label: __("Sales Person"),
|
||||
fieldtype: "Link",
|
||||
options: "Sales Person",
|
||||
},
|
||||
{
|
||||
fieldname: "show_gl_balance",
|
||||
label: __("Show GL Balance"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "show_sales_person",
|
||||
label: __("Show Sales Person"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "based_on_payment_terms",
|
||||
label: __("Based On Payment Terms"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "for_revaluation_journals",
|
||||
label: __("Revaluation Journals"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
collapsible_filters: true,
|
||||
separate_check_filters: true,
|
||||
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
if (data && data.bold) {
|
||||
value = value.bold();
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
||||
onload: function (report) {
|
||||
report.page.add_inner_button(__("Consolidated Accounts Receivable"), function () {
|
||||
frappe.set_route("query-report", "Consolidated Accounts Receivable", report.get_values());
|
||||
});
|
||||
|
||||
const company = frappe.defaults.get_user_default("Company");
|
||||
if (company && !(report.get_filter_value("companies") || []).length) {
|
||||
report.set_filter_value("companies", [company]);
|
||||
}
|
||||
|
||||
if (frappe.boot.sysdefaults.default_ageing_range) {
|
||||
report.set_filter_value("range", frappe.boot.sysdefaults.default_ageing_range);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function get_party_type_options() {
|
||||
let options = [];
|
||||
frappe.db
|
||||
.get_list("Party Type", { filters: { account_type: "Receivable" }, fields: ["name"] })
|
||||
.then((res) => {
|
||||
res.forEach((party_type) => {
|
||||
options.push(party_type.name);
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2026-08-07 10:00:00.000000",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-08-07 10:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Consolidated Accounts Receivable Summary",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Sales Invoice",
|
||||
"report_name": "Consolidated Accounts Receivable Summary",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import (
|
||||
AccountsReceivableSummary,
|
||||
)
|
||||
from erpnext.accounts.report.consolidated_accounts_receivable.consolidated_accounts_receivable import (
|
||||
add_company_columns,
|
||||
get_consolidated_companies,
|
||||
row_currencies,
|
||||
rows_per_company,
|
||||
)
|
||||
|
||||
# What a party owes (or is owed) across companies that need not be related to each other.
|
||||
# Each party gets one row per company, followed by a total row for that party.
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
args = {
|
||||
"account_type": "Receivable",
|
||||
"naming_by": ["Selling Settings", "cust_master_name"],
|
||||
}
|
||||
return ConsolidatedReceivablePayableSummary(filters).run(args)
|
||||
|
||||
|
||||
class ConsolidatedReceivablePayableSummary(AccountsReceivableSummary):
|
||||
def run(self, args):
|
||||
self.companies = get_consolidated_companies(self.filters)
|
||||
return super().run(args)
|
||||
|
||||
def get_columns(self):
|
||||
super().get_columns()
|
||||
add_company_columns(self.columns)
|
||||
|
||||
def get_data(self, args):
|
||||
self.data = []
|
||||
for rows in self.get_rows_by_party(args).values():
|
||||
self.data.extend(rows)
|
||||
# a total over companies of differing currencies would add unlike units
|
||||
if len(row_currencies(rows)) <= 1:
|
||||
self.data.append(self.total_row(rows))
|
||||
|
||||
def get_rows_by_party(self, args):
|
||||
"""Rows of every company, regrouped so a party's companies sit together."""
|
||||
by_party = OrderedDict()
|
||||
for row in rows_per_company(self.companies, self.filters, args, AccountsReceivableSummary):
|
||||
by_party.setdefault(row.party, []).append(row)
|
||||
|
||||
return by_party
|
||||
|
||||
def total_row(self, rows):
|
||||
# label sits in the first column, like the total row of the plain summary reports;
|
||||
# `bold` is picked up by the formatter in the report's js
|
||||
total = frappe._dict(
|
||||
party_type=_("Total"),
|
||||
party="",
|
||||
company="",
|
||||
currency=rows[0].get("currency"),
|
||||
bold=1,
|
||||
)
|
||||
for row in rows:
|
||||
for field, value in row.items():
|
||||
# `advance` arrives as an int when there is none, so don't filter on float alone
|
||||
if isinstance(value, int | float) and not isinstance(value, bool):
|
||||
total[field] = flt(total.get(field)) + value
|
||||
|
||||
return total
|
||||
@@ -0,0 +1,98 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import (
|
||||
execute as execute_summary,
|
||||
)
|
||||
from erpnext.accounts.report.consolidated_accounts_receivable.test_consolidated_accounts_receivable import (
|
||||
ConsolidatedReportMixin,
|
||||
)
|
||||
from erpnext.accounts.report.consolidated_accounts_receivable_summary.consolidated_accounts_receivable_summary import (
|
||||
execute,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestConsolidatedAccountsReceivableSummary(ERPNextTestSuite, ConsolidatedReportMixin):
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
# deliberately unrelated companies, there is no parent/child link between them
|
||||
self.company_a = self.create_test_company("_Test Unrelated A", "_TUNA")
|
||||
self.company_b = self.create_test_company("_Test Unrelated B", "_TUNB")
|
||||
self.create_customer("_Test Consolidation Customer")
|
||||
# the mixin would otherwise pass company=None, which the Item Default row rejects
|
||||
self.create_item("_Test Consolidation Item", company=self.company_a)
|
||||
|
||||
def test_party_gets_a_row_per_company_plus_a_total(self):
|
||||
self.create_invoice(self.company_a, "_TUNA", 200)
|
||||
self.create_invoice(self.company_b, "_TUNB", 300)
|
||||
|
||||
rows = execute(self.filters())[1]
|
||||
|
||||
self.assertEqual([r.company for r in rows], [self.company_a, self.company_b, ""])
|
||||
self.assertEqual([r.party for r in rows], [self.customer, self.customer, ""])
|
||||
self.assertEqual([r.outstanding for r in rows], [200.0, 300.0, 500.0])
|
||||
self.assertEqual(rows[-1].party_type, "Total")
|
||||
self.assertTrue(rows[-1].bold)
|
||||
|
||||
def test_total_matches_individual_company_summaries(self):
|
||||
self.create_invoice(self.company_a, "_TUNA", 200)
|
||||
self.create_invoice(self.company_b, "_TUNB", 300)
|
||||
|
||||
total = execute(self.filters())[1][-1].outstanding
|
||||
individual = sum(self.company_outstanding(company) for company in (self.company_a, self.company_b))
|
||||
|
||||
self.assertEqual(total, individual)
|
||||
|
||||
def test_company_without_transactions_is_omitted(self):
|
||||
self.create_invoice(self.company_a, "_TUNA", 200)
|
||||
|
||||
rows = execute(self.filters())[1]
|
||||
|
||||
self.assertEqual([r.company for r in rows], [self.company_a, ""])
|
||||
self.assertEqual(rows[-1].outstanding, 200.0)
|
||||
|
||||
def test_no_companies_selected_returns_nothing(self):
|
||||
self.create_invoice(self.company_a, "_TUNA", 200)
|
||||
|
||||
self.assertEqual(execute(self.filters(companies=[]))[1], [])
|
||||
|
||||
def test_all_parties_are_shown_when_no_party_is_selected(self):
|
||||
self.create_customer("_Test Second Consolidation Customer")
|
||||
other = self.customer
|
||||
self.create_customer("_Test Consolidation Customer")
|
||||
|
||||
self.create_invoice(self.company_a, "_TUNA", 200)
|
||||
self.create_invoice(self.company_a, "_TUNA", 300, customer=other)
|
||||
|
||||
filters = self.filters()
|
||||
filters.pop("party")
|
||||
parties = {r.party for r in execute(filters)[1] if r.party}
|
||||
|
||||
self.assertEqual(parties, {self.customer, other})
|
||||
|
||||
def test_group_by_party_carried_over_from_the_detail_report_is_ignored(self):
|
||||
# the detail report's button hands its own filters over, and the engine would
|
||||
# otherwise answer with separator rows the summary cannot total
|
||||
self.create_invoice(self.company_a, "_TUNA", 200)
|
||||
|
||||
rows = execute(self.filters(group_by_party=1))[1]
|
||||
|
||||
self.assertEqual([r.outstanding for r in rows], [200.0, 200.0])
|
||||
|
||||
def test_no_total_when_companies_use_different_currencies(self):
|
||||
usd = self.create_test_company("_Test Unrelated USD", "_TUNU", currency="USD")
|
||||
self.create_invoice(self.company_a, "_TUNA", 200)
|
||||
self.create_invoice(usd, "_TUNU", 300, currency="USD")
|
||||
|
||||
rows = execute(self.filters(companies=[self.company_a, usd]))[1]
|
||||
|
||||
self.assertEqual([r.company for r in rows], [self.company_a, usd])
|
||||
self.assertFalse(any(row.get("bold") for row in rows))
|
||||
|
||||
# ---------- helpers ----------
|
||||
def company_outstanding(self, company):
|
||||
filters = {"company": company, "report_date": today(), "range": "30, 60, 90, 120"}
|
||||
return sum(r.outstanding for r in execute_summary(filters)[1] if r.party == self.customer)
|
||||
Reference in New Issue
Block a user