mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-25 06:17:05 +00:00
Compare commits
8 Commits
v15.121.4
...
version-15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75b2130038 | ||
|
|
06d60f66b1 | ||
|
|
a344958a73 | ||
|
|
c39fdadd1f | ||
|
|
9703007188 | ||
|
|
cde7601804 | ||
|
|
6544048b51 | ||
|
|
134f20d6a1 |
@@ -424,6 +424,7 @@ def apply_pricing_rule(args, doc=None):
|
||||
for item in item_list:
|
||||
args_copy = copy.deepcopy(args)
|
||||
args_copy.update(item)
|
||||
set_transaction_type(args_copy)
|
||||
data = get_pricing_rule_for_item(args_copy, doc=doc)
|
||||
out.append(data)
|
||||
|
||||
|
||||
@@ -494,6 +494,7 @@ class SalesInvoice(SellingController):
|
||||
self.validate_standalone_serial_nos_customer()
|
||||
self.update_stock_reservation_entries()
|
||||
self.update_stock_ledger()
|
||||
self.validate_produced_serial_nos_against_reservation()
|
||||
|
||||
self.process_asset_depreciation()
|
||||
|
||||
|
||||
@@ -5042,6 +5042,34 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
frappe.db.set_value("Company", "_Test Company 1", "cost_center", cost_center)
|
||||
|
||||
@change_settings("Stock Settings", {"enable_stock_reservation": 1})
|
||||
def test_update_stock_restricted_to_reserved_produced_serial_nos(self):
|
||||
from erpnext.selling.doctype.sales_order.sales_order import (
|
||||
make_sales_invoice as make_si_from_so,
|
||||
)
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import (
|
||||
make_so_with_reserved_produced_serial_no,
|
||||
)
|
||||
|
||||
so, reserved, unreserved = make_so_with_reserved_produced_serial_no()
|
||||
|
||||
def make_si(serial_no):
|
||||
si = make_si_from_so(so.name)
|
||||
si.update_stock = 1
|
||||
si.items[0].warehouse = so.items[0].warehouse
|
||||
si.items[0].use_serial_batch_fields = 1
|
||||
si.items[0].serial_no = serial_no
|
||||
return si.save()
|
||||
|
||||
frappe.db.savepoint("unreserved_serial_no")
|
||||
si = make_si(unreserved[0])
|
||||
self.assertRaises(frappe.ValidationError, si.submit)
|
||||
frappe.db.rollback(save_point="unreserved_serial_no")
|
||||
|
||||
si = make_si(reserved[0])
|
||||
si.submit()
|
||||
self.assertEqual(get_serial_nos_from_bundle(si.items[0].serial_and_batch_bundle), reserved)
|
||||
|
||||
|
||||
def make_item_for_si(item_code, properties=None):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
@@ -896,6 +896,77 @@ class SellingController(StockController):
|
||||
title=_("Not Allowed"),
|
||||
)
|
||||
|
||||
def validate_produced_serial_nos_against_reservation(self):
|
||||
"""Restrict delivery to the serial nos reserved for a Sales Order Item with ensure delivery by serial no."""
|
||||
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||
get_sre_reserved_serial_nos_for_voucher_detail_nos,
|
||||
)
|
||||
|
||||
if self.is_return or not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"):
|
||||
return
|
||||
|
||||
so_field = "sales_order" if self.doctype == "Sales Invoice" else "against_sales_order"
|
||||
rows = [d for d in self.items if d.get(so_field) and d.so_detail]
|
||||
if not rows:
|
||||
return
|
||||
|
||||
flagged_so_details = frappe.get_all(
|
||||
"Sales Order Item",
|
||||
filters={
|
||||
"name": ("in", [d.so_detail for d in rows]),
|
||||
"ensure_delivery_based_on_produced_serial_no": 1,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
rows = [d for d in rows if d.so_detail in flagged_so_details]
|
||||
if not rows:
|
||||
return
|
||||
|
||||
reserved_serial_nos = get_sre_reserved_serial_nos_for_voucher_detail_nos(
|
||||
"Sales Order", flagged_so_details
|
||||
)
|
||||
bundle_map = dict(
|
||||
frappe.get_all(
|
||||
rows[0].doctype,
|
||||
filters={"name": ("in", [d.name for d in rows])},
|
||||
fields=["name", "serial_and_batch_bundle"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
bundle_serial_nos = frappe._dict()
|
||||
if bundles := [b for b in bundle_map.values() if b]:
|
||||
for entry in frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": ("in", bundles), "serial_no": ("is", "set")},
|
||||
fields=["parent", "serial_no"],
|
||||
):
|
||||
bundle_serial_nos.setdefault(entry.parent, []).append(entry.serial_no)
|
||||
|
||||
for row in rows:
|
||||
if not reserved_serial_nos.get(row.so_detail):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Delivery of Item {1} is ensured by produced Serial No, but no Serial No is reserved against Sales Order {2}. Reserve the produced Serial Nos from the Sales Order."
|
||||
).format(row.idx, frappe.bold(row.item_code), frappe.bold(row.get(so_field))),
|
||||
title=_("Serial No Not Reserved"),
|
||||
)
|
||||
|
||||
bundle = bundle_map.get(row.name)
|
||||
serial_nos = bundle_serial_nos.get(bundle, []) if bundle else get_serial_nos(row.serial_no)
|
||||
if invalid_serial_nos := [
|
||||
sn for sn in serial_nos if sn not in reserved_serial_nos[row.so_detail]
|
||||
]:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Serial No {1} is not reserved against Sales Order {2}. Deliver only the Serial Nos produced and reserved for it."
|
||||
).format(
|
||||
row.idx, frappe.bold(", ".join(invalid_serial_nos)), frappe.bold(row.get(so_field))
|
||||
),
|
||||
title=_("Serial No Not Reserved"),
|
||||
)
|
||||
|
||||
def update_stock_reservation_entries(self) -> None:
|
||||
"""Updates Delivered Qty in Stock Reservation Entries."""
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ erpnext.utils.CRMActivities = class CRMActivities {
|
||||
refresh() {
|
||||
var me = this;
|
||||
$(this.open_activities_wrapper).empty();
|
||||
|
||||
// an unsaved doc has no activities and its temp name can't be permission-checked
|
||||
if (this.frm.is_new()) return;
|
||||
|
||||
let cur_form_footer = this.form_wrapper.find(".form-footer");
|
||||
|
||||
// all activities
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from unittest import TestCase
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import change_settings
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
@@ -102,6 +103,106 @@ class TestUaeVat201(TestCase):
|
||||
self.assertEqual(get_standard_rated_expenses_total(filters), 917.5)
|
||||
self.assertEqual(get_standard_rated_expenses_tax(filters), 50)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": True}
|
||||
)
|
||||
def test_uae_vat_201_sales_vat_in_foreign_currency(self):
|
||||
"""VAT on a foreign currency invoice must be reported in company currency."""
|
||||
si = create_sales_invoice(
|
||||
company="_Test Company UAE VAT",
|
||||
customer="_Test UAE Customer",
|
||||
currency="USD",
|
||||
conversion_rate=3.67,
|
||||
rate=1000,
|
||||
qty=1,
|
||||
warehouse="Finished Goods - _TCUV",
|
||||
debit_to="Debtors - _TCUV",
|
||||
income_account="Sales - _TCUV",
|
||||
expense_account="Cost of Goods Sold - _TCUV",
|
||||
cost_center="Main - _TCUV",
|
||||
item="_Test UAE VAT Item",
|
||||
do_not_save=1,
|
||||
)
|
||||
si.vat_emirate = "Dubai"
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "VAT 5% - _TCUV",
|
||||
"cost_center": "Main - _TCUV",
|
||||
"description": "VAT 5% @ 5.0",
|
||||
"rate": 5.0,
|
||||
},
|
||||
)
|
||||
si.submit()
|
||||
|
||||
filters = {"company": "_Test Company UAE VAT"}
|
||||
amounts_by_emirate = dict(
|
||||
(emirate, (amount, vat)) for emirate, amount, vat in get_total_emiratewise(filters)
|
||||
)
|
||||
amount, vat = amounts_by_emirate["Dubai"]
|
||||
|
||||
self.assertEqual(amount, 3670)
|
||||
self.assertEqual(vat, 183.5)
|
||||
self.assertEqual(vat, si.taxes[0].base_tax_amount_after_discount_amount)
|
||||
self.assertNotEqual(vat, si.items[0].tax_amount)
|
||||
|
||||
def test_uae_vat_201_mixed_invoice_excludes_exempt_and_zero_rated_vat(self):
|
||||
si = create_sales_invoice(
|
||||
company="_Test Company UAE VAT",
|
||||
customer="_Test UAE Customer",
|
||||
currency="AED",
|
||||
rate=100,
|
||||
qty=1,
|
||||
warehouse="Finished Goods - _TCUV",
|
||||
debit_to="Debtors - _TCUV",
|
||||
income_account="Sales - _TCUV",
|
||||
expense_account="Cost of Goods Sold - _TCUV",
|
||||
cost_center="Main - _TCUV",
|
||||
item="_Test UAE VAT Item",
|
||||
do_not_save=1,
|
||||
)
|
||||
si.vat_emirate = "Ajman"
|
||||
for item_code in ("_Test UAE VAT Zero Rated Item", "_Test UAE VAT Exempt Item"):
|
||||
si.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": item_code,
|
||||
"qty": 1,
|
||||
"rate": 100,
|
||||
"warehouse": "Finished Goods - _TCUV",
|
||||
"income_account": "Sales - _TCUV",
|
||||
"expense_account": "Cost of Goods Sold - _TCUV",
|
||||
"cost_center": "Main - _TCUV",
|
||||
},
|
||||
)
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "VAT 5% - _TCUV",
|
||||
"cost_center": "Main - _TCUV",
|
||||
"description": "VAT 5% @ 5.0",
|
||||
"rate": 5.0,
|
||||
},
|
||||
)
|
||||
si.submit()
|
||||
|
||||
# the single On Net Total row taxes all three items, so the invoice level figure is 15
|
||||
self.assertEqual(si.taxes[0].base_tax_amount_after_discount_amount, 15)
|
||||
|
||||
filters = {"company": "_Test Company UAE VAT"}
|
||||
amounts_by_emirate = dict(
|
||||
(emirate, (amount, vat)) for emirate, amount, vat in get_total_emiratewise(filters)
|
||||
)
|
||||
amount, vat = amounts_by_emirate["Ajman"]
|
||||
|
||||
# only the standard rated row belongs in box 1
|
||||
self.assertEqual(amount, 100)
|
||||
self.assertEqual(vat, 5)
|
||||
self.assertEqual(get_zero_rated_total(filters), 100)
|
||||
self.assertEqual(get_exempt_total(filters), 100)
|
||||
|
||||
|
||||
def make_company(company_name, abbr):
|
||||
if not frappe.db.exists("Company", company_name):
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import json
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext import get_region
|
||||
|
||||
@@ -144,26 +149,79 @@ def append_data(data, no, legend, amount, vat_amount):
|
||||
|
||||
def get_total_emiratewise(filters):
|
||||
"""Returns Emiratewise Amount and Taxes."""
|
||||
conditions = get_conditions(filters)
|
||||
try:
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
s.vat_emirate as emirate, sum(i.base_net_amount) as total, sum(i.tax_amount)
|
||||
from
|
||||
`tabSales Invoice Item` i inner join `tabSales Invoice` s
|
||||
on
|
||||
i.parent = s.name
|
||||
where
|
||||
s.docstatus = 1 and i.is_exempt != 1 and i.is_zero_rated != 1
|
||||
{conditions}
|
||||
group by
|
||||
s.vat_emirate;
|
||||
""",
|
||||
filters,
|
||||
amounts = get_emiratewise_standard_rated_amount(filters)
|
||||
vat_amounts = get_emiratewise_vat_amount(filters)
|
||||
return [
|
||||
(emirate, amounts.get(emirate, 0), vat_amounts.get(emirate, 0))
|
||||
for emirate in dict.fromkeys([*amounts, *vat_amounts])
|
||||
]
|
||||
|
||||
|
||||
def get_emiratewise_standard_rated_amount(filters):
|
||||
"""Returns emiratewise net amount of standard rated supplies in company currency."""
|
||||
i = frappe.qb.DocType("Sales Invoice Item")
|
||||
s = frappe.qb.DocType("Sales Invoice")
|
||||
query = (
|
||||
frappe.qb.from_(i)
|
||||
.inner_join(s)
|
||||
.on(i.parent == s.name)
|
||||
.select(s.vat_emirate, Sum(i.base_net_amount))
|
||||
.where((s.docstatus == 1) & (i.is_exempt != 1) & (i.is_zero_rated != 1))
|
||||
.groupby(s.vat_emirate)
|
||||
)
|
||||
for condition in get_sales_conditions(filters, s):
|
||||
query = query.where(condition)
|
||||
return dict(query.run())
|
||||
|
||||
|
||||
def get_emiratewise_vat_amount(filters):
|
||||
"""Returns emiratewise VAT on standard rated supplies in company currency.
|
||||
|
||||
Sales Taxes and Charges.item_wise_tax_detail stores each item's share of the tax
|
||||
row in company currency, so it keeps the item level exempt / zero rated split.
|
||||
"""
|
||||
i = frappe.qb.DocType("Sales Invoice Item")
|
||||
s = frappe.qb.DocType("Sales Invoice")
|
||||
t = frappe.qb.DocType("Sales Taxes and Charges")
|
||||
uae_vat = frappe.qb.DocType("UAE VAT Account")
|
||||
|
||||
standard_rated_items_query = (
|
||||
frappe.qb.from_(i)
|
||||
.inner_join(s)
|
||||
.on(i.parent == s.name)
|
||||
.select(s.name, i.item_code)
|
||||
.where((s.docstatus == 1) & (i.is_exempt != 1) & (i.is_zero_rated != 1))
|
||||
)
|
||||
for condition in get_sales_conditions(filters, s):
|
||||
standard_rated_items_query = standard_rated_items_query.where(condition)
|
||||
|
||||
standard_rated_items = defaultdict(set)
|
||||
for invoice, item_code in standard_rated_items_query.run():
|
||||
standard_rated_items[invoice].add(item_code)
|
||||
|
||||
vat_query = (
|
||||
frappe.qb.from_(t)
|
||||
.inner_join(s)
|
||||
.on(t.parent == s.name)
|
||||
.select(s.name, s.vat_emirate, t.item_wise_tax_detail)
|
||||
.where(
|
||||
(s.docstatus == 1)
|
||||
& t.account_head.isin(
|
||||
frappe.qb.from_(uae_vat)
|
||||
.select(uae_vat.account)
|
||||
.where(uae_vat.parent == filters.get("company"))
|
||||
)
|
||||
)
|
||||
except (IndexError, TypeError):
|
||||
return 0
|
||||
)
|
||||
for condition in get_sales_conditions(filters, s):
|
||||
vat_query = vat_query.where(condition)
|
||||
|
||||
vat_amounts = defaultdict(float)
|
||||
for invoice, emirate, item_wise_tax_detail in vat_query.run():
|
||||
for item_code, tax_detail in json.loads(item_wise_tax_detail or "{}").items():
|
||||
if item_code in standard_rated_items[invoice]:
|
||||
vat_amounts[emirate] += flt(tax_detail[1])
|
||||
return vat_amounts
|
||||
|
||||
|
||||
def get_emirates():
|
||||
@@ -411,3 +469,15 @@ def get_conditions(filters):
|
||||
if filters.get(opts[0]):
|
||||
conditions += opts[1]
|
||||
return conditions
|
||||
|
||||
|
||||
def get_sales_conditions(filters, sales_invoice):
|
||||
"""Return Query Builder conditions for Sales Invoice report filters."""
|
||||
conditions = []
|
||||
if filters.get("company"):
|
||||
conditions.append(sales_invoice.company == filters.get("company"))
|
||||
if filters.get("from_date"):
|
||||
conditions.append(sales_invoice.posting_date >= filters.get("from_date"))
|
||||
if filters.get("to_date"):
|
||||
conditions.append(sales_invoice.posting_date <= filters.get("to_date"))
|
||||
return conditions
|
||||
|
||||
@@ -144,6 +144,11 @@ frappe.ui.form.on("Sales Order", {
|
||||
frm.set_df_property("reserve_stock", "read_only", 1);
|
||||
frm.set_df_property("reserve_stock", "hidden", 1);
|
||||
frm.fields_dict.items.grid.update_docfield_property("reserve_stock", "hidden", 1);
|
||||
frm.fields_dict.items.grid.update_docfield_property(
|
||||
"ensure_delivery_based_on_produced_serial_no",
|
||||
"hidden",
|
||||
1
|
||||
);
|
||||
frm.fields_dict.items.grid.update_docfield_property(
|
||||
"reserve_stock",
|
||||
"default",
|
||||
|
||||
@@ -759,6 +759,9 @@ class SalesOrder(SellingController):
|
||||
if item.reserve_stock and (not enable_stock_reservation or not cint(item.is_stock_item)):
|
||||
item.reserve_stock = 0
|
||||
|
||||
if item.ensure_delivery_based_on_produced_serial_no and not enable_stock_reservation:
|
||||
item.ensure_delivery_based_on_produced_serial_no = 0
|
||||
|
||||
def has_unreserved_stock(self) -> bool:
|
||||
"""Returns True if there is any unreserved item in the Sales Order."""
|
||||
|
||||
|
||||
@@ -507,6 +507,7 @@ class DeliveryNote(SellingController):
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
# because updating reserved qty in bin depends upon updated delivered qty in SO
|
||||
self.update_stock_ledger()
|
||||
self.validate_produced_serial_nos_against_reservation()
|
||||
self.make_gl_entries()
|
||||
self.repost_future_sle_and_gle()
|
||||
|
||||
|
||||
@@ -3111,6 +3111,74 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
dn.items[0].stock_qty = 2
|
||||
dn.save()
|
||||
|
||||
@change_settings("Stock Settings", {"enable_stock_reservation": 1})
|
||||
def test_delivery_restricted_to_reserved_produced_serial_nos(self):
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
|
||||
from erpnext.stock.serial_batch_bundle import get_serial_nos_from_bundle
|
||||
|
||||
so, reserved, unreserved = make_so_with_reserved_produced_serial_no()
|
||||
|
||||
frappe.db.savepoint("unreserved_serial_no")
|
||||
dn = make_delivery_note(so.name)
|
||||
dn.items[0].use_serial_batch_fields = 1
|
||||
dn.items[0].serial_no = unreserved[0]
|
||||
dn.save()
|
||||
self.assertRaises(frappe.ValidationError, dn.submit)
|
||||
frappe.db.rollback(save_point="unreserved_serial_no")
|
||||
|
||||
dn = make_delivery_note(so.name)
|
||||
dn.items[0].use_serial_batch_fields = 1
|
||||
dn.items[0].serial_no = reserved[0]
|
||||
dn.save()
|
||||
dn.submit()
|
||||
self.assertEqual(get_serial_nos_from_bundle(dn.items[0].serial_and_batch_bundle), reserved)
|
||||
|
||||
@change_settings("Stock Settings", {"enable_stock_reservation": 0})
|
||||
def test_ensure_delivery_by_serial_no_cleared_without_stock_reservation(self):
|
||||
item_code = make_item("Test Ensure Serial Without SRE", {"is_stock_item": 1, "has_serial_no": 1}).name
|
||||
|
||||
so = make_sales_order(item_code=item_code, qty=1, do_not_save=True)
|
||||
so.items[0].ensure_delivery_based_on_produced_serial_no = 1
|
||||
so.save()
|
||||
|
||||
self.assertEqual(so.items[0].ensure_delivery_based_on_produced_serial_no, 0)
|
||||
|
||||
|
||||
def make_so_with_reserved_produced_serial_no():
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||
get_sre_reserved_serial_nos_for_voucher_detail_nos,
|
||||
)
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
fg_item = make_item(
|
||||
"Test Produced Serial FG", {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "TPSFG-.####"}
|
||||
).name
|
||||
rm_item = make_item("Test Produced Serial RM", {"is_stock_item": 1}).name
|
||||
make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse=warehouse)
|
||||
make_stock_entry(item_code=fg_item, target=warehouse, qty=2, basic_rate=100)
|
||||
|
||||
so = make_sales_order(item_code=fg_item, qty=1, warehouse=warehouse, do_not_submit=True)
|
||||
so.items[0].ensure_delivery_based_on_produced_serial_no = 1
|
||||
so.items[0].reserve_stock = 1
|
||||
so.submit()
|
||||
so.create_stock_reservation_entries(
|
||||
items_details=[{"sales_order_item": so.items[0].name, "warehouse": warehouse, "qty_to_reserve": 1}]
|
||||
)
|
||||
|
||||
reserved = sorted(
|
||||
get_sre_reserved_serial_nos_for_voucher_detail_nos("Sales Order", [so.items[0].name])[
|
||||
so.items[0].name
|
||||
]
|
||||
)
|
||||
unreserved = frappe.get_all(
|
||||
"Serial No",
|
||||
filters={"item_code": fg_item, "status": "Active", "name": ("not in", reserved)},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
return so, reserved, unreserved
|
||||
|
||||
|
||||
def create_delivery_note(**args):
|
||||
dn = frappe.new_doc("Delivery Note")
|
||||
|
||||
@@ -770,6 +770,33 @@ def get_sre_reserved_serial_nos_details(
|
||||
return frappe._dict(query.run())
|
||||
|
||||
|
||||
def get_sre_reserved_serial_nos_for_voucher_detail_nos(voucher_type: str, voucher_detail_nos: list) -> dict:
|
||||
"""Returns {voucher_detail_no: set of reserved Serial Nos}, including the delivered ones."""
|
||||
|
||||
sre = frappe.qb.DocType("Stock Reservation Entry")
|
||||
sb_entry = frappe.qb.DocType("Serial and Batch Entry")
|
||||
query = (
|
||||
frappe.qb.from_(sre)
|
||||
.inner_join(sb_entry)
|
||||
.on(sre.name == sb_entry.parent)
|
||||
.select(sre.voucher_detail_no, sb_entry.serial_no)
|
||||
.distinct()
|
||||
.where(
|
||||
(sre.docstatus == 1)
|
||||
& (sre.voucher_type == voucher_type)
|
||||
& (sre.voucher_detail_no.isin(voucher_detail_nos))
|
||||
& (sre.reservation_based_on == "Serial and Batch")
|
||||
& (sb_entry.serial_no.isnotnull())
|
||||
)
|
||||
)
|
||||
|
||||
reserved_serial_nos = {}
|
||||
for voucher_detail_no, serial_no in query.run():
|
||||
reserved_serial_nos.setdefault(voucher_detail_no, set()).add(serial_no)
|
||||
|
||||
return reserved_serial_nos
|
||||
|
||||
|
||||
def get_sre_reserved_batch_nos_details(
|
||||
item_code: str, warehouse: str, batch_nos: list | None = None, ignore_voucher_nos: list | None = None
|
||||
) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user