feat: create sales invoice from pick list (backport #55594) (#55635)

This commit is contained in:
Mihir Kandoi
2026-06-04 22:47:26 +05:30
committed by GitHub
parent 84d205f553
commit 743afc972d
10 changed files with 432 additions and 124 deletions

View File

@@ -370,6 +370,8 @@ class SalesInvoice(SellingController):
if row.billing_amount: if row.billing_amount:
row.billing_amount = -abs(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.update_packing_list()
self.set_billing_hours_and_amount() self.set_billing_hours_and_amount()
self.update_timesheet_billing_for_project() self.update_timesheet_billing_for_project()
@@ -389,6 +391,18 @@ class SalesInvoice(SellingController):
self.validate_subcontracted_sales_order() self.validate_subcontracted_sales_order()
self.validate_scio_self_rm_qty() 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): def validate_accounts(self):
self.validate_write_off_account() self.validate_write_off_account()
self.validate_account_for_change_amount() self.validate_account_for_change_amount()
@@ -491,6 +505,7 @@ class SalesInvoice(SellingController):
if self.update_stock == 1: if self.update_stock == 1:
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
self.update_pick_list_status()
if not self.is_return: if not self.is_return:
self.update_billing_status_for_zero_amount_refdoc("Delivery Note") self.update_billing_status_for_zero_amount_refdoc("Delivery Note")
@@ -614,6 +629,7 @@ class SalesInvoice(SellingController):
if self.update_stock == 1: if self.update_stock == 1:
self.update_stock_reservation_entries() self.update_stock_reservation_entries()
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
self.update_pick_list_status()
self.db_set("status", "Cancelled") self.db_set("status", "Cancelled")
@@ -665,26 +681,41 @@ class SalesInvoice(SellingController):
if not cint(self.update_stock): if not cint(self.update_stock):
return return
self.status_updater.append( self.status_updater.extend(
{ [
"source_dt": "Sales Invoice Item", {
"target_dt": "Sales Order Item", "source_dt": "Sales Invoice Item",
"target_parent_dt": "Sales Order", "target_dt": "Sales Order Item",
"target_parent_field": "per_delivered", "target_parent_dt": "Sales Order",
"target_field": "delivered_qty", "target_parent_field": "per_delivered",
"target_ref_field": "qty", "target_field": "delivered_qty",
"source_field": "qty", "target_ref_field": "qty",
"join_field": "so_detail", "source_field": "qty",
"percent_join_field": "sales_order", "join_field": "so_detail",
"status_field": "delivery_status", "percent_join_field": "sales_order",
"keyword": "Delivered", "status_field": "delivery_status",
"second_source_dt": "Delivery Note Item", "keyword": "Delivered",
"second_source_field": "qty", "second_source_dt": "Delivery Note Item",
"second_join_field": "so_detail", "second_source_field": "qty",
"overflow_type": "delivery", "second_join_field": "so_detail",
"extra_cond": """ and exists(select name from `tabSales Invoice` "overflow_type": "delivery",
where name=`tabSales Invoice Item`.parent and update_stock = 1)""", "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): if not cint(self.is_return):

View File

@@ -104,6 +104,7 @@
"sales_order", "sales_order",
"so_detail", "so_detail",
"sales_invoice_item", "sales_invoice_item",
"pick_list_item",
"column_break_74", "column_break_74",
"delivery_note", "delivery_note",
"dn_detail", "dn_detail",
@@ -112,6 +113,7 @@
"pos_invoice", "pos_invoice",
"pos_invoice_item", "pos_invoice_item",
"scio_detail", "scio_detail",
"against_pick_list",
"internal_transfer_section", "internal_transfer_section",
"purchase_order", "purchase_order",
"column_break_92", "column_break_92",
@@ -855,8 +857,8 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rate of Stock UOM", "label": "Rate of Stock UOM",
"no_copy": 1, "no_copy": 1,
"print_hide": 1,
"options": "currency", "options": "currency",
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@@ -1011,13 +1013,30 @@
"label": "Consider for Tax Withholding", "label": "Consider for Tax Withholding",
"print_hide": 1, "print_hide": 1,
"read_only": 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, "grid_page_length": 50,
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2026-05-29 12:23:28.259905", "modified": "2026-06-03 13:17:36.145788",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@@ -22,6 +22,7 @@ class SalesInvoiceItem(Document):
actual_batch_qty: DF.Float actual_batch_qty: DF.Float
actual_qty: DF.Float actual_qty: DF.Float
against_pick_list: DF.Link | None
allow_zero_valuation_rate: DF.Check allow_zero_valuation_rate: DF.Check
amount: DF.Currency amount: DF.Currency
apply_tds: DF.Check apply_tds: DF.Check
@@ -72,6 +73,7 @@ class SalesInvoiceItem(Document):
parent: DF.Data parent: DF.Data
parentfield: DF.Data parentfield: DF.Data
parenttype: DF.Data parenttype: DF.Data
pick_list_item: DF.Data | None
pos_invoice: DF.Link | None pos_invoice: DF.Link | None
pos_invoice_item: DF.Data | None pos_invoice_item: DF.Data | None
price_list_rate: DF.Currency price_list_rate: DF.Currency

View File

@@ -1060,6 +1060,44 @@ class SellingController(StockController):
qty_to_undelivered -= qty_can_be_undelivered 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): def set_default_income_account_for_item(obj):
"""Set income account as default for items in the transaction. """Set income account as default for items in the transaction.

View File

@@ -1466,7 +1466,8 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, a
if is_unit_price_row(doc) if is_unit_price_row(doc)
else (doc.qty and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount))) 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": { "Sales Taxes and Charges": {
"doctype": "Sales Taxes and Charges", "doctype": "Sales Taxes and Charges",

View File

@@ -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): def validate_references(self):
self.validate_sales_order_references() self.validate_sales_order_references()
self.validate_sales_invoice_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): def check_next_docstatus(self):
submit_rv = frappe.db.sql( submit_rv = frappe.db.sql(
"""select t1.name """select t1.name

View File

@@ -135,7 +135,12 @@ frappe.ui.form.on("Pick List", {
if (frm.doc.purpose === "Delivery") { if (frm.doc.purpose === "Delivery") {
frm.add_custom_button( frm.add_custom_button(
__("Delivery Note"), __("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") __("Create")
); );
} else { } else {
@@ -232,9 +237,12 @@ frappe.ui.form.on("Pick List", {
frm.clear_table("locations"); frm.clear_table("locations");
frm.trigger("add_get_items_button"); frm.trigger("add_get_items_button");
}, },
create_delivery_note: (frm) => { create_delivery(frm, doctype) {
frappe.model.open_mapped_doc({ 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, frm: frm,
}); });
}, },

View File

@@ -17,6 +17,9 @@ from frappe.utils.nestedset import get_descendants_of
from erpnext.selling.doctype.sales_order.sales_order import ( from erpnext.selling.doctype.sales_order.sales_order import (
make_delivery_note as create_delivery_note_from_sales_order, 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 ( from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_auto_batch_nos, get_auto_batch_nos,
) )
@@ -1284,11 +1287,17 @@ def get_available_item_locations_for_other_item(
@frappe.whitelist() @frappe.whitelist()
def create_delivery_note(source_name, target_doc=None): 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) 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) validate_item_locations(pick_list)
sales_dict = dict() sales_dict = dict()
sales_orders = [] sales_orders = []
delivery_notes = [] documents = []
for location in pick_list.locations: for location in pick_list.locations:
if location.sales_order: if location.sales_order:
sales_orders.append( 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} sales_dict[key] = {row.sales_order for row in rows}
if sales_dict: 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): 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: if len(documents) == 1:
return delivery_notes[0] return documents[0]
else: else:
from frappe.utils import comma_and 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))) frappe.msgprint(_("{0} created").format(comma_and(doc_list)))
def create_dn_wo_so(pick_list, delivery_note=None): def create_dn_wo_so(pick_list, delivery_note=None):
if not delivery_note: return create_delivery_wo_so(pick_list, "Delivery Note", delivery_note)
delivery_note = frappe.new_doc("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 = { item_table_mapper_without_so = {
"doctype": "Delivery Note Item", "doctype": f"{target} Item",
"field_map": { "field_map": {
"rate": "rate", "rate": "rate",
"name": "name", "name": "name",
"parent": "", "parent": "",
}, },
} }
map_pl_locations(pick_list, item_table_mapper_without_so, delivery_note) map_pl_locations(pick_list, item_table_mapper_without_so, target_doc)
delivery_note.flags.ignore_mandatory = True target_doc.flags.ignore_mandatory = True
delivery_note.save() if target == "Sales Invoice":
target_doc.update_stock = 1
target_doc.save()
return delivery_note return target_doc
@frappe.whitelist() @frappe.whitelist()
@@ -1379,36 +1394,53 @@ def create_dn_for_pick_lists(source_name, target_doc=None, kwargs=None):
pluck="name", 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 not sales_order_arg and not all(item.sales_order for item in pick_list.locations):
if isinstance(delivery_note, str): if isinstance(delivery_note, str):
delivery_note = frappe.get_doc(frappe.parse_json(delivery_note)) 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 return delivery_note
def create_dn_with_so(sales_dict, pick_list): def create_dn_with_so(sales_dict, pick_list):
"""Create Delivery Note for each customer (based on SO) in a Pick List.""" return create_delivery_with_so(sales_dict, pick_list, "Delivery Note")
delivery_notes = []
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: for key in sales_dict:
delivery_note = create_dn_from_so(pick_list, sales_dict[key], None) document = create_delivery_from_so(pick_list, sales_dict[key], target)
if delivery_note: if document:
delivery_note.flags.ignore_mandatory = True document.flags.ignore_mandatory = True
# updates packed_items on save # updates packed_items on save
# save as multiple customers are possible # save as multiple customers are possible
delivery_note.save() if target == "Sales Invoice":
delivery_notes.append(delivery_note) 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): 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: if not sales_order_list:
return delivery_note return target_doc
if kwargs is None:
kwargs = {}
def select_item(d): def select_item(d):
filtered_items = kwargs.get("filtered_children", []) 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 return child_filter
item_table_mapper = { item_table_mapper = {
"doctype": "Delivery Note Item", "doctype": f"{target} Item",
"field_map": { "field_map": {
"rate": "rate", "rate": "rate",
"name": "so_detail", "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) "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty)
and doc.delivered_by_supplier != 1 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} kwargs = {"skip_item_mapping": True, "ignore_pricing_rule": pick_list.ignore_pricing_rule}
delivery_note = create_delivery_note_from_sales_order( target_doc = (
next(iter(sales_order_list)), delivery_note, kwargs=kwargs 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 return
for so in sales_order_list: 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: for location in pick_list.locations:
if location.sales_order != sales_order or location.product_bundle_item: if location.sales_order != sales_order or location.product_bundle_item:
continue 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 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: if child_item:
dn_item.against_pick_list = pick_list.name child_item.against_pick_list = pick_list.name
dn_item.pick_list_item = location.name child_item.pick_list_item = location.name
dn_item.warehouse = location.warehouse child_item.warehouse = location.warehouse
dn_item.qty = flt(location.picked_qty - location.delivered_qty) / ( child_item.qty = flt(location.picked_qty - location.delivered_qty) / (
flt(dn_item.conversion_factor) or 1 flt(child_item.conversion_factor) or 1
) )
dn_item.batch_no = location.batch_no child_item.batch_no = location.batch_no
dn_item.serial_no = location.serial_no child_item.serial_no = location.serial_no
dn_item.use_serial_batch_fields = location.use_serial_batch_fields 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) update_child_item(source_doc, child_item, target_doc)
set_delivery_note_missing_values(delivery_note)
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: 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( def add_product_bundles_to_delivery_note(
pick_list: "PickList", delivery_note, item_mapper, sales_order=None pick_list: "PickList", delivery_note, item_mapper, sales_order=None
) -> 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 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_bundles = pick_list._get_product_bundles()
product_bundle_qty_map = pick_list._get_product_bundle_qty_map(product_bundles.values()) 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: if sales_order and sales_order_item.parent != sales_order:
continue continue
dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper) target_bundle_item = map_child_doc(sales_order_item, target_doc, item_mapper)
dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle( target_bundle_item.qty = pick_list._compute_picked_qty_for_bundle(
so_row, product_bundle_qty_map[value.item_code] so_row, product_bundle_qty_map[value.item_code]
) )
dn_bundle_item.pick_list_item = value.pick_list_item target_bundle_item.pick_list_item = value.pick_list_item
dn_bundle_item.against_pick_list = pick_list.name target_bundle_item.against_pick_list = pick_list.name
update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note) update_child_item(sales_order_item, target_bundle_item, target_doc)
@frappe.whitelist() @frappe.whitelist()
@@ -1572,12 +1614,16 @@ def get_actual_qty(item_code, warehouse):
def update_delivery_note_item(source, target, delivery_note): 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: 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: 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 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): 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_missing_values")
target.run_method("set_po_nos") target.run_method("set_po_nos")
target.run_method("calculate_taxes_and_totals") target.run_method("calculate_taxes_and_totals")

View File

@@ -7,6 +7,7 @@ def get_data():
"non_standard_fieldnames": { "non_standard_fieldnames": {
"Stock Reservation Entry": "from_voucher_no", "Stock Reservation Entry": "from_voucher_no",
"Delivery Note": "against_pick_list", "Delivery Note": "against_pick_list",
"Sales Invoice": "against_pick_list",
}, },
"internal_links": { "internal_links": {
"Sales Order": ["locations", "sales_order"], "Sales Order": ["locations", "sales_order"],
@@ -14,7 +15,7 @@ def get_data():
"transactions": [ "transactions": [
{ {
"label": _("Sales"), "label": _("Sales"),
"items": ["Sales Order", "Delivery Note"], "items": ["Sales Order", "Delivery Note", "Sales Invoice"],
}, },
{ {
"label": _("Manufacturing"), "label": _("Manufacturing"),

View File

@@ -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.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.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.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.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import ( from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle, get_batch_from_bundle,
@@ -768,6 +772,198 @@ class TestPickList(ERPNextTestSuite):
if dn_item.item_code == "_Test Item 2": if dn_item.item_code == "_Test Item 2":
self.assertEqual(dn_item.qty, 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): def test_picklist_reserved_qty_validation(self):
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order