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 9336fc37068..57baac76819 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 9899219bdcb..2b91c74ce6d 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 798637a840c..6e7768dc542 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 c43cb794aa5..86062d1e7cc 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 5b0822e3234..61263ac7883 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,97 @@ 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)
+ 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))
- 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"))
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 +189,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 +255,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 +306,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 +322,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):
@@ -352,6 +365,21 @@ class POSInvoice(SalesInvoice):
return make_payment_request(**record)
+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])
+
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
latest_sle = frappe.db.sql("""select qty_after_transaction
@@ -373,11 +401,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()
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 3a229b1787b..add27e9dffb 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 b9e07b8030a..acac1c4072e 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 4d5e1eb798c..30ebd307c4f 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 999da759971..4e22218c6ec 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 1d160a5aa7e..ee76bba7500 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 05cb7f0b4b5..8890d594036 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 c5260a1239f..91c4dfb5877 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 801e688deb9..4b598877d90 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 fc329776581..983cfa8c152 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 e80e3ed126d..47f577131af 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 be30086d34a..99f3995a662 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 33911793f67..23705a87790 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 d9f6e1d4336..2623c3c1a73 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 8d4ac784229..9d44a9f862f 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 83bd71d5f32..e5b50d77892 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 5018254b0ac..3d0054647bb 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(`
-