fix: multiple pos issues (#23297)

* fix: returns can be made against unconsolidated invoices

* fix: indentation

* fix: mode of payment not fetching for pos returns

* patch: default pos profile print format

* fix: tests

* chore: clean up retail desk page
This commit is contained in:
Saqib 2020-09-10 19:28:46 +05:30 committed by GitHub
parent ec6a97fb6a
commit cd89994b33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 521 additions and 495 deletions

View File

@ -43,7 +43,7 @@
{ {
"hidden": 0, "hidden": 0,
"label": "Bank Statement", "label": "Bank Statement",
"links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"label\": \"Bank Clearance\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]" "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]"
}, },
{ {
"hidden": 0, "hidden": 0,
@ -98,7 +98,7 @@
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"label": "Accounting", "label": "Accounting",
"modified": "2020-09-03 10:37:07.865801", "modified": "2020-09-09 11:45:33.766400",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting", "name": "Accounting",
@ -147,11 +147,6 @@
"link_to": "Trial Balance", "link_to": "Trial Balance",
"type": "Report" "type": "Report"
}, },
{
"label": "Point of Sale",
"link_to": "point-of-sale",
"type": "Page"
},
{ {
"label": "Dashboard", "label": "Dashboard",
"link_to": "Accounts", "link_to": "Accounts",

View File

@ -55,14 +55,48 @@ frappe.ui.form.on('POS Closing Entry', {
}, },
callback: (r) => { callback: (r) => {
let pos_docs = r.message; let pos_docs = r.message;
set_form_data(pos_docs, frm) set_form_data(pos_docs, frm);
refresh_fields(frm) refresh_fields(frm);
set_html_data(frm) set_html_data(frm);
} }
}) })
} }
}); });
cur_frm.cscript.before_pos_transactions_remove = function(doc, cdt, cdn) {
const removed_row = locals[cdt][cdn];
if (!removed_row.pos_invoice) return;
frappe.db.get_doc("POS Invoice", removed_row.pos_invoice).then(doc => {
cur_frm.doc.grand_total -= flt(doc.grand_total);
cur_frm.doc.net_total -= flt(doc.net_total);
cur_frm.doc.total_quantity -= flt(doc.total_qty);
refresh_payments(doc, cur_frm, 1);
refresh_taxes(doc, cur_frm, 1);
refresh_fields(cur_frm);
set_html_data(cur_frm);
});
}
frappe.ui.form.on('POS Invoice Reference', {
pos_invoice(frm, cdt, cdn) {
const added_row = locals[cdt][cdn];
if (!added_row.pos_invoice) return;
frappe.db.get_doc("POS Invoice", added_row.pos_invoice).then(doc => {
frm.doc.grand_total += flt(doc.grand_total);
frm.doc.net_total += flt(doc.net_total);
frm.doc.total_quantity += flt(doc.total_qty);
refresh_payments(doc, frm);
refresh_taxes(doc, frm);
refresh_fields(frm);
set_html_data(frm);
});
}
})
frappe.ui.form.on('POS Closing Entry Detail', { frappe.ui.form.on('POS Closing Entry Detail', {
closing_amount: (frm, cdt, cdn) => { closing_amount: (frm, cdt, cdn) => {
const row = locals[cdt][cdn]; const row = locals[cdt][cdn];
@ -76,8 +110,8 @@ function set_form_data(data, frm) {
frm.doc.grand_total += flt(d.grand_total); frm.doc.grand_total += flt(d.grand_total);
frm.doc.net_total += flt(d.net_total); frm.doc.net_total += flt(d.net_total);
frm.doc.total_quantity += flt(d.total_qty); frm.doc.total_quantity += flt(d.total_qty);
add_to_payments(d, frm); refresh_payments(d, frm);
add_to_taxes(d, frm); refresh_taxes(d, frm);
}); });
} }
@ -90,11 +124,12 @@ function add_to_pos_transaction(d, frm) {
}) })
} }
function add_to_payments(d, frm) { function refresh_payments(d, frm, remove) {
d.payments.forEach(p => { d.payments.forEach(p => {
const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment); const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment);
if (payment) { if (payment) {
payment.expected_amount += flt(p.amount); if (!remove) payment.expected_amount += flt(p.amount);
else payment.expected_amount -= flt(p.amount);
} else { } else {
frm.add_child("payment_reconciliation", { frm.add_child("payment_reconciliation", {
mode_of_payment: p.mode_of_payment, mode_of_payment: p.mode_of_payment,
@ -105,11 +140,12 @@ function add_to_payments(d, frm) {
}) })
} }
function add_to_taxes(d, frm) { function refresh_taxes(d, frm, remove) {
d.taxes.forEach(t => { d.taxes.forEach(t => {
const tax = frm.doc.taxes.find(tx => tx.account_head === t.account_head && tx.rate === t.rate); const tax = frm.doc.taxes.find(tx => tx.account_head === t.account_head && tx.rate === t.rate);
if (tax) { if (tax) {
tax.amount += flt(t.tax_amount); if (!remove) tax.amount += flt(t.tax_amount);
else tax.amount -= flt(t.tax_amount);
} else { } else {
frm.add_child("taxes", { frm.add_child("taxes", {
account_head: t.account_head, account_head: t.account_head,

View File

@ -279,7 +279,8 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Return (Credit Note)", "label": "Is Return (Credit Note)",
"no_copy": 1, "no_copy": 1,
"print_hide": 1 "print_hide": 1,
"set_only_once": 1
}, },
{ {
"fieldname": "column_break1", "fieldname": "column_break1",
@ -1578,9 +1579,10 @@
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-05-29 15:08:39.337385", "modified": "2020-09-07 12:43:09.138720",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice", "name": "POS Invoice",

View File

@ -182,8 +182,9 @@ class TestPOSInvoice(unittest.TestCase):
def test_pos_returns_with_repayment(self): def test_pos_returns_with_repayment(self):
pos = create_pos_invoice(qty = 10, do_not_save=True) pos = create_pos_invoice(qty = 10, do_not_save=True)
pos.set('payments', [])
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500}) pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500}) pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500, 'default': 1})
pos.insert() pos.insert()
pos.submit() pos.submit()
@ -200,8 +201,9 @@ class TestPOSInvoice(unittest.TestCase):
income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105, income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105,
cost_center = "Main - _TC", do_not_save=True) cost_center = "Main - _TC", do_not_save=True)
pos.set('payments', [])
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 50}) pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 50})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60}) pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60, 'default': 1})
pos.insert() pos.insert()
pos.submit() pos.submit()

View File

@ -24,11 +24,20 @@ class POSInvoiceMergeLog(Document):
def validate_pos_invoice_status(self): def validate_pos_invoice_status(self):
for d in self.pos_invoices: for d in self.pos_invoices:
status, docstatus = frappe.db.get_value('POS Invoice', d.pos_invoice, ['status', 'docstatus']) status, docstatus, is_return, return_against = frappe.db.get_value(
'POS Invoice', d.pos_invoice, ['status', 'docstatus', 'is_return', 'return_against'])
if docstatus != 1: if docstatus != 1:
frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, d.pos_invoice)) frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, d.pos_invoice))
if status in ['Consolidated']: if status == "Consolidated":
frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, d.pos_invoice, status)) frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, d.pos_invoice, status))
if is_return and return_against not in [d.pos_invoice for d in self.pos_invoices] and status != "Consolidated":
# if return entry is not getting merged in the current pos closing and if it is not consolidated
frappe.throw(
_("Row #{}: Return Invoice {} cannot be made against unconsolidated invoice. \
You can add original invoice {} manually to proceed.")
.format(d.idx, frappe.bold(d.pos_invoice), frappe.bold(return_against))
)
def on_submit(self): def on_submit(self):
pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
@ -36,12 +45,12 @@ class POSInvoiceMergeLog(Document):
returns = [d for d in pos_invoice_docs if d.get('is_return') == 1] 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] sales = [d for d in pos_invoice_docs if d.get('is_return') == 0]
sales_invoice = self.process_merging_into_sales_invoice(sales) sales_invoice, credit_note = "", ""
if sales:
sales_invoice = self.process_merging_into_sales_invoice(sales)
if len(returns): if returns:
credit_note = self.process_merging_into_credit_note(returns) credit_note = self.process_merging_into_credit_note(returns)
else:
credit_note = ""
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log

View File

@ -242,7 +242,8 @@ def make_return_doc(doctype, source_name, target_doc=None):
'type': data.type, 'type': data.type,
'amount': -1 * paid_amount, 'amount': -1 * paid_amount,
'base_amount': -1 * base_paid_amount, 'base_amount': -1 * base_paid_amount,
'account': data.account 'account': data.account,
'default': data.default
}) })
if doc.is_pos: if doc.is_pos:
doc.paid_amount = -1 * source.paid_amount doc.paid_amount = -1 * source.paid_amount

View File

@ -725,3 +725,4 @@ erpnext.patches.v12_0.rename_lost_reason_detail
erpnext.patches.v13_0.drop_razorpay_payload_column erpnext.patches.v13_0.drop_razorpay_payload_column
erpnext.patches.v13_0.update_start_end_date_for_old_shift_assignment erpnext.patches.v13_0.update_start_end_date_for_old_shift_assignment
erpnext.patches.v13_0.setting_custom_roles_for_some_regional_reports erpnext.patches.v13_0.setting_custom_roles_for_some_regional_reports
erpnext.patches.v13_0.change_default_pos_print_format

View File

@ -0,0 +1,8 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.db.sql(
"""UPDATE `tabPOS Profile` profile
SET profile.`print_format` = 'POS Invoice'
WHERE profile.`print_format` = 'Point of Sale'""")

View File

@ -673,23 +673,14 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
); );
} }
frappe.db.get_value('Sales Invoice Payment', {'parent': this.frm.doc.pos_profile, 'default': 1}, this.frm.doc.payments.find(pay => {
['mode_of_payment', 'account', 'type'], (value) => { if (pay.default) {
if (this.frm.is_dirty()) { pay.amount = total_amount_to_pay;
frappe.model.clear_table(this.frm.doc, 'payments'); } else {
if (value) { pay.amount = 0.0
let row = frappe.model.add_child(this.frm.doc, 'Sales Invoice Payment', 'payments'); }
row.mode_of_payment = value.mode_of_payment; });
row.type = value.type; this.frm.refresh_fields();
row.account = value.account;
row.default = 1;
row.amount = total_amount_to_pay;
} else {
this.frm.set_value('is_pos', 1);
}
this.frm.refresh_fields();
}
}, 'Sales Invoice');
this.calculate_paid_amount(); this.calculate_paid_amount();
}, },

View File

@ -2,8 +2,18 @@
"cards": [ "cards": [
{ {
"hidden": 0, "hidden": 0,
"label": "Retail Operations", "label": "Settings & Configurations",
"links": "[\n {\n \"description\": \"Setup default values for POS Invoices\",\n \"label\": \"Point of Sale Profile\",\n \"name\": \"POS Profile\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"POS Profile\"\n ],\n \"description\": \"Point of Sale\",\n \"label\": \"Point of Sale\",\n \"name\": \"point-of-sale\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"description\": \"Setup mode of POS (Online / Offline)\",\n \"label\": \"POS Settings\",\n \"name\": \"POS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Cashier Closing\",\n \"label\": \"Cashier Closing\",\n \"name\": \"Cashier Closing\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To make Customer based incentive schemes.\",\n \"label\": \"Loyalty Program\",\n \"name\": \"Loyalty Program\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To view logs of Loyalty Points assigned to a Customer.\",\n \"label\": \"Loyalty Point Entry\",\n \"name\": \"Loyalty Point Entry\",\n \"type\": \"doctype\"\n }\n]" "links": "[\n {\n \"description\": \"Setup default values for POS Invoices\",\n \"label\": \"Point-of-Sale Profile\",\n \"name\": \"POS Profile\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"POS Settings\",\n \"name\": \"POS Settings\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
"label": "Loyalty Program",
"links": "[\n {\n \"description\": \"To make Customer based incentive schemes.\",\n \"label\": \"Loyalty Program\",\n \"name\": \"Loyalty Program\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To view logs of Loyalty Points assigned to a Customer.\",\n \"label\": \"Loyalty Point Entry\",\n \"name\": \"Loyalty Point Entry\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
"label": "Opening & Closing",
"links": "[\n {\n \"label\": \"POS Opening Entry\",\n \"name\": \"POS Opening Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"POS Closing Entry\",\n \"name\": \"POS Closing Entry\",\n \"type\": \"doctype\"\n }\n]"
} }
], ],
"category": "Domains", "category": "Domains",
@ -18,7 +28,7 @@
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"label": "Retail", "label": "Retail",
"modified": "2020-08-20 18:00:07.515691", "modified": "2020-09-09 11:46:28.297435",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Retail", "name": "Retail",
@ -28,25 +38,10 @@
"restrict_to_domain": "Retail", "restrict_to_domain": "Retail",
"shortcuts": [ "shortcuts": [
{ {
"color": "#9deca2",
"doc_view": "", "doc_view": "",
"format": "{} Active", "label": "Point Of Sale",
"label": "Point of Sale Profile",
"link_to": "POS Profile",
"stats_filter": "{\n \"disabled\": 0\n}",
"type": "DocType"
},
{
"doc_view": "",
"label": "Point of Sale",
"link_to": "point-of-sale", "link_to": "point-of-sale",
"type": "Page" "type": "Page"
},
{
"doc_view": "",
"label": "POS Settings",
"link_to": "POS Settings",
"type": "DocType"
} }
] ]
} }

View File

@ -9,7 +9,7 @@ frappe.pages['point-of-sale'].on_page_load = function(wrapper) {
title: __('Point of Sale'), title: __('Point of Sale'),
single_column: true single_column: true
}); });
// online
wrapper.pos = new erpnext.PointOfSale.Controller(wrapper); wrapper.pos = new erpnext.PointOfSale.Controller(wrapper);
window.cur_pos = wrapper.pos; window.cur_pos = wrapper.pos;
}; };

View File

@ -8,7 +8,7 @@
{% include "erpnext/selling/page/point_of_sale/pos_past_order_summary.js" %} {% include "erpnext/selling/page/point_of_sale/pos_past_order_summary.js" %}
erpnext.PointOfSale.Controller = class { erpnext.PointOfSale.Controller = class {
constructor(wrapper) { constructor(wrapper) {
this.wrapper = $(wrapper).find('.layout-main-section'); this.wrapper = $(wrapper).find('.layout-main-section');
this.page = wrapper.page; this.page = wrapper.page;
@ -36,7 +36,7 @@ erpnext.PointOfSale.Controller = class {
const table_fields = [ const table_fields = [
{ fieldname: "mode_of_payment", fieldtype: "Link", in_list_view: 1, label: "Mode of Payment", options: "Mode of Payment", reqd: 1 }, { fieldname: "mode_of_payment", fieldtype: "Link", in_list_view: 1, label: "Mode of Payment", options: "Mode of Payment", reqd: 1 },
{ fieldname: "opening_amount", fieldtype: "Currency", default: 0, in_list_view: 1, label: "Opening Amount", { fieldname: "opening_amount", fieldtype: "Currency", default: 0, in_list_view: 1, label: "Opening Amount",
options: "company:company_currency", reqd: 1 } options: "company:company_currency" }
]; ];
const dialog = new frappe.ui.Dialog({ const dialog = new frappe.ui.Dialog({
@ -51,29 +51,16 @@ erpnext.PointOfSale.Controller = class {
options: 'POS Profile', fieldname: 'pos_profile', reqd: 1, options: 'POS Profile', fieldname: 'pos_profile', reqd: 1,
onchange: () => { onchange: () => {
const pos_profile = dialog.fields_dict.pos_profile.get_value(); const pos_profile = dialog.fields_dict.pos_profile.get_value();
const company = dialog.fields_dict.company.get_value();
const user = frappe.session.user
if (!pos_profile || !company || !user) return; if (!pos_profile) return;
// auto fetch last closing entry's balance details frappe.db.get_doc("POS Profile", pos_profile).then(doc => {
frappe.db.get_list("POS Closing Entry", { dialog.fields_dict.balance_details.df.data = [];
filters: { company, pos_profile, user }, doc.payments.forEach(pay => {
limit: 1, const { mode_of_payment } = pay;
order_by: 'period_end_date desc' dialog.fields_dict.balance_details.df.data.push({ mode_of_payment });
}).then((res) => {
if (!res.length) return;
const pos_closing_entry = res[0];
frappe.db.get_doc("POS Closing Entry", pos_closing_entry.name).then(({ payment_reconciliation }) => {
dialog.fields_dict.balance_details.df.data = [];
payment_reconciliation.forEach(pay => {
const { mode_of_payment } = pay;
dialog.fields_dict.balance_details.df.data.push({
mode_of_payment: mode_of_payment
});
});
dialog.fields_dict.balance_details.grid.refresh();
}); });
dialog.fields_dict.balance_details.grid.refresh();
}); });
} }
}, },

View File

@ -1,36 +1,36 @@
erpnext.PointOfSale.ItemCart = class { erpnext.PointOfSale.ItemCart = class {
constructor({ wrapper, events }) { constructor({ wrapper, events }) {
this.wrapper = wrapper; this.wrapper = wrapper;
this.events = events; this.events = events;
this.customer_info = undefined; this.customer_info = undefined;
this.init_component(); this.init_component();
} }
init_component() { init_component() {
this.prepare_dom(); this.prepare_dom();
this.init_child_components(); this.init_child_components();
this.bind_events(); this.bind_events();
this.attach_shortcuts(); this.attach_shortcuts();
} }
prepare_dom() { prepare_dom() {
this.wrapper.append( this.wrapper.append(
`<section class="col-span-4 flex flex-col shadow rounded item-cart bg-white mx-h-70 h-100"></section>` `<section class="col-span-4 flex flex-col shadow rounded item-cart bg-white mx-h-70 h-100"></section>`
) )
this.$component = this.wrapper.find('.item-cart'); this.$component = this.wrapper.find('.item-cart');
} }
init_child_components() { init_child_components() {
this.init_customer_selector(); this.init_customer_selector();
this.init_cart_components(); this.init_cart_components();
} }
init_customer_selector() { init_customer_selector() {
this.$component.append( this.$component.append(
`<div class="customer-section rounded flex flex-col m-8 mb-0"></div>` `<div class="customer-section rounded flex flex-col m-8 mb-0"></div>`
) )
this.$customer_section = this.$component.find('.customer-section'); this.$customer_section = this.$component.find('.customer-section');
} }
@ -42,8 +42,8 @@ erpnext.PointOfSale.ItemCart = class {
this.customer_field.set_focus(); this.customer_field.set_focus();
} }
init_cart_components() { init_cart_components() {
this.$component.append( this.$component.append(
`<div class="cart-container flex flex-col items-center rounded flex-1 relative"> `<div class="cart-container flex flex-col items-center rounded flex-1 relative">
<div class="absolute flex flex-col p-8 pt-0 w-full h-full"> <div class="absolute flex flex-col p-8 pt-0 w-full h-full">
<div class="flex text-grey cart-header pt-2 pb-2 p-4 mt-2 mb-2 w-full f-shrink-0"> <div class="flex text-grey cart-header pt-2 pb-2 p-4 mt-2 mb-2 w-full f-shrink-0">
@ -55,23 +55,23 @@ erpnext.PointOfSale.ItemCart = class {
<div class="cart-totals-section flex flex-col w-full mt-4 f-shrink-0"></div> <div class="cart-totals-section flex flex-col w-full mt-4 f-shrink-0"></div>
<div class="numpad-section flex flex-col mt-4 d-none w-full p-8 pt-0 pb-0 f-shrink-0"></div> <div class="numpad-section flex flex-col mt-4 d-none w-full p-8 pt-0 pb-0 f-shrink-0"></div>
</div> </div>
</div>` </div>`
); );
this.$cart_container = this.$component.find('.cart-container'); this.$cart_container = this.$component.find('.cart-container');
this.make_cart_totals_section(); this.make_cart_totals_section();
this.make_cart_items_section(); this.make_cart_items_section();
this.make_cart_numpad(); this.make_cart_numpad();
} }
make_cart_items_section() { make_cart_items_section() {
this.$cart_header = this.$component.find('.cart-header'); this.$cart_header = this.$component.find('.cart-header');
this.$cart_items_wrapper = this.$component.find('.cart-items-section'); this.$cart_items_wrapper = this.$component.find('.cart-items-section');
this.make_no_items_placeholder(); this.make_no_items_placeholder();
} }
make_no_items_placeholder() { make_no_items_placeholder() {
this.$cart_header.addClass('d-none'); this.$cart_header.addClass('d-none');
this.$cart_items_wrapper.html( this.$cart_items_wrapper.html(
`<div class="no-item-wrapper flex items-center h-18"> `<div class="no-item-wrapper flex items-center h-18">
@ -81,8 +81,8 @@ erpnext.PointOfSale.ItemCart = class {
this.$cart_items_wrapper.addClass('mt-4 border-grey border-dashed'); this.$cart_items_wrapper.addClass('mt-4 border-grey border-dashed');
} }
make_cart_totals_section() { make_cart_totals_section() {
this.$totals_section = this.$component.find('.cart-totals-section'); this.$totals_section = this.$component.find('.cart-totals-section');
this.$totals_section.append( this.$totals_section.append(
`<div class="add-discount flex items-center pt-4 pb-4 pr-4 pl-4 text-grey pointer no-select d-none"> `<div class="add-discount flex items-center pt-4 pb-4 pr-4 pl-4 text-grey pointer no-select d-none">
@ -116,9 +116,9 @@ erpnext.PointOfSale.ItemCart = class {
) )
this.$add_discount_elem = this.$component.find(".add-discount"); this.$add_discount_elem = this.$component.find(".add-discount");
} }
make_cart_numpad() { make_cart_numpad() {
this.$numpad_section = this.$component.find('.numpad-section'); this.$numpad_section = this.$component.find('.numpad-section');
this.number_pad = new erpnext.PointOfSale.NumberPad({ this.number_pad = new erpnext.PointOfSale.NumberPad({
@ -155,9 +155,9 @@ erpnext.PointOfSale.ItemCart = class {
Checkout Checkout
</div>` </div>`
) )
} }
bind_events() { bind_events() {
const me = this; const me = this;
this.$customer_section.on('click', '.add-remove-customer', function (e) { this.$customer_section.on('click', '.add-remove-customer', function (e) {
const customer_info_is_visible = me.$cart_container.hasClass('d-none'); const customer_info_is_visible = me.$cart_container.hasClass('d-none');
@ -382,7 +382,7 @@ erpnext.PointOfSale.ItemCart = class {
); );
} }
update_customer_section() { update_customer_section() {
const { customer, email_id='', mobile_no='', image } = this.customer_info || {}; const { customer, email_id='', mobile_no='', image } = this.customer_info || {};
if (customer) { if (customer) {
@ -403,7 +403,7 @@ erpnext.PointOfSale.ItemCart = class {
</div>` </div>`
); );
} else { } else {
// reset customer selector // reset customer selector
this.reset_customer_selector(); this.reset_customer_selector();
} }
@ -430,9 +430,9 @@ erpnext.PointOfSale.ItemCart = class {
</div>` </div>`
} }
} }
} }
update_totals_section(frm) { update_totals_section(frm) {
if (!frm) frm = this.events.get_frm(); if (!frm) frm = this.events.get_frm();
this.render_net_total(frm.doc.base_net_total); this.render_net_total(frm.doc.base_net_total);
@ -440,9 +440,9 @@ erpnext.PointOfSale.ItemCart = class {
const taxes = frm.doc.taxes.map(t => { return { description: t.description, rate: t.rate }}) const taxes = frm.doc.taxes.map(t => { return { description: t.description, rate: t.rate }})
this.render_taxes(frm.doc.base_total_taxes_and_charges, taxes); this.render_taxes(frm.doc.base_total_taxes_and_charges, taxes);
} }
render_net_total(value) { render_net_total(value) {
const currency = this.events.get_frm().doc.currency; const currency = this.events.get_frm().doc.currency;
this.$totals_section.find('.net-total').html( this.$totals_section.find('.net-total').html(
`<div class="flex flex-col"> `<div class="flex flex-col">
@ -454,9 +454,9 @@ erpnext.PointOfSale.ItemCart = class {
) )
this.$numpad_section.find('.numpad-net-total').html(`Net Total: <span class="text-bold">${format_currency(value, currency)}</span>`) this.$numpad_section.find('.numpad-net-total').html(`Net Total: <span class="text-bold">${format_currency(value, currency)}</span>`)
} }
render_grand_total(value) { render_grand_total(value) {
const currency = this.events.get_frm().doc.currency; const currency = this.events.get_frm().doc.currency;
this.$totals_section.find('.grand-total').html( this.$totals_section.find('.grand-total').html(
`<div class="flex flex-col"> `<div class="flex flex-col">
@ -495,20 +495,20 @@ erpnext.PointOfSale.ItemCart = class {
} else { } else {
this.$totals_section.find('.taxes').html('') this.$totals_section.find('.taxes').html('')
} }
} }
get_cart_item({ item_code, batch_no, uom }) { get_cart_item({ item_code, batch_no, uom }) {
const batch_attr = `[data-batch-no="${escape(batch_no)}"]`; const batch_attr = `[data-batch-no="${escape(batch_no)}"]`;
const item_code_attr = `[data-item-code="${escape(item_code)}"]`; const item_code_attr = `[data-item-code="${escape(item_code)}"]`;
const uom_attr = `[data-uom=${escape(uom)}]`; const uom_attr = `[data-uom=${escape(uom)}]`;
const item_selector = batch_no ? const item_selector = batch_no ?
`.cart-item-wrapper${batch_attr}${uom_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}`; `.cart-item-wrapper${batch_attr}${uom_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}`;
return this.$cart_items_wrapper.find(item_selector); return this.$cart_items_wrapper.find(item_selector);
} }
update_item_html(item, remove_item) { update_item_html(item, remove_item) {
const $item = this.get_cart_item(item); const $item = this.get_cart_item(item);
if (remove_item) { if (remove_item) {
@ -528,29 +528,29 @@ erpnext.PointOfSale.ItemCart = class {
this.update_empty_cart_section(no_of_cart_items); this.update_empty_cart_section(no_of_cart_items);
} }
render_cart_item(item_data, $item_to_update) { render_cart_item(item_data, $item_to_update) {
const currency = this.events.get_frm().doc.currency; const currency = this.events.get_frm().doc.currency;
const me = this; const me = this;
if (!$item_to_update.length) { if (!$item_to_update.length) {
this.$cart_items_wrapper.append( this.$cart_items_wrapper.append(
`<div class="cart-item-wrapper flex items-center h-18 pr-4 pl-4 rounded border-grey pointer no-select" `<div class="cart-item-wrapper flex items-center h-18 pr-4 pl-4 rounded border-grey pointer no-select"
data-item-code="${escape(item_data.item_code)}" data-uom="${escape(item_data.uom)}" data-item-code="${escape(item_data.item_code)}" data-uom="${escape(item_data.uom)}"
data-batch-no="${escape(item_data.batch_no || '')}"> data-batch-no="${escape(item_data.batch_no || '')}">
</div>` </div>`
) )
$item_to_update = this.get_cart_item(item_data); $item_to_update = this.get_cart_item(item_data);
} }
$item_to_update.html( $item_to_update.html(
`<div class="flex flex-col flex-1 f-shrink-1 overflow-hidden whitespace-nowrap"> `<div class="flex flex-col flex-1 f-shrink-1 overflow-hidden whitespace-nowrap">
<div class="text-md text-dark-grey text-bold"> <div class="text-md text-dark-grey text-bold">
${item_data.item_name} ${item_data.item_name}
</div> </div>
${get_description_html()} ${get_description_html()}
</div> </div>
${get_rate_discount_html()} ${get_rate_discount_html()}
</div>` </div>`
) )
set_dynamic_rate_header_width(); set_dynamic_rate_header_width();
@ -625,7 +625,7 @@ erpnext.PointOfSale.ItemCart = class {
$item_to_update.attr(`data-${selector}`, value); $item_to_update.attr(`data-${selector}`, value);
} }
toggle_checkout_btn(show_checkout) { toggle_checkout_btn(show_checkout) {
if (show_checkout) { if (show_checkout) {
this.$totals_section.find('.checkout-btn').removeClass('d-none'); this.$totals_section.find('.checkout-btn').removeClass('d-none');
this.$totals_section.find('.edit-cart-btn').addClass('d-none'); this.$totals_section.find('.edit-cart-btn').addClass('d-none');
@ -635,7 +635,7 @@ erpnext.PointOfSale.ItemCart = class {
} }
} }
highlight_checkout_btn(toggle) { highlight_checkout_btn(toggle) {
const has_primary_class = this.$totals_section.find('.checkout-btn').hasClass('bg-primary'); const has_primary_class = this.$totals_section.find('.checkout-btn').hasClass('bg-primary');
if (toggle && !has_primary_class) { if (toggle && !has_primary_class) {
this.$totals_section.find('.checkout-btn').addClass('bg-primary text-white text-lg'); this.$totals_section.find('.checkout-btn').addClass('bg-primary text-white text-lg');
@ -644,7 +644,7 @@ erpnext.PointOfSale.ItemCart = class {
} }
} }
update_empty_cart_section(no_of_cart_items) { update_empty_cart_section(no_of_cart_items) {
const $no_item_element = this.$cart_items_wrapper.find('.no-item-wrapper'); const $no_item_element = this.$cart_items_wrapper.find('.no-item-wrapper');
// if cart has items and no item is present // if cart has items and no item is present
@ -652,26 +652,26 @@ erpnext.PointOfSale.ItemCart = class {
&& this.$cart_items_wrapper.removeClass('mt-4 border-grey border-dashed') && this.$cart_header.removeClass('d-none'); && this.$cart_items_wrapper.removeClass('mt-4 border-grey border-dashed') && this.$cart_header.removeClass('d-none');
no_of_cart_items === 0 && !$no_item_element.length && this.make_no_items_placeholder(); no_of_cart_items === 0 && !$no_item_element.length && this.make_no_items_placeholder();
} }
on_numpad_event($btn) { on_numpad_event($btn) {
const current_action = $btn.attr('data-button-value'); const current_action = $btn.attr('data-button-value');
const action_is_field_edit = ['qty', 'discount_percentage', 'rate'].includes(current_action); const action_is_field_edit = ['qty', 'discount_percentage', 'rate'].includes(current_action);
this.highlight_numpad_btn($btn, current_action); this.highlight_numpad_btn($btn, current_action);
const action_is_pressed_twice = this.prev_action === current_action; const action_is_pressed_twice = this.prev_action === current_action;
const first_click_event = !this.prev_action; const first_click_event = !this.prev_action;
const field_to_edit_changed = this.prev_action && this.prev_action != current_action; const field_to_edit_changed = this.prev_action && this.prev_action != current_action;
if (action_is_field_edit) { if (action_is_field_edit) {
if (first_click_event || field_to_edit_changed) { if (first_click_event || field_to_edit_changed) {
this.prev_action = current_action; this.prev_action = current_action;
} else if (action_is_pressed_twice) { } else if (action_is_pressed_twice) {
this.prev_action = undefined; this.prev_action = undefined;
} }
this.numpad_value = ''; this.numpad_value = '';
} else if (current_action === 'checkout') { } else if (current_action === 'checkout') {
this.prev_action = undefined; this.prev_action = undefined;
@ -688,7 +688,7 @@ erpnext.PointOfSale.ItemCart = class {
this.numpad_value = this.numpad_value || 0; this.numpad_value = this.numpad_value || 0;
} }
const first_click_event_is_not_field_edit = !action_is_field_edit && first_click_event; const first_click_event_is_not_field_edit = !action_is_field_edit && first_click_event;
if (first_click_event_is_not_field_edit) { if (first_click_event_is_not_field_edit) {
frappe.show_alert({ frappe.show_alert({
@ -708,34 +708,34 @@ erpnext.PointOfSale.ItemCart = class {
this.numpad_value = current_action; this.numpad_value = current_action;
} }
this.events.numpad_event(this.numpad_value, this.prev_action); this.events.numpad_event(this.numpad_value, this.prev_action);
} }
highlight_numpad_btn($btn, curr_action) { highlight_numpad_btn($btn, curr_action) {
const curr_action_is_highlighted = $btn.hasClass('shadow-inner'); const curr_action_is_highlighted = $btn.hasClass('shadow-inner');
const curr_action_is_action = ['qty', 'discount_percentage', 'rate', 'done'].includes(curr_action); const curr_action_is_action = ['qty', 'discount_percentage', 'rate', 'done'].includes(curr_action);
if (!curr_action_is_highlighted) { if (!curr_action_is_highlighted) {
$btn.addClass('shadow-inner bg-selected'); $btn.addClass('shadow-inner bg-selected');
} }
if (this.prev_action === curr_action && curr_action_is_highlighted) { if (this.prev_action === curr_action && curr_action_is_highlighted) {
// if Qty is pressed twice // if Qty is pressed twice
$btn.removeClass('shadow-inner bg-selected'); $btn.removeClass('shadow-inner bg-selected');
} }
if (this.prev_action && this.prev_action !== curr_action && curr_action_is_action) { if (this.prev_action && this.prev_action !== curr_action && curr_action_is_action) {
// Order: Qty -> Rate then remove Qty highlight // Order: Qty -> Rate then remove Qty highlight
const prev_btn = $(`[data-button-value='${this.prev_action}']`); const prev_btn = $(`[data-button-value='${this.prev_action}']`);
prev_btn.removeClass('shadow-inner bg-selected'); prev_btn.removeClass('shadow-inner bg-selected');
} }
if (!curr_action_is_action || curr_action === 'done') { if (!curr_action_is_action || curr_action === 'done') {
// if numbers are clicked // if numbers are clicked
setTimeout(() => { setTimeout(() => {
$btn.removeClass('shadow-inner bg-selected'); $btn.removeClass('shadow-inner bg-selected');
}, 100); }, 100);
} }
} }
toggle_numpad(show) { toggle_numpad(show) {
if (show) { if (show) {
this.$totals_section.addClass('d-none'); this.$totals_section.addClass('d-none');
this.$numpad_section.removeClass('d-none'); this.$numpad_section.removeClass('d-none');
@ -946,6 +946,6 @@ erpnext.PointOfSale.ItemCart = class {
toggle_component(show) { toggle_component(show) {
show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none'); show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
} }
} }

View File

@ -1,28 +1,28 @@
erpnext.PointOfSale.ItemDetails = class { erpnext.PointOfSale.ItemDetails = class {
constructor({ wrapper, events }) { constructor({ wrapper, events }) {
this.wrapper = wrapper; this.wrapper = wrapper;
this.events = events; this.events = events;
this.current_item = {}; this.current_item = {};
this.init_component(); this.init_component();
} }
init_component() { init_component() {
this.prepare_dom(); this.prepare_dom();
this.init_child_components(); this.init_child_components();
this.bind_events(); this.bind_events();
this.attach_shortcuts(); this.attach_shortcuts();
} }
prepare_dom() { prepare_dom() {
this.wrapper.append( this.wrapper.append(
`<section class="col-span-4 flex shadow rounded item-details bg-white mx-h-70 h-100 d-none"></section>` `<section class="col-span-4 flex shadow rounded item-details bg-white mx-h-70 h-100 d-none"></section>`
) )
this.$component = this.wrapper.find('.item-details'); this.$component = this.wrapper.find('.item-details');
} }
init_child_components() { init_child_components() {
this.$component.html( this.$component.html(
`<div class="details-container flex flex-col p-8 rounded w-full"> `<div class="details-container flex flex-col p-8 rounded w-full">
<div class="flex justify-between mb-2"> <div class="flex justify-between mb-2">
@ -49,27 +49,27 @@ erpnext.PointOfSale.ItemDetails = class {
this.$item_image = this.$component.find('.item-image'); this.$item_image = this.$component.find('.item-image');
this.$form_container = this.$component.find('.form-container'); this.$form_container = this.$component.find('.form-container');
this.$dicount_section = this.$component.find('.discount-section'); this.$dicount_section = this.$component.find('.discount-section');
} }
toggle_item_details_section(item) { toggle_item_details_section(item) {
const { item_code, batch_no, uom } = this.current_item; const { item_code, batch_no, uom } = this.current_item;
const item_code_is_same = item && item_code === item.item_code; const item_code_is_same = item && item_code === item.item_code;
const batch_is_same = item && batch_no == item.batch_no; const batch_is_same = item && batch_no == item.batch_no;
const uom_is_same = item && uom === item.uom; const uom_is_same = item && uom === item.uom;
this.item_has_changed = !item ? false : item_code_is_same && batch_is_same && uom_is_same ? false : true; this.item_has_changed = !item ? false : item_code_is_same && batch_is_same && uom_is_same ? false : true;
this.events.toggle_item_selector(this.item_has_changed); this.events.toggle_item_selector(this.item_has_changed);
this.toggle_component(this.item_has_changed); this.toggle_component(this.item_has_changed);
if (this.item_has_changed) { if (this.item_has_changed) {
this.doctype = item.doctype; this.doctype = item.doctype;
this.item_meta = frappe.get_meta(this.doctype); this.item_meta = frappe.get_meta(this.doctype);
this.name = item.name; this.name = item.name;
this.item_row = item; this.item_row = item;
this.currency = this.events.get_frm().doc.currency; this.currency = this.events.get_frm().doc.currency;
this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom }; this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom };
this.render_dom(item); this.render_dom(item);
this.render_discount_dom(item); this.render_discount_dom(item);
@ -103,8 +103,8 @@ erpnext.PointOfSale.ItemDetails = class {
} }
} }
render_dom(item) { render_dom(item) {
let { item_code ,item_name, description, image, price_list_rate } = item; let { item_code ,item_name, description, image, price_list_rate } = item;
function get_description_html() { function get_description_html() {
if (description) { if (description) {
@ -112,7 +112,7 @@ erpnext.PointOfSale.ItemDetails = class {
return description; return description;
} }
return ``; return ``;
} }
this.$item_name.html(item_name); this.$item_name.html(item_name);
this.$item_description.html(get_description_html()); this.$item_description.html(get_description_html());
@ -125,9 +125,9 @@ erpnext.PointOfSale.ItemDetails = class {
this.$item_image.html(frappe.get_abbr(item_code)); this.$item_image.html(frappe.get_abbr(item_code));
} }
} }
render_discount_dom(item) { render_discount_dom(item) {
if (item.discount_percentage) { if (item.discount_percentage) {
this.$dicount_section.html( this.$dicount_section.html(
`<div class="text-grey line-through mr-4 text-md mb-2"> `<div class="text-grey line-through mr-4 text-md mb-2">
@ -141,9 +141,9 @@ erpnext.PointOfSale.ItemDetails = class {
} else { } else {
this.$dicount_section.html(``) this.$dicount_section.html(``)
} }
} }
render_form(item) { render_form(item) {
const fields_to_display = this.get_form_fields(item); const fields_to_display = this.get_form_fields(item);
this.$form_container.html(''); this.$form_container.html('');
@ -174,16 +174,16 @@ erpnext.PointOfSale.ItemDetails = class {
this.make_auto_serial_selection_btn(item); this.make_auto_serial_selection_btn(item);
this.bind_custom_control_change_event(); this.bind_custom_control_change_event();
} }
get_form_fields(item) { get_form_fields(item) {
const fields = ['qty', 'uom', 'rate', 'price_list_rate', 'discount_percentage', 'warehouse', 'actual_qty']; const fields = ['qty', 'uom', 'rate', 'price_list_rate', 'discount_percentage', 'warehouse', 'actual_qty'];
if (item.has_serial_no) fields.push('serial_no'); if (item.has_serial_no) fields.push('serial_no');
if (item.has_batch_no) fields.push('batch_no'); if (item.has_batch_no) fields.push('batch_no');
return fields; return fields;
} }
make_auto_serial_selection_btn(item) { make_auto_serial_selection_btn(item) {
if (item.has_serial_no) { if (item.has_serial_no) {
this.$form_container.append( this.$form_container.append(
`<div class="grid-filler no-select"></div>` `<div class="grid-filler no-select"></div>`
@ -204,7 +204,7 @@ erpnext.PointOfSale.ItemDetails = class {
} }
} }
bind_custom_control_change_event() { bind_custom_control_change_event() {
const me = this; const me = this;
if (this.rate_control) { if (this.rate_control) {
this.rate_control.df.onchange = function() { this.rate_control.df.onchange = function() {
@ -276,8 +276,8 @@ erpnext.PointOfSale.ItemDetails = class {
}; };
this.batch_no_control.df.onchange = function() { this.batch_no_control.df.onchange = function() {
me.events.set_value_in_current_cart_item('batch-no', this.value); me.events.set_value_in_current_cart_item('batch-no', this.value);
me.events.form_updated(me.doctype, me.name, 'batch_no', this.value); me.events.form_updated(me.doctype, me.name, 'batch_no', this.value);
me.current_item.batch_no = this.value; me.current_item.batch_no = this.value;
} }
this.batch_no_control.refresh(); this.batch_no_control.refresh();
} }
@ -289,9 +289,9 @@ erpnext.PointOfSale.ItemDetails = class {
me.current_item.uom = this.value; me.current_item.uom = this.value;
} }
} }
} }
async auto_update_batch_no() { async auto_update_batch_no() {
if (this.serial_no_control && this.batch_no_control) { if (this.serial_no_control && this.batch_no_control) {
const selected_serial_nos = this.serial_no_control.get_value().split(`\n`).filter(s => s); const selected_serial_nos = this.serial_no_control.get_value().split(`\n`).filter(s => s);
if (!selected_serial_nos.length) return; if (!selected_serial_nos.length) return;
@ -310,9 +310,9 @@ erpnext.PointOfSale.ItemDetails = class {
const batch_no = Object.keys(batch_serial_map)[0]; const batch_no = Object.keys(batch_serial_map)[0];
const batch_serial_nos = batch_serial_map[batch_no].join(`\n`); const batch_serial_nos = batch_serial_map[batch_no].join(`\n`);
// eg. 10 selected serial no. -> 5 belongs to first batch other 5 belongs to second batch // eg. 10 selected serial no. -> 5 belongs to first batch other 5 belongs to second batch
const serial_nos_belongs_to_other_batch = selected_serial_nos.length !== batch_serial_map[batch_no].length; const serial_nos_belongs_to_other_batch = selected_serial_nos.length !== batch_serial_map[batch_no].length;
const current_batch_no = this.batch_no_control.get_value(); const current_batch_no = this.batch_no_control.get_value();
current_batch_no != batch_no && await this.batch_no_control.set_value(batch_no); current_batch_no != batch_no && await this.batch_no_control.set_value(batch_no);
if (serial_nos_belongs_to_other_batch) { if (serial_nos_belongs_to_other_batch) {
@ -327,7 +327,7 @@ erpnext.PointOfSale.ItemDetails = class {
} }
} }
bind_events() { bind_events() {
this.bind_auto_serial_fetch_event(); this.bind_auto_serial_fetch_event();
this.bind_fields_to_numpad_fields(); this.bind_fields_to_numpad_fields();
@ -345,7 +345,7 @@ erpnext.PointOfSale.ItemDetails = class {
}); });
} }
bind_fields_to_numpad_fields() { bind_fields_to_numpad_fields() {
const me = this; const me = this;
this.$form_container.on('click', '.input-with-feedback', function() { this.$form_container.on('click', '.input-with-feedback', function() {
const fieldname = $(this).attr('data-fieldname'); const fieldname = $(this).attr('data-fieldname');
@ -356,7 +356,7 @@ erpnext.PointOfSale.ItemDetails = class {
}); });
} }
bind_auto_serial_fetch_event() { bind_auto_serial_fetch_event() {
this.$form_container.on('click', '.auto-fetch-btn', () => { this.$form_container.on('click', '.auto-fetch-btn', () => {
this.batch_no_control.set_value(''); this.batch_no_control.set_value('');
let qty = this.qty_control.get_value(); let qty = this.qty_control.get_value();
@ -382,7 +382,7 @@ erpnext.PointOfSale.ItemDetails = class {
frappe.msgprint(`Fetched only ${records_length} available serial numbers.`); frappe.msgprint(`Fetched only ${records_length} available serial numbers.`);
this.qty_control.set_value(records_length); this.qty_control.set_value(records_length);
} }
numbers = auto_fetched_serial_numbers.join(`\n`); numbers = auto_fetched_serial_numbers.join(`\n`);
this.serial_no_control.set_value(numbers); this.serial_no_control.set_value(numbers);
}); });
}) })
@ -390,5 +390,5 @@ erpnext.PointOfSale.ItemDetails = class {
toggle_component(show) { toggle_component(show) {
show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none'); show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
} }
} }

View File

@ -1,115 +1,115 @@
erpnext.PointOfSale.ItemSelector = class { erpnext.PointOfSale.ItemSelector = class {
constructor({ frm, wrapper, events, pos_profile }) { constructor({ frm, wrapper, events, pos_profile }) {
this.wrapper = wrapper; this.wrapper = wrapper;
this.events = events; this.events = events;
this.pos_profile = pos_profile; this.pos_profile = pos_profile;
this.inti_component(); this.inti_component();
} }
inti_component() { inti_component() {
this.prepare_dom(); this.prepare_dom();
this.make_search_bar(); this.make_search_bar();
this.load_items_data(); this.load_items_data();
this.bind_events(); this.bind_events();
this.attach_shortcuts(); this.attach_shortcuts();
} }
prepare_dom() { prepare_dom() {
this.wrapper.append( this.wrapper.append(
`<section class="col-span-6 flex shadow rounded items-selector bg-white mx-h-70 h-100"> `<section class="col-span-6 flex shadow rounded items-selector bg-white mx-h-70 h-100">
<div class="flex flex-col rounded w-full scroll-y"> <div class="flex flex-col rounded w-full scroll-y">
<div class="filter-section flex p-8 pb-2 bg-white sticky z-100"> <div class="filter-section flex p-8 pb-2 bg-white sticky z-100">
<div class="search-field flex f-grow-3 mr-8 items-center text-grey"></div> <div class="search-field flex f-grow-3 mr-8 items-center text-grey"></div>
<div class="item-group-field flex f-grow-1 items-center text-grey text-bold"></div> <div class="item-group-field flex f-grow-1 items-center text-grey text-bold"></div>
</div> </div>
<div class="flex flex-1 flex-col p-8 pt-2"> <div class="flex flex-1 flex-col p-8 pt-2">
<div class="text-grey mb-6">ALL ITEMS</div> <div class="text-grey mb-6">ALL ITEMS</div>
<div class="items-container grid grid-cols-4 gap-8"> <div class="items-container grid grid-cols-4 gap-8">
</div> </div>
</div> </div>
</div> </div>
</section>` </section>`
); );
this.$component = this.wrapper.find('.items-selector'); this.$component = this.wrapper.find('.items-selector');
} }
async load_items_data() { async load_items_data() {
if (!this.item_group) { if (!this.item_group) {
const res = await frappe.db.get_value("Item Group", {lft: 1, is_group: 1}, "name"); const res = await frappe.db.get_value("Item Group", {lft: 1, is_group: 1}, "name");
this.parent_item_group = res.message.name; this.parent_item_group = res.message.name;
}; };
if (!this.price_list) { if (!this.price_list) {
const res = await frappe.db.get_value("POS Profile", this.pos_profile, "selling_price_list"); const res = await frappe.db.get_value("POS Profile", this.pos_profile, "selling_price_list");
this.price_list = res.message.selling_price_list; this.price_list = res.message.selling_price_list;
} }
this.get_items({}).then(({message}) => { this.get_items({}).then(({message}) => {
this.render_item_list(message.items); this.render_item_list(message.items);
}); });
} }
get_items({start = 0, page_length = 40, search_value=''}) { get_items({start = 0, page_length = 40, search_value=''}) {
const price_list = this.events.get_frm().doc?.selling_price_list || this.price_list; const price_list = this.events.get_frm().doc?.selling_price_list || this.price_list;
let { item_group, pos_profile } = this; let { item_group, pos_profile } = this;
!item_group && (item_group = this.parent_item_group); !item_group && (item_group = this.parent_item_group);
return frappe.call({ return frappe.call({
method: "erpnext.selling.page.point_of_sale.point_of_sale.get_items", method: "erpnext.selling.page.point_of_sale.point_of_sale.get_items",
freeze: true, freeze: true,
args: { start, page_length, price_list, item_group, search_value, pos_profile }, args: { start, page_length, price_list, item_group, search_value, pos_profile },
}); });
} }
render_item_list(items) { render_item_list(items) {
this.$items_container = this.$component.find('.items-container'); this.$items_container = this.$component.find('.items-container');
this.$items_container.html(''); this.$items_container.html('');
items.forEach(item => { items.forEach(item => {
const item_html = this.get_item_html(item); const item_html = this.get_item_html(item);
this.$items_container.append(item_html); this.$items_container.append(item_html);
}) })
} }
get_item_html(item) { get_item_html(item) {
const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom } = item; const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom } = item;
const indicator_color = actual_qty > 10 ? "green" : actual_qty !== 0 ? "orange" : "red"; const indicator_color = actual_qty > 10 ? "green" : actual_qty !== 0 ? "orange" : "red";
function get_item_image_html() { function get_item_image_html() {
if (item_image) { if (item_image) {
return `<div class="flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100"> return `<div class="flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100">
<img class="h-full" src="${item_image}" alt="${item_image}" style="object-fit: cover;"> <img class="h-full" src="${item_image}" alt="${item_image}" style="object-fit: cover;">
</div>` </div>`
} else { } else {
return `<div class="flex items-center justify-center h-32 bg-light-grey text-6xl text-grey-100"> return `<div class="flex items-center justify-center h-32 bg-light-grey text-6xl text-grey-100">
${frappe.get_abbr(item.item_name)} ${frappe.get_abbr(item.item_name)}
</div>` </div>`
} }
} }
return ( return (
`<div class="item-wrapper rounded shadow pointer no-select" data-item-code="${escape(item.item_code)}" `<div class="item-wrapper rounded shadow pointer no-select" data-item-code="${escape(item.item_code)}"
data-serial-no="${escape(serial_no)}" data-batch-no="${escape(batch_no)}" data-uom="${escape(stock_uom)}" data-serial-no="${escape(serial_no)}" data-batch-no="${escape(batch_no)}" data-uom="${escape(stock_uom)}"
title="Avaiable Qty: ${actual_qty}"> title="Avaiable Qty: ${actual_qty}">
${get_item_image_html()} ${get_item_image_html()}
<div class="flex items-center pr-4 pl-4 h-10 justify-between"> <div class="flex items-center pr-4 pl-4 h-10 justify-between">
<div class="flex items-center f-shrink-1 text-dark-grey overflow-hidden whitespace-nowrap"> <div class="flex items-center f-shrink-1 text-dark-grey overflow-hidden whitespace-nowrap">
<span class="indicator ${indicator_color}"></span> <span class="indicator ${indicator_color}"></span>
${frappe.ellipsis(item.item_name, 18)} ${frappe.ellipsis(item.item_name, 18)}
</div> </div>
<div class="f-shrink-0 text-dark-grey text-bold ml-4">${format_currency(item.price_list_rate, item.currency, 0) || 0}</div> <div class="f-shrink-0 text-dark-grey text-bold ml-4">${format_currency(item.price_list_rate, item.currency, 0) || 0}</div>
</div> </div>
</div>` </div>`
) )
} }
make_search_bar() { make_search_bar() {
const me = this; const me = this;
this.$component.find('.search-field').html(''); this.$component.find('.search-field').html('');
this.$component.find('.item-group-field').html(''); this.$component.find('.item-group-field').html('');
this.search_field = frappe.ui.form.make_control({ this.search_field = frappe.ui.form.make_control({
df: { df: {
@ -119,104 +119,104 @@ erpnext.PointOfSale.ItemSelector = class {
}, },
parent: this.$component.find('.search-field'), parent: this.$component.find('.search-field'),
render_input: true, render_input: true,
}); });
this.item_group_field = frappe.ui.form.make_control({ this.item_group_field = frappe.ui.form.make_control({
df: { df: {
label: __('Item Group'), label: __('Item Group'),
fieldtype: 'Link', fieldtype: 'Link',
options: 'Item Group', options: 'Item Group',
placeholder: __('Select item group'), placeholder: __('Select item group'),
onchange: function() { onchange: function() {
me.item_group = this.value; me.item_group = this.value;
!me.item_group && (me.item_group = me.parent_item_group); !me.item_group && (me.item_group = me.parent_item_group);
me.filter_items(); me.filter_items();
}, },
get_query: function () { get_query: function () {
return { return {
query: 'erpnext.selling.page.point_of_sale.point_of_sale.item_group_query', query: 'erpnext.selling.page.point_of_sale.point_of_sale.item_group_query',
filters: { filters: {
pos_profile: me.events.get_frm().doc?.pos_profile pos_profile: me.events.get_frm().doc?.pos_profile
} }
} }
}, },
}, },
parent: this.$component.find('.item-group-field'), parent: this.$component.find('.item-group-field'),
render_input: true, render_input: true,
}); });
this.search_field.toggle_label(false); this.search_field.toggle_label(false);
this.item_group_field.toggle_label(false); this.item_group_field.toggle_label(false);
} }
bind_events() { bind_events() {
const me = this; const me = this;
onScan.attachTo(document, { onScan.attachTo(document, {
onScan: (sScancode) => { onScan: (sScancode) => {
if (this.search_field && this.$component.is(':visible')) { if (this.search_field && this.$component.is(':visible')) {
this.search_field.set_focus(); this.search_field.set_focus();
$(this.search_field.$input[0]).val(sScancode).trigger("input"); $(this.search_field.$input[0]).val(sScancode).trigger("input");
this.barcode_scanned = true; this.barcode_scanned = true;
} }
} }
}); });
this.$component.on('click', '.item-wrapper', function() { this.$component.on('click', '.item-wrapper', function() {
const $item = $(this); const $item = $(this);
const item_code = unescape($item.attr('data-item-code')); const item_code = unescape($item.attr('data-item-code'));
let batch_no = unescape($item.attr('data-batch-no')); let batch_no = unescape($item.attr('data-batch-no'));
let serial_no = unescape($item.attr('data-serial-no')); let serial_no = unescape($item.attr('data-serial-no'));
let uom = unescape($item.attr('data-uom')); let uom = unescape($item.attr('data-uom'));
// escape(undefined) returns "undefined" then unescape returns "undefined" // escape(undefined) returns "undefined" then unescape returns "undefined"
batch_no = batch_no === "undefined" ? undefined : batch_no; batch_no = batch_no === "undefined" ? undefined : batch_no;
serial_no = serial_no === "undefined" ? undefined : serial_no; serial_no = serial_no === "undefined" ? undefined : serial_no;
uom = uom === "undefined" ? undefined : uom; uom = uom === "undefined" ? undefined : uom;
me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom }}); me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom }});
}) })
this.search_field.$input.on('input', (e) => { this.search_field.$input.on('input', (e) => {
clearTimeout(this.last_search); clearTimeout(this.last_search);
this.last_search = setTimeout(() => { this.last_search = setTimeout(() => {
const search_term = e.target.value; const search_term = e.target.value;
this.filter_items({ search_term }); this.filter_items({ search_term });
}, 300); }, 300);
}); });
} }
attach_shortcuts() { attach_shortcuts() {
frappe.ui.keys.on("ctrl+i", () => { frappe.ui.keys.on("ctrl+i", () => {
const selector_is_visible = this.$component.is(':visible'); const selector_is_visible = this.$component.is(':visible');
if (!selector_is_visible) return; if (!selector_is_visible) return;
this.search_field.set_focus(); this.search_field.set_focus();
}); });
frappe.ui.keys.on("ctrl+g", () => { frappe.ui.keys.on("ctrl+g", () => {
const selector_is_visible = this.$component.is(':visible'); const selector_is_visible = this.$component.is(':visible');
if (!selector_is_visible) return; if (!selector_is_visible) return;
this.item_group_field.set_focus(); this.item_group_field.set_focus();
}); });
// for selecting the last filtered item on search // for selecting the last filtered item on search
frappe.ui.keys.on("enter", () => { frappe.ui.keys.on("enter", () => {
const selector_is_visible = this.$component.is(':visible'); const selector_is_visible = this.$component.is(':visible');
if (!selector_is_visible || this.search_field.get_value() === "") return; if (!selector_is_visible || this.search_field.get_value() === "") return;
if (this.items.length == 1) { if (this.items.length == 1) {
this.$items_container.find(".item-wrapper").click(); this.$items_container.find(".item-wrapper").click();
frappe.utils.play_sound("submit"); frappe.utils.play_sound("submit");
$(this.search_field.$input[0]).val("").trigger("input"); $(this.search_field.$input[0]).val("").trigger("input");
} else if (this.items.length == 0 && this.barcode_scanned) { } else if (this.items.length == 0 && this.barcode_scanned) {
// only show alert of barcode is scanned and enter is pressed // only show alert of barcode is scanned and enter is pressed
frappe.show_alert({ frappe.show_alert({
message: __("No items found. Scan barcode again."), message: __("No items found. Scan barcode again."),
indicator: 'orange' indicator: 'orange'
}); });
frappe.utils.play_sound("error"); frappe.utils.play_sound("error");
this.barcode_scanned = false; this.barcode_scanned = false;
$(this.search_field.$input[0]).val("").trigger("input"); $(this.search_field.$input[0]).val("").trigger("input");
} }
}); });
} }
filter_items({ search_term='' }={}) { filter_items({ search_term='' }={}) {
if (search_term) { if (search_term) {
search_term = search_term.toLowerCase(); search_term = search_term.toLowerCase();
@ -227,39 +227,39 @@ erpnext.PointOfSale.ItemSelector = class {
this.items = items; this.items = items;
this.render_item_list(items); this.render_item_list(items);
return; return;
} }
} }
this.get_items({ search_value: search_term }) this.get_items({ search_value: search_term })
.then(({ message }) => { .then(({ message }) => {
const { items, serial_no, batch_no, barcode } = message; const { items, serial_no, batch_no, barcode } = message;
if (search_term && !barcode) { if (search_term && !barcode) {
this.search_index[search_term] = items; this.search_index[search_term] = items;
} }
this.items = items; this.items = items;
this.render_item_list(items); this.render_item_list(items);
}); });
} }
resize_selector(minimize) { resize_selector(minimize) {
minimize ? minimize ?
this.$component.find('.search-field').removeClass('mr-8') : this.$component.find('.search-field').removeClass('mr-8') :
this.$component.find('.search-field').addClass('mr-8'); this.$component.find('.search-field').addClass('mr-8');
minimize ? minimize ?
this.$component.find('.filter-section').addClass('flex-col') : this.$component.find('.filter-section').addClass('flex-col') :
this.$component.find('.filter-section').removeClass('flex-col'); this.$component.find('.filter-section').removeClass('flex-col');
minimize ? minimize ?
this.$component.removeClass('col-span-6').addClass('col-span-2') : this.$component.removeClass('col-span-6').addClass('col-span-2') :
this.$component.removeClass('col-span-2').addClass('col-span-6') this.$component.removeClass('col-span-2').addClass('col-span-6')
minimize ? minimize ?
this.$items_container.removeClass('grid-cols-4').addClass('grid-cols-1') : this.$items_container.removeClass('grid-cols-4').addClass('grid-cols-1') :
this.$items_container.removeClass('grid-cols-1').addClass('grid-cols-4') this.$items_container.removeClass('grid-cols-1').addClass('grid-cols-4')
} }
toggle_component(show) { toggle_component(show) {
show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none'); show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
} }
} }

View File

@ -1,49 +1,48 @@
erpnext.PointOfSale.NumberPad = class { erpnext.PointOfSale.NumberPad = class {
constructor({ wrapper, events, cols, keys, css_classes, fieldnames_map }) { constructor({ wrapper, events, cols, keys, css_classes, fieldnames_map }) {
this.wrapper = wrapper; this.wrapper = wrapper;
this.events = events; this.events = events;
this.cols = cols; this.cols = cols;
this.keys = keys; this.keys = keys;
this.css_classes = css_classes || []; this.css_classes = css_classes || [];
this.fieldnames = fieldnames_map || {}; this.fieldnames = fieldnames_map || {};
this.init_component(); this.init_component();
} }
init_component() { init_component() {
this.prepare_dom(); this.prepare_dom();
this.bind_events(); this.bind_events();
} }
prepare_dom() { prepare_dom() {
const { cols, keys, css_classes, fieldnames } = this; const { cols, keys, css_classes, fieldnames } = this;
function get_keys() { function get_keys() {
return keys.reduce((a, row, i) => { return keys.reduce((a, row, i) => {
return a + row.reduce((a2, number, j) => { return a + row.reduce((a2, number, j) => {
const class_to_append = css_classes && css_classes[i] ? css_classes[i][j] : ''; const class_to_append = css_classes && css_classes[i] ? css_classes[i][j] : '';
const fieldname = fieldnames && fieldnames[number] ? const fieldname = fieldnames && fieldnames[number] ?
fieldnames[number] : fieldnames[number] : typeof number === 'string' ? frappe.scrub(number) : number;
typeof number === 'string' ? frappe.scrub(number) : number;
return a2 + `<div class="numpad-btn pointer no-select rounded ${class_to_append} return a2 + `<div class="numpad-btn pointer no-select rounded ${class_to_append}
flex items-center justify-center h-16 text-md border-grey border" data-button-value="${fieldname}">${number}</div>` flex items-center justify-center h-16 text-md border-grey border" data-button-value="${fieldname}">${number}</div>`
}, '') }, '')
}, ''); }, '');
} }
this.wrapper.html( this.wrapper.html(
`<div class="grid grid-cols-${cols} gap-4"> `<div class="grid grid-cols-${cols} gap-4">
${get_keys()} ${get_keys()}
</div>` </div>`
) )
} }
bind_events() { bind_events() {
const me = this; const me = this;
this.wrapper.on('click', '.numpad-btn', function() { this.wrapper.on('click', '.numpad-btn', function() {
const $btn = $(this); const $btn = $(this);
me.events.numpad_event($btn); me.events.numpad_event($btn);
}) });
} }
} }