Merge branch 'develop' into pr-dn-return

This commit is contained in:
Marica 2020-09-11 16:10:48 +05:30 committed by GitHub
commit 4153a9f8de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 522 additions and 496 deletions

View File

@ -43,7 +43,7 @@
{
"hidden": 0,
"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,
@ -98,7 +98,7 @@
"idx": 0,
"is_standard": 1,
"label": "Accounting",
"modified": "2020-09-03 10:37:07.865801",
"modified": "2020-09-09 11:45:33.766400",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
@ -147,11 +147,6 @@
"link_to": "Trial Balance",
"type": "Report"
},
{
"label": "Point of Sale",
"link_to": "point-of-sale",
"type": "Page"
},
{
"label": "Dashboard",
"link_to": "Accounts",

View File

@ -55,14 +55,48 @@ frappe.ui.form.on('POS Closing Entry', {
},
callback: (r) => {
let pos_docs = r.message;
set_form_data(pos_docs, frm)
refresh_fields(frm)
set_html_data(frm)
set_form_data(pos_docs, frm);
refresh_fields(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', {
closing_amount: (frm, 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.net_total += flt(d.net_total);
frm.doc.total_quantity += flt(d.total_qty);
add_to_payments(d, frm);
add_to_taxes(d, frm);
refresh_payments(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 => {
const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_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 {
frm.add_child("payment_reconciliation", {
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 => {
const tax = frm.doc.taxes.find(tx => tx.account_head === t.account_head && tx.rate === t.rate);
if (tax) {
tax.amount += flt(t.tax_amount);
if (!remove) tax.amount += flt(t.tax_amount);
else tax.amount -= flt(t.tax_amount);
} else {
frm.add_child("taxes", {
account_head: t.account_head,

View File

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

View File

@ -182,8 +182,9 @@ class TestPOSInvoice(unittest.TestCase):
def test_pos_returns_with_repayment(self):
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': 'Cash', 'account': 'Cash - _TC', 'amount': 500})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500, 'default': 1})
pos.insert()
pos.submit()
@ -200,8 +201,9 @@ class TestPOSInvoice(unittest.TestCase):
income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105,
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': 'Cash', 'account': 'Cash - _TC', 'amount': 60})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60, 'default': 1})
pos.insert()
pos.submit()

View File

@ -24,11 +24,20 @@ class POSInvoiceMergeLog(Document):
def validate_pos_invoice_status(self):
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:
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))
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):
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]
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)
else:
credit_note = ""
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log

View File

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

View File

@ -725,4 +725,5 @@ erpnext.patches.v12_0.rename_lost_reason_detail
erpnext.patches.v13_0.drop_razorpay_payload_column
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.update_returned_qty_in_pr_dn
erpnext.patches.v13_0.change_default_pos_print_format
erpnext.patches.v13_0.update_returned_qty_in_pr_dn

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},
['mode_of_payment', 'account', 'type'], (value) => {
if (this.frm.is_dirty()) {
frappe.model.clear_table(this.frm.doc, 'payments');
if (value) {
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;
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.frm.doc.payments.find(pay => {
if (pay.default) {
pay.amount = total_amount_to_pay;
} else {
pay.amount = 0.0
}
});
this.frm.refresh_fields();
this.calculate_paid_amount();
},

View File

@ -2,8 +2,18 @@
"cards": [
{
"hidden": 0,
"label": "Retail Operations",
"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]"
"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 \"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",
@ -18,7 +28,7 @@
"idx": 0,
"is_standard": 1,
"label": "Retail",
"modified": "2020-08-20 18:00:07.515691",
"modified": "2020-09-09 11:46:28.297435",
"modified_by": "Administrator",
"module": "Selling",
"name": "Retail",
@ -28,25 +38,10 @@
"restrict_to_domain": "Retail",
"shortcuts": [
{
"color": "#9deca2",
"doc_view": "",
"format": "{} Active",
"label": "Point of Sale Profile",
"link_to": "POS Profile",
"stats_filter": "{\n \"disabled\": 0\n}",
"type": "DocType"
},
{
"doc_view": "",
"label": "Point of Sale",
"label": "Point Of Sale",
"link_to": "point-of-sale",
"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'),
single_column: true
});
// online
wrapper.pos = new erpnext.PointOfSale.Controller(wrapper);
window.cur_pos = wrapper.pos;
};

View File

@ -8,7 +8,7 @@
{% include "erpnext/selling/page/point_of_sale/pos_past_order_summary.js" %}
erpnext.PointOfSale.Controller = class {
constructor(wrapper) {
constructor(wrapper) {
this.wrapper = $(wrapper).find('.layout-main-section');
this.page = wrapper.page;
@ -36,7 +36,7 @@ erpnext.PointOfSale.Controller = class {
const table_fields = [
{ 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",
options: "company:company_currency", reqd: 1 }
options: "company:company_currency" }
];
const dialog = new frappe.ui.Dialog({
@ -51,29 +51,16 @@ erpnext.PointOfSale.Controller = class {
options: 'POS Profile', fieldname: 'pos_profile', reqd: 1,
onchange: () => {
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_list("POS Closing Entry", {
filters: { company, pos_profile, user },
limit: 1,
order_by: 'period_end_date desc'
}).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();
frappe.db.get_doc("POS Profile", pos_profile).then(doc => {
dialog.fields_dict.balance_details.df.data = [];
doc.payments.forEach(pay => {
const { mode_of_payment } = pay;
dialog.fields_dict.balance_details.df.data.push({ mode_of_payment });
});
dialog.fields_dict.balance_details.grid.refresh();
});
}
},

View File

@ -1,36 +1,36 @@
erpnext.PointOfSale.ItemCart = class {
constructor({ wrapper, events }) {
constructor({ wrapper, events }) {
this.wrapper = wrapper;
this.events = events;
this.customer_info = undefined;
this.init_component();
}
init_component() {
this.prepare_dom();
this.init_child_components();
this.customer_info = undefined;
this.init_component();
}
init_component() {
this.prepare_dom();
this.init_child_components();
this.bind_events();
this.attach_shortcuts();
}
}
prepare_dom() {
prepare_dom() {
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() {
this.init_customer_selector();
this.init_cart_components();
}
init_child_components() {
this.init_customer_selector();
this.init_cart_components();
}
init_customer_selector() {
init_customer_selector() {
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');
}
@ -41,9 +41,9 @@ erpnext.PointOfSale.ItemCart = class {
this.make_customer_selector();
this.customer_field.set_focus();
}
init_cart_components() {
this.$component.append(
init_cart_components() {
this.$component.append(
`<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="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="numpad-section flex flex-col mt-4 d-none w-full p-8 pt-0 pb-0 f-shrink-0"></div>
</div>
</div>`
);
</div>`
);
this.$cart_container = this.$component.find('.cart-container');
this.make_cart_totals_section();
this.make_cart_items_section();
this.make_cart_numpad();
}
this.make_cart_numpad();
}
make_cart_items_section() {
this.$cart_header = this.$component.find('.cart-header');
this.$cart_items_wrapper = this.$component.find('.cart-items-section');
make_cart_items_section() {
this.$cart_header = this.$component.find('.cart-header');
this.$cart_items_wrapper = this.$component.find('.cart-items-section');
this.make_no_items_placeholder();
}
make_no_items_placeholder() {
}
make_no_items_placeholder() {
this.$cart_header.addClass('d-none');
this.$cart_items_wrapper.html(
`<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');
}
make_cart_totals_section() {
this.$totals_section = this.$component.find('.cart-totals-section');
make_cart_totals_section() {
this.$totals_section = this.$component.find('.cart-totals-section');
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">
@ -116,9 +116,9 @@ erpnext.PointOfSale.ItemCart = class {
)
this.$add_discount_elem = this.$component.find(".add-discount");
}
make_cart_numpad() {
}
make_cart_numpad() {
this.$numpad_section = this.$component.find('.numpad-section');
this.number_pad = new erpnext.PointOfSale.NumberPad({
@ -155,9 +155,9 @@ erpnext.PointOfSale.ItemCart = class {
Checkout
</div>`
)
}
bind_events() {
}
bind_events() {
const me = this;
this.$customer_section.on('click', '.add-remove-customer', function (e) {
const customer_info_is_visible = me.$cart_container.hasClass('d-none');
@ -381,8 +381,8 @@ erpnext.PointOfSale.ItemCart = class {
`
);
}
update_customer_section() {
update_customer_section() {
const { customer, email_id='', mobile_no='', image } = this.customer_info || {};
if (customer) {
@ -403,7 +403,7 @@ erpnext.PointOfSale.ItemCart = class {
</div>`
);
} else {
// reset customer selector
// reset customer selector
this.reset_customer_selector();
}
@ -430,9 +430,9 @@ erpnext.PointOfSale.ItemCart = class {
</div>`
}
}
}
update_totals_section(frm) {
}
update_totals_section(frm) {
if (!frm) frm = this.events.get_frm();
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 }})
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;
this.$totals_section.find('.net-total').html(
`<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>`)
}
render_grand_total(value) {
}
render_grand_total(value) {
const currency = this.events.get_frm().doc.currency;
this.$totals_section.find('.grand-total').html(
`<div class="flex flex-col">
@ -495,20 +495,20 @@ erpnext.PointOfSale.ItemCart = class {
} else {
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 item_code_attr = `[data-item-code="${escape(item_code)}"]`;
const uom_attr = `[data-uom=${escape(uom)}]`;
const item_selector = batch_no ?
`.cart-item-wrapper${batch_attr}${uom_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}`;
return this.$cart_items_wrapper.find(item_selector);
}
update_item_html(item, remove_item) {
const item_selector = batch_no ?
`.cart-item-wrapper${batch_attr}${uom_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}`;
return this.$cart_items_wrapper.find(item_selector);
}
update_item_html(item, remove_item) {
const $item = this.get_cart_item(item);
if (remove_item) {
@ -524,33 +524,33 @@ erpnext.PointOfSale.ItemCart = class {
const no_of_cart_items = this.$cart_items_wrapper.children().length;
no_of_cart_items > 0 && this.highlight_checkout_btn(no_of_cart_items > 0);
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 me = this;
if (!$item_to_update.length) {
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"
if (!$item_to_update.length) {
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"
data-item-code="${escape(item_data.item_code)}" data-uom="${escape(item_data.uom)}"
data-batch-no="${escape(item_data.batch_no || '')}">
</div>`
)
$item_to_update = this.get_cart_item(item_data);
}
</div>`
)
$item_to_update = this.get_cart_item(item_data);
}
$item_to_update.html(
`<div class="flex flex-col flex-1 f-shrink-1 overflow-hidden whitespace-nowrap">
<div class="text-md text-dark-grey text-bold">
${item_data.item_name}
</div>
${get_description_html()}
</div>
${get_rate_discount_html()}
</div>`
<div class="text-md text-dark-grey text-bold">
${item_data.item_name}
</div>
${get_description_html()}
</div>
${get_rate_discount_html()}
</div>`
)
set_dynamic_rate_header_width();
@ -572,7 +572,7 @@ erpnext.PointOfSale.ItemCart = class {
me.$cart_header.find(".rate-list-header").css("width", max_width);
me.$cart_items_wrapper.find(".rate-col").css("width", max_width);
}
function get_rate_discount_html() {
if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) {
return `
@ -625,7 +625,7 @@ erpnext.PointOfSale.ItemCart = class {
$item_to_update.attr(`data-${selector}`, value);
}
toggle_checkout_btn(show_checkout) {
toggle_checkout_btn(show_checkout) {
if (show_checkout) {
this.$totals_section.find('.checkout-btn').removeClass('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');
if (toggle && !has_primary_class) {
this.$totals_section.find('.checkout-btn').addClass('bg-primary text-white text-lg');
@ -643,8 +643,8 @@ erpnext.PointOfSale.ItemCart = class {
this.$totals_section.find('.checkout-btn').removeClass('bg-primary text-white text-lg');
}
}
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');
// if cart has items and no item is present
@ -652,27 +652,27 @@ erpnext.PointOfSale.ItemCart = class {
&& 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();
}
on_numpad_event($btn) {
}
on_numpad_event($btn) {
const current_action = $btn.attr('data-button-value');
const action_is_field_edit = ['qty', 'discount_percentage', 'rate'].includes(current_action);
this.highlight_numpad_btn($btn, current_action);
const action_is_pressed_twice = this.prev_action === current_action;
const first_click_event = !this.prev_action;
const field_to_edit_changed = this.prev_action && this.prev_action != current_action;
const action_is_pressed_twice = this.prev_action === current_action;
const first_click_event = !this.prev_action;
const field_to_edit_changed = this.prev_action && this.prev_action != current_action;
if (action_is_field_edit) {
if (first_click_event || field_to_edit_changed) {
this.prev_action = current_action;
this.prev_action = current_action;
} else if (action_is_pressed_twice) {
this.prev_action = undefined;
}
this.numpad_value = '';
this.numpad_value = '';
} else if (current_action === 'checkout') {
this.prev_action = undefined;
this.toggle_item_highlight();
@ -688,7 +688,7 @@ erpnext.PointOfSale.ItemCart = class {
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) {
frappe.show_alert({
@ -708,34 +708,34 @@ erpnext.PointOfSale.ItemCart = class {
this.numpad_value = current_action;
}
this.events.numpad_event(this.numpad_value, this.prev_action);
}
highlight_numpad_btn($btn, curr_action) {
const curr_action_is_highlighted = $btn.hasClass('shadow-inner');
const curr_action_is_action = ['qty', 'discount_percentage', 'rate', 'done'].includes(curr_action);
this.events.numpad_event(this.numpad_value, this.prev_action);
}
highlight_numpad_btn($btn, curr_action) {
const curr_action_is_highlighted = $btn.hasClass('shadow-inner');
const curr_action_is_action = ['qty', 'discount_percentage', 'rate', 'done'].includes(curr_action);
if (!curr_action_is_highlighted) {
$btn.addClass('shadow-inner bg-selected');
}
if (this.prev_action === curr_action && curr_action_is_highlighted) {
// if Qty is pressed twice
$btn.removeClass('shadow-inner bg-selected');
}
if (this.prev_action && this.prev_action !== curr_action && curr_action_is_action) {
// Order: Qty -> Rate then remove Qty highlight
const prev_btn = $(`[data-button-value='${this.prev_action}']`);
prev_btn.removeClass('shadow-inner bg-selected');
}
if (!curr_action_is_action || curr_action === 'done') {
// if numbers are clicked
setTimeout(() => {
$btn.removeClass('shadow-inner bg-selected');
}, 100);
}
}
if (!curr_action_is_highlighted) {
$btn.addClass('shadow-inner bg-selected');
}
if (this.prev_action === curr_action && curr_action_is_highlighted) {
// if Qty is pressed twice
$btn.removeClass('shadow-inner bg-selected');
}
if (this.prev_action && this.prev_action !== curr_action && curr_action_is_action) {
// Order: Qty -> Rate then remove Qty highlight
const prev_btn = $(`[data-button-value='${this.prev_action}']`);
prev_btn.removeClass('shadow-inner bg-selected');
}
if (!curr_action_is_action || curr_action === 'done') {
// if numbers are clicked
setTimeout(() => {
$btn.removeClass('shadow-inner bg-selected');
}, 100);
}
}
toggle_numpad(show) {
toggle_numpad(show) {
if (show) {
this.$totals_section.addClass('d-none');
this.$numpad_section.removeClass('d-none');
@ -946,6 +946,6 @@ erpnext.PointOfSale.ItemCart = class {
toggle_component(show) {
show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
}
}
}

View File

@ -1,28 +1,28 @@
erpnext.PointOfSale.ItemDetails = class {
constructor({ wrapper, events }) {
constructor({ wrapper, events }) {
this.wrapper = wrapper;
this.events = events;
this.current_item = {};
this.events = events;
this.current_item = {};
this.init_component();
}
this.init_component();
}
init_component() {
this.prepare_dom();
this.init_child_components();
init_component() {
this.prepare_dom();
this.init_child_components();
this.bind_events();
this.attach_shortcuts();
}
}
prepare_dom() {
this.wrapper.append(
`<section class="col-span-4 flex shadow rounded item-details bg-white mx-h-70 h-100 d-none"></section>`
)
prepare_dom() {
this.wrapper.append(
`<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(
`<div class="details-container flex flex-col p-8 rounded w-full">
<div class="flex justify-between mb-2">
@ -49,28 +49,28 @@ erpnext.PointOfSale.ItemDetails = class {
this.$item_image = this.$component.find('.item-image');
this.$form_container = this.$component.find('.form-container');
this.$dicount_section = this.$component.find('.discount-section');
}
}
toggle_item_details_section(item) {
toggle_item_details_section(item) {
const { item_code, batch_no, uom } = this.current_item;
const item_code_is_same = item && item_code === item.item_code;
const batch_is_same = item && batch_no == item.batch_no;
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);
if (this.item_has_changed) {
this.doctype = item.doctype;
this.doctype = item.doctype;
this.item_meta = frappe.get_meta(this.doctype);
this.name = item.name;
this.item_row = item;
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.currency = this.events.get_frm().doc.currency;
this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom };
this.render_dom(item);
this.render_discount_dom(item);
this.render_form(item);
@ -102,9 +102,9 @@ erpnext.PointOfSale.ItemDetails = class {
this.events.remove_item_from_cart();
}
}
render_dom(item) {
let { item_code ,item_name, description, image, price_list_rate } = item;
render_dom(item) {
let { item_code ,item_name, description, image, price_list_rate } = item;
function get_description_html() {
if (description) {
@ -112,8 +112,8 @@ erpnext.PointOfSale.ItemDetails = class {
return description;
}
return ``;
}
}
this.$item_name.html(item_name);
this.$item_description.html(get_description_html());
this.$item_price.html(format_currency(price_list_rate, this.currency));
@ -125,9 +125,9 @@ erpnext.PointOfSale.ItemDetails = class {
this.$item_image.html(frappe.get_abbr(item_code));
}
}
render_discount_dom(item) {
}
render_discount_dom(item) {
if (item.discount_percentage) {
this.$dicount_section.html(
`<div class="text-grey line-through mr-4 text-md mb-2">
@ -141,9 +141,9 @@ erpnext.PointOfSale.ItemDetails = class {
} else {
this.$dicount_section.html(``)
}
}
}
render_form(item) {
render_form(item) {
const fields_to_display = this.get_form_fields(item);
this.$form_container.html('');
@ -157,7 +157,7 @@ erpnext.PointOfSale.ItemDetails = class {
const field_meta = this.item_meta.fields.find(df => df.fieldname === fieldname);
fieldname === 'discount_percentage' ? (field_meta.label = __('Discount (%)')) : '';
const me = this;
this[`${fieldname}_control`] = frappe.ui.form.make_control({
df: {
...field_meta,
@ -174,16 +174,16 @@ erpnext.PointOfSale.ItemDetails = class {
this.make_auto_serial_selection_btn(item);
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'];
if (item.has_serial_no) fields.push('serial_no');
if (item.has_batch_no) fields.push('batch_no');
return fields;
}
make_auto_serial_selection_btn(item) {
make_auto_serial_selection_btn(item) {
if (item.has_serial_no) {
this.$form_container.append(
`<div class="grid-filler no-select"></div>`
@ -203,8 +203,8 @@ erpnext.PointOfSale.ItemDetails = class {
this.$form_container.find('.serial_no-control').parent().addClass('row-span-2');
}
}
bind_custom_control_change_event() {
bind_custom_control_change_event() {
const me = this;
if (this.rate_control) {
this.rate_control.df.onchange = function() {
@ -276,8 +276,8 @@ erpnext.PointOfSale.ItemDetails = class {
};
this.batch_no_control.df.onchange = function() {
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.current_item.batch_no = this.value;
me.events.form_updated(me.doctype, me.name, 'batch_no', this.value);
me.current_item.batch_no = this.value;
}
this.batch_no_control.refresh();
}
@ -289,9 +289,9 @@ erpnext.PointOfSale.ItemDetails = class {
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) {
const selected_serial_nos = this.serial_no_control.get_value().split(`\n`).filter(s => s);
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_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
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 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();
current_batch_no != batch_no && await this.batch_no_control.set_value(batch_no);
if (serial_nos_belongs_to_other_batch) {
@ -326,8 +326,8 @@ erpnext.PointOfSale.ItemDetails = class {
this.events.clone_new_batch_item_in_frm(batch_serial_map, this.current_item);
}
}
bind_events() {
bind_events() {
this.bind_auto_serial_fetch_event();
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;
this.$form_container.on('click', '.input-with-feedback', function() {
const fieldname = $(this).attr('data-fieldname');
@ -355,8 +355,8 @@ erpnext.PointOfSale.ItemDetails = class {
}
});
}
bind_auto_serial_fetch_event() {
bind_auto_serial_fetch_event() {
this.$form_container.on('click', '.auto-fetch-btn', () => {
this.batch_no_control.set_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.`);
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);
});
})
@ -390,5 +390,5 @@ erpnext.PointOfSale.ItemDetails = class {
toggle_component(show) {
show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
}
}
}

View File

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

View File

@ -1,49 +1,48 @@
erpnext.PointOfSale.NumberPad = class {
constructor({ wrapper, events, cols, keys, css_classes, fieldnames_map }) {
this.wrapper = wrapper;
this.events = events;
this.cols = cols;
this.keys = keys;
this.css_classes = css_classes || [];
this.fieldnames = fieldnames_map || {};
constructor({ wrapper, events, cols, keys, css_classes, fieldnames_map }) {
this.wrapper = wrapper;
this.events = events;
this.cols = cols;
this.keys = keys;
this.css_classes = css_classes || [];
this.fieldnames = fieldnames_map || {};
this.init_component();
}
this.init_component();
}
init_component() {
this.prepare_dom();
this.bind_events();
}
init_component() {
this.prepare_dom();
this.bind_events();
}
prepare_dom() {
const { cols, keys, css_classes, fieldnames } = this;
prepare_dom() {
const { cols, keys, css_classes, fieldnames } = this;
function get_keys() {
return keys.reduce((a, row, i) => {
return a + row.reduce((a2, number, j) => {
const class_to_append = css_classes && css_classes[i] ? css_classes[i][j] : '';
const fieldname = fieldnames && fieldnames[number] ?
fieldnames[number] :
typeof number === 'string' ? frappe.scrub(number) : number;
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>`
}, '')
}, '');
}
function get_keys() {
return keys.reduce((a, row, i) => {
return a + row.reduce((a2, number, j) => {
const class_to_append = css_classes && css_classes[i] ? css_classes[i][j] : '';
const fieldname = fieldnames && fieldnames[number] ?
fieldnames[number] : typeof number === 'string' ? frappe.scrub(number) : number;
this.wrapper.html(
`<div class="grid grid-cols-${cols} gap-4">
${get_keys()}
</div>`
)
}
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>`
}, '')
}, '');
}
bind_events() {
const me = this;
this.wrapper.on('click', '.numpad-btn', function() {
const $btn = $(this);
me.events.numpad_event($btn);
})
}
this.wrapper.html(
`<div class="grid grid-cols-${cols} gap-4">
${get_keys()}
</div>`
)
}
bind_events() {
const me = this;
this.wrapper.on('click', '.numpad-btn', function() {
const $btn = $(this);
me.events.numpad_event($btn);
});
}
}