feat: serial and batch bundle for POS
This commit is contained in:
parent
467046436b
commit
0eaf6de5de
@ -3,7 +3,7 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, bold
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
|
||||
|
||||
@ -16,12 +16,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
update_multi_mode_option,
|
||||
)
|
||||
from erpnext.accounts.party import get_due_date, get_party_account
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty
|
||||
from erpnext.stock.doctype.serial_no.serial_no import (
|
||||
get_delivered_serial_nos,
|
||||
get_pos_reserved_serial_nos,
|
||||
get_serial_nos,
|
||||
)
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
|
||||
class POSInvoice(SalesInvoice):
|
||||
@ -71,6 +66,7 @@ class POSInvoice(SalesInvoice):
|
||||
self.apply_loyalty_points()
|
||||
self.check_phone_payments()
|
||||
self.set_status(update=True)
|
||||
self.submit_serial_batch_bundle()
|
||||
|
||||
if self.coupon_code:
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count
|
||||
@ -112,6 +108,14 @@ class POSInvoice(SalesInvoice):
|
||||
|
||||
update_coupon_code_count(self.coupon_code, "cancelled")
|
||||
|
||||
def submit_serial_batch_bundle(self):
|
||||
for item in self.items:
|
||||
if item.serial_and_batch_bundle:
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
|
||||
|
||||
if doc.docstatus == 0:
|
||||
doc.submit()
|
||||
|
||||
def check_phone_payments(self):
|
||||
for pay in self.payments:
|
||||
if pay.type == "Phone" and pay.amount >= 0:
|
||||
@ -129,88 +133,6 @@ class POSInvoice(SalesInvoice):
|
||||
if paid_amt and pay.amount != paid_amt:
|
||||
return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment))
|
||||
|
||||
def validate_pos_reserved_serial_nos(self, item):
|
||||
serial_nos = get_serial_nos(item.serial_no)
|
||||
filters = {"item_code": item.item_code, "warehouse": item.warehouse}
|
||||
if item.batch_no:
|
||||
filters["batch_no"] = item.batch_no
|
||||
|
||||
reserved_serial_nos = get_pos_reserved_serial_nos(filters)
|
||||
invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos]
|
||||
|
||||
bold_invalid_serial_nos = frappe.bold(", ".join(invalid_serial_nos))
|
||||
if len(invalid_serial_nos) == 1:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no."
|
||||
).format(item.idx, bold_invalid_serial_nos),
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
elif invalid_serial_nos:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{}: Serial Nos. {} have already been transacted into another POS Invoice. Please select valid serial no."
|
||||
).format(item.idx, bold_invalid_serial_nos),
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
|
||||
def validate_pos_reserved_batch_qty(self, item):
|
||||
filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no": item.batch_no}
|
||||
|
||||
available_batch_qty = get_batch_qty(item.batch_no, item.warehouse, item.item_code)
|
||||
reserved_batch_qty = get_pos_reserved_batch_qty(filters)
|
||||
|
||||
bold_item_name = frappe.bold(item.item_name)
|
||||
bold_extra_batch_qty_needed = frappe.bold(
|
||||
abs(available_batch_qty - reserved_batch_qty - item.stock_qty)
|
||||
)
|
||||
bold_invalid_batch_no = frappe.bold(item.batch_no)
|
||||
|
||||
if (available_batch_qty - reserved_batch_qty) == 0:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no."
|
||||
).format(item.idx, bold_invalid_batch_no, bold_item_name),
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
elif (available_batch_qty - reserved_batch_qty - item.stock_qty) < 0:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{}: Batch No. {} of item {} has less than required stock available, {} more required"
|
||||
).format(
|
||||
item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed
|
||||
),
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
|
||||
def validate_delivered_serial_nos(self, item):
|
||||
delivered_serial_nos = get_delivered_serial_nos(item.serial_no)
|
||||
|
||||
if delivered_serial_nos:
|
||||
bold_delivered_serial_nos = frappe.bold(", ".join(delivered_serial_nos))
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no."
|
||||
).format(item.idx, bold_delivered_serial_nos),
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
|
||||
def validate_invalid_serial_nos(self, item):
|
||||
serial_nos = get_serial_nos(item.serial_no)
|
||||
error_msg = []
|
||||
invalid_serials, msg = "", ""
|
||||
for serial_no in serial_nos:
|
||||
if not frappe.db.exists("Serial No", serial_no):
|
||||
invalid_serials = invalid_serials + (", " if invalid_serials else "") + serial_no
|
||||
msg = _("Row #{}: Following Serial numbers for item {} are <b>Invalid</b>: {}").format(
|
||||
item.idx, frappe.bold(item.get("item_code")), frappe.bold(invalid_serials)
|
||||
)
|
||||
if invalid_serials:
|
||||
error_msg.append(msg)
|
||||
|
||||
if error_msg:
|
||||
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
|
||||
|
||||
def validate_stock_availablility(self):
|
||||
if self.is_return:
|
||||
return
|
||||
@ -223,13 +145,7 @@ class POSInvoice(SalesInvoice):
|
||||
from erpnext.stock.stock_ledger import is_negative_stock_allowed
|
||||
|
||||
for d in self.get("items"):
|
||||
if d.serial_no:
|
||||
self.validate_pos_reserved_serial_nos(d)
|
||||
self.validate_delivered_serial_nos(d)
|
||||
self.validate_invalid_serial_nos(d)
|
||||
elif d.batch_no:
|
||||
self.validate_pos_reserved_batch_qty(d)
|
||||
else:
|
||||
if not d.serial_and_batch_bundle:
|
||||
if is_negative_stock_allowed(item_code=d.item_code):
|
||||
return
|
||||
|
||||
@ -258,36 +174,15 @@ class POSInvoice(SalesInvoice):
|
||||
def validate_serialised_or_batched_item(self):
|
||||
error_msg = []
|
||||
for d in self.get("items"):
|
||||
serialized = d.get("has_serial_no")
|
||||
batched = d.get("has_batch_no")
|
||||
no_serial_selected = not d.get("serial_no")
|
||||
no_batch_selected = not d.get("batch_no")
|
||||
error_msg = ""
|
||||
if d.get("has_serial_no") and not d.serial_and_batch_bundle:
|
||||
error_msg = f"Row #{d.idx}: Please select Serial No. for item {bold(d.item_code)}"
|
||||
|
||||
msg = ""
|
||||
item_code = frappe.bold(d.item_code)
|
||||
serial_nos = get_serial_nos(d.serial_no)
|
||||
if serialized and batched and (no_batch_selected or no_serial_selected):
|
||||
msg = _(
|
||||
"Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction."
|
||||
).format(d.idx, item_code)
|
||||
elif serialized and no_serial_selected:
|
||||
msg = _(
|
||||
"Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction."
|
||||
).format(d.idx, item_code)
|
||||
elif batched and no_batch_selected:
|
||||
msg = _(
|
||||
"Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction."
|
||||
).format(d.idx, item_code)
|
||||
elif serialized and not no_serial_selected and len(serial_nos) != d.qty:
|
||||
msg = _("Row #{}: You must select {} serial numbers for item {}.").format(
|
||||
d.idx, frappe.bold(cint(d.qty)), item_code
|
||||
)
|
||||
|
||||
if msg:
|
||||
error_msg.append(msg)
|
||||
elif d.get("has_batch_no") and not d.serial_and_batch_bundle:
|
||||
error_msg = f"Row #{d.idx}: Please select Batch No. for item {bold(d.item_code)}"
|
||||
|
||||
if error_msg:
|
||||
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
|
||||
frappe.throw(error_msg, title=_("Serial / Batch Bundle Missing"), as_list=True)
|
||||
|
||||
def validate_return_items_qty(self):
|
||||
if not self.get("is_return"):
|
||||
@ -652,7 +547,7 @@ def get_bundle_availability(bundle_item_code, warehouse):
|
||||
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
|
||||
available_qty = item_bin_qty - item_pos_reserved_qty
|
||||
|
||||
max_available_bundles = available_qty / item.stock_qty
|
||||
max_available_bundles = available_qty / item.qty
|
||||
if bundle_bin_qty > max_available_bundles and frappe.get_value(
|
||||
"Item", item.item_code, "is_stock_item"
|
||||
):
|
||||
|
@ -184,6 +184,8 @@ class POSInvoiceMergeLog(Document):
|
||||
item.base_amount = item.base_net_amount
|
||||
item.price_list_rate = 0
|
||||
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
|
||||
if item.serial_and_batch_bundle:
|
||||
si_item.serial_and_batch_bundle = item.serial_and_batch_bundle
|
||||
items.append(si_item)
|
||||
|
||||
for tax in doc.get("taxes"):
|
||||
|
@ -408,6 +408,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
|
||||
{
|
||||
"type_of_transaction": type_of_transaction,
|
||||
"serial_and_batch_bundle": source_doc.serial_and_batch_bundle,
|
||||
"returned_against": source_doc.name,
|
||||
}
|
||||
)
|
||||
|
||||
@ -429,6 +430,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
|
||||
{
|
||||
"type_of_transaction": type_of_transaction,
|
||||
"serial_and_batch_bundle": source_doc.rejected_serial_and_batch_bundle,
|
||||
"returned_against": source_doc.name,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -44,7 +44,8 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
<div class="item-image"></div>
|
||||
</div>
|
||||
<div class="discount-section"></div>
|
||||
<div class="form-container"></div>`
|
||||
<div class="form-container"></div>
|
||||
<div class="serial-batch-container"></div>`
|
||||
)
|
||||
|
||||
this.$item_name = this.$component.find('.item-name');
|
||||
@ -53,6 +54,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
this.$item_image = this.$component.find('.item-image');
|
||||
this.$form_container = this.$component.find('.form-container');
|
||||
this.$dicount_section = this.$component.find('.discount-section');
|
||||
this.$serial_batch_container = this.$component.find('.serial-batch-container');
|
||||
}
|
||||
|
||||
compare_with_current_item(item) {
|
||||
@ -101,12 +103,9 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
|
||||
const serialized = item_row.has_serial_no;
|
||||
const batched = item_row.has_batch_no;
|
||||
const no_serial_selected = !item_row.serial_no;
|
||||
const no_batch_selected = !item_row.batch_no;
|
||||
|
||||
if ((serialized && no_serial_selected) || (batched && no_batch_selected) ||
|
||||
(serialized && batched && (no_batch_selected || no_serial_selected))) {
|
||||
const no_bundle_selected = !item_row.serial_and_batch_bundle;
|
||||
|
||||
if ((serialized && no_bundle_selected) || (batched && no_bundle_selected)) {
|
||||
frappe.show_alert({
|
||||
message: __("Item is removed since no serial / batch no selected."),
|
||||
indicator: 'orange'
|
||||
@ -200,13 +199,8 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
}
|
||||
|
||||
make_auto_serial_selection_btn(item) {
|
||||
if (item.has_serial_no) {
|
||||
if (!item.has_batch_no) {
|
||||
this.$form_container.append(
|
||||
`<div class="grid-filler no-select"></div>`
|
||||
);
|
||||
}
|
||||
const label = __('Auto Fetch Serial Numbers');
|
||||
if (item.has_serial_no || item.has_batch_no) {
|
||||
const label = item.has_serial_no ? __('Select Serial No') : __('Select Batch No');
|
||||
this.$form_container.append(
|
||||
`<div class="btn btn-sm btn-secondary auto-fetch-btn">${label}</div>`
|
||||
);
|
||||
@ -382,40 +376,19 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
|
||||
bind_auto_serial_fetch_event() {
|
||||
this.$form_container.on('click', '.auto-fetch-btn', () => {
|
||||
this.batch_no_control && this.batch_no_control.set_value('');
|
||||
let qty = this.qty_control.get_value();
|
||||
let conversion_factor = this.conversion_factor_control.get_value();
|
||||
let expiry_date = this.item_row.has_batch_no ? this.events.get_frm().doc.posting_date : "";
|
||||
frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", () => {
|
||||
let frm = this.events.get_frm();
|
||||
let item_row = this.item_row;
|
||||
item_row.outward = 1;
|
||||
item_row.type_of_transaction = "Outward";
|
||||
|
||||
let numbers = frappe.call({
|
||||
method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number",
|
||||
args: {
|
||||
qty: qty * conversion_factor,
|
||||
item_code: this.current_item.item_code,
|
||||
warehouse: this.warehouse_control.get_value() || '',
|
||||
batch_nos: this.current_item.batch_no || '',
|
||||
posting_date: expiry_date,
|
||||
for_doctype: 'POS Invoice'
|
||||
new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => {
|
||||
if (r) {
|
||||
frm.refresh_fields();
|
||||
frappe.model.set_value(item_row.doctype, item_row.name,
|
||||
"serial_and_batch_bundle", r.name);
|
||||
}
|
||||
});
|
||||
|
||||
numbers.then((data) => {
|
||||
let auto_fetched_serial_numbers = data.message;
|
||||
let records_length = auto_fetched_serial_numbers.length;
|
||||
if (!records_length) {
|
||||
const warehouse = this.warehouse_control.get_value().bold();
|
||||
const item_code = this.current_item.item_code.bold();
|
||||
frappe.msgprint(
|
||||
__('Serial numbers unavailable for Item {0} under warehouse {1}. Please try changing warehouse.', [item_code, warehouse])
|
||||
);
|
||||
} else if (records_length < qty) {
|
||||
frappe.msgprint(
|
||||
__('Fetched only {0} available serial numbers.', [records_length])
|
||||
);
|
||||
this.qty_control.set_value(records_length);
|
||||
}
|
||||
numbers = auto_fetched_serial_numbers.join(`\n`);
|
||||
this.serial_no_control.set_value(numbers);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
@ -31,6 +31,7 @@
|
||||
"column_break_aouy",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"returned_against",
|
||||
"section_break_wzou",
|
||||
"is_cancelled",
|
||||
"is_rejected",
|
||||
@ -232,12 +233,18 @@
|
||||
"fieldtype": "Table",
|
||||
"options": "Serial and Batch Entry",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "returned_against",
|
||||
"fieldtype": "Data",
|
||||
"label": "Returned Against",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-22 18:56:37.035516",
|
||||
"modified": "2023-03-23 13:39:17.843812",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial and Batch Bundle",
|
||||
|
@ -2,6 +2,7 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import collections
|
||||
from collections import defaultdict
|
||||
from typing import Dict, List
|
||||
|
||||
import frappe
|
||||
@ -31,10 +32,10 @@ class SerialandBatchBundle(Document):
|
||||
self.check_future_entries_exists()
|
||||
self.validate_serial_nos_inventory()
|
||||
self.set_is_outward()
|
||||
self.validate_qty_and_stock_value_difference()
|
||||
self.calculate_qty_and_amount()
|
||||
self.set_warehouse()
|
||||
self.set_incoming_rate()
|
||||
self.validate_qty_and_stock_value_difference()
|
||||
|
||||
def validate_serial_nos_inventory(self):
|
||||
if not (self.has_serial_no and self.type_of_transaction == "Outward"):
|
||||
@ -100,7 +101,7 @@ class SerialandBatchBundle(Document):
|
||||
d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0))
|
||||
else:
|
||||
d.incoming_rate = abs(sn_obj.batch_avg_rate.get(d.batch_no))
|
||||
available_qty = flt(sn_obj.batch_available_qty.get(d.batch_no)) + flt(d.qty)
|
||||
available_qty = flt(sn_obj.available_qty.get(d.batch_no)) + flt(d.qty)
|
||||
|
||||
self.validate_negative_batch(d.batch_no, available_qty)
|
||||
|
||||
@ -417,6 +418,7 @@ class SerialandBatchBundle(Document):
|
||||
frappe.throw(_(msg))
|
||||
|
||||
def on_trash(self):
|
||||
self.validate_voucher_no_docstatus()
|
||||
self.delink_refernce_from_voucher()
|
||||
self.delink_reference_from_batch()
|
||||
self.clear_table()
|
||||
@ -439,23 +441,46 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_serial_batch_ledgers(item_code, voucher_no, name=None):
|
||||
def get_serial_batch_ledgers(item_code, docstatus=None, voucher_no=None, name=None):
|
||||
filters = get_filters_for_bundle(item_code, docstatus=docstatus, voucher_no=voucher_no, name=name)
|
||||
|
||||
return frappe.get_all(
|
||||
"Serial and Batch Bundle",
|
||||
fields=[
|
||||
"`tabSerial and Batch Entry`.`name`",
|
||||
"`tabSerial and Batch Bundle`.`name`",
|
||||
"`tabSerial and Batch Entry`.`qty`",
|
||||
"`tabSerial and Batch Entry`.`warehouse`",
|
||||
"`tabSerial and Batch Entry`.`batch_no`",
|
||||
"`tabSerial and Batch Entry`.`serial_no`",
|
||||
],
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
|
||||
def get_filters_for_bundle(item_code, docstatus=None, voucher_no=None, name=None):
|
||||
filters = [
|
||||
["Serial and Batch Bundle", "item_code", "=", item_code],
|
||||
["Serial and Batch Entry", "parent", "=", name],
|
||||
["Serial and Batch Bundle", "voucher_no", "=", voucher_no],
|
||||
["Serial and Batch Bundle", "docstatus", "!=", 2],
|
||||
],
|
||||
)
|
||||
["Serial and Batch Bundle", "is_cancelled", "=", 0],
|
||||
]
|
||||
|
||||
if not docstatus:
|
||||
docstatus = [0, 1]
|
||||
|
||||
if isinstance(docstatus, list):
|
||||
filters.append(["Serial and Batch Bundle", "docstatus", "in", docstatus])
|
||||
else:
|
||||
filters.append(["Serial and Batch Bundle", "docstatus", "=", docstatus])
|
||||
|
||||
if voucher_no:
|
||||
filters.append(["Serial and Batch Bundle", "voucher_no", "=", voucher_no])
|
||||
|
||||
if name:
|
||||
if isinstance(name, list):
|
||||
filters.append(["Serial and Batch Entry", "parent", "in", name])
|
||||
else:
|
||||
filters.append(["Serial and Batch Entry", "parent", "=", name])
|
||||
|
||||
return filters
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@ -603,15 +628,52 @@ def get_auto_serial_nos(kwargs):
|
||||
elif kwargs.based_on == "Expiry":
|
||||
order_by = "amc_expiry_date asc"
|
||||
|
||||
ignore_serial_nos = get_reserved_serial_nos_for_pos(kwargs)
|
||||
|
||||
return frappe.get_all(
|
||||
"Serial No",
|
||||
fields=fields,
|
||||
filters={"item_code": kwargs.item_code, "warehouse": kwargs.warehouse},
|
||||
filters={
|
||||
"item_code": kwargs.item_code,
|
||||
"warehouse": kwargs.warehouse,
|
||||
"name": ("not in", ignore_serial_nos),
|
||||
},
|
||||
limit=cint(kwargs.qty),
|
||||
order_by=order_by,
|
||||
)
|
||||
|
||||
|
||||
def get_reserved_serial_nos_for_pos(kwargs):
|
||||
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"],
|
||||
filters=[
|
||||
["POS Invoice", "consolidated_invoice", "is", "not set"],
|
||||
["POS Invoice", "docstatus", "=", 1],
|
||||
["POS Invoice Item", "item_code", "=", kwargs.item_code],
|
||||
],
|
||||
)
|
||||
|
||||
ids = [
|
||||
pos_invoice.serial_and_batch_bundle
|
||||
for pos_invoice in pos_invoices
|
||||
if pos_invoice.serial_and_batch_bundle
|
||||
]
|
||||
|
||||
for d in get_serial_batch_ledgers(ids, docstatus=1, name=ids):
|
||||
ignore_serial_nos.append(d.serial_no)
|
||||
|
||||
# Will be deprecated in v16
|
||||
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
|
||||
|
||||
|
||||
def get_auto_batch_nos(kwargs):
|
||||
available_batches = get_available_batches(kwargs)
|
||||
|
||||
@ -619,6 +681,10 @@ def get_auto_batch_nos(kwargs):
|
||||
|
||||
batches = []
|
||||
|
||||
reserved_batches = get_reserved_batches_for_pos(kwargs)
|
||||
if reserved_batches:
|
||||
remove_batches_reserved_for_pos(available_batches, reserved_batches)
|
||||
|
||||
for batch in available_batches:
|
||||
if qty > 0:
|
||||
batch_qty = flt(batch.qty)
|
||||
@ -642,6 +708,51 @@ def get_auto_batch_nos(kwargs):
|
||||
return batches
|
||||
|
||||
|
||||
def get_reserved_batches_for_pos(kwargs):
|
||||
reserved_batches = defaultdict(float)
|
||||
|
||||
pos_invoices = frappe.get_all(
|
||||
"POS Invoice",
|
||||
fields=[
|
||||
"`tabPOS Invoice Item`.batch_no",
|
||||
"`tabPOS Invoice Item`.qty",
|
||||
"`tabPOS Invoice Item`.serial_and_batch_bundle",
|
||||
],
|
||||
filters=[
|
||||
["POS Invoice", "consolidated_invoice", "is", "not set"],
|
||||
["POS Invoice", "docstatus", "=", 1],
|
||||
["POS Invoice Item", "item_code", "=", kwargs.item_code],
|
||||
],
|
||||
)
|
||||
|
||||
ids = [
|
||||
pos_invoice.serial_and_batch_bundle
|
||||
for pos_invoice in pos_invoices
|
||||
if pos_invoice.serial_and_batch_bundle
|
||||
]
|
||||
|
||||
for d in get_serial_batch_ledgers(ids, docstatus=1, name=ids):
|
||||
if not d.batch_no:
|
||||
continue
|
||||
|
||||
reserved_batches[d.batch_no] += flt(d.qty)
|
||||
|
||||
# Will be deprecated in v16
|
||||
for pos_invoice in pos_invoices:
|
||||
if not pos_invoice.batch_no:
|
||||
continue
|
||||
|
||||
reserved_batches[pos_invoice.batch_no] += flt(pos_invoice.qty)
|
||||
|
||||
return reserved_batches
|
||||
|
||||
|
||||
def remove_batches_reserved_for_pos(available_batches, reserved_batches):
|
||||
for batch in available_batches:
|
||||
if batch.batch_no in reserved_batches:
|
||||
available_batches[batch.batch_no] -= reserved_batches[batch.batch_no]
|
||||
|
||||
|
||||
def get_available_batches(kwargs):
|
||||
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
|
||||
batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
|
||||
@ -655,9 +766,7 @@ def get_available_batches(kwargs):
|
||||
.on(batch_ledger.batch_no == batch_table.name)
|
||||
.select(
|
||||
batch_ledger.batch_no,
|
||||
Sum(
|
||||
Case().when(stock_ledger_entry.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)
|
||||
).as_("qty"),
|
||||
Sum(batch_ledger.qty).as_("qty"),
|
||||
)
|
||||
.where(
|
||||
(stock_ledger_entry.item_code == kwargs.item_code)
|
||||
@ -699,7 +808,7 @@ def get_voucher_wise_serial_batch_from_bundle(**kwargs) -> Dict[str, Dict]:
|
||||
if key not in group_by_voucher:
|
||||
group_by_voucher.setdefault(
|
||||
key,
|
||||
frappe._dict({"serial_nos": [], "batch_nos": collections.defaultdict(float), "item_row": row}),
|
||||
frappe._dict({"serial_nos": [], "batch_nos": defaultdict(float), "item_row": row}),
|
||||
)
|
||||
|
||||
child_row = group_by_voucher[key]
|
||||
@ -771,7 +880,7 @@ def get_available_serial_nos(item_code, warehouse):
|
||||
|
||||
def get_available_batch_nos(item_code, warehouse):
|
||||
sl_entries = get_stock_ledger_entries(item_code, warehouse)
|
||||
batchwise_qty = collections.defaultdict(float)
|
||||
batchwise_qty = defaultdict(float)
|
||||
|
||||
precision = frappe.get_precision("Stock Ledger Entry", "qty")
|
||||
for entry in sl_entries:
|
||||
|
@ -131,7 +131,7 @@ def get_stock_ledger_entries_for_batch_bundle(filters):
|
||||
& (sle.has_batch_no == 1)
|
||||
& (sle.posting_date <= filters["to_date"])
|
||||
)
|
||||
.groupby(sle.voucher_no, sle.batch_no, sle.item_code, sle.warehouse)
|
||||
.groupby(batch_package.batch_no)
|
||||
.orderby(sle.item_code, sle.warehouse)
|
||||
)
|
||||
|
||||
|
@ -642,6 +642,8 @@ class SerialBatchCreation:
|
||||
package = frappe.get_doc("Serial and Batch Bundle", id)
|
||||
new_package = frappe.copy_doc(package)
|
||||
new_package.type_of_transaction = self.type_of_transaction
|
||||
new_package.returned_against = self.returned_against
|
||||
print(new_package.voucher_type, new_package.voucher_no)
|
||||
new_package.save()
|
||||
|
||||
self.serial_and_batch_bundle = new_package.name
|
||||
|
Loading…
x
Reference in New Issue
Block a user