[MERGE] point_of_sale barcode scanning (manual merge)

bzr revid: al@openerp.com-20120403130528-8awtj91yoa64gi61
This commit is contained in:
Antony Lesuisse 2012-04-03 15:05:28 +02:00
parent cb4c1cd677
commit 05ffbd28a1
5 changed files with 247 additions and 169 deletions

View File

@ -44,4 +44,5 @@ class res_currency_account(osv.osv):
res_currency_account()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

File diff suppressed because one or more lines are too long

View File

@ -2,22 +2,6 @@ openerp.point_of_sale = function(db) {
db.point_of_sale = {};
var __extends = function(child, parent) {
var __hasProp = Object.prototype.hasOwnProperty;
for (var key in parent) {
if (__hasProp.call(parent, key))
child[key] = parent[key];
}
function ctor() {
this.constructor = child;
}
ctor.prototype = parent.prototype;
child.prototype = new ctor;
child.__super__ = parent.prototype;
return child;
};
var QWeb = db.web.qweb;
var qweb_template = function(template) {
return function(ctx) {
@ -85,7 +69,8 @@ openerp.point_of_sale = function(db) {
}, this));
}, this));
$.when(this.fetch('pos.category', ['name', 'parent_id', 'child_id']),
this.fetch('product.product', ['name', 'list_price', 'pos_categ_id', 'taxes_id', 'product_image_small'], [['pos_categ_id', '!=', 'false']]),
this.fetch('product.product', ['name', 'list_price', 'pos_categ_id', 'taxes_id', 'product_image_small', 'ean13', 'id'], [['pos_categ_id', '!=', 'false']]),
this.fetch('product.packaging', ['product_id', 'ean']),
this.fetch('account.bank.statement', ['account_id', 'currency', 'journal_id', 'state', 'name'],
[['state', '=', 'open'], ['user_id', '=', this.session.uid]]),
this.fetch('account.journal', ['auto_cash', 'check_dtls', 'currency', 'name', 'type']),
@ -223,57 +208,28 @@ openerp.point_of_sale = function(db) {
Models
---
*/
var CashRegister = (function() {
__extends(CashRegister, Backbone.Model);
function CashRegister() {
CashRegister.__super__.constructor.apply(this, arguments);
}
return CashRegister;
})();
var CashRegisterCollection = (function() {
__extends(CashRegisterCollection, Backbone.Collection);
function CashRegisterCollection() {
CashRegisterCollection.__super__.constructor.apply(this, arguments);
}
var CashRegister = Backbone.Model.extend({
});
CashRegisterCollection.prototype.model = CashRegister;
return CashRegisterCollection;
})();
var Product = (function() {
__extends(Product, Backbone.Model);
function Product() {
Product.__super__.constructor.apply(this, arguments);
}
var CashRegisterCollection = Backbone.Collection.extend({
model: CashRegister,
});
return Product;
})();
var ProductCollection = (function() {
__extends(ProductCollection, Backbone.Collection);
function ProductCollection() {
ProductCollection.__super__.constructor.apply(this, arguments);
}
var Product = Backbone.Model.extend({
});
ProductCollection.prototype.model = Product;
return ProductCollection;
})();
var Category = (function() {
__extends(Category, Backbone.Model);
function Category() {
Category.__super__.constructor.apply(this, arguments);
}
var ProductCollection = Backbone.Collection.extend({
model: Product,
});
return Category;
})();
var CategoryCollection = (function() {
__extends(CategoryCollection, Backbone.Collection);
function CategoryCollection() {
CategoryCollection.__super__.constructor.apply(this, arguments);
}
var Category = Backbone.Model.extend({
});
var CategoryCollection = Backbone.Collection.extend({
model: Category,
});
CategoryCollection.prototype.model = Category;
return CategoryCollection;
})();
/*
Each Order contains zero or more Orderlines (i.e. the content of the "shopping cart".)
There should only ever be one Orderline per distinct product in an Order.
@ -351,91 +307,73 @@ openerp.point_of_sale = function(db) {
};
},
exportAsJSON: function() {
var result;
result = {
return {
qty: this.get('quantity'),
price_unit: this.get('list_price'),
discount: this.get('discount'),
product_id: this.get('id')
};
return result;
},
});
var OrderlineCollection = Backbone.Collection.extend({
model: Orderline,
});
/*
Every PaymentLine has all the attributes of the corresponding CashRegister.
*/
var Paymentline = (function() {
__extends(Paymentline, Backbone.Model);
function Paymentline() {
Paymentline.__super__.constructor.apply(this, arguments);
}
Paymentline.prototype.defaults = {
amount: 0
};
Paymentline.prototype.getAmount = function() {
// Every PaymentLine has all the attributes of the corresponding CashRegister.
var Paymentline = Backbone.Model.extend({
defaults: {
amount: 0,
},
initialize: function(attributes) {
Backbone.Model.prototype.initialize.apply(this, arguments);
},
getAmount: function(){
return this.get('amount');
};
Paymentline.prototype.exportAsJSON = function() {
var result;
result = {
},
exportAsJSON: function(){
return {
name: db.web.datetime_to_str(new Date()),
statement_id: this.get('id'),
account_id: (this.get('account_id'))[0],
journal_id: (this.get('journal_id'))[0],
amount: this.getAmount()
};
return result;
};
return Paymentline;
})();
var PaymentlineCollection = (function() {
__extends(PaymentlineCollection, Backbone.Collection);
function PaymentlineCollection() {
PaymentlineCollection.__super__.constructor.apply(this, arguments);
}
},
});
PaymentlineCollection.prototype.model = Paymentline;
return PaymentlineCollection;
})();
var Order = (function() {
__extends(Order, Backbone.Model);
function Order() {
Order.__super__.constructor.apply(this, arguments);
}
Order.prototype.defaults = {
var PaymentlineCollection = Backbone.Collection.extend({
model: Paymentline,
});
var Order = Backbone.Model.extend({
defaults:{
validated: false,
step: 'products',
};
Order.prototype.initialize = function() {
this.set({creationDate: new Date});
},
initialize: function(attributes){
Backbone.Model.prototype.initialize.apply(this, arguments);
this.set({
orderLines: new OrderlineCollection
});
this.set({
paymentLines: new PaymentlineCollection
creationDate: new Date,
orderLines: new OrderlineCollection,
paymentLines: new PaymentlineCollection,
name: "Order " + this.generateUniqueId(),
});
this.bind('change:validated', this.validatedChanged);
return this.set({
name: "Order " + this.generateUniqueId()
});
};
Order.prototype.events = {
return this;
},
events: {
'change:validated': 'validatedChanged'
};
Order.prototype.validatedChanged = function() {
},
validatedChanged: function() {
if (this.get("validated") && !this.previous("validated")) {
this.set({'step': 'receipt'});
}
}
Order.prototype.generateUniqueId = function() {
},
generateUniqueId: function() {
return new Date().getTime();
};
Order.prototype.addProduct = function(product) {
},
addProduct: function(product) {
var existing;
existing = (this.get('orderLines')).get(product.id);
if (existing != null) {
@ -447,8 +385,8 @@ openerp.point_of_sale = function(db) {
this.get('orderLines').remove(line);
}, this);
}
};
Order.prototype.addPaymentLine = function(cashRegister) {
},
addPaymentLine: function(cashRegister) {
var newPaymentline;
newPaymentline = new Paymentline(cashRegister);
/* TODO: Should be 0 for cash-like accounts */
@ -456,38 +394,38 @@ openerp.point_of_sale = function(db) {
amount: this.getDueLeft()
});
return (this.get('paymentLines')).add(newPaymentline);
};
Order.prototype.getName = function() {
},
getName: function() {
return this.get('name');
};
Order.prototype.getTotal = function() {
},
getTotal: function() {
return (this.get('orderLines')).reduce((function(sum, orderLine) {
return sum + orderLine.getPriceWithTax();
}), 0);
};
Order.prototype.getTotalTaxExcluded = function() {
},
getTotalTaxExcluded: function() {
return (this.get('orderLines')).reduce((function(sum, orderLine) {
return sum + orderLine.getPriceWithoutTax();
}), 0);
};
Order.prototype.getTax = function() {
},
getTax: function() {
return (this.get('orderLines')).reduce((function(sum, orderLine) {
return sum + orderLine.getTax();
}), 0);
};
Order.prototype.getPaidTotal = function() {
},
getPaidTotal: function() {
return (this.get('paymentLines')).reduce((function(sum, paymentLine) {
return sum + paymentLine.getAmount();
}), 0);
};
Order.prototype.getChange = function() {
},
getChange: function() {
return this.getPaidTotal() - this.getTotal();
};
Order.prototype.getDueLeft = function() {
},
getDueLeft: function() {
return this.getTotal() - this.getPaidTotal();
};
Order.prototype.exportAsJSON = function() {
var orderLines, paymentLines, result;
},
exportAsJSON: function() {
var orderLines, paymentLines;
orderLines = [];
(this.get('orderLines')).each(_.bind( function(item) {
return orderLines.push([0, 0, item.exportAsJSON()]);
@ -496,7 +434,7 @@ openerp.point_of_sale = function(db) {
(this.get('paymentLines')).each(_.bind( function(item) {
return paymentLines.push([0, 0, item.exportAsJSON()]);
}, this));
result = {
return {
name: this.getName(),
amount_paid: this.getPaidTotal(),
amount_total: this.getTotal(),
@ -505,26 +443,15 @@ openerp.point_of_sale = function(db) {
lines: orderLines,
statement_ids: paymentLines
};
return result;
};
return Order;
})();
var OrderCollection = (function() {
__extends(OrderCollection, Backbone.Collection);
function OrderCollection() {
OrderCollection.__super__.constructor.apply(this, arguments);
}
},
});
OrderCollection.prototype.model = Order;
return OrderCollection;
})();
var Shop = (function() {
__extends(Shop, Backbone.Model);
function Shop() {
Shop.__super__.constructor.apply(this, arguments);
}
var OrderCollection = Backbone.Collection.extend({
model: Order,
});
Shop.prototype.initialize = function() {
var Shop = Backbone.Model.extend({
initialize: function() {
this.set({
orders: new OrderCollection(),
products: new ProductCollection()
@ -542,15 +469,15 @@ openerp.point_of_sale = function(db) {
});
}
}, this));
};
Shop.prototype.addAndSelectOrder = function(newOrder) {
},
addAndSelectOrder: function(newOrder) {
(this.get('orders')).add(newOrder);
return this.set({
selectedOrder: newOrder
});
};
return Shop;
})();
},
});
/*
The numpad handles both the choice of the property currently being modified
(quantity, price or discount) and the edition of the corresponding numeric value.
@ -617,6 +544,7 @@ openerp.point_of_sale = function(db) {
}
},
});
/*
---
Views
@ -774,6 +702,7 @@ openerp.point_of_sale = function(db) {
},
on_selected: function() {},
});
var OrderWidget = db.web.OldWidget.extend({
init: function(parent, options) {
this._super(parent);
@ -855,6 +784,7 @@ openerp.point_of_sale = function(db) {
$('#total').html(total.toFixed(2)).hide().fadeIn();
},
});
/*
"Products" step.
*/
@ -893,6 +823,7 @@ openerp.point_of_sale = function(db) {
},
on_change_category: function(id) {},
});
var ProductWidget = db.web.OldWidget.extend({
tag_name:'li',
template_fct: qweb_template('pos-product-template'),
@ -915,6 +846,7 @@ openerp.point_of_sale = function(db) {
return this;
},
});
var ProductListWidget = db.web.OldWidget.extend({
init: function(parent, options) {
this._super(parent);
@ -1066,6 +998,7 @@ openerp.point_of_sale = function(db) {
this.currentPaymentLines.last().set({amount: val});
},
});
var ReceiptWidget = db.web.OldWidget.extend({
init: function(parent, options) {
this._super(parent);
@ -1108,6 +1041,7 @@ openerp.point_of_sale = function(db) {
$('.pos-receipt-container', this.$element).html(qweb_template('pos-ticket')({widget:this}));
},
});
var OrderButtonWidget = db.web.OldWidget.extend({
tag_name: 'li',
template_fct: qweb_template('pos-order-selector-button-template'),
@ -1147,6 +1081,7 @@ openerp.point_of_sale = function(db) {
this.$element.addClass('order-selector-button');
}
});
var ShopWidget = db.web.OldWidget.extend({
init: function(parent, options) {
this._super(parent);
@ -1227,7 +1162,9 @@ openerp.point_of_sale = function(db) {
}
},
});
var App = (function() {
function App($element) {
this.initialize($element);
}
@ -1243,22 +1180,153 @@ openerp.point_of_sale = function(db) {
this.categoryView.on_change_category.add_last(_.bind(this.category, this));
this.category();
};
App.prototype.category = function(id) {
var c, products;
if (id == null) {
id = 0;
}
var c, products, self = this;
id = !id ? 0 : id;
c = pos.categories[id];
this.categoryView.ancestors = c.ancestors;
this.categoryView.children = c.children;
this.categoryView.render_element();
this.categoryView.start();
allProducts = pos.store.get('product.product');
allPackages = pos.store.get('product.packaging');
products = pos.store.get('product.product').filter( function(p) {
var _ref;
return _ref = p.pos_categ_id[0], _.indexOf(c.subtree, _ref) >= 0;
});
(this.shop.get('products')).reset(products);
var self = this;
//returns true if the code is a valid EAN codebar number by checking the control digit.
var checkEan = function(code) {
var st1 = code.slice();
var st2 = st1.slice(0,st1.length-1).reverse();
// some EAN13 barcodes have a length of 12, as they start by 0
while (st2.length < 12) {
st2.push(0);
}
var countSt3 = 1;
var st3 = 0;
$.each(st2, function() {
if (countSt3%2 === 1) {
st3 += this;
}
countSt3 ++;
});
st3 *= 3;
var st4 = 0;
var countSt4 = 1;
$.each(st2, function() {
if (countSt4%2 === 0) {
st4 += this;
}
countSt4 ++;
});
var st5 = st3 + st4;
var cd = (10 - (st5%10)) % 10;
return code[code.length-1] === cd;
}
var codeNumbers = [];
// returns a product that has a packaging with an EAN matching to provided ean string.
// returns undefined if no such product is found.
var getProductByEAN = function(ean) {
var prefix = ean.substring(0,2);
var scannedProductModel = undefined;
if (prefix in {'02':'', '22':'', '24':'', '26':'', '28':''}) {
// PRICE barcode
var itemCode = ean.substring(0,7);
var scannedPackaging = _.detect(allPackages, function(pack) { return pack.ean !== undefined && pack.ean.substring(0,7) === itemCode;});
if (scannedPackaging !== undefined) {
scannedProductModel = _.detect(allProducts, function(pc) { return pc.id === scannedPackaging.product_id[0];});
scannedProductModel.list_price = Number(ean.substring(7,12))/100;
}
} else if (prefix in {'21':'','23':'','27':'','29':'','25':''}) {
// WEIGHT barcode
var weight = Number(barcode.substring(7,12))/1000;
var itemCode = ean.substring(0,7);
var scannedPackaging = _.detect(allPackages, function(pack) { return pack.ean !== undefined && pack.ean.substring(0,7) === itemCode;});
if (scannedPackaging !== undefined) {
scannedProductModel = _.detect(allProducts, function(pc) { return pc.id === scannedPackaging.product_id[0];});
scannedProductModel.list_price *= weight;
scannedProductModel.name += ' - ' + weight + ' Kg.';
}
} else {
// UNIT barcode
scannedProductModel = _.detect(allProducts, function(pc) { return pc.ean13 === ean;}); //TODO DOES NOT SCALE
}
return scannedProductModel;
}
// The barcode readers acts as a keyboard, we catch all keyup events and try to find a
// barcode sequence in the typed keys, then act accordingly.
$('body').delegate('','keyup', function (e){
//We only care about numbers
if (!isNaN(Number(String.fromCharCode(e.keyCode)))) {
// The barcode reader sends keystrokes with a specific interval.
// We look if the typed keys fit in the interval.
if (codeNumbers.length==0) {
timeStamp = new Date().getTime();
} else {
if (lastTimeStamp + 30 < new Date().getTime()) {
// not a barcode reader
codeNumbers = [];
timeStamp = new Date().getTime();
}
}
codeNumbers.push(e.keyCode - 48);
lastTimeStamp = new Date().getTime();
if (codeNumbers.length == 13) {
// a barcode reader
if (!checkEan(codeNumbers)) {
// barcode read error, raise warning
$(QWeb.render('pos-scan-warning')).dialog({
resizable: false,
height:220,
modal: true,
title: "Warning",
buttons: {
"OK": function() {
$( this ).dialog( "close" );
return;
},
}
});
}
var selectedOrder = self.shop.get('selectedOrder');
var scannedProductModel = getProductByEAN(codeNumbers.join(''));
if (scannedProductModel === undefined) {
// product not recognized, raise warning
$(QWeb.render('pos-scan-warning')).dialog({
resizable: false,
height:220,
modal: true,
title: "Warning",
buttons: {
"OK": function() {
$( this ).dialog( "close" );
return;
},
}
});
} else {
selectedOrder.addProduct(new Product(scannedProductModel));
}
codeNumbers = [];
}
} else {
// NaN
codeNumbers = [];
}
});
$('.searchbox input').keyup(function() {
var m, s;
s = $(this).val().toLowerCase();
@ -1274,7 +1342,7 @@ openerp.point_of_sale = function(db) {
return (self.shop.get('products')).reset(m);
});
return $('.search-clear').click( function() {
(this.shop.get('products')).reset(products);
(self.shop.get('products')).reset(products);
$('.searchbox input').val('').focus();
return $('.search-clear').fadeOut();
});
@ -1374,6 +1442,9 @@ openerp.point_of_sale = function(db) {
}, this));
},
close: function() {
// remove barcode reader event listener
$('body').undelegate('', 'keyup')
return new db.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_close_statement']], ['res_id']).pipe(
_.bind(function(res) {
return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) {

View File

@ -141,6 +141,9 @@
</a>
</span>
</t>
<t t-name="pos-scan-warning">
<div>The product could not be recognized. Please contact an employee.</div>
</t>
<t t-name="pos-close-warning">
<div>There are pending operations that could not be saved into the database, are you sure you want to exit?</div>
</t>

View File

@ -517,7 +517,7 @@ class product_product(osv.osv):
'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the product without removing it."),
'variants': fields.char('Variants', size=64),
'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade"),
'ean13': fields.char('EAN13', size=13),
'ean13': fields.char('EAN13', size=13, help='Barcode'),
'packaging' : fields.one2many('product.packaging', 'product_id', 'Logistical Units', help="Gives the different ways to package the same product. This has no impact on the picking order and is mainly used if you use the EDI module."),
'price_extra': fields.float('Variant Price Extra', digits_compute=dp.get_precision('Sale Price')),
'price_margin': fields.float('Variant Price Margin', digits_compute=dp.get_precision('Sale Price')),