Compare commits

..

32 Commits

Author SHA1 Message Date
MochaMind
44ea791da8 fix: Esperanto translations 2026-07-15 18:17:37 +05:30
MochaMind
1aa509c7ee fix: Serbian (Latin) translations 2026-07-15 18:17:32 +05:30
MochaMind
e17b7fbdcb fix: Norwegian Bokmal translations 2026-07-15 18:17:27 +05:30
MochaMind
77928224fb fix: Uzbek translations 2026-07-15 18:17:23 +05:30
MochaMind
691c1d6e31 fix: Bosnian translations 2026-07-15 18:17:18 +05:30
MochaMind
b227bacf68 fix: Burmese translations 2026-07-15 18:17:13 +05:30
MochaMind
d417a65e3c fix: Hindi translations 2026-07-15 18:17:09 +05:30
MochaMind
7ccbfdf186 fix: Croatian translations 2026-07-15 18:17:04 +05:30
MochaMind
957f91c6a3 fix: Thai translations 2026-07-15 18:17:00 +05:30
MochaMind
deccfe7717 fix: Persian translations 2026-07-15 18:16:55 +05:30
MochaMind
66c151cc2c fix: Indonesian translations 2026-07-15 18:16:50 +05:30
MochaMind
7a77d51a29 fix: Portuguese, Brazilian translations 2026-07-15 18:16:45 +05:30
MochaMind
194604eabc fix: Vietnamese translations 2026-07-15 18:16:41 +05:30
MochaMind
8970c5d708 fix: Chinese Simplified translations 2026-07-15 18:16:36 +05:30
MochaMind
3f8b9a0a2c fix: Turkish translations 2026-07-15 18:16:32 +05:30
MochaMind
9f96589c39 fix: Swedish translations 2026-07-15 18:16:26 +05:30
MochaMind
0ea48fe3b0 fix: Serbian (Cyrillic) translations 2026-07-15 18:16:22 +05:30
MochaMind
253e14c748 fix: Slovenian translations 2026-07-15 18:16:17 +05:30
MochaMind
5e950a8b0f fix: Russian translations 2026-07-15 18:16:11 +05:30
MochaMind
ad61dbee35 fix: Portuguese translations 2026-07-15 18:16:05 +05:30
MochaMind
f4b1210551 fix: Polish translations 2026-07-15 18:16:00 +05:30
MochaMind
21459078df fix: Dutch translations 2026-07-15 18:15:55 +05:30
MochaMind
f19bbbe3dc fix: Korean translations 2026-07-15 18:15:50 +05:30
MochaMind
acf4a9690d fix: Italian translations 2026-07-15 18:15:46 +05:30
MochaMind
4c13d31b8a fix: Hungarian translations 2026-07-15 18:15:41 +05:30
MochaMind
2ce5105b71 fix: German translations 2026-07-15 18:15:36 +05:30
MochaMind
d9cbd16d4d fix: Danish translations 2026-07-15 18:15:32 +05:30
MochaMind
5f38291a69 fix: Czech translations 2026-07-15 18:15:27 +05:30
MochaMind
db48b449c6 fix: Bulgarian translations 2026-07-15 18:15:23 +05:30
MochaMind
15c98d3f01 fix: Arabic translations 2026-07-15 18:15:19 +05:30
MochaMind
7a42b70830 fix: Spanish translations 2026-07-15 18:15:14 +05:30
MochaMind
8f623e2695 fix: French translations 2026-07-15 18:15:10 +05:30
43 changed files with 56269 additions and 37534 deletions

View File

@@ -169,10 +169,23 @@ frappe.ui.form.on("Dunning", {
},
get_dunning_letter_text: function (frm) {
if (frm.doc.dunning_type) {
frm.call("get_dunning_letter_text").then((r) => {
if (!r.exc) {
frm.refresh_fields();
}
frappe.call({
method: "erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text",
args: {
dunning_type: frm.doc.dunning_type,
language: frm.doc.language,
doc: frm.doc,
},
callback: function (r) {
if (r.message) {
frm.set_value("body_text", r.message.body_text);
frm.set_value("closing_text", r.message.closing_text);
frm.set_value("language", r.message.language);
} else {
frm.set_value("body_text", "");
frm.set_value("closing_text", "");
}
},
});
}
},

View File

@@ -163,46 +163,6 @@ class Dunning(AccountsController):
"Serial and Batch Bundle",
]
@frappe.whitelist()
def get_dunning_letter_text(self):
DOCTYPE = "Dunning Letter Text"
FIELDS = ["body_text", "closing_text", "language"]
if not self.dunning_type:
return
filters = {"parent": self.dunning_type, "is_default_language": 1}
if self.language:
filters.pop("is_default_language")
filters["language"] = self.language
letter_text = frappe.db.get_value(DOCTYPE, filters, FIELDS, as_dict=True)
if not letter_text:
msg = (
_("Dunning Letter for Dunning Type {0} in language '{1}' not found.").format(
frappe.bold(self.dunning_type), frappe.bold(self.language)
)
if self.language
else _("Dunning Letter for Dunning Type {0} not found.").format(
frappe.bold(self.dunning_type)
)
)
frappe.msgprint(msg, alert=True, indicator="yellow")
self.body_text = (
frappe.render_template(letter_text.body_text, self.as_dict(), restrict_globals=True)
if letter_text
else None
)
self.closing_text = (
frappe.render_template(letter_text.closing_text, self.as_dict(), restrict_globals=True)
if letter_text
else None
)
self.language = letter_text.language if letter_text else self.language
def update_linked_dunnings(doc, previous_outstanding_amount):
if (
@@ -281,3 +241,35 @@ def get_linked_dunnings_as_per_state(sales_invoice, state):
& (overdue_payment.sales_invoice == sales_invoice)
)
).run(as_dict=True)
@frappe.whitelist()
def get_dunning_letter_text(dunning_type: str, doc: str | dict, language: str | None = None) -> dict:
DOCTYPE = "Dunning Letter Text"
FIELDS = ["body_text", "closing_text", "language"]
if isinstance(doc, str):
doc = json.loads(doc)
if not language:
language = doc.get("language")
letter_text = None
if language:
letter_text = frappe.db.get_value(
DOCTYPE, {"parent": dunning_type, "language": language}, FIELDS, as_dict=1
)
if not letter_text:
letter_text = frappe.db.get_value(
DOCTYPE, {"parent": dunning_type, "is_default_language": 1}, FIELDS, as_dict=1
)
if not letter_text:
return {}
return {
"body_text": frappe.render_template(letter_text.body_text, doc),
"closing_text": frappe.render_template(letter_text.closing_text, doc),
"language": letter_text.language,
}

View File

@@ -3156,6 +3156,8 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False):
from frappe.model.mapper import get_mapped_doc
def postprocess_dunning(source, target):
from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text
dunning_type = frappe.db.exists("Dunning Type", {"is_default": 1, "company": source.company})
if dunning_type:
dunning_type = frappe.get_doc("Dunning Type", dunning_type)
@@ -3164,8 +3166,14 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False):
target.dunning_fee = dunning_type.dunning_fee
target.income_account = dunning_type.income_account
target.cost_center = dunning_type.cost_center
target.language = source.language
target.get_dunning_letter_text()
letter_text = get_dunning_letter_text(
dunning_type=dunning_type.name, doc=target.as_dict(), language=source.language
)
if letter_text:
target.body_text = letter_text.get("body_text")
target.closing_text = letter_text.get("closing_text")
target.language = letter_text.get("language")
# update outstanding from doc
if source.payment_schedule and len(source.payment_schedule) == 1:

View File

@@ -16,10 +16,7 @@ from erpnext.accounts.party import (
validate_party_accounts,
validate_party_currency_before_merging,
)
from erpnext.controllers.website_list_for_contact import (
add_role_for_portal_user,
link_portal_users_to_contacts,
)
from erpnext.controllers.website_list_for_contact import add_role_for_portal_user
from erpnext.utilities.transaction_base import TransactionBase
@@ -112,7 +109,6 @@ class Supplier(TransactionBase):
def on_update(self):
self.create_primary_contact()
self.create_primary_address()
link_portal_users_to_contacts(self)
def add_role_for_user(self):
for portal_user in self.portal_users:

View File

@@ -203,24 +203,3 @@ class TestSupplierPortal(ERPNextTestSuite):
_, suppliers = get_customers_suppliers("Purchase Order", user)
self.assertIn(supplier.name, suppliers)
def test_portal_user_contact_link(self):
user_email = frappe.generate_hash() + "@example.com"
user = frappe.new_doc("User")
user.email = user_email
user.first_name = "Test Portal Contact User"
user.send_welcome_email = False
user.insert(ignore_permissions=True)
contact = frappe.new_doc("Contact")
contact.first_name = "Test Portal Contact User"
contact.add_email(user_email, is_primary=1)
contact.links = []
contact.insert(ignore_permissions=True)
supplier = create_supplier()
supplier.append("portal_users", {"user": user.name})
supplier.save()
contact.reload()
self.assertTrue(contact.has_link("Supplier", supplier.name))

View File

@@ -7,8 +7,6 @@ import json
import frappe
from frappe import _
from frappe.modules.utils import get_module_app
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Lower
from frappe.utils import cint, flt, has_common
from frappe.utils.user import is_website_user
@@ -308,63 +306,3 @@ def add_role_for_portal_user(portal_user, role):
user_doc.add_roles(role)
frappe.msgprint(_("Added {1} Role to User {0}.").format(frappe.bold(user_doc.name), role), alert=True)
def link_portal_users_to_contacts(doc):
"""When portal users are added to Supplier/Customer, link them to the Contact profile."""
# a User's name is its (lowercased) email, so portal_users are already the emails
portal_users = {p.user for p in doc.get("portal_users") or [] if p.user}
if not portal_users:
return
before = doc.get_doc_before_save()
if before:
previous_users = {p.user for p in before.get("portal_users") or [] if p.user}
if portal_users == previous_users:
return
portal_users = list(portal_users)
contact = frappe.qb.DocType("Contact")
contact_email = frappe.qb.DocType("Contact Email")
query = (
frappe.qb.from_(contact)
.left_join(contact_email)
.on(contact_email.parent == contact.name)
.select(contact.name)
.distinct()
)
conditions = [
contact.user.isin(portal_users),
Lower(contact.email_id).isin(portal_users),
Lower(contact_email.email_id).isin(portal_users),
]
query = query.where(Criterion.any(conditions))
contacts = query.run(pluck=True)
if not contacts:
return
dynamic_link = frappe.qb.DocType("Dynamic Link")
existing_links = (
frappe.qb.from_(dynamic_link)
.select(dynamic_link.parent)
.where(
(dynamic_link.parenttype == "Contact")
& (dynamic_link.parent.isin(contacts))
& (dynamic_link.link_doctype == doc.doctype)
& (dynamic_link.link_name == doc.name)
)
.run(pluck=True)
)
contacts_to_link = [name for name in contacts if name not in existing_links]
for name in contacts_to_link:
contact_doc = frappe.get_doc("Contact", name)
if not contact_doc.has_link(doc.doctype, doc.name):
contact_doc.append("links", {"link_doctype": doc.doctype, "link_name": doc.name})
contact_doc.save(ignore_permissions=True)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -445,6 +445,8 @@ frappe.ui.form.on("Production Plan", {
frappe.throw(__("Select the Warehouse"));
}
frm.set_value("consider_minimum_order_qty", 0);
if (!frm.doc.ignore_existing_ordered_qty) {
frm.events.get_items_for_material_requests(frm);
} else {

View File

@@ -1823,13 +1823,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
if (ignore_existing_ordered_qty or get_parent_warehouse_data) and warehouses:
new_mr_items = []
for item in mr_items:
get_materials_from_other_locations(
item,
warehouses,
new_mr_items,
company,
consider_minimum_order_qty=doc.get("consider_minimum_order_qty"),
)
get_materials_from_other_locations(item, warehouses, new_mr_items, company)
mr_items = new_mr_items
@@ -1851,9 +1845,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
return mr_items
def get_materials_from_other_locations(
item, warehouses, new_mr_items, company, consider_minimum_order_qty=False
):
def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
from erpnext.stock.doctype.pick_list.pick_list import get_available_item_locations
purchase_uom = frappe.db.get_value("Item", item.get("item_code"), "purchase_uom")
@@ -1896,8 +1888,7 @@ def get_materials_from_other_locations(
precision = frappe.get_precision("Material Request Plan Item", "quantity")
if flt(required_qty, precision) > 0:
if consider_minimum_order_qty:
required_qty = max(required_qty, flt(item.get("min_order_qty")))
required_qty = required_qty
if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"):
required_qty = ceil(required_qty)

View File

@@ -2030,18 +2030,6 @@ class TestProductionPlan(ERPNextTestSuite):
for d in mr_items:
self.assertEqual(d.get("quantity"), 1000.0)
source_warehouse = create_warehouse("MOQ Source Warehouse", company="_Test Company")
make_stock_entry(item_code=rm_item, qty=7, rate=100, target=source_warehouse)
pln.ignore_existing_ordered_qty = 1
mr_items = get_items_for_material_requests(
pln.as_dict(), warehouses=[{"warehouse": source_warehouse}]
)
self.assertEqual(len(mr_items), 2)
items_by_type = {d.get("material_request_type"): d for d in mr_items}
self.assertEqual(items_by_type["Material Transfer"].get("quantity"), 7.0)
self.assertEqual(items_by_type["Purchase"].get("quantity"), 1000.0)
def test_fg_item_quantity(self):
fg_item = make_item(properties={"is_stock_item": 1}).name
rm_item = make_item(properties={"is_stock_item": 1}).name

View File

@@ -24,10 +24,7 @@ from erpnext.accounts.party import (
validate_party_accounts,
validate_party_currency_before_merging,
)
from erpnext.controllers.website_list_for_contact import (
add_role_for_portal_user,
link_portal_users_to_contacts,
)
from erpnext.controllers.website_list_for_contact import add_role_for_portal_user
from erpnext.utilities.transaction_base import TransactionBase
@@ -276,8 +273,6 @@ class Customer(TransactionBase):
self.update_customer_groups()
link_portal_users_to_contacts(self)
def add_role_for_user(self):
for portal_user in self.portal_users:
add_role_for_portal_user(portal_user, "Customer")

View File

@@ -398,33 +398,6 @@ class TestCustomer(ERPNextTestSuite):
self.assertEqual(middle, "Michael")
self.assertEqual(last, "Doe")
def test_portal_user_contact_link(self):
user_email = frappe.generate_hash() + "@example.com"
user = frappe.new_doc("User")
user.email = user_email
user.first_name = "Test Portal Customer User"
user.send_welcome_email = False
user.insert(ignore_permissions=True)
contact = frappe.new_doc("Contact")
contact.first_name = "Test Portal Customer User"
contact.add_email(user_email, is_primary=1)
contact.links = []
contact.insert(ignore_permissions=True)
customer = frappe.get_doc(
{
"doctype": "Customer",
"customer_name": "Test Portal Contact Customer",
"customer_type": "Individual",
}
)
customer.append("portal_users", {"user": user.name})
customer.insert()
contact.reload()
self.assertTrue(contact.has_link("Customer", customer.name))
def get_customer_dict(customer_name):
return {