Merge pull request #48980 from frappe/version-14-hotfix

chore: release v14
This commit is contained in:
ruthra kumar
2025-08-06 08:04:22 +05:30
committed by GitHub
11 changed files with 100 additions and 29 deletions

View File

@@ -4,6 +4,8 @@
import unittest import unittest
import frappe import frappe
from frappe.query_builder.functions import Sum
from frappe.tests.utils import change_settings
from frappe.utils import add_days, today from frappe.utils import add_days, today
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
@@ -190,6 +192,31 @@ class TestCostCenterAllocation(unittest.TestCase):
coa2.cancel() coa2.cancel()
jv.cancel() jv.cancel()
@change_settings("System Settings", {"rounding_method": "Commercial Rounding"})
def test_debit_credit_on_cost_center_allocation_for_commercial_rounding(self):
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
cca = create_cost_center_allocation(
"_Test Company",
"Main Cost Center 1 - _TC",
{"Sub Cost Center 2 - _TC": 50, "Sub Cost Center 3 - _TC": 50},
)
si = create_sales_invoice(rate=145.65, cost_center="Main Cost Center 1 - _TC")
gl_entry = frappe.qb.DocType("GL Entry")
gl_entries = (
frappe.qb.from_(gl_entry)
.select(Sum(gl_entry.credit).as_("cr"), Sum(gl_entry.debit).as_("dr"))
.where(gl_entry.voucher_type == "Sales Invoice")
.where(gl_entry.voucher_no == si.name)
).run(as_dict=1)
self.assertEqual(gl_entries[0].cr, gl_entries[0].dr)
si.cancel()
cca.cancel()
def create_cost_center_allocation( def create_cost_center_allocation(
company, company,

View File

@@ -75,7 +75,7 @@
}, },
{ {
"default": "0", "default": "0",
"description": "Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach", "description": "Only payment entries with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
"fieldname": "consider_party_ledger_amount", "fieldname": "consider_party_ledger_amount",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Consider Entire Party Ledger Amount", "label": "Consider Entire Party Ledger Amount",
@@ -102,10 +102,11 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-07-27 21:47:34.396071", "modified": "2025-07-30 07:13:51.785735",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Tax Withholding Category", "name": "Tax Withholding Category",
"naming_rule": "Set by user",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@@ -148,4 +149,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -179,6 +179,15 @@ def process_gl_map(gl_map, merge_entries=True, precision=None):
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None): def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
round_off_account, default_currency = frappe.get_cached_value(
"Company", gl_map[0].company, ["round_off_account", "default_currency"]
)
if not precision:
precision = get_field_precision(
frappe.get_meta("GL Entry").get_field("debit"),
currency=default_currency,
)
new_gl_map = [] new_gl_map = []
for d in gl_map: for d in gl_map:
cost_center = d.get("cost_center") cost_center = d.get("cost_center")
@@ -192,6 +201,11 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
new_gl_map.append(d) new_gl_map.append(d)
continue continue
if d.account == round_off_account:
d.cost_center = cost_center_allocation[0][0]
new_gl_map.append(d)
continue
for sub_cost_center, percentage in cost_center_allocation: for sub_cost_center, percentage in cost_center_allocation:
gle = copy.deepcopy(d) gle = copy.deepcopy(d)
gle.cost_center = sub_cost_center gle.cost_center = sub_cost_center

View File

@@ -197,6 +197,11 @@ frappe.query_reports["General Ledger"] = {
label: __("Show Net Values in Party Account"), label: __("Show Net Values in Party Account"),
fieldtype: "Check", fieldtype: "Check",
}, },
{
fieldname: "show_amount_in_company_currency",
label: __("Show Credit / Debit in Company Currency"),
fieldtype: "Check",
},
{ {
fieldname: "show_remarks", fieldname: "show_remarks",
label: __("Show Remarks"), label: __("Show Remarks"),

View File

@@ -580,6 +580,19 @@ def get_columns(filters):
company = filters.get("company") or get_default_company() company = filters.get("company") or get_default_company()
filters["presentation_currency"] = currency = get_company_currency(company) filters["presentation_currency"] = currency = get_company_currency(company)
company_currency = get_company_currency(filters.get("company") or get_default_company())
if (
filters.get("show_amount_in_company_currency")
and filters["presentation_currency"] != company_currency
):
frappe.throw(
_("Presentation Currency cannot be {0} , When {1} is enabled.").format(
frappe.bold(filters["presentation_currency"]),
frappe.bold("Show Credit / Debit in Company Currency"),
)
)
columns = [ columns = [
{ {
"label": _("GL Entry"), "label": _("GL Entry"),

View File

@@ -46,6 +46,7 @@ class PaymentLedger:
against_voucher_no=ple.against_voucher_no, against_voucher_no=ple.against_voucher_no,
amount=ple.amount, amount=ple.amount,
currency=ple.account_currency, currency=ple.account_currency,
company=ple.company,
) )
if self.filters.include_account_currency: if self.filters.include_account_currency:
@@ -77,6 +78,7 @@ class PaymentLedger:
against_voucher_no="Outstanding:", against_voucher_no="Outstanding:",
amount=total, amount=total,
currency=voucher_data[0].currency, currency=voucher_data[0].currency,
company=voucher_data[0].company,
) )
if self.filters.include_account_currency: if self.filters.include_account_currency:
@@ -85,7 +87,12 @@ class PaymentLedger:
voucher_data.append(entry) voucher_data.append(entry)
# empty row # empty row
voucher_data.append(frappe._dict()) voucher_data.append(
frappe._dict(
currency=voucher_data[0].currency,
company=voucher_data[0].company,
)
)
self.data.extend(voucher_data) self.data.extend(voucher_data)
def build_conditions(self): def build_conditions(self):

View File

@@ -118,7 +118,7 @@ def convert_to_presentation_currency(gl_entries, currency_info, filters=None):
len(account_currencies) == 1 len(account_currencies) == 1
and account_currency == presentation_currency and account_currency == presentation_currency
and not exchange_gain_or_loss and not exchange_gain_or_loss
): ) and not filters.get("show_amount_in_company_currency"):
entry["debit"] = debit_in_account_currency entry["debit"] = debit_in_account_currency
entry["credit"] = credit_in_account_currency entry["credit"] = credit_in_account_currency
else: else:

View File

@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from collections import defaultdict
from json import loads from json import loads
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
@@ -2195,25 +2196,37 @@ def run_ledger_health_checks():
doc.save() doc.save()
def get_link_fields_grouped_by_option(doctype):
meta = frappe.get_meta(doctype)
link_fields_map = defaultdict(list)
for df in meta.fields:
if df.fieldtype == "Link" and df.options and not df.ignore_user_permissions:
link_fields_map[df.options].append(df.fieldname)
return link_fields_map
def build_qb_match_conditions(doctype, user=None) -> list: def build_qb_match_conditions(doctype, user=None) -> list:
match_filters = build_match_conditions(doctype, user, False) match_filters = build_match_conditions(doctype, user, False)
link_fields_map = get_link_fields_grouped_by_option(doctype)
criterion = [] criterion = []
apply_strict_user_permissions = frappe.get_system_settings("apply_strict_user_permissions") apply_strict_user_permissions = frappe.get_system_settings("apply_strict_user_permissions")
if match_filters: if match_filters:
from frappe import qb
_dt = qb.DocType(doctype) _dt = qb.DocType(doctype)
for filter in match_filters: for filter in match_filters:
for d, names in filter.items(): for link_option, allowed_values in filter.items():
fieldname = d.lower().replace(" ", "_") fieldnames = link_fields_map.get(link_option, [])
field = _dt[fieldname]
cond = field.isin(names) for fieldname in fieldnames:
if not apply_strict_user_permissions: field = _dt[fieldname]
cond = (Coalesce(field, "") == "") | field.isin(names) cond = field.isin(allowed_values)
criterion.append(cond) if not apply_strict_user_permissions:
cond = (Coalesce(field, "") == "") | cond
criterion.append(cond)
return criterion return criterion

View File

@@ -60,7 +60,7 @@ def import_genericode():
"doctype": "File", "doctype": "File",
"attached_to_doctype": "Code List", "attached_to_doctype": "Code List",
"attached_to_name": code_list.name, "attached_to_name": code_list.name,
"folder": "Home/Attachments", "folder": frappe.db.get_value("File", {"is_attachments_folder": 1}),
"file_name": frappe.local.uploaded_filename, "file_name": frappe.local.uploaded_filename,
"file_url": frappe.local.uploaded_file_url, "file_url": frappe.local.uploaded_file_url,
"is_private": 1, "is_private": 1,

View File

@@ -274,14 +274,6 @@ class FIFOSlots:
d.actual_qty = d.bal_qty d.actual_qty = d.bal_qty
key, fifo_queue, transferred_item_key = self.__init_key_stores(d) key, fifo_queue, transferred_item_key = self.__init_key_stores(d)
serial_nos = d.serial_no if d.serial_no else []
if fifo_queue and isinstance(fifo_queue[0][0], str):
d.has_serial_no = 1
if d.actual_qty > 0:
self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos)
else:
self.__compute_outgoing_stock(d, fifo_queue, transferred_item_key, serial_nos)
self.__update_balances(d, key) self.__update_balances(d, key)
@@ -439,10 +431,10 @@ class FIFOSlots:
transfer_qty_to_pop = 0 transfer_qty_to_pop = 0
def __update_balances(self, row: dict, key: tuple | str): def __update_balances(self, row: dict, key: tuple | str):
self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction or row.bal_qty self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction or flt(row.bal_qty)
if "total_qty" not in self.item_details[key]: if "total_qty" not in self.item_details[key]:
self.item_details[key]["total_qty"] = row.actual_qty or row.bal_qty self.item_details[key]["total_qty"] = row.actual_qty or flt(row.bal_qty)
else: else:
self.item_details[key]["total_qty"] += row.actual_qty self.item_details[key]["total_qty"] += row.actual_qty

View File

@@ -277,8 +277,6 @@ def get_incoming_rate(args, raise_error_if_no_rate=True):
if isinstance(args, str): if isinstance(args, str):
args = json.loads(args) args = json.loads(args)
voucher_no = args.get("voucher_no") or args.get("name")
in_rate = None in_rate = None
if (args.get("serial_no") or "").strip(): if (args.get("serial_no") or "").strip():
in_rate = get_avg_purchase_rate(args.get("serial_no")) in_rate = get_avg_purchase_rate(args.get("serial_no"))
@@ -301,12 +299,13 @@ def get_incoming_rate(args, raise_error_if_no_rate=True):
in_rate = ( in_rate = (
_get_fifo_lifo_rate(previous_stock_queue, args.get("qty") or 0, valuation_method) _get_fifo_lifo_rate(previous_stock_queue, args.get("qty") or 0, valuation_method)
if previous_stock_queue if previous_stock_queue
else 0 else None
) )
elif valuation_method == "Moving Average": elif valuation_method == "Moving Average":
in_rate = previous_sle.get("valuation_rate") or 0 in_rate = previous_sle.get("valuation_rate")
if in_rate is None: if in_rate is None:
voucher_no = args.get("voucher_no") or args.get("name")
in_rate = get_valuation_rate( in_rate = get_valuation_rate(
args.get("item_code"), args.get("item_code"),
args.get("warehouse"), args.get("warehouse"),