mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-16 11:48:48 +00:00
Compare commits
32 Commits
version-16
...
l10n_versi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44ea791da8 | ||
|
|
1aa509c7ee | ||
|
|
e17b7fbdcb | ||
|
|
77928224fb | ||
|
|
691c1d6e31 | ||
|
|
b227bacf68 | ||
|
|
d417a65e3c | ||
|
|
7ccbfdf186 | ||
|
|
957f91c6a3 | ||
|
|
deccfe7717 | ||
|
|
66c151cc2c | ||
|
|
7a77d51a29 | ||
|
|
194604eabc | ||
|
|
8970c5d708 | ||
|
|
3f8b9a0a2c | ||
|
|
9f96589c39 | ||
|
|
0ea48fe3b0 | ||
|
|
253e14c748 | ||
|
|
5e950a8b0f | ||
|
|
ad61dbee35 | ||
|
|
f4b1210551 | ||
|
|
21459078df | ||
|
|
f19bbbe3dc | ||
|
|
acf4a9690d | ||
|
|
4c13d31b8a | ||
|
|
2ce5105b71 | ||
|
|
d9cbd16d4d | ||
|
|
5f38291a69 | ||
|
|
db48b449c6 | ||
|
|
15c98d3f01 | ||
|
|
7a42b70830 | ||
|
|
8f623e2695 |
@@ -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", "");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
2429
erpnext/locale/ar.po
2429
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
1892
erpnext/locale/bg.po
1892
erpnext/locale/bg.po
File diff suppressed because it is too large
Load Diff
4280
erpnext/locale/bs.po
4280
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
2453
erpnext/locale/cs.po
2453
erpnext/locale/cs.po
File diff suppressed because it is too large
Load Diff
1900
erpnext/locale/da.po
1900
erpnext/locale/da.po
File diff suppressed because it is too large
Load Diff
2361
erpnext/locale/de.po
2361
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
21841
erpnext/locale/eo.po
21841
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
2473
erpnext/locale/es.po
2473
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
2151
erpnext/locale/fa.po
2151
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
2461
erpnext/locale/fr.po
2461
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
1910
erpnext/locale/hi.po
1910
erpnext/locale/hi.po
File diff suppressed because it is too large
Load Diff
3298
erpnext/locale/hr.po
3298
erpnext/locale/hr.po
File diff suppressed because it is too large
Load Diff
2439
erpnext/locale/hu.po
2439
erpnext/locale/hu.po
File diff suppressed because it is too large
Load Diff
2310
erpnext/locale/id.po
2310
erpnext/locale/id.po
File diff suppressed because it is too large
Load Diff
2442
erpnext/locale/it.po
2442
erpnext/locale/it.po
File diff suppressed because it is too large
Load Diff
2045
erpnext/locale/ko.po
2045
erpnext/locale/ko.po
File diff suppressed because it is too large
Load Diff
1899
erpnext/locale/my.po
1899
erpnext/locale/my.po
File diff suppressed because it is too large
Load Diff
2029
erpnext/locale/nb.po
2029
erpnext/locale/nb.po
File diff suppressed because it is too large
Load Diff
2349
erpnext/locale/nl.po
2349
erpnext/locale/nl.po
File diff suppressed because it is too large
Load Diff
2419
erpnext/locale/pl.po
2419
erpnext/locale/pl.po
File diff suppressed because it is too large
Load Diff
2446
erpnext/locale/pt.po
2446
erpnext/locale/pt.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2328
erpnext/locale/ru.po
2328
erpnext/locale/ru.po
File diff suppressed because it is too large
Load Diff
2016
erpnext/locale/sl.po
2016
erpnext/locale/sl.po
File diff suppressed because it is too large
Load Diff
2109
erpnext/locale/sr.po
2109
erpnext/locale/sr.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2181
erpnext/locale/sv.po
2181
erpnext/locale/sv.po
File diff suppressed because it is too large
Load Diff
2102
erpnext/locale/th.po
2102
erpnext/locale/th.po
File diff suppressed because it is too large
Load Diff
2088
erpnext/locale/tr.po
2088
erpnext/locale/tr.po
File diff suppressed because it is too large
Load Diff
2153
erpnext/locale/uz.po
2153
erpnext/locale/uz.po
File diff suppressed because it is too large
Load Diff
2168
erpnext/locale/vi.po
2168
erpnext/locale/vi.po
File diff suppressed because it is too large
Load Diff
2136
erpnext/locale/zh.po
2136
erpnext/locale/zh.po
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user