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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -143,7 +143,7 @@ def preprocess_mt940_content(content: str) -> str:
@frappe.whitelist() @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) doc = frappe.get_doc("Bank Statement Import", data_import)
_file_doc, content = get_file(mt940_file_path) _file_doc, content = get_file(mt940_file_path)
@@ -208,26 +208,28 @@ def convert_mt940_to_csv(data_import, mt940_file_path):
@frappe.whitelist() @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( return frappe.get_doc("Bank Statement Import", data_import).get_preview_from_template(
import_file, google_sheets_url import_file, google_sheets_url
) )
@frappe.whitelist() @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() job_id = frappe.get_doc("Bank Statement Import", data_import).start_import()
return job_id is not None return job_id is not None
@frappe.whitelist() @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 = frappe.get_doc("Bank Statement Import", data_import_name)
data_import.export_errored_rows() data_import.export_errored_rows()
@frappe.whitelist() @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() 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() @frappe.whitelist()
def get_import_status(docname): def get_import_status(docname: str):
import_status = {} import_status = {}
data_import = frappe.get_doc("Bank Statement Import", docname) data_import = frappe.get_doc("Bank Statement Import", docname)

View File

@@ -139,6 +139,8 @@ class BankTransaction(Document):
self.set_status() self.set_status()
def on_cancel(self): def on_cancel(self):
self.ignore_linked_doctypes = ["GL Entry"]
for payment_entry in self.payment_entries: for payment_entry in self.payment_entries:
self.delink_payment_entry(payment_entry) 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"), ("unallocated_amount", "bank_account"),
as_dict=True, 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( frappe.throw(
_("Bank Account {} in Bank Transaction {} is not matching with Bank Account {}").format( _("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() @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) header_map = get_header_mapping(columns, bank_account)
success = 0 success = 0

View File

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

View File

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

View File

@@ -47,7 +47,7 @@ class ChequePrintTemplate(Document):
@frappe.whitelist() @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): if not frappe.db.exists("Print Format", template_name):
cheque_print = frappe.new_doc("Print Format") cheque_print = frappe.new_doc("Print Format")
cheque_print.update( cheque_print.update(

View File

@@ -1,6 +1,7 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
from datetime import date
import frappe import frappe
from frappe import _, qb from frappe import _, qb
@@ -614,7 +615,12 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
@frappe.whitelist() @frappe.whitelist()
def get_account_details( 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): if not (company and posting_date):
frappe.throw(_("Company and Posting Date is mandatory")) 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 import Case
from frappe.query_builder.functions import Sum from frappe.query_builder.functions import Sum
from frappe.utils import cstr, date_diff, flt, getdate 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 import get_company_currency
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -732,7 +732,7 @@ class FinancialQueryBuilder:
user_conditions = build_match_conditions(doctype) user_conditions = build_match_conditions(doctype)
if user_conditions: if user_conditions:
query = query.where(LiteralValue(user_conditions)) query = query.where(Bracket(LiteralValue(user_conditions)))
return query.run(as_dict=True) return query.run(as_dict=True)

View File

@@ -4,7 +4,7 @@
import frappe import frappe
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from frappe import _ from frappe import _, cint
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import add_days, add_years, cstr, getdate from frappe.utils import add_days, add_years, cstr, getdate
@@ -33,23 +33,11 @@ class FiscalYear(Document):
self.validate_dates() self.validate_dates()
self.validate_overlap() self.validate_overlap()
if not self.is_new(): def on_update(self):
year_start_end_dates = frappe.db.sql( frappe.cache().delete_key("fiscal_years")
"""select year_start_date, year_end_date
from `tabFiscal Year` where name=%s""",
(self.name),
)
if year_start_end_dates: def on_trash(self):
if ( frappe.cache().delete_key("fiscal_years")
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 validate_dates(self): def validate_dates(self):
self.validate_from_to_dates("year_start_date", "year_end_date") self.validate_from_to_dates("year_start_date", "year_end_date")
@@ -66,28 +54,20 @@ class FiscalYear(Document):
frappe.exceptions.InvalidDates, 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): def validate_overlap(self):
existing_fiscal_years = frappe.db.sql( fy = frappe.qb.DocType("Fiscal Year")
"""select name from `tabFiscal Year`
where ( name = self.name or self.year
(%(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) existing_fiscal_years = (
or (year_start_date between %(year_start_date)s and %(year_end_date)s) frappe.qb.from_(fy)
or (year_end_date between %(year_start_date)s and %(year_end_date)s) .select(fy.name)
) and name!=%(name)s""", .where(
{ (fy.year_start_date <= self.year_end_date)
"year_start_date": self.year_start_date, & (fy.year_end_date >= self.year_start_date)
"year_end_date": self.year_end_date, & (fy.name != name)
"name": self.name or "No Name", )
}, .run(as_dict=True)
as_dict=True,
) )
if existing_fiscal_years: if existing_fiscal_years:
@@ -110,37 +90,30 @@ class FiscalYear(Document):
frappe.throw( frappe.throw(
_( _(
"Year start date or end date is overlapping with {0}. To avoid please set company" "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.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(): def auto_create_fiscal_year():
for d in frappe.db.sql( fy = frappe.qb.DocType("Fiscal Year")
"""select name from `tabFiscal Year` where year_end_date = date_add(current_date, interval 3 day)"""
): # 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: try:
current_fy = frappe.get_doc("Fiscal Year", d[0]) 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_start_date = add_days(current_fy.year_end_date, 1)
new_fy.year_end_date = add_years(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) start_year = cstr(new_fy.year_start_date.year)
end_year = cstr(new_fy.year_end_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) 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.auto_created = 1
new_fy.insert(ignore_permissions=True) new_fy.insert(ignore_permissions=True)

View File

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

View File

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

View File

@@ -317,7 +317,7 @@ class InvoiceDiscounting(AccountsController):
@frappe.whitelist() @frappe.whitelist()
def get_invoices(filters): def get_invoices(filters: str):
filters = frappe._dict(json.loads(filters)) filters = frappe._dict(json.loads(filters))
cond = [] cond = []
if filters.customer: 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); 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() { load_defaults() {
//this.frm.show_print_first = true; //this.frm.show_print_first = true;
if (this.frm.doc.__islocal && this.frm.doc.company) { if (this.frm.doc.__islocal && this.frm.doc.company) {

View File

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

View File

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

View File

@@ -43,7 +43,7 @@
"fields": [ "fields": [
{ {
"bold": 1, "bold": 1,
"columns": 2, "columns": 4,
"fieldname": "account", "fieldname": "account",
"fieldtype": "Link", "fieldtype": "Link",
"in_global_search": 1, "in_global_search": 1,
@@ -185,20 +185,19 @@
"fieldtype": "Select", "fieldtype": "Select",
"label": "Reference Type", "label": "Reference Type",
"no_copy": 1, "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 "search_index": 1
}, },
{ {
"fieldname": "reference_name", "fieldname": "reference_name",
"fieldtype": "Dynamic Link", "fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Reference Name", "label": "Reference Name",
"no_copy": 1, "no_copy": 1,
"options": "reference_type", "options": "reference_type",
"search_index": 1 "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", "fieldname": "reference_due_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Reference Due Date", "label": "Reference Due Date",
@@ -294,7 +293,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2025-11-27 12:23:33.157655", "modified": "2026-02-19 17:01:22.642454",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry Account", "name": "Journal Entry Account",

View File

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

View File

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

View File

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

View File

@@ -67,7 +67,7 @@ class PartyLink(Document):
@frappe.whitelist() @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 = frappe.new_doc("Party Link")
party_link.primary_role = primary_role party_link.primary_role = primary_role
party_link.primary_party = primary_party 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_email", "");
frm.set_value("contact_person", ""); frm.set_value("contact_person", "");
} }
if (frm.doc.payment_type && frm.doc.party_type && frm.doc.party && frm.doc.company) { if (frm.doc.payment_type && frm.doc.party_type && frm.doc.party && frm.doc.company) {
if (!frm.doc.posting_date) { if (!frm.doc.posting_date) {
frappe.msgprint(__("Please select Posting Date before selecting Party")); frappe.msgprint(__("Please select Posting Date before selecting Party"));
frm.set_value("party", ""); frm.set_value("party", "");
return; return;
} }
erpnext.utils.get_employee_contact_details(frm);
frm.set_party_account_based_on_party = true; frm.set_party_account_based_on_party = true;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; 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", "fetch_from": "company.book_advance_payments_in_separate_party_account",
"fieldname": "book_advance_payments_in_separate_party_account", "fieldname": "book_advance_payments_in_separate_party_account",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1,
"label": "Book Advance Payments in Separate Party Account", "label": "Book Advance Payments in Separate Party Account",
"no_copy": 1, "no_copy": 1,
"read_only": 1 "read_only": 1
@@ -793,7 +792,7 @@
"table_fieldname": "payment_entries" "table_fieldname": "payment_entries"
} }
], ],
"modified": "2025-12-18 13:56:40.206038", "modified": "2026-02-03 16:08:49.800381",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",

View File

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

View File

@@ -59,7 +59,7 @@ class PaymentOrder(Document):
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @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( return frappe.db.sql(
""" select mode_of_payment from `tabPayment Order Reference` """ select mode_of_payment from `tabPayment Order Reference`
where parent = %(parent)s and mode_of_payment like %(txt)s 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.whitelist()
@frappe.validate_and_sanitize_search_inputs @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( return frappe.db.sql(
""" select supplier from `tabPayment Order Reference` """ select supplier from `tabPayment Order Reference`
where parent = %(parent)s and supplier like %(txt)s and 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() @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) doc = frappe.get_doc("Payment Order", name)
make_journal_entry(doc, supplier, mode_of_payment) 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") return frappe.get_single_value("Accounts Settings", "auto_reconcile_payments")
@frappe.whitelist() @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_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry)
invoice[0]["exchange_rate"] = invoice_exchange_map.get(invoice[0].get("invoice_number")) 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"]: if payment_entry[0].get("reference_type") in ["Sales Invoice", "Purchase Invoice"]:
@@ -445,7 +447,7 @@ class PaymentReconciliation(Document):
return new_difference_amount return new_difference_amount
@frappe.whitelist() @frappe.whitelist()
def allocate_entries(self, args): def allocate_entries(self, args: dict):
self.validate_entries() self.validate_entries()
exc_gain_loss_posting_date = frappe.db.get_single_value( 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", "column_break_4",
"reference_doctype", "reference_doctype",
"reference_name", "reference_name",
"payment_reference_section",
"payment_reference",
"transaction_details", "transaction_details",
"grand_total", "grand_total",
"currency", "currency",
@@ -157,6 +159,7 @@
"label": "Amount", "label": "Amount",
"non_negative": 1, "non_negative": 1,
"options": "currency", "options": "currency",
"read_only_depends_on": "eval:doc.payment_reference.length>0",
"reqd": 1 "reqd": 1
}, },
{ {
@@ -457,6 +460,17 @@
"fieldname": "phone_number", "fieldname": "phone_number",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Phone Number" "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, "grid_page_length": 50,
@@ -464,7 +478,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2025-08-29 11:52:48.555415", "modified": "2026-01-13 12:53:00.963274",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Request", "name": "Payment Request",

View File

@@ -45,6 +45,7 @@ class PaymentRequest(Document):
if TYPE_CHECKING: if TYPE_CHECKING:
from frappe.types import DF 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 ( from erpnext.accounts.doctype.subscription_plan_detail.subscription_plan_detail import (
SubscriptionPlanDetail, SubscriptionPlanDetail,
) )
@@ -78,6 +79,7 @@ class PaymentRequest(Document):
payment_gateway: DF.ReadOnly | None payment_gateway: DF.ReadOnly | None
payment_gateway_account: DF.Link | None payment_gateway_account: DF.Link | None
payment_order: DF.Link | None payment_order: DF.Link | None
payment_reference: DF.Table[PaymentReference]
payment_request_type: DF.Literal["Outward", "Inward"] payment_request_type: DF.Literal["Outward", "Inward"]
payment_url: DF.Data | None payment_url: DF.Data | None
phone_number: DF.Data | None phone_number: DF.Data | None
@@ -109,15 +111,36 @@ class PaymentRequest(Document):
if self.get("__islocal"): if self.get("__islocal"):
self.status = "Draft" self.status = "Draft"
self.validate_reference_document() self.validate_reference_document()
self.validate_against_payment_reference()
self.validate_payment_request_amount() self.validate_payment_request_amount()
# self.validate_currency() # self.validate_currency()
self.validate_subscription_details() 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): def validate_reference_document(self):
if not self.reference_doctype or not self.reference_name: if not self.reference_doctype or not self.reference_name:
frappe.throw(_("To create a Payment Request reference document is required")) frappe.throw(_("To create a Payment Request reference document is required"))
def validate_payment_request_amount(self): def validate_payment_request_amount(self):
if self.payment_reference:
return
if self.grand_total == 0: if self.grand_total == 0:
frappe.throw( frappe.throw(
_("{0} cannot be zero").format(self.get_label_from_fieldname("grand_total")), _("{0} cannot be zero").format(self.get_label_from_fieldname("grand_total")),
@@ -539,8 +562,6 @@ class PaymentRequest(Document):
def make_payment_request(**args): def make_payment_request(**args):
"""Make payment request""" """Make payment request"""
frappe.has_permission(doctype="Payment Request", ptype="write", throw=True)
args = frappe._dict(args) args = frappe._dict(args)
if args.dt not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST: if args.dt not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST:
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt))) 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): if args.dn and not isinstance(args.dn, str):
frappe.throw(_("Invalid parameter. 'dn' should be of type 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) ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
if not args.get("company"): if not args.get("company"):
args.company = ref_doc.company args.company = ref_doc.company
gateway_account = get_gateway_details(args) or frappe._dict() 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: if not grand_total:
frappe.throw(_("Payment Entry is already created")) 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 loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points)) # sets fields on ref_doc
ref_doc.db_update() ref_doc.db_update()
grand_total = grand_total - loyalty_amount grand_total = grand_total - loyalty_amount
# fetches existing payment request `grand_total` amount # fetches existing payment request `grand_total` amount
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc) existing_payment_request_amount = get_existing_payment_request_amount(ref_doc)
@@ -583,19 +660,20 @@ def make_payment_request(**args):
else: else:
# If PR's are processed, cancel all of them. # If PR's are processed, cancel all of them.
cancel_old_payment_requests(ref_doc.doctype, ref_doc.name) 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) grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
draft_payment_request = frappe.db.get_value( draft_payment_request = frappe.db.get_value(
"Payment Request", "Payment Request",
{"reference_doctype": ref_doc.doctype, "reference_name": ref_doc.name, "docstatus": 0}, {"reference_doctype": ref_doc.doctype, "reference_name": ref_doc.name, "docstatus": 0},
) )
if draft_payment_request: 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) pr = frappe.get_doc("Payment Request", draft_payment_request)
if selected_payment_schedules:
apply_payment_references(pr, payment_reference)
pr.save()
else: else:
bank_account = ( bank_account = (
get_party_bank_account(args.get("party_type"), args.get("party")) 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( pr.update(
{ {
"cost_center": ref_doc.get("cost_center"), "cost_center": ref_doc.get("cost_center"),
@@ -679,6 +760,51 @@ def make_payment_request(**args):
return pr.as_dict() 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): def get_amount(ref_doc, payment_account=None):
"""get amount based on doctype""" """get amount based on doctype"""
grand_total = 0 grand_total = 0
@@ -811,7 +937,7 @@ def get_payment_gateway_account(filter):
@frappe.whitelist() @frappe.whitelist()
def get_print_format_list(ref_doctype): def get_print_format_list(ref_doctype: str):
print_format_list = ["Standard"] print_format_list = ["Standard"]
print_format_list.extend( print_format_list.extend(
@@ -821,13 +947,13 @@ def get_print_format_list(ref_doctype):
return {"print_format": print_format_list} return {"print_format": print_format_list}
@frappe.whitelist(allow_guest=True) @frappe.whitelist()
def resend_payment_email(docname): def resend_payment_email(docname: str):
return frappe.get_doc("Payment Request", docname).send_email() return frappe.get_doc("Payment Request", docname).send_email()
@frappe.whitelist() @frappe.whitelist()
def make_payment_entry(docname): def make_payment_entry(docname: str):
doc = frappe.get_doc("Payment Request", docname) doc = frappe.get_doc("Payment Request", docname)
return doc.create_payment_entry(submit=False).as_dict() return doc.create_payment_entry(submit=False).as_dict()
@@ -920,7 +1046,7 @@ def get_dummy_message(doc):
@frappe.whitelist() @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": if reference_doctype == "Sales Invoice":
subscriptions = frappe.db.sql( subscriptions = frappe.db.sql(
"""SELECT parent as sub_name FROM `tabSubscription Invoice` WHERE invoice=%s""", """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() @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 from frappe.model.mapper import get_mapped_doc
def set_missing_values(source, target): def set_missing_values(source, target):
@@ -984,7 +1110,9 @@ def validate_payment(doc, method=None):
@frappe.whitelist() @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()` # permission checks in `get_list()`
filters = frappe._dict(filters) filters = frappe._dict(filters)
@@ -1023,3 +1151,44 @@ def get_irequests_of_payment_request(doc: str | None = None) -> list:
}, },
) )
return res return res
@frappe.whitelist()
def get_available_payment_schedules(reference_doctype: str, reference_name: str):
ref_doc = frappe.get_doc(reference_doctype, reference_name)
if not hasattr(ref_doc, "payment_schedule") or not ref_doc.payment_schedule:
return []
if get_existing_payment_entry(reference_name):
return []
existing_refs = get_existing_payment_references(reference_name)
existing_ids = {r["payment_schedule"] for r in existing_refs if r.get("payment_schedule")}
return [r for r in ref_doc.payment_schedule if r.name not in existing_ids]
def get_existing_payment_references(reference_name):
PR = frappe.qb.DocType("Payment Request")
PRF = frappe.qb.DocType("Payment Reference")
result = (
frappe.qb.from_(PR)
.join(PRF)
.on(PR.name == PRF.parent)
.select(
PRF.payment_term,
PRF.due_date,
PRF.amount.as_("payment_amount"),
PRF.payment_schedule,
PRF.parent,
)
.where(PR.reference_name == reference_name)
.where(PR.docstatus < 2)
.where(
PR.status.isin(["Draft", "Requested", "Initiated", "Partially Paid", "Payment Ordered", "Paid"])
)
).run(as_dict=True)
return result

View File

@@ -1,11 +1,13 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import json
import re import re
from unittest.mock import patch from unittest.mock import patch
import frappe import frappe
from frappe.tests import IntegrationTestCase 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.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template 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() pr.load_from_db()
self.assertEqual(pr.grand_total, pi.outstanding_amount) 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() @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( fy_start_date, fy_end_date = frappe.db.get_value(
"Fiscal Year", fiscal_year, ["year_start_date", "year_end_date"] "Fiscal Year", fiscal_year, ["year_start_date", "year_end_date"]
) )

View File

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

View File

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

View File

@@ -4,6 +4,7 @@
import frappe import frappe
from frappe import _, bold from frappe import _, bold
from frappe.model.document import Document
from frappe.model.mapper import map_child_doc, map_doc from frappe.model.mapper import map_child_doc, map_doc
from frappe.query_builder.functions import IfNull, Sum from frappe.query_builder.functions import IfNull, Sum
from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
@@ -753,7 +754,7 @@ class POSInvoice(SalesInvoice):
return profile return profile
@frappe.whitelist() @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) profile = self.set_pos_fields(for_validate)
if not self.debit_to: if not self.debit_to:
@@ -854,7 +855,7 @@ class POSInvoice(SalesInvoice):
return frappe.get_doc("Payment Request", pr) return frappe.get_doc("Payment Request", pr)
@frappe.whitelist() @frappe.whitelist()
def update_payments(self, payments): def update_payments(self, payments: list):
if self.status == "Consolidated": if self.status == "Consolidated":
frappe.throw(_("Create Payment Entry for Consolidated POS Invoices.")) frappe.throw(_("Create Payment Entry for Consolidated POS Invoices."))
@@ -897,7 +898,7 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist() @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"): if frappe.db.get_value("Item", item_code, "is_stock_item"):
is_stock_item = True is_stock_item = True
bin_qty = get_bin_qty(item_code, warehouse) 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() @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 from erpnext.controllers.sales_and_purchase_return import make_return_doc
return make_return_doc("POS Invoice", source_name, target_doc) return make_return_doc("POS Invoice", source_name, target_doc)
@frappe.whitelist() @frappe.whitelist()
def make_merge_log(invoices): def make_merge_log(invoices: str | list):
import json import json
if isinstance(invoices, str): if isinstance(invoices, str):
@@ -1077,7 +1078,15 @@ def add_return_modes(doc, pos_profile):
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @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]: if pos_profile := filters.get("pos_profile")[1]:
pos_profile = frappe.get_cached_doc("POS Profile", pos_profile) pos_profile = frappe.get_cached_doc("POS Profile", pos_profile)
if item_groups := get_item_group(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.whitelist()
@frappe.validate_and_sanitize_search_inputs @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"] user = frappe.session["user"]
company = filters.get("company") or frappe.defaults.get_user_default("company") 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() @frappe.whitelist()
def set_default_profile(pos_profile, company): def set_default_profile(pos_profile: str, company: str):
modified = now() modified = now()
user = frappe.session.user user = frappe.session.user

View File

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

View File

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

View File

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

View File

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

View File

@@ -420,7 +420,7 @@ def get_context(customer, doc):
@frappe.whitelist() @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 = [] customer_list = []
customers = [] customers = []
@@ -460,7 +460,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
@frappe.whitelist() @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 """Returns first email from Contact Email table as a Billing email
when Is Billing Contact checked when Is Billing Contact checked
and Primary email- email with Is Primary 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() @frappe.whitelist()
def download_statements(document_name): def download_statements(document_name: str):
doc = frappe.get_doc("Process Statement Of Accounts", document_name) doc = frappe.get_doc("Process Statement Of Accounts", document_name)
report = get_report_pdf(doc) report = get_report_pdf(doc)
if report: if report:
@@ -516,7 +516,7 @@ def download_statements(document_name):
@frappe.whitelist() @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) doc = frappe.get_doc("Process Statement Of Accounts", document_name)
report = get_report_pdf(doc, consolidated=False) report = get_report_pdf(doc, consolidated=False)

View File

@@ -18,8 +18,19 @@ class TestProcessStatementOfAccounts(AccountsTestMixin, IntegrationTestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() 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)) 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): def setUp(self):
self.create_company() self.create_company()
self.create_customer() self.create_customer()

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import json
import frappe import frappe
from frappe import _, qb, throw from frappe import _, qb, throw
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum from frappe.query_builder.functions import Sum
from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate 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() project_doc.db_update()
def validate_supplier_invoice(self): 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 self.bill_no:
if cint(frappe.get_single_value("Accounts Settings", "check_supplier_invoice_uniqueness")): 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) 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() @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 from erpnext.controllers.sales_and_purchase_return import make_return_doc
return make_return_doc("Purchase Invoice", source_name, target_doc) return make_return_doc("Purchase Invoice", source_name, target_doc)
@frappe.whitelist() @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( doc = get_mapped_doc(
"Purchase Invoice", "Purchase Invoice",
source_name, source_name,
@@ -1970,35 +1967,37 @@ def make_stock_entry(source_name, target_doc=None):
@frappe.whitelist() @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): if frappe.db.exists("Purchase Invoice", name):
pi = frappe.get_lazy_doc("Purchase Invoice", name) pi = frappe.get_lazy_doc("Purchase Invoice", name)
pi.db_set("release_date", release_date) pi.db_set("release_date", release_date)
@frappe.whitelist() @frappe.whitelist()
def unblock_invoice(name): def unblock_invoice(name: str):
if frappe.db.exists("Purchase Invoice", name): if frappe.db.exists("Purchase Invoice", name):
pi = frappe.get_lazy_doc("Purchase Invoice", name) pi = frappe.get_lazy_doc("Purchase Invoice", name)
pi.unblock_invoice() pi.unblock_invoice()
@frappe.whitelist() @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): if frappe.db.exists("Purchase Invoice", name):
pi = frappe.get_lazy_doc("Purchase Invoice", name) pi = frappe.get_lazy_doc("Purchase Invoice", name)
pi.block_invoice(hold_comment, release_date) pi.block_invoice(hold_comment, release_date)
@frappe.whitelist() @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 from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction
return make_inter_company_transaction("Purchase Invoice", source_name, target_doc) return make_inter_company_transaction("Purchase Invoice", source_name, target_doc)
@frappe.whitelist() @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: if args is None:
args = {} args = {}
if isinstance(args, str): if isinstance(args, str):

View File

@@ -152,7 +152,7 @@ class RepostAccountingLedger(Document):
@frappe.whitelist() @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 from erpnext.accounts.general_ledger import make_reverse_gl_entries
frappe.flags.through_repost_accounting_ledger = True frappe.flags.through_repost_accounting_ledger = True
@@ -286,7 +286,9 @@ def validate_docs_for_voucher_types(doc_voucher_types):
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @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} filters = {"allowed": True}
if txt: if txt:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
from datetime import date
import frappe import frappe
from dateutil import relativedelta from dateutil import relativedelta
@@ -43,7 +44,13 @@ class SubscriptionPlan(Document):
@frappe.whitelist() @frappe.whitelist()
def get_plan_rate( 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) plan = frappe.get_doc("Subscription Plan", plan)
if plan.price_determination == "Fixed Rate": if plan.price_determination == "Fixed Rate":

View File

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

View File

@@ -194,7 +194,7 @@ def get_linked_advances(company, docname):
@frappe.whitelist() @frappe.whitelist()
def create_unreconcile_doc_for_selection(selections=None): def create_unreconcile_doc_for_selection(selections: str | None = None):
if selections: if selections:
selections = json.loads(selections) selections = json.loads(selections)
# assuming each row is a unique voucher # 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, "attach_print": 0,
"channel": "Email", "channel": "Email",
"condition": "doc.auto_created", "condition": "doc.auto_created == 1",
"condition_type": "Python",
"creation": "2018-04-25 14:19:05.440361", "creation": "2018-04-25 14:19:05.440361",
"days_in_advance": 0, "days_in_advance": 0,
"docstatus": 0, "docstatus": 0,
@@ -11,8 +12,10 @@
"event": "New", "event": "New",
"idx": 0, "idx": 0,
"is_standard": 1, "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", "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", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Notification for new fiscal year", "name": "Notification for new fiscal year",
@@ -27,5 +30,5 @@
], ],
"send_system_notification": 0, "send_system_notification": 0,
"send_to_all_assignees": 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 # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from datetime import date
import frappe import frappe
from frappe import _, msgprint, qb, scrub from frappe import _, msgprint, qb, scrub
@@ -55,22 +56,22 @@ class DuplicatePartyAccountError(frappe.ValidationError):
@frappe.whitelist() @frappe.whitelist()
def get_party_details( def get_party_details(
party=None, party: str | None = None,
account=None, account: str | None = None,
party_type="Customer", party_type: str = "Customer",
company=None, company: str | None = None,
posting_date=None, posting_date: str | None = None,
bill_date=None, bill_date: str | None = None,
price_list=None, price_list: str | None = None,
currency=None, currency: str | None = None,
doctype=None, doctype: str | None = None,
ignore_permissions=False, ignore_permissions: bool | None = False,
fetch_payment_terms_template=True, fetch_payment_terms_template: bool = True,
party_address=None, party_address: str | None = None,
company_address=None, company_address: str | None = None,
shipping_address=None, shipping_address: str | None = None,
dispatch_address=None, dispatch_address: str | None = None,
pos_profile=None, pos_profile: str | None = None,
): ):
if not party: if not party:
return frappe._dict() return frappe._dict()
@@ -296,19 +297,9 @@ def complete_contact_details(party_details):
contact_details = frappe._dict() contact_details = frappe._dict()
if party_details.party_type == "Employee": if party_details.party_type == "Employee":
contact_details = frappe.db.get_value( from erpnext.setup.doctype.employee.employee import _get_contact_details as get_employee_contact
"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,
)
contact_details = get_employee_contact(party_details.party)
contact_details.update({"contact_person": None, "contact_phone": None}) contact_details.update({"contact_person": None, "contact_phone": None})
elif party_details.contact_person: elif party_details.contact_person:
contact_details = frappe.db.get_value( 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() @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`. """Returns the account for the given `party`.
Will first search in party (Customer / Supplier) record, if not found, Will first search in party (Customer / Supplier) record, if not found,
will search in group (Customer Group / Supplier Group), will search in group (Customer Group / Supplier Group),
@@ -501,7 +494,7 @@ def get_party_advance_account(party_type, party, company):
@frappe.whitelist() @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}) 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() @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`""" """Get due date from `Payment Terms Template`"""
due_date = None due_date = None
if (bill_date or posting_date) and party: 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() @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( addr_tax_category_from = frappe.get_single_value(
"Accounts Settings", "determine_address_tax_category_from" "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() @frappe.whitelist()
def set_taxes( def set_taxes(
party, party: str | None,
party_type, party_type: str,
posting_date, posting_date: str | date | None,
company, company: str | None,
customer_group=None, customer_group: str | None = None,
supplier_group=None, supplier_group: str | None = None,
tax_category=None, tax_category: str | None = None,
billing_address=None, billing_address: str | None = None,
shipping_address=None, shipping_address: str | None = None,
use_for_shopping_cart=None, use_for_shopping_cart: int | None = None,
): ):
from erpnext.accounts.doctype.tax_rule.tax_rule import get_party_details, get_tax_template from erpnext.accounts.doctype.tax_rule.tax_rule import get_party_details, get_tax_template
@@ -766,7 +768,7 @@ def set_taxes(
@frappe.whitelist() @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"): if party_type not in ("Customer", "Supplier"):
return return
template = None template = None

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -145,7 +145,7 @@ def execute(filters=None):
True, 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) report_summary = get_report_summary(summary_data, company_currency)
@@ -417,12 +417,12 @@ def get_report_summary(summary_data, currency):
return report_summary return report_summary
def get_chart_data(columns, data, currency): def get_chart_data(period_list, data, currency):
labels = [d.get("label") for d in columns[2:]] labels = [period.get("label") for period in period_list]
datasets = [ datasets = [
{ {
"name": section.get("section").replace("'", ""), "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 for section in data
if section.get("parent_section") is None and section.get("currency") 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 return columns, data, message, chart
fiscal_year = get_fiscal_year_data(filters.get("from_fiscal_year"), filters.get("to_fiscal_year")) fiscal_year = get_fiscal_year_data(filters.get("from_fiscal_year"), filters.get("to_fiscal_year"))
companies_column, companies = get_companies(filters) company_list, companies = get_companies(filters)
columns = get_columns(companies_column, filters) company_columns = get_company_columns(company_list, filters)
columns = get_columns(company_columns)
if filters.get("report") == "Balance Sheet": if filters.get("report") == "Balance Sheet":
data, message, chart, report_summary = get_balance_sheet_data( 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": 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: else:
data, report_summary = get_cash_flow_data(fiscal_year, companies, filters) data, report_summary = get_cash_flow_data(fiscal_year, companies, filters)
return columns, data, message, chart, report_summary 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) asset = get_data(companies, "Asset", "Debit", fiscal_year, filters=filters)
liability = get_data(companies, "Liability", "Credit", 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, 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 return data, message, chart, report_summary
@@ -164,7 +167,7 @@ def get_root_account_name(root_type, company):
return root_account[0][0] 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) income, expense, net_profit_loss = get_income_expense_data(companies, fiscal_year, filters)
company_currency = get_company_currency(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: if net_profit_loss:
data.append(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( report_summary, primitive_summary = get_pl_summary(
companies, "", income, expense, net_profit_loss, company_currency, filters, True 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 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 = [ columns = [
{ {
"fieldname": "account", "fieldname": "account",
@@ -298,23 +324,7 @@ def get_columns(companies, filters):
}, },
] ]
for company in companies: columns.extend(company_columns)
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,
}
)
return columns return columns
@@ -646,7 +656,11 @@ def set_gl_entries_by_account(
query = query.where(Criterion.all(additional_conditions)) query = query.where(Criterion.all(additional_conditions))
gl_entries = query.run(as_dict=True) gl_entries = query.run(as_dict=True)
if filters and filters.get("presentation_currency") != d.default_currency: if (
filters
and filters.get("presentation_currency")
and filters.get("presentation_currency") != d.default_currency
):
currency_info["company"] = d.name currency_info["company"] = d.name
currency_info["company_currency"] = d.default_currency currency_info["company_currency"] = d.default_currency
convert_to_presentation_currency(gl_entries, currency_info) convert_to_presentation_currency(gl_entries, currency_info)

View File

@@ -86,6 +86,12 @@ frappe.query_reports["Consolidated Trial Balance"] = {
fieldtype: "Check", fieldtype: "Check",
default: 1, default: 1,
}, },
{
fieldname: "show_net_values",
label: __("Show net values in opening and closing columns"),
fieldtype: "Check",
default: 1,
},
{ {
fieldname: "show_group_accounts", fieldname: "show_group_accounts",
label: __("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 ( from erpnext.accounts.report.trial_balance.trial_balance import (
accumulate_values_into_parents, accumulate_values_into_parents,
calculate_total_row,
calculate_values, calculate_values,
get_opening_balances, get_opening_balances,
hide_group_accounts, hide_group_accounts,
@@ -44,7 +45,6 @@ def execute(filters: dict | None = None):
def validate_filters(filters): def validate_filters(filters):
validate_companies(filters) validate_companies(filters)
filters.show_net_values = True
tb_validate_filters(filters) 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) tb_data = get_company_wise_tb_data(company_filter, reporting_currency, ignore_reporting_currency)
consolidate_trial_balance_data(data, tb_data) consolidate_trial_balance_data(data, tb_data)
for d in data: if filters.get("show_net_values"):
prepare_opening_closing(d) prepare_opening_closing_for_ctb(data)
total_row = calculate_total_row(data, reporting_currency)
data.extend([{}, total_row])
if not filters.get("show_group_accounts"): if not filters.get("show_group_accounts"):
data = hide_group_accounts(data) 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"): if filters.get("presentation_currency"):
update_to_presentation_currency( update_to_presentation_currency(
data, data,
@@ -207,10 +211,6 @@ def prepare_companywise_tb_data(accounts, filters, parent_children_map, reportin
data = [] data = []
for d in accounts: 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 has_value = False
row = { row = {
"account": d.name, "account": d.name,
@@ -242,35 +242,9 @@ def prepare_companywise_tb_data(accounts, filters, parent_children_map, reportin
return data return data
def calculate_total_row(data, reporting_currency): def calculate_foreign_currency_translation_reserve(total_row, data, filters):
total_row = { if not data or not total_row:
"account": "'" + _("Total") + "'", return
"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):
opening_dr_cr_diff = total_row["opening_debit"] - total_row["opening_credit"] opening_dr_cr_diff = total_row["opening_debit"] - total_row["opening_credit"]
dr_cr_diff = total_row["debit"] - total_row["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"), "root_type": data[idx].get("root_type"),
"account_type": "Equity", "account_type": "Equity",
"parent_account": data[idx].get("account"), "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, "has_value": True,
"currency": total_row.get("currency"), "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_debit"] = fctr_row["opening_debit"] + fctr_row["debit"]
fctr_row["closing_credit"] = fctr_row["opening_credit"] + fctr_row["credit"] 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) 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) d.update(currency=to_currency)
def prepare_opening_closing_for_ctb(data):
for d in data:
prepare_opening_closing(d)
def get_columns(): def get_columns():
return [ return [
{ {

View File

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

View File

@@ -11,7 +11,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.query_builder.functions import Max, Min, Sum 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 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 ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, 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) account_filter_query = get_account_filter_query(root_lft, root_rgt, root_type, gl_entry)
query = query.where(ExistsCriterion(account_filter_query)) query = query.where(ExistsCriterion(account_filter_query))
if group_by_account:
query = query.groupby("account")
from frappe.desk.reportview import build_match_conditions from frappe.desk.reportview import build_match_conditions
query, params = query.walk() if match_conditions := build_match_conditions(doctype):
match_conditions = build_match_conditions(doctype) query = query.where(Bracket(LiteralValue(match_conditions)))
if match_conditions: return query.run(as_dict=True)
query += "and" + match_conditions
if group_by_account:
query += " GROUP BY `account`"
return frappe.db.sql(query, params, as_dict=True)
def get_account_filter_query(root_lft, root_rgt, root_type, gl_entry): def get_account_filter_query(root_lft, root_rgt, root_type, gl_entry):

View File

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

View File

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

View File

@@ -324,10 +324,8 @@ def get_conditions(filters):
from frappe.desk.reportview import build_match_conditions from frappe.desk.reportview import build_match_conditions
match_conditions = build_match_conditions("GL Entry") if match_conditions := build_match_conditions("GL Entry"):
conditions.append(f"({match_conditions})")
if match_conditions:
conditions.append(match_conditions)
accounting_dimensions = get_accounting_dimensions(as_list=False) 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 qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
) )
sinv.is_return = 1 sinv.is_return = 1
sinv.items[0].allow_zero_valuation_rate = 1
sinv = sinv.save().submit() sinv = sinv.save().submit()
filters = frappe._dict( filters = frappe._dict(

View File

@@ -5,6 +5,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.utils import flt
from pypika.terms import Bracket, LiteralValue
import erpnext import erpnext
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import ( 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 from frappe.desk.reportview import build_match_conditions
query, params = query.walk() if match_conditions := build_match_conditions(doctype):
match_conditions = build_match_conditions(doctype) query = query.where(Bracket(LiteralValue(match_conditions)))
if match_conditions:
query += " and " + match_conditions
query = apply_order_by_conditions(doctype, query, filters) 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(): 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 import flt
from frappe.utils.nestedset import get_descendants_of from frappe.utils.nestedset import get_descendants_of
from frappe.utils.xlsxutils import handle_html 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.sales_register.sales_register import get_mode_of_payments
from erpnext.accounts.report.utils import get_values_for_columns 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): def apply_order_by_conditions(doctype, query, filters):
invoice = f"`tab{doctype}`" invoice = frappe.qb.DocType(doctype)
invoice_item = f"`tab{doctype} Item`" invoice_item = frappe.qb.DocType(f"{doctype} Item")
if not filters.get("group_by"): 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": 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": 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": 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"): elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
filter_field = frappe.scrub(filters.get("group_by")) 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 return query
@@ -481,15 +483,12 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
from frappe.desk.reportview import build_match_conditions from frappe.desk.reportview import build_match_conditions
query, params = query.walk() if match_conditions := build_match_conditions(doctype):
match_conditions = build_match_conditions(doctype) query = query.where(Bracket(LiteralValue(match_conditions)))
if match_conditions:
query += " and " + match_conditions
query = apply_order_by_conditions(doctype, query, filters) 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): 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( currency = filters.presentation_currency or frappe.get_cached_value(
"Company", filters.company, "default_currency" "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( report_summary, primitive_summary = get_report_summary(
period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters 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 return net_profit_loss
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency): def get_chart_data(filters, chart_columns, income, expense, net_profit_loss, currency):
labels = [d.get("label") for d in columns[4:]] labels = [col.get("label") for col in chart_columns]
income_data, expense_data, net_profit = [], [], [] 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: if income:
income_data.append(income[-2].get(p.get("fieldname"))) income_data.append(income[-2].get(key))
if expense: if expense:
expense_data.append(expense[-2].get(p.get("fieldname"))) expense_data.append(expense[-2].get(key))
if net_profit_loss: if net_profit_loss:
net_profit.append(net_profit_loss.get(p.get("fieldname"))) net_profit.append(net_profit_loss.get(key))
datasets = [] datasets = []
if income_data: if income_data:

View File

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

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