mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-24 00:58:29 +00:00
Merge pull request #48338 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -118,8 +118,9 @@ class BankClearance(Document):
|
||||
|
||||
else:
|
||||
# using db_set to trigger notification
|
||||
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
frappe.db.set_value(
|
||||
d.payment_document, d.payment_entry, "clearance_date", d.clearance_date
|
||||
)
|
||||
|
||||
clearance_date_updated = True
|
||||
|
||||
|
||||
@@ -826,7 +826,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
|
||||
|
||||
create_gain_loss_journal(
|
||||
company,
|
||||
today(),
|
||||
inv.difference_posting_date,
|
||||
inv.party_type,
|
||||
inv.party,
|
||||
inv.account,
|
||||
|
||||
@@ -1336,17 +1336,12 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
warehouse_debit_amount = stock_amount
|
||||
|
||||
elif self.is_return and self.update_stock and self.is_internal_supplier and warehouse_debit_amount:
|
||||
elif self.is_return and self.update_stock and (self.is_internal_supplier or not self.return_against):
|
||||
net_rate = item.base_net_amount
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
|
||||
stock_amount = (
|
||||
net_rate
|
||||
+ item.item_tax_amount
|
||||
+ flt(item.landed_cost_voucher_amount)
|
||||
+ flt(item.get("amount_difference_with_purchase_invoice"))
|
||||
)
|
||||
stock_amount = net_rate + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
|
||||
|
||||
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
|
||||
cost_of_goods_sold_account = self.get_company_default("default_expense_account")
|
||||
|
||||
@@ -2024,6 +2024,7 @@
|
||||
"fieldname": "amount_eligible_for_commission",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount Eligible for Commission",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -2188,7 +2189,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2025-03-17 19:32:31.809658",
|
||||
"modified": "2025-06-26 14:06:56.773552",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -62,8 +62,8 @@ frappe.ui.form.on("Asset Movement", {
|
||||
fieldnames_to_be_altered = {
|
||||
target_location: { read_only: 0, reqd: 1 },
|
||||
source_location: { read_only: 1, reqd: 0 },
|
||||
from_employee: { read_only: 0, reqd: 0 },
|
||||
to_employee: { read_only: 1, reqd: 0 },
|
||||
from_employee: { read_only: 1, reqd: 0 },
|
||||
to_employee: { read_only: 0, reqd: 0 },
|
||||
};
|
||||
} else if (frm.doc.purpose === "Issue") {
|
||||
fieldnames_to_be_altered = {
|
||||
@@ -72,6 +72,13 @@ frappe.ui.form.on("Asset Movement", {
|
||||
from_employee: { read_only: 1, reqd: 0 },
|
||||
to_employee: { read_only: 0, reqd: 1 },
|
||||
};
|
||||
} else if (frm.doc.purpose === "Transfer and Issue") {
|
||||
fieldnames_to_be_altered = {
|
||||
target_location: { read_only: 0, reqd: 1 },
|
||||
source_location: { read_only: 0, reqd: 1 },
|
||||
from_employee: { read_only: 0, reqd: 1 },
|
||||
to_employee: { read_only: 0, reqd: 1 },
|
||||
};
|
||||
}
|
||||
if (fieldnames_to_be_altered) {
|
||||
Object.keys(fieldnames_to_be_altered).forEach((fieldname) => {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"fieldname": "purpose",
|
||||
"fieldtype": "Select",
|
||||
"label": "Purpose",
|
||||
"options": "\nIssue\nReceipt\nTransfer",
|
||||
"options": "\nIssue\nReceipt\nTransfer\nTransfer and Issue",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -93,10 +93,11 @@
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-28 16:54:26.571083",
|
||||
"modified": "2025-05-30 17:01:55.864353",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Movement",
|
||||
@@ -149,7 +150,8 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,105 +24,81 @@ class AssetMovement(Document):
|
||||
amended_from: DF.Link | None
|
||||
assets: DF.Table[AssetMovementItem]
|
||||
company: DF.Link
|
||||
purpose: DF.Literal["", "Issue", "Receipt", "Transfer"]
|
||||
purpose: DF.Literal["", "Issue", "Receipt", "Transfer", "Transfer and Issue"]
|
||||
reference_doctype: DF.Link | None
|
||||
reference_name: DF.DynamicLink | None
|
||||
transaction_date: DF.Datetime
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_asset()
|
||||
self.validate_location()
|
||||
self.validate_employee()
|
||||
|
||||
def validate_asset(self):
|
||||
for d in self.assets:
|
||||
status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"])
|
||||
if self.purpose == "Transfer" and status in ("Draft", "Scrapped", "Sold"):
|
||||
frappe.throw(_("{0} asset cannot be transferred").format(status))
|
||||
self.validate_asset(d)
|
||||
self.validate_movement(d)
|
||||
|
||||
if company != self.company:
|
||||
frappe.throw(_("Asset {0} does not belong to company {1}").format(d.asset, self.company))
|
||||
def validate_asset(self, d):
|
||||
status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"])
|
||||
if self.purpose == "Transfer" and status in ("Draft", "Scrapped", "Sold"):
|
||||
frappe.throw(_("{0} asset cannot be transferred").format(status))
|
||||
|
||||
if not (d.source_location or d.target_location or d.from_employee or d.to_employee):
|
||||
frappe.throw(_("Either location or employee must be required"))
|
||||
if company != self.company:
|
||||
frappe.throw(_("Asset {0} does not belong to company {1}").format(d.asset, self.company))
|
||||
|
||||
def validate_location(self):
|
||||
for d in self.assets:
|
||||
if self.purpose in ["Transfer", "Issue"]:
|
||||
current_location = frappe.db.get_value("Asset", d.asset, "location")
|
||||
if d.source_location:
|
||||
if current_location != d.source_location:
|
||||
frappe.throw(
|
||||
_("Asset {0} does not belongs to the location {1}").format(
|
||||
d.asset, d.source_location
|
||||
)
|
||||
)
|
||||
else:
|
||||
d.source_location = current_location
|
||||
def validate_movement(self, d):
|
||||
if self.purpose == "Transfer and Issue":
|
||||
self.validate_location_and_employee(d)
|
||||
elif self.purpose in ["Receipt", "Transfer"]:
|
||||
self.validate_location(d)
|
||||
else:
|
||||
self.validate_employee(d)
|
||||
|
||||
if self.purpose == "Issue":
|
||||
if d.target_location:
|
||||
def validate_location_and_employee(self, d):
|
||||
self.validate_location(d)
|
||||
self.validate_employee(d)
|
||||
|
||||
def validate_location(self, d):
|
||||
if self.purpose in ["Transfer", "Transfer and Issue"]:
|
||||
current_location = frappe.db.get_value("Asset", d.asset, "location")
|
||||
if d.source_location:
|
||||
if current_location != d.source_location:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Issuing cannot be done to a location. Please enter employee to issue the Asset {0} to"
|
||||
).format(d.asset),
|
||||
title=_("Incorrect Movement Purpose"),
|
||||
)
|
||||
if not d.to_employee:
|
||||
frappe.throw(_("Employee is required while issuing Asset {0}").format(d.asset))
|
||||
|
||||
if self.purpose == "Transfer":
|
||||
if d.to_employee:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Transferring cannot be done to an Employee. Please enter location where Asset {0} has to be transferred"
|
||||
).format(d.asset),
|
||||
title=_("Incorrect Movement Purpose"),
|
||||
)
|
||||
if not d.target_location:
|
||||
frappe.throw(
|
||||
_("Target Location is required while transferring Asset {0}").format(d.asset)
|
||||
)
|
||||
if d.source_location == d.target_location:
|
||||
frappe.throw(_("Source and Target Location cannot be same"))
|
||||
|
||||
if self.purpose == "Receipt":
|
||||
if not (d.source_location) and not (d.target_location or d.to_employee):
|
||||
frappe.throw(
|
||||
_("Target Location or To Employee is required while receiving Asset {0}").format(
|
||||
d.asset
|
||||
)
|
||||
)
|
||||
elif d.source_location:
|
||||
if d.from_employee and not d.target_location:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Target Location is required while receiving Asset {0} from an employee"
|
||||
).format(d.asset)
|
||||
)
|
||||
elif d.to_employee and d.target_location:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Asset {0} cannot be received at a location and given to an employee in a single movement"
|
||||
).format(d.asset)
|
||||
)
|
||||
|
||||
def validate_employee(self):
|
||||
for d in self.assets:
|
||||
if d.from_employee:
|
||||
current_custodian = frappe.db.get_value("Asset", d.asset, "custodian")
|
||||
|
||||
if current_custodian != d.from_employee:
|
||||
frappe.throw(
|
||||
_("Asset {0} does not belongs to the custodian {1}").format(d.asset, d.from_employee)
|
||||
_("Asset {0} does not belongs to the location {1}").format(d.asset, d.source_location)
|
||||
)
|
||||
else:
|
||||
d.source_location = current_location
|
||||
if not d.target_location:
|
||||
frappe.throw(_("Target Location is required for transferring Asset {0}").format(d.asset))
|
||||
if d.source_location == d.target_location:
|
||||
frappe.throw(_("Source and Target Location cannot be same"))
|
||||
|
||||
if self.purpose == "Receipt":
|
||||
if not d.target_location:
|
||||
frappe.throw(_("Target Location is required while receiving Asset {0}").format(d.asset))
|
||||
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
|
||||
frappe.throw(
|
||||
_("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company)
|
||||
)
|
||||
|
||||
def validate_employee(self, d):
|
||||
if self.purpose == "Tranfer and Issue":
|
||||
if not d.from_employee:
|
||||
frappe.throw(_("From Employee is required while issuing Asset {0}").format(d.asset))
|
||||
|
||||
if d.from_employee:
|
||||
current_custodian = frappe.db.get_value("Asset", d.asset, "custodian")
|
||||
|
||||
if current_custodian != d.from_employee:
|
||||
frappe.throw(
|
||||
_("Asset {0} does not belongs to the custodian {1}").format(d.asset, d.from_employee)
|
||||
)
|
||||
|
||||
if not d.to_employee:
|
||||
frappe.throw(_("Employee is required while issuing Asset {0}").format(d.asset))
|
||||
|
||||
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
|
||||
frappe.throw(
|
||||
_("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company)
|
||||
)
|
||||
|
||||
def on_submit(self):
|
||||
self.set_latest_location_and_custodian_in_asset()
|
||||
|
||||
@@ -130,53 +106,63 @@ class AssetMovement(Document):
|
||||
self.set_latest_location_and_custodian_in_asset()
|
||||
|
||||
def set_latest_location_and_custodian_in_asset(self):
|
||||
for d in self.assets:
|
||||
current_location, current_employee = self.get_latest_location_and_custodian(d.asset)
|
||||
self.update_asset_location_and_custodian(d.asset, current_location, current_employee)
|
||||
self.log_asset_activity(d.asset, current_location, current_employee)
|
||||
|
||||
def get_latest_location_and_custodian(self, asset):
|
||||
current_location, current_employee = "", ""
|
||||
cond = "1=1"
|
||||
|
||||
for d in self.assets:
|
||||
args = {"asset": d.asset, "company": self.company}
|
||||
# latest entry corresponds to current document's location, employee when transaction date > previous dates
|
||||
# In case of cancellation it corresponds to previous latest document's location, employee
|
||||
args = {"asset": asset, "company": self.company}
|
||||
latest_movement_entry = frappe.db.sql(
|
||||
f"""
|
||||
SELECT asm_item.target_location, asm_item.to_employee
|
||||
FROM `tabAsset Movement Item` asm_item
|
||||
JOIN `tabAsset Movement` asm ON asm_item.parent = asm.name
|
||||
WHERE
|
||||
asm_item.asset = %(asset)s AND
|
||||
asm.company = %(company)s AND
|
||||
asm.docstatus = 1 AND {cond}
|
||||
ORDER BY asm.transaction_date DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
args,
|
||||
)
|
||||
|
||||
# latest entry corresponds to current document's location, employee when transaction date > previous dates
|
||||
# In case of cancellation it corresponds to previous latest document's location, employee
|
||||
latest_movement_entry = frappe.db.sql(
|
||||
f"""
|
||||
SELECT asm_item.target_location, asm_item.to_employee
|
||||
FROM `tabAsset Movement Item` asm_item, `tabAsset Movement` asm
|
||||
WHERE
|
||||
asm_item.parent=asm.name and
|
||||
asm_item.asset=%(asset)s and
|
||||
asm.company=%(company)s and
|
||||
asm.docstatus=1 and {cond}
|
||||
ORDER BY
|
||||
asm.transaction_date desc limit 1
|
||||
""",
|
||||
args,
|
||||
if latest_movement_entry:
|
||||
current_location = latest_movement_entry[0][0]
|
||||
current_employee = latest_movement_entry[0][1]
|
||||
|
||||
return current_location, current_employee
|
||||
|
||||
def update_asset_location_and_custodian(self, asset_id, location, employee):
|
||||
asset = frappe.get_doc("Asset", asset_id)
|
||||
|
||||
if employee and employee != asset.custodian:
|
||||
frappe.db.set_value("Asset", asset_id, "custodian", employee)
|
||||
if location and location != asset.location:
|
||||
frappe.db.set_value("Asset", asset_id, "location", location)
|
||||
|
||||
def log_asset_activity(self, asset_id, location, employee):
|
||||
if location and employee:
|
||||
add_asset_activity(
|
||||
asset_id,
|
||||
_("Asset received at Location {0} and issued to Employee {1}").format(
|
||||
get_link_to_form("Location", location),
|
||||
get_link_to_form("Employee", employee),
|
||||
),
|
||||
)
|
||||
elif location:
|
||||
add_asset_activity(
|
||||
asset_id,
|
||||
_("Asset transferred to Location {0}").format(get_link_to_form("Location", location)),
|
||||
)
|
||||
elif employee:
|
||||
add_asset_activity(
|
||||
asset_id,
|
||||
_("Asset issued to Employee {0}").format(get_link_to_form("Employee", employee)),
|
||||
)
|
||||
|
||||
if latest_movement_entry:
|
||||
current_location = latest_movement_entry[0][0]
|
||||
current_employee = latest_movement_entry[0][1]
|
||||
|
||||
frappe.db.set_value("Asset", d.asset, "location", current_location, update_modified=False)
|
||||
frappe.db.set_value("Asset", d.asset, "custodian", current_employee, update_modified=False)
|
||||
|
||||
if current_location and current_employee:
|
||||
add_asset_activity(
|
||||
d.asset,
|
||||
_("Asset received at Location {0} and issued to Employee {1}").format(
|
||||
get_link_to_form("Location", current_location),
|
||||
get_link_to_form("Employee", current_employee),
|
||||
),
|
||||
)
|
||||
elif current_location:
|
||||
add_asset_activity(
|
||||
d.asset,
|
||||
_("Asset transferred to Location {0}").format(
|
||||
get_link_to_form("Location", current_location)
|
||||
),
|
||||
)
|
||||
elif current_employee:
|
||||
add_asset_activity(
|
||||
d.asset,
|
||||
_("Asset issued to Employee {0}").format(get_link_to_form("Employee", current_employee)),
|
||||
)
|
||||
|
||||
@@ -88,7 +88,7 @@ class TestAssetMovement(unittest.TestCase):
|
||||
)
|
||||
|
||||
# after issuing, asset should belong to an employee not at a location
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), None)
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2")
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "custodian"), employee)
|
||||
|
||||
create_asset_movement(
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
frappe.ui.form.on("Asset Repair", {
|
||||
setup: function (frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
|
||||
|
||||
frm.fields_dict.cost_center.get_query = function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
@@ -167,4 +169,37 @@ frappe.ui.form.on("Asset Repair Consumed Item", {
|
||||
var row = locals[cdt][cdn];
|
||||
frappe.model.set_value(cdt, cdn, "total_value", row.consumed_quantity * row.valuation_rate);
|
||||
},
|
||||
|
||||
pick_serial_and_batch(frm, cdt, cdn) {
|
||||
let item = locals[cdt][cdn];
|
||||
let doc = frm.doc;
|
||||
|
||||
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]).then((r) => {
|
||||
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
|
||||
item.has_serial_no = r.message.has_serial_no;
|
||||
item.has_batch_no = r.message.has_batch_no;
|
||||
item.qty = item.consumed_quantity;
|
||||
item.type_of_transaction = item.consumed_quantity > 0 ? "Outward" : "Inward";
|
||||
|
||||
item.title = item.has_serial_no ? __("Select Serial No") : __("Select Batch No");
|
||||
|
||||
if (item.has_serial_no && item.has_batch_no) {
|
||||
item.title = __("Select Serial and Batch");
|
||||
}
|
||||
frm.doc.posting_date = frappe.datetime.get_today();
|
||||
frm.doc.posting_time = frappe.datetime.now_time();
|
||||
|
||||
new erpnext.SerialBatchPackageSelector(frm, item, (r) => {
|
||||
if (r) {
|
||||
frappe.model.set_value(item.doctype, item.name, {
|
||||
serial_and_batch_bundle: r.name,
|
||||
use_serial_batch_fields: 0,
|
||||
valuation_rate: r.avg_rate,
|
||||
consumed_quantity: Math.abs(r.total_qty),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -138,6 +138,7 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Asset",
|
||||
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",null]]]",
|
||||
"options": "Asset",
|
||||
"reqd": 1
|
||||
},
|
||||
@@ -248,7 +249,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-13 16:14:14.398356",
|
||||
"modified": "2025-06-29 22:30:00.589597",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair",
|
||||
@@ -292,4 +293,4 @@
|
||||
"title_field": "asset_name",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ class AssetRepair(AccountsController):
|
||||
|
||||
def validate(self):
|
||||
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
||||
self.validate_asset()
|
||||
self.validate_dates()
|
||||
self.update_status()
|
||||
|
||||
@@ -60,6 +61,14 @@ class AssetRepair(AccountsController):
|
||||
self.set_stock_items_cost()
|
||||
self.calculate_total_repair_cost()
|
||||
|
||||
def validate_asset(self):
|
||||
if self.asset_doc.status in ("Sold", "Fully Depreciated", "Scrapped"):
|
||||
frappe.throw(
|
||||
_("Asset {0} is in {1} status and cannot be repaired.").format(
|
||||
get_link_to_form("Asset", self.asset), self.asset_doc.status
|
||||
)
|
||||
)
|
||||
|
||||
def validate_dates(self):
|
||||
if self.completion_date and (self.failure_date > self.completion_date):
|
||||
frappe.throw(
|
||||
@@ -131,6 +140,13 @@ class AssetRepair(AccountsController):
|
||||
),
|
||||
)
|
||||
|
||||
def cancel_sabb(self):
|
||||
for row in self.stock_items:
|
||||
if sabb := row.serial_and_batch_bundle:
|
||||
row.db_set("serial_and_batch_bundle", None)
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", sabb)
|
||||
doc.cancel()
|
||||
|
||||
def before_cancel(self):
|
||||
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
||||
|
||||
@@ -172,6 +188,8 @@ class AssetRepair(AccountsController):
|
||||
),
|
||||
)
|
||||
|
||||
self.cancel_sabb()
|
||||
|
||||
def after_delete(self):
|
||||
frappe.get_doc("Asset", self.asset).set_status()
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt, nowdate, nowtime, today
|
||||
from frappe.utils import add_months, flt, get_first_day, nowdate, nowtime, today
|
||||
|
||||
from erpnext.assets.doctype.asset.asset import (
|
||||
get_asset_account,
|
||||
get_asset_value_after_depreciation,
|
||||
make_sales_invoice,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.test_asset import (
|
||||
create_asset,
|
||||
@@ -33,6 +34,33 @@ class TestAssetRepair(unittest.TestCase):
|
||||
create_item("_Test Stock Item")
|
||||
frappe.db.sql("delete from `tabTax Rule`")
|
||||
|
||||
def test_asset_status(self):
|
||||
date = nowdate()
|
||||
purchase_date = add_months(get_first_day(date), -2)
|
||||
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date=purchase_date,
|
||||
purchase_date=purchase_date,
|
||||
expected_value_after_useful_life=10000,
|
||||
total_number_of_depreciations=10,
|
||||
frequency_of_depreciation=1,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
|
||||
si.customer = "_Test Customer"
|
||||
si.due_date = date
|
||||
si.get("items")[0].rate = 25000
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
asset.reload()
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||
asset_repair = frappe.new_doc("Asset Repair")
|
||||
asset_repair.update({"company": "_Test Company", "asset": asset.name, "asset_name": asset.asset_name})
|
||||
self.assertRaises(frappe.ValidationError, asset_repair.save)
|
||||
|
||||
def test_update_status(self):
|
||||
asset = create_asset(submit=1)
|
||||
initial_status = asset.status
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
"consumed_quantity",
|
||||
"total_value",
|
||||
"serial_no",
|
||||
"column_break_xzfr",
|
||||
"pick_serial_and_batch",
|
||||
"serial_and_batch_bundle"
|
||||
],
|
||||
"fields": [
|
||||
@@ -61,12 +63,21 @@
|
||||
"label": "Warehouse",
|
||||
"options": "Warehouse",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "pick_serial_and_batch",
|
||||
"fieldtype": "Button",
|
||||
"label": "Pick Serial / Batch"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xzfr",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-13 12:01:47.147333",
|
||||
"modified": "2025-06-27 14:52:56.311166",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair Consumed Item",
|
||||
@@ -76,4 +87,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +192,10 @@ class AssetValueAdjustment(Document):
|
||||
if asset.calculate_depreciation:
|
||||
for row in asset.finance_books:
|
||||
if cstr(row.finance_book) == cstr(self.finance_book):
|
||||
salvage_value_adjustment = (
|
||||
self.get_adjusted_salvage_value_amount(row, difference_amount) or 0
|
||||
)
|
||||
row.expected_value_after_useful_life += salvage_value_adjustment
|
||||
row.value_after_depreciation += flt(difference_amount)
|
||||
row.db_update()
|
||||
|
||||
@@ -208,6 +212,11 @@ class AssetValueAdjustment(Document):
|
||||
asset.save()
|
||||
asset.set_status()
|
||||
|
||||
def get_adjusted_salvage_value_amount(self, row, difference_amount):
|
||||
if row.expected_value_after_useful_life:
|
||||
salvage_value_adjustment = (difference_amount * row.salvage_value_percentage) / 100
|
||||
return flt(salvage_value_adjustment if self.docstatus == 1 else -1 * salvage_value_adjustment)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_value_of_accounting_dimensions(asset_name):
|
||||
|
||||
@@ -292,6 +292,43 @@ class TestAssetValueAdjustment(unittest.TestCase):
|
||||
asset_doc.load_from_db()
|
||||
self.assertEqual(asset_doc.value_after_depreciation, 50000.0)
|
||||
|
||||
def test_expected_value_after_useful_life(self):
|
||||
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location")
|
||||
|
||||
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
|
||||
asset_doc = frappe.get_doc("Asset", asset_name)
|
||||
asset_doc.calculate_depreciation = 1
|
||||
asset_doc.available_for_use_date = "2023-01-15"
|
||||
asset_doc.purchase_date = "2023-01-15"
|
||||
|
||||
asset_doc.append(
|
||||
"finance_books",
|
||||
{
|
||||
"expected_value_after_useful_life": 5000,
|
||||
"salvage_value_percentage": 5,
|
||||
"depreciation_method": "Straight Line",
|
||||
"total_number_of_depreciations": 12,
|
||||
"frequency_of_depreciation": 1,
|
||||
"depreciation_start_date": "2023-01-31",
|
||||
},
|
||||
)
|
||||
asset_doc.submit()
|
||||
self.assertEqual(asset_doc.finance_books[0].expected_value_after_useful_life, 5000.0)
|
||||
|
||||
current_asset_value = get_asset_value_after_depreciation(asset_doc.name)
|
||||
adj_doc = make_asset_value_adjustment(
|
||||
asset=asset_doc.name,
|
||||
current_asset_value=current_asset_value,
|
||||
new_asset_value=40000,
|
||||
date="2023-08-21",
|
||||
)
|
||||
adj_doc.submit()
|
||||
difference_amount = adj_doc.new_asset_value - adj_doc.current_asset_value
|
||||
self.assertEqual(difference_amount, -60000)
|
||||
asset_doc.load_from_db()
|
||||
self.assertEqual(asset_doc.finance_books[0].value_after_depreciation, 40000.0)
|
||||
self.assertEqual(asset_doc.finance_books[0].expected_value_after_useful_life, 2000.0)
|
||||
|
||||
|
||||
def make_asset_value_adjustment(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -156,6 +156,17 @@ status_map = {
|
||||
["Draft", None],
|
||||
["Completed", "eval:self.docstatus == 1"],
|
||||
],
|
||||
"Pick List": [
|
||||
["Draft", None],
|
||||
["Open", "eval:self.docstatus == 1"],
|
||||
["Completed", "stock_entry_exists"],
|
||||
[
|
||||
"Partly Delivered",
|
||||
"eval:self.purpose == 'Delivery' and self.delivery_status == 'Partly Delivered'",
|
||||
],
|
||||
["Completed", "eval:self.purpose == 'Delivery' and self.delivery_status == 'Fully Delivered'"],
|
||||
["Cancelled", "eval:self.docstatus == 2"],
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ def get_columns(filters, trans):
|
||||
def validate_filters(filters):
|
||||
for f in ["Fiscal Year", "Based On", "Period", "Company"]:
|
||||
if not filters.get(f.lower().replace(" ", "_")):
|
||||
frappe.throw(_("{0} is mandatory").format(f))
|
||||
frappe.throw(_("{0} is mandatory").format(_(f)))
|
||||
|
||||
if not frappe.db.exists("Fiscal Year", filters.get("fiscal_year")):
|
||||
frappe.throw(_("Fiscal Year {0} Does Not Exist").format(filters.get("fiscal_year")))
|
||||
|
||||
@@ -497,7 +497,7 @@
|
||||
{
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"label": "State"
|
||||
"label": "State/Province"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
@@ -512,11 +512,12 @@
|
||||
"show_dashboard": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-user",
|
||||
"idx": 5,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2025-01-31 13:40:08.094759",
|
||||
"modified": "2025-06-26 11:02:01.158901",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Lead",
|
||||
@@ -575,6 +576,7 @@
|
||||
"role": "Sales User"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "lead_name,lead_owner,status",
|
||||
"sender_field": "email_id",
|
||||
"sender_name_field": "lead_name",
|
||||
|
||||
@@ -323,7 +323,8 @@ def _make_customer(source_name, target_doc=None, ignore_permissions=False):
|
||||
target.customer_type = "Individual"
|
||||
target.customer_name = source.lead_name
|
||||
|
||||
target.customer_group = frappe.db.get_default("Customer Group")
|
||||
if not target.customer_group:
|
||||
target.customer_group = frappe.db.get_default("Customer Group")
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Lead",
|
||||
|
||||
@@ -613,7 +613,7 @@
|
||||
{
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"label": "State"
|
||||
"label": "State/Province"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
@@ -622,10 +622,11 @@
|
||||
"options": "Country"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-info-sign",
|
||||
"idx": 195,
|
||||
"links": [],
|
||||
"modified": "2024-08-20 04:12:29.095761",
|
||||
"modified": "2025-06-26 11:16:13.665866",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Opportunity",
|
||||
@@ -657,6 +658,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "status,transaction_date,party_name,opportunity_type,territory,company",
|
||||
"sender_field": "contact_email",
|
||||
"show_name_in_global_search": 1,
|
||||
|
||||
@@ -58,7 +58,7 @@ def get_columns():
|
||||
{"label": _("Address"), "fieldname": "address", "fieldtype": "Data", "width": 130},
|
||||
{"label": _("Postal Code"), "fieldname": "pincode", "fieldtype": "Data", "width": 90},
|
||||
{"label": _("City"), "fieldname": "city", "fieldtype": "Data", "width": 100},
|
||||
{"label": _("State"), "fieldname": "state", "fieldtype": "Data", "width": 100},
|
||||
{"label": _("State/Province"), "fieldname": "state", "fieldtype": "Data", "width": 100},
|
||||
{
|
||||
"label": _("Country"),
|
||||
"fieldname": "country",
|
||||
|
||||
@@ -2368,6 +2368,105 @@ class TestWorkOrder(FrappeTestCase):
|
||||
|
||||
stock_entry.submit()
|
||||
|
||||
def test_disassembly_order_with_qty_behavior(self):
|
||||
# Create raw material and FG item
|
||||
raw_item = make_item("Test Raw for Disassembly", {"is_stock_item": 1}).name
|
||||
fg_item = make_item("Test FG for Disassembly", {"is_stock_item": 1}).name
|
||||
bom = make_bom(item=fg_item, quantity=10, raw_materials=[raw_item], rm_qty=5)
|
||||
|
||||
# Create and submit a Work Order for 10 qty
|
||||
wo = make_wo_order_test_record(production_item=fg_item, qty=10, bom_no=bom.name, status="Not Started")
|
||||
|
||||
# create material receipt stock entry for raw material
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||
make_stock_entry as make_stock_entry_test_record,
|
||||
)
|
||||
|
||||
make_stock_entry_test_record(
|
||||
item_code=raw_item,
|
||||
purpose="Material Receipt",
|
||||
target=wo.wip_warehouse,
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
make_stock_entry_test_record(
|
||||
item_code=raw_item,
|
||||
purpose="Material Receipt",
|
||||
target=wo.fg_warehouse,
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
# create material transfer for manufacture stock entry
|
||||
se_for_material_tranfer_mfr = frappe.get_doc(
|
||||
make_stock_entry(wo.name, "Material Transfer for Manufacture", wo.qty)
|
||||
)
|
||||
se_for_material_tranfer_mfr.items[0].s_warehouse = wo.wip_warehouse
|
||||
se_for_material_tranfer_mfr.save()
|
||||
se_for_material_tranfer_mfr.submit()
|
||||
|
||||
se_for_manufacture = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", wo.qty))
|
||||
se_for_manufacture.submit()
|
||||
|
||||
# Simulate a disassembly stock entry
|
||||
disassemble_qty = 4
|
||||
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Disassemble", disassemble_qty))
|
||||
stock_entry.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": fg_item,
|
||||
"qty": disassemble_qty,
|
||||
"s_warehouse": wo.fg_warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
for bom_item in bom.items:
|
||||
stock_entry.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": bom_item.item_code,
|
||||
"qty": (bom_item.qty / bom.quantity) * disassemble_qty,
|
||||
"t_warehouse": wo.source_warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
wo.reload()
|
||||
stock_entry.save()
|
||||
stock_entry.submit()
|
||||
|
||||
# Assert FG item is present with correct qty
|
||||
finished_good_entry = next((item for item in stock_entry.items if item.item_code == fg_item), None)
|
||||
self.assertIsNotNone(finished_good_entry, "Finished good item missing from stock entry")
|
||||
self.assertEqual(
|
||||
finished_good_entry.qty,
|
||||
disassemble_qty,
|
||||
f"Expected FG qty {disassemble_qty}, found {finished_good_entry.qty}",
|
||||
)
|
||||
|
||||
# Assert raw materials
|
||||
for item in stock_entry.items:
|
||||
if item.item_code == fg_item:
|
||||
continue
|
||||
bom_item = next((i for i in bom.items if i.item_code == item.item_code), None)
|
||||
if bom_item:
|
||||
expected_qty = (bom_item.qty / bom.quantity) * disassemble_qty
|
||||
self.assertAlmostEqual(
|
||||
item.qty,
|
||||
expected_qty,
|
||||
places=3,
|
||||
msg=f"Raw item {item.item_code} qty mismatch: expected {expected_qty}, got {item.qty}",
|
||||
)
|
||||
else:
|
||||
self.fail(f"Unexpected item {item.item_code} found in stock entry")
|
||||
|
||||
wo.reload()
|
||||
# Assert disassembled_qty field updated in Work Order
|
||||
self.assertEqual(
|
||||
wo.disassembled_qty,
|
||||
disassemble_qty,
|
||||
f"Work Order disassembled_qty mismatch: expected {disassemble_qty}, got {wo.disassembled_qty}",
|
||||
)
|
||||
|
||||
def test_components_alternate_item_for_bom_based_manufacture_entry(self):
|
||||
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
|
||||
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 1)
|
||||
@@ -3118,6 +3217,7 @@ def make_wo_order_test_record(**args):
|
||||
wo_order.transfer_material_against = args.transfer_material_against or "Work Order"
|
||||
wo_order.from_wip_warehouse = args.from_wip_warehouse or 0
|
||||
wo_order.batch_size = args.batch_size or 0
|
||||
wo_order.status = args.status or "Draft"
|
||||
|
||||
if args.source_warehouse:
|
||||
wo_order.source_warehouse = args.source_warehouse
|
||||
|
||||
@@ -803,7 +803,7 @@ erpnext.work_order = {
|
||||
get_max_transferable_qty: (frm, purpose) => {
|
||||
let max = 0;
|
||||
if (purpose === "Disassemble") {
|
||||
return flt(frm.doc.produced_qty);
|
||||
return flt(frm.doc.produced_qty - frm.doc.disassembled_qty);
|
||||
}
|
||||
|
||||
if (frm.doc.skip_transfer) {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"qty",
|
||||
"material_transferred_for_manufacturing",
|
||||
"produced_qty",
|
||||
"disassembled_qty",
|
||||
"process_loss_qty",
|
||||
"project",
|
||||
"section_break_ndpq",
|
||||
@@ -586,6 +587,14 @@
|
||||
{
|
||||
"fieldname": "section_break_ndpq",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus==1",
|
||||
"fieldname": "disassembled_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Disassembled Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cogs",
|
||||
@@ -593,7 +602,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-11 15:47:13.454422",
|
||||
"modified": "2025-06-21 00:55:45.916224",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order",
|
||||
@@ -629,4 +638,4 @@
|
||||
"title_field": "production_item",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ class WorkOrder(Document):
|
||||
company: DF.Link
|
||||
corrective_operation_cost: DF.Currency
|
||||
description: DF.SmallText | None
|
||||
disassembled_qty: DF.Float
|
||||
expected_delivery_date: DF.Date | None
|
||||
fg_warehouse: DF.Link
|
||||
from_wip_warehouse: DF.Check
|
||||
@@ -406,6 +407,18 @@ class WorkOrder(Document):
|
||||
self.set_produced_qty_for_sub_assembly_item()
|
||||
self.update_production_plan_status()
|
||||
|
||||
def update_disassembled_qty(self, qty, is_cancel=False):
|
||||
if is_cancel:
|
||||
self.disassembled_qty = max(0, self.disassembled_qty - qty)
|
||||
else:
|
||||
if self.docstatus == 1:
|
||||
self.disassembled_qty += qty
|
||||
|
||||
if not is_cancel and self.disassembled_qty > self.produced_qty:
|
||||
frappe.throw(_("Cannot disassemble more than produced quantity."))
|
||||
|
||||
self.db_set("disassembled_qty", self.disassembled_qty)
|
||||
|
||||
def get_transferred_or_manufactured_qty(self, purpose):
|
||||
table = frappe.qb.DocType("Stock Entry")
|
||||
query = frappe.qb.from_(table).where(
|
||||
@@ -1475,7 +1488,7 @@ def make_stock_entry(work_order_id, purpose, qty=None, target_warehouse=None):
|
||||
stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse
|
||||
|
||||
stock_entry.set_stock_entry_type()
|
||||
stock_entry.get_items()
|
||||
stock_entry.get_items(qty, work_order.production_item)
|
||||
|
||||
if purpose != "Disassemble":
|
||||
stock_entry.set_serial_no_batch_for_finished_good()
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Floor, Sum
|
||||
from frappe.utils import cint
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -20,8 +18,7 @@ def execute(filters=None):
|
||||
|
||||
|
||||
def get_columns():
|
||||
"""return columns"""
|
||||
columns = [
|
||||
return [
|
||||
_("Item") + ":Link/Item:150",
|
||||
_("Item Name") + "::240",
|
||||
_("Description") + "::300",
|
||||
@@ -32,55 +29,54 @@ def get_columns():
|
||||
_("Enough Parts to Build") + ":Float:200",
|
||||
]
|
||||
|
||||
return columns
|
||||
|
||||
|
||||
def get_bom_stock(filters):
|
||||
qty_to_produce = filters.get("qty_to_produce")
|
||||
if cint(qty_to_produce) <= 0:
|
||||
frappe.throw(_("Quantity to Produce should be greater than zero."))
|
||||
|
||||
if filters.get("show_exploded_view"):
|
||||
bom_item_table = "BOM Explosion Item"
|
||||
else:
|
||||
bom_item_table = "BOM Item"
|
||||
bom_item_table = "BOM Explosion Item" if filters.get("show_exploded_view") else "BOM Item"
|
||||
|
||||
warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1)
|
||||
warehouse = filters.get("warehouse")
|
||||
warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1)
|
||||
|
||||
BOM = frappe.qb.DocType("BOM")
|
||||
BOM_ITEM = frappe.qb.DocType(bom_item_table)
|
||||
BIN = frappe.qb.DocType("Bin")
|
||||
WH = frappe.qb.DocType("Warehouse")
|
||||
CONDITIONS = ()
|
||||
|
||||
if warehouse_details:
|
||||
CONDITIONS = ExistsCriterion(
|
||||
frappe.qb.from_(WH)
|
||||
.select(WH.name)
|
||||
.where(
|
||||
(WH.lft >= warehouse_details.lft)
|
||||
& (WH.rgt <= warehouse_details.rgt)
|
||||
& (BIN.warehouse == WH.name)
|
||||
)
|
||||
bin_subquery = (
|
||||
frappe.qb.from_(BIN)
|
||||
.join(WH)
|
||||
.on(BIN.warehouse == WH.name)
|
||||
.select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty"))
|
||||
.where((WH.lft >= warehouse_details.lft) & (WH.rgt <= warehouse_details.rgt))
|
||||
.groupby(BIN.item_code)
|
||||
)
|
||||
else:
|
||||
CONDITIONS = BIN.warehouse == filters.get("warehouse")
|
||||
bin_subquery = (
|
||||
frappe.qb.from_(BIN)
|
||||
.select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty"))
|
||||
.where(BIN.warehouse == warehouse)
|
||||
.groupby(BIN.item_code)
|
||||
)
|
||||
|
||||
QUERY = (
|
||||
frappe.qb.from_(BOM)
|
||||
.inner_join(BOM_ITEM)
|
||||
.join(BOM_ITEM)
|
||||
.on(BOM.name == BOM_ITEM.parent)
|
||||
.left_join(BIN)
|
||||
.on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS))
|
||||
.left_join(bin_subquery)
|
||||
.on(BOM_ITEM.item_code == bin_subquery.item_code)
|
||||
.select(
|
||||
BOM_ITEM.item_code,
|
||||
BOM_ITEM.item_name,
|
||||
BOM_ITEM.description,
|
||||
BOM_ITEM.stock_qty,
|
||||
Sum(BOM_ITEM.stock_qty),
|
||||
BOM_ITEM.stock_uom,
|
||||
BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity,
|
||||
BIN.actual_qty.as_("actual_qty"),
|
||||
Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))),
|
||||
(Sum(BOM_ITEM.stock_qty) * qty_to_produce) / BOM.quantity,
|
||||
bin_subquery.actual_qty,
|
||||
Floor(bin_subquery.actual_qty / ((Sum(BOM_ITEM.stock_qty) * qty_to_produce) / BOM.quantity)),
|
||||
)
|
||||
.where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM"))
|
||||
.groupby(BOM_ITEM.item_code)
|
||||
|
||||
@@ -409,4 +409,5 @@ erpnext.patches.v15_0.set_cancelled_status_to_cancelled_pos_invoice
|
||||
erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports
|
||||
erpnext.patches.v14_0.update_full_name_in_contract
|
||||
erpnext.patches.v15_0.drop_sle_indexes
|
||||
erpnext.patches.v15_0.update_pick_list_fields
|
||||
erpnext.patches.v15_0.update_pegged_currencies
|
||||
|
||||
28
erpnext/patches/v15_0/update_pick_list_fields.py
Normal file
28
erpnext/patches/v15_0/update_pick_list_fields.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import frappe
|
||||
from frappe.query_builder.functions import IfNull
|
||||
|
||||
|
||||
def execute():
|
||||
update_delivery_note()
|
||||
update_pick_list_items()
|
||||
|
||||
|
||||
def update_delivery_note():
|
||||
DN = frappe.qb.DocType("Delivery Note")
|
||||
DNI = frappe.qb.DocType("Delivery Note Item")
|
||||
|
||||
frappe.qb.update(DNI).join(DN).on(DN.name == DNI.parent).set(DNI.against_pick_list, DN.pick_list).where(
|
||||
IfNull(DN.pick_list, "") != ""
|
||||
).run()
|
||||
|
||||
|
||||
def update_pick_list_items():
|
||||
PL = frappe.qb.DocType("Pick List")
|
||||
PLI = frappe.qb.DocType("Pick List Item")
|
||||
|
||||
pick_lists = frappe.qb.from_(PL).select(PL.name).where(PL.status == "Completed").run(pluck="name")
|
||||
|
||||
if not pick_lists:
|
||||
return
|
||||
|
||||
frappe.qb.update(PLI).set(PLI.delivered_qty, PLI.picked_qty).where(PLI.parent.isin(pick_lists)).run()
|
||||
@@ -8,7 +8,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
let me = this;
|
||||
|
||||
this.set_fields_onload_for_line_item();
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
|
||||
this.frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
|
||||
|
||||
frappe.flags.hide_serial_batch_dialog = true;
|
||||
frappe.ui.form.on(this.frm.doctype + " Item", "rate", function(frm, cdt, cdn) {
|
||||
|
||||
@@ -1004,7 +1004,7 @@ erpnext.utils.map_current_doc = function (opts) {
|
||||
|
||||
if (
|
||||
opts.allow_child_item_selection ||
|
||||
["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)
|
||||
["Purchase Receipt", "Delivery Note", "Pick List"].includes(opts.source_doctype)
|
||||
) {
|
||||
// args contains filtered child docnames
|
||||
opts.args = args;
|
||||
|
||||
@@ -81,7 +81,7 @@ frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm
|
||||
fieldtype: "Data",
|
||||
},
|
||||
{
|
||||
label: __("State"),
|
||||
label: __("State/Province"),
|
||||
fieldname: "state",
|
||||
fieldtype: "Data",
|
||||
},
|
||||
|
||||
@@ -284,13 +284,24 @@
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
margin-top: var(--margin-md);
|
||||
column-gap: var(--padding-sm);
|
||||
row-gap: var(--padding-xs);
|
||||
row-gap: var(--padding-sm);
|
||||
}
|
||||
|
||||
> .transactions-label {
|
||||
@extend .label;
|
||||
> .transactions-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: var(--margin-md);
|
||||
margin-bottom: var(--margin-sm);
|
||||
|
||||
> .recent-transactions {
|
||||
@extend .label;
|
||||
}
|
||||
|
||||
> .last-transaction {
|
||||
font-weight: 400;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,8 +310,8 @@
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
margin-right: -12px;
|
||||
padding-right: 12px;
|
||||
margin-left: -10px;
|
||||
scrollbar-width: thin;
|
||||
|
||||
> .no-transactions-placeholder {
|
||||
height: 100%;
|
||||
@@ -611,6 +622,11 @@
|
||||
background-color: var(--gray-50);
|
||||
}
|
||||
|
||||
&.invoice-selected {
|
||||
background-color: var(--control-bg);
|
||||
}
|
||||
|
||||
> .invoice-name-customer,
|
||||
> .invoice-name-date {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -630,6 +646,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
> .invoice-name-date {
|
||||
> .invoice-name {
|
||||
font-size: var(--text-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
> .invoice-date {
|
||||
@extend .nowrap;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
}
|
||||
|
||||
> .invoice-total-date,
|
||||
> .invoice-total-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -650,6 +681,18 @@
|
||||
justify-content: right;
|
||||
}
|
||||
}
|
||||
|
||||
> .invoice-total-status {
|
||||
> .invoice-total {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
> .invoice-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .item-details-container {
|
||||
|
||||
@@ -1741,8 +1741,8 @@ def create_pick_list(source_name, target_doc=None):
|
||||
"doctype": "Pick List Item",
|
||||
"field_map": {
|
||||
"parent": "sales_order",
|
||||
"name": "sales_order_item",
|
||||
"parent_detail_docname": "product_bundle_item",
|
||||
"parent_detail_docname": "sales_order_item",
|
||||
"name": "product_bundle_item",
|
||||
},
|
||||
"field_no_map": ["picked_qty"],
|
||||
"postprocess": update_packed_item_qty,
|
||||
|
||||
@@ -344,7 +344,11 @@ def get_past_order_list(search_term, status, limit=20):
|
||||
if search_term and status:
|
||||
invoices_by_customer = frappe.db.get_list(
|
||||
"POS Invoice",
|
||||
filters={"customer": ["like", f"%{search_term}%"], "status": status},
|
||||
filters={"status": status},
|
||||
or_filters={
|
||||
"customer_name": ["like", f"%{search_term}%"],
|
||||
"customer": ["like", f"%{search_term}%"],
|
||||
},
|
||||
fields=fields,
|
||||
page_length=limit,
|
||||
)
|
||||
|
||||
@@ -342,7 +342,13 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
if (customer) {
|
||||
return new Promise((resolve) => {
|
||||
frappe.db
|
||||
.get_value("Customer", customer, ["email_id", "mobile_no", "image", "loyalty_program"])
|
||||
.get_value("Customer", customer, [
|
||||
"email_id",
|
||||
"customer_name",
|
||||
"mobile_no",
|
||||
"image",
|
||||
"loyalty_program",
|
||||
])
|
||||
.then(({ message }) => {
|
||||
const { loyalty_program } = message;
|
||||
// if loyalty program then fetch loyalty points too
|
||||
@@ -439,7 +445,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
|
||||
update_customer_section() {
|
||||
const me = this;
|
||||
const { customer, email_id = "", mobile_no = "", image } = this.customer_info || {};
|
||||
const { customer, customer_name, email_id = "", mobile_no = "", image } = this.customer_info || {};
|
||||
|
||||
if (customer) {
|
||||
this.$customer_section.html(
|
||||
@@ -447,7 +453,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
<div class="customer-display">
|
||||
${this.get_customer_image()}
|
||||
<div class="customer-name-desc">
|
||||
<div class="customer-name">${customer}</div>
|
||||
<div class="customer-name">${customer_name}</div>
|
||||
${get_customer_description()}
|
||||
</div>
|
||||
<div class="reset-customer-btn" data-customer="${escape(customer)}">
|
||||
@@ -867,7 +873,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
|
||||
toggle_customer_info(show) {
|
||||
if (show) {
|
||||
const { customer } = this.customer_info || {};
|
||||
const { customer, customer_name } = this.customer_info || {};
|
||||
|
||||
this.$cart_container.css("display", "none");
|
||||
this.$customer_section.css({
|
||||
@@ -886,8 +892,8 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
<div class="customer-display">
|
||||
${this.get_customer_image()}
|
||||
<div class="customer-name-desc">
|
||||
<div class="customer-name">${customer}</div>
|
||||
<div class="customer-desc"></div>
|
||||
<div class="customer-name">${customer_name}</div>
|
||||
<div class="customer-desc">${customer}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="customer-fields-container">
|
||||
@@ -896,7 +902,10 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
<div class="loyalty_program-field"></div>
|
||||
<div class="loyalty_points-field"></div>
|
||||
</div>
|
||||
<div class="transactions-label">${__("Recent Transactions")}</div>`
|
||||
<div class="transactions-section">
|
||||
<div class="recent-transactions">${__("Recent Transactions")}</div>
|
||||
<div class="last-transaction"></div>
|
||||
</div>`
|
||||
);
|
||||
// transactions need to be in diff div from sticky elem for scrolling
|
||||
this.$customer_section.append(`<div class="customer-transactions"></div>`);
|
||||
@@ -1005,7 +1014,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
|
||||
const elapsed_time = moment(res[0].posting_date + " " + res[0].posting_time).fromNow();
|
||||
this.$customer_section
|
||||
.find(".customer-desc")
|
||||
.find(".last-transaction")
|
||||
.html(`${__("Last transacted")} ${__(elapsed_time)}`);
|
||||
|
||||
res.forEach((invoice) => {
|
||||
@@ -1027,7 +1036,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
</div>
|
||||
<div class="invoice-total-status">
|
||||
<div class="invoice-total">
|
||||
${format_currency(invoice.grand_total, invoice.currency, 0) || 0}
|
||||
${format_currency(invoice.grand_total, invoice.currency, frappe.sys_defaults.currency_precision) || 0}
|
||||
</div>
|
||||
<div class="invoice-status">
|
||||
<span class="indicator-pill whitespace-nowrap ${indicator_color[invoice.status]}">
|
||||
|
||||
@@ -51,7 +51,7 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
|
||||
let stock_uom = unescape(element.attr("data-stock-uom"));
|
||||
|
||||
if (disable_quick_entry) {
|
||||
open_stock_entry(item, warehouse, entry_type);
|
||||
open_stock_entry(item, warehouse, entry_type, stock_uom);
|
||||
} else {
|
||||
if (action === "Add") {
|
||||
let rate = unescape($(this).attr("data-rate"));
|
||||
@@ -66,7 +66,7 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
|
||||
}
|
||||
}
|
||||
|
||||
function open_stock_entry(item, warehouse, entry_type) {
|
||||
function open_stock_entry(item, warehouse, entry_type, stock_uom) {
|
||||
frappe.model.with_doctype("Stock Entry", function () {
|
||||
var doc = frappe.model.get_new_doc("Stock Entry");
|
||||
if (entry_type) {
|
||||
@@ -75,6 +75,9 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
|
||||
|
||||
var row = frappe.model.add_child(doc, "items");
|
||||
row.item_code = item;
|
||||
row.uom = stock_uom;
|
||||
row.stock_uom = stock_uom;
|
||||
row.conversion_factor = 1;
|
||||
|
||||
if (entry_type === "Material Transfer") {
|
||||
row.s_warehouse = warehouse;
|
||||
|
||||
@@ -188,6 +188,55 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!doc.is_return &&
|
||||
doc.status != "Closed" &&
|
||||
this.frm.has_perm("write") &&
|
||||
frappe.model.can_read("Pick List") &&
|
||||
this.frm.doc.docstatus === 0
|
||||
) {
|
||||
this.frm.add_custom_button(
|
||||
__("Pick List"),
|
||||
function () {
|
||||
if (!me.frm.doc.customer) {
|
||||
frappe.throw({
|
||||
title: __("Mandatory"),
|
||||
message: __("Please Select a Customer"),
|
||||
});
|
||||
}
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.stock.doctype.pick_list.pick_list.create_dn_for_pick_lists",
|
||||
source_doctype: "Pick List",
|
||||
target: me.frm,
|
||||
setters: [
|
||||
{
|
||||
fieldname: "customer",
|
||||
default: me.frm.doc.customer,
|
||||
label: __("Customer"),
|
||||
fieldtype: "Link",
|
||||
options: "Customer",
|
||||
reqd: 1,
|
||||
read_only: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "sales_order",
|
||||
label: __("Sales Order"),
|
||||
fieldtype: "Link",
|
||||
options: "Sales Order",
|
||||
link_filters: `[["Sales Order","customer","=","${me.frm.doc.customer}"],["Sales Order","docstatus","=","1"],["Sales Order","delivery_status","not in",["Closed","Fully Delivered"]]]`,
|
||||
},
|
||||
],
|
||||
get_query_filters: {
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
get_query_method: "erpnext.stock.doctype.pick_list.pick_list.get_pick_list_query",
|
||||
size: "extra-large",
|
||||
});
|
||||
},
|
||||
__("Get Items From")
|
||||
);
|
||||
}
|
||||
|
||||
if (!doc.is_return && doc.status != "Closed") {
|
||||
if (doc.docstatus == 1 && frappe.model.can_create("Shipment")) {
|
||||
this.frm.add_custom_button(
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
"ignore_pricing_rule",
|
||||
"items_section",
|
||||
"scan_barcode",
|
||||
"pick_list",
|
||||
"col_break_warehouse",
|
||||
"set_warehouse",
|
||||
"set_target_warehouse",
|
||||
@@ -1218,15 +1217,6 @@
|
||||
"options": "Sales Team",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "pick_list",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Pick List",
|
||||
"options": "Pick List",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "customer.is_internal_customer",
|
||||
|
||||
@@ -174,6 +174,19 @@ class DeliveryNote(SellingController):
|
||||
"overflow_type": "delivery",
|
||||
"no_allowance": 1,
|
||||
},
|
||||
{
|
||||
"source_dt": "Delivery Note Item",
|
||||
"target_dt": "Pick List Item",
|
||||
"join_field": "pick_list_item",
|
||||
"target_field": "delivered_qty",
|
||||
"target_parent_dt": "Pick List",
|
||||
"target_parent_field": "per_delivered",
|
||||
"target_ref_field": "picked_qty",
|
||||
"source_field": "stock_qty",
|
||||
"percent_join_field": "against_pick_list",
|
||||
"status_field": "delivery_status",
|
||||
"keyword": "Delivered",
|
||||
},
|
||||
]
|
||||
if cint(self.is_return):
|
||||
self.status_updater.extend(
|
||||
@@ -326,18 +339,15 @@ class DeliveryNote(SellingController):
|
||||
def set_serial_and_batch_bundle_from_pick_list(self):
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
if not self.pick_list:
|
||||
return
|
||||
|
||||
for item in self.items:
|
||||
if item.use_serial_batch_fields:
|
||||
if item.use_serial_batch_fields or not item.against_pick_list:
|
||||
continue
|
||||
|
||||
if item.pick_list_item and not item.serial_and_batch_bundle:
|
||||
filters = {
|
||||
"item_code": item.item_code,
|
||||
"voucher_type": "Pick List",
|
||||
"voucher_no": self.pick_list,
|
||||
"voucher_no": item.against_pick_list,
|
||||
"voucher_detail_no": item.pick_list_item,
|
||||
}
|
||||
|
||||
@@ -586,7 +596,9 @@ class DeliveryNote(SellingController):
|
||||
def update_pick_list_status(self):
|
||||
from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status
|
||||
|
||||
update_pick_list_status(self.pick_list)
|
||||
pick_lists = {row.against_pick_list for row in self.items if row.against_pick_list}
|
||||
for pick_list in pick_lists:
|
||||
update_pick_list_status(pick_list)
|
||||
|
||||
def check_next_docstatus(self):
|
||||
submit_rv = frappe.db.sql(
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
"against_sales_invoice",
|
||||
"si_detail",
|
||||
"dn_detail",
|
||||
"against_pick_list",
|
||||
"pick_list_item",
|
||||
"section_break_40",
|
||||
"pick_serial_and_batch",
|
||||
@@ -935,13 +936,23 @@
|
||||
{
|
||||
"fieldname": "column_break_fguf",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "against_pick_list",
|
||||
"fieldtype": "Link",
|
||||
"label": "Against Pick List",
|
||||
"no_copy": 1,
|
||||
"options": "Pick List",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-02-05 14:28:33.322181",
|
||||
"modified": "2025-05-31 18:51:32.651562",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note Item",
|
||||
|
||||
@@ -16,6 +16,7 @@ class DeliveryNoteItem(Document):
|
||||
|
||||
actual_batch_qty: DF.Float
|
||||
actual_qty: DF.Float
|
||||
against_pick_list: DF.Link | None
|
||||
against_sales_invoice: DF.Link | None
|
||||
against_sales_order: DF.Link | None
|
||||
allow_zero_valuation_rate: DF.Check
|
||||
|
||||
@@ -80,6 +80,10 @@ def make_packing_list(doc):
|
||||
update_packed_item_basic_data(item_row, pi_row, bundle_item, item_data)
|
||||
update_packed_item_stock_data(item_row, pi_row, bundle_item, item_data, doc)
|
||||
update_packed_item_price_data(pi_row, item_data, doc)
|
||||
|
||||
if item_row.get("against_pick_list"):
|
||||
update_packed_item_with_pick_list_info(item_row, pi_row)
|
||||
|
||||
update_packed_item_from_cancelled_doc(item_row, bundle_item, pi_row, doc)
|
||||
|
||||
if set_price_from_children: # create/update bundle item wise price dict
|
||||
@@ -228,6 +232,28 @@ def update_packed_item_stock_data(main_item_row, pi_row, packing_item, item_data
|
||||
pi_row.use_serial_batch_fields = frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields")
|
||||
|
||||
|
||||
def update_packed_item_with_pick_list_info(main_item_row, pi_row):
|
||||
pl_row = frappe.db.get_value(
|
||||
"Pick List Item",
|
||||
{
|
||||
"item_code": pi_row.item_code,
|
||||
"sales_order": main_item_row.get("against_sales_order"),
|
||||
"sales_order_item": main_item_row.get("so_detail"),
|
||||
"parent": main_item_row.against_pick_list,
|
||||
},
|
||||
["warehouse", "batch_no", "serial_no"],
|
||||
as_dict=True,
|
||||
order_by="qty desc",
|
||||
)
|
||||
|
||||
if not pl_row:
|
||||
return
|
||||
|
||||
pi_row.warehouse = pl_row.warehouse
|
||||
pi_row.batch_no = pl_row.batch_no
|
||||
pi_row.serial_no = pl_row.serial_no
|
||||
|
||||
|
||||
def update_packed_item_price_data(pi_row, item_data, doc):
|
||||
"Set price as per price list or from the Item master."
|
||||
if pi_row.rate:
|
||||
|
||||
@@ -98,34 +98,28 @@ frappe.ui.form.on("Pick List", {
|
||||
refresh: (frm) => {
|
||||
frm.trigger("add_get_items_button");
|
||||
if (frm.doc.docstatus === 1) {
|
||||
frappe
|
||||
.xcall("erpnext.stock.doctype.pick_list.pick_list.target_document_exists", {
|
||||
pick_list_name: frm.doc.name,
|
||||
purpose: frm.doc.purpose,
|
||||
})
|
||||
.then((target_document_exists) => {
|
||||
frm.set_df_property("locations", "allow_on_submit", target_document_exists ? 0 : 1);
|
||||
const status_completed = frm.doc.status === "Completed";
|
||||
frm.set_df_property("locations", "allow_on_submit", status_completed ? 0 : 1);
|
||||
|
||||
if (target_document_exists) return;
|
||||
if (!status_completed) {
|
||||
frm.add_custom_button(__("Update Current Stock"), () =>
|
||||
frm.trigger("update_pick_list_stock")
|
||||
);
|
||||
|
||||
frm.add_custom_button(__("Update Current Stock"), () =>
|
||||
frm.trigger("update_pick_list_stock")
|
||||
if (frm.doc.purpose === "Delivery") {
|
||||
frm.add_custom_button(
|
||||
__("Create Delivery Note"),
|
||||
() => frm.trigger("create_delivery_note"),
|
||||
__("Create")
|
||||
);
|
||||
|
||||
if (frm.doc.purpose === "Delivery") {
|
||||
frm.add_custom_button(
|
||||
__("Delivery Note"),
|
||||
() => frm.trigger("create_delivery_note"),
|
||||
__("Create")
|
||||
);
|
||||
} else {
|
||||
frm.add_custom_button(
|
||||
__("Stock Entry"),
|
||||
() => frm.trigger("create_stock_entry"),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
frm.add_custom_button(
|
||||
__("Create Stock Entry"),
|
||||
() => frm.trigger("create_stock_entry"),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (frm.doc.purpose === "Delivery" && frm.doc.status === "Open") {
|
||||
if (frm.doc.__onload && frm.doc.__onload.has_unreserved_stock) {
|
||||
|
||||
@@ -30,7 +30,11 @@
|
||||
"amended_from",
|
||||
"print_settings_section",
|
||||
"group_same_items",
|
||||
"status"
|
||||
"status_section",
|
||||
"status",
|
||||
"column_break_qyam",
|
||||
"delivery_status",
|
||||
"per_delivered"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -181,7 +185,7 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"no_copy": 1,
|
||||
"options": "Draft\nOpen\nCompleted\nCancelled",
|
||||
"options": "Draft\nOpen\nPartly Delivered\nCompleted\nCancelled",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"report_hide": 1,
|
||||
@@ -208,11 +212,42 @@
|
||||
"fieldname": "ignore_pricing_rule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Pricing Rule"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "status_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Status",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "delivery_status",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Delivery Status",
|
||||
"no_copy": 1,
|
||||
"options": "Not Delivered\nFully Delivered\nPartly Delivered",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal && doc.purpose === \"Delivery\"",
|
||||
"description": "% of materials delivered against this Pick List",
|
||||
"fieldname": "per_delivered",
|
||||
"fieldtype": "Percent",
|
||||
"label": "% Delivered",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_qyam",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-08-14 13:20:42.168827",
|
||||
"modified": "2025-05-31 19:18:30.860044",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Pick List",
|
||||
@@ -280,6 +315,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
|
||||
@@ -7,8 +7,7 @@ from itertools import groupby
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import map_child_doc
|
||||
from frappe.model.mapper import get_mapped_doc, map_child_doc
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.custom import GROUP_CONCAT
|
||||
from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum
|
||||
@@ -28,11 +27,12 @@ from erpnext.stock.serial_batch_bundle import (
|
||||
get_batches_from_bundle,
|
||||
get_serial_nos_from_bundle,
|
||||
)
|
||||
from erpnext.utilities.transaction_base import TransactionBase
|
||||
|
||||
# TODO: Prioritize SO or WO group warehouse
|
||||
|
||||
|
||||
class PickList(Document):
|
||||
class PickList(TransactionBase):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
@@ -48,6 +48,7 @@ class PickList(Document):
|
||||
consider_rejected_warehouses: DF.Check
|
||||
customer: DF.Link | None
|
||||
customer_name: DF.Data | None
|
||||
delivery_status: DF.Literal["Not Delivered", "Fully Delivered", "Partly Delivered"]
|
||||
for_qty: DF.Float
|
||||
group_same_items: DF.Check
|
||||
ignore_pricing_rule: DF.Check
|
||||
@@ -55,12 +56,13 @@ class PickList(Document):
|
||||
material_request: DF.Link | None
|
||||
naming_series: DF.Literal["STO-PICK-.YYYY.-"]
|
||||
parent_warehouse: DF.Link | None
|
||||
per_delivered: DF.Percent
|
||||
pick_manually: DF.Check
|
||||
prompt_qty: DF.Check
|
||||
purpose: DF.Literal["Material Transfer for Manufacture", "Material Transfer", "Delivery"]
|
||||
scan_barcode: DF.Data | None
|
||||
scan_mode: DF.Check
|
||||
status: DF.Literal["Draft", "Open", "Completed", "Cancelled"]
|
||||
status: DF.Literal["Draft", "Open", "Partly Delivered", "Completed", "Cancelled"]
|
||||
work_order: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -77,6 +79,7 @@ class PickList(Document):
|
||||
self.validate_for_qty()
|
||||
self.validate_stock_qty()
|
||||
self.check_serial_no_status()
|
||||
self.validate_with_previous_doc()
|
||||
|
||||
def before_save(self):
|
||||
self.update_status()
|
||||
@@ -150,6 +153,18 @@ class PickList(Document):
|
||||
title=_("Incorrect Warehouse"),
|
||||
)
|
||||
|
||||
def validate_with_previous_doc(self):
|
||||
super().validate_with_previous_doc(
|
||||
{
|
||||
"Sales Order": {
|
||||
"ref_dn_field": "sales_order",
|
||||
"compare_fields": [
|
||||
["company", "="],
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
def validate_sales_order_percentage(self):
|
||||
# set percentage picked in SO
|
||||
for location in self.get("locations"):
|
||||
@@ -326,19 +341,19 @@ class PickList(Document):
|
||||
doc.submit()
|
||||
|
||||
def update_status(self, status=None, update_modified=True):
|
||||
if not status:
|
||||
if self.docstatus == 0:
|
||||
status = "Draft"
|
||||
elif self.docstatus == 1:
|
||||
if target_document_exists(self.name, self.purpose):
|
||||
status = "Completed"
|
||||
else:
|
||||
status = "Open"
|
||||
elif self.docstatus == 2:
|
||||
status = "Cancelled"
|
||||
|
||||
if status:
|
||||
self.db_set("status", status)
|
||||
self.db_set("status", status, update_modified=update_modified)
|
||||
else:
|
||||
self.set_status(update=True)
|
||||
|
||||
def stock_entry_exists(self):
|
||||
if self.docstatus != 1:
|
||||
return False
|
||||
|
||||
if self.purpose == "Delivery":
|
||||
return False
|
||||
|
||||
return stock_entry_exists(self.name)
|
||||
|
||||
def update_reference_qty(self):
|
||||
packed_items = []
|
||||
@@ -346,7 +361,7 @@ class PickList(Document):
|
||||
|
||||
for item in self.locations:
|
||||
if item.product_bundle_item:
|
||||
packed_items.append(item.sales_order_item)
|
||||
packed_items.append(item.product_bundle_item)
|
||||
elif item.sales_order_item:
|
||||
so_items.append(item.sales_order_item)
|
||||
|
||||
@@ -357,12 +372,12 @@ class PickList(Document):
|
||||
self.update_sales_order_item_qty(so_items)
|
||||
|
||||
def update_packed_items_qty(self, packed_items):
|
||||
picked_items = get_picked_items_qty(packed_items)
|
||||
picked_items = get_picked_items_qty(packed_items, contains_packed_items=True)
|
||||
self.validate_picked_qty(picked_items)
|
||||
|
||||
picked_qty = frappe._dict()
|
||||
for d in picked_items:
|
||||
picked_qty[d.sales_order_item] = d.picked_qty
|
||||
picked_qty[d.product_bundle_item] = d.picked_qty
|
||||
|
||||
for packed_item in packed_items:
|
||||
frappe.db.set_value(
|
||||
@@ -575,7 +590,6 @@ class PickList(Document):
|
||||
# maintain count of each item (useful to limit get query)
|
||||
self.item_count_map.setdefault(item_code, 0)
|
||||
self.item_count_map[item_code] += flt(item.stock_qty, item.precision("stock_qty"))
|
||||
|
||||
return item_map.values()
|
||||
|
||||
def validate_for_qty(self):
|
||||
@@ -739,9 +753,10 @@ class PickList(Document):
|
||||
for item in self.locations:
|
||||
if not item.product_bundle_item:
|
||||
continue
|
||||
product_bundles[item.product_bundle_item] = frappe.db.get_value(
|
||||
|
||||
product_bundles[item.sales_order_item] = frappe.db.get_value(
|
||||
"Sales Order Item",
|
||||
item.product_bundle_item,
|
||||
item.sales_order_item,
|
||||
"item_code",
|
||||
)
|
||||
return product_bundles
|
||||
@@ -757,17 +772,16 @@ class PickList(Document):
|
||||
def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> int:
|
||||
"""Compute how many full bundles can be created from picked items."""
|
||||
precision = frappe.get_precision("Stock Ledger Entry", "qty_after_transaction")
|
||||
|
||||
possible_bundles = []
|
||||
possible_bundles = {}
|
||||
for item in self.locations:
|
||||
if item.product_bundle_item != bundle_row:
|
||||
if item.sales_order_item != bundle_row:
|
||||
continue
|
||||
|
||||
if qty_in_bundle := bundle_items.get(item.item_code):
|
||||
possible_bundles.append(item.picked_qty / qty_in_bundle)
|
||||
else:
|
||||
possible_bundles.append(0)
|
||||
return int(flt(min(possible_bundles), precision or 6))
|
||||
possible_bundles.setdefault(item.product_bundle_item, 0)
|
||||
possible_bundles[item.product_bundle_item] += item.picked_qty / qty_in_bundle
|
||||
|
||||
return int(flt(min(possible_bundles.values()), precision or 6)) if possible_bundles else 0
|
||||
|
||||
def has_unreserved_stock(self):
|
||||
if self.purpose == "Delivery":
|
||||
@@ -800,24 +814,35 @@ def update_pick_list_status(pick_list):
|
||||
doc.run_method("update_status")
|
||||
|
||||
|
||||
def get_picked_items_qty(items) -> list[dict]:
|
||||
def get_picked_items_qty(items, contains_packed_items=False) -> list[dict]:
|
||||
pi_item = frappe.qb.DocType("Pick List Item")
|
||||
return (
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(pi_item)
|
||||
.select(
|
||||
pi_item.sales_order_item,
|
||||
pi_item.product_bundle_item,
|
||||
pi_item.item_code,
|
||||
pi_item.sales_order,
|
||||
Sum(pi_item.stock_qty).as_("stock_qty"),
|
||||
Sum(pi_item.picked_qty).as_("picked_qty"),
|
||||
)
|
||||
.where((pi_item.docstatus == 1) & (pi_item.sales_order_item.isin(items)))
|
||||
.groupby(
|
||||
.where(pi_item.docstatus == 1)
|
||||
.for_update()
|
||||
)
|
||||
|
||||
if contains_packed_items:
|
||||
query = query.groupby(
|
||||
pi_item.product_bundle_item,
|
||||
pi_item.sales_order,
|
||||
).where(pi_item.product_bundle_item.isin(items))
|
||||
else:
|
||||
query = query.groupby(
|
||||
pi_item.sales_order_item,
|
||||
pi_item.sales_order,
|
||||
)
|
||||
.for_update()
|
||||
).run(as_dict=True)
|
||||
).where(pi_item.sales_order_item.isin(items))
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def validate_item_locations(pick_list):
|
||||
@@ -1188,13 +1213,17 @@ def create_delivery_note(source_name, target_doc=None):
|
||||
|
||||
if not all(item.sales_order for item in pick_list.locations):
|
||||
delivery_note = create_dn_wo_so(pick_list)
|
||||
delivery_note.flags.ignore_mandatory = True
|
||||
delivery_note.save()
|
||||
|
||||
frappe.msgprint(_("Delivery Note(s) created for the Pick List"))
|
||||
return delivery_note
|
||||
|
||||
|
||||
def create_dn_wo_so(pick_list):
|
||||
delivery_note = frappe.new_doc("Delivery Note")
|
||||
def create_dn_wo_so(pick_list, delivery_note=None):
|
||||
if not delivery_note:
|
||||
delivery_note = frappe.new_doc("Delivery Note")
|
||||
|
||||
delivery_note.company = pick_list.company
|
||||
|
||||
item_table_mapper_without_so = {
|
||||
@@ -1206,14 +1235,61 @@ def create_dn_wo_so(pick_list):
|
||||
},
|
||||
}
|
||||
map_pl_locations(pick_list, item_table_mapper_without_so, delivery_note)
|
||||
delivery_note.insert(ignore_mandatory=True)
|
||||
|
||||
return delivery_note
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_dn_for_pick_lists(source_name, target_doc=None, kwargs=None):
|
||||
"""Get Items from Multiple Pick Lists and create a Delivery Note for filtered customer"""
|
||||
pick_list = frappe.get_doc("Pick List", source_name)
|
||||
validate_item_locations(pick_list)
|
||||
|
||||
sales_order_arg = kwargs.get("sales_order") if kwargs else None
|
||||
customer_arg = kwargs.get("customer") if kwargs else None
|
||||
|
||||
if sales_order_arg:
|
||||
sales_orders = {sales_order_arg}
|
||||
else:
|
||||
sales_orders = {row.sales_order for row in pick_list.locations if row.sales_order}
|
||||
|
||||
if customer_arg:
|
||||
sales_orders = frappe.get_all(
|
||||
"Sales Order",
|
||||
filters={"customer": customer_arg, "name": ["in", list(sales_orders)]},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
delivery_note = create_dn_from_so(pick_list, sales_orders, delivery_note=target_doc)
|
||||
|
||||
if not sales_order_arg and not all(item.sales_order for item in pick_list.locations):
|
||||
if isinstance(delivery_note, str):
|
||||
delivery_note = frappe.get_doc(frappe.parse_json(delivery_note))
|
||||
|
||||
delivery_note = create_dn_wo_so(pick_list, delivery_note)
|
||||
|
||||
return delivery_note
|
||||
|
||||
|
||||
def create_dn_with_so(sales_dict, pick_list):
|
||||
"""Create Delivery Note for each customer (based on SO) in a Pick List."""
|
||||
delivery_note = None
|
||||
|
||||
for customer in sales_dict:
|
||||
delivery_note = create_dn_from_so(pick_list, sales_dict[customer], None)
|
||||
if delivery_note:
|
||||
delivery_note.flags.ignore_mandatory = True
|
||||
# updates packed_items on save
|
||||
# save as multiple customers are possible
|
||||
delivery_note.save()
|
||||
|
||||
return delivery_note
|
||||
|
||||
|
||||
def create_dn_from_so(pick_list, sales_order_list, delivery_note=None):
|
||||
if not sales_order_list:
|
||||
return delivery_note
|
||||
|
||||
item_table_mapper = {
|
||||
"doctype": "Delivery Note Item",
|
||||
"field_map": {
|
||||
@@ -1224,20 +1300,17 @@ def create_dn_with_so(sales_dict, pick_list):
|
||||
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier != 1,
|
||||
}
|
||||
|
||||
for customer in sales_dict:
|
||||
for so in sales_dict[customer]:
|
||||
delivery_note = None
|
||||
kwargs = {"skip_item_mapping": True, "ignore_pricing_rule": pick_list.ignore_pricing_rule}
|
||||
delivery_note = create_delivery_note_from_sales_order(so, delivery_note, kwargs=kwargs)
|
||||
break
|
||||
if delivery_note:
|
||||
# map all items of all sales orders of that customer
|
||||
for so in sales_dict[customer]:
|
||||
map_pl_locations(pick_list, item_table_mapper, delivery_note, so)
|
||||
delivery_note.flags.ignore_mandatory = True
|
||||
delivery_note.insert()
|
||||
update_packed_item_details(pick_list, delivery_note)
|
||||
delivery_note.save()
|
||||
kwargs = {"skip_item_mapping": True, "ignore_pricing_rule": pick_list.ignore_pricing_rule}
|
||||
|
||||
delivery_note = create_delivery_note_from_sales_order(
|
||||
next(iter(sales_order_list)), delivery_note, kwargs=kwargs
|
||||
)
|
||||
|
||||
if not delivery_note:
|
||||
return
|
||||
|
||||
for so in sales_order_list:
|
||||
map_pl_locations(pick_list, item_table_mapper, delivery_note, so)
|
||||
|
||||
return delivery_note
|
||||
|
||||
@@ -1257,24 +1330,29 @@ def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None):
|
||||
dn_item = map_child_doc(source_doc, delivery_note, item_mapper)
|
||||
|
||||
if dn_item:
|
||||
dn_item.against_pick_list = pick_list.name
|
||||
dn_item.pick_list_item = location.name
|
||||
dn_item.warehouse = location.warehouse
|
||||
dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
|
||||
dn_item.qty = flt(location.picked_qty - location.delivered_qty) / (
|
||||
flt(dn_item.conversion_factor) or 1
|
||||
)
|
||||
dn_item.batch_no = location.batch_no
|
||||
dn_item.serial_no = location.serial_no
|
||||
dn_item.use_serial_batch_fields = location.use_serial_batch_fields
|
||||
|
||||
update_delivery_note_item(source_doc, dn_item, delivery_note)
|
||||
|
||||
add_product_bundles_to_delivery_note(pick_list, delivery_note, item_mapper)
|
||||
add_product_bundles_to_delivery_note(pick_list, delivery_note, item_mapper, sales_order)
|
||||
set_delivery_note_missing_values(delivery_note)
|
||||
|
||||
delivery_note.pick_list = pick_list.name
|
||||
delivery_note.company = pick_list.company
|
||||
delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer")
|
||||
if sales_order:
|
||||
delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer")
|
||||
|
||||
|
||||
def add_product_bundles_to_delivery_note(pick_list: "PickList", delivery_note, item_mapper) -> None:
|
||||
def add_product_bundles_to_delivery_note(
|
||||
pick_list: "PickList", delivery_note, item_mapper, sales_order=None
|
||||
) -> None:
|
||||
"""Add product bundles found in pick list to delivery note.
|
||||
|
||||
When mapping pick list items, the bundle item itself isn't part of the
|
||||
@@ -1284,38 +1362,17 @@ def add_product_bundles_to_delivery_note(pick_list: "PickList", delivery_note, i
|
||||
|
||||
for so_row, item_code in product_bundles.items():
|
||||
sales_order_item = frappe.get_doc("Sales Order Item", so_row)
|
||||
if sales_order and sales_order_item.parent != sales_order:
|
||||
continue
|
||||
|
||||
dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper)
|
||||
dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle(
|
||||
so_row, product_bundle_qty_map[item_code]
|
||||
)
|
||||
dn_bundle_item.against_pick_list = pick_list.name
|
||||
update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note)
|
||||
|
||||
|
||||
def update_packed_item_details(pick_list: "PickList", delivery_note) -> None:
|
||||
"""Update stock details on packed items table of delivery note."""
|
||||
|
||||
def _find_so_row(packed_item):
|
||||
for item in delivery_note.items:
|
||||
if packed_item.parent_detail_docname == item.name:
|
||||
return item.so_detail
|
||||
|
||||
def _find_pick_list_location(bundle_row, packed_item):
|
||||
if not bundle_row:
|
||||
return
|
||||
for loc in pick_list.locations:
|
||||
if loc.product_bundle_item == bundle_row and loc.item_code == packed_item.item_code:
|
||||
return loc
|
||||
|
||||
for packed_item in delivery_note.packed_items:
|
||||
so_row = _find_so_row(packed_item)
|
||||
location = _find_pick_list_location(so_row, packed_item)
|
||||
if not location:
|
||||
continue
|
||||
packed_item.warehouse = location.warehouse
|
||||
packed_item.batch_no = location.batch_no
|
||||
packed_item.serial_no = location.serial_no
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_stock_entry(pick_list):
|
||||
pick_list = frappe.get_doc(json.loads(pick_list))
|
||||
@@ -1362,14 +1419,6 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte
|
||||
).run(as_dict=as_dict)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def target_document_exists(pick_list_name, purpose):
|
||||
if purpose == "Delivery":
|
||||
return frappe.db.exists("Delivery Note", {"pick_list": pick_list_name, "docstatus": 1})
|
||||
|
||||
return stock_entry_exists(pick_list_name)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_item_details(item_code, uom=None):
|
||||
details = frappe.db.get_value("Item", item_code, ["stock_uom", "name"], as_dict=1)
|
||||
@@ -1490,3 +1539,50 @@ def get_rejected_warehouses():
|
||||
)
|
||||
|
||||
return frappe.local.rejected_warehouses
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_pick_list_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
frappe.has_permission("Pick List", throw=True)
|
||||
|
||||
if not filters.get("company"):
|
||||
frappe.throw(_("Please select a Company"))
|
||||
|
||||
PICK_LIST = frappe.qb.DocType("Pick List")
|
||||
PICK_LIST_ITEM = frappe.qb.DocType("Pick List Item")
|
||||
SALES_ORDER = frappe.qb.DocType("Sales Order")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(PICK_LIST)
|
||||
.join(PICK_LIST_ITEM)
|
||||
.on(PICK_LIST.name == PICK_LIST_ITEM.parent)
|
||||
.join(SALES_ORDER)
|
||||
.on(PICK_LIST_ITEM.sales_order == SALES_ORDER.name)
|
||||
.select(
|
||||
PICK_LIST.name,
|
||||
SALES_ORDER.customer,
|
||||
Replace(GROUP_CONCAT(PICK_LIST_ITEM.sales_order).distinct(), ",", "<br>").as_("sales_order"),
|
||||
)
|
||||
.where(PICK_LIST.docstatus == 1)
|
||||
.where(PICK_LIST.status.isin(["Open", "Partly Delivered"]))
|
||||
.where(PICK_LIST.company == filters.get("company"))
|
||||
.where(SALES_ORDER.customer == filters.get("customer"))
|
||||
.groupby(PICK_LIST.name)
|
||||
)
|
||||
|
||||
if filters.get("sales_order"):
|
||||
query = query.where(PICK_LIST_ITEM.sales_order == filters.get("sales_order"))
|
||||
|
||||
if txt:
|
||||
meta = frappe.get_meta("Pick List")
|
||||
search_fields = meta.get_search_fields()
|
||||
|
||||
txt = f"%{txt}%"
|
||||
txt_condition = PICK_LIST[search_fields[-1]].like(txt)
|
||||
|
||||
for field in search_fields[:-1]:
|
||||
txt_condition |= PICK_LIST[field].like(txt)
|
||||
|
||||
query = query.where(txt_condition)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
@@ -3,6 +3,7 @@ def get_data():
|
||||
"fieldname": "pick_list",
|
||||
"non_standard_fieldnames": {
|
||||
"Stock Reservation Entry": "from_voucher_no",
|
||||
"Delivery Note": "against_pick_list",
|
||||
},
|
||||
"internal_links": {
|
||||
"Sales Order": ["locations", "sales_order"],
|
||||
|
||||
@@ -6,6 +6,7 @@ frappe.listview_settings["Pick List"] = {
|
||||
const status_colors = {
|
||||
Draft: "red",
|
||||
Open: "orange",
|
||||
"Partly Delivered": "orange",
|
||||
Completed: "green",
|
||||
Cancelled: "red",
|
||||
};
|
||||
|
||||
@@ -5,11 +5,12 @@ import frappe
|
||||
from frappe import _dict
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
from erpnext.selling.doctype.sales_order.sales_order import create_pick_list
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import create_item, make_item
|
||||
from erpnext.stock.doctype.packed_item.test_packed_item import create_product_bundle
|
||||
from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note
|
||||
from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note, create_dn_for_pick_lists
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
|
||||
get_batch_from_bundle,
|
||||
@@ -398,7 +399,13 @@ class TestPickList(FrappeTestCase):
|
||||
self.assertEqual(pick_list.locations[1].sales_order_item, sales_order.items[0].name)
|
||||
|
||||
def test_pick_list_for_items_with_multiple_UOM(self):
|
||||
item_code = make_item().name
|
||||
item_code = make_item(
|
||||
uoms=[
|
||||
{"uom": "Nos", "conversion_factor": 1},
|
||||
{"uom": "Hand", "conversion_factor": 5},
|
||||
{"uom": "Unit", "conversion_factor": 0.5},
|
||||
]
|
||||
).name
|
||||
purchase_receipt = make_purchase_receipt(item_code=item_code, qty=10)
|
||||
purchase_receipt.submit()
|
||||
|
||||
@@ -411,8 +418,7 @@ class TestPickList(FrappeTestCase):
|
||||
{
|
||||
"item_code": item_code,
|
||||
"qty": 1,
|
||||
"conversion_factor": 5,
|
||||
"stock_qty": 5,
|
||||
"uom": "Hand",
|
||||
"delivery_date": frappe.utils.today(),
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
},
|
||||
@@ -426,6 +432,7 @@ class TestPickList(FrappeTestCase):
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
sales_order.submit()
|
||||
|
||||
pick_list = frappe.get_doc(
|
||||
@@ -440,6 +447,7 @@ class TestPickList(FrappeTestCase):
|
||||
"item_code": item_code,
|
||||
"qty": 2,
|
||||
"stock_qty": 1,
|
||||
"uom": "Unit",
|
||||
"conversion_factor": 0.5,
|
||||
"sales_order": sales_order.name,
|
||||
"sales_order_item": sales_order.items[0].name,
|
||||
@@ -461,7 +469,11 @@ class TestPickList(FrappeTestCase):
|
||||
delivery_note = create_delivery_note(pick_list.name)
|
||||
pick_list.load_from_db()
|
||||
|
||||
self.assertEqual(pick_list.locations[0].qty, delivery_note.items[0].qty)
|
||||
# pick list stk_qty / dn conversion_factor = dn qty (1/5 = 0.2)
|
||||
self.assertEqual(
|
||||
pick_list.locations[0].picked_qty,
|
||||
delivery_note.items[0].qty * delivery_note.items[0].conversion_factor,
|
||||
)
|
||||
self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty)
|
||||
self.assertEqual(sales_order.items[0].conversion_factor, delivery_note.items[0].conversion_factor)
|
||||
|
||||
@@ -554,10 +566,10 @@ class TestPickList(FrappeTestCase):
|
||||
"company": "_Test Company",
|
||||
"items_based_on": "Sales Order",
|
||||
"purpose": "Delivery",
|
||||
"picker": "P001",
|
||||
"customer": "_Test Customer",
|
||||
"locations": [
|
||||
{
|
||||
"item_code": "_Test Item ",
|
||||
"item_code": "_Test Item",
|
||||
"qty": 1,
|
||||
"stock_qty": 1,
|
||||
"conversion_factor": 1,
|
||||
@@ -580,32 +592,34 @@ class TestPickList(FrappeTestCase):
|
||||
create_delivery_note(pick_list.name)
|
||||
for dn in frappe.get_all(
|
||||
"Delivery Note",
|
||||
filters={"pick_list": pick_list.name, "customer": "_Test Customer"},
|
||||
filters={"against_pick_list": pick_list.name, "customer": "_Test Customer"},
|
||||
fields={"name"},
|
||||
):
|
||||
for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
|
||||
self.assertEqual(dn_item.item_code, "_Test Item")
|
||||
self.assertEqual(dn_item.against_sales_order, sales_order_1.name)
|
||||
self.assertEqual(dn_item.pick_list_item, pick_list.locations[dn_item.idx - 1].name)
|
||||
self.assertEqual(dn_item.against_pick_list, pick_list.name)
|
||||
self.assertEqual(dn_item.pick_list_item, pick_list.locations[0].name)
|
||||
|
||||
for dn in frappe.get_all(
|
||||
"Delivery Note",
|
||||
filters={"pick_list": pick_list.name, "customer": "_Test Customer 1"},
|
||||
filters={"against_pick_list": pick_list.name, "customer": "_Test Customer 1"},
|
||||
fields={"name"},
|
||||
):
|
||||
for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
|
||||
self.assertEqual(dn_item.item_code, "_Test Item 2")
|
||||
self.assertEqual(dn_item.against_sales_order, sales_order_2.name)
|
||||
self.assertEqual(dn_item.against_pick_list, pick_list.name)
|
||||
self.assertEqual(dn_item.pick_list_item, pick_list.locations[1].name)
|
||||
# test DN creation without so
|
||||
pick_list_1 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Pick List",
|
||||
"company": "_Test Company",
|
||||
"purpose": "Delivery",
|
||||
"picker": "P001",
|
||||
"locations": [
|
||||
{
|
||||
"item_code": "_Test Item ",
|
||||
"item_code": "_Test Item",
|
||||
"qty": 1,
|
||||
"stock_qty": 1,
|
||||
"conversion_factor": 1,
|
||||
@@ -622,7 +636,9 @@ class TestPickList(FrappeTestCase):
|
||||
pick_list_1.set_item_locations()
|
||||
pick_list_1.submit()
|
||||
create_delivery_note(pick_list_1.name)
|
||||
for dn in frappe.get_all("Delivery Note", filters={"pick_list": pick_list_1.name}, fields={"name"}):
|
||||
for dn in frappe.get_all(
|
||||
"Delivery Note", filters={"against_pick_list": pick_list_1.name}, fields={"name"}
|
||||
):
|
||||
for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
|
||||
if dn_item.item_code == "_Test Item":
|
||||
self.assertEqual(dn_item.qty, 1)
|
||||
@@ -759,7 +775,6 @@ class TestPickList(FrappeTestCase):
|
||||
quantities = [5, 2]
|
||||
bundle, components = create_product_bundle(quantities, warehouse=warehouse)
|
||||
bundle_items = dict(zip(components, quantities, strict=False))
|
||||
|
||||
so = make_sales_order(item_code=bundle, qty=3, rate=42)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
@@ -1307,3 +1322,166 @@ class TestPickList(FrappeTestCase):
|
||||
|
||||
for loc in pl.locations:
|
||||
self.assertEqual(loc.batch_no, batch2)
|
||||
|
||||
def test_multiple_pick_lists_delivery_note(self):
|
||||
from erpnext.stock.doctype.pick_list.pick_list import create_dn_for_pick_lists
|
||||
|
||||
item_code = make_item().name
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
stock_entry = make_stock_entry(item=item_code, to_warehouse=warehouse, qty=500, basic_rate=100)
|
||||
|
||||
def create_pick_list(qty):
|
||||
pick_list = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Pick List",
|
||||
"company": "_Test Company",
|
||||
"customer": "_Test Customer",
|
||||
"purpose": "Delivery",
|
||||
"locations": [
|
||||
{
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"qty": qty,
|
||||
"stock_qty": qty,
|
||||
"picked_qty": 0,
|
||||
"sales_order": sales_order.name,
|
||||
"sales_order_item": sales_order.items[0].name,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
pick_list.submit()
|
||||
return pick_list
|
||||
|
||||
sales_order = make_sales_order(item_code=item_code, qty=50, rate=100)
|
||||
pick_list_1 = create_pick_list(10)
|
||||
pick_list_2 = create_pick_list(20)
|
||||
|
||||
delivery_note = create_dn_for_pick_lists(pick_list_1.name)
|
||||
delivery_note = create_dn_for_pick_lists(pick_list_2.name, delivery_note)
|
||||
delivery_note.items[0].qty = 5
|
||||
delivery_note.submit()
|
||||
|
||||
sales_order.reload()
|
||||
pick_list_1.reload()
|
||||
pick_list_2.reload()
|
||||
|
||||
self.assertEqual(sales_order.items[0].picked_qty, 30)
|
||||
self.assertEqual(pick_list_1.locations[0].delivered_qty, delivery_note.items[0].qty)
|
||||
self.assertEqual(pick_list_1.status, "Partly Delivered")
|
||||
self.assertEqual(pick_list_2.status, "Completed")
|
||||
|
||||
pick_list_1.cancel()
|
||||
pick_list_2.cancel()
|
||||
delivery_note.cancel()
|
||||
sales_order.reload()
|
||||
sales_order.cancel()
|
||||
stock_entry.cancel()
|
||||
|
||||
def test_packed_item_in_pick_list(self):
|
||||
warehouse_1 = "RJ Warehouse - _TC"
|
||||
warehouse_2 = "_Test Warehouse 2 - _TC"
|
||||
item_1 = make_item(properties={"is_stock_item": 0}).name
|
||||
item_2 = make_item().name
|
||||
item_3 = make_item().name
|
||||
|
||||
make_product_bundle(item_1, items=[item_2, item_3])
|
||||
|
||||
stock_entry_1 = make_stock_entry(item=item_2, to_warehouse=warehouse_1, qty=10, basic_rate=100)
|
||||
stock_entry_2 = make_stock_entry(item=item_3, to_warehouse=warehouse_1, qty=4, basic_rate=100)
|
||||
stock_entry_3 = make_stock_entry(item=item_3, to_warehouse=warehouse_2, qty=6, basic_rate=100)
|
||||
|
||||
sales_order = make_sales_order(item_code=item_1, qty=10, rate=100)
|
||||
|
||||
pick_list = create_pick_list(sales_order.name)
|
||||
pick_list.submit()
|
||||
self.assertEqual(len(pick_list.locations), 3)
|
||||
delivery_note = create_delivery_note(pick_list.name)
|
||||
|
||||
self.assertEqual(delivery_note.items[0].qty, 10)
|
||||
self.assertEqual(delivery_note.packed_items[0].warehouse, warehouse_1)
|
||||
self.assertEqual(delivery_note.packed_items[1].warehouse, warehouse_2)
|
||||
|
||||
pick_list.cancel()
|
||||
sales_order.cancel()
|
||||
stock_entry_1.cancel()
|
||||
stock_entry_2.cancel()
|
||||
stock_entry_3.cancel()
|
||||
|
||||
def test_packed_item_multiple_times_in_so(self):
|
||||
frappe.db.delete("Item Price")
|
||||
warehouse_1 = "RJ Warehouse - _TC"
|
||||
warehouse_2 = "_Test Warehouse 2 - _TC"
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item_1 = make_item(properties={"is_stock_item": 0}).name
|
||||
item_2 = make_item().name
|
||||
item_3 = make_item().name
|
||||
|
||||
make_product_bundle(item_1, items=[item_2, item_3])
|
||||
|
||||
stock_entry_1 = make_stock_entry(item=item_2, to_warehouse=warehouse_1, qty=20, basic_rate=100)
|
||||
stock_entry_2 = make_stock_entry(item=item_3, to_warehouse=warehouse_1, qty=8, basic_rate=100)
|
||||
stock_entry_3 = make_stock_entry(item=item_3, to_warehouse=warehouse_2, qty=12, basic_rate=100)
|
||||
|
||||
sales_order = make_sales_order(
|
||||
item_list=[
|
||||
{"item_code": item_1, "qty": 8, "rate": 100, "warehouse": warehouse},
|
||||
{"item_code": item_1, "qty": 12, "rate": 100, "warehouse": warehouse},
|
||||
]
|
||||
)
|
||||
|
||||
pick_list = create_pick_list(sales_order.name)
|
||||
pick_list.submit()
|
||||
self.assertEqual(len(pick_list.locations), 4)
|
||||
delivery_note = create_delivery_note(pick_list.name)
|
||||
|
||||
self.assertEqual(delivery_note.items[0].qty, 8)
|
||||
self.assertEqual(delivery_note.items[1].qty, 12)
|
||||
|
||||
self.assertEqual(delivery_note.packed_items[0].qty, 8)
|
||||
self.assertEqual(delivery_note.packed_items[2].qty, 12)
|
||||
|
||||
self.assertEqual(delivery_note.packed_items[0].warehouse, warehouse_1)
|
||||
self.assertEqual(delivery_note.packed_items[1].warehouse, warehouse_1)
|
||||
self.assertEqual(delivery_note.packed_items[2].warehouse, warehouse_1)
|
||||
self.assertEqual(delivery_note.packed_items[3].warehouse, warehouse_2)
|
||||
|
||||
pick_list.cancel()
|
||||
sales_order.cancel()
|
||||
stock_entry_1.cancel()
|
||||
stock_entry_2.cancel()
|
||||
stock_entry_3.cancel()
|
||||
|
||||
def test_pick_list_with_and_without_so(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item().name
|
||||
|
||||
sales_order = make_sales_order(item_code=item, qty=20, rate=100)
|
||||
stock_entry = make_stock_entry(item=item, to_warehouse=warehouse, qty=500, basic_rate=100)
|
||||
|
||||
pick_list = create_pick_list(sales_order.name)
|
||||
pick_list.append(
|
||||
"locations",
|
||||
{
|
||||
"item_code": item,
|
||||
"qty": 10,
|
||||
"stock_qty": 10,
|
||||
"warehouse": warehouse,
|
||||
"picked_qty": 0,
|
||||
},
|
||||
)
|
||||
pick_list.submit()
|
||||
|
||||
delivery_note = create_dn_for_pick_lists(pick_list.name)
|
||||
|
||||
self.assertEqual(delivery_note.items[0].against_pick_list, pick_list.name)
|
||||
self.assertEqual(delivery_note.items[0].against_sales_order, sales_order.name)
|
||||
self.assertEqual(delivery_note.items[0].qty, 20)
|
||||
|
||||
self.assertEqual(delivery_note.items[1].against_pick_list, pick_list.name)
|
||||
self.assertEqual(delivery_note.items[1].qty, 10)
|
||||
|
||||
pick_list.cancel()
|
||||
sales_order.cancel()
|
||||
stock_entry.cancel()
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"uom",
|
||||
"conversion_factor",
|
||||
"stock_uom",
|
||||
"delivered_qty",
|
||||
"serial_no_and_batch_section",
|
||||
"pick_serial_and_batch",
|
||||
"serial_and_batch_bundle",
|
||||
@@ -237,17 +238,28 @@
|
||||
{
|
||||
"fieldname": "column_break_belw",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "delivered_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Delivered Qty (in Stock UOM)",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"report_hide": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-05-07 15:32:42.905446",
|
||||
"modified": "2025-05-31 19:57:43.531298",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Pick List Item",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
|
||||
@@ -17,6 +17,7 @@ class PickListItem(Document):
|
||||
|
||||
batch_no: DF.Link | None
|
||||
conversion_factor: DF.Float
|
||||
delivered_qty: DF.Float
|
||||
description: DF.Text | None
|
||||
item_code: DF.Link
|
||||
item_group: DF.Data | None
|
||||
|
||||
@@ -27,6 +27,7 @@ from erpnext.buying.utils import check_on_hold_or_closed_status
|
||||
from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
|
||||
from erpnext.manufacturing.doctype.bom.bom import (
|
||||
add_additional_cost,
|
||||
get_bom_items_as_dict,
|
||||
get_op_cost_from_sub_assemblies,
|
||||
get_scrap_items_from_sub_assemblies,
|
||||
validate_bom_no,
|
||||
@@ -243,6 +244,7 @@ class StockEntry(StockController):
|
||||
def on_submit(self):
|
||||
self.validate_closed_subcontracting_order()
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
self.update_disassembled_order()
|
||||
self.update_stock_ledger()
|
||||
self.update_work_order()
|
||||
self.validate_subcontract_order()
|
||||
@@ -263,6 +265,7 @@ class StockEntry(StockController):
|
||||
self.set_material_request_transfer_status("Completed")
|
||||
|
||||
def on_cancel(self):
|
||||
self.delink_asset_repair_sabb()
|
||||
self.validate_closed_subcontracting_order()
|
||||
self.update_subcontract_order_supplied_items()
|
||||
self.update_subcontracting_order_status()
|
||||
@@ -271,6 +274,7 @@ class StockEntry(StockController):
|
||||
self.validate_work_order_status()
|
||||
|
||||
self.update_work_order()
|
||||
self.update_disassembled_order(is_cancel=True)
|
||||
self.update_stock_ledger()
|
||||
|
||||
self.ignore_linked_doctypes = (
|
||||
@@ -364,6 +368,27 @@ class StockEntry(StockController):
|
||||
):
|
||||
frappe.delete_doc("Stock Entry", d.name)
|
||||
|
||||
def delink_asset_repair_sabb(self):
|
||||
if not self.asset_repair:
|
||||
return
|
||||
|
||||
for row in self.items:
|
||||
if row.serial_and_batch_bundle:
|
||||
voucher_detail_no = frappe.db.get_value(
|
||||
"Asset Repair Consumed Item",
|
||||
{"parent": self.asset_repair, "serial_and_batch_bundle": row.serial_and_batch_bundle},
|
||||
"name",
|
||||
)
|
||||
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
|
||||
doc.db_set(
|
||||
{
|
||||
"voucher_type": "Asset Repair",
|
||||
"voucher_no": self.asset_repair,
|
||||
"voucher_detail_no": voucher_detail_no,
|
||||
}
|
||||
)
|
||||
|
||||
def set_transfer_qty(self):
|
||||
self.validate_qty_is_not_zero()
|
||||
for item in self.get("items"):
|
||||
@@ -1617,6 +1642,13 @@ class StockEntry(StockController):
|
||||
if not pro_doc.operations:
|
||||
pro_doc.set_actual_dates()
|
||||
|
||||
def update_disassembled_order(self, is_cancel=False):
|
||||
if not self.work_order:
|
||||
return
|
||||
if self.purpose == "Disassemble" and self.fg_completed_qty:
|
||||
pro_doc = frappe.get_doc("Work Order", self.work_order)
|
||||
pro_doc.run_method("update_disassembled_qty", self.fg_completed_qty, is_cancel)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_item_details(self, args=None, for_update=False):
|
||||
item = frappe.qb.DocType("Item")
|
||||
@@ -1759,7 +1791,7 @@ class StockEntry(StockController):
|
||||
},
|
||||
)
|
||||
|
||||
def get_items_for_disassembly(self):
|
||||
def get_items_for_disassembly(self, disassemble_qty, production_item):
|
||||
"""Get items for Disassembly Order"""
|
||||
|
||||
if not self.work_order:
|
||||
@@ -1767,9 +1799,9 @@ class StockEntry(StockController):
|
||||
|
||||
items = self.get_items_from_manufacture_entry()
|
||||
|
||||
s_warehouse = ""
|
||||
if self.work_order:
|
||||
s_warehouse = frappe.db.get_value("Work Order", self.work_order, "fg_warehouse")
|
||||
s_warehouse = frappe.db.get_value("Work Order", self.work_order, "fg_warehouse")
|
||||
|
||||
items_dict = get_bom_items_as_dict(self.bom_no, self.company, disassemble_qty)
|
||||
|
||||
for row in items:
|
||||
child_row = self.append("items", {})
|
||||
@@ -1777,6 +1809,15 @@ class StockEntry(StockController):
|
||||
if value is not None:
|
||||
child_row.set(field, value)
|
||||
|
||||
# update qty and amount from BOM items
|
||||
bom_items = items_dict.get(row.item_code)
|
||||
if bom_items:
|
||||
child_row.qty = bom_items.get("qty", child_row.qty)
|
||||
child_row.amount = bom_items.get("amount", child_row.amount)
|
||||
|
||||
if row.item_code == production_item:
|
||||
child_row.qty = disassemble_qty
|
||||
|
||||
child_row.s_warehouse = (self.from_warehouse or s_warehouse) if row.is_finished_item else ""
|
||||
child_row.t_warehouse = self.to_warehouse if not row.is_finished_item else ""
|
||||
child_row.is_finished_item = 0 if row.is_finished_item else 1
|
||||
@@ -1809,12 +1850,12 @@ class StockEntry(StockController):
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_items(self):
|
||||
def get_items(self, qty=None, production_item=None):
|
||||
self.set("items", [])
|
||||
self.validate_work_order()
|
||||
|
||||
if self.purpose == "Disassemble":
|
||||
return self.get_items_for_disassembly()
|
||||
if self.purpose == "Disassemble" and qty is not None:
|
||||
return self.get_items_for_disassembly(qty, production_item)
|
||||
|
||||
if not self.posting_date or not self.posting_time:
|
||||
frappe.throw(_("Posting date and posting time is mandatory"))
|
||||
|
||||
@@ -174,7 +174,7 @@
|
||||
{
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"label": "State",
|
||||
"label": "State/Province",
|
||||
"oldfieldname": "state",
|
||||
"oldfieldtype": "Select"
|
||||
},
|
||||
@@ -259,11 +259,12 @@
|
||||
"label": "Is Rejected Warehouse"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-building",
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2024-08-14 16:08:15.733597",
|
||||
"modified": "2025-06-26 11:19:04.673115",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Warehouse",
|
||||
@@ -316,6 +317,7 @@
|
||||
"role": "Manufacturing User"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
|
||||
@@ -257,7 +257,7 @@ class SerialBatchBundle:
|
||||
frappe.throw(_(msg))
|
||||
|
||||
def delink_serial_and_batch_bundle(self):
|
||||
if self.is_pos_transaction():
|
||||
if self.is_pos_or_asset_repair_transaction():
|
||||
return
|
||||
|
||||
update_values = {
|
||||
@@ -306,21 +306,29 @@ class SerialBatchBundle:
|
||||
self.cancel_serial_and_batch_bundle()
|
||||
|
||||
def cancel_serial_and_batch_bundle(self):
|
||||
if self.is_pos_transaction():
|
||||
if self.is_pos_or_asset_repair_transaction():
|
||||
return
|
||||
|
||||
doc = frappe.get_cached_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle)
|
||||
if doc.docstatus == 1:
|
||||
doc.cancel()
|
||||
|
||||
def is_pos_transaction(self):
|
||||
def is_pos_or_asset_repair_transaction(self):
|
||||
voucher_type = frappe.get_cached_value(
|
||||
"Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "voucher_type"
|
||||
)
|
||||
|
||||
if (
|
||||
self.sle.voucher_type == "Sales Invoice"
|
||||
and self.sle.serial_and_batch_bundle
|
||||
and frappe.get_cached_value(
|
||||
"Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "voucher_type"
|
||||
)
|
||||
== "POS Invoice"
|
||||
and voucher_type == "POS Invoice"
|
||||
):
|
||||
return True
|
||||
|
||||
if (
|
||||
self.sle.voucher_type == "Stock Entry"
|
||||
and self.sle.serial_and_batch_bundle
|
||||
and voucher_type == "Asset Repair"
|
||||
):
|
||||
return True
|
||||
|
||||
|
||||
@@ -1254,12 +1254,19 @@ class update_entries_after:
|
||||
|
||||
def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
|
||||
if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
|
||||
if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
|
||||
sle.voucher_type, sle.voucher_no, "is_internal_supplier"
|
||||
):
|
||||
frappe.db.set_value(
|
||||
f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
|
||||
if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
details = frappe.get_cached_value(
|
||||
sle.voucher_type,
|
||||
sle.voucher_no,
|
||||
["is_internal_supplier", "is_return", "return_against"],
|
||||
as_dict=True,
|
||||
)
|
||||
if details.is_internal_supplier or (details.is_return and not details.return_against):
|
||||
rate = outgoing_rate if details.is_return else sle.outgoing_rate
|
||||
|
||||
frappe.db.set_value(
|
||||
f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", rate
|
||||
)
|
||||
else:
|
||||
frappe.db.set_value(
|
||||
"Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
|
||||
|
||||
@@ -1346,8 +1346,8 @@ Level,Ebene,
|
||||
Liability,Verbindlichkeit,
|
||||
Limit Crossed,Grenze überschritten,
|
||||
Link to Material Request,Verknüpfung zur Materialanforderung,
|
||||
List of all share transactions,Liste aller Aktientransaktionen,
|
||||
List of available Shareholders with folio numbers,Liste der verfügbaren Aktionäre mit Folio-Nummern,
|
||||
List of all share transactions,Liste aller Anteilstransaktionen,
|
||||
List of available Shareholders with folio numbers,Liste der verfügbaren Anteilseigner mit Folio-Nummern,
|
||||
Loading Payment System,Zahlungssystem wird geladen,
|
||||
Loan,Darlehen,
|
||||
Loan Start Date and Loan Period are mandatory to save the Invoice Discounting,"Das Ausleihbeginndatum und die Ausleihdauer sind obligatorisch, um die Rechnungsdiskontierung zu speichern",
|
||||
@@ -1564,7 +1564,7 @@ No items listed,Keine Artikel aufgeführt,
|
||||
No items to be received are overdue,Keine zu übergebenden Artikel sind überfällig,
|
||||
No material request created,Es wurde keine Materialanforderung erstellt,
|
||||
No of Interactions,Anzahl der Interaktionen,
|
||||
No of Shares,Anzahl der Aktien,
|
||||
No of Shares,Anzahl der Anteile,
|
||||
No pending Material Requests found to link for the given items.,"Es wurden keine ausstehenden Materialanforderungen gefunden, die für die angegebenen Artikel verknüpft sind.",
|
||||
No products found,Keine Produkte gefunden,
|
||||
No products found.,Keine Produkte gefunden,
|
||||
@@ -2103,7 +2103,7 @@ Rate,Preis,
|
||||
Rate:,Bewertung:,
|
||||
Rating,Wertung,
|
||||
Raw Material,Rohmaterial,
|
||||
Raw Materials,Rohes Material,
|
||||
Raw Materials,Rohmaterial,
|
||||
Raw Materials cannot be blank.,Rohmaterial kann nicht leer sein,
|
||||
Re-open,Wiedereröffnen,
|
||||
Read blog,Blog lesen,
|
||||
@@ -2488,11 +2488,11 @@ Setup default values for POS Invoices,Standardwerte für POS-Rechnungen einricht
|
||||
Setup mode of POS (Online / Offline),Einrichtungsmodus des POS (Online / Offline),
|
||||
Setup your Institute in ERPNext,Richten Sie Ihr Institut in ERPNext ein,
|
||||
Share Balance,Anteilsbestand,
|
||||
Share Ledger,Aktienbuch,
|
||||
Share Management,Aktienverwaltung,
|
||||
Share Transfer,Weitergabe übertragen,
|
||||
Share Type,Art der Freigabe,
|
||||
Shareholder,Aktionär,
|
||||
Share Ledger,Verzeichnis der Anteilseigner,
|
||||
Share Management,Anteilsverwaltung,
|
||||
Share Transfer,Anteilsübertragung,
|
||||
Share Type,Art des Anteils,
|
||||
Shareholder,Anteilseigner,
|
||||
Ship To State,Versende nach Land,
|
||||
Shipments,Lieferungen,
|
||||
Shipping Address,Lieferadresse,
|
||||
@@ -2724,24 +2724,24 @@ The Term End Date cannot be later than the Year End Date of the Academic Year to
|
||||
The Term Start Date cannot be earlier than the Year Start Date of the Academic Year to which the term is linked (Academic Year {}). Please correct the dates and try again.,Der Begriff Startdatum kann nicht früher als das Jahr Anfang des Akademischen Jahres an dem der Begriff verknüpft ist (Akademisches Jahr {}). Bitte korrigieren Sie die Daten und versuchen Sie es erneut.,
|
||||
The Year End Date cannot be earlier than the Year Start Date. Please correct the dates and try again.,Das Jahr Enddatum kann nicht früher als das Jahr Startdatum. Bitte korrigieren Sie die Daten und versuchen Sie es erneut.,
|
||||
The amount of {0} set in this payment request is different from the calculated amount of all payment plans: {1}. Make sure this is correct before submitting the document.,"Der in dieser Zahlungsanforderung festgelegte Betrag von {0} unterscheidet sich von dem berechneten Betrag aller Zahlungspläne: {1}. Stellen Sie sicher, dass dies korrekt ist, bevor Sie das Dokument einreichen.",
|
||||
The field From Shareholder cannot be blank,Das Feld Von Aktionär darf nicht leer sein,
|
||||
The field To Shareholder cannot be blank,Das Feld An Aktionär darf nicht leer sein,
|
||||
The fields From Shareholder and To Shareholder cannot be blank,Die Felder Von Aktionär und An Anteilinhaber dürfen nicht leer sein,
|
||||
The field From Shareholder cannot be blank,Das Feld Von Anteilseigner darf nicht leer sein,
|
||||
The field To Shareholder cannot be blank,Das Feld An Anteilseigner darf nicht leer sein,
|
||||
The fields From Shareholder and To Shareholder cannot be blank,Die Felder Von Anteilseigner und An Anteilseigner dürfen nicht leer sein,
|
||||
The folio numbers are not matching,Die Folionummern stimmen nicht überein,
|
||||
The holiday on {0} is not between From Date and To Date,Der Urlaub am {0} ist nicht zwischen dem Von-Datum und dem Bis-Datum,
|
||||
The name of the institute for which you are setting up this system.,"Der Name des Instituts, für die Sie setzen dieses System.",
|
||||
The name of your company for which you are setting up this system.,"Firma des Unternehmens, für das dieses System eingerichtet wird.",
|
||||
The number of shares and the share numbers are inconsistent,Die Anzahl der Aktien und die Aktienanzahl sind inkonsistent,
|
||||
The number of shares and the share numbers are inconsistent,Die Anzahl der Anteile und die Anteilsanzahl sind inkonsistent,
|
||||
The payment gateway account in plan {0} is different from the payment gateway account in this payment request,Das Zahlungsgatewaykonto in Plan {0} unterscheidet sich von dem Zahlungsgatewaykonto in dieser Zahlungsanforderung,
|
||||
The selected BOMs are not for the same item,Die ausgewählten Stücklisten sind nicht für den gleichen Artikel,
|
||||
The selected item cannot have Batch,Der ausgewählte Artikel kann keine Charge haben,
|
||||
The seller and the buyer cannot be the same,Der Verkäufer und der Käufer können nicht identisch sein,
|
||||
The shareholder does not belong to this company,Der Aktionär gehört nicht zu diesem Unternehmen,
|
||||
The shares already exist,Die Aktien sind bereits vorhanden,
|
||||
The shares don't exist with the {0},Die Freigaben existieren nicht mit der {0},
|
||||
The shareholder does not belong to this company,Der Anteilseigner gehört nicht zu diesem Unternehmen,
|
||||
The shares already exist,Die Anteile sind bereits vorhanden,
|
||||
The shares don't exist with the {0},Die Anteile existieren nicht mit der {0},
|
||||
"The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage","Die Aufgabe wurde als Hintergrundjob in die Warteschlange gestellt. Falls bei der Verarbeitung im Hintergrund Probleme auftreten, fügt das System einen Kommentar zum Fehler in dieser Bestandsabstimmung hinzu und kehrt zum Entwurfsstadium zurück",
|
||||
"Then Pricing Rules are filtered out based on Customer, Customer Group, Territory, Supplier, Supplier Type, Campaign, Sales Partner etc.","Dann werden Preisregeln bezogen auf Kunde, Kundengruppe, Region, Lieferant, Lieferantentyp, Kampagne, Vertriebspartner usw. ausgefiltert",
|
||||
"There are inconsistencies between the rate, no of shares and the amount calculated","Es gibt Unstimmigkeiten zwischen dem Kurs, der Anzahl der Aktien und dem berechneten Betrag",
|
||||
"There are inconsistencies between the rate, no of shares and the amount calculated","Es gibt Unstimmigkeiten zwischen dem Kurs, der Anzahl der Anteile und dem berechneten Betrag",
|
||||
There can be multiple tiered collection factor based on the total spent. But the conversion factor for redemption will always be same for all the tier.,Abhängig von der Gesamtausgabenanzahl kann es einen mehrstufigen Sammelfaktor geben. Der Umrechnungsfaktor für die Einlösung wird jedoch für alle Stufen immer gleich sein.,
|
||||
There can only be 1 Account per Company in {0} {1},Es kann nur EIN Konto pro Unternehmen in {0} {1} geben,
|
||||
"There can only be one Shipping Rule Condition with 0 or blank value for ""To Value""","Es kann nur eine Versandbedingung mit dem Wert ""0"" oder ""leer"" für ""Bis-Wert"" geben",
|
||||
@@ -3648,7 +3648,7 @@ Reset,Zurücksetzen,
|
||||
Reset Service Level Agreement,Service Level Agreement zurücksetzen,
|
||||
Resetting Service Level Agreement.,Service Level Agreement zurücksetzen.,
|
||||
Return amount cannot be greater unclaimed amount,Der Rückgabebetrag kann nicht höher sein als der nicht beanspruchte Betrag,
|
||||
Review,Rezension,
|
||||
Review,Review,
|
||||
Room,Zimmer,
|
||||
Room Type,Zimmertyp,
|
||||
Row # ,Zeile #,
|
||||
@@ -4808,9 +4808,9 @@ To No,Zu Nein,
|
||||
Is Company,Ist Unternehmen,
|
||||
Current State,Aktuellen Zustand,
|
||||
Purchased,Gekauft,
|
||||
From Shareholder,Vom Aktionär,
|
||||
From Shareholder,Vom Anteilseigner,
|
||||
From Folio No,Aus Folio Nr,
|
||||
To Shareholder,An den Aktionär,
|
||||
To Shareholder,An Anteilseigner,
|
||||
To Folio No,Zu Folio Nein,
|
||||
Equity/Liability Account,Eigenkapital / Verbindlichkeitskonto,
|
||||
Asset Account,Anlagenkonto,
|
||||
@@ -4819,7 +4819,7 @@ ACC-SH-.YYYY.-,ACC-SH-.JJJJ.-,
|
||||
Folio no.,Folio Nr.,
|
||||
Address and Contacts,Adresse und Kontaktinformationen,
|
||||
Contact List,Kontaktliste,
|
||||
Hidden list maintaining the list of contacts linked to Shareholder,"Versteckte Liste, die die Liste der mit dem Aktionär verknüpften Kontakte enthält",
|
||||
Hidden list maintaining the list of contacts linked to Shareholder,"Versteckte Liste, die die Liste der mit dem Anteilseigner verknüpften Kontakte enthält",
|
||||
Specify conditions to calculate shipping amount,Bedingungen zur Berechnung der Versandkosten angeben,
|
||||
Shipping Rule Label,Bezeichnung der Versandregel,
|
||||
example: Next Day Shipping,Beispiel: Versand am nächsten Tag,
|
||||
@@ -4853,7 +4853,7 @@ Discounts,Rabatte,
|
||||
Additional DIscount Percentage,Zusätzlicher prozentualer Rabatt,
|
||||
Additional DIscount Amount,Zusätzlicher Rabatt,
|
||||
Subscription Invoice,Abonnementrechnung,
|
||||
Subscription Plan,Abonnement,
|
||||
Subscription Plan,Abonnementplan,
|
||||
Cost,Kosten,
|
||||
Billing Interval,Abrechnungsintervall,
|
||||
Billing Interval Count,Abrechnungsintervall Anzahl,
|
||||
@@ -6257,7 +6257,7 @@ Blanket Order Item,Rahmenauftragsposition,
|
||||
Ordered Quantity,Bestellte Menge,
|
||||
Item to be manufactured or repacked,Zu fertigender oder umzupackender Artikel,
|
||||
Quantity of item obtained after manufacturing / repacking from given quantities of raw materials,Menge eines Artikels nach der Herstellung/dem Umpacken auf Basis vorgegebener Mengen von Rohmaterial,
|
||||
Set rate of sub-assembly item based on BOM,Setzen Sie die Menge der Unterbaugruppe auf der Grundlage der Stückliste,
|
||||
Set rate of sub-assembly item based on BOM,Einzelpreis für Artikel der Unterbaugruppe auf Basis deren Stückliste festlegen,
|
||||
Allow Alternative Item,Alternative Artikel zulassen,
|
||||
Item UOM,Artikelmaßeinheit,
|
||||
Conversion Rate,Wechselkurs,
|
||||
|
||||
|
Can't render this file because it is too large.
|
Reference in New Issue
Block a user