mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-24 09:08:30 +00:00
Merge pull request #53293 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -728,9 +728,10 @@ class PurchaseInvoice(BuyingController):
|
||||
for item in self.get("items"):
|
||||
if item.purchase_receipt:
|
||||
frappe.throw(
|
||||
_("Stock cannot be updated against Purchase Receipt {0}").format(
|
||||
item.purchase_receipt
|
||||
)
|
||||
_(
|
||||
"Stock cannot be updated for Purchase Invoice {0} because a Purchase Receipt {1} has already been created for this transaction. Please disable the 'Update Stock' checkbox in the Purchase Invoice and save the invoice."
|
||||
).format(self.name, item.purchase_receipt),
|
||||
title=_("Stock Update Not Allowed"),
|
||||
)
|
||||
|
||||
def validate_for_repost(self):
|
||||
|
||||
@@ -1199,6 +1199,9 @@ class SalesInvoice(SellingController):
|
||||
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
|
||||
|
||||
def process_asset_depreciation(self):
|
||||
if self.is_internal_transfer():
|
||||
return
|
||||
|
||||
if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1):
|
||||
self.depreciate_asset_on_sale()
|
||||
else:
|
||||
|
||||
@@ -37,6 +37,20 @@ function get_filters() {
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "party_type",
|
||||
label: __("Party Type"),
|
||||
fieldtype: "Link",
|
||||
options: "Party Type",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "Dynamic Link",
|
||||
options: "party_type",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
fieldname: "voucher_no",
|
||||
label: __("Voucher No"),
|
||||
|
||||
@@ -68,6 +68,12 @@ class General_Payment_Ledger_Comparison:
|
||||
if self.filters.period_end_date:
|
||||
filter_criterion.append(gle.posting_date.lte(self.filters.period_end_date))
|
||||
|
||||
if self.filters.party_type:
|
||||
filter_criterion.append(gle.party_type.eq(self.filters.party_type))
|
||||
|
||||
if self.filters.party:
|
||||
filter_criterion.append(gle.party.eq(self.filters.party))
|
||||
|
||||
if acc_type == "receivable":
|
||||
outstanding = (Sum(gle.debit) - Sum(gle.credit)).as_("outstanding")
|
||||
else:
|
||||
@@ -111,6 +117,12 @@ class General_Payment_Ledger_Comparison:
|
||||
if self.filters.period_end_date:
|
||||
filter_criterion.append(ple.posting_date.lte(self.filters.period_end_date))
|
||||
|
||||
if self.filters.party_type:
|
||||
filter_criterion.append(ple.party_type.eq(self.filters.party_type))
|
||||
|
||||
if self.filters.party:
|
||||
filter_criterion.append(ple.party.eq(self.filters.party))
|
||||
|
||||
self.account_types[acc_type].ple = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
|
||||
@@ -649,7 +649,7 @@ class GrossProfitGenerator:
|
||||
new_row = row
|
||||
self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion)
|
||||
else:
|
||||
new_row.qty += flt(row.qty)
|
||||
new_row.qty = flt((new_row.qty + row.qty), self.float_precision)
|
||||
self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion, True)
|
||||
|
||||
new_row = self.set_average_rate(new_row)
|
||||
@@ -659,11 +659,17 @@ class GrossProfitGenerator:
|
||||
if i == 0:
|
||||
new_row = row
|
||||
else:
|
||||
new_row.qty += flt(row.qty)
|
||||
new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
|
||||
new_row.base_amount += flt(row.base_amount, self.currency_precision)
|
||||
new_row.qty = flt((new_row.qty + row.qty), self.float_precision)
|
||||
new_row.buying_amount = flt(
|
||||
(new_row.buying_amount + row.buying_amount), self.currency_precision
|
||||
)
|
||||
new_row.base_amount = flt(
|
||||
(new_row.base_amount + row.base_amount), self.currency_precision
|
||||
)
|
||||
if self.filters.get("group_by") == "Sales Person":
|
||||
new_row.allocated_amount += flt(row.allocated_amount, self.currency_precision)
|
||||
new_row.allocated_amount = flt(
|
||||
(new_row.allocated_amount + row.allocated_amount), self.currency_precision
|
||||
)
|
||||
new_row = self.set_average_rate(new_row)
|
||||
self.grouped_data.append(new_row)
|
||||
|
||||
|
||||
@@ -111,16 +111,12 @@ frappe.ui.form.on("Asset Repair", {
|
||||
purchase_invoice: function (frm) {
|
||||
if (frm.doc.purchase_invoice) {
|
||||
frappe.call({
|
||||
method: "frappe.client.get_value",
|
||||
method: "erpnext.assets.doctype.asset_repair.asset_repair.get_repair_cost_for_purchase_invoice",
|
||||
args: {
|
||||
doctype: "Purchase Invoice",
|
||||
fieldname: "base_net_total",
|
||||
filters: { name: frm.doc.purchase_invoice },
|
||||
purchase_invoice: frm.doc.purchase_invoice,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
frm.set_value("repair_cost", r.message.base_net_total);
|
||||
}
|
||||
frm.set_value("repair_cost", r.message || 0);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_diff_in_hours
|
||||
|
||||
import erpnext
|
||||
@@ -308,9 +309,14 @@ class AssetRepair(AccountsController):
|
||||
if flt(self.repair_cost) <= 0:
|
||||
return
|
||||
|
||||
pi_expense_account = (
|
||||
frappe.get_doc("Purchase Invoice", self.purchase_invoice).items[0].expense_account
|
||||
)
|
||||
expense_accounts = _get_expense_accounts_for_purchase_invoice(self.purchase_invoice)
|
||||
|
||||
if not expense_accounts:
|
||||
frappe.throw(
|
||||
_("No expense accounts found for Purchase Invoice {0}").format(self.purchase_invoice)
|
||||
)
|
||||
|
||||
pi_expense_account = expense_accounts[0]
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
@@ -473,3 +479,84 @@ class AssetRepair(AccountsController):
|
||||
def get_downtime(failure_date, completion_date):
|
||||
downtime = time_diff_in_hours(completion_date, failure_date)
|
||||
return round(downtime, 2)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_repair_cost_for_purchase_invoice(purchase_invoice: str) -> float:
|
||||
"""
|
||||
Get the total repair cost from GL entries for a purchase invoice.
|
||||
Only considers expense accounts for non-stock, non-fixed-asset items.
|
||||
"""
|
||||
if not purchase_invoice:
|
||||
return 0.0
|
||||
|
||||
frappe.has_permission("Purchase Invoice", "read", purchase_invoice, throw=True)
|
||||
|
||||
expense_accounts = _get_expense_accounts_for_purchase_invoice(purchase_invoice)
|
||||
|
||||
if not expense_accounts:
|
||||
return 0.0
|
||||
|
||||
return _get_total_expense_amount(purchase_invoice, expense_accounts)
|
||||
|
||||
|
||||
def _get_expense_accounts_for_purchase_invoice(purchase_invoice: str) -> list[str]:
|
||||
"""
|
||||
Get expense accounts for non-stock items from the purchase invoice.
|
||||
"""
|
||||
pi_items = frappe.get_all(
|
||||
"Purchase Invoice Item",
|
||||
filters={"parent": purchase_invoice},
|
||||
fields=["item_code", "expense_account", "is_fixed_asset"],
|
||||
)
|
||||
|
||||
if not pi_items:
|
||||
return []
|
||||
|
||||
# Get list of stock item codes from the invoice
|
||||
item_codes = {item.item_code for item in pi_items if item.item_code}
|
||||
stock_items = set()
|
||||
if item_codes:
|
||||
stock_items = set(
|
||||
frappe.db.get_all(
|
||||
"Item", filters={"name": ["in", list(item_codes)], "is_stock_item": 1}, pluck="name"
|
||||
)
|
||||
)
|
||||
|
||||
expense_accounts = set()
|
||||
|
||||
for item in pi_items:
|
||||
# Skip stock items - they use warehouse accounts
|
||||
if item.item_code and item.item_code in stock_items:
|
||||
continue
|
||||
|
||||
# Skip fixed assets - they use asset accounts
|
||||
if item.is_fixed_asset:
|
||||
continue
|
||||
|
||||
# Use expense account from Purchase Invoice Item
|
||||
if item.expense_account:
|
||||
expense_accounts.add(item.expense_account)
|
||||
|
||||
return list(expense_accounts)
|
||||
|
||||
|
||||
def _get_total_expense_amount(purchase_invoice: str, expense_accounts: list[str]) -> float:
|
||||
"""Get the total expense amount from GL entries for a purchase invoice and accounts."""
|
||||
if not expense_accounts:
|
||||
return 0.0
|
||||
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
|
||||
result = (
|
||||
frappe.qb.from_(gl_entry)
|
||||
.select((Sum(gl_entry.debit) - Sum(gl_entry.credit)).as_("total"))
|
||||
.where(
|
||||
(gl_entry.voucher_type == "Purchase Invoice")
|
||||
& (gl_entry.voucher_no == purchase_invoice)
|
||||
& (gl_entry.account.isin(expense_accounts))
|
||||
& (gl_entry.is_cancelled == 0)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
return flt(result[0].total) if result else 0.0
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe import qb
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import add_days, add_months, flt, get_first_day, nowdate, nowtime, today
|
||||
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.assets.doctype.asset.asset import (
|
||||
get_asset_account,
|
||||
get_asset_value_after_depreciation,
|
||||
@@ -21,6 +22,7 @@ from erpnext.assets.doctype.asset.test_asset import (
|
||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||
get_asset_depr_schedule_doc,
|
||||
)
|
||||
from erpnext.assets.doctype.asset_repair.asset_repair import get_repair_cost_for_purchase_invoice
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
|
||||
get_serial_nos_from_bundle,
|
||||
@@ -321,6 +323,59 @@ class TestAssetRepair(unittest.TestCase):
|
||||
self.assertEqual(asset.additional_asset_cost, asset_repair.repair_cost)
|
||||
self.assertEqual(booked_value, asset_repair.repair_cost)
|
||||
|
||||
def test_repair_cost_fetches_only_service_item_amount(self):
|
||||
"""Test that repair cost only includes service (non-stock) item amounts from purchase invoice."""
|
||||
|
||||
company = "_Test Company with perpetual inventory"
|
||||
warehouse = "Stores - TCP1"
|
||||
|
||||
service_item = create_item(
|
||||
"_Test Service Item for Repair",
|
||||
is_stock_item=0,
|
||||
warehouse=warehouse,
|
||||
company=company,
|
||||
)
|
||||
|
||||
stock_item = create_item(
|
||||
"_Test Stock Item for Repair",
|
||||
is_stock_item=1,
|
||||
warehouse=warehouse,
|
||||
company=company,
|
||||
)
|
||||
|
||||
service_expense_account = "Miscellaneous Expenses - TCP1"
|
||||
cost_center = frappe.db.get_value("Company", company, "cost_center")
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
item_code=service_item.name,
|
||||
qty=1,
|
||||
rate=500,
|
||||
expense_account=service_expense_account,
|
||||
cost_center=cost_center,
|
||||
warehouse=warehouse,
|
||||
update_stock=0,
|
||||
do_not_submit=1,
|
||||
company=company,
|
||||
)
|
||||
|
||||
pi.update_stock = 1
|
||||
pi.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": stock_item.name,
|
||||
"qty": 2,
|
||||
"rate": 300,
|
||||
"warehouse": warehouse,
|
||||
"cost_center": cost_center,
|
||||
},
|
||||
)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
repair_cost = get_repair_cost_for_purchase_invoice(pi.name)
|
||||
|
||||
self.assertEqual(repair_cost, 500)
|
||||
|
||||
|
||||
def num_of_depreciations(asset):
|
||||
return asset.finance_books[0].total_number_of_depreciations
|
||||
@@ -411,6 +466,7 @@ def create_asset_repair(**args):
|
||||
if asset.calculate_depreciation:
|
||||
asset_repair.increase_in_asset_life = 12
|
||||
pi = make_purchase_invoice(
|
||||
item=args.item or "_Test Non Stock Item",
|
||||
company=asset.company,
|
||||
expense_account=frappe.db.get_value("Company", asset.company, "default_expense_account"),
|
||||
cost_center=asset_repair.cost_center,
|
||||
|
||||
@@ -37,7 +37,7 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
self.assertEqual(sq.get("items")[0].qty, 5)
|
||||
self.assertEqual(sq.get("items")[1].rate, 300)
|
||||
|
||||
def test_update_supplier_quotation_child_rate_disallow(self):
|
||||
def test_update_supplier_quotation_child_rate(self):
|
||||
sq = frappe.copy_doc(test_records[0])
|
||||
sq.submit()
|
||||
trans_item = json.dumps(
|
||||
@@ -50,6 +50,22 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
},
|
||||
]
|
||||
)
|
||||
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
|
||||
sq.reload()
|
||||
self.assertEqual(sq.get("items")[0].rate, 300)
|
||||
po = make_purchase_order(sq.name)
|
||||
po.schedule_date = add_days(today(), 1)
|
||||
po.submit()
|
||||
trans_item = json.dumps(
|
||||
[
|
||||
{
|
||||
"item_code": sq.items[0].item_code,
|
||||
"rate": 20,
|
||||
"qty": sq.items[0].qty,
|
||||
"docname": sq.items[0].name,
|
||||
},
|
||||
]
|
||||
)
|
||||
self.assertRaises(
|
||||
frappe.ValidationError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name
|
||||
)
|
||||
|
||||
@@ -3838,20 +3838,28 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
return frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order") or False
|
||||
return False
|
||||
|
||||
def validate_quantity(child_item, new_data):
|
||||
def validate_quantity_and_rate(child_item, new_data):
|
||||
if not flt(new_data.get("qty")) and not is_allowed_zero_qty():
|
||||
frappe.throw(
|
||||
_("Row #{0}: Quantity for Item {1} cannot be zero.").format(
|
||||
_("Row #{0}:Quantity for Item {1} cannot be zero.").format(
|
||||
new_data.get("idx"), frappe.bold(new_data.get("item_code"))
|
||||
),
|
||||
title=_("Invalid Qty"),
|
||||
)
|
||||
|
||||
if parent_doctype == "Sales Order" and flt(new_data.get("qty")) < flt(child_item.delivered_qty):
|
||||
frappe.throw(_("Cannot set quantity less than delivered quantity"))
|
||||
qty_limits = {
|
||||
"Sales Order": ("delivered_qty", _("Cannot set quantity less than delivered quantity")),
|
||||
"Purchase Order": ("received_qty", _("Cannot set quantity less than received quantity")),
|
||||
}
|
||||
|
||||
if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(child_item.received_qty):
|
||||
frappe.throw(_("Cannot set quantity less than received quantity"))
|
||||
if parent_doctype in qty_limits:
|
||||
qty_field, error_message = qty_limits[parent_doctype]
|
||||
if flt(new_data.get("qty")) < flt(child_item.get(qty_field)):
|
||||
frappe.throw(
|
||||
_("Row #{0}:").format(new_data.get("idx"))
|
||||
+ error_message.format(frappe.bold(new_data.get("item_code"))),
|
||||
title=_("Invalid Qty"),
|
||||
)
|
||||
|
||||
if parent_doctype in ["Quotation", "Supplier Quotation"]:
|
||||
if (parent_doctype == "Quotation" and not ordered_items) or (
|
||||
@@ -3864,7 +3872,15 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
if parent_doctype == "Quotation"
|
||||
else purchased_items.get(child_item.name)
|
||||
)
|
||||
|
||||
if qty_to_check:
|
||||
if not rate_unchanged:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot update rate as item {0} is already ordered or purchased against this quotation"
|
||||
).format(frappe.bold(new_data.get("item_code")))
|
||||
)
|
||||
|
||||
if flt(new_data.get("qty")) < qty_to_check:
|
||||
frappe.throw(_("Cannot reduce quantity than ordered or purchased quantity"))
|
||||
|
||||
@@ -3980,10 +3996,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
):
|
||||
continue
|
||||
|
||||
validate_quantity(child_item, d)
|
||||
if parent_doctype in ["Quotation", "Supplier Quotation"]:
|
||||
if not rate_unchanged:
|
||||
frappe.throw(_("Rates cannot be modified for quoted items"))
|
||||
validate_quantity_and_rate(child_item, d)
|
||||
|
||||
if flt(child_item.get("qty")) != flt(d.get("qty")):
|
||||
any_qty_changed = True
|
||||
|
||||
@@ -1135,6 +1135,16 @@ class StockController(AccountsController):
|
||||
continue
|
||||
|
||||
if qi_required: # validate row only if inspection is required on item level
|
||||
if self.doctype in [
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
"Sales Invoice",
|
||||
"Delivery Note",
|
||||
] and frappe.get_single_value(
|
||||
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
|
||||
):
|
||||
return
|
||||
|
||||
self.validate_qi_presence(row)
|
||||
if self.docstatus == 1:
|
||||
self.validate_qi_submission(row)
|
||||
@@ -1142,16 +1152,6 @@ class StockController(AccountsController):
|
||||
|
||||
def validate_qi_presence(self, row):
|
||||
"""Check if QI is present on row level. Warn on save and stop on submit if missing."""
|
||||
if self.doctype in [
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
"Sales Invoice",
|
||||
"Delivery Note",
|
||||
] and frappe.db.get_single_value(
|
||||
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
|
||||
):
|
||||
return
|
||||
|
||||
if not row.quality_inspection:
|
||||
msg = _("Row #{0}: Quality Inspection is required for Item {1}").format(
|
||||
row.idx, frappe.bold(row.item_code)
|
||||
|
||||
@@ -1297,6 +1297,55 @@ def make_rm_stock_entry(
|
||||
if target_doc and target_doc.get("items"):
|
||||
target_doc.items = []
|
||||
|
||||
def post_process(source_doc, target_doc):
|
||||
target_doc.purpose = "Send to Subcontractor"
|
||||
|
||||
if order_doctype == "Purchase Order":
|
||||
target_doc.purchase_order = source_doc.name
|
||||
else:
|
||||
target_doc.subcontracting_order = source_doc.name
|
||||
|
||||
target_doc.set_stock_entry_type()
|
||||
|
||||
for fg_item_code in fg_item_code_list:
|
||||
for rm_item in rm_items:
|
||||
if (
|
||||
rm_item.get("main_item_code") == fg_item_code
|
||||
or rm_item.get("item_code") == fg_item_code
|
||||
):
|
||||
rm_item_code = rm_item.get("rm_item_code")
|
||||
|
||||
items_dict = {
|
||||
rm_item_code: {
|
||||
rm_detail_field: rm_item.get("name"),
|
||||
"item_name": rm_item.get("item_name")
|
||||
or item_wh.get(rm_item_code, {}).get("item_name", ""),
|
||||
"description": item_wh.get(rm_item_code, {}).get("description", ""),
|
||||
"qty": rm_item.get("qty")
|
||||
or max(
|
||||
rm_item.get("required_qty") - rm_item.get("total_supplied_qty"), 0
|
||||
),
|
||||
"from_warehouse": rm_item.get("warehouse")
|
||||
or rm_item.get("reserve_warehouse"),
|
||||
"to_warehouse": source_doc.supplier_warehouse,
|
||||
"stock_uom": rm_item.get("stock_uom"),
|
||||
"serial_and_batch_bundle": rm_item.get("serial_and_batch_bundle"),
|
||||
"main_item_code": fg_item_code,
|
||||
"allow_alternative_item": item_wh.get(rm_item_code, {}).get(
|
||||
"allow_alternative_item"
|
||||
),
|
||||
"use_serial_batch_fields": rm_item.get("use_serial_batch_fields"),
|
||||
"serial_no": rm_item.get("serial_no")
|
||||
if rm_item.get("use_serial_batch_fields")
|
||||
else None,
|
||||
"batch_no": rm_item.get("batch_no")
|
||||
if rm_item.get("use_serial_batch_fields")
|
||||
else None,
|
||||
}
|
||||
}
|
||||
|
||||
target_doc.add_to_stock_entry_detail(items_dict)
|
||||
|
||||
stock_entry = get_mapped_doc(
|
||||
order_doctype,
|
||||
subcontract_order.name,
|
||||
@@ -1317,53 +1366,9 @@ def make_rm_stock_entry(
|
||||
},
|
||||
target_doc,
|
||||
ignore_child_tables=True,
|
||||
postprocess=post_process,
|
||||
)
|
||||
|
||||
stock_entry.purpose = "Send to Subcontractor"
|
||||
|
||||
if order_doctype == "Purchase Order":
|
||||
stock_entry.purchase_order = subcontract_order.name
|
||||
else:
|
||||
stock_entry.subcontracting_order = subcontract_order.name
|
||||
|
||||
stock_entry.set_stock_entry_type()
|
||||
|
||||
for fg_item_code in fg_item_code_list:
|
||||
for rm_item in rm_items:
|
||||
if (
|
||||
rm_item.get("main_item_code") == fg_item_code
|
||||
or rm_item.get("item_code") == fg_item_code
|
||||
):
|
||||
rm_item_code = rm_item.get("rm_item_code")
|
||||
items_dict = {
|
||||
rm_item_code: {
|
||||
rm_detail_field: rm_item.get("name"),
|
||||
"item_name": rm_item.get("item_name")
|
||||
or item_wh.get(rm_item_code, {}).get("item_name", ""),
|
||||
"description": item_wh.get(rm_item_code, {}).get("description", ""),
|
||||
"qty": rm_item.get("qty")
|
||||
or max(rm_item.get("required_qty") - rm_item.get("total_supplied_qty"), 0),
|
||||
"from_warehouse": rm_item.get("warehouse")
|
||||
or rm_item.get("reserve_warehouse"),
|
||||
"to_warehouse": subcontract_order.supplier_warehouse,
|
||||
"stock_uom": rm_item.get("stock_uom"),
|
||||
"serial_and_batch_bundle": rm_item.get("serial_and_batch_bundle"),
|
||||
"main_item_code": fg_item_code,
|
||||
"allow_alternative_item": item_wh.get(rm_item_code, {}).get(
|
||||
"allow_alternative_item"
|
||||
),
|
||||
"use_serial_batch_fields": rm_item.get("use_serial_batch_fields"),
|
||||
"serial_no": rm_item.get("serial_no")
|
||||
if rm_item.get("use_serial_batch_fields")
|
||||
else None,
|
||||
"batch_no": rm_item.get("batch_no")
|
||||
if rm_item.get("use_serial_batch_fields")
|
||||
else None,
|
||||
}
|
||||
}
|
||||
|
||||
stock_entry.add_to_stock_entry_detail(items_dict)
|
||||
|
||||
if target_doc:
|
||||
return stock_entry
|
||||
else:
|
||||
@@ -1395,6 +1400,8 @@ def add_items_in_ste(ste_doc, row, qty, rm_details, rm_detail_field="sco_rm_deta
|
||||
def make_return_stock_entry_for_subcontract(
|
||||
available_materials, order_doc, rm_details, order_doctype="Subcontracting Order"
|
||||
):
|
||||
rm_detail_field = "po_detail" if order_doctype == "Purchase Order" else "sco_rm_detail"
|
||||
|
||||
def post_process(source_doc, target_doc):
|
||||
target_doc.purpose = "Material Transfer"
|
||||
|
||||
@@ -1405,6 +1412,21 @@ def make_return_stock_entry_for_subcontract(
|
||||
|
||||
target_doc.company = source_doc.company
|
||||
target_doc.is_return = 1
|
||||
for _key, value in available_materials.items():
|
||||
if not value.qty:
|
||||
continue
|
||||
|
||||
if item_details := value.get("item_details"):
|
||||
item_details["serial_and_batch_bundle"] = None
|
||||
|
||||
if value.batch_no:
|
||||
for batch_no, qty in value.batch_no.items():
|
||||
if qty > 0:
|
||||
add_items_in_ste(target_doc, value, qty, rm_details, rm_detail_field, batch_no)
|
||||
else:
|
||||
add_items_in_ste(target_doc, value, value.qty, rm_details, rm_detail_field)
|
||||
|
||||
target_doc.set_stock_entry_type()
|
||||
|
||||
ste_doc = get_mapped_doc(
|
||||
order_doctype,
|
||||
@@ -1419,27 +1441,6 @@ def make_return_stock_entry_for_subcontract(
|
||||
postprocess=post_process,
|
||||
)
|
||||
|
||||
if order_doctype == "Purchase Order":
|
||||
rm_detail_field = "po_detail"
|
||||
else:
|
||||
rm_detail_field = "sco_rm_detail"
|
||||
|
||||
for _key, value in available_materials.items():
|
||||
if not value.qty:
|
||||
continue
|
||||
|
||||
if item_details := value.get("item_details"):
|
||||
item_details["serial_and_batch_bundle"] = None
|
||||
|
||||
if value.batch_no:
|
||||
for batch_no, qty in value.batch_no.items():
|
||||
if qty > 0:
|
||||
add_items_in_ste(ste_doc, value, qty, rm_details, rm_detail_field, batch_no)
|
||||
else:
|
||||
add_items_in_ste(ste_doc, value, value.qty, rm_details, rm_detail_field)
|
||||
|
||||
ste_doc.set_stock_entry_type()
|
||||
|
||||
return ste_doc
|
||||
|
||||
|
||||
|
||||
@@ -1076,9 +1076,9 @@ class JobCard(Document):
|
||||
|
||||
def is_work_order_closed(self):
|
||||
if self.work_order:
|
||||
status = frappe.get_value("Work Order", self.work_order)
|
||||
status = frappe.get_value("Work Order", self.work_order, "status")
|
||||
|
||||
if status == "Closed":
|
||||
if status in ["Closed", "Stopped"]:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -425,10 +425,11 @@ frappe.ui.form.on("Work Order", {
|
||||
var added_min = false;
|
||||
|
||||
// produced qty
|
||||
var title = __("{0} items produced", [frm.doc.produced_qty]);
|
||||
let produced_qty = frm.doc.produced_qty - frm.doc.disassembled_qty;
|
||||
var title = __("{0} items produced", [produced_qty]);
|
||||
bars.push({
|
||||
title: title,
|
||||
width: (frm.doc.produced_qty / frm.doc.qty) * 100 + "%",
|
||||
width: (flt(produced_qty) / frm.doc.qty) * 100 + "%",
|
||||
progress_class: "progress-bar-success",
|
||||
});
|
||||
if (bars[0].width == "0%") {
|
||||
@@ -445,14 +446,27 @@ frappe.ui.form.on("Work Order", {
|
||||
if (pending_complete > 0) {
|
||||
var width = (pending_complete / frm.doc.qty) * 100 - added_min;
|
||||
title = __("{0} items in progress", [pending_complete]);
|
||||
let progress_class = "progress-bar-warning";
|
||||
if (frm.doc.status == "Closed") {
|
||||
if (frm.doc.required_items.find((d) => d.returned_qty > 0)) {
|
||||
title = __("{0} items returned", [pending_complete]);
|
||||
progress_class = "progress-bar-warning";
|
||||
} else {
|
||||
title = __("{0} items to return", [pending_complete]);
|
||||
progress_class = "progress-bar-info";
|
||||
}
|
||||
}
|
||||
|
||||
bars.push({
|
||||
title: title,
|
||||
width: (width > 100 ? "99.5" : width) + "%",
|
||||
progress_class: "progress-bar-warning",
|
||||
progress_class: progress_class,
|
||||
});
|
||||
message = message + ". " + title;
|
||||
}
|
||||
}
|
||||
|
||||
//process loss qty
|
||||
if (frm.doc.process_loss_qty) {
|
||||
var process_loss_width = (frm.doc.process_loss_qty / frm.doc.qty) * 100;
|
||||
title = __("{0} items lost during process.", [frm.doc.process_loss_qty]);
|
||||
@@ -463,6 +477,19 @@ frappe.ui.form.on("Work Order", {
|
||||
});
|
||||
message = message + ". " + title;
|
||||
}
|
||||
|
||||
// disassembled qty
|
||||
if (frm.doc.disassembled_qty) {
|
||||
var disassembled_width = (frm.doc.disassembled_qty / frm.doc.qty) * 100;
|
||||
title = __("{0} items disassembled", [frm.doc.disassembled_qty]);
|
||||
bars.push({
|
||||
title: title,
|
||||
width: disassembled_width + "%",
|
||||
progress_class: "progress-bar-secondary",
|
||||
});
|
||||
message = message + ". " + title;
|
||||
}
|
||||
|
||||
frm.dashboard.add_progress(__("Status"), bars, message);
|
||||
},
|
||||
|
||||
|
||||
@@ -220,39 +220,52 @@ class WorkOrder(Document):
|
||||
)
|
||||
|
||||
def validate_sales_order(self):
|
||||
if self.production_plan_sub_assembly_item:
|
||||
return
|
||||
|
||||
if self.sales_order:
|
||||
self.check_sales_order_on_hold_or_close()
|
||||
so = frappe.db.sql(
|
||||
"""
|
||||
select so.name, so_item.delivery_date, so.project
|
||||
from `tabSales Order` so
|
||||
inner join `tabSales Order Item` so_item on so_item.parent = so.name
|
||||
left join `tabProduct Bundle Item` pk_item on so_item.item_code = pk_item.parent
|
||||
where so.name=%s and so.docstatus = 1
|
||||
and so.skip_delivery_note = 0 and (
|
||||
so_item.item_code=%s or
|
||||
pk_item.item_code=%s )
|
||||
""",
|
||||
(self.sales_order, self.production_item, self.production_item),
|
||||
as_dict=1,
|
||||
|
||||
SalesOrder = frappe.qb.DocType("Sales Order")
|
||||
SalesOrderItem = frappe.qb.DocType("Sales Order Item")
|
||||
PackedItem = frappe.qb.DocType("Packed Item")
|
||||
ProductBundleItem = frappe.qb.DocType("Product Bundle Item")
|
||||
|
||||
so = (
|
||||
frappe.qb.from_(SalesOrder)
|
||||
.inner_join(SalesOrderItem)
|
||||
.on(SalesOrderItem.parent == SalesOrder.name)
|
||||
.left_join(ProductBundleItem)
|
||||
.on(ProductBundleItem.parent == SalesOrderItem.item_code)
|
||||
.select(SalesOrder.name, SalesOrder.project, SalesOrderItem.delivery_date)
|
||||
.where(
|
||||
(SalesOrder.skip_delivery_note == 0)
|
||||
& (SalesOrder.docstatus == 1)
|
||||
& (SalesOrder.name == self.sales_order)
|
||||
& (
|
||||
(SalesOrderItem.item_code == self.production_item)
|
||||
| (ProductBundleItem.item_code == self.production_item)
|
||||
)
|
||||
)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
if not so:
|
||||
so = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
so.name, so_item.delivery_date, so.project
|
||||
from
|
||||
`tabSales Order` so, `tabSales Order Item` so_item, `tabPacked Item` packed_item
|
||||
where so.name=%s
|
||||
and so.name=so_item.parent
|
||||
and so.name=packed_item.parent
|
||||
and so.skip_delivery_note = 0
|
||||
and so_item.item_code = packed_item.parent_item
|
||||
and so.docstatus = 1 and packed_item.item_code=%s
|
||||
""",
|
||||
(self.sales_order, self.production_item),
|
||||
as_dict=1,
|
||||
so = (
|
||||
frappe.qb.from_(SalesOrder)
|
||||
.inner_join(SalesOrderItem)
|
||||
.on(SalesOrderItem.parent == SalesOrder.name)
|
||||
.inner_join(PackedItem)
|
||||
.on(PackedItem.parent == SalesOrder.name)
|
||||
.select(SalesOrder.name, SalesOrder.project, SalesOrderItem.delivery_date)
|
||||
.where(
|
||||
(SalesOrder.name == self.sales_order)
|
||||
& (SalesOrder.skip_delivery_note == 0)
|
||||
& (SalesOrderItem.item_code == PackedItem.parent_item)
|
||||
& (SalesOrder.docstatus == 1)
|
||||
& (PackedItem.item_code == self.production_item)
|
||||
)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
if len(so):
|
||||
@@ -426,7 +439,7 @@ class WorkOrder(Document):
|
||||
|
||||
from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_in_so_item
|
||||
|
||||
if self.sales_order and self.sales_order_item:
|
||||
if self.sales_order and self.sales_order_item and not self.production_plan_sub_assembly_item:
|
||||
update_produced_qty_in_so_item(self.sales_order, self.sales_order_item)
|
||||
|
||||
if self.production_plan:
|
||||
@@ -818,7 +831,7 @@ class WorkOrder(Document):
|
||||
doc.db_set("status", doc.status)
|
||||
|
||||
def update_work_order_qty_in_so(self):
|
||||
if not self.sales_order and not self.sales_order_item:
|
||||
if (not self.sales_order and not self.sales_order_item) or self.production_plan_sub_assembly_item:
|
||||
return
|
||||
|
||||
total_bundle_qty = 1
|
||||
|
||||
@@ -75,7 +75,6 @@ erpnext.patches.v12_0.make_item_manufacturer
|
||||
erpnext.patches.v12_0.move_item_tax_to_item_tax_template
|
||||
erpnext.patches.v11_1.set_variant_based_on
|
||||
erpnext.patches.v11_1.woocommerce_set_creation_user
|
||||
erpnext.patches.v11_1.rename_depends_on_lwp
|
||||
execute:frappe.delete_doc("Report", "Inactive Items")
|
||||
erpnext.patches.v11_1.delete_scheduling_tool
|
||||
erpnext.patches.v12_0.rename_tolerance_fields
|
||||
@@ -432,3 +431,4 @@ erpnext.patches.v15_0.create_accounting_dimensions_in_advance_taxes_and_charges
|
||||
erpnext.patches.v16_0.set_ordered_qty_in_quotation_item
|
||||
erpnext.patches.v15_0.replace_http_with_https_in_sales_partner
|
||||
erpnext.patches.v16_0.add_portal_redirects
|
||||
erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import frappe
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
|
||||
def execute():
|
||||
PurchaseOrderItem = DocType("Purchase Order Item")
|
||||
MaterialRequestItem = DocType("Material Request Item")
|
||||
|
||||
poi_query = (
|
||||
frappe.qb.from_(PurchaseOrderItem)
|
||||
.select(PurchaseOrderItem.sales_order_item, Sum(PurchaseOrderItem.stock_qty))
|
||||
.where(PurchaseOrderItem.sales_order_item.isnotnull() & PurchaseOrderItem.docstatus == 1)
|
||||
.groupby(PurchaseOrderItem.sales_order_item)
|
||||
)
|
||||
|
||||
mri_query = (
|
||||
frappe.qb.from_(MaterialRequestItem)
|
||||
.select(MaterialRequestItem.sales_order_item, Sum(MaterialRequestItem.stock_qty))
|
||||
.where(MaterialRequestItem.sales_order_item.isnotnull() & MaterialRequestItem.docstatus == 1)
|
||||
.groupby(MaterialRequestItem.sales_order_item)
|
||||
)
|
||||
|
||||
poi_data = poi_query.run()
|
||||
mri_data = mri_query.run()
|
||||
|
||||
updates_against_poi = {data[0]: {"ordered_qty": data[1]} for data in poi_data}
|
||||
updates_against_mri = {data[0]: {"requested_qty": data[1], "ordered_qty": 0} for data in mri_data}
|
||||
|
||||
frappe.db.auto_commit_on_many_writes = 1
|
||||
frappe.db.bulk_update("Sales Order Item", updates_against_mri)
|
||||
frappe.db.bulk_update("Sales Order Item", updates_against_poi)
|
||||
frappe.db.auto_commit_on_many_writes = 0
|
||||
@@ -260,6 +260,33 @@ frappe.ui.form.on("Timesheet", {
|
||||
parent_project: function (frm) {
|
||||
set_project_in_timelog(frm);
|
||||
},
|
||||
|
||||
employee: function (frm) {
|
||||
if (frm.doc.employee && frm.doc.time_logs) {
|
||||
const selected_employee = frm.doc.employee;
|
||||
frm.doc.time_logs.forEach((row) => {
|
||||
if (row.activity_type) {
|
||||
frappe.call({
|
||||
method: "erpnext.projects.doctype.timesheet.timesheet.get_activity_cost",
|
||||
args: {
|
||||
employee: frm.doc.employee,
|
||||
activity_type: row.activity_type,
|
||||
currency: frm.doc.currency,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
if (selected_employee !== frm.doc.employee) return;
|
||||
row.billing_rate = r.message["billing_rate"];
|
||||
row.costing_rate = r.message["costing_rate"];
|
||||
frm.refresh_fields("time_logs");
|
||||
calculate_billing_costing_amount(frm, row.doctype, row.name);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Timesheet Detail", {
|
||||
|
||||
@@ -173,9 +173,15 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
if (!tax.dont_recompute_tax) {
|
||||
tax.item_wise_tax_detail = {};
|
||||
}
|
||||
var tax_fields = ["total", "tax_amount_after_discount_amount",
|
||||
"tax_amount_for_current_item", "grand_total_for_current_item",
|
||||
"tax_fraction_for_current_item", "grand_total_fraction_for_current_item"];
|
||||
var tax_fields = [
|
||||
"net_amount",
|
||||
"total",
|
||||
"tax_amount_after_discount_amount",
|
||||
"tax_amount_for_current_item",
|
||||
"grand_total_for_current_item",
|
||||
"tax_fraction_for_current_item",
|
||||
"grand_total_fraction_for_current_item",
|
||||
];
|
||||
|
||||
if (cstr(tax.charge_type) != "Actual" &&
|
||||
!(me.discount_amount_applied && me.frm.doc.apply_discount_on=="Grand Total")) {
|
||||
@@ -363,9 +369,14 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
|
||||
$.each(doc.taxes, function(i, tax) {
|
||||
// tax_amount represents the amount of tax for the current step
|
||||
var current_tax_amount = me.get_current_tax_amount(item, tax, item_tax_map);
|
||||
var [current_net_amount, current_tax_amount] = me.get_current_tax_amount(
|
||||
item,
|
||||
tax,
|
||||
item_tax_map
|
||||
);
|
||||
if (frappe.flags.round_row_wise_tax) {
|
||||
current_tax_amount = flt(current_tax_amount, precision("tax_amount", tax));
|
||||
current_net_amount = flt(current_net_amount, precision("net_amount", tax));
|
||||
}
|
||||
|
||||
// Adjust divisional loss to the last item
|
||||
@@ -380,6 +391,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
if (tax.charge_type != "Actual" &&
|
||||
!(me.discount_amount_applied && me.frm.doc.apply_discount_on=="Grand Total")) {
|
||||
tax.tax_amount += current_tax_amount;
|
||||
tax.net_amount += current_net_amount;
|
||||
}
|
||||
|
||||
// store tax_amount for current item as it will be used for
|
||||
@@ -430,8 +442,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
|
||||
for (const [i, tax] of doc.taxes.entries()) {
|
||||
me.round_off_totals(tax);
|
||||
me.set_in_company_currency(tax,
|
||||
["tax_amount", "tax_amount_after_discount_amount"]);
|
||||
me.set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount", "net_amount"]);
|
||||
|
||||
me.round_off_base_values(tax);
|
||||
|
||||
@@ -464,6 +475,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
get_current_tax_amount(item, tax, item_tax_map) {
|
||||
var tax_rate = this._get_tax_rate(tax, item_tax_map);
|
||||
var current_tax_amount = 0.0;
|
||||
var current_net_amount = 0.0;
|
||||
|
||||
// To set row_id by default as previous row.
|
||||
if(["On Previous Row Amount", "On Previous Row Total"].includes(tax.charge_type)) {
|
||||
@@ -476,21 +488,27 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
}
|
||||
if(tax.charge_type == "Actual") {
|
||||
current_net_amount = item.net_amount
|
||||
// distribute the tax amount proportionally to each item row
|
||||
var actual = flt(tax.tax_amount, precision("tax_amount", tax));
|
||||
current_tax_amount = this.frm.doc.net_total ?
|
||||
((item.net_amount / this.frm.doc.net_total) * actual) : 0.0;
|
||||
|
||||
} else if(tax.charge_type == "On Net Total") {
|
||||
if (tax.account_head in item_tax_map) {
|
||||
current_net_amount = item.net_amount
|
||||
};
|
||||
current_tax_amount = (tax_rate / 100.0) * item.net_amount;
|
||||
} else if(tax.charge_type == "On Previous Row Amount") {
|
||||
current_net_amount = this.frm.doc["taxes"][cint(tax.row_id) - 1].tax_amount_for_current_item
|
||||
current_tax_amount = (tax_rate / 100.0) *
|
||||
this.frm.doc["taxes"][cint(tax.row_id) - 1].tax_amount_for_current_item;
|
||||
|
||||
} else if(tax.charge_type == "On Previous Row Total") {
|
||||
current_net_amount = this.frm.doc["taxes"][cint(tax.row_id) - 1].grand_total_for_current_item
|
||||
current_tax_amount = (tax_rate / 100.0) *
|
||||
this.frm.doc["taxes"][cint(tax.row_id) - 1].grand_total_for_current_item;
|
||||
} else if (tax.charge_type == "On Item Quantity") {
|
||||
// don't sum current net amount due to the field being a currency field
|
||||
current_tax_amount = tax_rate * item.qty;
|
||||
}
|
||||
|
||||
@@ -498,7 +516,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount);
|
||||
}
|
||||
|
||||
return current_tax_amount;
|
||||
return [current_net_amount, current_tax_amount];
|
||||
}
|
||||
|
||||
set_item_wise_tax(item, tax, tax_rate, current_tax_amount) {
|
||||
@@ -532,7 +550,11 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
|
||||
tax.tax_amount = flt(tax.tax_amount, precision("tax_amount", tax));
|
||||
tax.tax_amount_after_discount_amount = flt(tax.tax_amount_after_discount_amount, precision("tax_amount", tax));
|
||||
tax.net_amount = flt(tax.net_amount, precision("net_amount", tax));
|
||||
tax.tax_amount_after_discount_amount = flt(
|
||||
tax.tax_amount_after_discount_amount,
|
||||
precision("tax_amount", tax)
|
||||
);
|
||||
}
|
||||
|
||||
round_off_base_values(tax) {
|
||||
|
||||
@@ -52,8 +52,22 @@ class TestQuotation(FrappeTestCase):
|
||||
self.assertEqual(qo.get("items")[0].qty, 11)
|
||||
self.assertEqual(qo.get("items")[-1].rate, 100)
|
||||
|
||||
def test_update_child_disallow_rate_change(self):
|
||||
qo = make_quotation(qty=4)
|
||||
def test_update_child_rate_change(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item_1 = make_item("_Test Item")
|
||||
item_2 = make_item("_Test Item 1")
|
||||
|
||||
item_list = [
|
||||
{"item_code": item_1.item_code, "warehouse": "_Test Warehouse - _TC", "qty": 10, "rate": 300},
|
||||
{"item_code": item_2.item_code, "warehouse": "_Test Warehouse - _TC", "qty": 5, "rate": 400},
|
||||
]
|
||||
|
||||
qo = make_quotation(item_list=item_list)
|
||||
so = make_sales_order(qo.name, args={"filtered_children": [qo.items[0].name]})
|
||||
so.delivery_date = nowdate()
|
||||
so.submit()
|
||||
qo.reload()
|
||||
trans_item = json.dumps(
|
||||
[
|
||||
{
|
||||
@@ -61,10 +75,35 @@ class TestQuotation(FrappeTestCase):
|
||||
"rate": 5000,
|
||||
"qty": qo.items[0].qty,
|
||||
"docname": qo.items[0].name,
|
||||
}
|
||||
},
|
||||
{
|
||||
"item_code": qo.items[1].item_code,
|
||||
"rate": qo.items[1].rate,
|
||||
"qty": qo.items[1].qty,
|
||||
"docname": qo.items[1].name,
|
||||
},
|
||||
]
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, update_child_qty_rate, "Quotation", trans_item, qo.name)
|
||||
trans_item = json.dumps(
|
||||
[
|
||||
{
|
||||
"item_code": qo.items[0].item_code,
|
||||
"rate": qo.items[0].rate,
|
||||
"qty": qo.items[0].qty,
|
||||
"docname": qo.items[0].name,
|
||||
},
|
||||
{
|
||||
"item_code": qo.items[1].item_code,
|
||||
"rate": 50,
|
||||
"qty": qo.items[1].qty,
|
||||
"docname": qo.items[1].name,
|
||||
},
|
||||
]
|
||||
)
|
||||
update_child_qty_rate("Quotation", trans_item, qo.name)
|
||||
qo.reload()
|
||||
self.assertEqual(qo.items[1].rate, 50)
|
||||
|
||||
def test_update_child_removing_item(self):
|
||||
qo = make_quotation(qty=10)
|
||||
|
||||
@@ -56,6 +56,13 @@ frappe.ui.form.on("Sales Order", {
|
||||
frm.set_df_property("packed_items", "cannot_add_rows", true);
|
||||
frm.set_df_property("packed_items", "cannot_delete_rows", true);
|
||||
},
|
||||
delivery_date(frm) {
|
||||
if (frm.doc.delivery_date) {
|
||||
frm.doc.items.forEach((d) => {
|
||||
frappe.model.set_value(d.doctype, d.name, "delivery_date", frm.doc.delivery_date);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.docstatus === 1) {
|
||||
@@ -145,7 +152,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
prevent_past_delivery_dates(frm);
|
||||
// Hide `Reserve Stock` field description in submitted or cancelled Sales Order.
|
||||
if (frm.doc.docstatus > 0) {
|
||||
frm.set_df_property("reserve_stock", "description", null);
|
||||
@@ -224,13 +231,6 @@ frappe.ui.form.on("Sales Order", {
|
||||
];
|
||||
},
|
||||
|
||||
delivery_date: function (frm) {
|
||||
$.each(frm.doc.items || [], function (i, d) {
|
||||
if (!d.delivery_date) d.delivery_date = frm.doc.delivery_date;
|
||||
});
|
||||
refresh_field("items");
|
||||
},
|
||||
|
||||
create_stock_reservation_entries(frm) {
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Stock Reservation"),
|
||||
@@ -1400,3 +1400,11 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
};
|
||||
|
||||
extend_cscript(cur_frm.cscript, new erpnext.selling.SalesOrderController({ frm: cur_frm }));
|
||||
|
||||
function prevent_past_delivery_dates(frm) {
|
||||
if (frm.doc.transaction_date) {
|
||||
frm.fields_dict["delivery_date"].datepicker?.update({
|
||||
minDate: new Date(frm.doc.transaction_date),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@
|
||||
"ordered_qty",
|
||||
"planned_qty",
|
||||
"production_plan_qty",
|
||||
"requested_qty",
|
||||
"column_break_69",
|
||||
"work_order_qty",
|
||||
"delivered_qty",
|
||||
@@ -966,12 +967,56 @@
|
||||
"label": "Project",
|
||||
"options": "Project",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sales_order_schedule_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Sales Order Schedule"
|
||||
},
|
||||
{
|
||||
"fieldname": "add_schedule",
|
||||
"fieldtype": "Button",
|
||||
"label": "Add Schedule"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "0",
|
||||
"depends_on": "eval:parent.is_subcontracted",
|
||||
"fieldname": "subcontracted_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Subcontracted Quantity",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.is_subcontracted",
|
||||
"fieldname": "fg_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Finished Good",
|
||||
"mandatory_depends_on": "eval:parent.is_subcontracted",
|
||||
"options": "Item"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.is_subcontracted",
|
||||
"fieldname": "fg_item_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Finished Good Qty",
|
||||
"mandatory_depends_on": "eval:parent.is_subcontracted"
|
||||
},
|
||||
{
|
||||
"fieldname": "requested_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Requested Qty",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-20 16:39:00.200328",
|
||||
"modified": "2026-02-21 16:39:00.200328",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Item",
|
||||
|
||||
@@ -78,6 +78,7 @@ class SalesOrderItem(Document):
|
||||
quotation_item: DF.Data | None
|
||||
rate: DF.Currency
|
||||
rate_with_margin: DF.Currency
|
||||
requested_qty: DF.Float
|
||||
reserve_stock: DF.Check
|
||||
returned_qty: DF.Float
|
||||
stock_qty: DF.Float
|
||||
|
||||
@@ -81,7 +81,7 @@ class MaterialRequest(BuyingController):
|
||||
{
|
||||
"source_dt": "Material Request Item",
|
||||
"target_dt": "Sales Order Item",
|
||||
"target_field": "ordered_qty",
|
||||
"target_field": "requested_qty",
|
||||
"target_parent_dt": "Sales Order",
|
||||
"target_parent_field": "",
|
||||
"join_field": "sales_order_item",
|
||||
@@ -248,6 +248,8 @@ class MaterialRequest(BuyingController):
|
||||
def on_cancel(self):
|
||||
self.update_requested_qty_in_production_plan()
|
||||
self.update_requested_qty()
|
||||
if self.material_request_type == "Purchase":
|
||||
self.update_prevdoc_status()
|
||||
|
||||
def get_mr_items_ordered_qty(self, mr_items):
|
||||
mr_items_ordered_qty = {}
|
||||
|
||||
@@ -333,7 +333,7 @@ class SerialBatchBundle:
|
||||
"Serial and Batch Entry", {"parent": self.sle.serial_and_batch_bundle, "docstatus": 0}
|
||||
)
|
||||
> 0
|
||||
):
|
||||
) and not self.sle.is_cancelled:
|
||||
frappe.throw(
|
||||
_("Serial and Batch Bundle {0} is not submitted").format(
|
||||
bold(self.sle.serial_and_batch_bundle)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<form action="/search_help" style="display: flex;">
|
||||
<input name='q' class='form-control' type='text'
|
||||
style='max-width: 400px; display: inline-block; margin-right: 10px;'
|
||||
value='{{ frappe.form_dict.q or ''}}'
|
||||
value='{{ (frappe.form_dict.q or '') | e }}'
|
||||
{% if not frappe.form_dict.q%}placeholder="{{ _("What do you need help with?") }}"{% endif %}>
|
||||
<input type='submit'
|
||||
class='btn btn-sm btn-light btn-search' value="{{ _("Search") }}">
|
||||
|
||||
Reference in New Issue
Block a user