fix: travis for POS merge invoice and putaway rule

This commit is contained in:
Rohit Waghchaure 2023-04-03 12:26:12 +05:30
parent f8bf4aa7c8
commit 74ab20f97a
11 changed files with 182 additions and 107 deletions

View File

@ -387,7 +387,7 @@ def split_invoices(invoices):
]
for pos_invoice in pos_return_docs:
for item in pos_invoice.items:
if not item.serial_no:
if not item.serial_no and not item.serial_and_batch_bundle:
continue
return_against_is_added = any(

View File

@ -13,6 +13,9 @@ from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_inv
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
consolidate_pos_invoices,
)
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_serial_nos_from_bundle,
)
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@ -410,13 +413,13 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
try:
se = make_serialized_item()
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
serial_no = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0]
init_user_and_profile()
pos_inv = create_pos_invoice(
item_code="_Test Serialized Item With Series",
serial_no=serial_no,
serial_no=[serial_no],
qty=1,
rate=100,
do_not_submit=1,
@ -430,7 +433,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
pos_inv2 = create_pos_invoice(
item_code="_Test Serialized Item With Series",
serial_no=serial_no,
serial_no=[serial_no],
qty=1,
rate=100,
do_not_submit=1,

View File

@ -2981,7 +2981,7 @@ class TestSalesInvoice(unittest.TestCase):
# Sales Invoice with Payment Schedule
si_with_payment_schedule = create_sales_invoice(do_not_submit=True)
si_with_payment_schedule.extend(
si_with_payment_schedule.set(
"payment_schedule",
[
{

View File

@ -663,19 +663,31 @@ def get_filters(
return filters
def get_returned_serial_nos(child_doc, parent_doc, serial_no_field=None):
def get_returned_serial_nos(
child_doc, parent_doc, serial_no_field=None, ignore_voucher_detail_no=None
):
from erpnext.stock.doctype.serial_no.serial_no import (
get_serial_nos as get_serial_nos_from_serial_no,
)
from erpnext.stock.serial_batch_bundle import get_serial_nos
if not serial_no_field:
serial_no_field = "serial_and_batch_bundle"
old_field = "serial_no"
if serial_no_field == "rejected_serial_and_batch_bundle":
old_field = "rejected_serial_no"
return_ref_field = frappe.scrub(child_doc.doctype)
if child_doc.doctype == "Delivery Note Item":
return_ref_field = "dn_detail"
serial_nos = []
fields = [f"`{'tab' + child_doc.doctype}`.`{serial_no_field}`"]
fields = [
f"`{'tab' + child_doc.doctype}`.`{serial_no_field}`",
f"`{'tab' + child_doc.doctype}`.`{old_field}`",
]
filters = [
[parent_doc.doctype, "return_against", "=", parent_doc.name],
@ -684,9 +696,15 @@ def get_returned_serial_nos(child_doc, parent_doc, serial_no_field=None):
[parent_doc.doctype, "docstatus", "=", 1],
]
# Required for POS Invoice
if ignore_voucher_detail_no:
filters.append([child_doc.doctype, "name", "!=", ignore_voucher_detail_no])
ids = []
for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters):
ids.append(row.get("serial_and_batch_bundle"))
if row.get(old_field):
serial_nos.extend(get_serial_nos_from_serial_no(row.get(old_field)))
serial_nos.extend(get_serial_nos(ids))

View File

@ -931,7 +931,7 @@ class TestDeliveryNote(FrappeTestCase):
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TESTBATCH.#####",
"batch_number_series": "TESTBATCHIUU.#####",
},
)
make_product_bundle(parent=batched_bundle.name, items=[batched_item.name])
@ -942,7 +942,7 @@ class TestDeliveryNote(FrappeTestCase):
dn = create_delivery_note(item_code=batched_bundle.name, qty=1)
dn.load_from_db()
batch_no = get_batch_from_bundle(dn.items[0].serial_and_batch_bundle)
batch_no = get_batch_from_bundle(dn.packed_items[0].serial_and_batch_bundle)
self.assertTrue(batch_no)
def test_payment_terms_are_fetched_when_creating_sales_invoice(self):

View File

@ -11,7 +11,6 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, cstr, floor, flt, nowdate
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.utils import get_stock_balance
@ -99,7 +98,6 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
item = frappe._dict(item)
source_warehouse = item.get("s_warehouse")
serial_nos = get_serial_nos(item.get("serial_no"))
item.conversion_factor = flt(item.conversion_factor) or 1.0
pending_qty, item_code = flt(item.qty), item.item_code
pending_stock_qty = flt(item.transfer_qty) if doctype == "Stock Entry" else flt(item.stock_qty)
@ -145,9 +143,7 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
if not qty_to_allocate:
break
updated_table = add_row(
item, qty_to_allocate, rule.warehouse, updated_table, rule.name, serial_nos=serial_nos
)
updated_table = add_row(item, qty_to_allocate, rule.warehouse, updated_table, rule.name)
pending_stock_qty -= stock_qty_to_allocate
pending_qty -= qty_to_allocate
@ -245,7 +241,7 @@ def get_ordered_putaway_rules(item_code, company, source_warehouse=None):
return False, vacant_rules
def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=None):
def add_row(item, to_allocate, warehouse, updated_table, rule=None):
new_updated_table_row = copy.deepcopy(item)
new_updated_table_row.idx = 1 if not updated_table else cint(updated_table[-1].idx) + 1
new_updated_table_row.name = None
@ -264,8 +260,8 @@ def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=N
if rule:
new_updated_table_row.putaway_rule = rule
if serial_nos:
new_updated_table_row.serial_no = get_serial_nos_to_allocate(serial_nos, to_allocate)
new_updated_table_row.serial_and_batch_bundle = ""
updated_table.append(new_updated_table_row)
return updated_table
@ -297,12 +293,3 @@ def show_unassigned_items_message(items_not_accomodated):
)
frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True)
def get_serial_nos_to_allocate(serial_nos, to_allocate):
if serial_nos:
allocated_serial_nos = serial_nos[0 : cint(to_allocate)]
serial_nos[:] = serial_nos[cint(to_allocate) :] # pop out allocated serial nos and modify list
return "\n".join(allocated_serial_nos) if allocated_serial_nos else ""
else:
return ""

View File

@ -7,6 +7,11 @@ from frappe.tests.utils import FrappeTestCase
from erpnext.stock.doctype.batch.test_batch import make_new_batch
from erpnext.stock.doctype.item.test_item import make_item
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,
get_serial_nos_from_bundle,
make_serial_batch_bundle,
)
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.get_item_details import get_conversion_factor
@ -382,42 +387,49 @@ class TestPutawayRule(FrappeTestCase):
make_new_batch(batch_id="BOTTL-BATCH-1", item_code="Water Bottle")
pr = make_purchase_receipt(item_code="Water Bottle", qty=5, do_not_submit=1)
pr.items[0].batch_no = "BOTTL-BATCH-1"
pr.save()
pr.submit()
pr.load_from_db()
serial_nos = frappe.get_list(
"Serial No", filters={"purchase_document_no": pr.name, "status": "Active"}
)
serial_nos = [d.name for d in serial_nos]
batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle)
serial_nos = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)
stock_entry = make_stock_entry(
item_code="Water Bottle",
source="_Test Warehouse - _TC",
qty=5,
serial_no=serial_nos,
target="Finished Goods - _TC",
purpose="Material Transfer",
apply_putaway_rule=1,
do_not_save=1,
)
stock_entry.items[0].batch_no = "BOTTL-BATCH-1"
stock_entry.items[0].serial_no = "\n".join(serial_nos)
stock_entry.save()
stock_entry.load_from_db()
self.assertEqual(stock_entry.items[0].t_warehouse, self.warehouse_1)
self.assertEqual(stock_entry.items[0].qty, 3)
self.assertEqual(stock_entry.items[0].putaway_rule, rule_1.name)
self.assertEqual(stock_entry.items[0].serial_no, "\n".join(serial_nos[:3]))
self.assertEqual(stock_entry.items[0].batch_no, "BOTTL-BATCH-1")
self.assertEqual(
get_serial_nos_from_bundle(stock_entry.items[0].serial_and_batch_bundle), serial_nos[0:3]
)
self.assertEqual(get_batch_from_bundle(stock_entry.items[0].serial_and_batch_bundle), batch_no)
self.assertEqual(stock_entry.items[1].t_warehouse, self.warehouse_2)
self.assertEqual(stock_entry.items[1].qty, 2)
self.assertEqual(stock_entry.items[1].putaway_rule, rule_2.name)
self.assertEqual(stock_entry.items[1].serial_no, "\n".join(serial_nos[3:]))
self.assertEqual(stock_entry.items[1].batch_no, "BOTTL-BATCH-1")
self.assertEqual(
get_serial_nos_from_bundle(stock_entry.items[1].serial_and_batch_bundle), serial_nos[3:5]
)
self.assertEqual(get_batch_from_bundle(stock_entry.items[1].serial_and_batch_bundle), batch_no)
self.assertUnchangedItemsOnResave(stock_entry)
for row in stock_entry.items:
if row.serial_and_batch_bundle:
frappe.delete_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
stock_entry.load_from_db()
stock_entry.delete()
pr.cancel()
rule_1.delete()

View File

@ -6,7 +6,7 @@ from collections import defaultdict
from typing import Dict, List
import frappe
from frappe import _, bold
from frappe import _, _dict, bold
from frappe.model.document import Document
from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.utils import add_days, cint, flt, get_link_to_form, nowtime, today
@ -82,16 +82,20 @@ class SerialandBatchBundle(Document):
return
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
available_serial_nos = get_available_serial_nos(
frappe._dict(
{
"item_code": self.item_code,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
}
)
kwargs = frappe._dict(
{
"item_code": self.item_code,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"serial_nos": serial_nos,
}
)
if self.returned_against and self.docstatus == 1:
kwargs["ignore_voucher_detail_no"] = self.voucher_detail_no
available_serial_nos = get_available_serial_nos(kwargs)
for data in available_serial_nos:
if data.serial_no in serial_nos:
self.throw_error_message(
@ -776,6 +780,10 @@ def get_available_serial_nos(kwargs):
ignore_serial_nos = get_reserved_serial_nos_for_pos(kwargs)
# To ignore serial nos in the same record for the draft state
if kwargs.get("ignore_serial_nos"):
ignore_serial_nos.extend(kwargs.get("ignore_serial_nos"))
if kwargs.get("posting_date"):
if kwargs.get("posting_time") is None:
kwargs.posting_time = nowtime()
@ -801,7 +809,7 @@ def get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos):
for d in data:
if d.serial_and_batch_bundle:
sns = get_serial_nos_from_bundle(d.serial_and_batch_bundle)
sns = get_serial_nos_from_bundle(d.serial_and_batch_bundle, kwargs.get("serial_nos", []))
if d.actual_qty > 0:
serial_nos.update(sns)
else:
@ -823,12 +831,19 @@ def get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos):
def get_reserved_serial_nos_for_pos(kwargs):
from erpnext.controllers.sales_and_purchase_return import get_returned_serial_nos
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
ignore_serial_nos = []
pos_invoices = frappe.get_all(
"POS Invoice",
fields=["`tabPOS Invoice Item`.serial_no", "`tabPOS Invoice Item`.serial_and_batch_bundle"],
fields=[
"`tabPOS Invoice Item`.serial_no",
"`tabPOS Invoice`.is_return",
"`tabPOS Invoice Item`.name as child_docname",
"`tabPOS Invoice`.name as parent_docname",
"`tabPOS Invoice Item`.serial_and_batch_bundle",
],
filters=[
["POS Invoice", "consolidated_invoice", "is", "not set"],
["POS Invoice", "docstatus", "=", 1],
@ -850,11 +865,35 @@ def get_reserved_serial_nos_for_pos(kwargs):
ignore_serial_nos.append(d.serial_no)
# Will be deprecated in v16
returned_serial_nos = []
for pos_invoice in pos_invoices:
if pos_invoice.serial_no:
ignore_serial_nos.extend(get_serial_nos(pos_invoice.serial_no))
return ignore_serial_nos
if pos_invoice.is_return:
continue
child_doc = _dict(
{
"doctype": "POS Invoice Item",
"name": pos_invoice.child_docname,
}
)
parent_doc = _dict(
{
"doctype": "POS Invoice",
"name": pos_invoice.parent_docname,
}
)
returned_serial_nos.extend(
get_returned_serial_nos(
child_doc, parent_doc, ignore_voucher_detail_no=kwargs.get("ignore_voucher_detail_no")
)
)
return list(set(ignore_serial_nos) - set(returned_serial_nos))
def get_auto_batch_nos(kwargs):

View File

@ -11,6 +11,11 @@ from frappe.tests.utils import FrappeTestCase
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.item.test_item import make_item
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,
get_serial_nos_from_bundle,
make_serial_batch_bundle,
)
from erpnext.stock.doctype.serial_no.serial_no import *
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@ -209,23 +214,6 @@ class TestSerialNo(FrappeTestCase):
self.assertEqual(sn_doc.warehouse, "_Test Warehouse - _TC")
self.assertEqual(sn_doc.purchase_document_no, se.name)
def test_auto_creation_of_serial_no(self):
"""
Test if auto created Serial No excludes existing serial numbers
"""
item_code = make_item(
"_Test Auto Serial Item ", {"has_serial_no": 1, "serial_no_series": "XYZ.###"}
).item_code
# Reserve XYZ005
pr_1 = make_purchase_receipt(item_code=item_code, qty=1, serial_no="XYZ005")
# XYZ005 is already used and will throw an error if used again
pr_2 = make_purchase_receipt(item_code=item_code, qty=10)
self.assertEqual(get_serial_nos(pr_1.get("items")[0].serial_no)[0], "XYZ005")
for serial_no in get_serial_nos(pr_2.get("items")[0].serial_no):
self.assertNotEqual(serial_no, "XYZ005")
def test_serial_no_sanitation(self):
"Test if Serial No input is sanitised before entering the DB."
item_code = "_Test Serialized Item"
@ -288,12 +276,12 @@ class TestSerialNo(FrappeTestCase):
in1.reload()
in2.reload()
batch1 = in1.items[0].batch_no
batch2 = in2.items[0].batch_no
batch1 = get_batch_from_bundle(in1.items[0].serial_and_batch_bundle)
batch2 = get_batch_from_bundle(in2.items[0].serial_and_batch_bundle)
batch_wise_serials = {
batch1: get_serial_nos(in1.items[0].serial_no),
batch2: get_serial_nos(in2.items[0].serial_no),
batch1: get_serial_nos_from_bundle(in1.items[0].serial_and_batch_bundle),
batch2: get_serial_nos_from_bundle(in2.items[0].serial_and_batch_bundle),
}
# Test FIFO

View File

@ -142,7 +142,6 @@ class StockEntry(StockController):
self.validate_job_card_item()
self.set_purpose_for_stock_entry()
self.clean_serial_nos()
self.validate_duplicate_serial_no()
if not self.from_bom:
self.fg_completed_qty = 0.0
@ -878,52 +877,63 @@ class StockEntry(StockController):
if self.stock_entry_type and not self.purpose:
self.purpose = frappe.get_cached_value("Stock Entry Type", self.stock_entry_type, "purpose")
def validate_duplicate_serial_no(self):
warehouse_wise_serial_nos = {}
# In case of repack the source and target serial nos could be same
for warehouse in ["s_warehouse", "t_warehouse"]:
serial_nos = []
for row in self.items:
if not (row.serial_no and row.get(warehouse)):
continue
for sn in get_serial_nos(row.serial_no):
if sn in serial_nos:
frappe.throw(
_("The serial no {0} has added multiple times in the stock entry {1}").format(
frappe.bold(sn), self.name
)
)
serial_nos.append(sn)
def make_serial_and_batch_bundle_for_outward(self):
if self.docstatus == 1:
return
serial_or_batch_items = get_serial_or_batch_items(self.items)
if not serial_or_batch_items:
return
already_picked_serial_nos = []
for row in self.items:
if not row.s_warehouse:
continue
if row.serial_and_batch_bundle or row.item_code not in serial_or_batch_items:
if row.item_code not in serial_or_batch_items:
continue
bundle_doc = SerialBatchCreation(
{
"item_code": row.item_code,
"warehouse": row.s_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"voucher_type": self.doctype,
"voucher_detail_no": row.name,
"qty": row.qty * -1,
"type_of_transaction": "Outward",
"company": self.company,
"do_not_submit": True,
}
).make_serial_and_batch_bundle()
bundle_doc = None
if row.serial_and_batch_bundle and abs(row.qty) != abs(
frappe.get_cached_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "total_qty")
):
bundle_doc = SerialBatchCreation(
{
"item_code": row.item_code,
"warehouse": row.s_warehouse,
"serial_and_batch_bundle": row.serial_and_batch_bundle,
"type_of_transaction": "Outward",
"ignore_serial_nos": already_picked_serial_nos,
"qty": row.qty * -1,
}
).update_serial_and_batch_entries()
elif not row.serial_and_batch_bundle:
bundle_doc = SerialBatchCreation(
{
"item_code": row.item_code,
"warehouse": row.s_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"voucher_type": self.doctype,
"voucher_detail_no": row.name,
"qty": row.qty * -1,
"ignore_serial_nos": already_picked_serial_nos,
"type_of_transaction": "Outward",
"company": self.company,
"do_not_submit": True,
}
).make_serial_and_batch_bundle()
if not bundle_doc:
continue
if self.docstatus == 0:
for entry in bundle_doc.entries:
if not entry.serial_no:
continue
already_picked_serial_nos.append(entry.serial_no)
row.serial_and_batch_bundle = bundle_doc.name

View File

@ -255,11 +255,14 @@ class SerialBatchBundle:
frappe.db.set_value("Batch", batch_no, "batch_qty", batches_qty.get(batch_no, 0))
def get_serial_nos(serial_and_batch_bundle):
def get_serial_nos(serial_and_batch_bundle, serial_nos=None):
filters = {"parent": serial_and_batch_bundle}
if isinstance(serial_and_batch_bundle, list):
filters = {"parent": ("in", serial_and_batch_bundle)}
if serial_nos:
filters["serial_no"] = ("in", serial_nos)
entries = frappe.get_all("Serial and Batch Entry", fields=["serial_no"], filters=filters)
return [d.serial_no for d in entries]
@ -694,6 +697,18 @@ class SerialBatchCreation:
return doc
def update_serial_and_batch_entries(self):
doc = frappe.get_doc("Serial and Batch Bundle", self.serial_and_batch_bundle)
doc.type_of_transaction = self.type_of_transaction
doc.set("entries", [])
self.set_auto_serial_batch_entries_for_outward()
self.set_serial_batch_entries(doc)
if not doc.get("entries"):
return frappe._dict({})
doc.save()
return doc
def set_auto_serial_batch_entries_for_outward(self):
from erpnext.stock.doctype.batch.batch import get_available_batches
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos_for_outward
@ -707,6 +722,9 @@ class SerialBatchCreation:
}
)
if self.get("ignore_serial_nos"):
kwargs["ignore_serial_nos"] = self.ignore_serial_nos
if self.has_serial_no and not self.get("serial_nos"):
self.serial_nos = get_serial_nos_for_outward(kwargs)
elif not self.has_serial_no and self.has_batch_no and not self.get("batches"):