{% include "erpnext/selling/page/point_of_sale/pos_number_pad.js" %} erpnext.PointOfSale.Payment = class { constructor({ events, wrapper }) { this.wrapper = wrapper; this.events = events; this.init_component(); } init_component() { this.prepare_dom(); this.initialize_numpad(); this.bind_events(); this.attach_shortcuts(); } prepare_dom() { this.wrapper.append( `
PAYMENT METHOD
Complete Order
` ) this.$component = this.wrapper.find('.payment-section'); this.$payment_modes = this.$component.find('.payment-modes'); this.$totals_remarks = this.$component.find('.totals-remarks'); this.$totals = this.$component.find('.totals'); this.$remarks = this.$component.find('.remarks'); this.$numpad = this.$component.find('.number-pad'); this.$invoice_details_section = this.$component.find('.invoice-details-section'); } make_invoice_fields_control() { frappe.db.get_doc("POS Settings", undefined).then((doc) => { const fields = doc.invoice_fields; if (!fields.length) return; this.$invoice_details_section.html( `
ADDITIONAL INFORMATION
` ); this.$invoice_fields = this.$invoice_details_section.find('.invoice-fields'); const frm = this.events.get_frm(); fields.forEach(df => { this.$invoice_fields.append( `
` ); this[`${df.fieldname}_field`] = frappe.ui.form.make_control({ df: { ...df, onchange: function() { frm.set_value(this.df.fieldname, this.value); } }, parent: this.$invoice_fields.find(`.${df.fieldname}-field`), render_input: true, }); this[`${df.fieldname}_field`].set_value(frm.doc[df.fieldname]); }) }); } initialize_numpad() { const me = this; this.number_pad = new erpnext.PointOfSale.NumberPad({ wrapper: this.$numpad, events: { numpad_event: function($btn) { me.on_numpad_clicked($btn); } }, cols: 3, keys: [ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7, 8, 9 ], [ '.', 0, 'Delete' ] ], }) this.numpad_value = ''; } on_numpad_clicked($btn) { const me = this; const button_value = $btn.attr('data-button-value'); highlight_numpad_btn($btn); this.numpad_value = button_value === 'delete' ? this.numpad_value.slice(0, -1) : this.numpad_value + button_value; this.selected_mode.$input.get(0).focus(); this.selected_mode.set_value(this.numpad_value); function highlight_numpad_btn($btn) { $btn.addClass('shadow-inner bg-selected'); setTimeout(() => { $btn.removeClass('shadow-inner bg-selected'); }, 100); } } bind_events() { const me = this; this.$payment_modes.on('click', '.mode-of-payment', function(e) { const mode_clicked = $(this); // if clicked element doesn't have .mode-of-payment class then return if (!$(e.target).is(mode_clicked)) return; const mode = mode_clicked.attr('data-mode'); // hide all control fields and shortcuts $(`.mode-of-payment-control`).addClass('d-none'); $(`.cash-shortcuts`).addClass('d-none'); me.$payment_modes.find(`.pay-amount`).removeClass('d-none'); me.$payment_modes.find(`.loyalty-amount-name`).addClass('d-none'); // remove highlight from all mode-of-payments $('.mode-of-payment').removeClass('border-primary'); if (mode_clicked.hasClass('border-primary')) { // clicked one is selected then unselect it mode_clicked.removeClass('border-primary'); me.selected_mode = ''; me.toggle_numpad(false); } else { // clicked one is not selected then select it mode_clicked.addClass('border-primary'); mode_clicked.find('.mode-of-payment-control').removeClass('d-none'); mode_clicked.find('.cash-shortcuts').removeClass('d-none'); me.$payment_modes.find(`.${mode}-amount`).addClass('d-none'); me.$payment_modes.find(`.${mode}-name`).removeClass('d-none'); me.toggle_numpad(true); me.selected_mode = me[`${mode}_control`]; const doc = me.events.get_frm().doc; me.selected_mode?.$input?.get(0).focus(); !me.selected_mode?.get_value() ? me.selected_mode?.set_value(doc.grand_total - doc.paid_amount) : ''; } }) this.$payment_modes.on('click', '.shortcut', function(e) { const value = $(this).attr('data-value'); me.selected_mode.set_value(value); }) // this.$totals_remarks.on('click', '.remarks', () => { // this.toggle_remarks_control(); // }) this.$component.on('click', '.submit-order', () => { const doc = this.events.get_frm().doc; const paid_amount = doc.paid_amount; const items = doc.items; if (paid_amount == 0 || !items.length) { const message = items.length ? __("You cannot submit the order without payment.") : __("You cannot submit empty order.") frappe.show_alert({ message, indicator: "orange" }); frappe.utils.play_sound("error"); return; } this.events.submit_invoice(); }) frappe.ui.form.on('POS Invoice', 'paid_amount', (frm) => { this.update_totals_section(frm.doc); // need to re calculate cash shortcuts after discount is applied const is_cash_shortcuts_invisible = this.$payment_modes.find('.cash-shortcuts').hasClass('d-none'); this.attach_cash_shortcuts(frm.doc); !is_cash_shortcuts_invisible && this.$payment_modes.find('.cash-shortcuts').removeClass('d-none'); }) frappe.ui.form.on('POS Invoice', 'loyalty_amount', (frm) => { const formatted_currency = format_currency(frm.doc.loyalty_amount, frm.doc.currency); this.$payment_modes.find(`.loyalty-amount-amount`).html(formatted_currency); }); frappe.ui.form.on("Sales Invoice Payment", "amount", (frm, cdt, cdn) => { // for setting correct amount after loyalty points are redeemed const default_mop = locals[cdt][cdn]; const mode = default_mop.mode_of_payment.replace(' ', '_').toLowerCase(); if (this[`${mode}_control`] && this[`${mode}_control`].get_value() != default_mop.amount) { this[`${mode}_control`].set_value(default_mop.amount); } }); this.$component.on('click', '.invoice-details-section', function(e) { if ($(e.target).closest('.invoice-fields').length) return; me.$payment_modes.addClass('d-none'); me.$invoice_fields.toggleClass("d-none"); me.toggle_numpad(false); }); this.$component.on('click', '.payment-section', () => { this.$invoice_fields.addClass("d-none"); this.$payment_modes.toggleClass('d-none'); this.toggle_numpad(true); }) } attach_shortcuts() { frappe.ui.keys.on("ctrl+enter", () => { const payment_is_visible = this.$component.is(":visible"); const active_mode = this.$payment_modes.find(".border-primary"); if (payment_is_visible && active_mode.length) { this.$component.find('.submit-order').click(); } }); frappe.ui.keys.on("tab", () => { const payment_is_visible = this.$component.is(":visible"); const mode_of_payments = Array.from(this.$payment_modes.find(".mode-of-payment")).map(m => $(m).attr("data-mode")); let active_mode = this.$payment_modes.find(".border-primary"); active_mode = active_mode.length ? active_mode.attr("data-mode") : undefined; if (!active_mode) return; const mode_index = mode_of_payments.indexOf(active_mode); const next_mode_index = (mode_index + 1) % mode_of_payments.length; const next_mode_to_be_clicked = this.$payment_modes.find(`.mode-of-payment[data-mode="${mode_of_payments[next_mode_index]}"]`); if (payment_is_visible && mode_index != next_mode_index) { next_mode_to_be_clicked.click(); } }); } toggle_numpad(show) { if (show) { this.$numpad.removeClass('d-none'); this.$remarks.addClass('d-none'); this.$totals_remarks.addClass('w-60 justify-center').removeClass('justify-end w-full'); } else { this.$numpad.addClass('d-none'); this.$remarks.removeClass('d-none'); this.$totals_remarks.removeClass('w-60 justify-center').addClass('justify-end w-full'); } } render_payment_section() { this.render_payment_mode_dom(); this.make_invoice_fields_control(); this.update_totals_section(); } edit_cart() { this.events.toggle_other_sections(false); this.toggle_component(false); } checkout() { this.events.toggle_other_sections(true); this.toggle_component(true); this.render_payment_section(); } toggle_remarks_control() { if (this.$remarks.find('.frappe-control').length) { this.$remarks.html('+ Add Remark'); } else { this.$remarks.html(''); this[`remark_control`] = frappe.ui.form.make_control({ df: { label: __('Remark'), fieldtype: 'Data', onchange: function() {} }, parent: this.$totals_remarks.find(`.remarks`), render_input: true, }); this[`remark_control`].set_value(''); } } render_payment_mode_dom() { const doc = this.events.get_frm().doc; const payments = doc.payments; const currency = doc.currency; this.$payment_modes.html( `${ payments.map((p, i) => { const mode = p.mode_of_payment.replace(' ', '_').toLowerCase(); const payment_type = p.type; const margin = i % 2 === 0 ? 'pr-2' : 'pl-2'; const amount = p.amount > 0 ? format_currency(p.amount, currency) : ''; return ( `
${p.mode_of_payment}
${amount}
` ) }).join('') }` ) payments.forEach(p => { const mode = p.mode_of_payment.replace(' ', '_').toLowerCase(); const me = this; this[`${mode}_control`] = frappe.ui.form.make_control({ df: { label: __(`${p.mode_of_payment}`), fieldtype: 'Currency', placeholder: __(`Enter ${p.mode_of_payment} amount.`), onchange: function() { if (this.value || this.value == 0) { frappe.model.set_value(p.doctype, p.name, 'amount', flt(this.value)) .then(() => me.update_totals_section()); const formatted_currency = format_currency(this.value, currency); me.$payment_modes.find(`.${mode}-amount`).html(formatted_currency); } } }, parent: this.$payment_modes.find(`.${mode}.mode-of-payment-control`), render_input: true, }); this[`${mode}_control`].toggle_label(false); this[`${mode}_control`].set_value(p.amount); if (p.default) { setTimeout(() => { this.$payment_modes.find(`.${mode}.mode-of-payment-control`).parent().click(); }, 500); } }) this.render_loyalty_points_payment_mode(); this.attach_cash_shortcuts(doc); } attach_cash_shortcuts(doc) { const grand_total = doc.grand_total; const currency = doc.currency; const shortcuts = this.get_cash_shortcuts(flt(grand_total)); this.$payment_modes.find('.cash-shortcuts').remove(); this.$payment_modes.find('[data-payment-type="Cash"]').find('.mode-of-payment-control').after( `
${ shortcuts.map(s => { return `
${format_currency(s, currency)}
` }).join('') }
` ) } get_cash_shortcuts(grand_total) { let steps = [1, 5, 10]; const digits = String(Math.round(grand_total)).length; steps = steps.map(x => x * (10 ** (digits - 2))); const get_nearest = (amount, x) => { let nearest_x = Math.ceil((amount / x)) * x; return nearest_x === amount ? nearest_x + x : nearest_x; } return steps.reduce((finalArr, x) => { let nearest_x = get_nearest(grand_total, x); nearest_x = finalArr.indexOf(nearest_x) != -1 ? nearest_x + x : nearest_x; return [...finalArr, nearest_x]; }, []); } render_loyalty_points_payment_mode() { const me = this; const doc = this.events.get_frm().doc; const { loyalty_program, loyalty_points, conversion_factor } = this.events.get_customer_details(); this.$payment_modes.find(`.mode-of-payment[data-mode="loyalty-amount"]`).parent().remove(); if (!loyalty_program) return; let description, read_only, max_redeemable_amount; if (!loyalty_points) { description = __(`You don't have enough points to redeem.`); read_only = true; } else { max_redeemable_amount = flt(flt(loyalty_points) * flt(conversion_factor), precision("loyalty_amount", doc)) description = __(`You can redeem upto ${format_currency(max_redeemable_amount)}.`); read_only = false; } const margin = this.$payment_modes.children().length % 2 === 0 ? 'pr-2' : 'pl-2'; const amount = doc.loyalty_amount > 0 ? format_currency(doc.loyalty_amount, doc.currency) : ''; this.$payment_modes.append( `
Redeem Loyalty Points
${amount}
${loyalty_program}
` ) this['loyalty-amount_control'] = frappe.ui.form.make_control({ df: { label: __('Redeem Loyalty Points'), fieldtype: 'Currency', placeholder: __(`Enter amount to be redeemed.`), options: 'company:currency', read_only, onchange: async function() { if (!loyalty_points) return; if (this.value > max_redeemable_amount) { frappe.show_alert({ message: __(`You cannot redeem more than ${format_currency(max_redeemable_amount)}.`), indicator: "red" }); frappe.utils.play_sound("submit"); me['loyalty-amount_control'].set_value(0); return; } const redeem_loyalty_points = this.value > 0 ? 1 : 0; await frappe.model.set_value(doc.doctype, doc.name, 'redeem_loyalty_points', redeem_loyalty_points); frappe.model.set_value(doc.doctype, doc.name, 'loyalty_points', parseInt(this.value / conversion_factor)); }, description }, parent: this.$payment_modes.find(`.loyalty-amount.mode-of-payment-control`), render_input: true, }); this['loyalty-amount_control'].toggle_label(false); // this.render_add_payment_method_dom(); } render_add_payment_method_dom() { const docstatus = this.events.get_frm().doc.docstatus; if (docstatus === 0) this.$payment_modes.append( `
+ Add Payment Method
` ) } update_totals_section(doc) { if (!doc) doc = this.events.get_frm().doc; const paid_amount = doc.paid_amount; const remaining = doc.grand_total - doc.paid_amount; const change = doc.change_amount || remaining <= 0 ? -1 * remaining : undefined; const currency = doc.currency const label = change ? __('Change') : __('To Be Paid'); this.$totals.html( `
Paid Amount
${format_currency(paid_amount, currency)}
${label}
${format_currency(change || remaining, currency)}
` ) } toggle_component(show) { show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none'); } }