From 60212ff9b999ad33f6e1fb390cbcfd5927389726 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 26 Oct 2020 11:17:04 +0530 Subject: [PATCH] fix: multiple pos issues (#23347) * fix: point of sale search * fix: pos fetches expired serial nos * fix: pos doesn't refresh on route change * fix: opening balances not set in opening entry * fix: remove debug statement * fix: invalid query if no serial no is reserved by pos invoice * chore: make new order btn primary * chore: filter warehouse by company * chore: add shortcuts for new order and email * fix: cannot fetch serial no if no batch control * chore: add shortcuts for menu items * feat: standard keyboard shortcuts for pos page * feat: display only items that are in stock * fix: empty point of sale page if opening entry dialog is closed * feat: conversion factor in pos item details * fix: show all invalid mode of payments * chore: show all items if allow negative stock is checked * fix: -ve amount set when changing mode of payment * fix: pos closing validations * fix: test * fix: non expired serial no fetching query * fix: cannot dismiss pos opening creation dialog * fix: transalation strings * fix: msgprint to throw * chore: use as_list in frappe.throw * chore: clean up pos invoice.js & .py * fix: codacy * fix: transalation syntax * fix: codacy * fix: set_missing_values called twice from pos page * fix: mode selector with double spaces * fix: do not allow tables in pos additional fields * fix: pos is not defined * feat: set mode of payments for returns * fix: remove naming series from pos profile * fix: error message * fix: minor bugs * chore: re-arrange pos closing entry detail fields * fix: sider & frappe linter * fix: more translation strings * fix: travis * fix: more translation strings * fix: sider * fix: travis * fix: unexpected end of string * fix: travis Co-authored-by: Nabin Hait --- .../pos_closing_entry/pos_closing_entry.js | 1 + .../pos_closing_entry/pos_closing_entry.py | 58 +- .../pos_closing_entry_detail.json | 8 +- .../doctype/pos_invoice/pos_invoice.js | 75 +- .../doctype/pos_invoice/pos_invoice.py | 211 +++-- .../pos_invoice_merge_log.py | 27 +- .../pos_opening_entry/pos_opening_entry.py | 15 +- .../pos_payment_method.json | 11 +- .../doctype/pos_profile/pos_profile.json | 20 +- .../doctype/pos_profile/pos_profile.py | 24 +- .../doctype/pos_settings/pos_settings.js | 3 +- .../purchase_invoice/purchase_invoice.py | 54 +- .../doctype/sales_invoice/sales_invoice.py | 30 +- erpnext/controllers/accounts_controller.py | 6 +- erpnext/public/css/pos.css | 1 + .../public/js/controllers/taxes_and_totals.js | 14 +- erpnext/public/js/controllers/transaction.js | 3 +- .../js/utils/serial_no_batch_selector.js | 48 +- .../page/point_of_sale/point_of_sale.js | 10 +- .../page/point_of_sale/point_of_sale.py | 123 ++- .../page/point_of_sale/pos_controller.js | 194 +++-- .../page/point_of_sale/pos_item_cart.js | 93 +- .../page/point_of_sale/pos_item_details.js | 57 +- .../page/point_of_sale/pos_item_selector.js | 27 +- .../page/point_of_sale/pos_past_order_list.js | 196 +++-- .../point_of_sale/pos_past_order_summary.js | 812 +++++++++--------- .../selling/page/point_of_sale/pos_payment.js | 68 +- erpnext/stock/doctype/serial_no/serial_no.py | 113 ++- 28 files changed, 1258 insertions(+), 1044 deletions(-) diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js index 9336fc3706..57baac7681 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js @@ -51,6 +51,7 @@ frappe.ui.form.on('POS Closing Entry', { args: { start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date), end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date), + pos_profile: frm.doc.pos_profile, user: frm.doc.user }, callback: (r) => { diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py index 9899219bdc..2b91c74ce6 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py @@ -14,19 +14,51 @@ from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import class POSClosingEntry(Document): def validate(self): - user = frappe.get_all('POS Closing Entry', - filters = { 'user': self.user, 'docstatus': 1 }, + if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open": + frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry")) + + self.validate_pos_closing() + self.validate_pos_invoices() + + def validate_pos_closing(self): + user = frappe.get_all("POS Closing Entry", + filters = { "user": self.user, "docstatus": 1, "pos_profile": self.pos_profile }, or_filters = { - 'period_start_date': ('between', [self.period_start_date, self.period_end_date]), - 'period_end_date': ('between', [self.period_start_date, self.period_end_date]) + "period_start_date": ("between", [self.period_start_date, self.period_end_date]), + "period_end_date": ("between", [self.period_start_date, self.period_end_date]) }) if user: - frappe.throw(_("POS Closing Entry {} against {} between selected period" - .format(frappe.bold("already exists"), frappe.bold(self.user))), title=_("Invalid Period")) + bold_already_exists = frappe.bold(_("already exists")) + bold_user = frappe.bold(self.user) + frappe.throw(_("POS Closing Entry {} against {} between selected period") + .format(bold_already_exists, bold_user), title=_("Invalid Period")) + + def validate_pos_invoices(self): + invalid_rows = [] + for d in self.pos_transactions: + invalid_row = {'idx': d.idx} + pos_invoice = frappe.db.get_values("POS Invoice", d.pos_invoice, + ["consolidated_invoice", "pos_profile", "docstatus", "owner"], as_dict=1)[0] + if pos_invoice.consolidated_invoice: + invalid_row.setdefault('msg', []).append(_('POS Invoice is {}').format(frappe.bold("already consolidated"))) + invalid_rows.append(invalid_row) + continue + if pos_invoice.pos_profile != self.pos_profile: + invalid_row.setdefault('msg', []).append(_("POS Profile doesn't matches {}").format(frappe.bold(self.pos_profile))) + if pos_invoice.docstatus != 1: + invalid_row.setdefault('msg', []).append(_('POS Invoice is not {}').format(frappe.bold("submitted"))) + if pos_invoice.owner != self.user: + invalid_row.setdefault('msg', []).append(_("POS Invoice isn't created by user {}").format(frappe.bold(self.owner))) - if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open": - frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry")) + if invalid_row.get('msg'): + invalid_rows.append(invalid_row) + + if not invalid_rows: + return + + error_list = [_("Row #{}: {}").format(row.get('idx'), row.get('msg')) for row in invalid_rows] + frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True) def on_submit(self): merge_pos_invoices(self.pos_transactions) @@ -47,16 +79,15 @@ def get_cashiers(doctype, txt, searchfield, start, page_len, filters): return [c['user'] for c in cashiers_list] @frappe.whitelist() -def get_pos_invoices(start, end, user): +def get_pos_invoices(start, end, pos_profile, user): data = frappe.db.sql(""" select name, timestamp(posting_date, posting_time) as "timestamp" from `tabPOS Invoice` where - owner = %s and docstatus = 1 and - (consolidated_invoice is NULL or consolidated_invoice = '') - """, (user), as_dict=1) + owner = %s and docstatus = 1 and pos_profile = %s and ifnull(consolidated_invoice,'') = '' + """, (user, pos_profile), as_dict=1) data = list(filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data)) # need to get taxes and payments so can't avoid get_doc @@ -76,7 +107,8 @@ def make_closing_entry_from_opening(opening_entry): closing_entry.net_total = 0 closing_entry.total_quantity = 0 - invoices = get_pos_invoices(closing_entry.period_start_date, closing_entry.period_end_date, closing_entry.user) + invoices = get_pos_invoices(closing_entry.period_start_date, closing_entry.period_end_date, + closing_entry.pos_profile, closing_entry.user) pos_transactions = [] taxes = [] diff --git a/erpnext/accounts/doctype/pos_closing_entry_detail/pos_closing_entry_detail.json b/erpnext/accounts/doctype/pos_closing_entry_detail/pos_closing_entry_detail.json index 798637a840..6e7768dc54 100644 --- a/erpnext/accounts/doctype/pos_closing_entry_detail/pos_closing_entry_detail.json +++ b/erpnext/accounts/doctype/pos_closing_entry_detail/pos_closing_entry_detail.json @@ -7,8 +7,8 @@ "field_order": [ "mode_of_payment", "opening_amount", - "closing_amount", "expected_amount", + "closing_amount", "difference" ], "fields": [ @@ -26,8 +26,7 @@ "in_list_view": 1, "label": "Expected Amount", "options": "company:company_currency", - "read_only": 1, - "reqd": 1 + "read_only": 1 }, { "fieldname": "difference", @@ -55,9 +54,10 @@ "reqd": 1 } ], + "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-05-29 15:03:34.533607", + "modified": "2020-10-23 16:45:43.662034", "modified_by": "Administrator", "module": "Accounts", "name": "POS Closing Entry Detail", diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js index c43cb794aa..86062d1e7c 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js @@ -9,80 +9,63 @@ erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend( this._super(doc); }, - onload() { + onload(doc) { this._super(); - if(this.frm.doc.__islocal && this.frm.doc.is_pos) { - //Load pos profile data on the invoice if the default value of Is POS is 1 - - me.frm.script_manager.trigger("is_pos"); - me.frm.refresh_fields(); + if(doc.__islocal && doc.is_pos && frappe.get_route_str() !== 'point-of-sale') { + this.frm.script_manager.trigger("is_pos"); + this.frm.refresh_fields(); } }, refresh(doc) { this._super(); if (doc.docstatus == 1 && !doc.is_return) { - if(doc.outstanding_amount >= 0 || Math.abs(flt(doc.outstanding_amount)) < flt(doc.grand_total)) { - cur_frm.add_custom_button(__('Return'), - this.make_sales_return, __('Create')); - cur_frm.page.set_inner_btn_group_as_primary(__('Create')); - } + this.frm.add_custom_button(__('Return'), this.make_sales_return, __('Create')); + this.frm.page.set_inner_btn_group_as_primary(__('Create')); } - if (this.frm.doc.is_return) { + if (doc.is_return && doc.__islocal) { this.frm.return_print_format = "Sales Invoice Return"; - cur_frm.set_value('consolidated_invoice', ''); + this.frm.set_value('consolidated_invoice', ''); } }, - is_pos: function(frm){ + is_pos: function() { this.set_pos_data(); }, - set_pos_data: function() { + set_pos_data: async function() { if(this.frm.doc.is_pos) { this.frm.set_value("allocate_advances_automatically", 0); if(!this.frm.doc.company) { this.frm.set_value("is_pos", 0); frappe.msgprint(__("Please specify Company to proceed")); } else { - var me = this; - return this.frm.call({ - doc: me.frm.doc, + const r = await this.frm.call({ + doc: this.frm.doc, method: "set_missing_values", - callback: function(r) { - if(!r.exc) { - if(r.message) { - me.frm.pos_print_format = r.message.print_format || ""; - me.frm.meta.default_print_format = r.message.print_format || ""; - me.frm.allow_edit_rate = r.message.allow_edit_rate; - me.frm.allow_edit_discount = r.message.allow_edit_discount; - me.frm.doc.campaign = r.message.campaign; - me.frm.allow_print_before_pay = r.message.allow_print_before_pay; - } - me.frm.script_manager.trigger("update_stock"); - me.calculate_taxes_and_totals(); - if(me.frm.doc.taxes_and_charges) { - me.frm.script_manager.trigger("taxes_and_charges"); - } - frappe.model.set_default_values(me.frm.doc); - me.set_dynamic_labels(); - - } - } + freeze: true }); + if(!r.exc) { + if(r.message) { + this.frm.pos_print_format = r.message.print_format || ""; + this.frm.meta.default_print_format = r.message.print_format || ""; + this.frm.doc.campaign = r.message.campaign; + this.frm.allow_print_before_pay = r.message.allow_print_before_pay; + } + this.frm.script_manager.trigger("update_stock"); + this.calculate_taxes_and_totals(); + this.frm.doc.taxes_and_charges && this.frm.script_manager.trigger("taxes_and_charges"); + frappe.model.set_default_values(this.frm.doc); + this.set_dynamic_labels(); + } } } - else this.frm.trigger("refresh"); }, customer() { if (!this.frm.doc.customer) return - - if (this.frm.doc.is_pos){ - var pos_profile = this.frm.doc.pos_profile; - } - var me = this; + const pos_profile = this.frm.doc.pos_profile; if(this.frm.updating_party_details) return; erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details", { @@ -92,8 +75,8 @@ erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend( account: this.frm.doc.debit_to, price_list: this.frm.doc.selling_price_list, pos_profile: pos_profile - }, function() { - me.apply_pricing_rule(); + }, () => { + this.apply_pricing_rule(); }); }, diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 5b0822e323..b0a7547ce8 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -10,12 +10,10 @@ from erpnext.controllers.selling_controller import SellingController from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate from erpnext.accounts.utils import get_account_currency from erpnext.accounts.party import get_party_account, get_due_date -from erpnext.accounts.doctype.loyalty_program.loyalty_program import \ - get_loyalty_program_details_with_points, validate_loyalty_points - -from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice, get_bank_cash_account, update_multi_mode_option -from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request +from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points +from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos +from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice, get_bank_cash_account, update_multi_mode_option, get_mode_of_payment_info from six import iteritems @@ -30,8 +28,7 @@ class POSInvoice(SalesInvoice): # run on validate method of selling controller super(SalesInvoice, self).validate() self.validate_auto_set_posting_time() - self.validate_pos_paid_amount() - self.validate_pos_return() + self.validate_mode_of_payment() self.validate_uom_is_integer("stock_uom", "stock_qty") self.validate_uom_is_integer("uom", "qty") self.validate_debit_to_acc() @@ -41,11 +38,11 @@ class POSInvoice(SalesInvoice): self.validate_item_cost_centers() self.validate_serialised_or_batched_item() self.validate_stock_availablility() - self.validate_return_items() + self.validate_return_items_qty() self.set_status() self.set_account_for_mode_of_payment() self.validate_pos() - self.verify_payment_amount() + self.validate_payment_amount() self.validate_loyalty_transaction() def on_submit(self): @@ -84,70 +81,98 @@ class POSInvoice(SalesInvoice): return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment)) def validate_stock_availablility(self): - allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock') + if self.is_return: + return + allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock') + error_msg = [] for d in self.get('items'): + msg = "" if d.serial_no: - filters = { - "item_code": d.item_code, - "warehouse": d.warehouse, - "delivery_document_no": "", - "sales_invoice": "" - } + filters = { "item_code": d.item_code, "warehouse": d.warehouse } if d.batch_no: filters["batch_no"] = d.batch_no - reserved_serial_nos, unreserved_serial_nos = get_pos_reserved_serial_nos(filters) - serial_nos = d.serial_no.split("\n") - serial_nos = ' '.join(serial_nos).split() # remove whitespaces - invalid_serial_nos = [] - for s in serial_nos: - if s in reserved_serial_nos: - invalid_serial_nos.append(s) - if len(invalid_serial_nos): - multiple_nos = 's' if len(invalid_serial_nos) > 1 else '' - frappe.throw(_("Row #{}: Serial No{}. {} has already been transacted into another POS Invoice. Please select valid serial no.").format( - d.idx, multiple_nos, frappe.bold(', '.join(invalid_serial_nos))), title=_("Not Available")) + reserved_serial_nos = get_pos_reserved_serial_nos(filters) + serial_nos = get_serial_nos(d.serial_no) + invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos] + + bold_invalid_serial_nos = frappe.bold(', '.join(invalid_serial_nos)) + if len(invalid_serial_nos) == 1: + msg = (_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.") + .format(d.idx, bold_invalid_serial_nos)) + elif invalid_serial_nos: + msg = (_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.") + .format(d.idx, bold_invalid_serial_nos)) + else: if allow_negative_stock: return available_stock = get_stock_availability(d.item_code, d.warehouse) - if not (flt(available_stock) > 0): - frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.').format( - d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse)), title=_("Not Available")) + item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty) + if flt(available_stock) <= 0: + msg = (_('Row #{}: Item Code: {} is not available under warehouse {}.').format(d.idx, item_code, warehouse)) elif flt(available_stock) < flt(d.qty): - frappe.msgprint(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.').format( - d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)), title=_("Not Available")) + msg = (_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.') + .format(d.idx, item_code, warehouse, qty)) + if msg: + error_msg.append(msg) + + if error_msg: + frappe.throw(error_msg, title=_("Item Unavailable"), as_list=True) def validate_serialised_or_batched_item(self): + error_msg = [] for d in self.get("items"): serialized = d.get("has_serial_no") batched = d.get("has_batch_no") no_serial_selected = not d.get("serial_no") no_batch_selected = not d.get("batch_no") - + msg = "" + item_code = frappe.bold(d.item_code) if serialized and batched and (no_batch_selected or no_serial_selected): - frappe.throw(_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.').format( - d.idx, frappe.bold(d.item_code)), title=_("Invalid Item")) + msg = (_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.') + .format(d.idx, item_code)) if serialized and no_serial_selected: - frappe.throw(_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.').format( - d.idx, frappe.bold(d.item_code)), title=_("Invalid Item")) + msg = (_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.') + .format(d.idx, item_code)) if batched and no_batch_selected: - frappe.throw(_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.').format( - d.idx, frappe.bold(d.item_code)), title=_("Invalid Item")) + msg = (_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.') + .format(d.idx, item_code)) + if msg: + error_msg.append(msg) - def validate_return_items(self): + if error_msg: + frappe.throw(error_msg, title=_("Invalid Item"), as_list=True) + + def validate_return_items_qty(self): if not self.get("is_return"): return for d in self.get("items"): if d.get("qty") > 0: - frappe.throw(_("Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return.") - .format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item")) + frappe.throw( + _("Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return.") + .format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item") + ) + if d.get("serial_no"): + serial_nos = get_serial_nos(d.serial_no) + for sr in serial_nos: + serial_no_exists = frappe.db.exists("POS Invoice Item", { + "parent": self.return_against, + "serial_no": ["like", d.get("serial_no")] + }) + if not serial_no_exists: + bold_return_against = frappe.bold(self.return_against) + bold_serial_no = frappe.bold(sr) + frappe.throw( + _("Row #{}: Serial No {} cannot be returned since it was not transacted in original invoice {}") + .format(d.idx, bold_serial_no, bold_return_against) + ) - def validate_pos_paid_amount(self): - if len(self.payments) == 0 and self.is_pos: + def validate_mode_of_payment(self): + if len(self.payments) == 0: frappe.throw(_("At least one mode of payment is required for POS invoice.")) def validate_change_account(self): @@ -165,20 +190,18 @@ class POSInvoice(SalesInvoice): if flt(self.change_amount) and not self.account_for_change_amount: frappe.msgprint(_("Please enter Account for Change Amount"), raise_exception=1) - def verify_payment_amount(self): + def validate_payment_amount(self): + total_amount_in_payments = 0 for entry in self.payments: + total_amount_in_payments += entry.amount if not self.is_return and entry.amount < 0: frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx)) if self.is_return and entry.amount > 0: frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx)) - def validate_pos_return(self): - if self.is_pos and self.is_return: - total_amount_in_payments = 0 - for payment in self.payments: - total_amount_in_payments += payment.amount + if self.is_return: invoice_total = self.rounded_total or self.grand_total - if total_amount_in_payments < invoice_total: + if total_amount_in_payments and total_amount_in_payments < invoice_total: frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total)) def validate_loyalty_transaction(self): @@ -233,55 +256,45 @@ class POSInvoice(SalesInvoice): pos_profile = get_pos_profile(self.company) or {} self.pos_profile = pos_profile.get('name') - pos = {} + profile = {} if self.pos_profile: - pos = frappe.get_doc('POS Profile', self.pos_profile) + profile = frappe.get_doc('POS Profile', self.pos_profile) if not self.get('payments') and not for_validate: - update_multi_mode_option(self, pos) - - if not self.account_for_change_amount: - self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account') - - if pos: - if not for_validate: - self.tax_category = pos.get("tax_category") + update_multi_mode_option(self, profile) + + if self.is_return and not for_validate: + add_return_modes(self, profile) + if profile: if not for_validate and not self.customer: - self.customer = pos.customer + self.customer = profile.customer - self.ignore_pricing_rule = pos.ignore_pricing_rule - if pos.get('account_for_change_amount'): - self.account_for_change_amount = pos.get('account_for_change_amount') - if pos.get('warehouse'): - self.set_warehouse = pos.get('warehouse') + self.ignore_pricing_rule = profile.ignore_pricing_rule + self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount + self.set_warehouse = profile.get('warehouse') or self.set_warehouse - for fieldname in ('naming_series', 'currency', 'letter_head', 'tc_name', + for fieldname in ('currency', 'letter_head', 'tc_name', 'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges', - 'write_off_cost_center', 'apply_discount_on', 'cost_center'): - if (not for_validate) or (for_validate and not self.get(fieldname)): - self.set(fieldname, pos.get(fieldname)) - - if pos.get("company_address"): - self.company_address = pos.get("company_address") + 'write_off_cost_center', 'apply_discount_on', 'cost_center', 'tax_category', + 'ignore_pricing_rule', 'company_address', 'update_stock'): + if not for_validate: + self.set(fieldname, profile.get(fieldname)) if self.customer: customer_price_list, customer_group = frappe.db.get_value("Customer", self.customer, ['default_price_list', 'customer_group']) customer_group_price_list = frappe.db.get_value("Customer Group", customer_group, 'default_price_list') - selling_price_list = customer_price_list or customer_group_price_list or pos.get('selling_price_list') + selling_price_list = customer_price_list or customer_group_price_list or profile.get('selling_price_list') else: - selling_price_list = pos.get('selling_price_list') + selling_price_list = profile.get('selling_price_list') if selling_price_list: self.set('selling_price_list', selling_price_list) - if not for_validate: - self.update_stock = cint(pos.get("update_stock")) - # set pos values in items for item in self.get("items"): if item.get('item_code'): - profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos) + profile_details = get_pos_profile_item_details(profile.get("company"), frappe._dict(item.as_dict()), profile) for fname, val in iteritems(profile_details): if (not for_validate) or (for_validate and not item.get(fname)): item.set(fname, val) @@ -294,10 +307,13 @@ class POSInvoice(SalesInvoice): if self.taxes_and_charges and not len(self.get("taxes")): self.set_taxes() - return pos + if not self.account_for_change_amount: + self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account') + + return profile def set_missing_values(self, for_validate=False): - pos = self.set_pos_fields(for_validate) + profile = self.set_pos_fields(for_validate) if not self.debit_to: self.debit_to = get_party_account("Customer", self.customer, self.company) @@ -307,17 +323,15 @@ class POSInvoice(SalesInvoice): super(SalesInvoice, self).set_missing_values(for_validate) - print_format = pos.get("print_format") if pos else None + print_format = profile.get("print_format") if profile else None if not print_format and not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')): print_format = 'POS Invoice' - if pos: + if profile: return { "print_format": print_format, - "allow_edit_rate": pos.get("allow_user_to_edit_rate"), - "allow_edit_discount": pos.get("allow_user_to_edit_discount"), - "campaign": pos.get("campaign"), - "allow_print_before_pay": pos.get("allow_print_before_pay") + "campaign": profile.get("campaign"), + "allow_print_before_pay": profile.get("allow_print_before_pay") } def set_account_for_mode_of_payment(self): @@ -373,11 +387,9 @@ def get_stock_availability(item_code, warehouse): sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0 pos_sales_qty = pos_sales_qty[0].qty or 0 if pos_sales_qty else 0 - if sle_qty and pos_sales_qty and sle_qty > pos_sales_qty: + if sle_qty and pos_sales_qty: return sle_qty - pos_sales_qty else: - # when sle_qty is 0 - # when sle_qty > 0 and pos_sales_qty is 0 return sle_qty @frappe.whitelist() @@ -410,4 +422,19 @@ def make_merge_log(invoices): }) if merge_log.get('pos_invoices'): - return merge_log.as_dict() \ No newline at end of file + return merge_log.as_dict() + +def add_return_modes(doc, pos_profile): + def append_payment(payment_mode): + payment = doc.append('payments', {}) + payment.default = payment_mode.default + payment.mode_of_payment = payment_mode.parent + payment.account = payment_mode.default_account + payment.type = payment_mode.type + + for pos_payment_method in pos_profile.get('payments'): + pos_payment_method = pos_payment_method.as_dict() + mode_of_payment = pos_payment_method.mode_of_payment + if pos_payment_method.allow_in_returns and not [d for d in doc.get('payments') if d.mode_of_payment == mode_of_payment]: + payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company) + append_payment(payment_mode[0]) \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 3a229b1787..add27e9dff 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -26,18 +26,25 @@ class POSInvoiceMergeLog(Document): for d in self.pos_invoices: status, docstatus, is_return, return_against = frappe.db.get_value( 'POS Invoice', d.pos_invoice, ['status', 'docstatus', 'is_return', 'return_against']) - + + bold_pos_invoice = frappe.bold(d.pos_invoice) + bold_status = frappe.bold(status) if docstatus != 1: - frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, d.pos_invoice)) + frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, bold_pos_invoice)) 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)) - ) + frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, bold_pos_invoice, bold_status)) + if is_return and return_against and return_against not in [d.pos_invoice for d in self.pos_invoices]: + bold_return_against = frappe.bold(return_against) + return_against_status = frappe.db.get_value('POS Invoice', return_against, "status") + if return_against_status != "Consolidated": + # if return entry is not getting merged in the current pos closing and if it is not consolidated + bold_unconsolidated = frappe.bold("not Consolidated") + msg = (_("Row #{}: Original Invoice {} of return invoice {} is {}. ") + .format(d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated)) + msg += _("Original invoice should be consolidated before or along with the return invoice.") + msg += "

" + msg += _("You can add original invoice {} manually to proceed.").format(bold_return_against) + frappe.throw(msg) def on_submit(self): pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] diff --git a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py index b9e07b8030..acac1c4072 100644 --- a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py +++ b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py @@ -17,18 +17,25 @@ class POSOpeningEntry(StatusUpdater): def validate_pos_profile_and_cashier(self): if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"): - frappe.throw(_("POS Profile {} does not belongs to company {}".format(self.pos_profile, self.company))) + frappe.throw(_("POS Profile {} does not belongs to company {}").format(self.pos_profile, self.company)) if not cint(frappe.db.get_value("User", self.user, "enabled")): - frappe.throw(_("User {} has been disabled. Please select valid user/cashier".format(self.user))) + frappe.throw(_("User {} is disabled. Please select valid user/cashier").format(self.user)) def validate_payment_method_account(self): + invalid_modes = [] for d in self.balance_details: account = frappe.db.get_value("Mode of Payment Account", {"parent": d.mode_of_payment, "company": self.company}, "default_account") if not account: - frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}") - .format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account")) + invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment)) + + if invalid_modes: + if invalid_modes == 1: + msg = _("Please set default Cash or Bank account in Mode of Payment {}") + else: + msg = _("Please set default Cash or Bank account in Mode of Payments {}") + frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account")) def on_submit(self): self.set_status(update=True) \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_payment_method/pos_payment_method.json b/erpnext/accounts/doctype/pos_payment_method/pos_payment_method.json index 4d5e1eb798..30ebd307c4 100644 --- a/erpnext/accounts/doctype/pos_payment_method/pos_payment_method.json +++ b/erpnext/accounts/doctype/pos_payment_method/pos_payment_method.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "default", + "allow_in_returns", "mode_of_payment" ], "fields": [ @@ -24,11 +25,19 @@ "label": "Mode of Payment", "options": "Mode of Payment", "reqd": 1 + }, + { + "default": "0", + "fieldname": "allow_in_returns", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Allow In Returns" } ], + "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-05-29 15:08:41.704844", + "modified": "2020-10-20 12:58:46.114456", "modified_by": "Administrator", "module": "Accounts", "name": "POS Payment Method", diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index 999da75997..4e22218c6e 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -290,28 +290,30 @@ "fieldname": "warehouse", "fieldtype": "Link", "label": "Warehouse", - "mandatory_depends_on": "update_stock", "oldfieldname": "warehouse", "oldfieldtype": "Link", - "options": "Warehouse" - }, - { - "default": "0", - "fieldname": "update_stock", - "fieldtype": "Check", - "label": "Update Stock" + "options": "Warehouse", + "reqd": 1 }, { "default": "0", "fieldname": "ignore_pricing_rule", "fieldtype": "Check", "label": "Ignore Pricing Rule" + }, + { + "default": "1", + "fieldname": "update_stock", + "fieldtype": "Check", + "label": "Update Stock", + "read_only": 1 } ], "icon": "icon-cog", "idx": 1, + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-10-01 17:29:27.759088", + "modified": "2020-10-20 13:16:50.665081", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index 1d160a5aa7..ee76bba750 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -56,19 +56,29 @@ class POSProfile(Document): if not self.payments: frappe.throw(_("Payment methods are mandatory. Please add at least one payment method.")) - default_mode_of_payment = [d.default for d in self.payments if d.default] - if not default_mode_of_payment: + default_mode = [d.default for d in self.payments if d.default] + if not default_mode: frappe.throw(_("Please select a default mode of payment")) - if len(default_mode_of_payment) > 1: + if len(default_mode) > 1: frappe.throw(_("You can only select one mode of payment as default")) + invalid_modes = [] for d in self.payments: - account = frappe.db.get_value("Mode of Payment Account", - {"parent": d.mode_of_payment, "company": self.company}, "default_account") + account = frappe.db.get_value( + "Mode of Payment Account", + {"parent": d.mode_of_payment, "company": self.company}, + "default_account" + ) if not account: - frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}") - .format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account")) + invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment)) + + if invalid_modes: + if invalid_modes == 1: + msg = _("Please set default Cash or Bank account in Mode of Payment {}") + else: + msg = _("Please set default Cash or Bank account in Mode of Payments {}") + frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account")) def on_update(self): self.set_defaults() diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.js b/erpnext/accounts/doctype/pos_settings/pos_settings.js index 05cb7f0b4b..8890d59403 100644 --- a/erpnext/accounts/doctype/pos_settings/pos_settings.js +++ b/erpnext/accounts/doctype/pos_settings/pos_settings.js @@ -9,8 +9,7 @@ frappe.ui.form.on('POS Settings', { get_invoice_fields: function(frm) { frappe.model.with_doctype("POS Invoice", () => { var fields = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function(d) { - if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 || - ['Table', 'Button'].includes(d.fieldtype)) { + if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 || ['Button'].includes(d.fieldtype)) { return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname }; } else { return null; diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index c5260a1239..91c4dfb587 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -151,14 +151,16 @@ class PurchaseInvoice(BuyingController): ["account_type", "report_type", "account_currency"], as_dict=True) if account.report_type != "Balance Sheet": - frappe.throw(_("Please ensure {} account is a Balance Sheet account. \ - You can change the parent account to a Balance Sheet account or select a different account.") - .format(frappe.bold("Credit To")), title=_("Invalid Account")) + frappe.throw( + _("Please ensure {} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account.") + .format(frappe.bold("Credit To")), title=_("Invalid Account") + ) if self.supplier and account.account_type != "Payable": - frappe.throw(_("Please ensure {} account is a Payable account. \ - Change the account type to Payable or select a different account.") - .format(frappe.bold("Credit To")), title=_("Invalid Account")) + frappe.throw( + _("Please ensure {} account is a Payable account. Change the account type to Payable or select a different account.") + .format(frappe.bold("Credit To")), title=_("Invalid Account") + ) self.party_account_currency = account.account_currency @@ -244,10 +246,10 @@ class PurchaseInvoice(BuyingController): if self.update_stock and (not item.from_warehouse): if for_validate and item.expense_account and item.expense_account != warehouse_account[item.warehouse]["account"]: - frappe.msgprint(_('''Row {0}: Expense Head changed to {1} because account {2} - is not linked to warehouse {3} or it is not the default inventory account'''.format( - item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]), - frappe.bold(item.expense_account), frappe.bold(item.warehouse)))) + msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(warehouse_account[item.warehouse]["account"])) + msg += _("because account {} is not linked to warehouse {} ").format(frappe.bold(item.expense_account), frappe.bold(item.warehouse)) + msg += _("or it is not the default inventory account") + frappe.msgprint(msg, title=_("Expense Head Changed")) item.expense_account = warehouse_account[item.warehouse]["account"] else: @@ -259,19 +261,19 @@ class PurchaseInvoice(BuyingController): if negative_expense_booked_in_pr: if for_validate and item.expense_account and item.expense_account != stock_not_billed_account: - frappe.msgprint(_('''Row {0}: Expense Head changed to {1} because - expense is booked against this account in Purchase Receipt {2}'''.format( - item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.purchase_receipt)))) + msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account)) + msg += _("because expense is booked against this account in Purchase Receipt {}").format(frappe.bold(item.purchase_receipt)) + frappe.msgprint(msg, title=_("Expense Head Changed")) item.expense_account = stock_not_billed_account else: # If no purchase receipt present then book expense in 'Stock Received But Not Billed' # This is done in cases when Purchase Invoice is created before Purchase Receipt if for_validate and item.expense_account and item.expense_account != stock_not_billed_account: - frappe.msgprint(_('''Row {0}: Expense Head changed to {1} as no Purchase - Receipt is created against Item {2}. This is done to handle accounting for cases - when Purchase Receipt is created after Purchase Invoice'''.format( - item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.item_code)))) + msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account)) + msg += _("as no Purchase Receipt is created against Item {}. ").format(frappe.bold(item.item_code)) + msg += _("This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice") + frappe.msgprint(msg, title=_("Expense Head Changed")) item.expense_account = stock_not_billed_account @@ -299,10 +301,11 @@ class PurchaseInvoice(BuyingController): for d in self.get('items'): if not d.purchase_order: - throw(_("""Purchase Order Required for item {0} - To submit the invoice without purchase order please set - {1} as {2} in {3}""").format(frappe.bold(d.item_code), frappe.bold(_('Purchase Order Required')), - frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))) + msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code)) + msg += "

" + msg += _("To submit the invoice without purchase order please set {} ").format(frappe.bold(_('Purchase Order Required'))) + msg += _("as {} in {}").format(frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings')) + throw(msg, title=_("Mandatory Purchase Order")) def pr_required(self): stock_items = self.get_stock_items() @@ -313,10 +316,11 @@ class PurchaseInvoice(BuyingController): for d in self.get('items'): if not d.purchase_receipt and d.item_code in stock_items: - throw(_("""Purchase Receipt Required for item {0} - To submit the invoice without purchase receipt please set - {1} as {2} in {3}""").format(frappe.bold(d.item_code), frappe.bold(_('Purchase Receipt Required')), - frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))) + msg = _("Purchase Receipt Required for item {}").format(frappe.bold(d.item_code)) + msg += "

" + msg += _("To submit the invoice without purchase receipt please set {} ").format(frappe.bold(_('Purchase Receipt Required'))) + msg += _("as {} in {}").format(frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings')) + throw(msg, title=_("Mandatory Purchase Receipt")) def validate_write_off_account(self): if self.write_off_amount and not self.write_off_account: diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 801e688deb..4b598877d9 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -479,14 +479,14 @@ class SalesInvoice(SellingController): frappe.throw(_("Debit To is required"), title=_("Account Missing")) if account.report_type != "Balance Sheet": - frappe.throw(_("Please ensure {} account is a Balance Sheet account. \ - You can change the parent account to a Balance Sheet account or select a different account.") - .format(frappe.bold("Debit To")), title=_("Invalid Account")) + msg = _("Please ensure {} account is a Balance Sheet account. ").format(frappe.bold("Debit To")) + msg += _("You can change the parent account to a Balance Sheet account or select a different account.") + frappe.throw(msg, title=_("Invalid Account")) if self.customer and account.account_type != "Receivable": - frappe.throw(_("Please ensure {} account is a Receivable account. \ - Change the account type to Receivable or select a different account.") - .format(frappe.bold("Debit To")), title=_("Invalid Account")) + msg = _("Please ensure {} account is a Receivable account. ").format(frappe.bold("Debit To")) + msg += _("Change the account type to Receivable or select a different account.") + frappe.throw(msg, title=_("Invalid Account")) self.party_account_currency = account.account_currency @@ -1141,8 +1141,10 @@ class SalesInvoice(SellingController): where redeem_against=%s''', (lp_entry[0].name), as_dict=1) if against_lp_entry: invoice_list = ", ".join([d.invoice for d in against_lp_entry]) - frappe.throw(_('''{} can't be cancelled since the Loyalty Points earned has been redeemed. - First cancel the {} No {}''').format(self.doctype, self.doctype, invoice_list)) + frappe.throw( + _('''{} can't be cancelled since the Loyalty Points earned has been redeemed. First cancel the {} No {}''') + .format(self.doctype, self.doctype, invoice_list) + ) else: frappe.db.sql('''delete from `tabLoyalty Point Entry` where invoice=%s''', (self.name)) # Set loyalty program @@ -1613,17 +1615,25 @@ def update_multi_mode_option(doc, pos_profile): payment.type = payment_mode.type doc.set('payments', []) + invalid_modes = [] for pos_payment_method in pos_profile.get('payments'): pos_payment_method = pos_payment_method.as_dict() payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company) if not payment_mode: - frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}") - .format(get_link_to_form("Mode of Payment", pos_payment_method.mode_of_payment)), title=_("Missing Account")) + invalid_modes.append(get_link_to_form("Mode of Payment", pos_payment_method.mode_of_payment)) + continue payment_mode[0].default = pos_payment_method.default append_payment(payment_mode[0]) + if invalid_modes: + if invalid_modes == 1: + msg = _("Please set default Cash or Bank account in Mode of Payment {}") + else: + msg = _("Please set default Cash or Bank account in Mode of Payments {}") + frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account")) + def get_all_mode_of_payments(doc): return frappe.db.sql(""" select mpa.default_account, mpa.parent, mp.type as type diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index fc32977658..983cfa8c15 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -946,8 +946,10 @@ def validate_conversion_rate(currency, conversion_rate, conversion_rate_label, c company_currency = frappe.get_cached_value('Company', company, "default_currency") if not conversion_rate: - throw(_("{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}.").format( - conversion_rate_label, currency, company_currency)) + throw( + _("{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}.") + .format(conversion_rate_label, currency, company_currency) + ) def validate_taxes_and_charges(tax): diff --git a/erpnext/public/css/pos.css b/erpnext/public/css/pos.css index e80e3ed126..47f577131a 100644 --- a/erpnext/public/css/pos.css +++ b/erpnext/public/css/pos.css @@ -210,6 +210,7 @@ [data-route="point-of-sale"] .item-summary-wrapper:last-child { border-bottom: none; } [data-route="point-of-sale"] .total-summary-wrapper:last-child { border-bottom: none; } [data-route="point-of-sale"] .invoices-container .invoice-wrapper:last-child { border-bottom: none; } +[data-route="point-of-sale"] .new-btn { background-color: #5e64ff; color: white; border: none;} [data-route="point-of-sale"] .summary-btns:last-child { margin-right: 0px; } [data-route="point-of-sale"] ::-webkit-scrollbar { width: 1px } diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index be30086d34..99f3995a66 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -6,6 +6,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ apply_pricing_rule_on_item: function(item){ let effective_item_rate = item.price_list_rate; + let item_rate = item.rate; if (in_list(["Sales Order", "Quotation"], item.parenttype) && item.blanket_order_rate) { effective_item_rate = item.blanket_order_rate; } @@ -17,15 +18,17 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } item.base_rate_with_margin = flt(item.rate_with_margin) * flt(this.frm.doc.conversion_rate); - item.rate = flt(item.rate_with_margin , precision("rate", item)); + item_rate = flt(item.rate_with_margin , precision("rate", item)); if(item.discount_percentage){ item.discount_amount = flt(item.rate_with_margin) * flt(item.discount_percentage) / 100; } if (item.discount_amount) { - item.rate = flt((item.rate_with_margin) - (item.discount_amount), precision('rate', item)); + item_rate = flt((item.rate_with_margin) - (item.discount_amount), precision('rate', item)); } + + frappe.model.set_value(item.doctype, item.name, "rate", item_rate); }, calculate_taxes_and_totals: function(update_paid_amount) { @@ -88,11 +91,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ if(this.frm.doc.currency == company_currency) { this.frm.set_value("conversion_rate", 1); } else { - const err_message = __('{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}', [ - conversion_rate_label, - this.frm.doc.currency, - company_currency - ]); + const subs = [conversion_rate_label, this.frm.doc.currency, company_currency]; + const err_message = __('{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}', subs); frappe.throw(err_message); } } diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 33911793f6..23705a8779 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1049,14 +1049,13 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ if(item.item_code && item.uom) { return this.frm.call({ method: "erpnext.stock.get_item_details.get_conversion_factor", - child: item, args: { item_code: item.item_code, uom: item.uom }, callback: function(r) { if(!r.exc) { - me.conversion_factor(me.frm.doc, cdt, cdn); + frappe.model.set_value(cdt, cdn, 'conversion_factor', r.message.conversion_factor); } } }); diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index d9f6e1d433..2623c3c1a7 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -75,7 +75,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ fieldtype:'Float', read_only: me.has_batch && !me.has_serial_no, label: __(me.has_batch && !me.has_serial_no ? 'Total Qty' : 'Qty'), - default: 0 + default: flt(me.item.stock_qty), }, { fieldname: 'auto_fetch_button', @@ -91,7 +91,8 @@ erpnext.SerialNoBatchSelector = Class.extend({ qty: qty, item_code: me.item_code, warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '', - batch_no: me.item.batch_no || null + batch_no: me.item.batch_no || null, + posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date } }); @@ -100,11 +101,12 @@ erpnext.SerialNoBatchSelector = Class.extend({ let records_length = auto_fetched_serial_numbers.length; if (!records_length) { const warehouse = me.dialog.fields_dict.warehouse.get_value().bold(); - frappe.msgprint(__(`Serial numbers unavailable for Item ${me.item.item_code.bold()} - under warehouse ${warehouse}. Please try changing warehouse.`)); + frappe.msgprint( + __('Serial numbers unavailable for Item {0} under warehouse {1}. Please try changing warehouse.', [me.item.item_code.bold(), warehouse]) + ); } if (records_length < qty) { - frappe.msgprint(__(`Fetched only ${records_length} available serial numbers.`)); + frappe.msgprint(__('Fetched only {0} available serial numbers.', [records_length])); } let serial_no_list_field = this.dialog.fields_dict.serial_no; numbers = auto_fetched_serial_numbers.join('\n'); @@ -189,15 +191,12 @@ erpnext.SerialNoBatchSelector = Class.extend({ } if(this.has_batch && !this.has_serial_no) { if(values.batches.length === 0 || !values.batches) { - frappe.throw(__("Please select batches for batched item " - + values.item_code)); - return false; + frappe.throw(__("Please select batches for batched item {0}", [values.item_code])); } values.batches.map((batch, i) => { if(!batch.selected_qty || batch.selected_qty === 0 ) { if (!this.show_dialog) { - frappe.throw(__("Please select quantity on row " + (i+1))); - return false; + frappe.throw(__("Please select quantity on row {0}", [i+1])); } } }); @@ -206,9 +205,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ } else { let serial_nos = values.serial_no || ''; if (!serial_nos || !serial_nos.replace(/\s/g, '').length) { - frappe.throw(__("Please enter serial numbers for serialized item " - + values.item_code)); - return false; + frappe.throw(__("Please enter serial numbers for serialized item {0}", [values.item_code])); } return true; } @@ -355,8 +352,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ }); if (selected_batches.includes(val)) { this.set_value(""); - frappe.throw(__(`Batch ${val} already selected.`)); - return; + frappe.throw(__('Batch {0} already selected.', [val])); } if (me.warehouse_details.name) { @@ -375,8 +371,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ } else { this.set_value(""); - frappe.throw(__(`Please select a warehouse to get available - quantities`)); + frappe.throw(__('Please select a warehouse to get available quantities')); } // e.stopImmediatePropagation(); } @@ -411,8 +406,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ parseFloat(available_qty) < parseFloat(selected_qty)) { this.set_value('0'); - frappe.throw(__(`For transfer from source, selected quantity cannot be - greater than available quantity`)); + frappe.throw(__('For transfer from source, selected quantity cannot be greater than available quantity')); } else { this.grid.refresh(); } @@ -451,20 +445,12 @@ erpnext.SerialNoBatchSelector = Class.extend({ frappe.call({ method: "erpnext.stock.doctype.serial_no.serial_no.get_pos_reserved_serial_nos", args: { - item_code: me.item_code, - warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '' + filters: { + item_code: me.item_code, + warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '', + } } }).then((data) => { - if (!data.message[1].length) { - this.showing_reserved_serial_nos_error = true; - const warehouse = me.dialog.fields_dict.warehouse.get_value().bold(); - const d = frappe.msgprint(__(`Serial numbers unavailable for Item ${me.item.item_code.bold()} - under warehouse ${warehouse}. Please try changing warehouse.`)); - d.get_close_btn().on('click', () => { - this.showing_reserved_serial_nos_error = false; - d.hide(); - }); - } serial_no_filters['name'] = ["not in", data.message[0]] }) } diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 8d4ac78422..9d44a9f862 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -12,4 +12,12 @@ frappe.pages['point-of-sale'].on_page_load = function(wrapper) { wrapper.pos = new erpnext.PointOfSale.Controller(wrapper); window.cur_pos = wrapper.pos; -}; \ No newline at end of file +}; + +frappe.pages['point-of-sale'].refresh = function(wrapper) { + if (document.scannerDetectionData) { + onScan.detachFrom(document); + wrapper.pos.wrapper.html(""); + wrapper.pos.check_opening_entry(); + } +} \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 83bd71d5f3..e5b50d7789 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -11,54 +11,67 @@ from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availabil from six import string_types @frappe.whitelist() -def get_items(start, page_length, price_list, item_group, search_value="", pos_profile=None): +def get_items(start, page_length, price_list, item_group, pos_profile, search_value=""): data = dict() - warehouse = "" + result = [] + warehouse, show_only_available_items = "", False - if pos_profile: - warehouse = frappe.db.get_value('POS Profile', pos_profile, ['warehouse']) + allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock') + if not allow_negative_stock: + warehouse, show_only_available_items = frappe.db.get_value('POS Profile', pos_profile, ['warehouse', 'show_only_available_items']) if not frappe.db.exists('Item Group', item_group): item_group = get_root_of('Item Group') if search_value: data = search_serial_or_batch_or_barcode_number(search_value) - + item_code = data.get("item_code") if data.get("item_code") else search_value serial_no = data.get("serial_no") if data.get("serial_no") else "" batch_no = data.get("batch_no") if data.get("batch_no") else "" barcode = data.get("barcode") if data.get("barcode") else "" - condition = get_conditions(item_code, serial_no, batch_no, barcode) + if data: + item_info = frappe.db.get_value( + "Item", data.get("item_code"), + ["name as item_code", "item_name", "description", "stock_uom", "image as item_image", "is_stock_item"] + , as_dict=1) + item_info.setdefault('serial_no', serial_no) + item_info.setdefault('batch_no', batch_no) + item_info.setdefault('barcode', barcode) - if pos_profile: - condition += get_item_group_condition(pos_profile) + return { 'items': [item_info] } + + condition = get_conditions(item_code, serial_no, batch_no, barcode) + condition += get_item_group_condition(pos_profile) lft, rgt = frappe.db.get_value('Item Group', item_group, ['lft', 'rgt']) - # locate function is used to sort by closest match from the beginning of the value - result = [] + bin_join_selection, bin_join_condition = "", "" + if show_only_available_items: + bin_join_selection = ", `tabBin` bin" + bin_join_condition = "AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name AND bin.actual_qty > 0" items_data = frappe.db.sql(""" SELECT - name AS item_code, - item_name, - description, - stock_uom, - image AS item_image, - idx AS idx, - is_stock_item + item.name AS item_code, + item.item_name, + item.description, + item.stock_uom, + item.image AS item_image, + item.is_stock_item FROM - `tabItem` + `tabItem` item {bin_join_selection} WHERE - disabled = 0 - AND has_variants = 0 - AND is_sales_item = 1 - AND is_fixed_asset = 0 - AND item_group in (SELECT name FROM `tabItem Group` WHERE lft >= {lft} AND rgt <= {rgt}) - AND {condition} + item.disabled = 0 + AND item.has_variants = 0 + AND item.is_sales_item = 1 + AND item.is_fixed_asset = 0 + AND item.item_group in (SELECT name FROM `tabItem Group` WHERE lft >= {lft} AND rgt <= {rgt}) + AND {condition} + {bin_join_condition} ORDER BY - name asc + item.name asc LIMIT {start}, {page_length}""" .format( @@ -66,8 +79,10 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p page_length=page_length, lft=lft, rgt=rgt, - condition=condition - ), as_dict=1) + condition=condition, + bin_join_selection=bin_join_selection, + bin_join_condition=bin_join_condition + ), {'warehouse': warehouse}, as_dict=1) if items_data: items = [d.item_code for d in items_data] @@ -82,46 +97,24 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p for item in items_data: item_code = item.item_code item_price = item_prices.get(item_code) or {} - item_stock_qty = get_stock_availability(item_code, warehouse) - - if not item_stock_qty: - pass + if not allow_negative_stock: + item_stock_qty = frappe.db.sql("""select ifnull(sum(actual_qty), 0) from `tabBin` where item_code = %s""", item_code)[0][0] else: - row = {} - row.update(item) - row.update({ - 'price_list_rate': item_price.get('price_list_rate'), - 'currency': item_price.get('currency'), - 'actual_qty': item_stock_qty, - }) - result.append(row) + item_stock_qty = get_stock_availability(item_code, warehouse) + + row = {} + row.update(item) + row.update({ + 'price_list_rate': item_price.get('price_list_rate'), + 'currency': item_price.get('currency'), + 'actual_qty': item_stock_qty, + }) + result.append(row) res = { 'items': result } - if len(res['items']) == 1: - res['items'][0].setdefault('serial_no', serial_no) - res['items'][0].setdefault('batch_no', batch_no) - res['items'][0].setdefault('barcode', barcode) - - return res - - if serial_no: - res.update({ - 'serial_no': serial_no - }) - - if batch_no: - res.update({ - 'batch_no': batch_no - }) - - if barcode: - res.update({ - 'barcode': barcode - }) - return res @frappe.whitelist() @@ -145,16 +138,16 @@ def search_serial_or_batch_or_barcode_number(search_value): def get_conditions(item_code, serial_no, batch_no, barcode): if serial_no or batch_no or barcode: - return "name = {0}".format(frappe.db.escape(item_code)) + return "item.name = {0}".format(frappe.db.escape(item_code)) - return """(name like {item_code} - or item_name like {item_code})""".format(item_code = frappe.db.escape('%' + item_code + '%')) + return """(item.name like {item_code} + or item.item_name like {item_code})""".format(item_code = frappe.db.escape('%' + item_code + '%')) def get_item_group_condition(pos_profile): cond = "and 1=1" item_groups = get_item_groups(pos_profile) if item_groups: - cond = "and item_group in (%s)"%(', '.join(['%s']*len(item_groups))) + cond = "and item.item_group in (%s)"%(', '.join(['%s']*len(item_groups))) return cond % tuple(item_groups) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 5018254b0a..3d0054647b 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -20,27 +20,58 @@ erpnext.PointOfSale.Controller = class { frappe.require(['assets/erpnext/css/pos.css'], this.check_opening_entry.bind(this)); } + fetch_opening_entry() { + return frappe.call("erpnext.selling.page.point_of_sale.point_of_sale.check_opening_entry", { "user": frappe.session.user }); + } + check_opening_entry() { - return frappe.call("erpnext.selling.page.point_of_sale.point_of_sale.check_opening_entry", { "user": frappe.session.user }) - .then((r) => { - if (r.message.length) { - // assuming only one opening voucher is available for the current user - this.prepare_app_defaults(r.message[0]); - } else { - this.create_opening_voucher(); - } - }); + this.fetch_opening_entry().then((r) => { + if (r.message.length) { + // assuming only one opening voucher is available for the current user + this.prepare_app_defaults(r.message[0]); + } else { + this.create_opening_voucher(); + } + }); } create_opening_voucher() { 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" } + { + 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", + in_list_view: 1, label: "Opening Amount", + options: "company:company_currency", + change: function () { + dialog.fields_dict.balance_details.df.data.some(d => { + if (d.idx == this.doc.idx) { + d.opening_amount = this.value; + dialog.fields_dict.balance_details.grid.refresh(); + return true; + } + }); + } + } ]; - + const fetch_pos_payment_methods = () => { + const pos_profile = dialog.fields_dict.pos_profile.get_value(); + if (!pos_profile) return; + frappe.db.get_doc("POS Profile", pos_profile).then(({ payments }) => { + dialog.fields_dict.balance_details.df.data = []; + payments.forEach(pay => { + const { mode_of_payment } = pay; + dialog.fields_dict.balance_details.df.data.push({ mode_of_payment, opening_amount: '0' }); + }); + dialog.fields_dict.balance_details.grid.refresh(); + }); + } const dialog = new frappe.ui.Dialog({ title: __('Create POS Opening Entry'), + static: true, fields: [ { fieldtype: 'Link', label: __('Company'), default: frappe.defaults.get_default('company'), @@ -49,20 +80,7 @@ erpnext.PointOfSale.Controller = class { { fieldtype: 'Link', label: __('POS Profile'), options: 'POS Profile', fieldname: 'pos_profile', reqd: 1, - onchange: () => { - const pos_profile = dialog.fields_dict.pos_profile.get_value(); - - if (!pos_profile) return; - - 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(); - }); - } + onchange: () => fetch_pos_payment_methods() }, { fieldname: "balance_details", @@ -75,25 +93,18 @@ erpnext.PointOfSale.Controller = class { fields: table_fields } ], - primary_action: ({ company, pos_profile, balance_details }) => { + primary_action: async ({ company, pos_profile, balance_details }) => { if (!balance_details.length) { frappe.show_alert({ message: __("Please add Mode of payments and opening balance details."), indicator: 'red' }) - frappe.utils.play_sound("error"); - return; + return frappe.utils.play_sound("error"); } - frappe.dom.freeze(); - return frappe.call("erpnext.selling.page.point_of_sale.point_of_sale.create_opening_voucher", - { pos_profile, company, balance_details }) - .then((r) => { - frappe.dom.unfreeze(); - dialog.hide(); - if (r.message) { - this.prepare_app_defaults(r.message); - } - }) + const method = "erpnext.selling.page.point_of_sale.point_of_sale.create_opening_voucher"; + const res = await frappe.call({ method, args: { pos_profile, company, balance_details }, freeze:true }); + !res.exc && this.prepare_app_defaults(res.message); + dialog.hide(); }, primary_action_label: __('Submit') }); @@ -145,8 +156,8 @@ erpnext.PointOfSale.Controller = class { } prepare_dom() { - this.wrapper.append(` -
` + this.wrapper.append( + `
` ); this.$components_wrapper = this.wrapper.find('.app'); @@ -162,26 +173,25 @@ erpnext.PointOfSale.Controller = class { } prepare_menu() { - var me = this; this.page.clear_menu(); - this.page.add_menu_item(__("Form View"), function () { - frappe.model.sync(me.frm.doc); - frappe.set_route("Form", me.frm.doc.doctype, me.frm.doc.name); - }); + this.page.add_menu_item(__("Open Form View"), this.open_form_view.bind(this), false, 'Ctrl+F'); - this.page.add_menu_item(__("Toggle Recent Orders"), () => { - const show = this.recent_order_list.$component.hasClass('d-none'); - this.toggle_recent_order_list(show); - }); + this.page.add_menu_item(__("Toggle Recent Orders"), this.toggle_recent_order.bind(this), false, 'Ctrl+O'); - this.page.add_menu_item(__("Save as Draft"), this.save_draft_invoice.bind(this)); + this.page.add_menu_item(__("Save as Draft"), this.save_draft_invoice.bind(this), false, 'Ctrl+S'); - frappe.ui.keys.on("ctrl+s", this.save_draft_invoice.bind(this)); + this.page.add_menu_item(__('Close the POS'), this.close_pos.bind(this), false, 'Shift+Ctrl+C'); + } - this.page.add_menu_item(__('Close the POS'), this.close_pos.bind(this)); + open_form_view() { + frappe.model.sync(this.frm.doc); + frappe.set_route("Form", this.frm.doc.doctype, this.frm.doc.name); + } - frappe.ui.keys.on("shift+ctrl+s", this.close_pos.bind(this)); + toggle_recent_order() { + const show = this.recent_order_list.$component.hasClass('d-none'); + this.toggle_recent_order_list(show); } save_draft_invoice() { @@ -356,13 +366,12 @@ erpnext.PointOfSale.Controller = class { submit_invoice: () => { this.frm.savesubmit() .then((r) => { - // this.set_invoice_status(); this.toggle_components(false); this.order_summary.toggle_component(true); this.order_summary.load_summary_of(this.frm.doc, true); frappe.show_alert({ indicator: 'green', - message: __(`POS invoice ${r.doc.name} created succesfully`) + message: __('POS invoice {0} created succesfully', [r.doc.name]) }); }); } @@ -495,31 +504,7 @@ erpnext.PointOfSale.Controller = class { if (this.pos_profile && !this.frm.doc.pos_profile) this.frm.doc.pos_profile = this.pos_profile; if (!this.frm.doc.company) return; - return new Promise(resolve => { - return this.frm.call({ - doc: this.frm.doc, - method: "set_missing_values", - }).then((r) => { - if(!r.exc) { - if (!this.frm.doc.pos_profile) { - frappe.dom.unfreeze(); - this.raise_exception_for_pos_profile(); - } - this.frm.trigger("update_stock"); - this.frm.trigger('calculate_taxes_and_totals'); - if(this.frm.doc.taxes_and_charges) this.frm.script_manager.trigger("taxes_and_charges"); - frappe.model.set_default_values(this.frm.doc); - if (r.message) { - this.frm.pos_print_format = r.message.print_format || ""; - this.frm.meta.default_print_format = r.message.print_format || ""; - this.frm.allow_edit_rate = r.message.allow_edit_rate; - this.frm.allow_edit_discount = r.message.allow_edit_discount; - this.frm.doc.campaign = r.message.campaign; - } - } - resolve(); - }); - }); + return this.frm.trigger("set_pos_data"); } raise_exception_for_pos_profile() { @@ -529,11 +514,11 @@ erpnext.PointOfSale.Controller = class { set_invoice_status() { const [status, indicator] = frappe.listview_settings["POS Invoice"].get_indicator(this.frm.doc); - this.page.set_indicator(__(`${status}`), indicator); + this.page.set_indicator(status, indicator); } set_pos_profile_status() { - this.page.set_indicator(__(`${this.pos_profile}`), "blue"); + this.page.set_indicator(this.pos_profile, "blue"); } async on_cart_update(args) { @@ -550,8 +535,10 @@ erpnext.PointOfSale.Controller = class { field === 'qty' && (value = flt(value)); - if (field === 'qty' && value > 0 && !this.allow_negative_stock) - await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse); + if (['qty', 'conversion_factor'].includes(field) && value > 0 && !this.allow_negative_stock) { + const qty_needed = field === 'qty' ? value * item_row.conversion_factor : item_row.qty * value; + await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse); + } if (this.is_current_item_being_edited(item_row) || item_selected_from_selector) { await frappe.model.set_value(item_row.doctype, item_row.name, field, value); @@ -572,7 +559,10 @@ erpnext.PointOfSale.Controller = class { const args = { item_code, batch_no, [field]: value }; - if (serial_no) args['serial_no'] = serial_no; + if (serial_no) { + await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no); + args['serial_no'] = serial_no; + } if (field === 'serial_no') args['qty'] = value.split(`\n`).length || 0; @@ -633,29 +623,47 @@ erpnext.PointOfSale.Controller = class { } async trigger_new_item_events(item_row) { - await this.frm.script_manager.trigger('item_code', item_row.doctype, item_row.name) - await this.frm.script_manager.trigger('qty', item_row.doctype, item_row.name) + await this.frm.script_manager.trigger('item_code', item_row.doctype, item_row.name); + await this.frm.script_manager.trigger('qty', item_row.doctype, item_row.name); } async check_stock_availability(item_row, qty_needed, warehouse) { const available_qty = (await this.get_available_stock(item_row.item_code, warehouse)).message; frappe.dom.unfreeze(); + const bold_item_code = item_row.item_code.bold(); + const bold_warehouse = warehouse.bold(); + const bold_available_qty = available_qty.toString().bold() if (!(available_qty > 0)) { frappe.model.clear_doc(item_row.doctype, item_row.name); - frappe.throw(__(`Item Code: ${item_row.item_code.bold()} is not available under warehouse ${warehouse.bold()}.`)) + frappe.throw({ + title: _("Not Available"), + message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse]) + }) } else if (available_qty < qty_needed) { frappe.show_alert({ - message: __(`Stock quantity not enough for Item Code: ${item_row.item_code.bold()} under warehouse ${warehouse.bold()}. - Available quantity ${available_qty.toString().bold()}.`), + message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', + [bold_item_code, bold_warehouse, bold_available_qty]), indicator: 'orange' }); frappe.utils.play_sound("error"); - this.item_details.qty_control.set_value(flt(available_qty)); } frappe.dom.freeze(); } + async check_serial_no_availablilty(item_code, warehouse, serial_no) { + const method = "erpnext.stock.doctype.serial_no.serial_no.get_pos_reserved_serial_nos"; + const args = {filters: { item_code, warehouse }} + const res = await frappe.call({ method, args }); + + if (res.message.includes(serial_no)) { + frappe.throw({ + title: __("Not Available"), + message: __('Serial No: {0} has already been transacted into another POS Invoice.', [serial_no.bold()]) + }); + } + } + get_available_stock(item_code, warehouse) { const me = this; return frappe.call({ diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 724b60b973..7799dacacb 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -223,6 +223,8 @@ erpnext.PointOfSale.ItemCart = class { attach_shortcuts() { for (let row of this.number_pad.keys) { for (let btn of row) { + if (typeof btn !== 'string') continue; // do not make shortcuts for numbers + let shortcut_key = `ctrl+${frappe.scrub(String(btn))[0]}`; if (btn === 'Delete') shortcut_key = 'ctrl+backspace'; if (btn === 'Remove') shortcut_key = 'shift+ctrl+backspace' @@ -232,6 +234,10 @@ erpnext.PointOfSale.ItemCart = class { const fieldname = this.number_pad.fieldnames[btn] ? this.number_pad.fieldnames[btn] : typeof btn === 'string' ? frappe.scrub(btn) : btn; + let shortcut_label = shortcut_key.split('+').map(frappe.utils.to_title_case).join('+'); + shortcut_label = frappe.utils.is_mac() ? shortcut_label.replace('Ctrl', '⌘') : shortcut_label; + this.$numpad_section.find(`.numpad-btn[data-button-value="${fieldname}"]`).attr("title", shortcut_label); + frappe.ui.keys.on(`${shortcut_key}`, () => { const cart_is_visible = this.$component.is(":visible"); if (cart_is_visible && this.item_is_selected && this.$numpad_section.is(":visible")) { @@ -240,12 +246,36 @@ erpnext.PointOfSale.ItemCart = class { }) } } - - frappe.ui.keys.on("ctrl+enter", () => { - const cart_is_visible = this.$component.is(":visible"); - const payment_section_hidden = this.$totals_section.find('.edit-cart-btn').hasClass('d-none'); - if (cart_is_visible && payment_section_hidden) { - this.$component.find(".checkout-btn").click(); + const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl'; + this.$component.find(".checkout-btn").attr("title", `${ctrl_label}+Enter`); + frappe.ui.keys.add_shortcut({ + shortcut: "ctrl+enter", + action: () => this.$component.find(".checkout-btn").click(), + condition: () => this.$component.is(":visible") && this.$totals_section.find('.edit-cart-btn').hasClass('d-none'), + description: __("Checkout Order / Submit Order / New Order"), + ignore_inputs: true, + page: cur_page.page.page + }); + this.$component.find(".edit-cart-btn").attr("title", `${ctrl_label}+E`); + frappe.ui.keys.on("ctrl+e", () => { + const item_cart_visible = this.$component.is(":visible"); + if (item_cart_visible && this.$totals_section.find('.checkout-btn').hasClass('d-none')) { + this.$component.find(".edit-cart-btn").click() + } + }); + this.$component.find(".add-discount").attr("title", `${ctrl_label}+D`); + frappe.ui.keys.add_shortcut({ + shortcut: "ctrl+d", + action: () => this.$component.find(".add-discount").click(), + condition: () => this.$add_discount_elem.is(":visible"), + description: __("Add Order Discount"), + ignore_inputs: true, + page: cur_page.page.page + }); + frappe.ui.keys.on("escape", () => { + const item_cart_visible = this.$component.is(":visible"); + if (item_cart_visible && this.discount_field && this.discount_field.parent.is(":visible")) { + this.discount_field.set_value(0); } }); } @@ -343,8 +373,7 @@ erpnext.PointOfSale.ItemCart = class { show_discount_control() { this.$add_discount_elem.removeClass("pr-4 pl-4"); this.$add_discount_elem.html( - `
-
` + `
` ); const me = this; @@ -354,14 +383,18 @@ erpnext.PointOfSale.ItemCart = class { fieldtype: 'Data', placeholder: __('Enter discount percentage.'), onchange: function() { - if (this.value || this.value == 0) { - const frm = me.events.get_frm(); + const frm = me.events.get_frm(); + if (this.value.length || this.value === 0) { frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', flt(this.value)); me.hide_discount_control(this.value); + } else { + frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', 0); + me.$add_discount_elem.html(`+ Add Discount`); + me.discount_field = undefined; } }, }, - parent: this.$add_discount_elem.find('.add-dicount-field'), + parent: this.$add_discount_elem.find('.add-discount-field'), render_input: true, }); this.discount_field.toggle_label(false); @@ -369,17 +402,24 @@ erpnext.PointOfSale.ItemCart = class { } hide_discount_control(discount) { - this.$add_discount_elem.addClass('pr-4 pl-4'); - this.$add_discount_elem.html( - ` - - -
- ${String(discount).bold()}% off -
- ` - ); + if (!discount) { + this.$add_discount_elem.removeClass("pr-4 pl-4"); + this.$add_discount_elem.html( + `
` + ); + } else { + this.$add_discount_elem.addClass('pr-4 pl-4'); + this.$add_discount_elem.html( + ` + + +
+ ${String(discount).bold()}% off +
+ ` + ); + } } update_customer_section() { @@ -475,19 +515,20 @@ erpnext.PointOfSale.ItemCart = class { const currency = this.events.get_frm().doc.currency; this.$totals_section.find('.taxes').html( `
-
+
Tax Charges
-
+
${ taxes.map((t, i) => { let margin_left = ''; if (i !== 0) margin_left = 'ml-2'; - return `${t.description}` + const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`; + return `${description}` }).join('') }
-
+
${format_currency(value, currency)}
` diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index 3a5f89ba93..9874d1b5f9 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -177,7 +177,7 @@ erpnext.PointOfSale.ItemDetails = class { } get_form_fields(item) { - const fields = ['qty', 'uom', 'rate', 'price_list_rate', 'discount_percentage', 'warehouse', 'actual_qty']; + const fields = ['qty', 'uom', 'rate', 'conversion_factor', 'discount_percentage', 'warehouse', 'actual_qty', 'price_list_rate']; if (item.has_serial_no) fields.push('serial_no'); if (item.has_batch_no) fields.push('batch_no'); return fields; @@ -208,7 +208,7 @@ erpnext.PointOfSale.ItemDetails = class { const me = this; if (this.rate_control) { this.rate_control.df.onchange = function() { - if (this.value) { + if (this.value || flt(this.value) === 0) { me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => { const item_row = frappe.get_doc(me.doctype, me.name); const doc = me.events.get_frm().doc; @@ -234,24 +234,22 @@ erpnext.PointOfSale.ItemDetails = class { }) } else if (available_qty === 0) { me.warehouse_control.set_value(''); - frappe.throw(__(`Item Code: ${me.item_row.item_code.bold()} is not available under warehouse ${this.value.bold()}.`)); + const bold_item_code = me.item_row.item_code.bold(); + const bold_warehouse = this.value.bold(); + frappe.throw( + __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse]) + ); } me.actual_qty_control.set_value(available_qty); }); } } - this.warehouse_control.refresh(); - } - - if (this.discount_percentage_control) { - this.discount_percentage_control.df.onchange = function() { - if (this.value) { - me.events.form_updated(me.doctype, me.name, 'discount_percentage', this.value).then(() => { - const item_row = frappe.get_doc(me.doctype, me.name); - me.rate_control.set_value(item_row.rate); - }); + this.warehouse_control.df.get_query = () => { + return { + filters: { company: this.events.get_frm().doc.company } } - } + }; + this.warehouse_control.refresh(); } if (this.serial_no_control) { @@ -270,7 +268,8 @@ erpnext.PointOfSale.ItemDetails = class { query: 'erpnext.controllers.queries.get_batch_no', filters: { item_code: me.item_row.item_code, - warehouse: me.item_row.warehouse + warehouse: me.item_row.warehouse, + posting_date: me.events.get_frm().doc.posting_date } } }; @@ -287,8 +286,20 @@ erpnext.PointOfSale.ItemDetails = class { me.events.set_value_in_current_cart_item('uom', this.value); me.events.form_updated(me.doctype, me.name, 'uom', this.value); me.current_item.uom = this.value; + + const item_row = frappe.get_doc(me.doctype, me.name); + me.conversion_factor_control.df.read_only = (item_row.stock_uom == this.value); + me.conversion_factor_control.refresh(); } } + + frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => { + const field_control = me[`${fieldname}_control`]; + if (field_control) { + field_control.set_value(value); + cur_pos.update_cart_html(item_row); + } + }); } async auto_update_batch_no() { @@ -337,6 +348,7 @@ erpnext.PointOfSale.ItemDetails = class { } attach_shortcuts() { + this.wrapper.find('.close-btn').attr("title", "Esc"); frappe.ui.keys.on("escape", () => { const item_details_visible = this.$component.is(":visible"); if (item_details_visible) { @@ -358,8 +370,10 @@ erpnext.PointOfSale.ItemDetails = class { bind_auto_serial_fetch_event() { this.$form_container.on('click', '.auto-fetch-btn', () => { - this.batch_no_control.set_value(''); + this.batch_no_control && this.batch_no_control.set_value(''); let qty = this.qty_control.get_value(); + let expiry_date = this.item_row.has_batch_no ? this.events.get_frm().doc.posting_date : ""; + let numbers = frappe.call({ method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number", args: { @@ -367,6 +381,7 @@ erpnext.PointOfSale.ItemDetails = class { item_code: this.current_item.item_code, warehouse: this.warehouse_control.get_value() || '', batch_nos: this.current_item.batch_no || '', + posting_date: expiry_date, for_doctype: 'POS Invoice' } }); @@ -376,10 +391,14 @@ erpnext.PointOfSale.ItemDetails = class { let records_length = auto_fetched_serial_numbers.length; if (!records_length) { const warehouse = this.warehouse_control.get_value().bold(); - frappe.msgprint(__(`Serial numbers unavailable for Item ${this.current_item.item_code.bold()} - under warehouse ${warehouse}. Please try changing warehouse.`)); + const item_code = this.current_item.item_code.bold(); + frappe.msgprint( + __('Serial numbers unavailable for Item {0} under warehouse {1}. Please try changing warehouse.', [item_code, warehouse]) + ); } else if (records_length < qty) { - frappe.msgprint(`Fetched only ${records_length} available serial numbers.`); + frappe.msgprint( + __('Fetched only {0} available serial numbers.', [records_length]) + ); this.qty_control.set_value(records_length); } numbers = auto_fetched_serial_numbers.join(`\n`); diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index c87b845a41..4139e29947 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -76,7 +76,7 @@ erpnext.PointOfSale.ItemSelector = class { 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"; + const indicator_color = actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange"; function get_item_image_html() { if (item_image) { @@ -184,15 +184,24 @@ erpnext.PointOfSale.ItemSelector = class { } 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(); + const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl'; + this.search_field.parent.attr("title", `${ctrl_label}+I`); + frappe.ui.keys.add_shortcut({ + shortcut: "ctrl+i", + action: () => this.search_field.set_focus(), + condition: () => this.$component.is(':visible'), + description: __("Focus on search input"), + ignore_inputs: true, + page: cur_page.page.page }); - 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(); + this.item_group_field.parent.attr("title", `${ctrl_label}+G`); + frappe.ui.keys.add_shortcut({ + shortcut: "ctrl+g", + action: () => this.item_group_field.set_focus(), + condition: () => this.$component.is(':visible'), + description: __("Focus on Item Group filter"), + ignore_inputs: true, + page: cur_page.page.page }); // for selecting the last filtered item on search frappe.ui.keys.on("enter", () => { diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_list.js b/erpnext/selling/page/point_of_sale/pos_past_order_list.js index 9181ee8000..b256247924 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_list.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_list.js @@ -1,55 +1,55 @@ erpnext.PointOfSale.PastOrderList = class { - constructor({ wrapper, events }) { - this.wrapper = wrapper; - this.events = events; + constructor({ wrapper, events }) { + this.wrapper = wrapper; + this.events = events; - this.init_component(); - } + this.init_component(); + } - init_component() { - this.prepare_dom(); - this.make_filter_section(); - this.bind_events(); - } + init_component() { + this.prepare_dom(); + this.make_filter_section(); + this.bind_events(); + } - prepare_dom() { - this.wrapper.append( - `
-
-
-
-
-
-
-
RECENT ORDERS
-
-
-
-
` - ) + prepare_dom() { + this.wrapper.append( + `
+
+
+
+
+
+
+
RECENT ORDERS
+
+
+
+
` + ); - this.$component = this.wrapper.find('.past-order-list'); - this.$invoices_container = this.$component.find('.invoices-container'); - } + this.$component = this.wrapper.find('.past-order-list'); + this.$invoices_container = this.$component.find('.invoices-container'); + } - bind_events() { - this.search_field.$input.on('input', (e) => { + bind_events() { + this.search_field.$input.on('input', (e) => { clearTimeout(this.last_search); this.last_search = setTimeout(() => { - const search_term = e.target.value; - this.refresh_list(search_term, this.status_field.get_value()); + const search_term = e.target.value; + this.refresh_list(search_term, this.status_field.get_value()); }, 300); - }); - const me = this; - this.$invoices_container.on('click', '.invoice-wrapper', function() { - const invoice_name = unescape($(this).attr('data-invoice-name')); + }); + const me = this; + this.$invoices_container.on('click', '.invoice-wrapper', function() { + const invoice_name = unescape($(this).attr('data-invoice-name')); - me.events.open_invoice_data(invoice_name); - }) - } + me.events.open_invoice_data(invoice_name); + }); + } - make_filter_section() { - const me = this; + make_filter_section() { + const me = this; this.search_field = frappe.ui.form.make_control({ df: { label: __('Search'), @@ -58,73 +58,71 @@ erpnext.PointOfSale.PastOrderList = class { }, parent: this.$component.find('.search-field'), render_input: true, - }); + }); this.status_field = frappe.ui.form.make_control({ df: { label: __('Invoice Status'), - fieldtype: 'Select', + fieldtype: 'Select', options: `Draft\nPaid\nConsolidated\nReturn`, - placeholder: __('Filter by invoice status'), - onchange: function() { - me.refresh_list(me.search_field.get_value(), this.value); - } + placeholder: __('Filter by invoice status'), + onchange: function() { + me.refresh_list(me.search_field.get_value(), this.value); + } }, - parent: this.$component.find('.status-field'), + parent: this.$component.find('.status-field'), render_input: true, - }); - this.search_field.toggle_label(false); - this.status_field.toggle_label(false); - this.status_field.set_value('Paid'); - } - - toggle_component(show) { - show ? - this.$component.removeClass('d-none') && this.refresh_list() : - this.$component.addClass('d-none'); - } + }); + this.search_field.toggle_label(false); + this.status_field.toggle_label(false); + this.status_field.set_value('Draft'); + } - refresh_list() { - frappe.dom.freeze(); - this.events.reset_summary(); - const search_term = this.search_field.get_value(); - const status = this.status_field.get_value(); + toggle_component(show) { + show ? this.$component.removeClass('d-none') && this.refresh_list() : this.$component.addClass('d-none'); + } - this.$invoices_container.html(''); + refresh_list() { + frappe.dom.freeze(); + this.events.reset_summary(); + const search_term = this.search_field.get_value(); + const status = this.status_field.get_value(); - return frappe.call({ + this.$invoices_container.html(''); + + return frappe.call({ method: "erpnext.selling.page.point_of_sale.point_of_sale.get_past_order_list", freeze: true, - args: { search_term, status }, - callback: (response) => { - frappe.dom.unfreeze(); - response.message.forEach(invoice => { - const invoice_html = this.get_invoice_html(invoice); - this.$invoices_container.append(invoice_html); - }); - } - }); - } + args: { search_term, status }, + callback: (response) => { + frappe.dom.unfreeze(); + response.message.forEach(invoice => { + const invoice_html = this.get_invoice_html(invoice); + this.$invoices_container.append(invoice_html); + }); + } + }); + } - get_invoice_html(invoice) { - const posting_datetime = moment(invoice.posting_date+" "+invoice.posting_time).format("Do MMMM, h:mma"); - return ( - `
-
-
${invoice.name}
-
-
- - - - ${invoice.customer} -
-
-
-
-
${format_currency(invoice.grand_total, invoice.currency, 0) || 0}
-
${posting_datetime}
-
-
` - ) - } -} \ No newline at end of file + get_invoice_html(invoice) { + const posting_datetime = moment(invoice.posting_date+" "+invoice.posting_time).format("Do MMMM, h:mma"); + return ( + `
+
+
${invoice.name}
+
+
+ + + + ${invoice.customer} +
+
+
+
+
${format_currency(invoice.grand_total, invoice.currency, 0) || 0}
+
${posting_datetime}
+
+
` + ); + } +}; \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index 30e0918ba6..6fd4c26bea 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -1,456 +1,476 @@ erpnext.PointOfSale.PastOrderSummary = class { - constructor({ wrapper, events }) { - this.wrapper = wrapper; - this.events = events; + constructor({ wrapper, events }) { + this.wrapper = wrapper; + this.events = events; - this.init_component(); - } + this.init_component(); + } - init_component() { - this.prepare_dom(); - this.init_child_components(); - this.bind_events(); - this.attach_shortcuts(); - } + init_component() { + this.prepare_dom(); + this.init_child_components(); + this.bind_events(); + this.attach_shortcuts(); + } - prepare_dom() { - this.wrapper.append( - `
-
-
-
Select an invoice to load summary data
-
-
-
-
-
-
` - ) + prepare_dom() { + this.wrapper.append( + `
+
+
+
Select an invoice to load summary data
+
+
+
+
+
+
` + ); - this.$component = this.wrapper.find('.past-order-summary'); - this.$summary_wrapper = this.$component.find('.summary-wrapper'); - this.$summary_container = this.$component.find('.summary-container'); - } + this.$component = this.wrapper.find('.past-order-summary'); + this.$summary_wrapper = this.$component.find('.summary-wrapper'); + this.$summary_container = this.$component.find('.summary-container'); + } - init_child_components() { - this.init_upper_section(); - this.init_items_summary(); - this.init_totals_summary(); - this.init_payments_summary(); - this.init_summary_buttons(); - this.init_email_print_dialog(); - } + init_child_components() { + this.init_upper_section(); + this.init_items_summary(); + this.init_totals_summary(); + this.init_payments_summary(); + this.init_summary_buttons(); + this.init_email_print_dialog(); + } - init_upper_section() { - this.$summary_container.append( - `
` - ); + init_upper_section() { + this.$summary_container.append( + `
` + ); - this.$upper_section = this.$summary_container.find('.upper-section'); - } + this.$upper_section = this.$summary_container.find('.upper-section'); + } - init_items_summary() { - this.$summary_container.append( - `
-
ITEMS
-
-
` - ) + init_items_summary() { + this.$summary_container.append( + `
+
ITEMS
+
+
` + ); - this.$items_summary_container = this.$summary_container.find('.items-summary-container'); - } + this.$items_summary_container = this.$summary_container.find('.items-summary-container'); + } - init_totals_summary() { - this.$summary_container.append( - `
-
TOTALS
-
-
` - ) + init_totals_summary() { + this.$summary_container.append( + `
+
TOTALS
+
+
` + ); - this.$totals_summary_container = this.$summary_container.find('.summary-totals-container'); - } + this.$totals_summary_container = this.$summary_container.find('.summary-totals-container'); + } - init_payments_summary() { - this.$summary_container.append( - `
-
PAYMENTS
-
-
` - ) + init_payments_summary() { + this.$summary_container.append( + `
+
PAYMENTS
+
+
` + ); - this.$payment_summary_container = this.$summary_container.find('.payments-summary-container'); - } + this.$payment_summary_container = this.$summary_container.find('.payments-summary-container'); + } - init_summary_buttons() { - this.$summary_container.append( - `
` - ) + init_summary_buttons() { + this.$summary_container.append( + `
` + ); - this.$summary_btns = this.$summary_container.find('.summary-btns'); - } + this.$summary_btns = this.$summary_container.find('.summary-btns'); + } - init_email_print_dialog() { - const email_dialog = new frappe.ui.Dialog({ - title: 'Email Receipt', - fields: [ - {fieldname:'email_id', fieldtype:'Data', options: 'Email', label:'Email ID'}, - // {fieldname:'remarks', fieldtype:'Text', label:'Remarks (if any)'} - ], - primary_action: () => { - this.send_email(); - }, - primary_action_label: __('Send'), - }); - this.email_dialog = email_dialog; + init_email_print_dialog() { + const email_dialog = new frappe.ui.Dialog({ + title: 'Email Receipt', + fields: [ + {fieldname:'email_id', fieldtype:'Data', options: 'Email', label:'Email ID'}, + // {fieldname:'remarks', fieldtype:'Text', label:'Remarks (if any)'} + ], + primary_action: () => { + this.send_email(); + }, + primary_action_label: __('Send'), + }); + this.email_dialog = email_dialog; - const print_dialog = new frappe.ui.Dialog({ - title: 'Print Receipt', - fields: [ - {fieldname:'print', fieldtype:'Data', label:'Print Preview'} - ], - primary_action: () => { - const frm = this.events.get_frm(); - frm.doc = this.doc; - frm.print_preview.lang_code = frm.doc.language; - frm.print_preview.printit(true); - }, - primary_action_label: __('Print'), - }); - this.print_dialog = print_dialog; - } + const print_dialog = new frappe.ui.Dialog({ + title: 'Print Receipt', + fields: [ + {fieldname:'print', fieldtype:'Data', label:'Print Preview'} + ], + primary_action: () => { + const frm = this.events.get_frm(); + frm.doc = this.doc; + frm.print_preview.lang_code = frm.doc.language; + frm.print_preview.printit(true); + }, + primary_action_label: __('Print'), + }); + this.print_dialog = print_dialog; + } - get_upper_section_html(doc) { - const { status } = doc; let indicator_color = ''; + get_upper_section_html(doc) { + const { status } = doc; let indicator_color = ''; - in_list(['Paid', 'Consolidated'], status) && (indicator_color = 'green'); - status === 'Draft' && (indicator_color = 'red'); - status === 'Return' && (indicator_color = 'grey'); + in_list(['Paid', 'Consolidated'], status) && (indicator_color = 'green'); + status === 'Draft' && (indicator_color = 'red'); + status === 'Return' && (indicator_color = 'grey'); - return `
-
${doc.customer}
-
${this.customer_email}
-
Sold by: ${doc.owner}
-
-
-
${format_currency(doc.paid_amount, doc.currency)}
-
-
${doc.name}
-
${doc.status}
-
-
` - } + return `
+
${doc.customer}
+
${this.customer_email}
+
Sold by: ${doc.owner}
+
+
+
${format_currency(doc.paid_amount, doc.currency)}
+
+
${doc.name}
+
${doc.status}
+
+
`; + } - get_discount_html(doc) { - if (doc.discount_amount) { - return `
-
-
- Discount -
- (${doc.additional_discount_percentage} %) -
-
-
${format_currency(doc.discount_amount, doc.currency)}
-
-
`; - } else { - return ``; - } - } + get_discount_html(doc) { + if (doc.discount_amount) { + return `
+
+
+ Discount +
+ (${doc.additional_discount_percentage} %) +
+
+
${format_currency(doc.discount_amount, doc.currency)}
+
+
`; + } else { + return ``; + } + } - get_net_total_html(doc) { - return `
-
-
- Net Total -
-
-
-
${format_currency(doc.net_total, doc.currency)}
-
-
` - } + get_net_total_html(doc) { + return `
+
+
+ Net Total +
+
+
+
${format_currency(doc.net_total, doc.currency)}
+
+
`; + } - get_taxes_html(doc) { - return `
-
-
Tax Charges
-
- ${ - doc.taxes.map((t, i) => { - let margin_left = ''; - if (i !== 0) margin_left = 'ml-2'; - return `${t.description} @${t.rate}%` - }).join('') - } -
-
-
-
${format_currency(doc.base_total_taxes_and_charges, doc.currency)}
-
-
` - } + get_taxes_html(doc) { + const taxes = doc.taxes.map((t, i) => { + let margin_left = ''; + if (i !== 0) margin_left = 'ml-2'; + return `${t.description} @${t.rate}%`; + }).join(''); - get_grand_total_html(doc) { - return `
-
-
- Grand Total -
-
-
-
${format_currency(doc.grand_total, doc.currency)}
-
-
` - } + return ` +
+
+
Tax Charges
+
${taxes}
+
+
+
+ ${format_currency(doc.base_total_taxes_and_charges, doc.currency)} +
+
+
`; + } - get_item_html(doc, item_data) { - return `
-
- ${item_data.qty || 0} -
-
-
- ${item_data.item_name} -
-
-
- ${get_rate_discount_html()} -
-
` + get_grand_total_html(doc) { + return `
+
+
+ Grand Total +
+
+
+
${format_currency(doc.grand_total, doc.currency)}
+
+
`; + } - function get_rate_discount_html() { - if (item_data.rate && item_data.price_list_rate && item_data.rate !== item_data.price_list_rate) { - return `(${item_data.discount_percentage}% off) -
${format_currency(item_data.rate, doc.currency)}
` - } else { - return `
${format_currency(item_data.price_list_rate || item_data.rate, doc.currency)}
` - } - } - } + get_item_html(doc, item_data) { + return `
+
+ ${item_data.qty || 0} +
+
+
+ ${item_data.item_name} +
+
+
+ ${get_rate_discount_html()} +
+
`; - get_payment_html(doc, payment) { - return `
-
-
- ${payment.mode_of_payment} -
-
-
-
${format_currency(payment.amount, doc.currency)}
-
-
` - } + function get_rate_discount_html() { + if (item_data.rate && item_data.price_list_rate && item_data.rate !== item_data.price_list_rate) { + return ` + (${item_data.discount_percentage}% off) + +
+ ${format_currency(item_data.rate, doc.currency)} +
`; + } else { + return `
+ ${format_currency(item_data.price_list_rate || item_data.rate, doc.currency)} +
`; + } + } + } - bind_events() { - this.$summary_container.on('click', '.return-btn', () => { - this.events.process_return(this.doc.name); - this.toggle_component(false); - this.$component.find('.no-summary-placeholder').removeClass('d-none'); - this.$summary_wrapper.addClass('d-none'); - }); + get_payment_html(doc, payment) { + return `
+
+
+ ${payment.mode_of_payment} +
+
+
+
${format_currency(payment.amount, doc.currency)}
+
+
`; + } - this.$summary_container.on('click', '.edit-btn', () => { - this.events.edit_order(this.doc.name); - this.toggle_component(false); - this.$component.find('.no-summary-placeholder').removeClass('d-none'); - this.$summary_wrapper.addClass('d-none'); - }); + bind_events() { + this.$summary_container.on('click', '.return-btn', () => { + this.events.process_return(this.doc.name); + this.toggle_component(false); + this.$component.find('.no-summary-placeholder').removeClass('d-none'); + this.$summary_wrapper.addClass('d-none'); + }); - this.$summary_container.on('click', '.new-btn', () => { - this.events.new_order(); - this.toggle_component(false); - this.$component.find('.no-summary-placeholder').removeClass('d-none'); - this.$summary_wrapper.addClass('d-none'); - }); + this.$summary_container.on('click', '.edit-btn', () => { + this.events.edit_order(this.doc.name); + this.toggle_component(false); + this.$component.find('.no-summary-placeholder').removeClass('d-none'); + this.$summary_wrapper.addClass('d-none'); + }); - this.$summary_container.on('click', '.email-btn', () => { - this.email_dialog.fields_dict.email_id.set_value(this.customer_email); - this.email_dialog.show(); - }); + this.$summary_container.on('click', '.new-btn', () => { + this.events.new_order(); + this.toggle_component(false); + this.$component.find('.no-summary-placeholder').removeClass('d-none'); + this.$summary_wrapper.addClass('d-none'); + }); - this.$summary_container.on('click', '.print-btn', () => { - // this.print_dialog.show(); - const frm = this.events.get_frm(); - frm.doc = this.doc; - frm.print_preview.lang_code = frm.doc.language; - frm.print_preview.printit(true); - }); - } + this.$summary_container.on('click', '.email-btn', () => { + this.email_dialog.fields_dict.email_id.set_value(this.customer_email); + this.email_dialog.show(); + }); - attach_shortcuts() { - frappe.ui.keys.on("ctrl+p", () => { - const print_btn_visible = this.$summary_container.find('.print-btn').is(":visible"); - const summary_visible = this.$component.is(":visible"); - if (!summary_visible || !print_btn_visible) return; + this.$summary_container.on('click', '.print-btn', () => { + const frm = this.events.get_frm(); + frm.doc = this.doc; + frm.print_preview.lang_code = frm.doc.language; + frm.print_preview.printit(true); + }); + } - this.$summary_container.find('.print-btn').click(); - }); - } + attach_shortcuts() { + const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl'; + this.$summary_container.find('.print-btn').attr("title", `${ctrl_label}+P`); + frappe.ui.keys.add_shortcut({ + shortcut: "ctrl+p", + action: () => this.$summary_container.find('.print-btn').click(), + condition: () => this.$component.is(':visible') && this.$summary_container.find('.print-btn').is(":visible"), + description: __("Print Receipt"), + page: cur_page.page.page + }); + this.$summary_container.find('.new-btn').attr("title", `${ctrl_label}+Enter`); + frappe.ui.keys.on("ctrl+enter", () => { + const summary_is_visible = this.$component.is(":visible"); + if (summary_is_visible && this.$summary_container.find('.new-btn').is(":visible")) { + this.$summary_container.find('.new-btn').click(); + } + }); + this.$summary_container.find('.edit-btn').attr("title", `${ctrl_label}+E`); + frappe.ui.keys.add_shortcut({ + shortcut: "ctrl+e", + action: () => this.$summary_container.find('.edit-btn').click(), + condition: () => this.$component.is(':visible') && this.$summary_container.find('.edit-btn').is(":visible"), + description: __("Edit Receipt"), + page: cur_page.page.page + }); + } - toggle_component(show) { - show ? - this.$component.removeClass('d-none') : - this.$component.addClass('d-none'); - } + toggle_component(show) { + show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none'); + } - send_email() { - const frm = this.events.get_frm(); - const recipients = this.email_dialog.get_values().recipients; - const doc = this.doc || frm.doc; - const print_format = frm.pos_print_format; + send_email() { + const frm = this.events.get_frm(); + const recipients = this.email_dialog.get_values().recipients; + const doc = this.doc || frm.doc; + const print_format = frm.pos_print_format; - frappe.call({ - method:"frappe.core.doctype.communication.email.make", - args: { - recipients: recipients, - subject: __(frm.meta.name) + ': ' + doc.name, - doctype: doc.doctype, - name: doc.name, - send_email: 1, - print_format, - sender_full_name: frappe.user.full_name(), - _lang : doc.language - }, - callback: r => { - if(!r.exc) { - frappe.utils.play_sound("email"); - if(r.message["emails_not_sent_to"]) { - frappe.msgprint(__("Email not sent to {0} (unsubscribed / disabled)", - [ frappe.utils.escape_html(r.message["emails_not_sent_to"]) ]) ); - } else { - frappe.show_alert({ - message: __('Email sent successfully.'), - indicator: 'green' - }); - } - this.email_dialog.hide(); - } else { - frappe.msgprint(__("There were errors while sending email. Please try again.")); - } - } - }); - } + frappe.call({ + method:"frappe.core.doctype.communication.email.make", + args: { + recipients: recipients, + subject: __(frm.meta.name) + ': ' + doc.name, + doctype: doc.doctype, + name: doc.name, + send_email: 1, + print_format, + sender_full_name: frappe.user.full_name(), + _lang : doc.language + }, + callback: r => { + if(!r.exc) { + frappe.utils.play_sound("email"); + if(r.message["emails_not_sent_to"]) { + frappe.msgprint(__("Email not sent to {0} (unsubscribed / disabled)", + [ frappe.utils.escape_html(r.message["emails_not_sent_to"]) ]) ); + } else { + frappe.show_alert({ + message: __('Email sent successfully.'), + indicator: 'green' + }); + } + this.email_dialog.hide(); + } else { + frappe.msgprint(__("There were errors while sending email. Please try again.")); + } + } + }); + } - add_summary_btns(map) { - this.$summary_btns.html(''); - map.forEach(m => { - if (m.condition) { - m.visible_btns.forEach(b => { - const class_name = b.split(' ')[0].toLowerCase(); - this.$summary_btns.append( - `
- ${b} -
` - ) - }); - } - }); - this.$summary_btns.children().last().removeClass('mr-4'); - } + add_summary_btns(map) { + this.$summary_btns.html(''); + map.forEach(m => { + if (m.condition) { + m.visible_btns.forEach(b => { + const class_name = b.split(' ')[0].toLowerCase(); + this.$summary_btns.append( + `
+ ${b} +
` + ); + }); + } + }); + this.$summary_btns.children().last().removeClass('mr-4'); + } - show_summary_placeholder() { - this.$summary_wrapper.addClass("d-none"); - this.$component.find('.no-summary-placeholder').removeClass('d-none'); - } + show_summary_placeholder() { + this.$summary_wrapper.addClass("d-none"); + this.$component.find('.no-summary-placeholder').removeClass('d-none'); + } - switch_to_post_submit_summary() { - // switch to full width view - this.$component.removeClass('col-span-6').addClass('col-span-10'); - this.$summary_wrapper.removeClass('w-66').addClass('w-40'); + switch_to_post_submit_summary() { + // switch to full width view + this.$component.removeClass('col-span-6').addClass('col-span-10'); + this.$summary_wrapper.removeClass('w-66').addClass('w-40'); - // switch place holder with summary container - this.$component.find('.no-summary-placeholder').addClass('d-none'); - this.$summary_wrapper.removeClass('d-none'); - } + // switch place holder with summary container + this.$component.find('.no-summary-placeholder').addClass('d-none'); + this.$summary_wrapper.removeClass('d-none'); + } - switch_to_recent_invoice_summary() { - // switch full width view with 60% view - this.$component.removeClass('col-span-10').addClass('col-span-6'); - this.$summary_wrapper.removeClass('w-40').addClass('w-66'); + switch_to_recent_invoice_summary() { + // switch full width view with 60% view + this.$component.removeClass('col-span-10').addClass('col-span-6'); + this.$summary_wrapper.removeClass('w-40').addClass('w-66'); - // switch place holder with summary container - this.$component.find('.no-summary-placeholder').addClass('d-none'); - this.$summary_wrapper.removeClass('d-none'); - } + // switch place holder with summary container + this.$component.find('.no-summary-placeholder').addClass('d-none'); + this.$summary_wrapper.removeClass('d-none'); + } - get_condition_btn_map(after_submission) { - if (after_submission) - return [{ condition: true, visible_btns: ['Print Receipt', 'Email Receipt', 'New Order'] }]; + get_condition_btn_map(after_submission) { + if (after_submission) + return [{ condition: true, visible_btns: ['Print Receipt', 'Email Receipt', 'New Order'] }]; - return [ - { condition: this.doc.docstatus === 0, visible_btns: ['Edit Order'] }, - { condition: !this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt', 'Return']}, - { condition: this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt']} - ]; - } + return [ + { condition: this.doc.docstatus === 0, visible_btns: ['Edit Order'] }, + { condition: !this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt', 'Return']}, + { condition: this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt']} + ]; + } - load_summary_of(doc, after_submission=false) { - this.$summary_wrapper.removeClass("d-none"); + load_summary_of(doc, after_submission=false) { + this.$summary_wrapper.removeClass("d-none"); - after_submission ? - this.switch_to_post_submit_summary() : this.switch_to_recent_invoice_summary(); + after_submission ? + this.switch_to_post_submit_summary() : this.switch_to_recent_invoice_summary(); - this.doc = doc; + this.doc = doc; - this.attach_basic_info(doc); + this.attach_basic_info(doc); - this.attach_items_info(doc); + this.attach_items_info(doc); - this.attach_totals_info(doc); + this.attach_totals_info(doc); - this.attach_payments_info(doc); + this.attach_payments_info(doc); - const condition_btns_map = this.get_condition_btn_map(after_submission); + const condition_btns_map = this.get_condition_btn_map(after_submission); - this.add_summary_btns(condition_btns_map); - } + this.add_summary_btns(condition_btns_map); + } - attach_basic_info(doc) { - frappe.db.get_value('Customer', this.doc.customer, 'email_id').then(({ message }) => { - this.customer_email = message.email_id || ''; - const upper_section_dom = this.get_upper_section_html(doc); - this.$upper_section.html(upper_section_dom); - }); - } + attach_basic_info(doc) { + frappe.db.get_value('Customer', this.doc.customer, 'email_id').then(({ message }) => { + this.customer_email = message.email_id || ''; + const upper_section_dom = this.get_upper_section_html(doc); + this.$upper_section.html(upper_section_dom); + }); + } - attach_items_info(doc) { - this.$items_summary_container.html(''); - doc.items.forEach(item => { - const item_dom = this.get_item_html(doc, item); - this.$items_summary_container.append(item_dom); - }); - } + attach_items_info(doc) { + this.$items_summary_container.html(''); + doc.items.forEach(item => { + const item_dom = this.get_item_html(doc, item); + this.$items_summary_container.append(item_dom); + }); + } - attach_payments_info(doc) { - this.$payment_summary_container.html(''); - doc.payments.forEach(p => { - if (p.amount) { - const payment_dom = this.get_payment_html(doc, p); - this.$payment_summary_container.append(payment_dom); - } - }); - if (doc.redeem_loyalty_points && doc.loyalty_amount) { - const payment_dom = this.get_payment_html(doc, { - mode_of_payment: 'Loyalty Points', - amount: doc.loyalty_amount, - }); - this.$payment_summary_container.append(payment_dom); - } - } + attach_payments_info(doc) { + this.$payment_summary_container.html(''); + doc.payments.forEach(p => { + if (p.amount) { + const payment_dom = this.get_payment_html(doc, p); + this.$payment_summary_container.append(payment_dom); + } + }); + if (doc.redeem_loyalty_points && doc.loyalty_amount) { + const payment_dom = this.get_payment_html(doc, { + mode_of_payment: 'Loyalty Points', + amount: doc.loyalty_amount, + }); + this.$payment_summary_container.append(payment_dom); + } + } - attach_totals_info(doc) { - this.$totals_summary_container.html(''); + attach_totals_info(doc) { + this.$totals_summary_container.html(''); - const discount_dom = this.get_discount_html(doc); - const net_total_dom = this.get_net_total_html(doc); - const taxes_dom = this.get_taxes_html(doc); - const grand_total_dom = this.get_grand_total_html(doc); - this.$totals_summary_container.append(discount_dom); - this.$totals_summary_container.append(net_total_dom); - this.$totals_summary_container.append(taxes_dom); - this.$totals_summary_container.append(grand_total_dom); - } - -} \ No newline at end of file + const discount_dom = this.get_discount_html(doc); + const net_total_dom = this.get_net_total_html(doc); + const taxes_dom = this.get_taxes_html(doc); + const grand_total_dom = this.get_grand_total_html(doc); + this.$totals_summary_container.append(discount_dom); + this.$totals_summary_container.append(net_total_dom); + this.$totals_summary_container.append(taxes_dom); + this.$totals_summary_container.append(grand_total_dom); + } +}; \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index ec886d7957..e89cf01f79 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -26,7 +26,7 @@ erpnext.PointOfSale.Payment = class {
-
+
@@ -170,7 +170,8 @@ erpnext.PointOfSale.Payment = class { 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) : ''; + const current_value = me.selected_mode?.get_value() + !current_value && doc.grand_total > doc.paid_amount ? me.selected_mode?.set_value(doc.grand_total - doc.paid_amount) : ''; } }) @@ -197,10 +198,6 @@ erpnext.PointOfSale.Payment = class { 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; @@ -233,7 +230,7 @@ erpnext.PointOfSale.Payment = class { 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(); + const mode = default_mop.mode_of_payment.replace(/ +/g, "_").toLowerCase(); if (this[`${mode}_control`] && this[`${mode}_control`].get_value() != default_mop.amount) { this[`${mode}_control`].set_value(default_mop.amount); } @@ -254,6 +251,8 @@ erpnext.PointOfSale.Payment = class { } attach_shortcuts() { + const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl'; + this.$component.find('.submit-order').attr("title", `${ctrl_label}+Enter`); frappe.ui.keys.on("ctrl+enter", () => { const payment_is_visible = this.$component.is(":visible"); const active_mode = this.$payment_modes.find(".border-primary"); @@ -262,21 +261,28 @@ erpnext.PointOfSale.Payment = class { } }); - 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(); - } + frappe.ui.keys.add_shortcut({ + shortcut: "tab", + action: () => { + const payment_is_visible = this.$component.is(":visible"); + 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_of_payments = Array.from(this.$payment_modes.find(".mode-of-payment")).map(m => $(m).attr("data-mode")); + 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(); + } + }, + condition: () => this.$component.is(':visible') && this.$payment_modes.find(".border-primary").length, + description: __("Switch Between Payment Modes"), + ignore_inputs: true, + page: cur_page.page.page }); } @@ -336,7 +342,7 @@ erpnext.PointOfSale.Payment = class { this.$payment_modes.html( `${ payments.map((p, i) => { - const mode = p.mode_of_payment.replace(' ', '_').toLowerCase(); + const mode = p.mode_of_payment.replace(/ +/g, "_").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) : ''; @@ -356,13 +362,13 @@ erpnext.PointOfSale.Payment = class { ) payments.forEach(p => { - const mode = p.mode_of_payment.replace(' ', '_').toLowerCase(); + const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase(); const me = this; this[`${mode}_control`] = frappe.ui.form.make_control({ df: { - label: __(`${p.mode_of_payment}`), + label: p.mode_of_payment, fieldtype: 'Currency', - placeholder: __(`Enter ${p.mode_of_payment} amount.`), + placeholder: __('Enter {0} amount.', [p.mode_of_payment]), onchange: function() { if (this.value || this.value == 0) { frappe.model.set_value(p.doctype, p.name, 'amount', flt(this.value)) @@ -440,11 +446,11 @@ erpnext.PointOfSale.Payment = class { let description, read_only, max_redeemable_amount; if (!loyalty_points) { - description = __(`You don't have enough points to redeem.`); + 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)}.`); + description = __("You can redeem upto {0}.", [format_currency(max_redeemable_amount)]); read_only = false; } @@ -464,9 +470,9 @@ erpnext.PointOfSale.Payment = class { this['loyalty-amount_control'] = frappe.ui.form.make_control({ df: { - label: __('Redeem Loyalty Points'), + label: __("Redeem Loyalty Points"), fieldtype: 'Currency', - placeholder: __(`Enter amount to be redeemed.`), + placeholder: __("Enter amount to be redeemed."), options: 'company:currency', read_only, onchange: async function() { @@ -474,7 +480,7 @@ erpnext.PointOfSale.Payment = class { if (this.value > max_redeemable_amount) { frappe.show_alert({ - message: __(`You cannot redeem more than ${format_currency(max_redeemable_amount)}.`), + message: __("You cannot redeem more than {0}.", [format_currency(max_redeemable_amount)]), indicator: "red" }); frappe.utils.play_sound("submit"); diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index f7ff916c5a..dec4fe2da8 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -12,6 +12,7 @@ from erpnext.stock.get_item_details import get_reserved_qty_for_so from frappe import _, ValidationError from erpnext.controllers.stock_controller import StockController +from six import string_types from six.moves import map class SerialNoCannotCreateDirectError(ValidationError): pass class SerialNoCannotCannotChangeError(ValidationError): pass @@ -285,8 +286,10 @@ def validate_serial_no(sle, item_det): if sle.voucher_type == "Sales Invoice": if not frappe.db.exists("Sales Invoice Item", {"parent": sle.voucher_no, "item_code": sle.item_code, "sales_order": sr.sales_order}): - frappe.throw(_("Cannot deliver Serial No {0} of item {1} as it is reserved \ - to fullfill Sales Order {2}").format(sr.name, sle.item_code, sr.sales_order)) + frappe.throw( + _("Cannot deliver Serial No {0} of item {1} as it is reserved to fullfill Sales Order {2}") + .format(sr.name, sle.item_code, sr.sales_order) + ) elif sle.voucher_type == "Delivery Note": if not frappe.db.exists("Delivery Note Item", {"parent": sle.voucher_no, "item_code": sle.item_code, "against_sales_order": sr.sales_order}): @@ -295,8 +298,10 @@ def validate_serial_no(sle, item_det): if not invoice or frappe.db.exists("Sales Invoice Item", {"parent": invoice, "item_code": sle.item_code, "sales_order": sr.sales_order}): - frappe.throw(_("Cannot deliver Serial No {0} of item {1} as it is reserved to \ - fullfill Sales Order {2}").format(sr.name, sle.item_code, sr.sales_order)) + frappe.throw( + _("Cannot deliver Serial No {0} of item {1} as it is reserved to fullfill Sales Order {2}") + .format(sr.name, sle.item_code, sr.sales_order) + ) # if Sales Order reference in Delivery Note or Invoice validate SO reservations for item if sle.voucher_type == "Sales Invoice": sales_order = frappe.db.get_value("Sales Invoice Item", {"parent": sle.voucher_no, @@ -336,11 +341,12 @@ def validate_material_transfer_entry(sle_doc): else: sle_doc.skip_serial_no_validaiton = True -def validate_so_serial_no(sr, sales_order,): +def validate_so_serial_no(sr, sales_order): if not sr.sales_order or sr.sales_order!= sales_order: - frappe.throw(_("""Sales Order {0} has reservation for item {1}, you can - only deliver reserved {1} against {0}. Serial No {2} cannot - be delivered""").format(sales_order, sr.item_code, sr.name)) + msg = _("Sales Order {0} has reservation for item {1}") + msg += _(", you can only deliver reserved {1} against {0}.") + msg += _(" Serial No {2} cannot be delivered") + frappe.throw(msg.format(sales_order, sr.item_code, sr.name)) def has_duplicate_serial_no(sn, sle): if (sn.warehouse and not sle.skip_serial_no_validaiton @@ -538,54 +544,81 @@ def get_delivery_note_serial_no(item_code, qty, delivery_note): return serial_nos @frappe.whitelist() -def auto_fetch_serial_number(qty, item_code, warehouse, batch_nos=None, for_doctype=None): - filters = { - "item_code": item_code, - "warehouse": warehouse, - "delivery_document_no": "", - "sales_invoice": "" - } +def auto_fetch_serial_number(qty, item_code, warehouse, posting_date=None, batch_nos=None, for_doctype=None): + filters = { "item_code": item_code, "warehouse": warehouse } if batch_nos: try: - filters["batch_no"] = ["in", json.loads(batch_nos)] + filters["batch_no"] = json.loads(batch_nos) except: - filters["batch_no"] = ["in", [batch_nos]] + filters["batch_no"] = [batch_nos] + if posting_date: + filters["expiry_date"] = posting_date + + serial_numbers = [] if for_doctype == 'POS Invoice': - reserved_serial_nos, unreserved_serial_nos = get_pos_reserved_serial_nos(filters, qty) - return unreserved_serial_nos + reserved_sr_nos = get_pos_reserved_serial_nos(filters) + serial_numbers = fetch_serial_numbers(filters, qty, do_not_include=reserved_sr_nos) + else: + serial_numbers = fetch_serial_numbers(filters, qty) - serial_numbers = frappe.get_list("Serial No", filters=filters, limit=qty, order_by="creation") - return [item['name'] for item in serial_numbers] + return [d.get('name') for d in serial_numbers] @frappe.whitelist() -def get_pos_reserved_serial_nos(filters, qty=None): - batch_no_cond = "" - if filters.get("batch_no"): - batch_no_cond = "and item.batch_no = {}".format(frappe.db.escape(filters.get('batch_no'))) +def get_pos_reserved_serial_nos(filters): + if isinstance(filters, string_types): + filters = json.loads(filters) - reserved_serial_nos_str = [d.serial_no for d in frappe.db.sql("""select item.serial_no as serial_no + pos_transacted_sr_nos = frappe.db.sql("""select item.serial_no as serial_no from `tabPOS Invoice` p, `tabPOS Invoice Item` item where p.name = item.parent and p.consolidated_invoice is NULL and p.docstatus = 1 and item.docstatus = 1 - and item.item_code = %s - and item.warehouse = %s - {} - """.format(batch_no_cond), [filters.get('item_code'), filters.get('warehouse')], as_dict=1)] + and item.item_code = %(item_code)s + and item.warehouse = %(warehouse)s + and item.serial_no is NOT NULL and item.serial_no != '' + """, filters, as_dict=1) - reserved_serial_nos = [] - for s in reserved_serial_nos_str: - if not s: continue + reserved_sr_nos = [] + for d in pos_transacted_sr_nos: + reserved_sr_nos += get_serial_nos(d.serial_no) - serial_nos = s.split("\n") - serial_nos = ' '.join(serial_nos).split() # remove whitespaces - if len(serial_nos): reserved_serial_nos += serial_nos + return reserved_sr_nos - filters["name"] = ["not in", reserved_serial_nos] - serial_numbers = frappe.get_list("Serial No", filters=filters, limit=qty, order_by="creation") - unreserved_serial_nos = [item['name'] for item in serial_numbers] +def fetch_serial_numbers(filters, qty, do_not_include=[]): + batch_join_selection = "" + batch_no_condition = "" + batch_nos = filters.get("batch_no") + expiry_date = filters.get("expiry_date") + if batch_nos: + batch_no_condition = """and sr.batch_no in ({}) """.format(', '.join(["'%s'" % d for d in batch_nos])) - return reserved_serial_nos, unreserved_serial_nos \ No newline at end of file + if expiry_date: + batch_join_selection = "LEFT JOIN `tabBatch` batch on sr.batch_no = batch.name " + expiry_date_cond = "AND ifnull(batch.expiry_date, '2500-12-31') >= %(expiry_date)s " + batch_no_condition += expiry_date_cond + + excluded_sr_nos = ", ".join(["" + frappe.db.escape(sr) + "" for sr in do_not_include]) or "''" + serial_numbers = frappe.db.sql(""" + SELECT sr.name FROM `tabSerial No` sr {batch_join_selection} + WHERE + sr.name not in ({excluded_sr_nos}) AND + sr.item_code = %(item_code)s AND + sr.warehouse = %(warehouse)s AND + ifnull(sr.sales_invoice,'') = '' AND + ifnull(sr.delivery_document_no, '') = '' + {batch_no_condition} + ORDER BY + sr.creation + LIMIT + {qty} + """.format( + excluded_sr_nos=excluded_sr_nos, + qty=qty or 1, + batch_join_selection=batch_join_selection, + batch_no_condition=batch_no_condition + ), filters, as_dict=1) + + return serial_numbers \ No newline at end of file