[WIP] point_of_sale: improved performance of the product screen & order widget + ongoing cleanup

bzr revid: fva@openerp.com-20131202162144-6se3s0wjwz0jdfih
This commit is contained in:
Frédéric van der Essen
2013-12-02 17:21:44 +01:00
parent 14e995b452
commit 7d062a0b45
9 changed files with 339 additions and 1797 deletions
+5 -5
View File
@@ -56,7 +56,6 @@ class PosController(http.Controller):
@http.route('/pos/web', type='http', auth='none')
def a(self, debug=False, **k):
print '\nDEBUG',debug,'\n'
js_list = manifest_list('js',db=request.db, debug=debug)
css_list = manifest_list('css',db=request.db, debug=debug)
@@ -64,8 +63,6 @@ class PosController(http.Controller):
print css_list
print js_list
js_list = [ js for js in js_list if 'select2' not in js ]
js = "\n".join('<script type="text/javascript" src="%s"></script>' % i for i in js_list)
css = "\n".join('<link rel="stylesheet" href="%s">' % i for i in css_list)
r = html_template % {
@@ -73,15 +70,18 @@ class PosController(http.Controller):
'css': css,
'modules': simplejson.dumps(module_boot(request.db)),
'init': """
window.navigator.standalone = true;
console.log('LOADED');
var wc = new s.web.WebClient();
wc.appendTo($(document.body));
wc.show_application = function(){
console.log('SHOW APPLICATION');
wc.action_manager.do_action("pos.ui");
};
wc.show_login = function(){
console.log('SHOW LOGIN');
window.location.href = '/';
}
wc.appendTo($(document.body));
console.log('APPENDED');
"""
}
return r
+2 -1
View File
@@ -1163,7 +1163,8 @@
background: rgba(0,0,0,0.82);
color: white;
padding-bottom: 10px;
cursor:move;
cursor: move;
-webkit-transform: translate3d(0,0,0);
}
.pos .debug-widget .toggle{
position: absolute;
File diff suppressed because it is too large Load Diff
@@ -1,63 +0,0 @@
v Affichage Catégories doivent respecter le poids
v Affichage Catégories par poids doivent avoir une hiérarchie plate
v Le Onscreen keyboard ne fonctionne plus
v Scrolling dans la sélection produit et dans
la liste de courses
v Réductions
v Redesign liste de courses
v bugs scans produits par prix par poids
v si pas photo photo par defaut
v WebSQL
v Redesign header, bouton retour aux backend en mode caissière
v bouton exit en mode caissière doit retourner au backend.
v posting orders
v bug ajout ligne payment à zero + curseur dedans + suppression + design plus grand
v si case print via proxy cochée, alors on skip l'ecran receipt
v bouton exit self checkout
v demarrage en mode self-checkout si self-checkout
v produits poid code barre non reconnus
v activer popup client aux écrans self-checkout
v discount pas plus grand que 100
v numpad dans l'écran payment pour pouvoir entrer le montant
v différence de total. Methode d'arrondis
- login alternatif
- user preferences : désactiver la balance etc.
- numpad state parfois sans state
- numpad state en mode pesée ?? le cacher ?
- Redesign du receipt
- retirer le receipt screen
- le numpad en mode paymenet marche pas
- l'écran de payement est hyper laid
- l'écran de pesée est hyper laid
x générer les données de printing
x popups d'erreur certainement buggés
TODO AUG 20
-----------
* GREEN STATUS
- make it green !
* TRUNK
- Connection status tooltip
v Remove receipt screen
- Finish the receipt JSON generation
- Modifie le widget de liste course
v supprimer l'écran
v bloquer sur l'impression
v L'impression est foireuse
- description_sales
* CLIENT
v create a new branch
- Self-checkout welcome screen
~ removal of products for the root category
- Terminal de payement
- Code à barres
- Vidanges
- Design
- Backport
- CSS pour écran résistif
- Test sur matos
+23 -28
View File
@@ -43,7 +43,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
'orders': new module.OrderCollection(),
//this is the product list as seen by the product list widgets, it will change based on the category filters
'products': new module.ProductCollection(),
'cashRegisters': null,
'bank_statements': null,
@@ -444,11 +443,11 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
}
if(parsed_code.type === 'price'){
selectedOrder.addProduct(new module.Product(product), {price:parsed_code.value});
selectedOrder.addProduct(product, {price:parsed_code.value});
}else if(parsed_code.type === 'weight'){
selectedOrder.addProduct(new module.Product(product), {quantity:parsed_code.value, merge:false});
selectedOrder.addProduct(product, {quantity:parsed_code.value, merge:false});
}else{
selectedOrder.addProduct(new module.Product(product));
selectedOrder.addProduct(product);
}
return true;
},
@@ -461,16 +460,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
model: module.CashRegister,
});
module.Product = Backbone.Model.extend({
get_image_url: function(){
return instance.session.url('/web/binary/image', {model: 'product.product', field: 'image_medium', id: this.get('id')});
},
});
module.ProductCollection = Backbone.Collection.extend({
model: module.Product,
});
// An orderline represent one element of the content of a client's shopping cart.
// An orderline contains a product, its quantity, its price, discount. etc.
// An Order contains zero or more Orderlines.
@@ -479,7 +468,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
this.pos = options.pos;
this.order = options.order;
this.product = options.product;
this.price = options.product.get('price');
this.price = options.product.price;
this.quantity = 1;
this.quantityStr = '1';
this.discount = 0;
@@ -492,7 +481,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
var disc = Math.min(Math.max(parseFloat(discount) || 0, 0),100);
this.discount = disc;
this.discountStr = '' + disc;
this.trigger('change');
this.trigger('change',this);
},
// returns the discount [0,100]%
get_discount: function(){
@@ -522,7 +511,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
this.quantityStr = '' + this.quantity;
}
}
this.trigger('change');
this.trigger('change',this);
},
// return the quantity of product
get_quantity: function(){
@@ -541,7 +530,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
},
// return the unit of measure of the product
get_unit: function(){
var unit_id = (this.product.get('uos_id') || this.product.get('uom_id'));
var unit_id = (this.product.uos_id || this.product.uom_id);
if(!unit_id){
return undefined;
}
@@ -558,7 +547,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
// selects or deselects this orderline
set_selected: function(selected){
this.selected = selected;
this.trigger('change');
this.trigger('change',this);
},
// returns true if this orderline is selected
is_selected: function(){
@@ -567,7 +556,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
// when we add an new orderline we want to merge it with the last line to see reduce the number of items
// in the orderline. This returns true if it makes sense to merge the two
can_be_merged_with: function(orderline){
if( this.get_product().get('id') !== orderline.get_product().get('id')){ //only orderline of the same product can be merged
if( this.get_product().id !== orderline.get_product().id){ //only orderline of the same product can be merged
return false;
}else if(this.get_product_type() !== orderline.get_product_type()){
return false;
@@ -587,7 +576,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
qty: this.get_quantity(),
price_unit: this.get_unit_price(),
discount: this.get_discount(),
product_id: this.get_product().get('id'),
product_id: this.get_product().id,
};
},
//used to create a json of the ticket, to be sent to the printer
@@ -597,19 +586,19 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
unit_name: this.get_unit().name,
price: this.get_unit_price(),
discount: this.get_discount(),
product_name: this.get_product().get('name'),
product_name: this.get_product().name,
price_display : this.get_display_price(),
price_with_tax : this.get_price_with_tax(),
price_without_tax: this.get_price_without_tax(),
tax: this.get_tax(),
product_description: this.get_product().get('description'),
product_description_sale: this.get_product().get('description_sale'),
product_description: this.get_product().description,
product_description_sale: this.get_product().description_sale,
};
},
// changes the base price of the product for this orderline
set_unit_price: function(price){
this.price = round_di(parseFloat(price) || 0, 2);
this.trigger('change');
this.trigger('change',this);
},
get_unit_price: function(){
var rounding = this.pos.get('currency').rounding;
@@ -637,8 +626,8 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
var product_list = this.pos.get('product_list');
var product = this.get_product();
var taxes_ids = product.get('taxes_id');;
var taxes = self.pos.get('taxes');
var taxes_ids = product.taxes_id;
var taxes = self.pos.taxes;
var taxtotal = 0;
_.each(taxes_ids, function(el) {
var tax = _.detect(taxes, function(t) {return t.id === el;});
@@ -749,7 +738,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
},
addProduct: function(product, options){
options = options || {};
var attr = product.toJSON();
var attr = JSON.parse(JSON.stringify(product));
attr.pos = this.pos;
attr.order = this;
var line = new module.Orderline({}, {pos: this.pos, order: this, product: product});
@@ -951,6 +940,12 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
this.selected_orderline = undefined;
}
},
deselectLine: function(){
if(this.selected_orderline){
this.selected_orderline.set_selected(false);
this.selected_orderline = undefined;
}
},
});
module.OrderCollection = Backbone.Collection.extend({
+52 -48
View File
@@ -634,7 +634,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
}
var cashregister = selfCheckoutRegisters[0] || self.pos.get('cashRegisters').models[0];
currentOrder.addPaymentLine(cashregister);
currentOrder.add_payment_line(cashregister);
self.pos.push_order(currentOrder)
currentOrder.destroy();
self.pos.proxy.transaction_end();
@@ -745,19 +745,23 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
start: function(){ //FIXME this should work as renderElement... but then the categories aren't properly set. explore why
var self = this;
this.product_categories_widget = new module.ProductCategoriesWidget(this,{});
this.product_categories_widget.replace($('.placeholder-ProductCategoriesWidget'));
this.product_list_widget = new module.ProductListWidget(this,{
click_product_action: function(product){
if(product.get('to_weight') && self.pos.iface_electronic_scale){
if(product.to_weight && self.pos.iface_electronic_scale){
self.pos_widget.screen_selector.set_current_screen( self.cashier_mode ? self.scale_screen : self.client_scale_screen, {product: product});
}else{
self.pos.get('selectedOrder').addProduct(product);
}
},
product_list: this.pos.db.get_product_by_category(0)
});
this.product_list_widget.replace($('.placeholder-ProductListWidget'));
this.product_categories_widget = new module.ProductCategoriesWidget(this,{
product_list_widget: this.product_list_widget,
});
this.product_categories_widget.replace($('.placeholder-ProductCategoriesWidget'));
},
show: function(){
@@ -766,7 +770,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.product_categories_widget.reset_category();
this.pos_widget.order_widget.set_numpad_state(this.pos_widget.numpad.state);
this.pos_widget.order_widget.set_editable(true);
if(this.pos_widget.screen_selector.current_mode === 'client'){
this.add_action_button({
@@ -781,13 +785,13 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
close: function(){
this._super();
this.pos_widget.order_widget.set_numpad_state(null);
this.pos_widget.payment_screen.set_numpad_state(null);
this.pos_widget.order_widget.set_editable(false);
if(this.pos.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
this.pos_widget.onscreen_keyboard.hide();
}
},
});
module.ReceiptScreenWidget = module.ScreenWidget.extend({
@@ -812,8 +816,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this._super();
var self = this;
this.pos_widget.set_numpad_visible(false);
var print_button = this.add_action_button({
label: _t('Print'),
icon: '/point_of_sale/static/src/img/icons/png48/printer.png',
@@ -876,7 +878,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
},
close: function(){
this._super();
this.pos_widget.set_numpad_visible(true);
}
});
@@ -888,7 +889,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this._super(parent,options);
this.model = options.model;
this.pos.bind('change:selectedOrder', this.change_selected_order, this);
this.bindPaymentLineEvents();
this.bind_payment_line_events();
this.bind_orderline_events();
this.paymentlinewidgets = [];
this.focusedLine = null;
@@ -899,7 +900,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.hotkey_handler = function(event){
if(event.which === 13){
self.validateCurrentOrder();
self.validate();
}else if(event.which === 27){
self.back();
}
@@ -912,8 +913,8 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.pos.proxy.open_cashbox();
}
this.set_numpad_state(this.pos_widget.numpad.state);
this.enable_numpad();
this.add_action_button({
label: _t('Back'),
icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
@@ -927,7 +928,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
name: 'validation',
icon: '/point_of_sale/static/src/img/icons/png48/validate.png',
click: function(){
self.validateCurrentOrder();
self.validate();
},
});
@@ -937,7 +938,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
name: 'invoice',
icon: '/point_of_sale/static/src/img/icons/png48/invoice.png',
click: function(){
self.validateCurrentOrder({invoice: true});
self.validate({invoice: true});
},
});
}
@@ -953,13 +954,12 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
});
}
this.updatePaymentSummary();
this.update_payment_summary();
this.line_refocus();
},
close: function(){
this._super();
this.pos_widget.order_widget.set_numpad_state(null);
this.pos_widget.payment_screen.set_numpad_state(null);
this.disable_numpad();
$('body').off('keyup',this.hotkey_handler);
},
back: function() {
@@ -970,13 +970,13 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
});
this.pos_widget.screen_selector.set_current_screen(this.back_screen);
},
validateCurrentOrder: function(options) {
validate: function(options) {
var self = this;
options = options || {};
var currentOrder = this.pos.get('selectedOrder');
if(!this.isPaymentPaid()){
if(!this.is_paid()){
return;
}
@@ -1018,19 +1018,19 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
$("input").blur();
},250);
},
bindPaymentLineEvents: function() {
bind_payment_line_events: function() {
this.currentPaymentLines = (this.pos.get('selectedOrder')).get('paymentLines');
this.currentPaymentLines.bind('add', this.addPaymentLine, this);
this.currentPaymentLines.bind('add', this.add_payment_line, this);
this.currentPaymentLines.bind('remove', this.renderElement, this);
this.currentPaymentLines.bind('all', this.updatePaymentSummary, this);
this.currentPaymentLines.bind('all', this.update_payment_summary, this);
},
bind_orderline_events: function() {
this.currentOrderLines = (this.pos.get('selectedOrder')).get('orderLines');
this.currentOrderLines.bind('all', this.updatePaymentSummary, this);
this.currentOrderLines.bind('all', this.update_payment_summary, this);
},
change_selected_order: function() {
this.currentPaymentLines.unbind();
this.bindPaymentLineEvents();
this.bind_payment_line_events();
this.currentOrderLines.unbind();
this.bind_orderline_events();
this.renderElement();
@@ -1045,7 +1045,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.focusedLine.focus();
}
},
addPaymentLine: function(newPaymentLine) {
add_payment_line: function(newPaymentLine) {
var self = this;
var l = new module.PaymentlineWidget(this, {
payment_line: newPaymentLine,
@@ -1069,21 +1069,21 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.paymentlinewidgets = [];
this.currentPaymentLines.each(_.bind( function(paymentLine) {
this.addPaymentLine(paymentLine);
this.add_payment_line(paymentLine);
}, this));
this.updatePaymentSummary();
this.update_payment_summary();
},
deleteLine: function(lineWidget) {
this.currentPaymentLines.remove([lineWidget.payment_line]);
lineWidget.destroy();
},
isPaymentPaid: function(){
is_paid: function(){
var currentOrder = this.pos.get('selectedOrder');
return (currentOrder.getTotalTaxIncluded() >= 0.000001
&& currentOrder.getPaidTotal() + 0.000001 >= currentOrder.getTotalTaxIncluded());
},
updatePaymentSummary: function() {
update_payment_summary: function() {
var currentOrder = this.pos.get('selectedOrder');
var paidTotal = currentOrder.getPaidTotal();
var dueTotal = currentOrder.getTotalTaxIncluded();
@@ -1099,25 +1099,29 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
}
if(this.pos_widget.action_bar){
this.pos_widget.action_bar.set_button_disabled('validation', !this.isPaymentPaid());
this.pos_widget.action_bar.set_button_disabled('invoice', !this.isPaymentPaid());
this.pos_widget.action_bar.set_button_disabled('validation', !this.is_paid());
this.pos_widget.action_bar.set_button_disabled('invoice', !this.is_paid());
}
},
set_numpad_state: function(numpadState) {
if (this.numpadState) {
this.numpadState.unbind('set_value', this.set_value);
this.numpadState.unbind('change:mode', this.setNumpadMode);
}
this.numpadState = numpadState;
if (this.numpadState) {
this.numpadState.bind('set_value', this.set_value, this);
this.numpadState.bind('change:mode', this.setNumpadMode, this);
this.numpadState.reset();
this.setNumpadMode();
}
enable_numpad: function(){
this.disable_numpad(); //ensure we don't register the callbacks twice
this.numpad_state = this.pos_widget.numpad.state;
if(this.numpad_state){
this.numpad_state.reset();
this.numpad_state.changeMode('payment');
this.numpad_state.bind('set_value', this.set_value, this);
this.numpad_state.bind('change:mode', this.set_mode_back_to_payment, this);
}
},
setNumpadMode: function() {
this.numpadState.set({mode: 'payment'});
disable_numpad: function(){
if(this.numpad_state){
this.numpad_state.unbind('set_value', this.set_value);
this.numpad_state.unbind('change:mode',this.set_mode_back_to_payment);
}
},
set_mode_back_to_payment: function() {
this.numpad_state.set({mode: 'payment'});
},
set_value: function(val) {
this.currentPaymentLines.last().set_amount(val);
@@ -1,265 +0,0 @@
/*
* This Widget provides a javascript scrollbar that is suitable to use with resistive
* tactile screens.
*
* Options:
* target_widget : the widget that will be scrolled.
* target_selector : if you don't want to scroll the root element of the the widget, you can provide a
* jquery selector string that will match on the widget's dom element. If there is no widget provided,
* it will match on the document
* step: on each click, the target will be scrolled by it's deplayed size multiplied by this value.
* duration: this is the duration of the scrolling animation
* wheel_step: the target will be scrolled by wheel_step pixels on each mouse scroll.
* track_bottom: the target will be kept on bottom when it's on the bottom and the size has changed
* on_show: this function will be called with the scrollbar as sole argument when the scrollbar is shown
* on_hide: this function will be called with the scrollbar as sole argument when the scrollbar is hidden
*/
function openerp_pos_scrollbar(instance, module){ //module is instance.point_of_sale
module.ScrollbarWidget = instance.web.Widget.extend({
template:'ScrollbarWidget',
init: function(parent,options){
var self = this;
options = options || {};
this._super(parent,options);
this.target_widget = options.target_widget;
this.target_selector = options.target_selector;
this.scroll_target = this.target().scrollTop();
this.scroll_step = options.step || 0.8;
this.scroll_duration = options.duration || 250;
this.wheel_step = options.wheel_step || 80;
this.name = options.name || 'unnamed';
this.bottom = false; // true if the scroller cannot be scrolled further
this.track_bottom = options.track_bottom || false;
this.on_show = options.on_show || function(){};
this.on_hide = options.on_hide || function(){};
// these handlers are declared once for the object's lifetime so that we can bind and unbind them.
this.resize_handler = function(){
setTimeout(function(){
if(self.bottom && self.track_bottom){
self.set_position(Number.MAX_VALUE);
}
self.update_scroller_dimensions();
self.update_button_status();
self.auto_hide(false);
},0);
};
this.target_mousewheel_handler = function(event,delta){
self.scroll(delta*self.wheel_step);
}
},
renderElement: function(){
this._super();
var self = this;
this.$('.up-button').off('click').click(function(){
self.page_up();
});
this.$('.down-button').off('click').click(function(){
self.page_down();
});
this.update_scroller_dimensions(false);
this.update_button_status();
this.auto_hide(false);
this.$el.bind('mousewheel',function(event,delta){
self.scroll(delta*self.wheel_step);
return false;
});
this.$el.bind('click',function(event){
var vpos = event.pageY - self.$el.offset().top;
var spos = self.scroller_dimensions();
if(vpos > spos.bar_pos && vpos < spos.pos){
self.page_up();
}else if( (vpos < spos.bar_pos + spos.bar_height) &&
(vpos > spos.pos + spos.height) ){
self.page_down();
}
});
// FIXME: use the event bus to handle window resize events
$(window).unbind('resize',this.resize_handler);
$(window).bind('resize',this.resize_handler);
this.target().unbind('mousewheel',this.target_mousewheel_handler);
this.target().bind('mousewheel',this.target_mousewheel_handler);
// because the rendering is asynchronous we must wait for the next javascript update
// for good dimensions values
setTimeout(function(){
self.update_scroller_dimensions(false);
self.update_button_status();
self.auto_hide(false);
},0);
},
destroy: function(){
$(window).unbind('resize',this.resize_handler);
this.target().unbind('mousewheel',this.target_mousewheel_handler);
this._super();
},
// shows the scrollbar. if animated is true, it will do it in an animated fashion
show: function(animated){ //FIXME: animated show and hide don't work ... ?
if(animated){
this.$el.removeClass('oe_hidden').animate({'width':'48px'}, 500, 'swing');
}else{
this.$el.removeClass('oe_hidden').css('width','48px');
}
this.on_show(this);
},
// hides the scrollbar. if animated is true, it will do it in a animated fashion
hide: function(animated){
var self = this;
if(animated){
this.$el.animate({'width':'0px'}, 500, 'swing', function(){ self.$el.addClass('oe_hidden');});
}else{
this.$el.addClass('oe_hidden').css('width','0px');
}
this.on_hide(this);
},
// returns the scroller position and other information as a dictionnary with the following fields:
// pos: the position in pixels of the top of the scroller starting from the top of the scrollbar
// height: the height of the scroller in pixels
// bar_pos: the position of the top of the scrollbar's inner region, starting from the top
// bar_height: the height of the scrollbar's inner region
scroller_dimensions: function(){
var target = this.target()[0];
var scroller_height = target.clientHeight / target.scrollHeight || 0;
var scroller_pos = this.scroll_target / target.scrollHeight || 0;
var button_up_height = this.$('.up-button')[0].offsetHeight || 48;
var button_down_height = this.$('.down-button')[0].offsetHeight || 48;
var bar_height = this.$el[0].offsetHeight || 96;
var scrollbar_height = bar_height - button_up_height - button_down_height;
scroller_pos = scroller_pos * scrollbar_height + button_up_height;
scroller_height = scroller_height * scrollbar_height;
return { pos: Math.round(scroller_pos),
height: Math.round(scroller_height),
bar_pos: button_up_height,
bar_height: scrollbar_height };
},
//checks if it should show or hide the scrollbar based on the target content and then show or hide it
// if animated is true, then the scrollbar will be shown or hidden with an animation
auto_hide: function(animated){
var target = this.target()[0];
if(target.clientHeight && (target.clientHeight === target.scrollHeight)){
this.hide(animated);
}else{
this.show(animated);
}
},
//returns the pageup/down scrolling distance in pixels
get_scroll_step: function(){
var target = this.target()[0];
var step = target.clientHeight * this.scroll_step;
var c = target.scrollHeight / step;
var c = Math.max(1,Math.ceil(c));
return target.scrollHeight / c;
},
//sets the scroller to the correct size and position based on the target scrolling status
//if animated is true, the scroller will move smoothly to its destination
update_scroller_dimensions: function(animated){
var dim = this.scroller_dimensions();
var target = this.target()[0];
if(animated){
this.$('.scroller').animate({'top':dim.pos+'px', 'height': dim.height+'px'},this.scroll_duration);
}else{
this.$('.scroller').css({'top':dim.pos+'px', 'height': dim.height+'px'});
}
if(this.scroll_target + target.clientHeight >= target.scrollHeight){
this.bottom = true;
}else{
this.bottom = false;
}
},
//disable or enable the up/down buttons according to the scrolled position
update_button_status: function(){
var target = this.target()[0];
this.$('.up-button').removeClass('disabled');
this.$('.down-button').removeClass('disabled');
if(this.scroll_target === 0){
this.$('.up-button').addClass('disabled');
}
if(this.scroll_target + target.clientHeight >= target.scrollHeight){
this.$('.down-button').addClass('disabled');
}
},
//returns the jquery object of the scrolling target
target: function(){
if(this.target_widget){
if(this.target_selector){
return this.target_widget.$(this.target_selector);
}else{
return this.target_widget.$el;
}
}else if(this.target_selector){
return $(this.target_selector);
}else{
return undefined;
}
},
//scroll one page up
page_up: function(){
return this.set_position(this.scroll_target - this.get_scroll_step(), true);
},
//scroll one page down
page_down: function(){
return this.set_position(this.scroll_target + this.get_scroll_step(), true);
},
//scrolls up or down by pixels
scroll: function(pixels){
return this.set_position(this.scroll_target - pixels, false);
},
//scroll to a specific position (in pixels).
//if animated is true, it will do this in an animated fashion with a duration equal to scroll_duration
set_position: function(position,animated){
var self = this;
var target = this.target()[0];
var bottom = target.scrollHeight-target.clientHeight;
this.scroll_target = Math.max(0,Math.min(bottom,position));
if(this.scroll_target === 0){
this.position = 'top';
}else if(this.scroll_target === 'bottom'){
this.position = 'bottom';
}else{
this.position = 'center';
}
if(animated){
this.target().animate({'scrollTop':this.scroll_target},this.scroll_duration);
this.update_button_status();
this.update_scroller_dimensions(true);
}else{
this.target().scrollTop(this.scroll_target);
this.update_scroller_dimensions(false);
this.update_button_status();
}
return this.scroll_target;
},
//returns the current position of the scrollbar
get_position: function(){
return this.scroll_target;
},
//returns true if it cannot be scrolled further down
is_at_bottom: function(){
return this.bottom;
}
});
}
+232 -178
View File
@@ -58,6 +58,11 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
init: function(parent, options) {
this._super(parent);
this.state = new module.NumpadState();
window.numpadstate = this.state;
var self = this;
this.state.bind('change:buffer',function(){
console.log('BUFFER:',self.state.get('buffer'));
})
},
start: function() {
this.state.bind('change:mode', this.changedMode, this);
@@ -129,61 +134,50 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
},
});
module.OrderlineWidget = module.PosBaseWidget.extend({
template: 'OrderlineWidget',
init: function(parent, options) {
this._super(parent,options);
this.model = options.model;
this.order = options.order;
this.model.bind('change', this.refresh, this);
},
renderElement: function() {
var self = this;
this._super();
this.$el.click(function(){
self.order.selectLine(self.model);
self.trigger('order_line_selected');
});
if(this.model.is_selected()){
this.$el.addClass('selected');
}
},
refresh: function(){
this.renderElement();
this.trigger('order_line_refreshed');
},
destroy: function(){
this.model.unbind('change',this.refresh,this);
this._super();
},
});
module.OrderWidget = module.PosBaseWidget.extend({
template:'OrderWidget',
init: function(parent, options) {
var self = this;
this._super(parent,options);
this.display_mode = options.display_mode || 'numpad'; // 'maximized' | 'actionbar' | 'numpad'
this.set_numpad_state(options.numpadState);
this.editable = false;
this.pos.bind('change:selectedOrder', this.change_selected_order, this);
this.bind_orderline_events();
this.orderlinewidgets = [];
this.line_click_handler = function(event){
if(!self.editable){
return;
}
self.pos.get('selectedOrder').selectLine(this.orderline);
self.pos_widget.numpad.state.reset();
};
},
set_numpad_state: function(numpadState) {
if (this.numpadState) {
this.numpadState.unbind('set_value', this.set_value);
}
this.numpadState = numpadState;
if (this.numpadState) {
this.numpadState.bind('set_value', this.set_value, this);
this.numpadState.reset();
}
enable_numpad: function(){
this.disable_numpad(); //ensure we don't register the callbacks twice
this.numpad_state = this.pos_widget.numpad.state;
if(this.numpad_state){
this.numpad_state.reset();
this.numpad_state.bind('set_value', this.set_value, this);
}
},
disable_numpad: function(){
if(this.numpad_state){
this.numpad_state.unbind('set_value', this.set_value);
this.numpad_state.reset();
}
},
set_editable: function(editable){
this.editable = editable;
if(editable){
this.enable_numpad();
}else{
this.disable_numpad();
this.pos.get('selectedOrder').deselectLine();
}
},
set_value: function(val) {
var order = this.pos.get('selectedOrder');
if (order.get('orderLines').length !== 0) {
var mode = this.numpadState.get('mode');
if (this.editable && order.getSelectedLine()) {
var mode = this.numpad_state.get('mode');
if( mode === 'quantity'){
order.getSelectedLine().set_quantity(val);
}else if( mode === 'discount'){
@@ -194,81 +188,96 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
}
},
change_selected_order: function() {
this.currentOrderLines.unbind();
this.bind_orderline_events();
this.renderElement();
},
bind_orderline_events: function() {
this.currentOrderLines = (this.pos.get('selectedOrder')).get('orderLines');
this.currentOrderLines.bind('add', function(){ this.renderElement(true);}, this);
this.currentOrderLines.bind('remove', this.renderElement, this);
var lines = this.pos.get('selectedOrder').get('orderLines');
lines.unbind();
lines.bind('add', function(){
this.numpad_state.reset();
this.renderElement(true);
},this);
lines.bind('remove', function(line){
this.remove_orderline(line);
this.numpad_state.reset();
this.update_summary();
},this);
lines.bind('change', function(line){
this.rerender_orderline(line);
this.update_summary();
},this);
},
update_numpad: function() {
this.selected_line = this.pos.get('selectedOrder').getSelectedLine();
if (this.numpadState)
this.numpadState.reset();
},
renderElement: function(goto_bottom) {
var self = this;
var scroller = this.$('.order-scroller')[0];
var scrollbottom = true;
var scrollTop = 0;
if(scroller){
var overflow_bottom = scroller.scrollHeight - scroller.clientHeight;
scrollTop = scroller.scrollTop;
if( !goto_bottom && scrollTop < 0.9 * overflow_bottom){
scrollbottom = false;
}
}
this._super();
render_orderline: function(orderline){
var el_str = openerp.qweb.render('Orderline',{widget:this, line:orderline});
var el_node = document.createElement('div');
el_node.innerHTML = _.str.trim(el_str);
el_node = el_node.childNodes[0];
el_node.orderline = orderline;
el_node.addEventListener('click',this.line_click_handler);
// freeing subwidgets
for(var i = 0, len = this.orderlinewidgets.length; i < len; i++){
this.orderlinewidgets[i].destroy();
orderline.node = el_node;
return el_node;
},
remove_orderline: function(order_line){
if(this.pos.get('selectedOrder').get('orderLines').length === 0){
this.renderElement();
}else{
order_line.node.parentNode.removeChild(order_line.node);
}
this.orderlinewidgets = [];
},
rerender_orderline: function(order_line){
var node = order_line.node;
var replacement_line = this.render_orderline(order_line);
node.parentNode.replaceChild(replacement_line,node);
},
replace: function($target){
this.renderElement();
var target = $target[0];
target.parentNode.replaceChild(this.el,target);
},
renderElement: function(scrollbottom){
this.pos_widget.numpad.state.reset();
var $content = this.$('.orderlines');
this.currentOrderLines.each(_.bind( function(orderLine) {
var line = new module.OrderlineWidget(this, {
model: orderLine,
order: this.pos.get('selectedOrder'),
});
line.on('order_line_selected', self, self.update_numpad);
line.on('order_line_refreshed', self, self.update_summary);
line.appendTo($content);
self.orderlinewidgets.push(line);
}, this));
this.update_numpad();
var order = this.pos.get('selectedOrder');
var orderlines = order.get('orderLines').models;
var el_str = openerp.qweb.render('OrderWidget',{widget:this, order:order, orderlines:orderlines});
var el_node = document.createElement('div');
el_node.innerHTML = _.str.trim(el_str);
el_node = el_node.childNodes[0];
var list_container = el_node.querySelector('.orderlines');
for(var i = 0, len = orderlines.length; i < len; i++){
var orderline = this.render_orderline(orderlines[i]);
list_container.appendChild(orderline);
}
if(this.el && this.el.parentNode){
this.el.parentNode.replaceChild(el_node,this.el);
}
this.el = el_node;
this.update_summary();
scroller = this.$('.order-scroller')[0];
if(scroller){
if(scrollbottom){
scroller.scrollTop = scroller.scrollHeight - scroller.clientHeight;
}else{
scroller.scrollTop = scrollTop;
}
if(scrollbottom){
this.el.querySelector('.order-scroller').scrollTop = 100 * orderlines.length;
}
},
update_summary: function(){
var order = this.pos.get('selectedOrder');
var total = order ? order.getTotalTaxIncluded() : 0;
var taxes = order ? total - order.getTotalTaxExcluded() : 0;
this.$('.summary .total > .value').html(this.format_currency(total));
this.$('.summary .total .subentry .value').html(this.format_currency(taxes));
},
set_display_mode: function(mode){
if(this.display_mode !== mode){
this.display_mode = mode;
this.renderElement();
}
this.el.querySelector('.summary .total > .value').innerText = this.format_currency(total);
this.el.querySelector('.summary .total .subentry .value').innerText = this.format_currency(taxes);
},
});
module.PaymentlineWidget = module.PosBaseWidget.extend({
template: 'PaymentlineWidget',
init: function(parent, options) {
@@ -439,8 +448,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
},
});
module.CategoryButton = module.PosBaseWidget.extend({
});
module.ProductCategoriesWidget = module.PosBaseWidget.extend({
template: 'ProductCategoriesWidget',
init: function(parent, options){
@@ -451,7 +458,13 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.category = this.pos.root_category;
this.breadcrumb = [];
this.subcategories = [];
this.product_list_widget = options.product_list_widget || null;
this.category_cache = {};
this.set_category();
this.switch_category_handler = function(event){
self.set_category(self.pos.db.get_category_by_id(Number(this.dataset['categoryId'])));
self.renderElement();
};
},
// changes the category. if undefined, sets to root category
@@ -474,46 +487,77 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
},
get_image_url: function(category){
return instance.session.url('/web/binary/image', {model: 'pos.category', field: 'image_medium', id: category.id});
return window.location.origin + '/web/binary/image?model=pos.category&field=image_medium&id='+category.id;
},
render_category: function( category, with_image ){
if(!this.category_cache[category.id]){
if(with_image){
var image_url = this.get_image_url(category);
var category_html = QWeb.render('CategoryButton',{
widget: this,
category: category,
});
category_html = _.str.trim(category_html);
var category_node = document.createElement('div');
category_node.innerHTML = category_html;
category_node = category_node.childNodes[0];
var img = category_node.querySelector('img');
img.parentNode.replaceChild(this.pos_widget.image_cache.get_image(image_url),img);
}else{
var category_html = QWeb.render('CategorySimpleButton',{
widget: this,
category: category,
});
category_html = _.str.trim(category_html);
var category_node = document.createElement('div');
category_node.innerHTML = category_html;
category_node = category_node.childNodes[0];
}
this.category_cache[category.id] = category_node;
}
return this.category_cache[category.id];
},
replace: function($target){
this.renderElement();
var target = $target[0];
target.parentNode.replaceChild(this.el,target);
},
renderElement: function(){
var self = this;
this._super();
var el_str = openerp.qweb.render(this.template, {widget: this});
var el_node = document.createElement('div');
el_node.innerHTML = el_str;
el_node = el_node.childNodes[1];
if(this.el && this.el.parentNode){
this.el.parentNode.replaceChild(el_node,this.el);
}
this.el = el_node;
var hasimages = false; //if none of the subcategories have images, we don't display buttons with icons
_.each(this.subcategories, function(category){
if(category.image){
for(var i = 0; i < this.subcategories.length; i++){
if(this.subcategories[i].image){
hasimages = true;
break;
}
});
}
_.each(this.subcategories, function(category){
if(hasimages){
var button = QWeb.render('CategoryButton',{category:category});
var button = _.str.trim(button);
var button = $(button);
button.find('img').replaceWith(self.pos_widget.image_cache.get_image(self.get_image_url(category)));
}else{
var button = QWeb.render('CategorySimpleButton',{category:category});
button = _.str.trim(button); // we remove whitespace between buttons to fix spacing
var button = $(button);
}
var list_container = el_node.querySelector('.category-list');
for(var i = 0, len = this.subcategories.length; i < len; i++){
list_container.appendChild(this.render_category(this.subcategories[i],hasimages));
};
var buttons = el_node.querySelectorAll('.js-category-switch');
for(var i = 0; i < buttons.length; i++){
buttons[i].addEventListener('click',this.switch_category_handler);
}
button.appendTo(this.$('.category-list')).click(function(event){
var id = category.id;
var cat = self.pos.db.get_category_by_id(id);
self.set_category(cat);
self.renderElement();
});
});
// breadcrumb click actions
this.$(".oe-pos-categories-list a").click(function(event){
var id = $(event.target).data("category-id");
var category = self.pos.db.get_category_by_id(id);
self.set_category(category);
self.renderElement();
});
this.search_and_categories();
@@ -522,11 +566,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
}
},
set_product_type: function(type){ // 'all' | 'weightable'
this.product_type = type;
this.reset_category();
},
// resets the current category to the root category
reset_category: function(){
this.set_category();
@@ -536,7 +575,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
// empties the content of the search box
clear_search: function(){
var products = this.pos.db.get_product_by_category(this.category.id);
this.pos.get('products').reset(products);
this.product_list_widget.set_product_list(products);
this.$('.searchbox input').val('').focus();
this.$('.search-clear').fadeOut();
},
@@ -547,9 +586,10 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
// find all products belonging to the current category
var products = this.pos.db.get_product_by_category(this.category.id);
self.pos.get('products').reset(products);
this.product_list_widget.set_product_list(products);
/*
var searchtimeout = null;
// filter the products according to the search string
this.$('.searchbox input').keyup(function(event){
@@ -562,17 +602,17 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
if(query){
if(event.which === 13){
if( self.pos.get('products').size() === 1 ){
self.pos.get('selectedOrder').addProduct(self.pos.get('products').at(0));
self.pos.get('selectedOrder').addProduct(self.pos.get('products').at(0)); // FIXME
self.clear_search();
}
}else{
var products = self.pos.db.search_product_in_category(self.category.id, query);
self.pos.get('products').reset(products);
this.product_list_widget.set_product_list(products);
self.$('.search-clear').fadeIn();
}
}else{
var products = self.pos.db.get_product_by_category(self.category.id);
self.pos.get('products').reset(products);
this.product_list_widget.set_product_list(products);
self.$('.search-clear').fadeOut();
}
},200);
@@ -581,11 +621,11 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
//reset the search when clicking on reset
this.$('.search-clear').click(function(){
self.clear_search();
});
});*/
},
});
module.ProductListWidget = module.ScreenWidget.extend({
module.ProductListWidget = module.PosBaseWidget.extend({
template:'ProductListWidget',
init: function(parent, options) {
var self = this;
@@ -595,38 +635,65 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.weight = options.weight || 0;
this.show_scale = options.show_scale || false;
this.next_screen = options.next_screen || false;
this.click_product_action = options.click_product_action;
this.pos.get('products').bind('reset', function(){
self.renderElement();
});
this.click_product_handler = function(event){
var product = self.pos.db.get_product_by_id(this.dataset['productId']);
options.click_product_action(product);
};
this.product_list = options.product_list || [];
this.product_cache = {};
},
set_product_list: function(product_list){
this.product_list = product_list;
this.renderElement();
},
get_product_image_url: function(product){
return window.location.origin + '/web/binary/image?model=product.product&field=image_medium&id='+product.id;
},
replace: function($target){
this.renderElement();
var target = $target[0];
target.parentNode.replaceChild(this.el,target);
},
render_product: function(product){
if(!this.product_cache[product.id]){
var image_url = this.get_product_image_url(product);
var product_html = QWeb.render('Product',{
widget: this,
product: product,
});
var product_node = document.createElement('div');
product_node.innerHTML = product_html;
product_node = product_node.childNodes[1];
var img = product_node.querySelector('img');
img.parentNode.replaceChild(this.pos_widget.image_cache.get_image(image_url),img);
this.product_cache[product.id] = product_node;
}
return this.product_cache[product.id];
},
renderElement: function() {
var self = this;
this._super();
var products = this.pos.get('products').models || [];
// this._super()
var el_str = openerp.qweb.render(this.template, {widget: this});
var el_node = document.createElement('div');
el_node.innerHTML = el_str;
el_node = el_node.childNodes[1];
_.each(products,function(product,i){
var $product = $(QWeb.render('Product',{ widget:self, product: products[i] }));
$product.find('img').replaceWith(self.pos_widget.image_cache.get_image(products[i].get_image_url()));
$product.appendTo(self.$('.product-list'));
});
this.$el.delegate('.product','touchstart',function TOUCHSTART(){
console.log('Touchstart',(new Date()).getTime());
});
this.$el.delegate('.product','touchend',function TOUCHEND(){
console.log('Touchend',(new Date()).getTime());
});
this.$el.delegate('.product','click',function TOUCHCLICK(){
console.log('ClickStart',(new Date()).getTime());
self.click_product_action(new module.Product(self.pos.db.get_product_by_id(+$(this).data('product-id'))));
console.log('ClickEnd',(new Date()).getTime());
});
if(this.el && this.el.parentNode){
this.el.parentNode.replaceChild(el_node,this.el);
}
this.el = el_node;
var list_container = el_node.querySelector('.product-list');
for(var i = 0, len = this.product_list.length; i < len; i++){
var product_node = this.render_product(this.product_list[i]);
product_node.addEventListener('click',this.click_product_handler);
list_container.appendChild(product_node);
};
},
});
@@ -1046,13 +1113,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.set_left_action_bar_visible(false);
this.numpad.show();
this.paypad.show();
this.order_widget.set_display_mode('numpad');
}else{
this.numpad.hide();
this.paypad.hide();
if(this.order_widget.display_mode === 'numpad'){
this.order_widget.set_display_mode('maximized');
}
}
}
},
@@ -1062,20 +1125,11 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
if(visible){
this.set_numpad_visible(false);
this.left_action_bar.show();
this.order_widget.set_display_mode('actionbar');
}else{
this.left_action_bar.hide();
if(this.order_widget.display_mode === 'actionbar'){
this.order_widget.set_display_mode('maximized');
}
}
}
},
set_fullscreen: function(){
if(this.el.webkitRequestFullscreen){
this.el.webkitRequestFullscreen();
}
},
//shows or hide the leftpane (contains the list of orderlines, the numpad, the paypad, etc.)
set_leftpane_visible: function(visible){
if(visible !== this.leftpane_visible){
+25 -41
View File
@@ -137,7 +137,7 @@
</t>
<t t-name="CategoryButton">
<li class='category-button'>
<li class='category-button js-category-switch' t-att-data-category-id="category.id" >
<div class="category-img">
<img src="" />
</div>
@@ -148,23 +148,24 @@
</t>
<t t-name="CategorySimpleButton">
<li class='category-simple-button'>
<li class='category-simple-button js-category-switch' t-att-data-category-id="category.id" >
<t t-esc="category.name"/>
</li>
</t>
<t t-name="ProductCategoriesWidget">
<div>
<header>
<ol class="breadcrumb">
<li class="oe-pos-categories-list">
<a href="javascript:void(0)">
<a href="javascript:void(0)" class="js-category-switch">
<img src="/point_of_sale/static/src/img/home.png" class="homeimg" />
</a>
</li>
<t t-foreach="widget.breadcrumb" t-as="category">
<li class="oe-pos-categories-list">
<img src="/point_of_sale/static/src/img/bc-arrow.png" class="bc-arrow" />
<a href="javascript:void(0)" t-att-data-category-id="category.id">
<a href="javascript:void(0)" class="js-category-switch" t-att-data-category-id="category.id">
<t t-esc="category.name"/>
</a>
</li>
@@ -181,6 +182,7 @@
</ol>
</div>
</t>
</div>
</t>
<t t-name="ProductListWidget">
@@ -425,52 +427,36 @@
<li class='product' t-att-data-product-id="product.id">
<div class="product-img">
<img src='' /> <!-- the product thumbnail -->
<t t-if="!product.get('to_weight')">
<t t-if="!product.to_weight">
<span class="price-tag">
<t t-esc="widget.format_currency(product.get('price'))"/>
<t t-esc="widget.format_currency(product.price)"/>
</span>
</t>
<t t-if="product.get('to_weight')">
<t t-if="product.to_weight">
<span class="price-tag">
<t t-esc="widget.format_currency(product.get('price'))+'/Kg'"/>
<t t-esc="widget.format_currency(product.price)+'/Kg'"/>
</span>
</t>
</div>
<div class="product-name">
<t t-esc="product.get('name')"/>
<t t-esc="product.name"/>
</div>
</li>
</t>
<t t-name="ScrollbarWidget">
<div class='scrollbar'>
<div class='up-button button oe_e'>
&amp;atilde;
</div>
<div class='scroller'>
</div>
<div class='down-button button oe_e'>
&amp;auml;
</div>
</div>
</t>
<t t-name="OrderWidget">
<div class="order-container">
<div class="order-scroller">
<div class="order">
<ul class="orderlines">
<t t-if="widget.pos.get('selectedOrder').get('orderLines').length === 0">
<t t-if="orderlines.length === 0">
<li class="orderline empty">
Your shopping cart is empty
</li>
</t>
</ul>
<div class="summary">
<div t-attf-class="line #{widget.pos.get('selectedOrder').get('orderLines').length === 0 ? 'empty' : ''}">
<div t-attf-class="line #{orderlines.length === 0 ? 'empty' : ''}">
<div class='entry total'>
<span class="label">Total: </span> <span class="value">0.00 €</span>
<div class='subentry'>Taxes: <span class="value">0.00€</span></div>
@@ -480,8 +466,6 @@
</div>
</div>
</div>
<div class="shadow-top"></div>
<span class="placeholder-ScrollbarWidget"></span>
</div>
</t>
@@ -538,32 +522,32 @@
</div>
</t>
<t t-name="OrderlineWidget">
<li class="orderline">
<t t-name="Orderline">
<li t-attf-class="orderline #{ line.selected ? 'selected' : '' }">
<span class="product-name">
<t t-esc="widget.model.get_product().get('name')"/>
<t t-esc="line.get_product().name"/>
</span>
<span class="price">
<t t-esc="widget.format_currency(widget.model.get_display_price())"/>
<t t-esc="widget.format_currency(line.get_display_price())"/>
</span>
<ul class="info-list">
<t t-if="widget.model.get_quantity_str() !== '1'">
<t t-if="line.get_quantity_str() !== '1' || line.selected ">
<li class="info">
<em>
<t t-esc="widget.model.get_quantity_str()" />
<t t-esc="line.get_quantity_str()" />
</em>
<t t-esc="widget.model.get_unit().name" />
<t t-esc="line.get_unit().name" />
at
<t t-esc="widget.format_currency(widget.model.get_unit_price())" />
<t t-esc="widget.format_currency(line.get_unit_price())" />
/
<t t-esc="widget.model.get_unit().name" />
<t t-esc="line.get_unit().name" />
</li>
</t>
<t t-if="widget.model.get_discount_str() !== '0'">
<t t-if="line.get_discount_str() !== '0'">
<li class="info">
With a
<em>
<t t-esc="widget.model.get_discount_str()" />%
<t t-esc="line.get_discount_str()" />%
</em>
discount
</li>
@@ -621,7 +605,7 @@
</colgroup>
<tr t-foreach="widget.currentOrderLines.toArray()" t-as="orderline">
<td>
<t t-esc="orderline.get_product().get('name')"/>
<t t-esc="orderline.get_product().name"/>
<t t-if="orderline.get_discount() > 0">
<div class="pos-disc-font">
With a <t t-esc="orderline.get_discount()"/>% discount