diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index af6d8f26a73..751b46f4b35 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -23,7 +23,7 @@ jobs: run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules - name: Download semgrep - run: pip install semgrep==0.97.0 + run: pip install semgrep - name: Run Semgrep rules run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index ad1d200ec53..8ec055e28a4 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -145,10 +145,27 @@ frappe.ui.form.on('Payment Entry', { filters: filters }; }); - }, - refresh: function(frm) { - erpnext.hide_company(); + frm.set_query("sales_taxes_and_charges_template", function () { + return { + filters: { + company: frm.doc.company, + disabled: false, + }, + }; + }); + + frm.set_query("purchase_taxes_and_charges_template", function () { + return { + filters: { + company: frm.doc.company, + disabled: false, + }, + }; + }); + }, + refresh: function (frm) { + erpnext.hide_company(frm); frm.events.hide_unhide_fields(frm); frm.events.set_dynamic_labels(frm); frm.events.show_general_ledger(frm); diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index c26c7568924..1283f2fdaaf 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -343,12 +343,14 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): AND ja.party in %s AND j.apply_tds = 1 AND j.tax_withholding_category = %s + AND j.company = %s """, ( tax_details.from_date, tax_details.to_date, tuple(parties), tax_details.get("tax_withholding_category"), + company, ), as_dict=1, ) @@ -465,6 +467,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers): "unallocated_amount": (">", 0), "posting_date": ["between", (tax_details.from_date, tax_details.to_date)], "tax_withholding_category": tax_details.get("tax_withholding_category"), + "company": inv.company, } field = "sum(tax_withholding_net_total)" diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 794721b6c92..a780e466ba9 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -109,7 +109,7 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters): if total_credit: data.append(total_credit) - report_summary = get_bs_summary( + report_summary, primitive_summary = get_bs_summary( companies, asset, liability, @@ -180,7 +180,7 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters): chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss) - report_summary = get_pl_summary( + report_summary, primitive_summary = get_pl_summary( companies, "", income, expense, net_profit_loss, company_currency, filters, True ) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 3d0a2b78e5e..3431b76dfb1 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -199,6 +199,11 @@ frappe.query_reports["General Ledger"] = { label: __("Ignore Exchange Rate Revaluation Journals"), fieldtype: "Check", }, + { + fieldname: "ignore_cr_dr_notes", + label: __("Ignore System Generated Credit / Debit Notes"), + fieldtype: "Check", + }, ], }; diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 0c4e1eca3f9..05bbc4a79c9 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -234,6 +234,20 @@ def get_conditions(filters): if err_journals: filters.update({"voucher_no_not_in": [x[0] for x in err_journals]}) + if filters.get("ignore_cr_dr_notes"): + system_generated_cr_dr_journals = frappe.db.get_all( + "Journal Entry", + filters={ + "company": filters.get("company"), + "docstatus": 1, + "voucher_type": ("in", ["Credit Note", "Debit Note"]), + "is_system_generated": 1, + }, + as_list=True, + ) + if system_generated_cr_dr_journals: + filters.update({"voucher_no_not_in": [x[0] for x in system_generated_cr_dr_journals]}) + if filters.get("voucher_no_not_in"): conditions.append("voucher_no not in %(voucher_no_not_in)s") diff --git a/erpnext/accounts/report/general_ledger/test_general_ledger.py b/erpnext/accounts/report/general_ledger/test_general_ledger.py index 33b356feb33..ee2a6772892 100644 --- a/erpnext/accounts/report/general_ledger/test_general_ledger.py +++ b/erpnext/accounts/report/general_ledger/test_general_ledger.py @@ -2,13 +2,32 @@ # MIT License. See license.txt import frappe +from frappe import qb from frappe.tests.utils import FrappeTestCase from frappe.utils import flt, today +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.report.general_ledger.general_ledger import execute +from erpnext.controllers.sales_and_purchase_return import make_return_doc class TestGeneralLedger(FrappeTestCase): + def setUp(self): + self.company = "_Test Company" + self.clear_old_records() + + def clear_old_records(self): + doctype_list = [ + "GL Entry", + "Payment Ledger Entry", + "Sales Invoice", + "Purchase Invoice", + "Payment Entry", + "Journal Entry", + ] + for doctype in doctype_list: + qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run() + def test_foreign_account_balance_after_exchange_rate_revaluation(self): """ Checks the correctness of balance after exchange rate revaluation @@ -248,3 +267,68 @@ class TestGeneralLedger(FrappeTestCase): ) ) self.assertIn(revaluation_jv.name, set([x.voucher_no for x in data])) + + def test_ignore_cr_dr_notes_filter(self): + si = create_sales_invoice() + + cr_note = make_return_doc(si.doctype, si.name) + cr_note.submit() + + pr = frappe.get_doc("Payment Reconciliation") + pr.company = si.company + pr.party_type = "Customer" + pr.party = si.customer + pr.receivable_payable_account = si.debit_to + + pr.get_unreconciled_entries() + + invoices = [invoice.as_dict() for invoice in pr.invoices if invoice.invoice_number == si.name] + payments = [payment.as_dict() for payment in pr.payments if payment.reference_name == cr_note.name] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + + system_generated_journal = frappe.db.get_all( + "Journal Entry", + filters={ + "docstatus": 1, + "reference_type": si.doctype, + "reference_name": si.name, + "voucher_type": "Credit Note", + "is_system_generated": True, + }, + fields=["name"], + ) + self.assertEqual(len(system_generated_journal), 1) + expected = set([si.name, cr_note.name, system_generated_journal[0].name]) + # Without ignore_cr_dr_notes + columns, data = execute( + frappe._dict( + { + "company": si.company, + "from_date": si.posting_date, + "to_date": si.posting_date, + "account": [si.debit_to], + "group_by": "Group by Voucher (Consolidated)", + "ignore_cr_dr_notes": False, + } + ) + ) + actual = set([x.voucher_no for x in data if x.voucher_no]) + self.assertEqual(expected, actual) + + # Without ignore_cr_dr_notes + expected = set([si.name, cr_note.name]) + columns, data = execute( + frappe._dict( + { + "company": si.company, + "from_date": si.posting_date, + "to_date": si.posting_date, + "account": [si.debit_to], + "group_by": "Group by Voucher (Consolidated)", + "ignore_cr_dr_notes": True, + } + ) + ) + actual = set([x.voucher_no for x in data if x.voucher_no]) + self.assertEqual(expected, actual) diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index f55fef068c1..83f664c984a 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -289,7 +289,7 @@ def get_columns(additional_table_columns, filters): def apply_conditions(query, pi, pii, filters): - for opts in ("company", "supplier", "item_code", "mode_of_payment"): + for opts in ("company", "supplier", "mode_of_payment"): if filters.get(opts): query = query.where(pi[opts] == filters[opts]) @@ -299,6 +299,9 @@ def apply_conditions(query, pi, pii, filters): if filters.get("to_date"): query = query.where(pi.posting_date <= filters.get("to_date")) + if filters.get("item_code"): + query = query.where(pii.item_code == filters.get("item_code")) + if filters.get("item_group"): query = query.where(pii.item_group == filters.get("item_group")) diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js index d019b718ffb..6fb015f7b74 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js @@ -54,6 +54,12 @@ frappe.query_reports["Item-wise Sales Register"] = { fieldtype: "Link", options: "Brand", }, + { + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + options: "Item", + }, { fieldname: "item_group", label: __("Item Group"), diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 7bb73143c28..604c0a6569d 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -342,7 +342,7 @@ def get_columns(additional_table_columns, filters): def apply_conditions(query, si, sii, filters, additional_conditions=None): - for opts in ("company", "customer", "item_code"): + for opts in ("company", "customer"): if filters.get(opts): query = query.where(si[opts] == filters[opts]) @@ -371,6 +371,9 @@ def apply_conditions(query, si, sii, filters, additional_conditions=None): if filters.get("brand"): query = query.where(sii.brand == filters.get("brand")) + if filters.get("item_code"): + query = query.where(sii.item_code == filters.get("item_code")) + if filters.get("item_group"): query = query.where(sii.item_group == filters.get("item_group")) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 405eda8d09f..88e60d71d8d 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -362,11 +362,16 @@ class Asset(AccountsController): final_number_of_depreciations = cint(finance_book.total_number_of_depreciations) - cint( self.number_of_depreciations_booked ) - has_pro_rata = self.check_is_pro_rata(finance_book) + for_income_tax = 0 + if frappe.db.has_column("Finance Book", "for_income_tax"): + for_income_tax = frappe.db.get_value("Finance Book", finance_book.finance_book, "for_income_tax") + has_pro_rata = False + if not for_income_tax: + has_pro_rata = self.check_is_pro_rata(finance_book) depr_already_booked = any( [d.journal_entry for d in self.get("schedules") if d.finance_book == finance_book.finance_book] ) - if has_pro_rata and not depr_already_booked: + if has_pro_rata and not depr_already_booked and not for_income_tax: final_number_of_depreciations += 1 has_wdv_or_dd_non_yearly_pro_rata = False @@ -517,10 +522,13 @@ class Asset(AccountsController): ) # Adjust depreciation amount in the last period based on the expected value after useful life - if ( - n == cint(final_number_of_depreciations) - 1 - and flt(value_after_depreciation) != flt(finance_book.expected_value_after_useful_life) - ) or flt(value_after_depreciation) < flt(finance_book.expected_value_after_useful_life): + if not for_income_tax and ( + ( + n == cint(final_number_of_depreciations) - 1 + and flt(value_after_depreciation) != flt(finance_book.expected_value_after_useful_life) + ) + or flt(value_after_depreciation) < flt(finance_book.expected_value_after_useful_life) + ): depreciation_amount += flt(value_after_depreciation) - flt( finance_book.expected_value_after_useful_life ) diff --git a/erpnext/buying/utils.py b/erpnext/buying/utils.py index 1d708b36980..8223917792c 100644 --- a/erpnext/buying/utils.py +++ b/erpnext/buying/utils.py @@ -14,6 +14,9 @@ from erpnext.stock.doctype.item.item import get_last_purchase_details, validate_ def update_last_purchase_rate(doc, is_submit) -> None: """updates last_purchase_rate in item table for each item""" + if doc.get("is_internal_supplier"): + return + this_purchase_date = getdate(doc.get("posting_date") or doc.get("transaction_date")) for d in doc.get("items"): diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py index 9cc69d24a2b..e5d34231a87 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py @@ -8,7 +8,7 @@ from itertools import groupby import frappe from dateutil.relativedelta import relativedelta from frappe import _ -from frappe.utils import cint, flt +from frappe.utils import cint, flt, getdate from erpnext.setup.utils import get_exchange_rate @@ -21,7 +21,15 @@ class SalesPipelineAnalytics: def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) + def validate_filters(self): + if not self.filters.from_date: + frappe.throw(_("From Date is mandatory")) + + if not self.filters.to_date: + frappe.throw(_("To Date is mandatory")) + def run(self): + self.validate_filters() self.get_columns() self.get_data() self.get_chart_data() @@ -185,7 +193,7 @@ class SalesPipelineAnalytics: count_or_amount = info.get(based_on) if self.filters.get("pipeline_by") == "Owner": - if value == "Not Assigned" or value == "[]" or value is None: + if value == "Not Assigned" or value == "[]" or value is None or not value: assigned_to = ["Not Assigned"] else: assigned_to = json.loads(value) @@ -227,10 +235,9 @@ class SalesPipelineAnalytics: def get_month_list(self): month_list = [] - current_date = date.today() - month_number = date.today().month + current_date = getdate(self.filters.get("from_date")) - for _month in range(month_number, 13): + while current_date < getdate(self.filters.get("to_date")): month_list.append(current_date.strftime("%B")) current_date = current_date + relativedelta(months=1) diff --git a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py index 02d82b637e8..bf3f946d6ab 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py @@ -1,19 +1,21 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.crm.report.sales_pipeline_analytics.sales_pipeline_analytics import execute -class TestSalesPipelineAnalytics(unittest.TestCase): - @classmethod - def setUpClass(self): +class TestSalesPipelineAnalytics(FrappeTestCase): + def setUp(self): frappe.db.delete("Opportunity") create_company() create_customer() create_opportunity() def test_sales_pipeline_analytics(self): + self.from_date = "2021-01-01" + self.to_date = "2021-12-31" self.check_for_monthly_and_number() self.check_for_monthly_and_amount() self.check_for_quarterly_and_number() @@ -28,6 +30,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase): "status": "Open", "opportunity_type": "Sales", "company": "Best Test", + "from_date": self.from_date, + "to_date": self.to_date, } report = execute(filters) @@ -43,6 +47,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase): "status": "Open", "opportunity_type": "Sales", "company": "Best Test", + "from_date": self.from_date, + "to_date": self.to_date, } report = execute(filters) @@ -59,6 +65,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase): "status": "Open", "opportunity_type": "Sales", "company": "Best Test", + "from_date": self.from_date, + "to_date": self.to_date, } report = execute(filters) @@ -74,6 +82,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase): "status": "Open", "opportunity_type": "Sales", "company": "Best Test", + "from_date": self.from_date, + "to_date": self.to_date, } report = execute(filters) @@ -90,6 +100,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase): "status": "Open", "opportunity_type": "Sales", "company": "Best Test", + "from_date": self.from_date, + "to_date": self.to_date, } report = execute(filters) @@ -105,6 +117,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase): "status": "Open", "opportunity_type": "Sales", "company": "Best Test", + "from_date": self.from_date, + "to_date": self.to_date, } report = execute(filters) @@ -121,6 +135,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase): "status": "Open", "opportunity_type": "Sales", "company": "Best Test", + "from_date": self.from_date, + "to_date": self.to_date, } report = execute(filters) @@ -136,6 +152,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase): "status": "Open", "opportunity_type": "Sales", "company": "Best Test", + "from_date": self.from_date, + "to_date": self.to_date, } report = execute(filters) @@ -153,8 +171,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase): "opportunity_type": "Sales", "company": "Best Test", "opportunity_source": "Cold Calling", - "from_date": "2021-08-01", - "to_date": "2021-08-31", + "from_date": self.from_date, + "to_date": self.to_date, } report = execute(filters) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index d4b978b6e7e..bf7e5b9e48f 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -472,6 +472,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe quotation_to: me.frm.doc.quotation_to, supplier: me.frm.doc.supplier, currency: me.frm.doc.currency, + is_internal_supplier: me.frm.doc.is_internal_supplier, + is_internal_customer: me.frm.doc.is_internal_customer, update_stock: update_stock, conversion_rate: me.frm.doc.conversion_rate, price_list: me.frm.doc.selling_price_list || me.frm.doc.buying_price_list, @@ -1489,7 +1491,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe "update_stock": ['Sales Invoice', 'Purchase Invoice'].includes(me.frm.doc.doctype) ? cint(me.frm.doc.update_stock) : 0, "conversion_factor": me.frm.doc.conversion_factor, "pos_profile": me.frm.doc.doctype == 'Sales Invoice' ? me.frm.doc.pos_profile : '', - "coupon_code": me.frm.doc.coupon_code + "coupon_code": me.frm.doc.coupon_code, + "is_internal_supplier": me.frm.doc.is_internal_supplier, + "is_internal_customer": me.frm.doc.is_internal_customer, }; } diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.js b/erpnext/selling/page/sales_funnel/sales_funnel.js index d8546ba51ce..954705b5c32 100644 --- a/erpnext/selling/page/sales_funnel/sales_funnel.js +++ b/erpnext/selling/page/sales_funnel/sales_funnel.js @@ -193,6 +193,9 @@ erpnext.SalesFunnel = class SalesFunnel { this.options.width = ($(this.elements.funnel_wrapper).width() * 2.0) / 3.0; this.options.height = (Math.sqrt(3) * this.options.width) / 2.0; + const min_height = (this.options.height * 0.1) / this.options.data.length; + const height = this.options.height * 0.9; + // calculate total weightage // as height decreases, area decreases by the square of the reduction // hence, compensating by squaring the index value @@ -202,7 +205,7 @@ erpnext.SalesFunnel = class SalesFunnel { // calculate height for each data $.each(this.options.data, function (i, d) { - d.height = (me.options.height * d.value * Math.pow(i + 1, 2)) / me.options.total_weightage; + d.height = (height * d.value * Math.pow(i + 1, 2)) / me.options.total_weightage + min_height; }); this.elements.canvas = $("") diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 45b251f6630..0a8b7d407e4 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -855,6 +855,9 @@ def get_price_list_rate(args, item_doc, out=None): if price_list_rate is None or frappe.db.get_single_value( "Stock Settings", "update_existing_price_list_rate" ): + if args.get("is_internal_supplier") or args.get("is_internal_customer"): + return out + if args.price_list and args.rate: insert_item_price(args) @@ -866,7 +869,11 @@ def get_price_list_rate(args, item_doc, out=None): if frappe.db.get_single_value("Buying Settings", "disable_last_purchase_rate"): return out - if not out.price_list_rate and args.transaction_type == "buying": + if ( + not args.get("is_internal_supplier") + and not out.price_list_rate + and args.transaction_type == "buying" + ): from erpnext.stock.doctype.item.item import get_last_purchase_details out.update(get_last_purchase_details(item_doc.name, args.name, args.conversion_rate)) diff --git a/erpnext/stock/report/available_batch_report/available_batch_report.py b/erpnext/stock/report/available_batch_report/available_batch_report.py index cb651dd7972..99af98c23bb 100644 --- a/erpnext/stock/report/available_batch_report/available_batch_report.py +++ b/erpnext/stock/report/available_batch_report/available_batch_report.py @@ -54,6 +54,12 @@ def get_columns(filters): "width": 150, "options": "Batch", }, + { + "label": _("Expiry Date"), + "fieldname": "expiry_date", + "fieldtype": "Date", + "width": 120, + }, {"label": _("Balance Qty"), "fieldname": "balance_qty", "fieldtype": "Float", "width": 150}, ] ) @@ -96,6 +102,7 @@ def get_batchwise_data_from_stock_ledger(filters): table.item_code, table.batch_no, table.warehouse, + batch.expiry_date, Sum(table.actual_qty).as_("balance_qty"), ) .where(table.is_cancelled == 0) @@ -118,10 +125,14 @@ def get_query_based_on_filters(query, batch, table, filters): if filters.batch_no: query = query.where(batch.name == filters.batch_no) - if not filters.include_expired_batches: - query = query.where((batch.expiry_date >= today()) | (batch.expiry_date.isnull())) - if filters.to_date == today(): - query = query.where(batch.batch_qty > 0) + if filters.to_date == today(): + if not filters.include_expired_batches: + query = query.where((batch.expiry_date >= today()) | (batch.expiry_date.isnull())) + + query = query.where(batch.batch_qty > 0) + + else: + query = query.where(table.posting_date <= filters.to_date) if filters.warehouse: lft, rgt = frappe.db.get_value("Warehouse", filters.warehouse, ["lft", "rgt"])