mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-04 22:18: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
|
import frappe
|
||||||
from frappe import _, throw
|
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
|
from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
@@ -400,6 +400,7 @@ def validate_account_number(name, account_number, company):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def update_account_number(name, account_name, account_number=None, from_descendant=False):
|
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)
|
account = frappe.db.get_value("Account", name, "company", as_dict=True)
|
||||||
if not account:
|
if not account:
|
||||||
return
|
return
|
||||||
@@ -461,6 +462,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def merge_account(old, new):
|
def merge_account(old, new):
|
||||||
|
_ensure_idle_system()
|
||||||
# Validate properties before merging
|
# Validate properties before merging
|
||||||
new_account = frappe.get_cached_doc("Account", new)
|
new_account = frappe.get_cached_doc("Account", new)
|
||||||
old_account = frappe.get_cached_doc("Account", old)
|
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):
|
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)
|
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
|
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 paid_amount > total_negative_outstanding:
|
||||||
if total_negative_outstanding == 0:
|
if total_negative_outstanding == 0:
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
|
|||||||
@@ -1623,6 +1623,5 @@
|
|||||||
"states": [],
|
"states": [],
|
||||||
"timeline_field": "customer",
|
"timeline_field": "customer",
|
||||||
"title_field": "title",
|
"title_field": "title",
|
||||||
"track_changes": 1,
|
"track_changes": 1
|
||||||
"track_seen": 1
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,17 +13,15 @@
|
|||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldname": "voucher_type",
|
"fieldname": "voucher_type",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Data",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Voucher Type",
|
"label": "Voucher Type"
|
||||||
"options": "DocType"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "voucher_name",
|
"fieldname": "voucher_name",
|
||||||
"fieldtype": "Dynamic Link",
|
"fieldtype": "Data",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Voucher Name",
|
"label": "Voucher Name"
|
||||||
"options": "voucher_type"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "taxable_amount",
|
"fieldname": "taxable_amount",
|
||||||
@@ -36,7 +34,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-01-13 13:40:41.479208",
|
"modified": "2025-02-05 16:39:14.863698",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Tax Withheld Vouchers",
|
"name": "Tax Withheld Vouchers",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ def get_report_filters(report_filters):
|
|||||||
["Purchase Invoice", "docstatus", "=", 1],
|
["Purchase Invoice", "docstatus", "=", 1],
|
||||||
["Purchase Invoice", "per_received", "<", 100],
|
["Purchase Invoice", "per_received", "<", 100],
|
||||||
["Purchase Invoice", "update_stock", "=", 0],
|
["Purchase Invoice", "update_stock", "=", 0],
|
||||||
|
["Purchase Invoice", "is_opening", "!=", "Yes"],
|
||||||
]
|
]
|
||||||
|
|
||||||
if report_filters.get("purchase_invoice"):
|
if report_filters.get("purchase_invoice"):
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ def get_actual_details(name, filters):
|
|||||||
and ba.account=gl.account
|
and ba.account=gl.account
|
||||||
and b.{budget_against} = gl.{budget_against}
|
and b.{budget_against} = gl.{budget_against}
|
||||||
and gl.fiscal_year between %s and %s
|
and gl.fiscal_year between %s and %s
|
||||||
|
and gl.is_cancelled = 0
|
||||||
and b.{budget_against} = %s
|
and b.{budget_against} = %s
|
||||||
and exists(
|
and exists(
|
||||||
select
|
select
|
||||||
|
|||||||
@@ -506,6 +506,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
|||||||
for dim in accounting_dimensions:
|
for dim in accounting_dimensions:
|
||||||
keylist.append(gle.get(dim))
|
keylist.append(gle.get(dim))
|
||||||
keylist.append(gle.get("cost_center"))
|
keylist.append(gle.get("cost_center"))
|
||||||
|
keylist.append(gle.get("project"))
|
||||||
|
|
||||||
key = tuple(keylist)
|
key = tuple(keylist)
|
||||||
if key not in consolidated_gle:
|
if key not in consolidated_gle:
|
||||||
@@ -617,10 +618,11 @@ def get_columns(filters):
|
|||||||
{"label": _("Against Account"), "fieldname": "against", "width": 120},
|
{"label": _("Against Account"), "fieldname": "against", "width": 120},
|
||||||
{"label": _("Party Type"), "fieldname": "party_type", "width": 100},
|
{"label": _("Party Type"), "fieldname": "party_type", "width": 100},
|
||||||
{"label": _("Party"), "fieldname": "party", "width": 100},
|
{"label": _("Party"), "fieldname": "party", "width": 100},
|
||||||
{"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
if filters.get("include_dimensions"):
|
if filters.get("include_dimensions"):
|
||||||
|
columns.append({"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100})
|
||||||
|
|
||||||
for dim in get_accounting_dimensions(as_list=False):
|
for dim in get_accounting_dimensions(as_list=False):
|
||||||
columns.append(
|
columns.append(
|
||||||
{"label": _(dim.label), "options": dim.label, "fieldname": dim.fieldname, "width": 100}
|
{"label": _(dim.label), "options": dim.label, "fieldname": dim.fieldname, "width": 100}
|
||||||
|
|||||||
@@ -801,7 +801,7 @@ class StockController(AccountsController):
|
|||||||
child_tab.item_code,
|
child_tab.item_code,
|
||||||
child_tab.qty,
|
child_tab.qty,
|
||||||
)
|
)
|
||||||
.where(parent_tab.docstatus < 2)
|
.where(parent_tab.docstatus == 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.doctype == "Purchase Invoice":
|
if self.doctype == "Purchase Invoice":
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ app_publisher = "Frappe Technologies Pvt. Ltd."
|
|||||||
app_description = """ERP made simple"""
|
app_description = """ERP made simple"""
|
||||||
app_icon = "fa fa-th"
|
app_icon = "fa fa-th"
|
||||||
app_color = "#e74c3c"
|
app_color = "#e74c3c"
|
||||||
app_email = "info@erpnext.com"
|
app_email = "hello@frappe.io"
|
||||||
app_license = "GNU General Public License (v3)"
|
app_license = "GNU General Public License (v3)"
|
||||||
source_link = "https://github.com/frappe/erpnext"
|
source_link = "https://github.com/frappe/erpnext"
|
||||||
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
|
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 = """
|
default_mail_footer = """
|
||||||
<span>
|
<span>
|
||||||
Sent via
|
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
|
ERPNext
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</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
|
erpnext.patches.v14_0.disable_add_row_in_gross_profit
|
||||||
execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment")
|
execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment")
|
||||||
erpnext.patches.v14_0.update_posting_datetime
|
erpnext.patches.v14_0.update_posting_datetime
|
||||||
|
erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{{ address_line1 }}<br>
|
{{ address_line1 }}<br>
|
||||||
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
|
{% 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 -%}
|
{% if country != "United States" %}{{ country }}{% endif -%}
|
||||||
|
|||||||
@@ -275,12 +275,12 @@ def get_past_order_list(search_term, status, limit=20):
|
|||||||
invoice_list = []
|
invoice_list = []
|
||||||
|
|
||||||
if search_term and status:
|
if search_term and status:
|
||||||
invoices_by_customer = frappe.db.get_all(
|
invoices_by_customer = frappe.db.get_list(
|
||||||
"POS Invoice",
|
"POS Invoice",
|
||||||
filters={"customer": ["like", f"%{search_term}%"], "status": status},
|
filters={"customer": ["like", f"%{search_term}%"], "status": status},
|
||||||
fields=fields,
|
fields=fields,
|
||||||
)
|
)
|
||||||
invoices_by_name = frappe.db.get_all(
|
invoices_by_name = frappe.db.get_list(
|
||||||
"POS Invoice",
|
"POS Invoice",
|
||||||
filters={"name": ["like", f"%{search_term}%"], "status": status},
|
filters={"name": ["like", f"%{search_term}%"], "status": status},
|
||||||
fields=fields,
|
fields=fields,
|
||||||
@@ -288,7 +288,7 @@ def get_past_order_list(search_term, status, limit=20):
|
|||||||
|
|
||||||
invoice_list = invoices_by_customer + invoices_by_name
|
invoice_list = invoices_by_customer + invoices_by_name
|
||||||
elif status:
|
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
|
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
|
from .default_success_action import get_default_success_action
|
||||||
|
|
||||||
default_mail_footer = """<div style="padding: 7px; text-align: right; color: #888"><small>Sent via
|
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():
|
def after_install():
|
||||||
|
|||||||
@@ -196,8 +196,17 @@ class InventoryDimension(Document):
|
|||||||
custom_fields["Stock Ledger Entry"] = dimension_field
|
custom_fields["Stock Ledger Entry"] = dimension_field
|
||||||
|
|
||||||
filter_custom_fields = {}
|
filter_custom_fields = {}
|
||||||
|
|
||||||
|
ignore_doctypes = [
|
||||||
|
"Pick List Item",
|
||||||
|
"Maintenance Visit Purpose",
|
||||||
|
]
|
||||||
|
|
||||||
if custom_fields:
|
if custom_fields:
|
||||||
for doctype, fields in custom_fields.items():
|
for doctype, fields in custom_fields.items():
|
||||||
|
if doctype in ignore_doctypes:
|
||||||
|
continue
|
||||||
|
|
||||||
if isinstance(fields, dict):
|
if isinstance(fields, dict):
|
||||||
fields = [fields]
|
fields = [fields]
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on("Pick List", {
|
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) => {
|
setup: (frm) => {
|
||||||
frm.set_indicator_formatter("item_code", function (doc) {
|
frm.set_indicator_formatter("item_code", function (doc) {
|
||||||
return doc.stock_qty === 0 ? "red" : "green";
|
return doc.stock_qty === 0 ? "red" : "green";
|
||||||
|
|||||||
@@ -25,10 +25,38 @@ from erpnext.stock.get_item_details import get_conversion_factor
|
|||||||
|
|
||||||
class PickList(Document):
|
class PickList(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
self.validate_expired_batches()
|
||||||
self.validate_for_qty()
|
self.validate_for_qty()
|
||||||
self.validate_stock_qty()
|
self.validate_stock_qty()
|
||||||
self.check_serial_no_status()
|
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):
|
def before_save(self):
|
||||||
self.update_status()
|
self.update_status()
|
||||||
if not self.pick_manually:
|
if not self.pick_manually:
|
||||||
@@ -271,6 +299,7 @@ class PickList(Document):
|
|||||||
self.remove(row)
|
self.remove(row)
|
||||||
|
|
||||||
updated_locations = frappe._dict()
|
updated_locations = frappe._dict()
|
||||||
|
len_idx = len(self.get("locations")) or 0
|
||||||
for item_doc in items:
|
for item_doc in items:
|
||||||
item_code = item_doc.item_code
|
item_code = item_doc.item_code
|
||||||
|
|
||||||
@@ -313,6 +342,8 @@ class PickList(Document):
|
|||||||
if location.picked_qty > location.stock_qty:
|
if location.picked_qty > location.stock_qty:
|
||||||
location.picked_qty = location.stock_qty
|
location.picked_qty = location.stock_qty
|
||||||
|
|
||||||
|
len_idx += 1
|
||||||
|
location.idx = len_idx
|
||||||
self.append("locations", location)
|
self.append("locations", location)
|
||||||
|
|
||||||
# If table is empty on update after submit, set stock_qty, picked_qty to 0 so that indicator is red
|
# 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:
|
for location in locations_replica:
|
||||||
location.stock_qty = 0
|
location.stock_qty = 0
|
||||||
location.picked_qty = 0
|
location.picked_qty = 0
|
||||||
|
|
||||||
|
len_idx += 1
|
||||||
|
location.idx = len_idx
|
||||||
self.append("locations", location)
|
self.append("locations", location)
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
_(
|
_(
|
||||||
@@ -430,9 +464,11 @@ class PickList(Document):
|
|||||||
pi_item.item_code,
|
pi_item.item_code,
|
||||||
pi_item.warehouse,
|
pi_item.warehouse,
|
||||||
pi_item.batch_no,
|
pi_item.batch_no,
|
||||||
Sum(Case().when(pi_item.picked_qty > 0, pi_item.picked_qty).else_(pi_item.stock_qty)).as_(
|
Sum(
|
||||||
"picked_qty"
|
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"),
|
Replace(GROUP_CONCAT(pi_item.serial_no), ",", "\n").as_("serial_no"),
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
@@ -465,8 +501,32 @@ class PickList(Document):
|
|||||||
else:
|
else:
|
||||||
picked_items[item_data.item_code][key] = data
|
picked_items[item_data.item_code][key] = data
|
||||||
|
|
||||||
|
self.update_picked_item_from_current_pick_list(picked_items)
|
||||||
|
|
||||||
return 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]:
|
def _get_product_bundles(self) -> dict[str, str]:
|
||||||
# Dict[so_item_row: item_code]
|
# Dict[so_item_row: item_code]
|
||||||
product_bundles = {}
|
product_bundles = {}
|
||||||
|
|||||||
@@ -865,15 +865,19 @@ def get_billed_amount_against_po(po_items):
|
|||||||
if not po_items:
|
if not po_items:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
|
||||||
purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
|
purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(purchase_invoice_item)
|
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)
|
.select(fn.Sum(purchase_invoice_item.amount).as_("billed_amt"), purchase_invoice_item.po_detail)
|
||||||
.where(
|
.where(
|
||||||
(purchase_invoice_item.po_detail.isin(po_items))
|
(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_item.pr_detail.isnull())
|
||||||
|
& (purchase_invoice.update_stock == 0)
|
||||||
)
|
)
|
||||||
.groupby(purchase_invoice_item.po_detail)
|
.groupby(purchase_invoice_item.po_detail)
|
||||||
).run(as_dict=1)
|
).run(as_dict=1)
|
||||||
|
|||||||
@@ -2731,6 +2731,36 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
self.assertEqual(return_pr.per_billed, 100)
|
self.assertEqual(return_pr.per_billed, 100)
|
||||||
self.assertEqual(return_pr.status, "Completed")
|
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():
|
def prepare_data_for_internal_transfer():
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
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",
|
"type": "git",
|
||||||
"url": "git+https://github.com/frappe/erpnext.git"
|
"url": "git+https://github.com/frappe/erpnext.git"
|
||||||
},
|
},
|
||||||
"homepage": "https://erpnext.com",
|
"homepage": "https://frappe.io/erpnext",
|
||||||
"author": "Frappe Technologies Pvt. Ltd.",
|
"author": "Frappe Technologies Pvt. Ltd.",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
|||||||
Reference in New Issue
Block a user