Merge branch 'develop' into feat/employee-creation-and-lifecycle

This commit is contained in:
Krishna Pramod Shirsath
2026-02-27 13:10:24 +05:30
committed by GitHub
337 changed files with 63686 additions and 33517 deletions

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

@@ -691,7 +691,7 @@
}
],
"grid_page_length": 50,
"hide_toolbar": 1,
"hide_toolbar": 0,
"icon": "icon-cog",
"idx": 1,
"index_web_pages_for_search": 1,

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, reference_name):
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

@@ -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": ""}, ...],
@@ -618,7 +618,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 +671,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)
@@ -704,7 +709,7 @@ def set_transaction_type(args):
@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
},
{
@@ -2307,6 +2310,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 +2331,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2026-02-10 11:59:07.819903",
"modified": "2026-02-25 12:41:57.043459",
"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:
@@ -2409,7 +2410,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 +2425,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 +2442,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 +2491,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 +2585,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 +2963,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 +2981,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 +3073,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

@@ -135,7 +135,7 @@ class TaxRule(Document):
@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

@@ -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

@@ -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

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

@@ -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):

View File

@@ -7,6 +7,7 @@ from frappe import _, msgprint
from frappe.model.meta import get_field_precision
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 (
@@ -457,15 +458,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("Sales Invoice")
if match_conditions := build_match_conditions("Sales 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):

View File

@@ -390,7 +390,7 @@ def calculate_values(
prepare_opening_closing(d)
def calculate_total_row(accounts, company_currency):
def calculate_total_row(data, company_currency, show_group_accounts=True):
total_row = {
"account": "'" + _("Total") + "'",
"account_name": "'" + _("Total") + "'",
@@ -407,10 +407,16 @@ def calculate_total_row(accounts, company_currency):
"currency": company_currency,
}
for d in accounts:
if not d.parent_account:
for field in value_fields:
total_row[field] += d[field]
def sum_value_fields(row):
for field in value_fields:
total_row[field] += row[field]
for d in data:
if not show_group_accounts:
sum_value_fields(d)
elif show_group_accounts and not d.get("parent_account"):
sum_value_fields(d)
return total_row
@@ -456,11 +462,13 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
row["has_value"] = has_value
data.append(row)
total_row = calculate_total_row(accounts, company_currency)
if not filters.get("show_group_accounts"):
data = hide_group_accounts(data)
total_row = calculate_total_row(
data, company_currency, show_group_accounts=filters.get("show_group_accounts")
)
data.extend([{}, total_row])
return data

View File

@@ -147,7 +147,12 @@ def get_appropriate_company(filters):
@frappe.whitelist()
def get_invoiced_item_gross_margin(sales_invoice=None, item_code=None, company=None, with_item_data=False):
def get_invoiced_item_gross_margin(
sales_invoice: str | None = None,
item_code: str | None = None,
company: str | None = None,
with_item_data: bool = False,
):
from erpnext.accounts.report.gross_profit.gross_profit import GrossProfitGenerator
sales_invoice = sales_invoice or frappe.form_dict.get("sales_invoice")

View File

@@ -3,6 +3,7 @@
from collections import defaultdict
from datetime import date, datetime
from json import loads
from typing import TYPE_CHECKING, Optional
@@ -60,15 +61,15 @@ OUTSTANDING_DOCTYPES = frozenset(["Sales Invoice", "Purchase Invoice", "Fees"])
@frappe.whitelist()
def get_fiscal_year(
date=None,
fiscal_year=None,
label="Date",
verbose=1,
company=None,
as_dict=False,
boolean=None,
raise_on_missing=True,
truncate=False,
date: str | datetime | None = None,
fiscal_year: str | None = None,
label: str = "Date",
verbose: int = 1,
company: str | None = None,
as_dict: bool = False,
boolean: str | bool | None = None,
raise_on_missing: bool = True,
truncate: bool = False,
):
if isinstance(raise_on_missing, str):
raise_on_missing = loads(raise_on_missing)
@@ -93,14 +94,14 @@ def get_fiscal_year(
def get_fiscal_years(
transaction_date=None,
fiscal_year=None,
label="Date",
verbose=1,
company=None,
as_dict=False,
boolean=None,
raise_on_missing=True,
transaction_date: str | None = None,
fiscal_year: str | None = None,
label: str = "Date",
verbose: int = 1,
company: str | None = None,
as_dict: bool = False,
boolean: str | None = None,
raise_on_missing: bool = True,
):
if transaction_date:
transaction_date = getdate(transaction_date)
@@ -171,7 +172,7 @@ def _get_fiscal_years(company=None):
@frappe.whitelist()
def get_fiscal_year_filter_field(company=None):
def get_fiscal_year_filter_field(company: str | None = None):
field = {"fieldtype": "Select", "options": [], "operator": "Between", "query_value": True}
fiscal_years = get_fiscal_years(company=company)
for fiscal_year in fiscal_years:
@@ -199,18 +200,18 @@ def validate_fiscal_year(date, fiscal_year, company, label="Date", doc=None):
@frappe.whitelist()
def get_balance_on(
account=None,
date=None,
party_type=None,
party=None,
company=None,
in_account_currency=True,
cost_center=None,
ignore_account_permission=False,
account_type=None,
start_date=None,
finance_book=None,
include_default_fb_balances=False,
account: str | None = None,
date: str | date | None = None,
party_type: str | None = None,
party: str | None = None,
company: str | None = None,
in_account_currency: bool = True,
cost_center: str | None = None,
ignore_account_permission: bool = False,
account_type: str | None = None,
start_date: str | None = None,
finance_book: str | None = None,
include_default_fb_balances: bool = False,
):
if not account and frappe.form_dict.get("account"):
account = frappe.form_dict.get("account")
@@ -437,7 +438,7 @@ def get_count_on(account, fieldname, date):
@frappe.whitelist()
def add_ac(args=None):
def add_ac(args: frappe._dict | None = None):
from frappe.desk.treeview import make_tree_args
if not args:
@@ -469,7 +470,7 @@ def add_ac(args=None):
@frappe.whitelist()
def add_cc(args=None):
def add_cc(args: frappe._dict | None = None):
from frappe.desk.treeview import make_tree_args
if not args:
@@ -500,7 +501,8 @@ def _build_dimensions_dict_for_exc_gain_loss(
dimensions_dict = frappe._dict()
if entry and active_dimensions:
for dim in active_dimensions:
dimensions_dict[dim.fieldname] = entry.get(dim.fieldname)
if entry_dimension := entry.get(dim.fieldname):
dimensions_dict[dim.fieldname] = entry_dimension
return dimensions_dict
@@ -1153,7 +1155,7 @@ def remove_ref_doc_link_from_pe(
@frappe.whitelist()
def get_company_default(company, fieldname, ignore_validation=False):
def get_company_default(company: str, fieldname: str, ignore_validation: bool = False):
value = frappe.get_cached_value("Company", company, fieldname)
if not ignore_validation and not value:
@@ -1338,7 +1340,9 @@ def get_companies():
@frappe.whitelist()
def get_children(doctype, parent, company, is_root=False, include_disabled=False):
def get_children(
doctype: str, parent: str, company: str, is_root: bool = False, include_disabled: bool = False
):
if isinstance(include_disabled, str):
include_disabled = loads(include_disabled)
from erpnext.accounts.report.financial_statements import sort_accounts
@@ -1371,7 +1375,12 @@ def get_children(doctype, parent, company, is_root=False, include_disabled=False
@frappe.whitelist()
def get_account_balances(accounts, company, finance_book=None, include_default_fb_balances=False):
def get_account_balances(
accounts: str | list,
company: str,
finance_book: str | None = None,
include_default_fb_balances: bool = False,
):
if isinstance(accounts, str):
accounts = loads(accounts)
@@ -1464,7 +1473,9 @@ def create_payment_gateway_account(gateway, payment_channel="Email", company=Non
@frappe.whitelist()
def update_cost_center(docname, cost_center_name, cost_center_number, company, merge):
def update_cost_center(
docname: str, cost_center_name: str, cost_center_number: str, company: str, merge: bool
):
"""
Renames the document by adding the number as a prefix to the current name and updates
all transaction where it was present.
@@ -1544,7 +1555,7 @@ def parse_naming_series_variable(doc, variable):
@frappe.whitelist()
def get_coa(doctype, parent, is_root=None, chart=None):
def get_coa(doctype: str, parent: str, is_root: bool | None = None, chart: str | None = None):
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import (
build_tree_from_json,
)

View File

@@ -1092,7 +1092,7 @@ def get_asset_naming_series():
@frappe.whitelist()
def make_sales_invoice(asset, item_code, company, sell_qty, serial_no=None):
def make_sales_invoice(asset: str, item_code: str, company: str, sell_qty: int, serial_no: str | None = None):
asset_doc = frappe.get_doc("Asset", asset)
si = frappe.new_doc("Sales Invoice")
si.company = company
@@ -1125,7 +1125,13 @@ def make_sales_invoice(asset, item_code, company, sell_qty, serial_no=None):
@frappe.whitelist()
def create_asset_maintenance(asset, item_code, item_name, asset_category, company):
def create_asset_maintenance(
asset: str,
item_code: str,
item_name: str,
asset_category: str,
company: str,
):
asset_maintenance = frappe.new_doc("Asset Maintenance")
asset_maintenance.update(
{
@@ -1140,14 +1146,23 @@ def create_asset_maintenance(asset, item_code, item_name, asset_category, compan
@frappe.whitelist()
def create_asset_repair(company, asset, asset_name):
def create_asset_repair(
company: str,
asset: str,
asset_name: str,
):
asset_repair = frappe.new_doc("Asset Repair")
asset_repair.update({"company": company, "asset": asset, "asset_name": asset_name})
return asset_repair
@frappe.whitelist()
def create_asset_capitalization(company, asset, asset_name, item_code):
def create_asset_capitalization(
company: str,
asset: str,
asset_name: str,
item_code: str,
):
asset_capitalization = frappe.new_doc("Asset Capitalization")
asset_capitalization.update(
{
@@ -1161,35 +1176,22 @@ def create_asset_capitalization(company, asset, asset_name, item_code):
@frappe.whitelist()
def create_asset_value_adjustment(asset, asset_category, company):
def create_asset_value_adjustment(
asset: str,
asset_category: str,
company: str,
):
asset_value_adjustment = frappe.new_doc("Asset Value Adjustment")
asset_value_adjustment.update({"asset": asset, "company": company, "asset_category": asset_category})
return asset_value_adjustment
@frappe.whitelist()
def transfer_asset(args):
args = json.loads(args)
if args.get("serial_no"):
args["quantity"] = len(args.get("serial_no").split("\n"))
movement_entry = frappe.new_doc("Asset Movement")
movement_entry.update(args)
movement_entry.insert()
movement_entry.submit()
frappe.db.commit()
frappe.msgprint(
_("Asset Movement record {0} created")
.format("<a href='/app/Form/Asset Movement/{0}'>{0}</a>")
.format(movement_entry.name)
)
@frappe.whitelist()
def get_item_details(item_code, asset_category, net_purchase_amount):
def get_item_details(
item_code: str,
asset_category: str,
net_purchase_amount: float,
):
asset_category_doc = frappe.get_cached_doc("Asset Category", asset_category)
books = []
for d in asset_category_doc.finance_books:
@@ -1239,7 +1241,7 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non
@frappe.whitelist()
def make_journal_entry(asset_name):
def make_journal_entry(asset_name: str):
asset = frappe.get_doc("Asset", asset_name)
(
fixed_asset_account,
@@ -1281,7 +1283,10 @@ def make_journal_entry(asset_name):
@frappe.whitelist()
def make_asset_movement(assets, purpose=None):
def make_asset_movement(
assets: list[dict] | str,
purpose: str = "Transfer",
):
import json
if isinstance(assets, str):
@@ -1291,7 +1296,7 @@ def make_asset_movement(assets, purpose=None):
frappe.throw(_("At least one asset has to be selected."))
asset_movement = frappe.new_doc("Asset Movement")
asset_movement.quantity = len(assets)
asset_movement.purpose = purpose
for asset in assets:
asset = frappe.get_doc("Asset", asset.get("name"))
asset_movement.company = asset.get("company")
@@ -1313,7 +1318,10 @@ def is_cwip_accounting_enabled(asset_category):
@frappe.whitelist()
def get_asset_value_after_depreciation(asset_name, finance_book=None):
def get_asset_value_after_depreciation(
asset_name: str,
finance_book: str | None = None,
):
asset = frappe.get_doc("Asset", asset_name)
if not asset.calculate_depreciation:
return flt(asset.value_after_depreciation)
@@ -1322,7 +1330,7 @@ def get_asset_value_after_depreciation(asset_name, finance_book=None):
@frappe.whitelist()
def has_active_capitalization(asset):
def has_active_capitalization(asset: str):
active_capitalizations = frappe.db.count(
"Asset Capitalization", filters={"target_asset": asset, "docstatus": 1}
)
@@ -1330,7 +1338,11 @@ def has_active_capitalization(asset):
@frappe.whitelist()
def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
def get_values_from_purchase_doc(
purchase_doc_name: str,
item_code: str,
doctype: str,
):
purchase_doc = frappe.get_doc(doctype, purchase_doc_name)
matching_items = [item for item in purchase_doc.items if item.item_code == item_code]
@@ -1352,7 +1364,7 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
@frappe.whitelist()
def split_asset(asset_name, split_qty):
def split_asset(asset_name: str, split_qty: int):
"""Split an asset into two based on the given quantity."""
existing_asset = frappe.get_doc("Asset", asset_name)
split_qty = cint(split_qty)

View File

@@ -7,6 +7,7 @@ from frappe import _
from frappe.query_builder import Order
from frappe.query_builder.functions import Max, Min
from frappe.utils import (
DateTimeLikeObject,
add_months,
cint,
flt,
@@ -161,11 +162,11 @@ def get_depr_cost_center_and_series():
@frappe.whitelist()
def make_depreciation_entry(
depr_schedule_name,
date=None,
sch_start_idx=None,
sch_end_idx=None,
accounting_dimensions=None,
depr_schedule_name: str,
date: DateTimeLikeObject | None = None,
sch_start_idx: int | None = None,
sch_end_idx: int | None = None,
accounting_dimensions: list[dict] | None = None,
):
frappe.has_permission("Journal Entry", throw=True)
date = date or today()
@@ -356,7 +357,7 @@ def get_message_for_depr_entry_posting_error(asset_links, error_log_links):
@frappe.whitelist()
def scrap_asset(asset_name, scrap_date=None):
def scrap_asset(asset_name: str, scrap_date: DateTimeLikeObject | None = None):
asset = frappe.get_doc("Asset", asset_name)
scrap_date = getdate(scrap_date) or getdate(today())
asset.db_set("disposal_date", scrap_date)
@@ -445,7 +446,7 @@ def create_journal_entry_for_scrap(asset, scrap_date):
@frappe.whitelist()
def restore_asset(asset_name):
def restore_asset(asset_name: str):
asset = frappe.get_doc("Asset", asset_name)
reverse_depreciation_entry_made_on_disposal(asset)
reset_depreciation_schedule(asset, get_note_for_restore(asset))
@@ -772,7 +773,7 @@ def get_profit_gl_entries(
@frappe.whitelist()
def get_disposal_account_and_cost_center(company):
def get_disposal_account_and_cost_center(company: str):
disposal_account, depreciation_cost_center = frappe.get_cached_value(
"Company", company, ["disposal_account", "depreciation_cost_center"]
)
@@ -788,9 +789,9 @@ def get_disposal_account_and_cost_center(company):
@frappe.whitelist()
def get_value_after_depreciation_on_disposal_date(
asset: str,
disposal_date: str,
disposal_date: DateTimeLikeObject,
finance_book: str | None = None,
) -> float:
):
asset_doc = frappe.get_doc("Asset", asset)
if asset_doc.asset_type == "Composite Component":

View File

@@ -396,7 +396,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_warehouse_details",
child: item,
args: {
args: {
ctx: {
item_code: item.item_code,
warehouse: cstr(item.warehouse),
qty: -1 * flt(item.stock_qty),

View File

@@ -2,6 +2,7 @@
# For license information, please see license.txt
import json
from typing import Any
import frappe
@@ -609,7 +610,7 @@ class AssetCapitalization(StockController):
@frappe.whitelist()
def get_target_item_details(item_code: str | None = None, company: str | None = None) -> frappe._dict:
def get_target_item_details(item_code: str | None = None, company: str | None = None):
out = frappe._dict()
# Get Item Details
@@ -635,7 +636,7 @@ def get_target_item_details(item_code: str | None = None, company: str | None =
@frappe.whitelist()
def get_target_asset_details(asset: str | None = None, company: str | None = None) -> frappe._dict:
def get_target_asset_details(asset: str | None = None, company: str | None = None):
out = frappe._dict()
# Get Asset Details
@@ -710,24 +711,22 @@ def get_consumed_stock_item_details(ctx: ItemDetailsCtx):
@frappe.whitelist()
def get_warehouse_details(args):
if isinstance(args, str):
args = json.loads(args)
args = frappe._dict(args)
out = {}
if args.warehouse and args.item_code:
out = {
"actual_qty": get_previous_sle(args).get("qty_after_transaction") or 0,
"valuation_rate": get_incoming_rate(args, raise_error_if_no_rate=False),
}
@erpnext.normalize_ctx_input(ItemDetailsCtx)
def get_warehouse_details(ctx: ItemDetailsCtx) -> frappe._dict:
out = frappe._dict()
if ctx.warehouse and ctx.item_code:
out = frappe._dict(
{
"actual_qty": get_previous_sle(ctx).get("qty_after_transaction") or 0,
"valuation_rate": get_incoming_rate(ctx, raise_error_if_no_rate=False),
}
)
return out
@frappe.whitelist()
@erpnext.normalize_ctx_input(ItemDetailsCtx)
def get_consumed_asset_details(ctx):
def get_consumed_asset_details(ctx: ItemDetailsCtx) -> frappe._dict:
out = frappe._dict()
asset_details = frappe._dict()
@@ -773,7 +772,7 @@ def get_consumed_asset_details(ctx):
@frappe.whitelist()
@erpnext.normalize_ctx_input(ItemDetailsCtx)
def get_service_item_details(ctx):
def get_service_item_details(ctx: ItemDetailsCtx) -> frappe._dict:
out = frappe._dict()
item = frappe._dict()
@@ -795,7 +794,7 @@ def get_service_item_details(ctx):
@frappe.whitelist()
def get_items_tagged_to_wip_composite_asset(params):
def get_items_tagged_to_wip_composite_asset(params: dict | str):
if isinstance(params, str):
params = json.loads(params)

View File

@@ -271,7 +271,7 @@ def get_asset_shift_factors_map():
@frappe.whitelist()
def get_depr_schedule(asset_name, status, finance_book=None):
def get_depr_schedule(asset_name: str, status: str, finance_book: str | None = None):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_name, status, finance_book)
if not asset_depr_schedule_doc:
@@ -281,13 +281,13 @@ def get_depr_schedule(asset_name, status, finance_book=None):
@frappe.whitelist()
def get_asset_depr_schedule_doc(asset_name, status=None, finance_book=None):
def get_asset_depr_schedule_doc(asset_name: str, status: str | None = None, finance_book: str | None = None):
asset_depr_schedule = get_asset_depr_schedule_name(asset_name, status, finance_book)
if not asset_depr_schedule:
return
asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule[0].name)
asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule)
return asset_depr_schedule_doc
@@ -299,21 +299,23 @@ def get_asset_depr_schedule_name(asset_name, status=None, finance_book=None):
]
if status:
if isinstance(status, str):
status = [status]
filters.append(["status", "in", status])
status_list = [status] if isinstance(status, str) else status
filters.append(["status", "in", status_list])
if finance_book:
filters.append(["finance_book", "=", finance_book])
else:
filters.append(["finance_book", "is", "not set"])
finance_book_filter = (
["finance_book", "=", finance_book] if finance_book else ["finance_book", "is", "not set"]
)
filters.append(finance_book_filter)
return frappe.get_all(
depreciation_schedules = frappe.get_all(
doctype="Asset Depreciation Schedule",
filters=filters,
fields=["name"],
limit=1,
)
return depreciation_schedules[0].name if depreciation_schedules else None
def is_first_day_of_the_month(date):
first_day_of_the_month = get_first_day(date)

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