diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 99b4518b695..43a7b21aabc 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -517,6 +517,7 @@ def get_account_autoname(account_number, account_name, company): def update_account_number(name, account_name, account_number=None, from_descendant=False): _ensure_idle_system() account = frappe.get_cached_doc("Account", name) + account.check_permission("write") if not account: return diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json index 3210457c645..b57b257cbef 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json @@ -1,7 +1,6 @@ { "actions": [], "autoname": "format:Bank Statement Import on {creation}", - "beta": 1, "creation": "2019-08-04 14:16:08.318714", "doctype": "DocType", "editable_grid": 1, @@ -211,10 +210,11 @@ ], "hide_toolbar": 1, "links": [], - "modified": "2024-06-25 17:32:07.658250", + "modified": "2026-05-30 20:51:10.353723", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Statement Import", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -230,7 +230,9 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index 496097417ba..948fb84cf25 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -2,7 +2,6 @@ "actions": [], "allow_events_in_timeline": 1, "autoname": "naming_series:", - "beta": 1, "creation": "2019-07-05 16:34:31.013238", "doctype": "DocType", "engine": "InnoDB", @@ -400,7 +399,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2024-11-26 13:46:07.760867", + "modified": "2026-05-30 20:40:30.851842", "modified_by": "Administrator", "module": "Accounts", "name": "Dunning", @@ -449,9 +448,10 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "ASC", "states": [], "title_field": "customer_name", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.json b/erpnext/accounts/doctype/dunning_type/dunning_type.json index 5e39769735e..857a5a1f31d 100644 --- a/erpnext/accounts/doctype/dunning_type/dunning_type.json +++ b/erpnext/accounts/doctype/dunning_type/dunning_type.json @@ -1,7 +1,6 @@ { "actions": [], "allow_rename": 1, - "beta": 1, "creation": "2019-12-04 04:59:08.003664", "doctype": "DocType", "editable_grid": 1, @@ -107,7 +106,7 @@ "link_fieldname": "dunning_type" } ], - "modified": "2021-11-13 00:25:35.659283", + "modified": "2026-05-30 20:40:09.952533", "modified_by": "Administrator", "module": "Accounts", "name": "Dunning Type", @@ -151,7 +150,9 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 181edb4cd6b..ae3ee00e535 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -378,15 +378,17 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro accounts_add(doc, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); - row.exchange_rate = 1; - $.each(doc.accounts, function (i, d) { - if (d.account && d.party && d.party_type) { - row.account = d.account; - row.party = d.party; - row.party_type = d.party_type; - row.exchange_rate = d.exchange_rate; - } - }); + if (!row.exchange_rate) row.exchange_rate = 1; + if (!row.account) { + $.each(doc.accounts, function (i, d) { + if (d.account && d.party && d.party_type) { + row.account = d.account; + row.party = d.party; + row.party_type = d.party_type; + row.exchange_rate = d.exchange_rate; + } + }); + } // set difference if (doc.difference) { diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json index 5a43e1dbf3d..e39e4041e7a 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json @@ -1,6 +1,6 @@ { + "actions": [], "allow_copy": 1, - "beta": 1, "creation": "2017-08-29 02:22:54.947711", "doctype": "DocType", "editable_grid": 1, @@ -64,10 +64,10 @@ "options": "Cost Center" }, { - "fieldname": "project", - "fieldtype": "Link", - "label": "Project", - "options": "Project" + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" }, { "collapsible": 1, @@ -82,7 +82,8 @@ ], "hide_toolbar": 1, "issingle": 1, - "modified": "2022-01-04 15:25:06.053187", + "links": [], + "modified": "2026-05-30 20:43:36.282738", "modified_by": "Administrator", "module": "Accounts", "name": "Opening Invoice Creation Tool", @@ -99,7 +100,9 @@ } ], "quick_entry": 1, + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index 4f133a2fb7d..4003f533ded 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -31,6 +31,7 @@ class OpeningInvoiceCreationTool(Document): create_missing_party: DF.Check invoice_type: DF.Literal["Sales", "Purchase"] invoices: DF.Table[OpeningInvoiceCreationToolItem] + project: DF.Link | None # end: auto-generated types def onload(self): diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index f18025c0174..14fc2b51b19 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1773,6 +1773,35 @@ frappe.ui.form.on("Payment Entry", { }, }); }, + + before_cancel: function (frm) { + return new Promise((resolve, reject) => { + frappe.call({ + method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_linked_bank_transactions", + args: { payment_entry: frm.doc.name }, + callback: function (r) { + const linked = r.message || []; + if (!linked.length) { + resolve(); + return; + } + const bt_links = linked + .map((name) => frappe.utils.get_form_link("Bank Transaction", name, true)) + .join(", "); + frappe.confirm( + __( + "This Payment Entry is reconciled with {0}. Cancelling will automatically unreconcile it. Do you want to proceed?", + [bt_links] + ), + () => resolve(), + () => reject(), + __("Yes"), + __("No") + ); + }, + }); + }); + }, }); frappe.ui.form.on("Payment Entry Reference", { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 098a90acbbf..34120776681 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -3599,3 +3599,16 @@ def make_payment_order(source_name, target_doc=None): @erpnext.allow_regional def add_regional_gl_entries(gl_entries, doc): return + + +@frappe.whitelist() +def get_linked_bank_transactions(payment_entry: str) -> list: + frappe.has_permission("Payment Entry", ptype="read", doc=payment_entry, throw=True) + return frappe.get_all( + "Bank Transaction Payments", + filters={ + "payment_document": "Payment Entry", + "payment_entry": payment_entry, + }, + pluck="parent", + ) diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index 882b8c58eee..82c3103a039 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -202,15 +202,14 @@ class POSProfile(Document): def set_defaults(self, include_current_pos=True): frappe.defaults.clear_default("is_pos") - if not include_current_pos: - condition = " where pfu.name != '%s' and pfu.default = 1 " % self.name.replace("'", "'") - else: - condition = " where pfu.default = 1 " + pfu = frappe.qb.DocType("POS Profile User") - pos_view_users = frappe.db.sql_list( - f"""select pfu.user - from `tabPOS Profile User` as pfu {condition}""" - ) + query = frappe.qb.from_(pfu).select(pfu.user).where(pfu.default == 1) + + if not include_current_pos: + query = query.where(pfu.name != self.name) + + pos_view_users = query.run(as_list=1, pluck=True) for user in pos_view_users: if user: diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json index bfd4e0ad63a..5dc71280497 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json @@ -151,13 +151,13 @@ "label": "Default Advance Account", "mandatory_depends_on": "doc.party_type", "options": "Account", - "reqd": 1 + "reqd": 0 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-01-08 08:22:14.798085", + "modified": "2026-05-16 11:43:12.758685", "modified_by": "Administrator", "module": "Accounts", "name": "Process Payment Reconciliation", diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py index f650806966f..c4c6160fe52 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py @@ -23,7 +23,7 @@ class ProcessPaymentReconciliation(Document): bank_cash_account: DF.Link | None company: DF.Link cost_center: DF.Link | None - default_advance_account: DF.Link + default_advance_account: DF.Link | None error_log: DF.LongText | None from_invoice_date: DF.Date | None from_payment_date: DF.Date | None @@ -215,10 +215,7 @@ def trigger_reconciliation_for_queued_docs(): fields = ["company", "party_type", "party", "receivable_payable_account", "default_advance_account"] def get_filters_as_tuple(fields, doc): - filters = () - for x in fields: - filters += tuple(doc.get(x)) - return filters + return tuple(doc.get(x) or "" for x in fields) for x in all_queued: doc = frappe.get_doc("Process Payment Reconciliation", x) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 791dc3088bc..95b0ec8b389 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -6,6 +6,7 @@ import json import frappe from frappe import _ +from frappe.contacts.doctype.contact.contact import get_full_name from frappe.core.doctype.communication.email import make from frappe.desk.form.load import get_attachments from frappe.model.mapper import get_mapped_doc @@ -272,12 +273,20 @@ class RequestforQuotation(BuyingController): supplier_doc.save() def create_user(self, rfq_supplier, link): + contact_name = None + if rfq_supplier.contact: + name_fields = frappe.get_value( + "Contact", rfq_supplier.contact, ["first_name", "middle_name", "last_name"] + ) + if name_fields: + contact_name = get_full_name(*name_fields) + user = frappe.get_doc( { "doctype": "User", "send_welcome_email": 0, "email": rfq_supplier.email_id, - "first_name": rfq_supplier.supplier_name or rfq_supplier.supplier, + "first_name": contact_name or rfq_supplier.supplier_name or rfq_supplier.supplier, "user_type": "Website User", "redirect_url": link, } diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index b5bcadbdabc..3a5e7168034 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -370,10 +370,14 @@ def get_delivery_notes_to_be_billed( .where((DeliveryNote.docstatus == 1) & (DeliveryNote.is_return == 0) & (DeliveryNote.per_billed > 0)) ) + query = frappe.qb.get_query( + "Delivery Note", + fields=fields, + filters=filters, + ) + query = ( - frappe.qb.from_(DeliveryNote) - .select(*[DeliveryNote[f] for f in fields]) - .where( + query.where( (DeliveryNote.docstatus == 1) & (DeliveryNote.status.notin(["Stopped", "Closed"])) & (DeliveryNote[searchfield].like(f"%{txt}%")) @@ -387,12 +391,11 @@ def get_delivery_notes_to_be_billed( ) ) ) + .orderby(DeliveryNote[searchfield], order=Order.asc) + .limit(page_len) + .offset(start) ) - if filters and isinstance(filters, dict): - for key, value in filters.items(): - query = query.where(DeliveryNote[key] == value) - query = query.orderby(DeliveryNote[searchfield], order=Order.asc).limit(page_len).offset(start) return query.run(as_dict=as_dict) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index a4ee4daadb9..d9ca43bab60 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -1680,7 +1680,7 @@ def repost_required_for_queue(doc: StockController) -> bool: @frappe.whitelist() -def check_item_quality_inspection(doctype, items): +def check_item_quality_inspection(doctype: str, docstatus: str | int, items: str | list[dict]): if isinstance(items, str): items = json.loads(items) @@ -1692,13 +1692,30 @@ def check_item_quality_inspection(doctype, items): "Delivery Note": "inspection_required_before_delivery", } - items_to_remove = [] - for item in items: - if not frappe.db.get_value("Item", item.get("item_code"), inspection_fieldname_map.get(doctype)): - items_to_remove.append(item) - items = [item for item in items if item not in items_to_remove] + inspection_fieldname = inspection_fieldname_map.get(doctype) + if inspection_fieldname is None: + return [] - return items + allow_after_transaction = cint(docstatus) == 1 and frappe.get_single_value( + "Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery" + ) + + if allow_after_transaction: + return items + + item_codes = list({item.get("item_code") for item in items}) + + Item = frappe.qb.DocType("Item") + results = ( + frappe.qb.from_(Item) + .select(Item.name) + .where((Item.name.isin(item_codes)) & (Item[inspection_fieldname] == 1)) + .run(as_dict=True) + ) + + inspection_required_items = {row.name for row in results} + + return [item for item in items if item.get("item_code") in inspection_required_items] @frappe.whitelist() diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 3895f0bf17a..06b329e169e 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -438,7 +438,7 @@ frappe.ui.form.on("BOM", { }, routing(frm) { - if (frm.doc.routing) { + if (frm.doc.routing && frm.doc.with_operations && !frm.doc.operations) { frappe.call({ doc: frm.doc, method: "get_routing", diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index a40d0d714ed..5ba6157d009 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -126,11 +126,13 @@ "label": "Image" }, { + "default": "1", "fetch_from": "operation.batch_size", "fetch_if_empty": 1, "fieldname": "batch_size", - "fieldtype": "Int", - "label": "Batch Size" + "fieldtype": "Float", + "label": "Batch Size", + "non_negative": 1 }, { "depends_on": "eval:doc.parenttype == \"Routing\" || !parent.routing", @@ -196,13 +198,14 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-02-17 15:33:28.495850", + "modified": "2026-05-27 12:09:44.797434", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", "owner": "Administrator", "permissions": [], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", "states": [] -} +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.py b/erpnext/manufacturing/doctype/bom_operation/bom_operation.py index 66ac02891b9..02c4acc5881 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.py +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.py @@ -17,7 +17,7 @@ class BOMOperation(Document): base_cost_per_unit: DF.Float base_hour_rate: DF.Currency base_operating_cost: DF.Currency - batch_size: DF.Int + batch_size: DF.Float cost_per_unit: DF.Float description: DF.TextEditor | None fixed_time: DF.Check diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 0584320726f..5d8382e7ebd 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -158,7 +158,7 @@ class WorkOrder(Document): self.calculate_operating_cost() self.validate_qty() self.validate_transfer_against() - self.validate_operation_time() + self.validate_operations() self.status = self.get_status() self.validate_workstation_type() self.reset_use_multi_level_bom() @@ -1120,9 +1120,12 @@ class WorkOrder(Document): title=_("Missing value"), ) - def validate_operation_time(self): + def validate_operations(self): for d in self.operations: - if not d.time_in_mins > 0: + if not d.batch_size or d.batch_size <= 0: + d.batch_size = 1 + + if d.time_in_mins <= 0: frappe.throw(_("Operation Time must be greater than 0 for Operation {0}").format(d.operation)) def update_required_items(self): diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index 38b325b73ab..9c895251a51 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -185,10 +185,11 @@ "read_only": 1 }, { + "default": "1", "fieldname": "batch_size", "fieldtype": "Float", "label": "Batch Size", - "read_only": 1 + "non_negative": 1 }, { "fieldname": "sequence_id", @@ -225,14 +226,15 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-05-15 15:10:06.885440", + "modified": "2026-05-27 12:56:37.240431", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation", "owner": "Administrator", "permissions": [], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/portal/doctype/homepage/homepage.json b/erpnext/portal/doctype/homepage/homepage.json index 2b891f72680..8c19157d74a 100644 --- a/erpnext/portal/doctype/homepage/homepage.json +++ b/erpnext/portal/doctype/homepage/homepage.json @@ -1,6 +1,5 @@ { "actions": [], - "beta": 1, "creation": "2016-04-22 05:27:52.109319", "doctype": "DocType", "document_type": "Setup", @@ -87,7 +86,7 @@ ], "issingle": 1, "links": [], - "modified": "2022-12-19 21:10:29.127277", + "modified": "2026-05-30 20:51:04.415019", "modified_by": "Administrator", "module": "Portal", "name": "Homepage", @@ -114,6 +113,7 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", "states": [], diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 7c1981973a9..42163c1a6c3 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2506,11 +2506,29 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe method: "erpnext.controllers.stock_controller.check_item_quality_inspection", args: { doctype: this.frm.doc.doctype, - items: this.frm.doc.items + docstatus: this.frm.doc.docstatus, + items: this.frm.doc.items, }, freeze: true, callback: function (r) { - r.message.forEach(item => { + if (r.message.length == 0) { + let type = inspection_type === "Incoming" ? "Purchase" : "Delivery"; + let fieldname = + inspection_type === "Incoming" + ? "Inspection Required before Purchase" + : "Inspection Required before Delivery"; + + frappe.msgprint({ + title: __("Quality Inspection Not Configured"), + message: __(`Enable {0} on the Item master to proceed with {1} inspection.`, [ + fieldname, + type, + ]), + }); + return; + } + + r.message.forEach((item) => { if (me.has_inspection_required(item)) { let dialog_items = dialog.fields_dict.items; dialog_items.df.data.push({ diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index ae5b980bb9b..58f18dd731a 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -308,7 +308,6 @@ "read_only": 1 }, { - "depends_on": "eval:(doc.quotation_to=='Customer' && doc.party_name)", "fieldname": "col_break98", "fieldtype": "Column Break", "width": "50%" @@ -1108,7 +1107,7 @@ "idx": 82, "is_submittable": 1, "links": [], - "modified": "2025-07-31 17:23:48.875382", + "modified": "2026-05-30 17:40:02.667637", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 2a22865b403..eae6a1a4412 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -377,42 +377,80 @@ def get_past_order_list(search_term, status, limit=20): @frappe.whitelist() def set_customer_info(fieldname, customer, value=""): + customer_doc = frappe.get_doc("Customer", customer) + customer_doc.check_permission("write") + if fieldname == "loyalty_program": - frappe.db.set_value("Customer", customer, "loyalty_program", value) + customer_doc.loyalty_program = value + else: + contact = customer_doc.get("customer_primary_contact") + if not contact: + Contact = DocType("Contact") + DynamicLink = DocType("Dynamic Link") - contact = frappe.get_cached_value("Customer", customer, "customer_primary_contact") - if not contact: - contact = frappe.db.sql( - """ - SELECT parent FROM `tabDynamic Link` - WHERE - parenttype = 'Contact' AND - parentfield = 'links' AND - link_doctype = 'Customer' AND - link_name = %s - """, - (customer), - as_dict=1, - ) - contact = contact[0].get("parent") if contact else None + # Inner join with Contact DocType, to priorities records that have is_primary_contact set. + query = ( + frappe.qb.from_(DynamicLink) + .join(Contact) + .on(DynamicLink.parent == Contact.name) + .select(DynamicLink.parent) + .where( + (DynamicLink.link_name == customer) + & (DynamicLink.parentfield == "links") + & (DynamicLink.parenttype == "Contact") + & (DynamicLink.link_doctype == "Customer") + ) + .orderby(Contact.is_primary_contact, order=Order.desc) + ) - if not contact: - new_contact = frappe.new_doc("Contact") - new_contact.is_primary_contact = 1 - new_contact.first_name = customer - new_contact.set("links", [{"link_doctype": "Customer", "link_name": customer}]) - new_contact.save() - contact = new_contact.name - frappe.db.set_value("Customer", customer, "customer_primary_contact", contact) + contacts = query.run(pluck=DynamicLink.parent) - contact_doc = frappe.get_doc("Contact", contact) - if fieldname == "email_id": - contact_doc.set("email_ids", [{"email_id": value, "is_primary": 1}]) - frappe.db.set_value("Customer", customer, "email_id", value) - elif fieldname == "mobile_no": - contact_doc.set("phone_nos", [{"phone": value, "is_primary_mobile_no": 1}]) - frappe.db.set_value("Customer", customer, "mobile_no", value) - contact_doc.save() + contact = contacts[0] if contacts else None + + if not contact: + new_contact = frappe.new_doc("Contact") + new_contact.is_primary_contact = 1 + new_contact.first_name = customer + new_contact.set("links", [{"link_doctype": "Customer", "link_name": customer}]) + new_contact.save() + contact = new_contact.name + + def set_primary_phone_no_email(field, value): + # Create new record instead deleting existing email or phone_no and setting the new row as primary. + field_mapper = { + "email_ids": {"field": "email_id", "primary": "is_primary"}, + "phone_nos": {"field": "phone", "primary": "is_primary_mobile_no"}, + } + + value_already_exists = False + for d in contact_doc.get(field): + if d.get(field_mapper[field].get("field")) == value and not value_already_exists: + d.set(field_mapper[field]["primary"], 1) + value_already_exists = True + continue + d.set(field_mapper[field]["primary"], 0) + + if not value_already_exists: + contact_doc.append( + field, {field_mapper[field]["field"]: value, field_mapper[field]["primary"]: 1} + ) + + contact_doc = frappe.get_doc("Contact", contact) + # setting is_primary_contact = 1 on Contact to refetch the same contact incase it's removed from Customer records. + contact_doc.set("is_primary_contact", 1) + if fieldname == "email_id": + set_primary_phone_no_email("email_ids", value) + elif fieldname == "mobile_no": + set_primary_phone_no_email("phone_nos", value) + # Saving contact_doc to set mobile_no and email. + contact_doc.save() + + # Auto-fetches from Contact DocType, no need to set values separately. + customer_doc.customer_primary_contact = contact + + # using save method instead db.set_value which bypasses the validation for loyalty program + # and auto sets the mobile_no and email field on customer records. + customer_doc.save() @frappe.whitelist() diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index d7ef86da872..e232464b53a 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -174,8 +174,8 @@ erpnext.PointOfSale.Controller = class { set_opening_entry_status() { this.page.set_title_sub( ` - - Opened at ${frappe.datetime.str_to_user(this.pos_opening_time)} + + Opened at ${frappe.utils.escape_html(frappe.datetime.str_to_user(this.pos_opening_time))} ` ); diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 6f3c1d9ccb6..1461a3fb395 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -178,7 +178,7 @@ erpnext.PointOfSale.ItemCart = class { me.$totals_section.find(".edit-cart-btn").click(); } - const item_row_name = unescape($cart_item.attr("data-row-name")); + const item_row_name = $cart_item.attr("data-row-name"); me.events.cart_item_clicked({ name: item_row_name }); this.numpad_value = ""; }); @@ -453,10 +453,10 @@ erpnext.PointOfSale.ItemCart = class {
{{ __("Comments") }}
+{{ _("Comments") }}
{{comment.sender_full_name}}: - {{comment.subject}} {{ __("on") }} {{comment.creation.strftime('%Y-%m-%d')}}
+{{comment.comment_email}}: + {{comment.content|e}} {{ _("on") }} {{comment.creation.strftime('%Y-%m-%d')}}
{% endfor %}{{ __("Add Comment") }}
- -