mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-08 15:42:52 +00:00
@@ -370,6 +370,8 @@ class SalesInvoice(SellingController):
|
||||
if row.billing_amount:
|
||||
row.billing_amount = -abs(row.billing_amount)
|
||||
|
||||
self.validate_update_stock_for_pick_list_reference()
|
||||
self.set_serial_and_batch_bundle_from_pick_list()
|
||||
self.update_packing_list()
|
||||
self.set_billing_hours_and_amount()
|
||||
self.update_timesheet_billing_for_project()
|
||||
@@ -389,6 +391,18 @@ class SalesInvoice(SellingController):
|
||||
self.validate_subcontracted_sales_order()
|
||||
self.validate_scio_self_rm_qty()
|
||||
|
||||
def validate_update_stock_for_pick_list_reference(self):
|
||||
if self.update_stock or self.is_return:
|
||||
return
|
||||
|
||||
for row in self.items:
|
||||
if row.get("against_pick_list"):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Update Stock must be checked for item {1} because it is against Pick List {2}."
|
||||
).format(row.idx, frappe.bold(row.item_code), frappe.bold(row.against_pick_list))
|
||||
)
|
||||
|
||||
def validate_accounts(self):
|
||||
self.validate_write_off_account()
|
||||
self.validate_account_for_change_amount()
|
||||
@@ -491,6 +505,7 @@ class SalesInvoice(SellingController):
|
||||
|
||||
if self.update_stock == 1:
|
||||
self.repost_future_sle_and_gle()
|
||||
self.update_pick_list_status()
|
||||
|
||||
if not self.is_return:
|
||||
self.update_billing_status_for_zero_amount_refdoc("Delivery Note")
|
||||
@@ -614,6 +629,7 @@ class SalesInvoice(SellingController):
|
||||
if self.update_stock == 1:
|
||||
self.update_stock_reservation_entries()
|
||||
self.repost_future_sle_and_gle()
|
||||
self.update_pick_list_status()
|
||||
|
||||
self.db_set("status", "Cancelled")
|
||||
|
||||
@@ -665,7 +681,8 @@ class SalesInvoice(SellingController):
|
||||
if not cint(self.update_stock):
|
||||
return
|
||||
|
||||
self.status_updater.append(
|
||||
self.status_updater.extend(
|
||||
[
|
||||
{
|
||||
"source_dt": "Sales Invoice Item",
|
||||
"target_dt": "Sales Order Item",
|
||||
@@ -684,7 +701,21 @@ class SalesInvoice(SellingController):
|
||||
"overflow_type": "delivery",
|
||||
"extra_cond": """ and exists(select name from `tabSales Invoice`
|
||||
where name=`tabSales Invoice Item`.parent and update_stock = 1)""",
|
||||
}
|
||||
},
|
||||
{
|
||||
"source_dt": "Sales Invoice 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 not cint(self.is_return):
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
"sales_order",
|
||||
"so_detail",
|
||||
"sales_invoice_item",
|
||||
"pick_list_item",
|
||||
"column_break_74",
|
||||
"delivery_note",
|
||||
"dn_detail",
|
||||
@@ -112,6 +113,7 @@
|
||||
"pos_invoice",
|
||||
"pos_invoice_item",
|
||||
"scio_detail",
|
||||
"against_pick_list",
|
||||
"internal_transfer_section",
|
||||
"purchase_order",
|
||||
"column_break_92",
|
||||
@@ -855,8 +857,8 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rate of Stock UOM",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"options": "currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -1011,13 +1013,30 @@
|
||||
"label": "Consider for Tax Withholding",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "against_pick_list",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Against Pick List",
|
||||
"no_copy": 1,
|
||||
"options": "Pick List",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "pick_list_item",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Pick List Item",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-05-29 12:23:28.259905",
|
||||
"modified": "2026-06-03 13:17:36.145788",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
@@ -22,6 +22,7 @@ class SalesInvoiceItem(Document):
|
||||
|
||||
actual_batch_qty: DF.Float
|
||||
actual_qty: DF.Float
|
||||
against_pick_list: DF.Link | None
|
||||
allow_zero_valuation_rate: DF.Check
|
||||
amount: DF.Currency
|
||||
apply_tds: DF.Check
|
||||
@@ -72,6 +73,7 @@ class SalesInvoiceItem(Document):
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
pick_list_item: DF.Data | None
|
||||
pos_invoice: DF.Link | None
|
||||
pos_invoice_item: DF.Data | None
|
||||
price_list_rate: DF.Currency
|
||||
|
||||
@@ -1060,6 +1060,44 @@ class SellingController(StockController):
|
||||
|
||||
qty_to_undelivered -= qty_can_be_undelivered
|
||||
|
||||
def set_serial_and_batch_bundle_from_pick_list(self):
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
for item in self.items:
|
||||
if item.use_serial_batch_fields or not item.against_pick_list or not self.get("update_stock", 1):
|
||||
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": item.against_pick_list,
|
||||
"voucher_detail_no": item.pick_list_item,
|
||||
}
|
||||
|
||||
bundle_id = frappe.db.get_value("Serial and Batch Bundle", filters, "name")
|
||||
|
||||
if bundle_id:
|
||||
cls_obj = SerialBatchCreation(
|
||||
{
|
||||
"type_of_transaction": "Outward",
|
||||
"serial_and_batch_bundle": bundle_id,
|
||||
"item_code": item.get("item_code"),
|
||||
"warehouse": item.get("warehouse"),
|
||||
}
|
||||
)
|
||||
|
||||
cls_obj.duplicate_package()
|
||||
|
||||
item.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
|
||||
|
||||
def update_pick_list_status(self):
|
||||
from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status
|
||||
|
||||
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 set_default_income_account_for_item(obj):
|
||||
"""Set income account as default for items in the transaction.
|
||||
|
||||
@@ -1466,7 +1466,8 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, a
|
||||
if is_unit_price_row(doc)
|
||||
else (doc.qty and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)))
|
||||
)
|
||||
and select_item(doc),
|
||||
and select_item(doc)
|
||||
and not args.get("skip_item_mapping"),
|
||||
},
|
||||
"Sales Taxes and Charges": {
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
|
||||
@@ -356,37 +356,6 @@ class DeliveryNote(SellingController):
|
||||
]
|
||||
)
|
||||
|
||||
def set_serial_and_batch_bundle_from_pick_list(self):
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
for item in self.items:
|
||||
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": item.against_pick_list,
|
||||
"voucher_detail_no": item.pick_list_item,
|
||||
}
|
||||
|
||||
bundle_id = frappe.db.get_value("Serial and Batch Bundle", filters, "name")
|
||||
|
||||
if bundle_id:
|
||||
cls_obj = SerialBatchCreation(
|
||||
{
|
||||
"type_of_transaction": "Outward",
|
||||
"serial_and_batch_bundle": bundle_id,
|
||||
"item_code": item.get("item_code"),
|
||||
"warehouse": item.get("warehouse"),
|
||||
}
|
||||
)
|
||||
|
||||
cls_obj.duplicate_package()
|
||||
|
||||
item.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
|
||||
|
||||
def validate_references(self):
|
||||
self.validate_sales_order_references()
|
||||
self.validate_sales_invoice_references()
|
||||
@@ -617,13 +586,6 @@ class DeliveryNote(SellingController):
|
||||
)
|
||||
)
|
||||
|
||||
def update_pick_list_status(self):
|
||||
from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status
|
||||
|
||||
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(
|
||||
"""select t1.name
|
||||
|
||||
@@ -135,7 +135,12 @@ frappe.ui.form.on("Pick List", {
|
||||
if (frm.doc.purpose === "Delivery") {
|
||||
frm.add_custom_button(
|
||||
__("Delivery Note"),
|
||||
() => frm.trigger("create_delivery_note"),
|
||||
() => frm.events.create_delivery(frm, "Delivery Note"),
|
||||
__("Create")
|
||||
);
|
||||
frm.add_custom_button(
|
||||
__("Sales Invoice"),
|
||||
() => frm.events.create_delivery(frm, "Sales Invoice"),
|
||||
__("Create")
|
||||
);
|
||||
} else {
|
||||
@@ -232,9 +237,12 @@ frappe.ui.form.on("Pick List", {
|
||||
frm.clear_table("locations");
|
||||
frm.trigger("add_get_items_button");
|
||||
},
|
||||
create_delivery_note: (frm) => {
|
||||
create_delivery(frm, doctype) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.stock.doctype.pick_list.pick_list.create_delivery_note",
|
||||
method: "erpnext.stock.doctype.pick_list.pick_list.create_delivery",
|
||||
args: {
|
||||
target: doctype,
|
||||
},
|
||||
frm: frm,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -17,6 +17,9 @@ from frappe.utils.nestedset import get_descendants_of
|
||||
from erpnext.selling.doctype.sales_order.sales_order import (
|
||||
make_delivery_note as create_delivery_note_from_sales_order,
|
||||
)
|
||||
from erpnext.selling.doctype.sales_order.sales_order import (
|
||||
make_sales_invoice as create_sales_invoice_from_sales_order,
|
||||
)
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
get_auto_batch_nos,
|
||||
)
|
||||
@@ -1284,11 +1287,17 @@ def get_available_item_locations_for_other_item(
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_delivery_note(source_name, target_doc=None):
|
||||
return create_delivery(source_name, target_doc, "Delivery Note")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_delivery(source_name, target_doc=None, target=None):
|
||||
pick_list = frappe.get_doc("Pick List", source_name)
|
||||
target = target or (frappe.flags.args or {}).get("target") or "Delivery Note"
|
||||
validate_item_locations(pick_list)
|
||||
sales_dict = dict()
|
||||
sales_orders = []
|
||||
delivery_notes = []
|
||||
documents = []
|
||||
for location in pick_list.locations:
|
||||
if location.sales_order:
|
||||
sales_orders.append(
|
||||
@@ -1318,39 +1327,45 @@ def create_delivery_note(source_name, target_doc=None):
|
||||
sales_dict[key] = {row.sales_order for row in rows}
|
||||
|
||||
if sales_dict:
|
||||
delivery_notes.extend(create_dn_with_so(sales_dict, pick_list))
|
||||
documents.extend(create_delivery_with_so(sales_dict, pick_list, target))
|
||||
|
||||
if not all(item.sales_order for item in pick_list.locations):
|
||||
delivery_notes.append(create_dn_wo_so(pick_list))
|
||||
documents.append(create_delivery_wo_so(pick_list, target, target_doc))
|
||||
|
||||
if len(delivery_notes) == 1:
|
||||
return delivery_notes[0]
|
||||
if len(documents) == 1:
|
||||
return documents[0]
|
||||
else:
|
||||
from frappe.utils import comma_and
|
||||
|
||||
doc_list = [get_link_to_form("Delivery Note", p.name) for p in delivery_notes]
|
||||
doc_list = [get_link_to_form(target, p.name) for p in documents]
|
||||
frappe.msgprint(_("{0} created").format(comma_and(doc_list)))
|
||||
|
||||
|
||||
def create_dn_wo_so(pick_list, delivery_note=None):
|
||||
if not delivery_note:
|
||||
delivery_note = frappe.new_doc("Delivery Note")
|
||||
return create_delivery_wo_so(pick_list, "Delivery Note", delivery_note)
|
||||
|
||||
delivery_note.company = pick_list.company
|
||||
|
||||
def create_delivery_wo_so(pick_list, target, target_doc=None):
|
||||
if not target_doc:
|
||||
target_doc = frappe.new_doc(target)
|
||||
|
||||
target_doc.company = pick_list.company
|
||||
|
||||
item_table_mapper_without_so = {
|
||||
"doctype": "Delivery Note Item",
|
||||
"doctype": f"{target} Item",
|
||||
"field_map": {
|
||||
"rate": "rate",
|
||||
"name": "name",
|
||||
"parent": "",
|
||||
},
|
||||
}
|
||||
map_pl_locations(pick_list, item_table_mapper_without_so, delivery_note)
|
||||
delivery_note.flags.ignore_mandatory = True
|
||||
delivery_note.save()
|
||||
map_pl_locations(pick_list, item_table_mapper_without_so, target_doc)
|
||||
target_doc.flags.ignore_mandatory = True
|
||||
if target == "Sales Invoice":
|
||||
target_doc.update_stock = 1
|
||||
target_doc.save()
|
||||
|
||||
return delivery_note
|
||||
return target_doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -1379,36 +1394,53 @@ def create_dn_for_pick_lists(source_name, target_doc=None, kwargs=None):
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
delivery_note = create_dn_from_so(pick_list, sales_orders, delivery_note=target_doc, kwargs=kwargs)
|
||||
delivery_note = create_delivery_from_so(
|
||||
pick_list, sales_orders, "Delivery Note", target_doc=target_doc, kwargs=kwargs
|
||||
)
|
||||
|
||||
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)
|
||||
delivery_note = create_delivery_wo_so(pick_list, "Delivery Note", 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_notes = []
|
||||
return create_delivery_with_so(sales_dict, pick_list, "Delivery Note")
|
||||
|
||||
|
||||
def create_delivery_with_so(sales_dict, pick_list, target):
|
||||
"""Create target document for each customer (based on SO) in a Pick List."""
|
||||
documents = []
|
||||
|
||||
for key in sales_dict:
|
||||
delivery_note = create_dn_from_so(pick_list, sales_dict[key], None)
|
||||
if delivery_note:
|
||||
delivery_note.flags.ignore_mandatory = True
|
||||
document = create_delivery_from_so(pick_list, sales_dict[key], target)
|
||||
if document:
|
||||
document.flags.ignore_mandatory = True
|
||||
# updates packed_items on save
|
||||
# save as multiple customers are possible
|
||||
delivery_note.save()
|
||||
delivery_notes.append(delivery_note)
|
||||
if target == "Sales Invoice":
|
||||
document.update_stock = 1
|
||||
document.save()
|
||||
documents.append(document)
|
||||
|
||||
return delivery_notes
|
||||
return documents
|
||||
|
||||
|
||||
def create_dn_from_so(pick_list, sales_order_list, delivery_note=None, kwargs=None):
|
||||
return create_delivery_from_so(
|
||||
pick_list, sales_order_list, "Delivery Note", target_doc=delivery_note, kwargs=kwargs
|
||||
)
|
||||
|
||||
|
||||
def create_delivery_from_so(pick_list, sales_order_list, target, target_doc=None, kwargs=None):
|
||||
if not sales_order_list:
|
||||
return delivery_note
|
||||
return target_doc
|
||||
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = kwargs.get("filtered_children", [])
|
||||
@@ -1416,11 +1448,11 @@ def create_dn_from_so(pick_list, sales_order_list, delivery_note=None, kwargs=No
|
||||
return child_filter
|
||||
|
||||
item_table_mapper = {
|
||||
"doctype": "Delivery Note Item",
|
||||
"doctype": f"{target} Item",
|
||||
"field_map": {
|
||||
"rate": "rate",
|
||||
"name": "so_detail",
|
||||
"parent": "against_sales_order",
|
||||
"parent": "against_sales_order" if target == "Delivery Note" else "sales_order",
|
||||
},
|
||||
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty)
|
||||
and doc.delivered_by_supplier != 1
|
||||
@@ -1429,20 +1461,22 @@ def create_dn_from_so(pick_list, sales_order_list, delivery_note=None, kwargs=No
|
||||
|
||||
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
|
||||
target_doc = (
|
||||
create_delivery_note_from_sales_order(next(iter(sales_order_list)), target_doc, kwargs=kwargs)
|
||||
if target == "Delivery Note"
|
||||
else create_sales_invoice_from_sales_order(next(iter(sales_order_list)), target_doc, args=kwargs)
|
||||
)
|
||||
|
||||
if not delivery_note:
|
||||
if not target_doc:
|
||||
return
|
||||
|
||||
for so in sales_order_list:
|
||||
map_pl_locations(pick_list, item_table_mapper, delivery_note, so)
|
||||
map_pl_locations(pick_list, item_table_mapper, target_doc, so)
|
||||
|
||||
return delivery_note
|
||||
return target_doc
|
||||
|
||||
|
||||
def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None):
|
||||
def map_pl_locations(pick_list, item_mapper, target_doc, sales_order=None):
|
||||
for location in pick_list.locations:
|
||||
if location.sales_order != sales_order or location.product_bundle_item:
|
||||
continue
|
||||
@@ -1454,36 +1488,44 @@ def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None):
|
||||
|
||||
source_doc = sales_order_item or location
|
||||
|
||||
dn_item = map_child_doc(source_doc, delivery_note, item_mapper)
|
||||
child_item = map_child_doc(source_doc, target_doc, 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 - location.delivered_qty) / (
|
||||
flt(dn_item.conversion_factor) or 1
|
||||
if child_item:
|
||||
child_item.against_pick_list = pick_list.name
|
||||
child_item.pick_list_item = location.name
|
||||
child_item.warehouse = location.warehouse
|
||||
child_item.qty = flt(location.picked_qty - location.delivered_qty) / (
|
||||
flt(child_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
|
||||
child_item.batch_no = location.batch_no
|
||||
child_item.serial_no = location.serial_no
|
||||
child_item.use_serial_batch_fields = location.use_serial_batch_fields
|
||||
|
||||
update_delivery_note_item(source_doc, dn_item, delivery_note)
|
||||
if not child_item.qty:
|
||||
target_doc.items.remove(child_item)
|
||||
continue
|
||||
|
||||
add_product_bundles_to_delivery_note(pick_list, delivery_note, item_mapper, sales_order)
|
||||
set_delivery_note_missing_values(delivery_note)
|
||||
update_child_item(source_doc, child_item, target_doc)
|
||||
|
||||
delivery_note.company = pick_list.company
|
||||
add_product_bundles_to_target(pick_list, target_doc, item_mapper, sales_order)
|
||||
set_target_missing_values(target_doc)
|
||||
|
||||
target_doc.company = pick_list.company
|
||||
if sales_order:
|
||||
delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer")
|
||||
target_doc.customer = frappe.get_value("Sales Order", sales_order, "customer")
|
||||
|
||||
|
||||
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.
|
||||
return add_product_bundles_to_target(pick_list, delivery_note, item_mapper, sales_order)
|
||||
|
||||
|
||||
def add_product_bundles_to_target(pick_list, target_doc, item_mapper, sales_order=None) -> None:
|
||||
"""Add product bundles found in pick list to target document.
|
||||
|
||||
When mapping pick list items, the bundle item itself isn't part of the
|
||||
locations. Dynamically fetch and add parent bundle item into DN."""
|
||||
locations. Dynamically fetch and add parent bundle item into target document."""
|
||||
product_bundles = pick_list._get_product_bundles()
|
||||
product_bundle_qty_map = pick_list._get_product_bundle_qty_map(product_bundles.values())
|
||||
|
||||
@@ -1492,13 +1534,13 @@ def add_product_bundles_to_delivery_note(
|
||||
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(
|
||||
target_bundle_item = map_child_doc(sales_order_item, target_doc, item_mapper)
|
||||
target_bundle_item.qty = pick_list._compute_picked_qty_for_bundle(
|
||||
so_row, product_bundle_qty_map[value.item_code]
|
||||
)
|
||||
dn_bundle_item.pick_list_item = value.pick_list_item
|
||||
dn_bundle_item.against_pick_list = pick_list.name
|
||||
update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note)
|
||||
target_bundle_item.pick_list_item = value.pick_list_item
|
||||
target_bundle_item.against_pick_list = pick_list.name
|
||||
update_child_item(sales_order_item, target_bundle_item, target_doc)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -1572,12 +1614,16 @@ def get_actual_qty(item_code, warehouse):
|
||||
|
||||
|
||||
def update_delivery_note_item(source, target, delivery_note):
|
||||
cost_center = frappe.db.get_value("Project", delivery_note.project, "cost_center")
|
||||
return update_child_item(source, target, delivery_note)
|
||||
|
||||
|
||||
def update_child_item(source, target, target_doc):
|
||||
cost_center = frappe.db.get_value("Project", target_doc.project, "cost_center")
|
||||
if not cost_center:
|
||||
cost_center = get_cost_center(source.item_code, "Item", delivery_note.company)
|
||||
cost_center = get_cost_center(source.item_code, "Item", target_doc.company)
|
||||
|
||||
if not cost_center:
|
||||
cost_center = get_cost_center(source.item_group, "Item Group", delivery_note.company)
|
||||
cost_center = get_cost_center(source.item_group, "Item Group", target_doc.company)
|
||||
|
||||
target.cost_center = cost_center
|
||||
|
||||
@@ -1592,6 +1638,10 @@ def get_cost_center(for_item, from_doctype, company):
|
||||
|
||||
|
||||
def set_delivery_note_missing_values(target):
|
||||
return set_target_missing_values(target)
|
||||
|
||||
|
||||
def set_target_missing_values(target):
|
||||
target.run_method("set_missing_values")
|
||||
target.run_method("set_po_nos")
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
|
||||
@@ -7,6 +7,7 @@ def get_data():
|
||||
"non_standard_fieldnames": {
|
||||
"Stock Reservation Entry": "from_voucher_no",
|
||||
"Delivery Note": "against_pick_list",
|
||||
"Sales Invoice": "against_pick_list",
|
||||
},
|
||||
"internal_links": {
|
||||
"Sales Order": ["locations", "sales_order"],
|
||||
@@ -14,7 +15,7 @@ def get_data():
|
||||
"transactions": [
|
||||
{
|
||||
"label": _("Sales"),
|
||||
"items": ["Sales Order", "Delivery Note"],
|
||||
"items": ["Sales Order", "Delivery Note", "Sales Invoice"],
|
||||
},
|
||||
{
|
||||
"label": _("Manufacturing"),
|
||||
|
||||
@@ -9,7 +9,11 @@ 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, create_dn_for_pick_lists
|
||||
from erpnext.stock.doctype.pick_list.pick_list import (
|
||||
create_delivery,
|
||||
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,
|
||||
@@ -768,6 +772,198 @@ class TestPickList(ERPNextTestSuite):
|
||||
if dn_item.item_code == "_Test Item 2":
|
||||
self.assertEqual(dn_item.qty, 2)
|
||||
|
||||
@ERPNextTestSuite.change_settings("Stock Settings", {"use_serial_batch_fields": 1})
|
||||
def test_sales_invoice_from_pick_list_copies_old_batch_serial_fields(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item(
|
||||
f"_Test PLSI Old Fields {frappe.generate_hash(length=8)}",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"has_serial_no": 1,
|
||||
"batch_number_series": f"PLSI-OLD-B-{frappe.generate_hash(length=6)}-.#####",
|
||||
"serial_no_series": f"PLSI-OLD-S-{frappe.generate_hash(length=6)}-.#####",
|
||||
},
|
||||
).name
|
||||
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=2, basic_rate=100)
|
||||
sales_order = make_sales_order(item_code=item, warehouse=warehouse, qty=2, rate=100)
|
||||
|
||||
pick_list = create_pick_list(sales_order.name)
|
||||
pick_list.submit()
|
||||
pick_list_item = pick_list.locations[0]
|
||||
|
||||
self.assertTrue(pick_list_item.use_serial_batch_fields)
|
||||
self.assertTrue(pick_list_item.batch_no)
|
||||
self.assertTrue(pick_list_item.serial_no)
|
||||
|
||||
sales_invoice = create_delivery(pick_list.name, target="Sales Invoice")
|
||||
sales_invoice_item = sales_invoice.items[0]
|
||||
|
||||
self.assertEqual(sales_invoice.update_stock, 1)
|
||||
self.assertEqual(sales_invoice_item.against_pick_list, pick_list.name)
|
||||
self.assertEqual(sales_invoice_item.pick_list_item, pick_list_item.name)
|
||||
self.assertEqual(sales_invoice_item.use_serial_batch_fields, 1)
|
||||
self.assertEqual(sales_invoice_item.batch_no, pick_list_item.batch_no)
|
||||
self.assertEqual(
|
||||
set(sales_invoice_item.serial_no.split("\n")), set(pick_list_item.serial_no.split("\n"))
|
||||
)
|
||||
|
||||
@ERPNextTestSuite.change_settings("Stock Settings", {"use_serial_batch_fields": 0})
|
||||
def test_sales_invoice_from_pick_list_copies_serial_and_batch_bundle(self):
|
||||
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item(
|
||||
f"_Test PLSI Bundle {frappe.generate_hash(length=8)}",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"has_serial_no": 1,
|
||||
"batch_number_series": f"PLSI-BND-B-{frappe.generate_hash(length=6)}-.#####",
|
||||
"serial_no_series": f"PLSI-BND-S-{frappe.generate_hash(length=6)}-.#####",
|
||||
},
|
||||
).name
|
||||
|
||||
stock_entry = make_stock_entry(item=item, to_warehouse=warehouse, qty=2, basic_rate=100)
|
||||
batch_no = get_batch_from_bundle(stock_entry.items[0].serial_and_batch_bundle)
|
||||
serial_nos = get_serial_nos_from_bundle(stock_entry.items[0].serial_and_batch_bundle)
|
||||
sales_order = make_sales_order(item_code=item, warehouse=warehouse, qty=2, rate=100)
|
||||
|
||||
pick_list = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Pick List",
|
||||
"company": "_Test Company",
|
||||
"customer": "_Test Customer",
|
||||
"items_based_on": "Sales Order",
|
||||
"purpose": "Delivery",
|
||||
"pick_manually": 1,
|
||||
"locations": [
|
||||
{
|
||||
"item_code": item,
|
||||
"warehouse": warehouse,
|
||||
"qty": 2,
|
||||
"stock_qty": 2,
|
||||
"picked_qty": 2,
|
||||
"conversion_factor": 1,
|
||||
"sales_order": sales_order.name,
|
||||
"sales_order_item": sales_order.items[0].name,
|
||||
"use_serial_batch_fields": 0,
|
||||
}
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
pick_list.locations[0].serial_and_batch_bundle = (
|
||||
SerialBatchCreation(
|
||||
{
|
||||
"item_code": item,
|
||||
"warehouse": warehouse,
|
||||
"voucher_type": "Pick List",
|
||||
"voucher_no": pick_list.name,
|
||||
"voucher_detail_no": pick_list.locations[0].name,
|
||||
"qty": -2,
|
||||
"batches": frappe._dict({batch_no: 2}),
|
||||
"serial_nos": serial_nos,
|
||||
"type_of_transaction": "Outward",
|
||||
"company": "_Test Company",
|
||||
"do_not_submit": True,
|
||||
}
|
||||
)
|
||||
.make_serial_and_batch_bundle()
|
||||
.name
|
||||
)
|
||||
pick_list.locations[0].db_set(
|
||||
{
|
||||
"use_serial_batch_fields": 0,
|
||||
"batch_no": None,
|
||||
"serial_no": None,
|
||||
"serial_and_batch_bundle": pick_list.locations[0].serial_and_batch_bundle,
|
||||
}
|
||||
)
|
||||
pick_list.reload()
|
||||
pick_list_item = pick_list.locations[0]
|
||||
|
||||
self.assertFalse(pick_list_item.use_serial_batch_fields)
|
||||
self.assertTrue(pick_list_item.serial_and_batch_bundle)
|
||||
|
||||
sales_invoice = create_delivery(pick_list.name, target="Sales Invoice")
|
||||
sales_invoice_item = sales_invoice.items[0]
|
||||
|
||||
self.assertEqual(sales_invoice_item.against_pick_list, pick_list.name)
|
||||
self.assertEqual(sales_invoice_item.pick_list_item, pick_list_item.name)
|
||||
self.assertFalse(sales_invoice_item.use_serial_batch_fields)
|
||||
self.assertTrue(sales_invoice_item.serial_and_batch_bundle)
|
||||
self.assertNotEqual(
|
||||
sales_invoice_item.serial_and_batch_bundle, pick_list_item.serial_and_batch_bundle
|
||||
)
|
||||
self.assertEqual(
|
||||
get_batch_from_bundle(sales_invoice_item.serial_and_batch_bundle),
|
||||
get_batch_from_bundle(pick_list_item.serial_and_batch_bundle),
|
||||
)
|
||||
self.assertEqual(
|
||||
set(get_serial_nos_from_bundle(sales_invoice_item.serial_and_batch_bundle)),
|
||||
set(get_serial_nos_from_bundle(pick_list_item.serial_and_batch_bundle)),
|
||||
)
|
||||
|
||||
def test_sales_invoice_from_sales_order_pick_list_updates_sales_order(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item().name
|
||||
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=5, basic_rate=100)
|
||||
sales_order = make_sales_order(item_code=item, warehouse=warehouse, qty=5, rate=100)
|
||||
|
||||
pick_list = create_pick_list(sales_order.name)
|
||||
pick_list.submit()
|
||||
|
||||
sales_invoice = create_delivery(pick_list.name, target="Sales Invoice")
|
||||
sales_invoice_item = sales_invoice.items[0]
|
||||
|
||||
self.assertEqual(sales_invoice_item.sales_order, sales_order.name)
|
||||
self.assertEqual(sales_invoice_item.so_detail, sales_order.items[0].name)
|
||||
self.assertEqual(sales_invoice_item.against_pick_list, pick_list.name)
|
||||
self.assertEqual(sales_invoice_item.pick_list_item, pick_list.locations[0].name)
|
||||
|
||||
sales_invoice.submit()
|
||||
pick_list.reload()
|
||||
sales_order.reload()
|
||||
|
||||
self.assertEqual(pick_list.locations[0].delivered_qty, pick_list.locations[0].picked_qty)
|
||||
self.assertEqual(pick_list.per_delivered, 100)
|
||||
self.assertEqual(pick_list.delivery_status, "Fully Delivered")
|
||||
self.assertEqual(pick_list.status, "Completed")
|
||||
|
||||
self.assertEqual(sales_order.items[0].picked_qty, 5)
|
||||
self.assertEqual(sales_order.items[0].delivered_qty, 5)
|
||||
self.assertEqual(sales_order.per_delivered, 100)
|
||||
self.assertEqual(sales_order.delivery_status, "Fully Delivered")
|
||||
self.assertEqual(sales_order.per_billed, 100)
|
||||
self.assertEqual(sales_order.billing_status, "Fully Billed")
|
||||
self.assertEqual(sales_order.status, "Completed")
|
||||
|
||||
def test_sales_invoice_against_pick_list_requires_update_stock(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item().name
|
||||
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=5, basic_rate=100)
|
||||
sales_order = make_sales_order(item_code=item, warehouse=warehouse, qty=5, rate=100)
|
||||
|
||||
pick_list = create_pick_list(sales_order.name)
|
||||
pick_list.submit()
|
||||
|
||||
sales_invoice = create_delivery(pick_list.name, target="Sales Invoice")
|
||||
sales_invoice.update_stock = 0
|
||||
|
||||
self.assertRaisesRegex(
|
||||
frappe.ValidationError,
|
||||
"Update Stock.*Pick List",
|
||||
sales_invoice.save,
|
||||
)
|
||||
|
||||
def test_picklist_reserved_qty_validation(self):
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
|
||||
Reference in New Issue
Block a user