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 {
${this.get_customer_image()}
-
${customer_name}
+
${frappe.utils.escape_html(customer_name)}
${get_customer_description()}
-
+
@@ -473,11 +473,13 @@ erpnext.PointOfSale.ItemCart = class { if (!email_id && !mobile_no) { return `
${__("Click to add email / phone")}
`; } else if (email_id && !mobile_no) { - return `
${email_id}
`; + return `
${frappe.utils.escape_html(email_id)}
`; } else if (mobile_no && !email_id) { - return `
${mobile_no}
`; + return `
${frappe.utils.escape_html(mobile_no)}
`; } else { - return `
${email_id} - ${mobile_no}
`; + return `
${frappe.utils.escape_html( + email_id + )} - ${frappe.utils.escape_html(mobile_no)}
`; } } } @@ -485,9 +487,13 @@ erpnext.PointOfSale.ItemCart = class { get_customer_image() { const { customer, image } = this.customer_info || {}; if (image) { - return `
${image}
`; + return `
${frappe.utils.escape_html(image)}
`; } else { - return `
${frappe.get_abbr(customer)}
`; + return `
${frappe.utils.escape_html( + frappe.get_abbr(customer) + )}
`; } } @@ -549,10 +555,10 @@ erpnext.PointOfSale.ItemCart = class { if (t.tax_amount_after_discount_amount == 0.0) return; // if tax rate is 0, don't print it. const description = /[0-9]+/.test(t.description) - ? t.description + ? frappe.utils.escape_html(t.description) : t.rate != 0 - ? `${t.description} @ ${t.rate}%` - : t.description; + ? `${frappe.utils.escape_html(t.description)} @ ${t.rate}%` + : frappe.utils.escape_html(t.description); return `
${description}
${format_currency(t.tax_amount_after_discount_amount, currency)}
@@ -566,8 +572,9 @@ erpnext.PointOfSale.ItemCart = class { } get_cart_item({ name }) { - const item_selector = `.cart-item-wrapper[data-row-name="${escape(name)}"]`; - return this.$cart_items_wrapper.find(item_selector); + return this.$cart_items_wrapper.find(".cart-item-wrapper").filter(function () { + return $(this).attr("data-row-name") === name; + }); } get_item_from_frm(item) { @@ -597,7 +604,9 @@ erpnext.PointOfSale.ItemCart = class { if (!$item_to_update.length) { this.$cart_items_wrapper.append( - `
+ `
` ); $item_to_update = this.get_cart_item(item_data); @@ -607,7 +616,7 @@ erpnext.PointOfSale.ItemCart = class { `${get_item_image_html()}
- ${item_data.item_name} + ${frappe.utils.escape_html(item_data.item_name)}
${get_description_html()}
@@ -636,7 +645,7 @@ erpnext.PointOfSale.ItemCart = class { if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) { return `
-
${item_data.qty || 0} ${item_data.uom}
+
${item_data.qty || 0} ${frappe.utils.escape_html(item_data.uom)}
${format_currency(item_data.amount, currency)}
${format_currency(item_data.rate, currency)}
@@ -645,7 +654,7 @@ erpnext.PointOfSale.ItemCart = class { } else { return `
-
${item_data.qty || 0} ${item_data.uom}
+
${item_data.qty || 0} ${frappe.utils.escape_html(item_data.uom)}
${format_currency(item_data.rate, currency)}
@@ -666,7 +675,7 @@ erpnext.PointOfSale.ItemCart = class { } } item_data.description = frappe.ellipsis(item_data.description, 45); - return `
${item_data.description}
`; + return `
${frappe.utils.escape_html(item_data.description)}
`; } return ``; } @@ -678,22 +687,26 @@ erpnext.PointOfSale.ItemCart = class {
${frappe.get_abbr(item_name)} + src="${frappe.utils.escape_html(image)}" alt="${frappe.utils.escape_html(frappe.get_abbr(item_name))}">
`; } else { - return `
${frappe.get_abbr(item_name)}
`; + return `
${frappe.utils.escape_html( + frappe.get_abbr(item_name) + )}
`; } } } handle_broken_image($img) { const item_abbr = $($img).attr("alt"); - $($img).parent().replaceWith(`
${item_abbr}
`); + $($img) + .parent() + .replaceWith(`
${frappe.utils.escape_html(item_abbr)}
`); } update_selector_value_in_cart_item(selector, value, item) { const $item_to_update = this.get_cart_item(item); - $item_to_update.attr(`data-${selector}`, escape(value)); + $item_to_update.attr(`data-${selector}`, value); } toggle_checkout_btn(show_checkout) { @@ -892,8 +905,8 @@ erpnext.PointOfSale.ItemCart = class {
${this.get_customer_image()}
-
${customer_name}
-
${customer}
+
${frappe.utils.escape_html(customer_name)}
+
${frappe.utils.escape_html(customer)}
@@ -980,6 +993,7 @@ erpnext.PointOfSale.ItemCart = class { customer: current_customer, value: this.value, }, + freeze: true, callback: (r) => { if (!r.exc) { me.customer_info[this.df.fieldname] = this.value; @@ -1029,9 +1043,11 @@ erpnext.PointOfSale.ItemCart = class { }; transaction_container.append( - `
+ `
-
${invoice.name}
+
${frappe.utils.escape_html(invoice.name)}
${posting_datetime}
@@ -1039,7 +1055,7 @@ erpnext.PointOfSale.ItemCart = class { ${format_currency(invoice.grand_total, invoice.currency, frappe.sys_defaults.currency_precision) || 0}
- + ${__(invoice.status)}
diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index 7dc780dad7e..3a518be8574 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -128,25 +128,27 @@ erpnext.PointOfSale.ItemDetails = class { return ``; } - this.$item_name.html(item_name); + this.$item_name.html(frappe.utils.escape_html(item_name)); this.$item_description.html(get_description_html()); this.$item_price.html(format_currency(price_list_rate, this.currency)); if (!this.hide_images && image) { this.$item_image.html( `${frappe.get_abbr(item_name)}` ); } else { - this.$item_image.html(`
${frappe.get_abbr(item_name)}
`); + this.$item_image.html( + `
${frappe.utils.escape_html(frappe.get_abbr(item_name))}
` + ); } } handle_broken_image($img) { const item_abbr = $($img).attr("alt"); - $($img).replaceWith(`
${item_abbr}
`); + $($img).replaceWith(`
${frappe.utils.escape_html(item_abbr)}
`); } render_discount_dom(item) { diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index e930322a2c4..f6530458b07 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -107,39 +107,45 @@ erpnext.PointOfSale.ItemSelector = class {
${frappe.get_abbr(item.item_name)}
`; } else { return `
${qty_to_display}
-
${frappe.get_abbr(item.item_name)}
`; +
${frappe.utils.escape_html(frappe.get_abbr(item.item_name))}
`; } } return `
+ data-item-code="${frappe.utils.escape_html(item.item_code)}" data-serial-no="${frappe.utils.escape_html( + serial_no + )}" + data-batch-no="${frappe.utils.escape_html(batch_no)}" data-uom="${frappe.utils.escape_html(uom)}" + data-rate="${frappe.utils.escape_html(price_list_rate || 0)}" + data-stock-uom="${frappe.utils.escape_html(item.stock_uom)}" + title="${frappe.utils.escape_html(item.item_name)}"> ${get_item_image_html()}
- ${frappe.ellipsis(item.item_name, 18)} + ${frappe.utils.escape_html(frappe.ellipsis(item.item_name, 18))}
-
${format_currency(price_list_rate, item.currency, precision) || 0} / ${uom}
+
${ + format_currency(price_list_rate, item.currency, precision) || 0 + } / ${frappe.utils.escape_html(uom)}
`; } handle_broken_image($img) { const item_abbr = $($img).attr("alt"); - $($img).parent().replaceWith(`
${item_abbr}
`); + $($img) + .parent() + .replaceWith(`
${frappe.utils.escape_html(item_abbr)}
`); } make_search_bar() { @@ -252,14 +258,13 @@ erpnext.PointOfSale.ItemSelector = class { this.$component.on("click", ".item-wrapper", function () { const $item = $(this); - const item_code = unescape($item.attr("data-item-code")); - let batch_no = unescape($item.attr("data-batch-no")); - let serial_no = unescape($item.attr("data-serial-no")); - let uom = unescape($item.attr("data-uom")); - let rate = unescape($item.attr("data-rate")); - let stock_uom = unescape($item.attr("data-stock-uom")); + const item_code = $item.attr("data-item-code"); + let batch_no = $item.attr("data-batch-no"); + let serial_no = $item.attr("data-serial-no"); + let uom = $item.attr("data-uom"); + let rate = $item.attr("data-rate"); + let stock_uom = $item.attr("data-stock-uom"); - // escape(undefined) returns "undefined" then unescape returns "undefined" batch_no = batch_no === "undefined" ? undefined : batch_no; serial_no = serial_no === "undefined" ? undefined : serial_no; uom = uom === "undefined" ? undefined : uom; diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_list.js b/erpnext/selling/page/point_of_sale/pos_past_order_list.js index 5ea58a43c09..1e606da46c3 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_list.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_list.js @@ -38,7 +38,7 @@ erpnext.PointOfSale.PastOrderList = class { }); const me = this; this.$invoices_container.on("click", ".invoice-wrapper", function () { - const invoice_name = unescape($(this).attr("data-invoice-name")); + const invoice_name = $(this).attr("data-invoice-name"); me.events.open_invoice_data(invoice_name); }); @@ -99,14 +99,14 @@ erpnext.PointOfSale.PastOrderList = class { const posting_datetime = frappe.datetime.str_to_user( invoice.posting_date + " " + invoice.posting_time ); - return `
+ return `
-
${invoice.name}
+
${frappe.utils.escape_html(invoice.name)}
- ${frappe.ellipsis(invoice.customer_name, 20)} + ${frappe.utils.escape_html(frappe.ellipsis(invoice.customer_name, 20))}
diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index 0a965c47f48..515669cdfc5 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -81,23 +81,27 @@ erpnext.PointOfSale.PastOrderSummary = class { return `
-
${doc.customer_name}
- ${is_customer_naming_by_customer_name ? `
${doc.customer}
` : ""} -
${this.customer_email}
+
${frappe.utils.escape_html(doc.customer_name)}
+ ${ + is_customer_naming_by_customer_name + ? `
${frappe.utils.escape_html(doc.customer)}
` + : "" + } +
${frappe.utils.escape_html(this.customer_email)}
-
${__("Sold by")}: ${doc.owner}
+
${__("Sold by")}: ${frappe.utils.escape_html(doc.owner)}
-
${doc.name}
+
${frappe.utils.escape_html(doc.name)}
${__(doc.status)}
`; } get_item_html(doc, item_data) { return `
-
${item_data.item_name}
-
${item_data.qty || 0} ${item_data.uom}
+
${frappe.utils.escape_html(item_data.item_name)}
+
${item_data.qty || 0} ${frappe.utils.escape_html(item_data.uom)}
${get_rate_discount_html()}
`; @@ -139,10 +143,10 @@ erpnext.PointOfSale.PastOrderSummary = class { .map((t) => { // if tax rate is 0, don't print it. const description = /[0-9]+/.test(t.description) - ? t.description + ? frappe.utils.escape_html(t.description) : t.rate != 0 - ? `${t.description} @ ${t.rate}%` - : t.description; + ? `${frappe.utils.escape_html(t.description)} @ ${t.rate}%` + : frappe.utils.escape_html(t.description); return `
${description}
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 165066a151a..7d995ac131c 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -408,8 +408,10 @@ erpnext.PointOfSale.Payment = class { return `
-
- ${p.mode_of_payment} +
+ ${frappe.utils.escape_html(p.mode_of_payment)}
${amount}
@@ -544,7 +546,7 @@ erpnext.PointOfSale.Payment = class {
Redeem Loyalty Points
${amount}
-
${loyalty_program}
+
${frappe.utils.escape_html(loyalty_program)}
` diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index d4f7ad42b43..5786adc6881 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -138,12 +138,30 @@ class Analytics: self.get_sales_transactions_based_on_project() self.get_rows() + def _get_permitted_parent_names(self): + return frappe.get_list( + self.filters.doc_type, + fields=["name"], + filters={ + "docstatus": 1, + "company": ["in", self.filters.company], + self.date_field: ("between", [self.filters.from_date, self.filters.to_date]), + }, + pluck="name", + ) + def get_sales_transactions_based_on_order_type(self): if self.filters["value_quantity"] == "Value": value_field = "base_net_total" else: value_field = "total_qty" + permitted_names = self._get_permitted_parent_names() + if not permitted_names: + self.entries = [] + self.get_teams() + return + doctype = DocType(self.filters.doc_type) self.entries = ( @@ -153,12 +171,7 @@ class Analytics: doctype[self.date_field], doctype[value_field].as_("value_field"), ) - .where( - (doctype.docstatus == 1) - & (doctype.company.isin(self.filters.company)) - & (doctype[self.date_field].between(self.filters.from_date, self.filters.to_date)) - & (IfNull(doctype.order_type, "") != "") - ) + .where((doctype.name.isin(permitted_names)) & (IfNull(doctype.order_type, "") != "")) .orderby(doctype.order_type) ).run(as_dict=True) @@ -186,8 +199,10 @@ class Analytics: if self.filters.doc_type in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]: filters.update({"is_opening": "No"}) - self.entries = frappe.get_all( - self.filters.doc_type, fields=[entity, entity_name, value_field, self.date_field], filters=filters + self.entries = frappe.get_list( + self.filters.doc_type, + fields=[entity, entity_name, value_field, self.date_field], + filters=filters, ) self.entity_names = {} @@ -200,6 +215,12 @@ class Analytics: else: value_field = "stock_qty" + permitted_names = self._get_permitted_parent_names() + if not permitted_names: + self.entries = [] + self.entity_names = {} + return + doctype = DocType(self.filters.doc_type) doctype_item = DocType(f"{self.filters.doc_type} Item") @@ -214,11 +235,7 @@ class Analytics: doctype_item[value_field].as_("value_field"), doctype[self.date_field], ) - .where( - (doctype_item.docstatus == 1) - & (doctype.company.isin(self.filters.company)) - & (doctype[self.date_field].between(self.filters.from_date, self.filters.to_date)) - ) + .where((doctype_item.docstatus == 1) & (doctype.name.isin(permitted_names))) ).run(as_dict=True) self.entity_names = {} @@ -248,7 +265,7 @@ class Analytics: if self.filters.doc_type in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]: filters.update({"is_opening": "No"}) - self.entries = frappe.get_all( + self.entries = frappe.get_list( self.filters.doc_type, fields=[entity_field, value_field, self.date_field], filters=filters, @@ -261,6 +278,12 @@ class Analytics: else: value_field = "qty" + permitted_names = self._get_permitted_parent_names() + if not permitted_names: + self.entries = [] + self.get_groups() + return + doctype = DocType(self.filters.doc_type) doctype_item = DocType(f"{self.filters.doc_type} Item") @@ -273,11 +296,7 @@ class Analytics: doctype_item[value_field].as_("value_field"), doctype[self.date_field], ) - .where( - (doctype_item.docstatus == 1) - & (doctype.company.isin(self.filters.company)) - & (doctype[self.date_field].between(self.filters.from_date, self.filters.to_date)) - ) + .where((doctype_item.docstatus == 1) & (doctype.name.isin(permitted_names))) ).run(as_dict=True) self.get_groups() @@ -300,8 +319,10 @@ class Analytics: if self.filters.doc_type in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]: filters.update({"is_opening": "No"}) - self.entries = frappe.get_all( - self.filters.doc_type, fields=[entity, value_field, self.date_field], filters=filters + self.entries = frappe.get_list( + self.filters.doc_type, + fields=[entity, value_field, self.date_field], + filters=filters, ) def get_rows(self): diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index 3259a2323b7..18c513b3c2b 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -4115,9 +4115,14 @@ }, "Japan": { - "Japan Tax": { - "account_name": "CT", - "tax_rate": 5.00 + "Japan Tax 10%": { + "account_name": "CT 10%", + "tax_rate": 10.00, + "default": 1 + }, + "Japan Tax 8%": { + "account_name": "CT 8%", + "tax_rate": 8.00 } }, diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 79e9776b91c..2af1f645494 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -263,8 +263,9 @@ def update_qty(bin_name, args): # actual qty is already updated by processing current voucher actual_qty = bin_details.actual_qty or 0.0 - # actual qty is not up to date in case of backdated transaction - if future_sle_exists(args): + # actual qty is not up to date in case of backdated transactions + # or when cancellations are the most recent SLE + if future_sle_exists(args) or args.get("is_cancelled"): actual_qty = get_actual_qty(args.get("item_code"), args.get("warehouse")) ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty")) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 8cf96a149d7..1ac6afd15b1 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -2194,25 +2194,6 @@ def update_serial_batch_no_ledgers(bundle, entries, child_row, parent_doc, wareh return doc -@frappe.whitelist() -def update_serial_or_batch(bundle_id, serial_no=None, batch_no=None): - if batch_no and not serial_no: - if qty := frappe.db.get_value( - "Serial and Batch Entry", {"parent": bundle_id, "batch_no": batch_no}, "qty" - ): - frappe.db.set_value( - "Serial and Batch Entry", {"parent": bundle_id, "batch_no": batch_no}, "qty", qty + 1 - ) - return - - doc = frappe.get_cached_doc("Serial and Batch Bundle", bundle_id) - if not serial_no and not batch_no: - return - - doc.append("entries", {"serial_no": serial_no, "batch_no": batch_no, "qty": 1}) - doc.save(ignore_permissions=True) - - def get_serial_and_batch_ledger(**kwargs): kwargs = frappe._dict(kwargs) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json index 3af946aa8f3..bfb4ec368dd 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_bulk_edit": 1, "allow_copy": 1, "autoname": "MAT-SLE-.YYYY.-.#####", "creation": "2013-01-29 19:25:42", @@ -204,7 +205,7 @@ { "fieldname": "valuation_rate", "fieldtype": "Currency", - "label": "Valuation Rate", + "label": "Average Rate", "oldfieldname": "valuation_rate", "oldfieldtype": "Currency", "options": "Company:company:default_currency", @@ -362,11 +363,11 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2025-10-04 09:59:15.546556", + "modified": "2026-05-26 19:07:43.537450", "modified_by": "Administrator", "module": "Stock", "name": "Stock Ledger Entry", - "naming_rule": "Expression (old style)", + "naming_rule": "Expression", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 3f59dc6be3b..2152fce31c0 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -109,10 +109,11 @@ def execute(filters=None): if sle.serial_no: update_available_serial_nos(available_serial_nos, sle) - if sle.actual_qty: + if sle.actual_qty < 0: sle["in_out_rate"] = flt(sle.stock_value_difference / sle.actual_qty, precision) + sle["incoming_rate"] = 0 - elif sle.voucher_type == "Stock Reconciliation": + elif sle.voucher_type == "Stock Reconciliation" and sle.actual_qty < 0: sle["in_out_rate"] = sle.valuation_rate if ( @@ -193,7 +194,7 @@ def get_segregated_bundle_entries(sle, bundle_details, batch_balance_dict, filte new_sle.update(row) new_sle.update( { - "in_out_rate": flt(new_sle.stock_value_difference / row.qty) if row.qty else 0, + "in_out_rate": flt(new_sle.stock_value_difference / row.qty) if row.qty < 0 else 0, "in_qty": row.qty if row.qty > 0 else 0, "out_qty": row.qty if row.qty < 0 else 0, "qty_after_transaction": qty_before_transaction + row.qty, @@ -375,7 +376,7 @@ def get_columns(filters): "convertible": "rate", }, { - "label": _("Valuation Rate"), + "label": _("Outgoing Rate"), "fieldname": "in_out_rate", "fieldtype": filters.valuation_field_type, "width": 140, diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index a50bc4f8325..a1dc9d13a4e 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -887,7 +887,7 @@ class update_entries_after: # Only run in reposting self.get_serialized_values(sle) self.wh_data.qty_after_transaction += flt(sle.actual_qty) - if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no: + if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and has_correct_data(sle): self.wh_data.qty_after_transaction = sle.qty_after_transaction self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt( @@ -2517,3 +2517,28 @@ def get_incoming_rate_for_serial_and_batch(item_code, row, sn_obj): @frappe.request_cache def is_repack_entry(stock_entry_id): return frappe.get_cached_value("Stock Entry", stock_entry_id, "purpose") == "Repack" + + +def has_correct_data(sle): + previous_sle = get_previous_sle( + { + "item_code": sle.item_code, + "warehouse": sle.warehouse, + "posting_date": sle.posting_date, + "posting_time": sle.posting_time, + "creation": sle.creation, + "sle": sle.name, + } + ) + + if not previous_sle: + return True + + previous_qty = previous_sle.get("qty_after_transaction") or 0 + if previous_qty and not frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_detail_no": sle.voucher_detail_no, "is_cancelled": 0, "actual_qty": ("<", 0)}, + ): + return False + + return True diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 70ced430257..faa12bd5419 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -218,11 +218,13 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord @frappe.whitelist() def set_multiple_status(names, status): for name in json.loads(names): - frappe.db.set_value("Issue", name, "status", status) + set_status(name, status) @frappe.whitelist() def set_status(name, status): + frappe.has_permission("Issue", "write", name, throw=True) + frappe.db.set_value("Issue", name, "status", status) diff --git a/erpnext/templates/pages/task_info.html b/erpnext/templates/pages/task_info.html index fe4d304a398..4a98b425e73 100644 --- a/erpnext/templates/pages/task_info.html +++ b/erpnext/templates/pages/task_info.html @@ -1,11 +1,11 @@ {% extends "templates/web.html" %} -{% block title %} {{ doc.name }} {% endblock %} +{% block title %} {{ doc.name|e }} {% endblock %} {% block breadcrumbs %} -