mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-04 22:18:27 +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",
|
"fieldtype": "Data",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "IBAN",
|
"label": "IBAN",
|
||||||
"length": 30
|
"length": 34,
|
||||||
|
"options": "IBAN"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_12",
|
"fieldname": "column_break_12",
|
||||||
@@ -208,6 +209,7 @@
|
|||||||
"label": "Disabled"
|
"label": "Disabled"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
"group": "Transactions",
|
"group": "Transactions",
|
||||||
@@ -250,7 +252,7 @@
|
|||||||
"link_fieldname": "default_bank_account"
|
"link_fieldname": "default_bank_account"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-10-30 09:41:14.113414",
|
"modified": "2025-08-29 12:32:01.081687",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank Account",
|
"name": "Bank Account",
|
||||||
@@ -282,6 +284,7 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"search_fields": "bank,account",
|
"search_fields": "bank,account",
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ from frappe.contacts.address_and_contact import (
|
|||||||
load_address_and_contact,
|
load_address_and_contact,
|
||||||
)
|
)
|
||||||
from frappe.model.document import Document
|
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):
|
class BankAccount(Document):
|
||||||
@@ -52,7 +53,6 @@ class BankAccount(Document):
|
|||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_company()
|
self.validate_company()
|
||||||
self.validate_iban()
|
|
||||||
self.validate_account()
|
self.validate_account()
|
||||||
self.update_default_bank_account()
|
self.update_default_bank_account()
|
||||||
|
|
||||||
@@ -72,34 +72,10 @@ class BankAccount(Document):
|
|||||||
if self.is_company_account and not self.company:
|
if self.is_company_account and not self.company:
|
||||||
frappe.throw(_("Company is manadatory for company account"))
|
frappe.throw(_("Company is manadatory for company account"))
|
||||||
|
|
||||||
|
@deprecated
|
||||||
def validate_iban(self):
|
def validate_iban(self):
|
||||||
"""
|
"""Kept for backward compatibility, will be removed in v16."""
|
||||||
Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN
|
validate_iban(self.iban, throw=True)
|
||||||
"""
|
|
||||||
# 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"))
|
|
||||||
|
|
||||||
def update_default_bank_account(self):
|
def update_default_bank_account(self):
|
||||||
if self.is_default and not self.disabled:
|
if self.is_default and not self.disabled:
|
||||||
|
|||||||
@@ -3,45 +3,8 @@
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe import ValidationError
|
|
||||||
|
|
||||||
# test_records = frappe.get_test_records('Bank Account')
|
# test_records = frappe.get_test_records('Bank Account')
|
||||||
|
|
||||||
|
|
||||||
class TestBankAccount(unittest.TestCase):
|
class TestBankAccount(unittest.TestCase):
|
||||||
def test_validate_iban(self):
|
pass
|
||||||
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()
|
|
||||||
|
|||||||
@@ -146,6 +146,7 @@
|
|||||||
"fieldname": "iban",
|
"fieldname": "iban",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "IBAN",
|
"label": "IBAN",
|
||||||
|
"options": "IBAN",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -216,7 +217,7 @@
|
|||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-09-25 23:44:12.473583",
|
"modified": "2025-09-26 00:38:17.584694",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank Guarantee",
|
"name": "Bank Guarantee",
|
||||||
|
|||||||
@@ -223,7 +223,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "bank_party_iban",
|
"fieldname": "bank_party_iban",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Party IBAN (Bank Statement)"
|
"label": "Party IBAN (Bank Statement)",
|
||||||
|
"options": "IBAN"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "bank_party_account_number",
|
"fieldname": "bank_party_account_number",
|
||||||
@@ -238,7 +239,7 @@
|
|||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-09-26 17:06:29.207673",
|
"modified": "2025-10-14 11:53:45.908169",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank Transaction",
|
"name": "Bank Transaction",
|
||||||
|
|||||||
@@ -272,7 +272,8 @@
|
|||||||
"label": "Advance Voucher Type",
|
"label": "Advance Voucher Type",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "DocType",
|
"options": "DocType",
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "advance_voucher_no",
|
"fieldname": "advance_voucher_no",
|
||||||
@@ -280,13 +281,14 @@
|
|||||||
"label": "Advance Voucher No",
|
"label": "Advance Voucher No",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "advance_voucher_type",
|
"options": "advance_voucher_type",
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-09-29 13:01:48.916517",
|
"modified": "2025-10-20 17:46:47.344089",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Journal Entry Account",
|
"name": "Journal Entry Account",
|
||||||
|
|||||||
@@ -226,7 +226,8 @@
|
|||||||
"fetch_from": "bank_account.iban",
|
"fetch_from": "bank_account.iban",
|
||||||
"fieldname": "iban",
|
"fieldname": "iban",
|
||||||
"fieldtype": "Read Only",
|
"fieldtype": "Read Only",
|
||||||
"label": "IBAN"
|
"label": "IBAN",
|
||||||
|
"options": "IBAN"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fetch_from": "bank_account.branch_code",
|
"fetch_from": "bank_account.branch_code",
|
||||||
@@ -443,11 +444,12 @@
|
|||||||
"label": "Phone Number"
|
"label": "Phone Number"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-12-27 21:29:10.361894",
|
"modified": "2025-08-29 11:52:48.555415",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Request",
|
"name": "Payment Request",
|
||||||
@@ -482,8 +484,9 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"show_preview_popup": 1,
|
"show_preview_popup": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -644,9 +644,8 @@ class PurchaseOrder(BuyingController):
|
|||||||
if not self.is_against_so():
|
if not self.is_against_so():
|
||||||
return
|
return
|
||||||
for item in removed_items:
|
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")
|
frappe.get_cached_value("Sales Order Item", item.get("sales_order_item"), "ordered_qty")
|
||||||
or 0.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
frappe.db.set_value(
|
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)
|
_("Account: {0} with currency: {1} can not be selected").format(gl_dict.account, account_currency)
|
||||||
)
|
)
|
||||||
|
|
||||||
gl_dict["account_currency"] = (
|
gl_dict["account_currency"] = account_currency
|
||||||
company_currency if account_currency == company_currency else account_currency
|
|
||||||
)
|
|
||||||
|
|
||||||
# set debit/credit in account currency if not provided
|
# set debit/credit in account currency if not provided
|
||||||
if flt(gl_dict.debit) and not flt(gl_dict.debit_in_account_currency):
|
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"):
|
if doc.get("update_stock") or doc.doctype in ("Purchase Receipt", "Delivery Note"):
|
||||||
doc.docstatus = 1
|
doc.docstatus = 1
|
||||||
|
doc.make_bundle_using_old_serial_batch_fields()
|
||||||
doc.update_stock_ledger()
|
doc.update_stock_ledger()
|
||||||
|
|
||||||
columns = get_sl_columns(filters)
|
columns = get_sl_columns(filters)
|
||||||
sl_entries = get_sl_entries_for_preview(doc.doctype, doc.name, fields)
|
sl_entries = get_sl_entries_for_preview(doc.doctype, doc.name, fields)
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,17 @@ erpnext.buying = {
|
|||||||
this.setup_queries(doc, cdt, cdn);
|
this.setup_queries(doc, cdt, cdn);
|
||||||
super.onload();
|
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 {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
"shipping_rule_type": "Buying"
|
"shipping_rule_type": "Buying"
|
||||||
|
|||||||
@@ -394,6 +394,10 @@ erpnext.PointOfSale.Payment = class {
|
|||||||
const payments = doc.payments;
|
const payments = doc.payments;
|
||||||
const currency = doc.currency;
|
const currency = doc.currency;
|
||||||
|
|
||||||
|
if (!this.$payment_modes.is(":visible")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.$payment_modes.html(
|
this.$payment_modes.html(
|
||||||
`${payments
|
`${payments
|
||||||
.map((p, i) => {
|
.map((p, i) => {
|
||||||
@@ -612,6 +616,10 @@ erpnext.PointOfSale.Payment = class {
|
|||||||
const currency = doc.currency;
|
const currency = doc.currency;
|
||||||
const label = __("Change Amount");
|
const label = __("Change Amount");
|
||||||
|
|
||||||
|
if (!this.$totals.is(":visible")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.$totals.html(
|
this.$totals.html(
|
||||||
`<div class="col">
|
`<div class="col">
|
||||||
<div class="total-label">${__("Grand Total")}</div>
|
<div class="total-label">${__("Grand Total")}</div>
|
||||||
|
|||||||
@@ -763,13 +763,11 @@ def update_company_current_month_sales(company):
|
|||||||
|
|
||||||
def update_company_monthly_sales(company):
|
def update_company_monthly_sales(company):
|
||||||
"""Cache past year monthly sales of every company based on sales invoices"""
|
"""Cache past year monthly sales of every company based on sales invoices"""
|
||||||
import json
|
|
||||||
|
|
||||||
from frappe.utils.goal import get_monthly_results
|
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(
|
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))
|
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'",
|
"depends_on": "eval:doc.salary_mode == 'Bank'",
|
||||||
"fieldname": "iban",
|
"fieldname": "iban",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "IBAN"
|
"label": "IBAN",
|
||||||
|
"options": "IBAN"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-user",
|
"icon": "fa fa-user",
|
||||||
@@ -822,7 +823,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"is_tree": 1,
|
"is_tree": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-07-04 08:29:34.347269",
|
"modified": "2025-08-29 11:52:12.819878",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Setup",
|
"module": "Setup",
|
||||||
"name": "Employee",
|
"name": "Employee",
|
||||||
@@ -864,6 +865,7 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"search_fields": "employee_name",
|
"search_fields": "employee_name",
|
||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
@@ -871,4 +873,4 @@
|
|||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "employee_name",
|
"title_field": "employee_name",
|
||||||
"track_changes": 1
|
"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:
|
if sle.is_adjustment_entry and flt(sle.qty_after_transaction, self.flt_precision) == 0:
|
||||||
sle.stock_value_difference = (
|
sle.stock_value_difference = (
|
||||||
get_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
|
* -1
|
||||||
)
|
)
|
||||||
@@ -2353,7 +2357,9 @@ def is_internal_transfer(sle):
|
|||||||
return True
|
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")
|
table = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
posting_datetime = get_combine_datetime(posting_date, posting_time)
|
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)
|
query = query.where(table.voucher_no != voucher_no)
|
||||||
|
|
||||||
difference_amount = query.run()
|
difference_amount = query.run()
|
||||||
|
|||||||
@@ -484,10 +484,16 @@ def get_documents_with_active_service_level_agreement():
|
|||||||
|
|
||||||
|
|
||||||
def set_documents_with_active_service_level_agreement():
|
def set_documents_with_active_service_level_agreement():
|
||||||
active = frozenset(
|
try:
|
||||||
sla.document_type for sla in frappe.get_all("Service Level Agreement", fields=["document_type"])
|
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)
|
)
|
||||||
|
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
|
return active
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user