mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-01 04:28:27 +00:00
Merge pull request #46264 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, throw
|
||||
from frappe.utils import cint, cstr
|
||||
from frappe.utils import add_to_date, cint, cstr, pretty_date
|
||||
from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of
|
||||
|
||||
import erpnext
|
||||
@@ -400,6 +400,7 @@ def validate_account_number(name, account_number, company):
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_account_number(name, account_name, account_number=None, from_descendant=False):
|
||||
_ensure_idle_system()
|
||||
account = frappe.db.get_value("Account", name, "company", as_dict=True)
|
||||
if not account:
|
||||
return
|
||||
@@ -461,6 +462,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
|
||||
@frappe.whitelist()
|
||||
def merge_account(old, new):
|
||||
_ensure_idle_system()
|
||||
# Validate properties before merging
|
||||
new_account = frappe.get_cached_doc("Account", new)
|
||||
old_account = frappe.get_cached_doc("Account", old)
|
||||
@@ -514,3 +516,27 @@ def sync_update_account_number_in_child(
|
||||
|
||||
for d in frappe.db.get_values("Account", filters=filters, fieldname=["company", "name"], as_dict=True):
|
||||
update_account_number(d["name"], account_name, account_number, from_descendant=True)
|
||||
|
||||
|
||||
def _ensure_idle_system():
|
||||
# Don't allow renaming if accounting entries are actively being updated, there are two main reasons:
|
||||
# 1. Correctness: It's next to impossible to ensure that renamed account is not being used *right now*.
|
||||
# 2. Performance: Renaming requires locking out many tables entirely and severely degrades performance.
|
||||
|
||||
if frappe.flags.in_test:
|
||||
return
|
||||
|
||||
try:
|
||||
# We also lock inserts to GL entry table with for_update here.
|
||||
last_gl_update = frappe.db.get_value("GL Entry", {}, "modified", for_update=True, wait=False)
|
||||
except frappe.QueryTimeoutError:
|
||||
# wait=False fails immediately if there's an active transaction.
|
||||
last_gl_update = add_to_date(None, seconds=-1)
|
||||
|
||||
if last_gl_update > add_to_date(None, minutes=-5):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Last GL Entry update was done {}. This operation is not allowed while system is actively being used. Please wait for 5 minutes before retrying."
|
||||
).format(pretty_date(last_gl_update)),
|
||||
title=_("System In Use"),
|
||||
)
|
||||
|
||||
@@ -1519,7 +1519,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
|
||||
|
||||
elif self.party_type in ("Supplier", "Employee"):
|
||||
elif self.party_type in ("Supplier", "Customer"):
|
||||
if paid_amount > total_negative_outstanding:
|
||||
if total_negative_outstanding == 0:
|
||||
frappe.msgprint(
|
||||
|
||||
@@ -1623,6 +1623,5 @@
|
||||
"states": [],
|
||||
"timeline_field": "customer",
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
"track_changes": 1
|
||||
}
|
||||
|
||||
@@ -13,17 +13,15 @@
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher Type",
|
||||
"options": "DocType"
|
||||
"label": "Voucher Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher Name",
|
||||
"options": "voucher_type"
|
||||
"label": "Voucher Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "taxable_amount",
|
||||
@@ -36,7 +34,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-01-13 13:40:41.479208",
|
||||
"modified": "2025-02-05 16:39:14.863698",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withheld Vouchers",
|
||||
|
||||
@@ -27,6 +27,7 @@ def get_report_filters(report_filters):
|
||||
["Purchase Invoice", "docstatus", "=", 1],
|
||||
["Purchase Invoice", "per_received", "<", 100],
|
||||
["Purchase Invoice", "update_stock", "=", 0],
|
||||
["Purchase Invoice", "is_opening", "!=", "Yes"],
|
||||
]
|
||||
|
||||
if report_filters.get("purchase_invoice"):
|
||||
|
||||
@@ -263,6 +263,7 @@ def get_actual_details(name, filters):
|
||||
and ba.account=gl.account
|
||||
and b.{budget_against} = gl.{budget_against}
|
||||
and gl.fiscal_year between %s and %s
|
||||
and gl.is_cancelled = 0
|
||||
and b.{budget_against} = %s
|
||||
and exists(
|
||||
select
|
||||
|
||||
@@ -506,6 +506,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
for dim in accounting_dimensions:
|
||||
keylist.append(gle.get(dim))
|
||||
keylist.append(gle.get("cost_center"))
|
||||
keylist.append(gle.get("project"))
|
||||
|
||||
key = tuple(keylist)
|
||||
if key not in consolidated_gle:
|
||||
@@ -617,10 +618,11 @@ def get_columns(filters):
|
||||
{"label": _("Against Account"), "fieldname": "against", "width": 120},
|
||||
{"label": _("Party Type"), "fieldname": "party_type", "width": 100},
|
||||
{"label": _("Party"), "fieldname": "party", "width": 100},
|
||||
{"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100},
|
||||
]
|
||||
|
||||
if filters.get("include_dimensions"):
|
||||
columns.append({"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100})
|
||||
|
||||
for dim in get_accounting_dimensions(as_list=False):
|
||||
columns.append(
|
||||
{"label": _(dim.label), "options": dim.label, "fieldname": dim.fieldname, "width": 100}
|
||||
|
||||
@@ -801,7 +801,7 @@ class StockController(AccountsController):
|
||||
child_tab.item_code,
|
||||
child_tab.qty,
|
||||
)
|
||||
.where(parent_tab.docstatus < 2)
|
||||
.where(parent_tab.docstatus == 1)
|
||||
)
|
||||
|
||||
if self.doctype == "Purchase Invoice":
|
||||
|
||||
@@ -4,7 +4,7 @@ app_publisher = "Frappe Technologies Pvt. Ltd."
|
||||
app_description = """ERP made simple"""
|
||||
app_icon = "fa fa-th"
|
||||
app_color = "#e74c3c"
|
||||
app_email = "info@erpnext.com"
|
||||
app_email = "hello@frappe.io"
|
||||
app_license = "GNU General Public License (v3)"
|
||||
source_link = "https://github.com/frappe/erpnext"
|
||||
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
|
||||
@@ -479,7 +479,7 @@ email_brand_image = "assets/erpnext/images/erpnext-logo.jpg"
|
||||
default_mail_footer = """
|
||||
<span>
|
||||
Sent via
|
||||
<a class="text-muted" href="https://erpnext.com?source=via_email_footer" target="_blank">
|
||||
<a class="text-muted" href="https://frappe.io/erpnext?source=via_email_footer" target="_blank">
|
||||
ERPNext
|
||||
</a>
|
||||
</span>
|
||||
|
||||
@@ -371,4 +371,4 @@ erpnext.patches.v14_0.update_stock_uom_in_work_order_item
|
||||
erpnext.patches.v14_0.disable_add_row_in_gross_profit
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment")
|
||||
erpnext.patches.v14_0.update_posting_datetime
|
||||
|
||||
erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{ address_line1 }}<br>
|
||||
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
|
||||
{{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}<br>{% endif -%}
|
||||
{{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}{% endif -%}<br>
|
||||
{% if country != "United States" %}{{ country }}{% endif -%}
|
||||
|
||||
@@ -275,12 +275,12 @@ def get_past_order_list(search_term, status, limit=20):
|
||||
invoice_list = []
|
||||
|
||||
if search_term and status:
|
||||
invoices_by_customer = frappe.db.get_all(
|
||||
invoices_by_customer = frappe.db.get_list(
|
||||
"POS Invoice",
|
||||
filters={"customer": ["like", f"%{search_term}%"], "status": status},
|
||||
fields=fields,
|
||||
)
|
||||
invoices_by_name = frappe.db.get_all(
|
||||
invoices_by_name = frappe.db.get_list(
|
||||
"POS Invoice",
|
||||
filters={"name": ["like", f"%{search_term}%"], "status": status},
|
||||
fields=fields,
|
||||
@@ -288,7 +288,7 @@ def get_past_order_list(search_term, status, limit=20):
|
||||
|
||||
invoice_list = invoices_by_customer + invoices_by_name
|
||||
elif status:
|
||||
invoice_list = frappe.db.get_all("POS Invoice", filters={"status": status}, fields=fields)
|
||||
invoice_list = frappe.db.get_list("POS Invoice", filters={"status": status}, fields=fields)
|
||||
|
||||
return invoice_list
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from erpnext.setup.doctype.incoterm.incoterm import create_incoterms
|
||||
from .default_success_action import get_default_success_action
|
||||
|
||||
default_mail_footer = """<div style="padding: 7px; text-align: right; color: #888"><small>Sent via
|
||||
<a style="color: #888" href="http://erpnext.org">ERPNext</a></div>"""
|
||||
<a style="color: #888" href="http://frappe.io/erpnext">ERPNext</a></div>"""
|
||||
|
||||
|
||||
def after_install():
|
||||
|
||||
@@ -196,8 +196,17 @@ class InventoryDimension(Document):
|
||||
custom_fields["Stock Ledger Entry"] = dimension_field
|
||||
|
||||
filter_custom_fields = {}
|
||||
|
||||
ignore_doctypes = [
|
||||
"Pick List Item",
|
||||
"Maintenance Visit Purpose",
|
||||
]
|
||||
|
||||
if custom_fields:
|
||||
for doctype, fields in custom_fields.items():
|
||||
if doctype in ignore_doctypes:
|
||||
continue
|
||||
|
||||
if isinstance(fields, dict):
|
||||
fields = [fields]
|
||||
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Pick List", {
|
||||
after_save(frm) {
|
||||
setTimeout(() => {
|
||||
// Added to fix the issue of locations table not getting updated after save
|
||||
frm.reload_doc();
|
||||
}, 500);
|
||||
},
|
||||
|
||||
setup: (frm) => {
|
||||
frm.set_indicator_formatter("item_code", function (doc) {
|
||||
return doc.stock_qty === 0 ? "red" : "green";
|
||||
|
||||
@@ -25,10 +25,38 @@ from erpnext.stock.get_item_details import get_conversion_factor
|
||||
|
||||
class PickList(Document):
|
||||
def validate(self):
|
||||
self.validate_expired_batches()
|
||||
self.validate_for_qty()
|
||||
self.validate_stock_qty()
|
||||
self.check_serial_no_status()
|
||||
|
||||
def validate_expired_batches(self):
|
||||
batches = []
|
||||
for row in self.get("locations"):
|
||||
if row.get("batch_no") and row.get("picked_qty"):
|
||||
batches.append(row.batch_no)
|
||||
|
||||
if batches:
|
||||
batch = frappe.qb.DocType("Batch")
|
||||
query = (
|
||||
frappe.qb.from_(batch)
|
||||
.select(batch.name)
|
||||
.where(
|
||||
(batch.name.isin(batches))
|
||||
& (batch.expiry_date <= frappe.utils.nowdate())
|
||||
& (batch.expiry_date.isnotnull())
|
||||
)
|
||||
)
|
||||
|
||||
expired_batches = query.run(as_dict=True)
|
||||
if expired_batches:
|
||||
msg = "<ul>" + "".join(f"<li>{batch.name}</li>" for batch in expired_batches) + "</ul>"
|
||||
|
||||
frappe.throw(
|
||||
_("The following batches are expired, please restock them: <br> {0}").format(msg),
|
||||
title=_("Expired Batches"),
|
||||
)
|
||||
|
||||
def before_save(self):
|
||||
self.update_status()
|
||||
if not self.pick_manually:
|
||||
@@ -271,6 +299,7 @@ class PickList(Document):
|
||||
self.remove(row)
|
||||
|
||||
updated_locations = frappe._dict()
|
||||
len_idx = len(self.get("locations")) or 0
|
||||
for item_doc in items:
|
||||
item_code = item_doc.item_code
|
||||
|
||||
@@ -313,6 +342,8 @@ class PickList(Document):
|
||||
if location.picked_qty > location.stock_qty:
|
||||
location.picked_qty = location.stock_qty
|
||||
|
||||
len_idx += 1
|
||||
location.idx = len_idx
|
||||
self.append("locations", location)
|
||||
|
||||
# If table is empty on update after submit, set stock_qty, picked_qty to 0 so that indicator is red
|
||||
@@ -321,6 +352,9 @@ class PickList(Document):
|
||||
for location in locations_replica:
|
||||
location.stock_qty = 0
|
||||
location.picked_qty = 0
|
||||
|
||||
len_idx += 1
|
||||
location.idx = len_idx
|
||||
self.append("locations", location)
|
||||
frappe.msgprint(
|
||||
_(
|
||||
@@ -430,9 +464,11 @@ class PickList(Document):
|
||||
pi_item.item_code,
|
||||
pi_item.warehouse,
|
||||
pi_item.batch_no,
|
||||
Sum(Case().when(pi_item.picked_qty > 0, pi_item.picked_qty).else_(pi_item.stock_qty)).as_(
|
||||
"picked_qty"
|
||||
),
|
||||
Sum(
|
||||
Case()
|
||||
.when((pi_item.picked_qty > 0) & (pi_item.docstatus == 1), pi_item.picked_qty)
|
||||
.else_(pi_item.stock_qty)
|
||||
).as_("picked_qty"),
|
||||
Replace(GROUP_CONCAT(pi_item.serial_no), ",", "\n").as_("serial_no"),
|
||||
)
|
||||
.where(
|
||||
@@ -465,8 +501,32 @@ class PickList(Document):
|
||||
else:
|
||||
picked_items[item_data.item_code][key] = data
|
||||
|
||||
self.update_picked_item_from_current_pick_list(picked_items)
|
||||
|
||||
return picked_items
|
||||
|
||||
def update_picked_item_from_current_pick_list(self, picked_items):
|
||||
for row in self.get("locations"):
|
||||
if flt(row.picked_qty) > 0:
|
||||
key = (row.warehouse, row.batch_no) if row.batch_no else row.warehouse
|
||||
serial_no = [x for x in row.serial_no.split("\n") if x] if row.serial_no else None
|
||||
if row.item_code not in picked_items:
|
||||
picked_items[row.item_code] = {}
|
||||
|
||||
if key not in picked_items[row.item_code]:
|
||||
picked_items[row.item_code][key] = frappe._dict(
|
||||
{
|
||||
"picked_qty": 0,
|
||||
"serial_no": [],
|
||||
"batch_no": row.batch_no or "",
|
||||
"warehouse": row.warehouse,
|
||||
}
|
||||
)
|
||||
|
||||
picked_items[row.item_code][key]["picked_qty"] += flt(row.stock_qty) or flt(row.picked_qty)
|
||||
if serial_no:
|
||||
picked_items[row.item_code][key]["serial_no"].extend(serial_no)
|
||||
|
||||
def _get_product_bundles(self) -> dict[str, str]:
|
||||
# Dict[so_item_row: item_code]
|
||||
product_bundles = {}
|
||||
|
||||
@@ -865,15 +865,19 @@ def get_billed_amount_against_po(po_items):
|
||||
if not po_items:
|
||||
return {}
|
||||
|
||||
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
|
||||
purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(purchase_invoice_item)
|
||||
.inner_join(purchase_invoice)
|
||||
.on(purchase_invoice_item.parent == purchase_invoice.name)
|
||||
.select(fn.Sum(purchase_invoice_item.amount).as_("billed_amt"), purchase_invoice_item.po_detail)
|
||||
.where(
|
||||
(purchase_invoice_item.po_detail.isin(po_items))
|
||||
& (purchase_invoice_item.docstatus == 1)
|
||||
& (purchase_invoice.docstatus == 1)
|
||||
& (purchase_invoice_item.pr_detail.isnull())
|
||||
& (purchase_invoice.update_stock == 0)
|
||||
)
|
||||
.groupby(purchase_invoice_item.po_detail)
|
||||
).run(as_dict=1)
|
||||
|
||||
@@ -2731,6 +2731,36 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
self.assertEqual(return_pr.per_billed, 100)
|
||||
self.assertEqual(return_pr.status, "Completed")
|
||||
|
||||
def test_pr_status_based_on_invoices_with_update_stock(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||
make_purchase_invoice as _make_purchase_invoice,
|
||||
)
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||
make_purchase_receipt as _make_purchase_receipt,
|
||||
)
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_pr_against_po,
|
||||
create_purchase_order,
|
||||
)
|
||||
|
||||
item_code = "Test Item for PR Status Based on Invoices"
|
||||
create_item(item_code)
|
||||
|
||||
po = create_purchase_order(item_code=item_code, qty=10)
|
||||
pi = _make_purchase_invoice(po.name)
|
||||
pi.update_stock = 1
|
||||
pi.items[0].qty = 5
|
||||
pi.submit()
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.per_billed, 50)
|
||||
|
||||
pr = _make_purchase_receipt(po.name)
|
||||
self.assertEqual(pr.items[0].qty, 5)
|
||||
pr.submit()
|
||||
pr.reload()
|
||||
self.assertEqual(pr.status, "To Bill")
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import (
|
||||
on_doctype_update as create_sle_indexes,
|
||||
)
|
||||
|
||||
|
||||
def execute():
|
||||
"""Ensure SLE Indexes"""
|
||||
|
||||
create_sle_indexes()
|
||||
@@ -1 +1 @@
|
||||
{{ _("Powered by {0}").format('<a href="https://erpnext.com?source=website_footer" target="_blank" class="text-muted">ERPNext</a>') }}
|
||||
{{ _("Powered by {0}").format('<a href="https://frappe.io/erpnext?source=website_footer" target="_blank" class="text-muted">ERPNext</a>') }}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/frappe/erpnext.git"
|
||||
},
|
||||
"homepage": "https://erpnext.com",
|
||||
"homepage": "https://frappe.io/erpnext",
|
||||
"author": "Frappe Technologies Pvt. Ltd.",
|
||||
"license": "GPL-3.0",
|
||||
"bugs": {
|
||||
|
||||
Reference in New Issue
Block a user