mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-03 20:29:09 +00:00
Merge pull request #48980 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
Reference in New Issue
Block a user