Merge pull request #55546 from frappe/version-15-hotfix

This commit is contained in:
Mihir Kandoi
2026-06-02 22:24:03 +05:30
committed by GitHub
42 changed files with 476 additions and 329 deletions

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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):

View File

@@ -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", {

View File

@@ -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",
)

View File

@@ -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:

View File

@@ -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",

View File

@@ -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)

View File

@@ -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,
}

View File

@@ -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)

View File

@@ -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()

View File

@@ -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",

View File

@@ -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": []
}
}

View File

@@ -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

View File

@@ -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):

View File

@@ -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
}
}

View File

@@ -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": [],

View File

@@ -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 <b>{0}</b> 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({

View File

@@ -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",

View File

@@ -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()

View File

@@ -174,8 +174,8 @@ erpnext.PointOfSale.Controller = class {
set_opening_entry_status() {
this.page.set_title_sub(
`<span class="indicator orange">
<a class="text-muted" href="#Form/POS%20Opening%20Entry/${this.pos_opening}">
Opened at ${frappe.datetime.str_to_user(this.pos_opening_time)}
<a class="text-muted" href="#Form/POS%20Opening%20Entry/${encodeURIComponent(this.pos_opening)}">
Opened at ${frappe.utils.escape_html(frappe.datetime.str_to_user(this.pos_opening_time))}
</a>
</span>`
);

View File

@@ -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 {
<div class="customer-display">
${this.get_customer_image()}
<div class="customer-name-desc">
<div class="customer-name">${customer_name}</div>
<div class="customer-name">${frappe.utils.escape_html(customer_name)}</div>
${get_customer_description()}
</div>
<div class="reset-customer-btn" data-customer="${escape(customer)}">
<div class="reset-customer-btn" data-customer="${frappe.utils.escape_html(customer)}">
<svg width="32" height="32" viewBox="0 0 14 14" fill="none">
<path d="M4.93764 4.93759L7.00003 6.99998M9.06243 9.06238L7.00003 6.99998M7.00003 6.99998L4.93764 9.06238L9.06243 4.93759" stroke="#8D99A6"/>
</svg>
@@ -473,11 +473,13 @@ erpnext.PointOfSale.ItemCart = class {
if (!email_id && !mobile_no) {
return `<div class="customer-desc">${__("Click to add email / phone")}</div>`;
} else if (email_id && !mobile_no) {
return `<div class="customer-desc">${email_id}</div>`;
return `<div class="customer-desc">${frappe.utils.escape_html(email_id)}</div>`;
} else if (mobile_no && !email_id) {
return `<div class="customer-desc">${mobile_no}</div>`;
return `<div class="customer-desc">${frappe.utils.escape_html(mobile_no)}</div>`;
} else {
return `<div class="customer-desc">${email_id} - ${mobile_no}</div>`;
return `<div class="customer-desc">${frappe.utils.escape_html(
email_id
)} - ${frappe.utils.escape_html(mobile_no)}</div>`;
}
}
}
@@ -485,9 +487,13 @@ erpnext.PointOfSale.ItemCart = class {
get_customer_image() {
const { customer, image } = this.customer_info || {};
if (image) {
return `<div class="customer-image"><img src="${image}" alt="${image}""></div>`;
return `<div class="customer-image"><img src="${frappe.utils.escape_html(
image
)}" alt="${frappe.utils.escape_html(image)}"></div>`;
} else {
return `<div class="customer-image customer-abbr">${frappe.get_abbr(customer)}</div>`;
return `<div class="customer-image customer-abbr">${frappe.utils.escape_html(
frappe.get_abbr(customer)
)}</div>`;
}
}
@@ -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 `<div class="tax-row">
<div class="tax-label">${description}</div>
<div class="tax-value">${format_currency(t.tax_amount_after_discount_amount, currency)}</div>
@@ -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(
`<div class="cart-item-wrapper" data-row-name="${escape(item_data.name)}"></div>
`<div class="cart-item-wrapper" data-row-name="${frappe.utils.escape_html(
item_data.name
)}"></div>
<div class="seperator"></div>`
);
$item_to_update = this.get_cart_item(item_data);
@@ -607,7 +616,7 @@ erpnext.PointOfSale.ItemCart = class {
`${get_item_image_html()}
<div class="item-name-desc">
<div class="item-name">
${item_data.item_name}
${frappe.utils.escape_html(item_data.item_name)}
</div>
${get_description_html()}
</div>
@@ -636,7 +645,7 @@ erpnext.PointOfSale.ItemCart = class {
if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) {
return `
<div class="item-qty-rate">
<div class="item-qty"><span>${item_data.qty || 0} ${item_data.uom}</span></div>
<div class="item-qty"><span>${item_data.qty || 0} ${frappe.utils.escape_html(item_data.uom)}</span></div>
<div class="item-rate-amount">
<div class="item-rate">${format_currency(item_data.amount, currency)}</div>
<div class="item-amount">${format_currency(item_data.rate, currency)}</div>
@@ -645,7 +654,7 @@ erpnext.PointOfSale.ItemCart = class {
} else {
return `
<div class="item-qty-rate">
<div class="item-qty"><span>${item_data.qty || 0} ${item_data.uom}</span></div>
<div class="item-qty"><span>${item_data.qty || 0} ${frappe.utils.escape_html(item_data.uom)}</span></div>
<div class="item-rate-amount">
<div class="item-rate">${format_currency(item_data.rate, currency)}</div>
</div>
@@ -666,7 +675,7 @@ erpnext.PointOfSale.ItemCart = class {
}
}
item_data.description = frappe.ellipsis(item_data.description, 45);
return `<div class="item-desc">${item_data.description}</div>`;
return `<div class="item-desc">${frappe.utils.escape_html(item_data.description)}</div>`;
}
return ``;
}
@@ -678,22 +687,26 @@ erpnext.PointOfSale.ItemCart = class {
<div class="item-image">
<img
onerror="cur_pos.cart.handle_broken_image(this)"
src="${image}" alt="${frappe.get_abbr(item_name)}"">
src="${frappe.utils.escape_html(image)}" alt="${frappe.utils.escape_html(frappe.get_abbr(item_name))}">
</div>`;
} else {
return `<div class="item-image item-abbr">${frappe.get_abbr(item_name)}</div>`;
return `<div class="item-image item-abbr">${frappe.utils.escape_html(
frappe.get_abbr(item_name)
)}</div>`;
}
}
}
handle_broken_image($img) {
const item_abbr = $($img).attr("alt");
$($img).parent().replaceWith(`<div class="item-image item-abbr">${item_abbr}</div>`);
$($img)
.parent()
.replaceWith(`<div class="item-image item-abbr">${frappe.utils.escape_html(item_abbr)}</div>`);
}
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 {
<div class="customer-display">
${this.get_customer_image()}
<div class="customer-name-desc">
<div class="customer-name">${customer_name}</div>
<div class="customer-desc">${customer}</div>
<div class="customer-name">${frappe.utils.escape_html(customer_name)}</div>
<div class="customer-desc">${frappe.utils.escape_html(customer)}</div>
</div>
</div>
<div class="customer-fields-container">
@@ -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(
`<div class="invoice-wrapper" data-invoice-name="${escape(invoice.name)}">
`<div class="invoice-wrapper" data-invoice-name="${frappe.utils.escape_html(
invoice.name
)}">
<div class="invoice-name-date">
<div class="invoice-name">${invoice.name}</div>
<div class="invoice-name">${frappe.utils.escape_html(invoice.name)}</div>
<div class="invoice-date">${posting_datetime}</div>
</div>
<div class="invoice-total-status">
@@ -1039,7 +1055,7 @@ erpnext.PointOfSale.ItemCart = class {
${format_currency(invoice.grand_total, invoice.currency, frappe.sys_defaults.currency_precision) || 0}
</div>
<div class="invoice-status">
<span class="indicator-pill whitespace-nowrap ${indicator_color[invoice.status]}">
<span class="indicator-pill whitespace-nowrap ${indicator_color[invoice.status] || ""}">
<span>${__(invoice.status)}</span>
</span>
</div>

View File

@@ -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(
`<img
onerror="cur_pos.item_details.handle_broken_image(this)"
class="h-full" src="${image}"
alt="${frappe.get_abbr(item_name)}"
class="h-full" src="${frappe.utils.escape_html(image)}"
alt="${frappe.utils.escape_html(frappe.get_abbr(item_name))}"
style="object-fit: cover;">`
);
} else {
this.$item_image.html(`<div class="item-abbr">${frappe.get_abbr(item_name)}</div>`);
this.$item_image.html(
`<div class="item-abbr">${frappe.utils.escape_html(frappe.get_abbr(item_name))}</div>`
);
}
}
handle_broken_image($img) {
const item_abbr = $($img).attr("alt");
$($img).replaceWith(`<div class="item-abbr">${item_abbr}</div>`);
$($img).replaceWith(`<div class="item-abbr">${frappe.utils.escape_html(item_abbr)}</div>`);
}
render_discount_dom(item) {

View File

@@ -107,39 +107,45 @@ erpnext.PointOfSale.ItemSelector = class {
<div class="flex items-center justify-center border-b-grey text-6xl text-grey-100" style="height:8rem; min-height:8rem">
<img
onerror="cur_pos.item_selector.handle_broken_image(this)"
class="h-full item-img" src="${item_image}"
alt="${frappe.get_abbr(item.item_name)}"
class="h-full item-img" src="${frappe.utils.escape_html(item_image)}"
alt="${frappe.utils.escape_html(frappe.get_abbr(item.item_name))}"
>
</div>`;
} else {
return `<div class="item-qty-pill">
<span class="indicator-pill whitespace-nowrap ${indicator_color}">${qty_to_display}</span>
</div>
<div class="item-display abbr">${frappe.get_abbr(item.item_name)}</div>`;
<div class="item-display abbr">${frappe.utils.escape_html(frappe.get_abbr(item.item_name))}</div>`;
}
}
return `<div class="item-wrapper"
data-item-code="${escape(item.item_code)}" data-serial-no="${escape(serial_no)}"
data-batch-no="${escape(batch_no)}" data-uom="${escape(uom)}"
data-rate="${escape(price_list_rate || 0)}"
data-stock-uom="${escape(item.stock_uom)}"
title="${item.item_name}">
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()}
<div class="item-detail">
<div class="item-name">
${frappe.ellipsis(item.item_name, 18)}
${frappe.utils.escape_html(frappe.ellipsis(item.item_name, 18))}
</div>
<div class="item-rate">${format_currency(price_list_rate, item.currency, precision) || 0} / ${uom}</div>
<div class="item-rate">${
format_currency(price_list_rate, item.currency, precision) || 0
} / ${frappe.utils.escape_html(uom)}</div>
</div>
</div>`;
}
handle_broken_image($img) {
const item_abbr = $($img).attr("alt");
$($img).parent().replaceWith(`<div class="item-display abbr">${item_abbr}</div>`);
$($img)
.parent()
.replaceWith(`<div class="item-display abbr">${frappe.utils.escape_html(item_abbr)}</div>`);
}
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;

View File

@@ -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 `<div class="invoice-wrapper" data-invoice-name="${escape(invoice.name)}">
return `<div class="invoice-wrapper" data-invoice-name="${frappe.utils.escape_html(invoice.name)}">
<div class="invoice-name-date">
<div class="invoice-name">${invoice.name}</div>
<div class="invoice-name">${frappe.utils.escape_html(invoice.name)}</div>
<div class="invoice-date">
<svg class="mr-2" width="12" height="12" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
</svg>
${frappe.ellipsis(invoice.customer_name, 20)}
${frappe.utils.escape_html(frappe.ellipsis(invoice.customer_name, 20))}
</div>
</div>
<div class="invoice-total-status">

View File

@@ -81,23 +81,27 @@ erpnext.PointOfSale.PastOrderSummary = class {
return `<div class="left-section">
<div class="customer-section">
<div class="customer-name">${doc.customer_name}</div>
${is_customer_naming_by_customer_name ? `<div class="customer-code">${doc.customer}</div>` : ""}
<div class="customer-email">${this.customer_email}</div>
<div class="customer-name">${frappe.utils.escape_html(doc.customer_name)}</div>
${
is_customer_naming_by_customer_name
? `<div class="customer-code">${frappe.utils.escape_html(doc.customer)}</div>`
: ""
}
<div class="customer-email">${frappe.utils.escape_html(this.customer_email)}</div>
</div>
<div class="cashier">${__("Sold by")}: ${doc.owner}</div>
<div class="cashier">${__("Sold by")}: ${frappe.utils.escape_html(doc.owner)}</div>
</div>
<div class="right-section">
<div class="paid-amount">${format_currency(doc.paid_amount, doc.currency)}</div>
<div class="invoice-name">${doc.name}</div>
<div class="invoice-name">${frappe.utils.escape_html(doc.name)}</div>
<span class="indicator-pill whitespace-nowrap ${indicator_color}"><span>${__(doc.status)}</span></span>
</div>`;
}
get_item_html(doc, item_data) {
return `<div class="item-row-wrapper">
<div class="item-name">${item_data.item_name}</div>
<div class="item-qty">${item_data.qty || 0} ${item_data.uom}</div>
<div class="item-name">${frappe.utils.escape_html(item_data.item_name)}</div>
<div class="item-qty">${item_data.qty || 0} ${frappe.utils.escape_html(item_data.uom)}</div>
<div class="item-rate-disc">${get_rate_discount_html()}</div>
</div>`;
@@ -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 `
<div class="tax-row">
<div class="tax-label">${description}</div>

View File

@@ -408,8 +408,10 @@ erpnext.PointOfSale.Payment = class {
return `
<div class="payment-mode-wrapper">
<div class="mode-of-payment" data-mode="${mode}" data-payment-type="${payment_type}">
${p.mode_of_payment}
<div class="mode-of-payment" data-mode="${mode}" data-payment-type="${frappe.utils.escape_html(
payment_type
)}">
${frappe.utils.escape_html(p.mode_of_payment)}
<div class="${mode}-amount pay-amount">${amount}</div>
<div class="${mode} mode-of-payment-control"></div>
</div>
@@ -544,7 +546,7 @@ erpnext.PointOfSale.Payment = class {
<div class="mode-of-payment loyalty-card" data-mode="loyalty-amount" data-payment-type="loyalty-amount">
Redeem Loyalty Points
<div class="loyalty-amount-amount pay-amount">${amount}</div>
<div class="loyalty-amount-name">${loyalty_program}</div>
<div class="loyalty-amount-name">${frappe.utils.escape_html(loyalty_program)}</div>
<div class="loyalty-amount mode-of-payment-control"></div>
</div>
</div>`

View File

@@ -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):

View File

@@ -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
}
},

View File

@@ -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"))

View File

@@ -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)

View File

@@ -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": [
{

View File

@@ -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,

View File

@@ -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

View File

@@ -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)

View File

@@ -1,11 +1,11 @@
{% extends "templates/web.html" %}
{% block title %} {{ doc.name }} {% endblock %}
{% block title %} {{ doc.name|e }} {% endblock %}
{% block breadcrumbs %}
<div class="page-breadcrumbs" data-html-block="breadcrumbs">
<div class="page-breadcrumbs container" data-html-block="breadcrumbs">
<ul class="breadcrumb">
<li>
<span class="fa fa-angle-left"></span>
<a href="/projects?project={{ doc.project }}">{{ doc.project }}</a>
<a href="/projects?project={{ doc.project|e }}">{{ doc.project|e }}</a>
</li>
</ul>
</div>
@@ -13,16 +13,7 @@
{% block page_content %}
<div class="row">
<div class=" col-sm-8 ">
<h1> {{ doc.subject }} </h1>
</div>
<div class="col-sm-4">
<div class="page-header-actions-block" data-html-block="header-actions">
<button type="submit" class="btn btn-primary btn-sm btn-form-submit">
{{ __("Update") }}</button>
<a href="tasks" class="btn btn-light btn-sm">
{{ __("Cancel") }}</a>
</div>
<h1> {{ doc.subject|e }} </h1>
</div>
</div>
@@ -31,50 +22,44 @@
<input type="hidden" name="web_form" value="tasks">
<input type="hidden" name="doctype" value="Task">
<input type="hidden" name="name" value="TASK00056">
<input type="hidden" name="name" value="{{ doc.name|e }}">
<div class="row">
<div class="col-sm-12" style="max-width: 500px;">
<div class="form-group">
<label for="project" class="control-label text-muted small">{{ __("Project") }}</label>
<input type="text" class="form-control" name="project" readonly value= "{{ doc.project }}">
<label for="project" class="control-label text-muted small">{{ _("Project") }}</label>
<input type="text" class="form-control" name="project" readonly value= "{{ doc.project or ""|e }}">
</div>
<div class="form-group">
<label for="subject" class="control-label text-muted small">{{ __("Subject") }}</label>
<input type="text" class="form-control" name="subject" readonly value="{{ doc.subject }}">
<label for="subject" class="control-label text-muted small">{{ _("Subject") }}</label>
<input type="text" class="form-control" name="subject" readonly value="{{ doc.subject or ""|e }}">
</div>
<div class="form-group">
<label for="description" class="control-label text-muted small">{{ __("Details") }}</label>
<textarea class="form-control" style="height: 200px;" name="description">{{ doc.description }}</textarea>
<label for="description" class="control-label text-muted small">{{ _("Details") }}</label>
<textarea class="form-control" style="height: 200px;" name="description" readonly>{{ doc.description or ""|e }}</textarea>
</div>
<div class="form-group">
<label for="priority" class="control-label text-muted small">{{ __("Priority") }}</label>
<input type="text" class="form-control" name="priority" readonly value="{{ doc.priority }}">
<label for="priority" class="control-label text-muted small">{{ _("Priority") }}</label>
<input type="text" class="form-control" name="priority" readonly value="{{ doc.priority or ""|e }}">
</div>
<div class="form-group">
<label for="exp_start_date" class="control-label text-muted small">{{ __("Expected Start Date") }}</label>
<input type="text" class="form-control hasDatepicker" name="exp_start_date" readonly value="{{ doc.exp_start_date }}">
<label for="exp_start_date" class="control-label text-muted small">{{ _("Expected Start Date") }}</label>
<input type="text" class="form-control hasDatepicker" name="exp_start_date" readonly value="{{ doc.exp_start_date or ""|e }}">
</div>
<div class="form-group">
<label for="exp_end_date" class="control-label text-muted small">{{ __("Expected End Date") }}</label>
<input type="text" class="form-control hasDatepicker" name="exp_end_date" readonly value="{{ doc.exp_end_date }}">
<label for="exp_end_date" class="control-label text-muted small">{{ _("Expected End Date") }}</label>
<input type="text" class="form-control hasDatepicker" name="exp_end_date" readonly value="{{ doc.exp_end_date or ""|e }}">
</div>
<div class="form-group">
<label for="status" class="control-label text-muted small">{{ __("Status") }}</label>
<select class="form-control" name="status" id="status" data-label="Status" data-fieldtype="Select">
<option value="Open" selected="selected">
{{ __("Open") }}</option><option value="Working">
{{ __("Working") }}</option><option value="Pending Review">
{{ __("Pending Review") }}</option><option value="Overdue">
{{ __("Overdue") }}</option><option value="Closed">
{{ __("Closed") }}</option><option value="Cancelled">
{{ __("Cancelled") }}</option>
<label for="status" class="control-label text-muted small">{{ _("Status") }}</label>
<select class="form-control" name="status" id="status" data-label="Status" data-fieldtype="Select" disabled>
<option value="{{ doc.status|e }}" selected="selected">{{ doc.status|e }}</option>
</select>
</div>
</div>
@@ -83,68 +68,13 @@
</div>
<div class="comments">
<h3>{{ __("Comments") }}</h3>
<h3>{{ _("Comments") }}</h3>
<div class="no-comment">
{% for comment in comments %}
<p class="text-muted">{{comment.sender_full_name}}:
{{comment.subject}} {{ __("on") }} {{comment.creation.strftime('%Y-%m-%d')}}</p>
<p class="text-muted">{{comment.comment_email}}:
{{comment.content|e}} {{ _("on") }} {{comment.creation.strftime('%Y-%m-%d')}}</p>
{% endfor %}
</div>
<div class="comment-form-wrapper">
<a class="add-comment btn btn-light btn-sm">{{ __("Add Comment") }}</a>
<div style="display: none;" id="comment-form">
<p>{{ __("Add Comment") }}</p>
<form>
<fieldset>
<textarea class="form-control" name="comment" rows="5" placeholder="Comment"></textarea>
<p>
<button class="btn btn-primary btn-sm" id="submit-comment">{{ __("Submit") }}</button>
</p>
</fieldset>
</form>
</div>
</div>
</div>
<script>
frappe.ready(function() {
var n_comments = $(".comment-row").length;
$(".add-comment").click(function() {
$(this).toggle(false);
$("#comment-form").toggle();
$("#comment-form textarea").val("");
})
$("#submit-comment").click(function() {
var args = {
comment_by_fullname: "test",
comment_by: "admin@localhost.com",
comment: $("[name='comment']").val(),
reference_doctype: "Task",
reference_name: "TASK00069",
comment_type: "Comment",
route: "tasks",
}
frappe.call({
btn: this,
type: "POST",
method: "frappe.templates.includes.comments.comments.add_comment",
args: args,
callback: function(r) {
if(r.exc) {
if(r._server_messages)
frappe.msgprint(r._server_messages);
} else {
$(r.message).appendTo("#comment-list");
$(".no-comment, .add-comment").toggle(false);
$("#comment-form")
.replaceWith('<div class="text-muted">Thank you for your comment!</div>')
}
}
})
return false;
})
});
</script>
{% endblock %}

View File

@@ -5,11 +5,12 @@ def get_context(context):
context.no_cache = 1
task = frappe.get_doc("Task", frappe.form_dict.task)
task.check_permission()
context.comments = frappe.get_all(
"Communication",
filters={"reference_name": task.name, "comment_type": "comment"},
fields=["subject", "sender_full_name", "communication_date"],
"Comment",
filters={"reference_doctype": "Task", "reference_name": task.name, "comment_type": "Comment"},
fields=["content", "comment_email", "creation"],
)
context.doc = task

View File

@@ -12,10 +12,13 @@ no_cache = 1
def get_context(context):
is_enabled = frappe.db.get_single_value("Appointment Booking Settings", "enable_scheduling")
if is_enabled:
return context
else:
handle_appointment_booking_disabled()
return context
def handle_appointment_booking_disabled():
if not frappe.get_single_value("Appointment Booking Settings", "enable_scheduling"):
frappe.redirect_to_message(
_("Appointment Scheduling Disabled"),
_("Appointment Scheduling has been disabled for this site"),
@@ -27,9 +30,9 @@ def get_context(context):
@frappe.whitelist(allow_guest=True)
def get_appointment_settings():
settings = frappe.get_cached_value(
handle_appointment_booking_disabled()
settings = frappe.get_single_value(
"Appointment Booking Settings",
None,
["advance_booking_days", "appointment_duration", "success_redirect_url"],
as_dict=True,
)
@@ -38,6 +41,7 @@ def get_appointment_settings():
@frappe.whitelist(allow_guest=True)
def get_timezones():
handle_appointment_booking_disabled()
import pytz
return pytz.all_timezones
@@ -46,6 +50,7 @@ def get_timezones():
@frappe.whitelist(allow_guest=True)
def get_appointment_slots(date, timezone):
# Convert query to local timezones
handle_appointment_booking_disabled()
format_string = "%Y-%m-%d %H:%M:%S"
query_start_time = datetime.datetime.strptime(date + " 00:00:00", format_string)
query_end_time = datetime.datetime.strptime(date + " 23:59:59", format_string)
@@ -54,7 +59,11 @@ def get_appointment_slots(date, timezone):
now = convert_to_guest_timezone(timezone, datetime.datetime.now())
# Database queries
settings = frappe.get_doc("Appointment Booking Settings")
settings = frappe.get_single_value(
"Appointment Booking Settings",
["holiday_list", "appointment_duration", "number_of_agents", "availability_of_slots"],
as_dict=True,
)
holiday_list = frappe.get_doc("Holiday List", settings.holiday_list)
timeslots = get_available_slots_between(query_start_time, query_end_time, settings)
@@ -95,6 +104,7 @@ def get_available_slots_between(query_start_time, query_end_time, settings):
@frappe.whitelist(allow_guest=True)
def create_appointment(date, time, tz, contact):
handle_appointment_booking_disabled()
format_string = "%Y-%m-%d %H:%M:%S"
scheduled_time = datetime.datetime.strptime(date + " " + time, format_string)
# Strip tzinfo from datetime objects since it's handled by the doctype