Added serial no, batch no, item group functionality

This commit is contained in:
Rohit Waghchaure 2017-08-28 17:19:28 +05:30
parent 3ae9e91bcd
commit ba3f0e6b70
11 changed files with 269 additions and 66 deletions

View File

@ -822,7 +822,7 @@
"columns": 0,
"fieldname": "apply_discount",
"fieldtype": "Check",
"hidden": 0,
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
@ -836,7 +836,7 @@
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
@ -851,7 +851,7 @@
"collapsible": 0,
"columns": 0,
"default": "Grand Total",
"depends_on": "apply_discount",
"depends_on": "",
"fieldname": "apply_discount_on",
"fieldtype": "Select",
"hidden": 0,
@ -1291,7 +1291,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-07-28 03:40:03.253088",
"modified": "2017-08-27 16:39:00.713225",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",

View File

@ -0,0 +1,8 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('POS Settings', {
refresh: function(frm) {
}
});

View File

@ -0,0 +1,93 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2017-08-28 16:46:41.732676",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "type_of_pos",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Type of POS",
"length": 0,
"no_copy": 0,
"options": "Online\nOffline",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2017-08-28 16:46:41.732676",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Settings",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View File

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class POSSettings(Document):
def validate(self):
link = 'point-of-sale' if self.type_of_pos == 'Online' else 'pos'
desktop_icon = frappe.db.get_value('Desktop Icon', {'module_name': 'POS'}, 'name')
if desktop_icon:
doc = frappe.get_doc('Desktop Icon', desktop_icon)
doc.link = link
doc.save()

View File

@ -0,0 +1,23 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: POS Settings", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new POS Settings
() => frappe.tests.make('POS Settings', [
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);
});

View File

@ -303,7 +303,7 @@ class SalesInvoice(SellingController):
for fieldname in ('territory', 'naming_series', 'currency', 'taxes_and_charges', 'letter_head', 'tc_name',
'selling_price_list', 'company', 'select_print_heading', 'cash_bank_account',
'write_off_account', 'write_off_cost_center'):
'write_off_account', 'write_off_cost_center', 'apply_discount_on'):
if (not for_validate) or (for_validate and not self.get(fieldname)):
self.set(fieldname, pos.get(fieldname))

View File

@ -1,15 +1,16 @@
erpnext.SerialNoBatchSelector = Class.extend({
init: function(opts) {
init: function(opts, show_dialog) {
$.extend(this, opts);
this.show_dialog = show_dialog;
// frm, item, warehouse_details, has_batch, oldest
let d = this.item;
// Don't show dialog if batch no or serial no already set
if(d && d.has_batch_no && !d.batch_no) {
if(d && d.has_batch_no && (!d.batch_no || this.show_dialog)) {
this.has_batch = 1;
this.setup();
} else if(d && d.has_serial_no && !d.serial_no) {
} else if(d && d.has_serial_no && (!d.serial_no || this.show_dialog)) {
this.has_batch = 0;
this.setup();
}
@ -93,6 +94,11 @@ erpnext.SerialNoBatchSelector = Class.extend({
}
});
if(this.show_dialog) {
let d = this.item;
this.dialog.set_value('serial_no', d.serial_no);
}
this.dialog.show();
},
@ -140,6 +146,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
this.map_row_values(this.item, this.values, 'serial_no', 'qty');
}
refresh_field("items");
this.callback && this.callback(this.item)
},
map_row_values: function(row, values, number, qty_field, warehouse) {

View File

@ -120,6 +120,7 @@ class PointOfSale {
if(!this.frm.doc.customer) {
frappe.throw(__('Please select a customer'));
}
this.update_item_in_cart(item_code, 'qty', '+1');
this.cart && this.cart.unselect_all();
},
@ -131,53 +132,76 @@ class PointOfSale {
}
update_item_in_cart(item_code, field='qty', value=1) {
if(this.cart.exists(item_code)) {
const item = this.frm.doc.items.find(i => i.item_code === item_code);
if (typeof value === 'string') {
if (typeof value === 'string' && !in_list(['serial_no', 'batch_no'], field)) {
// value can be of type '+1' or '-1'
value = item[field] + flt(value);
}
if (field === 'serial_no') {
value = item.serial_no + '\n' + value;
if(field === 'serial_no') {
value = item.serial_no + '\n'+ value;
}
this.update_item_in_frm(item, field, value)
.then(() => {
// update cart
this.cart.add_item(item);
})
.then(() => {
this.cart.update_taxes_and_totals();
this.cart.update_grand_total();
});
// if (barcode) {
// const value = barcode['serial_no'] ?
// item.serial_no + '\n' + barcode['serial_no'] : barcode['batch_no'];
// frappe.model.set_value(item.doctype, item.name,
// Object.keys(barcode)[0], value);
// } else {
// }
if(field === 'qty' && (item.serial_no || item.batch_no)) {
this.select_batch_and_serial_no(item)
} else {
this.update_item_in_frm(item, field, value)
.then(() => {
// update cart
this.update_cart_data(item);
})
}
return;
}
let args = { item_code: item_code };
if (in_list(['serial_no', 'batch_no'], field)) {
args[field] = value;
}
// add to cur_frm
const item = this.frm.add_child('items', { item_code: item_code });
const item = this.frm.add_child('items', args);
this.frm.script_manager
.trigger('item_code', item.doctype, item.name)
.then(() => {
// update cart
this.cart.add_item(item);
this.cart.update_taxes_and_totals();
this.cart.update_grand_total();
this.update_cart_data(item)
});
}
select_batch_and_serial_no(item) {
let dialog = new erpnext.SerialNoBatchSelector({
frm: this.frm,
item: item,
warehouse_details: {
type: "Warehouse",
name: item.warehouse
},
callback: (item) => {
this.update_item_in_frm(item)
.then(() => {
// update cart
this.update_cart_data(item);
})
}
}, true)
}
update_cart_data(item) {
this.cart.add_item(item);
this.cart.update_taxes_and_totals();
this.cart.update_grand_total();
}
update_item_in_frm(item, field, value) {
return frappe.model.set_value(item.doctype, item.name, field, value)
if (field) {
frappe.model.set_value(item.doctype, item.name, field, value)
}
return this.frm.script_manager
.trigger('qty', item.doctype, item.name)
.then(() => {
if (field === 'qty' && value === 0) {
frappe.model.clear_doc(item.doctype, item.name);
@ -307,7 +331,7 @@ class PointOfSale {
this.make_new_invoice();
});
this.page.add_menu_item(__("Email"), function () {
this.page.add_menu_item(__("Email"), () => {
this.frm.email_doc();
});
}
@ -398,11 +422,11 @@ class POSCart {
<div class="list-item__content list-item__content--flex-2 text-muted">${__('Discount')}</div>
<div class="list-item__content discount-inputs">
<input type="text"
class="form-control discount-percentage text-right"
class="form-control additional_discount_percentage text-right"
placeholder="% 0.00"
>
<input type="text"
class="form-control discount-amount text-right"
class="form-control discount_amount text-right"
placeholder="${get_currency_symbol(this.frm.doc.currency)} 0.00"
>
</div>
@ -561,7 +585,7 @@ class POSCart {
if(item.qty > 0) {
$item.find('.quantity input').val(item.qty);
$item.find('.discount').text(item.discount_percentage);
$item.find('.rate').text(item.rate);
$item.find('.rate').text(format_currency(item.rate, this.frm.doc.currency));
} else {
$item.remove();
}
@ -671,6 +695,28 @@ class POSCart {
// me.$cart_items.find('.list-item').removeClass('current-item qty disc rate');
// me.selected_item = null;
// });
this.wrapper.find('.additional_discount_percentage').on('change', (e) => {
frappe.model.set_value(this.frm.doctype, this.frm.docname,
'additional_discount_percentage', e.target.value)
.then(() => {
let discount_wrapper = this.wrapper.find('.discount_amount')
discount_wrapper.val(this.frm.doc.discount_amount)
discount_wrapper.trigger('change')
})
})
this.wrapper.find('.discount_amount').on('change', (e) => {
frappe.model.set_value(this.frm.doctype, this.frm.docname,
'discount_amount', e.target.value)
this.frm.trigger('discount_amount')
.then(() => {
let discount_wrapper = this.wrapper.find('.additional_discount_percentage');
discount_wrapper.val(this.frm.doc.additional_discount_percentage);
this.update_taxes_and_totals()
this.update_grand_total()
})
})
}
set_selected_item($item) {
@ -749,21 +795,19 @@ class POSItems {
this.search_field.$input.on('input', (e) => {
const search_term = e.target.value;
this.filter_items(search_term);
this.filter_items({ search_term });
});
// Item group field
this.item_group_field = frappe.ui.form.make_control({
df: {
fieldtype: 'Select',
fieldtype: 'Link',
label: 'Item Group',
options: [
'All Item Groups',
'Raw Materials',
'Finished Goods'
],
default: 'All Item Groups'
options: 'Item Group',
default: 'All Item Groups',
onchange: () => {
console.log("in the item_group")
this.filter_items({ item_group: this.item_group_field.get_value() })
},
},
parent: this.wrapper.find('.item-group-field'),
render_input: true
@ -805,21 +849,24 @@ class POSItems {
this.clusterize.update(row_items);
}
filter_items(search_term) {
search_term = search_term.toLowerCase();
filter_items({ search_term='', item_group='All Item Groups' }={}) {
if (search_term) {
search_term = search_term.toLowerCase();
// memoize
this.search_index = this.search_index || {};
if (this.search_index[search_term]) {
const items = this.search_index[search_term];
this.render_items(items);
return;
// memoize
this.search_index = this.search_index || {};
if (this.search_index[search_term]) {
const items = this.search_index[search_term];
this.render_items(items);
return;
}
}
this.get_items({search_value: search_term})
this.get_items({search_value: search_term, item_group })
.then((items) => {
this.search_index[search_term] = items;
if (search_term) {
this.search_index[search_term] = items;
}
this.render_items(items);
if(this.serial_no) {
@ -884,7 +931,7 @@ class POSItems {
return template;
}
get_items({start = 0, page_length = 40, search_value=''}={}) {
get_items({start = 0, page_length = 40, search_value='', item_group="All Item Groups"}={}) {
return new Promise(res => {
frappe.call({
method: "erpnext.selling.page.point_of_sale.point_of_sale.get_items",
@ -892,7 +939,8 @@ class POSItems {
start,
page_length,
'price_list': this.pos_profile.selling_price_list,
search_value,
item_group,
search_value
}
}).then(r => {
const { items, serial_no } = r.message;

View File

@ -12,7 +12,7 @@ from erpnext.accounts.party import get_party_account_currency
from erpnext.controllers.accounts_controller import get_taxes_and_charges
@frappe.whitelist()
def get_items(start, page_length, price_list, search_value=""):
def get_items(start, page_length, price_list, item_group, search_value=""):
condition = ""
serial_no = ""
item_code = search_value
@ -23,6 +23,7 @@ def get_items(start, page_length, price_list, search_value=""):
if serial_no_data:
serial_no, item_code = serial_no_data
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
res = frappe.db.sql("""select i.name as item_code, i.item_name, i.image as item_image,
item_det.price_list_rate, item_det.currency
@ -33,9 +34,10 @@ def get_items(start, page_length, price_list, search_value=""):
(item_det.item_code=i.name or item_det.item_code=i.variant_of)
where
i.disabled = 0 and i.has_variants = 0
and i.item_group in (select name from `tabItem Group` where lft >= {lft} and rgt <= {rgt})
and (i.item_code like %(item_code)s
or i.item_name like %(item_code)s)
limit {start}, {page_length}""".format(start=start, page_length=page_length),
limit {start}, {page_length}""".format(start=start, page_length=page_length, lft=lft, rgt=rgt),
{
'item_code': '%%%s%%'%(frappe.db.escape(item_code)),
'price_list': price_list

View File

@ -79,7 +79,7 @@ def get_item_details(args):
and out.warehouse and out.stock_qty > 0:
if out.has_serial_no:
out.serial_no = get_serial_no(out)
out.serial_no = get_serial_no(out, args.serial_no)
if out.has_batch_no and not args.get("batch_no"):
out.batch_no = get_batch_no(out.item_code, out.warehouse, out.qty)
@ -554,7 +554,8 @@ def get_gross_profit(out):
return out
@frappe.whitelist()
def get_serial_no(args):
def get_serial_no(args, serial_nos=None):
serial_no = None
if isinstance(args, basestring):
args = json.loads(args)
args = frappe._dict(args)
@ -568,4 +569,9 @@ def get_serial_no(args):
args = json.dumps({"item_code": args.get('item_code'),"warehouse": args.get('warehouse'),"stock_qty": args.get('stock_qty')})
args = process_args(args)
serial_no = get_serial_nos_by_fifo(args)
return serial_no
if not serial_no and serial_nos:
# For POS
serial_no = serial_nos
return serial_no