[MERGE] pos improvements

manual fix of fp-style merge
This commit is contained in:
Christophe Simonis
2014-08-07 15:17:43 +02:00
33 changed files with 1695 additions and 332 deletions
+2 -1
View File
@@ -394,7 +394,7 @@ class pos_session(osv.osv):
for obj in self.browse(cr, uid, ids, context=context):
for statement in obj.statement_ids:
statement.unlink(context=context)
return True
return super(pos_session, self).unlink(cr, uid, ids, context=context)
def open_cb(self, cr, uid, ids, context=None):
@@ -553,6 +553,7 @@ class pos_order(osv.osv):
orders_to_save = [o for o in orders if o['data']['name'] not in existing_references]
order_ids = []
for tmp_order in orders_to_save:
to_invoice = tmp_order['to_invoice']
order = tmp_order['data']
+27 -7
View File
@@ -679,13 +679,33 @@
<field name="receipt_header" placeholder="A custom receipt header message"/>
<field name="receipt_footer" placeholder="A custom receipt header footage"/>
</group>
<group string="Barcode Types" col="4">
<field name="barcode_product" />
<field name="barcode_cashier" />
<field name="barcode_customer" />
<field name="barcode_weight" />
<field name="barcode_discount" />
<field name="barcode_price" />
<group string="Barcode Types" col="1">
<p>
Barcode Patterns allow to match barcodes to actions or to embed information such as price and quantity in the barcode.
Barcode Patterns only work with EAN13 barcodes.
</p>
<p>
Each type of barcode accepts a list of patterns seprated by commas. A scanned
barcode will be attributed to a type if it matches one of its patterns.
The patterns take the form of EAN13 barcodes. Numbers in the pattern must match
the number in the scanned barcode. A 'x' or a '*' in a pattern will match
any one number. If the patterns are shorter than EAN13 barcodes, they are assumed
to be prefixes and match at the beginning. Weight, Price and Discount patterns also
tell how the weight, price or discount is encoded in the barcode. 'N' indicate the
positions where the integer part is en encoded, and 'D' where the decimals are encoded.
If multiple pattern match one barcode, the longest pattern with the less 'x' or '*' is
considered the matching one. If a barcode matches no pattern it will not be found in
the POS.
</p>
<group col="4">
<field name="barcode_product" />
<field name="barcode_cashier" />
<field name="barcode_customer" />
<field name="barcode_weight" />
<field name="barcode_discount" />
<field name="barcode_price" />
</group>
</group>
</sheet>
@@ -1665,6 +1665,16 @@ td {
font-size: 18px;
margin: 0px 16px;
}
.pos .popup .comment.traceback {
height: 264px;
overflow: auto;
font-size: 14px;
text-align: left;
font-family: 'Inconsolata';
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
}
.pos .popup .footer{
position:absolute;
bottom:0;
+3
View File
@@ -345,6 +345,9 @@ function openerp_pos_db(instance, module){
});
this.save('orders',orders);
},
remove_all_orders: function(){
this.save('orders',[]);
},
get_orders: function(){
return this.load('orders',[]);
},
@@ -19,8 +19,6 @@ openerp.point_of_sale = function(instance) {
openerp_pos_widgets(instance,module); // import pos_widgets.js
openerp_pos_tests(instance,module); // import pos_tests.js
instance.web.client_actions.add('pos.ui', 'instance.point_of_sale.PosWidget');
};
+259 -211
View File
@@ -114,180 +114,250 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
return done;
},
// helper function to load data from the server
// helper function to load data from the server. Obsolete use the models loader below.
fetch: function(model, fields, domain, ctx){
this._load_progress = (this._load_progress || 0) + 0.05;
this.pos_widget.loading_message(_t('Loading')+' '+model,this._load_progress);
return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all()
},
// Server side model loaders. This is the list of the models that need to be loaded from
// the server. The models are loaded one by one by this list's order. The 'loaded' callback
// is used to store the data in the appropriate place once it has been loaded. This callback
// can return a deferred that will pause the loading of the next module.
// a shared temporary dictionary is available for loaders to communicate private variables
// used during loading such as object ids, etc.
models: [
{
model: 'res.users',
fields: ['name','company_id'],
domain: function(self){ return [['id','=',self.session.uid]]; },
loaded: function(self,users){ self.user = users[0]; },
},{
model: 'res.company',
fields: [ 'currency_id', 'email', 'website', 'company_registry', 'vat', 'name', 'phone', 'partner_id' ],
domain: function(self){ return [['id','=',self.user.company_id[0]]]; },
loaded: function(self,companies){ self.company = companies[0]; },
},{
model: 'product.uom',
fields: [],
domain: null,
loaded: function(self,units){
self.units = units;
var units_by_id = {};
for(var i = 0, len = units.length; i < len; i++){
units_by_id[units[i].id] = units[i];
units[i].groupable = ( units[i].category_id[0] === 1 );
units[i].is_unit = ( units[i].id === 1 );
}
self.units_by_id = units_by_id;
}
},{
model: 'res.users',
fields: ['name','ean13'],
domain: null,
loaded: function(self,users){ self.users = users; },
},{
model: 'res.partner',
fields: ['name','street','city','country_id','phone','zip','mobile','email','ean13'],
domain: null,
loaded: function(self,partners){
self.partners = partners;
self.db.add_partners(partners);
},
},{
model: 'account.tax',
fields: ['name','amount', 'price_include', 'type'],
domain: null,
loaded: function(self,taxes){ self.taxes = taxes; },
},{
model: 'pos.session',
fields: ['id', 'journal_ids','name','user_id','config_id','start_at','stop_at','sequence_number'],
domain: function(self){ return [['state','=','opened'],['user_id','=',self.session.uid]]; },
loaded: function(self,pos_sessions){ self.pos_session = pos_sessions[0]; },
},{
model: 'pos.config',
fields: [],
domain: function(self){ return [['id','=', self.pos_session.config_id[0]]]; },
loaded: function(self,configs){
self.config = configs[0];
self.config.use_proxy = self.config.iface_payment_terminal ||
self.config.iface_electronic_scale ||
self.config.iface_print_via_proxy ||
self.config.iface_scan_via_proxy ||
self.config.iface_cashdrawer;
self.barcode_reader.add_barcode_patterns({
'product': self.config.barcode_product,
'cashier': self.config.barcode_cashier,
'client': self.config.barcode_customer,
'weight': self.config.barcode_weight,
'discount': self.config.barcode_discount,
'price': self.config.barcode_price,
});
},
},{
model: 'stock.location',
fields: [],
domain: function(self){ return [['id','=', self.config.stock_location_id[0]]]; },
loaded: function(self, locations){ self.shop = locations[0]; },
},{
model: 'product.pricelist',
fields: ['currency_id'],
domain: function(self){ return [['id','=',self.config.pricelist_id[0]]]; },
loaded: function(self, pricelists){ self.pricelist = pricelists[0]; },
},{
model: 'res.currency',
fields: ['symbol','position','rounding','accuracy'],
domain: function(self){ return [['id','=',self.pricelist.currency_id[0]]]; },
loaded: function(self, currencies){
self.currency = currencies[0];
},
},{
model: 'product.packaging',
fields: ['ean','product_tmpl_id'],
domain: null,
loaded: function(self, packagings){
self.db.add_packagings(packagings);
},
},{
model: 'pos.category',
fields: ['id','name','parent_id','child_id','image'],
domain: null,
loaded: function(self, categories){
self.db.add_categories(categories);
},
},{
model: 'product.product',
fields: ['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13', 'default_code', 'variants',
'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description',
'product_tmpl_id'],
domain: function(self){ return [['sale_ok','=',true],['available_in_pos','=',true]]; },
context: function(self){ return { pricelist: self.pricelist.id }; },
loaded: function(self, products){
self.db.add_products(products);
},
},{
model: 'account.bank.statement',
fields: ['account_id','currency','journal_id','state','name','user_id','pos_session_id'],
domain: function(self){ return [['state', '=', 'open'],['pos_session_id', '=', self.pos_session.id]]; },
loaded: function(self, bankstatements, tmp){
self.bankstatements = bankstatements;
tmp.journals = [];
_.each(bankstatements,function(statement){
tmp.journals.push(statement.journal_id[0]);
});
},
},{
model: 'account.journal',
fields: [],
domain: function(self,tmp){ return [['id','in',tmp.journals]]; },
loaded: function(self, journals){
self.journals = journals;
// associate the bank statements with their journals.
var bankstatements = self.bankstatements;
for(var i = 0, ilen = bankstatements.length; i < ilen; i++){
for(var j = 0, jlen = journals.length; j < jlen; j++){
if(bankstatements[i].journal_id[0] === journals[j].id){
bankstatements[i].journal = journals[j];
bankstatements[i].self_checkout_payment_method = journals[j].self_checkout_payment_method;
}
}
}
self.cashregisters = bankstatements;
},
},{
loaded: function(self){
self.company_logo = new Image();
self.company_logo.crossOrigin = 'anonymous';
var logo_loaded = new $.Deferred();
self.company_logo.onload = function(){
var img = self.company_logo;
var ratio = 1;
var targetwidth = 300;
var maxheight = 150;
if( img.width !== targetwidth ){
ratio = targetwidth / img.width;
}
if( img.height * ratio > maxheight ){
ratio = maxheight / img.height;
}
var width = Math.floor(img.width * ratio);
var height = Math.floor(img.height * ratio);
var c = document.createElement('canvas');
c.width = width;
c.height = height
var ctx = c.getContext('2d');
ctx.drawImage(self.company_logo,0,0, width, height);
self.company_logo_base64 = c.toDataURL();
window.logo64 = self.company_logo_base64;
logo_loaded.resolve();
};
self.company_logo.onerror = function(){
logo_loaded.reject();
};
self.company_logo.src = window.location.origin + '/web/binary/company_logo';
return logo_loaded;
},
},
],
// loads all the needed data on the sever. returns a deferred indicating when all the data has loaded.
load_server_data: function(){
var self = this;
var loaded = new $.Deferred();
var progress = 0;
var progress_step = 1.0 / self.models.length;
var tmp = {}; // this is used to share a temporary state between models loaders
var loaded = self.fetch('res.users',['name','company_id'],[['id','=',this.session.uid]])
.then(function(users){
self.user = users[0];
return self.fetch('res.company',
[
'currency_id',
'email',
'website',
'company_registry',
'vat',
'name',
'phone',
'partner_id',
],
[['id','=',users[0].company_id[0]]],
{show_address_only: true});
}).then(function(companies){
self.company = companies[0];
return self.fetch('product.uom', null, null);
}).then(function(units){
self.units = units;
var units_by_id = {};
for(var i = 0, len = units.length; i < len; i++){
units_by_id[units[i].id] = units[i];
units[i].groupable = ( units[i].category_id[0] === 1 );
units[i].is_unit = ( units[i].id === 1 );
}
self.units_by_id = units_by_id;
function load_model(index){
if(index >= self.models.length){
loaded.resolve();
}else{
var model = self.models[index];
self.pos_widget.loading_message(_t('Loading')+' '+(model.model || ''), progress);
var fields = typeof model.fields === 'function' ? model.fields(self,tmp) : model.fields;
var domain = typeof model.domain === 'function' ? model.domain(self,tmp) : model.domain;
var context = typeof model.context === 'function' ? model.context(self,tmp) : model.context;
progress += progress_step;
return self.fetch('res.users', ['name','ean13'], [['ean13', '!=', false]]);
}).then(function(users){
self.users = users;
return self.fetch('res.partner', ['name','street','city','country_id','phone','zip','mobile','email','ean13']);
}).then(function(partners){
self.partners = partners;
self.db.add_partners(partners);
return self.fetch('account.tax', ['name','amount', 'price_include', 'type']);
}).then(function(taxes){
self.taxes = taxes;
return self.fetch(
'pos.session',
['id', 'journal_ids','name','user_id','config_id','start_at','stop_at','sequence_number'],
[['state', '=', 'opened'], ['user_id', '=', self.session.uid]]
);
}).then(function(pos_sessions){
self.pos_session = pos_sessions[0];
return self.fetch('pos.config',[],[['id','=', self.pos_session.config_id[0]]]);
}).then(function(configs){
self.config = configs[0];
self.config.use_proxy = self.config.iface_payment_terminal ||
self.config.iface_electronic_scale ||
self.config.iface_print_via_proxy ||
self.config.iface_scan_via_proxy ||
self.config.iface_cashdrawer;
self.barcode_reader.add_barcode_patterns({
'product': self.config.barcode_product,
'cashier': self.config.barcode_cashier,
'client': self.config.barcode_customer,
'weight': self.config.barcode_weight,
'discount': self.config.barcode_discount,
'price': self.config.barcode_price,
});
return self.fetch('stock.location',[],[['id','=', self.config.stock_location_id[0]]]);
}).then(function(shops){
self.shop = shops[0];
return self.fetch('product.pricelist',['currency_id'],[['id','=',self.config.pricelist_id[0]]]);
}).then(function(pricelists){
self.pricelist = pricelists[0];
return self.fetch('res.currency',['symbol','position','rounding','accuracy'],[['id','=',self.pricelist.currency_id[0]]]);
}).then(function(currencies){
self.currency = currencies[0];
return self.fetch('product.packaging',['ean','product_tmpl_id']);
}).then(function(packagings){
self.db.add_packagings(packagings);
return self.fetch('pos.category', ['id','name','parent_id','child_id','image']);
}).then(function(categories){
self.db.add_categories(categories);
return self.fetch(
'product.product',
['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13', 'default_code', 'variants',
'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description',
'product_tmpl_id'],
[['sale_ok','=',true],['available_in_pos','=',true]],
{pricelist: self.pricelist.id} // context for price
);
}).then(function(products){
self.db.add_products(products);
return self.fetch(
'account.bank.statement',
['account_id','currency','journal_id','state','name','user_id','pos_session_id'],
[['state','=','open'],['pos_session_id', '=', self.pos_session.id]]
);
}).then(function(bankstatements){
var journals = [];
_.each(bankstatements,function(statement) {
journals.push(statement.journal_id[0]);
});
self.bankstatements = bankstatements;
return self.fetch('account.journal', undefined, [['id','in', journals]]);
}).then(function(journals){
self.journals = journals;
// associate the bank statements with their journals.
var bankstatements = self.bankstatements;
for(var i = 0, ilen = bankstatements.length; i < ilen; i++){
for(var j = 0, jlen = journals.length; j < jlen; j++){
if(bankstatements[i].journal_id[0] === journals[j].id){
bankstatements[i].journal = journals[j];
bankstatements[i].self_checkout_payment_method = journals[j].self_checkout_payment_method;
}
if( model.model ){
new instance.web.Model(model.model).query(fields).filter(domain).context(context).all()
.then(function(result){
try{ // catching exceptions in model.loaded(...)
$.when(model.loaded(self,result,tmp))
.then(function(){ load_model(index + 1); },
function(err){ loaded.reject(err); });
}catch(err){
loaded.reject(err);
}
},function(err){
loaded.reject(err);
});
}else if( model.loaded ){
try{ // catching exceptions in model.loaded(...)
$.when(model.loaded(self,tmp))
.then( function(){ load_model(index +1); },
function(err){ loaded.reject(err); });
}catch(err){
loaded.reject(err);
}
}else{
load_model(index + 1);
}
self.cashregisters = bankstatements;
}
}
// Load the company Logo
try{
load_model(0);
}catch(err){
loaded.reject(err);
}
self.company_logo = new Image();
self.company_logo.crossOrigin = 'anonymous';
var logo_loaded = new $.Deferred();
self.company_logo.onload = function(){
var img = self.company_logo;
var ratio = 1;
var targetwidth = 300;
var maxheight = 150;
if( img.width !== targetwidth ){
ratio = targetwidth / img.width;
}
if( img.height * ratio > maxheight ){
ratio = maxheight / img.height;
}
var width = Math.floor(img.width * ratio);
var height = Math.floor(img.height * ratio);
var c = document.createElement('canvas');
c.width = width;
c.height = height
var ctx = c.getContext('2d');
ctx.drawImage(self.company_logo,0,0, width, height);
self.company_logo_base64 = c.toDataURL();
window.logo64 = self.company_logo_base64;
logo_loaded.resolve();
};
self.company_logo.onerror = function(){
logo_loaded.reject();
};
self.company_logo.src = window.location.origin + '/web/binary/company_logo';
return logo_loaded;
});
return loaded;
},
@@ -324,20 +394,20 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
// it returns a deferred that succeeds after having tried to send the order and all the other pending orders.
push_order: function(order) {
var self = this;
this.proxy.log('push_order',order.export_as_JSON());
var order_id = this.db.add_order(order.export_as_JSON());
if(order){
this.proxy.log('push_order',order.export_as_JSON());
this.db.add_order(order.export_as_JSON());
}
var pushed = new $.Deferred();
this.set('synch',{state:'connecting', pending:self.db.get_orders().length});
this.flush_mutex.exec(function(){
var flushed = self._flush_all_orders();
var flushed = self._flush_orders(self.db.get_orders());
flushed.always(function(){
flushed.always(function(ids){
pushed.resolve();
});
return flushed;
});
return pushed;
},
@@ -361,8 +431,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
var order_id = this.db.add_order(order.export_as_JSON());
this.set('synch',{state:'connecting', pending:self.db.get_orders().length});
this.flush_mutex.exec(function(){
var done = new $.Deferred(); // holds the mutex
@@ -373,7 +441,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
// things will happen as a duplicate will be sent next time
// so we must make sure the server detects and ignores duplicated orders
var transfer = self._flush_order(order_id, {timeout:30000, to_invoice:true});
var transfer = self._flush_orders([self.db.get_order(order_id)], {timeout:30000, to_invoice:true});
transfer.fail(function(){
invoiced.reject('error-transfer');
@@ -382,10 +450,12 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
// on success, get the order id generated by the server
transfer.pipe(function(order_server_id){
// generate the pdf and download it
self.pos_widget.do_action('point_of_sale.pos_invoice_report',{additional_context:{
active_ids:order_server_id,
}});
invoiced.resolve();
done.resolve();
});
@@ -397,62 +467,33 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
return invoiced;
},
// attemps to send all pending orders ( stored in the pos_db ) to the server,
// and remove the successfully sent ones from the db once
// it has been confirmed that they have been sent correctly.
flush: function() {
// wrapper around the _save_to_server that updates the synch status widget
_flush_orders: function(orders, options) {
var self = this;
var flushed = new $.Deferred();
this.flush_mutex.exec(function(){
var done = new $.Deferred();
this.set('synch',{ state: 'connecting', pending: orders.length});
self._flush_all_orders()
.done( function(){ flushed.resolve();})
.fail( function(){ flushed.reject(); })
.always(function(){ done.resolve(); });
return done;
});
return flushed;
},
// attempts to send the locally stored order of id 'order_id'
// the sending is asynchronous and can take some time to decide if it is successful or not
// it is therefore important to only call this method from inside a mutex
// this method returns a deferred indicating wether the sending was successful or not
// there is a timeout parameter which is set to 2 seconds by default.
_flush_order: function( order_id, options) {
return this._flush_all_orders([this.db.get_order(order_id)], options);
},
// attempts to send all the locally stored orders. As with _flush_order, it should only be
// called from within a mutex.
// this method returns a deferred that always succeeds when all orders have been tried to be sent,
// even if none of them could actually be sent.
_flush_all_orders: function () {
var self = this;
self.set('synch', {
state: 'connecting',
pending: self.get('synch').pending
});
return self._save_to_server(self.db.get_orders()).done(function () {
return self._save_to_server(orders, options).done(function (server_ids) {
var pending = self.db.get_orders().length;
self.set('synch', {
state: pending ? 'connecting' : 'connected',
pending: pending
});
return server_ids;
});
},
// send an array of orders to the server
// available options:
// - timeout: timeout for the rpc call in ms
// returns a deferred that resolves with the list of
// server generated ids for the sent orders
_save_to_server: function (orders, options) {
if (!orders || !orders.length) {
var result = $.Deferred();
result.resolve();
result.resolve([]);
return result;
}
@@ -474,11 +515,18 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
shadow: !options.to_invoice,
timeout: timeout
}
).then(function () {
).then(function (server_ids) {
_.each(orders, function (order) {
self.db.remove_order(order.id);
});
}).fail(function (unused, event){
return server_ids;
}).fail(function (error, event){
if(error.code === 200 ){ // Business Logic Error, not a connection problem
self.pos_widget.screen_selector.show_popup('error-traceback',{
message: error.data.message,
comment: error.data.debug
});
}
// prevent an error popup creation by the rpc failure
// we want the failure to be silent as we send the orders in the background
event.preventDefault();
+44 -10
View File
@@ -73,8 +73,9 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
load_saved_screen: function(){
this.close_popup();
var selectedOrder = this.pos.get('selectedOrder');
// this.set_current_screen(selectedOrder.get_screen_data('screen') || this.default_screen,null,'refresh');
this.set_current_screen(this.default_screen,null,'refresh');
// FIXME : this changing screen behaviour is sometimes confusing ...
this.set_current_screen(selectedOrder.get_screen_data('screen') || this.default_screen,null,'refresh');
//this.set_current_screen(this.default_screen,null,'refresh');
},
set_user_mode: function(user_mode){
@@ -348,6 +349,8 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
var self = this;
this._super();
$('body').append('<audio src="/point_of_sale/static/src/sounds/error.wav" autoplay="true"></audio>');
if( text && (text.message || text.comment) ){
this.$('.message').text(text.message);
this.$('.comment').text(text.comment);
@@ -365,6 +368,9 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
},
});
module.ErrorTracebackPopupWidget = module.ErrorPopupWidget.extend({
template:'ErrorTracebackPopupWidget',
});
module.ErrorSessionPopupWidget = module.ErrorPopupWidget.extend({
template:'ErrorSessionPopupWidget',
@@ -375,6 +381,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
show: function(barcode){
this._super();
this.$('.barcode').text(barcode);
},
});
@@ -412,6 +419,18 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
template: 'ErrorInvoiceTransferPopupWidget',
});
module.UnsentOrdersPopupWidget = module.PopUpWidget.extend({
template: 'UnsentOrdersPopupWidget',
show: function(options){
var self = this;
this._super(options);
this.renderElement();
this.$('.button.confirm').click(function(){
self.pos_widget.screen_selector.close_popup();
});
},
});
module.ScaleScreenWidget = module.ScreenWidget.extend({
template:'ScaleScreenWidget',
@@ -557,6 +576,11 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
module.ClientListScreenWidget = module.ScreenWidget.extend({
template: 'ClientListScreenWidget',
init: function(parent, options){
this._super(parent, options);
this.partner_cache = new module.DomCache();
},
show_leftpane: false,
auto_back: true,
@@ -587,7 +611,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
}
this.$('.client-list-contents').delegate('.client-line','click',function(event){
console.log('uh');
self.line_select(event,$(this),parseInt($(this).data('id')));
});
@@ -609,6 +632,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
perform_search: function(query, associate_result){
if(query){
var customers = this.pos.db.search_partner(query);
console.log(customers);
this.display_client_details('hide');
if ( associate_result && customers.length === 1){
this.new_client = customers[0];
@@ -628,14 +652,24 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.$('.searchbox input').focus();
},
render_list: function(partners){
var contents = this.$('.client-list-contents');
contents.empty();
for(var i = 0, len = partners.length; i < len; i++){
var clientline = $(QWeb.render('ClientLine',{partner:partners[i]}));
if( partners[i] === this.new_client ){
clientline.addClass('highlight');
var contents = this.$el[0].querySelector('.client-list-contents');
contents.innerHTML = "";
for(var i = 0, len = Math.min(partners.length,1000); i < len; i++){
var partner = partners[i];
var clientline = this.partner_cache.get_node(partner.id);
if(!clientline){
var clientline_html = QWeb.render('ClientLine',{partner:partners[i]});
var clientline = document.createElement('tbody');
clientline.innerHTML = clientline_html;
clientline = clientline.childNodes[1];
this.partner_cache.cache_node(partner.id,clientline);
}
contents.append(clientline);
if( partners === this.new_client ){
clientline.classList.add('highlight');
}else{
clientline.classList.remove('highlight');
}
contents.appendChild(clientline);
}
},
save_changes: function(){
+86 -90
View File
@@ -1,93 +1,89 @@
function openerp_pos_tests(instance, module){ //module is instance.point_of_sale
(function() {
'use strict';
// Various UI Tests to measure performance and memory leaks.
module.UiTester = function(){
var running = false;
var queue = new module.JobQueue();
openerp.Tour.register({
id: 'pos_basic_order',
name: 'Complete a basic order trough the Front-End',
path: '/web#model=pos.session.opening&action=point_of_sale.action_pos_session_opening',
mode: 'test',
steps: [
{
title: 'Wait fot the bloody screen to be ready',
wait: 200,
},
{
title: 'Load the Session',
waitNot: '.oe_loading:visible',
element: 'span:contains("Resume Session"),span:contains("Start Session")',
},
{
title: 'Loading the Loading Screen',
waitFor: '.loader'
},
{
title: 'Waiting for the end of loading...',
waitFor: '.loader:hidden',
},
{
title: 'Loading The Point of Sale',
waitFor: '.pos',
},
{
title: 'On va manger des CHIPS!',
element: '.product-list .product-name:contains("250g Lays Pickels")',
},
{
title: 'The chips have been added to the Order',
waitFor: '.order .product-name:contains("250g Lays Pickels")',
},
{
title: 'The order total has been updated to the correct value',
wait: 2000,
waitFor: '.order .total .value:contains("1.48 €")',
},
{
title: "Let's buy more chips",
element: '.product-list .product-name:contains("250g Lays Pickels")',
},
{
title: "Let's veryify we pay the correct price for two bags of chips",
waitFor: '.order .total .value:contains("2.96 €")',
},
{
title: "Let's pay with a debit card",
element: ".paypad-button:contains('Bank')",
},
{
title: "Let's accept the payment",
onload: function(){
// The test cannot validate or cancel the print() ... so we replace it by a noop !.
window._print = window.print;
window.print = function(){ console.log('Print!') };
},
element: ".button .iconlabel:contains('Validate'):visible",
},
{
title: "Let's finish the order",
element: ".button:not(.disabled) .iconlabel:contains('Next'):visible",
},
{
onload: function(){
window.print = window._print;
window._print = undefined;
},
title: "Let's wait for the order posting",
waitFor: ".oe_status.js_synch .js_connected:visible",
},
{
title: "Let's close the Point of Sale",
element: ".header-button:contains('Close')",
},
{
title: "Wait for the backend to ready itself",
element: 'span:contains("Resume Session"),span:contains("Start Session")',
},
],
});
// stop the currently running test
this.stop = function(){
queue.clear();
};
})();
// randomly switch product categories
this.category_switch = function(interval){
queue.schedule(function(){
var breadcrumbs = $('.breadcrumb-button');
var categories = $('.category-button');
if(categories.length > 0){
var rnd = Math.floor(Math.random()*categories.length);
categories.eq(rnd).click();
}else{
var rnd = Math.floor(Math.random()*breadcrumbs.length);
breadcrumbs.eq(rnd).click();
}
},{repeat:true, duration:interval});
};
// randomly order products then resets the order
this.order_products = function(interval){
queue.schedule(function(){
var def = new $.Deferred();
var order_queue = new module.JobQueue();
var order_size = 1 + Math.floor(Math.random()*10);
while(order_size--){
order_queue.schedule(function(){
var products = $('.product');
if(products.length > 0){
var rnd = Math.floor(Math.random()*products.length);
products.eq(rnd).click();
}
},{duration:20});
}
order_queue.finished().then(function(){
$('.deleteorder-button').click();
def.resolve();
});
return def;
},{repeat:true, duration: interval});
};
// makes a complete product order cycle ( print via proxy must be activated, and scale deactivated )
this.full_order_cycle = function(interval){
queue.schedule(function(){
var def = new $.Deferred();
var order_queue = new module.JobQueue();
var order_size = 1 + Math.floor(Math.random()*50);
while(order_size--){
order_queue.schedule(function(){
var products = $('.product');
if(products.length > 0){
var rnd = Math.floor(Math.random()*products.length);
products.eq(rnd).click();
}
},{duration:50});
}
order_queue.schedule(function(){
$('.paypad-button:first').click();
},{duration:250});
order_queue.schedule(function(){
$('.paymentline-input:first').val(10000);
$('.paymentline-input:first').keydown();
$('.paymentline-input:first').keyup();
},{duration:250});
order_queue.schedule(function(){
$('.pos-actionbar-button-list .button:eq(2)').click();
},{duration:250});
order_queue.schedule(function(){
def.resolve();
});
return def;
},{repeat: true, duration: interval});
};
};
if(jQuery.deparam(jQuery.param.querystring()).debug !== undefined){
window.pos_test_ui = new module.UiTester();
}
}
+55 -11
View File
@@ -797,6 +797,19 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.$('.button.reference').click(function(){
self.pos.barcode_reader.scan(self.$('input.ean').val());
});
this.$('.button.show_orders').click(function(){
self.pos.pos_widget.screen_selector.show_popup('unsent-orders');
});
this.$('.button.delete_orders').click(function(){
self.pos.pos_widget.screen_selector.show_popup('confirm',{
message: _t('Delete Unsent Orders ?'),
comment: _t('This operation will permanently destroy all unsent orders from the local storage. You will lose all the data. This operation cannot be undone.'),
confirm: function(){
self.pos.db.remove_all_orders();
self.pos.set({synch: { state:'connected', pending: 0 }});
},
});
});
_.each(this.eans, function(ean, name){
self.$('.button.'+name).click(function(){
self.$('input.ean').val(ean);
@@ -840,7 +853,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
self.set_status(synch.state, synch.pending);
});
this.$el.click(function(){
self.pos.flush();
self.pos.push_order();
});
},
});
@@ -1005,19 +1018,42 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').addClass('oe_hidden');});
self.pos.flush();
self.pos.push_order();
}).fail(function(){ // error when loading models data from the backend
return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_session_opening']], ['res_id'])
.pipe( _.bind(function(res){
return instance.session.rpc('/web/action/load', {'action_id': res[0]['res_id']})
.pipe(_.bind(function(result){
var action = result.result;
this.do_action(action);
}, this));
}, self));
}).fail(function(err){ // error when loading models data from the backend
self.loading_error(err);
});
},
loading_error: function(err){
var self = this;
var message = err.message;
var comment = err.stack;
if(err.message === 'XmlHttpRequestError '){
message = 'Network Failure (XmlHttpRequestError)';
comment = 'The Point of Sale could not be loaded due to a network problem.\n Please check your internet connection.';
}else if(err.message === 'OpenERP Server Error'){
message = err.data.message;
comment = err.data.debug;
}
if( typeof comment !== 'string' ){
comment = 'Traceback not available.';
}
var popup = $(QWeb.render('ErrorTracebackPopupWidget',{
widget: { message: message, comment: comment },
}));
popup.find('.button').click(function(){
self.close();
});
popup.css({ zindex: 9001 });
popup.appendTo(this.$el);
},
loading_progress: function(fac){
this.$('.loader .loader-feedback').removeClass('oe_hidden');
this.$('.loader .progress').css({'width': ''+Math.floor(fac*100)+'%'});
@@ -1082,9 +1118,15 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.error_invoice_transfer_popup = new module.ErrorInvoiceTransferPopupWidget(this, {});
this.error_invoice_transfer_popup.appendTo(this.$el);
this.error_traceback_popup = new module.ErrorTracebackPopupWidget(this,{});
this.error_traceback_popup.appendTo(this.$el);
this.confirm_popup = new module.ConfirmPopupWidget(this,{});
this.confirm_popup.appendTo(this.$el);
this.unsent_orders_popup = new module.UnsentOrdersPopupWidget(this,{});
this.unsent_orders_popup.appendTo(this.$el);
// -------- Misc ---------
this.close_button = new module.HeaderButtonWidget(this,{
@@ -1139,7 +1181,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
'choose-receipt': this.choose_receipt_popup,
'error-no-client': this.error_no_client_popup,
'error-invoice-transfer': this.error_invoice_transfer_popup,
'error-traceback': this.error_traceback_popup,
'confirm': this.confirm_popup,
'unsent-orders': this.unsent_orders_popup,
},
default_screen: 'products',
default_mode: 'cashier',
Binary file not shown.
@@ -635,6 +635,20 @@
</div>
</t>
<t t-name="ErrorTracebackPopupWidget">
<div class="modal-dialog">
<div class="popup popup-error">
<p class="message"><t t-esc=" widget.message || 'Error' " /></p>
<p class="comment traceback"><t t-esc=" widget.comment || '' "/></p>
<div class="footer">
<div class="button">
Ok
</div>
</div>
</div>
</div>
</t>
<t t-name="ErrorBarcodePopupWidget">
<div class="modal-dialog">
<div class="popup popup-barcode">
@@ -672,6 +686,29 @@
</div>
</t>
<t t-name="UnsentOrdersPopupWidget">
<div class="modal-dialog">
<div class="popup popup-unsent-orders">
<p class="message">Unsent Orders</p>
<t t-if='widget.pos.db.get_orders().length === 0'>
<p class='comment'>
There are no unsent orders
</p>
</t>
<t t-if='widget.pos.db.get_orders().length > 0'>
<p class='comment traceback'>
<t t-esc='JSON.stringify(widget.pos.db.get_orders(),null,2)' />
</p>
</t>
<div class="footer">
<div class="button confirm">
Ok
</div>
</div>
</div>
</div>
</t>
<t t-name="Product">
<span class='product' t-att-data-product-id="product.id">
<div class="product-img">
@@ -779,6 +816,12 @@
<li class="button reference">Reference</li>
</ul>
<p class="category">Unsent Orders</p>
<ul>
<li class="button show_orders">Show All Unsent Orders</li>
<li class="button delete_orders">Delete All Unsent Orders</li>
</ul>
<p class="category">Hardware Status</p>
<ul>
<li class="status weighting">Weighting</li>
@@ -0,0 +1,9 @@
import openerp.tests
@openerp.tests.common.at_install(False)
@openerp.tests.common.post_install(True)
class TestUi(openerp.tests.HttpCase):
def test_01_pos_basic_order(self):
self.phantom_js("/", "openerp.Tour.run('pos_basic_order', 'test')", "openerp.Tour.tours.pos_basic_order", login="admin")
+25
View File
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import discount
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+47
View File
@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'Point of Sale Discounts',
'version': '1.0',
'category': 'Point of Sale',
'sequence': 6,
'summary': 'Simple Discounts in the Point of Sale ',
'description': """
=======================
This module allows the cashier to quickly give a percentage
sale discount to a customer.
""",
'author': 'OpenERP SA',
'depends': ['point_of_sale'],
'data': [
'views/views.xml',
'views/templates.xml'
],
'installable': True,
'auto_install': False,
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+39
View File
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import logging
import openerp
from openerp import tools
from openerp.osv import fields, osv
from openerp.tools.translate import _
class pos_config(osv.osv):
_inherit = 'pos.config'
_columns = {
'discount_pc': fields.float('Discount Percentage', help='The discount percentage'),
'discount_product_id': fields.many2one('product.product','Discount Product', help='The product used to model the discount'),
}
_defaults = {
'discount_pc': 10,
}
@@ -0,0 +1,34 @@
openerp.pos_discount = function(instance){
var module = instance.point_of_sale;
var round_pr = instance.web.round_precision
var QWeb = instance.web.qweb;
QWeb.add_template('/pos_discount/static/src/xml/discount.xml');
module.PosWidget.include({
build_widgets: function(){
var self = this;
this._super();
if(!this.pos.config.discount_product_id){
return;
}
var discount = $(QWeb.render('DiscountButton'));
discount.click(function(){
var order = self.pos.get('selectedOrder');
var product = self.pos.db.get_product_by_id(self.pos.config.discount_product_id[0]);
var discount = - self.pos.config.discount_pc/ 100.0 * order.getTotalTaxIncluded();
if( discount < 0 ){
order.addProduct(product, { price: discount });
}
});
discount.appendTo(this.$('.control-buttons'));
this.$('.control-buttons').removeClass('oe_hidden');
},
});
};
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="DiscountButton">
<div class='control-button js_discount'>
Discount
</div>
</t>
</templates>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<template id="assets_frontend" inherit_id="web.assets_common">
<xpath expr="." position="inside">
<script type="text/javascript" src="/pos_discount/static/src/js/discount.js"></script>
</xpath>
</template>
</data>
</openerp>
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0"?>
<openerp>
<data>
<record model="ir.ui.view" id="view_pos_config_form">
<field name="name">pos.config.form.view</field>
<field name="model">pos.config</field>
<field name="inherit_id" ref="point_of_sale.view_pos_config_form" />
<field name="arch" type="xml">
<xpath expr="//group[@string='Receipt']" position="after">
<group string="Discounts" col="4" >
<field name='discount_pc' />
<field name="discount_product_id" />
</group>
</xpath>
</field>
</record>
</data>
</openerp>
+25
View File
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import restaurant
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+55
View File
@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'Restaurant',
'version': '1.0',
'category': 'Point of Sale',
'sequence': 6,
'summary': 'Restaurant extensions for the Point of Sale ',
'description': """
=======================
This module adds several restaurant features to the Point of Sale:
- Bill Printing: Allows you to print a receipt before the order is paid
- Bill Splitting: Allows you to split an order into different orders
- Kitchen Order Printing: allows you to print orders updates to kitchen or bar printers
""",
'author': 'OpenERP SA',
'depends': ['point_of_sale'],
'data': [
'restaurant_view.xml',
'security/ir.model.access.csv',
'views/templates.xml',
],
'qweb':[
'static/src/xml/multiprint.xml',
'static/src/xml/splitbill.xml',
'static/src/xml/printbill.xml',
],
'installable': True,
'auto_install': False,
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+55
View File
@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import logging
import openerp
from openerp import tools
from openerp.osv import fields, osv
from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
class restaurant_printer(osv.osv):
_name = 'restaurant.printer'
_columns = {
'name' : fields.char('Printer Name', size=32, required=True, help='An internal identification of the printer'),
'proxy_ip': fields.char('Proxy IP Address', size=32, help="The IP Address or hostname of the Printer's hardware proxy"),
'product_categories_ids': fields.many2many('pos.category','printer_category_rel', 'printer_id','category_id',string='Printed Product Categories'),
}
_defaults = {
'name' : 'Printer',
}
class pos_config(osv.osv):
_inherit = 'pos.config'
_columns = {
'iface_splitbill': fields.boolean('Bill Splitting', help='Enables Bill Splitting in the Point of Sale'),
'iface_printbill': fields.boolean('Bill Printing', help='Allows to print the Bill before payment'),
'printer_ids': fields.many2many('restaurant.printer','pos_config_printer_rel', 'config_id','printer_id',string='Order Printers'),
}
_defaults = {
'iface_splitbill': False,
'iface_printbill': False,
}
+74
View File
@@ -0,0 +1,74 @@
<?xml version="1.0"?>
<openerp>
<data>
<record model="ir.ui.view" id="view_restaurant_printer_form">
<field name="name">Order Printer</field>
<field name="model">restaurant.printer</field>
<field name="arch" type="xml">
<form string="POS Printer" version="7.0">
<group col="2">
<field name="name" />
<field name="proxy_ip" />
<field name="product_categories_ids" />
</group>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="action_restaurant_printer_form">
<field name="name">Order Printers</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">restaurant.printer</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to add a Restaurant Order Printer.
</p><p>
Order Printers are used by restaurants and bars to print the
order updates in the kitchen/bar when the waiter updates the order.
</p><p>
Each Order Printer has an IP Address that defines the PosBox/Hardware
Proxy where the printer can be found, and a list of product categories.
An Order Printer will only print updates for prodcuts belonging to one of
its categories.
</p>
</field>
</record>
<record model="ir.ui.view" id="view_restaurant_printer">
<field name="name">Order Printers</field>
<field name="model">restaurant.printer</field>
<field name="arch" type="xml">
<tree string="Restaurant Order Printers">
<field name="name" />
<field name="proxy_ip" />
<field name="product_categories_ids" />
</tree>
</field>
</record>
<menuitem
parent="point_of_sale.menu_point_config_product"
action="action_restaurant_printer_form"
id="menu_restaurant_printer_all"
sequence="30"
groups="point_of_sale.group_pos_manager"/>
<record model="ir.ui.view" id="view_pos_config_form">
<field name="name">pos.config.form.view.inherit</field>
<field name="model">pos.config</field>
<field name="inherit_id" ref="point_of_sale.view_pos_config_form"></field>
<field name="arch" type="xml">
<sheet position='inside'>
<group string="Bar &amp; Restaurant" >
<field name="iface_splitbill" />
<field name="iface_printbill" />
<field name="printer_ids" />
</group>
</sheet>
</field>
</record>
</data>
</openerp>
@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_restaurant_printer,restaurant.printer.user,model_restaurant_printer,point_of_sale.group_pos_user,1,0,0,0
access_restaurant_printer_manager,restaurant.printer.manager,model_restaurant_printer,point_of_sale.group_pos_manager,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_restaurant_printer restaurant.printer.user model_restaurant_printer point_of_sale.group_pos_user 1 0 0 0
3 access_restaurant_printer_manager restaurant.printer.manager model_restaurant_printer point_of_sale.group_pos_manager 1 0 0 0
@@ -0,0 +1,2 @@
/* --- Restaurant Specific CSS --- */
@@ -0,0 +1,11 @@
openerp.pos_restaurant = function(instance){
var module = instance.point_of_sale;
openerp_restaurant_multiprint(instance,module);
openerp_restaurant_splitbill(instance,module);
openerp_restaurant_printbill(instance,module);
};
@@ -0,0 +1,215 @@
function openerp_restaurant_multiprint(instance,module){
var QWeb = instance.web.qweb;
var _t = instance.web._t;
module.Printer = instance.web.Class.extend(openerp.PropertiesMixin,{
init: function(parent,options){
openerp.PropertiesMixin.init.call(this,parent);
var self = this;
options = options || {};
var url = options.url || 'http://localhost:8069';
this.connection = new instance.web.Session(undefined,url, { use_cors: true});
this.host = url;
this.receipt_queue = [];
},
print: function(receipt){
var self = this;
if(receipt){
this.receipt_queue.push(receipt);
}
var aborted = false;
function send_printing_job(){
if(self.receipt_queue.length > 0){
var r = self.receipt_queue.shift();
self.connection.rpc('/hw_proxy/print_xml_receipt',{receipt: r},{timeout: 5000})
.then(function(){
send_printing_job();
},function(){
self.receipt_queue.unshift(r);
});
}
}
send_printing_job();
},
});
module.PosModel.prototype.models.push({
model: 'restaurant.printer',
fields: ['name','proxy_ip','product_categories_ids'],
domain: null,
loaded: function(self,printers){
var active_printers = {};
for (var i = 0; i < self.config.printer_ids.length; i++) {
active_printers[self.config.printer_ids[i]] = true;
}
self.printers = [];
for(var i = 0; i < printers.length; i++){
if(active_printers[printers[i].id]){
var printer = new module.Printer(self,{url:'http://'+printers[i].proxy_ip+':8069'});
printer.config = printers[i];
self.printers.push(printer);
}
}
},
});
module.Order = module.Order.extend({
lineResume: function(){
var resume = {};
this.get('orderLines').each(function(item){
var line = item.export_as_JSON();
if( typeof resume[line.product_id] === 'undefined'){
resume[line.product_id] = line.qty;
}else{
resume[line.product_id] += line.qty;
}
});
return resume;
},
saveChanges: function(){
this.old_resume = this.lineResume();
},
computeChanges: function(categories){
var current = this.lineResume();
var old = this.old_resume || {};
var json = this.export_as_JSON();
var add = [];
var rem = [];
for( product in current){
if (typeof old[product] === 'undefined'){
add.push({
'id': product,
'name': this.pos.db.get_product_by_id(product).name,
'quantity': current[product],
});
}else if( old[product] < current[product]){
add.push({
'id': product,
'name': this.pos.db.get_product_by_id(product).name,
'quantity': current[product] - old[product],
});
}else if( old[product] > current[product]){
rem.push({
'id': product,
'name': this.pos.db.get_product_by_id(product).name,
'quantity': old[product] - current[product],
});
}
}
for( product in old){
if(typeof current[product] === 'undefined'){
rem.push({
'id': product,
'name': this.pos.db.get_product_by_id(product).name,
'quantity': old[product],
});
}
}
if(categories && categories.length > 0){
// filter the added and removed orders to only contains
// products that belong to one of the categories supplied as a parameter
var self = this;
function product_in_category(product_id){
var cat = self.pos.db.get_product_by_id(product_id).pos_categ_id[0];
while(cat){
for(var i = 0; i < categories.length; i++){
if(cat === categories[i]){
return true;
}
}
cat = self.pos.db.get_category_parent_id(cat);
}
return false;
}
var _add = [];
var _rem = [];
for(var i = 0; i < add.length; i++){
if(product_in_category(add[i].id)){
_add.push(add[i]);
}
}
add = _add;
for(var i = 0; i < rem.length; i++){
if(product_in_category(rem[i].id)){
_rem.push(rem[i]);
}
}
rem = _rem;
}
return {
'new': add,
'cancelled': rem,
'table': json.table || 'unknown table',
'name': json.name || 'unknown order',
};
},
printChanges: function(){
var printers = this.pos.printers;
for(var i = 0; i < printers.length; i++){
var changes = this.computeChanges(printers[i].config.product_categories_ids);
if ( changes['new'].length > 0 || changes['cancelled'].length > 0){
var receipt = QWeb.render('OrderChangeReceipt',{changes:changes, widget:this});
printers[i].print(receipt);
}
}
},
hasChangesToPrint: function(){
var printers = this.pos.printers;
for(var i = 0; i < printers.length; i++){
var changes = this.computeChanges(printers[i].config.product_categories_ids);
if ( changes['new'].length > 0 || changes['cancelled'].length > 0){
return true;
}
}
return false;
},
});
module.PosWidget.include({
build_widgets: function(){
var self = this;
this._super();
if(this.pos.printers.length){
var submitorder = $(QWeb.render('SubmitOrderButton'));
submitorder.click(function(){
var order = self.pos.get('selectedOrder');
if(order.hasChangesToPrint()){
order.printChanges();
order.saveChanges();
self.pos_widget.order_widget.update_summary();
}
});
submitorder.appendTo(this.$('.control-buttons'));
this.$('.control-buttons').removeClass('oe_hidden');
}
},
});
module.OrderWidget.include({
update_summary: function(){
this._super();
var order = this.pos.get('selectedOrder');
if(order.hasChangesToPrint()){
this.pos_widget.$('.order-submit').addClass('highlight');
}else{
this.pos_widget.$('.order-submit').removeClass('highlight');
}
},
});
}
@@ -0,0 +1,28 @@
function openerp_restaurant_printbill(instance,module){
var QWeb = instance.web.qweb;
var _t = instance.web._t;
module.PosWidget.include({
build_widgets: function(){
var self = this;
this._super();
if(this.pos.config.iface_printbill){
var printbill = $(QWeb.render('PrintBillButton'));
printbill.click(function(){
var order = self.pos.get('selectedOrder');
if(order.get('orderLines').models.length > 0){
var receipt = order.export_for_printing();
self.pos.proxy.print_receipt(QWeb.render('BillReceipt',{
receipt: receipt, widget: self,
}));
}
});
printbill.appendTo(this.$('.control-buttons'));
this.$('.control-buttons').removeClass('oe_hidden');
}
},
});
}
@@ -0,0 +1,193 @@
function openerp_restaurant_splitbill(instance, module){
var QWeb = instance.web.qweb;
var _t = instance.web._t;
module.SplitbillScreenWidget = module.ScreenWidget.extend({
template: 'SplitbillScreenWidget',
show_leftpane: false,
previous_screen: 'products',
renderElement: function(){
var self = this;
this._super();
var order = this.pos.get('selectedOrder');
if(!order){
return;
}
var orderlines = order.get('orderLines').models;
for(var i = 0; i < orderlines.length; i++){
var line = orderlines[i];
linewidget = $(QWeb.render('SplitOrderline',{
widget:this,
line:line,
selected: false,
quantity: 0,
id: line.id,
}));
linewidget.data('id',line.id);
this.$('.orderlines').append(linewidget);
}
this.$('.back').click(function(){
self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
});
},
lineselect: function($el,order,neworder,splitlines,line_id){
var split = splitlines[line_id] || {'quantity': 0, line: null};
var line = order.getOrderline(line_id);
if( !line.get_unit().groupable ){
if( split.quantity !== line.get_quantity()){
split.quantity = line.get_quantity();
}else{
split.quantity = 0;
}
}else{
if( split.quantity < line.get_quantity()){
split.quantity += line.get_unit().rounding;
if(split.quantity > line.get_quantity()){
split.quantity = line.get_quantity();
}
}else{
split.quantity = 0;
}
}
if( split.quantity ){
if ( !split.line ){
split.line = line.clone();
neworder.addOrderline(split.line);
}
split.line.set_quantity(split.quantity);
}else if( split.line ) {
neworder.removeOrderline(split.line);
split.line = null;
}
splitlines[line_id] = split;
$el.replaceWith($(QWeb.render('SplitOrderline',{
widget: this,
line: line,
selected: split.quantity !== 0,
quantity: split.quantity,
id: line_id,
})));
this.$('.order-info .subtotal').text(this.format_currency(neworder.getSubtotal()));
},
pay: function($el,order,neworder,splitlines,cashregister_id){
var orderlines = order.get('orderLines').models;
var empty = true;
var full = true;
for(var i = 0; i < orderlines.length; i++){
var id = orderlines[i].id;
var split = splitlines[id];
if(!split){
full = false;
}else{
if(split.quantity){
empty = false;
if(split.quantity !== orderlines[i].get_quantity()){
full = false;
}
}
}
}
if(empty){
return;
}
for(var i = 0; i < this.pos.cashregisters.length; i++){
if(this.pos.cashregisters[i].id === cashregister_id){
var cashregister = this.pos.cashregisters[i];
break;
}
}
if(full){
order.addPaymentline(cashregister);
this.pos_widget.screen_selector.set_current_screen('payment');
}else{
for(var id in splitlines){
var split = splitlines[id];
var line = order.getOrderline(parseInt(id));
line.set_quantity(line.get_quantity() - split.quantity);
if(Math.abs(line.get_quantity()) < 0.00001){
order.removeOrderline(line);
}
delete splitlines[id];
}
neworder.addPaymentline(cashregister);
neworder.set_screen_data('screen','payment');
// for the kitchen printer we assume that everything
// has already been sent to the kitchen before splitting
// the bill. So we save all changes both for the old
// order and for the new one. This is not entirely correct
// but avoids flooding the kitchen with unnecessary orders.
// Not sure what to do in this case.
if ( neworder.saveChanges ) {
order.saveChanges();
neworder.saveChanges();
}
this.pos.get('orders').add(neworder);
this.pos.set('selectedOrder',neworder);
}
},
show: function(){
var self = this;
this._super();
this.renderElement();
var order = this.pos.get('selectedOrder');
var neworder = new module.Order({
pos: this.pos,
temporary: true,
});
neworder.set('client',order.get('client'));
var splitlines = {};
this.$('.orderlines').on('click','.orderline',function(){
var id = parseInt($(this).data('id'));
var $el = $(this);
self.lineselect($el,order,neworder,splitlines,id);
});
this.$('.paymentmethod').click(function(){
var id = parseInt($(this).data('id'));
var $el = $(this);
self.pay($el,order,neworder,splitlines,id);
});
},
});
module.PosWidget.include({
build_widgets: function(){
var self = this;
this._super();
if(this.pos.config.iface_splitbill){
this.splitbill_screen = new module.SplitbillScreenWidget(this,{});
this.splitbill_screen.appendTo(this.$('.screens'));
this.screen_selector.add_screen('splitbill',this.splitbill_screen);
var splitbill = $(QWeb.render('SplitbillButton'));
splitbill.click(function(){
if(self.pos.get('selectedOrder').get('orderLines').models.length > 0){
self.pos_widget.screen_selector.set_current_screen('splitbill');
}
});
splitbill.appendTo(this.$('.control-buttons'));
this.$('.control-buttons').removeClass('oe_hidden');
}
},
});
}
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="SubmitOrderButton">
<span class="control-button order-submit">
<i class="fa fa-cutlery"></i>
Order
</span>
</t>
<t t-name="OrderChangeReceipt">
<receipt
align='center'
width='40'
size='double-height'
line-ratio='0.6'
value-decimals='3'
value-thousands-separator=''
value-autoint='on'
>
<div><t t-esc="changes.name" /></div>
<br />
<br />
<t t-if="changes.cancelled.length > 0">
<div color='red'>
<div bold='on' size='double'>CANCELLED</div>
<br />
<br />
<t t-foreach="changes.cancelled" t-as="change">
<line>
<left><t t-esc="change.name" /></left>
<right><value><t t-esc="change.quantity" /></value></right>
</line>
</t>
<br />
<br />
</div>
</t>
<t t-if="changes.new.length > 0">
<div bold='on' size='double'>NEW</div>
<br />
<br />
<t t-foreach="changes.new" t-as="change">
<line>
<left><t t-esc="change.name" /></left>
<right><value><t t-esc="change.quantity" /></value></right>
</line>
</t>
<br />
<br />
</t>
</receipt>
</t>
</templates>
@@ -0,0 +1,143 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="PrintBillButton">
<span class="control-button order-printbill">
<i class="fa fa-print"></i>
Bill
</span>
</t>
<t t-name="BillReceipt">
<receipt align='center' width='40' value-thousands-separator='' >
<t t-if='receipt.company.logo'>
<img t-att-src='receipt.company.logo' />
<br/>
</t>
<t t-if='!receipt.company.logo'>
<h1><t t-esc='receipt.company.name' /></h1>
<br/>
</t>
<div font='b'>
<t t-if='receipt.shop.name'>
<div><t t-esc='receipt.shop.name' /></div>
</t>
<t t-if='receipt.company.contact_address'>
<div><t t-esc='receipt.company.contact_address' /></div>
</t>
<t t-if='receipt.company.phone'>
<div>Tel:<t t-esc='receipt.company.phone' /></div>
</t>
<t t-if='receipt.company.vat'>
<div>VAT:<t t-esc='receipt.company.vat' /></div>
</t>
<t t-if='receipt.company.email'>
<div><t t-esc='receipt.company.email' /></div>
</t>
<t t-if='receipt.company.website'>
<div><t t-esc='receipt.company.website' /></div>
</t>
<t t-if='receipt.header'>
<div><t t-esc='receipt.header' /></div>
</t>
<t t-if='receipt.cashier'>
<div>--------------------------------</div>
<div>Served by <t t-esc='receipt.cashier' /></div>
</t>
</div>
<br /><br />
<!-- Orderlines -->
<div line-ratio='0.6'>
<t t-foreach='receipt.orderlines' t-as='line'>
<t t-set='simple' t-value='line.discount === 0 and line.unit_name === "Unit(s)" and line.quantity === 1' />
<t t-if='simple'>
<line>
<left><t t-esc='line.product_name' /></left>
<right><value><t t-esc='line.price_display' /></value></right>
</line>
</t>
<t t-if='!simple'>
<line><left><t t-esc='line.product_name' /></left></line>
<t t-if='line.discount !== 0'>
<line indent='1'><left>Discount: <t t-esc='line.discount' />%</left></line>
</t>
<line indent='1'>
<left>
<value value-decimals='3' value-autoint='on'>
<t t-esc='line.quantity' />
</value>
<t t-if='line.unit_name !== "Unit(s)"'>
<t t-esc='line.unit_name' />
</t>
x
<value value-decimals='2'>
<t t-esc='line.price' />
</value>
</left>
<right>
<value><t t-esc='line.price_display' /></value>
</right>
</line>
</t>
</t>
</div>
<!-- Subtotal -->
<t t-set='taxincluded' t-value='Math.abs(receipt.subtotal - receipt.total_with_tax) &lt;= 0.000001' />
<t t-if='!taxincluded'>
<line><right>--------</right></line>
<line><left>Subtotal</left><right> <value><t t-esc="receipt.subtotal" /></value></right></line>
<t t-foreach='receipt.tax_details' t-as='tax'>
<line>
<left><t t-esc='tax.name' /></left>
<right><value><t t-esc='tax.amount' /></value></right>
</line>
</t>
</t>
<!-- Total -->
<line><right>--------</right></line>
<line size='double-height'>
<left><pre> TOTAL</pre></left>
<right><value><t t-esc='receipt.total_with_tax' /></value></right>
</line>
<br/><br/>
<!-- Extra Payment Info -->
<t t-if='receipt.total_discount'>
<line>
<left>Discounts</left>
<right><value><t t-esc='receipt.total_discount'/></value></right>
</line>
</t>
<t t-if='taxincluded'>
<t t-foreach='receipt.tax_details' t-as='tax'>
<line>
<left><t t-esc='tax.name' /></left>
<right><value><t t-esc='tax.amount' /></value></right>
</line>
</t>
</t>
<!-- Footer -->
<t t-if='receipt.footer'>
<br/>
<pre><t t-esc='receipt.footer' /></pre>
<br/>
<br/>
</t>
<br/>
<div font='b'>
<div><t t-esc='receipt.name' /></div>
<div><t t-esc='receipt.date.localestring' /></div>
</div>
</receipt>
</t>
</templates>
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="SplitbillButton">
<span class="control-button order-split">
<i class="fa fa-copy"></i>
Split
</span>
</t>
<t t-name="SplitOrderline">
<li t-attf-class="orderline #{ selected ? 'selected' : ''} #{ quantity !== line.get_quantity() ? 'partially' : '' }"
t-att-data-id="id">
<span class="product-name">
<t t-esc="line.get_product().name"/>
</span>
<span class="price">
<t t-esc="widget.format_currency(line.get_display_price())"/>
</span>
<ul class="info-list">
<t t-if="line.get_quantity_str() !== '1'">
<li class="info">
<t t-if='selected and line.get_unit().groupable'>
<em class='big'>
<t t-esc='quantity' />
</em>
/
<t t-esc="line.get_quantity_str()" />
</t>
<t t-if='!(selected and line.get_unit().groupable)'>
<em>
<t t-esc="line.get_quantity_str()" />
</em>
</t>
<t t-esc="line.get_unit().name" />
at
<t t-esc="widget.format_currency(line.get_unit_price())" />
/
<t t-esc="line.get_unit().name" />
</li>
</t>
<t t-if="line.get_discount_str() !== '0'">
<li class="info">
With a
<em>
<t t-esc="line.get_discount_str()" />%
</em>
discount
</li>
</t>
</ul>
</li>
</t>
<t t-name="SplitbillScreenWidget">
<div class='splitbill-screen screen'>
<div class='screen-content'>
<div class='top-content'>
<span class='button back'>
<i class='fa fa-angle-double-left'></i>
Back
</span>
<h1>Bill Splitting</h1>
</div>
<div class='left-content touch-scrollable scrollable-y'>
<div class='order'>
<ul class='orderlines'>
</ul>
</div>
</div>
<div class='right-content touch-scrollable scrollable-y'>
<div class='order-info'>
<span class='subtotal'><t t-esc='widget.format_currency(0.0)'/></span>
</div>
<div class='paymentmethods'>
<t t-foreach="widget.pos.cashregisters" t-as="cashregister">
<div class='button paymentmethod' t-att-data-id="cashregister.id">
<t t-esc='cashregister.journal.name' />
</div>
</t>
</div>
</div>
</div>
</div>
</t>
</templates>
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- vim:fdn=3:
-->
<openerp>
<data>
<template id="index" inherit_id='point_of_sale.index' name="Restaurant Index">&lt;!DOCTYPE html&gt;
<xpath expr="//link[@id='pos-stylesheet']" position="after">
<link rel="stylesheet" href="/pos_restaurant/static/src/css/restaurant.css" />
</xpath>
</template>
<template id="assets_frontend" inherit_id="web.assets_common">
<xpath expr="." position="inside">
<script type="text/javascript" src="/pos_restaurant/static/src/js/multiprint.js"></script>
<script type="text/javascript" src="/pos_restaurant/static/src/js/splitbill.js"></script>
<script type="text/javascript" src="/pos_restaurant/static/src/js/printbill.js"></script>
<script type="text/javascript" src="/pos_restaurant/static/src/js/main.js"></script>
</xpath>
</template>
</data>
</openerp>