mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-13 11:55:11 +00:00
Merge pull request #50182 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -132,7 +132,8 @@
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "IBAN",
|
||||
"length": 30
|
||||
"length": 34,
|
||||
"options": "IBAN"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
@@ -208,6 +209,7 @@
|
||||
"label": "Disabled"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"links": [
|
||||
{
|
||||
"group": "Transactions",
|
||||
@@ -250,7 +252,7 @@
|
||||
"link_fieldname": "default_bank_account"
|
||||
}
|
||||
],
|
||||
"modified": "2024-10-30 09:41:14.113414",
|
||||
"modified": "2025-08-29 12:32:01.081687",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account",
|
||||
@@ -282,6 +284,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "bank,account",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
|
||||
@@ -9,7 +9,8 @@ from frappe.contacts.address_and_contact import (
|
||||
load_address_and_contact,
|
||||
)
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import comma_and, get_link_to_form
|
||||
from frappe.utils import comma_and, get_link_to_form, validate_iban
|
||||
from frappe.utils.deprecations import deprecated
|
||||
|
||||
|
||||
class BankAccount(Document):
|
||||
@@ -52,7 +53,6 @@ class BankAccount(Document):
|
||||
|
||||
def validate(self):
|
||||
self.validate_company()
|
||||
self.validate_iban()
|
||||
self.validate_account()
|
||||
self.update_default_bank_account()
|
||||
|
||||
@@ -72,34 +72,10 @@ class BankAccount(Document):
|
||||
if self.is_company_account and not self.company:
|
||||
frappe.throw(_("Company is manadatory for company account"))
|
||||
|
||||
@deprecated
|
||||
def validate_iban(self):
|
||||
"""
|
||||
Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN
|
||||
"""
|
||||
# IBAN field is optional
|
||||
if not self.iban:
|
||||
return
|
||||
|
||||
def encode_char(c):
|
||||
# Position in the alphabet (A=1, B=2, ...) plus nine
|
||||
return str(9 + ord(c) - 64)
|
||||
|
||||
# remove whitespaces, upper case to get the right number from ord()
|
||||
iban = "".join(self.iban.split(" ")).upper()
|
||||
|
||||
# Move country code and checksum from the start to the end
|
||||
flipped = iban[4:] + iban[:4]
|
||||
|
||||
# Encode characters as numbers
|
||||
encoded = [encode_char(c) if ord(c) >= 65 and ord(c) <= 90 else c for c in flipped]
|
||||
|
||||
try:
|
||||
to_check = int("".join(encoded))
|
||||
except ValueError:
|
||||
frappe.throw(_("IBAN is not valid"))
|
||||
|
||||
if to_check % 97 != 1:
|
||||
frappe.throw(_("IBAN is not valid"))
|
||||
"""Kept for backward compatibility, will be removed in v16."""
|
||||
validate_iban(self.iban, throw=True)
|
||||
|
||||
def update_default_bank_account(self):
|
||||
if self.is_default and not self.disabled:
|
||||
|
||||
@@ -3,45 +3,8 @@
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import ValidationError
|
||||
|
||||
# test_records = frappe.get_test_records('Bank Account')
|
||||
|
||||
|
||||
class TestBankAccount(unittest.TestCase):
|
||||
def test_validate_iban(self):
|
||||
valid_ibans = [
|
||||
"GB82 WEST 1234 5698 7654 32",
|
||||
"DE91 1000 0000 0123 4567 89",
|
||||
"FR76 3000 6000 0112 3456 7890 189",
|
||||
]
|
||||
|
||||
invalid_ibans = [
|
||||
# wrong checksum (3rd place)
|
||||
"GB72 WEST 1234 5698 7654 32",
|
||||
"DE81 1000 0000 0123 4567 89",
|
||||
"FR66 3000 6000 0112 3456 7890 189",
|
||||
]
|
||||
|
||||
bank_account = frappe.get_doc({"doctype": "Bank Account"})
|
||||
|
||||
try:
|
||||
bank_account.validate_iban()
|
||||
except AttributeError:
|
||||
msg = "BankAccount.validate_iban() failed for empty IBAN"
|
||||
self.fail(msg=msg)
|
||||
|
||||
for iban in valid_ibans:
|
||||
bank_account.iban = iban
|
||||
try:
|
||||
bank_account.validate_iban()
|
||||
except ValidationError:
|
||||
msg = f"BankAccount.validate_iban() failed for valid IBAN {iban}"
|
||||
self.fail(msg=msg)
|
||||
|
||||
for not_iban in invalid_ibans:
|
||||
bank_account.iban = not_iban
|
||||
msg = f"BankAccount.validate_iban() accepted invalid IBAN {not_iban}"
|
||||
with self.assertRaises(ValidationError, msg=msg):
|
||||
bank_account.validate_iban()
|
||||
pass
|
||||
|
||||
@@ -146,6 +146,7 @@
|
||||
"fieldname": "iban",
|
||||
"fieldtype": "Data",
|
||||
"label": "IBAN",
|
||||
"options": "IBAN",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -216,7 +217,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-25 23:44:12.473583",
|
||||
"modified": "2025-09-26 00:38:17.584694",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Guarantee",
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
{
|
||||
"fieldname": "bank_party_iban",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party IBAN (Bank Statement)"
|
||||
"label": "Party IBAN (Bank Statement)",
|
||||
"options": "IBAN"
|
||||
},
|
||||
{
|
||||
"fieldname": "bank_party_account_number",
|
||||
@@ -238,7 +239,7 @@
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-26 17:06:29.207673",
|
||||
"modified": "2025-10-14 11:53:45.908169",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
|
||||
@@ -272,7 +272,8 @@
|
||||
"label": "Advance Voucher Type",
|
||||
"no_copy": 1,
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "advance_voucher_no",
|
||||
@@ -280,13 +281,14 @@
|
||||
"label": "Advance Voucher No",
|
||||
"no_copy": 1,
|
||||
"options": "advance_voucher_type",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-29 13:01:48.916517",
|
||||
"modified": "2025-10-20 17:46:47.344089",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
|
||||
@@ -226,7 +226,8 @@
|
||||
"fetch_from": "bank_account.iban",
|
||||
"fieldname": "iban",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "IBAN"
|
||||
"label": "IBAN",
|
||||
"options": "IBAN"
|
||||
},
|
||||
{
|
||||
"fetch_from": "bank_account.branch_code",
|
||||
@@ -443,11 +444,12 @@
|
||||
"label": "Phone Number"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-12-27 21:29:10.361894",
|
||||
"modified": "2025-08-29 11:52:48.555415",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
@@ -482,8 +484,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_preview_popup": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -644,9 +644,8 @@ class PurchaseOrder(BuyingController):
|
||||
if not self.is_against_so():
|
||||
return
|
||||
for item in removed_items:
|
||||
prev_ordered_qty = (
|
||||
prev_ordered_qty = flt(
|
||||
frappe.get_cached_value("Sales Order Item", item.get("sales_order_item"), "ordered_qty")
|
||||
or 0.0
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -3073,9 +3073,7 @@ def set_balance_in_account_currency(
|
||||
_("Account: {0} with currency: {1} can not be selected").format(gl_dict.account, account_currency)
|
||||
)
|
||||
|
||||
gl_dict["account_currency"] = (
|
||||
company_currency if account_currency == company_currency else account_currency
|
||||
)
|
||||
gl_dict["account_currency"] = account_currency
|
||||
|
||||
# set debit/credit in account currency if not provided
|
||||
if flt(gl_dict.debit) and not flt(gl_dict.debit_in_account_currency):
|
||||
|
||||
@@ -1552,7 +1552,9 @@ def get_stock_ledger_preview(doc, filters):
|
||||
|
||||
if doc.get("update_stock") or doc.doctype in ("Purchase Receipt", "Delivery Note"):
|
||||
doc.docstatus = 1
|
||||
doc.make_bundle_using_old_serial_batch_fields()
|
||||
doc.update_stock_ledger()
|
||||
|
||||
columns = get_sl_columns(filters)
|
||||
sl_entries = get_sl_entries_for_preview(doc.doctype, doc.name, fields)
|
||||
|
||||
|
||||
@@ -17,7 +17,17 @@ erpnext.buying = {
|
||||
this.setup_queries(doc, cdt, cdn);
|
||||
super.onload();
|
||||
|
||||
this.frm.set_query('shipping_rule', function() {
|
||||
if (["Purchase Order", "Purchase Receipt", "Purchase Invoice"].includes(this.frm.doctype)) {
|
||||
this.frm.set_query("supplier", function () {
|
||||
return {
|
||||
filters: {
|
||||
is_transporter: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
this.frm.set_query("shipping_rule", function () {
|
||||
return {
|
||||
filters: {
|
||||
"shipping_rule_type": "Buying"
|
||||
|
||||
@@ -394,6 +394,10 @@ erpnext.PointOfSale.Payment = class {
|
||||
const payments = doc.payments;
|
||||
const currency = doc.currency;
|
||||
|
||||
if (!this.$payment_modes.is(":visible")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$payment_modes.html(
|
||||
`${payments
|
||||
.map((p, i) => {
|
||||
@@ -612,6 +616,10 @@ erpnext.PointOfSale.Payment = class {
|
||||
const currency = doc.currency;
|
||||
const label = __("Change Amount");
|
||||
|
||||
if (!this.$totals.is(":visible")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$totals.html(
|
||||
`<div class="col">
|
||||
<div class="total-label">${__("Grand Total")}</div>
|
||||
|
||||
@@ -763,13 +763,11 @@ def update_company_current_month_sales(company):
|
||||
|
||||
def update_company_monthly_sales(company):
|
||||
"""Cache past year monthly sales of every company based on sales invoices"""
|
||||
import json
|
||||
|
||||
from frappe.utils.goal import get_monthly_results
|
||||
|
||||
filter_str = f"company = {frappe.db.escape(company)} and status != 'Draft' and docstatus=1"
|
||||
filter_dict = {"company": company, "status": ["!=", "Draft"], "docstatus": 1}
|
||||
month_to_value_dict = get_monthly_results(
|
||||
"Sales Invoice", "base_grand_total", "posting_date", filter_str, "sum"
|
||||
"Sales Invoice", "base_grand_total", "posting_date", filter_dict, "sum"
|
||||
)
|
||||
|
||||
frappe.db.set_value("Company", company, "sales_monthly_history", json.dumps(month_to_value_dict))
|
||||
|
||||
@@ -814,7 +814,8 @@
|
||||
"depends_on": "eval:doc.salary_mode == 'Bank'",
|
||||
"fieldname": "iban",
|
||||
"fieldtype": "Data",
|
||||
"label": "IBAN"
|
||||
"label": "IBAN",
|
||||
"options": "IBAN"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-user",
|
||||
@@ -822,7 +823,7 @@
|
||||
"image_field": "image",
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-04 08:29:34.347269",
|
||||
"modified": "2025-08-29 11:52:12.819878",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Employee",
|
||||
@@ -864,6 +865,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "employee_name",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
@@ -871,4 +873,4 @@
|
||||
"states": [],
|
||||
"title_field": "employee_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -896,7 +896,11 @@ class update_entries_after:
|
||||
if sle.is_adjustment_entry and flt(sle.qty_after_transaction, self.flt_precision) == 0:
|
||||
sle.stock_value_difference = (
|
||||
get_stock_value_difference(
|
||||
sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time, sle.voucher_no
|
||||
sle.item_code,
|
||||
sle.warehouse,
|
||||
sle.posting_date,
|
||||
sle.posting_time,
|
||||
voucher_detail_no=sle.voucher_detail_no,
|
||||
)
|
||||
* -1
|
||||
)
|
||||
@@ -2353,7 +2357,9 @@ def is_internal_transfer(sle):
|
||||
return True
|
||||
|
||||
|
||||
def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, voucher_no=None):
|
||||
def get_stock_value_difference(
|
||||
item_code, warehouse, posting_date, posting_time, voucher_no=None, voucher_detail_no=None
|
||||
):
|
||||
table = frappe.qb.DocType("Stock Ledger Entry")
|
||||
posting_datetime = get_combine_datetime(posting_date, posting_time)
|
||||
|
||||
@@ -2368,7 +2374,10 @@ def get_stock_value_difference(item_code, warehouse, posting_date, posting_time,
|
||||
)
|
||||
)
|
||||
|
||||
if voucher_no:
|
||||
if voucher_detail_no:
|
||||
query = query.where(table.voucher_detail_no != voucher_detail_no)
|
||||
|
||||
elif voucher_no:
|
||||
query = query.where(table.voucher_no != voucher_no)
|
||||
|
||||
difference_amount = query.run()
|
||||
|
||||
@@ -484,10 +484,16 @@ def get_documents_with_active_service_level_agreement():
|
||||
|
||||
|
||||
def set_documents_with_active_service_level_agreement():
|
||||
active = frozenset(
|
||||
sla.document_type for sla in frappe.get_all("Service Level Agreement", fields=["document_type"])
|
||||
)
|
||||
frappe.cache.set_value("doctypes_with_active_sla", active)
|
||||
try:
|
||||
active = frozenset(
|
||||
sla.document_type for sla in frappe.get_all("Service Level Agreement", fields=["document_type"])
|
||||
)
|
||||
frappe.cache.set_value("doctypes_with_active_sla", active)
|
||||
except (frappe.DoesNotExistError, frappe.db.TableMissingError):
|
||||
# This happens during install / uninstall when wildcard hook for SLA intercepts some doc action.
|
||||
# In both cases, the error can be safely ignored.
|
||||
active = frozenset()
|
||||
|
||||
return active
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user