diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index 2fb3c7875cf..a5855c41093 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -120,6 +120,7 @@ class Account(NestedSet):
self.validate_account_currency()
self.validate_root_company_and_sync_account_to_children()
self.validate_receivable_payable_account_type()
+ self.validate_stock_account_type_change()
def validate_parent_child_account_type(self):
if self.parent_account:
@@ -208,6 +209,36 @@ class Account(NestedSet):
frappe.msgprint(msg)
self.add_comment("Comment", msg)
+ def validate_stock_account_type_change(self):
+ doc_before_save = self.get_doc_before_save()
+ if not (doc_before_save and doc_before_save.account_type == "Stock"):
+ return
+
+ if self.account_type == "Stock":
+ return
+
+ if self.stock_ledger_entry_exists():
+ frappe.throw(
+ _(
+ "The account type of {0} cannot be changed from {1} because stock ledger entries exist against it."
+ ).format(frappe.bold(self.name), frappe.bold(_("Stock")))
+ )
+
+ def stock_ledger_entry_exists(self):
+ from erpnext.stock import get_warehouse_account_map
+
+ warehouse_account = get_warehouse_account_map(self.company)
+ warehouses = [wh for wh, details in warehouse_account.items() if details.account == self.name]
+ if not warehouses:
+ return False
+
+ return bool(
+ frappe.db.count(
+ "Stock Ledger Entry",
+ filters={"warehouse": ("in", warehouses), "is_cancelled": 0},
+ )
+ )
+
def validate_root_details(self):
doc_before_save = self.get_doc_before_save()
diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py
index f840ac86207..dace7d34613 100644
--- a/erpnext/accounts/doctype/account/test_account.py
+++ b/erpnext/accounts/doctype/account/test_account.py
@@ -307,6 +307,31 @@ class TestAccount(ERPNextTestSuite):
acc.account_currency = "USD"
self.assertRaises(frappe.ValidationError, acc.save)
+ def test_stock_account_type_change_with_ledger_entries(self):
+ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+
+ company = "_Test Company with perpetual inventory"
+ warehouse = "Stores - TCP1"
+ stock_account = get_warehouse_account(frappe.get_doc("Warehouse", warehouse))
+
+ make_stock_entry(
+ item_code="_Test Item",
+ target=warehouse,
+ company=company,
+ qty=5,
+ basic_rate=100,
+ )
+
+ account = frappe.get_doc("Account", stock_account)
+ self.assertEqual(account.account_type, "Stock")
+
+ account.account_type = ""
+ self.assertRaises(frappe.ValidationError, account.save)
+
+ account.reload()
+ account.account_name = f"{account.account_name} Updated"
+ account.save() # non-type change stays allowed
+
def test_account_balance(self):
from erpnext.accounts.utils import get_balance_on
diff --git a/erpnext/accounts/doctype/payment_reference/payment_reference.json b/erpnext/accounts/doctype/payment_reference/payment_reference.json
index a1adb181d35..4e1e0ac22e3 100644
--- a/erpnext/accounts/doctype/payment_reference/payment_reference.json
+++ b/erpnext/accounts/doctype/payment_reference/payment_reference.json
@@ -14,7 +14,8 @@
"section_break_mjlv",
"due_date",
"column_break_qghl",
- "amount"
+ "amount",
+ "currency"
],
"fields": [
{
@@ -55,8 +56,18 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
+ "options": "currency",
"precision": "2"
},
+ {
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Currency",
+ "options": "Currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
{
"fieldname": "column_break_lnjp",
"fieldtype": "Column Break"
@@ -74,7 +85,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2026-01-19 02:21:36.455830",
+ "modified": "2026-07-11 00:00:00.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reference",
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 4e12deb5097..70b28141cf6 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -784,6 +784,7 @@ def set_payment_references(payment_schedules):
"description": row.get("description"),
"due_date": row.get("due_date"),
"amount": row.get("payment_amount"),
+ "currency": row.get("currency"),
}
)
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 62384f3a85a..3e9803888f0 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -1371,8 +1371,9 @@ class StockController(AccountsController):
if outstanding > 0:
reservations[key].append(row)
+ precision = frappe.get_precision("Serial and Batch Entry", "qty")
for (batch_no, warehouse), reserved_qty in outstanding_qty.items():
- if flt(reserved_qty, 6) <= 0:
+ if flt(reserved_qty, precision) <= 0:
continue
batch_qty = get_batch_qty(
@@ -1383,7 +1384,7 @@ class StockController(AccountsController):
consider_negative_batches=True,
)
- if flt(batch_qty, 6) >= flt(reserved_qty, 6):
+ if flt(batch_qty, precision) >= flt(reserved_qty, precision):
continue
vouchers = ", ".join(
diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json
index c600eb088c3..b7a92dba6d1 100644
--- a/erpnext/crm/doctype/appointment/appointment.json
+++ b/erpnext/crm/doctype/appointment/appointment.json
@@ -7,7 +7,11 @@
"engine": "InnoDB",
"field_order": [
"scheduled_time",
+ "column_break_xaox",
"status",
+ "created_through_portal",
+ "email_verified",
+ "verification_token",
"customer_details_section",
"customer_name",
"customer_phone_number",
@@ -54,7 +58,8 @@
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "Scheduled Time",
- "reqd": 1
+ "reqd": 1,
+ "search_index": 1
},
{
"fieldname": "status",
@@ -77,8 +82,8 @@
"fieldname": "customer_email",
"fieldtype": "Data",
"label": "Email",
- "reqd": 1,
- "options": "Email"
+ "options": "Email",
+ "reqd": 1
},
{
"fieldname": "linked_docs_section",
@@ -100,13 +105,43 @@
"fieldtype": "Dynamic Link",
"label": "Party",
"options": "appointment_with"
+ },
+ {
+ "default": "0",
+ "fieldname": "created_through_portal",
+ "fieldtype": "Check",
+ "label": "Created through Portal",
+ "read_only": 1,
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "column_break_xaox",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.created_through_portal === 1;",
+ "fieldname": "email_verified",
+ "fieldtype": "Check",
+ "label": "Email Verified",
+ "read_only": 1
+ },
+ {
+ "fieldname": "verification_token",
+ "fieldtype": "Data",
+ "label": "Verification Token",
+ "hidden": 1,
+ "read_only": 1,
+ "no_copy": 1,
+ "search_index": 1
}
],
"links": [],
- "modified": "2026-06-06 13:05:59.300573",
+ "modified": "2026-07-20 02:00:00.000000",
"modified_by": "Administrator",
"module": "CRM",
"name": "Appointment",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -158,8 +193,9 @@
}
],
"quick_entry": 1,
+ "row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py
index 0f7c52688a3..da91a73f105 100644
--- a/erpnext/crm/doctype/appointment/appointment.py
+++ b/erpnext/crm/doctype/appointment/appointment.py
@@ -3,14 +3,20 @@
from collections import Counter
+from datetime import timedelta
+from urllib.parse import urlencode
import frappe
from frappe import _
from frappe.desk.form.assign_to import add as add_assignment
from frappe.model.document import Document
from frappe.share import add_docshare
-from frappe.utils import get_url, getdate, now
-from frappe.utils.verified_command import get_signed_params
+from frappe.utils import add_to_date, cint, date_diff, get_datetime, get_url, getdate, now, now_datetime
+from frappe.utils.data import sha256_hash
+
+from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday
+
+WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
class Appointment(Document):
@@ -24,104 +30,227 @@ class Appointment(Document):
appointment_with: DF.Link | None
calendar_event: DF.Link | None
+ created_through_portal: DF.Check
customer_details: DF.LongText | None
customer_email: DF.Data
customer_name: DF.Data
customer_phone_number: DF.Data | None
customer_skype: DF.Data | None
+ email_verified: DF.Check
party: DF.DynamicLink | None
scheduled_time: DF.Datetime
status: DF.Literal["Open", "Unverified", "Closed"]
+ verification_token: DF.Data | None
# end: auto-generated types
- def find_lead_by_email(self):
- lead_list = frappe.get_list(
- "Lead", filters={"email_id": self.customer_email}, ignore_permissions=True
- )
- if lead_list:
- return lead_list[0].name
- return None
+ def validate(self):
+ self.validate_status_update()
+ if not self.has_value_changed("scheduled_time"):
+ return
- def find_customer_by_email(self):
- customer_list = frappe.get_list(
- "Customer", filters={"email_id": self.customer_email}, ignore_permissions=True
+ self.validate_backdated_booking()
+
+ if is_appointment_scheduling_enabled():
+ self.validate_advanced_booking()
+ self.validate_holiday()
+ self.validate_slot_timing()
+
+ self.validate_available_time_slot()
+
+ def validate_status_update(self):
+ if not self.has_value_changed("status"):
+ return
+
+ if not self.created_through_portal:
+ if self.status == "Unverified":
+ frappe.throw(_("Appointments created manually cannot have 'Unverified' status."))
+ return
+
+ if self.status == "Unverified" and self.email_verified:
+ frappe.throw(_("A verified appointment cannot be moved back to 'Unverified' status."))
+
+ if self.status == "Open" and not self.email_verified:
+ frappe.throw(
+ _("An appointment booked through the portal can only be opened via email verification.")
+ )
+
+ def validate_backdated_booking(self):
+ if get_datetime(self.scheduled_time) < now_datetime():
+ frappe.throw(_("Appointment cannot be scheduled for a past time."))
+
+ def validate_advanced_booking(self):
+ advance_booking_days = cint(get_booking_settings().advance_booking_days)
+
+ if advance_booking_days and date_diff(self.scheduled_time, now_datetime()) > advance_booking_days:
+ frappe.throw(
+ _("Appointment can only be scheduled up to {0} day(s) in advance.").format(
+ advance_booking_days
+ )
+ )
+
+ def validate_holiday(self):
+ holiday_list = get_booking_settings().holiday_list
+
+ if not holiday_list:
+ frappe.throw(_("Please add a valid Holiday List on Appointment Booking Settings."))
+
+ if is_holiday(holiday_list, getdate(self.scheduled_time)):
+ frappe.throw(_("Appointment cannot be scheduled on a holiday."))
+
+ def validate_slot_timing(self):
+ settings = get_booking_settings()
+ if not settings.availability_of_slots:
+ frappe.throw(_("No availability of slots are found. Please add on Appointment Booking Settings."))
+
+ scheduled_time = get_datetime(self.scheduled_time)
+ day_of_week = WEEKDAYS[scheduled_time.weekday()]
+ slot_start = timedelta(
+ hours=scheduled_time.hour, minutes=scheduled_time.minute, seconds=scheduled_time.second
)
- if customer_list:
- return customer_list[0].name
- return None
+ slot_end = slot_start + timedelta(minutes=cint(settings.appointment_duration))
+
+ for slot in settings.availability_of_slots:
+ if slot.day_of_week == day_of_week and slot.from_time <= slot_start and slot_end <= slot.to_time:
+ return
+
+ frappe.throw(_("Appointment must be scheduled within the available slot timings."))
+
+ def validate_available_time_slot(self):
+ settings = get_booking_settings()
+ if not cint(settings.number_of_agents):
+ return
+
+ # the locking read serializes concurrent bookings for the same window,
+ # so two simultaneous requests cannot both pass the capacity check
+ booked = count_overlapping_appointments(
+ self.scheduled_time,
+ cint(settings.appointment_duration),
+ exclude_appointment=self.name,
+ for_update=True,
+ )
+
+ if booked >= cint(settings.number_of_agents):
+ frappe.throw(_("Time slot is not available"))
def before_insert(self):
- number_of_appointments_in_same_slot = frappe.db.count(
- "Appointment", filters={"scheduled_time": self.scheduled_time}
- )
- number_of_agents = frappe.db.get_single_value("Appointment Booking Settings", "number_of_agents")
- if number_of_agents != 0:
- if number_of_appointments_in_same_slot >= number_of_agents:
- frappe.throw(_("Time slot is not available"))
- # Link lead
- if not self.party:
- lead = self.find_lead_by_email()
- customer = self.find_customer_by_email()
- if customer:
- self.appointment_with = "Customer"
- self.party = customer
- else:
- self.appointment_with = "Lead"
- self.party = lead
+ # Set status to "Unverified" for new Appointments.
+ if self.created_through_portal:
+ self.status = "Unverified"
+ return
+
+ self.link_customer_lead()
def after_insert(self):
- if self.party:
- # Create Calendar event
+ if not self.created_through_portal and self.party:
self.auto_assign()
self.create_calendar_event()
- else:
- # Set status to unverified
- self.db_set("status", "Unverified")
- # Send email to confirm
- self.send_confirmation_email()
+ return
+
+ # Send email to confirm
+ self.send_confirmation_email()
+
+ def on_update(self):
+ # capture transitions before nested saves during materialization
+ # refresh the before-save snapshot
+ status_changed = self.has_value_changed("status")
+ email_just_verified = bool(
+ self.created_through_portal and self.email_verified
+ ) and self.has_value_changed("email_verified")
+
+ self.link_auto_assign_and_create_calendar_event()
+
+ if email_just_verified:
+ self.send_appointment_confirmed_email()
+
+ if status_changed:
+ self.update_event_and_assignments_status()
+
+ def on_trash(self):
+ # the Event only references the party, not the appointment,
+ # so it must be cleaned up explicitly
+ if not self.calendar_event:
+ return
+
+ event = self.calendar_event
+ self.db_set("calendar_event", None, update_modified=False)
+ frappe.delete_doc("Event", event, ignore_permissions=True)
def send_confirmation_email(self):
- verify_url = self._get_verify_url()
- template = "confirm_appointment"
- args = {
- "link": verify_url,
- "site_url": frappe.utils.get_url(),
- "full_name": self.customer_name,
- }
+ self.send_email_to_customer(
+ template="confirm_appointment",
+ subject=_("Appointment Confirmation"),
+ args={"link": self._get_verify_url(), "expiry_minutes": get_verification_link_expiry()},
+ )
+ frappe.msgprint(_("Please check your email to confirm the appointment."))
+
+ def send_appointment_confirmed_email(self):
+ self.send_email_to_customer(
+ template="appointment_confirmed",
+ subject=_("Appointment Confirmed"),
+ args={"scheduled_time": frappe.utils.format_datetime(self.scheduled_time)},
+ reference_doctype="Appointment",
+ reference_name=self.name,
+ )
+
+ def send_email_to_customer(self, template, subject, args, **kwargs):
frappe.sendmail(
recipients=[self.customer_email],
template=template,
- args=args,
- subject=_("Appointment Confirmation"),
+ args={"full_name": self.customer_name, "site_url": frappe.utils.get_url(), **args},
+ subject=subject,
+ **kwargs,
)
- if frappe.session.user == "Guest":
- frappe.msgprint(_("Please check your email to confirm the appointment"))
- else:
- frappe.msgprint(
- _("Appointment was created. But no lead was found. Please check the email to confirm")
- )
- def on_change(self):
- # Sync Calendar
- if not self.calendar_event:
+ def link_auto_assign_and_create_calendar_event(self):
+ if self.is_new() or (self.created_through_portal and not self.email_verified):
return
+
+ if not self.calendar_event:
+ # first materialization: link the party, assign an agent, create the event
+ self.link_customer_lead()
+ self.auto_assign()
+ self.create_calendar_event()
+
+ self.sync_calendar_event()
+
+ def sync_calendar_event(self):
+ if not self.calendar_event or not self.has_value_changed("scheduled_time"):
+ return
+
cal_event = frappe.get_doc("Event", self.calendar_event)
cal_event.starts_on = self.scheduled_time
cal_event.save(ignore_permissions=True)
- def set_verified(self, email):
- if email != self.customer_email:
- frappe.throw(_("Email verification failed."))
- # Create new lead
+ def update_event_and_assignments_status(self):
+ """Close or reopen the calendar event and assignments along with the appointment."""
+ if self.status == "Unverified":
+ return
+
+ is_closed = self.status == "Closed"
+ new_status = "Closed" if is_closed else "Open"
+
+ if self.calendar_event:
+ frappe.db.set_value("Event", self.calendar_event, "status", new_status)
+
+ # only move ToDos between Open and Closed - never touch Cancelled ones
+ todo_filters = {
+ "reference_type": "Appointment",
+ "reference_name": self.name,
+ "status": "Open" if is_closed else "Closed",
+ }
+ frappe.db.set_value("ToDo", todo_filters, "status", new_status)
+
+ def link_customer_lead(self):
+ if not self.party:
+ customer = self.find_party_by_email("Customer")
+ self.appointment_with = "Customer" if customer else "Lead"
+ self.party = customer or self.find_party_by_email("Lead")
+
self.create_lead_and_link()
- # Remove unverified status
- self.status = "Open"
- # Create calender event
- self.auto_assign()
- self.create_calendar_event()
- self.save(ignore_permissions=True)
- if not frappe.in_test:
- frappe.db.commit()
+
+ def find_party_by_email(self, doctype):
+ party = frappe.get_all(doctype, filters={"email_id": self.customer_email}, limit=1, pluck="name")
+ return party[0] if party else None
def create_lead_and_link(self):
# Return if already linked
@@ -140,86 +269,39 @@ class Appointment(Document):
if self.customer_details:
lead.append(
"notes",
- {
- "note": self.customer_details,
- "added_by": frappe.session.user,
- "added_on": now(),
- },
+ {"note": self.customer_details, "added_by": frappe.session.user, "added_on": now()},
)
- lead.insert(ignore_permissions=True)
-
- # Link lead
- self.party = lead.name
+ self.party = lead.insert(ignore_permissions=True).name
def auto_assign(self):
- existing_assignee = self.get_assignee_from_latest_opportunity()
- if existing_assignee:
- # If the latest opportunity is assigned to someone
- # Assign the appointment to the same
- self.assign_agent(existing_assignee)
- return
if self._assign:
return
- available_agents = _get_agents_sorted_by_asc_workload(getdate(self.scheduled_time))
- for agent in available_agents:
- if _check_agent_availability(agent, self.scheduled_time):
- self.assign_agent(agent[0])
- break
+
+ if existing_assignee := self.get_assignee_from_latest_opportunity():
+ # assign to whoever handles the party's latest opportunity
+ self.assign_agent(existing_assignee)
+ return
+
+ busy_agents = get_busy_agents(self.scheduled_time)
+ for agent in _get_agents_sorted_by_asc_workload(getdate(self.scheduled_time)):
+ if agent not in busy_agents:
+ self.assign_agent(agent)
+ break
def get_assignee_from_latest_opportunity(self):
- if not self.party:
+ if not self.party or not frappe.db.exists("Lead", self.party):
return None
- if not frappe.db.exists("Lead", self.party):
- return None
- opporutnities = frappe.get_list(
+
+ opportunities = frappe.get_all(
"Opportunity",
- filters={
- "party_name": self.party,
- },
- ignore_permissions=True,
+ filters={"party_name": self.party},
+ fields=["_assign"],
order_by="creation desc",
+ limit=1,
)
- if not opporutnities:
- return None
- latest_opportunity = frappe.get_doc("Opportunity", opporutnities[0].name)
- assignee = latest_opportunity._assign
- if not assignee:
- return None
- assignee = frappe.parse_json(assignee)[0]
- return assignee
-
- def create_calendar_event(self):
- if self.calendar_event:
- return
- appointment_event = frappe.get_doc(
- {
- "doctype": "Event",
- "subject": " ".join(["Appointment with", self.customer_name]),
- "starts_on": self.scheduled_time,
- "status": "Open",
- "type": "Public",
- "send_reminder": frappe.db.get_single_value(
- "Appointment Booking Settings", "email_reminders"
- ),
- "event_participants": [
- dict(reference_doctype=self.appointment_with, reference_docname=self.party)
- ],
- }
- )
- employee = _get_employee_from_user(self._assign)
- if employee:
- appointment_event.append(
- "event_participants", dict(reference_doctype="Employee", reference_docname=employee.name)
- )
- appointment_event.insert(ignore_permissions=True)
- self.calendar_event = appointment_event.name
- self.save(ignore_permissions=True)
-
- def _get_verify_url(self):
- verify_route = "/book_appointment/verify"
- params = {"email": self.customer_email, "appointment": self.name}
- return get_url(verify_route + "?" + get_signed_params(params))
+ assignees = opportunities and frappe.parse_json(opportunities[0]._assign or "[]")
+ return assignees[0] if assignees else None
def assign_agent(self, agent):
if not frappe.has_permission(doc=self, user=agent):
@@ -227,45 +309,157 @@ class Appointment(Document):
add_assignment({"doctype": self.doctype, "name": self.name, "assign_to": [agent]})
+ def create_calendar_event(self):
+ if self.calendar_event:
+ return
+
+ event = frappe.get_doc(
+ {
+ "doctype": "Event",
+ "subject": f"Appointment with {self.customer_name}",
+ "starts_on": self.scheduled_time,
+ "status": "Open",
+ "type": "Public",
+ "send_reminder": cint(get_booking_settings().email_reminders),
+ "event_participants": self.get_event_participants(),
+ }
+ ).insert(ignore_permissions=True)
+
+ self.calendar_event = event.name
+ self.save(ignore_permissions=True)
+
+ def get_event_participants(self):
+ participants = [dict(reference_doctype=self.appointment_with, reference_docname=self.party)]
+
+ if employee := _get_employee_from_user(self._assign):
+ participants.append(dict(reference_doctype="Employee", reference_docname=employee.name))
+
+ return participants
+
+ def _get_verify_url(self):
+ key = self.generate_verification_key()
+ return get_url("/book_appointment/verify?" + urlencode({"key": key}))
+
+ def generate_verification_key(self):
+ # store only the hash; the raw key lives solely in the emailed link
+ key = frappe.generate_hash()
+ self.db_set("verification_token", sha256_hash(key), update_modified=False)
+ return key
+
+
+def get_booking_settings():
+ return frappe.get_cached_doc("Appointment Booking Settings")
+
+
+def is_appointment_scheduling_enabled():
+ return bool(cint(get_booking_settings().enable_scheduling))
+
+
+def get_verification_link_expiry():
+ """Verification link expiry window in minutes."""
+ return cint(get_booking_settings().verification_link_expiry_duration)
+
+
+def count_overlapping_appointments(
+ scheduled_time, appointment_duration, exclude_appointment=None, for_update=False
+):
+ """Count non-Closed appointments whose duration window overlaps `scheduled_time`.
+ With `for_update`, the range stays locked until commit, serializing concurrent bookings."""
+ # select the rows (not COUNT) so `for_update` stays valid: PostgreSQL
+ # rejects `FOR UPDATE` combined with an aggregate function
+ appointment = frappe.qb.DocType("Appointment")
+ query = (
+ frappe.qb.from_(appointment)
+ .select(appointment.name)
+ .where(appointment.scheduled_time > add_to_date(scheduled_time, minutes=-appointment_duration))
+ .where(appointment.scheduled_time < add_to_date(scheduled_time, minutes=appointment_duration))
+ .where(appointment.status != "Closed")
+ )
+
+ if exclude_appointment:
+ query = query.where(appointment.name != exclude_appointment)
+
+ if for_update:
+ query = query.for_update()
+
+ return len(query.run())
+
+
+def handle_expired_unverified_appointments():
+ """Close or delete Unverified appointments whose verification link has expired."""
+ expiry = get_verification_link_expiry()
+ if not expiry:
+ return
+
+ cutoff = add_to_date(now_datetime(), minutes=-expiry)
+ filters = {"status": "Unverified", "creation": ("<", cutoff)}
+ action = get_booking_settings().action_for_expired_unverified_appointments or "Mark as Closed"
+
+ if action == "Mark as Closed":
+ frappe.db.set_value("Appointment", filters, "status", "Closed")
+ elif action == "Delete Permanently":
+ for name in frappe.get_all("Appointment", filters=filters, pluck="name"):
+ frappe.delete_doc("Appointment", name, ignore_permissions=True)
+
def _get_agents_sorted_by_asc_workload(date):
- appointments = frappe.get_all("Appointment", fields="*")
- agent_list = _get_agent_list_as_strings()
- if not appointments:
- return agent_list
- appointment_counter = Counter(agent_list)
- for appointment in appointments:
- assign_data = appointment._assign
- if isinstance(assign_data, str):
- assign_data = assign_data.strip()
- if not assign_data:
- continue
- assigned_to = frappe.parse_json(assign_data)
- if assigned_to and (assigned_to[0] in agent_list) and getdate(appointment.scheduled_time) == date:
- appointment_counter[assigned_to[0]] += 1
- sorted_agent_list = appointment_counter.most_common()
- sorted_agent_list.reverse()
- return sorted_agent_list
+ # count only the given day's assignments; scheduled_time is indexed so the
+ # date range is resolved in SQL instead of scanning every appointment ever
+ workload = Counter(agent.user for agent in get_booking_settings().agent_list)
+ assigns = frappe.get_all(
+ "Appointment",
+ filters=[
+ ["_assign", "is", "set"],
+ ["scheduled_time", ">=", getdate(date)],
+ ["scheduled_time", "<", add_to_date(getdate(date), days=1)],
+ ],
+ pluck="_assign",
+ )
+
+ for assign in assigns:
+ assignees = frappe.parse_json((assign or "").strip() or "[]")
+ if assignees and assignees[0] in workload:
+ workload[assignees[0]] += 1
+
+ return [agent for agent, _workload in reversed(workload.most_common())]
-def _get_agent_list_as_strings():
- agent_list_as_strings = []
- agent_list = frappe.get_doc("Appointment Booking Settings").agent_list
- for agent in agent_list:
- agent_list_as_strings.append(agent.user)
- return agent_list_as_strings
+def get_busy_agents(scheduled_time):
+ """Agents already assigned to a non-Closed appointment overlapping `scheduled_time`."""
+ duration = _get_appointment_duration()
+ assigns = frappe.get_all(
+ "Appointment",
+ filters=[
+ ["scheduled_time", ">", add_to_date(scheduled_time, minutes=-duration)],
+ ["scheduled_time", "<", add_to_date(scheduled_time, minutes=duration)],
+ ["status", "!=", "Closed"],
+ ],
+ pluck="_assign",
+ )
+ return {assignee for assign in assigns for assignee in frappe.parse_json(assign or "[]")}
def _check_agent_availability(agent_email, scheduled_time):
- appointemnts_at_scheduled_time = frappe.get_all("Appointment", filters={"scheduled_time": scheduled_time})
- for appointment in appointemnts_at_scheduled_time:
- if appointment._assign == agent_email:
- return False
- return True
+ return agent_email not in get_busy_agents(scheduled_time)
+
+
+def get_booked_slot_times(from_time, to_time):
+ """scheduled_times of non-Closed appointments within (from_time, to_time), for slot availability."""
+ return frappe.get_all(
+ "Appointment",
+ filters=[
+ ["scheduled_time", ">", from_time],
+ ["scheduled_time", "<", to_time],
+ ["status", "!=", "Closed"],
+ ],
+ pluck="scheduled_time",
+ )
+
+
+def _get_appointment_duration():
+ return cint(get_booking_settings().appointment_duration)
def _get_employee_from_user(user):
employee_docname = frappe.db.get_value("Employee", {"user_id": user})
- if employee_docname:
- return frappe.get_doc("Employee", employee_docname)
- return None
+ return frappe.get_doc("Employee", employee_docname) if employee_docname else None
diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py
index 24974ecf472..80c0ced648e 100644
--- a/erpnext/crm/doctype/appointment/test_appointment.py
+++ b/erpnext/crm/doctype/appointment/test_appointment.py
@@ -1,37 +1,167 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import datetime
-import unittest
+from unittest.mock import patch
+from urllib.parse import parse_qs, urlparse
import frappe
+from frappe.utils import add_to_date, getdate, now_datetime, set_request
+from frappe.utils.data import sha256_hash
+from erpnext.crm.doctype.appointment.appointment import (
+ Appointment,
+ _check_agent_availability,
+ handle_expired_unverified_appointments,
+)
+from erpnext.setup.doctype.holiday_list.test_holiday_list import make_holiday_list
from erpnext.tests.utils import ERPNextTestSuite
+from erpnext.www.book_appointment.index import create_appointment, get_appointment_slots
+from erpnext.www.book_appointment.verify import index as verify_index
LEAD_EMAIL = "test_appointment_lead@example.com"
+VERIFICATION_EXPIRY_MINUTES = 30
+ALL_WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
-def create_test_appointment():
- test_appointment = frappe.get_doc(
- {
- "doctype": "Appointment",
- "status": "Open",
- "customer_name": "Test Lead",
- "customer_phone_number": "666",
- "customer_skype": "test",
- "customer_email": LEAD_EMAIL,
- "scheduled_time": datetime.datetime.now(),
- "customer_details": "Hello, Friend!",
- }
- )
+def create_test_appointment(**kwargs):
+ args = {
+ "doctype": "Appointment",
+ "status": "Open",
+ "customer_name": "Test Lead",
+ "customer_phone_number": "666",
+ "customer_skype": "test",
+ "customer_email": LEAD_EMAIL,
+ "scheduled_time": add_to_date(now_datetime(), hours=2),
+ "customer_details": "Hello, Friend!",
+ }
+ args.update(kwargs)
+ test_appointment = frappe.get_doc(args)
test_appointment.insert()
return test_appointment
+def create_lead(email, name="Existing Lead"):
+ frappe.db.delete("Lead", {"email_id": email})
+ return frappe.get_doc({"doctype": "Lead", "lead_name": name, "email_id": email}).insert(
+ ignore_permissions=True
+ )
+
+
+def set_booking_setting(field, value):
+ frappe.db.set_single_value("Appointment Booking Settings", field, value)
+
+
+def slot_on(days_from_now, hour, minute=0):
+ day = datetime.date.today() + datetime.timedelta(days=days_from_now)
+ return datetime.datetime.combine(day, datetime.time(hour, minute))
+
+
+def backdate_creation(appointment_name, minutes):
+ frappe.db.set_value(
+ "Appointment",
+ appointment_name,
+ "creation",
+ add_to_date(now_datetime(), minutes=-minutes),
+ update_modified=False,
+ )
+
+
+def get_status(appointment_name):
+ return frappe.db.get_value("Appointment", appointment_name, "status")
+
+
+def get_assignees(appointment_name):
+ return frappe.parse_json(frappe.db.get_value("Appointment", appointment_name, "_assign") or "[]")
+
+
+def get_todo_statuses(appointment_name):
+ return frappe.get_all(
+ "ToDo",
+ filters={"reference_type": "Appointment", "reference_name": appointment_name},
+ pluck="status",
+ )
+
+
+def parse_verify_url(verify_url):
+ parsed = urlparse(verify_url)
+ return parsed, {key: value[0] for key, value in parse_qs(parsed.query).items()}
+
+
class TestAppointment(ERPNextTestSuite):
def setUp(self):
+ set_booking_setting("verification_link_expiry_duration", VERIFICATION_EXPIRY_MINUTES)
frappe.db.delete("Lead", {"email_id": LEAD_EMAIL})
self.test_appointment = create_test_appointment()
- self.test_appointment.set_verified(self.test_appointment.customer_email)
+
+ def _configure_booking_settings(self, holiday_dates=None, agents=None):
+ holiday_list = make_holiday_list(
+ "_Test Appointment Holiday List",
+ from_date=getdate(),
+ to_date=add_to_date(getdate(), days=60),
+ holiday_dates=holiday_dates or [],
+ )
+
+ settings = frappe.get_doc("Appointment Booking Settings")
+ settings.enable_scheduling = 1
+ settings.enable_appointment_portal = 1
+ settings.appointment_duration = 30
+ settings.advance_booking_days = 30
+ settings.verification_link_expiry_duration = VERIFICATION_EXPIRY_MINUTES
+ settings.holiday_list = holiday_list.name
+ settings.set("agent_list", [])
+ for agent in agents or ["Administrator"]:
+ settings.append("agent_list", {"user": agent})
+ settings.set("availability_of_slots", [])
+ for day in ALL_WEEKDAYS:
+ settings.append(
+ "availability_of_slots", {"day_of_week": day, "from_time": "09:00:00", "to_time": "17:00:00"}
+ )
+ settings.save()
+
+ def _create_portal_appointment(self, email, days_from_now=7, time="10:00:00"):
+ """Book as Guest. The verification email is mocked and kept on
+ ``self._verification_email_mock`` for assertions."""
+ if not getattr(self, "_booking_settings_configured", False):
+ self._configure_booking_settings()
+ self._booking_settings_configured = True
+
+ with self.set_user("Guest"), patch.object(Appointment, "send_confirmation_email") as mock_send:
+ appointment = create_appointment(
+ date=str(datetime.date.today() + datetime.timedelta(days=days_from_now)),
+ time=time,
+ tz="UTC",
+ contact={"name": "Portal Visitor", "email": email, "number": "123", "skype": "", "notes": ""},
+ )
+ self._verification_email_mock = mock_send
+ return appointment
+
+ def _request_verification(self, appointment, verify_url=None):
+ """Simulate the GET request made by clicking the emailed verification link.
+
+ The confirmation email sent on successful verification is mocked and kept
+ on ``self._confirmed_email_mock`` for assertions.
+ """
+ parsed, params = parse_verify_url(verify_url or appointment._get_verify_url())
+
+ old_request = getattr(frappe.local, "request", None)
+ old_form_dict = frappe.local.form_dict
+ old_user = frappe.session.user
+ try:
+ # the real link is clicked by an anonymous visitor; set_user resets
+ # form_dict, so switch the user before populating the request
+ frappe.set_user("Guest")
+ set_request(method="GET", path=f"{parsed.path}?{parsed.query}")
+ frappe.local.form_dict = frappe._dict(params)
+ context = frappe._dict()
+ with patch.object(Appointment, "send_appointment_confirmed_email") as mock_confirmed:
+ verify_index.get_context(context)
+ self._confirmed_email_mock = mock_confirmed
+ return context
+ finally:
+ frappe.set_user(old_user)
+ frappe.local.request = old_request
+ frappe.local.form_dict = old_form_dict
+ frappe.local.flags.commit = False
def test_calendar_event_created(self):
cal_event = frappe.get_doc("Event", self.test_appointment.calendar_event)
@@ -39,3 +169,371 @@ class TestAppointment(ERPNextTestSuite):
def test_lead_linked(self):
self.assertTrue(self.test_appointment.party)
+
+ def test_desk_created_appointment_skips_email_verification(self):
+ """Appointments created from the desk (created_through_portal unset) must be
+ linked and confirmed immediately - no verification email should be sent."""
+ with patch.object(Appointment, "send_confirmation_email") as mock_send:
+ appointment = create_test_appointment(customer_email="another_desk_lead@example.com")
+
+ mock_send.assert_not_called()
+ self.assertEqual(appointment.status, "Open")
+ self.assertTrue(appointment.party)
+ frappe.db.delete("Lead", {"email_id": "another_desk_lead@example.com"})
+
+ def test_portal_booking_stays_unverified_for_existing_lead(self):
+ """A portal booking whose email matches an existing Lead/Customer must NOT
+ be auto-linked - it must stay Unverified until the email is confirmed."""
+ create_lead("existing_lead@example.com")
+ appointment = self._create_portal_appointment("existing_lead@example.com", days_from_now=5)
+
+ self._verification_email_mock.assert_called_once()
+ self.assertTrue(appointment.created_through_portal)
+ self.assertEqual(appointment.status, "Unverified")
+ self.assertFalse(appointment.email_verified)
+ self.assertFalse(appointment.party)
+
+ def test_verify_url_uses_opaque_token(self):
+ appointment = self._create_portal_appointment("portal_visitor@example.com")
+ parsed, params = parse_verify_url(appointment._get_verify_url())
+
+ # the link carries only an opaque key - no email, name or signed params
+ self.assertEqual(set(params), {"key"})
+ self.assertNotIn("email", parsed.query)
+ # only the hash of that key is stored on the appointment
+ stored = frappe.db.get_value("Appointment", appointment.name, "verification_token")
+ self.assertEqual(stored, sha256_hash(params["key"]))
+
+ def test_email_verification_within_expiry_window(self):
+ # Link used within the validity window - verification succeeds and the
+ # appointment gets linked, assigned and added to the calendar
+ on_time = self._create_portal_appointment("portal_visitor_on_time@example.com")
+ context = self._request_verification(on_time)
+
+ self.assertTrue(context.success)
+ self._confirmed_email_mock.assert_called_once()
+ on_time.reload()
+ self.assertEqual(on_time.status, "Open")
+ self.assertTrue(on_time.email_verified)
+ self.assertTrue(on_time.party)
+ self.assertTrue(on_time.calendar_event)
+
+ # Link used after the validity window - verification fails
+ late = self._create_portal_appointment("portal_visitor_late@example.com", days_from_now=10)
+ after_expiry = add_to_date(now_datetime(), minutes=VERIFICATION_EXPIRY_MINUTES + 1)
+ with patch.object(verify_index, "now_datetime", return_value=after_expiry):
+ context = self._request_verification(late)
+
+ self.assertFalse(context.success)
+ self._confirmed_email_mock.assert_not_called()
+ late.reload()
+ self.assertEqual(late.status, "Unverified")
+ self.assertFalse(late.email_verified)
+ self.assertFalse(late.party)
+
+ def test_verification_link_reused_after_success(self):
+ appointment = self._create_portal_appointment("portal_visitor_twice@example.com")
+ verify_url = appointment._get_verify_url()
+
+ context = self._request_verification(appointment, verify_url=verify_url)
+ self.assertTrue(context.success)
+ self._confirmed_email_mock.assert_called_once()
+
+ # re-clicking the link is idempotent and does not send another email
+ context = self._request_verification(appointment, verify_url=verify_url)
+ self.assertTrue(context.success)
+ self.assertIn("already verified", context.message)
+ self._confirmed_email_mock.assert_not_called()
+
+ def test_verification_link_for_deleted_appointment(self):
+ """A verification link can outlive its appointment - clicking it must
+ render a friendly message, not crash."""
+ appointment = self._create_portal_appointment("portal_visitor_gone@example.com")
+ verify_url = appointment._get_verify_url()
+ frappe.delete_doc("Appointment", appointment.name, ignore_permissions=True)
+
+ context = self._request_verification(appointment, verify_url=verify_url)
+
+ self.assertFalse(context.success)
+ self.assertIn("book the appointment again", context.message)
+
+ def test_reschedule_syncs_calendar_event(self):
+ new_time = add_to_date(self.test_appointment.scheduled_time, hours=1)
+ self.test_appointment.scheduled_time = new_time
+ self.test_appointment.save()
+
+ starts_on = frappe.db.get_value("Event", self.test_appointment.calendar_event, "starts_on")
+ self.assertEqual(starts_on, new_time)
+
+ def test_portal_endpoint_disabled(self):
+ self._configure_booking_settings()
+ set_booking_setting("enable_appointment_portal", 0)
+
+ with self.set_user("Guest"), self.assertRaises(frappe.Redirect):
+ create_appointment(
+ date=str(datetime.date.today() + datetime.timedelta(days=3)),
+ time="10:00:00",
+ tz="UTC",
+ contact={
+ "name": "Blocked",
+ "email": "blocked@example.com",
+ "number": "1",
+ "skype": "",
+ "notes": "",
+ },
+ )
+
+ def test_booked_slot_unavailable_on_portal(self):
+ from frappe.utils.data import get_system_timezone
+
+ self._configure_booking_settings()
+ tz = get_system_timezone()
+ day = datetime.date.today() + datetime.timedelta(days=2)
+
+ def get_availability():
+ with self.set_user("Guest"):
+ slots = get_appointment_slots(str(day), tz)
+ return {slot["time"].strftime("%H:%M"): slot["availability"] for slot in slots}
+
+ booked = create_test_appointment(
+ customer_email="slot_taken@example.com", scheduled_time=slot_on(2, 10)
+ )
+
+ availability = get_availability()
+ self.assertFalse(availability["10:00"])
+ self.assertTrue(availability["13:00"])
+
+ # closing the appointment frees its slot on the portal
+ booked.status = "Closed"
+ booked.save()
+ self.assertTrue(get_availability()["10:00"])
+
+ # an off-grid desk appointment blocks every portal slot it overlaps
+ create_test_appointment(customer_email="off_grid@example.com", scheduled_time=slot_on(2, 13, 15))
+ availability = get_availability()
+ self.assertFalse(availability["13:00"])
+ self.assertFalse(availability["13:30"])
+ self.assertTrue(availability["14:00"])
+
+ def test_expired_unverified_appointments_are_closed(self):
+ stale = self._create_portal_appointment("portal_visitor_stale@example.com", days_from_now=8)
+ fresh = self._create_portal_appointment("portal_visitor_fresh@example.com", days_from_now=9)
+ verify_url = stale._get_verify_url()
+
+ backdate_creation(stale.name, VERIFICATION_EXPIRY_MINUTES + 15)
+ set_booking_setting("action_for_expired_unverified_appointments", "Mark as Closed")
+
+ handle_expired_unverified_appointments()
+
+ self.assertEqual(get_status(stale.name), "Closed")
+ self.assertEqual(get_status(fresh.name), "Unverified")
+ # Open appointments are never touched, regardless of age
+ self.assertEqual(get_status(self.test_appointment.name), "Open")
+
+ # clicking the link of a closed appointment renders a friendly message
+ context = self._request_verification(stale, verify_url=verify_url)
+ self.assertFalse(context.success)
+ self.assertIn("closed", context.message)
+
+ def test_expired_unverified_appointments_are_deleted(self):
+ stale = self._create_portal_appointment("portal_visitor_purged@example.com", days_from_now=8)
+ fresh = self._create_portal_appointment("portal_visitor_kept@example.com", days_from_now=9)
+
+ backdate_creation(stale.name, VERIFICATION_EXPIRY_MINUTES + 15)
+ set_booking_setting("action_for_expired_unverified_appointments", "Delete Permanently")
+
+ handle_expired_unverified_appointments()
+
+ self.assertFalse(frappe.db.exists("Appointment", stale.name))
+ self.assertTrue(frappe.db.exists("Appointment", fresh.name))
+ self.assertTrue(frappe.db.exists("Appointment", self.test_appointment.name))
+
+ def test_cleanup_skipped_when_expiry_not_configured(self):
+ appointment = self._create_portal_appointment("portal_visitor_no_expiry@example.com")
+ backdate_creation(appointment.name, 5)
+ set_booking_setting("verification_link_expiry_duration", 0)
+
+ handle_expired_unverified_appointments()
+
+ self.assertEqual(get_status(appointment.name), "Unverified")
+
+ def test_status_transition_rules(self):
+ # desk appointments can never be Unverified
+ with self.assertRaises(frappe.ValidationError):
+ create_test_appointment(customer_email="desk_unverified@example.com", status="Unverified")
+
+ # portal appointments cannot be opened manually before verification
+ unverified = self._create_portal_appointment("manual_open@example.com")
+ unverified.status = "Open"
+ with self.assertRaises(frappe.ValidationError):
+ unverified.save(ignore_permissions=True)
+
+ # verified appointments cannot be reverted to Unverified
+ verified = self._create_portal_appointment("revert_unverified@example.com", days_from_now=8)
+ self._request_verification(verified)
+ verified.reload()
+ verified.status = "Unverified"
+ with self.assertRaises(frappe.ValidationError):
+ verified.save(ignore_permissions=True)
+
+ # both desk and verified portal appointments can be closed and reopened
+ for appointment in (self.test_appointment, verified):
+ appointment.reload()
+ appointment.status = "Closed"
+ appointment.save(ignore_permissions=True)
+ appointment.status = "Open"
+ appointment.save(ignore_permissions=True)
+ self.assertEqual(appointment.status, "Open")
+
+ def test_agent_auto_assignment(self):
+ agent_email = "appointment_agent@example.com"
+ if not frappe.db.exists("User", agent_email):
+ frappe.get_doc(
+ {"doctype": "User", "email": agent_email, "first_name": "Appointment Agent"}
+ ).insert(ignore_permissions=True)
+
+ self._configure_booking_settings(agents=["Administrator", agent_email])
+ first = create_test_appointment(
+ customer_email="assigned_one@example.com", scheduled_time=slot_on(2, 11)
+ )
+ second = create_test_appointment(
+ customer_email="assigned_two@example.com", scheduled_time=slot_on(2, 11)
+ )
+
+ # both appointments in the same slot get an agent, and never the same one
+ self.assertTrue(get_assignees(first.name))
+ self.assertTrue(get_assignees(second.name))
+ self.assertNotEqual(get_assignees(first.name), get_assignees(second.name))
+
+ # closing an assigned appointment closes its ToDo without re-assigning
+ first.reload()
+ first.status = "Closed"
+ first.save()
+ self.assertTrue(get_todo_statuses(first.name))
+ self.assertTrue(all(status == "Closed" for status in get_todo_statuses(first.name)))
+
+ # reopening brings the ToDos back
+ first.status = "Open"
+ first.save()
+ self.assertTrue(all(status == "Open" for status in get_todo_statuses(first.name)))
+
+ def test_agent_busy_for_the_whole_appointment_duration(self):
+ self._configure_booking_settings()
+ slot = slot_on(3, 11)
+ appointment = create_test_appointment(customer_email="busy_agent@example.com", scheduled_time=slot)
+ assignee = get_assignees(appointment.name)[0]
+
+ # busy anywhere inside the 30-minute appointment window, free right after it
+ self.assertFalse(_check_agent_availability(assignee, slot))
+ self.assertFalse(_check_agent_availability(assignee, slot + datetime.timedelta(minutes=15)))
+ self.assertTrue(_check_agent_availability(assignee, slot + datetime.timedelta(minutes=30)))
+
+ def test_closed_appointment_closes_calendar_event(self):
+ self.test_appointment.status = "Closed"
+ self.test_appointment.save()
+ event_status = frappe.db.get_value("Event", self.test_appointment.calendar_event, "status")
+ self.assertEqual(event_status, "Closed")
+
+ # reopening the appointment reopens the calendar event
+ self.test_appointment.status = "Open"
+ self.test_appointment.save()
+ event_status = frappe.db.get_value("Event", self.test_appointment.calendar_event, "status")
+ self.assertEqual(event_status, "Open")
+
+ def test_deleting_appointment_deletes_calendar_event(self):
+ event = self.test_appointment.calendar_event
+ self.assertTrue(frappe.db.exists("Event", event))
+
+ frappe.delete_doc("Appointment", self.test_appointment.name)
+
+ self.assertFalse(frappe.db.exists("Event", event))
+
+ def test_backdated_appointment_is_rejected(self):
+ with self.assertRaises(frappe.ValidationError):
+ create_test_appointment(
+ customer_email="backdated@example.com",
+ scheduled_time=add_to_date(now_datetime(), hours=-1),
+ )
+
+ def test_booking_beyond_advance_window_is_rejected(self):
+ self._configure_booking_settings()
+ set_booking_setting("advance_booking_days", 7)
+
+ # within the advance booking window - allowed
+ within = create_test_appointment(
+ customer_email="advance_within@example.com", scheduled_time=slot_on(5, 10)
+ )
+ self.assertTrue(frappe.db.exists("Appointment", within.name))
+
+ # beyond the advance booking window - rejected
+ with self.assertRaises(frappe.ValidationError):
+ create_test_appointment(
+ customer_email="advance_beyond@example.com", scheduled_time=slot_on(8, 10)
+ )
+
+ def test_appointment_on_holiday_is_rejected(self):
+ holiday = add_to_date(getdate(), days=3)
+ self._configure_booking_settings(
+ holiday_dates=[{"holiday_date": holiday, "description": "Test Holiday"}]
+ )
+
+ with self.assertRaises(frappe.ValidationError):
+ create_test_appointment(customer_email="on_holiday@example.com", scheduled_time=slot_on(3, 10))
+
+ # the day after the holiday is bookable
+ after_holiday = create_test_appointment(
+ customer_email="after_holiday@example.com", scheduled_time=slot_on(4, 10)
+ )
+ self.assertTrue(frappe.db.exists("Appointment", after_holiday.name))
+
+ def test_appointment_outside_slot_timing_is_rejected(self):
+ self._configure_booking_settings()
+
+ # before the slot opens
+ with self.assertRaises(frappe.ValidationError):
+ create_test_appointment(customer_email="before_opening@example.com", scheduled_time=slot_on(2, 8))
+
+ # starts within the slot but would end after it closes
+ with self.assertRaises(frappe.ValidationError):
+ create_test_appointment(
+ customer_email="past_closing@example.com", scheduled_time=slot_on(2, 16, 45)
+ )
+
+ # within the slot timings
+ within = create_test_appointment(
+ customer_email="within_slot@example.com", scheduled_time=slot_on(2, 10)
+ )
+ self.assertTrue(frappe.db.exists("Appointment", within.name))
+
+ def test_overlapping_time_slot_capacity(self):
+ set_booking_setting("number_of_agents", 1)
+ set_booking_setting("appointment_duration", 30)
+
+ slot = slot_on(1, 10)
+ first = create_test_appointment(customer_email="slot_first@example.com", scheduled_time=slot)
+
+ # a booking starting inside the first appointment's duration is rejected
+ with self.assertRaises(frappe.ValidationError):
+ create_test_appointment(
+ customer_email="slot_overlap@example.com",
+ scheduled_time=slot + datetime.timedelta(minutes=15),
+ )
+
+ # rescheduling must not count the appointment's own booked slot
+ first.scheduled_time = slot + datetime.timedelta(minutes=10)
+ first.save()
+
+ # a booking starting exactly when the rescheduled one ends is allowed
+ adjacent = create_test_appointment(
+ customer_email="slot_adjacent@example.com",
+ scheduled_time=slot + datetime.timedelta(minutes=40),
+ )
+ self.assertTrue(frappe.db.exists("Appointment", adjacent.name))
+
+ # a closed (cancelled) appointment frees its slot
+ first.status = "Closed"
+ first.save()
+ after_cancellation = create_test_appointment(
+ customer_email="after_cancellation@example.com", scheduled_time=slot
+ )
+ self.assertTrue(frappe.db.exists("Appointment", after_cancellation.name))
diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
index b79e974e301..8557dcf8791 100644
--- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
+++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
@@ -1,48 +1,56 @@
{
"actions": [],
+ "allow_bulk_edit": 1,
"creation": "2019-08-27 10:56:48.309824",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "enable_scheduling",
- "agent_detail_section",
- "availability_of_slots",
- "number_of_agents",
- "agent_list",
- "holiday_list",
"appointment_details_section",
"appointment_duration",
"email_reminders",
+ "column_break_ehiq",
+ "agent_list",
+ "number_of_agents",
+ "agent_detail_section",
+ "enable_scheduling",
+ "availability_of_slots",
+ "section_break_bkln",
+ "column_break_alwa",
"advance_booking_days",
+ "column_break_bspp",
+ "holiday_list",
"success_details",
- "success_redirect_url"
+ "enable_appointment_portal",
+ "verification_link_expiry_duration",
+ "column_break_fovk",
+ "success_redirect_url",
+ "action_for_expired_unverified_appointments"
],
"fields": [
{
+ "depends_on": "eval:doc.enable_scheduling === 1;",
"fieldname": "availability_of_slots",
"fieldtype": "Table",
"label": "Availability Of Slots",
- "options": "Appointment Booking Slots",
- "reqd": 1
+ "mandatory_depends_on": "eval:doc.enable_scheduling === 1;",
+ "options": "Appointment Booking Slots"
},
{
- "default": "1",
"fieldname": "number_of_agents",
"fieldtype": "Int",
- "hidden": 1,
"in_list_view": 1,
"label": "Number of Concurrent Appointments",
- "read_only": 1,
- "reqd": 1
+ "read_only": 1
},
{
+ "depends_on": "eval:doc.enable_scheduling === 1;",
"fieldname": "holiday_list",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Holiday List",
- "options": "Holiday List",
- "reqd": 1
+ "mandatory_depends_on": "eval:doc.enable_scheduling === 1;",
+ "options": "Holiday List"
},
{
"default": "60",
@@ -60,29 +68,31 @@
},
{
"default": "7",
+ "depends_on": "eval:doc.enable_scheduling === 1;",
"fieldname": "advance_booking_days",
"fieldtype": "Int",
"label": "Number of days appointments can be booked in advance",
- "reqd": 1
+ "mandatory_depends_on": "eval:doc.enable_scheduling === 1;"
},
{
"fieldname": "agent_list",
"fieldtype": "Table MultiSelect",
"label": "Agents",
- "options": "Assignment Rule User",
- "reqd": 1
+ "mandatory_depends_on": "eval:doc.enable_scheduling === 1;",
+ "options": "Assignment Rule User"
},
{
"default": "0",
"fieldname": "enable_scheduling",
"fieldtype": "Check",
"label": "Enable Appointment Scheduling",
- "reqd": 1
+ "mandatory_depends_on": "eval:doc.enable_appointment_portal === 1;"
},
{
"fieldname": "agent_detail_section",
"fieldtype": "Section Break",
- "label": "Agent Details"
+ "hide_border": 1,
+ "label": "Appointment Scheduling"
},
{
"fieldname": "appointment_details_section",
@@ -92,20 +102,68 @@
{
"fieldname": "success_details",
"fieldtype": "Section Break",
- "label": "Success Settings"
+ "label": "Appointment Booking Portal Settings"
},
{
"description": "Leave blank for home.\nThis is relative to site URL, for example \"about\" will redirect to \"https://yoursitename.com/about\"",
"fieldname": "success_redirect_url",
"fieldtype": "Data",
- "label": "Success Redirect URL"
+ "label": "Success Redirect URL",
+ "permlevel": 1
+ },
+ {
+ "default": "30",
+ "depends_on": "eval: doc.enable_scheduling === 1;",
+ "description": "In Minutes (min: 15 mins, max: 60 mins)",
+ "fieldname": "verification_link_expiry_duration",
+ "fieldtype": "Int",
+ "label": "Verification Link Expiry Duration",
+ "mandatory_depends_on": "eval:doc.enable_appointment_portal === 1;",
+ "max_value": 60.0,
+ "min_value": 15.0,
+ "non_negative": 1,
+ "permlevel": 1
+ },
+ {
+ "fieldname": "column_break_ehiq",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "enable_appointment_portal",
+ "fieldtype": "Check",
+ "label": "Enable Appointment Booking Through Portal",
+ "permlevel": 1
+ },
+ {
+ "fieldname": "column_break_fovk",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "Mark as Closed",
+ "fieldname": "action_for_expired_unverified_appointments",
+ "fieldtype": "Select",
+ "label": "Action for Expired Unverified Appointments",
+ "options": "Mark as Closed\nDelete Permanently",
+ "permlevel": 1
+ },
+ {
+ "fieldname": "section_break_bkln",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_alwa",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_bspp",
+ "fieldtype": "Column Break"
}
],
"grid_page_length": 50,
- "hide_toolbar": 0,
"issingle": 1,
"links": [],
- "modified": "2026-03-16 13:28:21.198138",
+ "modified": "2026-07-20 00:11:18.996384",
"modified_by": "Administrator",
"module": "CRM",
"name": "Appointment Booking Settings",
@@ -139,6 +197,15 @@
"role": "Sales Manager",
"share": 1,
"write": 1
+ },
+ {
+ "email": 1,
+ "permlevel": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
}
],
"quick_entry": 1,
diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py
index 9ef01283c31..67aab6fe8c9 100644
--- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py
+++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py
@@ -3,11 +3,11 @@
import datetime
-import typing
import frappe
from frappe import _
from frappe.model.document import Document
+from frappe.utils import getdate
class AppointmentBookingSettings(Document):
@@ -24,33 +24,43 @@ class AppointmentBookingSettings(Document):
AppointmentBookingSlots,
)
+ action_for_expired_unverified_appointments: DF.Literal["Mark as Closed", "Delete Permanently"]
advance_booking_days: DF.Int
agent_list: DF.TableMultiSelect[AssignmentRuleUser]
appointment_duration: DF.Int
availability_of_slots: DF.Table[AppointmentBookingSlots]
email_reminders: DF.Check
+ enable_appointment_portal: DF.Check
enable_scheduling: DF.Check
- holiday_list: DF.Link
+ holiday_list: DF.Link | None
number_of_agents: DF.Int
success_redirect_url: DF.Data | None
+ verification_link_expiry_duration: DF.Int
# end: auto-generated types
- agent_list: typing.ClassVar[list] = [] # Hack
- min_date = "01/01/1970 "
- format_string = "%d/%m/%Y %H:%M:%S"
-
def validate(self):
- self.validate_availability_of_slots()
-
- def save(self):
self.number_of_agents = len(self.agent_list)
- super().save()
+ self.validate_appointment_scheduling()
+ self.validate_portal_booking()
+
+ def validate_appointment_scheduling(self):
+ if not self.enable_scheduling:
+ return
+
+ self.validate_availability_of_slots()
+ self.validate_holiday_list()
+ self.validate_advance_booking_days()
def validate_availability_of_slots(self):
+ if not self.availability_of_slots:
+ frappe.throw(
+ _("Please fill up the Availability of Slots table to enable Appointment Scheduling.")
+ )
+
+ format_string = "%Y-%m-%d %H:%M:%S"
for record in self.availability_of_slots:
- from_time = datetime.datetime.strptime(self.min_date + record.from_time, self.format_string)
- to_time = datetime.datetime.strptime(self.min_date + record.to_time, self.format_string)
- to_time - from_time
+ from_time = datetime.datetime.strptime(f"1970-01-01 {record.from_time}", format_string)
+ to_time = datetime.datetime.strptime(f"1970-01-01 {record.to_time}", format_string)
self.validate_from_and_to_time(from_time, to_time, record)
self.duration_is_divisible(from_time, to_time)
@@ -65,3 +75,38 @@ class AppointmentBookingSettings(Document):
timedelta = to_time - from_time
if timedelta.total_seconds() % (self.appointment_duration * 60):
frappe.throw(_("The difference between from time and To Time must be a multiple of Appointment"))
+
+ def validate_holiday_list(self):
+ if not self.holiday_list:
+ frappe.throw(_("Please select a Holiday List to enable Appointment Scheduling."))
+
+ hl_from_date, hl_to_date = frappe.get_cached_value(
+ "Holiday List", self.holiday_list, ["from_date", "to_date"]
+ )
+ now = getdate()
+
+ if not (now >= hl_from_date and now <= hl_to_date):
+ frappe.throw(_("Holiday List - {0} is not valid for current date.").format(self.holiday_list))
+
+ def validate_advance_booking_days(self):
+ if not self.advance_booking_days:
+ frappe.throw(_("Advance Booking Days is mandatory for Appointment Scheduling."))
+
+ def validate_portal_booking(self):
+ if not self.enable_appointment_portal:
+ return
+
+ if not self.enable_scheduling:
+ frappe.throw(
+ _("Appointment Scheduling needs to be enabled for Appointment Booking through portal.")
+ )
+
+ self.validate_link_expiry_duration()
+
+ def validate_link_expiry_duration(self):
+ if (
+ not self.verification_link_expiry_duration
+ or self.verification_link_expiry_duration > 60
+ or self.verification_link_expiry_duration < 15
+ ):
+ frappe.throw(_("'Verification Link Expiry Duration' must be between 15 to 60 minutes."))
diff --git a/erpnext/crm/doctype/appointment_booking_settings/test_appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/test_appointment_booking_settings.py
index 96d86a224ed..ae121ab6883 100644
--- a/erpnext/crm/doctype/appointment_booking_settings/test_appointment_booking_settings.py
+++ b/erpnext/crm/doctype/appointment_booking_settings/test_appointment_booking_settings.py
@@ -1,10 +1,125 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-# import frappe
-import unittest
+import datetime
+
+import frappe
+from frappe.utils import add_to_date, getdate
+
+from erpnext.setup.doctype.holiday_list.test_holiday_list import make_holiday_list
from erpnext.tests.utils import ERPNextTestSuite
class TestAppointmentBookingSettings(ERPNextTestSuite):
- pass
+ def assert_invalid(self, settings):
+ with self.assertRaises(frappe.ValidationError):
+ settings.save()
+
+ def make_settings(self, appointment_duration=30):
+ doc = frappe.new_doc("Appointment Booking Settings")
+ doc.appointment_duration = appointment_duration
+ return doc
+
+ def dt(self, hms):
+ # the controller parses times against a fixed epoch date
+ return datetime.datetime.strptime("1970-01-01 " + hms, "%Y-%m-%d %H:%M:%S")
+
+ def get_valid_scheduling_settings(self):
+ holiday_list = make_holiday_list(
+ "_Test Booking Settings Holiday List",
+ from_date=getdate(),
+ to_date=add_to_date(getdate(), days=30),
+ holiday_dates=[],
+ )
+
+ settings = frappe.get_doc("Appointment Booking Settings")
+ settings.enable_scheduling = 1
+ settings.appointment_duration = 30
+ settings.advance_booking_days = 7
+ settings.verification_link_expiry_duration = 30
+ settings.holiday_list = holiday_list.name
+ settings.set("agent_list", [])
+ settings.append("agent_list", {"user": "Administrator"})
+ settings.set("availability_of_slots", [])
+ settings.append(
+ "availability_of_slots",
+ {"day_of_week": "Monday", "from_time": "09:00:00", "to_time": "17:00:00"},
+ )
+ return settings
+
+ def test_from_time_must_precede_to_time(self):
+ doc = self.make_settings()
+ record = frappe._dict(day_of_week="Monday")
+ self.assertRaises(
+ frappe.ValidationError,
+ doc.validate_from_and_to_time,
+ self.dt("18:00:00"),
+ self.dt("09:00:00"),
+ record,
+ )
+ doc.validate_from_and_to_time(self.dt("09:00:00"), self.dt("18:00:00"), record) # valid order
+
+ def test_slot_length_must_be_a_multiple_of_the_duration(self):
+ doc = self.make_settings(appointment_duration=30)
+ # 60 minutes is two 30-minute appointments -> fine
+ doc.duration_is_divisible(self.dt("09:00:00"), self.dt("10:00:00"))
+ # 45 minutes leaves a partial appointment -> rejected
+ self.assertRaises(
+ frappe.ValidationError, doc.duration_is_divisible, self.dt("09:00:00"), self.dt("09:45:00")
+ )
+
+ def test_scheduling_requires_slots(self):
+ settings = self.get_valid_scheduling_settings()
+ settings.set("availability_of_slots", [])
+
+ self.assert_invalid(settings)
+
+ def test_validate_checks_every_slot(self):
+ settings = self.get_valid_scheduling_settings()
+ settings.append(
+ "availability_of_slots",
+ {"day_of_week": "Tuesday", "from_time": "09:00:00", "to_time": "09:45:00"},
+ )
+
+ self.assert_invalid(settings)
+
+ def test_scheduling_requires_holiday_list_covering_today(self):
+ settings = self.get_valid_scheduling_settings()
+ settings.holiday_list = None
+ self.assert_invalid(settings)
+
+ expired_list = make_holiday_list(
+ "_Test Booking Settings Expired Holiday List",
+ from_date=add_to_date(getdate(), days=-60),
+ to_date=add_to_date(getdate(), days=-30),
+ holiday_dates=[],
+ )
+ settings.holiday_list = expired_list.name
+ self.assert_invalid(settings)
+
+ def test_scheduling_requires_advance_booking_days(self):
+ settings = self.get_valid_scheduling_settings()
+ settings.advance_booking_days = 0
+
+ self.assert_invalid(settings)
+
+ def test_portal_requires_scheduling(self):
+ settings = frappe.get_doc("Appointment Booking Settings")
+ settings.enable_scheduling = 0
+ settings.enable_appointment_portal = 1
+
+ self.assert_invalid(settings)
+
+ def test_portal_expiry_duration_bounds(self):
+ settings = self.get_valid_scheduling_settings()
+ settings.enable_appointment_portal = 1
+ settings.verification_link_expiry_duration = 5
+
+ self.assert_invalid(settings)
+
+ def test_number_of_agents_derived_from_agent_list(self):
+ settings = self.get_valid_scheduling_settings()
+ settings.number_of_agents = 99
+ settings.save()
+
+ self.assertEqual(frappe.db.get_single_value("Appointment Booking Settings", "number_of_agents"), 1)
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index fbc8d6c8687..4ce4e8047f6 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -447,6 +447,7 @@ scheduler_events = {
],
"hourly_long": [],
"hourly_maintenance": [
+ "erpnext.crm.doctype.appointment.appointment.handle_expired_unverified_appointments",
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
"erpnext.utilities.bulk_transaction.retry",
"erpnext.projects.doctype.project.project.collect_project_status",
diff --git a/erpnext/locale/main.pot b/erpnext/locale/main.pot
index 31ce55edea2..c1d1045f1af 100644
--- a/erpnext/locale/main.pot
+++ b/erpnext/locale/main.pot
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: ERPNext VERSION\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
-"POT-Creation-Date: 2026-07-12 10:05+0000\n"
-"PO-Revision-Date: 2026-07-12 10:05+0000\n"
+"POT-Creation-Date: 2026-07-19 10:04+0000\n"
+"PO-Revision-Date: 2026-07-19 10:04+0000\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: hello@frappe.io\n"
"MIME-Version: 1.0\n"
@@ -287,7 +287,7 @@ msgstr ""
msgid "'Default {0} Account' in Company {1}"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1234
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1235
msgid "'Entries' cannot be empty"
msgstr ""
@@ -611,8 +611,8 @@ msgstr ""
msgid "90 Above"
msgstr ""
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1291
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1292
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1293
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1294
msgid "<0"
msgstr ""
@@ -998,7 +998,7 @@ msgstr ""
msgid "A - C"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:356
+#: erpnext/selling/doctype/customer/customer.py:361
msgid "A Customer Group exists with same name please change the Customer name or rename the Customer Group"
msgstr ""
@@ -1032,7 +1032,7 @@ msgstr ""
msgid "A Reconciliation Job {0} is running for the same filters. Cannot reconcile now"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1772
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1773
msgid "A Reverse Journal Entry {0} already exists for this Journal Entry."
msgstr ""
@@ -1191,7 +1191,7 @@ msgstr ""
msgid "Abbreviation: {0} must appear only once"
msgstr ""
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1288
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1290
msgid "Above"
msgstr ""
@@ -1487,7 +1487,7 @@ msgstr ""
msgid "Account Type"
msgstr ""
-#: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:162
+#: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:167
msgid "Account Value"
msgstr ""
@@ -2000,8 +2000,8 @@ msgid "Accounting Entry for {0}: {1} can only be made in currency: {2}"
msgstr ""
#: erpnext/accounts/doctype/invoice_discounting/invoice_discounting.js:193
-#: erpnext/assets/doctype/asset/asset.js:190
-#: erpnext/assets/doctype/asset_repair/asset_repair.js:92
+#: erpnext/assets/doctype/asset/asset.js:198
+#: erpnext/assets/doctype/asset_repair/asset_repair.js:101
#: erpnext/buying/doctype/supplier/supplier.js:123
#: erpnext/public/js/controllers/stock_controller.js:88
#: erpnext/public/js/utils/ledger_preview.js:8
@@ -2192,7 +2192,7 @@ msgstr ""
msgid "Accounts Setup"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1337
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1338
msgid "Accounts table cannot be blank."
msgstr ""
@@ -2226,7 +2226,7 @@ msgstr ""
#. Label of the accumulated_depreciation_amount (Currency) field in DocType
#. 'Depreciation Schedule'
#: erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py:178
-#: erpnext/assets/doctype/asset/asset.js:385
+#: erpnext/assets/doctype/asset/asset.js:393
#: erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json
msgid "Accumulated Depreciation Amount"
msgstr ""
@@ -2508,7 +2508,7 @@ msgstr ""
msgid "Actual End Time"
msgstr ""
-#: erpnext/accounts/report/budget_variance_report/budget_variance_report.py:465
+#: erpnext/accounts/report/budget_variance_report/budget_variance_report.py:470
msgid "Actual Expense"
msgstr ""
@@ -2880,11 +2880,11 @@ msgstr ""
msgid "Added On"
msgstr ""
-#: erpnext/buying/doctype/supplier/supplier.py:135
+#: erpnext/buying/doctype/supplier/supplier.py:139
msgid "Added Supplier Role to User {0}."
msgstr ""
-#: erpnext/controllers/website_list_for_contact.py:308
+#: erpnext/controllers/website_list_for_contact.py:310
msgid "Added {1} Role to User {0}."
msgstr ""
@@ -3420,7 +3420,7 @@ msgstr ""
msgid "Advance amount cannot be greater than {0} {1}"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:881
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:882
msgid "Advance paid against {0} {1} cannot be greater than Grand Total {2}"
msgstr ""
@@ -3555,7 +3555,7 @@ msgstr ""
msgid "Against Income Account"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:743
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:744
#: erpnext/accounts/doctype/payment_entry/payment_entry.py:792
msgid "Against Journal Entry {0} does not have any unmatched {1} entry"
msgstr ""
@@ -3644,7 +3644,7 @@ msgstr ""
#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:154
#: erpnext/accounts/report/accounts_payable/accounts_payable.html:138
#: erpnext/accounts/report/accounts_receivable/accounts_receivable.html:139
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1222
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1224
msgid "Age (Days)"
msgstr ""
@@ -3753,7 +3753,7 @@ msgstr ""
#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:165
#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:185
#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:169
-#: erpnext/accounts/utils.py:1632 erpnext/public/js/setup_wizard.js:279
+#: erpnext/accounts/utils.py:1626 erpnext/public/js/setup_wizard.js:279
msgid "All Accounts"
msgstr ""
@@ -3950,7 +3950,7 @@ msgstr ""
msgid "All linked Sales Orders must be subcontracted."
msgstr ""
-#: erpnext/stock/doctype/pick_list/pick_list.py:1598
+#: erpnext/stock/doctype/pick_list/pick_list.py:1605
msgid "All picked items have already been transferred against this Pick List"
msgstr ""
@@ -4609,7 +4609,7 @@ msgstr ""
msgid "Alternative item must not be same as item code"
msgstr ""
-#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:381
+#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:382
msgid "Alternatively, you can download the template and fill your data in."
msgstr ""
@@ -5024,7 +5024,7 @@ msgid "An error has been appeared while reposting item valuation via {0}"
msgstr ""
#: erpnext/public/js/controllers/buying.js:382
-#: erpnext/public/js/utils/sales_common.js:489
+#: erpnext/public/js/utils/sales_common.js:487
msgid "An error occurred during the update process"
msgstr ""
@@ -5581,7 +5581,7 @@ msgstr ""
msgid "As there are sufficient Sub Assembly Items, Work Order is not required for Warehouse {0}."
msgstr ""
-#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1836
+#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1842
msgid "As there are sufficient raw materials, Material Request is not required for Warehouse {0}."
msgstr ""
@@ -5914,6 +5914,7 @@ msgstr ""
#. Label of the asset_repair (Link) field in DocType 'Stock Entry'
#. Label of a Workspace Sidebar Item
#: erpnext/assets/doctype/asset/asset.js:113
+#: erpnext/assets/doctype/asset/asset.js:152
#: erpnext/assets/doctype/asset_repair/asset_repair.json
#: erpnext/assets/workspace/assets/assets.json
#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
@@ -5964,8 +5965,7 @@ msgstr ""
#. Label of the asset_value (Currency) field in DocType 'Asset Capitalization
#. Asset Item'
-#: erpnext/assets/dashboard_fixtures.py:180
-#: erpnext/assets/doctype/asset/asset.js:517
+#: erpnext/assets/doctype/asset/asset.js:525
#: erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json
#: erpnext/assets/report/fixed_asset_register/fixed_asset_register.py:209
#: erpnext/assets/report/fixed_asset_register/fixed_asset_register.py:460
@@ -5988,7 +5988,6 @@ msgid "Asset Value Adjustment cannot be posted before Asset's purchase date {
msgstr ""
#. Label of a chart in the Assets Workspace
-#: erpnext/assets/dashboard_fixtures.py:56
#: erpnext/assets/workspace/assets/assets.json
msgid "Asset Value Analytics"
msgstr ""
@@ -6025,7 +6024,7 @@ msgstr ""
msgid "Asset issued to Employee {0}"
msgstr ""
-#: erpnext/assets/doctype/asset_repair/asset_repair.py:179
+#: erpnext/assets/doctype/asset_repair/asset_repair.py:182
msgid "Asset out of order due to Asset Repair {0}"
msgstr ""
@@ -6070,7 +6069,7 @@ msgstr ""
msgid "Asset updated after being split into Asset {0}"
msgstr ""
-#: erpnext/assets/doctype/asset_repair/asset_repair.py:442
+#: erpnext/assets/doctype/asset_repair/asset_repair.py:445
msgid "Asset updated due to Asset Repair {0} {1}."
msgstr ""
@@ -6144,7 +6143,7 @@ msgstr ""
#. Title of a Workspace Sidebar
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
#: erpnext/accounts/doctype/finance_book/finance_book_dashboard.py:9
-#: erpnext/accounts/report/balance_sheet/balance_sheet.py:251
+#: erpnext/accounts/report/balance_sheet/balance_sheet.py:260
#: erpnext/assets/doctype/asset_capitalization/asset_capitalization.json
#: erpnext/assets/doctype/asset_movement/asset_movement.json
#: erpnext/assets/workspace/assets/assets.json erpnext/desktop_icon/assets.json
@@ -6493,6 +6492,18 @@ msgstr ""
msgid "Auto Repeat Detail"
msgstr ""
+#. Label of the repost_incorrect_valuation_entries (Check) field in DocType
+#. 'Stock Reposting Settings'
+#: erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
+msgid "Auto Repost Incorrect Valuation Entries (Weekly)"
+msgstr ""
+
+#. Label of the auto_reposting_section (Section Break) field in DocType 'Stock
+#. Reposting Settings'
+#: erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
+msgid "Auto Reposting of Incorrect Valuation"
+msgstr ""
+
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:201
msgid "Auto Tax Settings Error"
msgstr ""
@@ -6554,7 +6565,7 @@ msgid "Auto reconcile Payments"
msgstr ""
#: erpnext/public/js/controllers/buying.js:377
-#: erpnext/public/js/utils/sales_common.js:484
+#: erpnext/public/js/utils/sales_common.js:482
msgid "Auto repeat document updated"
msgstr ""
@@ -6903,7 +6914,7 @@ msgstr ""
#: erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py:109
#: erpnext/manufacturing/workspace/manufacturing/manufacturing.json
#: erpnext/selling/doctype/sales_order/sales_order.js:1458
-#: erpnext/stock/doctype/material_request/material_request.js:351
+#: erpnext/stock/doctype/material_request/material_request.js:352
#: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
#: erpnext/stock/doctype/stock_entry/stock_entry.js:810
#: erpnext/stock/report/bom_search/bom_search.py:38
@@ -7176,7 +7187,7 @@ msgstr ""
msgid "BOM and Production"
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.js:386
+#: erpnext/stock/doctype/material_request/material_request.js:387
#: erpnext/stock/doctype/stock_entry/stock_entry.js:862
msgid "BOM does not contain any stock item"
msgstr ""
@@ -7348,6 +7359,10 @@ msgstr ""
msgid "Balance Sheet Summary"
msgstr ""
+#: erpnext/accounts/report/balance_sheet/balance_sheet.py:284
+msgid "Balance Sheet requires {0} to be synced to DuckDB"
+msgstr ""
+
#: erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html:13
msgid "Balance Stock Qty"
msgstr ""
@@ -8055,7 +8070,7 @@ msgstr ""
#: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js:89
#: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py:115
#: erpnext/public/js/controllers/transaction.js:2912
-#: erpnext/public/js/utils/barcode_scanner.js:281
+#: erpnext/public/js/utils/barcode_scanner.js:286
#: erpnext/public/js/utils/serial_no_batch_selector.js:449
#: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
#: erpnext/stock/doctype/item_price/item_price.json
@@ -8089,7 +8104,7 @@ msgstr ""
msgid "Batch No is mandatory"
msgstr ""
-#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3530
+#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3532
msgid "Batch No {0} does not exists"
msgstr ""
@@ -8249,7 +8264,7 @@ msgstr ""
#. Label of the bill_date (Date) field in DocType 'Journal Entry'
#. Label of the bill_date (Date) field in DocType 'Subcontracting Receipt'
#: erpnext/accounts/doctype/journal_entry/journal_entry.json
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1207
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1209
#: erpnext/accounts/report/purchase_register/purchase_register.py:230
#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json
msgid "Bill Date"
@@ -8258,7 +8273,7 @@ msgstr ""
#. Label of the bill_no (Data) field in DocType 'Journal Entry'
#. Label of the bill_no (Data) field in DocType 'Subcontracting Receipt'
#: erpnext/accounts/doctype/journal_entry/journal_entry.json
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1206
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1208
#: erpnext/accounts/report/purchase_register/purchase_register.py:229
#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json
msgid "Bill No"
@@ -8275,14 +8290,14 @@ msgstr ""
#. Label of a Workspace Sidebar Item
#: erpnext/manufacturing/doctype/bom/bom.py:1373
#: erpnext/manufacturing/workspace/manufacturing/manufacturing.json
-#: erpnext/stock/doctype/material_request/material_request.js:139
+#: erpnext/stock/doctype/material_request/material_request.js:142
#: erpnext/stock/doctype/stock_entry/stock_entry.js:796
#: erpnext/workspace_sidebar/subcontracting.json
msgid "Bill of Materials"
msgstr ""
#. Option for the 'Status' (Select) field in DocType 'Timesheet'
-#: erpnext/controllers/website_list_for_contact.py:207
+#: erpnext/controllers/website_list_for_contact.py:209
#: erpnext/projects/doctype/timesheet/timesheet.json
#: erpnext/projects/doctype/timesheet/timesheet_list.js:9
msgid "Billed"
@@ -8866,7 +8881,7 @@ msgstr ""
#: erpnext/accounts/report/budget_variance_report/budget_variance_report.py:239
#: erpnext/accounts/report/budget_variance_report/budget_variance_report.py:321
#: erpnext/accounts/report/budget_variance_report/budget_variance_report.py:331
-#: erpnext/accounts/report/budget_variance_report/budget_variance_report.py:460
+#: erpnext/accounts/report/budget_variance_report/budget_variance_report.py:465
#: erpnext/accounts/workspace/invoicing/invoicing.json
#: erpnext/desktop_icon/budget.json erpnext/workspace_sidebar/budget.json
msgid "Budget"
@@ -9464,7 +9479,7 @@ msgstr ""
msgid "Can be approved by {0}"
msgstr ""
-#: erpnext/manufacturing/doctype/work_order/work_order.py:2841
+#: erpnext/manufacturing/doctype/work_order/work_order.py:2845
msgid "Can not close Work Order. Since {0} Job Cards are in Work In Progress state."
msgstr ""
@@ -9492,8 +9507,8 @@ msgstr ""
msgid "Can not filter based on Voucher No, if grouped by Voucher"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1396
-#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2901
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1397
+#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2899
msgid "Can only make payment against unbilled {0}"
msgstr ""
@@ -9619,7 +9634,7 @@ msgstr ""
msgid "Cannot cancel this Manufacturing Stock Entry as quantity of Finished Good produced cannot be less than quantity delivered in the linked Subcontracting Inward Order."
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:583
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:584
msgid "Cannot cancel this document as it is linked with the submitted Asset Value Adjustment {0}. Please cancel the Asset Value Adjustment to continue."
msgstr ""
@@ -9767,11 +9782,11 @@ msgstr ""
msgid "Cannot fetch selected rows for submitted Payment Request"
msgstr ""
-#: erpnext/public/js/utils/barcode_scanner.js:62
+#: erpnext/public/js/utils/barcode_scanner.js:67
msgid "Cannot find Item or Warehouse with this Barcode"
msgstr ""
-#: erpnext/public/js/utils/barcode_scanner.js:63
+#: erpnext/public/js/utils/barcode_scanner.js:68
msgid "Cannot find Item with this Barcode"
msgstr ""
@@ -9817,7 +9832,7 @@ msgstr ""
msgid "Cannot retrieve link token. Check Error Log for more information"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:369
+#: erpnext/selling/doctype/customer/customer.py:374
msgid "Cannot select a Group type Customer Group. Please select a non-group Customer Group."
msgstr ""
@@ -9939,7 +9954,7 @@ msgstr ""
msgid "Capital Work in Progress"
msgstr ""
-#: erpnext/assets/doctype/asset/asset.js:228
+#: erpnext/assets/doctype/asset/asset.js:236
msgid "Capitalize Asset"
msgstr ""
@@ -9948,7 +9963,7 @@ msgstr ""
msgid "Capitalize Repair Cost"
msgstr ""
-#: erpnext/assets/doctype/asset/asset.js:226
+#: erpnext/assets/doctype/asset/asset.js:234
msgid "Capitalize this asset before submitting."
msgstr ""
@@ -10133,11 +10148,7 @@ msgstr ""
msgid "Category Details"
msgstr ""
-#: erpnext/assets/dashboard_fixtures.py:93
-msgid "Category-wise Asset Value"
-msgstr ""
-
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:300
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:301
#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:144
msgid "Caution"
msgstr ""
@@ -10252,7 +10263,7 @@ msgstr ""
msgid "Change this date manually to setup the next synchronization start date"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:159
+#: erpnext/selling/doctype/customer/customer.py:162
msgid "Changed customer name to '{}' as '{}' already exists."
msgstr ""
@@ -10596,7 +10607,7 @@ msgstr ""
msgid "Clauses and Conditions"
msgstr ""
-#: erpnext/public/js/utils/barcode_scanner.js:493
+#: erpnext/public/js/utils/barcode_scanner.js:502
msgid "Clear Last Scanned Warehouse"
msgstr ""
@@ -10662,7 +10673,7 @@ msgstr ""
msgid "Clearing Demo Data..."
msgstr ""
-#: erpnext/manufacturing/doctype/production_plan/production_plan.js:720
+#: erpnext/manufacturing/doctype/production_plan/production_plan.js:718
msgid "Click on 'Get Finished Goods for Manufacture' to fetch the items from the above Sales Orders. Items only for which a BOM is present will be fetched."
msgstr ""
@@ -10670,7 +10681,7 @@ msgstr ""
msgid "Click on Add to Holidays. This will populate the holidays table with all the dates that fall on the selected weekly off. Repeat the process for populating the dates for all your weekly holidays"
msgstr ""
-#: erpnext/manufacturing/doctype/production_plan/production_plan.js:715
+#: erpnext/manufacturing/doctype/production_plan/production_plan.js:713
msgid "Click on Get Sales Orders to fetch sales orders based on the above filters."
msgstr ""
@@ -10736,7 +10747,7 @@ msgstr ""
msgid "Closed Documents"
msgstr ""
-#: erpnext/manufacturing/doctype/work_order/work_order.py:2764
+#: erpnext/manufacturing/doctype/work_order/work_order.py:2768
msgid "Closed Work Order can not be stopped or Re-opened"
msgstr ""
@@ -11678,7 +11689,7 @@ msgstr ""
msgid "Company Tax ID"
msgstr ""
-#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:624
+#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:682
msgid "Company and Posting Date is mandatory"
msgstr ""
@@ -11686,7 +11697,7 @@ msgstr ""
msgid "Company currencies of both the companies should match for Inter Company Transactions."
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.js:380
+#: erpnext/stock/doctype/material_request/material_request.js:381
#: erpnext/stock/doctype/stock_entry/stock_entry.js:856
msgid "Company field is required"
msgstr ""
@@ -11794,7 +11805,7 @@ msgstr ""
#. Label of the competitors (Table MultiSelect) field in DocType 'Opportunity'
#. Label of the competitors (Table MultiSelect) field in DocType 'Quotation'
#: erpnext/crm/doctype/opportunity/opportunity.json
-#: erpnext/public/js/utils/sales_common.js:606
+#: erpnext/public/js/utils/sales_common.js:604
#: erpnext/selling/doctype/quotation/quotation.json
msgid "Competitors"
msgstr ""
@@ -11889,7 +11900,7 @@ msgstr ""
msgid "Completion Date"
msgstr ""
-#: erpnext/assets/doctype/asset_repair/asset_repair.py:83
+#: erpnext/assets/doctype/asset_repair/asset_repair.py:86
msgid "Completion Date can not be before Failure Date. Please adjust the dates accordingly."
msgstr ""
@@ -12240,7 +12251,7 @@ msgstr ""
msgid "Consumed Qty"
msgstr ""
-#: erpnext/manufacturing/doctype/work_order/work_order.py:1940
+#: erpnext/manufacturing/doctype/work_order/work_order.py:1944
msgid "Consumed Qty cannot be greater than Reserved Qty for item {0}"
msgstr ""
@@ -12857,7 +12868,7 @@ msgstr ""
#: erpnext/accounts/report/accounts_payable/accounts_payable.js:28
#: erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js:47
#: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:30
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1192
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1194
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:47
#: erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.js:42
#: erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py:204
@@ -12940,12 +12951,16 @@ msgstr ""
msgid "Cost Center Number"
msgstr ""
+#: erpnext/accounts/doctype/dunning_type/dunning_type.py:122
+msgid "Cost Center Validation Error"
+msgstr ""
+
#. Label of a Card Break in the Invoicing Workspace
#: erpnext/accounts/workspace/invoicing/invoicing.json
msgid "Cost Center and Budgeting"
msgstr ""
-#: erpnext/public/js/utils/sales_common.js:540
+#: erpnext/public/js/utils/sales_common.js:538
msgid "Cost Center for Item rows has been updated to {0}"
msgstr ""
@@ -13532,7 +13547,7 @@ msgid "Create Service Item"
msgstr ""
#: erpnext/stock/dashboard/item_dashboard.js:283
-#: erpnext/stock/doctype/material_request/material_request.js:478
+#: erpnext/stock/doctype/material_request/material_request.js:479
msgid "Create Stock Entry"
msgstr ""
@@ -13727,7 +13742,7 @@ msgstr ""
msgid "Creating Dimensions..."
msgstr ""
-#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js:92
+#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js:102
msgid "Creating Journal Entries..."
msgstr ""
@@ -13921,7 +13936,7 @@ msgstr ""
msgid "Credit Limit"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:645
+#: erpnext/selling/doctype/customer/customer.py:650
msgid "Credit Limit Crossed"
msgstr ""
@@ -13956,7 +13971,7 @@ msgstr ""
#: erpnext/accounts/doctype/journal_entry/journal_entry.json
#: erpnext/accounts/doctype/journal_entry_template/journal_entry_template.json
#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:176
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1216
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1218
#: erpnext/controllers/sales_and_purchase_return.py:455
#: erpnext/setup/setup_wizard/operations/install_fixtures.py:303
#: erpnext/stock/doctype/delivery_note/delivery_note.js:89
@@ -14001,16 +14016,16 @@ msgstr ""
msgid "Credit in Company Currency"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:611
-#: erpnext/selling/doctype/customer/customer.py:666
+#: erpnext/selling/doctype/customer/customer.py:616
+#: erpnext/selling/doctype/customer/customer.py:671
msgid "Credit limit has been crossed for customer {0} ({1}/{2})"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:396
+#: erpnext/selling/doctype/customer/customer.py:401
msgid "Credit limit is already defined for the Company {0}"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:665
+#: erpnext/selling/doctype/customer/customer.py:670
msgid "Credit limit reached for customer {0}"
msgstr ""
@@ -14199,7 +14214,7 @@ msgstr ""
#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1625
#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1693
-#: erpnext/accounts/utils.py:2533
+#: erpnext/accounts/utils.py:2527
msgid "Currency for {0} must be {1}"
msgstr ""
@@ -14655,7 +14670,7 @@ msgstr ""
#. Label of the customer_contact_display (Small Text) field in DocType
#. 'Purchase Order'
#. Label of the customer_contact (Small Text) field in DocType 'Delivery Stop'
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1186
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1188
#: erpnext/buying/doctype/purchase_order/purchase_order.json
#: erpnext/stock/doctype/delivery_stop/delivery_stop.json
msgid "Customer Contact"
@@ -14761,7 +14776,7 @@ msgstr ""
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.json
#: erpnext/accounts/doctype/tax_rule/tax_rule.json
#: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:115
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1244
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1246
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:96
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:185
#: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js:56
@@ -14823,7 +14838,7 @@ msgstr ""
msgid "Customer Items"
msgstr ""
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1235
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1237
msgid "Customer LPO"
msgstr ""
@@ -14875,7 +14890,7 @@ msgstr ""
#: erpnext/accounts/doctype/pos_invoice/pos_invoice.json
#: erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.json
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1175
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1177
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:156
#: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js:92
#: erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py:35
@@ -15153,7 +15168,7 @@ msgstr ""
msgid "DFS"
msgstr ""
-#: erpnext/projects/doctype/project/project.py:680
+#: erpnext/projects/doctype/project/project.py:710
msgid "Daily Project Summary for {0}"
msgstr ""
@@ -15466,7 +15481,7 @@ msgstr ""
#: erpnext/accounts/doctype/journal_entry/journal_entry.json
#: erpnext/accounts/doctype/journal_entry_template/journal_entry_template.json
#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:178
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1219
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1221
#: erpnext/controllers/sales_and_purchase_return.py:459
#: erpnext/setup/setup_wizard/operations/install_fixtures.py:304
#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:45
@@ -15576,7 +15591,7 @@ msgstr ""
msgid "Decimeter"
msgstr ""
-#: erpnext/public/js/utils/sales_common.js:633
+#: erpnext/public/js/utils/sales_common.js:631
msgid "Declare Lost"
msgstr ""
@@ -15680,7 +15695,7 @@ msgstr ""
msgid "Default BOM ({0}) must be active for this item or its template"
msgstr ""
-#: erpnext/manufacturing/doctype/work_order/work_order.py:2532
+#: erpnext/manufacturing/doctype/work_order/work_order.py:2536
msgid "Default BOM for {0} not found"
msgstr ""
@@ -15688,7 +15703,7 @@ msgstr ""
msgid "Default BOM not found for FG Item {0}"
msgstr ""
-#: erpnext/manufacturing/doctype/work_order/work_order.py:2529
+#: erpnext/manufacturing/doctype/work_order/work_order.py:2533
msgid "Default BOM not found for Item {0} and Project {1}"
msgstr ""
@@ -16326,7 +16341,7 @@ msgstr ""
#. Order'
#: erpnext/buying/doctype/purchase_order/purchase_order.json
#: erpnext/buying/doctype/purchase_order/purchase_order_list.js:20
-#: erpnext/controllers/website_list_for_contact.py:213
+#: erpnext/controllers/website_list_for_contact.py:215
#: erpnext/stock/doctype/serial_no/serial_no.json
#: erpnext/stock/doctype/shipment/shipment.json
#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
@@ -16397,11 +16412,11 @@ msgstr ""
msgid "Delivered Qty (in Stock UOM)"
msgstr ""
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:611
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:612
msgid "Delivered Qty cannot be increased by more than {0} for item {1}"
msgstr ""
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:604
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:605
msgid "Delivered Qty cannot be reduced by more than {0} for item {1}"
msgstr ""
@@ -16547,7 +16562,7 @@ msgstr ""
msgid "Delivery Note {0} is not submitted"
msgstr ""
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1239
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1241
#: erpnext/stock/doctype/delivery_trip/delivery_trip.js:75
msgid "Delivery Notes"
msgstr ""
@@ -16763,7 +16778,7 @@ msgstr ""
#. Label of the depreciation_amount (Currency) field in DocType 'Depreciation
#. Schedule'
#: erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py:172
-#: erpnext/assets/doctype/asset/asset.js:384
+#: erpnext/assets/doctype/asset/asset.js:392
#: erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json
msgid "Depreciation Amount"
msgstr ""
@@ -16846,7 +16861,7 @@ msgstr ""
msgid "Depreciation Posting Date"
msgstr ""
-#: erpnext/assets/doctype/asset/asset.js:927
+#: erpnext/assets/doctype/asset/asset.js:935
msgid "Depreciation Posting Date cannot be before Available-for-use Date"
msgstr ""
@@ -16915,7 +16930,7 @@ msgstr ""
#. Label of the order_lost_reason (Small Text) field in DocType 'Opportunity'
#. Label of the order_lost_reason (Small Text) field in DocType 'Quotation'
#: erpnext/crm/doctype/opportunity/opportunity.json
-#: erpnext/public/js/utils/sales_common.js:612
+#: erpnext/public/js/utils/sales_common.js:610
#: erpnext/selling/doctype/quotation/quotation.json
msgid "Detailed Reason"
msgstr ""
@@ -17077,7 +17092,7 @@ msgid "Difference Qty"
msgstr ""
#: erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py:136
-#: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:168
+#: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:173
msgid "Difference Value"
msgstr ""
@@ -17495,7 +17510,7 @@ msgstr ""
msgid "Discount must be less than 100"
msgstr ""
-#: erpnext/accounts/doctype/payment_entry/payment_entry.py:3379
+#: erpnext/accounts/doctype/payment_entry/payment_entry.py:3377
msgid "Discount of {} applied as per Payment Term"
msgstr ""
@@ -17831,7 +17846,7 @@ msgstr ""
msgid "Do not use Batch-wise Valuation"
msgstr ""
-#: erpnext/assets/doctype/asset/asset.js:965
+#: erpnext/assets/doctype/asset/asset.js:973
msgid "Do you really want to restore this scrapped asset?"
msgstr ""
@@ -18143,6 +18158,14 @@ msgstr ""
msgid "Dunning Letter Text"
msgstr ""
+#: erpnext/accounts/doctype/dunning/dunning.py:184
+msgid "Dunning Letter for Dunning Type {0} in language '{1}' not found."
+msgstr ""
+
+#: erpnext/accounts/doctype/dunning/dunning.py:188
+msgid "Dunning Letter for Dunning Type {0} not found."
+msgstr ""
+
#. Label of the dunning_level (Int) field in DocType 'Overdue Payment'
#: erpnext/accounts/doctype/overdue_payment/overdue_payment.json
msgid "Dunning Level"
@@ -18232,6 +18255,10 @@ msgstr ""
msgid "Duplicate item group found in the item group table"
msgstr ""
+#: erpnext/accounts/doctype/dunning_type/dunning_type.py:133
+msgid "Duplicate languages found on Dunning Letter Text. Keep only one of them."
+msgstr ""
+
#: erpnext/projects/doctype/project/project.js:186
msgid "Duplicate project has been created"
msgstr ""
@@ -18798,7 +18825,7 @@ msgstr ""
msgid "Enable Accounting Dimensions"
msgstr ""
-#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1721
+#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1723
msgid "Enable Allow Partial Reservation in the Stock Settings to reserve partial stock."
msgstr ""
@@ -19276,7 +19303,7 @@ msgstr ""
msgid "Enter customer's phone number"
msgstr ""
-#: erpnext/assets/doctype/asset/asset.js:936
+#: erpnext/assets/doctype/asset/asset.js:944
msgid "Enter date to scrap asset"
msgstr ""
@@ -19372,7 +19399,7 @@ msgstr ""
#: erpnext/accounts/doctype/ledger_merge/ledger_merge.json
#: erpnext/accounts/report/account_balance/account_balance.js:29
#: erpnext/accounts/report/account_balance/account_balance.js:45
-#: erpnext/accounts/report/balance_sheet/balance_sheet.py:255
+#: erpnext/accounts/report/balance_sheet/balance_sheet.py:264
#: erpnext/setup/setup_wizard/operations/install_fixtures.py:306
msgid "Equity"
msgstr ""
@@ -19873,7 +19900,7 @@ msgstr ""
#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:601
#: erpnext/accounts/report/account_balance/account_balance.js:28
#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js:89
-#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:184
+#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:192
#: erpnext/accounts/report/profitability_analysis/profitability_analysis.py:199
msgid "Expense"
msgstr ""
@@ -20164,7 +20191,7 @@ msgstr ""
msgid "Failed to personalize your setup"
msgstr ""
-#: erpnext/assets/doctype/asset/asset.js:269
+#: erpnext/assets/doctype/asset/asset.js:277
msgid "Failed to post depreciation entries"
msgstr ""
@@ -20300,7 +20327,7 @@ msgstr ""
msgid "Fetch Value From"
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.js:372
+#: erpnext/stock/doctype/material_request/material_request.js:373
#: erpnext/stock/doctype/stock_entry/stock_entry.js:833
msgid "Fetch exploded BOM (including sub-assemblies)"
msgstr ""
@@ -20990,7 +21017,7 @@ msgstr ""
msgid "Following Material Requests have been raised automatically based on Item's re-order level"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:836
+#: erpnext/selling/doctype/customer/customer.py:841
msgid "Following fields are mandatory to create address:"
msgstr ""
@@ -21022,7 +21049,7 @@ msgstr ""
msgid "For"
msgstr ""
-#: erpnext/public/js/utils/sales_common.js:389
+#: erpnext/public/js/utils/sales_common.js:387
msgid "For 'Product Bundle' items, Warehouse, Serial No and Batch No will be considered from the 'Packing List' table. If Warehouse and Batch No are same for all packing items for any 'Product Bundle' item, those values can be entered in the main Item table, values will be copied to 'Packing List' table."
msgstr ""
@@ -21047,7 +21074,7 @@ msgstr ""
msgid "For Item"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1685
+#: erpnext/controllers/stock_controller.py:1683
msgid "For Item {0} cannot be received more than {1} qty against the {2} {3}"
msgstr ""
@@ -21108,10 +21135,10 @@ msgstr ""
#. Label of the warehouse (Link) field in DocType 'Material Request Plan Item'
#. Label of the for_warehouse (Link) field in DocType 'Production Plan'
#: erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json
-#: erpnext/manufacturing/doctype/production_plan/production_plan.js:471
+#: erpnext/manufacturing/doctype/production_plan/production_plan.js:469
#: erpnext/manufacturing/doctype/production_plan/production_plan.json
#: erpnext/selling/doctype/sales_order/sales_order.js:1450
-#: erpnext/stock/doctype/material_request/material_request.js:361
+#: erpnext/stock/doctype/material_request/material_request.js:362
#: erpnext/templates/form_grid/material_request_grid.html:36
msgid "For Warehouse"
msgstr ""
@@ -21176,7 +21203,7 @@ msgstr ""
msgid "For operation {0} at row {1}, please add raw materials or set a BOM against it."
msgstr ""
-#: erpnext/manufacturing/doctype/work_order/work_order.py:2911
+#: erpnext/manufacturing/doctype/work_order/work_order.py:2915
msgid "For operation {0}: Quantity ({1}) can not be greater than pending quantity({2})"
msgstr ""
@@ -21837,13 +21864,13 @@ msgid "Further nodes can be only created under 'Group' type nodes"
msgstr ""
#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:188
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1231
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1233
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:177
msgid "Future Payment Amount"
msgstr ""
#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:187
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1230
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1232
msgid "Future Payment Ref"
msgstr ""
@@ -21997,6 +22024,10 @@ msgstr ""
msgid "General Ledger remarks length"
msgstr ""
+#: erpnext/accounts/report/general_ledger/general_ledger.py:829
+msgid "General Ledger requires {0} to be synced to DuckDB"
+msgstr ""
+
#. Label of the gs (Section Break) field in DocType 'Item Group'
#: erpnext/setup/doctype/item_group/item_group.json
msgid "General Settings"
@@ -22179,8 +22210,8 @@ msgstr ""
#: erpnext/selling/doctype/sales_order/sales_order.js:1216
#: erpnext/stock/doctype/delivery_note/delivery_note.js:187
#: erpnext/stock/doctype/delivery_note/delivery_note.js:239
-#: erpnext/stock/doctype/material_request/material_request.js:141
-#: erpnext/stock/doctype/material_request/material_request.js:238
+#: erpnext/stock/doctype/material_request/material_request.js:144
+#: erpnext/stock/doctype/material_request/material_request.js:241
#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:144
#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:244
#: erpnext/stock/doctype/stock_entry/stock_entry.js:461
@@ -22202,7 +22233,7 @@ msgstr ""
msgid "Get Items for Purchase Only"
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.js:346
+#: erpnext/stock/doctype/material_request/material_request.js:347
#: erpnext/stock/doctype/stock_entry/stock_entry.js:836
#: erpnext/stock/doctype/stock_entry/stock_entry.js:849
msgid "Get Items from BOM"
@@ -22288,7 +22319,7 @@ msgstr ""
msgid "Get Started Sections"
msgstr ""
-#: erpnext/manufacturing/doctype/production_plan/production_plan.js:552
+#: erpnext/manufacturing/doctype/production_plan/production_plan.js:550
msgid "Get Stock"
msgstr ""
@@ -23486,6 +23517,12 @@ msgstr ""
msgid "If enabled, a print of this document will be attached to each email"
msgstr ""
+#. Description of the 'Auto Repost Incorrect Valuation Entries (Weekly)'
+#. (Check) field in DocType 'Stock Reposting Settings'
+#: erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
+msgid "If enabled, a weekly scheduler scans the Stock Ledger Variance for item-warehouses with incorrect valuation in the current financial year and auto-creates Item & Warehouse based reposts to fix them."
+msgstr ""
+
#. Description of the 'Enable discount accounting for selling' (Check) field in
#. DocType 'Selling Settings'
#: erpnext/selling/doctype/selling_settings/selling_settings.json
@@ -23826,7 +23863,7 @@ msgstr ""
msgid "If you still want to proceed, please disable '{0}' checkbox."
msgstr ""
-#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1841
+#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1847
msgid "If you still want to proceed, please enable {0}."
msgstr ""
@@ -24178,11 +24215,11 @@ msgstr ""
msgid "In Transit"
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.js:477
+#: erpnext/stock/doctype/material_request/material_request.js:478
msgid "In Transit Transfer"
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.js:446
+#: erpnext/stock/doctype/material_request/material_request.js:447
msgid "In Transit Warehouse"
msgstr ""
@@ -24535,7 +24572,7 @@ msgstr ""
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:460
#: erpnext/accounts/report/account_balance/account_balance.js:27
#: erpnext/accounts/report/financial_statements.py:776
-#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:182
+#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:190
#: erpnext/accounts/report/profitability_analysis/profitability_analysis.py:192
msgid "Income"
msgstr ""
@@ -24558,6 +24595,10 @@ msgstr ""
msgid "Income Account"
msgstr ""
+#: erpnext/accounts/doctype/dunning_type/dunning_type.py:86
+msgid "Income Account Validation Error"
+msgstr ""
+
#. Label of the income_and_expense_account (Section Break) field in DocType
#. 'POS Profile'
#: erpnext/accounts/doctype/pos_profile/pos_profile.json
@@ -24674,6 +24715,10 @@ msgstr ""
msgid "Incorrect Serial and Batch Bundle"
msgstr ""
+#: erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py:301
+msgid "Incorrect Stock Asset Account in {0}"
+msgstr ""
+
#. Name of a report
#: erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json
msgid "Incorrect Stock Value Report"
@@ -24849,14 +24894,14 @@ msgstr ""
msgid "Inspected By"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1579
+#: erpnext/controllers/stock_controller.py:1577
#: erpnext/manufacturing/doctype/job_card/job_card.py:834
msgid "Inspection Rejected"
msgstr ""
#. Label of the inspection_required (Check) field in DocType 'Stock Entry'
+#: erpnext/controllers/stock_controller.py:1547
#: erpnext/controllers/stock_controller.py:1549
-#: erpnext/controllers/stock_controller.py:1551
#: erpnext/stock/doctype/stock_entry/stock_entry.json
msgid "Inspection Required"
msgstr ""
@@ -24873,7 +24918,7 @@ msgstr ""
msgid "Inspection Required before Purchase"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1564
+#: erpnext/controllers/stock_controller.py:1562
#: erpnext/manufacturing/doctype/job_card/job_card.py:815
msgid "Inspection Submission"
msgstr ""
@@ -25091,7 +25136,7 @@ msgstr ""
msgid "Interest Income"
msgstr ""
-#: erpnext/accounts/doctype/payment_entry/payment_entry.py:3013
+#: erpnext/accounts/doctype/payment_entry/payment_entry.py:3011
msgid "Interest and/or dunning fee"
msgstr ""
@@ -25116,7 +25161,7 @@ msgstr ""
msgid "Internal Customer Accounting"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:257
+#: erpnext/selling/doctype/customer/customer.py:260
msgid "Internal Customer for company {0} already exists"
msgstr ""
@@ -25142,7 +25187,7 @@ msgstr ""
msgid "Internal Supplier Details"
msgstr ""
-#: erpnext/buying/doctype/supplier/supplier.py:181
+#: erpnext/buying/doctype/supplier/supplier.py:185
msgid "Internal Supplier for company {0} already exists"
msgstr ""
@@ -25187,7 +25232,7 @@ msgstr ""
msgid "Internal notes about this customer. Not visible on transactions or the portal."
msgstr ""
-#: erpnext/controllers/stock_controller.py:1646
+#: erpnext/controllers/stock_controller.py:1644
msgid "Internal transfers can only be done in company's default currency"
msgstr ""
@@ -25272,7 +25317,7 @@ msgstr ""
msgid "Invalid Cost Center"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:370
+#: erpnext/selling/doctype/customer/customer.py:375
msgid "Invalid Customer Group"
msgstr ""
@@ -25619,7 +25664,7 @@ msgstr ""
msgid "Invoice Document Type Selection Error"
msgstr ""
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1211
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1213
msgid "Invoice Grand Total"
msgstr ""
@@ -25724,7 +25769,7 @@ msgstr ""
#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:171
#: erpnext/accounts/report/accounts_payable/accounts_payable.html:139
#: erpnext/accounts/report/accounts_receivable/accounts_receivable.html:140
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1213
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1215
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:164
#: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py:194
msgid "Invoiced Amount"
@@ -26354,7 +26399,7 @@ msgstr ""
msgid "Issue Date"
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.js:180
+#: erpnext/stock/doctype/material_request/material_request.js:183
msgid "Issue Material"
msgstr ""
@@ -27393,8 +27438,8 @@ msgstr ""
msgid "Item Price Stock"
msgstr ""
-#: erpnext/stock/get_item_details.py:1143
-#: erpnext/stock/get_item_details.py:1167
+#: erpnext/stock/get_item_details.py:1142
+#: erpnext/stock/get_item_details.py:1166
msgid "Item Price added for {0} in Price List - {1}"
msgstr ""
@@ -27406,7 +27451,7 @@ msgstr ""
msgid "Item Price created at rate {0}"
msgstr ""
-#: erpnext/stock/get_item_details.py:1126
+#: erpnext/stock/get_item_details.py:1125
msgid "Item Price updated for {0} in Price List {1}"
msgstr ""
@@ -27604,7 +27649,7 @@ msgstr ""
msgid "Item Variants updated"
msgstr ""
-#: erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py:87
+#: erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py:97
msgid "Item Warehouse based reposting has been enabled."
msgstr ""
@@ -27744,6 +27789,10 @@ msgstr ""
msgid "Item rate has been updated to zero as Allow Zero Valuation Rate is checked for item {0}"
msgstr ""
+#: erpnext/stock/doctype/material_request/material_request.py:227
+msgid "Item rates have been updated based on the selected Buying Price List {0}"
+msgstr ""
+
#. Label of the item (Link) field in DocType 'BOM'
#. Label of the finished_good (Link) field in DocType 'Job Card'
#: erpnext/manufacturing/doctype/bom/bom.json
@@ -27763,7 +27812,7 @@ msgstr ""
msgid "Item variant {0} exists with same attributes"
msgstr ""
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:578
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:579
msgid "Item with name {0} not found in the Purchase Order"
msgstr ""
@@ -27808,7 +27857,7 @@ msgstr ""
msgid "Item {0} has no Serial No. Only serialized items can have delivery based on Serial No"
msgstr ""
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:597
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:598
msgid "Item {0} has no changes in delivered quantity. Please unselect the row if you do not wish to update its quantity."
msgstr ""
@@ -27832,7 +27881,7 @@ msgstr ""
msgid "Item {0} is disabled"
msgstr ""
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:583
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:584
msgid "Item {0} is not a drop ship item. Only drop ship items can have Delivered Qty updated."
msgstr ""
@@ -27860,11 +27909,11 @@ msgstr ""
msgid "Item {0} must be a Fixed Asset Item"
msgstr ""
-#: erpnext/stock/get_item_details.py:351
+#: erpnext/stock/get_item_details.py:350
msgid "Item {0} must be a Non-Stock Item"
msgstr ""
-#: erpnext/stock/get_item_details.py:348
+#: erpnext/stock/get_item_details.py:347
msgid "Item {0} must be a Sub-contracted Item"
msgstr ""
@@ -27880,11 +27929,11 @@ msgstr ""
msgid "Item {0} not found."
msgstr ""
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:327
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:328
msgid "Item {0}: Ordered qty {1} cannot be less than minimum order qty {2} (defined in Item)."
msgstr ""
-#: erpnext/manufacturing/doctype/production_plan/production_plan.js:573
+#: erpnext/manufacturing/doctype/production_plan/production_plan.js:571
msgid "Item {0}: {1} qty produced. "
msgstr ""
@@ -27934,7 +27983,7 @@ msgstr ""
msgid "Item-wise sales Register"
msgstr ""
-#: erpnext/stock/get_item_details.py:731
+#: erpnext/stock/get_item_details.py:730
msgid "Item/Item Code required to get Item Tax Template."
msgstr ""
@@ -28217,7 +28266,7 @@ msgstr ""
msgid "Job Worker Warehouse"
msgstr ""
-#: erpnext/manufacturing/doctype/work_order/work_order.py:2966
+#: erpnext/manufacturing/doctype/work_order/work_order.py:2970
msgid "Job card {0} created"
msgstr ""
@@ -28240,7 +28289,7 @@ msgstr ""
msgid "Joule/Meter"
msgstr ""
-#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js:30
+#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js:31
msgid "Journal Entries"
msgstr ""
@@ -28268,8 +28317,8 @@ msgstr ""
#: erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json
#: erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html:10
#: erpnext/accounts/workspace/invoicing/invoicing.json
-#: erpnext/assets/doctype/asset/asset.js:390
-#: erpnext/assets/doctype/asset/asset.js:399
+#: erpnext/assets/doctype/asset/asset.js:398
+#: erpnext/assets/doctype/asset/asset.js:407
#: erpnext/assets/doctype/asset/asset.json
#: erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.json
#: erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json
@@ -28303,7 +28352,7 @@ msgstr ""
msgid "Journal Entry Type"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:561
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:562
msgid "Journal Entry for Asset scrapping cannot be cancelled. Please restore the Asset."
msgstr ""
@@ -28316,7 +28365,7 @@ msgstr ""
msgid "Journal Entry type should be set as Depreciation Entry for asset depreciation"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:731
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:732
msgid "Journal Entry {0} does not have account {1} or already matched against other voucher"
msgstr ""
@@ -28324,7 +28373,7 @@ msgstr ""
msgid "Journal Template Accounts"
msgstr ""
-#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js:97
+#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js:107
msgid "Journal entries have been created"
msgstr ""
@@ -28866,7 +28915,7 @@ msgstr ""
msgid "Ledger Merge Accounts"
msgstr ""
-#: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:146
+#: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:151
msgid "Ledger Type"
msgstr ""
@@ -28948,7 +28997,7 @@ msgstr ""
msgid "Lft"
msgstr ""
-#: erpnext/accounts/report/balance_sheet/balance_sheet.py:253
+#: erpnext/accounts/report/balance_sheet/balance_sheet.py:262
msgid "Liabilities"
msgstr ""
@@ -29241,7 +29290,7 @@ msgstr ""
#. 'Quotation'
#: erpnext/crm/doctype/opportunity/opportunity.json
#: erpnext/crm/report/lost_opportunity/lost_opportunity.py:55
-#: erpnext/public/js/utils/sales_common.js:596
+#: erpnext/public/js/utils/sales_common.js:594
#: erpnext/selling/doctype/quotation/quotation.json
msgid "Lost Reasons"
msgstr ""
@@ -30358,7 +30407,7 @@ msgstr ""
#. Option for the 'Purpose' (Select) field in DocType 'Stock Entry'
#. Option for the 'Purpose' (Select) field in DocType 'Stock Entry Type'
#: erpnext/setup/setup_wizard/operations/install_fixtures.py:77
-#: erpnext/stock/doctype/material_request/material_request.js:188
+#: erpnext/stock/doctype/material_request/material_request.js:191
#: erpnext/stock/doctype/stock_entry/stock_entry.json
#: erpnext/stock/doctype/stock_entry_type/stock_entry_type.json
msgid "Material Receipt"
@@ -30417,8 +30466,8 @@ msgstr ""
#: erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py:36
#: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
#: erpnext/stock/doctype/material_request/material_request.json
-#: erpnext/stock/doctype/material_request/material_request.py:434
-#: erpnext/stock/doctype/material_request/material_request.py:484
+#: erpnext/stock/doctype/material_request/material_request.py:473
+#: erpnext/stock/doctype/material_request/material_request.py:523
#: erpnext/stock/doctype/pick_list/pick_list.json
#: erpnext/stock/doctype/pick_list_item/pick_list_item.json
#: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -30511,7 +30560,7 @@ msgstr ""
msgid "Material Request not created, as quantity for Raw Materials already available."
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.py:145
+#: erpnext/stock/doctype/material_request/material_request.py:146
msgid "Material Request of maximum {0} can be made for Item {1} against Sales Order {2}"
msgstr ""
@@ -30579,7 +30628,7 @@ msgstr ""
#: erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json
#: erpnext/setup/setup_wizard/operations/install_fixtures.py:83
#: erpnext/stock/doctype/item/item.json
-#: erpnext/stock/doctype/material_request/material_request.js:166
+#: erpnext/stock/doctype/material_request/material_request.js:169
#: erpnext/stock/doctype/material_request/material_request.json
#: erpnext/stock/doctype/pick_list/pick_list.json
#: erpnext/stock/doctype/stock_entry/stock_entry.json
@@ -30587,7 +30636,7 @@ msgstr ""
msgid "Material Transfer"
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.js:172
+#: erpnext/stock/doctype/material_request/material_request.js:175
msgid "Material Transfer (In Transit)"
msgstr ""
@@ -30783,7 +30832,7 @@ msgstr ""
msgid "Maximum discount for Item {0} is {1}%"
msgstr ""
-#: erpnext/public/js/utils/barcode_scanner.js:120
+#: erpnext/public/js/utils/barcode_scanner.js:125
msgid "Maximum quantity scanned for item {0}."
msgstr ""
@@ -31262,7 +31311,7 @@ msgstr ""
msgid "Missing Required Filter"
msgstr ""
-#: erpnext/assets/doctype/asset_repair/asset_repair.py:297
+#: erpnext/assets/doctype/asset_repair/asset_repair.py:300
msgid "Missing Serial No Bundle"
msgstr ""
@@ -31528,7 +31577,7 @@ msgstr ""
msgid "Multiple Accounts (Journal Template)"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:441
+#: erpnext/selling/doctype/customer/customer.py:446
msgid "Multiple Loyalty Programs found for Customer {}. Please select manually."
msgstr ""
@@ -31843,7 +31892,7 @@ msgstr ""
#: erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py:214
#: erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py:215
-#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:121
+#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:129
msgid "Net Profit"
msgstr ""
@@ -31851,7 +31900,7 @@ msgstr ""
msgid "Net Profit Ratio"
msgstr ""
-#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:186
+#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:194
msgid "Net Profit/Loss"
msgstr ""
@@ -32034,10 +32083,6 @@ msgstr ""
msgid "New Asset Value"
msgstr ""
-#: erpnext/assets/dashboard_fixtures.py:169
-msgid "New Assets (This Year)"
-msgstr ""
-
#. Label of the new_bom (Link) field in DocType 'BOM Update Log'
#. Label of the new_bom (Link) field in DocType 'BOM Update Tool'
#: erpnext/manufacturing/doctype/bom/bom_tree.js:62
@@ -32197,7 +32242,7 @@ msgstr ""
msgid "New Workplace"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:406
+#: erpnext/selling/doctype/customer/customer.py:411
msgid "New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0}"
msgstr ""
@@ -32285,11 +32330,11 @@ msgstr ""
msgid "No Impact on Accounting Ledger"
msgstr ""
-#: erpnext/stock/get_item_details.py:322
+#: erpnext/stock/get_item_details.py:321
msgid "No Item with Barcode {0}"
msgstr ""
-#: erpnext/stock/get_item_details.py:326
+#: erpnext/stock/get_item_details.py:325
msgid "No Item with Serial No {0}"
msgstr ""
@@ -32325,9 +32370,9 @@ msgstr ""
msgid "No POS Profile found. Please create a New POS Profile first"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1582
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1642
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1656
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1583
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1643
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1657
#: erpnext/stock/doctype/item/item.py:1495
msgid "No Permission"
msgstr ""
@@ -32390,6 +32435,10 @@ msgstr ""
msgid "No Work Orders were created"
msgstr ""
+#: erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py:296
+msgid "No account set"
+msgstr ""
+
#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:832
#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:905
msgid "No accounting entries for the following warehouses"
@@ -32415,7 +32464,7 @@ msgstr ""
msgid "No additional fields available"
msgstr ""
-#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1361
+#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1362
msgid "No available quantity to reserve for item {0} in warehouse {1}"
msgstr ""
@@ -32609,11 +32658,11 @@ msgstr ""
msgid "No open task"
msgstr ""
-#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:330
+#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:355
msgid "No outstanding invoices found"
msgstr ""
-#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:328
+#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:353
msgid "No outstanding invoices require exchange rate revaluation"
msgstr ""
@@ -33291,10 +33340,16 @@ msgstr ""
msgid "On enabling this cancellation entries will be posted on the actual cancellation date and reports will consider cancelled entries as well"
msgstr ""
-#: erpnext/manufacturing/doctype/production_plan/production_plan.js:727
+#: erpnext/manufacturing/doctype/production_plan/production_plan.js:725
msgid "On expanding a row in the Items to Manufacture table, you'll see an option to 'Include Exploded Items'. Ticking this includes raw materials of the sub-assembly items in the production process."
msgstr ""
+#. Option for the 'Status' (Select) field in DocType 'Project'
+#: erpnext/projects/doctype/project/project.json
+#: erpnext/projects/doctype/project/project_list.js:8
+msgid "On hold"
+msgstr ""
+
#. Description of the 'Excluded Fee' (Currency) field in DocType 'Bank
#. Transaction'
#: erpnext/accounts/doctype/bank_transaction/bank_transaction.json
@@ -34454,7 +34509,7 @@ msgstr ""
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.json
#: erpnext/accounts/report/accounts_payable/accounts_payable.html:140
#: erpnext/accounts/report/accounts_receivable/accounts_receivable.html:141
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1220
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1222
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:167
#: erpnext/accounts/report/purchase_register/purchase_register.py:305
#: erpnext/accounts/report/sales_register/sales_register.py:333
@@ -34525,7 +34580,7 @@ msgstr ""
msgid "Over Picking Allowance (%)"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1816
+#: erpnext/controllers/stock_controller.py:1814
msgid "Over Receipt"
msgstr ""
@@ -35058,7 +35113,7 @@ msgstr ""
msgid "Packed Items"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1650
+#: erpnext/controllers/stock_controller.py:1648
msgid "Packed Items cannot be transferred internally"
msgstr ""
@@ -35140,7 +35195,7 @@ msgstr ""
#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:173
#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.json
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1214
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1216
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:165
#: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py:201
#: erpnext/accounts/report/pos_register/pos_register.py:209
@@ -35286,7 +35341,7 @@ msgstr ""
msgid "Parent Account"
msgstr ""
-#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:382
+#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:383
msgid "Parent Account Missing"
msgstr ""
@@ -35431,7 +35486,7 @@ msgstr ""
msgid "Partial Payment in POS Transactions are not allowed."
msgstr ""
-#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1724
+#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1726
msgid "Partial Stock Reservation"
msgstr ""
@@ -35647,7 +35702,7 @@ msgstr ""
#: erpnext/accounts/report/accounts_payable/accounts_payable.js:105
#: erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js:82
#: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:65
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1147
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1149
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:82
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:147
#: erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js:49
@@ -35676,7 +35731,7 @@ msgstr ""
#. Name of a DocType
#: erpnext/accounts/doctype/party_account/party_account.json
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1159
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1161
msgid "Party Account"
msgstr ""
@@ -35861,7 +35916,7 @@ msgstr ""
#: erpnext/accounts/report/accounts_payable/accounts_payable.js:92
#: erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js:69
#: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:52
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1141
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1143
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:69
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:141
#: erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js:42
@@ -35888,7 +35943,7 @@ msgstr ""
msgid "Party Type and Party can only be set for Receivable / Payable account
{0}"
msgstr ""
-#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:631
+#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:689
msgid "Party Type and Party is mandatory for {0} account"
msgstr ""
@@ -36031,7 +36086,7 @@ msgid "Payable"
msgstr ""
#: erpnext/accounts/report/accounts_payable/accounts_payable.js:50
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1157
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1159
#: erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py:209
#: erpnext/accounts/report/purchase_register/purchase_register.py:210
#: erpnext/accounts/report/purchase_register/purchase_register.py:251
@@ -36247,7 +36302,7 @@ msgstr ""
msgid "Payment Gateway Account"
msgstr ""
-#: erpnext/accounts/utils.py:1509
+#: erpnext/accounts/utils.py:1503
msgid "Payment Gateway Account not created, please create one manually."
msgstr ""
@@ -36522,7 +36577,7 @@ msgstr ""
#: erpnext/accounts/doctype/payment_schedule/payment_schedule.json
#: erpnext/accounts/doctype/payment_term/payment_term.json
#: erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1210
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1212
#: erpnext/accounts/report/gross_profit/gross_profit.py:449
#: erpnext/accounts/workspace/invoicing/invoicing.json
#: erpnext/public/js/controllers/transaction.js:544
@@ -36637,7 +36692,7 @@ msgstr ""
msgid "Payment Unlink Error"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:903
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:904
msgid "Payment against {0} {1} cannot be greater than Outstanding Amount {2}"
msgstr ""
@@ -37196,7 +37251,7 @@ msgstr ""
#. Label of a Workspace Sidebar Item
#: erpnext/selling/doctype/sales_order/sales_order.js:1028
#: erpnext/stock/doctype/delivery_note/delivery_note.js:199
-#: erpnext/stock/doctype/material_request/material_request.js:156
+#: erpnext/stock/doctype/material_request/material_request.js:159
#: erpnext/stock/doctype/pick_list/pick_list.json
#: erpnext/stock/doctype/stock_entry/stock_entry.json
#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
@@ -37569,7 +37624,7 @@ msgstr ""
msgid "Please Specify Account"
msgstr ""
-#: erpnext/buying/doctype/supplier/supplier.py:129
+#: erpnext/buying/doctype/supplier/supplier.py:133
msgid "Please add 'Supplier' role to user {0}."
msgstr ""
@@ -37585,7 +37640,7 @@ msgstr ""
msgid "Please add Request for Quotation to the sidebar in Portal Settings."
msgstr ""
-#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:419
+#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:420
msgid "Please add Root Account for - {0}"
msgstr ""
@@ -37617,11 +37672,11 @@ msgstr ""
msgid "Please add the account to root level Company - {}"
msgstr ""
-#: erpnext/controllers/website_list_for_contact.py:302
+#: erpnext/controllers/website_list_for_contact.py:304
msgid "Please add {1} role to user {0}."
msgstr ""
-#: erpnext/controllers/stock_controller.py:1827
+#: erpnext/controllers/stock_controller.py:1825
msgid "Please adjust the qty or edit {0} to proceed."
msgstr ""
@@ -37629,7 +37684,7 @@ msgstr ""
msgid "Please attach CSV file"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3244
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3236
msgid "Please cancel and amend the Payment Entry"
msgstr ""
@@ -37647,7 +37702,7 @@ msgstr ""
msgid "Please capitalize this asset before submitting."
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:977
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:978
msgid "Please check Multi Currency option to allow accounts with other currency"
msgstr ""
@@ -37696,7 +37751,7 @@ msgstr ""
msgid "Please configure accounts for the Bank Entry rule."
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:637
+#: erpnext/selling/doctype/customer/customer.py:642
msgid "Please contact any of the following users to extend the credit limits for {0}: {1}"
msgstr ""
@@ -37704,7 +37759,7 @@ msgstr ""
msgid "Please contact any of the following users to {} this transaction."
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:630
+#: erpnext/selling/doctype/customer/customer.py:635
msgid "Please contact your administrator to extend the credit limits for {0}."
msgstr ""
@@ -37854,11 +37909,11 @@ msgstr ""
msgid "Please enter Receipt Document"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1041
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1042
msgid "Please enter Reference date"
msgstr ""
-#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:398
+#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:399
msgid "Please enter Root Type for account- {0}"
msgstr ""
@@ -37923,7 +37978,7 @@ msgstr ""
msgid "Please enter parent cost center"
msgstr ""
-#: erpnext/public/js/utils/barcode_scanner.js:186
+#: erpnext/public/js/utils/barcode_scanner.js:191
msgid "Please enter quantity for item {0}"
msgstr ""
@@ -37999,7 +38054,7 @@ msgstr ""
msgid "Please make sure the employees above report to another Active employee."
msgstr ""
-#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:377
+#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:378
msgid "Please make sure the file you are using has 'Parent Account' column present in the header."
msgstr ""
@@ -38096,7 +38151,7 @@ msgstr ""
msgid "Please select Company"
msgstr ""
-#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js:139
+#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js:157
#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:75
msgid "Please select Company and Posting Date to getting entries"
msgstr ""
@@ -38125,8 +38180,8 @@ msgstr ""
msgid "Please select Finished Good Item for Service Item {0}"
msgstr ""
-#: erpnext/assets/doctype/asset/asset.js:762
-#: erpnext/assets/doctype/asset/asset.js:777
+#: erpnext/assets/doctype/asset/asset.js:770
+#: erpnext/assets/doctype/asset/asset.js:785
msgid "Please select Item Code first"
msgstr ""
@@ -38191,7 +38246,7 @@ msgid "Please select a BOM"
msgstr ""
#: erpnext/accounts/party.py:445
-#: erpnext/stock/doctype/pick_list/pick_list.py:1788
+#: erpnext/stock/doctype/pick_list/pick_list.py:1853
msgid "Please select a Company"
msgstr ""
@@ -38292,7 +38347,7 @@ msgstr ""
msgid "Please select a value for {0} quotation_to {1}"
msgstr ""
-#: erpnext/assets/doctype/asset_repair/asset_repair.js:194
+#: erpnext/assets/doctype/asset_repair/asset_repair.js:203
msgid "Please select an item code before setting the warehouse."
msgstr ""
@@ -38304,7 +38359,7 @@ msgstr ""
msgid "Please select at least one filter: Item Code, Batch, or Serial No."
msgstr ""
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:571
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:572
msgid "Please select at least one item to update delivered quantity."
msgstr ""
@@ -38328,7 +38383,7 @@ msgstr ""
msgid "Please select atleast one operation to create Job Card"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1721
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1722
msgid "Please select correct account"
msgstr ""
@@ -38510,7 +38565,7 @@ msgstr ""
msgid "Please set Tax ID for the customer '%s'"
msgstr ""
-#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:339
+#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:364
msgid "Please set Unrealized Exchange Gain/Loss Account in Company {0}"
msgstr ""
@@ -38530,7 +38585,7 @@ msgstr ""
msgid "Please set a Cost Center for the Asset or set an Asset Depreciation Cost Center for the Company {}"
msgstr ""
-#: erpnext/projects/doctype/project/project.py:736
+#: erpnext/projects/doctype/project/project.py:766
msgid "Please set a default Holiday List for Company {0}"
msgstr ""
@@ -38583,11 +38638,11 @@ msgstr ""
msgid "Please set default Cash or Bank account in Mode of Payments {}"
msgstr ""
-#: erpnext/accounts/utils.py:2528
+#: erpnext/accounts/utils.py:2522
msgid "Please set default Exchange Gain/Loss Account in Company {}"
msgstr ""
-#: erpnext/assets/doctype/asset_repair/asset_repair.py:386
+#: erpnext/assets/doctype/asset_repair/asset_repair.py:389
msgid "Please set default Expense Account in Company {0}"
msgstr ""
@@ -38691,7 +38746,7 @@ msgstr ""
msgid "Please share this email with your support team so that they can find and fix the issue."
msgstr ""
-#: erpnext/stock/get_item_details.py:333
+#: erpnext/stock/get_item_details.py:332
msgid "Please specify Company"
msgstr ""
@@ -38730,7 +38785,7 @@ msgstr ""
msgid "Please uncheck 'Show in Bucket View' to create Orders"
msgstr ""
-#: erpnext/assets/doctype/asset_repair/asset_repair.py:237
+#: erpnext/assets/doctype/asset_repair/asset_repair.py:240
msgid "Please update Repair Status."
msgstr ""
@@ -38903,7 +38958,7 @@ msgstr ""
#: erpnext/accounts/report/accounts_payable/accounts_payable.js:16
#: erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js:15
#: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:18
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1139
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1141
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:15
#: erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py:38
#: erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.html:7
@@ -38944,7 +38999,7 @@ msgstr ""
#: erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py:104
#: erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py:86
#: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:25
-#: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:150
+#: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:155
#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:155
#: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:36
#: erpnext/templates/form_grid/bank_reconciliation_grid.html:6
@@ -39018,7 +39073,7 @@ msgstr ""
#: erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py:105
#: erpnext/stock/report/serial_no_ledger/serial_no_ledger.js:63
#: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:26
-#: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:151
+#: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:156
#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:160
#: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:41
#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json
@@ -39214,7 +39269,7 @@ msgstr ""
msgid "Preview Transactions"
msgstr ""
-#: erpnext/accounts/report/balance_sheet/balance_sheet.py:182
+#: erpnext/accounts/report/balance_sheet/balance_sheet.py:191
#: erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py:142
msgid "Previous Financial Year is not closed"
msgstr ""
@@ -39352,7 +39407,7 @@ msgstr ""
msgid "Price List Currency"
msgstr ""
-#: erpnext/stock/get_item_details.py:1345
+#: erpnext/stock/get_item_details.py:1344
msgid "Price List Currency not selected"
msgstr ""
@@ -40294,7 +40349,7 @@ msgstr ""
msgid "Profit & Loss"
msgstr ""
-#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:117
+#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:125
msgid "Profit This Year"
msgstr ""
@@ -40323,6 +40378,10 @@ msgstr ""
msgid "Profit and Loss Statement"
msgstr ""
+#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:215
+msgid "Profit and Loss Statement requires {0} to be synced to DuckDB"
+msgstr ""
+
#. Label of the heading_cppb (Heading) field in DocType 'Bisect Accounting
#. Statements'
#. Label of the profit_loss_summary (Float) field in DocType 'Bisect Nodes'
@@ -40331,8 +40390,8 @@ msgstr ""
msgid "Profit and Loss Summary"
msgstr ""
-#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:141
-#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:142
+#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:149
+#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:150
msgid "Profit for the year"
msgstr ""
@@ -40361,7 +40420,7 @@ msgstr ""
msgid "Progress (%)"
msgstr ""
-#: erpnext/projects/doctype/project/project.py:375
+#: erpnext/projects/doctype/project/project.py:377
msgid "Project Collaboration Invitation"
msgstr ""
@@ -40409,7 +40468,7 @@ msgstr ""
msgid "Project Summary"
msgstr ""
-#: erpnext/projects/doctype/project/project.py:674
+#: erpnext/projects/doctype/project/project.py:704
msgid "Project Summary for {0}"
msgstr ""
@@ -40540,7 +40599,7 @@ msgstr ""
#. Label of a Card Break in the Projects Workspace
#. Title of a Workspace Sidebar
#: erpnext/config/projects.py:7 erpnext/desktop_icon/projects.json
-#: erpnext/projects/doctype/project/project.py:452
+#: erpnext/projects/doctype/project/project.py:482
#: erpnext/projects/workspace/projects/projects.json
#: erpnext/selling/doctype/customer/customer_dashboard.py:26
#: erpnext/selling/doctype/sales_order/sales_order_dashboard.py:28
@@ -40711,9 +40770,9 @@ msgstr ""
msgid "Provisional Expense Account"
msgstr ""
-#: erpnext/accounts/report/balance_sheet/balance_sheet.py:159
-#: erpnext/accounts/report/balance_sheet/balance_sheet.py:160
-#: erpnext/accounts/report/balance_sheet/balance_sheet.py:227
+#: erpnext/accounts/report/balance_sheet/balance_sheet.py:168
+#: erpnext/accounts/report/balance_sheet/balance_sheet.py:169
+#: erpnext/accounts/report/balance_sheet/balance_sheet.py:236
msgid "Provisional Profit / Loss (Credit)"
msgstr ""
@@ -40992,7 +41051,7 @@ msgstr ""
#: erpnext/selling/doctype/sales_order_item/sales_order_item.json
#: erpnext/setup/doctype/authorization_rule/authorization_rule.json
#: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
-#: erpnext/stock/doctype/material_request/material_request.js:196
+#: erpnext/stock/doctype/material_request/material_request.js:199
#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:217
#: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
#: erpnext/stock/doctype/stock_entry/stock_entry.json
@@ -41105,7 +41164,7 @@ msgstr ""
msgid "Purchase Order {0} is not submitted"
msgstr ""
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:939
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:940
msgid "Purchase Orders"
msgstr ""
@@ -41120,7 +41179,7 @@ msgstr ""
msgid "Purchase Orders Items Overdue"
msgstr ""
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:288
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:289
msgid "Purchase Orders are not allowed for {0} due to a scorecard standing of {1}."
msgstr ""
@@ -42118,7 +42177,7 @@ msgstr ""
#: erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py:39
#: erpnext/stock/dashboard/item_dashboard.js:248
#: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
-#: erpnext/stock/doctype/material_request/material_request.js:368
+#: erpnext/stock/doctype/material_request/material_request.js:369
#: erpnext/stock/doctype/material_request_item/material_request_item.json
#: erpnext/stock/doctype/packing_slip_item/packing_slip_item.json
#: erpnext/stock/doctype/pick_list_item/pick_list_item.json
@@ -42232,7 +42291,7 @@ msgstr ""
msgid "Quantity and Warehouse"
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.py:210
+#: erpnext/stock/doctype/material_request/material_request.py:249
msgid "Quantity cannot be greater than {0} for Item {1}"
msgstr ""
@@ -42272,7 +42331,7 @@ msgstr ""
msgid "Quantity to Manufacture"
msgstr ""
-#: erpnext/manufacturing/doctype/work_order/work_order.py:2904
+#: erpnext/manufacturing/doctype/work_order/work_order.py:2908
msgid "Quantity to Manufacture can not be zero for the operation {0}"
msgstr ""
@@ -42280,7 +42339,7 @@ msgstr ""
msgid "Quantity to Manufacture must be greater than 0."
msgstr ""
-#: erpnext/public/js/utils/barcode_scanner.js:257
+#: erpnext/public/js/utils/barcode_scanner.js:262
msgid "Quantity to Scan"
msgstr ""
@@ -42960,7 +43019,7 @@ msgstr ""
#: erpnext/manufacturing/doctype/work_order/work_order.js:779
#: erpnext/selling/doctype/sales_order/sales_order.js:974
#: erpnext/selling/doctype/sales_order/sales_order_list.js:70
-#: erpnext/stock/doctype/material_request/material_request.js:243
+#: erpnext/stock/doctype/material_request/material_request.js:246
#: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.js:116
#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:164
msgid "Re-open"
@@ -43148,7 +43207,7 @@ msgid "Receivable / Payable Account"
msgstr ""
#: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:79
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1155
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1157
#: erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py:241
#: erpnext/accounts/report/sales_register/sales_register.py:231
#: erpnext/accounts/report/sales_register/sales_register.py:285
@@ -43605,7 +43664,7 @@ msgstr ""
msgid "Reference #"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1039
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1040
msgid "Reference #{0} dated {1}"
msgstr ""
@@ -43647,7 +43706,7 @@ msgstr ""
msgid "Reference No"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:653
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:654
msgid "Reference No & Reference Date is required for {0}"
msgstr ""
@@ -43655,7 +43714,7 @@ msgstr ""
msgid "Reference No and Reference Date is mandatory for Bank transaction"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:658
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:659
msgid "Reference No is mandatory if you entered Reference Date"
msgstr ""
@@ -43915,7 +43974,7 @@ msgid "Remaining Amount"
msgstr ""
#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:189
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1232
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1234
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:178
msgid "Remaining Balance"
msgstr ""
@@ -43973,7 +44032,7 @@ msgstr ""
#: erpnext/accounts/print_format/payment_receipt_voucher/payment_receipt_voucher.html:11
#: erpnext/accounts/report/accounts_payable/accounts_payable.html:135
#: erpnext/accounts/report/accounts_receivable/accounts_receivable.html:136
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1264
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1266
#: erpnext/accounts/report/general_ledger/general_ledger.html:163
#: erpnext/accounts/report/general_ledger/general_ledger.py:818
#: erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py:112
@@ -44321,7 +44380,7 @@ msgstr ""
msgid "Reposting Vouchers Progress"
msgstr ""
-#: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:222
+#: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:227
#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:338
msgid "Reposting entries created: {0}"
msgstr ""
@@ -44429,7 +44488,7 @@ msgstr ""
#: erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js:70
#: erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py:272
#: erpnext/buying/workspace/buying/buying.json
-#: erpnext/stock/doctype/material_request/material_request.js:202
+#: erpnext/stock/doctype/material_request/material_request.js:205
#: erpnext/workspace_sidebar/buying.json
msgid "Request for Quotation"
msgstr ""
@@ -44695,7 +44754,7 @@ msgstr ""
msgid "Reserved"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1408
+#: erpnext/controllers/stock_controller.py:1405
msgid "Reserved Batch Conflict"
msgstr ""
@@ -44833,7 +44892,7 @@ msgstr ""
#: erpnext/public/js/stock_reservation.js:203
#: erpnext/selling/doctype/sales_order/sales_order.js:418
-#: erpnext/stock/doctype/pick_list/pick_list.js:306
+#: erpnext/stock/doctype/pick_list/pick_list.js:307
#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:293
msgid "Reserving Stock..."
msgstr ""
@@ -45004,7 +45063,7 @@ msgstr ""
msgid "Restart Subscription"
msgstr ""
-#: erpnext/assets/doctype/asset/asset.js:183
+#: erpnext/assets/doctype/asset/asset.js:191
msgid "Restore Asset"
msgstr ""
@@ -45300,6 +45359,10 @@ msgstr ""
msgid "Revaluation Surplus"
msgstr ""
+#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:624
+msgid "Revaluation journal for {0} has been created: {1}"
+msgstr ""
+
#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js:88
msgid "Revenue"
msgstr ""
@@ -45310,11 +45373,19 @@ msgstr ""
msgid "Revenue received in advance (e.g. annual subscription) is held here and recognized gradually over time"
msgstr ""
+#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js:39
+msgid "Reversal Journal Entries"
+msgstr ""
+
#. Label of the reversal_of (Link) field in DocType 'Journal Entry'
#: erpnext/accounts/doctype/journal_entry/journal_entry.json
msgid "Reversal Of"
msgstr ""
+#: erpnext/accounts/doctype/journal_entry/journal_entry_list.js:6
+msgid "Reversal Of Exchange Rate Revaluation"
+msgstr ""
+
#: erpnext/accounts/doctype/journal_entry/journal_entry.js:100
msgid "Reverse Journal Entry"
msgstr ""
@@ -45324,6 +45395,10 @@ msgstr ""
msgid "Reverse Sign"
msgstr ""
+#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js:118
+msgid "Reversing Journals..."
+msgstr ""
+
#. Label of the review (Link) field in DocType 'Quality Action'
#. Group in Quality Goal's connections
#. Label of the sb_00 (Section Break) field in DocType 'Quality Review'
@@ -45521,7 +45596,7 @@ msgstr ""
msgid "Root Type"
msgstr ""
-#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:402
+#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:403
msgid "Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity"
msgstr ""
@@ -45682,7 +45757,7 @@ msgstr ""
msgid "Rounding Loss Allowance"
msgstr ""
-#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js:45
+#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js:55
#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:48
msgid "Rounding Loss Allowance should be between 0 and 1"
msgstr ""
@@ -45795,7 +45870,7 @@ msgstr ""
msgid "Row #{0}: Asset {1} is already sold"
msgstr ""
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:336
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:337
msgid "Row #{0}: BOM is not specified for subcontracting item {0}"
msgstr ""
@@ -45938,7 +46013,7 @@ msgstr ""
msgid "Row #{0}: Dates overlapping with other row in group {1}"
msgstr ""
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:360
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:361
msgid "Row #{0}: Default BOM not found for FG Item {1}"
msgstr ""
@@ -45958,16 +46033,16 @@ msgstr ""
msgid "Row #{0}: Expense Account not set for the Item {1}. {2}"
msgstr ""
-#: erpnext/assets/doctype/asset_repair/asset_repair.py:146
+#: erpnext/assets/doctype/asset_repair/asset_repair.py:149
msgid "Row #{0}: Expense account {1} is not valid for Purchase Invoice {2}. Only expense accounts from non-stock items are allowed."
msgstr ""
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:365
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:366
#: erpnext/selling/doctype/sales_order/sales_order.py:305
msgid "Row #{0}: Finished Good Item Qty can not be zero"
msgstr ""
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:347
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:348
#: erpnext/selling/doctype/sales_order/sales_order.py:285
msgid "Row #{0}: Finished Good Item is not specified for service item {1}"
msgstr ""
@@ -45976,7 +46051,7 @@ msgstr ""
msgid "Row #{0}: Finished Good Item {1} cannot be added in the Secondary Items table."
msgstr ""
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:354
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:355
#: erpnext/selling/doctype/sales_order/sales_order.py:292
msgid "Row #{0}: Finished Good Item {1} must be a sub-contracted item"
msgstr ""
@@ -45994,11 +46069,11 @@ msgstr ""
msgid "Row #{0}: For Customer Provided Item {1}, Source Warehouse must be {2}"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:701
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:702
msgid "Row #{0}: For {1}, you can select reference document only if account gets credited"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:711
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:712
msgid "Row #{0}: For {1}, you can select reference document only if account gets debited"
msgstr ""
@@ -46014,7 +46089,7 @@ msgstr ""
msgid "Row #{0}: From Time and To Time fields are required"
msgstr ""
-#: erpnext/public/js/utils/barcode_scanner.js:427
+#: erpnext/public/js/utils/barcode_scanner.js:435
msgid "Row #{0}: Item added"
msgstr ""
@@ -46026,7 +46101,7 @@ msgstr ""
msgid "Row #{0}: Item {1} does not exist"
msgstr ""
-#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1628
+#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1630
msgid "Row #{0}: Item {1} has been picked, please reserve stock from the Pick List."
msgstr ""
@@ -46095,7 +46170,7 @@ msgstr ""
msgid "Row #{0}: Not allowed to change Supplier as Purchase Order already exists"
msgstr ""
-#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1711
+#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1713
msgid "Row #{0}: Only {1} available to reserve for the Item {2}"
msgstr ""
@@ -46141,7 +46216,7 @@ msgstr ""
msgid "Row #{0}: Process Loss Percentage should be less than 100% for {1} Item {2}"
msgstr ""
-#: erpnext/public/js/utils/barcode_scanner.js:425
+#: erpnext/public/js/utils/barcode_scanner.js:433
msgid "Row #{0}: Qty increased by {1}"
msgstr ""
@@ -46154,15 +46229,15 @@ msgstr ""
msgid "Row #{0}: Qty should be less than or equal to Available Qty to Reserve (Actual Qty - Reserved Qty) {1} for Iem {2} against Batch {3} in Warehouse {4}."
msgstr ""
-#: erpnext/controllers/stock_controller.py:1545
+#: erpnext/controllers/stock_controller.py:1543
msgid "Row #{0}: Quality Inspection is required for Item {1}"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1560
+#: erpnext/controllers/stock_controller.py:1558
msgid "Row #{0}: Quality Inspection {1} is not submitted for the item: {2}"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1575
+#: erpnext/controllers/stock_controller.py:1573
msgid "Row #{0}: Quality Inspection {1} was rejected for item {2}"
msgstr ""
@@ -46178,7 +46253,7 @@ msgstr ""
msgid "Row #{0}: Quantity of Item {1} cannot be more than {2} {3} against Subcontracting Inward Order {4}"
msgstr ""
-#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1696
+#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1698
msgid "Row #{0}: Quantity to reserve for the Item {1} should be greater than 0."
msgstr ""
@@ -46205,7 +46280,7 @@ msgstr ""
msgid "Row #{0}: Rejected Warehouse is mandatory for the rejected Item {1}"
msgstr ""
-#: erpnext/assets/doctype/asset_repair/asset_repair.py:164
+#: erpnext/assets/doctype/asset_repair/asset_repair.py:167
msgid "Row #{0}: Repair cost {1} exceeds available amount {2} for Purchase Invoice {3} and Account {4}"
msgstr ""
@@ -46301,7 +46376,7 @@ msgstr ""
msgid "Row #{0}: Status is mandatory"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:463
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:464
msgid "Row #{0}: Status must be {1} for Invoice Discounting {2}"
msgstr ""
@@ -46309,15 +46384,15 @@ msgstr ""
msgid "Row #{0}: Stock cannot be reserved for Item {1} against a disabled Batch {2}."
msgstr ""
-#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1641
+#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1643
msgid "Row #{0}: Stock cannot be reserved for a non-stock Item {1}"
msgstr ""
-#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1654
+#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1656
msgid "Row #{0}: Stock cannot be reserved in group warehouse {1}."
msgstr ""
-#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1668
+#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1670
msgid "Row #{0}: Stock is already reserved for the Item {1}."
msgstr ""
@@ -46330,7 +46405,7 @@ msgid "Row #{0}: Stock not available to reserve for Item {1} against Batch {2} i
msgstr ""
#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1234
-#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1682
+#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1684
msgid "Row #{0}: Stock not available to reserve for the Item {1} in Warehouse {2}."
msgstr ""
@@ -46531,7 +46606,7 @@ msgstr ""
msgid "Row {0}: Accepted Qty and Rejected Qty can't be zero at the same time."
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:616
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:617
msgid "Row {0}: Account {1} and Party Type {2} have different account types"
msgstr ""
@@ -46539,11 +46614,11 @@ msgstr ""
msgid "Row {0}: Activity Type is mandatory."
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:682
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:683
msgid "Row {0}: Advance against Customer must be credit"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:684
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:685
msgid "Row {0}: Advance against Supplier must be debit"
msgstr ""
@@ -46559,11 +46634,11 @@ msgstr ""
msgid "Row {0}: As {1} is enabled, raw materials cannot be added to {2} entry. Use {3} entry to consume raw materials."
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.py:869
+#: erpnext/stock/doctype/material_request/material_request.py:908
msgid "Row {0}: Bill of Materials not found for the Item {1}"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:935
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:936
msgid "Row {0}: Both Debit and Credit values cannot be zero"
msgstr ""
@@ -46583,7 +46658,7 @@ msgstr ""
msgid "Row {0}: Cost center is required for an item {1}"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:781
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:782
msgid "Row {0}: Credit entry can not be linked with a {1}"
msgstr ""
@@ -46591,7 +46666,7 @@ msgstr ""
msgid "Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2}"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:776
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:777
msgid "Row {0}: Debit entry can not be linked with a {1}"
msgstr ""
@@ -46611,7 +46686,7 @@ msgstr ""
msgid "Row {0}: Either Delivery Note Item or Packed Item reference is mandatory."
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1026
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1027
#: erpnext/controllers/taxes_and_totals.py:1377
msgid "Row {0}: Exchange Rate is mandatory"
msgstr ""
@@ -46653,7 +46728,7 @@ msgstr ""
msgid "Row {0}: From Time and To Time of {1} is overlapping with {2}"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1641
+#: erpnext/controllers/stock_controller.py:1639
msgid "Row {0}: From Warehouse is mandatory for internal transfers"
msgstr ""
@@ -46665,7 +46740,7 @@ msgstr ""
msgid "Row {0}: Hours value must be greater than zero."
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:801
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:802
msgid "Row {0}: Invalid reference {1}"
msgstr ""
@@ -46705,11 +46780,11 @@ msgstr ""
msgid "Row {0}: Packing Slip is already created for Item {1}."
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:827
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:828
msgid "Row {0}: Party / Account does not match with {1} / {2} in {3} {4}"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:605
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:606
msgid "Row {0}: Party Type and Party is required for Receivable / Payable account {1}"
msgstr ""
@@ -46717,11 +46792,11 @@ msgstr ""
msgid "Row {0}: Payment Term is mandatory"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:675
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:676
msgid "Row {0}: Payment against Sales/Purchase Order should always be marked as advance"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:668
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:669
msgid "Row {0}: Please check 'Is Advance' against Account {1} if this is an advance entry."
msgstr ""
@@ -46797,7 +46872,7 @@ msgstr ""
msgid "Row {0}: Subcontracted Item is mandatory for the raw material {1}"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1632
+#: erpnext/controllers/stock_controller.py:1630
msgid "Row {0}: Target Warehouse is mandatory for internal transfers"
msgstr ""
@@ -46805,7 +46880,7 @@ msgstr ""
msgid "Row {0}: Task {1} does not belong to Project {2}"
msgstr ""
-#: erpnext/assets/doctype/asset_repair/asset_repair.js:178
+#: erpnext/assets/doctype/asset_repair/asset_repair.js:187
msgid "Row {0}: The entire expense amount for account {1} in {2} has already been allocated."
msgstr ""
@@ -46862,7 +46937,7 @@ msgstr ""
msgid "Row {0}: {1} {2} cannot be same as {3} (Party Account) {4}"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:841
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:842
msgid "Row {0}: {1} {2} does not match with {3}"
msgstr ""
@@ -47434,7 +47509,7 @@ msgstr ""
#: erpnext/setup/doctype/authorization_rule/authorization_rule.json
#: erpnext/stock/doctype/delivery_note/delivery_note.js:157
#: erpnext/stock/doctype/delivery_note/delivery_note.js:223
-#: erpnext/stock/doctype/material_request/material_request.js:236
+#: erpnext/stock/doctype/material_request/material_request.js:239
#: erpnext/stock/doctype/material_request_item/material_request_item.json
#: erpnext/stock/doctype/pick_list_item/pick_list_item.json
#: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -47615,7 +47690,7 @@ msgstr ""
#: erpnext/accounts/doctype/promotional_scheme/promotional_scheme.json
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.json
#: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:130
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1253
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1255
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:114
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:194
#: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js:74
@@ -47721,7 +47796,7 @@ msgstr ""
#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:158
#: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:136
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1250
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1252
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:120
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:191
#: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js:80
@@ -48040,7 +48115,7 @@ msgstr ""
#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.json
#: erpnext/buying/doctype/purchase_order/purchase_order.json
-#: erpnext/public/js/utils/barcode_scanner.js:236
+#: erpnext/public/js/utils/barcode_scanner.js:241
#: erpnext/selling/doctype/quotation/quotation.json
#: erpnext/selling/doctype/sales_order/sales_order.json
#: erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -48072,7 +48147,7 @@ msgstr ""
msgid "Scan Serial No"
msgstr ""
-#: erpnext/public/js/utils/barcode_scanner.js:200
+#: erpnext/public/js/utils/barcode_scanner.js:205
msgid "Scan barcode for item {0}"
msgstr ""
@@ -48086,14 +48161,14 @@ msgstr ""
msgid "Scanned Cheque"
msgstr ""
-#: erpnext/public/js/utils/barcode_scanner.js:268
+#: erpnext/public/js/utils/barcode_scanner.js:273
msgid "Scanned Quantity"
msgstr ""
#. Label of the schedule_date (Date) field in DocType 'Depreciation Schedule'
#. Label of the schedule_date (Datetime) field in DocType 'Production Plan Sub
#. Assembly Item'
-#: erpnext/assets/doctype/asset/asset.js:383
+#: erpnext/assets/doctype/asset/asset.js:391
#: erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json
#: erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json
msgid "Schedule Date"
@@ -48224,7 +48299,7 @@ msgstr ""
msgid "Scrap"
msgstr ""
-#: erpnext/assets/doctype/asset/asset.js:168
+#: erpnext/assets/doctype/asset/asset.js:176
msgid "Scrap Asset"
msgstr ""
@@ -48417,9 +48492,9 @@ msgstr ""
msgid "Select BOM and Qty for Production"
msgstr ""
-#: erpnext/assets/doctype/asset_repair/asset_repair.js:234
-#: erpnext/public/js/utils/sales_common.js:443
-#: erpnext/stock/doctype/pick_list/pick_list.js:398
+#: erpnext/assets/doctype/asset_repair/asset_repair.js:243
+#: erpnext/public/js/utils/sales_common.js:441
+#: erpnext/stock/doctype/pick_list/pick_list.js:399
msgid "Select Batch No"
msgstr ""
@@ -48551,15 +48626,15 @@ msgstr ""
msgid "Select Quantity"
msgstr ""
-#: erpnext/assets/doctype/asset_repair/asset_repair.js:234
-#: erpnext/public/js/utils/sales_common.js:443
-#: erpnext/stock/doctype/pick_list/pick_list.js:398
+#: erpnext/assets/doctype/asset_repair/asset_repair.js:243
+#: erpnext/public/js/utils/sales_common.js:441
+#: erpnext/stock/doctype/pick_list/pick_list.js:399
msgid "Select Serial No"
msgstr ""
-#: erpnext/assets/doctype/asset_repair/asset_repair.js:237
-#: erpnext/public/js/utils/sales_common.js:446
-#: erpnext/stock/doctype/pick_list/pick_list.js:401
+#: erpnext/assets/doctype/asset_repair/asset_repair.js:246
+#: erpnext/public/js/utils/sales_common.js:444
+#: erpnext/stock/doctype/pick_list/pick_list.js:402
msgid "Select Serial and Batch"
msgstr ""
@@ -48597,7 +48672,7 @@ msgstr ""
msgid "Select Warehouse..."
msgstr ""
-#: erpnext/manufacturing/doctype/production_plan/production_plan.js:551
+#: erpnext/manufacturing/doctype/production_plan/production_plan.js:549
msgid "Select Warehouses to get Stock for Materials Planning"
msgstr ""
@@ -48649,6 +48724,7 @@ msgid "Select an Item Group."
msgstr ""
#: erpnext/accounts/report/general_ledger/general_ledger.py:36
+#: erpnext/accounts/report/general_ledger/general_ledger.py:839
msgid "Select an account to print in account currency"
msgstr ""
@@ -48727,7 +48803,7 @@ msgstr ""
msgid "Select the customer or supplier."
msgstr ""
-#: erpnext/assets/doctype/asset/asset.js:939
+#: erpnext/assets/doctype/asset/asset.js:947
msgid "Select the date"
msgstr ""
@@ -48753,7 +48829,7 @@ msgstr ""
msgid "Select variant item code for the template item {0}"
msgstr ""
-#: erpnext/manufacturing/doctype/production_plan/production_plan.js:708
+#: erpnext/manufacturing/doctype/production_plan/production_plan.js:706
msgid ""
"Select whether to get items from a Sales Order or a Material Request. For now select Sales Order.\n"
" A Production Plan can also be created manually where you can select the Items to manufacture."
@@ -48804,22 +48880,22 @@ msgstr ""
msgid "Self delivery"
msgstr ""
-#: erpnext/assets/doctype/asset/asset.js:646
+#: erpnext/assets/doctype/asset/asset.js:654
#: erpnext/stock/doctype/batch/batch_dashboard.py:9
#: erpnext/stock/doctype/item/item_dashboard.py:20
msgid "Sell"
msgstr ""
-#: erpnext/assets/doctype/asset/asset.js:176
-#: erpnext/assets/doctype/asset/asset.js:635
+#: erpnext/assets/doctype/asset/asset.js:184
+#: erpnext/assets/doctype/asset/asset.js:643
msgid "Sell Asset"
msgstr ""
-#: erpnext/assets/doctype/asset/asset.js:640
+#: erpnext/assets/doctype/asset/asset.js:648
msgid "Sell Qty"
msgstr ""
-#: erpnext/assets/doctype/asset/asset.js:656
+#: erpnext/assets/doctype/asset/asset.js:664
msgid "Sell quantity cannot exceed the asset quantity"
msgstr ""
@@ -48827,7 +48903,7 @@ msgstr ""
msgid "Sell quantity cannot exceed the asset quantity. Asset {0} has only {1} item(s)."
msgstr ""
-#: erpnext/assets/doctype/asset/asset.js:652
+#: erpnext/assets/doctype/asset/asset.js:660
msgid "Sell quantity must be greater than zero"
msgstr ""
@@ -49142,7 +49218,7 @@ msgstr ""
msgid "Serial No Range"
msgstr ""
-#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2735
+#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2737
msgid "Serial No Reserved"
msgstr ""
@@ -49211,7 +49287,7 @@ msgstr ""
msgid "Serial No {0} already exists"
msgstr ""
-#: erpnext/public/js/utils/barcode_scanner.js:342
+#: erpnext/public/js/utils/barcode_scanner.js:347
msgid "Serial No {0} already scanned"
msgstr ""
@@ -49228,7 +49304,7 @@ msgstr ""
msgid "Serial No {0} does not exist"
msgstr ""
-#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3524
+#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3526
msgid "Serial No {0} does not exists"
msgstr ""
@@ -49236,7 +49312,7 @@ msgstr ""
msgid "Serial No {0} is already Delivered. You cannot use them again in Manufacture / Repack entry."
msgstr ""
-#: erpnext/public/js/utils/barcode_scanner.js:435
+#: erpnext/public/js/utils/barcode_scanner.js:443
msgid "Serial No {0} is already added"
msgstr ""
@@ -49264,7 +49340,7 @@ msgstr ""
msgid "Serial No: {0} has already been transacted into another POS Invoice."
msgstr ""
-#: erpnext/public/js/utils/barcode_scanner.js:292
+#: erpnext/public/js/utils/barcode_scanner.js:297
#: erpnext/public/js/utils/serial_no_batch_selector.js:16
#: erpnext/public/js/utils/serial_no_batch_selector.js:201
#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js:50
@@ -49452,7 +49528,7 @@ msgstr ""
msgid "Series for Asset Depreciation Entry (Journal Entry)"
msgstr ""
-#: erpnext/buying/doctype/supplier/supplier.py:143
+#: erpnext/buying/doctype/supplier/supplier.py:147
msgid "Series is mandatory"
msgstr ""
@@ -49788,7 +49864,7 @@ msgstr ""
#. Label of the set_warehouse (Link) field in DocType 'Sales Order'
#. Label of the set_warehouse (Link) field in DocType 'Delivery Note'
#. Label of the set_from_warehouse (Link) field in DocType 'Material Request'
-#: erpnext/public/js/utils/sales_common.js:568
+#: erpnext/public/js/utils/sales_common.js:566
#: erpnext/selling/doctype/sales_order/sales_order.json
#: erpnext/stock/doctype/delivery_note/delivery_note.json
#: erpnext/stock/doctype/material_request/material_request.json
@@ -49806,7 +49882,7 @@ msgstr ""
#. Label of the set_warehouse (Link) field in DocType 'Subcontracting Order'
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.json
#: erpnext/buying/doctype/purchase_order/purchase_order.json
-#: erpnext/public/js/utils/sales_common.js:565
+#: erpnext/public/js/utils/sales_common.js:563
#: erpnext/stock/doctype/delivery_note/delivery_note.json
#: erpnext/stock/doctype/material_request/material_request.json
#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
@@ -49832,7 +49908,7 @@ msgstr ""
msgid "Set as Completed"
msgstr ""
-#: erpnext/public/js/utils/sales_common.js:592
+#: erpnext/public/js/utils/sales_common.js:590
#: erpnext/selling/doctype/quotation/quotation.js:146
msgid "Set as Lost"
msgstr ""
@@ -50146,7 +50222,7 @@ msgid "Shelf Life in Days"
msgstr ""
#. Label of the shift (Link) field in DocType 'Depreciation Schedule'
-#: erpnext/assets/doctype/asset/asset.js:396
+#: erpnext/assets/doctype/asset/asset.js:404
#: erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json
msgid "Shift"
msgstr ""
@@ -50956,7 +51032,7 @@ msgstr ""
#: erpnext/manufacturing/doctype/work_order_item/work_order_item.json
#: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
#: erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py:126
-#: erpnext/public/js/utils/sales_common.js:564
+#: erpnext/public/js/utils/sales_common.js:562
#: erpnext/selling/doctype/sales_order_item/sales_order_item.json
#: erpnext/stock/dashboard/item_dashboard.js:227
#: erpnext/stock/doctype/material_request_item/material_request_item.json
@@ -51050,15 +51126,15 @@ msgstr ""
msgid "Spent"
msgstr ""
-#: erpnext/assets/doctype/asset/asset.js:696
+#: erpnext/assets/doctype/asset/asset.js:704
#: erpnext/stock/doctype/batch/batch.js:104
#: erpnext/stock/doctype/batch/batch.js:185
#: erpnext/support/doctype/issue/issue.js:114
msgid "Split"
msgstr ""
-#: erpnext/assets/doctype/asset/asset.js:152
-#: erpnext/assets/doctype/asset/asset.js:680
+#: erpnext/assets/doctype/asset/asset.js:160
+#: erpnext/assets/doctype/asset/asset.js:688
msgid "Split Asset"
msgstr ""
@@ -51082,7 +51158,7 @@ msgstr ""
msgid "Split Issue"
msgstr ""
-#: erpnext/assets/doctype/asset/asset.js:686
+#: erpnext/assets/doctype/asset/asset.js:694
msgid "Split Qty"
msgstr ""
@@ -51367,7 +51443,7 @@ msgstr ""
msgid "Status and Reference"
msgstr ""
-#: erpnext/projects/doctype/project/project.py:717
+#: erpnext/projects/doctype/project/project.py:747
msgid "Status must be Cancelled or Completed"
msgstr ""
@@ -51580,7 +51656,7 @@ msgstr ""
msgid "Stock Entry {0} has created"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1324
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1325
msgid "Stock Entry {0} is not submitted"
msgstr ""
@@ -51628,7 +51704,7 @@ msgid "Stock Ledger Entry"
msgstr ""
#: erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py:98
-#: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:139
+#: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:144
msgid "Stock Ledger ID"
msgstr ""
@@ -51830,12 +51906,12 @@ msgstr ""
#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:746
#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:674
#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1237
-#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1644
-#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1657
-#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1671
-#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1685
-#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1699
-#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1716
+#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1646
+#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1659
+#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1673
+#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1687
+#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1701
+#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1718
#: erpnext/stock/doctype/stock_settings/stock_settings.json
#: erpnext/stock/doctype/stock_settings/stock_settings.py:217
#: erpnext/stock/doctype/stock_settings/stock_settings.py:229
@@ -51848,14 +51924,14 @@ msgstr ""
msgid "Stock Reservation"
msgstr ""
-#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1825
+#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1827
msgid "Stock Reservation Entries Cancelled"
msgstr ""
#: erpnext/controllers/subcontracting_inward_controller.py:1037
-#: erpnext/manufacturing/doctype/production_plan/production_plan.py:2245
-#: erpnext/manufacturing/doctype/work_order/work_order.py:2412
-#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1777
+#: erpnext/manufacturing/doctype/production_plan/production_plan.py:2254
+#: erpnext/manufacturing/doctype/work_order/work_order.py:2416
+#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1779
msgid "Stock Reservation Entries Created"
msgstr ""
@@ -52129,7 +52205,7 @@ msgstr ""
#: erpnext/stock/doctype/bin/bin.json
#: erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.json
#: erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py:134
-#: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:160
+#: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:165
msgid "Stock Value"
msgstr ""
@@ -52154,11 +52230,15 @@ msgstr ""
msgid "Stock and Manufacturing"
msgstr ""
+#: erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py:303
+msgid "Stock and accounting values could not be reconciled by reposting for {0}."
+msgstr ""
+
#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:255
msgid "Stock cannot be reserved in group warehouse {0}."
msgstr ""
-#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1589
+#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1591
msgid "Stock cannot be reserved in the group warehouse {0}."
msgstr ""
@@ -52378,7 +52458,7 @@ msgstr ""
msgid "Subcontracted Item To Be Received"
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.js:224
+#: erpnext/stock/doctype/material_request/material_request.js:227
msgid "Subcontracted Purchase Order"
msgstr ""
@@ -52563,7 +52643,7 @@ msgstr ""
msgid "Subcontracting Order Supplied Item"
msgstr ""
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:976
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:977
msgid "Subcontracting Order {0} created."
msgstr ""
@@ -52656,7 +52736,7 @@ msgstr ""
msgid "Subdivision"
msgstr ""
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:972
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:973
#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:1092
msgid "Submit Action Failed"
msgstr ""
@@ -53132,7 +53212,7 @@ msgstr ""
#: erpnext/accounts/doctype/tax_rule/tax_rule.json
#: erpnext/accounts/report/accounts_payable/accounts_payable.js:119
#: erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js:102
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1257
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1259
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:198
#: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py:178
#: erpnext/accounts/report/purchase_register/purchase_register.js:27
@@ -53233,7 +53313,7 @@ msgstr ""
#. Label of the supplier_name (Data) field in DocType 'Purchase Receipt'
#. Label of the supplier_name (Data) field in DocType 'Stock Entry'
#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1172
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1174
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:156
#: erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py:196
#: erpnext/accounts/report/purchase_register/purchase_register.py:193
@@ -53316,7 +53396,7 @@ msgstr ""
#: erpnext/buying/workspace/buying/buying.json
#: erpnext/crm/doctype/opportunity/opportunity.js:81
#: erpnext/selling/doctype/quotation/quotation.json
-#: erpnext/stock/doctype/material_request/material_request.js:208
+#: erpnext/stock/doctype/material_request/material_request.js:211
#: erpnext/workspace_sidebar/buying.json
msgid "Supplier Quotation"
msgstr ""
@@ -54474,7 +54554,7 @@ msgstr ""
msgid "Template Item"
msgstr ""
-#: erpnext/stock/get_item_details.py:342
+#: erpnext/stock/get_item_details.py:341
msgid "Template Item Selected"
msgstr ""
@@ -54686,7 +54766,7 @@ msgstr ""
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.json
#: erpnext/accounts/doctype/territory_item/territory_item.json
#: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:142
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1241
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1243
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:108
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:182
#: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js:68
@@ -54851,7 +54931,7 @@ msgstr ""
msgid "The Serial No at Row #{0}: {1} is not available in warehouse {2}."
msgstr ""
-#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2732
+#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2734
msgid "The Serial No {0} is reserved against the {1} {2} and cannot be used for any other transaction."
msgstr ""
@@ -54891,8 +54971,8 @@ msgstr ""
msgid "The bank account is not a company account. Please select a company account"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1397
-msgid "The batch {0} is already reserved in {1} {2}. So, cannot proceed with the {3} {4}, which is created against the {5} {6}."
+#: erpnext/controllers/stock_controller.py:1396
+msgid "The batch {0} is reserved for {1} in the warehouse {2} and the remaining quantity is not enough to cover the reservations. So, cannot proceed with the {3} {4}."
msgstr ""
#: erpnext/regional/report/vat_audit_report/vat_audit_report.py:43
@@ -54985,7 +55065,7 @@ msgstr ""
msgid "The following Items, having Putaway Rules, could not be accomodated:"
msgstr ""
-#: erpnext/assets/doctype/asset_repair/asset_repair.py:138
+#: erpnext/assets/doctype/asset_repair/asset_repair.py:141
msgid "The following Purchase Invoices are not submitted:"
msgstr ""
@@ -55019,11 +55099,11 @@ msgid ""
"{0}"
msgstr ""
-#: erpnext/assets/doctype/asset_repair/asset_repair.py:112
+#: erpnext/assets/doctype/asset_repair/asset_repair.py:115
msgid "The following rows are duplicates:"
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.py:879
+#: erpnext/stock/doctype/material_request/material_request.py:918
msgid "The following {0} were created: {1}"
msgstr ""
@@ -55070,7 +55150,7 @@ msgstr ""
msgid "The last account row must not have any debit or credit amounts set."
msgstr ""
-#: erpnext/public/js/utils/barcode_scanner.js:533
+#: erpnext/public/js/utils/barcode_scanner.js:542
msgid "The last scanned warehouse has been cleared and won't be set in the subsequently scanned items"
msgstr ""
@@ -55112,7 +55192,7 @@ msgstr ""
msgid "The outstanding amount {0} in {1} is lesser than {2}. Updating the outstanding to this invoice."
msgstr ""
-#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:232
+#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:233
msgid "The parent account {0} does not exists in the uploaded template"
msgstr ""
@@ -55183,7 +55263,7 @@ msgstr ""
msgid "The selected item cannot have Batch"
msgstr ""
-#: erpnext/assets/doctype/asset/asset.js:661
+#: erpnext/assets/doctype/asset/asset.js:669
msgid "The sell quantity is less than the total asset quantity. The remaining quantity will be split into a new asset. This action cannot be undone.
Do you want to continue?"
msgstr ""
@@ -55246,11 +55326,11 @@ msgstr ""
msgid "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage"
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.py:349
+#: erpnext/stock/doctype/material_request/material_request.py:388
msgid "The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than allowed requested quantity {2} for Item {3}"
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.py:356
+#: erpnext/stock/doctype/material_request/material_request.py:395
msgid "The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than requested quantity {2} for Item {3}"
msgstr ""
@@ -55290,6 +55370,10 @@ msgstr ""
msgid "The value {0} is already assigned to an existing Item {1}."
msgstr ""
+#: erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py:307
+msgid "The warehouse account(s) below are not of type 'Stock'. Please set a correct Stock asset account on the warehouse (Account Type must be 'Stock'):"
+msgstr ""
+
#: erpnext/manufacturing/doctype/work_order/work_order.js:1269
msgid "The warehouse where you store finished Items before they are shipped."
msgstr ""
@@ -55318,7 +55402,7 @@ msgstr ""
msgid "The {0} prefix '{1}' already exists. Please change the Serial No Series, otherwise you will get a Duplicate Entry error."
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.py:885
+#: erpnext/stock/doctype/material_request/material_request.py:924
msgid "The {0} {1} created successfully"
msgstr ""
@@ -55479,7 +55563,7 @@ msgstr ""
msgid "This Payment Entry is reconciled with {0}. Cancelling will automatically unreconcile it. Do you want to proceed?"
msgstr ""
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:985
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:986
msgid "This Purchase Order has been fully subcontracted."
msgstr ""
@@ -55705,7 +55789,7 @@ msgstr ""
msgid "This schedule was created when Asset {0} was consumed through Asset Capitalization {1}."
msgstr ""
-#: erpnext/assets/doctype/asset_repair/asset_repair.py:435
+#: erpnext/assets/doctype/asset_repair/asset_repair.py:438
msgid "This schedule was created when Asset {0} was repaired through Asset Repair {1}."
msgstr ""
@@ -56265,7 +56349,7 @@ msgstr ""
msgid "To add Operations tick the 'With Operations' checkbox."
msgstr ""
-#: erpnext/manufacturing/doctype/production_plan/production_plan.js:741
+#: erpnext/manufacturing/doctype/production_plan/production_plan.js:739
msgid "To add subcontracted Item's raw materials if include exploded items is disabled."
msgstr ""
@@ -56303,7 +56387,7 @@ msgstr ""
msgid "To enable Capital Work in Progress Accounting,"
msgstr ""
-#: erpnext/manufacturing/doctype/production_plan/production_plan.js:734
+#: erpnext/manufacturing/doctype/production_plan/production_plan.js:732
msgid "To include non-stock items in the material request planning. i.e. Items for which 'Maintain Stock' checkbox is unticked."
msgstr ""
@@ -56354,7 +56438,9 @@ msgstr ""
#: erpnext/accounts/doctype/financial_report_template/financial_report_engine.py:749
#: erpnext/accounts/report/financial_statements.py:621
#: erpnext/accounts/report/general_ledger/general_ledger.py:318
+#: erpnext/accounts/report/general_ledger/general_ledger.py:1071
#: erpnext/accounts/report/trial_balance/trial_balance.py:310
+#: erpnext/accounts/report/trial_balance/trial_balance.py:640
msgid "To use a different finance book, please uncheck 'Include Default FB Entries'"
msgstr ""
@@ -56453,8 +56539,8 @@ msgstr ""
msgid "Total (Company Currency)"
msgstr ""
-#: erpnext/accounts/report/balance_sheet/balance_sheet.py:127
-#: erpnext/accounts/report/balance_sheet/balance_sheet.py:128
+#: erpnext/accounts/report/balance_sheet/balance_sheet.py:136
+#: erpnext/accounts/report/balance_sheet/balance_sheet.py:137
msgid "Total (Credit)"
msgstr ""
@@ -56563,7 +56649,7 @@ msgstr ""
msgid "Total Applicable Charges in Purchase Receipt Items table must be same as Total Taxes and Charges"
msgstr ""
-#: erpnext/accounts/report/balance_sheet/balance_sheet.py:217
+#: erpnext/accounts/report/balance_sheet/balance_sheet.py:226
msgid "Total Asset"
msgstr ""
@@ -56572,10 +56658,6 @@ msgstr ""
msgid "Total Asset Cost"
msgstr ""
-#: erpnext/assets/dashboard_fixtures.py:158
-msgid "Total Assets"
-msgstr ""
-
#. Label of the total_billable_amount (Currency) field in DocType 'Timesheet'
#: erpnext/projects/doctype/timesheet/timesheet.json
msgid "Total Billable Amount"
@@ -56726,7 +56808,7 @@ msgstr ""
msgid "Total Debit Transactions"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:941
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:942
msgid "Total Debit must be equal to Total Credit. The difference is {0}"
msgstr ""
@@ -56745,7 +56827,7 @@ msgstr ""
msgid "Total Demand (Past Data)"
msgstr ""
-#: erpnext/accounts/report/balance_sheet/balance_sheet.py:224
+#: erpnext/accounts/report/balance_sheet/balance_sheet.py:233
msgid "Total Equity"
msgstr ""
@@ -56754,11 +56836,11 @@ msgstr ""
msgid "Total Estimated Distance"
msgstr ""
-#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:123
+#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:131
msgid "Total Expense"
msgstr ""
-#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:119
+#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:127
msgid "Total Expense This Year"
msgstr ""
@@ -56796,11 +56878,11 @@ msgstr ""
msgid "Total Holidays"
msgstr ""
-#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:122
+#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:130
msgid "Total Income"
msgstr ""
-#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:118
+#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:126
msgid "Total Income This Year"
msgstr ""
@@ -56843,7 +56925,7 @@ msgstr ""
msgid "Total Ledgers"
msgstr ""
-#: erpnext/accounts/report/balance_sheet/balance_sheet.py:220
+#: erpnext/accounts/report/balance_sheet/balance_sheet.py:229
msgid "Total Liability"
msgstr ""
@@ -57253,7 +57335,7 @@ msgstr ""
msgid "Total allocated percentage for sales team should be 100"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:195
+#: erpnext/selling/doctype/customer/customer.py:198
msgid "Total contribution percentage should be equal to 100"
msgstr ""
@@ -57633,7 +57715,7 @@ msgstr ""
msgid "Transfer Account"
msgstr ""
-#: erpnext/assets/doctype/asset/asset.js:160
+#: erpnext/assets/doctype/asset/asset.js:168
msgid "Transfer Asset"
msgstr ""
@@ -57643,7 +57725,7 @@ msgstr ""
msgid "Transfer Extra Raw Materials to WIP (%)"
msgstr ""
-#: erpnext/manufacturing/doctype/production_plan/production_plan.js:458
+#: erpnext/manufacturing/doctype/production_plan/production_plan.js:456
msgid "Transfer From Warehouses"
msgstr ""
@@ -57659,7 +57741,7 @@ msgstr ""
msgid "Transfer Materials"
msgstr ""
-#: erpnext/manufacturing/doctype/production_plan/production_plan.js:453
+#: erpnext/manufacturing/doctype/production_plan/production_plan.js:451
msgid "Transfer Materials For Warehouse {0}"
msgstr ""
@@ -57832,6 +57914,10 @@ msgstr ""
msgid "Trial Balance for Party"
msgstr ""
+#: erpnext/accounts/report/trial_balance/trial_balance.py:585
+msgid "Trial Balance requires {0} to be synced to DuckDB"
+msgstr ""
+
#. Label of the trial_period_end (Date) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Trial Period End Date"
@@ -58259,8 +58345,10 @@ msgstr ""
msgid "Unblock Invoice"
msgstr ""
-#: erpnext/accounts/report/balance_sheet/balance_sheet.py:84
-#: erpnext/accounts/report/balance_sheet/balance_sheet.py:85
+#: erpnext/accounts/report/balance_sheet/balance_sheet.py:93
+#: erpnext/accounts/report/balance_sheet/balance_sheet.py:94
+#: erpnext/accounts/report/balance_sheet/balance_sheet.py:316
+#: erpnext/accounts/report/balance_sheet/balance_sheet.py:317
#: erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py:90
#: erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py:91
msgid "Unclosed Fiscal Years Profit / Loss (Credit)"
@@ -58514,7 +58602,7 @@ msgstr ""
#: erpnext/public/js/stock_reservation.js:281
#: erpnext/selling/doctype/sales_order/sales_order.js:522
-#: erpnext/stock/doctype/pick_list/pick_list.js:321
+#: erpnext/stock/doctype/pick_list/pick_list.js:322
#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:390
msgid "Unreserving Stock..."
msgstr ""
@@ -58985,7 +59073,7 @@ msgstr ""
msgid "Use Transaction Date Exchange Rate"
msgstr ""
-#: erpnext/projects/doctype/project/project.py:568
+#: erpnext/projects/doctype/project/project.py:598
msgid "Use a name that is different from previous project name"
msgstr ""
@@ -59991,7 +60079,7 @@ msgstr ""
#: erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json
#: erpnext/accounts/doctype/repost_payment_ledger_items/repost_payment_ledger_items.json
#: erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1196
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1198
#: erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js:56
#: erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py:221
#: erpnext/accounts/report/general_ledger/general_ledger.js:49
@@ -60017,7 +60105,7 @@ msgstr ""
#: erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.js:51
#: erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py:112
#: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:34
-#: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:154
+#: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:159
#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:176
#: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:74
msgid "Voucher No"
@@ -60065,7 +60153,7 @@ msgstr ""
#: erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json
#: erpnext/accounts/doctype/repost_payment_ledger_items/repost_payment_ledger_items.json
#: erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1194
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1196
#: erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py:212
#: erpnext/accounts/report/general_ledger/general_ledger.py:760
#: erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py:31
@@ -60091,7 +60179,7 @@ msgstr ""
#: erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py:107
#: erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py:486
#: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:28
-#: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:152
+#: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:157
#: erpnext/stock/report/stock_ledger/stock_ledger.py:402
#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:170
#: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:68
@@ -60318,7 +60406,7 @@ msgstr ""
#. Label of the warehouses (Table MultiSelect) field in DocType 'Production
#. Plan'
-#: erpnext/manufacturing/doctype/production_plan/production_plan.js:526
+#: erpnext/manufacturing/doctype/production_plan/production_plan.js:524
#: erpnext/manufacturing/doctype/production_plan/production_plan.json
#: erpnext/stock/report/stock_balance/stock_balance.js:76
#: erpnext/stock/report/stock_ledger/stock_ledger.js:30
@@ -60433,11 +60521,11 @@ msgstr ""
msgid "Warning: Account changed for warehouse"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1330
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1331
msgid "Warning: Another {0} # {1} exists against stock entry {2}"
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.js:534
+#: erpnext/stock/doctype/material_request/material_request.js:535
msgid "Warning: Material Requested Qty is less than Minimum Order Qty"
msgstr ""
@@ -60951,9 +61039,9 @@ msgstr ""
#: erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py:104
#: erpnext/manufacturing/workspace/manufacturing/manufacturing.json
#: erpnext/selling/doctype/sales_order/sales_order.js:1056
-#: erpnext/stock/doctype/material_request/material_request.js:216
+#: erpnext/stock/doctype/material_request/material_request.js:219
#: erpnext/stock/doctype/material_request/material_request.json
-#: erpnext/stock/doctype/material_request/material_request.py:886
+#: erpnext/stock/doctype/material_request/material_request.py:925
#: erpnext/stock/doctype/pick_list/pick_list.json
#: erpnext/stock/doctype/serial_no/serial_no.json
#: erpnext/stock/doctype/stock_entry/stock_entry.json
@@ -61033,7 +61121,7 @@ msgstr ""
msgid "Work Order Summary Report"
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.py:892
+#: erpnext/stock/doctype/material_request/material_request.py:931
msgid "Work Order cannot be created for following reason:
{0}"
msgstr ""
@@ -61041,8 +61129,8 @@ msgstr ""
msgid "Work Order cannot be raised against a Item Template"
msgstr ""
-#: erpnext/manufacturing/doctype/work_order/work_order.py:2768
-#: erpnext/manufacturing/doctype/work_order/work_order.py:2848
+#: erpnext/manufacturing/doctype/work_order/work_order.py:2772
+#: erpnext/manufacturing/doctype/work_order/work_order.py:2852
msgid "Work Order has been {0}"
msgstr ""
@@ -61063,7 +61151,7 @@ msgid "Work Order {0}: Job Card not found for the operation {1}"
msgstr ""
#: erpnext/manufacturing/report/job_card_summary/job_card_summary.js:56
-#: erpnext/stock/doctype/material_request/material_request.py:880
+#: erpnext/stock/doctype/material_request/material_request.py:919
msgid "Work Orders"
msgstr ""
@@ -61426,7 +61514,7 @@ msgstr ""
msgid "You can change the parent account to a Balance Sheet account or select a different account."
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:717
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:718
msgid "You can not enter current voucher in 'Against Journal Entry' column"
msgstr ""
@@ -61491,7 +61579,7 @@ msgstr ""
msgid "You cannot create/amend any accounting entries till this date."
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:950
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:951
msgid "You cannot credit and debit same account at the same time"
msgstr ""
@@ -61568,7 +61656,7 @@ msgstr ""
msgid "You don't have permission to update Company details. Please contact your System Manager."
msgstr ""
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:590
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:591
msgid "You don't have permission to update Received Qty DocField for item {0}"
msgstr ""
@@ -61584,7 +61672,7 @@ msgstr ""
msgid "You have already selected items from {0} {1}"
msgstr ""
-#: erpnext/projects/doctype/project/project.py:363
+#: erpnext/projects/doctype/project/project.py:365
msgid "You have been invited to collaborate on the project {0}."
msgstr ""
@@ -61742,7 +61830,7 @@ msgstr ""
msgid "by {}"
msgstr ""
-#: erpnext/public/js/utils/sales_common.js:336
+#: erpnext/public/js/utils/sales_common.js:334
msgid "cannot be greater than 100"
msgstr ""
@@ -61993,7 +62081,7 @@ msgstr ""
msgid "to"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3246
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3238
msgid "to unallocate the amount of this Return Invoice before cancelling it."
msgstr ""
@@ -62020,7 +62108,7 @@ msgstr ""
msgid "unique e.g. SAVE20 To be used to get discount"
msgstr ""
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:620
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:621
msgid "updated delivered quantity for item {0} to {1}"
msgstr ""
@@ -62082,7 +62170,7 @@ msgstr ""
msgid "{0} Digest"
msgstr ""
-#: erpnext/accounts/utils.py:1570
+#: erpnext/accounts/utils.py:1564
msgid "{0} Number {1} is already used in {2} {3}"
msgstr ""
@@ -62094,7 +62182,7 @@ msgstr ""
msgid "{0} Operations: {1}"
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.py:228
+#: erpnext/stock/doctype/material_request/material_request.py:267
msgid "{0} Request for {1}"
msgstr ""
@@ -62118,19 +62206,19 @@ msgstr ""
msgid "{0} account not found while submitting purchase receipt"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1070
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1071
msgid "{0} against Bill {1} dated {2}"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1079
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1080
msgid "{0} against Purchase Order {1}"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1046
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1047
msgid "{0} against Sales Invoice {1}"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1053
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1054
msgid "{0} against Sales Order {1}"
msgstr ""
@@ -62182,7 +62270,7 @@ msgstr ""
msgid "{0} currency must be same as company's default currency. Please select another account."
msgstr ""
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:297
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:298
msgid "{0} currently has a {1} Supplier Scorecard standing, and Purchase Orders to this supplier should be issued with caution."
msgstr ""
@@ -62198,6 +62286,14 @@ msgstr ""
msgid "{0} does not belong to the Company {1}."
msgstr ""
+#: erpnext/accounts/doctype/dunning_type/dunning_type.py:100
+msgid "{0} doesn't belong to Company {1}. Please select a Cost Center that belongs to Company {1}."
+msgstr ""
+
+#: erpnext/accounts/doctype/dunning_type/dunning_type.py:57
+msgid "{0} doesn't belong to Company {1}. Please select an Income Account that belongs to Company {1}."
+msgstr ""
+
#: erpnext/accounts/doctype/item_tax_template/item_tax_template.py:74
msgid "{0} entered twice in Item Tax"
msgstr ""
@@ -62236,6 +62332,14 @@ msgstr ""
msgid "{0} is a child table and will be deleted automatically with its parent"
msgstr ""
+#: erpnext/accounts/doctype/dunning_type/dunning_type.py:114
+msgid "{0} is a group Cost Center. Please select a non-group Cost Center."
+msgstr ""
+
+#: erpnext/accounts/doctype/dunning_type/dunning_type.py:78
+msgid "{0} is a group account. Please select a non-group Income Account."
+msgstr ""
+
#: erpnext/accounts/doctype/pos_profile/pos_profile.py:94
msgid "{0} is a mandatory Accounting Dimension.
Please set a value for {0} in Accounting Dimensions section."
msgstr ""
@@ -62254,6 +62358,14 @@ msgstr ""
msgid "{0} is blocked so this transaction cannot proceed"
msgstr ""
+#: erpnext/accounts/doctype/dunning_type/dunning_type.py:64
+msgid "{0} is disabled. Please select a valid Income Account."
+msgstr ""
+
+#: erpnext/accounts/doctype/dunning_type/dunning_type.py:107
+msgid "{0} is disabled. Please select an enabled Cost Center."
+msgstr ""
+
#: erpnext/assets/doctype/asset/asset.py:509
msgid "{0} is in Draft. Submit it before creating the Asset."
msgstr ""
@@ -62279,7 +62391,7 @@ msgstr ""
msgid "{0} is not a CSV file."
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:237
+#: erpnext/selling/doctype/customer/customer.py:240
msgid "{0} is not a company bank account"
msgstr ""
@@ -62307,6 +62419,10 @@ msgstr ""
msgid "{0} is not added in the table"
msgstr ""
+#: erpnext/accounts/doctype/dunning_type/dunning_type.py:71
+msgid "{0} is not an Income Account. Please select a valid Income Account."
+msgstr ""
+
#: erpnext/support/doctype/service_level_agreement/service_level_agreement.py:146
msgid "{0} is not enabled in {1}"
msgstr ""
@@ -62315,11 +62431,11 @@ msgstr ""
msgid "{0} is not running. Cannot trigger events for this Document"
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.py:652
+#: erpnext/stock/doctype/material_request/material_request.py:691
msgid "{0} is not the default supplier for any items."
msgstr ""
-#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2975
+#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2973
msgid "{0} is on hold till {1}"
msgstr ""
@@ -62351,6 +62467,10 @@ msgstr ""
msgid "{0} items to return"
msgstr ""
+#: erpnext/accounts/doctype/dunning_type/dunning_type.py:144
+msgid "{0} languages are marked as default languages. Please select only one of them."
+msgstr ""
+
#: erpnext/controllers/sales_and_purchase_return.py:218
msgid "{0} must be negative in return document"
msgstr ""
@@ -62371,7 +62491,7 @@ msgstr ""
msgid "{0} payment entries can not be filtered by {1}"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1819
+#: erpnext/controllers/stock_controller.py:1817
msgid "{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}."
msgstr ""
@@ -62433,7 +62553,7 @@ msgstr ""
msgid "{0} will be given as discount."
msgstr ""
-#: erpnext/public/js/utils/barcode_scanner.js:523
+#: erpnext/public/js/utils/barcode_scanner.js:532
msgid "{0} will be set as the {1} in subsequently scanned items"
msgstr ""
@@ -62475,13 +62595,13 @@ msgstr ""
msgid "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
msgstr ""
-#: erpnext/buying/doctype/purchase_order/purchase_order.py:425
+#: erpnext/buying/doctype/purchase_order/purchase_order.py:426
#: erpnext/selling/doctype/sales_order/sales_order.py:600
-#: erpnext/stock/doctype/material_request/material_request.py:255
+#: erpnext/stock/doctype/material_request/material_request.py:294
msgid "{0} {1} has been modified. Please refresh."
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.py:282
+#: erpnext/stock/doctype/material_request/material_request.py:321
msgid "{0} {1} has not been submitted so the action cannot be completed"
msgstr ""
@@ -62502,15 +62622,15 @@ msgstr ""
msgid "{0} {1} is cancelled or closed"
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.py:434
+#: erpnext/stock/doctype/material_request/material_request.py:473
msgid "{0} {1} is cancelled or stopped"
msgstr ""
-#: erpnext/stock/doctype/material_request/material_request.py:272
+#: erpnext/stock/doctype/material_request/material_request.py:311
msgid "{0} {1} is cancelled so the action cannot be completed"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:865
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:866
msgid "{0} {1} is closed"
msgstr ""
@@ -62522,7 +62642,7 @@ msgstr ""
msgid "{0} {1} is frozen"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:862
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:863
msgid "{0} {1} is fully billed"
msgstr ""
@@ -62538,8 +62658,8 @@ msgstr ""
msgid "{0} {1} is not in any active Fiscal Year"
msgstr ""
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:859
-#: erpnext/accounts/doctype/journal_entry/journal_entry.py:898
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:860
+#: erpnext/accounts/doctype/journal_entry/journal_entry.py:899
msgid "{0} {1} is not submitted"
msgstr ""
@@ -62618,11 +62738,11 @@ msgstr ""
msgid "{0}%"
msgstr ""
-#: erpnext/controllers/website_list_for_contact.py:207
+#: erpnext/controllers/website_list_for_contact.py:209
msgid "{0}% Billed"
msgstr ""
-#: erpnext/controllers/website_list_for_contact.py:215
+#: erpnext/controllers/website_list_for_contact.py:217
msgid "{0}% Delivered"
msgstr ""
@@ -62672,7 +62792,7 @@ msgstr ""
msgid "{0}: {1} does not belong to the Company: {2}"
msgstr ""
-#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1353
+#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1355
msgid "{0}: {1} does not exist"
msgstr ""
@@ -62696,11 +62816,11 @@ msgstr ""
msgid "{field_label} is mandatory for sub-contracted {doctype}."
msgstr ""
-#: erpnext/controllers/stock_controller.py:2285
+#: erpnext/controllers/stock_controller.py:2283
msgid "{item_name}'s Sample Size ({sample_size}) cannot be greater than the Accepted Quantity ({accepted_quantity})"
msgstr ""
-#: erpnext/controllers/stock_controller.py:2048
+#: erpnext/controllers/stock_controller.py:2046
msgid "{ref_doctype} {ref_name} status is {status}."
msgstr ""
diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json
index b55cec332bd..ec780e63e68 100644
--- a/erpnext/projects/doctype/project/project.json
+++ b/erpnext/projects/doctype/project/project.json
@@ -121,7 +121,7 @@
"in_list_view": 1,
"label": "% Completed",
"no_copy": 1,
- "read_only": 1
+ "read_only_depends_on": "eval:doc.percent_complete_method != 'Manual'"
},
{
"fieldname": "column_break_5",
@@ -484,7 +484,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 4,
- "modified": "2026-07-14 14:32:11.328347",
+ "modified": "2026-07-21 11:23:22.000000",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project",
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index 814c59525a9..981acff98c3 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -222,6 +222,8 @@ class Project(Document):
if self.percent_complete_method == "Manual":
if self.status == "Completed":
self.percent_complete = 100
+ elif flt(self.percent_complete) < 0 or flt(self.percent_complete) > 100:
+ frappe.throw(_("% Complete must be between 0 and 100"))
return
total = frappe.db.count("Task", dict(project=self.name))
diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py
index dd10fb48ba7..56d74cb4b2e 100644
--- a/erpnext/projects/doctype/project/test_project.py
+++ b/erpnext/projects/doctype/project/test_project.py
@@ -244,6 +244,61 @@ class TestProject(ERPNextTestSuite):
project.save()
self.assertEqual(project.status, "Completed")
+ def _project_with_tasks(self, method, count):
+ name = f"_Test PercentComplete {frappe.generate_hash(length=8)}"
+ project = frappe.get_doc(
+ {
+ "doctype": "Project",
+ "project_name": name,
+ "status": "Open",
+ "percent_complete_method": method,
+ "company": "_Test Company",
+ "expected_start_date": nowdate(),
+ }
+ ).insert()
+ task_names = []
+ for i in range(count):
+ task = frappe.get_doc(
+ {
+ "doctype": "Task",
+ "subject": f"{name} Task {i}",
+ "project": project.name,
+ "status": "Open",
+ "exp_start_date": nowdate(),
+ "exp_end_date": nowdate(),
+ }
+ ).insert()
+ task_names.append(task.name)
+ return project, task_names
+
+ def test_percent_complete_manual(self):
+ project, tasks = self._project_with_tasks("Manual", 2)
+
+ # manual value is preserved on save, even with linked tasks
+ project.percent_complete = 42
+ project.save()
+ self.assertEqual(project.percent_complete, 42)
+
+ # task updates do not overwrite the manual value
+ frappe.db.set_value("Task", tasks[0], "status", "Completed")
+ project.update_percent_complete()
+ self.assertEqual(project.percent_complete, 42)
+
+ # out-of-range values are rejected
+ project.percent_complete = 150
+ self.assertRaises(frappe.ValidationError, project.save)
+ project.reload()
+
+ project.percent_complete = -10
+ self.assertRaises(frappe.ValidationError, project.save)
+ project.reload()
+
+ # Completed status forces 100 regardless of the manual value
+ project.percent_complete = 42
+ project.status = "Completed"
+ project.save()
+ self.assertEqual(project.percent_complete, 100)
+
def _create_portal_user(self, email):
"""A user with no Project-related role, so read access can only come from
control_access_for_project_users() sharing the doc with them."""
diff --git a/erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.py b/erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.py
index a6e7150e410..316db1f3507 100644
--- a/erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.py
+++ b/erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.py
@@ -116,31 +116,37 @@ def get_data(filters, group_fieldname=None):
def group_by(data, fieldname):
- groups = {row.get(fieldname) for row in data}
- grouped_data = []
- for group in sorted(groups):
- group_row = {
- fieldname: group,
- "hours": sum(row.get("hours") for row in data if row.get(fieldname) == group),
- "billing_hours": sum(row.get("billing_hours") for row in data if row.get(fieldname) == group),
- "billing_amount": sum(row.get("billing_amount") for row in data if row.get(fieldname) == group),
- "indent": 0,
- "is_group": 1,
- }
- if fieldname == "employee":
- group_row["employee_name"] = next(
- row.get("employee_name") for row in data if row.get(fieldname) == group
- )
+ groups = {}
+ for row in data:
+ groups.setdefault(row.get(fieldname), []).append(row)
- grouped_data.append(group_row)
- for row in data:
- if row.get(fieldname) != group:
- continue
+ grouped_data = []
+ for group in sorted(groups, key=lambda g: (g is None, g)):
+ hours = billing_hours = billing_amount = 0
+ child_rows = []
+ for row in groups[group]:
+ hours += row.get("hours") or 0
+ billing_hours += row.get("billing_hours") or 0
+ billing_amount += row.get("billing_amount") or 0
_row = row.copy()
_row[fieldname] = None
_row["indent"] = 1
_row["is_group"] = 0
- grouped_data.append(_row)
+ child_rows.append(_row)
+
+ group_row = {
+ fieldname: group,
+ "hours": hours,
+ "billing_hours": billing_hours,
+ "billing_amount": billing_amount,
+ "indent": 0,
+ "is_group": 1,
+ }
+ if fieldname == "employee":
+ group_row["employee_name"] = groups[group][0].get("employee_name")
+
+ grouped_data.append(group_row)
+ grouped_data.extend(child_rows)
return grouped_data
diff --git a/erpnext/public/js/controllers/stock_controller.js b/erpnext/public/js/controllers/stock_controller.js
index a205412e75d..eef7f2f0a37 100644
--- a/erpnext/public/js/controllers/stock_controller.js
+++ b/erpnext/public/js/controllers/stock_controller.js
@@ -11,6 +11,36 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con
}
}
+ onload_post_render() {
+ this.set_route_options_for_new_doc();
+ }
+
+ set_route_options_for_new_doc() {
+ // While creating a Batch or Serial and Batch Bundle from the link
+ // field, copy details from the line item to the new form
+ if (!this.frm.fields_dict.items) return;
+
+ let batch_no_field = this.frm.get_docfield("items", "batch_no");
+ if (batch_no_field) {
+ batch_no_field.get_route_options_for_new_doc = (row) => {
+ return {
+ item: row.doc.item_code,
+ };
+ };
+ }
+
+ let sbb_field = this.frm.get_docfield("items", "serial_and_batch_bundle");
+ if (sbb_field) {
+ sbb_field.get_route_options_for_new_doc = (row) => {
+ return {
+ item_code: row.doc.item_code,
+ warehouse: row.doc.warehouse || row.doc.s_warehouse || row.doc.t_warehouse,
+ voucher_type: this.frm.doc.doctype,
+ };
+ };
+ }
+ }
+
barcode(doc, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.barcode) {
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 410ab292170..ad110712d71 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -518,7 +518,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
return;
}
- schedules.forEach((schedule) => (schedule.__checked = 1));
+ schedules.forEach((schedule) => {
+ schedule.__checked = 1;
+ schedule.currency = frm.doc.currency;
+ });
const dialog = new frappe.ui.Dialog({
title: __("Select Payment Schedule"),
@@ -552,10 +555,19 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
in_list_view: 1,
read_only: 1,
},
+ {
+ fieldtype: "Link",
+ fieldname: "currency",
+ label: __("Currency"),
+ options: "Currency",
+ hidden: 1,
+ read_only: 1,
+ },
{
fieldtype: "Currency",
fieldname: "payment_amount",
label: __("Amount"),
+ options: "currency",
in_list_view: 1,
read_only: 1,
},
@@ -637,34 +649,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
erpnext.toggle_serial_batch_fields(this.frm);
}
- set_route_options_for_new_doc() {
- // While creating the batch from the link field, copy item from line item to batch form
-
- if (this.frm.fields_dict["items"].grid.get_field("batch_no")) {
- let batch_no_field = this.frm.get_docfield("items", "batch_no");
- if (batch_no_field) {
- batch_no_field.get_route_options_for_new_doc = function (row) {
- return {
- item: row.doc.item_code,
- };
- };
- }
- }
-
- // While creating the SABB from the link field, copy item, doctype from line item to SABB form
- if (this.frm.fields_dict["items"].grid.get_field("serial_and_batch_bundle")) {
- let sbb_field = this.frm.get_docfield("items", "serial_and_batch_bundle");
- if (sbb_field) {
- sbb_field.get_route_options_for_new_doc = (row) => {
- return {
- item_code: row.doc.item_code,
- voucher_type: this.frm.doc.doctype,
- };
- };
- }
- }
- }
-
scan_barcode() {
frappe.flags.dialog_set = false;
this.barcode_scanner.process_scan();
diff --git a/erpnext/stock/dashboard/item_dashboard.py b/erpnext/stock/dashboard/item_dashboard.py
index 9d1c7b55122..d01e8401bcf 100644
--- a/erpnext/stock/dashboard/item_dashboard.py
+++ b/erpnext/stock/dashboard/item_dashboard.py
@@ -59,6 +59,11 @@ def get_data(
"reserved_qty": ["!=", 0],
"reserved_qty_for_production": ["!=", 0],
"reserved_qty_for_sub_contract": ["!=", 0],
+ "reserved_qty_for_production_plan": ["!=", 0],
+ "reserved_stock": ["!=", 0],
+ "ordered_qty": ["!=", 0],
+ "indented_qty": ["!=", 0],
+ "planned_qty": ["!=", 0],
"actual_qty": ["!=", 0],
},
filters=filters,
diff --git a/erpnext/stock/doctype/bin/bin.js b/erpnext/stock/doctype/bin/bin.js
index c725b691db4..5817d318965 100644
--- a/erpnext/stock/doctype/bin/bin.js
+++ b/erpnext/stock/doctype/bin/bin.js
@@ -3,17 +3,17 @@
frappe.ui.form.on("Bin", {
refresh(frm) {
- frm.trigger("recalculate_bin_quantity");
+ frm.trigger("recalculate_values");
},
- recalculate_bin_quantity(frm) {
- frm.add_custom_button(__("Recalculate Bin Qty"), () => {
+ recalculate_values(frm) {
+ frm.add_custom_button(__("Recalculate Values"), () => {
frappe.call({
- method: "recalculate_qty",
+ method: "recalculate_values",
freeze: true,
doc: frm.doc,
callback: function (r) {
- frappe.show_alert(__("Bin Qty Recalculated"), 2);
+ frappe.show_alert(__("Bin Values Recalculated"), 2);
},
});
});
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index 346de69532d..64004b13d19 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -37,7 +37,7 @@ class Bin(Document):
# end: auto-generated types
@frappe.whitelist()
- def recalculate_qty(self):
+ def recalculate_values(self):
from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production
from erpnext.stock.stock_balance import (
get_indented_qty,
@@ -46,7 +46,19 @@ class Bin(Document):
get_reserved_qty,
)
- self.actual_qty = get_actual_qty(self.item_code, self.warehouse)
+ last_sle = get_last_sle_values(self.item_code, self.warehouse)
+ self.actual_qty = last_sle.qty_after_transaction
+ self.valuation_rate = last_sle.valuation_rate
+ self.stock_value = last_sle.stock_value
+
+ from erpnext.stock.utils import get_valuation_method
+
+ if get_valuation_method(self.item_code) == "Standard Cost":
+ from erpnext.stock.doctype.item_standard_cost.item_standard_cost import get_item_standard_rate
+
+ self.stock_value = flt(self.actual_qty) * flt(
+ get_item_standard_rate(self.item_code, self.company)
+ )
self.planned_qty = get_planned_qty(self.item_code, self.warehouse)
self.indented_qty = get_indented_qty(self.item_code, self.warehouse)
self.ordered_qty = get_ordered_qty(self.item_code, self.warehouse)
@@ -302,20 +314,23 @@ def update_qty(bin_name, args):
def get_actual_qty(item_code, warehouse):
+ return get_last_sle_values(item_code, warehouse).qty_after_transaction
+
+
+def get_last_sle_values(item_code, warehouse):
sle = frappe.qb.DocType("Stock Ledger Entry")
- last_sle_qty = (
+ last_sle = (
frappe.qb.from_(sle)
- .select(sle.qty_after_transaction)
+ .select(sle.qty_after_transaction, sle.valuation_rate, sle.stock_value)
.where((sle.item_code == item_code) & (sle.warehouse == warehouse) & (sle.is_cancelled == 0))
.orderby(sle.posting_datetime, order=Order.desc)
.orderby(sle.creation, order=Order.desc)
.limit(1)
- .run()
+ .run(as_dict=True)
)
- actual_qty = 0.0
- if last_sle_qty:
- actual_qty = last_sle_qty[0][0]
+ if last_sle:
+ return last_sle[0]
- return actual_qty
+ return frappe._dict(qty_after_transaction=0.0, valuation_rate=0.0, stock_value=0.0)
diff --git a/erpnext/stock/doctype/bin/test_bin.py b/erpnext/stock/doctype/bin/test_bin.py
index ef21bcf7833..45302c66f01 100644
--- a/erpnext/stock/doctype/bin/test_bin.py
+++ b/erpnext/stock/doctype/bin/test_bin.py
@@ -26,6 +26,35 @@ class TestBin(ERPNextTestSuite):
bin = _create_bin(item_code, warehouse)
self.assertEqual(bin.item_code, item_code)
+ def test_recalculate_values(self):
+ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+
+ item_code = make_item().name
+ warehouse = "_Test Warehouse - _TC"
+ make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=100)
+
+ bin = frappe.get_doc("Bin", {"item_code": item_code, "warehouse": warehouse})
+ bin.db_set({"actual_qty": 0, "valuation_rate": 0, "stock_value": 0})
+ bin.reload()
+ bin.recalculate_values()
+
+ self.assertEqual(bin.actual_qty, 10)
+ self.assertEqual(bin.valuation_rate, 100)
+ self.assertEqual(bin.stock_value, 1000)
+
+ def test_recalculate_values_without_sle(self):
+ item_code = make_item().name
+ warehouse = "_Test Warehouse - _TC"
+
+ bin = _create_bin(item_code, warehouse)
+ bin.db_set({"actual_qty": 5, "valuation_rate": 50, "stock_value": 250})
+ bin.reload()
+ bin.recalculate_values()
+
+ self.assertEqual(bin.actual_qty, 0)
+ self.assertEqual(bin.valuation_rate, 0)
+ self.assertEqual(bin.stock_value, 0)
+
def test_index_exists(self):
indexes = frappe.db.sql("show index from tabBin where Non_unique = 0", as_dict=1)
if not any(index.get("Key_name") == "unique_item_warehouse" for index in indexes):
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index c627c6bbdb1..94ce0652931 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -569,8 +569,6 @@ frappe.ui.form.on("Stock Entry", {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}
- frm.events.set_route_options_for_new_doc(frm);
-
frm.set_df_property(
"items",
"cannot_add_rows",
@@ -583,28 +581,6 @@ frappe.ui.form.on("Stock Entry", {
);
},
- set_route_options_for_new_doc(frm) {
- let batch_no_field = frm.get_docfield("items", "batch_no");
- if (batch_no_field) {
- batch_no_field.get_route_options_for_new_doc = function (row) {
- return {
- item: row.doc.item_code,
- };
- };
- }
-
- let sbb_field = frm.get_docfield("items", "serial_and_batch_bundle");
- if (sbb_field) {
- sbb_field.get_route_options_for_new_doc = (row) => {
- return {
- item_code: row.doc.item_code,
- voucher_type: frm.doc.doctype,
- warehouse: row.doc.s_warehouse || row.doc.t_warehouse,
- };
- };
- }
- },
-
get_items_from_transit_entry: function (frm) {
if (frm.doc.docstatus === 0 && !frm.doc.subcontracting_inward_order) {
frm.add_custom_button(
@@ -1312,6 +1288,7 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
}
onload_post_render() {
+ super.onload_post_render();
var me = this;
if (me.frm.doc.__islocal && me.frm.doc.company && !me.frm.doc.amended_from) {
me.company();
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index bc2d255a041..e050429eb98 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -3151,7 +3151,7 @@ class StockEntry(StockController, SubcontractingInwardController):
self.process_loss_qty = flt(
(flt(self.fg_completed_qty) * flt(self.process_loss_percentage)) / 100
)
- elif self.process_loss_qty and not self.process_loss_percentage:
+ elif self.process_loss_qty and self.fg_completed_qty:
self.process_loss_percentage = flt(
(flt(self.process_loss_qty) / flt(self.fg_completed_qty)) * 100
)
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index ce316f5105b..ee3c7886b17 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -2943,6 +2943,28 @@ class TestStockEntry(ERPNextTestSuite):
self.assertEqual(se.items[2].qty, 4.5)
self.assertEqual(se.items[2].amount, 5)
+ def test_process_loss_percentage_resyncs_from_qty(self):
+ # changing fg qty recomputes process_loss_qty
+ se = frappe.new_doc("Stock Entry")
+ se.purpose = "Manufacture"
+ se.fg_completed_qty = 200
+ se.process_loss_qty = 100
+ se.process_loss_percentage = 80
+
+ se.set_process_loss_qty()
+
+ self.assertEqual(se.process_loss_percentage, 50)
+
+ def test_process_loss_qty_derived_from_percentage_when_qty_blank(self):
+ se = frappe.new_doc("Stock Entry")
+ se.purpose = "Manufacture"
+ se.fg_completed_qty = 200
+ se.process_loss_percentage = 25
+
+ se.set_process_loss_qty()
+
+ self.assertEqual(se.process_loss_qty, 50)
+
def make_serialized_item(self, **args):
args = frappe._dict(args)
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
index ef4672899cc..e711d7248f7 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
@@ -46,17 +46,6 @@ frappe.ui.form.on("Stock Reconciliation", {
};
});
- let sbb_field = frm.get_docfield("items", "serial_and_batch_bundle");
- if (sbb_field) {
- sbb_field.get_route_options_for_new_doc = (row) => {
- return {
- item_code: row.doc.item_code,
- warehouse: row.doc.warehouse,
- voucher_type: frm.doc.doctype,
- };
- };
- }
-
if (frm.doc.company) {
erpnext.queries.setup_queries(frm, "Warehouse", function () {
return erpnext.queries.warehouse(frm.doc);
diff --git a/erpnext/stock/page/stock_balance/stock_balance.js b/erpnext/stock/page/stock_balance/stock_balance.js
index a5fba9f98f3..531e335dfdb 100644
--- a/erpnext/stock/page/stock_balance/stock_balance.js
+++ b/erpnext/stock/page/stock_balance/stock_balance.js
@@ -48,11 +48,19 @@ frappe.pages["stock-balance"].on_page_load = function (wrapper) {
sort_by: "projected_qty",
sort_order: "asc",
options: [
- { fieldname: "projected_qty", label: __("Projected qty") },
- { fieldname: "reserved_qty", label: __("Reserved for sale") },
- { fieldname: "reserved_qty_for_production", label: __("Reserved for manufacturing") },
- { fieldname: "reserved_qty_for_sub_contract", label: __("Reserved for sub contracting") },
- { fieldname: "actual_qty", label: __("Actual qty in stock") },
+ { fieldname: "projected_qty", label: __("Projected Qty") },
+ { fieldname: "reserved_qty", label: __("Reserved Qty") },
+ { fieldname: "reserved_qty_for_production", label: __("Reserved Qty for Production") },
+ { fieldname: "reserved_qty_for_sub_contract", label: __("Reserved Qty for Subcontract") },
+ {
+ fieldname: "reserved_qty_for_production_plan",
+ label: __("Reserved Qty for Production Plan"),
+ },
+ { fieldname: "reserved_stock", label: __("Reserved Stock") },
+ { fieldname: "ordered_qty", label: __("Ordered Qty") },
+ { fieldname: "indented_qty", label: __("Requested Qty") },
+ { fieldname: "planned_qty", label: __("Planned Qty") },
+ { fieldname: "actual_qty", label: __("Actual Qty") },
],
},
change: function (sort_by, sort_order) {
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index 343ec5539fe..e880e8db9b9 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -306,6 +306,7 @@ class FIFOSlots:
# prepare single sle voucher detail lookup
self.prepare_stock_reco_voucher_wise_count()
+ self.float_precision = get_float_precision()
if stock_ledger_entries is None:
# streaming path: nested queries invalidate the streaming cursor below,
@@ -370,6 +371,7 @@ class FIFOSlots:
row, fifo_queue, transferred_item_key, serial_nos, batch_nos, from_end
)
+ self._revalue_stock_reconciliation_slots(row, fifo_queue, batch_nos)
self._update_balances(row, key)
self._trim_serial_fifo_queue(row, key, fifo_queue)
@@ -393,6 +395,36 @@ class FIFOSlots:
# Stock reconciliation stores the final balance; FIFO needs the movement delta.
row.actual_qty = flt(row.qty_after_transaction) - flt(prev_balance_qty)
+ def _revalue_stock_reconciliation_slots(self, row: dict, fifo_queue: list, batch_nos: list) -> None:
+ if row.voucher_type != "Stock Reconciliation" or row.has_serial_no:
+ return
+
+ if row.has_batch_no:
+ if flt(row.actual_qty) > 0:
+ self._revalue_reconciled_batch_slots(fifo_queue, batch_nos)
+ return
+
+ for slot in fifo_queue:
+ if is_qty_slot(slot):
+ slot[FIFO_VALUE_INDEX] = flt(slot[FIFO_QTY_INDEX] * flt(row.valuation_rate))
+
+ def _revalue_reconciled_batch_slots(self, fifo_queue: list, batch_nos: list) -> None:
+ for batch_no, _use_batchwise_valuation, qty, stock_value_difference in batch_nos:
+ if not flt(qty):
+ continue
+
+ slots = [
+ slot
+ for slot in fifo_queue
+ if is_batch_slot(slot) and slot[BATCH_SLOT_BATCH_INDEX] == batch_no
+ ]
+ if flt(sum(flt(slot[BATCH_SLOT_QTY_INDEX]) for slot in slots) - flt(qty), self.float_precision):
+ continue
+
+ rate = flt(stock_value_difference) / flt(qty)
+ for slot in slots:
+ slot[BATCH_SLOT_VALUE_INDEX] = flt(slot[BATCH_SLOT_QTY_INDEX] * rate)
+
def _get_serial_and_batch_nos(
self, row: dict, bundle_wise_serial_nos: dict, bundle_wise_batch_nos: dict
) -> tuple[list, list]:
diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
index 180a424b209..f072dfeba4d 100644
--- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
@@ -379,6 +379,243 @@ class TestStockAgeing(ERPNextTestSuite):
self.assertEqual(queue, [[60.0, "2025-11-30", 60.0], [30.0, "2026-01-31", 30.0]])
self.assertEqual(report_data[0][7:15], [30.0, 30.0, 0.0, 0.0, 60.0, 60.0, 0.0, 0.0])
+ def test_stock_reco_revaluation_rescales_queue_values(self):
+ "Ledger (same wh): [+15 @ 100, reco reset >> 20 @ 50]"
+ sle = [
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=15,
+ qty_after_transaction=15,
+ stock_value_difference=1500,
+ valuation_rate=100,
+ warehouse="WH 1",
+ posting_date="2021-12-01",
+ voucher_type="Stock Entry",
+ voucher_no="001",
+ has_serial_no=False,
+ serial_no=None,
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=0,
+ qty_after_transaction=20,
+ stock_value_difference=(-500),
+ valuation_rate=50,
+ warehouse="WH 1",
+ posting_date="2021-12-02",
+ voucher_type="Stock Reconciliation",
+ voucher_no="002",
+ has_serial_no=False,
+ serial_no=None,
+ ),
+ ]
+
+ slots = FIFOSlots(self.filters, sle).generate()
+ queue = slots["Flask Item"]["fifo_queue"]
+
+ self.assertEqual(queue, [[15.0, "2021-12-01", 750.0], [5.0, "2021-12-02", 250.0]])
+
+ def test_stock_reco_with_split_out_and_in_sles_revalues_queue(self):
+ "Ledger (same wh): [+10 @ 100, reco out >> 0, reco in >> 12 @ 2]"
+ sle = [
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=10,
+ qty_after_transaction=10,
+ stock_value_difference=1000,
+ valuation_rate=100,
+ warehouse="WH 1",
+ posting_date="2021-12-01",
+ voucher_type="Stock Entry",
+ voucher_no="001",
+ has_serial_no=False,
+ serial_no=None,
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=(-10),
+ qty_after_transaction=0,
+ stock_value_difference=(-1000),
+ valuation_rate=100,
+ warehouse="WH 1",
+ posting_date="2021-12-02",
+ voucher_type="Stock Reconciliation",
+ voucher_no="002",
+ has_serial_no=False,
+ serial_no=None,
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=12,
+ qty_after_transaction=12,
+ stock_value_difference=24,
+ valuation_rate=2,
+ warehouse="WH 1",
+ posting_date="2021-12-02",
+ voucher_type="Stock Reconciliation",
+ voucher_no="002",
+ has_serial_no=False,
+ serial_no=None,
+ ),
+ ]
+
+ slots = FIFOSlots(self.filters, sle).generate()
+ queue = slots["Flask Item"]["fifo_queue"]
+
+ self.assertEqual(queue, [[10.0, "2021-12-01", 20.0], [2.0, "2021-12-02", 4.0]])
+
+ def test_stock_reco_decrease_rescales_slots_at_reco_rate(self):
+ """Ledger (same wh): [+10 @ 100, +20 @ 250, reco reset >> 25 @ 220]
+ The valuation engine collapses the FIFO stack to qty_after * valuation_rate
+ on a reco, so remaining slot values follow the reco rate, not the lot rates."""
+ sle = [
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=10,
+ qty_after_transaction=10,
+ stock_value_difference=1000,
+ valuation_rate=100,
+ warehouse="WH 1",
+ posting_date="2021-12-01",
+ voucher_type="Stock Entry",
+ voucher_no="001",
+ has_serial_no=False,
+ serial_no=None,
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=20,
+ qty_after_transaction=30,
+ stock_value_difference=5000,
+ valuation_rate=200,
+ warehouse="WH 1",
+ posting_date="2021-12-02",
+ voucher_type="Stock Entry",
+ voucher_no="002",
+ has_serial_no=False,
+ serial_no=None,
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=0,
+ qty_after_transaction=25,
+ stock_value_difference=(-500),
+ valuation_rate=220,
+ warehouse="WH 1",
+ posting_date="2021-12-03",
+ voucher_type="Stock Reconciliation",
+ voucher_no="003",
+ has_serial_no=False,
+ serial_no=None,
+ ),
+ ]
+
+ slots = FIFOSlots(self.filters, sle).generate()
+ queue = slots["Flask Item"]["fifo_queue"]
+
+ self.assertEqual(queue, [[5.0, "2021-12-01", 1100.0], [20.0, "2021-12-02", 4400.0]])
+
+ def test_batch_stock_reco_revaluation_rescales_slot_values(self):
+ "Ledger (same wh, batch B): [+10 @ 100, reco out >> 0, reco in >> 12 @ 2]"
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ item_code = make_item(
+ "Test Stock Ageing Batch Reco Revaluation",
+ {"is_stock_item": 1, "has_batch_no": 1, "valuation_method": "FIFO"},
+ ).name
+
+ batch_no = "SA-RECO-REVALUE-BATCH"
+ if not frappe.db.exists("Batch", batch_no):
+ frappe.get_doc({"doctype": "Batch", "batch_id": batch_no, "item": item_code}).insert(
+ ignore_permissions=True
+ )
+ frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
+
+ def make_sle(posting_date, voucher_type, voucher_no, actual_qty, qty_after, stock_value_difference):
+ return frappe._dict(
+ name=item_code,
+ actual_qty=actual_qty,
+ qty_after_transaction=qty_after,
+ stock_value_difference=stock_value_difference,
+ valuation_rate=abs(stock_value_difference / actual_qty) if actual_qty else 0,
+ warehouse="WH 1",
+ posting_date=posting_date,
+ voucher_type=voucher_type,
+ voucher_no=voucher_no,
+ has_serial_no=False,
+ has_batch_no=True,
+ serial_no=None,
+ batch_no=batch_no,
+ )
+
+ sle = [
+ make_sle("2021-12-01", "Stock Entry", "001", 10, 10, 1000),
+ make_sle("2021-12-02", "Stock Reconciliation", "002", -10, 0, -1000),
+ make_sle("2021-12-02", "Stock Reconciliation", "002", 12, 12, 24),
+ ]
+
+ slots = FIFOSlots(self.filters, sle).generate()
+ queue = slots[item_code]["fifo_queue"]
+
+ self.assertEqual(
+ queue,
+ [
+ [batch_no, 1, 10.0, "2021-12-01", 20.0],
+ [batch_no, 1, 2.0, "2021-12-02", 4.0],
+ ],
+ )
+
+ def test_partial_batch_reco_keeps_existing_slot_values(self):
+ """Ledger (same wh, batch B): [+10 @ 100, single-SLE reco >> 12]
+ The reco entry qty (delta 2) does not cover the whole batch, so
+ stock_value_difference / qty is not the batch rate: skip the rescale."""
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ item_code = make_item(
+ "Test Stock Ageing Partial Batch Reco",
+ {"is_stock_item": 1, "has_batch_no": 1, "valuation_method": "FIFO"},
+ ).name
+
+ batch_no = "SA-PARTIAL-RECO-BATCH"
+ if not frappe.db.exists("Batch", batch_no):
+ frappe.get_doc({"doctype": "Batch", "batch_id": batch_no, "item": item_code}).insert(
+ ignore_permissions=True
+ )
+ frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
+
+ def make_sle(posting_date, voucher_type, voucher_no, actual_qty, qty_after, stock_value_difference):
+ return frappe._dict(
+ name=item_code,
+ actual_qty=actual_qty,
+ qty_after_transaction=qty_after,
+ stock_value_difference=stock_value_difference,
+ valuation_rate=abs(stock_value_difference / actual_qty) if actual_qty else 0,
+ warehouse="WH 1",
+ posting_date=posting_date,
+ voucher_type=voucher_type,
+ voucher_no=voucher_no,
+ has_serial_no=False,
+ has_batch_no=True,
+ serial_no=None,
+ batch_no=batch_no,
+ )
+
+ sle = [
+ make_sle("2021-12-01", "Stock Entry", "001", 10, 10, 1000),
+ make_sle("2021-12-02", "Stock Reconciliation", "002", 0, 12, -400),
+ ]
+
+ slots = FIFOSlots(self.filters, sle).generate()
+ queue = slots[item_code]["fifo_queue"]
+
+ self.assertEqual(
+ queue,
+ [
+ [batch_no, 1, 10.0, "2021-12-01", 1000.0],
+ [batch_no, 1, 2.0, "2021-12-01", 400.0],
+ ],
+ )
+
def test_sequential_stock_reco_same_warehouse(self):
"""
Test back to back stock recos (same warehouse).
diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
index 011d117e2b2..fe14169e558 100644
--- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
+++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
@@ -214,7 +214,7 @@ def create_reposting_entries(rows: str | list, company: str):
"posting_date": sle.posting_date,
"posting_time": sle.posting_time,
"company": company,
- "allow_nagative_stock": 1,
+ "allow_negative_stock": 1,
}
).submit()
@@ -260,7 +260,7 @@ def repost_based_on_transaction(rows, company=None, entries=None):
"posting_date": row.get("posting_date"),
"posting_time": row.get("posting_time"),
"company": company,
- "allow_nagative_stock": 1,
+ "allow_negative_stock": 1,
"recalculate_valuation_rate": 1,
}
).submit()
diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
index aef9fec6414..ffb024acfb1 100644
--- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
+++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
@@ -325,7 +325,7 @@ def create_reposting_entries(rows, item_code=None, warehouse=None):
"warehouse": warehouse or row.warehouse,
"posting_date": row.posting_date,
"posting_time": row.posting_time,
- "allow_nagative_stock": 1,
+ "allow_negative_stock": 1,
}
).submit()
diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py
index 3193ba3de51..3bb557d42c2 100644
--- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py
+++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py
@@ -84,6 +84,7 @@ def execute(filters=None):
bin.reserved_qty_for_production_plan,
bin.reserved_qty_for_sub_contract,
reserved_qty_for_pos,
+ bin.reserved_stock,
bin.projected_qty,
re_order_level,
re_order_qty,
@@ -200,6 +201,13 @@ def get_columns():
"width": 100,
"convertible": "qty",
},
+ {
+ "label": _("Reserved Stock"),
+ "fieldname": "reserved_stock",
+ "fieldtype": "Float",
+ "width": 100,
+ "convertible": "qty",
+ },
{
"label": _("Projected Qty"),
"fieldname": "projected_qty",
@@ -246,6 +254,7 @@ def get_bin_list(filters):
bin.reserved_qty_for_production,
bin.reserved_qty_for_sub_contract,
bin.reserved_qty_for_production_plan,
+ bin.reserved_stock,
bin.projected_qty,
)
.orderby(bin.item_code, bin.warehouse)
diff --git a/erpnext/templates/emails/appointment_confirmed.html b/erpnext/templates/emails/appointment_confirmed.html
new file mode 100644
index 00000000000..12fa2232f58
--- /dev/null
+++ b/erpnext/templates/emails/appointment_confirmed.html
@@ -0,0 +1,6 @@
+
{{_("Dear")}} {{ full_name }},
+{{_("Your email has been verified and your appointment has been confirmed for {0}").format(scheduled_time)}}.
+{{_("We look forward to meeting you")}}.
+ +{{_("This email was sent from {0}").format(site_url)}}
diff --git a/erpnext/templates/emails/confirm_appointment.html b/erpnext/templates/emails/confirm_appointment.html index 6c9b28bc136..ce6a9f88a99 100644 --- a/erpnext/templates/emails/confirm_appointment.html +++ b/erpnext/templates/emails/confirm_appointment.html @@ -1,6 +1,7 @@{{_("Dear")}} {{ full_name }}{% if last_name %} {{ last_name}}{% endif %},
{{_("A new appointment has been created for you with {0}").format(site_url)}}.
{{_("Click on the link below to verify your email and confirm the appointment")}}.
+{{_("This link is valid for {0} minutes").format(expiry_minutes)}}.
{{ _("Verify Email") }} diff --git a/erpnext/utilities/__init__.py b/erpnext/utilities/__init__.py index f01aa1312f6..162e07e2558 100644 --- a/erpnext/utilities/__init__.py +++ b/erpnext/utilities/__init__.py @@ -47,7 +47,11 @@ def payment_app_import_guard(): msg = _("payments app is not installed. Please install it from {} or {}").format( marketplace_link, github_link ) + + if "payments" not in frappe.get_installed_apps(): + frappe.throw(msg, title=_("Missing Payments App"), exc=frappe.AppNotInstalledError) + try: yield except ImportError: - frappe.throw(msg, title=_("Missing Payments App")) + frappe.throw(msg, title=_("Missing Payments App"), exc=frappe.AppNotInstalledError) diff --git a/erpnext/utilities/doctype/video_settings/video_settings.py b/erpnext/utilities/doctype/video_settings/video_settings.py index 762a795a733..fb7da9ed754 100644 --- a/erpnext/utilities/doctype/video_settings/video_settings.py +++ b/erpnext/utilities/doctype/video_settings/video_settings.py @@ -3,9 +3,9 @@ import frappe -from apiclient.discovery import build from frappe import _ from frappe.model.document import Document +from pyyoutube import Api, PyYouTubeException class VideoSettings(Document): @@ -28,7 +28,7 @@ class VideoSettings(Document): def validate_youtube_api_key(self): if self.enable_youtube_tracking and self.api_key: try: - build("youtube", "v3", developerKey=self.api_key) + Api(api_key=self.api_key).get_i18n_languages(parts="snippet") except Exception: title = _("Failed to Authenticate the API key.") self.log_error("Failed to authenticate API key") diff --git a/erpnext/www/book_appointment/index.js b/erpnext/www/book_appointment/index.js index 0770d102046..0021e47fcf1 100644 --- a/erpnext/www/book_appointment/index.js +++ b/erpnext/www/book_appointment/index.js @@ -237,9 +237,9 @@ async function submit() { frappe.show_alert(__("Appointment Created Successfully")); } setTimeout(() => { - let redirect_url = "/"; + let redirect_url = "/book_appointment"; if (window.appointment_settings.success_redirect_url) { - redirect_url += window.appointment_settings.success_redirect_url; + redirect_url = `/${window.appointment_settings.success_redirect_url}`; } window.location.href = redirect_url; }, 5000); diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py index 84b16d733ba..b4cdebab0a1 100644 --- a/erpnext/www/book_appointment/index.py +++ b/erpnext/www/book_appointment/index.py @@ -4,6 +4,7 @@ import zoneinfo import frappe from frappe import _ +from frappe.rate_limiter import rate_limit from frappe.utils.data import get_system_timezone WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] @@ -18,7 +19,7 @@ def get_context(context): def handle_appointment_booking_disabled(): - if not frappe.get_single_value("Appointment Booking Settings", "enable_scheduling"): + if not frappe.get_single_value("Appointment Booking Settings", "enable_appointment_portal"): frappe.redirect_to_message( _("Appointment Scheduling Disabled"), _("Appointment Scheduling has been disabled for this site"), @@ -64,6 +65,8 @@ def get_appointment_slots(date, timezone): ) holiday_list = frappe.get_doc("Holiday List", settings.holiday_list) timeslots = get_available_slots_between(query_start_time, query_end_time, settings) + # fetch the day's booked slots once instead of querying per timeslot + booked_times = get_booked_slot_times_for(timeslots, settings.appointment_duration) # Filter and convert timeslots converted_timeslots = [] @@ -74,7 +77,7 @@ def get_appointment_slots(date, timezone): converted_timeslots.append(dict(time=converted_timeslot, availability=False)) continue # Check availability - if check_availabilty(timeslot, settings) and converted_timeslot >= now: + if is_slot_available(timeslot, booked_times, settings) and converted_timeslot >= now: converted_timeslots.append(dict(time=converted_timeslot, availability=True)) else: converted_timeslots.append(dict(time=converted_timeslot, availability=False)) @@ -100,7 +103,8 @@ def get_available_slots_between(query_start_time, query_end_time, settings): return timeslots -@frappe.whitelist(allow_guest=True) +@frappe.whitelist(allow_guest=True, methods=["POST"]) +@rate_limit(limit=5, seconds=300) def create_appointment(date, time, tz, contact): handle_appointment_booking_disabled() format_string = "%Y-%m-%d %H:%M:%S" @@ -112,13 +116,13 @@ def create_appointment(date, time, tz, contact): # Create a appointment document from form appointment = frappe.new_doc("Appointment") appointment.scheduled_time = scheduled_time - contact = json.loads(contact) + contact = frappe.parse_json(contact) appointment.customer_name = contact.get("name", None) appointment.customer_phone_number = contact.get("number", None) appointment.customer_skype = contact.get("skype", None) appointment.customer_details = contact.get("notes", None) appointment.customer_email = contact.get("email", None) - appointment.status = "Open" + appointment.created_through_portal = 1 appointment.insert(ignore_permissions=True) return appointment @@ -148,8 +152,23 @@ def convert_to_system_timezone(guest_tz, datetimeobject): return datetimeobject -def check_availabilty(timeslot, settings): - return frappe.db.count("Appointment", {"scheduled_time": timeslot}) < settings.number_of_agents +def get_booked_slot_times_for(timeslots, appointment_duration): + if not timeslots: + return [] + + from erpnext.crm.doctype.appointment.appointment import get_booked_slot_times + + duration = datetime.timedelta(minutes=appointment_duration) + return get_booked_slot_times(min(timeslots) - duration, max(timeslots) + duration) + + +def is_slot_available(timeslot, booked_times, settings): + # mirror the server capacity check: count non-Closed appointments whose + # duration window overlaps this slot, without a per-slot query + duration = datetime.timedelta(minutes=settings.appointment_duration) + lower, upper = timeslot - duration, timeslot + duration + overlapping = sum(1 for booked in booked_times if lower < booked < upper) + return overlapping < settings.number_of_agents def _is_holiday(date, holiday_list): diff --git a/erpnext/www/book_appointment/verify/index.html b/erpnext/www/book_appointment/verify/index.html index 58c07e85ccc..8e8a1096e5e 100644 --- a/erpnext/www/book_appointment/verify/index.html +++ b/erpnext/www/book_appointment/verify/index.html @@ -12,7 +12,7 @@ {% else %}