diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
index d9cf1bb3b34..f2380bde125 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
@@ -137,7 +137,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
"erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
args: {
bank_account: frm.doc.bank_account,
- till_date: frm.doc.bank_statement_from_date,
+ till_date: frappe.datetime.add_days(frm.doc.bank_statement_from_date, -1)
},
callback: (response) => {
frm.set_value("account_opening_balance", response.message);
diff --git a/erpnext/accounts/doctype/cashier_closing/cashier_closing.json b/erpnext/accounts/doctype/cashier_closing/cashier_closing.json
index 1b38f0d36d7..051b44b5868 100644
--- a/erpnext/accounts/doctype/cashier_closing/cashier_closing.json
+++ b/erpnext/accounts/doctype/cashier_closing/cashier_closing.json
@@ -1,457 +1,152 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
+ "actions": [],
"autoname": "naming_series:",
- "beta": 0,
"creation": "2018-06-18 16:51:49.994750",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
- "document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
+ "field_order": [
+ "naming_series",
+ "user",
+ "date",
+ "from_time",
+ "time",
+ "expense",
+ "custody",
+ "returns",
+ "outstanding_amount",
+ "payments",
+ "net_amount",
+ "amended_from"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "POS-CLO-",
"fieldname": "naming_series",
"fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 1,
- "in_list_view": 0,
"in_standard_filter": 1,
"label": "Series",
- "length": 0,
- "no_copy": 0,
"options": "POS-CLO-",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "user",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
"in_filter": 1,
- "in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "User",
- "length": 0,
- "no_copy": 0,
"options": "User",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "Today",
"fieldname": "date",
"fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
"in_filter": 1,
- "in_global_search": 0,
- "in_list_view": 0,
"in_standard_filter": 1,
"label": "Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "from_time",
"fieldtype": "Time",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
"in_filter": 1,
- "in_global_search": 0,
- "in_list_view": 0,
"in_standard_filter": 1,
"label": "From Time",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
"fieldname": "time",
"fieldtype": "Time",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
"in_filter": 1,
- "in_global_search": 0,
- "in_list_view": 0,
"in_standard_filter": 1,
"label": "To Time",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "0.00",
"fieldname": "expense",
"fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
"in_filter": 1,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Expense",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Expense"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "0.00",
"fieldname": "custody",
"fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
"in_filter": 1,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Custody",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Custody"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "0.00",
"fieldname": "returns",
"fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
"in_filter": 1,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Returns",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "precision": "2"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "0.00",
"fieldname": "outstanding_amount",
"fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Outstanding Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "0.0",
"fieldname": "payments",
"fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
"in_filter": 1,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Payments",
- "length": 0,
- "no_copy": 0,
- "options": "Cashier Closing Payments",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Cashier Closing Payments"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "net_amount",
"fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
"in_filter": 1,
- "in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Net Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "amended_from",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Amended From",
- "length": 0,
"no_copy": 1,
"options": "Cashier Closing",
- "permlevel": 0,
"print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
"is_submittable": 1,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-02-19 08:35:24.157327",
+ "links": [],
+ "modified": "2023-12-28 13:15:46.858427",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cashier Closing",
- "name_case": "",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
- "set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
}
],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/journal_entry/test_records.json b/erpnext/accounts/doctype/journal_entry/test_records.json
index 5077305cf22..73777d4008c 100644
--- a/erpnext/accounts/doctype/journal_entry/test_records.json
+++ b/erpnext/accounts/doctype/journal_entry/test_records.json
@@ -81,7 +81,6 @@
},
{
"account": "Sales - _TC",
- "cost_center": "_Test Cost Center - _TC",
"credit_in_account_currency": 400.0,
"debit_in_account_currency": 0.0,
"doctype": "Journal Entry Account",
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index df5e221e195..2405aba38a3 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -705,7 +705,7 @@ def get_pos_reserved_qty(item_code, warehouse):
reserved_qty = (
frappe.qb.from_(p_inv)
.from_(p_item)
- .select(Sum(p_item.qty).as_("qty"))
+ .select(Sum(p_item.stock_qty).as_("stock_qty"))
.where(
(p_inv.name == p_item.parent)
& (IfNull(p_inv.consolidated_invoice, "") == "")
@@ -716,7 +716,7 @@ def get_pos_reserved_qty(item_code, warehouse):
)
).run(as_dict=True)
- return reserved_qty[0].qty or 0 if reserved_qty else 0
+ return flt(reserved_qty[0].stock_qty) if reserved_qty else 0
@frappe.whitelist()
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
index 31660306db0..a93fd46caf6 100644
--- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
@@ -415,7 +415,7 @@ def reconcile(doc: None | str = None) -> None:
# Update the parent doc about the exception
frappe.db.rollback()
- traceback = frappe.get_traceback()
+ traceback = frappe.get_traceback(with_context=True)
if traceback:
message = "Traceback:
" + traceback
frappe.db.set_value("Process Payment Reconciliation Log", log, "error_log", message)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 7171bddac4b..cc1109b7dc8 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -976,11 +976,17 @@ class PurchaseInvoice(BuyingController):
)
assets = frappe.db.get_all(
- "Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
+ "Asset",
+ filters={"purchase_invoice": self.name, "item_code": item.item_code},
+ fields=["name", "asset_quantity"],
)
for asset in assets:
- frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
- frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate))
+ frappe.db.set_value(
+ "Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate) * asset.asset_quantity
+ )
+ frappe.db.set_value(
+ "Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate) * asset.asset_quantity
+ )
def make_stock_adjustment_entry(
self, gl_entries, item, voucher_wise_stock_value, account_currency
diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py
index 209cad4f905..d603635ac5a 100644
--- a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py
+++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py
@@ -43,7 +43,7 @@ def start_payment_ledger_repost(docname=None):
except Exception as e:
frappe.db.rollback()
- traceback = frappe.get_traceback()
+ traceback = frappe.get_traceback(with_context=True)
if traceback:
message = "Traceback:
" + traceback
frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_error_log", message)
diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js
index 5955c2e0fc9..ca1cca13115 100644
--- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js
+++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js
@@ -2,7 +2,44 @@
// License: GNU General Public License v3. See license.txt
frappe.query_reports["Budget Variance Report"] = {
- "filters": [
+ "filters": get_filters(),
+ "formatter": function (value, row, column, data, default_formatter) {
+ value = default_formatter(value, row, column, data);
+
+ if (column.fieldname.includes(__("variance"))) {
+
+ if (data[column.fieldname] < 0) {
+ value = "" + value + "";
+ }
+ else if (data[column.fieldname] > 0) {
+ value = "" + value + "";
+ }
+ }
+
+ return value;
+ }
+}
+function get_filters() {
+ function get_dimensions() {
+ let result = [];
+ frappe.call({
+ method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions",
+ args: {
+ 'with_cost_center_and_project': true
+ },
+ async: false,
+ callback: function(r) {
+ if(!r.exc) {
+ result = r.message[0].map(elem => elem.document_type);
+ }
+ }
+ });
+ return result;
+ }
+
+ let budget_against_options = get_dimensions();
+
+ let filters = [
{
fieldname: "from_fiscal_year",
label: __("From Fiscal Year"),
@@ -44,9 +81,13 @@ frappe.query_reports["Budget Variance Report"] = {
fieldname: "budget_against",
label: __("Budget Against"),
fieldtype: "Select",
- options: ["Cost Center", "Project"],
+ options: budget_against_options,
default: "Cost Center",
reqd: 1,
+ get_data: function() {
+ console.log(this.options);
+ return ["Emacs", "Rocks"];
+ },
on_change: function() {
frappe.query_report.set_filter_value("budget_against_filter", []);
frappe.query_report.refresh();
@@ -71,24 +112,8 @@ frappe.query_reports["Budget Variance Report"] = {
fieldtype: "Check",
default: 0,
},
- ],
- "formatter": function (value, row, column, data, default_formatter) {
- value = default_formatter(value, row, column, data);
+ ]
- if (column.fieldname.includes(__("variance"))) {
-
- if (data[column.fieldname] < 0) {
- value = "" + value + "";
- }
- else if (data[column.fieldname] > 0) {
- value = "" + value + "";
- }
- }
-
- return value;
- }
+ return filters;
}
-erpnext.dimension_filters.forEach((dimension) => {
- frappe.query_reports["Budget Variance Report"].filters[4].options.push(dimension["document_type"]);
-});
diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py
index d870a1aaf83..b2222c2d6a7 100644
--- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py
+++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py
@@ -3,7 +3,7 @@
import frappe
-from frappe import _, scrub
+from frappe import _, qb, scrub
from frappe.utils import getdate, nowdate
@@ -38,7 +38,6 @@ class PartyLedgerSummaryReport(object):
"""
Additional Columns for 'User Permission' based access control
"""
- from frappe import qb
if self.filters.party_type == "Customer":
self.territories = frappe._dict({})
@@ -365,13 +364,29 @@ class PartyLedgerSummaryReport(object):
def get_party_adjustment_amounts(self):
conditions = self.prepare_conditions()
- income_or_expense = (
- "Expense Account" if self.filters.party_type == "Customer" else "Income Account"
+ account_type = "Expense Account" if self.filters.party_type == "Customer" else "Income Account"
+ income_or_expense_accounts = frappe.db.get_all(
+ "Account", filters={"account_type": account_type, "company": self.filters.company}, pluck="name"
)
invoice_dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit"
reverse_dr_or_cr = "credit" if self.filters.party_type == "Customer" else "debit"
round_off_account = frappe.get_cached_value("Company", self.filters.company, "round_off_account")
+ gl = qb.DocType("GL Entry")
+ if not income_or_expense_accounts:
+ # prevent empty 'in' condition
+ income_or_expense_accounts.append("")
+
+ accounts_query = (
+ qb.from_(gl)
+ .select(gl.voucher_type, gl.voucher_no)
+ .where(
+ (gl.account.isin(income_or_expense_accounts))
+ & (gl.posting_date.gte(self.filters.from_date))
+ & (gl.posting_date.lte(self.filters.to_date))
+ )
+ )
+
gl_entries = frappe.db.sql(
"""
select
@@ -381,16 +396,15 @@ class PartyLedgerSummaryReport(object):
where
docstatus < 2 and is_cancelled = 0
and (voucher_type, voucher_no) in (
- select voucher_type, voucher_no from `tabGL Entry` gle, `tabAccount` acc
- where acc.name = gle.account and acc.account_type = '{income_or_expense}'
- and gle.posting_date between %(from_date)s and %(to_date)s and gle.docstatus < 2
+ {accounts_query}
) and (voucher_type, voucher_no) in (
select voucher_type, voucher_no from `tabGL Entry` gle
where gle.party_type=%(party_type)s and ifnull(party, '') != ''
and gle.posting_date between %(from_date)s and %(to_date)s and gle.docstatus < 2 {conditions}
)
- """.format(
- conditions=conditions, income_or_expense=income_or_expense
+ """.format(
+ accounts_query=accounts_query,
+ conditions=conditions,
),
self.filters,
as_dict=True,
@@ -414,7 +428,7 @@ class PartyLedgerSummaryReport(object):
elif gle.party:
parties.setdefault(gle.party, 0)
parties[gle.party] += gle.get(reverse_dr_or_cr) - gle.get(invoice_dr_or_cr)
- elif frappe.get_cached_value("Account", gle.account, "account_type") == income_or_expense:
+ elif frappe.get_cached_value("Account", gle.account, "account_type") == account_type:
accounts.setdefault(gle.account, 0)
accounts[gle.account] += gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr)
else:
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 ce1a62d0065..5d3d4d74978 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
@@ -309,7 +309,8 @@ def get_conditions(filters):
def get_items(filters, additional_query_columns):
conditions = get_conditions(filters)
-
+ if additional_query_columns:
+ additional_query_columns = "," + ",".join(additional_query_columns)
return frappe.db.sql(
"""
select
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 19bb449cd94..ce22d7566c1 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
@@ -381,7 +381,8 @@ def get_group_by_conditions(filters, doctype):
def get_items(filters, additional_query_columns, additional_conditions=None):
conditions = get_conditions(filters, additional_conditions)
-
+ if additional_query_columns:
+ additional_query_columns = "," + ",".join(additional_query_columns)
return frappe.db.sql(
"""
select
diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js
index c4054a977a8..4dbc687366d 100644
--- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js
+++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js
@@ -119,8 +119,4 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"initial_depth": 3
}
- erpnext.dimension_filters.forEach((dimension) => {
- frappe.query_reports["Profitability Analysis"].filters[1].options.push(dimension["document_type"]);
- });
-
});
diff --git a/erpnext/accounts/report/purchase_register/purchase_register.js b/erpnext/accounts/report/purchase_register/purchase_register.js
index aaf76c42997..57cb703baea 100644
--- a/erpnext/accounts/report/purchase_register/purchase_register.js
+++ b/erpnext/accounts/report/purchase_register/purchase_register.js
@@ -52,6 +52,12 @@ frappe.query_reports["Purchase Register"] = {
"label": __("Item Group"),
"fieldtype": "Link",
"options": "Item Group"
+ },
+ {
+ "fieldname": "include_payments",
+ "label": __("Show Ledger View"),
+ "fieldtype": "Check",
+ "default": 0
}
]
}
diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py
index 69827aca694..4eb135b0487 100644
--- a/erpnext/accounts/report/purchase_register/purchase_register.py
+++ b/erpnext/accounts/report/purchase_register/purchase_register.py
@@ -4,13 +4,22 @@
import frappe
from frappe import _, msgprint
-from frappe.utils import flt
+from frappe.query_builder.custom import ConstantColumn
+from frappe.utils import flt, getdate
+from pypika import Order
-from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
- get_accounting_dimensions,
- get_dimension_with_children,
+from erpnext.accounts.party import get_party_account
+from erpnext.accounts.report.utils import (
+ apply_common_conditions,
+ get_advance_taxes_and_charges,
+ get_journal_entries,
+ get_opening_row,
+ get_party_details,
+ get_payment_entries,
+ get_query_columns,
+ get_taxes_query,
+ get_values_for_columns,
)
-from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
def execute(filters=None):
@@ -21,9 +30,15 @@ def _execute(filters=None, additional_table_columns=None):
if not filters:
filters = {}
+ include_payments = filters.get("include_payments")
+ if filters.get("include_payments") and not filters.get("supplier"):
+ frappe.throw(_("Please select a supplier for fetching payments."))
invoice_list = get_invoices(filters, get_query_columns(additional_table_columns))
+ if filters.get("include_payments"):
+ invoice_list += get_payments(filters)
+
columns, expense_accounts, tax_accounts, unrealized_profit_loss_accounts = get_columns(
- invoice_list, additional_table_columns
+ invoice_list, additional_table_columns, include_payments
)
if not invoice_list:
@@ -33,14 +48,28 @@ def _execute(filters=None, additional_table_columns=None):
invoice_expense_map = get_invoice_expense_map(invoice_list)
internal_invoice_map = get_internal_invoice_map(invoice_list)
invoice_expense_map, invoice_tax_map = get_invoice_tax_map(
- invoice_list, invoice_expense_map, expense_accounts
+ invoice_list, invoice_expense_map, expense_accounts, include_payments
)
invoice_po_pr_map = get_invoice_po_pr_map(invoice_list)
suppliers = list(set(d.supplier for d in invoice_list))
- supplier_details = get_supplier_details(suppliers)
+ supplier_details = get_party_details("Supplier", suppliers)
company_currency = frappe.get_cached_value("Company", filters.company, "default_currency")
+ res = []
+ if include_payments:
+ opening_row = get_opening_row(
+ "Supplier", filters.supplier, getdate(filters.from_date), filters.company
+ )[0]
+ res.append(
+ {
+ "payable_account": opening_row.account,
+ "debit": flt(opening_row.debit),
+ "credit": flt(opening_row.credit),
+ "balance": flt(opening_row.balance),
+ }
+ )
+
data = []
for inv in invoice_list:
# invoice details
@@ -48,24 +77,23 @@ def _execute(filters=None, additional_table_columns=None):
purchase_receipt = list(set(invoice_po_pr_map.get(inv.name, {}).get("purchase_receipt", [])))
project = list(set(invoice_po_pr_map.get(inv.name, {}).get("project", [])))
- row = [
- inv.name,
- inv.posting_date,
- inv.supplier,
- inv.supplier_name,
- *get_values_for_columns(additional_table_columns, inv).values(),
- supplier_details.get(inv.supplier), # supplier_group
- inv.tax_id,
- inv.credit_to,
- inv.mode_of_payment,
- ", ".join(project),
- inv.bill_no,
- inv.bill_date,
- inv.remarks,
- ", ".join(purchase_order),
- ", ".join(purchase_receipt),
- company_currency,
- ]
+ row = {
+ "voucher_type": inv.doctype,
+ "voucher_no": inv.name,
+ "posting_date": inv.posting_date,
+ "supplier_id": inv.supplier,
+ "supplier_name": inv.supplier_name,
+ **get_values_for_columns(additional_table_columns, inv),
+ "supplier_group": supplier_details.get(inv.supplier).get("supplier_group"), # supplier_group
+ "tax_id": supplier_details.get(inv.supplier).get("tax_id"),
+ "payable_account": inv.credit_to,
+ "mode_of_payment": inv.mode_of_payment,
+ "project": ", ".join(project) if inv.doctype == "Purchase Invoice" else inv.project,
+ "remarks": inv.remarks,
+ "purchase_order": ", ".join(purchase_order),
+ "purchase_receipt": ", ".join(purchase_receipt),
+ "currency": company_currency,
+ }
# map expense values
base_net_total = 0
@@ -75,14 +103,16 @@ def _execute(filters=None, additional_table_columns=None):
else:
expense_amount = flt(invoice_expense_map.get(inv.name, {}).get(expense_acc))
base_net_total += expense_amount
- row.append(expense_amount)
+ row.update({frappe.scrub(expense_acc): expense_amount})
# Add amount in unrealized account
for account in unrealized_profit_loss_accounts:
- row.append(flt(internal_invoice_map.get((inv.name, account))))
+ row.update(
+ {frappe.scrub(account + "_unrealized"): flt(internal_invoice_map.get((inv.name, account)))}
+ )
# net total
- row.append(base_net_total or inv.base_net_total)
+ row.update({"net_total": base_net_total or inv.base_net_total})
# tax account
total_tax = 0
@@ -90,45 +120,190 @@ def _execute(filters=None, additional_table_columns=None):
if tax_acc not in expense_accounts:
tax_amount = flt(invoice_tax_map.get(inv.name, {}).get(tax_acc))
total_tax += tax_amount
- row.append(tax_amount)
+ row.update({frappe.scrub(tax_acc): tax_amount})
# total tax, grand total, rounded total & outstanding amount
- row += [total_tax, inv.base_grand_total, flt(inv.base_grand_total, 0), inv.outstanding_amount]
+ row.update(
+ {
+ "total_tax": total_tax,
+ "grand_total": inv.base_grand_total,
+ "rounded_total": inv.base_rounded_total,
+ "outstanding_amount": inv.outstanding_amount,
+ }
+ )
+
+ if inv.doctype == "Purchase Invoice":
+ row.update({"debit": inv.base_grand_total, "credit": 0.0})
+ else:
+ row.update({"debit": 0.0, "credit": inv.base_grand_total})
data.append(row)
- return columns, data
+ res += sorted(data, key=lambda x: x["posting_date"])
+
+ if include_payments:
+ running_balance = flt(opening_row.balance)
+ for row in range(1, len(res)):
+ running_balance += res[row]["debit"] - res[row]["credit"]
+ res[row].update({"balance": running_balance})
+
+ return columns, res, None, None, None, include_payments
-def get_columns(invoice_list, additional_table_columns):
+def get_columns(invoice_list, additional_table_columns, include_payments=False):
"""return columns based on filters"""
columns = [
- _("Invoice") + ":Link/Purchase Invoice:120",
- _("Posting Date") + ":Date:80",
- _("Supplier Id") + "::120",
- _("Supplier Name") + "::120",
+ {
+ "label": _("Voucher Type"),
+ "fieldname": "voucher_type",
+ "width": 120,
+ },
+ {
+ "label": _("Voucher"),
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "options": "voucher_type",
+ "width": 120,
+ },
+ {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 80},
+ {
+ "label": _("Supplier"),
+ "fieldname": "supplier_id",
+ "fieldtype": "Link",
+ "options": "Supplier",
+ "width": 120,
+ },
+ {"label": _("Supplier Name"), "fieldname": "supplier_name", "fieldtype": "Data", "width": 120},
]
- if additional_table_columns:
+ if additional_table_columns and not include_payments:
columns += additional_table_columns
- columns += [
- _("Supplier Group") + ":Link/Supplier Group:120",
- _("Tax Id") + "::80",
- _("Payable Account") + ":Link/Account:120",
- _("Mode of Payment") + ":Link/Mode of Payment:80",
- _("Project") + ":Link/Project:80",
- _("Bill No") + "::120",
- _("Bill Date") + ":Date:80",
- _("Remarks") + "::150",
- _("Purchase Order") + ":Link/Purchase Order:100",
- _("Purchase Receipt") + ":Link/Purchase Receipt:100",
- {"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80},
- ]
+ if not include_payments:
+ columns += [
+ {
+ "label": _("Supplier Group"),
+ "fieldname": "supplier_group",
+ "fieldtype": "Link",
+ "options": "Supplier Group",
+ "width": 120,
+ },
+ {"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 80},
+ {
+ "label": _("Payable Account"),
+ "fieldname": "payable_account",
+ "fieldtype": "Link",
+ "options": "Account",
+ "width": 100,
+ },
+ {
+ "label": _("Mode Of Payment"),
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Data",
+ "width": 120,
+ },
+ {
+ "label": _("Project"),
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "options": "Project",
+ "width": 80,
+ },
+ {"label": _("Bill No"), "fieldname": "bill_no", "fieldtype": "Data", "width": 120},
+ {"label": _("Bill Date"), "fieldname": "bill_date", "fieldtype": "Date", "width": 80},
+ {
+ "label": _("Purchase Order"),
+ "fieldname": "purchase_order",
+ "fieldtype": "Link",
+ "options": "Purchase Order",
+ "width": 100,
+ },
+ {
+ "label": _("Purchase Receipt"),
+ "fieldname": "purchase_receipt",
+ "fieldtype": "Link",
+ "options": "Purchase Receipt",
+ "width": 100,
+ },
+ {"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80},
+ ]
+ else:
+ columns += [
+ {
+ "fieldname": "payable_account",
+ "label": _("Payable Account"),
+ "fieldtype": "Link",
+ "options": "Account",
+ "width": 120,
+ },
+ {"fieldname": "debit", "label": _("Debit"), "fieldtype": "Currency", "width": 120},
+ {"fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "width": 120},
+ {"fieldname": "balance", "label": _("Balance"), "fieldtype": "Currency", "width": 120},
+ ]
+ account_columns, accounts = get_account_columns(invoice_list, include_payments)
+
+ columns = (
+ columns
+ + account_columns[0]
+ + account_columns[1]
+ + [
+ {
+ "label": _("Net Total"),
+ "fieldname": "net_total",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ }
+ ]
+ + account_columns[2]
+ + [
+ {
+ "label": _("Total Tax"),
+ "fieldname": "total_tax",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ }
+ ]
+ )
+
+ if not include_payments:
+ columns += [
+ {
+ "label": _("Grand Total"),
+ "fieldname": "grand_total",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ },
+ {
+ "label": _("Rounded Total"),
+ "fieldname": "rounded_total",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ },
+ {
+ "label": _("Outstanding Amount"),
+ "fieldname": "outstanding_amount",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ },
+ ]
+ columns += [{"label": _("Remarks"), "fieldname": "remarks", "fieldtype": "Data", "width": 120}]
+ return columns, accounts[0], accounts[2], accounts[1]
+
+
+def get_account_columns(invoice_list, include_payments):
expense_accounts = []
tax_accounts = []
unrealized_profit_loss_accounts = []
+ expense_columns = []
+ tax_columns = []
+ unrealized_profit_loss_account_columns = []
+
if invoice_list:
expense_accounts = frappe.db.sql_list(
"""select distinct expense_account
@@ -139,15 +314,18 @@ def get_columns(invoice_list, additional_table_columns):
tuple([inv.name for inv in invoice_list]),
)
- tax_accounts = frappe.db.sql_list(
- """select distinct account_head
- from `tabPurchase Taxes and Charges` where parenttype = 'Purchase Invoice'
- and docstatus = 1 and (account_head is not null and account_head != '')
- and category in ('Total', 'Valuation and Total')
- and parent in (%s) order by account_head"""
- % ", ".join(["%s"] * len(invoice_list)),
- tuple(inv.name for inv in invoice_list),
+ purchase_taxes_query = get_taxes_query(
+ invoice_list, "Purchase Taxes and Charges", "Purchase Invoice"
)
+ purchase_tax_accounts = purchase_taxes_query.run(as_dict=True, pluck="account_head")
+ tax_accounts = purchase_tax_accounts
+
+ if include_payments:
+ advance_taxes_query = get_taxes_query(
+ invoice_list, "Advance Taxes and Charges", "Payment Entry"
+ )
+ advance_tax_accounts = advance_taxes_query.run(as_dict=True, pluck="account_head")
+ tax_accounts = set(tax_accounts + advance_tax_accounts)
unrealized_profit_loss_accounts = frappe.db.sql_list(
"""SELECT distinct unrealized_profit_loss_account
@@ -158,108 +336,109 @@ def get_columns(invoice_list, additional_table_columns):
tuple(inv.name for inv in invoice_list),
)
- expense_columns = [(account + ":Currency/currency:120") for account in expense_accounts]
- unrealized_profit_loss_account_columns = [
- (account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts
- ]
- tax_columns = [
- (account + ":Currency/currency:120")
- for account in tax_accounts
- if account not in expense_accounts
- ]
+ for account in expense_accounts:
+ expense_columns.append(
+ {
+ "label": account,
+ "fieldname": frappe.scrub(account),
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ }
+ )
- columns = (
- columns
- + expense_columns
- + unrealized_profit_loss_account_columns
- + [_("Net Total") + ":Currency/currency:120"]
- + tax_columns
- + [
- _("Total Tax") + ":Currency/currency:120",
- _("Grand Total") + ":Currency/currency:120",
- _("Rounded Total") + ":Currency/currency:120",
- _("Outstanding Amount") + ":Currency/currency:120",
- ]
- )
+ for account in tax_accounts:
+ if account not in expense_accounts:
+ tax_columns.append(
+ {
+ "label": account,
+ "fieldname": frappe.scrub(account),
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ }
+ )
- return columns, expense_accounts, tax_accounts, unrealized_profit_loss_accounts
+ for account in unrealized_profit_loss_accounts:
+ unrealized_profit_loss_account_columns.append(
+ {
+ "label": account,
+ "fieldname": frappe.scrub(account),
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ }
+ )
+ columns = [expense_columns, unrealized_profit_loss_account_columns, tax_columns]
+ accounts = [expense_accounts, unrealized_profit_loss_accounts, tax_accounts]
-def get_conditions(filters):
- conditions = ""
-
- if filters.get("company"):
- conditions += " and company=%(company)s"
- if filters.get("supplier"):
- conditions += " and supplier = %(supplier)s"
-
- if filters.get("from_date"):
- conditions += " and posting_date>=%(from_date)s"
- if filters.get("to_date"):
- conditions += " and posting_date<=%(to_date)s"
-
- if filters.get("mode_of_payment"):
- conditions += " and ifnull(mode_of_payment, '') = %(mode_of_payment)s"
-
- if filters.get("cost_center"):
- conditions += """ and exists(select name from `tabPurchase Invoice Item`
- where parent=`tabPurchase Invoice`.name
- and ifnull(`tabPurchase Invoice Item`.cost_center, '') = %(cost_center)s)"""
-
- if filters.get("warehouse"):
- conditions += """ and exists(select name from `tabPurchase Invoice Item`
- where parent=`tabPurchase Invoice`.name
- and ifnull(`tabPurchase Invoice Item`.warehouse, '') = %(warehouse)s)"""
-
- if filters.get("item_group"):
- conditions += """ and exists(select name from `tabPurchase Invoice Item`
- where parent=`tabPurchase Invoice`.name
- and ifnull(`tabPurchase Invoice Item`.item_group, '') = %(item_group)s)"""
-
- accounting_dimensions = get_accounting_dimensions(as_list=False)
-
- if accounting_dimensions:
- common_condition = """
- and exists(select name from `tabPurchase Invoice Item`
- where parent=`tabPurchase Invoice`.name
- """
- for dimension in accounting_dimensions:
- if filters.get(dimension.fieldname):
- if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
- filters[dimension.fieldname] = get_dimension_with_children(
- dimension.document_type, filters.get(dimension.fieldname)
- )
-
- conditions += (
- common_condition
- + "and ifnull(`tabPurchase Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname)
- )
- else:
- conditions += (
- common_condition
- + "and ifnull(`tabPurchase Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname)
- )
-
- return conditions
+ return columns, accounts
def get_invoices(filters, additional_query_columns):
- conditions = get_conditions(filters)
- return frappe.db.sql(
- """
- select
- name, posting_date, credit_to, supplier, supplier_name, tax_id, bill_no, bill_date,
- remarks, base_net_total, base_grand_total, outstanding_amount,
- mode_of_payment {0}
- from `tabPurchase Invoice`
- where docstatus = 1 {1}
- order by posting_date desc, name desc""".format(
- additional_query_columns, conditions
- ),
- filters,
- as_dict=1,
+ pi = frappe.qb.DocType("Purchase Invoice")
+ query = (
+ frappe.qb.from_(pi)
+ .select(
+ ConstantColumn("Purchase Invoice").as_("doctype"),
+ pi.name,
+ pi.posting_date,
+ pi.credit_to,
+ pi.supplier,
+ pi.supplier_name,
+ pi.tax_id,
+ pi.bill_no,
+ pi.bill_date,
+ pi.remarks,
+ pi.base_net_total,
+ pi.base_grand_total,
+ pi.base_rounded_total,
+ pi.outstanding_amount,
+ pi.mode_of_payment,
+ )
+ .where((pi.docstatus == 1))
+ .orderby(pi.posting_date, pi.name, order=Order.desc)
)
+ if additional_query_columns:
+ for col in additional_query_columns:
+ query = query.select(col)
+
+ if filters.get("supplier"):
+ query = query.where(pi.supplier == filters.supplier)
+
+ query = get_conditions(filters, query, "Purchase Invoice")
+
+ query = apply_common_conditions(
+ filters, query, doctype="Purchase Invoice", child_doctype="Purchase Invoice Item"
+ )
+
+ invoices = query.run(as_dict=True)
+ return invoices
+
+
+def get_conditions(filters, query, doctype):
+ parent_doc = frappe.qb.DocType(doctype)
+
+ if filters.get("mode_of_payment"):
+ query = query.where(parent_doc.mode_of_payment == filters.mode_of_payment)
+
+ return query
+
+
+def get_payments(filters):
+ args = frappe._dict(
+ account="credit_to",
+ account_fieldname="paid_to",
+ party="supplier",
+ party_name="supplier_name",
+ party_account=get_party_account("Supplier", filters.supplier, filters.company),
+ )
+ payment_entries = get_payment_entries(filters, args)
+ journal_entries = get_journal_entries(filters, args)
+ return payment_entries + journal_entries
+
def get_invoice_expense_map(invoice_list):
expense_details = frappe.db.sql(
@@ -300,7 +479,9 @@ def get_internal_invoice_map(invoice_list):
return internal_invoice_map
-def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts):
+def get_invoice_tax_map(
+ invoice_list, invoice_expense_map, expense_accounts, include_payments=False
+):
tax_details = frappe.db.sql(
"""
select parent, account_head, case add_deduct_tax when "Add" then sum(base_tax_amount_after_discount_amount)
@@ -315,6 +496,9 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts):
as_dict=1,
)
+ if include_payments:
+ tax_details += get_advance_taxes_and_charges(invoice_list)
+
invoice_tax_map = {}
for d in tax_details:
if d.account_head in expense_accounts:
@@ -382,17 +566,3 @@ def get_account_details(invoice_list):
account_map[acc.name] = acc.parent_account
return account_map
-
-
-def get_supplier_details(suppliers):
- supplier_details = {}
- for supp in frappe.db.sql(
- """select name, supplier_group from `tabSupplier`
- where name in (%s)"""
- % ", ".join(["%s"] * len(suppliers)),
- tuple(suppliers),
- as_dict=1,
- ):
- supplier_details.setdefault(supp.name, supp.supplier_group)
-
- return supplier_details
diff --git a/erpnext/accounts/report/purchase_register/test_purchase_register.py b/erpnext/accounts/report/purchase_register/test_purchase_register.py
new file mode 100644
index 00000000000..6903662e687
--- /dev/null
+++ b/erpnext/accounts/report/purchase_register/test_purchase_register.py
@@ -0,0 +1,128 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_months, getdate, today
+
+from erpnext.accounts.report.purchase_register.purchase_register import execute
+
+
+class TestPurchaseRegister(FrappeTestCase):
+ def test_purchase_register(self):
+ frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
+ frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
+
+ filters = frappe._dict(
+ company="_Test Company 6", from_date=add_months(today(), -1), to_date=today()
+ )
+
+ pi = make_purchase_invoice()
+
+ report_results = execute(filters)
+ first_row = frappe._dict(report_results[1][0])
+ self.assertEqual(first_row.voucher_type, "Purchase Invoice")
+ self.assertEqual(first_row.voucher_no, pi.name)
+ self.assertEqual(first_row.payable_account, "Creditors - _TC6")
+ self.assertEqual(first_row.net_total, 1000)
+ self.assertEqual(first_row.total_tax, 100)
+ self.assertEqual(first_row.grand_total, 1100)
+
+ def test_purchase_register_ledger_view(self):
+ frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
+ frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
+
+ filters = frappe._dict(
+ company="_Test Company 6",
+ from_date=add_months(today(), -1),
+ to_date=today(),
+ include_payments=True,
+ supplier="_Test Supplier",
+ )
+
+ pi = make_purchase_invoice()
+ pe = make_payment_entry()
+
+ report_results = execute(filters)
+ first_row = frappe._dict(report_results[1][2])
+ self.assertEqual(first_row.voucher_type, "Payment Entry")
+ self.assertEqual(first_row.voucher_no, pe.name)
+ self.assertEqual(first_row.payable_account, "Creditors - _TC6")
+ self.assertEqual(first_row.debit, 0)
+ self.assertEqual(first_row.credit, 600)
+ self.assertEqual(first_row.balance, 500)
+
+
+def make_purchase_invoice():
+ from erpnext.accounts.doctype.account.test_account import create_account
+ from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
+ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+
+ gst_acc = create_account(
+ account_name="GST",
+ account_type="Tax",
+ parent_account="Duties and Taxes - _TC6",
+ company="_Test Company 6",
+ account_currency="INR",
+ )
+ create_warehouse(warehouse_name="_Test Warehouse - _TC6", company="_Test Company 6")
+ create_cost_center(cost_center_name="_Test Cost Center", company="_Test Company 6")
+ pi = create_purchase_invoice_with_taxes()
+ pi.submit()
+ return pi
+
+
+def create_purchase_invoice_with_taxes():
+ return frappe.get_doc(
+ {
+ "doctype": "Purchase Invoice",
+ "posting_date": today(),
+ "supplier": "_Test Supplier",
+ "company": "_Test Company 6",
+ "cost_center": "_Test Cost Center - _TC6",
+ "taxes_and_charges": "",
+ "currency": "INR",
+ "credit_to": "Creditors - _TC6",
+ "items": [
+ {
+ "doctype": "Purchase Invoice Item",
+ "cost_center": "_Test Cost Center - _TC6",
+ "item_code": "_Test Item",
+ "qty": 1,
+ "rate": 1000,
+ "expense_account": "Stock Received But Not Billed - _TC6",
+ }
+ ],
+ "taxes": [
+ {
+ "account_head": "GST - _TC6",
+ "cost_center": "_Test Cost Center - _TC6",
+ "add_deduct_tax": "Add",
+ "category": "Valuation and Total",
+ "charge_type": "Actual",
+ "description": "Shipping Charges",
+ "doctype": "Purchase Taxes and Charges",
+ "parentfield": "taxes",
+ "rate": 100,
+ "tax_amount": 100.0,
+ }
+ ],
+ }
+ )
+
+
+def make_payment_entry():
+ frappe.set_user("Administrator")
+ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
+
+ return create_payment_entry(
+ company="_Test Company 6",
+ party_type="Supplier",
+ party="_Test Supplier",
+ payment_type="Pay",
+ paid_from="Cash - _TC6",
+ paid_to="Creditors - _TC6",
+ paid_amount=600,
+ save=1,
+ submit=1,
+ )
diff --git a/erpnext/accounts/report/sales_register/sales_register.js b/erpnext/accounts/report/sales_register/sales_register.js
index 2c9b01bbaa3..1a41172a970 100644
--- a/erpnext/accounts/report/sales_register/sales_register.js
+++ b/erpnext/accounts/report/sales_register/sales_register.js
@@ -64,6 +64,12 @@ frappe.query_reports["Sales Register"] = {
"label": __("Item Group"),
"fieldtype": "Link",
"options": "Item Group"
+ },
+ {
+ "fieldname": "include_payments",
+ "label": __("Show Ledger View"),
+ "fieldtype": "Check",
+ "default": 0
}
]
}
diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py
index 291c7d976e4..61b1fe2293c 100644
--- a/erpnext/accounts/report/sales_register/sales_register.py
+++ b/erpnext/accounts/report/sales_register/sales_register.py
@@ -5,13 +5,22 @@
import frappe
from frappe import _, msgprint
from frappe.model.meta import get_field_precision
-from frappe.utils import flt
+from frappe.query_builder.custom import ConstantColumn
+from frappe.utils import flt, getdate
+from pypika import Order
-from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
- get_accounting_dimensions,
- get_dimension_with_children,
+from erpnext.accounts.party import get_party_account
+from erpnext.accounts.report.utils import (
+ apply_common_conditions,
+ get_advance_taxes_and_charges,
+ get_journal_entries,
+ get_opening_row,
+ get_party_details,
+ get_payment_entries,
+ get_query_columns,
+ get_taxes_query,
+ get_values_for_columns,
)
-from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
def execute(filters=None):
@@ -22,9 +31,15 @@ def _execute(filters, additional_table_columns=None):
if not filters:
filters = frappe._dict({})
+ include_payments = filters.get("include_payments")
+ if filters.get("include_payments") and not filters.get("customer"):
+ frappe.throw(_("Please select a customer for fetching payments."))
invoice_list = get_invoices(filters, get_query_columns(additional_table_columns))
- columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts = get_columns(
- invoice_list, additional_table_columns
+ if filters.get("include_payments"):
+ invoice_list += get_payments(filters)
+
+ columns, income_accounts, unrealized_profit_loss_accounts, tax_accounts = get_columns(
+ invoice_list, additional_table_columns, include_payments
)
if not invoice_list:
@@ -34,13 +49,29 @@ def _execute(filters, additional_table_columns=None):
invoice_income_map = get_invoice_income_map(invoice_list)
internal_invoice_map = get_internal_invoice_map(invoice_list)
invoice_income_map, invoice_tax_map = get_invoice_tax_map(
- invoice_list, invoice_income_map, income_accounts
+ invoice_list, invoice_income_map, income_accounts, include_payments
)
# Cost Center & Warehouse Map
invoice_cc_wh_map = get_invoice_cc_wh_map(invoice_list)
invoice_so_dn_map = get_invoice_so_dn_map(invoice_list)
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
mode_of_payments = get_mode_of_payments([inv.name for inv in invoice_list])
+ customers = list(set(d.customer for d in invoice_list))
+ customer_details = get_party_details("Customer", customers)
+
+ res = []
+ if include_payments:
+ opening_row = get_opening_row(
+ "Customer", filters.customer, getdate(filters.from_date), filters.company
+ )[0]
+ res.append(
+ {
+ "receivable_account": opening_row.account,
+ "debit": flt(opening_row.debit),
+ "credit": flt(opening_row.credit),
+ "balance": flt(opening_row.balance),
+ }
+ )
data = []
for inv in invoice_list:
@@ -51,14 +82,15 @@ def _execute(filters, additional_table_columns=None):
warehouse = list(set(invoice_cc_wh_map.get(inv.name, {}).get("warehouse", [])))
row = {
- "invoice": inv.name,
+ "voucher_type": inv.doctype,
+ "voucher_no": inv.name,
"posting_date": inv.posting_date,
"customer": inv.customer,
"customer_name": inv.customer_name,
**get_values_for_columns(additional_table_columns, inv),
- "customer_group": inv.get("customer_group"),
- "territory": inv.get("territory"),
- "tax_id": inv.get("tax_id"),
+ "customer_group": customer_details.get(inv.customer).get("customer_group"),
+ "territory": customer_details.get(inv.customer).get("territory"),
+ "tax_id": customer_details.get(inv.customer).get("tax_id"),
"receivable_account": inv.debit_to,
"mode_of_payment": ", ".join(mode_of_payments.get(inv.name, [])),
"project": inv.project,
@@ -66,7 +98,7 @@ def _execute(filters, additional_table_columns=None):
"remarks": inv.remarks,
"sales_order": ", ".join(sales_order),
"delivery_note": ", ".join(delivery_note),
- "cost_center": ", ".join(cost_center),
+ "cost_center": ", ".join(cost_center) if inv.doctype == "Sales Invoice" else inv.cost_center,
"warehouse": ", ".join(warehouse),
"currency": company_currency,
}
@@ -116,19 +148,36 @@ def _execute(filters, additional_table_columns=None):
}
)
+ if inv.doctype == "Sales Invoice":
+ row.update({"debit": inv.base_grand_total, "credit": 0.0})
+ else:
+ row.update({"debit": 0.0, "credit": inv.base_grand_total})
data.append(row)
- return columns, data
+ res += sorted(data, key=lambda x: x["posting_date"])
+
+ if include_payments:
+ running_balance = flt(opening_row.balance)
+ for row in range(1, len(res)):
+ running_balance += res[row]["debit"] - res[row]["credit"]
+ res[row].update({"balance": running_balance})
+
+ return columns, res, None, None, None, include_payments
-def get_columns(invoice_list, additional_table_columns):
+def get_columns(invoice_list, additional_table_columns, include_payments=False):
"""return columns based on filters"""
columns = [
{
- "label": _("Invoice"),
- "fieldname": "invoice",
- "fieldtype": "Link",
- "options": "Sales Invoice",
+ "label": _("Voucher Type"),
+ "fieldname": "voucher_type",
+ "width": 120,
+ },
+ {
+ "label": _("Voucher"),
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "options": "voucher_type",
"width": 120,
},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 80},
@@ -142,83 +191,156 @@ def get_columns(invoice_list, additional_table_columns):
{"label": _("Customer Name"), "fieldname": "customer_name", "fieldtype": "Data", "width": 120},
]
- if additional_table_columns:
+ if additional_table_columns and not include_payments:
columns += additional_table_columns
- columns += [
+ if not include_payments:
+ columns += [
+ {
+ "label": _("Customer Group"),
+ "fieldname": "customer_group",
+ "fieldtype": "Link",
+ "options": "Customer Group",
+ "width": 120,
+ },
+ {
+ "label": _("Territory"),
+ "fieldname": "territory",
+ "fieldtype": "Link",
+ "options": "Territory",
+ "width": 80,
+ },
+ {"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 80},
+ {
+ "label": _("Receivable Account"),
+ "fieldname": "receivable_account",
+ "fieldtype": "Link",
+ "options": "Account",
+ "width": 100,
+ },
+ {
+ "label": _("Mode Of Payment"),
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Data",
+ "width": 120,
+ },
+ {
+ "label": _("Project"),
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "options": "Project",
+ "width": 80,
+ },
+ {"label": _("Owner"), "fieldname": "owner", "fieldtype": "Data", "width": 100},
+ {
+ "label": _("Sales Order"),
+ "fieldname": "sales_order",
+ "fieldtype": "Link",
+ "options": "Sales Order",
+ "width": 100,
+ },
+ {
+ "label": _("Delivery Note"),
+ "fieldname": "delivery_note",
+ "fieldtype": "Link",
+ "options": "Delivery Note",
+ "width": 100,
+ },
+ {
+ "label": _("Cost Center"),
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "options": "Cost Center",
+ "width": 100,
+ },
+ {
+ "label": _("Warehouse"),
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "options": "Warehouse",
+ "width": 100,
+ },
+ {"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80},
+ ]
+ else:
+ columns += [
+ {
+ "fieldname": "receivable_account",
+ "label": _("Receivable Account"),
+ "fieldtype": "Link",
+ "options": "Account",
+ "width": 120,
+ },
+ {"fieldname": "debit", "label": _("Debit"), "fieldtype": "Currency", "width": 120},
+ {"fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "width": 120},
+ {"fieldname": "balance", "label": _("Balance"), "fieldtype": "Currency", "width": 120},
+ ]
+
+ account_columns, accounts = get_account_columns(invoice_list, include_payments)
+
+ net_total_column = [
{
- "label": _("Customer Group"),
- "fieldname": "customer_group",
- "fieldtype": "Link",
- "options": "Customer Group",
+ "label": _("Net Total"),
+ "fieldname": "net_total",
+ "fieldtype": "Currency",
+ "options": "currency",
"width": 120,
- },
- {
- "label": _("Territory"),
- "fieldname": "territory",
- "fieldtype": "Link",
- "options": "Territory",
- "width": 80,
- },
- {"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 120},
- {
- "label": _("Receivable Account"),
- "fieldname": "receivable_account",
- "fieldtype": "Link",
- "options": "Account",
- "width": 80,
- },
- {
- "label": _("Mode Of Payment"),
- "fieldname": "mode_of_payment",
- "fieldtype": "Data",
- "width": 120,
- },
- {
- "label": _("Project"),
- "fieldname": "project",
- "fieldtype": "Link",
- "options": "Project",
- "width": 80,
- },
- {"label": _("Owner"), "fieldname": "owner", "fieldtype": "Data", "width": 150},
- {"label": _("Remarks"), "fieldname": "remarks", "fieldtype": "Data", "width": 150},
- {
- "label": _("Sales Order"),
- "fieldname": "sales_order",
- "fieldtype": "Link",
- "options": "Sales Order",
- "width": 100,
- },
- {
- "label": _("Delivery Note"),
- "fieldname": "delivery_note",
- "fieldtype": "Link",
- "options": "Delivery Note",
- "width": 100,
- },
- {
- "label": _("Cost Center"),
- "fieldname": "cost_center",
- "fieldtype": "Link",
- "options": "Cost Center",
- "width": 100,
- },
- {
- "label": _("Warehouse"),
- "fieldname": "warehouse",
- "fieldtype": "Link",
- "options": "Warehouse",
- "width": 100,
- },
- {"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80},
+ }
]
+ total_columns = [
+ {
+ "label": _("Tax Total"),
+ "fieldname": "tax_total",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ }
+ ]
+ if not include_payments:
+ total_columns += [
+ {
+ "label": _("Grand Total"),
+ "fieldname": "grand_total",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ },
+ {
+ "label": _("Rounded Total"),
+ "fieldname": "rounded_total",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ },
+ {
+ "label": _("Outstanding Amount"),
+ "fieldname": "outstanding_amount",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ },
+ ]
+
+ columns = (
+ columns
+ + account_columns[0]
+ + account_columns[2]
+ + net_total_column
+ + account_columns[1]
+ + total_columns
+ )
+ columns += [{"label": _("Remarks"), "fieldname": "remarks", "fieldtype": "Data", "width": 150}]
+ return columns, accounts[0], accounts[1], accounts[2]
+
+
+def get_account_columns(invoice_list, include_payments):
income_accounts = []
tax_accounts = []
+ unrealized_profit_loss_accounts = []
+
income_columns = []
tax_columns = []
- unrealized_profit_loss_accounts = []
unrealized_profit_loss_account_columns = []
if invoice_list:
@@ -230,14 +352,16 @@ def get_columns(invoice_list, additional_table_columns):
tuple(inv.name for inv in invoice_list),
)
- tax_accounts = frappe.db.sql_list(
- """select distinct account_head
- from `tabSales Taxes and Charges` where parenttype = 'Sales Invoice'
- and docstatus = 1 and base_tax_amount_after_discount_amount != 0
- and parent in (%s) order by account_head"""
- % ", ".join(["%s"] * len(invoice_list)),
- tuple(inv.name for inv in invoice_list),
- )
+ sales_taxes_query = get_taxes_query(invoice_list, "Sales Taxes and Charges", "Sales Invoice")
+ sales_tax_accounts = sales_taxes_query.run(as_dict=True, pluck="account_head")
+ tax_accounts = sales_tax_accounts
+
+ if include_payments:
+ advance_taxes_query = get_taxes_query(
+ invoice_list, "Advance Taxes and Charges", "Payment Entry"
+ )
+ advance_tax_accounts = advance_taxes_query.run(as_dict=True, pluck="account_head")
+ tax_accounts = set(tax_accounts + advance_tax_accounts)
unrealized_profit_loss_accounts = frappe.db.sql_list(
"""SELECT distinct unrealized_profit_loss_account
@@ -283,134 +407,82 @@ def get_columns(invoice_list, additional_table_columns):
}
)
- net_total_column = [
- {
- "label": _("Net Total"),
- "fieldname": "net_total",
- "fieldtype": "Currency",
- "options": "currency",
- "width": 120,
- }
- ]
+ columns = [income_columns, unrealized_profit_loss_account_columns, tax_columns]
+ accounts = [income_accounts, unrealized_profit_loss_accounts, tax_accounts]
- total_columns = [
- {
- "label": _("Tax Total"),
- "fieldname": "tax_total",
- "fieldtype": "Currency",
- "options": "currency",
- "width": 120,
- },
- {
- "label": _("Grand Total"),
- "fieldname": "grand_total",
- "fieldtype": "Currency",
- "options": "currency",
- "width": 120,
- },
- {
- "label": _("Rounded Total"),
- "fieldname": "rounded_total",
- "fieldtype": "Currency",
- "options": "currency",
- "width": 120,
- },
- {
- "label": _("Outstanding Amount"),
- "fieldname": "outstanding_amount",
- "fieldtype": "Currency",
- "options": "currency",
- "width": 120,
- },
- ]
-
- columns = (
- columns
- + income_columns
- + unrealized_profit_loss_account_columns
- + net_total_column
- + tax_columns
- + total_columns
- )
-
- return columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts
-
-
-def get_conditions(filters):
- conditions = ""
-
- accounting_dimensions = get_accounting_dimensions(as_list=False) or []
- accounting_dimensions_list = [d.fieldname for d in accounting_dimensions]
-
- if filters.get("company"):
- conditions += " and company=%(company)s"
-
- if filters.get("customer") and "customer" not in accounting_dimensions_list:
- conditions += " and customer = %(customer)s"
-
- if filters.get("from_date"):
- conditions += " and posting_date >= %(from_date)s"
- if filters.get("to_date"):
- conditions += " and posting_date <= %(to_date)s"
-
- if filters.get("owner"):
- conditions += " and owner = %(owner)s"
-
- def get_sales_invoice_item_field_condition(field, table="Sales Invoice Item") -> str:
- if not filters.get(field) or field in accounting_dimensions_list:
- return ""
- return f""" and exists(select name from `tab{table}`
- where parent=`tabSales Invoice`.name
- and ifnull(`tab{table}`.{field}, '') = %({field})s)"""
-
- conditions += get_sales_invoice_item_field_condition("mode_of_payment", "Sales Invoice Payment")
- conditions += get_sales_invoice_item_field_condition("cost_center")
- conditions += get_sales_invoice_item_field_condition("warehouse")
- conditions += get_sales_invoice_item_field_condition("brand")
- conditions += get_sales_invoice_item_field_condition("item_group")
-
- if accounting_dimensions:
- common_condition = """
- and exists(select name from `tabSales Invoice Item`
- where parent=`tabSales Invoice`.name
- """
- for dimension in accounting_dimensions:
- if filters.get(dimension.fieldname):
- if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
- filters[dimension.fieldname] = get_dimension_with_children(
- dimension.document_type, filters.get(dimension.fieldname)
- )
-
- conditions += (
- common_condition
- + "and ifnull(`tabSales Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname)
- )
- else:
- conditions += (
- common_condition
- + "and ifnull(`tabSales Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname)
- )
-
- return conditions
+ return columns, accounts
def get_invoices(filters, additional_query_columns):
- conditions = get_conditions(filters)
- return frappe.db.sql(
- """
- select name, posting_date, debit_to, project, customer,
- customer_name, owner, remarks, territory, tax_id, customer_group,
- base_net_total, base_grand_total, base_rounded_total, outstanding_amount,
- is_internal_customer, represents_company, company {0}
- from `tabSales Invoice`
- where docstatus = 1 {1}
- order by posting_date desc, name desc""".format(
- additional_query_columns, conditions
- ),
- filters,
- as_dict=1,
+ si = frappe.qb.DocType("Sales Invoice")
+ query = (
+ frappe.qb.from_(si)
+ .select(
+ ConstantColumn("Sales Invoice").as_("doctype"),
+ si.name,
+ si.posting_date,
+ si.debit_to,
+ si.project,
+ si.customer,
+ si.customer_name,
+ si.owner,
+ si.remarks,
+ si.territory,
+ si.tax_id,
+ si.customer_group,
+ si.base_net_total,
+ si.base_grand_total,
+ si.base_rounded_total,
+ si.outstanding_amount,
+ si.is_internal_customer,
+ si.represents_company,
+ si.company,
+ )
+ .where((si.docstatus == 1))
+ .orderby(si.posting_date, si.name, order=Order.desc)
)
+ if additional_query_columns:
+ for col in additional_query_columns:
+ query = query.select(col)
+
+ if filters.get("customer"):
+ query = query.where(si.customer == filters.customer)
+
+ query = get_conditions(filters, query, "Sales Invoice")
+ query = apply_common_conditions(
+ filters, query, doctype="Sales Invoice", child_doctype="Sales Invoice Item"
+ )
+
+ invoices = query.run(as_dict=True)
+ return invoices
+
+
+def get_conditions(filters, query, doctype):
+ parent_doc = frappe.qb.DocType(doctype)
+ if filters.get("owner"):
+ query = query.where(parent_doc.owner == filters.owner)
+
+ if filters.get("mode_of_payment"):
+ payment_doc = frappe.qb.DocType("Sales Invoice Payment")
+ query = query.inner_join(payment_doc).on(parent_doc.name == payment_doc.parent)
+ query = query.where(payment_doc.mode_of_payment == filters.mode_of_payment).distinct()
+
+ return query
+
+
+def get_payments(filters):
+ args = frappe._dict(
+ account="debit_to",
+ account_fieldname="paid_from",
+ party="customer",
+ party_name="customer_name",
+ party_account=get_party_account("Customer", filters.customer, filters.company),
+ )
+ payment_entries = get_payment_entries(filters, args)
+ journal_entries = get_journal_entries(filters, args)
+ return payment_entries + journal_entries
+
def get_invoice_income_map(invoice_list):
income_details = frappe.db.sql(
@@ -447,7 +519,7 @@ def get_internal_invoice_map(invoice_list):
return internal_invoice_map
-def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts):
+def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts, include_payments=False):
tax_details = frappe.db.sql(
"""select parent, account_head,
sum(base_tax_amount_after_discount_amount) as tax_amount
@@ -457,6 +529,9 @@ def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts):
as_dict=1,
)
+ if include_payments:
+ tax_details += get_advance_taxes_and_charges(invoice_list)
+
invoice_tax_map = {}
for d in tax_details:
if d.account_head in income_accounts:
diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py
index 7ea1fac1056..7dd61d2c64c 100644
--- a/erpnext/accounts/report/utils.py
+++ b/erpnext/accounts/report/utils.py
@@ -1,8 +1,16 @@
import frappe
+from frappe.query_builder.custom import ConstantColumn
+from frappe.query_builder.functions import Sum
from frappe.utils import flt, formatdate, get_datetime_str, get_table_name
+from pypika import Order
from erpnext import get_company_currency, get_default_company
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
+ get_accounting_dimensions,
+ get_dimension_with_children,
+)
from erpnext.accounts.doctype.fiscal_year.fiscal_year import get_from_and_to_date
+from erpnext.accounts.party import get_party_account
from erpnext.setup.utils import get_exchange_rate
__exchange_rates = {}
@@ -165,7 +173,7 @@ def get_query_columns(report_columns):
else:
columns.append(fieldname)
- return ", " + ", ".join(columns)
+ return columns
def get_values_for_columns(report_columns, report_row):
@@ -179,3 +187,203 @@ def get_values_for_columns(report_columns, report_row):
values[fieldname] = report_row.get(fieldname)
return values
+
+
+def get_party_details(party_type, party_list):
+ party_details = {}
+ party = frappe.qb.DocType(party_type)
+ query = frappe.qb.from_(party).select(party.name, party.tax_id).where(party.name.isin(party_list))
+ if party_type == "Supplier":
+ query = query.select(party.supplier_group)
+ else:
+ query = query.select(party.customer_group, party.territory)
+
+ party_detail_list = query.run(as_dict=True)
+ for party_dict in party_detail_list:
+ party_details[party_dict.name] = party_dict
+ return party_details
+
+
+def get_taxes_query(invoice_list, doctype, parenttype):
+ taxes = frappe.qb.DocType(doctype)
+
+ query = (
+ frappe.qb.from_(taxes)
+ .select(taxes.account_head)
+ .distinct()
+ .where(
+ (taxes.parenttype == parenttype)
+ & (taxes.docstatus == 1)
+ & (taxes.account_head.isnotnull())
+ & (taxes.parent.isin([inv.name for inv in invoice_list]))
+ )
+ .orderby(taxes.account_head)
+ )
+
+ if doctype == "Purchase Taxes and Charges":
+ return query.where(taxes.category.isin(["Total", "Valuation and Total"]))
+ elif doctype == "Sales Taxes and Charges":
+ return query
+ return query.where(taxes.charge_type.isin(["On Paid Amount", "Actual"]))
+
+
+def get_journal_entries(filters, args):
+ je = frappe.qb.DocType("Journal Entry")
+ journal_account = frappe.qb.DocType("Journal Entry Account")
+ query = (
+ frappe.qb.from_(je)
+ .inner_join(journal_account)
+ .on(je.name == journal_account.parent)
+ .select(
+ je.voucher_type.as_("doctype"),
+ je.name,
+ je.posting_date,
+ journal_account.account.as_(args.account),
+ journal_account.party.as_(args.party),
+ journal_account.party.as_(args.party_name),
+ je.bill_no,
+ je.bill_date,
+ je.remark.as_("remarks"),
+ je.total_amount.as_("base_net_total"),
+ je.total_amount.as_("base_grand_total"),
+ je.mode_of_payment,
+ journal_account.project,
+ )
+ .where(
+ (je.voucher_type == "Journal Entry")
+ & (journal_account.party == filters.get(args.party))
+ & (journal_account.account == args.party_account)
+ )
+ .orderby(je.posting_date, je.name, order=Order.desc)
+ )
+ query = apply_common_conditions(filters, query, doctype="Journal Entry", payments=True)
+
+ journal_entries = query.run(as_dict=True)
+ return journal_entries
+
+
+def get_payment_entries(filters, args):
+ pe = frappe.qb.DocType("Payment Entry")
+ query = (
+ frappe.qb.from_(pe)
+ .select(
+ ConstantColumn("Payment Entry").as_("doctype"),
+ pe.name,
+ pe.posting_date,
+ pe[args.account_fieldname].as_(args.account),
+ pe.party.as_(args.party),
+ pe.party_name.as_(args.party_name),
+ pe.remarks,
+ pe.paid_amount.as_("base_net_total"),
+ pe.paid_amount_after_tax.as_("base_grand_total"),
+ pe.mode_of_payment,
+ pe.project,
+ pe.cost_center,
+ )
+ .where(
+ (pe.party == filters.get(args.party)) & (pe[args.account_fieldname] == args.party_account)
+ )
+ .orderby(pe.posting_date, pe.name, order=Order.desc)
+ )
+ query = apply_common_conditions(filters, query, doctype="Payment Entry", payments=True)
+ payment_entries = query.run(as_dict=True)
+ return payment_entries
+
+
+def apply_common_conditions(filters, query, doctype, child_doctype=None, payments=False):
+ parent_doc = frappe.qb.DocType(doctype)
+ if child_doctype:
+ child_doc = frappe.qb.DocType(child_doctype)
+
+ join_required = False
+
+ if filters.get("company"):
+ query = query.where(parent_doc.company == filters.company)
+ if filters.get("from_date"):
+ query = query.where(parent_doc.posting_date >= filters.from_date)
+ if filters.get("to_date"):
+ query = query.where(parent_doc.posting_date <= filters.to_date)
+
+ if payments:
+ if filters.get("cost_center"):
+ query = query.where(parent_doc.cost_center == filters.cost_center)
+ else:
+ if filters.get("cost_center"):
+ query = query.where(child_doc.cost_center == filters.cost_center)
+ join_required = True
+ if filters.get("warehouse"):
+ query = query.where(child_doc.warehouse == filters.warehouse)
+ join_required = True
+ if filters.get("item_group"):
+ query = query.where(child_doc.item_group == filters.item_group)
+ join_required = True
+
+ if not payments:
+ if filters.get("brand"):
+ query = query.where(child_doc.brand == filters.brand)
+ join_required = True
+
+ if join_required:
+ query = query.inner_join(child_doc).on(parent_doc.name == child_doc.parent)
+ query = query.distinct()
+
+ if parent_doc.get_table_name() != "tabJournal Entry":
+ query = filter_invoices_based_on_dimensions(filters, query, parent_doc)
+
+ return query
+
+
+def get_advance_taxes_and_charges(invoice_list):
+ adv_taxes = frappe.qb.DocType("Advance Taxes and Charges")
+ return (
+ frappe.qb.from_(adv_taxes)
+ .select(
+ adv_taxes.parent,
+ adv_taxes.account_head,
+ (
+ frappe.qb.terms.Case()
+ .when(adv_taxes.add_deduct_tax == "Add", Sum(adv_taxes.base_tax_amount))
+ .else_(Sum(adv_taxes.base_tax_amount) * -1)
+ ).as_("tax_amount"),
+ )
+ .where(
+ (adv_taxes.parent.isin([inv.name for inv in invoice_list]))
+ & (adv_taxes.charge_type.isin(["On Paid Amount", "Actual"]))
+ & (adv_taxes.base_tax_amount != 0)
+ )
+ .groupby(adv_taxes.parent, adv_taxes.account_head, adv_taxes.add_deduct_tax)
+ ).run(as_dict=True)
+
+
+def filter_invoices_based_on_dimensions(filters, query, parent_doc):
+ accounting_dimensions = get_accounting_dimensions(as_list=False)
+ if accounting_dimensions:
+ for dimension in accounting_dimensions:
+ if filters.get(dimension.fieldname):
+ if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
+ filters[dimension.fieldname] = get_dimension_with_children(
+ dimension.document_type, filters.get(dimension.fieldname)
+ )
+ fieldname = dimension.fieldname
+ query = query.where(parent_doc[fieldname] == filters.fieldname)
+ return query
+
+
+def get_opening_row(party_type, party, from_date, company):
+ party_account = get_party_account(party_type, party, company)
+ gle = frappe.qb.DocType("GL Entry")
+ return (
+ frappe.qb.from_(gle)
+ .select(
+ ConstantColumn("Opening").as_("account"),
+ Sum(gle.debit).as_("debit"),
+ Sum(gle.credit).as_("credit"),
+ (Sum(gle.debit) - Sum(gle.credit)).as_("balance"),
+ )
+ .where(
+ (gle.account == party_account)
+ & (gle.party == party)
+ & (gle.posting_date < from_date)
+ & (gle.is_cancelled == 0)
+ )
+ ).run(as_dict=True)
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 47aba077b49..e580748866d 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -2365,6 +2365,7 @@ def validate_taxes_and_charges(tax):
def validate_account_head(idx, account, company, context=""):
account_company = frappe.get_cached_value("Account", account, "company")
+ is_group = frappe.get_cached_value("Account", account, "is_group")
if account_company != company:
frappe.throw(
@@ -2374,6 +2375,12 @@ def validate_account_head(idx, account, company, context=""):
title=_("Invalid Account"),
)
+ if is_group:
+ frappe.throw(
+ _("Row {0}: Account {1} is a Group Account").format(idx, frappe.bold(account)),
+ title=_("Invalid Account"),
+ )
+
def validate_cost_center(tax, doc):
if not tax.cost_center:
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index c22af3f22cb..e281fbc1ec9 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -1395,3 +1395,47 @@ def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None):
)
return doc
+
+
+def get_op_cost_from_sub_assemblies(bom_no, op_cost=0):
+ # Get operating cost from sub-assemblies
+
+ bom_items = frappe.get_all(
+ "BOM Item", filters={"parent": bom_no, "docstatus": 1}, fields=["bom_no"], order_by="idx asc"
+ )
+
+ for row in bom_items:
+ if not row.bom_no:
+ continue
+
+ if cost := frappe.get_cached_value("BOM", row.bom_no, "operating_cost_per_bom_quantity"):
+ op_cost += flt(cost)
+ get_op_cost_from_sub_assemblies(row.bom_no, op_cost)
+
+ return op_cost
+
+
+def get_scrap_items_from_sub_assemblies(bom_no, company, qty, scrap_items=None):
+ if not scrap_items:
+ scrap_items = {}
+
+ bom_items = frappe.get_all(
+ "BOM Item",
+ filters={"parent": bom_no, "docstatus": 1},
+ fields=["bom_no", "qty"],
+ order_by="idx asc",
+ )
+
+ for row in bom_items:
+ if not row.bom_no:
+ continue
+
+ qty = flt(row.qty) * flt(qty)
+ items = get_bom_items_as_dict(
+ row.bom_no, company, qty=qty, fetch_exploded=0, fetch_scrap_items=1
+ )
+ scrap_items.update(items)
+
+ get_scrap_items_from_sub_assemblies(row.bom_no, company, qty, scrap_items)
+
+ return scrap_items
diff --git a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json
index c75ac32cd12..27ecd57b873 100644
--- a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json
+++ b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json
@@ -37,7 +37,8 @@
"oldfieldname": "item_code",
"oldfieldtype": "Link",
"options": "Item",
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"fieldname": "item_name",
@@ -170,7 +171,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-11-14 18:35:40.856895",
+ "modified": "2024-01-02 13:49:36.211586",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Explosion Item",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 455aa7e5766..f47858ba9ff 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -227,35 +227,39 @@ class JobCard(Document):
def has_overlap(self, production_capacity, time_logs):
overlap = False
- if production_capacity == 1 and len(time_logs) > 0:
+ if production_capacity == 1 and len(time_logs) >= 1:
return True
+ if not len(time_logs):
+ return False
- # Check overlap exists or not between the overlapping time logs with the current Job Card
- for row in time_logs:
- count = 1
- for next_row in time_logs:
- if row.name == next_row.name:
- continue
-
- if (
- (
- get_datetime(next_row.from_time) >= get_datetime(row.from_time)
- and get_datetime(next_row.from_time) <= get_datetime(row.to_time)
- )
- or (
- get_datetime(next_row.to_time) >= get_datetime(row.from_time)
- and get_datetime(next_row.to_time) <= get_datetime(row.to_time)
- )
- or (
- get_datetime(next_row.from_time) <= get_datetime(row.from_time)
- and get_datetime(next_row.to_time) >= get_datetime(row.to_time)
- )
- ):
- count += 1
-
- if count > production_capacity:
- return True
-
+ # sorting overlapping job cards as per from_time
+ time_logs = sorted(time_logs, key=lambda x: x.get("from_time"))
+ # alloted_capacity has key number starting from 1. Key number will increment by 1 if non sequential job card found
+ # if key number reaches/crosses to production_capacity means capacity is full and overlap error generated
+ # this will store last to_time of sequential job cards
+ alloted_capacity = {1: time_logs[0]["to_time"]}
+ # flag for sequential Job card found
+ sequential_job_card_found = False
+ for i in range(1, len(time_logs)):
+ # scanning for all Existing keys
+ for key in alloted_capacity.keys():
+ # if current Job Card from time is greater than last to_time in that key means these job card are sequential
+ if alloted_capacity[key] <= time_logs[i]["from_time"]:
+ # So update key's value with last to_time
+ alloted_capacity[key] = time_logs[i]["to_time"]
+ # flag is true as we get sequential Job Card for that key
+ sequential_job_card_found = True
+ # Immediately break so that job card to time is not added with any other key except this
+ break
+ # if sequential job card not found above means it is overlapping so increment key number to alloted_capacity
+ if not sequential_job_card_found:
+ # increment key number
+ key = key + 1
+ # for that key last to time is assigned.
+ alloted_capacity[key] = time_logs[i]["to_time"]
+ if len(alloted_capacity) >= production_capacity:
+ # if number of keys greater or equal to production caoacity means full capacity is utilized and we should throw overlap error
+ return True
return overlap
def get_workstation_based_on_available_slot(self, existing) -> Optional[str]:
diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
index 01647d56c91..d3ad51f7236 100644
--- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
+++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
@@ -31,6 +31,7 @@
"job_card_excess_transfer",
"other_settings_section",
"update_bom_costs_automatically",
+ "set_op_cost_and_scrape_from_sub_assemblies",
"column_break_23",
"make_serial_no_batch_from_work_order"
],
@@ -194,13 +195,20 @@
"fieldname": "job_card_excess_transfer",
"fieldtype": "Check",
"label": "Allow Excess Material Transfer"
+ },
+ {
+ "default": "0",
+ "description": "In the case of 'Use Multi-Level BOM' in a work order, if the user wishes to add sub-assembly costs to Finished Goods items without using a job card as well the scrap items, then this option needs to be enable.",
+ "fieldname": "set_op_cost_and_scrape_from_sub_assemblies",
+ "fieldtype": "Check",
+ "label": "Set Operating Cost / Scrape Items From Sub-assemblies"
}
],
"icon": "icon-wrench",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-09-13 22:09:09.401559",
+ "modified": "2023-12-28 16:37:44.874096",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Settings",
@@ -216,5 +224,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 8c8e6efd92d..3695ae9d9d8 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -1591,6 +1591,10 @@ def make_bom(**args):
}
)
+ if args.operating_cost_per_bom_quantity:
+ bom.fg_based_operating_cost = 1
+ bom.operating_cost_per_bom_quantity = args.operating_cost_per_bom_quantity
+
for item in args.raw_materials:
item_doc = frappe.get_doc("Item", item)
bom.append(
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index b77e4e08735..74c2ece6e7e 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -1726,6 +1726,93 @@ class TestWorkOrder(FrappeTestCase):
frappe.db.set_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order", 0)
+ def test_op_cost_and_scrap_based_on_sub_assemblies(self):
+ # Make Sub Assembly BOM 1
+
+ frappe.db.set_single_value(
+ "Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies", 1
+ )
+
+ items = {
+ "Test Final FG Item": 0,
+ "Test Final SF Item 1": 0,
+ "Test Final SF Item 2": 0,
+ "Test Final RM Item 1": 100,
+ "Test Final RM Item 2": 200,
+ "Test Final Scrap Item 1": 50,
+ "Test Final Scrap Item 2": 60,
+ }
+
+ for item in items:
+ if not frappe.db.exists("Item", item):
+ item_properties = {"is_stock_item": 1, "valuation_rate": items[item]}
+
+ make_item(item_code=item, properties=item_properties),
+
+ prepare_boms_for_sub_assembly_test()
+
+ wo_order = make_wo_order_test_record(
+ production_item="Test Final FG Item",
+ qty=10,
+ use_multi_level_bom=1,
+ skip_transfer=1,
+ from_wip_warehouse=1,
+ )
+
+ se_doc = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
+ se_doc.save()
+
+ self.assertTrue(se_doc.additional_costs)
+ scrap_items = []
+ for item in se_doc.items:
+ if item.is_scrap_item:
+ scrap_items.append(item.item_code)
+
+ self.assertEqual(
+ sorted(scrap_items), sorted(["Test Final Scrap Item 1", "Test Final Scrap Item 2"])
+ )
+ for row in se_doc.additional_costs:
+ self.assertEqual(row.amount, 3000)
+
+ frappe.db.set_single_value(
+ "Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies", 0
+ )
+
+
+def prepare_boms_for_sub_assembly_test():
+ if not frappe.db.exists("BOM", {"item": "Test Final SF Item 1"}):
+ bom = make_bom(
+ item="Test Final SF Item 1",
+ source_warehouse="Stores - _TC",
+ raw_materials=["Test Final RM Item 1"],
+ operating_cost_per_bom_quantity=100,
+ do_not_submit=True,
+ )
+
+ bom.append("scrap_items", {"item_code": "Test Final Scrap Item 1", "qty": 1})
+
+ bom.submit()
+
+ if not frappe.db.exists("BOM", {"item": "Test Final SF Item 2"}):
+ bom = make_bom(
+ item="Test Final SF Item 2",
+ source_warehouse="Stores - _TC",
+ raw_materials=["Test Final RM Item 2"],
+ operating_cost_per_bom_quantity=200,
+ do_not_submit=True,
+ )
+
+ bom.append("scrap_items", {"item_code": "Test Final Scrap Item 2", "qty": 1})
+
+ bom.submit()
+
+ if not frappe.db.exists("BOM", {"item": "Test Final FG Item"}):
+ bom = make_bom(
+ item="Test Final FG Item",
+ source_warehouse="Stores - _TC",
+ raw_materials=["Test Final SF Item 1", "Test Final SF Item 2"],
+ )
+
def prepare_data_for_workstation_type_check():
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
@@ -1955,7 +2042,7 @@ def make_wo_order_test_record(**args):
wo_order.sales_order = args.sales_order or None
wo_order.planned_start_date = args.planned_start_date or now()
wo_order.transfer_material_against = args.transfer_material_against or "Work Order"
- wo_order.from_wip_warehouse = args.from_wip_warehouse or None
+ wo_order.from_wip_warehouse = args.from_wip_warehouse or 0
if args.source_warehouse:
for item in wo_order.get("required_items"):
diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js
index a585aa614fb..516c98a143d 100644
--- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js
+++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js
@@ -68,7 +68,7 @@ erpnext.HierarchyChart = class {
show() {
this.setup_actions();
- if ($(`[data-fieldname="company"]`).length) return;
+ if (this.page.main.find('[data-fieldname="company"]').length) return;
let me = this;
let company = this.page.add_field({
@@ -80,7 +80,7 @@ erpnext.HierarchyChart = class {
only_select: true,
reqd: 1,
change: () => {
- me.company = undefined;
+ me.company = '';
$('#hierarchy-chart-wrapper').remove();
if (company.get_value()) {
@@ -219,8 +219,8 @@ erpnext.HierarchyChart = class {
}
}).then(r => {
if (r.message.length) {
- let expand_node = undefined;
- let node = undefined;
+ let expand_node;
+ let node;
$.each(r.message, (_i, data) => {
if ($(`[id="${data.id}"]`).length)
@@ -229,7 +229,7 @@ erpnext.HierarchyChart = class {
node = new me.Node({
id: data.id,
parent: $('