Merge pull request #48338 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2025-07-01 17:30:39 +05:30
committed by GitHub
55 changed files with 1222 additions and 403 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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")

View File

@@ -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",

View File

@@ -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) => {

View File

@@ -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": []
}
}

View File

@@ -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)),
)

View File

@@ -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(

View File

@@ -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),
});
}
});
}
});
},
});

View File

@@ -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
}
}

View File

@@ -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()

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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):

View File

@@ -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)

View File

@@ -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"],
],
}

View File

@@ -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")))

View File

@@ -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",

View File

@@ -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",

View File

@@ -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,

View File

@@ -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",

View File

@@ -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

View File

@@ -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) {

View File

@@ -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
}
}

View File

@@ -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()

View File

@@ -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)

View File

@@ -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

View 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()

View File

@@ -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) {

View File

@@ -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;

View File

@@ -81,7 +81,7 @@ frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm
fieldtype: "Data",
},
{
label: __("State"),
label: __("State/Province"),
fieldname: "state",
fieldtype: "Data",
},

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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,
)

View File

@@ -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]}">

View File

@@ -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;

View File

@@ -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(

View File

@@ -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",

View File

@@ -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(

View File

@@ -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",

View File

@@ -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

View File

@@ -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:

View File

@@ -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) {

View File

@@ -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": [],

View File

@@ -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)

View File

@@ -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"],

View File

@@ -6,6 +6,7 @@ frappe.listview_settings["Pick List"] = {
const status_colors = {
Draft: "red",
Open: "orange",
"Partly Delivered": "orange",
Completed: "green",
Cancelled: "red",
};

View File

@@ -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()

View File

@@ -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": [],

View File

@@ -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

View File

@@ -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"))

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -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.