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 <nabinhait@gmail.com>
This commit is contained in:
parent
33881fd7e2
commit
60212ff9b9
@ -51,6 +51,7 @@ frappe.ui.form.on('POS Closing Entry', {
|
|||||||
args: {
|
args: {
|
||||||
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
|
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
|
||||||
end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date),
|
end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date),
|
||||||
|
pos_profile: frm.doc.pos_profile,
|
||||||
user: frm.doc.user
|
user: frm.doc.user
|
||||||
},
|
},
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
|
@ -14,19 +14,51 @@ from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import
|
|||||||
|
|
||||||
class POSClosingEntry(Document):
|
class POSClosingEntry(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
user = frappe.get_all('POS Closing Entry',
|
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
|
||||||
filters = { 'user': self.user, 'docstatus': 1 },
|
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 = {
|
or_filters = {
|
||||||
'period_start_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])
|
"period_end_date": ("between", [self.period_start_date, self.period_end_date])
|
||||||
})
|
})
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
frappe.throw(_("POS Closing Entry {} against {} between selected period"
|
bold_already_exists = frappe.bold(_("already exists"))
|
||||||
.format(frappe.bold("already exists"), frappe.bold(self.user))), title=_("Invalid Period"))
|
bold_user = frappe.bold(self.user)
|
||||||
|
frappe.throw(_("POS Closing Entry {} against {} between selected period")
|
||||||
|
.format(bold_already_exists, bold_user), title=_("Invalid Period"))
|
||||||
|
|
||||||
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
|
def validate_pos_invoices(self):
|
||||||
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
|
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 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):
|
def on_submit(self):
|
||||||
merge_pos_invoices(self.pos_transactions)
|
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]
|
return [c['user'] for c in cashiers_list]
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_pos_invoices(start, end, user):
|
def get_pos_invoices(start, end, pos_profile, user):
|
||||||
data = frappe.db.sql("""
|
data = frappe.db.sql("""
|
||||||
select
|
select
|
||||||
name, timestamp(posting_date, posting_time) as "timestamp"
|
name, timestamp(posting_date, posting_time) as "timestamp"
|
||||||
from
|
from
|
||||||
`tabPOS Invoice`
|
`tabPOS Invoice`
|
||||||
where
|
where
|
||||||
owner = %s and docstatus = 1 and
|
owner = %s and docstatus = 1 and pos_profile = %s and ifnull(consolidated_invoice,'') = ''
|
||||||
(consolidated_invoice is NULL or consolidated_invoice = '')
|
""", (user, pos_profile), as_dict=1)
|
||||||
""", (user), as_dict=1)
|
|
||||||
|
|
||||||
data = list(filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data))
|
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
|
# 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.net_total = 0
|
||||||
closing_entry.total_quantity = 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 = []
|
pos_transactions = []
|
||||||
taxes = []
|
taxes = []
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
"field_order": [
|
"field_order": [
|
||||||
"mode_of_payment",
|
"mode_of_payment",
|
||||||
"opening_amount",
|
"opening_amount",
|
||||||
"closing_amount",
|
|
||||||
"expected_amount",
|
"expected_amount",
|
||||||
|
"closing_amount",
|
||||||
"difference"
|
"difference"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@ -26,8 +26,7 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Expected Amount",
|
"label": "Expected Amount",
|
||||||
"options": "company:company_currency",
|
"options": "company:company_currency",
|
||||||
"read_only": 1,
|
"read_only": 1
|
||||||
"reqd": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "difference",
|
"fieldname": "difference",
|
||||||
@ -55,9 +54,10 @@
|
|||||||
"reqd": 1
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-05-29 15:03:34.533607",
|
"modified": "2020-10-23 16:45:43.662034",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Closing Entry Detail",
|
"name": "POS Closing Entry Detail",
|
||||||
|
@ -9,80 +9,63 @@ erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend(
|
|||||||
this._super(doc);
|
this._super(doc);
|
||||||
},
|
},
|
||||||
|
|
||||||
onload() {
|
onload(doc) {
|
||||||
this._super();
|
this._super();
|
||||||
if(this.frm.doc.__islocal && this.frm.doc.is_pos) {
|
if(doc.__islocal && doc.is_pos && frappe.get_route_str() !== 'point-of-sale') {
|
||||||
//Load pos profile data on the invoice if the default value of Is POS is 1
|
this.frm.script_manager.trigger("is_pos");
|
||||||
|
this.frm.refresh_fields();
|
||||||
me.frm.script_manager.trigger("is_pos");
|
|
||||||
me.frm.refresh_fields();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh(doc) {
|
refresh(doc) {
|
||||||
this._super();
|
this._super();
|
||||||
if (doc.docstatus == 1 && !doc.is_return) {
|
if (doc.docstatus == 1 && !doc.is_return) {
|
||||||
if(doc.outstanding_amount >= 0 || Math.abs(flt(doc.outstanding_amount)) < flt(doc.grand_total)) {
|
this.frm.add_custom_button(__('Return'), this.make_sales_return, __('Create'));
|
||||||
cur_frm.add_custom_button(__('Return'),
|
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||||
this.make_sales_return, __('Create'));
|
|
||||||
cur_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";
|
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();
|
this.set_pos_data();
|
||||||
},
|
},
|
||||||
|
|
||||||
set_pos_data: function() {
|
set_pos_data: async function() {
|
||||||
if(this.frm.doc.is_pos) {
|
if(this.frm.doc.is_pos) {
|
||||||
this.frm.set_value("allocate_advances_automatically", 0);
|
this.frm.set_value("allocate_advances_automatically", 0);
|
||||||
if(!this.frm.doc.company) {
|
if(!this.frm.doc.company) {
|
||||||
this.frm.set_value("is_pos", 0);
|
this.frm.set_value("is_pos", 0);
|
||||||
frappe.msgprint(__("Please specify Company to proceed"));
|
frappe.msgprint(__("Please specify Company to proceed"));
|
||||||
} else {
|
} else {
|
||||||
var me = this;
|
const r = await this.frm.call({
|
||||||
return this.frm.call({
|
doc: this.frm.doc,
|
||||||
doc: me.frm.doc,
|
|
||||||
method: "set_missing_values",
|
method: "set_missing_values",
|
||||||
callback: function(r) {
|
freeze: true
|
||||||
|
});
|
||||||
if(!r.exc) {
|
if(!r.exc) {
|
||||||
if(r.message) {
|
if(r.message) {
|
||||||
me.frm.pos_print_format = r.message.print_format || "";
|
this.frm.pos_print_format = r.message.print_format || "";
|
||||||
me.frm.meta.default_print_format = r.message.print_format || "";
|
this.frm.meta.default_print_format = r.message.print_format || "";
|
||||||
me.frm.allow_edit_rate = r.message.allow_edit_rate;
|
this.frm.doc.campaign = r.message.campaign;
|
||||||
me.frm.allow_edit_discount = r.message.allow_edit_discount;
|
this.frm.allow_print_before_pay = r.message.allow_print_before_pay;
|
||||||
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");
|
this.frm.script_manager.trigger("update_stock");
|
||||||
me.calculate_taxes_and_totals();
|
this.calculate_taxes_and_totals();
|
||||||
if(me.frm.doc.taxes_and_charges) {
|
this.frm.doc.taxes_and_charges && this.frm.script_manager.trigger("taxes_and_charges");
|
||||||
me.frm.script_manager.trigger("taxes_and_charges");
|
frappe.model.set_default_values(this.frm.doc);
|
||||||
}
|
this.set_dynamic_labels();
|
||||||
frappe.model.set_default_values(me.frm.doc);
|
|
||||||
me.set_dynamic_labels();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else this.frm.trigger("refresh");
|
|
||||||
},
|
},
|
||||||
|
|
||||||
customer() {
|
customer() {
|
||||||
if (!this.frm.doc.customer) return
|
if (!this.frm.doc.customer) return
|
||||||
|
const pos_profile = this.frm.doc.pos_profile;
|
||||||
if (this.frm.doc.is_pos){
|
|
||||||
var pos_profile = this.frm.doc.pos_profile;
|
|
||||||
}
|
|
||||||
var me = this;
|
|
||||||
if(this.frm.updating_party_details) return;
|
if(this.frm.updating_party_details) return;
|
||||||
erpnext.utils.get_party_details(this.frm,
|
erpnext.utils.get_party_details(this.frm,
|
||||||
"erpnext.accounts.party.get_party_details", {
|
"erpnext.accounts.party.get_party_details", {
|
||||||
@ -92,8 +75,8 @@ erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend(
|
|||||||
account: this.frm.doc.debit_to,
|
account: this.frm.doc.debit_to,
|
||||||
price_list: this.frm.doc.selling_price_list,
|
price_list: this.frm.doc.selling_price_list,
|
||||||
pos_profile: pos_profile
|
pos_profile: pos_profile
|
||||||
}, function() {
|
}, () => {
|
||||||
me.apply_pricing_rule();
|
this.apply_pricing_rule();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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 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.utils import get_account_currency
|
||||||
from erpnext.accounts.party import get_party_account, get_due_date
|
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.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
|
from six import iteritems
|
||||||
|
|
||||||
@ -30,8 +28,7 @@ class POSInvoice(SalesInvoice):
|
|||||||
# run on validate method of selling controller
|
# run on validate method of selling controller
|
||||||
super(SalesInvoice, self).validate()
|
super(SalesInvoice, self).validate()
|
||||||
self.validate_auto_set_posting_time()
|
self.validate_auto_set_posting_time()
|
||||||
self.validate_pos_paid_amount()
|
self.validate_mode_of_payment()
|
||||||
self.validate_pos_return()
|
|
||||||
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||||
self.validate_uom_is_integer("uom", "qty")
|
self.validate_uom_is_integer("uom", "qty")
|
||||||
self.validate_debit_to_acc()
|
self.validate_debit_to_acc()
|
||||||
@ -41,11 +38,11 @@ class POSInvoice(SalesInvoice):
|
|||||||
self.validate_item_cost_centers()
|
self.validate_item_cost_centers()
|
||||||
self.validate_serialised_or_batched_item()
|
self.validate_serialised_or_batched_item()
|
||||||
self.validate_stock_availablility()
|
self.validate_stock_availablility()
|
||||||
self.validate_return_items()
|
self.validate_return_items_qty()
|
||||||
self.set_status()
|
self.set_status()
|
||||||
self.set_account_for_mode_of_payment()
|
self.set_account_for_mode_of_payment()
|
||||||
self.validate_pos()
|
self.validate_pos()
|
||||||
self.verify_payment_amount()
|
self.validate_payment_amount()
|
||||||
self.validate_loyalty_transaction()
|
self.validate_loyalty_transaction()
|
||||||
|
|
||||||
def on_submit(self):
|
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))
|
return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment))
|
||||||
|
|
||||||
def validate_stock_availablility(self):
|
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'):
|
for d in self.get('items'):
|
||||||
|
msg = ""
|
||||||
if d.serial_no:
|
if d.serial_no:
|
||||||
filters = {
|
filters = { "item_code": d.item_code, "warehouse": d.warehouse }
|
||||||
"item_code": d.item_code,
|
|
||||||
"warehouse": d.warehouse,
|
|
||||||
"delivery_document_no": "",
|
|
||||||
"sales_invoice": ""
|
|
||||||
}
|
|
||||||
if d.batch_no:
|
if d.batch_no:
|
||||||
filters["batch_no"] = 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):
|
reserved_serial_nos = get_pos_reserved_serial_nos(filters)
|
||||||
multiple_nos = 's' if len(invalid_serial_nos) > 1 else ''
|
serial_nos = get_serial_nos(d.serial_no)
|
||||||
frappe.throw(_("Row #{}: Serial No{}. {} has already been transacted into another POS Invoice. Please select valid serial no.").format(
|
invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos]
|
||||||
d.idx, multiple_nos, frappe.bold(', '.join(invalid_serial_nos))), title=_("Not Available"))
|
|
||||||
|
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:
|
else:
|
||||||
if allow_negative_stock:
|
if allow_negative_stock:
|
||||||
return
|
return
|
||||||
|
|
||||||
available_stock = get_stock_availability(d.item_code, d.warehouse)
|
available_stock = get_stock_availability(d.item_code, d.warehouse)
|
||||||
if not (flt(available_stock) > 0):
|
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
|
||||||
frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.').format(
|
if flt(available_stock) <= 0:
|
||||||
d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse)), title=_("Not Available"))
|
msg = (_('Row #{}: Item Code: {} is not available under warehouse {}.').format(d.idx, item_code, warehouse))
|
||||||
elif flt(available_stock) < flt(d.qty):
|
elif flt(available_stock) < flt(d.qty):
|
||||||
frappe.msgprint(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.').format(
|
msg = (_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.')
|
||||||
d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)), title=_("Not Available"))
|
.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):
|
def validate_serialised_or_batched_item(self):
|
||||||
|
error_msg = []
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
serialized = d.get("has_serial_no")
|
serialized = d.get("has_serial_no")
|
||||||
batched = d.get("has_batch_no")
|
batched = d.get("has_batch_no")
|
||||||
no_serial_selected = not d.get("serial_no")
|
no_serial_selected = not d.get("serial_no")
|
||||||
no_batch_selected = not d.get("batch_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):
|
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(
|
msg = (_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.')
|
||||||
d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
|
.format(d.idx, item_code))
|
||||||
if serialized and no_serial_selected:
|
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(
|
msg = (_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.')
|
||||||
d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
|
.format(d.idx, item_code))
|
||||||
if batched and no_batch_selected:
|
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(
|
msg = (_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.')
|
||||||
d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
|
.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
|
if not self.get("is_return"): return
|
||||||
|
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
if d.get("qty") > 0:
|
if d.get("qty") > 0:
|
||||||
frappe.throw(_("Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return.")
|
frappe.throw(
|
||||||
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
|
_("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):
|
def validate_mode_of_payment(self):
|
||||||
if len(self.payments) == 0 and self.is_pos:
|
if len(self.payments) == 0:
|
||||||
frappe.throw(_("At least one mode of payment is required for POS invoice."))
|
frappe.throw(_("At least one mode of payment is required for POS invoice."))
|
||||||
|
|
||||||
def validate_change_account(self):
|
def validate_change_account(self):
|
||||||
@ -165,20 +190,18 @@ class POSInvoice(SalesInvoice):
|
|||||||
if flt(self.change_amount) and not self.account_for_change_amount:
|
if flt(self.change_amount) and not self.account_for_change_amount:
|
||||||
frappe.msgprint(_("Please enter Account for Change Amount"), raise_exception=1)
|
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:
|
for entry in self.payments:
|
||||||
|
total_amount_in_payments += entry.amount
|
||||||
if not self.is_return and entry.amount < 0:
|
if not self.is_return and entry.amount < 0:
|
||||||
frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx))
|
frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx))
|
||||||
if self.is_return and entry.amount > 0:
|
if self.is_return and entry.amount > 0:
|
||||||
frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx))
|
frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx))
|
||||||
|
|
||||||
def validate_pos_return(self):
|
if self.is_return:
|
||||||
if self.is_pos and self.is_return:
|
|
||||||
total_amount_in_payments = 0
|
|
||||||
for payment in self.payments:
|
|
||||||
total_amount_in_payments += payment.amount
|
|
||||||
invoice_total = self.rounded_total or self.grand_total
|
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))
|
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
|
||||||
|
|
||||||
def validate_loyalty_transaction(self):
|
def validate_loyalty_transaction(self):
|
||||||
@ -233,55 +256,45 @@ class POSInvoice(SalesInvoice):
|
|||||||
pos_profile = get_pos_profile(self.company) or {}
|
pos_profile = get_pos_profile(self.company) or {}
|
||||||
self.pos_profile = pos_profile.get('name')
|
self.pos_profile = pos_profile.get('name')
|
||||||
|
|
||||||
pos = {}
|
profile = {}
|
||||||
if self.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:
|
if not self.get('payments') and not for_validate:
|
||||||
update_multi_mode_option(self, pos)
|
update_multi_mode_option(self, profile)
|
||||||
|
|
||||||
if not self.account_for_change_amount:
|
if self.is_return and not for_validate:
|
||||||
self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
|
add_return_modes(self, profile)
|
||||||
|
|
||||||
if pos:
|
|
||||||
if not for_validate:
|
|
||||||
self.tax_category = pos.get("tax_category")
|
|
||||||
|
|
||||||
|
if profile:
|
||||||
if not for_validate and not self.customer:
|
if not for_validate and not self.customer:
|
||||||
self.customer = pos.customer
|
self.customer = profile.customer
|
||||||
|
|
||||||
self.ignore_pricing_rule = pos.ignore_pricing_rule
|
self.ignore_pricing_rule = profile.ignore_pricing_rule
|
||||||
if pos.get('account_for_change_amount'):
|
self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount
|
||||||
self.account_for_change_amount = pos.get('account_for_change_amount')
|
self.set_warehouse = profile.get('warehouse') or self.set_warehouse
|
||||||
if pos.get('warehouse'):
|
|
||||||
self.set_warehouse = pos.get('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',
|
'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges',
|
||||||
'write_off_cost_center', 'apply_discount_on', 'cost_center'):
|
'write_off_cost_center', 'apply_discount_on', 'cost_center', 'tax_category',
|
||||||
if (not for_validate) or (for_validate and not self.get(fieldname)):
|
'ignore_pricing_rule', 'company_address', 'update_stock'):
|
||||||
self.set(fieldname, pos.get(fieldname))
|
if not for_validate:
|
||||||
|
self.set(fieldname, profile.get(fieldname))
|
||||||
if pos.get("company_address"):
|
|
||||||
self.company_address = pos.get("company_address")
|
|
||||||
|
|
||||||
if self.customer:
|
if self.customer:
|
||||||
customer_price_list, customer_group = frappe.db.get_value("Customer", self.customer, ['default_price_list', 'customer_group'])
|
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')
|
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:
|
else:
|
||||||
selling_price_list = pos.get('selling_price_list')
|
selling_price_list = profile.get('selling_price_list')
|
||||||
|
|
||||||
if selling_price_list:
|
if selling_price_list:
|
||||||
self.set('selling_price_list', 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
|
# set pos values in items
|
||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
if item.get('item_code'):
|
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):
|
for fname, val in iteritems(profile_details):
|
||||||
if (not for_validate) or (for_validate and not item.get(fname)):
|
if (not for_validate) or (for_validate and not item.get(fname)):
|
||||||
item.set(fname, val)
|
item.set(fname, val)
|
||||||
@ -294,10 +307,13 @@ class POSInvoice(SalesInvoice):
|
|||||||
if self.taxes_and_charges and not len(self.get("taxes")):
|
if self.taxes_and_charges and not len(self.get("taxes")):
|
||||||
self.set_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):
|
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:
|
if not self.debit_to:
|
||||||
self.debit_to = get_party_account("Customer", self.customer, self.company)
|
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)
|
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')):
|
if not print_format and not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')):
|
||||||
print_format = 'POS Invoice'
|
print_format = 'POS Invoice'
|
||||||
|
|
||||||
if pos:
|
if profile:
|
||||||
return {
|
return {
|
||||||
"print_format": print_format,
|
"print_format": print_format,
|
||||||
"allow_edit_rate": pos.get("allow_user_to_edit_rate"),
|
"campaign": profile.get("campaign"),
|
||||||
"allow_edit_discount": pos.get("allow_user_to_edit_discount"),
|
"allow_print_before_pay": profile.get("allow_print_before_pay")
|
||||||
"campaign": pos.get("campaign"),
|
|
||||||
"allow_print_before_pay": pos.get("allow_print_before_pay")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def set_account_for_mode_of_payment(self):
|
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
|
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
|
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
|
return sle_qty - pos_sales_qty
|
||||||
else:
|
else:
|
||||||
# when sle_qty is 0
|
|
||||||
# when sle_qty > 0 and pos_sales_qty is 0
|
|
||||||
return sle_qty
|
return sle_qty
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@ -411,3 +423,18 @@ def make_merge_log(invoices):
|
|||||||
|
|
||||||
if merge_log.get('pos_invoices'):
|
if merge_log.get('pos_invoices'):
|
||||||
return merge_log.as_dict()
|
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])
|
@ -27,17 +27,24 @@ class POSInvoiceMergeLog(Document):
|
|||||||
status, docstatus, is_return, return_against = frappe.db.get_value(
|
status, docstatus, is_return, return_against = frappe.db.get_value(
|
||||||
'POS Invoice', d.pos_invoice, ['status', 'docstatus', 'is_return', 'return_against'])
|
'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:
|
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":
|
if status == "Consolidated":
|
||||||
frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, d.pos_invoice, status))
|
frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, bold_pos_invoice, bold_status))
|
||||||
if is_return and return_against not in [d.pos_invoice for d in self.pos_invoices] and status != "Consolidated":
|
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
|
# if return entry is not getting merged in the current pos closing and if it is not consolidated
|
||||||
frappe.throw(
|
bold_unconsolidated = frappe.bold("not Consolidated")
|
||||||
_("Row #{}: Return Invoice {} cannot be made against unconsolidated invoice. \
|
msg = (_("Row #{}: Original Invoice {} of return invoice {} is {}. ")
|
||||||
You can add original invoice {} manually to proceed.")
|
.format(d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated))
|
||||||
.format(d.idx, frappe.bold(d.pos_invoice), frappe.bold(return_against))
|
msg += _("Original invoice should be consolidated before or along with the return invoice.")
|
||||||
)
|
msg += "<br><br>"
|
||||||
|
msg += _("You can add original invoice {} manually to proceed.").format(bold_return_against)
|
||||||
|
frappe.throw(msg)
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
|
pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
|
||||||
|
@ -17,18 +17,25 @@ class POSOpeningEntry(StatusUpdater):
|
|||||||
|
|
||||||
def validate_pos_profile_and_cashier(self):
|
def validate_pos_profile_and_cashier(self):
|
||||||
if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"):
|
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")):
|
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):
|
def validate_payment_method_account(self):
|
||||||
|
invalid_modes = []
|
||||||
for d in self.balance_details:
|
for d in self.balance_details:
|
||||||
account = frappe.db.get_value("Mode of Payment Account",
|
account = frappe.db.get_value("Mode of Payment Account",
|
||||||
{"parent": d.mode_of_payment, "company": self.company}, "default_account")
|
{"parent": d.mode_of_payment, "company": self.company}, "default_account")
|
||||||
if not account:
|
if not account:
|
||||||
frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}")
|
invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment))
|
||||||
.format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account"))
|
|
||||||
|
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):
|
def on_submit(self):
|
||||||
self.set_status(update=True)
|
self.set_status(update=True)
|
@ -6,6 +6,7 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"default",
|
"default",
|
||||||
|
"allow_in_returns",
|
||||||
"mode_of_payment"
|
"mode_of_payment"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@ -24,11 +25,19 @@
|
|||||||
"label": "Mode of Payment",
|
"label": "Mode of Payment",
|
||||||
"options": "Mode of Payment",
|
"options": "Mode of Payment",
|
||||||
"reqd": 1
|
"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,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-05-29 15:08:41.704844",
|
"modified": "2020-10-20 12:58:46.114456",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Payment Method",
|
"name": "POS Payment Method",
|
||||||
|
@ -290,28 +290,30 @@
|
|||||||
"fieldname": "warehouse",
|
"fieldname": "warehouse",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Warehouse",
|
"label": "Warehouse",
|
||||||
"mandatory_depends_on": "update_stock",
|
|
||||||
"oldfieldname": "warehouse",
|
"oldfieldname": "warehouse",
|
||||||
"oldfieldtype": "Link",
|
"oldfieldtype": "Link",
|
||||||
"options": "Warehouse"
|
"options": "Warehouse",
|
||||||
},
|
"reqd": 1
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "update_stock",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Update Stock"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "ignore_pricing_rule",
|
"fieldname": "ignore_pricing_rule",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Ignore Pricing Rule"
|
"label": "Ignore Pricing Rule"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "update_stock",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Update Stock",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-10-01 17:29:27.759088",
|
"modified": "2020-10-20 13:16:50.665081",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Profile",
|
"name": "POS Profile",
|
||||||
|
@ -56,19 +56,29 @@ class POSProfile(Document):
|
|||||||
if not self.payments:
|
if not self.payments:
|
||||||
frappe.throw(_("Payment methods are mandatory. Please add at least one payment method."))
|
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]
|
default_mode = [d.default for d in self.payments if d.default]
|
||||||
if not default_mode_of_payment:
|
if not default_mode:
|
||||||
frappe.throw(_("Please select a default mode of payment"))
|
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"))
|
frappe.throw(_("You can only select one mode of payment as default"))
|
||||||
|
|
||||||
|
invalid_modes = []
|
||||||
for d in self.payments:
|
for d in self.payments:
|
||||||
account = frappe.db.get_value("Mode of Payment Account",
|
account = frappe.db.get_value(
|
||||||
{"parent": d.mode_of_payment, "company": self.company}, "default_account")
|
"Mode of Payment Account",
|
||||||
|
{"parent": d.mode_of_payment, "company": self.company},
|
||||||
|
"default_account"
|
||||||
|
)
|
||||||
if not account:
|
if not account:
|
||||||
frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}")
|
invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment))
|
||||||
.format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account"))
|
|
||||||
|
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):
|
def on_update(self):
|
||||||
self.set_defaults()
|
self.set_defaults()
|
||||||
|
@ -9,8 +9,7 @@ frappe.ui.form.on('POS Settings', {
|
|||||||
get_invoice_fields: function(frm) {
|
get_invoice_fields: function(frm) {
|
||||||
frappe.model.with_doctype("POS Invoice", () => {
|
frappe.model.with_doctype("POS Invoice", () => {
|
||||||
var fields = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function(d) {
|
var fields = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function(d) {
|
||||||
if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 ||
|
if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 || ['Button'].includes(d.fieldtype)) {
|
||||||
['Table', 'Button'].includes(d.fieldtype)) {
|
|
||||||
return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname };
|
return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname };
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
|
@ -151,14 +151,16 @@ class PurchaseInvoice(BuyingController):
|
|||||||
["account_type", "report_type", "account_currency"], as_dict=True)
|
["account_type", "report_type", "account_currency"], as_dict=True)
|
||||||
|
|
||||||
if account.report_type != "Balance Sheet":
|
if account.report_type != "Balance Sheet":
|
||||||
frappe.throw(_("Please ensure {} account is a Balance Sheet account. \
|
frappe.throw(
|
||||||
You can change the parent account to a Balance Sheet account or select a different account.")
|
_("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"))
|
.format(frappe.bold("Credit To")), title=_("Invalid Account")
|
||||||
|
)
|
||||||
|
|
||||||
if self.supplier and account.account_type != "Payable":
|
if self.supplier and account.account_type != "Payable":
|
||||||
frappe.throw(_("Please ensure {} account is a Payable account. \
|
frappe.throw(
|
||||||
Change the account type to Payable or select a different account.")
|
_("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"))
|
.format(frappe.bold("Credit To")), title=_("Invalid Account")
|
||||||
|
)
|
||||||
|
|
||||||
self.party_account_currency = account.account_currency
|
self.party_account_currency = account.account_currency
|
||||||
|
|
||||||
@ -244,10 +246,10 @@ class PurchaseInvoice(BuyingController):
|
|||||||
|
|
||||||
if self.update_stock and (not item.from_warehouse):
|
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"]:
|
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}
|
msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]))
|
||||||
is not linked to warehouse {3} or it is not the default inventory account'''.format(
|
msg += _("because account {} is not linked to warehouse {} ").format(frappe.bold(item.expense_account), frappe.bold(item.warehouse))
|
||||||
item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]),
|
msg += _("or it is not the default inventory account")
|
||||||
frappe.bold(item.expense_account), frappe.bold(item.warehouse))))
|
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
||||||
|
|
||||||
item.expense_account = warehouse_account[item.warehouse]["account"]
|
item.expense_account = warehouse_account[item.warehouse]["account"]
|
||||||
else:
|
else:
|
||||||
@ -259,19 +261,19 @@ class PurchaseInvoice(BuyingController):
|
|||||||
|
|
||||||
if negative_expense_booked_in_pr:
|
if negative_expense_booked_in_pr:
|
||||||
if for_validate and item.expense_account and item.expense_account != stock_not_billed_account:
|
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
|
msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account))
|
||||||
expense is booked against this account in Purchase Receipt {2}'''.format(
|
msg += _("because expense is booked against this account in Purchase Receipt {}").format(frappe.bold(item.purchase_receipt))
|
||||||
item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.purchase_receipt))))
|
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
||||||
|
|
||||||
item.expense_account = stock_not_billed_account
|
item.expense_account = stock_not_billed_account
|
||||||
else:
|
else:
|
||||||
# If no purchase receipt present then book expense in 'Stock Received But Not Billed'
|
# 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
|
# 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:
|
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
|
msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account))
|
||||||
Receipt is created against Item {2}. This is done to handle accounting for cases
|
msg += _("as no Purchase Receipt is created against Item {}. ").format(frappe.bold(item.item_code))
|
||||||
when Purchase Receipt is created after Purchase Invoice'''.format(
|
msg += _("This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice")
|
||||||
item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.item_code))))
|
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
||||||
|
|
||||||
item.expense_account = stock_not_billed_account
|
item.expense_account = stock_not_billed_account
|
||||||
|
|
||||||
@ -299,10 +301,11 @@ class PurchaseInvoice(BuyingController):
|
|||||||
|
|
||||||
for d in self.get('items'):
|
for d in self.get('items'):
|
||||||
if not d.purchase_order:
|
if not d.purchase_order:
|
||||||
throw(_("""Purchase Order Required for item {0}
|
msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code))
|
||||||
To submit the invoice without purchase order please set
|
msg += "<br><br>"
|
||||||
{1} as {2} in {3}""").format(frappe.bold(d.item_code), frappe.bold(_('Purchase Order Required')),
|
msg += _("To submit the invoice without purchase order please set {} ").format(frappe.bold(_('Purchase Order Required')))
|
||||||
frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings')))
|
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):
|
def pr_required(self):
|
||||||
stock_items = self.get_stock_items()
|
stock_items = self.get_stock_items()
|
||||||
@ -313,10 +316,11 @@ class PurchaseInvoice(BuyingController):
|
|||||||
|
|
||||||
for d in self.get('items'):
|
for d in self.get('items'):
|
||||||
if not d.purchase_receipt and d.item_code in stock_items:
|
if not d.purchase_receipt and d.item_code in stock_items:
|
||||||
throw(_("""Purchase Receipt Required for item {0}
|
msg = _("Purchase Receipt Required for item {}").format(frappe.bold(d.item_code))
|
||||||
To submit the invoice without purchase receipt please set
|
msg += "<br><br>"
|
||||||
{1} as {2} in {3}""").format(frappe.bold(d.item_code), frappe.bold(_('Purchase Receipt Required')),
|
msg += _("To submit the invoice without purchase receipt please set {} ").format(frappe.bold(_('Purchase Receipt Required')))
|
||||||
frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings')))
|
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):
|
def validate_write_off_account(self):
|
||||||
if self.write_off_amount and not self.write_off_account:
|
if self.write_off_amount and not self.write_off_account:
|
||||||
|
@ -479,14 +479,14 @@ class SalesInvoice(SellingController):
|
|||||||
frappe.throw(_("Debit To is required"), title=_("Account Missing"))
|
frappe.throw(_("Debit To is required"), title=_("Account Missing"))
|
||||||
|
|
||||||
if account.report_type != "Balance Sheet":
|
if account.report_type != "Balance Sheet":
|
||||||
frappe.throw(_("Please ensure {} account is a Balance Sheet account. \
|
msg = _("Please ensure {} account is a Balance Sheet account. ").format(frappe.bold("Debit To"))
|
||||||
You can change the parent account to a Balance Sheet account or select a different account.")
|
msg += _("You can change the parent account to a Balance Sheet account or select a different account.")
|
||||||
.format(frappe.bold("Debit To")), title=_("Invalid Account"))
|
frappe.throw(msg, title=_("Invalid Account"))
|
||||||
|
|
||||||
if self.customer and account.account_type != "Receivable":
|
if self.customer and account.account_type != "Receivable":
|
||||||
frappe.throw(_("Please ensure {} account is a Receivable account. \
|
msg = _("Please ensure {} account is a Receivable account. ").format(frappe.bold("Debit To"))
|
||||||
Change the account type to Receivable or select a different account.")
|
msg += _("Change the account type to Receivable or select a different account.")
|
||||||
.format(frappe.bold("Debit To")), title=_("Invalid Account"))
|
frappe.throw(msg, title=_("Invalid Account"))
|
||||||
|
|
||||||
self.party_account_currency = account.account_currency
|
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)
|
where redeem_against=%s''', (lp_entry[0].name), as_dict=1)
|
||||||
if against_lp_entry:
|
if against_lp_entry:
|
||||||
invoice_list = ", ".join([d.invoice for d in 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.
|
frappe.throw(
|
||||||
First cancel the {} No {}''').format(self.doctype, self.doctype, invoice_list))
|
_('''{} can't be cancelled since the Loyalty Points earned has been redeemed. First cancel the {} No {}''')
|
||||||
|
.format(self.doctype, self.doctype, invoice_list)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
frappe.db.sql('''delete from `tabLoyalty Point Entry` where invoice=%s''', (self.name))
|
frappe.db.sql('''delete from `tabLoyalty Point Entry` where invoice=%s''', (self.name))
|
||||||
# Set loyalty program
|
# Set loyalty program
|
||||||
@ -1613,17 +1615,25 @@ def update_multi_mode_option(doc, pos_profile):
|
|||||||
payment.type = payment_mode.type
|
payment.type = payment_mode.type
|
||||||
|
|
||||||
doc.set('payments', [])
|
doc.set('payments', [])
|
||||||
|
invalid_modes = []
|
||||||
for pos_payment_method in pos_profile.get('payments'):
|
for pos_payment_method in pos_profile.get('payments'):
|
||||||
pos_payment_method = pos_payment_method.as_dict()
|
pos_payment_method = pos_payment_method.as_dict()
|
||||||
|
|
||||||
payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company)
|
payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company)
|
||||||
if not payment_mode:
|
if not payment_mode:
|
||||||
frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}")
|
invalid_modes.append(get_link_to_form("Mode of Payment", pos_payment_method.mode_of_payment))
|
||||||
.format(get_link_to_form("Mode of Payment", pos_payment_method.mode_of_payment)), title=_("Missing Account"))
|
continue
|
||||||
|
|
||||||
payment_mode[0].default = pos_payment_method.default
|
payment_mode[0].default = pos_payment_method.default
|
||||||
append_payment(payment_mode[0])
|
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):
|
def get_all_mode_of_payments(doc):
|
||||||
return frappe.db.sql("""
|
return frappe.db.sql("""
|
||||||
select mpa.default_account, mpa.parent, mp.type as type
|
select mpa.default_account, mpa.parent, mp.type as type
|
||||||
|
@ -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")
|
company_currency = frappe.get_cached_value('Company', company, "default_currency")
|
||||||
|
|
||||||
if not conversion_rate:
|
if not conversion_rate:
|
||||||
throw(_("{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}.").format(
|
throw(
|
||||||
conversion_rate_label, currency, company_currency))
|
_("{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):
|
def validate_taxes_and_charges(tax):
|
||||||
|
@ -210,6 +210,7 @@
|
|||||||
[data-route="point-of-sale"] .item-summary-wrapper:last-child { border-bottom: none; }
|
[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"] .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"] .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"] .summary-btns:last-child { margin-right: 0px; }
|
||||||
[data-route="point-of-sale"] ::-webkit-scrollbar { width: 1px }
|
[data-route="point-of-sale"] ::-webkit-scrollbar { width: 1px }
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
|||||||
|
|
||||||
apply_pricing_rule_on_item: function(item){
|
apply_pricing_rule_on_item: function(item){
|
||||||
let effective_item_rate = item.price_list_rate;
|
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) {
|
if (in_list(["Sales Order", "Quotation"], item.parenttype) && item.blanket_order_rate) {
|
||||||
effective_item_rate = 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.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){
|
if(item.discount_percentage){
|
||||||
item.discount_amount = flt(item.rate_with_margin) * flt(item.discount_percentage) / 100;
|
item.discount_amount = flt(item.rate_with_margin) * flt(item.discount_percentage) / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.discount_amount) {
|
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) {
|
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) {
|
if(this.frm.doc.currency == company_currency) {
|
||||||
this.frm.set_value("conversion_rate", 1);
|
this.frm.set_value("conversion_rate", 1);
|
||||||
} else {
|
} else {
|
||||||
const err_message = __('{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}', [
|
const subs = [conversion_rate_label, this.frm.doc.currency, company_currency];
|
||||||
conversion_rate_label,
|
const err_message = __('{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}', subs);
|
||||||
this.frm.doc.currency,
|
|
||||||
company_currency
|
|
||||||
]);
|
|
||||||
frappe.throw(err_message);
|
frappe.throw(err_message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1049,14 +1049,13 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
|||||||
if(item.item_code && item.uom) {
|
if(item.item_code && item.uom) {
|
||||||
return this.frm.call({
|
return this.frm.call({
|
||||||
method: "erpnext.stock.get_item_details.get_conversion_factor",
|
method: "erpnext.stock.get_item_details.get_conversion_factor",
|
||||||
child: item,
|
|
||||||
args: {
|
args: {
|
||||||
item_code: item.item_code,
|
item_code: item.item_code,
|
||||||
uom: item.uom
|
uom: item.uom
|
||||||
},
|
},
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if(!r.exc) {
|
if(!r.exc) {
|
||||||
me.conversion_factor(me.frm.doc, cdt, cdn);
|
frappe.model.set_value(cdt, cdn, 'conversion_factor', r.message.conversion_factor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -75,7 +75,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
|||||||
fieldtype:'Float',
|
fieldtype:'Float',
|
||||||
read_only: me.has_batch && !me.has_serial_no,
|
read_only: me.has_batch && !me.has_serial_no,
|
||||||
label: __(me.has_batch && !me.has_serial_no ? 'Total Qty' : 'Qty'),
|
label: __(me.has_batch && !me.has_serial_no ? 'Total Qty' : 'Qty'),
|
||||||
default: 0
|
default: flt(me.item.stock_qty),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: 'auto_fetch_button',
|
fieldname: 'auto_fetch_button',
|
||||||
@ -91,7 +91,8 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
|||||||
qty: qty,
|
qty: qty,
|
||||||
item_code: me.item_code,
|
item_code: me.item_code,
|
||||||
warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '',
|
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;
|
let records_length = auto_fetched_serial_numbers.length;
|
||||||
if (!records_length) {
|
if (!records_length) {
|
||||||
const warehouse = me.dialog.fields_dict.warehouse.get_value().bold();
|
const warehouse = me.dialog.fields_dict.warehouse.get_value().bold();
|
||||||
frappe.msgprint(__(`Serial numbers unavailable for Item ${me.item.item_code.bold()}
|
frappe.msgprint(
|
||||||
under warehouse ${warehouse}. Please try changing warehouse.`));
|
__('Serial numbers unavailable for Item {0} under warehouse {1}. Please try changing warehouse.', [me.item.item_code.bold(), warehouse])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (records_length < qty) {
|
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;
|
let serial_no_list_field = this.dialog.fields_dict.serial_no;
|
||||||
numbers = auto_fetched_serial_numbers.join('\n');
|
numbers = auto_fetched_serial_numbers.join('\n');
|
||||||
@ -189,15 +191,12 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
|||||||
}
|
}
|
||||||
if(this.has_batch && !this.has_serial_no) {
|
if(this.has_batch && !this.has_serial_no) {
|
||||||
if(values.batches.length === 0 || !values.batches) {
|
if(values.batches.length === 0 || !values.batches) {
|
||||||
frappe.throw(__("Please select batches for batched item "
|
frappe.throw(__("Please select batches for batched item {0}", [values.item_code]));
|
||||||
+ values.item_code));
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
values.batches.map((batch, i) => {
|
values.batches.map((batch, i) => {
|
||||||
if(!batch.selected_qty || batch.selected_qty === 0 ) {
|
if(!batch.selected_qty || batch.selected_qty === 0 ) {
|
||||||
if (!this.show_dialog) {
|
if (!this.show_dialog) {
|
||||||
frappe.throw(__("Please select quantity on row " + (i+1)));
|
frappe.throw(__("Please select quantity on row {0}", [i+1]));
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -206,9 +205,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
|||||||
} else {
|
} else {
|
||||||
let serial_nos = values.serial_no || '';
|
let serial_nos = values.serial_no || '';
|
||||||
if (!serial_nos || !serial_nos.replace(/\s/g, '').length) {
|
if (!serial_nos || !serial_nos.replace(/\s/g, '').length) {
|
||||||
frappe.throw(__("Please enter serial numbers for serialized item "
|
frappe.throw(__("Please enter serial numbers for serialized item {0}", [values.item_code]));
|
||||||
+ values.item_code));
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -355,8 +352,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
|||||||
});
|
});
|
||||||
if (selected_batches.includes(val)) {
|
if (selected_batches.includes(val)) {
|
||||||
this.set_value("");
|
this.set_value("");
|
||||||
frappe.throw(__(`Batch ${val} already selected.`));
|
frappe.throw(__('Batch {0} already selected.', [val]));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (me.warehouse_details.name) {
|
if (me.warehouse_details.name) {
|
||||||
@ -375,8 +371,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.set_value("");
|
this.set_value("");
|
||||||
frappe.throw(__(`Please select a warehouse to get available
|
frappe.throw(__('Please select a warehouse to get available quantities'));
|
||||||
quantities`));
|
|
||||||
}
|
}
|
||||||
// e.stopImmediatePropagation();
|
// e.stopImmediatePropagation();
|
||||||
}
|
}
|
||||||
@ -411,8 +406,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
|||||||
parseFloat(available_qty) < parseFloat(selected_qty)) {
|
parseFloat(available_qty) < parseFloat(selected_qty)) {
|
||||||
|
|
||||||
this.set_value('0');
|
this.set_value('0');
|
||||||
frappe.throw(__(`For transfer from source, selected quantity cannot be
|
frappe.throw(__('For transfer from source, selected quantity cannot be greater than available quantity'));
|
||||||
greater than available quantity`));
|
|
||||||
} else {
|
} else {
|
||||||
this.grid.refresh();
|
this.grid.refresh();
|
||||||
}
|
}
|
||||||
@ -451,20 +445,12 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
|||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.stock.doctype.serial_no.serial_no.get_pos_reserved_serial_nos",
|
method: "erpnext.stock.doctype.serial_no.serial_no.get_pos_reserved_serial_nos",
|
||||||
args: {
|
args: {
|
||||||
|
filters: {
|
||||||
item_code: me.item_code,
|
item_code: me.item_code,
|
||||||
warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : ''
|
warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}).then((data) => {
|
}).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]]
|
serial_no_filters['name'] = ["not in", data.message[0]]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -13,3 +13,11 @@ frappe.pages['point-of-sale'].on_page_load = function(wrapper) {
|
|||||||
wrapper.pos = new erpnext.PointOfSale.Controller(wrapper);
|
wrapper.pos = new erpnext.PointOfSale.Controller(wrapper);
|
||||||
window.cur_pos = wrapper.pos;
|
window.cur_pos = wrapper.pos;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
frappe.pages['point-of-sale'].refresh = function(wrapper) {
|
||||||
|
if (document.scannerDetectionData) {
|
||||||
|
onScan.detachFrom(document);
|
||||||
|
wrapper.pos.wrapper.html("");
|
||||||
|
wrapper.pos.check_opening_entry();
|
||||||
|
}
|
||||||
|
}
|
@ -11,12 +11,14 @@ from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availabil
|
|||||||
from six import string_types
|
from six import string_types
|
||||||
|
|
||||||
@frappe.whitelist()
|
@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()
|
data = dict()
|
||||||
warehouse = ""
|
result = []
|
||||||
|
warehouse, show_only_available_items = "", False
|
||||||
|
|
||||||
if pos_profile:
|
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
|
||||||
warehouse = frappe.db.get_value('POS Profile', pos_profile, ['warehouse'])
|
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):
|
if not frappe.db.exists('Item Group', item_group):
|
||||||
item_group = get_root_of('Item Group')
|
item_group = get_root_of('Item Group')
|
||||||
@ -29,36 +31,47 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p
|
|||||||
batch_no = data.get("batch_no") if data.get("batch_no") else ""
|
batch_no = data.get("batch_no") if data.get("batch_no") else ""
|
||||||
barcode = data.get("barcode") if data.get("barcode") 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:
|
return { 'items': [item_info] }
|
||||||
|
|
||||||
|
condition = get_conditions(item_code, serial_no, batch_no, barcode)
|
||||||
condition += get_item_group_condition(pos_profile)
|
condition += get_item_group_condition(pos_profile)
|
||||||
|
|
||||||
lft, rgt = frappe.db.get_value('Item Group', item_group, ['lft', 'rgt'])
|
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("""
|
items_data = frappe.db.sql("""
|
||||||
SELECT
|
SELECT
|
||||||
name AS item_code,
|
item.name AS item_code,
|
||||||
item_name,
|
item.item_name,
|
||||||
description,
|
item.description,
|
||||||
stock_uom,
|
item.stock_uom,
|
||||||
image AS item_image,
|
item.image AS item_image,
|
||||||
idx AS idx,
|
item.is_stock_item
|
||||||
is_stock_item
|
|
||||||
FROM
|
FROM
|
||||||
`tabItem`
|
`tabItem` item {bin_join_selection}
|
||||||
WHERE
|
WHERE
|
||||||
disabled = 0
|
item.disabled = 0
|
||||||
AND has_variants = 0
|
AND item.has_variants = 0
|
||||||
AND is_sales_item = 1
|
AND item.is_sales_item = 1
|
||||||
AND is_fixed_asset = 0
|
AND item.is_fixed_asset = 0
|
||||||
AND item_group in (SELECT name FROM `tabItem Group` WHERE lft >= {lft} AND rgt <= {rgt})
|
AND item.item_group in (SELECT name FROM `tabItem Group` WHERE lft >= {lft} AND rgt <= {rgt})
|
||||||
AND {condition}
|
AND {condition}
|
||||||
|
{bin_join_condition}
|
||||||
ORDER BY
|
ORDER BY
|
||||||
name asc
|
item.name asc
|
||||||
LIMIT
|
LIMIT
|
||||||
{start}, {page_length}"""
|
{start}, {page_length}"""
|
||||||
.format(
|
.format(
|
||||||
@ -66,8 +79,10 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p
|
|||||||
page_length=page_length,
|
page_length=page_length,
|
||||||
lft=lft,
|
lft=lft,
|
||||||
rgt=rgt,
|
rgt=rgt,
|
||||||
condition=condition
|
condition=condition,
|
||||||
), as_dict=1)
|
bin_join_selection=bin_join_selection,
|
||||||
|
bin_join_condition=bin_join_condition
|
||||||
|
), {'warehouse': warehouse}, as_dict=1)
|
||||||
|
|
||||||
if items_data:
|
if items_data:
|
||||||
items = [d.item_code for d in items_data]
|
items = [d.item_code for d in items_data]
|
||||||
@ -82,11 +97,11 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p
|
|||||||
for item in items_data:
|
for item in items_data:
|
||||||
item_code = item.item_code
|
item_code = item.item_code
|
||||||
item_price = item_prices.get(item_code) or {}
|
item_price = item_prices.get(item_code) or {}
|
||||||
|
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:
|
||||||
item_stock_qty = get_stock_availability(item_code, warehouse)
|
item_stock_qty = get_stock_availability(item_code, warehouse)
|
||||||
|
|
||||||
if not item_stock_qty:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
row = {}
|
row = {}
|
||||||
row.update(item)
|
row.update(item)
|
||||||
row.update({
|
row.update({
|
||||||
@ -100,28 +115,6 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p
|
|||||||
'items': result
|
'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
|
return res
|
||||||
|
|
||||||
@frappe.whitelist()
|
@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):
|
def get_conditions(item_code, serial_no, batch_no, barcode):
|
||||||
if serial_no or batch_no or 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}
|
return """(item.name like {item_code}
|
||||||
or item_name like {item_code})""".format(item_code = frappe.db.escape('%' + item_code + '%'))
|
or item.item_name like {item_code})""".format(item_code = frappe.db.escape('%' + item_code + '%'))
|
||||||
|
|
||||||
def get_item_group_condition(pos_profile):
|
def get_item_group_condition(pos_profile):
|
||||||
cond = "and 1=1"
|
cond = "and 1=1"
|
||||||
item_groups = get_item_groups(pos_profile)
|
item_groups = get_item_groups(pos_profile)
|
||||||
if item_groups:
|
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)
|
return cond % tuple(item_groups)
|
||||||
|
|
||||||
|
@ -20,9 +20,12 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
frappe.require(['assets/erpnext/css/pos.css'], this.check_opening_entry.bind(this));
|
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() {
|
check_opening_entry() {
|
||||||
return frappe.call("erpnext.selling.page.point_of_sale.point_of_sale.check_opening_entry", { "user": frappe.session.user })
|
this.fetch_opening_entry().then((r) => {
|
||||||
.then((r) => {
|
|
||||||
if (r.message.length) {
|
if (r.message.length) {
|
||||||
// assuming only one opening voucher is available for the current user
|
// assuming only one opening voucher is available for the current user
|
||||||
this.prepare_app_defaults(r.message[0]);
|
this.prepare_app_defaults(r.message[0]);
|
||||||
@ -34,13 +37,41 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
|
|
||||||
create_opening_voucher() {
|
create_opening_voucher() {
|
||||||
const table_fields = [
|
const table_fields = [
|
||||||
{ fieldname: "mode_of_payment", fieldtype: "Link", in_list_view: 1, label: "Mode of Payment", options: "Mode of Payment", reqd: 1 },
|
{
|
||||||
{ fieldname: "opening_amount", fieldtype: "Currency", default: 0, in_list_view: 1, label: "Opening Amount",
|
fieldname: "mode_of_payment", fieldtype: "Link",
|
||||||
options: "company:company_currency" }
|
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({
|
const dialog = new frappe.ui.Dialog({
|
||||||
title: __('Create POS Opening Entry'),
|
title: __('Create POS Opening Entry'),
|
||||||
|
static: true,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
fieldtype: 'Link', label: __('Company'), default: frappe.defaults.get_default('company'),
|
fieldtype: 'Link', label: __('Company'), default: frappe.defaults.get_default('company'),
|
||||||
@ -49,20 +80,7 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
{
|
{
|
||||||
fieldtype: 'Link', label: __('POS Profile'),
|
fieldtype: 'Link', label: __('POS Profile'),
|
||||||
options: 'POS Profile', fieldname: 'pos_profile', reqd: 1,
|
options: 'POS Profile', fieldname: 'pos_profile', reqd: 1,
|
||||||
onchange: () => {
|
onchange: () => 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(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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "balance_details",
|
fieldname: "balance_details",
|
||||||
@ -75,25 +93,18 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
fields: table_fields
|
fields: table_fields
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
primary_action: ({ company, pos_profile, balance_details }) => {
|
primary_action: async ({ company, pos_profile, balance_details }) => {
|
||||||
if (!balance_details.length) {
|
if (!balance_details.length) {
|
||||||
frappe.show_alert({
|
frappe.show_alert({
|
||||||
message: __("Please add Mode of payments and opening balance details."),
|
message: __("Please add Mode of payments and opening balance details."),
|
||||||
indicator: 'red'
|
indicator: 'red'
|
||||||
})
|
})
|
||||||
frappe.utils.play_sound("error");
|
return frappe.utils.play_sound("error");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
frappe.dom.freeze();
|
const method = "erpnext.selling.page.point_of_sale.point_of_sale.create_opening_voucher";
|
||||||
return frappe.call("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 });
|
||||||
{ pos_profile, company, balance_details })
|
!res.exc && this.prepare_app_defaults(res.message);
|
||||||
.then((r) => {
|
|
||||||
frappe.dom.unfreeze();
|
|
||||||
dialog.hide();
|
dialog.hide();
|
||||||
if (r.message) {
|
|
||||||
this.prepare_app_defaults(r.message);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
primary_action_label: __('Submit')
|
primary_action_label: __('Submit')
|
||||||
});
|
});
|
||||||
@ -145,8 +156,8 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prepare_dom() {
|
prepare_dom() {
|
||||||
this.wrapper.append(`
|
this.wrapper.append(
|
||||||
<div class="app grid grid-cols-10 pt-8 gap-6"></div>`
|
`<div class="app grid grid-cols-10 pt-8 gap-6"></div>`
|
||||||
);
|
);
|
||||||
|
|
||||||
this.$components_wrapper = this.wrapper.find('.app');
|
this.$components_wrapper = this.wrapper.find('.app');
|
||||||
@ -162,26 +173,25 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prepare_menu() {
|
prepare_menu() {
|
||||||
var me = this;
|
|
||||||
this.page.clear_menu();
|
this.page.clear_menu();
|
||||||
|
|
||||||
this.page.add_menu_item(__("Form View"), function () {
|
this.page.add_menu_item(__("Open Form View"), this.open_form_view.bind(this), false, 'Ctrl+F');
|
||||||
frappe.model.sync(me.frm.doc);
|
|
||||||
frappe.set_route("Form", me.frm.doc.doctype, me.frm.doc.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.page.add_menu_item(__("Toggle Recent Orders"), () => {
|
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), false, 'Ctrl+S');
|
||||||
|
|
||||||
|
this.page.add_menu_item(__('Close the POS'), this.close_pos.bind(this), false, 'Shift+Ctrl+C');
|
||||||
|
}
|
||||||
|
|
||||||
|
open_form_view() {
|
||||||
|
frappe.model.sync(this.frm.doc);
|
||||||
|
frappe.set_route("Form", this.frm.doc.doctype, this.frm.doc.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle_recent_order() {
|
||||||
const show = this.recent_order_list.$component.hasClass('d-none');
|
const show = this.recent_order_list.$component.hasClass('d-none');
|
||||||
this.toggle_recent_order_list(show);
|
this.toggle_recent_order_list(show);
|
||||||
});
|
|
||||||
|
|
||||||
this.page.add_menu_item(__("Save as Draft"), this.save_draft_invoice.bind(this));
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
frappe.ui.keys.on("shift+ctrl+s", this.close_pos.bind(this));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
save_draft_invoice() {
|
save_draft_invoice() {
|
||||||
@ -356,13 +366,12 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
submit_invoice: () => {
|
submit_invoice: () => {
|
||||||
this.frm.savesubmit()
|
this.frm.savesubmit()
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
// this.set_invoice_status();
|
|
||||||
this.toggle_components(false);
|
this.toggle_components(false);
|
||||||
this.order_summary.toggle_component(true);
|
this.order_summary.toggle_component(true);
|
||||||
this.order_summary.load_summary_of(this.frm.doc, true);
|
this.order_summary.load_summary_of(this.frm.doc, true);
|
||||||
frappe.show_alert({
|
frappe.show_alert({
|
||||||
indicator: 'green',
|
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.pos_profile && !this.frm.doc.pos_profile) this.frm.doc.pos_profile = this.pos_profile;
|
||||||
if (!this.frm.doc.company) return;
|
if (!this.frm.doc.company) return;
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return this.frm.trigger("set_pos_data");
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
raise_exception_for_pos_profile() {
|
raise_exception_for_pos_profile() {
|
||||||
@ -529,11 +514,11 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
|
|
||||||
set_invoice_status() {
|
set_invoice_status() {
|
||||||
const [status, indicator] = frappe.listview_settings["POS Invoice"].get_indicator(this.frm.doc);
|
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() {
|
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) {
|
async on_cart_update(args) {
|
||||||
@ -550,8 +535,10 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
|
|
||||||
field === 'qty' && (value = flt(value));
|
field === 'qty' && (value = flt(value));
|
||||||
|
|
||||||
if (field === 'qty' && value > 0 && !this.allow_negative_stock)
|
if (['qty', 'conversion_factor'].includes(field) && value > 0 && !this.allow_negative_stock) {
|
||||||
await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse);
|
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) {
|
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);
|
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 };
|
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;
|
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) {
|
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('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('qty', item_row.doctype, item_row.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async check_stock_availability(item_row, qty_needed, warehouse) {
|
async check_stock_availability(item_row, qty_needed, warehouse) {
|
||||||
const available_qty = (await this.get_available_stock(item_row.item_code, warehouse)).message;
|
const available_qty = (await this.get_available_stock(item_row.item_code, warehouse)).message;
|
||||||
|
|
||||||
frappe.dom.unfreeze();
|
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)) {
|
if (!(available_qty > 0)) {
|
||||||
frappe.model.clear_doc(item_row.doctype, item_row.name);
|
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) {
|
} else if (available_qty < qty_needed) {
|
||||||
frappe.show_alert({
|
frappe.show_alert({
|
||||||
message: __(`Stock quantity not enough for Item Code: ${item_row.item_code.bold()} under warehouse ${warehouse.bold()}.
|
message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.',
|
||||||
Available quantity ${available_qty.toString().bold()}.`),
|
[bold_item_code, bold_warehouse, bold_available_qty]),
|
||||||
indicator: 'orange'
|
indicator: 'orange'
|
||||||
});
|
});
|
||||||
frappe.utils.play_sound("error");
|
frappe.utils.play_sound("error");
|
||||||
this.item_details.qty_control.set_value(flt(available_qty));
|
|
||||||
}
|
}
|
||||||
frappe.dom.freeze();
|
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) {
|
get_available_stock(item_code, warehouse) {
|
||||||
const me = this;
|
const me = this;
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
|
@ -223,6 +223,8 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
attach_shortcuts() {
|
attach_shortcuts() {
|
||||||
for (let row of this.number_pad.keys) {
|
for (let row of this.number_pad.keys) {
|
||||||
for (let btn of row) {
|
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]}`;
|
let shortcut_key = `ctrl+${frappe.scrub(String(btn))[0]}`;
|
||||||
if (btn === 'Delete') shortcut_key = 'ctrl+backspace';
|
if (btn === 'Delete') shortcut_key = 'ctrl+backspace';
|
||||||
if (btn === 'Remove') shortcut_key = 'shift+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] :
|
const fieldname = this.number_pad.fieldnames[btn] ? this.number_pad.fieldnames[btn] :
|
||||||
typeof btn === 'string' ? frappe.scrub(btn) : 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}`, () => {
|
frappe.ui.keys.on(`${shortcut_key}`, () => {
|
||||||
const cart_is_visible = this.$component.is(":visible");
|
const cart_is_visible = this.$component.is(":visible");
|
||||||
if (cart_is_visible && this.item_is_selected && this.$numpad_section.is(":visible")) {
|
if (cart_is_visible && this.item_is_selected && this.$numpad_section.is(":visible")) {
|
||||||
@ -240,12 +246,36 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl';
|
||||||
frappe.ui.keys.on("ctrl+enter", () => {
|
this.$component.find(".checkout-btn").attr("title", `${ctrl_label}+Enter`);
|
||||||
const cart_is_visible = this.$component.is(":visible");
|
frappe.ui.keys.add_shortcut({
|
||||||
const payment_section_hidden = this.$totals_section.find('.edit-cart-btn').hasClass('d-none');
|
shortcut: "ctrl+enter",
|
||||||
if (cart_is_visible && payment_section_hidden) {
|
action: () => this.$component.find(".checkout-btn").click(),
|
||||||
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() {
|
show_discount_control() {
|
||||||
this.$add_discount_elem.removeClass("pr-4 pl-4");
|
this.$add_discount_elem.removeClass("pr-4 pl-4");
|
||||||
this.$add_discount_elem.html(
|
this.$add_discount_elem.html(
|
||||||
`<div class="add-dicount-field flex flex-1 items-center"></div>
|
`<div class="add-discount-field flex flex-1 items-center"></div>`
|
||||||
<div class="submit-field flex items-center"></div>`
|
|
||||||
);
|
);
|
||||||
const me = this;
|
const me = this;
|
||||||
|
|
||||||
@ -354,14 +383,18 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
fieldtype: 'Data',
|
fieldtype: 'Data',
|
||||||
placeholder: __('Enter discount percentage.'),
|
placeholder: __('Enter discount percentage.'),
|
||||||
onchange: function() {
|
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));
|
frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', flt(this.value));
|
||||||
me.hide_discount_control(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,
|
render_input: true,
|
||||||
});
|
});
|
||||||
this.discount_field.toggle_label(false);
|
this.discount_field.toggle_label(false);
|
||||||
@ -369,6 +402,12 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hide_discount_control(discount) {
|
hide_discount_control(discount) {
|
||||||
|
if (!discount) {
|
||||||
|
this.$add_discount_elem.removeClass("pr-4 pl-4");
|
||||||
|
this.$add_discount_elem.html(
|
||||||
|
`<div class="add-discount-field flex flex-1 items-center"></div>`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
this.$add_discount_elem.addClass('pr-4 pl-4');
|
this.$add_discount_elem.addClass('pr-4 pl-4');
|
||||||
this.$add_discount_elem.html(
|
this.$add_discount_elem.html(
|
||||||
`<svg class="mr-2" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"
|
`<svg class="mr-2" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"
|
||||||
@ -381,6 +420,7 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
`
|
`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
update_customer_section() {
|
update_customer_section() {
|
||||||
const { customer, email_id='', mobile_no='', image } = this.customer_info || {};
|
const { customer, email_id='', mobile_no='', image } = this.customer_info || {};
|
||||||
@ -475,19 +515,20 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
const currency = this.events.get_frm().doc.currency;
|
const currency = this.events.get_frm().doc.currency;
|
||||||
this.$totals_section.find('.taxes').html(
|
this.$totals_section.find('.taxes').html(
|
||||||
`<div class="flex items-center justify-between h-16 pr-8 pl-8 border-b-grey">
|
`<div class="flex items-center justify-between h-16 pr-8 pl-8 border-b-grey">
|
||||||
<div class="flex">
|
<div class="flex overflow-hidden whitespace-nowrap">
|
||||||
<div class="text-md text-dark-grey text-bold w-fit">Tax Charges</div>
|
<div class="text-md text-dark-grey text-bold w-fit">Tax Charges</div>
|
||||||
<div class="flex ml-6 text-dark-grey">
|
<div class="flex ml-4 text-dark-grey">
|
||||||
${
|
${
|
||||||
taxes.map((t, i) => {
|
taxes.map((t, i) => {
|
||||||
let margin_left = '';
|
let margin_left = '';
|
||||||
if (i !== 0) margin_left = 'ml-2';
|
if (i !== 0) margin_left = 'ml-2';
|
||||||
return `<span class="border-grey p-1 pl-2 pr-2 rounded ${margin_left}">${t.description}</span>`
|
const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`;
|
||||||
|
return `<span class="border-grey p-1 pl-2 pr-2 rounded ${margin_left}">${description}</span>`
|
||||||
}).join('')
|
}).join('')
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col text-right">
|
<div class="flex flex-col text-right f-shrink-0 ml-4">
|
||||||
<div class="text-md text-dark-grey text-bold">${format_currency(value, currency)}</div>
|
<div class="text-md text-dark-grey text-bold">${format_currency(value, currency)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>`
|
||||||
|
@ -177,7 +177,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get_form_fields(item) {
|
get_form_fields(item) {
|
||||||
const fields = ['qty', 'uom', 'rate', 'price_list_rate', 'discount_percentage', 'warehouse', 'actual_qty'];
|
const fields = ['qty', 'uom', 'rate', 'conversion_factor', 'discount_percentage', 'warehouse', 'actual_qty', 'price_list_rate'];
|
||||||
if (item.has_serial_no) fields.push('serial_no');
|
if (item.has_serial_no) fields.push('serial_no');
|
||||||
if (item.has_batch_no) fields.push('batch_no');
|
if (item.has_batch_no) fields.push('batch_no');
|
||||||
return fields;
|
return fields;
|
||||||
@ -208,7 +208,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
|||||||
const me = this;
|
const me = this;
|
||||||
if (this.rate_control) {
|
if (this.rate_control) {
|
||||||
this.rate_control.df.onchange = function() {
|
this.rate_control.df.onchange = function() {
|
||||||
if (this.value) {
|
if (this.value || flt(this.value) === 0) {
|
||||||
me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => {
|
me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => {
|
||||||
const item_row = frappe.get_doc(me.doctype, me.name);
|
const item_row = frappe.get_doc(me.doctype, me.name);
|
||||||
const doc = me.events.get_frm().doc;
|
const doc = me.events.get_frm().doc;
|
||||||
@ -234,26 +234,24 @@ erpnext.PointOfSale.ItemDetails = class {
|
|||||||
})
|
})
|
||||||
} else if (available_qty === 0) {
|
} else if (available_qty === 0) {
|
||||||
me.warehouse_control.set_value('');
|
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);
|
me.actual_qty_control.set_value(available_qty);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.warehouse_control.df.get_query = () => {
|
||||||
|
return {
|
||||||
|
filters: { company: this.events.get_frm().doc.company }
|
||||||
|
}
|
||||||
|
};
|
||||||
this.warehouse_control.refresh();
|
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.serial_no_control) {
|
if (this.serial_no_control) {
|
||||||
this.serial_no_control.df.reqd = 1;
|
this.serial_no_control.df.reqd = 1;
|
||||||
this.serial_no_control.df.onchange = async function() {
|
this.serial_no_control.df.onchange = async function() {
|
||||||
@ -270,7 +268,8 @@ erpnext.PointOfSale.ItemDetails = class {
|
|||||||
query: 'erpnext.controllers.queries.get_batch_no',
|
query: 'erpnext.controllers.queries.get_batch_no',
|
||||||
filters: {
|
filters: {
|
||||||
item_code: me.item_row.item_code,
|
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.set_value_in_current_cart_item('uom', this.value);
|
||||||
me.events.form_updated(me.doctype, me.name, 'uom', this.value);
|
me.events.form_updated(me.doctype, me.name, 'uom', this.value);
|
||||||
me.current_item.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() {
|
async auto_update_batch_no() {
|
||||||
@ -337,6 +348,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
attach_shortcuts() {
|
attach_shortcuts() {
|
||||||
|
this.wrapper.find('.close-btn').attr("title", "Esc");
|
||||||
frappe.ui.keys.on("escape", () => {
|
frappe.ui.keys.on("escape", () => {
|
||||||
const item_details_visible = this.$component.is(":visible");
|
const item_details_visible = this.$component.is(":visible");
|
||||||
if (item_details_visible) {
|
if (item_details_visible) {
|
||||||
@ -358,8 +370,10 @@ erpnext.PointOfSale.ItemDetails = class {
|
|||||||
|
|
||||||
bind_auto_serial_fetch_event() {
|
bind_auto_serial_fetch_event() {
|
||||||
this.$form_container.on('click', '.auto-fetch-btn', () => {
|
this.$form_container.on('click', '.auto-fetch-btn', () => {
|
||||||
this.batch_no_control.set_value('');
|
this.batch_no_control && this.batch_no_control.set_value('');
|
||||||
let qty = this.qty_control.get_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({
|
let numbers = frappe.call({
|
||||||
method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number",
|
method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number",
|
||||||
args: {
|
args: {
|
||||||
@ -367,6 +381,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
|||||||
item_code: this.current_item.item_code,
|
item_code: this.current_item.item_code,
|
||||||
warehouse: this.warehouse_control.get_value() || '',
|
warehouse: this.warehouse_control.get_value() || '',
|
||||||
batch_nos: this.current_item.batch_no || '',
|
batch_nos: this.current_item.batch_no || '',
|
||||||
|
posting_date: expiry_date,
|
||||||
for_doctype: 'POS Invoice'
|
for_doctype: 'POS Invoice'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -376,10 +391,14 @@ erpnext.PointOfSale.ItemDetails = class {
|
|||||||
let records_length = auto_fetched_serial_numbers.length;
|
let records_length = auto_fetched_serial_numbers.length;
|
||||||
if (!records_length) {
|
if (!records_length) {
|
||||||
const warehouse = this.warehouse_control.get_value().bold();
|
const warehouse = this.warehouse_control.get_value().bold();
|
||||||
frappe.msgprint(__(`Serial numbers unavailable for Item ${this.current_item.item_code.bold()}
|
const item_code = this.current_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.', [item_code, warehouse])
|
||||||
|
);
|
||||||
} else if (records_length < qty) {
|
} 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);
|
this.qty_control.set_value(records_length);
|
||||||
}
|
}
|
||||||
numbers = auto_fetched_serial_numbers.join(`\n`);
|
numbers = auto_fetched_serial_numbers.join(`\n`);
|
||||||
|
@ -76,7 +76,7 @@ erpnext.PointOfSale.ItemSelector = class {
|
|||||||
|
|
||||||
get_item_html(item) {
|
get_item_html(item) {
|
||||||
const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom } = item;
|
const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom } = item;
|
||||||
const indicator_color = actual_qty > 10 ? "green" : actual_qty !== 0 ? "orange" : "red";
|
const indicator_color = actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange";
|
||||||
|
|
||||||
function get_item_image_html() {
|
function get_item_image_html() {
|
||||||
if (item_image) {
|
if (item_image) {
|
||||||
@ -184,15 +184,24 @@ erpnext.PointOfSale.ItemSelector = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
attach_shortcuts() {
|
attach_shortcuts() {
|
||||||
frappe.ui.keys.on("ctrl+i", () => {
|
const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl';
|
||||||
const selector_is_visible = this.$component.is(':visible');
|
this.search_field.parent.attr("title", `${ctrl_label}+I`);
|
||||||
if (!selector_is_visible) return;
|
frappe.ui.keys.add_shortcut({
|
||||||
this.search_field.set_focus();
|
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", () => {
|
this.item_group_field.parent.attr("title", `${ctrl_label}+G`);
|
||||||
const selector_is_visible = this.$component.is(':visible');
|
frappe.ui.keys.add_shortcut({
|
||||||
if (!selector_is_visible) return;
|
shortcut: "ctrl+g",
|
||||||
this.item_group_field.set_focus();
|
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
|
// for selecting the last filtered item on search
|
||||||
frappe.ui.keys.on("enter", () => {
|
frappe.ui.keys.on("enter", () => {
|
||||||
|
@ -26,7 +26,7 @@ erpnext.PointOfSale.PastOrderList = class {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>`
|
</section>`
|
||||||
)
|
);
|
||||||
|
|
||||||
this.$component = this.wrapper.find('.past-order-list');
|
this.$component = this.wrapper.find('.past-order-list');
|
||||||
this.$invoices_container = this.$component.find('.invoices-container');
|
this.$invoices_container = this.$component.find('.invoices-container');
|
||||||
@ -45,7 +45,7 @@ erpnext.PointOfSale.PastOrderList = class {
|
|||||||
const invoice_name = unescape($(this).attr('data-invoice-name'));
|
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() {
|
make_filter_section() {
|
||||||
@ -74,13 +74,11 @@ erpnext.PointOfSale.PastOrderList = class {
|
|||||||
});
|
});
|
||||||
this.search_field.toggle_label(false);
|
this.search_field.toggle_label(false);
|
||||||
this.status_field.toggle_label(false);
|
this.status_field.toggle_label(false);
|
||||||
this.status_field.set_value('Paid');
|
this.status_field.set_value('Draft');
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle_component(show) {
|
toggle_component(show) {
|
||||||
show ?
|
show ? this.$component.removeClass('d-none') && this.refresh_list() : this.$component.addClass('d-none');
|
||||||
this.$component.removeClass('d-none') && this.refresh_list() :
|
|
||||||
this.$component.addClass('d-none');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh_list() {
|
refresh_list() {
|
||||||
@ -125,6 +123,6 @@ erpnext.PointOfSale.PastOrderList = class {
|
|||||||
<div class="f-shrink-0 text-grey ml-4">${posting_datetime}</div>
|
<div class="f-shrink-0 text-grey ml-4">${posting_datetime}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>`
|
||||||
)
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
@ -25,7 +25,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
<div class="summary-container absolute flex flex-col pt-16 pb-16 pr-8 pl-8 w-full h-full"></div>
|
<div class="summary-container absolute flex flex-col pt-16 pb-16 pr-8 pl-8 w-full h-full"></div>
|
||||||
</div>
|
</div>
|
||||||
</section>`
|
</section>`
|
||||||
)
|
);
|
||||||
|
|
||||||
this.$component = this.wrapper.find('.past-order-summary');
|
this.$component = this.wrapper.find('.past-order-summary');
|
||||||
this.$summary_wrapper = this.$component.find('.summary-wrapper');
|
this.$summary_wrapper = this.$component.find('.summary-wrapper');
|
||||||
@ -55,7 +55,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
<div class="text-grey mb-4 sticky bg-white">ITEMS</div>
|
<div class="text-grey mb-4 sticky bg-white">ITEMS</div>
|
||||||
<div class="items-summary-container border rounded flex flex-col w-full"></div>
|
<div class="items-summary-container border rounded flex flex-col w-full"></div>
|
||||||
</div>`
|
</div>`
|
||||||
)
|
);
|
||||||
|
|
||||||
this.$items_summary_container = this.$summary_container.find('.items-summary-container');
|
this.$items_summary_container = this.$summary_container.find('.items-summary-container');
|
||||||
}
|
}
|
||||||
@ -66,7 +66,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
<div class="text-grey mb-4">TOTALS</div>
|
<div class="text-grey mb-4">TOTALS</div>
|
||||||
<div class="summary-totals-container border rounded flex flex-col w-full"></div>
|
<div class="summary-totals-container border rounded flex flex-col w-full"></div>
|
||||||
</div>`
|
</div>`
|
||||||
)
|
);
|
||||||
|
|
||||||
this.$totals_summary_container = this.$summary_container.find('.summary-totals-container');
|
this.$totals_summary_container = this.$summary_container.find('.summary-totals-container');
|
||||||
}
|
}
|
||||||
@ -77,7 +77,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
<div class="text-grey mb-4">PAYMENTS</div>
|
<div class="text-grey mb-4">PAYMENTS</div>
|
||||||
<div class="payments-summary-container border rounded flex flex-col w-full mb-4"></div>
|
<div class="payments-summary-container border rounded flex flex-col w-full mb-4"></div>
|
||||||
</div>`
|
</div>`
|
||||||
)
|
);
|
||||||
|
|
||||||
this.$payment_summary_container = this.$summary_container.find('.payments-summary-container');
|
this.$payment_summary_container = this.$summary_container.find('.payments-summary-container');
|
||||||
}
|
}
|
||||||
@ -85,7 +85,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
init_summary_buttons() {
|
init_summary_buttons() {
|
||||||
this.$summary_container.append(
|
this.$summary_container.append(
|
||||||
`<div class="summary-btns flex summary-btns justify-between w-full f-shrink-0"></div>`
|
`<div class="summary-btns flex summary-btns justify-between w-full f-shrink-0"></div>`
|
||||||
)
|
);
|
||||||
|
|
||||||
this.$summary_btns = this.$summary_container.find('.summary-btns');
|
this.$summary_btns = this.$summary_container.find('.summary-btns');
|
||||||
}
|
}
|
||||||
@ -138,7 +138,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
<div class="text-grey mr-4">${doc.name}</div>
|
<div class="text-grey mr-4">${doc.name}</div>
|
||||||
<div class="text-grey text-bold indicator ${indicator_color}">${doc.status}</div>
|
<div class="text-grey text-bold indicator ${indicator_color}">${doc.status}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get_discount_html(doc) {
|
get_discount_html(doc) {
|
||||||
@ -169,27 +169,28 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
<div class="flex flex-col f-shrink-0 ml-auto text-right">
|
<div class="flex flex-col f-shrink-0 ml-auto text-right">
|
||||||
<div class="text-md-0 text-dark-grey text-bold">${format_currency(doc.net_total, doc.currency)}</div>
|
<div class="text-md-0 text-dark-grey text-bold">${format_currency(doc.net_total, doc.currency)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get_taxes_html(doc) {
|
get_taxes_html(doc) {
|
||||||
return `<div class="total-summary-wrapper flex items-center justify-between h-12 pr-4 pl-4 border-b-grey">
|
const taxes = doc.taxes.map((t, i) => {
|
||||||
<div class="flex">
|
|
||||||
<div class="text-md-0 text-dark-grey text-bold w-fit">Tax Charges</div>
|
|
||||||
<div class="flex ml-6 text-dark-grey">
|
|
||||||
${
|
|
||||||
doc.taxes.map((t, i) => {
|
|
||||||
let margin_left = '';
|
let margin_left = '';
|
||||||
if (i !== 0) margin_left = 'ml-2';
|
if (i !== 0) margin_left = 'ml-2';
|
||||||
return `<span class="pl-2 pr-2 ${margin_left}">${t.description} @${t.rate}%</span>`
|
return `<span class="pl-2 pr-2 ${margin_left}">${t.description} @${t.rate}%</span>`;
|
||||||
}).join('')
|
}).join('');
|
||||||
}
|
|
||||||
</div>
|
return `
|
||||||
|
<div class="total-summary-wrapper flex items-center justify-between h-12 pr-4 pl-4 border-b-grey">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="text-md-0 text-dark-grey text-bold w-fit">Tax Charges</div>
|
||||||
|
<div class="flex ml-6 text-dark-grey">${taxes}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col text-right">
|
<div class="flex flex-col text-right">
|
||||||
<div class="text-md-0 text-dark-grey text-bold">${format_currency(doc.base_total_taxes_and_charges, doc.currency)}</div>
|
<div class="text-md-0 text-dark-grey text-bold">
|
||||||
|
${format_currency(doc.base_total_taxes_and_charges, doc.currency)}
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get_grand_total_html(doc) {
|
get_grand_total_html(doc) {
|
||||||
@ -202,7 +203,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
<div class="flex flex-col f-shrink-0 ml-auto text-right">
|
<div class="flex flex-col f-shrink-0 ml-auto text-right">
|
||||||
<div class="text-md-0 text-dark-grey text-bold">${format_currency(doc.grand_total, doc.currency)}</div>
|
<div class="text-md-0 text-dark-grey text-bold">${format_currency(doc.grand_total, doc.currency)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get_item_html(doc, item_data) {
|
get_item_html(doc, item_data) {
|
||||||
@ -218,14 +219,20 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
<div class="flex f-shrink-0 ml-auto text-right">
|
<div class="flex f-shrink-0 ml-auto text-right">
|
||||||
${get_rate_discount_html()}
|
${get_rate_discount_html()}
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>`;
|
||||||
|
|
||||||
function get_rate_discount_html() {
|
function get_rate_discount_html() {
|
||||||
if (item_data.rate && item_data.price_list_rate && item_data.rate !== item_data.price_list_rate) {
|
if (item_data.rate && item_data.price_list_rate && item_data.rate !== item_data.price_list_rate) {
|
||||||
return `<span class="text-grey mr-2">(${item_data.discount_percentage}% off)</span>
|
return `<span class="text-grey mr-2">
|
||||||
<div class="text-md-0 text-dark-grey text-bold">${format_currency(item_data.rate, doc.currency)}</div>`
|
(${item_data.discount_percentage}% off)
|
||||||
|
</span>
|
||||||
|
<div class="text-md-0 text-dark-grey text-bold">
|
||||||
|
${format_currency(item_data.rate, doc.currency)}
|
||||||
|
</div>`;
|
||||||
} else {
|
} else {
|
||||||
return `<div class="text-md-0 text-dark-grey text-bold">${format_currency(item_data.price_list_rate || item_data.rate, doc.currency)}</div>`
|
return `<div class="text-md-0 text-dark-grey text-bold">
|
||||||
|
${format_currency(item_data.price_list_rate || item_data.rate, doc.currency)}
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -240,7 +247,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
<div class="flex flex-col f-shrink-0 ml-auto text-right">
|
<div class="flex flex-col f-shrink-0 ml-auto text-right">
|
||||||
<div class="text-md-0 text-dark-grey text-bold">${format_currency(payment.amount, doc.currency)}</div>
|
<div class="text-md-0 text-dark-grey text-bold">${format_currency(payment.amount, doc.currency)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
bind_events() {
|
bind_events() {
|
||||||
@ -271,7 +278,6 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.$summary_container.on('click', '.print-btn', () => {
|
this.$summary_container.on('click', '.print-btn', () => {
|
||||||
// this.print_dialog.show();
|
|
||||||
const frm = this.events.get_frm();
|
const frm = this.events.get_frm();
|
||||||
frm.doc = this.doc;
|
frm.doc = this.doc;
|
||||||
frm.print_preview.lang_code = frm.doc.language;
|
frm.print_preview.lang_code = frm.doc.language;
|
||||||
@ -280,19 +286,34 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
attach_shortcuts() {
|
attach_shortcuts() {
|
||||||
frappe.ui.keys.on("ctrl+p", () => {
|
const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl';
|
||||||
const print_btn_visible = this.$summary_container.find('.print-btn').is(":visible");
|
this.$summary_container.find('.print-btn').attr("title", `${ctrl_label}+P`);
|
||||||
const summary_visible = this.$component.is(":visible");
|
frappe.ui.keys.add_shortcut({
|
||||||
if (!summary_visible || !print_btn_visible) return;
|
shortcut: "ctrl+p",
|
||||||
|
action: () => this.$summary_container.find('.print-btn').click(),
|
||||||
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) {
|
toggle_component(show) {
|
||||||
show ?
|
show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
|
||||||
this.$component.removeClass('d-none') :
|
|
||||||
this.$component.addClass('d-none');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
send_email() {
|
send_email() {
|
||||||
@ -343,7 +364,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
`<div class="${class_name}-btn border rounded h-14 flex flex-1 items-center mr-4 justify-center text-md text-bold no-select pointer">
|
`<div class="${class_name}-btn border rounded h-14 flex flex-1 items-center mr-4 justify-center text-md text-bold no-select pointer">
|
||||||
${b}
|
${b}
|
||||||
</div>`
|
</div>`
|
||||||
)
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -452,5 +473,4 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
this.$totals_summary_container.append(taxes_dom);
|
this.$totals_summary_container.append(taxes_dom);
|
||||||
this.$totals_summary_container.append(grand_total_dom);
|
this.$totals_summary_container.append(grand_total_dom);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
|
@ -170,7 +170,8 @@ erpnext.PointOfSale.Payment = class {
|
|||||||
me.selected_mode = me[`${mode}_control`];
|
me.selected_mode = me[`${mode}_control`];
|
||||||
const doc = me.events.get_frm().doc;
|
const doc = me.events.get_frm().doc;
|
||||||
me.selected_mode?.$input?.get(0).focus();
|
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);
|
me.selected_mode.set_value(value);
|
||||||
})
|
})
|
||||||
|
|
||||||
// this.$totals_remarks.on('click', '.remarks', () => {
|
|
||||||
// this.toggle_remarks_control();
|
|
||||||
// })
|
|
||||||
|
|
||||||
this.$component.on('click', '.submit-order', () => {
|
this.$component.on('click', '.submit-order', () => {
|
||||||
const doc = this.events.get_frm().doc;
|
const doc = this.events.get_frm().doc;
|
||||||
const paid_amount = doc.paid_amount;
|
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) => {
|
frappe.ui.form.on("Sales Invoice Payment", "amount", (frm, cdt, cdn) => {
|
||||||
// for setting correct amount after loyalty points are redeemed
|
// for setting correct amount after loyalty points are redeemed
|
||||||
const default_mop = locals[cdt][cdn];
|
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) {
|
if (this[`${mode}_control`] && this[`${mode}_control`].get_value() != default_mop.amount) {
|
||||||
this[`${mode}_control`].set_value(default_mop.amount);
|
this[`${mode}_control`].set_value(default_mop.amount);
|
||||||
}
|
}
|
||||||
@ -254,6 +251,8 @@ erpnext.PointOfSale.Payment = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
attach_shortcuts() {
|
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", () => {
|
frappe.ui.keys.on("ctrl+enter", () => {
|
||||||
const payment_is_visible = this.$component.is(":visible");
|
const payment_is_visible = this.$component.is(":visible");
|
||||||
const active_mode = this.$payment_modes.find(".border-primary");
|
const active_mode = this.$payment_modes.find(".border-primary");
|
||||||
@ -262,14 +261,16 @@ erpnext.PointOfSale.Payment = class {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
frappe.ui.keys.on("tab", () => {
|
frappe.ui.keys.add_shortcut({
|
||||||
|
shortcut: "tab",
|
||||||
|
action: () => {
|
||||||
const payment_is_visible = this.$component.is(":visible");
|
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");
|
let active_mode = this.$payment_modes.find(".border-primary");
|
||||||
active_mode = active_mode.length ? active_mode.attr("data-mode") : undefined;
|
active_mode = active_mode.length ? active_mode.attr("data-mode") : undefined;
|
||||||
|
|
||||||
if (!active_mode) return;
|
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 mode_index = mode_of_payments.indexOf(active_mode);
|
||||||
const next_mode_index = (mode_index + 1) % mode_of_payments.length;
|
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]}"]`);
|
const next_mode_to_be_clicked = this.$payment_modes.find(`.mode-of-payment[data-mode="${mode_of_payments[next_mode_index]}"]`);
|
||||||
@ -277,6 +278,11 @@ erpnext.PointOfSale.Payment = class {
|
|||||||
if (payment_is_visible && mode_index != next_mode_index) {
|
if (payment_is_visible && mode_index != next_mode_index) {
|
||||||
next_mode_to_be_clicked.click();
|
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(
|
this.$payment_modes.html(
|
||||||
`${
|
`${
|
||||||
payments.map((p, i) => {
|
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 payment_type = p.type;
|
||||||
const margin = i % 2 === 0 ? 'pr-2' : 'pl-2';
|
const margin = i % 2 === 0 ? 'pr-2' : 'pl-2';
|
||||||
const amount = p.amount > 0 ? format_currency(p.amount, currency) : '';
|
const amount = p.amount > 0 ? format_currency(p.amount, currency) : '';
|
||||||
@ -356,13 +362,13 @@ erpnext.PointOfSale.Payment = class {
|
|||||||
)
|
)
|
||||||
|
|
||||||
payments.forEach(p => {
|
payments.forEach(p => {
|
||||||
const mode = p.mode_of_payment.replace(' ', '_').toLowerCase();
|
const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase();
|
||||||
const me = this;
|
const me = this;
|
||||||
this[`${mode}_control`] = frappe.ui.form.make_control({
|
this[`${mode}_control`] = frappe.ui.form.make_control({
|
||||||
df: {
|
df: {
|
||||||
label: __(`${p.mode_of_payment}`),
|
label: p.mode_of_payment,
|
||||||
fieldtype: 'Currency',
|
fieldtype: 'Currency',
|
||||||
placeholder: __(`Enter ${p.mode_of_payment} amount.`),
|
placeholder: __('Enter {0} amount.', [p.mode_of_payment]),
|
||||||
onchange: function() {
|
onchange: function() {
|
||||||
if (this.value || this.value == 0) {
|
if (this.value || this.value == 0) {
|
||||||
frappe.model.set_value(p.doctype, p.name, 'amount', flt(this.value))
|
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;
|
let description, read_only, max_redeemable_amount;
|
||||||
if (!loyalty_points) {
|
if (!loyalty_points) {
|
||||||
description = __(`You don't have enough points to redeem.`);
|
description = __("You don't have enough points to redeem.");
|
||||||
read_only = true;
|
read_only = true;
|
||||||
} else {
|
} else {
|
||||||
max_redeemable_amount = flt(flt(loyalty_points) * flt(conversion_factor), precision("loyalty_amount", doc))
|
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;
|
read_only = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -464,9 +470,9 @@ erpnext.PointOfSale.Payment = class {
|
|||||||
|
|
||||||
this['loyalty-amount_control'] = frappe.ui.form.make_control({
|
this['loyalty-amount_control'] = frappe.ui.form.make_control({
|
||||||
df: {
|
df: {
|
||||||
label: __('Redeem Loyalty Points'),
|
label: __("Redeem Loyalty Points"),
|
||||||
fieldtype: 'Currency',
|
fieldtype: 'Currency',
|
||||||
placeholder: __(`Enter amount to be redeemed.`),
|
placeholder: __("Enter amount to be redeemed."),
|
||||||
options: 'company:currency',
|
options: 'company:currency',
|
||||||
read_only,
|
read_only,
|
||||||
onchange: async function() {
|
onchange: async function() {
|
||||||
@ -474,7 +480,7 @@ erpnext.PointOfSale.Payment = class {
|
|||||||
|
|
||||||
if (this.value > max_redeemable_amount) {
|
if (this.value > max_redeemable_amount) {
|
||||||
frappe.show_alert({
|
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"
|
indicator: "red"
|
||||||
});
|
});
|
||||||
frappe.utils.play_sound("submit");
|
frappe.utils.play_sound("submit");
|
||||||
|
@ -12,6 +12,7 @@ from erpnext.stock.get_item_details import get_reserved_qty_for_so
|
|||||||
from frappe import _, ValidationError
|
from frappe import _, ValidationError
|
||||||
|
|
||||||
from erpnext.controllers.stock_controller import StockController
|
from erpnext.controllers.stock_controller import StockController
|
||||||
|
from six import string_types
|
||||||
from six.moves import map
|
from six.moves import map
|
||||||
class SerialNoCannotCreateDirectError(ValidationError): pass
|
class SerialNoCannotCreateDirectError(ValidationError): pass
|
||||||
class SerialNoCannotCannotChangeError(ValidationError): pass
|
class SerialNoCannotCannotChangeError(ValidationError): pass
|
||||||
@ -285,8 +286,10 @@ def validate_serial_no(sle, item_det):
|
|||||||
if sle.voucher_type == "Sales Invoice":
|
if sle.voucher_type == "Sales Invoice":
|
||||||
if not frappe.db.exists("Sales Invoice Item", {"parent": sle.voucher_no,
|
if not frappe.db.exists("Sales Invoice Item", {"parent": sle.voucher_no,
|
||||||
"item_code": sle.item_code, "sales_order": sr.sales_order}):
|
"item_code": sle.item_code, "sales_order": sr.sales_order}):
|
||||||
frappe.throw(_("Cannot deliver Serial No {0} of item {1} as it is reserved \
|
frappe.throw(
|
||||||
to fullfill Sales Order {2}").format(sr.name, sle.item_code, sr.sales_order))
|
_("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":
|
elif sle.voucher_type == "Delivery Note":
|
||||||
if not frappe.db.exists("Delivery Note Item", {"parent": sle.voucher_no,
|
if not frappe.db.exists("Delivery Note Item", {"parent": sle.voucher_no,
|
||||||
"item_code": sle.item_code, "against_sales_order": sr.sales_order}):
|
"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",
|
if not invoice or frappe.db.exists("Sales Invoice Item",
|
||||||
{"parent": invoice, "item_code": sle.item_code,
|
{"parent": invoice, "item_code": sle.item_code,
|
||||||
"sales_order": sr.sales_order}):
|
"sales_order": sr.sales_order}):
|
||||||
frappe.throw(_("Cannot deliver Serial No {0} of item {1} as it is reserved to \
|
frappe.throw(
|
||||||
fullfill Sales Order {2}").format(sr.name, sle.item_code, sr.sales_order))
|
_("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 Sales Order reference in Delivery Note or Invoice validate SO reservations for item
|
||||||
if sle.voucher_type == "Sales Invoice":
|
if sle.voucher_type == "Sales Invoice":
|
||||||
sales_order = frappe.db.get_value("Sales Invoice Item", {"parent": sle.voucher_no,
|
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:
|
else:
|
||||||
sle_doc.skip_serial_no_validaiton = True
|
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:
|
if not sr.sales_order or sr.sales_order!= sales_order:
|
||||||
frappe.throw(_("""Sales Order {0} has reservation for item {1}, you can
|
msg = _("Sales Order {0} has reservation for item {1}")
|
||||||
only deliver reserved {1} against {0}. Serial No {2} cannot
|
msg += _(", you can only deliver reserved {1} against {0}.")
|
||||||
be delivered""").format(sales_order, sr.item_code, sr.name))
|
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):
|
def has_duplicate_serial_no(sn, sle):
|
||||||
if (sn.warehouse and not sle.skip_serial_no_validaiton
|
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
|
return serial_nos
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def auto_fetch_serial_number(qty, item_code, warehouse, batch_nos=None, for_doctype=None):
|
def auto_fetch_serial_number(qty, item_code, warehouse, posting_date=None, batch_nos=None, for_doctype=None):
|
||||||
filters = {
|
filters = { "item_code": item_code, "warehouse": warehouse }
|
||||||
"item_code": item_code,
|
|
||||||
"warehouse": warehouse,
|
|
||||||
"delivery_document_no": "",
|
|
||||||
"sales_invoice": ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if batch_nos:
|
if batch_nos:
|
||||||
try:
|
try:
|
||||||
filters["batch_no"] = ["in", json.loads(batch_nos)]
|
filters["batch_no"] = json.loads(batch_nos)
|
||||||
except:
|
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':
|
if for_doctype == 'POS Invoice':
|
||||||
reserved_serial_nos, unreserved_serial_nos = get_pos_reserved_serial_nos(filters, qty)
|
reserved_sr_nos = get_pos_reserved_serial_nos(filters)
|
||||||
return unreserved_serial_nos
|
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 [d.get('name') for d in serial_numbers]
|
||||||
return [item['name'] for item in serial_numbers]
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_pos_reserved_serial_nos(filters, qty=None):
|
def get_pos_reserved_serial_nos(filters):
|
||||||
batch_no_cond = ""
|
if isinstance(filters, string_types):
|
||||||
if filters.get("batch_no"):
|
filters = json.loads(filters)
|
||||||
batch_no_cond = "and item.batch_no = {}".format(frappe.db.escape(filters.get('batch_no')))
|
|
||||||
|
|
||||||
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
|
from `tabPOS Invoice` p, `tabPOS Invoice Item` item
|
||||||
where p.name = item.parent
|
where p.name = item.parent
|
||||||
and p.consolidated_invoice is NULL
|
and p.consolidated_invoice is NULL
|
||||||
and p.docstatus = 1
|
and p.docstatus = 1
|
||||||
and item.docstatus = 1
|
and item.docstatus = 1
|
||||||
and item.item_code = %s
|
and item.item_code = %(item_code)s
|
||||||
and item.warehouse = %s
|
and item.warehouse = %(warehouse)s
|
||||||
{}
|
and item.serial_no is NOT NULL and item.serial_no != ''
|
||||||
""".format(batch_no_cond), [filters.get('item_code'), filters.get('warehouse')], as_dict=1)]
|
""", filters, as_dict=1)
|
||||||
|
|
||||||
reserved_serial_nos = []
|
reserved_sr_nos = []
|
||||||
for s in reserved_serial_nos_str:
|
for d in pos_transacted_sr_nos:
|
||||||
if not s: continue
|
reserved_sr_nos += get_serial_nos(d.serial_no)
|
||||||
|
|
||||||
serial_nos = s.split("\n")
|
return reserved_sr_nos
|
||||||
serial_nos = ' '.join(serial_nos).split() # remove whitespaces
|
|
||||||
if len(serial_nos): reserved_serial_nos += serial_nos
|
|
||||||
|
|
||||||
filters["name"] = ["not in", reserved_serial_nos]
|
def fetch_serial_numbers(filters, qty, do_not_include=[]):
|
||||||
serial_numbers = frappe.get_list("Serial No", filters=filters, limit=qty, order_by="creation")
|
batch_join_selection = ""
|
||||||
unreserved_serial_nos = [item['name'] for item in serial_numbers]
|
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
|
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
|
Loading…
x
Reference in New Issue
Block a user