mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-15 03:08:47 +00:00
Compare commits
11 Commits
version-15
...
v15.116.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41038979ec | ||
|
|
88b6779062 | ||
|
|
b5f784612d | ||
|
|
457424f7a4 | ||
|
|
25ee3695f0 | ||
|
|
ff205da810 | ||
|
|
2980171007 | ||
|
|
c6c4815e8d | ||
|
|
82a85818c2 | ||
|
|
57a2be6b56 | ||
|
|
47f54a4725 |
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.112.0"
|
||||
__version__ = "15.116.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -2811,7 +2811,9 @@ def get_reference_details(
|
||||
exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
|
||||
else:
|
||||
exchange_rate = 1
|
||||
outstanding_amount, total_amount = get_outstanding_on_journal_entry(reference_name, party_type, party)
|
||||
outstanding_amount, total_amount = get_outstanding_on_journal_entry(
|
||||
reference_name, party_type, party
|
||||
)
|
||||
|
||||
elif reference_doctype == "Payment Entry":
|
||||
if reverse_payment_details := frappe.db.get_all(
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-07-01 13:37:41.185347",
|
||||
"modified": "2017-02-24 20:09:46.150861",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Payable",
|
||||
@@ -15,11 +15,6 @@
|
||||
"ref_doctype": "Purchase Invoice",
|
||||
"report_name": "Accounts Payable",
|
||||
"report_type": "Script Report",
|
||||
"doctype_to_sync": [
|
||||
{
|
||||
"doc_type": "Payment Ledger Entry"
|
||||
}
|
||||
],
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
@@ -33,7 +28,5 @@
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"snapshot_report": 0,
|
||||
"timeout": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-07-01 13:37:44.167999",
|
||||
"modified": "2017-03-06 05:52:06.235584",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Receivable",
|
||||
@@ -15,11 +15,6 @@
|
||||
"ref_doctype": "Sales Invoice",
|
||||
"report_name": "Accounts Receivable",
|
||||
"report_type": "Script Report",
|
||||
"doctype_to_sync": [
|
||||
{
|
||||
"doc_type": "Payment Ledger Entry"
|
||||
}
|
||||
],
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
@@ -27,7 +22,5 @@
|
||||
{
|
||||
"role": "Accounts User"
|
||||
}
|
||||
],
|
||||
"snapshot_report": 0,
|
||||
"timeout": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
"doctype": "Report",
|
||||
"idx": 2,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-06-22 13:38:25.236839",
|
||||
"modified": "2018-09-07 12:18:21.850851",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Balance Sheet",
|
||||
@@ -15,11 +15,6 @@
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "Balance Sheet",
|
||||
"report_type": "Script Report",
|
||||
"doctype_to_sync": [
|
||||
{
|
||||
"doc_type": "GL Entry"
|
||||
}
|
||||
],
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
@@ -30,7 +25,5 @@
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"synced_report": 0,
|
||||
"timeout": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4,23 +4,14 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_days, cint, flt
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
accumulate_values_into_parents,
|
||||
add_total_row,
|
||||
calculate_values,
|
||||
compute_growth_view_data,
|
||||
filter_accounts,
|
||||
filter_out_zero_value_rows,
|
||||
get_accounting_entries,
|
||||
get_accounts,
|
||||
get_appropriate_currency,
|
||||
get_columns,
|
||||
get_data,
|
||||
get_filtered_list_for_consolidated_report,
|
||||
get_period_list,
|
||||
prepare_data,
|
||||
)
|
||||
|
||||
|
||||
@@ -268,196 +259,3 @@ def get_chart_data(filters, chart_columns, asset, liability, equity, currency):
|
||||
chart["currency"] = currency
|
||||
|
||||
return chart
|
||||
|
||||
|
||||
def execute_snapshot_report(filters):
|
||||
from frappe.database.duckdb.database import get_latest_sync
|
||||
|
||||
if not (conn := get_latest_sync("GL Entry")):
|
||||
frappe.throw(_("Balance Sheet requires {0} to be synced to DuckDB").format(frappe.bold("GL Entry")))
|
||||
|
||||
period_list = get_period_list(
|
||||
filters.from_fiscal_year,
|
||||
filters.to_fiscal_year,
|
||||
filters.period_start_date,
|
||||
filters.period_end_date,
|
||||
filters.filter_based_on,
|
||||
filters.periodicity,
|
||||
company=filters.company,
|
||||
)
|
||||
filters.period_start_date = period_list[0]["year_start_date"]
|
||||
|
||||
currency = filters.presentation_currency or frappe.get_cached_value(
|
||||
"Company", filters.company, "default_currency"
|
||||
)
|
||||
|
||||
asset = _get_data_duckdb(conn, filters, "Asset", "Debit", period_list)
|
||||
liability = _get_data_duckdb(conn, filters, "Liability", "Credit", period_list)
|
||||
equity = _get_data_duckdb(conn, filters, "Equity", "Credit", period_list)
|
||||
|
||||
provisional_profit_loss, total_credit = get_provisional_profit_loss(
|
||||
asset, liability, equity, period_list, filters.company, currency
|
||||
)
|
||||
message, opening_balance = check_opening_balance(asset, liability, equity)
|
||||
|
||||
data = []
|
||||
data.extend(asset or [])
|
||||
data.extend(liability or [])
|
||||
data.extend(equity or [])
|
||||
if opening_balance and round(opening_balance, 2) != 0:
|
||||
unclosed = {
|
||||
"account_name": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'",
|
||||
"account": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'",
|
||||
"warn_if_negative": True,
|
||||
"currency": currency,
|
||||
}
|
||||
for period in period_list:
|
||||
unclosed[period.key] = opening_balance
|
||||
if provisional_profit_loss:
|
||||
provisional_profit_loss[period.key] = provisional_profit_loss[period.key] - opening_balance
|
||||
unclosed["total"] = opening_balance
|
||||
data.append(unclosed)
|
||||
|
||||
if provisional_profit_loss:
|
||||
data.append(provisional_profit_loss)
|
||||
if total_credit:
|
||||
data.append(total_credit)
|
||||
|
||||
columns = get_columns(
|
||||
filters.periodicity, period_list, filters.accumulated_values, company=filters.company
|
||||
)
|
||||
chart = get_chart_data(filters, period_list, asset, liability, equity, currency)
|
||||
report_summary, primitive_summary = get_report_summary(
|
||||
period_list, asset, liability, equity, provisional_profit_loss, currency, filters
|
||||
)
|
||||
|
||||
if filters.get("selected_view") == "Growth":
|
||||
compute_growth_view_data(data, period_list)
|
||||
|
||||
return columns, data, message, chart, report_summary, primitive_summary
|
||||
|
||||
|
||||
def _get_data_duckdb(conn, filters, root_type, balance_must_be, period_list):
|
||||
accounts = get_accounts(filters.company, root_type)
|
||||
if not accounts:
|
||||
return None
|
||||
|
||||
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
|
||||
company_currency = get_appropriate_currency(filters.company, filters)
|
||||
|
||||
gl_entries_by_account = {}
|
||||
_load_gl_entries_duckdb(conn, filters, period_list, accounts, gl_entries_by_account, root_type)
|
||||
|
||||
calculate_values(
|
||||
accounts_by_name,
|
||||
gl_entries_by_account,
|
||||
period_list,
|
||||
filters.accumulated_values,
|
||||
False,
|
||||
)
|
||||
accumulate_values_into_parents(accounts, accounts_by_name, period_list)
|
||||
|
||||
out = prepare_data(
|
||||
accounts,
|
||||
balance_must_be,
|
||||
period_list,
|
||||
company_currency,
|
||||
accumulated_values=filters.accumulated_values,
|
||||
)
|
||||
out = filter_out_zero_value_rows(out, parent_children_map, filters.show_zero_values)
|
||||
|
||||
if out:
|
||||
add_total_row(out, root_type, balance_must_be, period_list, company_currency)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def _load_gl_entries_duckdb(conn, filters, period_list, accounts, gl_entries_by_account, root_type):
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import (
|
||||
_extra_gl_conditions,
|
||||
_fetch_gl_rows_duckdb,
|
||||
)
|
||||
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
|
||||
|
||||
company = filters.company
|
||||
year_start_date = period_list[0]["year_start_date"]
|
||||
last_to_date = period_list[-1]["to_date"]
|
||||
ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting")
|
||||
|
||||
leaf_accounts = [acc.name for acc in accounts if not acc.is_group]
|
||||
if not leaf_accounts:
|
||||
return
|
||||
|
||||
opening_from_date = None
|
||||
ignore_opening_entries = False
|
||||
|
||||
ignore_closing_balances = frappe.get_single_value("Accounts Settings", "ignore_account_closing_balance")
|
||||
if not ignore_closing_balances:
|
||||
last_pcv_list = frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"company": company,
|
||||
"period_end_date": ("<", filters.get("period_start_date") or year_start_date),
|
||||
},
|
||||
fields=["period_end_date", "name"],
|
||||
order_by="period_end_date desc",
|
||||
limit=1,
|
||||
)
|
||||
if last_pcv_list:
|
||||
last_pcv = last_pcv_list[0]
|
||||
pcv_entries = get_accounting_entries(
|
||||
"Account Closing Balance",
|
||||
None,
|
||||
last_to_date,
|
||||
filters,
|
||||
root_type=root_type,
|
||||
ignore_closing_entries=False,
|
||||
period_closing_voucher=last_pcv.name,
|
||||
)
|
||||
if filters.get("presentation_currency"):
|
||||
convert_to_presentation_currency(pcv_entries, get_currency(filters))
|
||||
for entry in pcv_entries:
|
||||
gl_entries_by_account.setdefault(entry.account, []).append(entry)
|
||||
opening_from_date = add_days(last_pcv.period_end_date, 1)
|
||||
ignore_opening_entries = True
|
||||
|
||||
extra_cond, extra_params = _extra_gl_conditions(filters)
|
||||
account_placeholders = ", ".join(["?"] * len(leaf_accounts))
|
||||
base_conds = [
|
||||
"company = ?",
|
||||
"is_cancelled = 0",
|
||||
f"account IN ({account_placeholders})",
|
||||
]
|
||||
base_params = [company, *leaf_accounts]
|
||||
if ignore_opening_entries and not ignore_is_opening:
|
||||
base_conds.append("is_opening = 'No'")
|
||||
base_conds.extend(extra_cond)
|
||||
base_params.extend(extra_params)
|
||||
|
||||
# Opening GL entries from DuckDB (entries before year_start_date)
|
||||
open_conds = [*base_conds, "posting_date < ?"]
|
||||
open_params = [*base_params, year_start_date]
|
||||
if opening_from_date:
|
||||
open_conds = [*open_conds, "posting_date >= ?"]
|
||||
open_params = [*open_params, opening_from_date]
|
||||
|
||||
opening_entries = _fetch_gl_rows_duckdb(conn, open_conds, open_params)
|
||||
if filters.get("presentation_currency"):
|
||||
convert_to_presentation_currency(opening_entries, get_currency(filters))
|
||||
synthetic_open_date = add_days(year_start_date, -1)
|
||||
for entry in opening_entries:
|
||||
entry.posting_date = synthetic_open_date
|
||||
gl_entries_by_account.setdefault(entry.account, []).append(entry)
|
||||
|
||||
# Period GL entries from DuckDB (one aggregated query per period)
|
||||
for period in period_list:
|
||||
period_conds = [*base_conds, "posting_date >= ?", "posting_date <= ?"]
|
||||
period_params = [*base_params, period.from_date, period.to_date]
|
||||
|
||||
period_entries = _fetch_gl_rows_duckdb(conn, period_conds, period_params)
|
||||
if filters.get("presentation_currency"):
|
||||
convert_to_presentation_currency(period_entries, get_currency(filters))
|
||||
for entry in period_entries:
|
||||
entry.posting_date = period.to_date
|
||||
gl_entries_by_account.setdefault(entry.account, []).append(entry)
|
||||
|
||||
@@ -6,15 +6,11 @@
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"doctype_to_sync": [
|
||||
{
|
||||
"doc_type": "GL Entry"
|
||||
}
|
||||
],
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-07-01 13:36:06.682661",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 15:47:59.597853",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "General Ledger",
|
||||
@@ -34,6 +30,5 @@
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"snapshot_report": 0,
|
||||
"timeout": 0
|
||||
}
|
||||
|
||||
@@ -792,288 +792,3 @@ def get_columns(filters):
|
||||
columns.extend([{"label": _("Remarks"), "fieldname": "remarks", "width": 400}])
|
||||
|
||||
return columns
|
||||
|
||||
|
||||
def execute_snapshot_report(filters):
|
||||
from frappe.database.duckdb.database import get_latest_sync
|
||||
|
||||
if conn := get_latest_sync("GL Entry"):
|
||||
return _execute_with_duckdb_conn(filters, conn)
|
||||
|
||||
frappe.throw(_("General Ledger requires {0} to be synced to DuckDB").format(frappe.bold("GL Entry")))
|
||||
|
||||
|
||||
def _execute_with_duckdb_conn(filters, conn):
|
||||
if not filters:
|
||||
return [], []
|
||||
|
||||
account_details = {}
|
||||
|
||||
if filters.get("print_in_account_currency") and not filters.get("account"):
|
||||
frappe.throw(_("Select an account to print in account currency"))
|
||||
|
||||
for acc in frappe.get_all("Account", fields=["name", "is_group"]):
|
||||
account_details.setdefault(acc.name, acc)
|
||||
|
||||
if filters.get("party"):
|
||||
filters.party = frappe.parse_json(filters.get("party"))
|
||||
|
||||
validate_filters(filters, account_details)
|
||||
validate_party(filters)
|
||||
filters = set_account_currency(filters)
|
||||
columns = get_columns(filters)
|
||||
res = get_result_duckdb(filters, account_details, conn)
|
||||
return columns, res
|
||||
|
||||
|
||||
def get_result_duckdb(filters, account_details, conn):
|
||||
accounting_dimensions = []
|
||||
if filters.get("include_dimensions"):
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
|
||||
gl_entries = get_gl_entries_duckdb(filters, accounting_dimensions, conn)
|
||||
data = get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries)
|
||||
return get_result_as_list(data, filters)
|
||||
|
||||
|
||||
def get_gl_entries_duckdb(filters, accounting_dimensions, conn):
|
||||
currency_map = get_currency(filters)
|
||||
|
||||
col_names = [
|
||||
"gl_entry",
|
||||
"posting_date",
|
||||
"account",
|
||||
"party_type",
|
||||
"party",
|
||||
"voucher_type",
|
||||
"voucher_subtype",
|
||||
"voucher_no",
|
||||
"cost_center",
|
||||
"project",
|
||||
"against_voucher_type",
|
||||
"against_voucher",
|
||||
"account_currency",
|
||||
"against",
|
||||
"is_opening",
|
||||
"creation",
|
||||
"debit",
|
||||
"credit",
|
||||
"debit_in_account_currency",
|
||||
"credit_in_account_currency",
|
||||
]
|
||||
select_exprs = [
|
||||
"name",
|
||||
"posting_date",
|
||||
"account",
|
||||
"party_type",
|
||||
"party",
|
||||
"voucher_type",
|
||||
"voucher_subtype",
|
||||
"voucher_no",
|
||||
"cost_center",
|
||||
"project",
|
||||
"against_voucher_type",
|
||||
"against_voucher",
|
||||
"account_currency",
|
||||
"against",
|
||||
"is_opening",
|
||||
"creation",
|
||||
"debit",
|
||||
"credit",
|
||||
"debit_in_account_currency",
|
||||
"credit_in_account_currency",
|
||||
]
|
||||
|
||||
if filters.get("show_remarks"):
|
||||
remarks_length = frappe.get_single_value("Accounts Settings", "general_ledger_remarks_length")
|
||||
if remarks_length:
|
||||
select_exprs.append(f"substr(remarks, 1, {int(remarks_length)})")
|
||||
else:
|
||||
select_exprs.append("remarks")
|
||||
col_names.append("remarks")
|
||||
|
||||
if filters.get("add_values_in_transaction_currency"):
|
||||
select_exprs += [
|
||||
"debit_in_transaction_currency",
|
||||
"credit_in_transaction_currency",
|
||||
"transaction_currency",
|
||||
]
|
||||
col_names += [
|
||||
"debit_in_transaction_currency",
|
||||
"credit_in_transaction_currency",
|
||||
"transaction_currency",
|
||||
]
|
||||
|
||||
if accounting_dimensions:
|
||||
select_exprs += accounting_dimensions
|
||||
col_names += accounting_dimensions
|
||||
|
||||
order_by = "posting_date, account, creation"
|
||||
if filters.get("include_dimensions"):
|
||||
order_by = "posting_date, creation"
|
||||
if filters.get("categorize_by") == "Categorize by Voucher":
|
||||
order_by = "posting_date, voucher_type, voucher_no"
|
||||
if filters.get("categorize_by") == "Categorize by Account":
|
||||
order_by = "account, posting_date, creation"
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
filters["company_fb"] = frappe.get_cached_value(
|
||||
"Company", filters.get("company"), "default_finance_book"
|
||||
)
|
||||
|
||||
conditions, params = _build_gl_conditions_duckdb(filters)
|
||||
select_clause = ", ".join(select_exprs)
|
||||
sql = f'SELECT {select_clause} FROM "tabGL Entry" WHERE {" AND ".join(conditions)} ORDER BY {order_by}'
|
||||
|
||||
rows = conn.execute(sql, params).fetchall()
|
||||
gl_entries = [frappe._dict(zip(col_names, row, strict=False)) for row in rows]
|
||||
|
||||
party_name_map = get_party_name_map()
|
||||
for gl_entry in gl_entries:
|
||||
if gl_entry.party_type and gl_entry.party:
|
||||
gl_entry.party_name = party_name_map.get(gl_entry.party_type, {}).get(gl_entry.party)
|
||||
|
||||
if filters.get("presentation_currency"):
|
||||
return convert_to_presentation_currency(gl_entries, currency_map, filters)
|
||||
return gl_entries
|
||||
|
||||
|
||||
def _build_gl_conditions_duckdb(filters):
|
||||
ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting")
|
||||
|
||||
conditions = ["company = ?"]
|
||||
params = [filters.company]
|
||||
|
||||
if filters.get("account"):
|
||||
filters.account = get_accounts_with_children(filters.account)
|
||||
if filters.account:
|
||||
conditions.append(f"account IN ({', '.join(['?'] * len(filters.account))})")
|
||||
params.extend(filters.account)
|
||||
|
||||
if filters.get("cost_center"):
|
||||
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
|
||||
conditions.append(f"cost_center IN ({', '.join(['?'] * len(filters.cost_center))})")
|
||||
params.extend(filters.cost_center)
|
||||
|
||||
if filters.get("voucher_no"):
|
||||
conditions.append("voucher_no = ?")
|
||||
params.append(filters.voucher_no)
|
||||
|
||||
if filters.get("against_voucher_no"):
|
||||
conditions.append("against_voucher = ?")
|
||||
params.append(filters.against_voucher_no)
|
||||
|
||||
if filters.get("ignore_err"):
|
||||
err_journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"company": filters.get("company"),
|
||||
"docstatus": 1,
|
||||
"voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]),
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
if err_journals:
|
||||
filters.update({"voucher_no_not_in": err_journals})
|
||||
|
||||
if filters.get("ignore_cr_dr_notes"):
|
||||
system_generated = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"company": filters.get("company"),
|
||||
"docstatus": 1,
|
||||
"voucher_type": ("in", ["Credit Note", "Debit Note"]),
|
||||
"is_system_generated": 1,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
if system_generated:
|
||||
vouchers_to_ignore = (filters.get("voucher_no_not_in") or []) + system_generated
|
||||
filters.update({"voucher_no_not_in": vouchers_to_ignore})
|
||||
|
||||
if filters.get("voucher_no_not_in"):
|
||||
vouchers = filters.voucher_no_not_in
|
||||
conditions.append(f"voucher_no NOT IN ({', '.join(['?'] * len(vouchers))})")
|
||||
params.extend(vouchers)
|
||||
|
||||
if filters.get("categorize_by") == "Categorize by Party" and not filters.get("party_type"):
|
||||
conditions.append("party_type IN ('Customer', 'Supplier')")
|
||||
|
||||
if filters.get("party_type"):
|
||||
conditions.append("party_type = ?")
|
||||
params.append(filters.party_type)
|
||||
|
||||
if filters.get("party"):
|
||||
conditions.append(f"party IN ({', '.join(['?'] * len(filters.party))})")
|
||||
params.extend(filters.party)
|
||||
|
||||
# from_date: skip when filtering by account/party to allow opening balance calc in Python
|
||||
if filters.get("disable_opening_balance_calculation"):
|
||||
if not ignore_is_opening:
|
||||
conditions.append("(posting_date >= ? OR is_opening = 'Yes')")
|
||||
else:
|
||||
conditions.append("posting_date >= ?")
|
||||
params.append(filters.from_date)
|
||||
elif not (
|
||||
filters.get("account")
|
||||
or filters.get("party")
|
||||
or filters.get("categorize_by") in ["Categorize by Account", "Categorize by Party"]
|
||||
):
|
||||
if not ignore_is_opening:
|
||||
conditions.append("(posting_date >= ? OR is_opening = 'Yes')")
|
||||
else:
|
||||
conditions.append("posting_date >= ?")
|
||||
params.append(filters.from_date)
|
||||
|
||||
if not ignore_is_opening:
|
||||
conditions.append("(posting_date <= ? OR is_opening = 'Yes')")
|
||||
else:
|
||||
conditions.append("posting_date <= ?")
|
||||
params.append(filters.to_date)
|
||||
|
||||
if filters.get("project"):
|
||||
conditions.append(f"project IN ({', '.join(['?'] * len(filters.project))})")
|
||||
params.extend(filters.project)
|
||||
|
||||
company_fb = filters.get("company_fb") or frappe.get_cached_value(
|
||||
"Company", filters.company, "default_finance_book"
|
||||
)
|
||||
if filters.get("include_default_book_entries"):
|
||||
if filters.get("finance_book"):
|
||||
if company_fb and cstr(filters.finance_book) != cstr(company_fb):
|
||||
frappe.throw(
|
||||
_("To use a different finance book, please uncheck 'Include Default FB Entries'")
|
||||
)
|
||||
fb_vals = [cstr(filters.finance_book), ""]
|
||||
else:
|
||||
fb_vals = [cstr(company_fb), ""]
|
||||
conditions.append(f"(finance_book IN ({', '.join(['?'] * len(fb_vals))}) OR finance_book IS NULL)")
|
||||
params.extend(fb_vals)
|
||||
else:
|
||||
if filters.get("finance_book"):
|
||||
conditions.append("(finance_book IN (?, '') OR finance_book IS NULL)")
|
||||
params.append(cstr(filters.finance_book))
|
||||
else:
|
||||
conditions.append("(finance_book IN ('') OR finance_book IS NULL)")
|
||||
|
||||
if not filters.get("show_cancelled_entries"):
|
||||
conditions.append("is_cancelled = 0")
|
||||
|
||||
accounting_dimensions_list = get_accounting_dimensions(as_list=False)
|
||||
if accounting_dimensions_list:
|
||||
for dimension in accounting_dimensions_list:
|
||||
if not dimension.disabled and dimension.document_type != "Finance Book":
|
||||
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)
|
||||
)
|
||||
vals = (
|
||||
filters[dimension.fieldname]
|
||||
if isinstance(filters[dimension.fieldname], list)
|
||||
else [filters[dimension.fieldname]]
|
||||
)
|
||||
conditions.append(f"{dimension.fieldname} IN ({', '.join(['?'] * len(vals))})")
|
||||
params.extend(vals)
|
||||
|
||||
return conditions, params
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"doctype": "Report",
|
||||
"idx": 2,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-07-01 13:36:14.934965",
|
||||
"modified": "2017-02-24 20:12:40.282376",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Profit and Loss Statement",
|
||||
@@ -15,12 +15,6 @@
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "Profit and Loss Statement",
|
||||
"report_type": "Script Report",
|
||||
"doctype_to_sync": [
|
||||
{
|
||||
"doc_type": "GL Entry"
|
||||
}
|
||||
],
|
||||
"filters": [],
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
@@ -31,7 +25,5 @@
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"snapshot_report": 0,
|
||||
"timeout": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -7,20 +7,12 @@ from frappe import _
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
accumulate_values_into_parents,
|
||||
add_total_row,
|
||||
calculate_values,
|
||||
compute_growth_view_data,
|
||||
compute_margin_view_data,
|
||||
filter_accounts,
|
||||
filter_out_zero_value_rows,
|
||||
get_accounts,
|
||||
get_appropriate_currency,
|
||||
get_columns,
|
||||
get_data,
|
||||
get_filtered_list_for_consolidated_report,
|
||||
get_period_list,
|
||||
prepare_data,
|
||||
)
|
||||
|
||||
|
||||
@@ -201,125 +193,3 @@ def get_chart_data(filters, chart_columns, income, expense, net_profit_loss, cur
|
||||
chart["currency"] = currency
|
||||
|
||||
return chart
|
||||
|
||||
|
||||
def execute_snapshot_report(filters):
|
||||
from frappe.database.duckdb.database import get_latest_sync
|
||||
|
||||
if not (conn := get_latest_sync("GL Entry")):
|
||||
frappe.throw(
|
||||
_("Profit and Loss Statement requires {0} to be synced to DuckDB").format(frappe.bold("GL Entry"))
|
||||
)
|
||||
|
||||
period_list = get_period_list(
|
||||
filters.from_fiscal_year,
|
||||
filters.to_fiscal_year,
|
||||
filters.period_start_date,
|
||||
filters.period_end_date,
|
||||
filters.filter_based_on,
|
||||
filters.periodicity,
|
||||
company=filters.company,
|
||||
)
|
||||
|
||||
income = _get_data_duckdb(conn, filters, "Income", "Credit", period_list)
|
||||
expense = _get_data_duckdb(conn, filters, "Expense", "Debit", period_list)
|
||||
|
||||
net_profit_loss = get_net_profit_loss(
|
||||
income, expense, period_list, filters.company, filters.presentation_currency
|
||||
)
|
||||
|
||||
data = []
|
||||
data.extend(income or [])
|
||||
data.extend(expense or [])
|
||||
if net_profit_loss:
|
||||
data.append(net_profit_loss)
|
||||
|
||||
columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company)
|
||||
|
||||
currency = filters.presentation_currency or frappe.get_cached_value(
|
||||
"Company", filters.company, "default_currency"
|
||||
)
|
||||
chart = get_chart_data(filters, period_list, income, expense, net_profit_loss, currency)
|
||||
|
||||
report_summary, primitive_summary = get_report_summary(
|
||||
period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters
|
||||
)
|
||||
|
||||
if filters.get("selected_view") == "Growth":
|
||||
compute_growth_view_data(data, period_list)
|
||||
|
||||
if filters.get("selected_view") == "Margin":
|
||||
compute_margin_view_data(data, period_list, filters.accumulated_values)
|
||||
|
||||
return columns, data, None, chart, report_summary, primitive_summary
|
||||
|
||||
|
||||
def _get_data_duckdb(conn, filters, root_type, balance_must_be, period_list):
|
||||
accounts = get_accounts(filters.company, root_type)
|
||||
if not accounts:
|
||||
return None
|
||||
|
||||
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
|
||||
company_currency = get_appropriate_currency(filters.company, filters)
|
||||
|
||||
gl_entries_by_account = {}
|
||||
_load_gl_entries_duckdb(conn, filters, period_list, accounts, gl_entries_by_account)
|
||||
|
||||
calculate_values(
|
||||
accounts_by_name,
|
||||
gl_entries_by_account,
|
||||
period_list,
|
||||
filters.accumulated_values,
|
||||
False,
|
||||
)
|
||||
accumulate_values_into_parents(accounts, accounts_by_name, period_list)
|
||||
|
||||
out = prepare_data(
|
||||
accounts,
|
||||
balance_must_be,
|
||||
period_list,
|
||||
company_currency,
|
||||
accumulated_values=filters.accumulated_values,
|
||||
)
|
||||
out = filter_out_zero_value_rows(out, parent_children_map, filters.show_zero_values)
|
||||
|
||||
if out:
|
||||
add_total_row(out, root_type, balance_must_be, period_list, company_currency)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def _load_gl_entries_duckdb(conn, filters, period_list, accounts, gl_entries_by_account):
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import (
|
||||
_extra_gl_conditions,
|
||||
_fetch_gl_rows_duckdb,
|
||||
)
|
||||
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
|
||||
|
||||
company = filters.company
|
||||
leaf_accounts = [acc.name for acc in accounts if not acc.is_group]
|
||||
if not leaf_accounts:
|
||||
return
|
||||
|
||||
extra_cond, extra_params = _extra_gl_conditions(filters)
|
||||
account_placeholders = ", ".join(["?"] * len(leaf_accounts))
|
||||
base_conds = [
|
||||
"company = ?",
|
||||
"is_cancelled = 0",
|
||||
f"account IN ({account_placeholders})",
|
||||
"voucher_type != 'Period Closing Voucher'",
|
||||
]
|
||||
base_params = [company, *leaf_accounts]
|
||||
base_conds.extend(extra_cond)
|
||||
base_params.extend(extra_params)
|
||||
|
||||
for period in period_list:
|
||||
period_conds = [*base_conds, "posting_date >= ?", "posting_date <= ?"]
|
||||
period_params = [*base_params, period.from_date, period.to_date]
|
||||
|
||||
period_entries = _fetch_gl_rows_duckdb(conn, period_conds, period_params)
|
||||
if filters.get("presentation_currency"):
|
||||
convert_to_presentation_currency(period_entries, get_currency(filters))
|
||||
for entry in period_entries:
|
||||
entry.posting_date = period.to_date
|
||||
gl_entries_by_account.setdefault(entry.account, []).append(entry)
|
||||
|
||||
@@ -7,18 +7,13 @@
|
||||
"doctype": "Report",
|
||||
"idx": 2,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-07-01 17:32:21.801141",
|
||||
"modified": "2017-02-24 20:12:33.520866",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Trial Balance",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "Trial Balance",
|
||||
"doctype_to_sync": [
|
||||
{
|
||||
"doc_type": "GL Entry"
|
||||
}
|
||||
],
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
@@ -30,7 +25,5 @@
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"snapshot_report": 0,
|
||||
"timeout": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -510,215 +510,3 @@ def hide_group_accounts(data):
|
||||
d.update(indent=0)
|
||||
non_group_accounts_data.append(d)
|
||||
return non_group_accounts_data
|
||||
|
||||
|
||||
def execute_snapshot_report(filters):
|
||||
from frappe.database.duckdb.database import get_latest_sync
|
||||
|
||||
if conn := get_latest_sync("GL Entry"):
|
||||
validate_filters(filters)
|
||||
columns = get_columns()
|
||||
data = get_data_duckdb(filters, conn)
|
||||
return columns, data
|
||||
else:
|
||||
frappe.throw(_("Trial Balance requires {0} to be synced to DuckDB").format(frappe.bold("GL Entry")))
|
||||
|
||||
|
||||
def get_data_duckdb(filters, conn):
|
||||
# accounts and all metadata via frappe.db — only GL Entry comes from DuckDB
|
||||
accounts = frappe.db.sql(
|
||||
"""select name, account_number, parent_account, account_name, root_type, report_type, is_group, lft, rgt
|
||||
from `tabAccount` where company=%s order by lft""",
|
||||
filters.company,
|
||||
as_dict=True,
|
||||
)
|
||||
if not accounts:
|
||||
return None
|
||||
|
||||
company_currency = filters.presentation_currency or erpnext.get_company_currency(filters.company)
|
||||
ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting")
|
||||
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
|
||||
|
||||
gl_entries_by_account = get_period_gl_entries_duckdb(conn, filters, ignore_is_opening)
|
||||
opening_balances = get_opening_balances_duckdb(conn, filters, ignore_is_opening)
|
||||
|
||||
calculate_values(
|
||||
accounts,
|
||||
gl_entries_by_account,
|
||||
opening_balances,
|
||||
filters.get("show_net_values"),
|
||||
ignore_is_opening=ignore_is_opening,
|
||||
)
|
||||
accumulate_values_into_parents(accounts, accounts_by_name)
|
||||
|
||||
data = prepare_data(accounts, filters, parent_children_map, company_currency)
|
||||
return filter_out_zero_value_rows(
|
||||
data, parent_children_map, show_zero_values=filters.get("show_zero_values")
|
||||
)
|
||||
|
||||
|
||||
def _extra_gl_conditions(filters):
|
||||
"""Returns (conditions, params) for optional shared GL Entry filters."""
|
||||
conditions, params = [], []
|
||||
|
||||
if filters.get("cost_center"):
|
||||
cc = get_cost_centers_with_children(filters.get("cost_center"))
|
||||
conditions.append(f"cost_center IN ({', '.join(['?'] * len(cc))})")
|
||||
params.extend(cc)
|
||||
|
||||
if filters.get("project"):
|
||||
proj = filters.project if isinstance(filters.project, list) else [filters.project]
|
||||
conditions.append(f"project IN ({', '.join(['?'] * len(proj))})")
|
||||
params.extend(proj)
|
||||
|
||||
if frappe.db.count("Finance Book"):
|
||||
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
|
||||
if filters.get("include_default_book_entries"):
|
||||
if filters.get("finance_book") and company_fb and cstr(filters.finance_book) != cstr(company_fb):
|
||||
frappe.throw(
|
||||
_("To use a different finance book, please uncheck 'Include Default FB Entries'")
|
||||
)
|
||||
fb_list = [cstr(filters.get("finance_book")), cstr(company_fb), ""]
|
||||
else:
|
||||
fb_list = [cstr(filters.get("finance_book")), ""]
|
||||
conditions.append(f"(finance_book IN ({', '.join(['?'] * len(fb_list))}) OR finance_book IS NULL)")
|
||||
params.extend(fb_list)
|
||||
|
||||
for dim in get_accounting_dimensions(as_list=False):
|
||||
if filters.get(dim.fieldname):
|
||||
if frappe.get_cached_value("DocType", dim.document_type, "is_tree"):
|
||||
filters[dim.fieldname] = get_dimension_with_children(
|
||||
dim.document_type, filters.get(dim.fieldname)
|
||||
)
|
||||
vals = (
|
||||
filters[dim.fieldname]
|
||||
if isinstance(filters[dim.fieldname], list)
|
||||
else [filters[dim.fieldname]]
|
||||
)
|
||||
conditions.append(f"{dim.fieldname} IN ({', '.join(['?'] * len(vals))})")
|
||||
params.extend(vals)
|
||||
|
||||
return conditions, params
|
||||
|
||||
|
||||
def _fetch_gl_rows_duckdb(conn, conditions, params):
|
||||
cols = [
|
||||
"account",
|
||||
"debit",
|
||||
"credit",
|
||||
"debit_in_account_currency",
|
||||
"credit_in_account_currency",
|
||||
"account_currency",
|
||||
]
|
||||
sql = f"""SELECT account, SUM(debit), SUM(credit),
|
||||
SUM(debit_in_account_currency), SUM(credit_in_account_currency), account_currency
|
||||
FROM "tabGL Entry" WHERE {" AND ".join(conditions)}
|
||||
GROUP BY account, account_currency"""
|
||||
return [frappe._dict(zip(cols, row, strict=False)) for row in conn.execute(sql, params).fetchall()]
|
||||
|
||||
|
||||
def get_period_gl_entries_duckdb(conn, filters, ignore_is_opening):
|
||||
conditions = ["company = ?", "is_cancelled = 0", "posting_date >= ?", "posting_date <= ?"]
|
||||
params = [filters.company, filters.from_date, filters.to_date]
|
||||
|
||||
if not ignore_is_opening:
|
||||
conditions.append("is_opening = 'No'")
|
||||
if not flt(filters.get("with_period_closing_entry_for_current_period")):
|
||||
conditions.append("voucher_type != 'Period Closing Voucher'")
|
||||
|
||||
extra_cond, extra_params = _extra_gl_conditions(filters)
|
||||
conditions.extend(extra_cond)
|
||||
params.extend(extra_params)
|
||||
|
||||
entries = _fetch_gl_rows_duckdb(conn, conditions, params)
|
||||
if filters.get("presentation_currency"):
|
||||
convert_to_presentation_currency(entries, get_currency(filters))
|
||||
|
||||
gl_entries_by_account = {}
|
||||
for entry in entries:
|
||||
gl_entries_by_account.setdefault(entry.account, []).append(entry)
|
||||
return gl_entries_by_account
|
||||
|
||||
|
||||
def get_opening_balances_duckdb(conn, filters, ignore_is_opening):
|
||||
bs = _get_rootwise_opening_duckdb(conn, filters, "Balance Sheet", ignore_is_opening)
|
||||
pl = _get_rootwise_opening_duckdb(conn, filters, "Profit and Loss", ignore_is_opening)
|
||||
bs.update(pl)
|
||||
return bs
|
||||
|
||||
|
||||
def _get_rootwise_opening_duckdb(conn, filters, report_type, ignore_is_opening):
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
ignore_closing_balances = frappe.get_single_value("Accounts Settings", "ignore_account_closing_balance")
|
||||
last_pcv = ""
|
||||
|
||||
if not ignore_closing_balances:
|
||||
last_pcv = frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={"docstatus": 1, "company": filters.company, "period_end_date": ("<", filters.from_date)},
|
||||
fields=["period_end_date", "name"],
|
||||
order_by="period_end_date desc",
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if last_pcv:
|
||||
# Account Closing Balance fetched via frappe (not GL Entry)
|
||||
gle = get_opening_balance(
|
||||
"Account Closing Balance",
|
||||
filters,
|
||||
report_type,
|
||||
accounting_dimensions,
|
||||
period_closing_voucher=last_pcv[0].name,
|
||||
ignore_is_opening=ignore_is_opening,
|
||||
)
|
||||
if getdate(last_pcv[0].period_end_date) < getdate(add_days(filters.from_date, -1)):
|
||||
start_date = add_days(last_pcv[0].period_end_date, 1)
|
||||
gle += _get_gl_entry_opening_duckdb(
|
||||
conn, filters, report_type, ignore_is_opening, start_date=start_date
|
||||
)
|
||||
else:
|
||||
gle = _get_gl_entry_opening_duckdb(conn, filters, report_type, ignore_is_opening)
|
||||
|
||||
opening = frappe._dict()
|
||||
for d in gle:
|
||||
opening.setdefault(d.account, {"account": d.account, "opening_debit": 0.0, "opening_credit": 0.0})
|
||||
opening[d.account]["opening_debit"] += flt(d.debit)
|
||||
opening[d.account]["opening_credit"] += flt(d.credit)
|
||||
return opening
|
||||
|
||||
|
||||
def _get_gl_entry_opening_duckdb(conn, filters, report_type, ignore_is_opening, start_date=None):
|
||||
accounts = frappe.db.get_all("Account", filters={"report_type": report_type}, pluck="name")
|
||||
if not accounts:
|
||||
return []
|
||||
|
||||
conditions = ["company = ?", f"account IN ({', '.join(['?'] * len(accounts))})", "is_cancelled = 0"]
|
||||
params = [filters.company, *accounts]
|
||||
|
||||
if start_date:
|
||||
conditions.append("posting_date >= ? AND posting_date < ?")
|
||||
params.extend([start_date, filters.from_date])
|
||||
if not ignore_is_opening:
|
||||
conditions.append("is_opening = 'No'")
|
||||
elif not ignore_is_opening:
|
||||
conditions.append("(posting_date < ? OR is_opening = 'Yes')")
|
||||
params.append(filters.from_date)
|
||||
else:
|
||||
conditions.append("posting_date < ?")
|
||||
params.append(filters.from_date)
|
||||
|
||||
if not filters.get("show_unclosed_fy_pl_balances") and report_type == "Profit and Loss":
|
||||
conditions.append("posting_date >= ?")
|
||||
params.append(filters.year_start_date)
|
||||
|
||||
if not flt(filters.get("with_period_closing_entry_for_opening")):
|
||||
conditions.append("voucher_type != 'Period Closing Voucher'")
|
||||
|
||||
extra_cond, extra_params = _extra_gl_conditions(filters)
|
||||
conditions.extend(extra_cond)
|
||||
params.extend(extra_params)
|
||||
|
||||
gle = _fetch_gl_rows_duckdb(conn, conditions, params)
|
||||
if filters.get("presentation_currency"):
|
||||
convert_to_presentation_currency(gle, get_currency(filters))
|
||||
return gle
|
||||
|
||||
@@ -74,15 +74,6 @@ frappe.ui.form.on("Asset Repair", {
|
||||
};
|
||||
};
|
||||
}
|
||||
if (frm.doc.asset) {
|
||||
frappe.db.get_value("Asset", frm.doc.asset, "status").then(({ message }) => {
|
||||
frm.set_df_property(
|
||||
"capitalize_repair_cost",
|
||||
"read_only",
|
||||
message && message.status === "Fully Depreciated"
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
repair_status: (frm) => {
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Asset",
|
||||
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Sold\",\"Scrapped\",\"Cancelled\",null]]]",
|
||||
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",\"Cancelled\",null]]]",
|
||||
"options": "Asset",
|
||||
"reqd": 1
|
||||
},
|
||||
@@ -251,7 +251,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-07-14 10:00:00.000000",
|
||||
"modified": "2026-01-06 15:48:13.862505",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair",
|
||||
|
||||
@@ -77,15 +77,12 @@ class AssetRepair(AccountsController):
|
||||
)
|
||||
|
||||
def validate_asset(self):
|
||||
if self.asset_doc.status in ("Sold", "Scrapped"):
|
||||
if self.asset_doc.status in ("Sold", "Fully Depreciated", "Scrapped"):
|
||||
frappe.throw(
|
||||
_("Asset {0} is in {1} status and cannot be repaired.").format(
|
||||
get_link_to_form("Asset", self.asset), self.asset_doc.status
|
||||
)
|
||||
)
|
||||
if self.asset_doc.get_status() == "Fully Depreciated":
|
||||
self.capitalize_repair_cost = 0
|
||||
self.increase_in_asset_life = 0
|
||||
|
||||
def validate_dates(self):
|
||||
if self.completion_date and (getdate(self.failure_date) > getdate(self.completion_date)):
|
||||
|
||||
@@ -347,49 +347,6 @@ class TestJobCard(FrappeTestCase):
|
||||
job_card.reload()
|
||||
self.assertEqual(job_card.transferred_qty, 0.0)
|
||||
|
||||
def test_work_order_transferred_qty_with_multiple_job_cards(self):
|
||||
create_bom_with_multiple_operations()
|
||||
work_order = make_wo_with_transfer_against_jc()
|
||||
self.generate_required_stock(work_order)
|
||||
|
||||
job_cards = frappe.get_all(
|
||||
"Job Card",
|
||||
filters={"work_order": work_order.name},
|
||||
pluck="name",
|
||||
order_by="sequence_id",
|
||||
)
|
||||
completed_qty = (4, 3)
|
||||
|
||||
for job_card_name, qty in zip(job_cards, completed_qty, strict=True):
|
||||
job_card = frappe.get_doc("Job Card", job_card_name)
|
||||
job_card.for_quantity = qty
|
||||
job_card.save()
|
||||
|
||||
transfer_entry = make_stock_entry_from_jc(job_card.name)
|
||||
transfer_entry.fg_completed_qty = qty
|
||||
transfer_entry.get_items()
|
||||
transfer_entry.submit()
|
||||
|
||||
job_card.reload()
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": now(),
|
||||
"to_time": add_to_date(now(), hours=1),
|
||||
"completed_qty": qty,
|
||||
},
|
||||
)
|
||||
job_card.submit()
|
||||
|
||||
work_order.reload()
|
||||
self.assertEqual(work_order.material_transferred_for_manufacturing, min(completed_qty))
|
||||
|
||||
# Refreshing required items must not replace the Job Card roll-up with the sum
|
||||
# of FG quantities from Material Transfer Stock Entries (4 + 3).
|
||||
work_order.update_required_items()
|
||||
work_order.reload()
|
||||
self.assertEqual(work_order.material_transferred_for_manufacturing, min(completed_qty))
|
||||
|
||||
def test_job_card_material_transfer_correctness(self):
|
||||
"""
|
||||
1. Test if only current Job Card Items are pulled in a Stock Entry against a Job Card
|
||||
|
||||
@@ -1275,10 +1275,6 @@ class WorkOrder(Document):
|
||||
|
||||
def recompute_material_transferred_for_manufacturing(self, transferred_items):
|
||||
"""Set material_transferred_for_manufacturing based on actual item-level transfers, not fg_completed_qty."""
|
||||
# Job Card transfers use the minimum completed quantity across operations.
|
||||
if self.operations and self.transfer_material_against == "Job Card":
|
||||
return
|
||||
|
||||
# When fg_completed_qty > 0 (direct stock entries, excess transfer), preserve the
|
||||
# SUM(fg_completed_qty) approach so excess-transfer tracking works correctly.
|
||||
sum_fg_completed_qty = self.get_transferred_or_manufactured_qty("Material Transfer for Manufacture")
|
||||
|
||||
@@ -15,11 +15,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
this.warehouse_field = opts.warehouse_field || "warehouse";
|
||||
// field name on row which defines max quantity to be scanned e.g. picklist
|
||||
this.max_qty_field = opts.max_qty_field;
|
||||
// row fields that, if set, mean max_qty_field is a real demand qty (e.g. from a
|
||||
// linked Sales Order) that scanning must not exceed. Rows with none of these set
|
||||
// have no real demand qty, so max_qty_field is just an arbitrary default and
|
||||
// shouldn't cap further scans.
|
||||
this.demand_ref_fields = opts.demand_ref_fields || [];
|
||||
// scanner won't add a new row if this flag is set.
|
||||
this.dont_allow_new_row = opts.dont_allow_new_row;
|
||||
// scanner will ask user to type the quantity instead of incrementing by 1
|
||||
@@ -395,9 +390,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
}
|
||||
|
||||
async set_barcode_uom(row, uom) {
|
||||
// e.g. Pick List: picked_qty is always tracked in stock UOM, so an incidental
|
||||
// barcode uom must not overwrite the row's own uom.
|
||||
if (this.max_qty_field) return;
|
||||
if (uom && frappe.meta.has_field(row.doctype, this.uom_field)) {
|
||||
await frappe.model.set_value(row.doctype, row.name, this.uom_field, uom);
|
||||
}
|
||||
@@ -464,9 +456,8 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
const matching_row = (row) => {
|
||||
const item_match = row.item_code == item_code;
|
||||
const batch_match = !row[this.batch_no_field] || row[this.batch_no_field] == batch_no;
|
||||
const uom_match = !uom || this.max_qty_field || row[this.uom_field] == uom;
|
||||
const has_demand_qty = this.demand_ref_fields.some((fieldname) => row[fieldname]);
|
||||
const qty_in_limit = !has_demand_qty || flt(row[this.qty_field]) < flt(row[this.max_qty_field]);
|
||||
const uom_match = !uom || row[this.uom_field] == uom;
|
||||
const qty_in_limit = flt(row[this.qty_field]) < flt(row[this.max_qty_field]);
|
||||
const item_scanned = row.has_item_scanned;
|
||||
|
||||
let warehouse_match = true;
|
||||
|
||||
@@ -280,17 +280,19 @@ erpnext.sales_common = {
|
||||
|
||||
set_actual_qty(doc, cdt, cdn) {
|
||||
let row = locals[cdt][cdn];
|
||||
let sales_doctypes = ["Sales Invoice", "Delivery Note", "Sales Order", "Quotation"];
|
||||
let sales_doctypes = ["Sales Invoice", "Delivery Note", "Sales Order"];
|
||||
|
||||
if (row.item_code && row.warehouse && sales_doctypes.includes(doc.doctype)) {
|
||||
return this.frm.call({
|
||||
frappe.call({
|
||||
method: "erpnext.stock.get_item_details.get_bin_details",
|
||||
child: row,
|
||||
args: {
|
||||
item_code: row.item_code,
|
||||
warehouse: row.warehouse,
|
||||
company: doc.company,
|
||||
include_child_warehouses: true,
|
||||
},
|
||||
callback(r) {
|
||||
if (r.message) {
|
||||
frappe.model.set_value(cdt, cdn, "actual_qty", r.message.actual_qty);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -297,8 +297,7 @@ frappe.ui.form.on("Pick List", {
|
||||
items_table_name: "locations",
|
||||
qty_field: "picked_qty",
|
||||
max_qty_field: "qty",
|
||||
demand_ref_fields: ["sales_order_item", "material_request_item", "product_bundle_item"],
|
||||
dont_allow_new_row: !frm.doc.pick_manually,
|
||||
dont_allow_new_row: true,
|
||||
prompt_qty: frm.doc.prompt_qty,
|
||||
serial_no_field: "serial_no",
|
||||
};
|
||||
|
||||
@@ -2477,24 +2477,22 @@ def get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos):
|
||||
|
||||
def get_bundle_wise_serial_nos(data, kwargs):
|
||||
bundle_wise_serial_nos = defaultdict(list)
|
||||
bundles = list({d.serial_and_batch_bundle for d in data if d.serial_and_batch_bundle})
|
||||
bundles = [d.serial_and_batch_bundle for d in data if d.serial_and_batch_bundle]
|
||||
if not bundles:
|
||||
return bundle_wise_serial_nos
|
||||
|
||||
sabe = frappe.qb.DocType("Serial and Batch Entry")
|
||||
query = (
|
||||
frappe.qb.from_(sabe)
|
||||
.select(sabe.serial_no, sabe.parent)
|
||||
.where(sabe.parent.isin(bundles))
|
||||
.where(sabe.docstatus == 1)
|
||||
.where(sabe.serial_no.isnotnull())
|
||||
.where(sabe.serial_no != "")
|
||||
)
|
||||
filters = {"parent": ("in", bundles), "docstatus": 1, "serial_no": ("is", "set")}
|
||||
|
||||
if kwargs.get("check_serial_nos") and kwargs.get("serial_nos"):
|
||||
query = query.where(sabe.serial_no.isin(kwargs.get("serial_nos")))
|
||||
filters["serial_no"] = ("in", kwargs.get("serial_nos"))
|
||||
|
||||
for d in query.run(as_dict=True):
|
||||
bundle_data = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
fields=["serial_no", "parent"],
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
for d in bundle_data:
|
||||
if d.parent:
|
||||
bundle_wise_serial_nos[d.parent].append(d.serial_no)
|
||||
|
||||
|
||||
@@ -11,12 +11,8 @@ from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
add_serial_batch_ledgers,
|
||||
combine_datetime,
|
||||
get_available_batches_qty,
|
||||
get_qty_based_available_batches,
|
||||
get_type_of_transaction,
|
||||
make_batch_nos,
|
||||
make_serial_nos,
|
||||
parse_serial_nos,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
@@ -1387,128 +1383,3 @@ def make_serial_batch_bundle(kwargs):
|
||||
return sb.make_serial_and_batch_bundle()
|
||||
|
||||
return sb
|
||||
|
||||
|
||||
class TestSerialandBatchBundleLogic(FrappeTestCase):
|
||||
"""Pure helpers and in-memory document validations, covering branches the
|
||||
integration suite doesn't reach (no stock-ledger / serial / batch fixtures)."""
|
||||
|
||||
def test_parse_serial_nos_splits_and_trims(self):
|
||||
self.assertEqual(parse_serial_nos("SN1\nSN2"), ["SN1", "SN2"])
|
||||
self.assertEqual(parse_serial_nos("SN1, SN2 , SN3"), ["SN1", "SN2", "SN3"])
|
||||
# blanks are dropped and an existing list is returned unchanged
|
||||
self.assertEqual(parse_serial_nos("SN1,,\n , SN2"), ["SN1", "SN2"])
|
||||
self.assertEqual(parse_serial_nos(["SN1", "SN2"]), ["SN1", "SN2"])
|
||||
|
||||
def test_get_qty_based_available_batches_allocates_across_batches(self):
|
||||
batches = [
|
||||
frappe._dict(batch_no="B1", qty=10, warehouse="W"),
|
||||
frappe._dict(batch_no="B2", qty=5, warehouse="W"),
|
||||
]
|
||||
# 12 consumes B1 fully then 2 from B2
|
||||
result = get_qty_based_available_batches(batches, 12)
|
||||
self.assertEqual([(b.batch_no, b.qty) for b in result], [("B1", 10), ("B2", 2)])
|
||||
# 8 is satisfied by B1 alone; B2 is not touched
|
||||
result = get_qty_based_available_batches(batches, 8)
|
||||
self.assertEqual([(b.batch_no, b.qty) for b in result], [("B1", 8)])
|
||||
|
||||
def test_get_available_batches_qty_aggregates_by_batch(self):
|
||||
batches = [
|
||||
frappe._dict(batch_no="B1", qty=10),
|
||||
frappe._dict(batch_no="B2", qty=5),
|
||||
frappe._dict(batch_no="B1", qty=3),
|
||||
]
|
||||
agg = get_available_batches_qty(batches)
|
||||
self.assertEqual(agg["B1"], 13)
|
||||
self.assertEqual(agg["B2"], 5)
|
||||
|
||||
def test_get_type_of_transaction_derives_direction(self):
|
||||
def se(**kw):
|
||||
return get_type_of_transaction(frappe._dict(doctype="Stock Entry"), frappe._dict(**kw))
|
||||
|
||||
self.assertEqual(se(s_warehouse="W"), "Outward") # issuing from a source warehouse
|
||||
self.assertEqual(se(), "Inward") # only a target warehouse
|
||||
self.assertEqual(
|
||||
get_type_of_transaction(frappe._dict(doctype="Purchase Receipt"), frappe._dict()), "Inward"
|
||||
)
|
||||
self.assertEqual(
|
||||
get_type_of_transaction(frappe._dict(doctype="Stock Reconciliation"), frappe._dict()), "Inward"
|
||||
)
|
||||
# a purchase return reverses the direction to Outward
|
||||
self.assertEqual(
|
||||
get_type_of_transaction(frappe._dict(doctype="Purchase Receipt", is_return=1), frappe._dict()),
|
||||
"Outward",
|
||||
)
|
||||
|
||||
def test_duplicate_serial_no_in_entries_is_rejected(self):
|
||||
doc = frappe.new_doc("Serial and Batch Bundle")
|
||||
doc.append("entries", {"serial_no": "SN1"})
|
||||
doc.append("entries", {"serial_no": "SN1"})
|
||||
self.assertRaises(frappe.ValidationError, doc.validate_duplicate_serial_and_batch_no)
|
||||
|
||||
def test_duplicate_batch_no_in_entries_is_rejected(self):
|
||||
doc = frappe.new_doc("Serial and Batch Bundle")
|
||||
doc.append("entries", {"batch_no": "B1"})
|
||||
doc.append("entries", {"batch_no": "B1"})
|
||||
self.assertRaises(frappe.ValidationError, doc.validate_duplicate_serial_and_batch_no)
|
||||
|
||||
def test_voucher_no_is_mandatory(self):
|
||||
doc = frappe.new_doc("Serial and Batch Bundle")
|
||||
self.assertRaises(frappe.ValidationError, doc.validate_serial_and_batch_data)
|
||||
|
||||
def test_calculate_total_qty_normalizes_and_signs(self):
|
||||
inward = frappe.new_doc("Serial and Batch Bundle")
|
||||
inward.type_of_transaction = "Inward"
|
||||
inward.append("entries", {"qty": 5})
|
||||
inward.append("entries", {"qty": 3})
|
||||
inward.calculate_total_qty(save=False)
|
||||
self.assertEqual(inward.total_qty, 8)
|
||||
|
||||
# Outward flips the sign
|
||||
outward = frappe.new_doc("Serial and Batch Bundle")
|
||||
outward.type_of_transaction = "Outward"
|
||||
outward.append("entries", {"qty": 5})
|
||||
outward.calculate_total_qty(save=False)
|
||||
self.assertEqual(outward.total_qty, -5)
|
||||
|
||||
# a serialized bundle normalizes each row qty to 1
|
||||
serialized = frappe.new_doc("Serial and Batch Bundle")
|
||||
serialized.has_serial_no = 1
|
||||
serialized.type_of_transaction = "Inward"
|
||||
serialized.append("entries", {"qty": 5})
|
||||
serialized.calculate_total_qty(save=False)
|
||||
self.assertEqual(serialized.total_qty, 1)
|
||||
|
||||
def test_get_bundle_wise_serial_nos(self):
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
get_bundle_wise_serial_nos,
|
||||
)
|
||||
|
||||
item_code = make_item(properties={"has_serial_no": 1, "serial_no_series": "TEST-BWSN-.#####"}).name
|
||||
|
||||
bundles = []
|
||||
for _ in range(2):
|
||||
se = make_stock_entry(
|
||||
item_code=item_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=3,
|
||||
rate=100,
|
||||
)
|
||||
bundles.append(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
data = [frappe._dict(serial_and_batch_bundle=bundle) for bundle in bundles]
|
||||
|
||||
self.assertEqual(get_bundle_wise_serial_nos([], {}), {})
|
||||
|
||||
bundle_wise_serial_nos = get_bundle_wise_serial_nos(data, {})
|
||||
for bundle in bundles:
|
||||
self.assertEqual(sorted(bundle_wise_serial_nos[bundle]), get_serial_nos_from_bundle(bundle))
|
||||
|
||||
# check_serial_nos must restrict the result to the requested serial nos
|
||||
serial_no = get_serial_nos_from_bundle(bundles[0])[0]
|
||||
bundle_wise_serial_nos = get_bundle_wise_serial_nos(
|
||||
data, {"check_serial_nos": True, "serial_nos": [serial_no]}
|
||||
)
|
||||
|
||||
self.assertNotIn(bundles[1], bundle_wise_serial_nos)
|
||||
self.assertEqual(bundle_wise_serial_nos[bundles[0]], [serial_no])
|
||||
|
||||
@@ -177,9 +177,10 @@ def update_bin_details(args, out, doc):
|
||||
out.update(get_bin_details(args.item_code, args.get("from_warehouse")))
|
||||
|
||||
elif out.get("warehouse"):
|
||||
bin_details = get_bin_details(
|
||||
args.item_code, out.warehouse, args.company, include_child_warehouses=True
|
||||
)
|
||||
company = args.company if (doc and doc.get("doctype") == "Purchase Order") else None
|
||||
|
||||
# calculate company_total_stock only for po
|
||||
bin_details = get_bin_details(args.item_code, out.warehouse, company, include_child_warehouses=True)
|
||||
|
||||
out.update(bin_details)
|
||||
|
||||
|
||||
@@ -35,40 +35,6 @@ class TestGetItemDetail(FrappeTestCase):
|
||||
details = get_item_details(args)
|
||||
self.assertEqual(details.get("price_list_rate"), 100)
|
||||
|
||||
def test_bin_details_for_selling_doctypes(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
item_code = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
make_purchase_receipt(item_code=item_code, warehouse="_Test Warehouse - _TC", qty=100, rate=100)
|
||||
make_purchase_receipt(item_code=item_code, warehouse="_Test Warehouse 1 - _TC", qty=50, rate=100)
|
||||
|
||||
args = frappe._dict(
|
||||
{
|
||||
"item_code": item_code,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"company": "_Test Company",
|
||||
"customer": "_Test Customer",
|
||||
"currency": "INR",
|
||||
"conversion_rate": 1.0,
|
||||
"price_list": "_Test Price List",
|
||||
"price_list_currency": "INR",
|
||||
"plc_conversion_rate": 1.0,
|
||||
"transaction_date": None,
|
||||
"name": None,
|
||||
"ignore_pricing_rule": 1,
|
||||
"qty": 1,
|
||||
}
|
||||
)
|
||||
|
||||
for doctype in ("Sales Order", "Quotation", "Sales Invoice", "Delivery Note", "Purchase Order"):
|
||||
with self.subTest(doctype=doctype):
|
||||
details = get_item_details(args.copy().update({"doctype": doctype}))
|
||||
|
||||
self.assertEqual(details.get("actual_qty"), 100)
|
||||
self.assertEqual(details.get("company_total_stock"), 150)
|
||||
|
||||
def test_fetch_asset_category_expense_account_on_purchase_receipt(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
|
||||
Reference in New Issue
Block a user