Merge branch 'frappe:develop' into feat/employee-milestone-indicators

This commit is contained in:
Krishna Pramod Shirsath
2026-03-09 13:06:20 +05:30
committed by GitHub
422 changed files with 68992 additions and 36663 deletions

View File

@@ -1,7 +1,7 @@
# Security Policy
The ERPNext team and community take security issues seriously. To report a security issue, fill out the form at [https://erpnext.com/security/report](https://erpnext.com/security/report).
The ERPNext team and community take security issues seriously. To report a security issue, please go through the information mentioned [here](https://frappe.io/security).
You can help us make ERPNext and all it's users more secure by following the [Reporting guidelines](https://erpnext.com/security).
You can help us make ERPNext and all its users more secure by following the [Reporting guidelines](https://frappe.io/security).
We appreciate your efforts to responsibly disclose your findings. We'll endeavor to respond quickly, and will keep you updated throughout the process.
We appreciate your efforts to responsibly disclose your findings. We'll endeavor to respond quickly, and will keep you updated throughout the process.

View File

@@ -52,7 +52,7 @@ class ERPNextAddress(Address):
@frappe.whitelist()
def get_shipping_address(company, address=None):
def get_shipping_address(company: str, address: str | None = None):
filters = [
["Dynamic Link", "link_doctype", "=", "Company"],
["Dynamic Link", "link_name", "=", company],

View File

@@ -13,15 +13,15 @@ from frappe.utils.nestedset import get_descendants_of
@frappe.whitelist()
@cache_source
def get(
chart_name=None,
chart=None,
no_cache=None,
filters=None,
from_date=None,
to_date=None,
timespan=None,
time_interval=None,
heatmap_year=None,
chart_name: str | None = None,
chart: str | dict | None = None,
no_cache: bool | None = None,
filters: str | dict | None = None,
from_date: str | None = None,
to_date: str | None = None,
timespan: str | None = None,
time_interval: str | None = None,
heatmap_year: str | None = None,
):
if chart_name:
chart = frappe.get_doc("Dashboard Chart", chart_name)

View File

@@ -471,7 +471,7 @@ class Account(NestedSet):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_parent_account(doctype, txt, searchfield, start, page_len, filters):
def get_parent_account(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
return frappe.db.sql(
"""select name from tabAccount
where is_group = 1 and docstatus != 2 and company = {}
@@ -515,7 +515,9 @@ def get_account_autoname(account_number, account_name, company):
@frappe.whitelist()
def update_account_number(name, account_name, account_number=None, from_descendant=False):
def update_account_number(
name: str, account_name: str, account_number: str | None = None, from_descendant: bool = False
):
_ensure_idle_system()
account = frappe.get_cached_doc("Account", name)
if not account:
@@ -577,7 +579,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
@frappe.whitelist()
def merge_account(old, new):
def merge_account(old: str, new: str):
_ensure_idle_system()
# Validate properties before merging
new_account = frappe.get_cached_doc("Account", new)
@@ -614,7 +616,7 @@ def merge_account(old, new):
@frappe.whitelist()
def get_root_company(company):
def get_root_company(company: str):
# return the topmost company in the hierarchy
ancestors = get_ancestors_of("Company", company, "lft asc")
return [ancestors[0]] if ancestors else []

View File

@@ -99,7 +99,7 @@ def identify_is_group(child):
@frappe.whitelist()
def get_chart(chart_template, existing_company=None):
def get_chart(chart_template: str | None, existing_company: str | None = None):
chart = {}
if existing_company:
return get_account_tree_from_existing_company(existing_company)
@@ -132,7 +132,7 @@ def get_chart(chart_template, existing_company=None):
@frappe.whitelist()
def get_charts_for_country(country, with_standard=False):
def get_charts_for_country(country: str, with_standard: bool = False):
charts = []
def _get_chart_name(content):
@@ -225,7 +225,7 @@ def build_account_tree(tree, parent, all_accounts):
@frappe.whitelist()
def validate_bank_account(coa, bank_account):
def validate_bank_account(coa: str, bank_account: str):
accounts = []
chart = get_chart(coa)
@@ -244,7 +244,9 @@ def validate_bank_account(coa, bank_account):
@frappe.whitelist()
def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=False):
def build_tree_from_json(
chart_template: str, chart_data: dict | None = None, from_coa_importer: bool = False
):
"""get chart template from its folder and parse the json to be rendered as tree"""
chart = chart_data or get_chart(chart_template)

View File

@@ -103,10 +103,6 @@ class AccountingDimension(Document):
if not self.fieldname:
self.fieldname = scrub(self.label)
def on_update(self):
frappe.flags.accounting_dimensions = None
frappe.flags.accounting_dimensions_details = None
def make_dimension_in_accounting_doctypes(doc, doclist=None):
if not doclist:
@@ -210,7 +206,7 @@ def delete_accounting_dimension(doc):
@frappe.whitelist()
def disable_dimension(doc):
def disable_dimension(doc: str):
if frappe.in_test:
toggle_disabling(doc=doc)
else:
@@ -241,34 +237,26 @@ def get_doctypes_with_dimensions():
return frappe.get_hooks("accounting_dimension_doctypes")
def get_accounting_dimensions(as_list=True, filters=None):
if not filters:
filters = {"disabled": 0}
if frappe.flags.accounting_dimensions is None:
frappe.flags.accounting_dimensions = frappe.get_all(
"Accounting Dimension",
fields=["label", "fieldname", "disabled", "document_type"],
filters=filters,
)
def get_accounting_dimensions(as_list=True):
accounting_dimensions = frappe.get_all(
"Accounting Dimension",
fields=["label", "fieldname", "disabled", "document_type"],
filters={"disabled": 0},
)
if as_list:
return [d.fieldname for d in frappe.flags.accounting_dimensions]
return [d.fieldname for d in accounting_dimensions]
else:
return frappe.flags.accounting_dimensions
return accounting_dimensions
def get_checks_for_pl_and_bs_accounts():
if frappe.flags.accounting_dimensions_details is None:
# nosemgrep
frappe.flags.accounting_dimensions_details = frappe.db.sql(
"""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
return frappe.db.sql(
"""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c
WHERE p.name = c.parent AND p.disabled = 0""",
as_dict=1,
)
return frappe.flags.accounting_dimensions_details
as_dict=1,
)
def get_dimension_with_children(doctype, dimensions):
@@ -286,7 +274,7 @@ def get_dimension_with_children(doctype, dimensions):
@frappe.whitelist()
def get_dimensions(with_cost_center_and_project=False):
def get_dimensions(with_cost_center_and_project: str | bool = False):
c = frappe.qb.DocType("Accounting Dimension Detail")
p = frappe.qb.DocType("Accounting Dimension")
dimension_filters = (

View File

@@ -69,37 +69,34 @@ class AccountingDimensionFilter(Document):
def get_dimension_filter_map():
if not frappe.flags.get("dimension_filter_map"):
filters = frappe.db.sql(
"""
SELECT
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
p.allow_or_restrict, p.fieldname, a.is_mandatory
FROM
`tabApplicable On Account` a,
`tabAccounting Dimension Filter` p
LEFT JOIN `tabAllowed Dimension` d ON d.parent = p.name
WHERE
p.name = a.parent
AND p.disabled = 0
""",
as_dict=1,
filters = frappe.db.sql(
"""
SELECT
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
p.allow_or_restrict, p.fieldname, a.is_mandatory
FROM
`tabApplicable On Account` a,
`tabAccounting Dimension Filter` p
LEFT JOIN `tabAllowed Dimension` d ON d.parent = p.name
WHERE
p.name = a.parent
AND p.disabled = 0
""",
as_dict=1,
)
dimension_filter_map = {}
for f in filters:
build_map(
dimension_filter_map,
f.fieldname,
f.applicable_on_account,
f.dimension_value,
f.allow_or_restrict,
f.is_mandatory,
)
dimension_filter_map = {}
for f in filters:
build_map(
dimension_filter_map,
f.fieldname,
f.applicable_on_account,
f.dimension_value,
f.allow_or_restrict,
f.is_mandatory,
)
frappe.flags.dimension_filter_map = dimension_filter_map
return frappe.flags.dimension_filter_map
return dimension_filter_map
def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory):

View File

@@ -205,7 +205,7 @@
"description": "Payment Terms from orders will be fetched into the invoices as is",
"fieldname": "automatically_fetch_payment_terms",
"fieldtype": "Check",
"label": "Automatically Fetch Payment Terms from Order"
"label": "Automatically Fetch Payment Terms from Order/Quotation"
},
{
"description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ",
@@ -691,13 +691,13 @@
}
],
"grid_page_length": 50,
"hide_toolbar": 1,
"hide_toolbar": 0,
"icon": "icon-cog",
"idx": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-02-04 17:15:38.609327",
"modified": "2026-02-27 01:04:09.415288",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -115,7 +115,7 @@ def get_default_company_bank_account(company, party_type, party):
@frappe.whitelist()
def get_bank_account_details(bank_account):
def get_bank_account_details(bank_account: str):
return frappe.get_cached_value(
"Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1
)

View File

@@ -1,8 +1,8 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import json
from datetime import date
import frappe
from frappe import _
@@ -47,7 +47,9 @@ class BankReconciliationTool(Document):
@frappe.whitelist()
def get_bank_transactions(bank_account, from_date=None, to_date=None):
def get_bank_transactions(
bank_account: str, from_date: str | date | None = None, to_date: str | date | None = None
):
# returns bank transactions for a bank account
filters = []
filters.append(["bank_account", "=", bank_account])
@@ -80,7 +82,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
@frappe.whitelist()
def get_account_balance(bank_account, till_date, company):
def get_account_balance(bank_account: str, till_date: str | date, company: str):
# returns account balance till the specified date
account = frappe.db.get_value("Bank Account", bank_account, "account")
filters = frappe._dict(
@@ -106,7 +108,9 @@ def get_account_balance(bank_account, till_date, company):
@frappe.whitelist()
def update_bank_transaction(bank_transaction_name, reference_number, party_type=None, party=None):
def update_bank_transaction(
bank_transaction_name: str, reference_number: str, party_type: str | None = None, party: str | None = None
):
# updates bank transaction based on the new parameters provided by the user from Vouchers
bank_transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
bank_transaction.reference_number = reference_number
@@ -135,16 +139,16 @@ def update_bank_transaction(bank_transaction_name, reference_number, party_type=
@frappe.whitelist()
def create_journal_entry_bts(
bank_transaction_name,
reference_number=None,
reference_date=None,
posting_date=None,
entry_type=None,
second_account=None,
mode_of_payment=None,
party_type=None,
party=None,
allow_edit=None,
bank_transaction_name: str,
reference_number: str | None = None,
reference_date: str | None = None,
posting_date: str | date | None = None,
entry_type: str | None = None,
second_account: str | None = None,
mode_of_payment: str | None = None,
party_type: str | None = None,
party: str | None = None,
allow_edit: bool | None = None,
):
# Create a new journal entry based on the bank transaction
bank_transaction = frappe.db.get_values(
@@ -294,17 +298,17 @@ def create_journal_entry_bts(
@frappe.whitelist()
def create_payment_entry_bts(
bank_transaction_name,
reference_number=None,
reference_date=None,
party_type=None,
party=None,
posting_date=None,
mode_of_payment=None,
project=None,
cost_center=None,
allow_edit=None,
company_bank_account=None,
bank_transaction_name: str,
reference_number: str | None = None,
reference_date: str | None = None,
party_type: str | None = None,
party: str | None = None,
posting_date: str | None = None,
mode_of_payment: str | None = None,
project: str | None = None,
cost_center: str | None = None,
allow_edit: bool | None = None,
company_bank_account: str | None = None,
):
# Create a new payment entry based on the bank transaction
bank_transaction = frappe.db.get_values(
@@ -371,12 +375,12 @@ def create_payment_entry_bts(
@frappe.whitelist()
def auto_reconcile_vouchers(
bank_account,
from_date=None,
to_date=None,
filter_by_reference_date=None,
from_reference_date=None,
to_reference_date=None,
bank_account: str,
from_date: str | date | None = None,
to_date: str | date | None = None,
filter_by_reference_date: bool | None = None,
from_reference_date: bool | None = None,
to_reference_date: str | None = None,
):
bank_transactions = get_bank_transactions(bank_account)
@@ -471,7 +475,7 @@ def get_auto_reconcile_message(partially_reconciled, reconciled):
@frappe.whitelist()
def reconcile_vouchers(bank_transaction_name, vouchers):
def reconcile_vouchers(bank_transaction_name: str, vouchers: str):
# updated clear date of all the vouchers based on the bank transaction
vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
@@ -487,13 +491,13 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
@frappe.whitelist()
def get_linked_payments(
bank_transaction_name,
document_types=None,
from_date=None,
to_date=None,
filter_by_reference_date=None,
from_reference_date=None,
to_reference_date=None,
bank_transaction_name: str,
document_types: str | list[str] | None = None,
from_date: str | date | None = None,
to_date: str | date | None = None,
filter_by_reference_date: bool | None = None,
from_reference_date: bool | None = None,
to_reference_date: str | None = None,
):
# get all matching payments for a bank transaction
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)

View File

@@ -143,7 +143,7 @@ def preprocess_mt940_content(content: str) -> str:
@frappe.whitelist()
def convert_mt940_to_csv(data_import, mt940_file_path):
def convert_mt940_to_csv(data_import: str, mt940_file_path: str):
doc = frappe.get_doc("Bank Statement Import", data_import)
_file_doc, content = get_file(mt940_file_path)
@@ -208,26 +208,28 @@ def convert_mt940_to_csv(data_import, mt940_file_path):
@frappe.whitelist()
def get_preview_from_template(data_import, import_file=None, google_sheets_url=None):
def get_preview_from_template(
data_import: str, import_file: str | None = None, google_sheets_url: str | None = None
):
return frappe.get_doc("Bank Statement Import", data_import).get_preview_from_template(
import_file, google_sheets_url
)
@frappe.whitelist()
def form_start_import(data_import):
def form_start_import(data_import: str):
job_id = frappe.get_doc("Bank Statement Import", data_import).start_import()
return job_id is not None
@frappe.whitelist()
def download_errored_template(data_import_name):
def download_errored_template(data_import_name: str):
data_import = frappe.get_doc("Bank Statement Import", data_import_name)
data_import.export_errored_rows()
@frappe.whitelist()
def download_import_log(data_import_name):
def download_import_log(data_import_name: str):
return frappe.get_doc("Bank Statement Import", data_import_name).download_import_log()
@@ -363,7 +365,7 @@ def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
@frappe.whitelist()
def get_import_status(docname):
def get_import_status(docname: str):
import_status = {}
data_import = frappe.get_doc("Bank Statement Import", docname)

View File

@@ -139,6 +139,8 @@ class BankTransaction(Document):
self.set_status()
def on_cancel(self):
self.ignore_linked_doctypes = ["GL Entry"]
for payment_entry in self.payment_entries:
self.delink_payment_entry(payment_entry)
@@ -373,11 +375,12 @@ def get_clearance_details(transaction, payment_entry, bt_allocations, gl_entries
("unallocated_amount", "bank_account"),
as_dict=True,
)
bt_bank_account = frappe.db.get_value("Bank Account", bt.bank_account, "account")
if bt.bank_account != gl_bank_account:
if bt_bank_account != gl_bank_account:
frappe.throw(
_("Bank Account {} in Bank Transaction {} is not matching with Bank Account {}").format(
bt.bank_account, payment_entry.payment_entry, gl_bank_account
bt_bank_account, payment_entry.payment_entry, gl_bank_account
)
)

View File

@@ -35,7 +35,7 @@ def upload_bank_statement():
@frappe.whitelist()
def create_bank_entries(columns, data, bank_account):
def create_bank_entries(columns: str, data: str, bank_account: str):
header_map = get_header_mapping(columns, bank_account)
success = 0

View File

@@ -845,7 +845,7 @@ def get_fiscal_year_date_range(from_fiscal_year, to_fiscal_year):
@frappe.whitelist()
def revise_budget(budget_name):
def revise_budget(budget_name: str):
old_budget = frappe.get_doc("Budget", budget_name)
if old_budget.docstatus == 1:

View File

@@ -57,7 +57,7 @@ def validate_columns(data):
@frappe.whitelist()
def validate_company(company):
def validate_company(company: str):
parent_company, allow_account_creation_against_child_company = frappe.get_cached_value(
"Company", company, ["parent_company", "allow_account_creation_against_child_company"]
)
@@ -74,7 +74,7 @@ def validate_company(company):
@frappe.whitelist()
def import_coa(file_name, company):
def import_coa(file_name: str, company: str):
# delete existing data for accounts
unset_existing_data(company)
@@ -159,7 +159,9 @@ def generate_data_from_excel(file_doc, extension, as_dict=False):
@frappe.whitelist()
def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0):
def get_coa(
doctype: str, parent: str, is_root: bool = False, file_name: str | None = None, for_validate: int = 0
):
"""called by tree view (to fetch node's children)"""
file_doc, extension = get_file(file_name)
@@ -307,7 +309,7 @@ def build_response_as_excel(writer):
@frappe.whitelist()
def download_template(file_type, template_type, company):
def download_template(file_type: str, template_type: str, company: str):
writer = get_template(template_type, company)
if file_type == "CSV":
@@ -361,7 +363,7 @@ def get_sample_template(writer, company):
@frappe.whitelist()
def validate_accounts(file_doc, extension):
def validate_accounts(file_doc: Document, extension: str):
if extension == "csv":
accounts = generate_data_from_csv(file_doc, as_dict=True)
else:

View File

@@ -47,7 +47,7 @@ class ChequePrintTemplate(Document):
@frappe.whitelist()
def create_or_update_cheque_print_format(template_name):
def create_or_update_cheque_print_format(template_name: str):
if not frappe.db.exists("Print Format", template_name):
cheque_print = frappe.new_doc("Print Format")
cheque_print.update(

View File

@@ -1,6 +1,7 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from datetime import date
import frappe
from frappe import _, qb
@@ -614,7 +615,12 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
@frappe.whitelist()
def get_account_details(
company, posting_date, account, party_type=None, party=None, rounding_loss_allowance: float | None = None
company: str,
posting_date: str | date,
account: str,
party_type: str | None = None,
party: str | None = None,
rounding_loss_allowance: float = 0.0,
):
if not (company and posting_date):
frappe.throw(_("Company and Posting Date is mandatory"))

View File

@@ -15,7 +15,7 @@ from frappe.database.operator_map import OPERATOR_MAP
from frappe.query_builder import Case
from frappe.query_builder.functions import Sum
from frappe.utils import cstr, date_diff, flt, getdate
from pypika.terms import LiteralValue
from pypika.terms import Bracket, LiteralValue
from erpnext import get_company_currency
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -732,7 +732,7 @@ class FinancialQueryBuilder:
user_conditions = build_match_conditions(doctype)
if user_conditions:
query = query.where(LiteralValue(user_conditions))
query = query.where(Bracket(LiteralValue(user_conditions)))
return query.run(as_dict=True)

View File

@@ -4,7 +4,7 @@
import frappe
from dateutil.relativedelta import relativedelta
from frappe import _
from frappe import _, cint
from frappe.model.document import Document
from frappe.utils import add_days, add_years, cstr, getdate
@@ -33,23 +33,11 @@ class FiscalYear(Document):
self.validate_dates()
self.validate_overlap()
if not self.is_new():
year_start_end_dates = frappe.db.sql(
"""select year_start_date, year_end_date
from `tabFiscal Year` where name=%s""",
(self.name),
)
def on_update(self):
frappe.cache().delete_key("fiscal_years")
if year_start_end_dates:
if (
getdate(self.year_start_date) != year_start_end_dates[0][0]
or getdate(self.year_end_date) != year_start_end_dates[0][1]
):
frappe.throw(
_(
"Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."
)
)
def on_trash(self):
frappe.cache().delete_key("fiscal_years")
def validate_dates(self):
self.validate_from_to_dates("year_start_date", "year_end_date")
@@ -66,28 +54,20 @@ class FiscalYear(Document):
frappe.exceptions.InvalidDates,
)
def on_update(self):
check_duplicate_fiscal_year(self)
frappe.cache().delete_value("fiscal_years")
def on_trash(self):
frappe.cache().delete_value("fiscal_years")
def validate_overlap(self):
existing_fiscal_years = frappe.db.sql(
"""select name from `tabFiscal Year`
where (
(%(year_start_date)s between year_start_date and year_end_date)
or (%(year_end_date)s between year_start_date and year_end_date)
or (year_start_date between %(year_start_date)s and %(year_end_date)s)
or (year_end_date between %(year_start_date)s and %(year_end_date)s)
) and name!=%(name)s""",
{
"year_start_date": self.year_start_date,
"year_end_date": self.year_end_date,
"name": self.name or "No Name",
},
as_dict=True,
fy = frappe.qb.DocType("Fiscal Year")
name = self.name or self.year
existing_fiscal_years = (
frappe.qb.from_(fy)
.select(fy.name)
.where(
(fy.year_start_date <= self.year_end_date)
& (fy.year_end_date >= self.year_start_date)
& (fy.name != name)
)
.run(as_dict=True)
)
if existing_fiscal_years:
@@ -110,37 +90,30 @@ class FiscalYear(Document):
frappe.throw(
_(
"Year start date or end date is overlapping with {0}. To avoid please set company"
).format(existing.name),
).format(frappe.get_desk_link("Fiscal Year", existing.name, open_in_new_tab=True)),
frappe.NameError,
)
@frappe.whitelist()
def check_duplicate_fiscal_year(doc):
year_start_end_dates = frappe.db.sql(
"""select name, year_start_date, year_end_date from `tabFiscal Year` where name!=%s""",
(doc.name),
)
for fiscal_year, ysd, yed in year_start_end_dates:
if (getdate(doc.year_start_date) == ysd and getdate(doc.year_end_date) == yed) and (
not frappe.in_test
):
frappe.throw(
_(
"Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}"
).format(fiscal_year)
)
@frappe.whitelist()
def auto_create_fiscal_year():
for d in frappe.db.sql(
"""select name from `tabFiscal Year` where year_end_date = date_add(current_date, interval 3 day)"""
):
fy = frappe.qb.DocType("Fiscal Year")
# Skipped auto-creating Short Year, as it has very rare use case.
# Reference: https://www.irs.gov/businesses/small-businesses-self-employed/tax-years (US)
follow_up_date = add_days(getdate(), days=3)
fiscal_year = (
frappe.qb.from_(fy)
.select(fy.name)
.where((fy.year_end_date == follow_up_date) & (fy.is_short_year == 0))
.run()
)
for d in fiscal_year:
try:
current_fy = frappe.get_doc("Fiscal Year", d[0])
new_fy = frappe.copy_doc(current_fy, ignore_no_copy=False)
new_fy = frappe.new_doc("Fiscal Year")
new_fy.disabled = cint(current_fy.disabled)
new_fy.year_start_date = add_days(current_fy.year_end_date, 1)
new_fy.year_end_date = add_years(current_fy.year_end_date, 1)
@@ -148,6 +121,10 @@ def auto_create_fiscal_year():
start_year = cstr(new_fy.year_start_date.year)
end_year = cstr(new_fy.year_end_date.year)
new_fy.year = start_year if start_year == end_year else (start_year + "-" + end_year)
for row in current_fy.companies:
new_fy.append("companies", {"company": row.company})
new_fy.auto_created = 1
new_fy.insert(ignore_permissions=True)

View File

@@ -15,20 +15,22 @@
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Company",
"options": "Company"
"options": "Company",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-03-27 13:09:44.659251",
"modified": "2026-02-20 23:02:26.193606",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Fiscal Year Company",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -14,7 +14,7 @@ class FiscalYearCompany(Document):
if TYPE_CHECKING:
from frappe.types import DF
company: DF.Link | None
company: DF.Link
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data

View File

@@ -317,7 +317,7 @@ class InvoiceDiscounting(AccountsController):
@frappe.whitelist()
def get_invoices(filters):
def get_invoices(filters: str):
filters = frappe._dict(json.loads(filters))
cond = []
if filters.customer:

View File

@@ -303,10 +303,6 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
}
onload_post_render() {
this.frm.get_field("accounts").grid.set_multiple_add("account");
}
load_defaults() {
//this.frm.show_print_first = true;
if (this.frm.doc.__islocal && this.frm.doc.company) {

View File

@@ -10,18 +10,15 @@
"field_order": [
"entry_type_and_date",
"company",
"is_system_generated",
"title",
"voucher_type",
"naming_series",
"process_deferred_accounting",
"reversal_of",
"column_break1",
"from_template",
"naming_series",
"posting_date",
"finance_book",
"multi_currency",
"apply_tds",
"tax_withholding_category",
"is_system_generated",
"amended_from",
"section_break_tcvw",
"for_all_stock_asset_accounts",
"column_break_wpau",
@@ -30,52 +27,60 @@
"get_balance_for_periodic_accounting",
"2_add_edit_gl_entries",
"accounts",
"section_break99",
"cheque_no",
"cheque_date",
"user_remark",
"column_break99",
"section_break_ouaq",
"total_debit",
"column_break_cixu",
"total_credit",
"difference",
"get_balance",
"multi_currency",
"total_amount_currency",
"total_amount",
"total_amount_in_words",
"section_break99",
"cheque_no",
"cheque_date",
"clearance_date",
"column_break_oizh",
"user_remark",
"subscription_section",
"auto_repeat",
"tax_withholding_tab",
"section_tax_withholding_entry",
"tax_withholding_group",
"ignore_tax_withholding_threshold",
"override_tax_withholding_entries",
"tax_withholding_entries",
"more_info_tab",
"reference",
"clearance_date",
"remark",
"inter_company_journal_entry_reference",
"column_break98",
"bill_no",
"bill_date",
"due_date",
"column_break_isfa",
"inter_company_journal_entry_reference",
"process_deferred_accounting",
"reversal_of",
"payment_order",
"stock_entry",
"printing_settings",
"pay_to_recd_from",
"letter_head",
"select_print_heading",
"column_break_35",
"total_amount_currency",
"total_amount",
"total_amount_in_words",
"write_off",
"write_off_based_on",
"get_outstanding_invoices",
"column_break_30",
"write_off_amount",
"printing_settings",
"pay_to_recd_from",
"column_break_35",
"letter_head",
"select_print_heading",
"addtional_info",
"mode_of_payment",
"payment_order",
"party_not_required",
"column_break3",
"is_opening",
"stock_entry",
"subscription_section",
"auto_repeat",
"amended_from"
"finance_book",
"from_template",
"title",
"column_break3",
"remark",
"mode_of_payment",
"party_not_required"
],
"fields": [
{
@@ -155,6 +160,7 @@
{
"fieldname": "2_add_edit_gl_entries",
"fieldtype": "Section Break",
"hide_border": 1,
"oldfieldtype": "Section Break",
"options": "fa fa-table"
},
@@ -202,10 +208,6 @@
"oldfieldtype": "Small Text",
"print_hide": 1
},
{
"fieldname": "column_break99",
"fieldtype": "Column Break"
},
{
"fieldname": "total_debit",
"fieldtype": "Currency",
@@ -429,7 +431,7 @@
"collapsible": 1,
"fieldname": "addtional_info",
"fieldtype": "Section Break",
"label": "More Information",
"label": "Additional Info",
"oldfieldtype": "Section Break",
"options": "fa fa-file-text"
},
@@ -476,7 +478,7 @@
{
"fieldname": "subscription_section",
"fieldtype": "Section Break",
"label": "Subscription Section"
"label": "Subscription"
},
{
"allow_on_submit": 1,
@@ -593,12 +595,10 @@
"no_copy": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "eval: doc.apply_tds && doc.docstatus == 0",
"depends_on": "eval: doc.apply_tds",
"fieldname": "section_tax_withholding_entry",
"fieldtype": "Section Break",
"label": "Tax Withholding Entry"
"fieldtype": "Section Break"
},
{
"fieldname": "tax_withholding_group",
@@ -624,6 +624,33 @@
"label": "Tax Withholding Entries",
"options": "Tax Withholding Entry",
"read_only_depends_on": "eval: !doc.override_tax_withholding_entries"
},
{
"fieldname": "more_info_tab",
"fieldtype": "Tab Break",
"label": "More Info"
},
{
"fieldname": "section_break_ouaq",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_cixu",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_oizh",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_isfa",
"fieldtype": "Column Break"
},
{
"depends_on": "eval: doc.apply_tds",
"fieldname": "tax_withholding_tab",
"fieldtype": "Tab Break",
"label": "Tax Withholding"
}
],
"icon": "fa fa-file-text",
@@ -638,7 +665,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2026-02-03 14:40:39.944524",
"modified": "2026-02-16 16:06:10.468482",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@@ -1,12 +1,13 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import json
from datetime import date
import frappe
from frappe import _, msgprint, scrub
from frappe.core.doctype.submission_queue.submission_queue import queue_submission
from frappe.model.document import Document
from frappe.utils import comma_and, cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
import erpnext
@@ -1215,7 +1216,7 @@ class JournalEntry(AccountsController):
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
@frappe.whitelist()
def get_balance(self, difference_account=None):
def get_balance(self, difference_account: str | None = None):
if not self.get("accounts"):
msgprint(_("'Entries' cannot be empty"), raise_exception=True)
else:
@@ -1321,7 +1322,12 @@ class JournalEntry(AccountsController):
@frappe.whitelist()
def get_default_bank_cash_account(
company, account_type=None, mode_of_payment=None, account=None, *, fetch_balance=True
company: str,
account_type: str | None = None,
mode_of_payment: str | None = None,
account: str | None = None,
*,
fetch_balance: bool = True,
):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
@@ -1370,7 +1376,12 @@ def get_default_bank_cash_account(
@frappe.whitelist()
def get_payment_entry_against_order(
dt, dn, amount=None, debit_in_account_currency=None, journal_entry=False, bank_account=None
dt: str,
dn: str,
amount: float | None = None,
debit_in_account_currency: str | float | None = None,
journal_entry: bool = False,
bank_account: str | None = None,
):
ref_doc = frappe.get_doc(dt, dn)
@@ -1415,7 +1426,12 @@ def get_payment_entry_against_order(
@frappe.whitelist()
def get_payment_entry_against_invoice(
dt, dn, amount=None, debit_in_account_currency=None, journal_entry=False, bank_account=None
dt: str,
dn: str,
amount: float | None = None,
debit_in_account_currency: str | None = None,
journal_entry: bool = False,
bank_account: str | None = None,
):
ref_doc = frappe.get_doc(dt, dn)
if dt == "Sales Invoice":
@@ -1528,7 +1544,7 @@ def get_payment_entry(ref_doc, args):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
def get_against_jv(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
if not frappe.db.has_column("Journal Entry", searchfield):
return []
@@ -1559,7 +1575,7 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist()
def get_outstanding(args):
def get_outstanding(args: str | dict):
if not frappe.has_permission("Account"):
frappe.msgprint(_("No Permission"), raise_exception=1)
@@ -1619,7 +1635,7 @@ def get_outstanding(args):
@frappe.whitelist()
def get_party_account_and_currency(company, party_type, party):
def get_party_account_and_currency(company: str, party_type: str, party: str):
if not frappe.has_permission("Account"):
frappe.msgprint(_("No Permission"), raise_exception=1)
@@ -1632,7 +1648,14 @@ def get_party_account_and_currency(company, party_type, party):
@frappe.whitelist()
def get_account_details_and_party_type(account, date, company, debit=None, credit=None, exchange_rate=None):
def get_account_details_and_party_type(
account: str,
date: str,
company: str,
debit: float | str | None = None,
credit: float | str | None = None,
exchange_rate: float | str | None = None,
):
"""Returns dict of account details and party type to be set in Journal Entry on selection of account."""
if not frappe.has_permission("Account"):
frappe.msgprint(_("No Permission"), raise_exception=1)
@@ -1681,15 +1704,15 @@ def get_account_details_and_party_type(account, date, company, debit=None, credi
@frappe.whitelist()
def get_exchange_rate(
posting_date,
account=None,
account_currency=None,
company=None,
reference_type=None,
reference_name=None,
debit=None,
credit=None,
exchange_rate=None,
posting_date: str | date,
account: str | None = None,
account_currency: str | None = None,
company: str | None = None,
reference_type: str | None = None,
reference_name: str | None = None,
debit: float | str | None = None,
credit: float | str | None = None,
exchange_rate: str | float | None = None,
):
# Ensure exchange_rate is always numeric to avoid calculation errors
if isinstance(exchange_rate, str):
@@ -1726,7 +1749,7 @@ def get_exchange_rate(
@frappe.whitelist()
def get_average_exchange_rate(account):
def get_average_exchange_rate(account: str):
exchange_rate = 0
bank_balance_in_account_currency = get_balance_on(account)
if bank_balance_in_account_currency:
@@ -1737,7 +1760,7 @@ def get_average_exchange_rate(account):
@frappe.whitelist()
def make_inter_company_journal_entry(name, voucher_type, company):
def make_inter_company_journal_entry(name: str, voucher_type: str, company: str):
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = voucher_type
journal_entry.company = company
@@ -1747,7 +1770,7 @@ def make_inter_company_journal_entry(name, voucher_type, company):
@frappe.whitelist()
def make_reverse_journal_entry(source_name, target_doc=None):
def make_reverse_journal_entry(source_name: str, target_doc: str | Document | None = None):
existing_reverse = frappe.db.exists("Journal Entry", {"reversal_of": source_name, "docstatus": 1})
if existing_reverse:
frappe.throw(

View File

@@ -43,7 +43,7 @@
"fields": [
{
"bold": 1,
"columns": 2,
"columns": 4,
"fieldname": "account",
"fieldtype": "Link",
"in_global_search": 1,
@@ -185,20 +185,19 @@
"fieldtype": "Select",
"label": "Reference Type",
"no_copy": 1,
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry",
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry\nBank Transaction",
"search_index": 1
},
{
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Reference Name",
"no_copy": 1,
"options": "reference_type",
"search_index": 1
},
{
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])",
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance', 'Bank Transaction'])",
"fieldname": "reference_due_date",
"fieldtype": "Date",
"label": "Reference Due Date",
@@ -294,7 +293,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-11-27 12:23:33.157655",
"modified": "2026-02-19 17:01:22.642454",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",

View File

@@ -55,6 +55,7 @@ class JournalEntryAccount(Document):
"Fees",
"Full and Final Statement",
"Payment Entry",
"Bank Transaction",
]
user_remark: DF.SmallText | None
# end: auto-generated types

View File

@@ -55,7 +55,7 @@ class LedgerMerge(Document):
@frappe.whitelist()
def form_start_merge(docname):
def form_start_merge(docname: str):
return frappe.get_doc("Ledger Merge", docname).start_merge()

View File

@@ -1,6 +1,7 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from datetime import date
import frappe
from frappe import _
@@ -88,13 +89,13 @@ def get_loyalty_details(
@frappe.whitelist()
def get_loyalty_program_details_with_points(
customer,
loyalty_program=None,
expiry_date=None,
company=None,
silent=False,
include_expired_entry=False,
current_transaction_amount=0,
customer: str,
loyalty_program: str | None = None,
expiry_date: str | date | None = None,
company: str | None = None,
silent: bool = False,
include_expired_entry: bool = False,
current_transaction_amount: int | float = 0,
):
lp_details = get_loyalty_program_details(customer, loyalty_program, company=company, silent=silent)
loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program)
@@ -119,12 +120,12 @@ def get_loyalty_program_details_with_points(
@frappe.whitelist()
def get_loyalty_program_details(
customer,
loyalty_program=None,
expiry_date=None,
company=None,
silent=False,
include_expired_entry=False,
customer: str,
loyalty_program: str | None = None,
expiry_date: str | date | None = None,
company: str | None = None,
silent: bool = False,
include_expired_entry: bool = False,
):
lp_details = frappe._dict()
@@ -146,7 +147,7 @@ def get_loyalty_program_details(
@frappe.whitelist()
def get_redeemption_factor(loyalty_program=None, customer=None):
def get_redeemption_factor(loyalty_program: str | None = None, customer: str | None = None):
customer_loyalty_program = None
if not loyalty_program:
customer_loyalty_program = frappe.db.get_value("Customer", customer, "loyalty_program")

View File

@@ -293,7 +293,7 @@ def publish(index, total, doctype):
@frappe.whitelist()
def get_temporary_opening_account(company=None):
def get_temporary_opening_account(company: str | None = None):
if not company:
return

View File

@@ -67,7 +67,7 @@ class PartyLink(Document):
@frappe.whitelist()
def create_party_link(primary_role, primary_party, secondary_party):
def create_party_link(primary_role: str, primary_party: str, secondary_party: str):
party_link = frappe.new_doc("Party Link")
party_link.primary_role = primary_role
party_link.primary_party = primary_party

View File

@@ -512,12 +512,16 @@ frappe.ui.form.on("Payment Entry", {
frm.set_value("contact_email", "");
frm.set_value("contact_person", "");
}
if (frm.doc.payment_type && frm.doc.party_type && frm.doc.party && frm.doc.company) {
if (!frm.doc.posting_date) {
frappe.msgprint(__("Please select Posting Date before selecting Party"));
frm.set_value("party", "");
return;
}
erpnext.utils.get_employee_contact_details(frm);
frm.set_party_account_based_on_party = true;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;

View File

@@ -701,7 +701,6 @@
"fetch_from": "company.book_advance_payments_in_separate_party_account",
"fieldname": "book_advance_payments_in_separate_party_account",
"fieldtype": "Check",
"hidden": 1,
"label": "Book Advance Payments in Separate Party Account",
"no_copy": 1,
"read_only": 1
@@ -793,7 +792,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2025-12-18 13:56:40.206038",
"modified": "2026-02-03 16:08:49.800381",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@@ -1,12 +1,13 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import json
from datetime import date
from functools import reduce
import frappe
from frappe import ValidationError, _, qb, scrub, throw
from frappe.model.document import Document
from frappe.model.meta import get_field_precision
from frappe.query_builder import Tuple
from frappe.query_builder.functions import Count
@@ -1064,8 +1065,12 @@ class PaymentEntry(AccountsController):
total_allocated_amount += flt(d.allocated_amount)
base_total_allocated_amount += self.calculate_base_allocated_amount_for_reference(d)
self.total_allocated_amount = abs(total_allocated_amount)
self.base_total_allocated_amount = abs(base_total_allocated_amount)
self.total_allocated_amount = flt(
abs(total_allocated_amount), self.precision("total_allocated_amount")
)
self.base_total_allocated_amount = flt(
abs(base_total_allocated_amount), self.precision("base_total_allocated_amount")
)
def set_unallocated_amount(self):
self.unallocated_amount = 0
@@ -1081,20 +1086,32 @@ class PaymentEntry(AccountsController):
self.base_paid_amount + deductions_to_consider
):
self.unallocated_amount = (
self.base_paid_amount
+ deductions_to_consider
- self.base_total_allocated_amount
- included_taxes
) / self.source_exchange_rate
flt(
(
self.base_paid_amount
+ deductions_to_consider
- self.base_total_allocated_amount
- included_taxes
),
self.precision("unallocated_amount"),
)
/ self.source_exchange_rate
)
elif self.payment_type == "Pay" and self.base_total_allocated_amount < (
self.base_received_amount - deductions_to_consider
):
self.unallocated_amount = (
self.base_received_amount
- deductions_to_consider
- self.base_total_allocated_amount
- included_taxes
) / self.target_exchange_rate
flt(
(
self.base_received_amount
- deductions_to_consider
- self.base_total_allocated_amount
- included_taxes
),
self.precision("unallocated_amount"),
)
/ self.target_exchange_rate
)
def set_exchange_gain_loss(self):
exchange_gain_loss = flt(
@@ -1867,7 +1884,9 @@ class PaymentEntry(AccountsController):
frappe.response["matched_payment_requests"] = matched_payment_requests
@frappe.whitelist()
def allocate_amount_to_references(self, paid_amount, paid_amount_change, allocate_payment_amount):
def allocate_amount_to_references(
self, paid_amount: float, paid_amount_change: bool, allocate_payment_amount: bool
):
"""
Allocate `Allocated Amount` and `Payment Request` against `Reference` based on `Paid Amount` and `Outstanding Amount`.\n
:param paid_amount: Paid Amount / Received Amount.
@@ -2039,7 +2058,7 @@ class PaymentEntry(AccountsController):
)
@frappe.whitelist()
def set_matched_payment_requests(self, matched_payment_requests):
def set_matched_payment_requests(self, matched_payment_requests: str | list | None):
"""
Set `Payment Request` against `Reference` based on `matched_payment_requests`.\n
:param matched_payment_requests: List of tuple of matched Payment Requests.
@@ -2255,7 +2274,7 @@ def validate_inclusive_tax(tax, doc):
@frappe.whitelist()
def get_outstanding_reference_documents(args, validate=False):
def get_outstanding_reference_documents(args: str | dict, validate: bool = False):
if isinstance(args, str):
args = json.loads(args)
@@ -2670,7 +2689,7 @@ def get_negative_outstanding_invoices(
@frappe.whitelist()
def get_party_details(company, party_type, party, date, cost_center=None):
def get_party_details(company: str, party_type: str, party: str, date: str, cost_center: str | None = None):
bank_account = ""
party_bank_account = ""
@@ -2696,7 +2715,7 @@ def get_party_details(company, party_type, party, date, cost_center=None):
@frappe.whitelist()
def get_account_details(account, date, cost_center=None):
def get_account_details(account: str, date: str | date, cost_center: str | None = None):
frappe.has_permission("Payment Entry", throw=True)
# to check if the passed account is accessible under reference doctype Payment Entry
@@ -2716,7 +2735,7 @@ def get_account_details(account, date, cost_center=None):
@frappe.whitelist()
def get_company_defaults(company):
def get_company_defaults(company: str):
fields = ["write_off_account", "exchange_gain_loss_account", "cost_center"]
return frappe.get_cached_value("Company", company, fields, as_dict=1)
@@ -2755,7 +2774,11 @@ def get_outstanding_on_journal_entry(voucher_no, party_type, party):
@frappe.whitelist()
def get_reference_details(
reference_doctype, reference_name, party_account_currency, party_type=None, party=None
reference_doctype: str,
reference_name: str,
party_account_currency: str,
party_type: str | None = None,
party: str | None = None,
):
total_amount = outstanding_amount = exchange_rate = account = None
@@ -2846,15 +2869,15 @@ def get_reference_details(
@frappe.whitelist()
def get_payment_entry(
dt,
dn,
party_amount=None,
bank_account=None,
bank_amount=None,
party_type=None,
payment_type=None,
reference_date=None,
created_from_payment_request=False,
dt: str,
dn: str,
party_amount: int | float | None = None,
bank_account: str | None = None,
bank_amount: int | float | None = None,
party_type: str | None = None,
payment_type: str | None = None,
reference_date: str | date | None = None,
created_from_payment_request: bool | None = None,
):
doc = frappe.get_doc(dt, dn)
over_billing_allowance = frappe.get_single_value("Accounts Settings", "over_billing_allowance")
@@ -3520,7 +3543,7 @@ def get_paid_amount(dt, dn, party_type, party, account, due_date):
@frappe.whitelist()
def make_payment_order(source_name, target_doc=None):
def make_payment_order(source_name: str, target_doc: str | Document | None = None):
from frappe.model.mapper import get_mapped_doc
def set_missing_values(source, target):

View File

@@ -59,7 +59,7 @@ class PaymentOrder(Document):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_mop_query(doctype, txt, searchfield, start, page_len, filters):
def get_mop_query(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
return frappe.db.sql(
""" select mode_of_payment from `tabPayment Order Reference`
where parent = %(parent)s and mode_of_payment like %(txt)s
@@ -70,7 +70,7 @@ def get_mop_query(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_supplier_query(doctype, txt, searchfield, start, page_len, filters):
def get_supplier_query(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
return frappe.db.sql(
""" select supplier from `tabPayment Order Reference`
where parent = %(parent)s and supplier like %(txt)s and
@@ -81,7 +81,7 @@ def get_supplier_query(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist()
def make_payment_records(name, supplier, mode_of_payment=None):
def make_payment_records(name: str, supplier: str, mode_of_payment: str | None = None):
doc = frappe.get_doc("Payment Order", name)
make_journal_entry(doc, supplier, mode_of_payment)

View File

@@ -433,7 +433,9 @@ class PaymentReconciliation(Document):
return frappe.get_single_value("Accounts Settings", "auto_reconcile_payments")
@frappe.whitelist()
def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount):
def calculate_difference_on_allocation_change(
self, payment_entry: list, invoice: list, allocated_amount: float
):
invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry)
invoice[0]["exchange_rate"] = invoice_exchange_map.get(invoice[0].get("invoice_number"))
if payment_entry[0].get("reference_type") in ["Sales Invoice", "Purchase Invoice"]:
@@ -445,7 +447,7 @@ class PaymentReconciliation(Document):
return new_difference_amount
@frappe.whitelist()
def allocate_entries(self, args):
def allocate_entries(self, args: dict):
self.validate_entries()
exc_gain_loss_posting_date = frappe.db.get_single_value(

View File

@@ -0,0 +1,88 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-12-02 17:50:08.648006",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"payment_term",
"column_break_lnjp",
"payment_schedule",
"section_break_fjhh",
"description",
"section_break_mjlv",
"due_date",
"column_break_qghl",
"amount"
],
"fields": [
{
"fieldname": "payment_term",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Payment Term",
"options": "Payment Term"
},
{
"collapsible": 1,
"fieldname": "section_break_fjhh",
"fieldtype": "Section Break",
"label": "Description"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Description"
},
{
"fieldname": "section_break_mjlv",
"fieldtype": "Section Break"
},
{
"fieldname": "due_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Due Date"
},
{
"fieldname": "column_break_qghl",
"fieldtype": "Column Break"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"precision": "2"
},
{
"fieldname": "column_break_lnjp",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "payment_schedule",
"fieldtype": "Link",
"label": "Payment Schedule",
"options": "Payment Schedule",
"read_only": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-01-19 02:21:36.455830",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reference",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,27 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class PaymentReference(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
amount: DF.Currency
description: DF.SmallText | None
due_date: DF.Date | None
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
payment_schedule: DF.Link | None
payment_term: DF.Link | None
# end: auto-generated types
pass

View File

@@ -105,3 +105,29 @@ frappe.ui.form.on("Payment Request", "is_a_subscription", function (frm) {
});
}
});
frappe.ui.form.on("Payment Request", "calculate_total_amount_by_selected_rows", function (frm) {
if (frm.doc.docstatus !== 0) {
frappe.msgprint(__("Cannot fetch selected rows for submitted Payment Request"));
return;
}
const selected = frm.get_selected()?.payment_reference || [];
if (!selected.length) {
frappe.throw(__("No rows selected"));
}
let total = 0;
selected.forEach((name) => {
const row = frm.doc.payment_reference.find((d) => d.name === name);
if (row) {
row.manually_selected = 1;
total += row.amount;
}
});
frm.doc.payment_reference.forEach((row) => {
row.auto_selected = 0;
});
frm.set_value("grand_total", total);
frm.refresh_field("grand_total");
frm.save();
});

View File

@@ -19,6 +19,8 @@
"column_break_4",
"reference_doctype",
"reference_name",
"payment_reference_section",
"payment_reference",
"transaction_details",
"grand_total",
"currency",
@@ -157,6 +159,7 @@
"label": "Amount",
"non_negative": 1,
"options": "currency",
"read_only_depends_on": "eval:doc.payment_reference.length>0",
"reqd": 1
},
{
@@ -457,6 +460,17 @@
"fieldname": "phone_number",
"fieldtype": "Data",
"label": "Phone Number"
},
{
"fieldname": "payment_reference_section",
"fieldtype": "Section Break"
},
{
"fieldname": "payment_reference",
"fieldtype": "Table",
"label": "Payment Reference",
"options": "Payment Reference",
"read_only": 1
}
],
"grid_page_length": 50,
@@ -464,7 +478,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-08-29 11:52:48.555415",
"modified": "2026-01-13 12:53:00.963274",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",

View File

@@ -45,6 +45,7 @@ class PaymentRequest(Document):
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.accounts.doctype.payment_reference.payment_reference import PaymentReference
from erpnext.accounts.doctype.subscription_plan_detail.subscription_plan_detail import (
SubscriptionPlanDetail,
)
@@ -78,6 +79,7 @@ class PaymentRequest(Document):
payment_gateway: DF.ReadOnly | None
payment_gateway_account: DF.Link | None
payment_order: DF.Link | None
payment_reference: DF.Table[PaymentReference]
payment_request_type: DF.Literal["Outward", "Inward"]
payment_url: DF.Data | None
phone_number: DF.Data | None
@@ -109,15 +111,36 @@ class PaymentRequest(Document):
if self.get("__islocal"):
self.status = "Draft"
self.validate_reference_document()
self.validate_against_payment_reference()
self.validate_payment_request_amount()
# self.validate_currency()
self.validate_subscription_details()
def validate_against_payment_reference(self):
if not self.payment_reference:
return
expected = sum(flt(r.amount) for r in self.payment_reference)
if flt(expected, self.precision("grand_total")) != flt(self.grand_total):
frappe.throw(_("Grand Total must match sum of Payment References"))
seen = set()
for r in self.payment_reference:
if not r.payment_schedule:
continue # legacy mode → skip
if r.payment_schedule in seen:
frappe.throw(_("Duplicate Payment Schedule selected"))
seen.add(r.payment_schedule)
def validate_reference_document(self):
if not self.reference_doctype or not self.reference_name:
frappe.throw(_("To create a Payment Request reference document is required"))
def validate_payment_request_amount(self):
if self.payment_reference:
return
if self.grand_total == 0:
frappe.throw(
_("{0} cannot be zero").format(self.get_label_from_fieldname("grand_total")),
@@ -539,8 +562,6 @@ class PaymentRequest(Document):
def make_payment_request(**args):
"""Make payment request"""
frappe.has_permission(doctype="Payment Request", ptype="write", throw=True)
args = frappe._dict(args)
if args.dt not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST:
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))
@@ -548,12 +569,69 @@ def make_payment_request(**args):
if args.dn and not isinstance(args.dn, str):
frappe.throw(_("Invalid parameter. 'dn' should be of type str"))
frappe.has_permission("Payment Request", "create", throw=True)
frappe.has_permission(args.dt, "read", args.dn, throw=True)
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
if not args.get("company"):
args.company = ref_doc.company
gateway_account = get_gateway_details(args) or frappe._dict()
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
# Schedule-based PRs are allowed only if no Payment Entry exists for this document.
# Any existing Payment Entry forces legacy (amount-based) flow.
selected_payment_schedules = json.loads(args.get("schedules")) if args.get("schedules") else []
# Backend guard:
# If any Payment Entry exists, schedule-based PRs are not allowed.
if selected_payment_schedules and get_existing_payment_entry(ref_doc.name):
frappe.throw(
_(
"Payment Schedule based Payment Requests cannot be created because a Payment Entry already exists for this document."
)
)
has_payment_entry = bool(get_existing_payment_entry(ref_doc.name))
payment_reference = []
if selected_payment_schedules:
existing_payment_references = get_existing_payment_references(ref_doc.name)
if existing_payment_references:
existing_ids = {r["payment_schedule"] for r in existing_payment_references}
selected_ids = {r["name"] for r in selected_payment_schedules}
duplicate_ids = existing_ids & selected_ids
if duplicate_ids:
duplicate_schedules = []
for row in selected_payment_schedules:
if row["name"] in duplicate_ids:
existing_ref = next(
(r for r in existing_payment_references if r["payment_schedule"] == row["name"]),
{},
)
existing_pr = existing_ref.get("parent")
duplicate_schedules.append(
f"Payment Term: {row.get('payment_term')}, "
f"Due Date: {row.get('due_date')}, "
f"Amount: {row.get('payment_amount')} "
f"(already requested in PR {existing_pr})"
)
frappe.throw(
_("The following payment schedule(s) already exist:\n{0}").format(
"\n".join(duplicate_schedules)
)
)
payment_reference = set_payment_references(args.get("schedules"))
# Determine grand_total
if selected_payment_schedules and not has_payment_entry:
grand_total = sum(row.get("payment_amount") for row in selected_payment_schedules)
else:
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
if not grand_total:
frappe.throw(_("Payment Entry is already created"))
@@ -563,7 +641,6 @@ def make_payment_request(**args):
loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points)) # sets fields on ref_doc
ref_doc.db_update()
grand_total = grand_total - loyalty_amount
# fetches existing payment request `grand_total` amount
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc)
@@ -583,19 +660,20 @@ def make_payment_request(**args):
else:
# If PR's are processed, cancel all of them.
cancel_old_payment_requests(ref_doc.doctype, ref_doc.name)
else:
elif not selected_payment_schedules:
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
draft_payment_request = frappe.db.get_value(
"Payment Request",
{"reference_doctype": ref_doc.doctype, "reference_name": ref_doc.name, "docstatus": 0},
)
if draft_payment_request:
frappe.db.set_value(
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
)
pr = frappe.get_doc("Payment Request", draft_payment_request)
if selected_payment_schedules:
apply_payment_references(pr, payment_reference)
pr.save()
else:
bank_account = (
get_party_bank_account(args.get("party_type"), args.get("party"))
@@ -650,7 +728,10 @@ def make_payment_request(**args):
}
)
# Update dimensions
if selected_payment_schedules:
apply_payment_references(pr, payment_reference)
# Dimensions
pr.update(
{
"cost_center": ref_doc.get("cost_center"),
@@ -679,6 +760,51 @@ def make_payment_request(**args):
return pr.as_dict()
def apply_payment_references(pr, payment_reference):
existing_refs = pr.get("payment_reference") or []
existing_ids = {r.get("payment_schedule") for r in existing_refs if r.get("payment_schedule")}
new_refs = [r for r in (payment_reference or []) if r.get("payment_schedule") not in existing_ids]
pr.set("payment_reference", existing_refs + new_refs)
pr.set("grand_total", sum(flt(r.get("amount")) for r in pr.get("payment_reference")))
def set_payment_references(payment_schedules):
payment_schedules = json.loads(payment_schedules) if payment_schedules else []
payment_reference = []
for row in payment_schedules:
payment_reference.append(
{
"payment_term": row.get("payment_term"),
"payment_schedule": row.get("name"),
"description": row.get("description"),
"due_date": row.get("due_date"),
"amount": row.get("payment_amount"),
}
)
return payment_reference
def get_existing_payment_entry(ref_docname):
pe = frappe.qb.DocType("Payment Entry")
per = frappe.qb.DocType("Payment Entry Reference")
existing_pe = (
frappe.qb.from_(pe)
.join(per)
.on(per.parent == pe.name)
.select(pe.name)
.where(pe.docstatus < 2)
.where(per.reference_name == ref_docname)
.limit(1)
.run()
)
return existing_pe
def get_amount(ref_doc, payment_account=None):
"""get amount based on doctype"""
grand_total = 0
@@ -811,7 +937,7 @@ def get_payment_gateway_account(filter):
@frappe.whitelist()
def get_print_format_list(ref_doctype):
def get_print_format_list(ref_doctype: str):
print_format_list = ["Standard"]
print_format_list.extend(
@@ -821,13 +947,13 @@ def get_print_format_list(ref_doctype):
return {"print_format": print_format_list}
@frappe.whitelist(allow_guest=True)
def resend_payment_email(docname):
@frappe.whitelist()
def resend_payment_email(docname: str):
return frappe.get_doc("Payment Request", docname).send_email()
@frappe.whitelist()
def make_payment_entry(docname):
def make_payment_entry(docname: str):
doc = frappe.get_doc("Payment Request", docname)
return doc.create_payment_entry(submit=False).as_dict()
@@ -920,7 +1046,7 @@ def get_dummy_message(doc):
@frappe.whitelist()
def get_subscription_details(reference_doctype, reference_name):
def get_subscription_details(reference_doctype: str, reference_name: str):
if reference_doctype == "Sales Invoice":
subscriptions = frappe.db.sql(
"""SELECT parent as sub_name FROM `tabSubscription Invoice` WHERE invoice=%s""",
@@ -936,7 +1062,7 @@ def get_subscription_details(reference_doctype, reference_name):
@frappe.whitelist()
def make_payment_order(source_name, target_doc=None):
def make_payment_order(source_name: str, target_doc: str | Document | None = None):
from frappe.model.mapper import get_mapped_doc
def set_missing_values(source, target):
@@ -984,7 +1110,9 @@ def validate_payment(doc, method=None):
@frappe.whitelist()
def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters):
def get_open_payment_requests_query(
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict
):
# permission checks in `get_list()`
filters = frappe._dict(filters)
@@ -1023,3 +1151,44 @@ def get_irequests_of_payment_request(doc: str | None = None) -> list:
},
)
return res
@frappe.whitelist()
def get_available_payment_schedules(reference_doctype: str, reference_name: str):
ref_doc = frappe.get_doc(reference_doctype, reference_name)
if not hasattr(ref_doc, "payment_schedule") or not ref_doc.payment_schedule:
return []
if get_existing_payment_entry(reference_name):
return []
existing_refs = get_existing_payment_references(reference_name)
existing_ids = {r["payment_schedule"] for r in existing_refs if r.get("payment_schedule")}
return [r for r in ref_doc.payment_schedule if r.name not in existing_ids]
def get_existing_payment_references(reference_name):
PR = frappe.qb.DocType("Payment Request")
PRF = frappe.qb.DocType("Payment Reference")
result = (
frappe.qb.from_(PR)
.join(PRF)
.on(PR.name == PRF.parent)
.select(
PRF.payment_term,
PRF.due_date,
PRF.amount.as_("payment_amount"),
PRF.payment_schedule,
PRF.parent,
)
.where(PR.reference_name == reference_name)
.where(PR.docstatus < 2)
.where(
PR.status.isin(["Draft", "Requested", "Initiated", "Partially Paid", "Payment Ordered", "Paid"])
)
).run(as_dict=True)
return result

View File

@@ -1,11 +1,13 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import json
import re
from unittest.mock import patch
import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import add_days, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template
@@ -850,3 +852,130 @@ class TestPaymentRequest(IntegrationTestCase):
pr.load_from_db()
self.assertEqual(pr.grand_total, pi.outstanding_amount)
def test_payment_request_grand_total_from_selected_schedules(self):
po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=100)
po.payment_schedule = []
po.append("payment_schedule", {"due_date": nowdate(), "payment_amount": 30})
po.append("payment_schedule", {"due_date": add_days(nowdate(), 1), "payment_amount": 30})
po.append("payment_schedule", {"due_date": add_days(nowdate(), 2), "payment_amount": 40})
po.save()
po.submit()
schedules = json.dumps(
[
{
"payment_term": row.payment_term,
"name": row.name,
"due_date": row.due_date,
"payment_amount": row.payment_amount,
"description": row.description,
}
for row in [po.payment_schedule[0], po.payment_schedule[2]]
]
)
pr = make_payment_request(
dt="Purchase Order",
dn=po.name,
mute_email=1,
submit_doc=False,
return_doc=True,
schedules=schedules,
)
pr.submit()
self.assertEqual(pr.grand_total, 70)
self.assertEqual(len(pr.payment_reference), 2)
def test_draft_pr_reuse_merges_payment_references(self):
from frappe.utils import add_days, nowdate
po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=100)
po.payment_schedule = []
po.append("payment_schedule", {"due_date": nowdate(), "payment_amount": 50})
po.append("payment_schedule", {"due_date": add_days(nowdate(), 1), "payment_amount": 50})
po.save()
po.submit()
schedules = json.dumps(
[
{
"payment_term": row.payment_term,
"name": row.name,
"due_date": row.due_date,
"payment_amount": row.payment_amount,
"description": row.description,
}
for row in [po.payment_schedule[0]]
]
)
pr = make_payment_request(
dt="Purchase Order",
dn=po.name,
mute_email=1,
submit_doc=False,
return_doc=True,
schedules=schedules,
)
pr.save()
schedules = json.dumps(
[
{
"payment_term": row.payment_term,
"name": row.name,
"due_date": row.due_date,
"payment_amount": row.payment_amount,
"description": row.description,
}
for row in [po.payment_schedule[1]]
]
)
# call make_payment_request again → reuse draft
pr_reused = make_payment_request(
dt="Purchase Order",
dn=po.name,
mute_email=1,
submit_doc=False,
return_doc=True,
schedules=schedules,
)
self.assertEqual(pr.name, pr_reused.name)
self.assertEqual(pr_reused.grand_total, 100)
self.assertEqual(len(pr_reused.payment_reference), 2)
def test_schedule_pr_not_allowed_if_payment_entry_exists(self):
po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=100)
po.payment_schedule = []
row = po.append("payment_schedule", {"due_date": nowdate(), "payment_amount": 100})
po.save()
po.submit()
# create PE first
pr = make_payment_request(dt="Purchase Order", dn=po.name, mute_email=1, submit_doc=1, return_doc=1)
pr.create_payment_entry()
schedules = json.dumps(
[
{
"name": row.name,
"payment_term": row.payment_term,
"due_date": row.due_date,
"payment_amount": row.payment_amount,
"description": row.description,
}
]
)
with self.assertRaises(frappe.ValidationError):
make_payment_request(
dt="Purchase Order",
dn=po.name,
mute_email=1,
submit_doc=False,
return_doc=True,
schedules=schedules,
)

View File

@@ -515,7 +515,7 @@ def delete_closing_entries(voucher_no):
@frappe.whitelist()
def get_period_start_end_date(fiscal_year, company):
def get_period_start_end_date(fiscal_year: str, company: str):
fy_start_date, fy_end_date = frappe.db.get_value(
"Fiscal Year", fiscal_year, ["year_start_date", "year_end_date"]
)

View File

@@ -4,19 +4,6 @@
frappe.ui.form.on("POS Closing Entry", {
onload: async function (frm) {
frm.ignore_doctypes_on_cancel_all = ["POS Invoice Merge Log", "Sales Invoice"];
frm.set_query("pos_profile", function (doc) {
return {
filters: { user: doc.user },
};
});
frm.set_query("user", function (doc) {
return {
query: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_cashiers",
filters: { parent: doc.pos_profile },
};
});
frm.set_query("pos_opening_entry", function (doc) {
return { filters: { status: "Open", docstatus: 1 } };
});

View File

@@ -2,6 +2,8 @@
# For license information, please see license.txt
from datetime import datetime
import frappe
from frappe import _
from frappe.query_builder import DocType
@@ -252,13 +254,13 @@ class POSClosingEntry(StatusUpdater):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_cashiers(doctype, txt, searchfield, start, page_len, filters):
def get_cashiers(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
cashiers_list = frappe.get_all("POS Profile User", filters=filters, fields=["user"], as_list=1)
return [c for c in cashiers_list]
@frappe.whitelist()
def get_invoices(start, end, pos_profile, user):
def get_invoices(start: str | datetime, end: str | datetime, pos_profile: str, user: str):
invoice_doctype = frappe.db.get_single_value("POS Settings", "invoice_type")
sales_inv_query = build_invoice_query("Sales Invoice", user, pos_profile, start, end)

View File

@@ -4,6 +4,7 @@
import frappe
from frappe import _, bold
from frappe.model.document import Document
from frappe.model.mapper import map_child_doc, map_doc
from frappe.query_builder.functions import IfNull, Sum
from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
@@ -753,7 +754,7 @@ class POSInvoice(SalesInvoice):
return profile
@frappe.whitelist()
def set_missing_values(self, for_validate=False):
def set_missing_values(self, for_validate: bool = False):
profile = self.set_pos_fields(for_validate)
if not self.debit_to:
@@ -854,7 +855,7 @@ class POSInvoice(SalesInvoice):
return frappe.get_doc("Payment Request", pr)
@frappe.whitelist()
def update_payments(self, payments):
def update_payments(self, payments: list):
if self.status == "Consolidated":
frappe.throw(_("Create Payment Entry for Consolidated POS Invoices."))
@@ -897,7 +898,7 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
def get_stock_availability(item_code: str | None, warehouse: str):
if frappe.db.get_value("Item", item_code, "is_stock_item"):
is_stock_item = True
bin_qty = get_bin_qty(item_code, warehouse)
@@ -1020,14 +1021,14 @@ def get_pos_reserved_qty_from_table(child_table, item_code, warehouse):
@frappe.whitelist()
def make_sales_return(source_name, target_doc=None):
def make_sales_return(source_name: str, target_doc: Document | None = None):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
return make_return_doc("POS Invoice", source_name, target_doc)
@frappe.whitelist()
def make_merge_log(invoices):
def make_merge_log(invoices: str | list):
import json
if isinstance(invoices, str):
@@ -1077,7 +1078,15 @@ def add_return_modes(doc, pos_profile):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
def item_query(
doctype: str,
txt: str,
searchfield: str,
start: int,
page_len: int,
filters: dict,
as_dict: bool = False,
):
if pos_profile := filters.get("pos_profile")[1]:
pos_profile = frappe.get_cached_doc("POS Profile", pos_profile)
if item_groups := get_item_group(pos_profile):

View File

@@ -275,7 +275,7 @@ def get_child_nodes(group_type, root):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
def pos_profile_query(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
user = frappe.session["user"]
company = filters.get("company") or frappe.defaults.get_user_default("company")
@@ -319,7 +319,7 @@ def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist()
def set_default_profile(pos_profile, company):
def set_default_profile(pos_profile: str, company: str):
modified = now()
user = frappe.session.user

View File

@@ -121,7 +121,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Apply On",
"options": "\nItem Code\nItem Group\nBrand\nTransaction",
"options": "Item Code\nItem Group\nBrand\nTransaction",
"reqd": 1
},
{
@@ -657,7 +657,7 @@
"icon": "fa fa-gift",
"idx": 1,
"links": [],
"modified": "2025-08-20 11:40:07.096854",
"modified": "2026-02-17 12:24:07.553505",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",
@@ -714,9 +714,10 @@
"write": 1
}
],
"row_format": "Dynamic",
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "title"
}
}

View File

@@ -45,7 +45,7 @@ class PricingRule(Document):
apply_discount_on: DF.Literal["Grand Total", "Net Total"]
apply_discount_on_rate: DF.Check
apply_multiple_pricing_rules: DF.Check
apply_on: DF.Literal["", "Item Code", "Item Group", "Brand", "Transaction"]
apply_on: DF.Literal["Item Code", "Item Group", "Brand", "Transaction"]
apply_recursion_over: DF.Float
apply_rule_on_other: DF.Literal["", "Item Code", "Item Group", "Brand"]
brands: DF.Table[PricingRuleBrand]
@@ -320,7 +320,7 @@ class PricingRule(Document):
@frappe.whitelist()
def apply_pricing_rule(args, doc=None):
def apply_pricing_rule(args: str | dict, doc: str | dict | Document | None = None):
"""
args = {
"items": [{"doctype": "", "name": "", "item_code": "", "brand": "", "item_group": ""}, ...],
@@ -346,8 +346,7 @@ def apply_pricing_rule(args, doc=None):
args = frappe._dict(args)
if not args.transaction_type:
set_transaction_type(args)
set_transaction_type(args)
# list of dictionaries
out = []
@@ -618,7 +617,12 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
@frappe.whitelist()
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, rate=None):
def remove_pricing_rule_for_item(
pricing_rules: str | None,
item_details: str | frappe._dict,
item_code: str | None = None,
rate: float | None = None,
):
from erpnext.accounts.doctype.pricing_rule.utils import (
get_applied_pricing_rules,
get_pricing_rule_items,
@@ -666,7 +670,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, ra
@frappe.whitelist()
def remove_pricing_rules(item_list):
def remove_pricing_rules(item_list: str | list):
if isinstance(item_list, str):
item_list = json.loads(item_list)
@@ -683,28 +687,28 @@ def remove_pricing_rules(item_list):
return out
def set_transaction_type(args):
if args.transaction_type:
def set_transaction_type(pricing_ctx: frappe._dict) -> None:
if pricing_ctx.transaction_type in ["buying", "selling"]:
return
if args.doctype in ("Opportunity", "Quotation", "Sales Order", "Delivery Note", "Sales Invoice"):
args.transaction_type = "selling"
elif args.doctype in (
if pricing_ctx.doctype in ("Opportunity", "Quotation", "Sales Order", "Delivery Note", "Sales Invoice"):
pricing_ctx.transaction_type = "selling"
elif pricing_ctx.doctype in (
"Material Request",
"Supplier Quotation",
"Purchase Order",
"Purchase Receipt",
"Purchase Invoice",
):
args.transaction_type = "buying"
elif args.customer:
args.transaction_type = "selling"
pricing_ctx.transaction_type = "buying"
elif pricing_ctx.customer:
pricing_ctx.transaction_type = "selling"
else:
args.transaction_type = "buying"
pricing_ctx.transaction_type = "buying"
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_item_uoms(doctype, txt, searchfield, start, page_len, filters):
def get_item_uoms(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
items = [filters.get("value")]
if filters.get("apply_on") != "Item Code":
field = frappe.scrub(filters.get("apply_on"))

View File

@@ -10,7 +10,7 @@
],
"fields": [
{
"depends_on": "eval:parent.apply_on == 'Item Code'",
"depends_on": "eval:parent.apply_on == 'Brand'",
"fieldname": "brand",
"fieldtype": "Link",
"in_list_view": 1,
@@ -28,14 +28,15 @@
],
"istable": 1,
"links": [],
"modified": "2024-03-27 13:10:17.857046",
"modified": "2026-02-17 12:17:13.073587",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule Brand",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -10,7 +10,7 @@
],
"fields": [
{
"depends_on": "eval:parent.apply_on == 'Item Code'",
"depends_on": "eval:parent.apply_on == 'Item Group'",
"fieldname": "item_group",
"fieldtype": "Link",
"in_list_view": 1,
@@ -28,14 +28,15 @@
],
"istable": 1,
"links": [],
"modified": "2024-03-27 13:10:18.221095",
"modified": "2026-02-17 12:16:57.778471",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule Item Group",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -420,7 +420,7 @@ def get_context(customer, doc):
@frappe.whitelist()
def fetch_customers(customer_collection, collection_name, primary_mandatory):
def fetch_customers(customer_collection: str, collection_name: str, primary_mandatory: str | int):
customer_list = []
customers = []
@@ -460,7 +460,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
@frappe.whitelist()
def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True):
def get_customer_emails(customer_name: str, primary_mandatory: str | int, billing_and_primary: bool = True):
"""Returns first email from Contact Email table as a Billing email
when Is Billing Contact checked
and Primary email- email with Is Primary checked"""
@@ -506,7 +506,7 @@ def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=Tr
@frappe.whitelist()
def download_statements(document_name):
def download_statements(document_name: str):
doc = frappe.get_doc("Process Statement Of Accounts", document_name)
report = get_report_pdf(doc)
if report:
@@ -516,7 +516,7 @@ def download_statements(document_name):
@frappe.whitelist()
def send_emails(document_name, from_scheduler=False, posting_date=None):
def send_emails(document_name: str, from_scheduler: bool = False, posting_date: str | None = None):
doc = frappe.get_doc("Process Statement Of Accounts", document_name)
report = get_report_pdf(doc, consolidated=False)

View File

@@ -18,8 +18,19 @@ class TestProcessStatementOfAccounts(AccountsTestMixin, IntegrationTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
letterhead = frappe.get_doc("Letter Head", "Company Letterhead - Grey")
letterhead.is_default = 0
letterhead.save()
cls.enterClassContext(cls.change_settings("Selling Settings", validate_selling_price=0))
@classmethod
def tearDownClass(cls):
super().tearDownClass()
letterhead = frappe.get_doc("Letter Head", "Company Letterhead - Grey")
letterhead.is_default = 1
letterhead.save()
frappe.db.commit() # nosemgrep
def setUp(self):
self.create_company()
self.create_customer()

View File

@@ -134,7 +134,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
this.frm.add_custom_button(
__("Payment Request"),
function () {
me.make_payment_request();
me.make_payment_request_with_schedule();
},
__("Create")
);

View File

@@ -86,18 +86,18 @@
"taxes_and_charges_deducted",
"total_taxes_and_charges",
"totals_section",
"use_company_roundoff_cost_center",
"grand_total",
"in_words",
"column_break8",
"disable_rounded_total",
"rounding_adjustment",
"column_break8",
"use_company_roundoff_cost_center",
"in_words",
"rounded_total",
"base_totals_section",
"base_grand_total",
"base_rounding_adjustment",
"column_break_hcca",
"base_in_words",
"column_break_hcca",
"base_rounding_adjustment",
"base_rounded_total",
"section_break_ttrv",
"total_advance",
@@ -1689,7 +1689,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2026-02-05 20:45:16.964500",
"modified": "2026-02-23 13:23:57.269770",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -6,6 +6,7 @@ import json
import frappe
from frappe import _, qb, throw
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
@@ -1745,10 +1746,6 @@ class PurchaseInvoice(BuyingController):
project_doc.db_update()
def validate_supplier_invoice(self):
if self.bill_date:
if getdate(self.bill_date) > getdate(self.posting_date):
frappe.throw(_("Supplier Invoice Date cannot be greater than Posting Date"))
if self.bill_no:
if cint(frappe.get_single_value("Accounts Settings", "check_supplier_invoice_uniqueness")):
fiscal_year = get_fiscal_year(self.posting_date, company=self.company, as_dict=True)
@@ -1945,14 +1942,14 @@ def make_regional_gl_entries(gl_entries, doc):
@frappe.whitelist()
def make_debit_note(source_name, target_doc=None):
def make_debit_note(source_name: str, target_doc: str | Document | None = None):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
return make_return_doc("Purchase Invoice", source_name, target_doc)
@frappe.whitelist()
def make_stock_entry(source_name, target_doc=None):
def make_stock_entry(source_name: str, target_doc: str | Document | None = None):
doc = get_mapped_doc(
"Purchase Invoice",
source_name,
@@ -1970,35 +1967,37 @@ def make_stock_entry(source_name, target_doc=None):
@frappe.whitelist()
def change_release_date(name, release_date=None):
def change_release_date(name: str, release_date: str | None = None):
if frappe.db.exists("Purchase Invoice", name):
pi = frappe.get_lazy_doc("Purchase Invoice", name)
pi.db_set("release_date", release_date)
@frappe.whitelist()
def unblock_invoice(name):
def unblock_invoice(name: str):
if frappe.db.exists("Purchase Invoice", name):
pi = frappe.get_lazy_doc("Purchase Invoice", name)
pi.unblock_invoice()
@frappe.whitelist()
def block_invoice(name, release_date, hold_comment=None):
def block_invoice(name: str, release_date: str, hold_comment: str | None = None):
if frappe.db.exists("Purchase Invoice", name):
pi = frappe.get_lazy_doc("Purchase Invoice", name)
pi.block_invoice(hold_comment, release_date)
@frappe.whitelist()
def make_inter_company_sales_invoice(source_name, target_doc=None):
def make_inter_company_sales_invoice(source_name: str, target_doc: Document | None = None):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction
return make_inter_company_transaction("Purchase Invoice", source_name, target_doc)
@frappe.whitelist()
def make_purchase_receipt(source_name, target_doc=None, args=None):
def make_purchase_receipt(
source_name: str, target_doc: str | Document | None = None, args: str | dict | None = None
):
if args is None:
args = {}
if isinstance(args, str):

View File

@@ -152,7 +152,7 @@ class RepostAccountingLedger(Document):
@frappe.whitelist()
def start_repost(account_repost_doc=str) -> None:
def start_repost(account_repost_doc: str | None = None) -> None:
from erpnext.accounts.general_ledger import make_reverse_gl_entries
frappe.flags.through_repost_accounting_ledger = True
@@ -286,7 +286,9 @@ def validate_docs_for_voucher_types(doc_voucher_types):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_repost_allowed_types(doctype, txt, searchfield, start, page_len, filters):
def get_repost_allowed_types(
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict
):
filters = {"allowed": True}
if txt:

View File

@@ -21,7 +21,7 @@ def repost_ple_for_voucher(voucher_type, voucher_no, gle_map=None):
@frappe.whitelist()
def start_payment_ledger_repost(docname=None):
def start_payment_ledger_repost(docname: str | None = None):
"""
Repost Payment Ledger Entries for Vouchers through Background Job
"""
@@ -119,7 +119,7 @@ class RepostPaymentLedger(Document):
@frappe.whitelist()
def execute_repost_payment_ledger(docname):
def execute_repost_payment_ledger(docname: str):
"""Repost Payment Ledger Entries by background job."""
job_name = "payment_ledger_repost_" + docname

View File

@@ -138,7 +138,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
this.frm.add_custom_button(
__("Payment Request"),
function () {
me.make_payment_request();
me.make_payment_request_with_schedule();
},
__("Create")
);

View File

@@ -78,21 +78,23 @@
"column_break_47",
"total_taxes_and_charges",
"totals_section",
"use_company_roundoff_cost_center",
"grand_total",
"rounding_adjustment",
"in_words",
"column_break5",
"rounded_total",
"disable_rounded_total",
"total_advance",
"outstanding_amount",
"use_company_roundoff_cost_center",
"rounding_adjustment",
"rounded_total",
"base_totals_section",
"base_grand_total",
"base_rounding_adjustment",
"base_in_words",
"column_break_xjag",
"base_rounding_adjustment",
"base_rounded_total",
"section_break_vacb",
"total_advance",
"column_break_rdks",
"outstanding_amount",
"section_tax_withholding_entry",
"tax_withholding_group",
"ignore_tax_withholding_threshold",
@@ -269,6 +271,7 @@
"oldfieldtype": "Link",
"options": "Customer",
"print_hide": 1,
"reqd": 1,
"search_index": 1
},
{
@@ -797,8 +800,7 @@
"hide_seconds": 1,
"label": "Time Sheets",
"options": "Sales Invoice Timesheet",
"print_hide": 1,
"read_only": 1
"print_hide": 1
},
{
"default": "0",
@@ -2307,6 +2309,14 @@
"fieldname": "utm_analytics_section",
"fieldtype": "Section Break",
"label": "UTM Analytics"
},
{
"fieldname": "section_break_vacb",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_rdks",
"fieldtype": "Column Break"
}
],
"grid_page_length": 50,
@@ -2320,7 +2330,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2026-02-10 11:59:07.819903",
"modified": "2026-02-28 17:58:56.453076",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -6,6 +6,7 @@ import frappe
import frappe.utils
from frappe import _, msgprint, throw
from frappe.contacts.doctype.address.address import get_address_display
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.model.utils import get_fetch_values
from frappe.query_builder import Case
@@ -119,7 +120,7 @@ class SalesInvoice(SellingController):
cost_center: DF.Link | None
coupon_code: DF.Link | None
currency: DF.Link
customer: DF.Link | None
customer: DF.Link
customer_address: DF.Link | None
customer_group: DF.Link | None
customer_name: DF.SmallText | None
@@ -741,7 +742,7 @@ class SalesInvoice(SellingController):
pos_invoice_doc.cancel()
@frappe.whitelist()
def set_missing_values(self, for_validate=False):
def set_missing_values(self, for_validate: bool = False):
pos = self.set_pos_fields(for_validate)
if not self.debit_to:
@@ -1451,6 +1452,9 @@ class SalesInvoice(SellingController):
return asset_qty_map
def process_asset_depreciation(self):
if self.is_internal_transfer():
return
if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1):
self.depreciate_asset_on_sale()
else:
@@ -2409,7 +2413,7 @@ def get_list_context(context=None):
@frappe.whitelist()
def get_bank_cash_account(mode_of_payment, company):
def get_bank_cash_account(mode_of_payment: str, company: str):
account = frappe.db.get_value(
"Mode of Payment Account", {"parent": mode_of_payment, "company": company}, "default_account"
)
@@ -2424,7 +2428,7 @@ def get_bank_cash_account(mode_of_payment, company):
@frappe.whitelist()
def make_maintenance_schedule(source_name, target_doc=None):
def make_maintenance_schedule(source_name: str, target_doc: str | Document | None = None):
doclist = get_mapped_doc(
"Sales Invoice",
source_name,
@@ -2441,7 +2445,7 @@ def make_maintenance_schedule(source_name, target_doc=None):
@frappe.whitelist()
def make_delivery_note(source_name, target_doc=None):
def make_delivery_note(source_name: str, target_doc: Document | None = None):
def set_missing_values(source, target):
target.run_method("set_missing_values")
target.run_method("set_po_nos")
@@ -2490,7 +2494,7 @@ def make_delivery_note(source_name, target_doc=None):
@frappe.whitelist()
def make_sales_return(source_name, target_doc=None):
def make_sales_return(source_name: str, target_doc: Document | None = None):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
return make_return_doc("Sales Invoice", source_name, target_doc)
@@ -2584,7 +2588,7 @@ def validate_inter_company_transaction(doc, doctype):
@frappe.whitelist()
def make_inter_company_purchase_invoice(source_name, target_doc=None):
def make_inter_company_purchase_invoice(source_name: str, target_doc: Document | None = None):
return make_inter_company_transaction("Sales Invoice", source_name, target_doc)
@@ -2962,7 +2966,7 @@ def update_address(doc, address_field, address_display_field, address_name):
@frappe.whitelist()
def get_loyalty_programs(customer):
def get_loyalty_programs(customer: str):
"""sets applicable loyalty program to the customer or returns a list of applicable programs"""
from erpnext.selling.doctype.customer.customer import get_loyalty_programs
@@ -2980,7 +2984,7 @@ def get_loyalty_programs(customer):
@frappe.whitelist()
def create_invoice_discounting(source_name, target_doc=None):
def create_invoice_discounting(source_name: str, target_doc: str | Document | None = None):
invoice = frappe.get_doc("Sales Invoice", source_name)
invoice_discounting = frappe.new_doc("Invoice Discounting")
invoice_discounting.company = invoice.company
@@ -3072,7 +3076,9 @@ def get_mode_of_payment_info(mode_of_payment, company):
@frappe.whitelist()
def create_dunning(source_name, target_doc=None, ignore_permissions=False):
def create_dunning(
source_name: str, target_doc: str | Document | None = None, ignore_permissions: bool = False
):
from frappe.model.mapper import get_mapped_doc
def postprocess_dunning(source, target):

View File

@@ -843,6 +843,7 @@
"fieldtype": "Currency",
"label": "Incoming Rate (Costing)",
"no_copy": 1,
"non_negative": 1,
"options": "Company:company:default_currency",
"print_hide": 1
},
@@ -1009,7 +1010,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2026-02-15 21:08:57.341638",
"modified": "2026-02-23 14:37:14.853941",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

@@ -342,14 +342,14 @@ class ShareTransfer(Document):
@frappe.whitelist()
def make_jv_entry(
company,
account,
amount,
payment_account,
credit_applicant_type,
credit_applicant,
debit_applicant_type,
debit_applicant,
company: str,
account: str,
amount: float,
payment_account: str,
credit_applicant_type: str,
credit_applicant: str,
debit_applicant_type: str,
debit_applicant: str,
):
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Journal Entry"

View File

@@ -1,6 +1,7 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from datetime import date
import frappe
from dateutil import relativedelta
@@ -43,7 +44,13 @@ class SubscriptionPlan(Document):
@frappe.whitelist()
def get_plan_rate(
plan, quantity=1, customer=None, start_date=None, end_date=None, prorate_factor=1, party=None
plan: str,
quantity: int = 1,
customer: str | None = None,
start_date: str | date | None = None,
end_date: str | date | None = None,
prorate_factor: float = 1,
party: str | None = None,
):
plan = frappe.get_doc("Subscription Plan", plan)
if plan.price_determination == "Fixed Rate":

View File

@@ -8,6 +8,8 @@ import frappe
from frappe import _
from frappe.contacts.doctype.address.address import get_default_address
from frappe.model.document import Document
from frappe.query_builder import DocType
from frappe.query_builder.functions import IfNull
from frappe.utils import cstr
from frappe.utils.nestedset import get_root_of
@@ -83,6 +85,8 @@ class TaxRule(Document):
frappe.throw(_("Tax Template is mandatory."))
def validate_filters(self):
TaxRule = DocType("Tax Rule")
filters = {
"tax_type": self.tax_type,
"customer": self.customer,
@@ -105,37 +109,38 @@ class TaxRule(Document):
"company": self.company,
}
conds = ""
for d in filters:
if conds:
conds += " and "
conds += f"""ifnull({d}, '') = {frappe.db.escape(cstr(filters[d]))}"""
if self.from_date and self.to_date:
conds += f""" and ((from_date > '{self.from_date}' and from_date < '{self.to_date}') or
(to_date > '{self.from_date}' and to_date < '{self.to_date}') or
('{self.from_date}' > from_date and '{self.from_date}' < to_date) or
('{self.from_date}' = from_date and '{self.to_date}' = to_date))"""
elif self.from_date and not self.to_date:
conds += f""" and to_date > '{self.from_date}'"""
elif self.to_date and not self.from_date:
conds += f""" and from_date < '{self.to_date}'"""
tax_rule = frappe.db.sql(
f"select name, priority \
from `tabTax Rule` where {conds} and name != '{self.name}'",
as_dict=1,
query = (
frappe.qb.from_(TaxRule).select(TaxRule.name, TaxRule.priority).where(TaxRule.name != self.name)
)
if tax_rule:
if tax_rule[0].priority == self.priority:
frappe.throw(_("Tax Rule Conflicts with {0}").format(tax_rule[0].name), ConflictingTaxRule)
for field, value in filters.items():
query = query.where(IfNull(TaxRule[field], "") == cstr(value))
if self.from_date and self.to_date:
query = query.where(
((TaxRule.from_date > self.from_date) & (TaxRule.from_date < self.to_date))
| ((TaxRule.to_date > self.from_date) & (TaxRule.to_date < self.to_date))
| ((self.from_date > TaxRule.from_date) & (self.from_date < TaxRule.to_date))
| ((TaxRule.from_date == self.from_date) & (TaxRule.to_date == self.to_date))
)
elif self.from_date:
query = query.where(TaxRule.to_date > self.from_date)
elif self.to_date:
query = query.where(TaxRule.from_date < self.to_date)
tax_rule = query.run(as_dict=True)
if tax_rule and tax_rule[0].priority == self.priority:
frappe.throw(
_("Tax Rule Conflicts with {0}").format(tax_rule[0].name),
ConflictingTaxRule,
)
@frappe.whitelist()
def get_party_details(party, party_type, args=None):
def get_party_details(party: str | None, party_type: str, args: dict | None = None):
out = {}
billing_address, shipping_address = None, None
if args:

View File

@@ -194,7 +194,7 @@ def get_linked_advances(company, docname):
@frappe.whitelist()
def create_unreconcile_doc_for_selection(selections=None):
def create_unreconcile_doc_for_selection(selections: str | None = None):
if selections:
selections = json.loads(selections)
# assuming each row is a unique voucher

View File

@@ -0,0 +1,41 @@
{
"allow_roles": [
{
"role": "Accounts Manager"
},
{
"role": "Accounts User"
}
],
"creation": "2026-02-22 18:26:42.015787",
"docstatus": 0,
"doctype": "Module Onboarding",
"idx": 4,
"is_complete": 0,
"modified": "2026-02-23 22:51:34.267812",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting Onboarding",
"owner": "Administrator",
"steps": [
{
"step": "Chart of Accounts"
},
{
"step": "Setup Sales taxes"
},
{
"step": "Create Sales Invoice"
},
{
"step": "Create Payment Entry"
},
{
"step": "View Balance Sheet"
},
{
"step": "Review Accounts Settings"
}
],
"title": "Accounting Onboarding"
}

View File

@@ -1,3 +1,43 @@
<h3>{{ _("Fiscal Year") }}</h3>
<h4>{{ _("New Fiscal Year - {0}").format(doc.name) }}</h4>
<p>{{ _("New fiscal year created :- ") }} {{ doc.name }}</p>
<p>{{ _("A new fiscal year has been automatically created.") }}</p>
<p>{{ _("Fiscal Year Details") }}</p>
<table style="margin-bottom: 1rem; width: 70%">
<tr>
<td style="font-weight:bold; width: 40%">{{ _("Year Name") }}</td>
<td>{{ doc.name }}</td>
</tr>
<tr>
<td style="font-weight:bold; width: 40%">{{ _("Start Date") }}</td>
<td>{{ frappe.format_value(doc.year_start_date) }}</td>
</tr>
<tr>
<td style="font-weight:bold; width: 40%">{{ _("End Date") }}</td>
<td>{{ frappe.format_value(doc.year_end_date) }}</td>
</tr>
{% if doc.companies|length > 0 %}
<tr>
<td style="vertical-align: top; font-weight: bold; width: 40%" rowspan="{{ doc.companies|length }}">
{% if doc.companies|length < 2 %}
{{ _("Company") }}
{% else %}
{{ _("Companies") }}
{% endif %}
</td>
<td>{{ doc.companies[0].company }}</td>
</tr>
{% for idx in range(1, doc.companies|length) %}
<tr>
<td>{{ doc.companies[idx].company }}</td>
</tr>
{% endfor %}
{% endif %}
</table>
{% if doc.disabled %}
<p>{{ _("The fiscal year has been automatically created in a Disabled state to maintain consistency with the previous fiscal year's status.") }}</p>
{% endif %}
<p>{{ _("Please review the {0} configuration and complete any required financial setup activities.").format(frappe.utils.get_link_to_form("Fiscal Year", doc.name, frappe.bold("Fiscal Year"))) }}</p>

View File

@@ -1,7 +1,8 @@
{
"attach_print": 0,
"channel": "Email",
"condition": "doc.auto_created",
"condition": "doc.auto_created == 1",
"condition_type": "Python",
"creation": "2018-04-25 14:19:05.440361",
"days_in_advance": 0,
"docstatus": 0,
@@ -11,8 +12,10 @@
"event": "New",
"idx": 0,
"is_standard": 1,
"message": "<h4>{{ _(\"New Fiscal Year - {0}\").format(doc.name) }}</h4>\n\n<p>{{ _(\"A new fiscal year has been automatically created.\") }}</p>\n\n<p>{{ _(\"Fiscal Year Details\") }}</p>\n\n<table style=\"margin-bottom: 1rem; width: 70%\">\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"Year Name\") }}</td>\n <td>{{ doc.name }}</td>\n </tr>\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"Start Date\") }}</td>\n <td>{{ frappe.format_value(doc.year_start_date) }}</td>\n </tr>\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"End Date\") }}</td>\n <td>{{ frappe.format_value(doc.year_end_date) }}</td>\n </tr>\n {% if doc.companies|length > 0 %}\n <tr>\n <td style=\"vertical-align: top; font-weight: bold; width: 40%\" rowspan=\"{{ doc.companies|length }}\">\n {% if doc.companies|length < 2 %}\n {{ _(\"Company\") }}\n {% else %}\n {{ _(\"Companies\") }}\n {% endif %}\n </td>\n <td>{{ doc.companies[0].company }}</td>\n </tr>\n {% for idx in range(1, doc.companies|length) %}\n <tr>\n <td>{{ doc.companies[idx].company }}</td>\n </tr>\n {% endfor %}\n {% endif %}\n</table>\n\n{% if doc.disabled %}\n<p>{{ _(\"The fiscal year has been automatically created in a Disabled state to maintain consistency with the previous fiscal year's status.\") }}</p>\n{% endif %}\n\n<p>{{ _(\"Please review the {0} configuration and complete any required financial setup activities.\").format(frappe.utils.get_link_to_form(\"Fiscal Year\", doc.name, frappe.bold(\"Fiscal Year\"))) }}</p>",
"message_type": "HTML",
"modified": "2023-11-17 08:54:51.532104",
"minutes_offset": 0,
"modified": "2026-02-23 17:37:03.755394",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Notification for new fiscal year",
@@ -27,5 +30,5 @@
],
"send_system_notification": 0,
"send_to_all_assignees": 0,
"subject": "Notification for new fiscal year {{ doc.name }}"
"subject": "New Fiscal Year {{ doc.name }} - Review Required"
}

View File

@@ -0,0 +1,20 @@
{
"action": "Go to Page",
"action_label": "Configure Chart of Accounts",
"creation": "2026-02-22 18:28:15.401383",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 1,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 22:44:45.540780",
"modified_by": "Administrator",
"name": "Chart of Accounts",
"owner": "Administrator",
"path": "Tree/Account",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Review Chart of Accounts",
"validate_action": 1
}

View File

@@ -0,0 +1,21 @@
{
"action": "Create Entry",
"action_label": "Create Payment Entry",
"creation": "2026-02-23 19:22:12.005360",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 20:19:56.482245",
"modified_by": "Administrator",
"name": "Create Payment Entry",
"owner": "Administrator",
"reference_document": "Payment Entry",
"route_options": "{\n \"payment_type\": \"Receive\",\n \"party_type\": \"Customer\"\n}",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Create Payment Entry",
"validate_action": 1
}

View File

@@ -0,0 +1,20 @@
{
"action": "Create Entry",
"action_label": "Create Sales Invoice",
"creation": "2026-02-20 13:42:38.439574",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 2,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 22:16:40.931428",
"modified_by": "Administrator",
"name": "Create Sales Invoice",
"owner": "Administrator",
"reference_document": "Sales Invoice",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Create Sales Invoice",
"validate_action": 1
}

View File

@@ -0,0 +1,21 @@
{
"action": "Update Settings",
"action_label": "Review Accounts Settings",
"creation": "2026-02-23 19:27:06.055104",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 1,
"is_skipped": 0,
"modified": "2026-02-23 22:16:40.855407",
"modified_by": "Administrator",
"name": "Review Accounts Settings",
"owner": "Administrator",
"path": "desk/accounts-settings",
"reference_document": "Accounts Settings",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Review Accounts Settings",
"validate_action": 0
}

View File

@@ -0,0 +1,22 @@
{
"action": "Go to Page",
"action_label": "Setup Sales Taxes",
"creation": "2026-02-22 18:30:18.750391",
"docstatus": 0,
"doctype": "Onboarding Step",
"form_tour": "",
"idx": 1,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 22:44:42.373227",
"modified_by": "Administrator",
"name": "Setup Sales taxes",
"owner": "Administrator",
"path": "/desk/sales-taxes-and-charges-template",
"reference_document": "Sales Taxes and Charges Template",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Setup Sales taxes",
"validate_action": 1
}

View File

@@ -0,0 +1,23 @@
{
"action": "View Report",
"action_label": "View Balance Sheet",
"creation": "2026-02-23 19:22:57.651194",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 22:44:39.178107",
"modified_by": "Administrator",
"name": "View Balance Sheet",
"owner": "Administrator",
"reference_report": "Balance Sheet",
"report_description": "View Balance Sheet",
"report_reference_doctype": "GL Entry",
"report_type": "Script Report",
"show_form_tour": 0,
"show_full_form": 0,
"title": "View Balance Sheet",
"validate_action": 1
}

View File

@@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from datetime import date
import frappe
from frappe import _, msgprint, qb, scrub
@@ -55,22 +56,22 @@ class DuplicatePartyAccountError(frappe.ValidationError):
@frappe.whitelist()
def get_party_details(
party=None,
account=None,
party_type="Customer",
company=None,
posting_date=None,
bill_date=None,
price_list=None,
currency=None,
doctype=None,
ignore_permissions=False,
fetch_payment_terms_template=True,
party_address=None,
company_address=None,
shipping_address=None,
dispatch_address=None,
pos_profile=None,
party: str | None = None,
account: str | None = None,
party_type: str = "Customer",
company: str | None = None,
posting_date: str | None = None,
bill_date: str | None = None,
price_list: str | None = None,
currency: str | None = None,
doctype: str | None = None,
ignore_permissions: bool | None = False,
fetch_payment_terms_template: bool = True,
party_address: str | None = None,
company_address: str | None = None,
shipping_address: str | None = None,
dispatch_address: str | None = None,
pos_profile: str | None = None,
):
if not party:
return frappe._dict()
@@ -296,19 +297,9 @@ def complete_contact_details(party_details):
contact_details = frappe._dict()
if party_details.party_type == "Employee":
contact_details = frappe.db.get_value(
"Employee",
party_details.party,
[
"employee_name as contact_display",
"prefered_email as contact_email",
"cell_number as contact_mobile",
"designation as contact_designation",
"department as contact_department",
],
as_dict=True,
)
from erpnext.setup.doctype.employee.employee import _get_contact_details as get_employee_contact
contact_details = get_employee_contact(party_details.party)
contact_details.update({"contact_person": None, "contact_phone": None})
elif party_details.contact_person:
contact_details = frappe.db.get_value(
@@ -416,7 +407,9 @@ def set_account_and_due_date(party, account, party_type, company, posting_date,
@frappe.whitelist()
def get_party_account(party_type, party=None, company=None, include_advance=False):
def get_party_account(
party_type: str, party: str | None = None, company: str | None = None, include_advance: bool = False
):
"""Returns the account for the given `party`.
Will first search in party (Customer / Supplier) record, if not found,
will search in group (Customer Group / Supplier Group),
@@ -501,7 +494,7 @@ def get_party_advance_account(party_type, party, company):
@frappe.whitelist()
def get_party_bank_account(party_type, party):
def get_party_bank_account(party_type: str, party: str):
return frappe.db.get_value("Bank Account", {"party_type": party_type, "party": party, "is_default": 1})
@@ -619,7 +612,14 @@ def validate_party_accounts(doc):
@frappe.whitelist()
def get_due_date(posting_date, party_type, party, company=None, bill_date=None, template_name=None):
def get_due_date(
posting_date: str | date | None,
party_type: str | None,
party: str | None,
company: str | None = None,
bill_date: str | None = None,
template_name: str | None = None,
):
"""Get due date from `Payment Terms Template`"""
due_date = None
if (bill_date or posting_date) and party:
@@ -701,7 +701,9 @@ def validate_due_date_with_template(posting_date, due_date, bill_date, template_
@frappe.whitelist()
def get_address_tax_category(tax_category=None, billing_address=None, shipping_address=None):
def get_address_tax_category(
tax_category: str | None = None, billing_address: str | None = None, shipping_address: str | None = None
):
addr_tax_category_from = frappe.get_single_value(
"Accounts Settings", "determine_address_tax_category_from"
)
@@ -717,16 +719,16 @@ def get_address_tax_category(tax_category=None, billing_address=None, shipping_a
@frappe.whitelist()
def set_taxes(
party,
party_type,
posting_date,
company,
customer_group=None,
supplier_group=None,
tax_category=None,
billing_address=None,
shipping_address=None,
use_for_shopping_cart=None,
party: str | None,
party_type: str,
posting_date: str | date | None,
company: str | None,
customer_group: str | None = None,
supplier_group: str | None = None,
tax_category: str | None = None,
billing_address: str | None = None,
shipping_address: str | None = None,
use_for_shopping_cart: int | None = None,
):
from erpnext.accounts.doctype.tax_rule.tax_rule import get_party_details, get_tax_template
@@ -766,7 +768,7 @@ def set_taxes(
@frappe.whitelist()
def get_payment_terms_template(party_name, party_type, company=None):
def get_payment_terms_template(party_name: str, party_type: str, company: str | None = None):
if party_type not in ("Customer", "Supplier"):
return
template = None

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import today
from frappe.utils import add_days, today
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.report.accounts_payable.accounts_payable import execute
@@ -57,3 +57,66 @@ class TestAccountsPayable(AccountsTestMixin, IntegrationTestCase):
if not do_not_submit:
pi = pi.submit()
return pi
def test_payment_terms_template_filters(self):
from erpnext.controllers.accounts_controller import get_payment_terms
payment_term1 = frappe.get_doc(
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 15 Days"}
).insert()
payment_term2 = frappe.get_doc(
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 30 Days"}
).insert()
template = frappe.get_doc(
{
"doctype": "Payment Terms Template",
"template_name": "_Test 50-50",
"terms": [
{
"doctype": "Payment Terms Template Detail",
"due_date_based_on": "Day(s) after invoice date",
"payment_term": payment_term1.name,
"description": "_Test 50-50",
"invoice_portion": 50,
"credit_days": 15,
},
{
"doctype": "Payment Terms Template Detail",
"due_date_based_on": "Day(s) after invoice date",
"payment_term": payment_term2.name,
"description": "_Test 50-50",
"invoice_portion": 50,
"credit_days": 30,
},
],
}
)
template.insert()
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
"based_on_payment_terms": 1,
"payment_terms_template": template.name,
"ageing_based_on": "Posting Date",
}
pi = self.create_purchase_invoice(do_not_submit=True)
pi.payment_terms_template = template.name
schedule = get_payment_terms(template.name)
pi.set("payment_schedule", [])
for row in schedule:
row["due_date"] = add_days(pi.posting_date, row.get("credit_days", 0))
pi.append("payment_schedule", row)
pi.save()
pi.submit()
report = execute(filters)
row = report[1][0]
self.assertEqual(len(report[1]), 2)
self.assertEqual([pi.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])

View File

@@ -1035,9 +1035,8 @@ class ReceivablePayableReport:
self,
):
self.customer = qb.DocType("Customer")
if self.filters.get("customer_group"):
groups = get_customer_group_with_children(self.filters.customer_group)
groups = get_party_group_with_children("Customer", self.filters.customer_group)
customers = (
qb.from_(self.customer)
.select(self.customer.name)
@@ -1049,14 +1048,18 @@ class ReceivablePayableReport:
self.get_hierarchical_filters("Territory", "territory")
if self.filters.get("payment_terms_template"):
self.qb_selection_filter.append(
self.ple.party.isin(
qb.from_(self.customer)
.select(self.customer.name)
.where(self.customer.payment_terms == self.filters.get("payment_terms_template"))
)
customer_ptt = self.ple.party.isin(
qb.from_(self.customer)
.select(self.customer.name)
.where(self.customer.payment_terms == self.filters.get("payment_terms_template"))
)
si_ptt = self.add_payment_term_template_filters("Sales Invoice")
sales_ptt = self.ple.against_voucher_no.isin(si_ptt)
self.qb_selection_filter.append(Criterion.any([customer_ptt, sales_ptt]))
if self.filters.get("sales_partner"):
self.qb_selection_filter.append(
self.ple.party.isin(
@@ -1081,14 +1084,53 @@ class ReceivablePayableReport:
)
if self.filters.get("payment_terms_template"):
self.qb_selection_filter.append(
self.ple.party.isin(
qb.from_(supplier)
.select(supplier.name)
.where(supplier.payment_terms == self.filters.get("supplier_group"))
)
supplier_ptt = self.ple.party.isin(
qb.from_(supplier)
.select(supplier.name)
.where(supplier.payment_terms == self.filters.get("payment_terms_template"))
)
pi_ptt = self.add_payment_term_template_filters("Purchase Invoice")
purchase_ptt = self.ple.against_voucher_no.isin(pi_ptt)
self.qb_selection_filter.append(Criterion.any([supplier_ptt, purchase_ptt]))
def add_payment_term_template_filters(self, dtype):
voucher_type = qb.DocType(dtype)
ptt = (
qb.from_(voucher_type)
.select(voucher_type.name)
.where(voucher_type.payment_terms_template == self.filters.get("payment_terms_template"))
.where(voucher_type.company == self.filters.company)
)
if dtype == "Purchase Invoice":
party = "Supplier"
party_group_type = "supplier_group"
acc_type = "credit_to"
else:
party = "Customer"
party_group_type = "customer_group"
acc_type = "debit_to"
if self.filters.get(party_group_type):
party_groups = get_party_group_with_children(party, self.filters.get(party_group_type))
ptt = ptt.where((voucher_type[party_group_type]).isin(party_groups))
if self.filters.party:
ptt = ptt.where((voucher_type[party.lower()]).isin(self.filters.party))
if self.filters.cost_center:
cost_centers = get_cost_centers_with_children(self.filters.cost_center)
ptt = ptt.where(voucher_type.cost_center.isin(cost_centers))
if self.filters.party_account:
ptt = ptt.where(voucher_type[acc_type] == self.filters.party_account)
return ptt
def get_hierarchical_filters(self, doctype, key):
lft, rgt = frappe.db.get_value(doctype, self.filters.get(key), ["lft", "rgt"])
@@ -1330,20 +1372,26 @@ class ReceivablePayableReport:
self.err_journals = [x[0] for x in results] if results else []
def get_customer_group_with_children(customer_groups):
if not isinstance(customer_groups, list):
customer_groups = [d.strip() for d in customer_groups.strip().split(",") if d]
def get_party_group_with_children(party, party_groups):
if party not in ("Customer", "Supplier"):
return []
all_customer_groups = []
for d in customer_groups:
if frappe.db.exists("Customer Group", d):
lft, rgt = frappe.db.get_value("Customer Group", d, ["lft", "rgt"])
children = frappe.get_all("Customer Group", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
all_customer_groups += [c.name for c in children]
group_dtype = f"{party} Group"
if not isinstance(party_groups, list):
party_groups = [d.strip() for d in party_groups.strip().split(",") if d]
all_party_groups = []
for d in party_groups:
if frappe.db.exists(group_dtype, d):
lft, rgt = frappe.db.get_value(group_dtype, d, ["lft", "rgt"])
children = frappe.get_all(
group_dtype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, pluck="name"
)
all_party_groups += children
else:
frappe.throw(_("Customer Group: {0} does not exist").format(d))
frappe.throw(_("{0}: {1} does not exist").format(group_dtype, d))
return list(set(all_customer_groups))
return list(set(all_party_groups))
class InitSQLProceduresForAR:

View File

@@ -1139,3 +1139,66 @@ class TestAccountsReceivable(AccountsTestMixin, IntegrationTestCase):
self.assertEqual(len(report[1]), 1)
row = report[1][0]
self.assertEqual(expected_data_after_payment, [row.voucher_no, row.cost_center, row.outstanding])
def test_payment_terms_template_filters(self):
from erpnext.controllers.accounts_controller import get_payment_terms
payment_term1 = frappe.get_doc(
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 15 Days"}
).insert()
payment_term2 = frappe.get_doc(
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 30 Days"}
).insert()
template = frappe.get_doc(
{
"doctype": "Payment Terms Template",
"template_name": "_Test 50-50",
"terms": [
{
"doctype": "Payment Terms Template Detail",
"due_date_based_on": "Day(s) after invoice date",
"payment_term": payment_term1.name,
"description": "_Test 50-50",
"invoice_portion": 50,
"credit_days": 15,
},
{
"doctype": "Payment Terms Template Detail",
"due_date_based_on": "Day(s) after invoice date",
"payment_term": payment_term2.name,
"description": "_Test 50-50",
"invoice_portion": 50,
"credit_days": 30,
},
],
}
)
template.insert()
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
"based_on_payment_terms": 1,
"payment_terms_template": template.name,
"ageing_based_on": "Posting Date",
}
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.payment_terms_template = template.name
schedule = get_payment_terms(template.name)
si.set("payment_schedule", [])
for row in schedule:
row["due_date"] = add_days(si.posting_date, row.get("credit_days", 0))
si.append("payment_schedule", row)
si.save()
si.submit()
report = execute(filters)
row = report[1][0]
self.assertEqual(len(report[1]), 2)
self.assertEqual([si.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])

View File

@@ -102,7 +102,7 @@ def execute(filters=None):
filters.periodicity, period_list, filters.accumulated_values, company=filters.company
)
chart = get_chart_data(filters, columns, asset, liability, equity, currency)
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
@@ -231,18 +231,19 @@ def get_report_summary(
], (net_asset - net_liability + net_equity)
def get_chart_data(filters, columns, asset, liability, equity, currency):
labels = [d.get("label") for d in columns[4:]]
def get_chart_data(filters, chart_columns, asset, liability, equity, currency):
labels = [col.get("label") for col in chart_columns]
asset_data, liability_data, equity_data = [], [], []
for p in columns[4:]:
for col in chart_columns:
key = col.get("key") or col.get("fieldname")
if asset:
asset_data.append(asset[-2].get(p.get("fieldname")))
asset_data.append(asset[-2].get(key))
if liability:
liability_data.append(liability[-2].get(p.get("fieldname")))
liability_data.append(liability[-2].get(key))
if equity:
equity_data.append(equity[-2].get(p.get("fieldname")))
equity_data.append(equity[-2].get(key))
datasets = []
if asset_data:

View File

@@ -5,6 +5,7 @@ import frappe
from frappe import _
from frappe.utils import add_months, flt, formatdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.accounts.utils import get_fiscal_year
from erpnext.controllers.trends import get_period_date_ranges
@@ -13,6 +14,8 @@ def execute(filters=None):
if not filters:
filters = {}
validate_filters(filters)
columns = get_columns(filters)
if filters.get("budget_against_filter"):
dimensions = filters.get("budget_against_filter")
@@ -31,6 +34,10 @@ def execute(filters=None):
return columns, data, None, chart_data
def validate_filters(filters):
validate_budget_dimensions(filters)
def get_budget_records(filters, dimensions):
budget_against_field = frappe.scrub(filters["budget_against"])
@@ -51,7 +58,7 @@ def get_budget_records(filters, dimensions):
b.company = %s
AND b.docstatus = 1
AND b.budget_against = %s
AND b.{budget_against_field} IN ({', '.join(['%s'] * len(dimensions))})
AND b.{budget_against_field} IN ({", ".join(["%s"] * len(dimensions))})
AND (
b.from_fiscal_year <= %s
AND b.to_fiscal_year >= %s
@@ -404,6 +411,17 @@ def get_budget_dimensions(filters):
) # nosec
def validate_budget_dimensions(filters):
dimensions = [d.get("document_type") for d in get_dimensions(with_cost_center_and_project=True)[0]]
if filters.get("budget_against") and filters.get("budget_against") not in dimensions:
frappe.throw(
title=_("Invalid Accounting Dimension"),
msg=_("{0} is not a valid Accounting Dimension.").format(
frappe.bold(filters.get("budget_against"))
),
)
def build_comparison_chart_data(filters, columns, data):
if not data:
return None

View File

@@ -145,7 +145,7 @@ def execute(filters=None):
True,
)
chart = get_chart_data(columns, data, company_currency)
chart = get_chart_data(period_list, data, company_currency)
report_summary = get_report_summary(summary_data, company_currency)
@@ -417,12 +417,12 @@ def get_report_summary(summary_data, currency):
return report_summary
def get_chart_data(columns, data, currency):
labels = [d.get("label") for d in columns[2:]]
def get_chart_data(period_list, data, currency):
labels = [period.get("label") for period in period_list]
datasets = [
{
"name": section.get("section").replace("'", ""),
"values": [section.get(d.get("fieldname")) for d in columns[2:]],
"values": [section.get(period.get("key")) for period in period_list],
}
for section in data
if section.get("parent_section") is None and section.get("currency")

View File

@@ -48,22 +48,25 @@ def execute(filters=None):
return columns, data, message, chart
fiscal_year = get_fiscal_year_data(filters.get("from_fiscal_year"), filters.get("to_fiscal_year"))
companies_column, companies = get_companies(filters)
columns = get_columns(companies_column, filters)
company_list, companies = get_companies(filters)
company_columns = get_company_columns(company_list, filters)
columns = get_columns(company_columns)
if filters.get("report") == "Balance Sheet":
data, message, chart, report_summary = get_balance_sheet_data(
fiscal_year, companies, columns, filters
fiscal_year, companies, company_columns, filters
)
elif filters.get("report") == "Profit and Loss Statement":
data, message, chart, report_summary = get_profit_loss_data(fiscal_year, companies, columns, filters)
data, message, chart, report_summary = get_profit_loss_data(
fiscal_year, companies, company_columns, filters
)
else:
data, report_summary = get_cash_flow_data(fiscal_year, companies, filters)
return columns, data, message, chart, report_summary
def get_balance_sheet_data(fiscal_year, companies, columns, filters):
def get_balance_sheet_data(fiscal_year, companies, company_columns, filters):
asset = get_data(companies, "Asset", "Debit", fiscal_year, filters=filters)
liability = get_data(companies, "Liability", "Credit", fiscal_year, filters=filters)
@@ -116,7 +119,7 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
True,
)
chart = get_chart_data(filters, columns, asset, liability, equity, company_currency)
chart = get_chart_data(filters, company_columns, asset, liability, equity, company_currency)
return data, message, chart, report_summary
@@ -164,7 +167,7 @@ def get_root_account_name(root_type, company):
return root_account[0][0]
def get_profit_loss_data(fiscal_year, companies, columns, filters):
def get_profit_loss_data(fiscal_year, companies, company_columns, filters):
income, expense, net_profit_loss = get_income_expense_data(companies, fiscal_year, filters)
company_currency = get_company_currency(filters)
@@ -174,7 +177,7 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters):
if net_profit_loss:
data.append(net_profit_loss)
chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss, company_currency)
chart = get_pl_chart_data(filters, company_columns, income, expense, net_profit_loss, company_currency)
report_summary, primitive_summary = get_pl_summary(
companies, "", income, expense, net_profit_loss, company_currency, filters, True
@@ -280,7 +283,30 @@ def get_account_type_based_data(account_type, companies, fiscal_year, filters):
return data
def get_columns(companies, filters):
def get_company_columns(companies, filters):
company_columns = []
for company in companies:
apply_currency_formatter = 1 if not filters.presentation_currency else 0
currency = filters.presentation_currency
if not currency:
currency = erpnext.get_company_currency(company)
company_columns.append(
{
"fieldname": company,
"label": f"{company} ({currency})",
"fieldtype": "Currency",
"options": "currency",
"width": 150,
"apply_currency_formatter": apply_currency_formatter,
"company_name": company,
}
)
return company_columns
def get_columns(company_columns):
columns = [
{
"fieldname": "account",
@@ -298,23 +324,7 @@ def get_columns(companies, filters):
},
]
for company in companies:
apply_currency_formatter = 1 if not filters.presentation_currency else 0
currency = filters.presentation_currency
if not currency:
currency = erpnext.get_company_currency(company)
columns.append(
{
"fieldname": company,
"label": f"{company} ({currency})",
"fieldtype": "Currency",
"options": "currency",
"width": 150,
"apply_currency_formatter": apply_currency_formatter,
"company_name": company,
}
)
columns.extend(company_columns)
return columns
@@ -646,7 +656,11 @@ def set_gl_entries_by_account(
query = query.where(Criterion.all(additional_conditions))
gl_entries = query.run(as_dict=True)
if filters and filters.get("presentation_currency") != d.default_currency:
if (
filters
and filters.get("presentation_currency")
and filters.get("presentation_currency") != d.default_currency
):
currency_info["company"] = d.name
currency_info["company_currency"] = d.default_currency
convert_to_presentation_currency(gl_entries, currency_info)

View File

@@ -86,6 +86,12 @@ frappe.query_reports["Consolidated Trial Balance"] = {
fieldtype: "Check",
default: 1,
},
{
fieldname: "show_net_values",
label: __("Show net values in opening and closing columns"),
fieldtype: "Check",
default: 1,
},
{
fieldname: "show_group_accounts",
label: __("Show Group Accounts"),

View File

@@ -14,6 +14,7 @@ from erpnext.accounts.report.financial_statements import (
)
from erpnext.accounts.report.trial_balance.trial_balance import (
accumulate_values_into_parents,
calculate_total_row,
calculate_values,
get_opening_balances,
hide_group_accounts,
@@ -44,7 +45,6 @@ def execute(filters: dict | None = None):
def validate_filters(filters):
validate_companies(filters)
filters.show_net_values = True
tb_validate_filters(filters)
@@ -99,16 +99,20 @@ def get_data(filters) -> list[list]:
tb_data = get_company_wise_tb_data(company_filter, reporting_currency, ignore_reporting_currency)
consolidate_trial_balance_data(data, tb_data)
for d in data:
prepare_opening_closing(d)
total_row = calculate_total_row(data, reporting_currency)
data.extend([{}, total_row])
if filters.get("show_net_values"):
prepare_opening_closing_for_ctb(data)
if not filters.get("show_group_accounts"):
data = hide_group_accounts(data)
total_row = calculate_total_row(
data, reporting_currency, show_group_accounts=filters.get("show_group_accounts")
)
calculate_foreign_currency_translation_reserve(total_row, data, filters=filters)
data.extend([total_row])
if filters.get("presentation_currency"):
update_to_presentation_currency(
data,
@@ -207,10 +211,6 @@ def prepare_companywise_tb_data(accounts, filters, parent_children_map, reportin
data = []
for d in accounts:
# Prepare opening closing for group account
if parent_children_map.get(d.account) and filters.get("show_net_values"):
prepare_opening_closing(d)
has_value = False
row = {
"account": d.name,
@@ -242,35 +242,9 @@ def prepare_companywise_tb_data(accounts, filters, parent_children_map, reportin
return data
def calculate_total_row(data, reporting_currency):
total_row = {
"account": "'" + _("Total") + "'",
"account_name": "'" + _("Total") + "'",
"warn_if_negative": True,
"opening_debit": 0.0,
"opening_credit": 0.0,
"debit": 0.0,
"credit": 0.0,
"closing_debit": 0.0,
"closing_credit": 0.0,
"parent_account": None,
"indent": 0,
"has_value": True,
"currency": reporting_currency,
}
for d in data:
if not d.get("parent_account"):
for field in value_fields:
total_row[field] += d[field]
if data:
calculate_foreign_currency_translation_reserve(total_row, data)
return total_row
def calculate_foreign_currency_translation_reserve(total_row, data):
def calculate_foreign_currency_translation_reserve(total_row, data, filters):
if not data or not total_row:
return
opening_dr_cr_diff = total_row["opening_debit"] - total_row["opening_credit"]
dr_cr_diff = total_row["debit"] - total_row["credit"]
@@ -289,7 +263,7 @@ def calculate_foreign_currency_translation_reserve(total_row, data):
"root_type": data[idx].get("root_type"),
"account_type": "Equity",
"parent_account": data[idx].get("account"),
"indent": data[idx].get("indent") + 1,
"indent": data[idx].get("indent") + 1 if filters.get("show_group_accounts") else 0,
"has_value": True,
"currency": total_row.get("currency"),
}
@@ -297,7 +271,8 @@ def calculate_foreign_currency_translation_reserve(total_row, data):
fctr_row["closing_debit"] = fctr_row["opening_debit"] + fctr_row["debit"]
fctr_row["closing_credit"] = fctr_row["opening_credit"] + fctr_row["credit"]
prepare_opening_closing(fctr_row)
if filters.get("show_net_values"):
prepare_opening_closing(fctr_row)
data.insert(idx + 1, fctr_row)
@@ -396,6 +371,11 @@ def update_to_presentation_currency(data, from_currency, to_currency, date, igno
d.update(currency=to_currency)
def prepare_opening_closing_for_ctb(data):
for d in data:
prepare_opening_closing(d)
def get_columns():
return [
{

View File

@@ -8,7 +8,7 @@ from frappe.query_builder import Criterion, Tuple
from frappe.query_builder.functions import IfNull
from frappe.utils import getdate, nowdate
from frappe.utils.nestedset import get_descendants_of
from pypika.terms import LiteralValue
from pypika.terms import Bracket, LiteralValue
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
@@ -84,10 +84,8 @@ class PartyLedgerSummaryReport:
from frappe.desk.reportview import build_match_conditions
match_conditions = build_match_conditions(party_type)
if match_conditions:
query = query.where(LiteralValue(match_conditions))
if match_conditions := build_match_conditions(party_type):
query = query.where(Bracket(LiteralValue(match_conditions)))
party_details = query.run(as_dict=True)

View File

@@ -11,7 +11,7 @@ import frappe
from frappe import _
from frappe.query_builder.functions import Max, Min, Sum
from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
from pypika.terms import ExistsCriterion
from pypika.terms import Bracket, ExistsCriterion, LiteralValue
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
@@ -564,18 +564,15 @@ def get_accounting_entries(
account_filter_query = get_account_filter_query(root_lft, root_rgt, root_type, gl_entry)
query = query.where(ExistsCriterion(account_filter_query))
if group_by_account:
query = query.groupby("account")
from frappe.desk.reportview import build_match_conditions
query, params = query.walk()
match_conditions = build_match_conditions(doctype)
if match_conditions := build_match_conditions(doctype):
query = query.where(Bracket(LiteralValue(match_conditions)))
if match_conditions:
query += "and" + match_conditions
if group_by_account:
query += " GROUP BY `account`"
return frappe.db.sql(query, params, as_dict=True)
return query.run(as_dict=True)
def get_account_filter_query(root_lft, root_rgt, root_type, gl_entry):

View File

@@ -37,6 +37,20 @@ function get_filters() {
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),
fieldtype: "Link",
options: "Party Type",
width: 100,
},
{
fieldname: "party",
label: __("Party"),
fieldtype: "Dynamic Link",
options: "party_type",
width: 100,
},
{
fieldname: "voucher_no",
label: __("Voucher No"),

View File

@@ -68,6 +68,12 @@ class General_Payment_Ledger_Comparison:
if self.filters.period_end_date:
filter_criterion.append(gle.posting_date.lte(self.filters.period_end_date))
if self.filters.party_type:
filter_criterion.append(gle.party_type.eq(self.filters.party_type))
if self.filters.party:
filter_criterion.append(gle.party.eq(self.filters.party))
if acc_type == "receivable":
outstanding = (Sum(gle.debit) - Sum(gle.credit)).as_("outstanding")
else:
@@ -111,6 +117,12 @@ class General_Payment_Ledger_Comparison:
if self.filters.period_end_date:
filter_criterion.append(ple.posting_date.lte(self.filters.period_end_date))
if self.filters.party_type:
filter_criterion.append(ple.party_type.eq(self.filters.party_type))
if self.filters.party:
filter_criterion.append(ple.party.eq(self.filters.party))
self.account_types[acc_type].ple = (
qb.from_(ple)
.select(

View File

@@ -324,10 +324,8 @@ def get_conditions(filters):
from frappe.desk.reportview import build_match_conditions
match_conditions = build_match_conditions("GL Entry")
if match_conditions:
conditions.append(match_conditions)
if match_conditions := build_match_conditions("GL Entry"):
conditions.append(f"({match_conditions})")
accounting_dimensions = get_accounting_dimensions(as_list=False)

View File

@@ -444,6 +444,7 @@ class TestGrossProfit(IntegrationTestCase):
qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
sinv.is_return = 1
sinv.items[0].allow_zero_valuation_rate = 1
sinv = sinv.save().submit()
filters = frappe._dict(

View File

@@ -5,6 +5,7 @@
import frappe
from frappe import _
from frappe.utils import flt
from pypika.terms import Bracket, LiteralValue
import erpnext
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
@@ -361,15 +362,12 @@ def get_items(filters, additional_table_columns):
from frappe.desk.reportview import build_match_conditions
query, params = query.walk()
match_conditions = build_match_conditions(doctype)
if match_conditions:
query += " and " + match_conditions
if match_conditions := build_match_conditions(doctype):
query = query.where(Bracket(LiteralValue(match_conditions)))
query = apply_order_by_conditions(doctype, query, filters)
return frappe.db.sql(query, params, as_dict=True)
return query.run(as_dict=True)
def get_aii_accounts():

View File

@@ -8,6 +8,7 @@ from frappe.query_builder import functions as fn
from frappe.utils import flt
from frappe.utils.nestedset import get_descendants_of
from frappe.utils.xlsxutils import handle_html
from pypika.terms import Bracket, LiteralValue, Order
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
from erpnext.accounts.report.utils import get_values_for_columns
@@ -390,20 +391,21 @@ def apply_conditions(query, si, sii, sip, filters, additional_conditions=None):
def apply_order_by_conditions(doctype, query, filters):
invoice = f"`tab{doctype}`"
invoice_item = f"`tab{doctype} Item`"
invoice = frappe.qb.DocType(doctype)
invoice_item = frappe.qb.DocType(f"{doctype} Item")
if not filters.get("group_by"):
query += f" order by {invoice}.posting_date desc, {invoice_item}.item_group desc"
query = query.orderby(invoice.posting_date, order=Order.desc)
query = query.orderby(invoice_item.item_group, order=Order.desc)
elif filters.get("group_by") == "Invoice":
query += f" order by {invoice_item}.parent desc"
query = query.orderby(invoice_item.parent, order=Order.desc)
elif filters.get("group_by") == "Item":
query += f" order by {invoice_item}.item_code"
query = query.orderby(invoice_item.item_code)
elif filters.get("group_by") == "Item Group":
query += f" order by {invoice_item}.item_group"
query = query.orderby(invoice_item.item_group)
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
filter_field = frappe.scrub(filters.get("group_by"))
query += f" order by {filter_field} desc"
query = query.orderby(filter_field, order=Order.desc)
return query
@@ -481,15 +483,12 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
from frappe.desk.reportview import build_match_conditions
query, params = query.walk()
match_conditions = build_match_conditions(doctype)
if match_conditions:
query += " and " + match_conditions
if match_conditions := build_match_conditions(doctype):
query = query.where(Bracket(LiteralValue(match_conditions)))
query = apply_order_by_conditions(doctype, query, filters)
return frappe.db.sql(query, params, as_dict=True)
return query.run(as_dict=True)
def get_delivery_notes_against_sales_order(item_list):

View File

@@ -68,7 +68,7 @@ def execute(filters=None):
currency = filters.presentation_currency or frappe.get_cached_value(
"Company", filters.company, "default_currency"
)
chart = get_chart_data(filters, columns, income, expense, net_profit_loss, 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
@@ -162,18 +162,19 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co
return net_profit_loss
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency):
labels = [d.get("label") for d in columns[4:]]
def get_chart_data(filters, chart_columns, income, expense, net_profit_loss, currency):
labels = [col.get("label") for col in chart_columns]
income_data, expense_data, net_profit = [], [], []
for p in columns[4:]:
for col in chart_columns:
key = col.get("key") or col.get("fieldname")
if income:
income_data.append(income[-2].get(p.get("fieldname")))
income_data.append(income[-2].get(key))
if expense:
expense_data.append(expense[-2].get(p.get("fieldname")))
expense_data.append(expense[-2].get(key))
if net_profit_loss:
net_profit.append(net_profit_loss.get(p.get("fieldname")))
net_profit.append(net_profit_loss.get(key))
datasets = []
if income_data:

View File

@@ -6,6 +6,7 @@ import frappe
from frappe import _, msgprint
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, getdate
from pypika.terms import Bracket, LiteralValue, Order
from erpnext.accounts.party import get_party_account
from erpnext.accounts.report.utils import (
@@ -421,15 +422,13 @@ def get_invoices(filters, additional_query_columns):
from frappe.desk.reportview import build_match_conditions
query, params = query.walk()
match_conditions = build_match_conditions("Purchase Invoice")
if match_conditions := build_match_conditions("Purchase Invoice"):
query = query.where(Bracket(LiteralValue(match_conditions)))
if match_conditions:
query += " and " + match_conditions
query = query.orderby("posting_date", order=Order.desc)
query = query.orderby("name", order=Order.desc)
query += " order by posting_date desc, name desc"
return frappe.db.sql(query, params, as_dict=True)
return query.run(as_dict=True)
def get_conditions(filters, query, doctype):

Some files were not shown because too many files have changed in this diff Show More