");
+ const frm = _frm || new frappe.ui.form.Form(doctype, page, false);
+ const name = frappe.model.make_new_doc_and_get_name(doctype, true);
+ frm.refresh(name);
+
+ return frm;
+ }
+
+ async make_return_invoice(doc) {
+ frappe.dom.freeze();
+ this.frm = this.get_new_frm(this.frm);
+ this.frm.doc.items = [];
+ return frappe.call({
+ method: "posnext.posnext.page.posnext.point_of_sale.make_sales_return",
+ args: {
+ source_name: doc.name,
+ target_doc: this.frm.doc,
+ },
+ callback: (r) => {
+ // console.log(r.message)
+ frappe.model.sync(r.message);
+ frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers =
+ false;
+ this.set_pos_profile_data().then(() => {
+ frappe.dom.unfreeze();
+ });
+ },
+ });
+ }
+
+ set_pos_profile_data() {
+ if (this.company && !this.frm.doc.company)
+ this.frm.doc.company = this.company;
+ if (
+ (this.pos_profile && !this.frm.doc.pos_profile) |
+ (this.frm.doc.is_return && this.pos_profile != this.frm.doc.pos_profile)
+ ) {
+ this.frm.doc.pos_profile = this.pos_profile;
+ }
+
+ if (!this.frm.doc.company) return;
+
+ return this.frm.trigger("set_pos_data");
+ }
+
+ set_pos_profile_status() {
+ this.page.set_indicator(this.pos_profile, "blue");
+ }
+
+ async on_cart_update(args) {
+ // frappe.dom.freeze();
+ let item_row = undefined;
+ try {
+ let { field, value, item } = args;
+ item_row = this.get_item_from_frm(item);
+ const item_row_exists = !$.isEmptyObject(item_row);
+
+ const from_selector = field === "qty" && value === "+1";
+ if (from_selector) value = flt(item_row.stock_qty) + flt(value);
+
+ if (item_row_exists) {
+ if (field === "qty") value = flt(value);
+
+ if (
+ ["qty", "conversion_factor"].includes(field) &&
+ value > 0 &&
+ !this.allow_negative_stock
+ ) {
+ const qty_needed =
+ field === "qty"
+ ? value * item_row.conversion_factor
+ : item_row.qty * value;
+ // await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse);
+ }
+
+ if (this.is_current_item_being_edited(item_row) || from_selector) {
+ await frappe.model.set_value(
+ item_row.doctype,
+ item_row.name,
+ field,
+ value,
+ );
+ // this.update_cart_html(item_row);
+ }
+ } else {
+ if (
+ !this.frm.doc.customer &&
+ !this.settings.custom_mobile_number_based_customer
+ ) {
+ return this.raise_customer_selection_alert();
+ }
+ frappe.flags.ignore_company_party_validation = true;
+ const {
+ item_code,
+ batch_no,
+ serial_no,
+ rate,
+ uom,
+ valuation_rate,
+ custom_item_uoms,
+ custom_logical_rack,
+ } = item;
+ if (!item_code) return;
+
+ const new_item = { item_code, batch_no, rate, uom, [field]: value };
+ if (value) {
+ new_item["qty"] = value;
+ }
+ if (serial_no) {
+ await this.check_serial_no_availablilty(
+ item_code,
+ this.frm.doc.set_warehouse,
+ serial_no,
+ );
+ new_item["serial_no"] = serial_no;
+ }
+
+ if (field === "serial_no")
+ new_item["qty"] = value.split(`\n`).length || 0;
+ item_row = this.frm.add_child("items", new_item);
+
+ // if (field === 'qty' && value !== 0 && !this.allow_negative_stock) {
+ // const qty_needed = value * item_row.conversion_factor;
+ // await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse);
+ // }
+
+ await this.trigger_new_item_events(item_row);
+ item_row["rate"] = rate;
+ item_row["valuation_rate"] = valuation_rate;
+ item_row["custom_valuation_rate"] = valuation_rate;
+ item_row["custom_item_uoms"] = custom_item_uoms;
+ item_row["custom_logical_rack"] = custom_logical_rack;
+ // this.update_cart_html(item_row);
+ if (this.item_details.$component.is(":visible"))
+ this.edit_item_details_of(item_row);
+
+ if (
+ this.check_serial_batch_selection_needed(item_row) &&
+ !this.item_details.$component.is(":visible")
+ )
+ this.edit_item_details_of(item_row);
+ }
+ } catch (error) {
+ console.log(error);
+ } finally {
+ // frappe.dom.unfreeze();
+
+ var total_incoming_rate = 0;
+ this.frm.doc.items.forEach((item) => {
+ total_incoming_rate += parseFloat(item.valuation_rate) * item.qty;
+ });
+ this.item_selector.update_total_incoming_rate(total_incoming_rate);
+
+ return item_row; // eslint-disable-line no-unsafe-finally
+ }
+ }
+
+ raise_customer_selection_alert() {
+ frappe.dom.unfreeze();
+ frappe.show_alert({
+ message: __("You must select a customer before adding an item."),
+ indicator: "orange",
+ });
+ frappe.utils.play_sound("error");
+ }
+
+ get_item_from_frm({ name, item_code, batch_no, uom, rate }) {
+ let item_row = null;
+
+ if (name) {
+ item_row = this.frm.doc.items.find((i) => i.name == name);
+ } else {
+ // if item is clicked twice from item selector
+ // then "item_code, batch_no, uom, rate" will help in getting the exact item
+ // to increase the qty by one
+ const has_batch_no = batch_no !== "null" && batch_no !== null;
+ const batch_no_check = this.settings
+ .custom_allow_add_new_items_on_new_line
+ ? has_batch_no && cur_frm.doc.items[i].batch_no === batch_no // nosemgrep Overrides erpnext code
+ : true;
+ // prettier-ignore
+ for (var i = 0; i < cur_frm.doc.items.length; i += 1) { // nosemgrep
+ if (
+ cur_frm.doc.items[i].item_code === item_code && // nosemgrep Overrides erpnext code
+ cur_frm.doc.items[i].uom === uom && // nosemgrep Overrides erpnext code
+ parseFloat(cur_frm.doc.items[i].rate) === parseFloat(rate) // nosemgrep Overrides erpnext code
+ ) {
+ item_row = cur_frm.doc.items[i]; // nosemgrep Overrides erpnext code
+ break;
+ }
+ }
+ console.log(item_row);
+ }
+ return item_row || {};
+ }
+
+ edit_item_details_of(item_row) {
+ this.item_details.toggle_item_details_section(item_row);
+ }
+
+ is_current_item_being_edited(item_row) {
+ return item_row.name == this.item_details.current_item.name;
+ }
+
+ update_cart_html(item_row, remove_item) {
+ this.cart.update_item_html(item_row, remove_item);
+
+ this.cart.update_totals_section(this.frm);
+ }
+
+ check_serial_batch_selection_needed(item_row) {
+ // right now item details is shown for every type of item.
+ // if item details is not shown for every item then this fn will be needed
+ 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))
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ async trigger_new_item_events(item_row) {
+ await this.frm.script_manager.trigger(
+ "item_code",
+ item_row.doctype,
+ item_row.name,
+ );
+ await this.frm.script_manager.trigger(
+ "qty",
+ item_row.doctype,
+ item_row.name,
+ );
+ await this.frm.script_manager.trigger(
+ "discount_percentage",
+ item_row.doctype,
+ item_row.name,
+ );
+ }
+
+ async check_stock_availability(item_row, qty_needed, warehouse) {
+ const resp = (await this.get_available_stock(item_row.item_code, warehouse))
+ .message;
+ const available_qty = resp[0];
+ const is_stock_item = resp[1];
+
+ frappe.dom.unfreeze();
+ const bold_uom = item_row.uom.bold();
+ const bold_item_code = item_row.item_code.bold();
+ const bold_warehouse = warehouse.bold();
+ const bold_available_qty = available_qty.toString().bold();
+ if (!(available_qty > 0)) {
+ if (is_stock_item) {
+ frappe.model.clear_doc(item_row.doctype, item_row.name);
+ frappe.throw({
+ title: __("Not Available"),
+ message: __("Item Code: {0} is not available under warehouse {1}.", [
+ bold_item_code,
+ bold_warehouse,
+ ]),
+ });
+ } else {
+ return;
+ }
+ } else if (is_stock_item && available_qty < qty_needed) {
+ frappe.throw({
+ message: __(
+ "Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2} {3}.",
+ [bold_item_code, bold_warehouse, bold_available_qty, bold_uom],
+ ),
+ indicator: "orange",
+ });
+ frappe.utils.play_sound("error");
+ }
+ frappe.dom.freeze();
+ }
+
+ async check_serial_no_availablilty(item_code, warehouse, serial_no) {
+ const method =
+ "erpnext.stock.doctype.serial_no.serial_no.get_pos_reserved_serial_nos";
+ const args = { filters: { item_code, warehouse } };
+ const res = await frappe.call({ method, args });
+
+ if (res.message.includes(serial_no)) {
+ frappe.throw({
+ title: __("Not Available"),
+ message: __(
+ "Serial No: {0} has already been transacted into another Sales Invoice.",
+ [serial_no.bold()],
+ ),
+ });
+ }
+ }
+
+ get_available_stock(item_code, warehouse) {
+ const me = this;
+ return frappe.call({
+ method:
+ "erpnext.accounts.doctype.pos_invoice.pos_invoice.get_stock_availability",
+ args: {
+ item_code: item_code,
+ warehouse: warehouse,
+ },
+ callback(res) {
+ if (!me.item_stock_map[item_code]) me.item_stock_map[item_code] = {};
+ me.item_stock_map[item_code][warehouse] = res.message;
+ },
+ });
+ }
+
+ update_item_field(value, field_or_action) {
+ if (field_or_action === "checkout") {
+ this.item_details.toggle_item_details_section(null);
+ } else if (field_or_action === "remove") {
+ this.remove_item_from_cart();
+ } else {
+ const field_control = this.item_details[`${field_or_action}_control`];
+ if (!field_control) return;
+ field_control.set_focus();
+ value != "" && field_control.set_value(value);
+ }
+ }
+
+ remove_item_from_cart() {
+ frappe.dom.freeze();
+ const { doctype, name, current_item } = this.item_details;
+ return frappe.model
+ .set_value(doctype, name, "qty", 0)
+ .then(() => {
+ frappe.model.clear_doc(doctype, name);
+ this.update_cart_html(current_item, true);
+ this.item_details.toggle_item_details_section(null);
+ frappe.dom.unfreeze();
+
+ var total_incoming_rate = 0;
+ this.frm.doc.items.forEach((item) => {
+ total_incoming_rate += parseFloat(item.valuation_rate) * item.qty;
+ });
+ this.item_selector.update_total_incoming_rate(total_incoming_rate);
+ })
+ .catch((e) => console.log(e));
+ }
+
+ async save_and_checkout() {
+ if (this.frm.is_dirty()) {
+ const div = document.getElementById("customer-cart-container2");
+ div.style.gridColumn = "";
+ let save_error = false;
+ await this.frm.save(null, null, null, () => (save_error = true));
+ // only move to payment section if save is successful
+ !save_error && this.payment.checkout();
+ // show checkout button on error
+ save_error &&
+ setTimeout(() => {
+ this.cart.toggle_checkout_btn(true);
+ }, 300); // wait for save to finish
+ } else {
+ this.payment.checkout();
+ }
+ }
};
diff --git a/posnext/.gitignore b/posnext/.gitignore
new file mode 100644
index 0000000..64f3937
--- /dev/null
+++ b/posnext/.gitignore
@@ -0,0 +1,12 @@
+# Ignore vitepress dist files
+www/posnext_*.py
+www/404.py
+www/posnext_*.html
+www/404.html
+www/hashmap.json
+www/vp-icons.css
+www/assets
+public/app.*.js
+public/posnext_*.js
+public/style.*.css
+public/chunks
\ No newline at end of file
diff --git a/posnext/__init__.py b/posnext/__init__.py
index f102a9c..3dc1f76 100644
--- a/posnext/__init__.py
+++ b/posnext/__init__.py
@@ -1 +1 @@
-__version__ = "0.0.1"
+__version__ = "0.1.0"
diff --git a/posnext/controllers/queries.py b/posnext/controllers/queries.py
index a7a810b..24ea2fd 100644
--- a/posnext/controllers/queries.py
+++ b/posnext/controllers/queries.py
@@ -1,24 +1,25 @@
import frappe
-from frappe.utils import nowdate, unique
from frappe.desk.reportview import get_filters_cond, get_match_cond
+from frappe.utils import unique
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def customer_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
- doctype = "Customer"
- conditions = []
- cust_master_name = frappe.defaults.get_user_default("cust_master_name")
+ doctype = "Customer"
+ conditions = []
+ cust_master_name = frappe.defaults.get_user_default("cust_master_name")
- fields = ["name"]
- if cust_master_name != "Customer Name":
- fields.append("customer_name")
+ fields = ["name"]
+ if cust_master_name != "Customer Name":
+ fields.append("customer_name")
- fields = get_fields(doctype, fields)
- searchfields = frappe.get_meta(doctype).get_search_fields()
- searchfields = " or ".join(field + " like %(txt)s" for field in searchfields)
+ fields = get_fields(doctype, fields)
+ searchfields = frappe.get_meta(doctype).get_search_fields()
+ searchfields = " or ".join(field + " like %(txt)s" for field in searchfields)
- return frappe.db.sql(
- """select {fields} from `tabCustomer`
+ return frappe.db.sql(
+ """select {fields} from `tabCustomer`
where docstatus < 2
and ({scond}) and disabled=0
{fcond} {mcond}
@@ -28,24 +29,32 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters, as_dict=
idx desc,
name, customer_name
limit %(page_len)s offset %(start)s""".format(
- **{
- "fields": ", ".join(fields),
- "scond": searchfields,
- "mcond": get_match_cond(doctype),
- "fcond": get_filters_cond(doctype, filters, conditions).replace("%", "%%"),
- }
- ),
- {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
- as_dict=as_dict,
- )
+ **{
+ "fields": ", ".join(fields),
+ "scond": searchfields,
+ "mcond": get_match_cond(doctype),
+ "fcond": get_filters_cond(doctype, filters, conditions).replace(
+ "%", "%%"
+ ),
+ }
+ ),
+ {
+ "txt": "%%%s%%" % txt,
+ "_txt": txt.replace("%", ""),
+ "start": start,
+ "page_len": page_len,
+ },
+ as_dict=as_dict,
+ )
+
def get_fields(doctype, fields=None):
- if fields is None:
- fields = []
- meta = frappe.get_meta(doctype)
- fields.extend(meta.get_search_fields())
+ if fields is None:
+ fields = []
+ meta = frappe.get_meta(doctype)
+ fields.extend(meta.get_search_fields())
- if meta.title_field and meta.title_field.strip() not in fields:
- fields.insert(1, meta.title_field.strip())
+ if meta.title_field and meta.title_field.strip() not in fields:
+ fields.insert(1, meta.title_field.strip())
- return unique(fields)
\ No newline at end of file
+ return unique(fields)
diff --git a/posnext/doc_events/item.py b/posnext/doc_events/item.py
index eefd491..9fe58ad 100644
--- a/posnext/doc_events/item.py
+++ b/posnext/doc_events/item.py
@@ -1,10 +1,11 @@
-import frappe
-from frappe import _
-from frappe.utils.pdf import get_pdf
-from frappe.www.printview import get_context
import json
+
+import frappe
+from frappe import _
from frappe.utils import now
from frappe.utils.file_manager import save_file
+from frappe.utils.pdf import get_pdf
+
def validate_item(doc, method):
for x in doc.custom_items:
@@ -18,31 +19,27 @@ def get_product_bundle_with_items(item_code):
bundle = frappe.db.get_value("Product Bundle", {"new_item_code": item_code}, "name")
if not bundle:
- return None
+ return None
bundle_doc = frappe.get_doc("Product Bundle", bundle)
bundle_items = []
for item in bundle_doc.items:
- bundle_items.append({
- "item_code": item.item_code,
- "qty": item.qty,
- "uom": item.uom
- })
+ bundle_items.append(
+ {"item_code": item.item_code, "qty": item.qty, "uom": item.uom}
+ )
return {
"name": bundle_doc.name,
"new_item_code": bundle_doc.new_item_code,
- "items": bundle_items
+ "items": bundle_items,
}
@frappe.whitelist()
-def print_barcodes(item_codes):
+def print_barcodes(item_codes):
if isinstance(item_codes, str):
-
item_codes = json.loads(item_codes)
-
items_with_barcodes = [
frappe.get_doc("Item", code)
@@ -60,8 +57,8 @@ def print_barcodes(item_codes):
url = f"/printview?doctype=Item&name={item_name}&format={print_format}&no_letterhead=1"
return {"url": url}
- html_content = ''.join(
- f'
{frappe.get_print("Item", item.name, print_format, doc=item)}
'
+ html_content = "".join(
+ f"
{frappe.get_print('Item', item.name, print_format, doc=item)}
"
for item in items_with_barcodes
)
@@ -73,11 +70,11 @@ def print_barcodes(item_codes):
content=pdf_data,
dt="Item",
dn=items_with_barcodes[0].name,
- is_private=0
+ is_private=0,
)
return {
"url": file_doc.file_url,
"message": _(f"Generated barcodes for {len(items_with_barcodes)} items."),
- "is_pdf": True
- }
\ No newline at end of file
+ "is_pdf": True,
+ }
diff --git a/posnext/doc_events/pos_profile.py b/posnext/doc_events/pos_profile.py
index dbdcbc5..4ecc494 100644
--- a/posnext/doc_events/pos_profile.py
+++ b/posnext/doc_events/pos_profile.py
@@ -1,7 +1,8 @@
import frappe
+from frappe import _
-def validate_pf(doc,method):
+def validate_pf(doc, method):
if not doc.custom_edit_rate_and_uom:
doc.custom_use_discount_percentage = 0
doc.custom_use_discount_amount = 0
@@ -10,7 +11,7 @@ def validate_pf(doc,method):
@frappe.whitelist()
def get_pos_profile_branch(pos_profile_name):
if not pos_profile_name:
- frappe.throw("POS Profile name is required.")
+ frappe.throw(_("POS Profile name is required."))
branch = frappe.db.get_value("POS Profile", pos_profile_name, "branch")
return {"branch": branch}
diff --git a/posnext/doc_events/sales_invoice.py b/posnext/doc_events/sales_invoice.py
index 8fb317e..464055a 100644
--- a/posnext/doc_events/sales_invoice.py
+++ b/posnext/doc_events/sales_invoice.py
@@ -1,22 +1,32 @@
import frappe
+from frappe import _
-def validate_si(doc,method):
+def validate_si(doc, method):
if doc.pos_profile:
- show_branch = frappe.db.get_value("POS Profile",doc.pos_profile,'show_branch')
- if 'branch' not in doc.__dict__ and show_branch==1:
- frappe.throw("Create Branch Accounting Dimensions.")
+ show_branch = frappe.db.get_value("POS Profile", doc.pos_profile, "show_branch")
+ if "branch" not in doc.__dict__ and show_branch == 1:
+ frappe.throw(_("Create Branch Accounting Dimensions."))
if doc.is_return and doc.is_pos:
doc.update_outstanding_for_self = 0
if doc.payments:
doc.payments[0].amount = doc.rounded_total or doc.grand_total
else:
- mop = frappe.db.get_all("POS Payment Method", {"default": True, "allow_in_returns": True, "parent": doc.pos_profile}, "mode_of_payment")
+ mop = frappe.db.get_all(
+ "POS Payment Method",
+ {"default": True, "allow_in_returns": True, "parent": doc.pos_profile},
+ "mode_of_payment",
+ )
if mop:
- doc.append("payments", {
- "mode_of_payment": mop[0].mode_of_payment,
- "amount": doc.rounded_total or doc.grand_total
- })
+ doc.append(
+ "payments",
+ {
+ "mode_of_payment": mop[0].mode_of_payment,
+ "amount": doc.rounded_total or doc.grand_total,
+ },
+ )
+
+
def create_delivery_note(doc, method):
if doc.update_stock:
return
@@ -32,45 +42,63 @@ def create_delivery_note(doc, method):
all_items_sufficient = True
for item in doc.items:
- available_qty = frappe.db.get_value(
- "Bin", {"item_code": item.item_code, "warehouse": item.warehouse}, "actual_qty"
- ) or 0
+ available_qty = (
+ frappe.db.get_value(
+ "Bin",
+ {"item_code": item.item_code, "warehouse": item.warehouse},
+ "actual_qty",
+ )
+ or 0
+ )
- delivery_note.append("items", {
- "item_code": item.item_code,
- "uom": item.uom,
- "qty": item.qty,
- "rate": item.rate,
- "warehouse": item.warehouse,
- "against_sales_invoice": item.parent,
- "si_detail": item.name,
- "cost_center": item.cost_center
- })
+ delivery_note.append(
+ "items",
+ {
+ "item_code": item.item_code,
+ "uom": item.uom,
+ "qty": item.qty,
+ "rate": item.rate,
+ "warehouse": item.warehouse,
+ "against_sales_invoice": item.parent,
+ "si_detail": item.name,
+ "cost_center": item.cost_center,
+ },
+ )
if item.qty > available_qty:
all_items_sufficient = False
for tax in doc.taxes:
- delivery_note.append("taxes", {
- "charge_type": tax.charge_type,
- "account_head": tax.account_head,
- "description": tax.description,
- "rate": tax.rate,
- "tax_amount": tax.tax_amount,
- "total": tax.total,
- "cost_center": tax.cost_center
- })
+ delivery_note.append(
+ "taxes",
+ {
+ "charge_type": tax.charge_type,
+ "account_head": tax.account_head,
+ "description": tax.description,
+ "rate": tax.rate,
+ "tax_amount": tax.tax_amount,
+ "total": tax.total,
+ "cost_center": tax.cost_center,
+ },
+ )
for sp in doc.sales_team:
- delivery_note.append("sales_team", {
- "sales_person": sp.sales_person,
- "allocated_percentage": sp.allocated_percentage
- })
+ delivery_note.append(
+ "sales_team",
+ {
+ "sales_person": sp.sales_person,
+ "allocated_percentage": sp.allocated_percentage,
+ },
+ )
delivery_note.save()
if all_items_sufficient:
delivery_note.submit()
- frappe.msgprint(f"Delivery Note {delivery_note.name} submitted as sufficient stock is available.")
+ frappe.msgprint(
+ f"Delivery Note {delivery_note.name} submitted as sufficient stock is available."
+ )
else:
- frappe.msgprint(f"Delivery Note {delivery_note.name} saved as draft due to insufficient stock.")
+ frappe.msgprint(
+ f"Delivery Note {delivery_note.name} saved as draft due to insufficient stock."
+ )
diff --git a/posnext/hooks.py b/posnext/hooks.py
index 4f87b62..11ebf70 100644
--- a/posnext/hooks.py
+++ b/posnext/hooks.py
@@ -39,10 +39,12 @@
# page_js = {"page" : "public/js/file.js"}
# include js in doctype views
-doctype_js = {"POS Profile" : "public/js/pos_profile.js",
-"Sales Invoice" : "public/js/sales_invoice.js"}
+doctype_js = {
+ "POS Profile": "public/js/pos_profile.js",
+ "Sales Invoice": "public/js/sales_invoice.js",
+}
-doctype_list_js = {"Item" : "public/js/item_list.js"}
+doctype_list_js = {"Item": "public/js/item_list.js"}
# doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"}
# doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"}
@@ -136,20 +138,16 @@
# Hook on document methods and events
doc_events = {
- "Item": {
- "validate": "posnext.doc_events.item.validate_item"
- },
- "Sales Invoice": {
- "validate": [
- "posnext.doc_events.sales_invoice.validate_si",
- ],
- "on_submit": [
- "posnext.doc_events.sales_invoice.create_delivery_note",
- ]
- },
- "POS Profile": {
- "validate": "posnext.doc_events.pos_profile.validate_pf"
- }
+ "Item": {"validate": "posnext.doc_events.item.validate_item"},
+ "Sales Invoice": {
+ "validate": [
+ "posnext.doc_events.sales_invoice.validate_si",
+ ],
+ "on_submit": [
+ "posnext.doc_events.sales_invoice.create_delivery_note",
+ ],
+ },
+ "POS Profile": {"validate": "posnext.doc_events.pos_profile.validate_pf"},
}
# Scheduled Tasks
@@ -176,14 +174,14 @@
# Testing
# -------
-# before_tests = "posnext.install.before_tests"
+before_tests = "posnext.utils.before_tests"
# Overriding Methods
# ------------------------------
#
override_whitelisted_methods = {
- "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices": "posnext.overrides.pos_closing_entry.get_pos_invoices",
- "erpnext.accounts.doctype.pos_invoice.pos_invoice.get_stock_availability": "posnext.overrides.pos_invoice.get_stock_availability"
+ "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices": "posnext.overrides.pos_closing_entry.get_pos_invoices",
+ "erpnext.accounts.doctype.pos_invoice.pos_invoice.get_stock_availability": "posnext.overrides.pos_invoice.get_stock_availability",
}
#
# each overriding function accepts a `data` argument;
@@ -250,31 +248,22 @@
# "Logging DocType Name": 30 # days to retain logs
# }
override_doctype_class = {
- "Sales Invoice": "posnext.overrides.sales_invoice.PosnextSalesInvoice",
- "POS Closing Entry": "posnext.overrides.pos_closing_entry.PosnextPOSClosingEntry",
- "POS Invoice Merge Log": "posnext.overrides.pos_invoice_merge_log.PosnextPOSInvoiceMergeLog",
+ "Sales Invoice": "posnext.overrides.sales_invoice.PosnextSalesInvoice",
+ "POS Closing Entry": "posnext.overrides.pos_closing_entry.PosnextPOSClosingEntry",
+ "POS Invoice Merge Log": "posnext.overrides.pos_invoice_merge_log.PosnextPOSInvoiceMergeLog",
}
fixtures = [
- {
- "doctype":"Custom Field",
- "filters": [
- [
- "module",
- "in",
- ["Posnext"]
- ]
- ]
- },
- {
- "doctype":"Property Setter",
- "filters": [
- [
- "module",
- "in",
- ["Posnext"]
- ]
- ]
- },
-]
\ No newline at end of file
+ {"doctype": "Custom Field", "filters": [["module", "in", ["Posnext"]]]},
+ {"doctype": "Property Setter", "filters": [["module", "in", ["Posnext"]]]},
+]
+
+standard_help_items = [
+ {
+ "item_label": "POSNext Documentation",
+ "item_type": "Route",
+ "route": "/posnext_introduction",
+ "is_standard": 1,
+ },
+]
diff --git a/posnext/overrides/pos_closing_entry.py b/posnext/overrides/pos_closing_entry.py
index c8b5ba0..88d03f9 100644
--- a/posnext/overrides/pos_closing_entry.py
+++ b/posnext/overrides/pos_closing_entry.py
@@ -1,5 +1,14 @@
import frappe
-from frappe.utils import flt, get_datetime
+from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import POSClosingEntry
+from frappe import _
+from frappe.utils import get_datetime
+
+from posnext.overrides.pos_invoice_merge_log import (
+ consolidate_pos_invoices,
+ unconsolidate_pos_invoices,
+)
+
+
@frappe.whitelist()
def get_pos_invoices(start, end, pos_profile, user):
print("HEEEEEEEEEEEEEEEEERE")
@@ -16,17 +25,15 @@ def get_pos_invoices(start, end, pos_profile, user):
as_dict=1,
)
- data = list(filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data))
+ start_dt = get_datetime(start)
+ end_dt = get_datetime(end)
+ data = [d for d in data if start_dt <= get_datetime(d.timestamp) <= end_dt]
+
# need to get taxes and payments so can't avoid get_doc
data = [frappe.get_doc("Sales Invoice", d.name).as_dict() for d in data]
return data
-from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import POSClosingEntry
-from posnext.overrides.pos_invoice_merge_log import (
- consolidate_pos_invoices,
- unconsolidate_pos_invoices,
-)
class PosnextPOSClosingEntry(POSClosingEntry):
def on_submit(self):
consolidate_pos_invoices(closing_entry=self)
@@ -56,7 +63,9 @@ def validate_pos_invoices(self):
# continue
if pos_invoice.pos_profile != self.pos_profile:
invalid_row.setdefault("msg", []).append(
- _("Sales Profile doesn't matches {}").format(frappe.bold(self.pos_profile))
+ _("Sales Profile doesn't matches {}").format(
+ frappe.bold(self.pos_profile)
+ )
)
if pos_invoice.docstatus != 1:
invalid_row.setdefault("msg", []).append(
@@ -64,7 +73,9 @@ def validate_pos_invoices(self):
)
if pos_invoice.owner != self.user:
invalid_row.setdefault("msg", []).append(
- _("Sales Invoice isn't created by user {}").format(frappe.bold(self.owner))
+ _("Sales Invoice isn't created by user {}").format(
+ frappe.bold(self.owner)
+ )
)
if invalid_row.get("msg"):
@@ -78,4 +89,4 @@ def validate_pos_invoices(self):
for msg in row.get("msg"):
error_list.append(_("Row #{}: {}").format(row.get("idx"), msg))
- frappe.throw(error_list, title=_("Invalid Sales Invoices"), as_list=True)
\ No newline at end of file
+ frappe.throw(error_list, title=_("Invalid Sales Invoices"), as_list=True)
diff --git a/posnext/overrides/pos_invoice.py b/posnext/overrides/pos_invoice.py
index fb8b6ec..251fd10 100644
--- a/posnext/overrides/pos_invoice.py
+++ b/posnext/overrides/pos_invoice.py
@@ -1,21 +1,23 @@
import frappe
-from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_bin_qty,get_bundle_availability
+from erpnext.accounts.doctype.pos_invoice.pos_invoice import (
+ get_bin_qty,
+ get_bundle_availability,
+)
+
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
- if frappe.db.get_value("Item", item_code, "is_stock_item"):
- is_stock_item = True
- bin_qty = get_bin_qty(item_code, warehouse)
- # pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
-
- return bin_qty, is_stock_item
- else:
- is_stock_item = True
- if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}):
- return get_bundle_availability(item_code, warehouse), is_stock_item
- else:
- is_stock_item = False
- # Is a service item or non_stock item
- return 0, is_stock_item
-
+ if frappe.db.get_value("Item", item_code, "is_stock_item"):
+ is_stock_item = True
+ bin_qty = get_bin_qty(item_code, warehouse)
+ # pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
+ return bin_qty, is_stock_item
+ else:
+ is_stock_item = True
+ if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}):
+ return get_bundle_availability(item_code, warehouse), is_stock_item
+ else:
+ is_stock_item = False
+ # Is a service item or non_stock item
+ return 0, is_stock_item
diff --git a/posnext/overrides/pos_invoice_merge_log.py b/posnext/overrides/pos_invoice_merge_log.py
index ce80e4b..4572beb 100644
--- a/posnext/overrides/pos_invoice_merge_log.py
+++ b/posnext/overrides/pos_invoice_merge_log.py
@@ -1,9 +1,14 @@
+import json
+
import frappe
-from frappe.utils.scheduler import is_scheduler_inactive
-from frappe.utils.background_jobs import enqueue, is_job_enqueued
-from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import POSInvoiceMergeLog
-from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime
+from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
+ POSInvoiceMergeLog,
+)
from frappe import _
+from frappe.utils import get_time, getdate, nowdate, nowtime
+from frappe.utils.background_jobs import enqueue, is_job_enqueued
+from frappe.utils.scheduler import is_scheduler_inactive
+
class PosnextPOSInvoiceMergeLog(POSInvoiceMergeLog):
def serial_and_batch_bundle_reference_for_pos_invoice(self):
@@ -11,14 +16,22 @@ def serial_and_batch_bundle_reference_for_pos_invoice(self):
pos_invoice = frappe.get_doc("Sales Invoice", d.pos_invoice)
for table_name in ["items", "packed_items"]:
pos_invoice.set_serial_and_batch_bundle(table_name)
+
def on_cancel(self):
- pos_invoice_docs = [frappe.get_cached_doc("Sales Invoice", d.pos_invoice) for d in self.pos_invoices]
+ pos_invoice_docs = [
+ frappe.get_cached_doc("Sales Invoice", d.pos_invoice)
+ for d in self.pos_invoices
+ ]
self.update_pos_invoices(pos_invoice_docs)
self.serial_and_batch_bundle_reference_for_pos_invoice()
self.cancel_linked_invoices()
+
def on_submit(self):
- pos_invoice_docs = [frappe.get_cached_doc("Sales Invoice", d.pos_invoice) for d in self.pos_invoices]
+ pos_invoice_docs = [
+ frappe.get_cached_doc("Sales Invoice", d.pos_invoice)
+ for d in self.pos_invoices
+ ]
returns = [d for d in pos_invoice_docs if d.get("is_return") == 1]
sales = [d for d in pos_invoice_docs if d.get("is_return") == 0]
@@ -32,45 +45,60 @@ def on_submit(self):
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
+
def validate_pos_invoice_status(self):
for d in self.pos_invoices:
status, docstatus, is_return, return_against = frappe.db.get_value(
- "Sales Invoice", d.pos_invoice, ["status", "docstatus", "is_return", "return_against"]
+ "Sales Invoice",
+ d.pos_invoice,
+ ["status", "docstatus", "is_return", "return_against"],
)
bold_pos_invoice = frappe.bold(d.pos_invoice)
bold_status = frappe.bold(status)
if docstatus != 1:
frappe.throw(
- _("Row #{}: Sales Invoice {} is not submitted yet").format(d.idx, bold_pos_invoice)
+ _("Row #{}: Sales Invoice {} is not submitted yet").format(
+ d.idx, bold_pos_invoice
+ )
)
if status == "Consolidated":
frappe.throw(
- _("Row #{}: Sales Invoice {} has been {}").format(d.idx, bold_pos_invoice, bold_status)
+ _("Row #{}: Sales Invoice {} has been {}").format(
+ d.idx, bold_pos_invoice, bold_status
+ )
)
if (
- is_return
- and return_against
- and return_against not in [d.pos_invoice for d in self.pos_invoices]
+ is_return
+ and return_against
+ and return_against not in [d.pos_invoice for d in self.pos_invoices]
):
bold_return_against = frappe.bold(return_against)
- return_against_status = frappe.db.get_value("Sales Invoice", return_against, "status")
+ return_against_status = frappe.db.get_value(
+ "Sales Invoice", return_against, "status"
+ )
if return_against_status != "Consolidated":
# if return entry is not getting merged in the current pos closing and if it is not consolidated
bold_unconsolidated = frappe.bold("not Consolidated")
- msg = _("Row #{}: Original Invoice {} of return invoice {} is {}.").format(
- d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated
+ msg = _(
+ "Row #{}: Original Invoice {} of return invoice {} is {}."
+ ).format(
+ d.idx,
+ bold_return_against,
+ bold_pos_invoice,
+ bold_unconsolidated,
)
msg += " "
msg += _(
"Original invoice should be consolidated before or along with the return invoice."
)
msg += "
"
- msg += _("You can add original invoice {} manually to proceed.").format(
- bold_return_against
- )
+ msg += _(
+ "You can add original invoice {} manually to proceed."
+ ).format(bold_return_against)
frappe.throw(msg)
+
def split_invoices(invoices):
"""
Splits invoices into multiple groups
@@ -103,18 +131,24 @@ def split_invoices(invoices):
if not item.serial_no and not item.serial_and_batch_bundle:
continue
- return_against_is_added = any(d for d in _invoices if d.pos_invoice == pos_invoice.return_against)
+ return_against_is_added = any(
+ d for d in _invoices if d.pos_invoice == pos_invoice.return_against
+ )
if return_against_is_added:
break
return_against_is_consolidated = (
- frappe.db.get_value("POS Invoice", pos_invoice.return_against, "status", cache=True)
+ frappe.db.get_value(
+ "POS Invoice", pos_invoice.return_against, "status", cache=True
+ )
== "Consolidated"
)
if return_against_is_consolidated:
break
- pos_invoice_row = [d for d in invoices if d.pos_invoice == pos_invoice.return_against]
+ pos_invoice_row = [
+ d for d in invoices if d.pos_invoice == pos_invoice.return_against
+ ]
_invoices.append(pos_invoice_row)
special_invoices.append(pos_invoice.return_against)
break
@@ -123,6 +157,7 @@ def split_invoices(invoices):
return _invoices
+
def consolidate_pos_invoices(pos_invoices=None, closing_entry=None):
invoices = pos_invoices or (closing_entry and closing_entry.get("pos_transactions"))
if frappe.flags.in_test and not invoices:
@@ -132,10 +167,15 @@ def consolidate_pos_invoices(pos_invoices=None, closing_entry=None):
if len(invoices) >= 10 and closing_entry:
closing_entry.set_status(update=True, status="Queued")
- enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry)
+ enqueue_job(
+ create_merge_logs,
+ invoice_by_customer=invoice_by_customer,
+ closing_entry=closing_entry,
+ )
else:
create_merge_logs(invoice_by_customer, closing_entry)
+
def get_all_unconsolidated_invoices():
filters = {
"consolidated_invoice": ["in", ["", None]],
@@ -157,6 +197,7 @@ def get_all_unconsolidated_invoices():
return pos_invoices
+
def get_invoice_customer_map(pos_invoices):
# pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Customer 2' : [{}] }
pos_invoice_customer_map = {}
@@ -170,14 +211,20 @@ def get_invoice_customer_map(pos_invoices):
def unconsolidate_pos_invoices(closing_entry):
merge_logs = frappe.get_all(
- "POS Invoice Merge Log", filters={"pos_closing_entry": closing_entry.name}, pluck="name"
+ "POS Invoice Merge Log",
+ filters={"pos_closing_entry": closing_entry.name},
+ pluck="name",
)
if len(merge_logs) >= 10:
closing_entry.set_status(update=True, status="Queued")
- enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry)
+ enqueue_job(
+ cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry
+ )
else:
cancel_merge_logs(merge_logs, closing_entry)
+
+
def cancel_merge_logs(merge_logs, closing_entry=None):
try:
for log in merge_logs:
@@ -201,22 +248,29 @@ def cancel_merge_logs(merge_logs, closing_entry=None):
raise
finally:
- frappe.db.commit()
+ # frappe.db.commit()
frappe.publish_realtime("closing_process_complete", user=frappe.session.user)
+
def create_merge_logs(invoice_by_customer, closing_entry=None):
try:
for customer, invoices in invoice_by_customer.items():
for _invoices in split_invoices(invoices):
merge_log = frappe.new_doc("POS Invoice Merge Log")
merge_log.posting_date = (
- getdate(closing_entry.get("posting_date")) if closing_entry else nowdate()
+ getdate(closing_entry.get("posting_date"))
+ if closing_entry
+ else nowdate()
)
merge_log.posting_time = (
- get_time(closing_entry.get("posting_time")) if closing_entry else nowtime()
+ get_time(closing_entry.get("posting_time"))
+ if closing_entry
+ else nowtime()
)
merge_log.customer = customer
- merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
+ merge_log.pos_closing_entry = (
+ closing_entry.get("name") if closing_entry else None
+ )
merge_log.set("pos_invoices", _invoices)
merge_log.save(ignore_permissions=True)
merge_log.submit()
@@ -238,8 +292,10 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
raise
finally:
- frappe.db.commit()
+ # frappe.db.commit()
frappe.publish_realtime("closing_process_complete", user=frappe.session.user)
+
+
def enqueue_job(job, **kwargs):
check_scheduler_status()
@@ -264,13 +320,17 @@ def enqueue_job(job, **kwargs):
frappe.msgprint(msg, alert=1)
+
def check_scheduler_status():
if is_scheduler_inactive() and not frappe.flags.in_test:
- frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive"))
+ frappe.throw(
+ _("Scheduler is inactive. Cannot enqueue job."),
+ title=_("Scheduler Inactive"),
+ )
def get_error_message(message) -> str:
- try:
- return message["message"]
- except Exception:
- return str(message)
\ No newline at end of file
+ try:
+ return message["message"]
+ except Exception:
+ return str(message)
diff --git a/posnext/overrides/sales_invoice.py b/posnext/overrides/sales_invoice.py
index 53094d7..71cb1eb 100644
--- a/posnext/overrides/sales_invoice.py
+++ b/posnext/overrides/sales_invoice.py
@@ -1,36 +1,46 @@
import frappe
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
SalesInvoice,
- update_multi_mode_option
+ update_multi_mode_option,
)
from frappe import _
-from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
+from frappe.utils import flt
-from six import iteritems
-from frappe import msgprint
-class PosnextSalesInvoice(SalesInvoice):
+class PosnextSalesInvoice(SalesInvoice):
@frappe.whitelist()
def reset_mode_of_payments(self):
if self.pos_profile:
pos_profile = frappe.get_cached_doc("POS Profile", self.pos_profile)
update_multi_mode_option(self, pos_profile)
self.paid_amount = 0
+
def validate_pos(self):
if self.is_return:
- self.paid_amount = self.paid_amount if not self.is_pos else self.base_rounded_total
+ self.paid_amount = (
+ self.paid_amount if not self.is_pos else self.base_rounded_total
+ )
self.outstanding_amount = 0
for x in self.payments:
- x.amount = self.paid_amount
+ x.amount = self.paid_amount
x.amount = x.amount * -1 if x.amount > 0 else x.amount
invoice_total = self.rounded_total or self.grand_total
- if flt(self.paid_amount) + flt(self.write_off_amount) - abs(flt(invoice_total)) > 1.0 / (10.0 ** (self.precision("grand_total") + 1.0)):
- frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total"))
+ if flt(self.paid_amount) + flt(self.write_off_amount) - abs(
+ flt(invoice_total)
+ ) > 1.0 / (10.0 ** (self.precision("grand_total") + 1.0)):
+ frappe.throw(
+ _(
+ "Paid amount + Write Off Amount can not be greater than Grand Total"
+ )
+ )
+
def validate_pos_paid_amount(self):
if len(self.payments) == 0 and self.is_pos:
- custom_show_credit_sales = frappe.get_value("POS Profile",self.pos_profile,"custom_show_credit_sales")
+ custom_show_credit_sales = frappe.get_value(
+ "POS Profile", self.pos_profile, "custom_show_credit_sales"
+ )
if not custom_show_credit_sales:
- frappe.throw(_("At least one mode of payment is required for POS invoice."))
-
-
+ frappe.throw(
+ _("At least one mode of payment is required for POS invoice.")
+ )
diff --git a/posnext/posnext/doctype/alternative_items/alternative_items.py b/posnext/posnext/doctype/alternative_items/alternative_items.py
index 7955cb3..cfc3468 100644
--- a/posnext/posnext/doctype/alternative_items/alternative_items.py
+++ b/posnext/posnext/doctype/alternative_items/alternative_items.py
@@ -6,4 +6,4 @@
class AlternativeItems(Document):
- pass
+ pass
diff --git a/posnext/posnext/doctype/logical_rack/logical_rack.py b/posnext/posnext/doctype/logical_rack/logical_rack.py
index c34821e..36b81d5 100644
--- a/posnext/posnext/doctype/logical_rack/logical_rack.py
+++ b/posnext/posnext/doctype/logical_rack/logical_rack.py
@@ -6,4 +6,4 @@
class LogicalRack(Document):
- pass
+ pass
diff --git a/posnext/posnext/doctype/logical_rack/test_logical_rack.py b/posnext/posnext/doctype/logical_rack/test_logical_rack.py
index 1152271..186c290 100644
--- a/posnext/posnext/doctype/logical_rack/test_logical_rack.py
+++ b/posnext/posnext/doctype/logical_rack/test_logical_rack.py
@@ -6,4 +6,4 @@
class TestLogicalRack(FrappeTestCase):
- pass
+ pass
diff --git a/posnext/posnext/doctype/pos_profile_whatsapp_field_names/pos_profile_whatsapp_field_names.py b/posnext/posnext/doctype/pos_profile_whatsapp_field_names/pos_profile_whatsapp_field_names.py
index dacfb54..30ab987 100644
--- a/posnext/posnext/doctype/pos_profile_whatsapp_field_names/pos_profile_whatsapp_field_names.py
+++ b/posnext/posnext/doctype/pos_profile_whatsapp_field_names/pos_profile_whatsapp_field_names.py
@@ -6,4 +6,4 @@
class POSProfileWhatsappFieldNames(Document):
- pass
+ pass
diff --git a/posnext/posnext/page/posnext/point_of_sale.py b/posnext/posnext/page/posnext/point_of_sale.py
index 45b8478..c10e730 100644
--- a/posnext/posnext/page/posnext/point_of_sale.py
+++ b/posnext/posnext/page/posnext/point_of_sale.py
@@ -6,137 +6,187 @@
from typing import Dict, Optional
import frappe
-from frappe.utils import cint
-from frappe.utils.nestedset import get_root_of
-
from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability
-from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes, get_item_groups
+from erpnext.accounts.doctype.pos_profile.pos_profile import (
+ get_child_nodes,
+ get_item_groups,
+)
from erpnext.stock.utils import scan_barcode
+from frappe.utils import cint
+from frappe.utils.file_manager import save_file
+from frappe.utils.nestedset import get_root_of
+from frappe.utils.pdf import get_pdf
-def search_by_term(search_term,custom_show_alternative_item_for_pos_search, warehouse, price_list):
- result = search_for_serial_or_batch_or_barcode_number(search_term) or {}
-
- item_code = result.get("item_code", "")
- serial_no = result.get("serial_no", "")
- batch_no = result.get("batch_no", "")
- barcode = result.get("barcode", "")
-
- if not result:
- return
- print("RESSSSULT")
- print(result)
- item_doc = frappe.get_doc("Item", item_code)
-
- if not item_doc:
- return
- item = {
- "barcode": barcode,
- "batch_no": batch_no,
- "description": item_doc.description,
- "is_stock_item": item_doc.is_stock_item,
- "item_code": item_doc.name,
- "item_image": item_doc.image,
- "item_name": item_doc.item_name,
- "serial_no": serial_no,
- "stock_uom": item_doc.stock_uom,
- "uom": item_doc.stock_uom,
- "item_uoms": frappe.db.get_all("UOM Conversion Detail", {"parent": item_doc.item_code}, ["uom"], pluck="uom")
- }
-
- if barcode:
- barcode_info = next(filter(lambda x: x.barcode == barcode, item_doc.get("barcodes", [])), None)
- if barcode_info and barcode_info.uom:
- uom = next(filter(lambda x: x.uom == barcode_info.uom, item_doc.uoms), {})
- item.update(
- {
- "uom": barcode_info.uom,
- "conversion_factor": uom.get("conversion_factor", 1),
- }
- )
-
- item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
- item_stock_qty = item_stock_qty // item.get("conversion_factor", 1)
- item.update({"actual_qty": item_stock_qty})
-
- price = frappe.get_list(
- doctype="Item Price",
- filters={
- "price_list": price_list,
- "item_code": item_code,
- "batch_no": batch_no,
- },
- fields=["uom", "currency", "price_list_rate", "batch_no"],
- )
-
- def __sort(p):
- p_uom = p.get("uom")
-
- if p_uom == item.get("uom"):
- return 0
- elif p_uom == item.get("stock_uom"):
- return 1
- else:
- return 2
-
- # sort by fallback preference. always pick exact uom match if available
- price = sorted(price, key=__sort)
-
- if len(price) > 0:
- p = price.pop(0)
- item.update(
- {
- "currency": p.get("currency"),
- "price_list_rate": p.get("price_list_rate"),
- }
- )
-
-
- return {"items": [item]}
+def search_by_term(
+ search_term, custom_show_alternative_item_for_pos_search, warehouse, price_list
+):
+ result = search_for_serial_or_batch_or_barcode_number(search_term) or {}
+
+ item_code = result.get("item_code", "")
+ serial_no = result.get("serial_no", "")
+ batch_no = result.get("batch_no", "")
+ barcode = result.get("barcode", "")
+
+ if not result:
+ return
+ print("RESSSSULT")
+ print(result)
+ item_doc = frappe.get_doc("Item", item_code)
+
+ if not item_doc:
+ return
+ item = {
+ "barcode": barcode,
+ "batch_no": batch_no,
+ "description": item_doc.description,
+ "is_stock_item": item_doc.is_stock_item,
+ "item_code": item_doc.name,
+ "item_image": item_doc.image,
+ "item_name": item_doc.item_name,
+ "serial_no": serial_no,
+ "stock_uom": item_doc.stock_uom,
+ "uom": item_doc.stock_uom,
+ "item_uoms": frappe.db.get_all(
+ "UOM Conversion Detail",
+ {"parent": item_doc.item_code},
+ ["uom"],
+ pluck="uom",
+ ),
+ }
+
+ if barcode:
+ barcode_info = next(
+ (x for x in item_doc.get("barcodes", []) if x.barcode == barcode), None
+ )
+ if barcode_info and barcode_info.uom:
+ uom = next((x for x in item_doc.uoms if x.uom == barcode_info.uom), {})
+ item.update(
+ {
+ "uom": barcode_info.uom,
+ "conversion_factor": uom.get("conversion_factor", 1),
+ }
+ )
+
+ item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
+ item_stock_qty = item_stock_qty // item.get("conversion_factor", 1)
+ item.update({"actual_qty": item_stock_qty})
+
+ price = frappe.get_list(
+ doctype="Item Price",
+ filters={
+ "price_list": price_list,
+ "item_code": item_code,
+ "batch_no": batch_no,
+ },
+ fields=["uom", "currency", "price_list_rate", "batch_no"],
+ )
+
+ def __sort(p):
+ p_uom = p.get("uom")
+
+ if p_uom == item.get("uom"):
+ return 0
+ elif p_uom == item.get("stock_uom"):
+ return 1
+ else:
+ return 2
+
+ # sort by fallback preference. always pick exact uom match if available
+ price = sorted(price, key=__sort)
+
+ if len(price) > 0:
+ p = price.pop(0)
+ item.update(
+ {
+ "currency": p.get("currency"),
+ "price_list_rate": p.get("price_list_rate"),
+ }
+ )
+
+ return {"items": [item]}
@frappe.whitelist()
def get_items(start, page_length, price_list, item_group, pos_profile, search_term=""):
- warehouse, hide_unavailable_items,custom_show_last_incoming_rate, custom_show_alternative_item_for_pos_search,custom_show_logical_rack, custom_skip_stock_transaction_validation = frappe.db.get_value(
- "POS Profile", pos_profile, ["warehouse", "hide_unavailable_items","custom_show_last_incoming_rate","custom_show_alternative_item_for_pos_search","custom_show_logical_rack", "custom_skip_stock_transaction_validation"]
- )
-
- result = []
-
- if search_term:
- result = search_by_term(search_term,custom_show_alternative_item_for_pos_search, warehouse, price_list) or []
- if result:
- return result
- alt_items = []
- if custom_show_alternative_item_for_pos_search:
- alt_items = frappe.db.sql(""" SELECT * FROM `tabAlternative Items`
- WHERE parent like %s or parent_item_name like %s or parent_item_description like %s or parent_oem_part_number like %s""",('%' + search_term + '%','%' + search_term + '%','%' + search_term + '%','%' + search_term + '%'),as_dict=1)
- if not frappe.db.exists("Item Group", item_group):
- item_group = get_root_of("Item Group")
-
- condition = get_conditions(search_term,alt_items)
- condition += get_item_group_condition(pos_profile)
-
- lft, rgt = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"])
-
- bin_join_selection, bin_join_condition,bin_valuation_rate,bin_join_condition_valuation = "", "","",""
- if not custom_skip_stock_transaction_validation:
- if hide_unavailable_items:
- bin_join_selection = ", `tabBin` bin"
- bin_join_condition = (
- "AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name AND bin.actual_qty > 0"
- )
-
- if not bin_join_selection:
- bin_join_selection = ", `tabBin` bin"
- bin_valuation_rate = "bin.valuation_rate, bin.valuation_rate as custom_valuation_rate,"
-
- bin_join_condition_valuation = (
- "AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name"
- )
-
- items_data = frappe.db.sql(
- """
+ (
+ warehouse,
+ hide_unavailable_items,
+ custom_show_last_incoming_rate,
+ custom_show_alternative_item_for_pos_search,
+ custom_show_logical_rack,
+ custom_skip_stock_transaction_validation,
+ ) = frappe.db.get_value(
+ "POS Profile",
+ pos_profile,
+ [
+ "warehouse",
+ "hide_unavailable_items",
+ "custom_show_last_incoming_rate",
+ "custom_show_alternative_item_for_pos_search",
+ "custom_show_logical_rack",
+ "custom_skip_stock_transaction_validation",
+ ],
+ )
+
+ result = []
+
+ if search_term:
+ result = (
+ search_by_term(
+ search_term,
+ custom_show_alternative_item_for_pos_search,
+ warehouse,
+ price_list,
+ )
+ or []
+ )
+ if result:
+ return result
+ alt_items = []
+ if custom_show_alternative_item_for_pos_search:
+ alt_items = frappe.db.sql(
+ """ SELECT * FROM `tabAlternative Items`
+ WHERE parent like %s or parent_item_name like %s or parent_item_description like %s or parent_oem_part_number like %s""",
+ (
+ "%" + search_term + "%",
+ "%" + search_term + "%",
+ "%" + search_term + "%",
+ "%" + search_term + "%",
+ ),
+ as_dict=1,
+ )
+ if not frappe.db.exists("Item Group", item_group):
+ item_group = get_root_of("Item Group")
+
+ condition = get_conditions(search_term, alt_items)
+ condition += get_item_group_condition(pos_profile)
+
+ lft, rgt = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"])
+
+ (
+ bin_join_selection,
+ bin_join_condition,
+ bin_valuation_rate,
+ bin_join_condition_valuation,
+ ) = "", "", "", ""
+ if not custom_skip_stock_transaction_validation:
+ if hide_unavailable_items:
+ bin_join_selection = ", `tabBin` bin"
+ bin_join_condition = "AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name AND bin.actual_qty > 0"
+
+ if not bin_join_selection:
+ bin_join_selection = ", `tabBin` bin"
+ bin_valuation_rate = (
+ "bin.valuation_rate, bin.valuation_rate as custom_valuation_rate,"
+ )
+
+ bin_join_condition_valuation = (
+ "AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name"
+ )
+
+ items_data = frappe.db.sql(
+ """
SELECT
item.name AS item_code,
item.custom_oem_part_number,
@@ -161,217 +211,240 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
item.name asc
LIMIT
{page_length} offset {start}""".format(
- start=cint(start),
- page_length=cint(page_length),
- lft=cint(lft),
- rgt=cint(rgt),
- condition=condition,
- bin_join_selection=bin_join_selection,
- bin_valuation_rate=bin_valuation_rate,
- bin_join_condition=bin_join_condition,
- bin_join_condition_valuation=bin_join_condition_valuation
- ),
- {"warehouse": warehouse},
- as_dict=1,
- )
-
- # return (empty) list if there are no results
- if not items_data:
- return result
-
- for item in items_data:
- if custom_show_logical_rack:
- rack = frappe.db.sql(""" SELECT * FROM `tabLogical Rack` WHERE item=%s and pos_profile=%s """,(item.item_code,pos_profile),as_dict=1)
- if len(rack) > 0:
- item['rack'] = rack[0].rack_id
- item['custom_logical_rack'] = rack[0].rack_id
- uoms = frappe.get_doc("Item", item.item_code).get("uoms", [])
- item["custom_item_uoms"] = frappe.db.get_all("UOM Conversion Detail", {"parent": item.item_code}, ["uom"], pluck="uom")
- item.actual_qty, _ = get_stock_availability(item.item_code, warehouse)
- item.uom = item.stock_uom
- item_price = frappe.get_all(
- "Item Price",
- fields=["price_list_rate", "currency", "uom", "batch_no"],
- filters={
- "price_list": price_list,
- "item_code": item.item_code,
- "selling": True,
- },
- order_by="creation desc",
- limit=1
- )
-
- if not item_price:
- result.append(item)
-
- for price in item_price:
- uom = next(filter(lambda x: x.uom == price.uom, uoms), {})
-
- if price.uom != item.stock_uom and uom and uom.conversion_factor:
- item.actual_qty = item.actual_qty // uom.conversion_factor
-
- result.append(
- {
- **item,
- "price_list_rate": price.get("price_list_rate"),
- "currency": price.get("currency"),
- "uom": price.uom or item.uom,
- "batch_no": price.batch_no,
- }
- )
- return {"items": result}
+ start=cint(start),
+ page_length=cint(page_length),
+ lft=cint(lft),
+ rgt=cint(rgt),
+ condition=condition,
+ bin_join_selection=bin_join_selection,
+ bin_valuation_rate=bin_valuation_rate,
+ bin_join_condition=bin_join_condition,
+ bin_join_condition_valuation=bin_join_condition_valuation,
+ ),
+ {"warehouse": warehouse},
+ as_dict=1,
+ )
+
+ # return (empty) list if there are no results
+ if not items_data:
+ return result
+
+ for item in items_data:
+ if custom_show_logical_rack:
+ rack = frappe.db.sql(
+ """ SELECT * FROM `tabLogical Rack` WHERE item=%s and pos_profile=%s """,
+ (item.item_code, pos_profile),
+ as_dict=1,
+ )
+ if len(rack) > 0:
+ item["rack"] = rack[0].rack_id
+ item["custom_logical_rack"] = rack[0].rack_id
+ uoms = frappe.get_doc("Item", item.item_code).get("uoms", [])
+ item["custom_item_uoms"] = frappe.db.get_all(
+ "UOM Conversion Detail", {"parent": item.item_code}, ["uom"], pluck="uom"
+ )
+ item.actual_qty, _ = get_stock_availability(item.item_code, warehouse)
+ item.uom = item.stock_uom
+ item_price = frappe.get_all(
+ "Item Price",
+ fields=["price_list_rate", "currency", "uom", "batch_no"],
+ filters={
+ "price_list": price_list,
+ "item_code": item.item_code,
+ "selling": True,
+ },
+ order_by="creation desc",
+ limit=1,
+ )
+
+ if not item_price:
+ result.append(item)
+
+ for price in item_price:
+ uom = next((x for x in uoms if x.uom == price.uom), {})
+
+ if price.uom != item.stock_uom and uom and uom.conversion_factor:
+ item.actual_qty = item.actual_qty // uom.conversion_factor
+
+ result.append(
+ {
+ **item,
+ "price_list_rate": price.get("price_list_rate"),
+ "currency": price.get("currency"),
+ "uom": price.uom or item.uom,
+ "batch_no": price.batch_no,
+ }
+ )
+ return {"items": result}
@frappe.whitelist()
-def search_for_serial_or_batch_or_barcode_number(search_value: str) -> Dict[str, Optional[str]]:
- return scan_barcode(search_value)
+def search_for_serial_or_batch_or_barcode_number(
+ search_value: str,
+) -> Dict[str, Optional[str]]:
+ return scan_barcode(search_value)
-def get_conditions(search_term,new_items):
- condition = "("
+def get_conditions(search_term, new_items):
+ condition = "("
- condition += """(item.name like {search_term}
+ condition += """(item.name like {search_term}
or item.item_name like {search_term} or item.description like {search_term} or item.custom_oem_part_number like {search_term}) """.format(
- search_term=frappe.db.escape("%" + search_term + "%")
- )
- if len(new_items) > 0:
- for xx in new_items:
- condition += """or (item.name like {xx}
+ search_term=frappe.db.escape("%" + search_term + "%")
+ )
+ if len(new_items) > 0:
+ for xx in new_items:
+ condition += """or (item.name like {xx}
or item.item_name like {xx}) """.format(
- xx=frappe.db.escape("%" + xx.item + "%")
- )
- condition += add_search_fields_condition(search_term)
- condition += ")"
+ xx=frappe.db.escape("%" + xx.item + "%")
+ )
+ condition += add_search_fields_condition(search_term)
+ condition += ")"
- return condition
+ return condition
def add_search_fields_condition(search_term):
- condition = ""
- search_fields = frappe.get_all("POS Search Fields", fields=["fieldname"])
- if search_fields:
- for field in search_fields:
- condition += " or item.`{0}` like {1}".format(
- field["fieldname"], frappe.db.escape("%" + search_term + "%")
- )
- return condition
+ condition = ""
+ search_fields = frappe.get_all("POS Search Fields", fields=["fieldname"])
+ if search_fields:
+ for field in search_fields:
+ condition += " or item.`{0}` like {1}".format(
+ field["fieldname"], frappe.db.escape("%" + search_term + "%")
+ )
+ return condition
def get_item_group_condition(pos_profile):
- cond = "and 1=1"
- item_groups = get_item_groups(pos_profile)
- if item_groups:
- cond = "and item.item_group in (%s)" % (", ".join(["%s"] * len(item_groups)))
+ cond = "and 1=1"
+ item_groups = get_item_groups(pos_profile)
+ if item_groups:
+ cond = "and item.item_group in (%s)" % (", ".join(["%s"] * len(item_groups)))
- return cond % tuple(item_groups)
+ return cond % tuple(item_groups)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def item_group_query(doctype, txt, searchfield, start, page_len, filters):
- item_groups = []
- cond = "1=1"
- pos_profile = filters.get("pos_profile")
+ item_groups = []
+ cond = "1=1"
+ pos_profile = filters.get("pos_profile")
- if pos_profile:
- item_groups = get_item_groups(pos_profile)
+ if pos_profile:
+ item_groups = get_item_groups(pos_profile)
- if item_groups:
- cond = "name in (%s)" % (", ".join(["%s"] * len(item_groups)))
- cond = cond % tuple(item_groups)
+ if item_groups:
+ cond = "name in (%s)" % (", ".join(["%s"] * len(item_groups)))
+ cond = cond % tuple(item_groups)
- return frappe.db.sql(
- """ select distinct name from `tabItem Group`
+ return frappe.db.sql(
+ """ select distinct name from `tabItem Group`
where {condition} and (name like %(txt)s) limit {page_len} offset {start}""".format(
- condition=cond, start=start, page_len=page_len
- ),
- {"txt": "%%%s%%" % txt},
- )
+ condition=cond, start=start, page_len=page_len
+ ),
+ {"txt": "%%%s%%" % txt},
+ )
@frappe.whitelist()
-def check_opening_entry(user,value):
- filters = {"user": user, "pos_closing_entry": ["in", ["", None]], "docstatus": 1}
- if value:
- filters['pos_profile'] = value
- open_vouchers = frappe.db.get_all(
- "POS Opening Entry",
- filters=filters,
- fields=["name", "company", "pos_profile", "period_start_date"],
- order_by="period_start_date desc",
- )
+def check_opening_entry(user, value):
+ filters = {"user": user, "pos_closing_entry": ["in", ["", None]], "docstatus": 1}
+ if value:
+ filters["pos_profile"] = value
+ open_vouchers = frappe.db.get_all(
+ "POS Opening Entry",
+ filters=filters,
+ fields=["name", "company", "pos_profile", "period_start_date"],
+ order_by="period_start_date desc",
+ )
- return open_vouchers
+ return open_vouchers
@frappe.whitelist()
def create_opening_voucher(pos_profile, company, balance_details):
- balance_details = json.loads(balance_details)
+ balance_details = json.loads(balance_details)
- new_pos_opening = frappe.get_doc(
- {
- "doctype": "POS Opening Entry",
- "period_start_date": frappe.utils.get_datetime(),
- "posting_date": frappe.utils.getdate(),
- "user": frappe.session.user,
- "pos_profile": pos_profile,
- "company": company,
- }
- )
- new_pos_opening.set("balance_details", balance_details)
- new_pos_opening.submit()
+ new_pos_opening = frappe.get_doc(
+ {
+ "doctype": "POS Opening Entry",
+ "period_start_date": frappe.utils.get_datetime(),
+ "posting_date": frappe.utils.getdate(),
+ "user": frappe.session.user,
+ "pos_profile": pos_profile,
+ "company": company,
+ }
+ )
+ new_pos_opening.set("balance_details", balance_details)
+ new_pos_opening.submit()
- return new_pos_opening.as_dict()
+ return new_pos_opening.as_dict()
@frappe.whitelist()
def get_past_order_list(search_term, status, pos_profile=None, limit=20):
- fields = ["name", "grand_total", "currency", "customer", "posting_time", "posting_date"]
- invoice_list = []
- if status == "Unpaid":
- status = ["in", ["Unpaid", "Partly Paid", "Overdue"]]
-
- if search_term and status:
- fltr1 = {"customer": ["like", "%{}%".format(search_term)], "status": status}
- if pos_profile:
- fltr1 = {"customer": ["like", "%{}%".format(search_term)], "status": status, "pos_profile": pos_profile}
- invoices_by_customer = frappe.db.get_all(
- "Sales Invoice",
- filters=fltr1,
- fields=fields,
- page_length=limit,
- )
- fltr2 = {"name": ["like", "%{}%".format(search_term)], "status": status}
- if pos_profile:
- fltr2 = {"name": ["like", "%{}%".format(search_term)], "status": status, "pos_profile": pos_profile}
- invoices_by_name = frappe.db.get_all(
- "Sales Invoice",
- filters=fltr2,
- fields=fields,
- page_length=limit,
- )
-
- invoice_list = invoices_by_customer + invoices_by_name
- elif status:
- fltr = {"status": status}
- if pos_profile:
- fltr = {"status": status, "pos_profile": pos_profile}
- invoice_list = frappe.db.get_all(
- "Sales Invoice", filters=fltr, fields=fields, page_length=limit
- )
-
- return invoice_list
+ fields = [
+ "name",
+ "grand_total",
+ "currency",
+ "customer",
+ "posting_time",
+ "posting_date",
+ ]
+ invoice_list = []
+ if status == "Unpaid":
+ status = ["in", ["Unpaid", "Partly Paid", "Overdue"]]
+
+ if search_term and status:
+ fltr1 = {"customer": ["like", "%{}%".format(search_term)], "status": status}
+ if pos_profile:
+ fltr1 = {
+ "customer": ["like", "%{}%".format(search_term)],
+ "status": status,
+ "pos_profile": pos_profile,
+ }
+ invoices_by_customer = frappe.db.get_all(
+ "Sales Invoice",
+ filters=fltr1,
+ fields=fields,
+ page_length=limit,
+ )
+ fltr2 = {"name": ["like", "%{}%".format(search_term)], "status": status}
+ if pos_profile:
+ fltr2 = {
+ "name": ["like", "%{}%".format(search_term)],
+ "status": status,
+ "pos_profile": pos_profile,
+ }
+ invoices_by_name = frappe.db.get_all(
+ "Sales Invoice",
+ filters=fltr2,
+ fields=fields,
+ page_length=limit,
+ )
+
+ invoice_list = invoices_by_customer + invoices_by_name
+ elif status:
+ fltr = {"status": status}
+ if pos_profile:
+ fltr = {"status": status, "pos_profile": pos_profile}
+ invoice_list = frappe.db.get_all(
+ "Sales Invoice", filters=fltr, fields=fields, page_length=limit
+ )
+
+ return invoice_list
@frappe.whitelist()
def set_customer_info(fieldname, customer, value=""):
- if fieldname == "loyalty_program":
- frappe.db.set_value("Customer", customer, "loyalty_program", value)
+ if fieldname == "loyalty_program":
+ frappe.db.set_value("Customer", customer, "loyalty_program", value)
- contact = frappe.get_cached_value("Customer", customer, "customer_primary_contact")
- if not contact:
- contact = frappe.db.sql(
- """
+ contact = frappe.get_cached_value("Customer", customer, "customer_primary_contact")
+ if not contact:
+ contact = frappe.db.sql(
+ """
SELECT parent FROM `tabDynamic Link`
WHERE
parenttype = 'Contact' AND
@@ -379,111 +452,116 @@ def set_customer_info(fieldname, customer, value=""):
link_doctype = 'Customer' AND
link_name = %s
""",
- (customer),
- as_dict=1,
- )
- contact = contact[0].get("parent") if contact else None
-
- if not contact:
- new_contact = frappe.new_doc("Contact")
- new_contact.is_primary_contact = 1
- new_contact.first_name = customer
- new_contact.set("links", [{"link_doctype": "Customer", "link_name": customer}])
- new_contact.save()
- contact = new_contact.name
- frappe.db.set_value("Customer", customer, "customer_primary_contact", contact)
-
- contact_doc = frappe.get_doc("Contact", contact)
- if fieldname == "email_id":
- contact_doc.set("email_ids", [{"email_id": value, "is_primary": 1}])
- frappe.db.set_value("Customer", customer, "email_id", value)
- elif fieldname == "mobile_no":
- contact_doc.set("phone_nos", [{"phone": value, "is_primary_mobile_no": 1}])
- frappe.db.set_value("Customer", customer, "mobile_no", value)
- contact_doc.save()
+ (customer),
+ as_dict=1,
+ )
+ contact = contact[0].get("parent") if contact else None
+
+ if not contact:
+ new_contact = frappe.new_doc("Contact")
+ new_contact.is_primary_contact = 1
+ new_contact.first_name = customer
+ new_contact.set("links", [{"link_doctype": "Customer", "link_name": customer}])
+ new_contact.save()
+ contact = new_contact.name
+ frappe.db.set_value("Customer", customer, "customer_primary_contact", contact)
+
+ contact_doc = frappe.get_doc("Contact", contact)
+ if fieldname == "email_id":
+ contact_doc.set("email_ids", [{"email_id": value, "is_primary": 1}])
+ frappe.db.set_value("Customer", customer, "email_id", value)
+ elif fieldname == "mobile_no":
+ contact_doc.set("phone_nos", [{"phone": value, "is_primary_mobile_no": 1}])
+ frappe.db.set_value("Customer", customer, "mobile_no", value)
+ contact_doc.save()
@frappe.whitelist()
def get_pos_profile_data(pos_profile):
- pos_profile = frappe.get_doc("POS Profile", pos_profile)
- pos_profile = pos_profile.as_dict()
+ pos_profile = frappe.get_doc("POS Profile", pos_profile)
+ pos_profile = pos_profile.as_dict()
- _customer_groups_with_children = []
- for row in pos_profile.customer_groups:
- children = get_child_nodes("Customer Group", row.customer_group)
- _customer_groups_with_children.extend(children)
- for row in pos_profile.payments:
- if row.default:
- pos_profile['default_payment'] = row.mode_of_payment
- pos_profile.customer_groups = _customer_groups_with_children
- return pos_profile
+ _customer_groups_with_children = []
+ for row in pos_profile.customer_groups:
+ children = get_child_nodes("Customer Group", row.customer_group)
+ _customer_groups_with_children.extend(children)
+ for row in pos_profile.payments:
+ if row.default:
+ pos_profile["default_payment"] = row.mode_of_payment
+ pos_profile.customer_groups = _customer_groups_with_children
+ return pos_profile
@frappe.whitelist()
def create_customer(customer):
- customer_check = frappe.db.sql(""" SELECT * FROM `tabCustomer` WHERE name=%s""",customer,as_dict=1)
- if len(customer_check) == 0:
- obj = {
- "doctype": "Customer",
- "customer_name": customer
- }
+ customer_check = frappe.db.sql(
+ """ SELECT * FROM `tabCustomer` WHERE name=%s""", customer, as_dict=1
+ )
+ if len(customer_check) == 0:
+ obj = {"doctype": "Customer", "customer_name": customer}
- frappe.get_doc(obj).insert()
- frappe.db.commit()
+ frappe.get_doc(obj).insert()
+ # frappe.db.commit()
-import frappe
-from frappe.utils.pdf import get_pdf
-from frappe.utils.file_manager import save_file
-
@frappe.whitelist()
def generate_pdf_and_save(docname, doctype, print_format=None):
- # Get the HTML content of the print format
- data = frappe.get_doc(doctype,docname)
- html = frappe.get_print(doctype, docname, print_format)
+ # Get the HTML content of the print format
+ data = frappe.get_doc(doctype, docname)
+ html = frappe.get_print(doctype, docname, print_format)
+
+ # Generate PDF from HTML
+ pdf_data = get_pdf(html)
- # Generate PDF from HTML
- pdf_data = get_pdf(html)
+ # Define file name
+ file_name = f"{data.customer_name + docname.split('-')[-1]}.pdf"
- # Define file name
- file_name = f"{data.customer_name + docname.split('-')[-1]}.pdf"
+ # Save the PDF as a file
+ file_doc = save_file(file_name, pdf_data, doctype, docname, is_private=0)
+ print("FILE DOOOOC")
+ print(file_doc)
+ return file_doc
- # Save the PDF as a file
- file_doc = save_file(file_name, pdf_data, doctype, docname, is_private=0)
- print("FILE DOOOOC")
- print(file_doc)
- return file_doc
@frappe.whitelist()
def make_sales_return(source_name, target_doc=None):
- from erpnext.controllers.sales_and_purchase_return import make_return_doc
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
- return make_return_doc("Sales Invoice", source_name, target_doc)
+ return make_return_doc("Sales Invoice", source_name, target_doc)
@frappe.whitelist()
def get_lcr(customer=None, item_code=None):
- d = None
- if customer and item_code:
- d = frappe.db.sql(f"""
+ d = None
+ if customer and item_code:
+ d = frappe.db.sql(
+ f"""
SELECT item.rate FROM `tabSales Invoice Item` item INNER JOIN `tabSales Invoice` SI ON SI.name=item.parent
- WHERE SI.customer='{customer}' AND item.item_code='{item_code}'
- ORDER BY SI.creation desc
+ WHERE SI.customer='{customer}' AND item.item_code='{item_code}'
+ ORDER BY SI.creation desc
LIMIT 1
- """, as_dict=True)
- if d:
- return d[0].rate
- else:
- return 0
+ """,
+ as_dict=True,
+ )
+ if d:
+ return d[0].rate
+ else:
+ return 0
+
@frappe.whitelist()
def get_uoms(item_code):
- d = frappe.db.get_all("UOM Conversion Detail", {"parent": item_code}, ["uom"], pluck="uom")
- if d:
- return d
- else:
- return []
-
+ d = frappe.db.get_all(
+ "UOM Conversion Detail", {"parent": item_code}, ["uom"], pluck="uom"
+ )
+ if d:
+ return d
+ else:
+ return []
+
+
@frappe.whitelist()
def get_barcodes(item_code):
- return frappe.db.get_all("Item Barcode", filters={"parent": item_code}, fields=["barcode"])
+ return frappe.db.get_all(
+ "Item Barcode", filters={"parent": item_code}, fields=["barcode"]
+ )
diff --git a/posnext/posnext/page/posnext/posnext.js b/posnext/posnext/page/posnext/posnext.js
index 97a05e2..0afef3f 100644
--- a/posnext/posnext/page/posnext/posnext.js
+++ b/posnext/posnext/page/posnext/posnext.js
@@ -9,28 +9,28 @@
//
// document.head.appendChild(script);
// })();'console.log("POSNEXT POINTSALE")
-frappe.pages['posnext'].on_page_load = function(wrapper) {
- let fullwidth = JSON.parse(localStorage.container_fullwidth || 'false');
- if(!fullwidth){
- localStorage.container_fullwidth = true;
- frappe.ui.toolbar.set_fullwidth_if_enabled();
- }
- frappe.ui.make_app_page({
- parent: wrapper,
- title: __('Point of Sales'),
- single_column: true
- });
+frappe.pages["posnext"].on_page_load = function (wrapper) {
+ let fullwidth = JSON.parse(localStorage.container_fullwidth || "false");
+ if (!fullwidth) {
+ localStorage.container_fullwidth = true;
+ frappe.ui.toolbar.set_fullwidth_if_enabled();
+ }
+ frappe.ui.make_app_page({
+ parent: wrapper,
+ title: __("Point of Sales"),
+ single_column: true,
+ });
- window.wrapper = wrapper
- wrapper.pos = new posnext.PointOfSale.Controller(wrapper);
- window.cur_pos = wrapper.pos;
-}
-frappe.pages['posnext'].refresh = function(wrapper,onscan = "",value="") {
- // if (document.scannerDetectionData) {
- if(!onscan){
- window.onScan.detachFrom(document)
- }
- wrapper.pos.wrapper.html("");
- wrapper.pos.check_opening_entry(value);
- // }
-};
\ No newline at end of file
+ window.wrapper = wrapper;
+ wrapper.pos = new posnext.PointOfSale.Controller(wrapper);
+ window.cur_pos = wrapper.pos;
+};
+frappe.pages["posnext"].refresh = function (wrapper, onscan = "", value = "") {
+ // if (document.scannerDetectionData) {
+ if (!onscan) {
+ window.onScan.detachFrom(document);
+ }
+ wrapper.pos.wrapper.html("");
+ wrapper.pos.check_opening_entry(value);
+ // }
+};
diff --git a/posnext/posnext/report/stock_balance_rack/stock_balance_rack.js b/posnext/posnext/report/stock_balance_rack/stock_balance_rack.js
index c632246..f70f1ce 100644
--- a/posnext/posnext/report/stock_balance_rack/stock_balance_rack.js
+++ b/posnext/posnext/report/stock_balance_rack/stock_balance_rack.js
@@ -2,130 +2,131 @@
// For license information, please see license.txt
frappe.query_reports["Stock Balance Rack"] = {
- filters: [
- {
- fieldname: "company",
- label: __("Company"),
- fieldtype: "Link",
- width: "80",
- options: "Company",
- default: frappe.defaults.get_default("company"),
- },
- {
- fieldname: "pos_profile",
- label: __("POS Profile"),
- fieldtype: "Link",
- width: "80",
- options: "POS Profile",
- reqd: true
- },
- {
- fieldname: "from_date",
- label: __("From Date"),
- fieldtype: "Date",
- width: "80",
- reqd: 1,
- default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
- },
- {
- fieldname: "to_date",
- label: __("To Date"),
- fieldtype: "Date",
- width: "80",
- reqd: 1,
- default: frappe.datetime.get_today(),
- },
- {
- fieldname: "item_group",
- label: __("Item Group"),
- fieldtype: "Link",
- width: "80",
- options: "Item Group",
- },
- {
- fieldname: "item_code",
- label: __("Item"),
- fieldtype: "Link",
- width: "80",
- options: "Item",
- get_query: function () {
- return {
- query: "erpnext.controllers.queries.item_query",
- };
- },
- },
- {
- fieldname: "warehouse",
- label: __("Warehouse"),
- fieldtype: "Link",
- width: "80",
- options: "Warehouse",
- get_query: () => {
- let warehouse_type = frappe.query_report.get_filter_value("warehouse_type");
- let company = frappe.query_report.get_filter_value("company");
+ filters: [
+ {
+ fieldname: "company",
+ label: __("Company"),
+ fieldtype: "Link",
+ width: "80",
+ options: "Company",
+ default: frappe.defaults.get_default("company"),
+ },
+ {
+ fieldname: "pos_profile",
+ label: __("POS Profile"),
+ fieldtype: "Link",
+ width: "80",
+ options: "POS Profile",
+ reqd: true,
+ },
+ {
+ fieldname: "from_date",
+ label: __("From Date"),
+ fieldtype: "Date",
+ width: "80",
+ reqd: 1,
+ default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
+ },
+ {
+ fieldname: "to_date",
+ label: __("To Date"),
+ fieldtype: "Date",
+ width: "80",
+ reqd: 1,
+ default: frappe.datetime.get_today(),
+ },
+ {
+ fieldname: "item_group",
+ label: __("Item Group"),
+ fieldtype: "Link",
+ width: "80",
+ options: "Item Group",
+ },
+ {
+ fieldname: "item_code",
+ label: __("Item"),
+ fieldtype: "Link",
+ width: "80",
+ options: "Item",
+ get_query: function () {
+ return {
+ query: "erpnext.controllers.queries.item_query",
+ };
+ },
+ },
+ {
+ fieldname: "warehouse",
+ label: __("Warehouse"),
+ fieldtype: "Link",
+ width: "80",
+ options: "Warehouse",
+ get_query: () => {
+ let warehouse_type =
+ frappe.query_report.get_filter_value("warehouse_type");
+ let company = frappe.query_report.get_filter_value("company");
- return {
- filters: {
- ...(warehouse_type && { warehouse_type }),
- ...(company && { company }),
- },
- };
- },
- },
- {
- fieldname: "warehouse_type",
- label: __("Warehouse Type"),
- fieldtype: "Link",
- width: "80",
- options: "Warehouse Type",
- },
- {
- fieldname: "valuation_field_type",
- label: __("Valuation Field Type"),
- fieldtype: "Select",
- width: "80",
- options: "Currency\nFloat",
- default: "Currency",
- },
- {
- fieldname: "include_uom",
- label: __("Include UOM"),
- fieldtype: "Link",
- options: "UOM",
- },
- {
- fieldname: "show_variant_attributes",
- label: __("Show Variant Attributes"),
- fieldtype: "Check",
- },
- {
- fieldname: "show_stock_ageing_data",
- label: __("Show Stock Ageing Data"),
- fieldtype: "Check",
- },
- {
- fieldname: "ignore_closing_balance",
- label: __("Ignore Closing Balance"),
- fieldtype: "Check",
- default: 0,
- },
- {
- fieldname: "include_zero_stock_items",
- label: __("Include Zero Stock Items"),
- fieldtype: "Check",
- default: 0,
- },
- ],
+ return {
+ filters: {
+ ...(warehouse_type && { warehouse_type }),
+ ...(company && { company }),
+ },
+ };
+ },
+ },
+ {
+ fieldname: "warehouse_type",
+ label: __("Warehouse Type"),
+ fieldtype: "Link",
+ width: "80",
+ options: "Warehouse Type",
+ },
+ {
+ fieldname: "valuation_field_type",
+ label: __("Valuation Field Type"),
+ fieldtype: "Select",
+ width: "80",
+ options: "Currency\nFloat",
+ default: "Currency",
+ },
+ {
+ fieldname: "include_uom",
+ label: __("Include UOM"),
+ fieldtype: "Link",
+ options: "UOM",
+ },
+ {
+ fieldname: "show_variant_attributes",
+ label: __("Show Variant Attributes"),
+ fieldtype: "Check",
+ },
+ {
+ fieldname: "show_stock_ageing_data",
+ label: __("Show Stock Ageing Data"),
+ fieldtype: "Check",
+ },
+ {
+ fieldname: "ignore_closing_balance",
+ label: __("Ignore Closing Balance"),
+ fieldtype: "Check",
+ default: 0,
+ },
+ {
+ fieldname: "include_zero_stock_items",
+ label: __("Include Zero Stock Items"),
+ fieldtype: "Check",
+ default: 0,
+ },
+ ],
- formatter: function (value, row, column, data, default_formatter) {
- value = default_formatter(value, row, column, data);
+ formatter: function (value, row, column, data, default_formatter) {
+ value = default_formatter(value, row, column, data);
- if (column.fieldname == "out_qty" && data && data.out_qty > 0) {
- value = "
" + value + "";
- } else if (column.fieldname == "in_qty" && data && data.in_qty > 0) {
- value = "
" + value + "";
- }
+ if (column.fieldname == "out_qty" && data && data.out_qty > 0) {
+ value = "
" + value + "";
+ } else if (column.fieldname == "in_qty" && data && data.in_qty > 0) {
+ value = "
" + value + "";
+ }
- return value;
- },
+ return value;
+ },
};
diff --git a/posnext/posnext/report/stock_balance_rack/stock_balance_rack.py b/posnext/posnext/report/stock_balance_rack/stock_balance_rack.py
index a7e9eca..c9ece24 100644
--- a/posnext/posnext/report/stock_balance_rack/stock_balance_rack.py
+++ b/posnext/posnext/report/stock_balance_rack/stock_balance_rack.py
@@ -5,653 +5,709 @@
from operator import itemgetter
from typing import Any, TypedDict
+import erpnext
import frappe
+from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
+ get_inventory_dimensions,
+)
+from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter
+from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, get_average_age
+from erpnext.stock.utils import add_additional_uom_columns
from frappe import _
from frappe.query_builder import Order
from frappe.query_builder.functions import Coalesce
from frappe.utils import add_days, cint, date_diff, flt, getdate
from frappe.utils.nestedset import get_descendants_of
-import erpnext
-from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
-from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter
-from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, get_average_age
-from erpnext.stock.utils import add_additional_uom_columns
-
class StockBalanceFilter(TypedDict):
- company: str | None
- from_date: str
- to_date: str
- item_group: str | None
- item: str | None
- warehouse: str | None
- warehouse_type: str | None
- include_uom: str | None # include extra info in converted UOM
- show_stock_ageing_data: bool
- show_variant_attributes: bool
+ company: str | None
+ from_date: str
+ to_date: str
+ item_group: str | None
+ item: str | None
+ warehouse: str | None
+ warehouse_type: str | None
+ include_uom: str | None # include extra info in converted UOM
+ show_stock_ageing_data: bool
+ show_variant_attributes: bool
SLEntry = dict[str, Any]
def execute(filters: StockBalanceFilter | None = None):
- return StockBalanceReport(filters).run()
+ return StockBalanceReport(filters).run()
class StockBalanceReport:
- def __init__(self, filters: StockBalanceFilter | None) -> None:
- self.filters = filters
- self.from_date = getdate(filters.get("from_date"))
- self.to_date = getdate(filters.get("to_date"))
-
- self.start_from = None
- self.data = []
- self.columns = []
- self.sle_entries: list[SLEntry] = []
- self.set_company_currency()
-
- def set_company_currency(self) -> None:
- if self.filters.get("company"):
- self.company_currency = erpnext.get_company_currency(self.filters.get("company"))
- else:
- self.company_currency = frappe.db.get_single_value("Global Defaults", "default_currency")
-
- def run(self):
- self.float_precision = cint(frappe.db.get_default("float_precision")) or 3
-
- self.inventory_dimensions = self.get_inventory_dimension_fields()
- self.prepare_opening_data_from_closing_balance()
- self.prepare_stock_ledger_entries()
- self.prepare_new_data()
-
- if not self.columns:
- self.columns = self.get_columns()
-
- self.add_additional_uom_columns()
-
- return self.columns, self.data
-
- def prepare_opening_data_from_closing_balance(self) -> None:
- self.opening_data = frappe._dict({})
-
- closing_balance = self.get_closing_balance()
- if not closing_balance:
- return
-
- self.start_from = add_days(closing_balance[0].to_date, 1)
- res = frappe.get_doc("Closing Stock Balance", closing_balance[0].name).get_prepared_data()
-
- for entry in res.data:
- entry = frappe._dict(entry)
-
- group_by_key = self.get_group_by_key(entry)
- if group_by_key not in self.opening_data:
- self.opening_data.setdefault(group_by_key, entry)
-
- def prepare_new_data(self):
- self.item_warehouse_map = self.get_item_warehouse_map()
-
- if self.filters.get("show_stock_ageing_data"):
- self.filters["show_warehouse_wise_stock"] = True
- item_wise_fifo_queue = FIFOSlots(self.filters, self.sle_entries).generate()
-
- _func = itemgetter(1)
-
- del self.sle_entries
-
- sre_details = self.get_sre_reserved_qty_details()
- variant_values = {}
- if self.filters.get("show_variant_attributes"):
- variant_values = self.get_variant_values_for()
-
- for _key, report_data in self.item_warehouse_map.items():
- if variant_data := variant_values.get(report_data.item_code):
- report_data.update(variant_data)
-
- if self.filters.get("show_stock_ageing_data"):
- opening_fifo_queue = self.get_opening_fifo_queue(report_data) or []
-
- fifo_queue = []
- if fifo_queue := item_wise_fifo_queue.get((report_data.item_code, report_data.warehouse)):
- fifo_queue = fifo_queue.get("fifo_queue")
-
- if fifo_queue:
- opening_fifo_queue.extend(fifo_queue)
-
- stock_ageing_data = {"average_age": 0, "earliest_age": 0, "latest_age": 0}
- if opening_fifo_queue:
- fifo_queue = sorted(filter(_func, opening_fifo_queue), key=_func)
- if not fifo_queue:
- continue
-
- to_date = self.to_date
- stock_ageing_data["average_age"] = get_average_age(fifo_queue, to_date)
- stock_ageing_data["earliest_age"] = date_diff(to_date, fifo_queue[0][1])
- stock_ageing_data["latest_age"] = date_diff(to_date, fifo_queue[-1][1])
- stock_ageing_data["fifo_queue"] = fifo_queue
-
- report_data.update(stock_ageing_data)
-
- report_data.update(
- {"reserved_stock": sre_details.get((report_data.item_code, report_data.warehouse), 0.0)}
- )
-
- if (
- not self.filters.get("include_zero_stock_items")
- and report_data
- and report_data.bal_qty == 0
- and report_data.bal_val == 0
- ):
- continue
-
- self.data.append(report_data)
-
- def get_item_warehouse_map(self):
- item_warehouse_map = {}
- self.opening_vouchers = self.get_opening_vouchers()
-
- if self.filters.get("show_stock_ageing_data"):
- self.sle_entries = self.sle_query.run(as_dict=True)
-
- # HACK: This is required to avoid causing db query in flt
- _system_settings = frappe.get_cached_doc("System Settings")
- with frappe.db.unbuffered_cursor():
- if not self.filters.get("show_stock_ageing_data"):
- self.sle_entries = self.sle_query.run(as_dict=True, as_iterator=True)
-
- for entry in self.sle_entries:
- group_by_key = self.get_group_by_key(entry)
- if group_by_key not in item_warehouse_map:
- self.initialize_data(item_warehouse_map, group_by_key, entry)
-
- self.prepare_item_warehouse_map(item_warehouse_map, entry, group_by_key)
-
- if self.opening_data.get(group_by_key):
- del self.opening_data[group_by_key]
- for group_by_key, entry in self.opening_data.items():
- if group_by_key not in item_warehouse_map:
- self.initialize_data(item_warehouse_map, group_by_key, entry)
- item_warehouse_map = filter_items_with_no_transactions(
- item_warehouse_map, self.float_precision, self.inventory_dimensions
- )
- return item_warehouse_map
-
- def get_sre_reserved_qty_details(self) -> dict:
- from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
- get_sre_reserved_qty_for_items_and_warehouses as get_reserved_qty_details,
- )
-
- item_code_list, warehouse_list = [], []
- for d in self.item_warehouse_map:
- item_code_list.append(d[1])
- warehouse_list.append(d[2])
-
- return get_reserved_qty_details(item_code_list, warehouse_list)
-
- def prepare_item_warehouse_map(self, item_warehouse_map, entry, group_by_key):
- qty_dict = item_warehouse_map[group_by_key]
- for field in self.inventory_dimensions:
- qty_dict[field] = entry.get(field)
-
- if entry.voucher_type == "Stock Reconciliation" and (not entry.batch_no or entry.serial_no):
- qty_diff = flt(entry.qty_after_transaction) - flt(qty_dict.bal_qty)
- else:
- qty_diff = flt(entry.actual_qty)
-
- value_diff = flt(entry.stock_value_difference)
-
- if entry.posting_date < self.from_date or entry.voucher_no in self.opening_vouchers.get(
- entry.voucher_type, []
- ):
- qty_dict.opening_qty += qty_diff
- qty_dict.opening_val += value_diff
-
- elif entry.posting_date >= self.from_date and entry.posting_date <= self.to_date:
- if flt(qty_diff, self.float_precision) >= 0:
- qty_dict.in_qty += qty_diff
- qty_dict.in_val += value_diff
- else:
- qty_dict.out_qty += abs(qty_diff)
- qty_dict.out_val += abs(value_diff)
-
- qty_dict.val_rate = entry.valuation_rate
- qty_dict.bal_qty += qty_diff
- qty_dict.bal_val += value_diff
-
- def initialize_data(self, item_warehouse_map, group_by_key, entry):
- opening_data = self.opening_data.get(group_by_key, {})
- item_warehouse_map[group_by_key] = frappe._dict(
- {
- "item_code": entry.item_code,
- "warehouse": entry.warehouse,
- "item_group": entry.item_group,
- "company": entry.company,
- "currency": self.company_currency,
- "stock_uom": entry.stock_uom,
- "item_name": entry.item_name,
- "opening_qty": opening_data.get("bal_qty") or 0.0,
- "opening_val": opening_data.get("bal_val") or 0.0,
- "opening_fifo_queue": opening_data.get("fifo_queue") or [],
- "in_qty": 0.0,
- "in_val": 0.0,
- "out_qty": 0.0,
- "out_val": 0.0,
- "bal_qty": opening_data.get("bal_qty") or 0.0,
- "bal_val": opening_data.get("bal_val") or 0.0,
- "val_rate": 0.0,
- "rack": entry.rack
- }
- )
-
- def get_group_by_key(self, row) -> tuple:
- group_by_key = [row.company, row.item_code, row.warehouse]
-
- for fieldname in self.inventory_dimensions:
- if self.filters.get(fieldname):
- group_by_key.append(row.get(fieldname))
-
- return tuple(group_by_key)
-
- def get_closing_balance(self) -> list[dict[str, Any]]:
- if self.filters.get("ignore_closing_balance"):
- return []
-
- table = frappe.qb.DocType("Closing Stock Balance")
-
- query = (
- frappe.qb.from_(table)
- .select(table.name, table.to_date)
- .where(
- (table.docstatus == 1)
- & (table.company == self.filters.company)
- & (table.to_date <= self.from_date)
- & (table.status == "Completed")
- )
- .orderby(table.to_date, order=Order.desc)
- .limit(1)
- )
-
- for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]:
- if self.filters.get(fieldname):
- query = query.where(table[fieldname] == self.filters.get(fieldname))
-
- return query.run(as_dict=True)
-
- def prepare_stock_ledger_entries(self):
- sle = frappe.qb.DocType("Stock Ledger Entry")
- item_table = frappe.qb.DocType("Item")
- logical_rack = frappe.qb.DocType("Logical Rack")
- pos_profile = frappe.qb.DocType("POS Profile")
- query = (
- frappe.qb.from_(sle)
- .inner_join(item_table)
- .on(sle.item_code == item_table.name)
- .inner_join(logical_rack)
- .on(logical_rack.pos_profile == self.filters.get("pos_profile"))
- .inner_join(pos_profile)
- .on(pos_profile.warehouse == sle.warehouse)
- .select(
- sle.item_code,
- sle.warehouse,
- sle.posting_date,
- sle.actual_qty,
- sle.valuation_rate,
- sle.company,
- sle.voucher_type,
- sle.qty_after_transaction,
- sle.stock_value_difference,
- sle.item_code.as_("name"),
- sle.voucher_no,
- sle.stock_value,
- sle.batch_no,
- sle.serial_no,
- sle.serial_and_batch_bundle,
- sle.has_serial_no,
- item_table.item_group,
- item_table.stock_uom,
- item_table.item_name,
- logical_rack.rack_id.as_("rack")
- )
- .where((sle.docstatus < 2) & (sle.is_cancelled == 0))
- .where(logical_rack.item == sle.item_code)
- .orderby(sle.posting_datetime)
- .orderby(sle.creation)
- .orderby(sle.actual_qty)
- )
-
- query = self.apply_inventory_dimensions_filters(query, sle)
- query = self.apply_warehouse_filters(query, sle)
- query = self.apply_items_filters(query, item_table)
- query = self.apply_date_filters(query, sle)
-
- if self.filters.get("company"):
- query = query.where(sle.company == self.filters.get("company"))
-
- self.sle_query = query
-
- def apply_inventory_dimensions_filters(self, query, sle) -> str:
- inventory_dimension_fields = self.get_inventory_dimension_fields()
- if inventory_dimension_fields:
- for fieldname in inventory_dimension_fields:
- query = query.select(fieldname)
- if self.filters.get(fieldname):
- query = query.where(sle[fieldname].isin(self.filters.get(fieldname)))
-
- return query
-
- def apply_warehouse_filters(self, query, sle) -> str:
- warehouse_table = frappe.qb.DocType("Warehouse")
-
- if self.filters.get("warehouse"):
- query = apply_warehouse_filter(query, sle, self.filters)
- elif warehouse_type := self.filters.get("warehouse_type"):
- query = (
- query.join(warehouse_table)
- .on(warehouse_table.name == sle.warehouse)
- .where(warehouse_table.warehouse_type == warehouse_type)
- )
-
- return query
-
- def apply_items_filters(self, query, item_table) -> str:
- if item_group := self.filters.get("item_group"):
- children = get_descendants_of("Item Group", item_group, ignore_permissions=True)
- query = query.where(item_table.item_group.isin([*children, item_group]))
-
- for field in ["item_code", "brand"]:
- if not self.filters.get(field):
- continue
- elif field == "item_code":
- query = query.where(item_table.name == self.filters.get(field))
- else:
- query = query.where(item_table[field] == self.filters.get(field))
-
- return query
-
- def apply_date_filters(self, query, sle) -> str:
- if not self.filters.ignore_closing_balance and self.start_from:
- query = query.where(sle.posting_date >= self.start_from)
-
- if self.to_date:
- query = query.where(sle.posting_date <= self.to_date)
-
- return query
-
- def get_columns(self):
- columns = [
- {
- "label": _("Item"),
- "fieldname": "item_code",
- "fieldtype": "Link",
- "options": "Item",
- "width": 100,
- },
- {"label": _("Item Name"), "fieldname": "item_name", "width": 150},
- {
- "label": _("Item Group"),
- "fieldname": "item_group",
- "fieldtype": "Link",
- "options": "Item Group",
- "width": 100,
- },
- {
- "label": _("Warehouse"),
- "fieldname": "warehouse",
- "fieldtype": "Link",
- "options": "Warehouse",
- "width": 100,
- },
- {
- "label": _("Rack"),
- "fieldname": "rack",
- "fieldtype": "Data",
- "width": 100,
- },
- ]
-
- for dimension in get_inventory_dimensions():
- columns.append(
- {
- "label": _(dimension.doctype),
- "fieldname": dimension.fieldname,
- "fieldtype": "Link",
- "options": dimension.doctype,
- "width": 110,
- }
- )
-
- columns.extend(
- [
- {
- "label": _("Stock UOM"),
- "fieldname": "stock_uom",
- "fieldtype": "Link",
- "options": "UOM",
- "width": 90,
- },
- {
- "label": _("Balance Qty"),
- "fieldname": "bal_qty",
- "fieldtype": "Float",
- "width": 100,
- "convertible": "qty",
- },
- {
- "label": _("Balance Value"),
- "fieldname": "bal_val",
- "fieldtype": "Currency",
- "width": 100,
- "options": "Company:company:default_currency",
- },
- {
- "label": _("Opening Qty"),
- "fieldname": "opening_qty",
- "fieldtype": "Float",
- "width": 100,
- "convertible": "qty",
- },
- {
- "label": _("Opening Value"),
- "fieldname": "opening_val",
- "fieldtype": "Currency",
- "width": 110,
- "options": "Company:company:default_currency",
- },
- {
- "label": _("In Qty"),
- "fieldname": "in_qty",
- "fieldtype": "Float",
- "width": 80,
- "convertible": "qty",
- },
- {"label": _("In Value"), "fieldname": "in_val", "fieldtype": "Float", "width": 80},
- {
- "label": _("Out Qty"),
- "fieldname": "out_qty",
- "fieldtype": "Float",
- "width": 80,
- "convertible": "qty",
- },
- {"label": _("Out Value"), "fieldname": "out_val", "fieldtype": "Float", "width": 80},
- {
- "label": _("Valuation Rate"),
- "fieldname": "val_rate",
- "fieldtype": self.filters.valuation_field_type or "Currency",
- "width": 90,
- "convertible": "rate",
- "options": "Company:company:default_currency"
- if self.filters.valuation_field_type == "Currency"
- else None,
- },
- {
- "label": _("Reserved Stock"),
- "fieldname": "reserved_stock",
- "fieldtype": "Float",
- "width": 80,
- "convertible": "qty",
- },
- {
- "label": _("Company"),
- "fieldname": "company",
- "fieldtype": "Link",
- "options": "Company",
- "width": 100,
- },
- ]
- )
-
- if self.filters.get("show_stock_ageing_data"):
- columns += [
- {"label": _("Average Age"), "fieldname": "average_age", "width": 100},
- {"label": _("Earliest Age"), "fieldname": "earliest_age", "width": 100},
- {"label": _("Latest Age"), "fieldname": "latest_age", "width": 100},
- ]
-
- if self.filters.get("show_variant_attributes"):
- columns += [
- {"label": att_name, "fieldname": att_name, "width": 100}
- for att_name in get_variants_attributes()
- ]
-
- return columns
-
- def add_additional_uom_columns(self):
- if not self.filters.get("include_uom"):
- return
-
- conversion_factors = self.get_itemwise_conversion_factor()
- add_additional_uom_columns(self.columns, self.data, self.filters.include_uom, conversion_factors)
-
- def get_itemwise_conversion_factor(self):
- items = []
- if self.filters.item_code or self.filters.item_group:
- items = [d.item_code for d in self.data]
-
- table = frappe.qb.DocType("UOM Conversion Detail")
- query = (
- frappe.qb.from_(table)
- .select(
- table.conversion_factor,
- table.parent,
- )
- .where((table.parenttype == "Item") & (table.uom == self.filters.include_uom))
- )
-
- if items:
- query = query.where(table.parent.isin(items))
-
- result = query.run(as_dict=1)
- if not result:
- return {}
-
- return {d.parent: d.conversion_factor for d in result}
-
- def get_variant_values_for(self):
- """Returns variant values for items."""
- attribute_map = {}
- items = []
- if self.filters.item_code or self.filters.item_group:
- items = [d.item_code for d in self.data]
-
- filters = {}
- if items:
- filters = {"parent": ("in", items)}
-
- attribute_info = frappe.get_all(
- "Item Variant Attribute",
- fields=["parent", "attribute", "attribute_value"],
- filters=filters,
- )
-
- for attr in attribute_info:
- attribute_map.setdefault(attr["parent"], {})
- attribute_map[attr["parent"]].update({attr["attribute"]: attr["attribute_value"]})
-
- return attribute_map
-
- def get_opening_vouchers(self):
- opening_vouchers = {"Stock Entry": [], "Stock Reconciliation": []}
-
- se = frappe.qb.DocType("Stock Entry")
- sr = frappe.qb.DocType("Stock Reconciliation")
-
- vouchers_data = (
- frappe.qb.from_(
- (
- frappe.qb.from_(se)
- .select(se.name, Coalesce("Stock Entry").as_("voucher_type"))
- .where((se.docstatus == 1) & (se.posting_date <= self.to_date) & (se.is_opening == "Yes"))
- )
- + (
- frappe.qb.from_(sr)
- .select(sr.name, Coalesce("Stock Reconciliation").as_("voucher_type"))
- .where(
- (sr.docstatus == 1)
- & (sr.posting_date <= self.to_date)
- & (sr.purpose == "Opening Stock")
- )
- )
- ).select("voucher_type", "name")
- ).run(as_dict=True)
-
- if vouchers_data:
- for d in vouchers_data:
- opening_vouchers[d.voucher_type].append(d.name)
-
- return opening_vouchers
-
- @staticmethod
- def get_inventory_dimension_fields():
- return [dimension.fieldname for dimension in get_inventory_dimensions()]
-
- @staticmethod
- def get_opening_fifo_queue(report_data):
- opening_fifo_queue = report_data.get("opening_fifo_queue") or []
- for row in opening_fifo_queue:
- row[1] = getdate(row[1])
-
- return opening_fifo_queue
+ def __init__(self, filters: StockBalanceFilter | None) -> None:
+ self.filters = filters
+ self.from_date = getdate(filters.get("from_date"))
+ self.to_date = getdate(filters.get("to_date"))
+
+ self.start_from = None
+ self.data = []
+ self.columns = []
+ self.sle_entries: list[SLEntry] = []
+ self.set_company_currency()
+
+ def set_company_currency(self) -> None:
+ if self.filters.get("company"):
+ self.company_currency = erpnext.get_company_currency(
+ self.filters.get("company")
+ )
+ else:
+ self.company_currency = frappe.db.get_single_value(
+ "Global Defaults", "default_currency"
+ )
+
+ def run(self):
+ self.float_precision = cint(frappe.db.get_default("float_precision")) or 3
+
+ self.inventory_dimensions = self.get_inventory_dimension_fields()
+ self.prepare_opening_data_from_closing_balance()
+ self.prepare_stock_ledger_entries()
+ self.prepare_new_data()
+
+ if not self.columns:
+ self.columns = self.get_columns()
+
+ self.add_additional_uom_columns()
+
+ return self.columns, self.data
+
+ def prepare_opening_data_from_closing_balance(self) -> None:
+ self.opening_data = frappe._dict({})
+
+ closing_balance = self.get_closing_balance()
+ if not closing_balance:
+ return
+
+ self.start_from = add_days(closing_balance[0].to_date, 1)
+ res = frappe.get_doc(
+ "Closing Stock Balance", closing_balance[0].name
+ ).get_prepared_data()
+
+ for entry in res.data:
+ entry = frappe._dict(entry)
+
+ group_by_key = self.get_group_by_key(entry)
+ if group_by_key not in self.opening_data:
+ self.opening_data.setdefault(group_by_key, entry)
+
+ def prepare_new_data(self):
+ self.item_warehouse_map = self.get_item_warehouse_map()
+
+ if self.filters.get("show_stock_ageing_data"):
+ self.filters["show_warehouse_wise_stock"] = True
+ item_wise_fifo_queue = FIFOSlots(self.filters, self.sle_entries).generate()
+
+ _func = itemgetter(1)
+
+ del self.sle_entries
+
+ sre_details = self.get_sre_reserved_qty_details()
+ variant_values = {}
+ if self.filters.get("show_variant_attributes"):
+ variant_values = self.get_variant_values_for()
+
+ for _key, report_data in self.item_warehouse_map.items():
+ if variant_data := variant_values.get(report_data.item_code):
+ report_data.update(variant_data)
+
+ if self.filters.get("show_stock_ageing_data"):
+ opening_fifo_queue = self.get_opening_fifo_queue(report_data) or []
+
+ fifo_queue = []
+ if fifo_queue := item_wise_fifo_queue.get(
+ (report_data.item_code, report_data.warehouse)
+ ):
+ fifo_queue = fifo_queue.get("fifo_queue")
+
+ if fifo_queue:
+ opening_fifo_queue.extend(fifo_queue)
+
+ stock_ageing_data = {
+ "average_age": 0,
+ "earliest_age": 0,
+ "latest_age": 0,
+ }
+ if opening_fifo_queue:
+ fifo_queue = sorted(
+ [x for x in opening_fifo_queue if _func(x)], key=_func
+ )
+ if not fifo_queue:
+ continue
+
+ to_date = self.to_date
+ stock_ageing_data["average_age"] = get_average_age(
+ fifo_queue, to_date
+ )
+ stock_ageing_data["earliest_age"] = date_diff(
+ to_date, fifo_queue[0][1]
+ )
+ stock_ageing_data["latest_age"] = date_diff(
+ to_date, fifo_queue[-1][1]
+ )
+ stock_ageing_data["fifo_queue"] = fifo_queue
+
+ report_data.update(stock_ageing_data)
+
+ report_data.update(
+ {
+ "reserved_stock": sre_details.get(
+ (report_data.item_code, report_data.warehouse), 0.0
+ )
+ }
+ )
+
+ if (
+ not self.filters.get("include_zero_stock_items")
+ and report_data
+ and report_data.bal_qty == 0
+ and report_data.bal_val == 0
+ ):
+ continue
+
+ self.data.append(report_data)
+
+ def get_item_warehouse_map(self):
+ item_warehouse_map = {}
+ self.opening_vouchers = self.get_opening_vouchers()
+
+ if self.filters.get("show_stock_ageing_data"):
+ self.sle_entries = self.sle_query.run(as_dict=True)
+
+ # HACK: This is required to avoid causing db query in flt
+ _system_settings = frappe.get_cached_doc("System Settings")
+ with frappe.db.unbuffered_cursor():
+ if not self.filters.get("show_stock_ageing_data"):
+ self.sle_entries = self.sle_query.run(as_dict=True, as_iterator=True)
+
+ for entry in self.sle_entries:
+ group_by_key = self.get_group_by_key(entry)
+ if group_by_key not in item_warehouse_map:
+ self.initialize_data(item_warehouse_map, group_by_key, entry)
+
+ self.prepare_item_warehouse_map(item_warehouse_map, entry, group_by_key)
+
+ if self.opening_data.get(group_by_key):
+ del self.opening_data[group_by_key]
+ for group_by_key, entry in self.opening_data.items():
+ if group_by_key not in item_warehouse_map:
+ self.initialize_data(item_warehouse_map, group_by_key, entry)
+ item_warehouse_map = filter_items_with_no_transactions(
+ item_warehouse_map, self.float_precision, self.inventory_dimensions
+ )
+ return item_warehouse_map
+
+ def get_sre_reserved_qty_details(self) -> dict:
+ from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+ get_sre_reserved_qty_for_items_and_warehouses as get_reserved_qty_details,
+ )
+
+ item_code_list, warehouse_list = [], []
+ for d in self.item_warehouse_map:
+ item_code_list.append(d[1])
+ warehouse_list.append(d[2])
+
+ return get_reserved_qty_details(item_code_list, warehouse_list)
+
+ def prepare_item_warehouse_map(self, item_warehouse_map, entry, group_by_key):
+ qty_dict = item_warehouse_map[group_by_key]
+ for field in self.inventory_dimensions:
+ qty_dict[field] = entry.get(field)
+
+ if entry.voucher_type == "Stock Reconciliation" and (
+ not entry.batch_no or entry.serial_no
+ ):
+ qty_diff = flt(entry.qty_after_transaction) - flt(qty_dict.bal_qty)
+ else:
+ qty_diff = flt(entry.actual_qty)
+
+ value_diff = flt(entry.stock_value_difference)
+
+ if (
+ entry.posting_date < self.from_date
+ or entry.voucher_no in self.opening_vouchers.get(entry.voucher_type, [])
+ ):
+ qty_dict.opening_qty += qty_diff
+ qty_dict.opening_val += value_diff
+
+ elif (
+ entry.posting_date >= self.from_date and entry.posting_date <= self.to_date
+ ):
+ if flt(qty_diff, self.float_precision) >= 0:
+ qty_dict.in_qty += qty_diff
+ qty_dict.in_val += value_diff
+ else:
+ qty_dict.out_qty += abs(qty_diff)
+ qty_dict.out_val += abs(value_diff)
+
+ qty_dict.val_rate = entry.valuation_rate
+ qty_dict.bal_qty += qty_diff
+ qty_dict.bal_val += value_diff
+
+ def initialize_data(self, item_warehouse_map, group_by_key, entry):
+ opening_data = self.opening_data.get(group_by_key, {})
+ item_warehouse_map[group_by_key] = frappe._dict(
+ {
+ "item_code": entry.item_code,
+ "warehouse": entry.warehouse,
+ "item_group": entry.item_group,
+ "company": entry.company,
+ "currency": self.company_currency,
+ "stock_uom": entry.stock_uom,
+ "item_name": entry.item_name,
+ "opening_qty": opening_data.get("bal_qty") or 0.0,
+ "opening_val": opening_data.get("bal_val") or 0.0,
+ "opening_fifo_queue": opening_data.get("fifo_queue") or [],
+ "in_qty": 0.0,
+ "in_val": 0.0,
+ "out_qty": 0.0,
+ "out_val": 0.0,
+ "bal_qty": opening_data.get("bal_qty") or 0.0,
+ "bal_val": opening_data.get("bal_val") or 0.0,
+ "val_rate": 0.0,
+ "rack": entry.rack,
+ }
+ )
+
+ def get_group_by_key(self, row) -> tuple:
+ group_by_key = [row.company, row.item_code, row.warehouse]
+
+ for fieldname in self.inventory_dimensions:
+ if self.filters.get(fieldname):
+ group_by_key.append(row.get(fieldname))
+
+ return tuple(group_by_key)
+
+ def get_closing_balance(self) -> list[dict[str, Any]]:
+ if self.filters.get("ignore_closing_balance"):
+ return []
+
+ table = frappe.qb.DocType("Closing Stock Balance")
+
+ query = (
+ frappe.qb.from_(table)
+ .select(table.name, table.to_date)
+ .where(
+ (table.docstatus == 1)
+ & (table.company == self.filters.company)
+ & (table.to_date <= self.from_date)
+ & (table.status == "Completed")
+ )
+ .orderby(table.to_date, order=Order.desc)
+ .limit(1)
+ )
+
+ for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]:
+ if self.filters.get(fieldname):
+ query = query.where(table[fieldname] == self.filters.get(fieldname))
+
+ return query.run(as_dict=True)
+
+ def prepare_stock_ledger_entries(self):
+ sle = frappe.qb.DocType("Stock Ledger Entry")
+ item_table = frappe.qb.DocType("Item")
+ logical_rack = frappe.qb.DocType("Logical Rack")
+ pos_profile = frappe.qb.DocType("POS Profile")
+ query = (
+ frappe.qb.from_(sle)
+ .inner_join(item_table)
+ .on(sle.item_code == item_table.name)
+ .inner_join(logical_rack)
+ .on(logical_rack.pos_profile == self.filters.get("pos_profile"))
+ .inner_join(pos_profile)
+ .on(pos_profile.warehouse == sle.warehouse)
+ .select(
+ sle.item_code,
+ sle.warehouse,
+ sle.posting_date,
+ sle.actual_qty,
+ sle.valuation_rate,
+ sle.company,
+ sle.voucher_type,
+ sle.qty_after_transaction,
+ sle.stock_value_difference,
+ sle.item_code.as_("name"),
+ sle.voucher_no,
+ sle.stock_value,
+ sle.batch_no,
+ sle.serial_no,
+ sle.serial_and_batch_bundle,
+ sle.has_serial_no,
+ item_table.item_group,
+ item_table.stock_uom,
+ item_table.item_name,
+ logical_rack.rack_id.as_("rack"),
+ )
+ .where((sle.docstatus < 2) & (sle.is_cancelled == 0))
+ .where(logical_rack.item == sle.item_code)
+ .orderby(sle.posting_datetime)
+ .orderby(sle.creation)
+ .orderby(sle.actual_qty)
+ )
+
+ query = self.apply_inventory_dimensions_filters(query, sle)
+ query = self.apply_warehouse_filters(query, sle)
+ query = self.apply_items_filters(query, item_table)
+ query = self.apply_date_filters(query, sle)
+
+ if self.filters.get("company"):
+ query = query.where(sle.company == self.filters.get("company"))
+
+ self.sle_query = query
+
+ def apply_inventory_dimensions_filters(self, query, sle) -> str:
+ inventory_dimension_fields = self.get_inventory_dimension_fields()
+ if inventory_dimension_fields:
+ for fieldname in inventory_dimension_fields:
+ query = query.select(fieldname)
+ if self.filters.get(fieldname):
+ query = query.where(
+ sle[fieldname].isin(self.filters.get(fieldname))
+ )
+
+ return query
+
+ def apply_warehouse_filters(self, query, sle) -> str:
+ warehouse_table = frappe.qb.DocType("Warehouse")
+
+ if self.filters.get("warehouse"):
+ query = apply_warehouse_filter(query, sle, self.filters)
+ elif warehouse_type := self.filters.get("warehouse_type"):
+ query = (
+ query.join(warehouse_table)
+ .on(warehouse_table.name == sle.warehouse)
+ .where(warehouse_table.warehouse_type == warehouse_type)
+ )
+
+ return query
+
+ def apply_items_filters(self, query, item_table) -> str:
+ if item_group := self.filters.get("item_group"):
+ children = get_descendants_of(
+ "Item Group", item_group, ignore_permissions=True
+ )
+ query = query.where(item_table.item_group.isin([*children, item_group]))
+
+ for field in ["item_code", "brand"]:
+ if not self.filters.get(field):
+ continue
+ elif field == "item_code":
+ query = query.where(item_table.name == self.filters.get(field))
+ else:
+ query = query.where(item_table[field] == self.filters.get(field))
+
+ return query
+
+ def apply_date_filters(self, query, sle) -> str:
+ if not self.filters.ignore_closing_balance and self.start_from:
+ query = query.where(sle.posting_date >= self.start_from)
+
+ if self.to_date:
+ query = query.where(sle.posting_date <= self.to_date)
+
+ return query
+
+ def get_columns(self):
+ columns = [
+ {
+ "label": _("Item"),
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": 100,
+ },
+ {"label": _("Item Name"), "fieldname": "item_name", "width": 150},
+ {
+ "label": _("Item Group"),
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "options": "Item Group",
+ "width": 100,
+ },
+ {
+ "label": _("Warehouse"),
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "options": "Warehouse",
+ "width": 100,
+ },
+ {
+ "label": _("Rack"),
+ "fieldname": "rack",
+ "fieldtype": "Data",
+ "width": 100,
+ },
+ ]
+
+ for dimension in get_inventory_dimensions():
+ columns.append(
+ {
+ "label": _(dimension.doctype),
+ "fieldname": dimension.fieldname,
+ "fieldtype": "Link",
+ "options": dimension.doctype,
+ "width": 110,
+ }
+ )
+
+ columns.extend(
+ [
+ {
+ "label": _("Stock UOM"),
+ "fieldname": "stock_uom",
+ "fieldtype": "Link",
+ "options": "UOM",
+ "width": 90,
+ },
+ {
+ "label": _("Balance Qty"),
+ "fieldname": "bal_qty",
+ "fieldtype": "Float",
+ "width": 100,
+ "convertible": "qty",
+ },
+ {
+ "label": _("Balance Value"),
+ "fieldname": "bal_val",
+ "fieldtype": "Currency",
+ "width": 100,
+ "options": "Company:company:default_currency",
+ },
+ {
+ "label": _("Opening Qty"),
+ "fieldname": "opening_qty",
+ "fieldtype": "Float",
+ "width": 100,
+ "convertible": "qty",
+ },
+ {
+ "label": _("Opening Value"),
+ "fieldname": "opening_val",
+ "fieldtype": "Currency",
+ "width": 110,
+ "options": "Company:company:default_currency",
+ },
+ {
+ "label": _("In Qty"),
+ "fieldname": "in_qty",
+ "fieldtype": "Float",
+ "width": 80,
+ "convertible": "qty",
+ },
+ {
+ "label": _("In Value"),
+ "fieldname": "in_val",
+ "fieldtype": "Float",
+ "width": 80,
+ },
+ {
+ "label": _("Out Qty"),
+ "fieldname": "out_qty",
+ "fieldtype": "Float",
+ "width": 80,
+ "convertible": "qty",
+ },
+ {
+ "label": _("Out Value"),
+ "fieldname": "out_val",
+ "fieldtype": "Float",
+ "width": 80,
+ },
+ {
+ "label": _("Valuation Rate"),
+ "fieldname": "val_rate",
+ "fieldtype": self.filters.valuation_field_type or "Currency",
+ "width": 90,
+ "convertible": "rate",
+ "options": "Company:company:default_currency"
+ if self.filters.valuation_field_type == "Currency"
+ else None,
+ },
+ {
+ "label": _("Reserved Stock"),
+ "fieldname": "reserved_stock",
+ "fieldtype": "Float",
+ "width": 80,
+ "convertible": "qty",
+ },
+ {
+ "label": _("Company"),
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "options": "Company",
+ "width": 100,
+ },
+ ]
+ )
+
+ if self.filters.get("show_stock_ageing_data"):
+ columns += [
+ {"label": _("Average Age"), "fieldname": "average_age", "width": 100},
+ {"label": _("Earliest Age"), "fieldname": "earliest_age", "width": 100},
+ {"label": _("Latest Age"), "fieldname": "latest_age", "width": 100},
+ ]
+
+ if self.filters.get("show_variant_attributes"):
+ columns += [
+ {"label": att_name, "fieldname": att_name, "width": 100}
+ for att_name in get_variants_attributes()
+ ]
+
+ return columns
+
+ def add_additional_uom_columns(self):
+ if not self.filters.get("include_uom"):
+ return
+
+ conversion_factors = self.get_itemwise_conversion_factor()
+ add_additional_uom_columns(
+ self.columns, self.data, self.filters.include_uom, conversion_factors
+ )
+
+ def get_itemwise_conversion_factor(self):
+ items = []
+ if self.filters.item_code or self.filters.item_group:
+ items = [d.item_code for d in self.data]
+
+ table = frappe.qb.DocType("UOM Conversion Detail")
+ query = (
+ frappe.qb.from_(table)
+ .select(
+ table.conversion_factor,
+ table.parent,
+ )
+ .where(
+ (table.parenttype == "Item") & (table.uom == self.filters.include_uom)
+ )
+ )
+
+ if items:
+ query = query.where(table.parent.isin(items))
+
+ result = query.run(as_dict=1)
+ if not result:
+ return {}
+
+ return {d.parent: d.conversion_factor for d in result}
+
+ def get_variant_values_for(self):
+ """Returns variant values for items."""
+ attribute_map = {}
+ items = []
+ if self.filters.item_code or self.filters.item_group:
+ items = [d.item_code for d in self.data]
+
+ filters = {}
+ if items:
+ filters = {"parent": ("in", items)}
+
+ attribute_info = frappe.get_all(
+ "Item Variant Attribute",
+ fields=["parent", "attribute", "attribute_value"],
+ filters=filters,
+ )
+
+ for attr in attribute_info:
+ attribute_map.setdefault(attr["parent"], {})
+ attribute_map[attr["parent"]].update(
+ {attr["attribute"]: attr["attribute_value"]}
+ )
+
+ return attribute_map
+
+ def get_opening_vouchers(self):
+ opening_vouchers = {"Stock Entry": [], "Stock Reconciliation": []}
+
+ se = frappe.qb.DocType("Stock Entry")
+ sr = frappe.qb.DocType("Stock Reconciliation")
+
+ vouchers_data = (
+ frappe.qb.from_(
+ (
+ frappe.qb.from_(se)
+ .select(se.name, Coalesce("Stock Entry").as_("voucher_type"))
+ .where(
+ (se.docstatus == 1)
+ & (se.posting_date <= self.to_date)
+ & (se.is_opening == "Yes")
+ )
+ )
+ + (
+ frappe.qb.from_(sr)
+ .select(
+ sr.name, Coalesce("Stock Reconciliation").as_("voucher_type")
+ )
+ .where(
+ (sr.docstatus == 1)
+ & (sr.posting_date <= self.to_date)
+ & (sr.purpose == "Opening Stock")
+ )
+ )
+ ).select("voucher_type", "name")
+ ).run(as_dict=True)
+
+ if vouchers_data:
+ for d in vouchers_data:
+ opening_vouchers[d.voucher_type].append(d.name)
+
+ return opening_vouchers
+
+ @staticmethod
+ def get_inventory_dimension_fields():
+ return [dimension.fieldname for dimension in get_inventory_dimensions()]
+
+ @staticmethod
+ def get_opening_fifo_queue(report_data):
+ opening_fifo_queue = report_data.get("opening_fifo_queue") or []
+ for row in opening_fifo_queue:
+ row[1] = getdate(row[1])
+
+ return opening_fifo_queue
def filter_items_with_no_transactions(
- iwb_map, float_precision: float, inventory_dimensions: list | None = None
+ iwb_map, float_precision: float, inventory_dimensions: list | None = None
):
- pop_keys = []
- for group_by_key in iwb_map:
- qty_dict = iwb_map[group_by_key]
-
- no_transactions = True
- for key, val in qty_dict.items():
- if inventory_dimensions and key in inventory_dimensions:
- continue
-
- if key in [
- "item_code",
- "warehouse",
- "item_name",
- "item_group",
- "project",
- "stock_uom",
- "company",
- "opening_fifo_queue",
- "rack",
- ]:
- continue
-
- val = flt(val, float_precision)
- qty_dict[key] = val
- if key != "val_rate" and val:
- no_transactions = False
-
- if no_transactions:
- pop_keys.append(group_by_key)
-
- for key in pop_keys:
- iwb_map.pop(key)
- return iwb_map
+ pop_keys = []
+ for group_by_key in iwb_map:
+ qty_dict = iwb_map[group_by_key]
+
+ no_transactions = True
+ for key, val in qty_dict.items():
+ if inventory_dimensions and key in inventory_dimensions:
+ continue
+
+ if key in [
+ "item_code",
+ "warehouse",
+ "item_name",
+ "item_group",
+ "project",
+ "stock_uom",
+ "company",
+ "opening_fifo_queue",
+ "rack",
+ ]:
+ continue
+
+ val = flt(val, float_precision)
+ qty_dict[key] = val
+ if key != "val_rate" and val:
+ no_transactions = False
+
+ if no_transactions:
+ pop_keys.append(group_by_key)
+
+ for key in pop_keys:
+ iwb_map.pop(key)
+ return iwb_map
def get_variants_attributes() -> list[str]:
- """Return all item variant attributes."""
- return frappe.get_all("Item Attribute", pluck="name")
+ """Return all item variant attributes."""
+ return frappe.get_all("Item Attribute", pluck="name")
diff --git a/posnext/public/dist/js/posnext.bundle.TN4KQRHJ.js b/posnext/public/dist/js/posnext.bundle.TN4KQRHJ.js
index 623e630..c5f13be 100644
--- a/posnext/public/dist/js/posnext.bundle.TN4KQRHJ.js
+++ b/posnext/public/dist/js/posnext.bundle.TN4KQRHJ.js
@@ -928,7 +928,7 @@
` + tir + `
-
+