mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-15 11:09:17 +00:00
# [16.15.0](https://github.com/frappe/erpnext/compare/v16.14.0...v16.15.0) (2026-04-22) ### Bug Fixes * **accounts:** fetch project name from payment entry to journal entry (backport [#54307](https://github.com/frappe/erpnext/issues/54307)) ([#54453](https://github.com/frappe/erpnext/issues/54453)) ([62a9a76](62a9a761b7)) * add portal user ownership check to supplier quotation (backport [#54298](https://github.com/frappe/erpnext/issues/54298)) ([#54300](https://github.com/frappe/erpnext/issues/54300)) ([d7da5b0](d7da5b047d)) * add project filter to accounts payable and receivable reports (backport [#54344](https://github.com/frappe/erpnext/issues/54344)) ([#54442](https://github.com/frappe/erpnext/issues/54442)) ([57cd2a0](57cd2a06e8)) * append row level user remarks in gl map ([aa359ad](aa359aded4)) * changed qty validation from qty field to stock_qty (backport [#54352](https://github.com/frappe/erpnext/issues/54352)) ([#54357](https://github.com/frappe/erpnext/issues/54357)) ([fa76e8a](fa76e8ac7f)) * clear conditions table when calculate_based_on is set to Fixed ([7849733](78497336c7)) * clear shipping rule conditions for fixed shipping rule ([319d769](319d769c6f)) * **dashboard-trends:** set default fiscal year and company before val… (backport [#54339](https://github.com/frappe/erpnext/issues/54339)) ([#54400](https://github.com/frappe/erpnext/issues/54400)) ([b1825c0](b1825c0cbe)) * default company perms for HR manager ([47abaf7](47abaf70b2)) * default perm for HR manager & HR user ([95213fb](95213fb9b8)) * default perm for HR manager & HR user ([a7b1fec](a7b1fec21d)) * default permission for HR manager role ([534891a](534891aac4)) * default permission for HR User role ([0d6d64f](0d6d64ff05)) * Disallow negative rates in Purchase invoice (backport [#54254](https://github.com/frappe/erpnext/issues/54254)) ([#54393](https://github.com/frappe/erpnext/issues/54393)) ([cac9073](cac907383b)) * dropship logic should come above non stock logic in gross profit… (backport [#54383](https://github.com/frappe/erpnext/issues/54383)) ([#54385](https://github.com/frappe/erpnext/issues/54385)) ([78aaf6c](78aaf6c7e8)) * fetch item tax template from item group when creating item (backport [#54258](https://github.com/frappe/erpnext/issues/54258)) ([#54368](https://github.com/frappe/erpnext/issues/54368)) ([3914d5d](3914d5d1b7)) * hide operations field in bom creator if phantom (backport [#54336](https://github.com/frappe/erpnext/issues/54336)) ([#54337](https://github.com/frappe/erpnext/issues/54337)) ([b252ad4](b252ad49b7)) * make Target Warehouse mandatory on UI ([46f5de0](46f5de0b1c)) * **manufacturing:** handle empty list in query builder ([d2cc549](d2cc549696)) * move make_dimension_in_accounting_doctypes from after_insert to on_update ([f287edd](f287edd8c2)) * negative batch report showing same batch-warehouse multiple times ([493f36b](493f36b3ce)) * non-collapsible in customer quick entry ([101f68c](101f68c8e8)) * **pos_invoice_item:** fetch `grant_commission` from `item_code` (backport [#54413](https://github.com/frappe/erpnext/issues/54413)) ([#54418](https://github.com/frappe/erpnext/issues/54418)) ([dd6d4d1](dd6d4d1910)) * **purchase_register:** filter tax rows by parenttype in invoice tax map query (backport [#54272](https://github.com/frappe/erpnext/issues/54272)) ([#54444](https://github.com/frappe/erpnext/issues/54444)) ([01aff64](01aff6492c)) * recalculate operating costs if workstation type is changed (backport [#54390](https://github.com/frappe/erpnext/issues/54390)) ([#54398](https://github.com/frappe/erpnext/issues/54398)) ([cfcba1f](cfcba1fcf2)) * remove unwanted perm for HR user role ([4940aeb](4940aeb712)) * reset base_rounded_total when rounded_total resets (backport [#54241](https://github.com/frappe/erpnext/issues/54241)) ([#54304](https://github.com/frappe/erpnext/issues/54304)) ([45052ce](45052ce8a7)) * resolve conflict ([9e6300b](9e6300bf76)) * sales order is not valid when creating WO from MR from PP (backport [#54435](https://github.com/frappe/erpnext/issues/54435)) ([#54436](https://github.com/frappe/erpnext/issues/54436)) ([5397b7d](5397b7da25)) * Table row in dialog should not have delete row option ([5916e57](5916e570af)) * **taxes_and_totals:** apply conversion_rate to taxable_amount in get_itemised_tax ([d506e57](d506e574d2)) * **test:** missing repost allowed defaults ([d49c343](d49c34389b)) * use qty instead of stock qty dropship gross profit report (backport [#54389](https://github.com/frappe/erpnext/issues/54389)) ([#54391](https://github.com/frappe/erpnext/issues/54391)) ([7556550](7556550158)) * validate south africa company in vat audit report (backport [#54030](https://github.com/frappe/erpnext/issues/54030)) ([#54394](https://github.com/frappe/erpnext/issues/54394)) ([aa2cba9](aa2cba9780)) * zero valuation rate popup on SI (backport [#54376](https://github.com/frappe/erpnext/issues/54376)) ([#54377](https://github.com/frappe/erpnext/issues/54377)) ([104eac2](104eac21e8)) ### Features * add option to create production plan from sales order (backport [#53662](https://github.com/frappe/erpnext/issues/53662)) ([#54323](https://github.com/frappe/erpnext/issues/54323)) ([b487f69](b487f69b59)) * add support for 'not applicable' tax in item tax templates ([#50898](https://github.com/frappe/erpnext/issues/50898)) ([52a4ca9](52a4ca9c41)) * backflush based on in BOM ([2c73e37](2c73e37f80)) * make fg phantom-able in bom creator (backport [#54332](https://github.com/frappe/erpnext/issues/54332)) ([#54333](https://github.com/frappe/erpnext/issues/54333)) ([10dbfd3](10dbfd310f)) * use single remark field with custom remark toggle ([27c5dab](27c5dab7e4))
196 lines
5.3 KiB
Python
196 lines
5.3 KiB
Python
import functools
|
|
import inspect
|
|
from typing import TypeVar
|
|
|
|
import frappe
|
|
from frappe.model.document import Document
|
|
from frappe.utils.user import is_website_user
|
|
|
|
__version__ = "16.15.0"
|
|
|
|
|
|
def get_default_company(user=None):
|
|
"""Get default company for user"""
|
|
from frappe.defaults import get_user_default_as_list
|
|
|
|
if not user:
|
|
user = frappe.session.user
|
|
|
|
companies = get_user_default_as_list("company", user)
|
|
if companies:
|
|
default_company = companies[0]
|
|
else:
|
|
default_company = frappe.db.get_single_value("Global Defaults", "default_company")
|
|
|
|
return default_company
|
|
|
|
|
|
def get_default_currency():
|
|
"""Returns the currency of the default company"""
|
|
company = get_default_company()
|
|
if company:
|
|
return frappe.get_cached_value("Company", company, "default_currency")
|
|
|
|
|
|
def get_default_cost_center(company):
|
|
"""Returns the default cost center of the company"""
|
|
if not company:
|
|
return None
|
|
|
|
if not frappe.flags.company_cost_center:
|
|
frappe.flags.company_cost_center = {}
|
|
if company not in frappe.flags.company_cost_center:
|
|
frappe.flags.company_cost_center[company] = frappe.get_cached_value("Company", company, "cost_center")
|
|
return frappe.flags.company_cost_center[company]
|
|
|
|
|
|
def get_company_currency(company):
|
|
"""Returns the default company currency"""
|
|
if not frappe.flags.company_currency:
|
|
frappe.flags.company_currency = {}
|
|
if company not in frappe.flags.company_currency:
|
|
frappe.flags.company_currency[company] = frappe.db.get_value(
|
|
"Company", company, "default_currency", cache=True
|
|
)
|
|
return frappe.flags.company_currency[company]
|
|
|
|
|
|
def set_perpetual_inventory(enable=1, company=None):
|
|
if not company:
|
|
company = "_Test Company" if frappe.in_test else get_default_company()
|
|
|
|
company = frappe.get_doc("Company", company)
|
|
company.enable_perpetual_inventory = enable
|
|
company.save()
|
|
|
|
|
|
def encode_company_abbr(name, company=None, abbr=None):
|
|
"""Returns name encoded with company abbreviation"""
|
|
company_abbr = abbr or frappe.get_cached_value("Company", company, "abbr")
|
|
parts = name.rsplit(" - ", 1)
|
|
|
|
if parts[-1].lower() != company_abbr.lower():
|
|
parts.append(company_abbr)
|
|
|
|
return " - ".join(parts)
|
|
|
|
|
|
def is_perpetual_inventory_enabled(company):
|
|
if not company:
|
|
company = "_Test Company" if frappe.in_test else get_default_company()
|
|
|
|
if not hasattr(frappe.local, "enable_perpetual_inventory"):
|
|
frappe.local.enable_perpetual_inventory = {}
|
|
|
|
if company not in frappe.local.enable_perpetual_inventory:
|
|
frappe.local.enable_perpetual_inventory[company] = (
|
|
frappe.get_cached_value("Company", company, "enable_perpetual_inventory") or 0
|
|
)
|
|
|
|
return frappe.local.enable_perpetual_inventory[company]
|
|
|
|
|
|
def get_default_finance_book(company=None):
|
|
if not company:
|
|
company = get_default_company()
|
|
|
|
if not hasattr(frappe.local, "default_finance_book"):
|
|
frappe.local.default_finance_book = {}
|
|
|
|
if company not in frappe.local.default_finance_book:
|
|
frappe.local.default_finance_book[company] = frappe.get_cached_value(
|
|
"Company", company, "default_finance_book"
|
|
)
|
|
|
|
return frappe.local.default_finance_book[company]
|
|
|
|
|
|
def get_party_account_type(party_type):
|
|
if not hasattr(frappe.local, "party_account_types"):
|
|
frappe.local.party_account_types = {}
|
|
|
|
if party_type not in frappe.local.party_account_types:
|
|
frappe.local.party_account_types[party_type] = (
|
|
frappe.db.get_value("Party Type", party_type, "account_type") or ""
|
|
)
|
|
|
|
return frappe.local.party_account_types[party_type]
|
|
|
|
|
|
def get_region(company=None):
|
|
"""Return the default country based on flag, company or global settings
|
|
|
|
You can also set global company flag in `frappe.flags.company`
|
|
"""
|
|
|
|
if not company:
|
|
company = frappe.local.flags.company
|
|
|
|
if company:
|
|
return frappe.get_cached_value("Company", company, "country")
|
|
|
|
return frappe.flags.country or frappe.get_system_settings("country")
|
|
|
|
|
|
def allow_regional(fn):
|
|
"""Decorator to make a function regionally overridable
|
|
|
|
Example:
|
|
@erpnext.allow_regional
|
|
def myfunction():
|
|
pass"""
|
|
|
|
@functools.wraps(fn)
|
|
def caller(*args, **kwargs):
|
|
overrides = frappe.get_hooks("regional_overrides", {}).get(get_region())
|
|
function_path = f"{inspect.getmodule(fn).__name__}.{fn.__name__}"
|
|
|
|
if not overrides or function_path not in overrides:
|
|
return fn(*args, **kwargs)
|
|
|
|
# Priority given to last installed app
|
|
return frappe.get_attr(overrides[function_path][-1])(*args, **kwargs)
|
|
|
|
return caller
|
|
|
|
|
|
def check_app_permission():
|
|
if frappe.session.user == "Administrator":
|
|
return True
|
|
|
|
if is_website_user():
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
def normalize_ctx_input(T: type) -> callable:
|
|
"""
|
|
Normalizes the first argument (ctx) of the decorated function by:
|
|
- Converting Document objects to dictionaries
|
|
- Parsing JSON strings
|
|
- Casting the result to the specified type T
|
|
"""
|
|
|
|
def decorator(func: callable):
|
|
# conserve annotations for frappe.utils.typing_validations
|
|
@functools.wraps(func, assigned=(a for a in functools.WRAPPER_ASSIGNMENTS if a != "__annotations__"))
|
|
def wrapper(ctx: T | Document | dict | str, *args, **kwargs):
|
|
if isinstance(ctx, Document):
|
|
ctx = T(**ctx.as_dict())
|
|
elif isinstance(ctx, dict):
|
|
ctx = T(**ctx)
|
|
else:
|
|
ctx = T(**frappe.parse_json(ctx))
|
|
|
|
return func(ctx, *args, **kwargs)
|
|
|
|
# set annotations from function
|
|
wrapper.__annotations__.update({k: v for k, v in func.__annotations__.items() if k != "ctx"})
|
|
return wrapper
|
|
|
|
return decorator
|