mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-18 19:07:55 +00:00
Merge upstream/develop into erpnext-refactoring
Resolve 4 conflicts from Phase 7 service/mapper extraction vs upstream: - asset.py: take extraction; repoint dangling make_asset_movement JS to mapper - job_card: port upstream field_no_map(naming_series) into mapper.make_subcontracting_po - sales_order: port upstream rows-index fix into mapper.make_delivery_note - sales_invoice (Phase 7): take service delegations; port upstream SQL->QB/ORM changes for get_warehouse, get_all_mode_of_payments, get_discounting_status, clear_unallocated_mode_of_payments, and set_pos_fields(POS DN skip) into services
This commit is contained in:
@@ -9,11 +9,10 @@ import frappe
|
||||
from frappe import qb, scrub
|
||||
from frappe.desk.reportview import get_filters_cond, get_match_cond
|
||||
from frappe.permissions import has_permission
|
||||
from frappe.query_builder import Case, Criterion, DocType, Field
|
||||
from frappe.query_builder import Case, Criterion, DocType
|
||||
from frappe.query_builder.functions import Concat, CustomFunction, Length, Locate, Substring, Sum
|
||||
from frappe.utils import nowdate, today, unique
|
||||
from pypika import Order
|
||||
from pypika.terms import LiteralValue
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.utils import build_qb_match_conditions
|
||||
@@ -433,10 +432,15 @@ def get_delivery_notes_to_be_billed(
|
||||
.where((DeliveryNote.docstatus == 1) & (DeliveryNote.is_return == 0) & (DeliveryNote.per_billed > 0))
|
||||
)
|
||||
|
||||
query = frappe.qb.get_query(
|
||||
"Delivery Note",
|
||||
fields=fields,
|
||||
filters=filters,
|
||||
ignore_permissions=False,
|
||||
)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(DeliveryNote)
|
||||
.select(*[DeliveryNote[f] for f in fields])
|
||||
.where(
|
||||
query.where(
|
||||
(DeliveryNote.docstatus == 1)
|
||||
& (DeliveryNote.status.notin(["Stopped", "Closed"]))
|
||||
& (DeliveryNote[searchfield].like(f"%{txt}%"))
|
||||
@@ -450,12 +454,11 @@ def get_delivery_notes_to_be_billed(
|
||||
)
|
||||
)
|
||||
)
|
||||
.orderby(DeliveryNote[searchfield], order=Order.asc)
|
||||
.limit(page_len)
|
||||
.offset(start)
|
||||
)
|
||||
if filters and isinstance(filters, dict):
|
||||
for key, value in filters.items():
|
||||
query = query.where(DeliveryNote[key] == value)
|
||||
|
||||
query = query.orderby(DeliveryNote[searchfield], order=Order.asc).limit(page_len).offset(start)
|
||||
return query.run(as_dict=as_dict)
|
||||
|
||||
|
||||
|
||||
@@ -262,15 +262,17 @@ class StatusUpdater(Document):
|
||||
|
||||
def validate_qty(self):
|
||||
"""Validates qty at row level"""
|
||||
self.item_allowance = {}
|
||||
self.global_qty_allowance = None
|
||||
self.global_amount_allowance = None
|
||||
|
||||
for args in self.status_updater:
|
||||
if "target_ref_field" not in args or args.get("validate_qty") is False:
|
||||
# if target_ref_field is not specified or validate_qty is explicitly set to False, skip validation
|
||||
continue
|
||||
|
||||
# Reset per-args so each config block uses its own allowance source without
|
||||
# leaking cached values from a previous config block.
|
||||
self.item_allowance = {}
|
||||
self.global_qty_allowance = None
|
||||
self.global_amount_allowance = None
|
||||
|
||||
items_to_validate = []
|
||||
selling_negative_rate_allowed = frappe.get_single_value(
|
||||
"Selling Settings", "allow_negative_rates_for_items"
|
||||
@@ -402,9 +404,12 @@ class StatusUpdater(Document):
|
||||
|
||||
def check_overflow_with_allowance(self, item, args):
|
||||
"""
|
||||
Checks if there is overflow condering a relaxation allowance
|
||||
Checks if there is overflow considering a relaxation allowance.
|
||||
"""
|
||||
qty_or_amount = "qty" if "qty" in args["target_ref_field"] else "amount"
|
||||
global_qty_allowance_field = args.get("global_allowance_field", "over_delivery_receipt_allowance")
|
||||
global_qty_allowance_doctype = args.get("global_allowance_doctype", "Stock Settings")
|
||||
item_qty_allowance_field = args.get("item_allowance_field", "over_delivery_receipt_allowance")
|
||||
|
||||
# check if overflow is within allowance
|
||||
(
|
||||
@@ -419,6 +424,9 @@ class StatusUpdater(Document):
|
||||
self.global_qty_allowance,
|
||||
self.global_amount_allowance,
|
||||
qty_or_amount,
|
||||
global_qty_allowance_field,
|
||||
global_qty_allowance_doctype,
|
||||
item_qty_allowance_field,
|
||||
)
|
||||
if args["source_dt"] != "Pick List Item"
|
||||
else (0, {}, None, None)
|
||||
@@ -463,7 +471,9 @@ class StatusUpdater(Document):
|
||||
"Quotation Item",
|
||||
"Packed Item",
|
||||
]:
|
||||
if qty_or_amount == "qty":
|
||||
if args.get("target_dt") == "Material Request Item":
|
||||
action_msg = _('To allow over ordering, update "Over Order Allowance" in Buying Settings.')
|
||||
elif qty_or_amount == "qty":
|
||||
action_msg = _(
|
||||
'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.'
|
||||
)
|
||||
@@ -724,16 +734,28 @@ class StatusUpdater(Document):
|
||||
ref_doc.set_status(update=True)
|
||||
|
||||
|
||||
@frappe.request_cache
|
||||
def get_allowance_for(
|
||||
item_code,
|
||||
item_allowance=None,
|
||||
global_qty_allowance=None,
|
||||
global_amount_allowance=None,
|
||||
qty_or_amount="qty",
|
||||
global_qty_allowance_field="over_delivery_receipt_allowance",
|
||||
global_qty_allowance_doctype="Stock Settings",
|
||||
item_qty_allowance_field="over_delivery_receipt_allowance",
|
||||
):
|
||||
"""
|
||||
Returns the allowance for the item, if not set, returns global allowance
|
||||
Returns the allowance for the item, if not set, returns global allowance.
|
||||
|
||||
Args:
|
||||
item_code: The item to get allowance for.
|
||||
item_allowance: Cached per-item allowances from a previous call.
|
||||
global_qty_allowance: Cached global qty allowance from a previous call.
|
||||
global_amount_allowance: Cached global amount allowance from a previous call.
|
||||
qty_or_amount: Whether to return qty or amount allowance.
|
||||
global_qty_allowance_field: The field name on the settings doctype to use for the global qty allowance.
|
||||
global_qty_allowance_doctype: The settings doctype to read the global qty allowance from.
|
||||
item_qty_allowance_field: The field name on the Item doctype to use for the item-level qty allowance override.
|
||||
"""
|
||||
if item_allowance is None:
|
||||
item_allowance = {}
|
||||
@@ -755,13 +777,13 @@ def get_allowance_for(
|
||||
)
|
||||
|
||||
qty_allowance, over_billing_allowance = frappe.get_cached_value(
|
||||
"Item", item_code, ["over_delivery_receipt_allowance", "over_billing_allowance"]
|
||||
"Item", item_code, [item_qty_allowance_field, "over_billing_allowance"]
|
||||
)
|
||||
|
||||
if qty_or_amount == "qty" and not qty_allowance:
|
||||
if global_qty_allowance is None:
|
||||
global_qty_allowance = flt(
|
||||
frappe.get_cached_value("Stock Settings", None, "over_delivery_receipt_allowance")
|
||||
frappe.get_single_value(global_qty_allowance_doctype, global_qty_allowance_field)
|
||||
)
|
||||
qty_allowance = global_qty_allowance
|
||||
elif qty_or_amount == "amount" and not over_billing_allowance:
|
||||
|
||||
@@ -1311,7 +1311,7 @@ class StockController(AccountsController):
|
||||
elif self.doctype == "Stock Entry" and row.t_warehouse:
|
||||
qi_required = True # inward stock needs inspection
|
||||
|
||||
if row.get("type") or row.get("is_legacy_scrap_item"):
|
||||
if row.get("secondary_item_type") or row.get("is_legacy_scrap_item"):
|
||||
continue
|
||||
|
||||
if qi_required: # validate row only if inspection is required on item level
|
||||
@@ -1979,7 +1979,7 @@ def repost_required_for_queue(doc: StockController) -> bool:
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_item_quality_inspection(doctype: str, items: str | list[dict]):
|
||||
def check_item_quality_inspection(doctype: str, docstatus: str | int, items: str | list[dict]):
|
||||
if isinstance(items, str):
|
||||
items = json.loads(items)
|
||||
|
||||
@@ -1991,13 +1991,30 @@ def check_item_quality_inspection(doctype: str, items: str | list[dict]):
|
||||
"Delivery Note": "inspection_required_before_delivery",
|
||||
}
|
||||
|
||||
items_to_remove = []
|
||||
for item in items:
|
||||
if not frappe.db.get_value("Item", item.get("item_code"), inspection_fieldname_map.get(doctype)):
|
||||
items_to_remove.append(item)
|
||||
items = [item for item in items if item not in items_to_remove]
|
||||
inspection_fieldname = inspection_fieldname_map.get(doctype)
|
||||
if inspection_fieldname is None:
|
||||
return []
|
||||
|
||||
return items
|
||||
allow_after_transaction = cint(docstatus) == 1 and frappe.get_single_value(
|
||||
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
|
||||
)
|
||||
|
||||
if allow_after_transaction:
|
||||
return items
|
||||
|
||||
item_codes = list({item.get("item_code") for item in items})
|
||||
|
||||
Item = frappe.qb.DocType("Item")
|
||||
results = (
|
||||
frappe.qb.from_(Item)
|
||||
.select(Item.name)
|
||||
.where((Item.name.isin(item_codes)) & (Item[inspection_fieldname] == 1))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
inspection_required_items = {row.name for row in results}
|
||||
|
||||
return [item for item in items if item.get("item_code") in inspection_required_items]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -7,6 +7,7 @@ from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import cint, flt, get_link_to_form
|
||||
|
||||
@@ -150,7 +151,7 @@ class SubcontractingController(StockController):
|
||||
).format(item.idx, get_link_to_form("Item", item.item_code))
|
||||
)
|
||||
|
||||
if not item.get("type") and not item.get("is_legacy_scrap_item"):
|
||||
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item"):
|
||||
if not is_sub_contracted_item:
|
||||
frappe.throw(
|
||||
_("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
|
||||
@@ -1243,10 +1244,10 @@ class SubcontractingController(StockController):
|
||||
total_amt = sum(
|
||||
flt(item.amount)
|
||||
for item in self.get("items")
|
||||
if not item.get("type") and not item.get("is_legacy_scrap_item")
|
||||
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item")
|
||||
)
|
||||
for item in self.items:
|
||||
if not item.get("type") and not item.get("is_legacy_scrap_item"):
|
||||
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item"):
|
||||
item.additional_cost_per_qty = (
|
||||
(item.amount * self.total_additional_costs) / total_amt
|
||||
) / item.qty
|
||||
@@ -1254,15 +1255,15 @@ class SubcontractingController(StockController):
|
||||
total_qty = sum(
|
||||
flt(item.qty)
|
||||
for item in self.get("items")
|
||||
if not item.get("type") and not item.get("is_legacy_scrap_item")
|
||||
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item")
|
||||
)
|
||||
additional_cost_per_qty = self.total_additional_costs / total_qty
|
||||
for item in self.items:
|
||||
if not item.get("type") and not item.get("is_legacy_scrap_item"):
|
||||
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item"):
|
||||
item.additional_cost_per_qty = additional_cost_per_qty
|
||||
else:
|
||||
for item in self.items:
|
||||
if not item.get("type") and not item.get("is_legacy_scrap_item"):
|
||||
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item"):
|
||||
item.additional_cost_per_qty = 0
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -1529,9 +1530,13 @@ def make_return_stock_entry_for_subcontract(
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_materials_from_supplier(
|
||||
subcontract_order: str, rm_details: str | list, order_doctype: str = "Subcontracting Order"
|
||||
):
|
||||
def get_materials_from_supplier(source_name: str, target_doc: Document | str | None = None):
|
||||
args = frappe.flags.args or {}
|
||||
|
||||
subcontract_order = args.get("subcontract_order") or source_name
|
||||
rm_details = args.get("rm_details")
|
||||
order_doctype = args.get("order_doctype") or "Subcontracting Order"
|
||||
|
||||
if isinstance(rm_details, str):
|
||||
rm_details = json.loads(rm_details)
|
||||
|
||||
|
||||
@@ -241,7 +241,7 @@ class SubcontractingInwardController:
|
||||
item
|
||||
for item in self.get("items")
|
||||
if not item.is_finished_item
|
||||
and not item.type
|
||||
and not item.secondary_item_type
|
||||
and not item.is_legacy_scrap_item
|
||||
and frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item")
|
||||
]
|
||||
@@ -372,7 +372,7 @@ class SubcontractingInwardController:
|
||||
if self.purpose in ["Subcontracting Delivery", "Subcontracting Return", "Manufacture"]:
|
||||
for item in self.items:
|
||||
if (
|
||||
item.is_finished_item or item.type or item.is_legacy_scrap_item
|
||||
item.is_finished_item or item.secondary_item_type or item.is_legacy_scrap_item
|
||||
) and item.valuation_rate == 0:
|
||||
item.allow_zero_valuation_rate = 1
|
||||
|
||||
@@ -472,7 +472,7 @@ class SubcontractingInwardController:
|
||||
self.validate_delivery_on_save()
|
||||
else:
|
||||
for item in self.items:
|
||||
if not item.type and not item.is_legacy_scrap_item:
|
||||
if not item.secondary_item_type and not item.is_legacy_scrap_item:
|
||||
delivered_qty, returned_qty = frappe.get_value(
|
||||
"Subcontracting Inward Order Item",
|
||||
item.scio_detail,
|
||||
@@ -543,7 +543,7 @@ class SubcontractingInwardController:
|
||||
bold(
|
||||
frappe.get_cached_value(
|
||||
"Subcontracting Inward Order Item"
|
||||
if not item.type and not item.is_legacy_scrap_item
|
||||
if not item.secondary_item_type and not item.is_legacy_scrap_item
|
||||
else "Subcontracting Inward Order Secondary Item",
|
||||
item.scio_detail,
|
||||
"stock_uom",
|
||||
@@ -595,7 +595,7 @@ class SubcontractingInwardController:
|
||||
)
|
||||
|
||||
for item in [item for item in self.items if not item.is_finished_item]:
|
||||
if item.type or item.is_legacy_scrap_item:
|
||||
if item.secondary_item_type or item.is_legacy_scrap_item:
|
||||
scio_secondary_item = frappe.get_value(
|
||||
"Subcontracting Inward Order Secondary Item",
|
||||
{
|
||||
@@ -655,7 +655,7 @@ class SubcontractingInwardController:
|
||||
for item in self.items:
|
||||
doctype = (
|
||||
"Subcontracting Inward Order Item"
|
||||
if not item.type and not item.is_legacy_scrap_item
|
||||
if not item.secondary_item_type and not item.is_legacy_scrap_item
|
||||
else "Subcontracting Inward Order Secondary Item"
|
||||
)
|
||||
qty_map[doctype][item.scio_detail] += (
|
||||
@@ -781,7 +781,7 @@ class SubcontractingInwardController:
|
||||
items = [
|
||||
item
|
||||
for item in self.items
|
||||
if not item.is_finished_item and not item.type and not item.is_legacy_scrap_item
|
||||
if not item.is_finished_item and not item.secondary_item_type and not item.is_legacy_scrap_item
|
||||
]
|
||||
item_code_wh = frappe._dict(
|
||||
{
|
||||
@@ -884,7 +884,9 @@ class SubcontractingInwardController:
|
||||
|
||||
def update_inward_order_secondary_items(self):
|
||||
if (scio := self.subcontracting_inward_order) and self.purpose == "Manufacture":
|
||||
secondary_items_list = [item for item in self.items if item.type or item.is_legacy_scrap_item]
|
||||
secondary_items_list = [
|
||||
item for item in self.items if item.secondary_item_type or item.is_legacy_scrap_item
|
||||
]
|
||||
|
||||
secondary_items = defaultdict(float)
|
||||
for item in secondary_items_list:
|
||||
@@ -958,7 +960,7 @@ class SubcontractingInwardController:
|
||||
stock_uom=secondary_item.stock_uom,
|
||||
warehouse=secondary_item.t_warehouse,
|
||||
produced_qty=secondary_item.transfer_qty,
|
||||
type=secondary_item.type,
|
||||
secondary_item_type=secondary_item.secondary_item_type,
|
||||
delivered_qty=0,
|
||||
reference_name=frappe.get_value(
|
||||
"Work Order", self.work_order, "subcontracting_inward_order_item"
|
||||
|
||||
@@ -347,7 +347,12 @@ class TestSubcontractingController(ERPNextTestSuite):
|
||||
|
||||
sco.load_from_db()
|
||||
self.assertEqual(sco.supplied_items[0].consumed_qty, 5)
|
||||
doc = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items])
|
||||
frappe.flags.args = frappe._dict(
|
||||
subcontract_order=sco.name,
|
||||
rm_details=[d.name for d in sco.supplied_items],
|
||||
order_doctype=sco.doctype,
|
||||
)
|
||||
doc = get_materials_from_supplier(sco.name)
|
||||
doc.save()
|
||||
self.assertEqual(doc.items[0].qty, 1)
|
||||
self.assertEqual(doc.items[0].s_warehouse, "_Test Warehouse 1 - _TC")
|
||||
@@ -404,7 +409,12 @@ class TestSubcontractingController(ERPNextTestSuite):
|
||||
|
||||
sco.load_from_db()
|
||||
self.assertEqual(sco.supplied_items[0].consumed_qty, 5)
|
||||
doc = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items])
|
||||
frappe.flags.args = frappe._dict(
|
||||
subcontract_order=sco.name,
|
||||
rm_details=[d.name for d in sco.supplied_items],
|
||||
order_doctype=sco.doctype,
|
||||
)
|
||||
doc = get_materials_from_supplier(sco.name)
|
||||
self.assertEqual(doc.items[0].qty, 1)
|
||||
self.assertEqual(doc.items[0].s_warehouse, "_Test Warehouse 1 - _TC")
|
||||
self.assertEqual(doc.items[0].t_warehouse, "_Test Warehouse - _TC")
|
||||
@@ -1133,7 +1143,12 @@ class TestSubcontractingController(ERPNextTestSuite):
|
||||
|
||||
sco.load_from_db()
|
||||
self.assertEqual(sco.supplied_items[0].consumed_qty, 5)
|
||||
doc = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items])
|
||||
frappe.flags.args = frappe._dict(
|
||||
subcontract_order=sco.name,
|
||||
rm_details=[d.name for d in sco.supplied_items],
|
||||
order_doctype=sco.doctype,
|
||||
)
|
||||
doc = get_materials_from_supplier(sco.name)
|
||||
self.assertEqual(doc.items[0].qty, 1)
|
||||
self.assertEqual(doc.items[0].s_warehouse, "_Test Warehouse 1 - _TC")
|
||||
self.assertEqual(doc.items[0].t_warehouse, "_Test Warehouse - _TC")
|
||||
|
||||
Reference in New Issue
Block a user