[WIP] point_of_sale: moved the screen related widgets from chrome.js to screens.js
This commit is contained in:
@@ -4,255 +4,10 @@ openerp.point_of_sale.load_chrome = function load_chrome(instance, module){ //mo
|
||||
var QWeb = instance.web.qweb;
|
||||
var _t = instance.web._t;
|
||||
|
||||
module.DomCache = instance.web.Class.extend({
|
||||
init: function(options){
|
||||
options = options || {};
|
||||
this.max_size = options.max_size || 2000;
|
||||
|
||||
this.cache = {};
|
||||
this.access_time = {};
|
||||
this.size = 0;
|
||||
},
|
||||
cache_node: function(key,node){
|
||||
var cached = this.cache[key];
|
||||
this.cache[key] = node;
|
||||
this.access_time[key] = new Date().getTime();
|
||||
if(!cached){
|
||||
this.size++;
|
||||
while(this.size >= this.max_size){
|
||||
var oldest_key = null;
|
||||
var oldest_time = new Date().getTime();
|
||||
for(var key in this.cache){
|
||||
var time = this.access_time[key];
|
||||
if(time <= oldest_time){
|
||||
oldest_time = time;
|
||||
oldest_key = key;
|
||||
}
|
||||
}
|
||||
if(oldest_key){
|
||||
delete this.cache[oldest_key];
|
||||
delete this.access_time[oldest_key];
|
||||
}
|
||||
this.size--;
|
||||
}
|
||||
}
|
||||
return node;
|
||||
},
|
||||
get_node: function(key){
|
||||
var cached = this.cache[key];
|
||||
if(cached){
|
||||
this.access_time[key] = new Date().getTime();
|
||||
}
|
||||
return cached;
|
||||
},
|
||||
});
|
||||
|
||||
module.NumpadWidget = module.PosBaseWidget.extend({
|
||||
template:'NumpadWidget',
|
||||
init: function(parent, options) {
|
||||
this._super(parent);
|
||||
this.state = new module.NumpadState();
|
||||
window.numpadstate = this.state;
|
||||
var self = this;
|
||||
},
|
||||
start: function() {
|
||||
this.state.bind('change:mode', this.changedMode, this);
|
||||
this.changedMode();
|
||||
this.$el.find('.numpad-backspace').click(_.bind(this.clickDeleteLastChar, this));
|
||||
this.$el.find('.numpad-minus').click(_.bind(this.clickSwitchSign, this));
|
||||
this.$el.find('.number-char').click(_.bind(this.clickAppendNewChar, this));
|
||||
this.$el.find('.mode-button').click(_.bind(this.clickChangeMode, this));
|
||||
},
|
||||
clickDeleteLastChar: function() {
|
||||
return this.state.deleteLastChar();
|
||||
},
|
||||
clickSwitchSign: function() {
|
||||
return this.state.switchSign();
|
||||
},
|
||||
clickAppendNewChar: function(event) {
|
||||
var newChar;
|
||||
newChar = event.currentTarget.innerText || event.currentTarget.textContent;
|
||||
return this.state.appendNewChar(newChar);
|
||||
},
|
||||
clickChangeMode: function(event) {
|
||||
var newMode = event.currentTarget.attributes['data-mode'].nodeValue;
|
||||
return this.state.changeMode(newMode);
|
||||
},
|
||||
changedMode: function() {
|
||||
var mode = this.state.get('mode');
|
||||
$('.selected-mode').removeClass('selected-mode');
|
||||
$(_.str.sprintf('.mode-button[data-mode="%s"]', mode), this.$el).addClass('selected-mode');
|
||||
},
|
||||
});
|
||||
|
||||
// The action pad contains the payment button and the customer selection button
|
||||
module.ActionpadWidget = module.PosBaseWidget.extend({
|
||||
template: 'ActionpadWidget',
|
||||
renderElement: function() {
|
||||
var self = this;
|
||||
this._super();
|
||||
this.$('.pay').click(function(){
|
||||
self.gui.show_screen('payment');
|
||||
});
|
||||
this.$('.set-customer').click(function(){
|
||||
self.gui.show_screen('clientlist');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.OrderWidget = module.PosBaseWidget.extend({
|
||||
template:'OrderWidget',
|
||||
init: function(parent, options) {
|
||||
var self = this;
|
||||
this._super(parent,options);
|
||||
this.editable = false;
|
||||
this.pos.bind('change:selectedOrder', this.change_selected_order, this);
|
||||
this.line_click_handler = function(event){
|
||||
if(!self.editable){
|
||||
return;
|
||||
}
|
||||
self.pos.get_order().select_orderline(this.orderline);
|
||||
self.chrome.widget.numpad.state.reset();
|
||||
};
|
||||
this.client_change_handler = function(event){
|
||||
self.update_summary();
|
||||
}
|
||||
if (this.pos.get_order()) {
|
||||
this.bind_order_events();
|
||||
}
|
||||
},
|
||||
enable_numpad: function(){
|
||||
this.disable_numpad(); //ensure we don't register the callbacks twice
|
||||
this.numpad_state = this.chrome.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_value: function(val) {
|
||||
var order = this.pos.get_order();
|
||||
if (this.editable && order.get_selected_orderline()) {
|
||||
var mode = this.numpad_state.get('mode');
|
||||
if( mode === 'quantity'){
|
||||
order.get_selected_orderline().set_quantity(val);
|
||||
}else if( mode === 'discount'){
|
||||
order.get_selected_orderline().set_discount(val);
|
||||
}else if( mode === 'price'){
|
||||
order.get_selected_orderline().set_unit_price(val);
|
||||
}
|
||||
}
|
||||
},
|
||||
change_selected_order: function() {
|
||||
if (this.pos.get_order()) {
|
||||
this.bind_order_events();
|
||||
this.renderElement();
|
||||
}
|
||||
},
|
||||
orderline_add: function(){
|
||||
this.numpad_state.reset();
|
||||
this.renderElement('and_scroll_to_bottom');
|
||||
},
|
||||
orderline_remove: function(line){
|
||||
this.remove_orderline(line);
|
||||
this.numpad_state.reset();
|
||||
this.update_summary();
|
||||
},
|
||||
orderline_change: function(line){
|
||||
this.rerender_orderline(line);
|
||||
this.update_summary();
|
||||
},
|
||||
bind_order_events: function() {
|
||||
var order = this.pos.get_order();
|
||||
order.unbind('change:client', this.client_change_handler);
|
||||
order.bind('change:client', this.client_change_handler);
|
||||
|
||||
var lines = order.orderlines;
|
||||
lines.unbind('add', this.orderline_add, this);
|
||||
lines.bind('add', this.orderline_add, this);
|
||||
lines.unbind('remove', this.orderline_remove, this);
|
||||
lines.bind('remove', this.orderline_remove, this);
|
||||
lines.unbind('change', this.orderline_change, this);
|
||||
lines.bind('change', this.orderline_change, this);
|
||||
|
||||
},
|
||||
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);
|
||||
|
||||
orderline.node = el_node;
|
||||
return el_node;
|
||||
},
|
||||
remove_orderline: function(order_line){
|
||||
if(this.pos.get_order().get_orderlines().length === 0){
|
||||
this.renderElement();
|
||||
}else{
|
||||
order_line.node.parentNode.removeChild(order_line.node);
|
||||
}
|
||||
},
|
||||
rerender_orderline: function(order_line){
|
||||
var node = order_line.node;
|
||||
var replacement_line = this.render_orderline(order_line);
|
||||
node.parentNode.replaceChild(replacement_line,node);
|
||||
},
|
||||
// overriding the openerp framework replace method for performance reasons
|
||||
replace: function($target){
|
||||
this.renderElement();
|
||||
var target = $target[0];
|
||||
target.parentNode.replaceChild(this.el,target);
|
||||
},
|
||||
renderElement: function(scrollbottom){
|
||||
this.chrome.widget.numpad.state.reset(); //FIXME WTF
|
||||
|
||||
var order = this.pos.get_order();
|
||||
if (!order) {
|
||||
return;
|
||||
}
|
||||
var orderlines = order.get_orderlines();
|
||||
|
||||
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();
|
||||
|
||||
if(scrollbottom){
|
||||
this.el.querySelector('.order-scroller').scrollTop = 100 * orderlines.length;
|
||||
}
|
||||
},
|
||||
update_summary: function(){
|
||||
var order = this.pos.get_order();
|
||||
var total = order ? order.get_total_with_tax() : 0;
|
||||
var taxes = order ? total - order.get_total_without_tax() : 0;
|
||||
|
||||
this.el.querySelector('.summary .total > .value').textContent = this.format_currency(total);
|
||||
this.el.querySelector('.summary .total .subentry .value').textContent = this.format_currency(taxes);
|
||||
|
||||
},
|
||||
});
|
||||
/* -------- The Order Selector -------- */
|
||||
|
||||
// Allows the cashier to create / delete and
|
||||
// switch between orders.
|
||||
|
||||
module.OrderSelectorWidget = module.PosBaseWidget.extend({
|
||||
template: 'OrderSelectorWidget',
|
||||
@@ -311,254 +66,10 @@ openerp.point_of_sale.load_chrome = function load_chrome(instance, module){ //mo
|
||||
},
|
||||
});
|
||||
|
||||
module.ProductCategoriesWidget = module.PosBaseWidget.extend({
|
||||
template: 'ProductCategoriesWidget',
|
||||
init: function(parent, options){
|
||||
var self = this;
|
||||
this._super(parent,options);
|
||||
this.product_type = options.product_type || 'all'; // 'all' | 'weightable'
|
||||
this.onlyWeightable = options.onlyWeightable || false;
|
||||
this.category = this.pos.root_category;
|
||||
this.breadcrumb = [];
|
||||
this.subcategories = [];
|
||||
this.product_list_widget = options.product_list_widget || null;
|
||||
this.category_cache = new module.DomCache();
|
||||
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();
|
||||
};
|
||||
|
||||
this.clear_search_handler = function(event){
|
||||
self.clear_search();
|
||||
};
|
||||
|
||||
var search_timeout = null;
|
||||
this.search_handler = function(event){
|
||||
clearTimeout(search_timeout);
|
||||
|
||||
var query = this.value;
|
||||
|
||||
search_timeout = setTimeout(function(){
|
||||
self.perform_search(self.category, query, event.which === 13);
|
||||
},70);
|
||||
};
|
||||
},
|
||||
|
||||
// changes the category. if undefined, sets to root category
|
||||
set_category : function(category){
|
||||
var db = this.pos.db;
|
||||
if(!category){
|
||||
this.category = db.get_category_by_id(db.root_category_id);
|
||||
}else{
|
||||
this.category = category;
|
||||
}
|
||||
this.breadcrumb = [];
|
||||
var ancestors_ids = db.get_category_ancestors_ids(this.category.id);
|
||||
for(var i = 1; i < ancestors_ids.length; i++){
|
||||
this.breadcrumb.push(db.get_category_by_id(ancestors_ids[i]));
|
||||
}
|
||||
if(this.category.id !== db.root_category_id){
|
||||
this.breadcrumb.push(this.category);
|
||||
}
|
||||
this.subcategories = db.get_category_by_id(db.get_category_childs_ids(this.category.id));
|
||||
},
|
||||
|
||||
get_image_url: function(category){
|
||||
return window.location.origin + '/web/binary/image?model=pos.category&field=image_medium&id='+category.id;
|
||||
},
|
||||
|
||||
render_category: function( category, with_image ){
|
||||
var cached = this.category_cache.get_node(category.id);
|
||||
if(!cached){
|
||||
if(with_image){
|
||||
var image_url = this.get_image_url(category);
|
||||
var category_html = QWeb.render('CategoryButton',{
|
||||
widget: this,
|
||||
category: category,
|
||||
image_url: this.get_image_url(category),
|
||||
});
|
||||
category_html = _.str.trim(category_html);
|
||||
var category_node = document.createElement('div');
|
||||
category_node.innerHTML = category_html;
|
||||
category_node = category_node.childNodes[0];
|
||||
}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.cache_node(category.id,category_node);
|
||||
return category_node;
|
||||
}
|
||||
return cached;
|
||||
},
|
||||
|
||||
replace: function($target){
|
||||
this.renderElement();
|
||||
var target = $target[0];
|
||||
target.parentNode.replaceChild(this.el,target);
|
||||
},
|
||||
|
||||
renderElement: function(){
|
||||
var self = this;
|
||||
|
||||
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
|
||||
for(var i = 0; i < this.subcategories.length; i++){
|
||||
if(this.subcategories[i].image){
|
||||
hasimages = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var list_container = el_node.querySelector('.category-list');
|
||||
if (list_container) {
|
||||
if (!hasimages) {
|
||||
list_container.classList.add('simple');
|
||||
} else {
|
||||
list_container.classList.remove('simple');
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
var products = this.pos.db.get_product_by_category(this.category.id);
|
||||
this.product_list_widget.set_product_list(products);
|
||||
|
||||
this.el.querySelector('.searchbox input').addEventListener('keyup',this.search_handler);
|
||||
|
||||
this.el.querySelector('.search-clear').addEventListener('click',this.clear_search_handler);
|
||||
|
||||
if(this.pos.config.iface_vkeyboard && this.chrome.widget.keyboard){
|
||||
this.chrome.widget.keyboard.connect($(this.el.querySelector('.searchbox input')));
|
||||
}
|
||||
},
|
||||
|
||||
// resets the current category to the root category
|
||||
reset_category: function(){
|
||||
this.set_category();
|
||||
this.renderElement();
|
||||
},
|
||||
|
||||
// empties the content of the search box
|
||||
clear_search: function(){
|
||||
var products = this.pos.db.get_product_by_category(this.category.id);
|
||||
this.product_list_widget.set_product_list(products);
|
||||
var input = this.el.querySelector('.searchbox input');
|
||||
input.value = '';
|
||||
input.focus();
|
||||
},
|
||||
perform_search: function(category, query, buy_result){
|
||||
if(query){
|
||||
var products = this.pos.db.search_product_in_category(category.id,query)
|
||||
if(buy_result && products.length === 1){
|
||||
this.pos.get_order().add_product(products[0]);
|
||||
this.clear_search();
|
||||
}else{
|
||||
this.product_list_widget.set_product_list(products);
|
||||
}
|
||||
}else{
|
||||
var products = this.pos.db.get_product_by_category(this.category.id);
|
||||
this.product_list_widget.set_product_list(products);
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
module.ProductListWidget = module.PosBaseWidget.extend({
|
||||
template:'ProductListWidget',
|
||||
init: function(parent, options) {
|
||||
var self = this;
|
||||
this._super(parent,options);
|
||||
this.model = options.model;
|
||||
this.productwidgets = [];
|
||||
this.weight = options.weight || 0;
|
||||
this.show_scale = options.show_scale || false;
|
||||
this.next_screen = options.next_screen || false;
|
||||
|
||||
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 = new module.DomCache();
|
||||
},
|
||||
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){
|
||||
var cached = this.product_cache.get_node(product.id);
|
||||
if(!cached){
|
||||
var image_url = this.get_product_image_url(product);
|
||||
var product_html = QWeb.render('Product',{
|
||||
widget: this,
|
||||
product: product,
|
||||
image_url: this.get_product_image_url(product),
|
||||
});
|
||||
var product_node = document.createElement('div');
|
||||
product_node.innerHTML = product_html;
|
||||
product_node = product_node.childNodes[1];
|
||||
this.product_cache.cache_node(product.id,product_node);
|
||||
return product_node;
|
||||
}
|
||||
return cached;
|
||||
},
|
||||
|
||||
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 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);
|
||||
};
|
||||
},
|
||||
});
|
||||
/* ------- The User Name Widget ------- */
|
||||
|
||||
// Displays the current cashier's name and allows
|
||||
// to switch between cashiers.
|
||||
|
||||
module.UsernameWidget = module.PosBaseWidget.extend({
|
||||
template: 'UsernameWidget',
|
||||
@@ -595,6 +106,12 @@ openerp.point_of_sale.load_chrome = function load_chrome(instance, module){ //mo
|
||||
},
|
||||
});
|
||||
|
||||
/* -------- The Header Button --------- */
|
||||
|
||||
// Used to quickly add buttons with simple
|
||||
// labels and actions to the point of sale
|
||||
// header.
|
||||
|
||||
module.HeaderButtonWidget = module.PosBaseWidget.extend({
|
||||
template: 'HeaderButtonWidget',
|
||||
init: function(parent, options){
|
||||
@@ -616,8 +133,14 @@ openerp.point_of_sale.load_chrome = function load_chrome(instance, module){ //mo
|
||||
hide: function(){ this.$el.addClass('oe_hidden'); },
|
||||
});
|
||||
|
||||
// The debug widget lets the user control and monitor the hardware and software status
|
||||
// without the use of the proxy
|
||||
/* --------- The Debug Widget --------- */
|
||||
|
||||
// The debug widget lets the user control
|
||||
// and monitor the hardware and software status
|
||||
// without the use of the proxy, or to access
|
||||
// the raw locally stored db values, useful
|
||||
// for debugging
|
||||
|
||||
module.DebugWidget = module.PosBaseWidget.extend({
|
||||
template: "DebugWidget",
|
||||
eans:{
|
||||
@@ -760,7 +283,10 @@ openerp.point_of_sale.load_chrome = function load_chrome(instance, module){ //mo
|
||||
},
|
||||
});
|
||||
|
||||
// ---------- Main Point of Sale Widget ----------
|
||||
/* --------- The Status Widget -------- */
|
||||
|
||||
// Base class for widgets that want to display
|
||||
// status in the point of sale header.
|
||||
|
||||
module.StatusWidget = module.PosBaseWidget.extend({
|
||||
status: ['connected','connecting','disconnected','warning'],
|
||||
@@ -779,7 +305,11 @@ openerp.point_of_sale.load_chrome = function load_chrome(instance, module){ //mo
|
||||
},
|
||||
});
|
||||
|
||||
// this is used to notify the user that data is being synchronized on the network
|
||||
/* ------- Synch. Notifications ------- */
|
||||
|
||||
// Displays if there are orders that could
|
||||
// not be submitted, and how many.
|
||||
|
||||
module.SynchNotificationWidget = module.StatusWidget.extend({
|
||||
template: 'SynchNotificationWidget',
|
||||
start: function(){
|
||||
@@ -793,7 +323,11 @@ openerp.point_of_sale.load_chrome = function load_chrome(instance, module){ //mo
|
||||
},
|
||||
});
|
||||
|
||||
// this is used to notify the user if the pos is connected to the proxy
|
||||
/* --------- The Proxy Status --------- */
|
||||
|
||||
// Displays the status of the hardware proxy
|
||||
// (connected, disconnected, errors ... )
|
||||
|
||||
module.ProxyStatusWidget = module.StatusWidget.extend({
|
||||
template: 'ProxyStatusWidget',
|
||||
set_smart_status: function(status){
|
||||
@@ -846,14 +380,27 @@ openerp.point_of_sale.load_chrome = function load_chrome(instance, module){ //mo
|
||||
});
|
||||
|
||||
|
||||
// The Chrome is the main widget that contains all other widgets in the PointOfSale.
|
||||
/*--------------------------------------*\
|
||||
| THE CHROME |
|
||||
\*======================================*/
|
||||
|
||||
// Displays the status of the hardware proxy
|
||||
// The Chrome is the main widget that contains
|
||||
// all other widgets in the PointOfSale.
|
||||
//
|
||||
// It is the first object instanciated and the
|
||||
// starting point of the point of sale code.
|
||||
//
|
||||
// It is mainly composed of :
|
||||
// - a header, containing the list of orders
|
||||
// - a leftpane, containing the list of bought products (orderlines)
|
||||
// - a rightpane, containing the screens (see pos_screens.js)
|
||||
// - a leftpane, containing the list of bought
|
||||
// products (orderlines)
|
||||
// - a rightpane, containing the screens
|
||||
// (see pos_screens.js)
|
||||
// - popups
|
||||
// - an onscreen keyboard
|
||||
// a gui which controls the switching between screens and the showing/closing of popups
|
||||
// - .gui which controls the switching between
|
||||
// screens and the showing/closing of popups
|
||||
|
||||
module.Chrome = module.PosBaseWidget.extend({
|
||||
template: 'Chrome',
|
||||
|
||||
@@ -34,6 +34,13 @@ openerp.point_of_sale.load_screens = function load_screens(instance, module){ //
|
||||
|
||||
var round_pr = instance.web.round_precision
|
||||
|
||||
/*--------------------------------------*\
|
||||
| THE SCREEN WIDGET |
|
||||
\*======================================*/
|
||||
|
||||
// The screen widget is the base class inherited
|
||||
// by all screens.
|
||||
|
||||
module.ScreenWidget = module.PosBaseWidget.extend({
|
||||
|
||||
show_numpad: true,
|
||||
@@ -158,6 +165,69 @@ openerp.point_of_sale.load_screens = function load_screens(instance, module){ //
|
||||
},
|
||||
});
|
||||
|
||||
/*--------------------------------------*\
|
||||
| THE DOM CACHE |
|
||||
\*======================================*/
|
||||
|
||||
// The Dom Cache is used by various screens to improve
|
||||
// their performances when displaying many time the
|
||||
// same piece of DOM.
|
||||
//
|
||||
// It is a simple map from string 'keys' to DOM Nodes.
|
||||
//
|
||||
// The cache empties itself based on usage frequency
|
||||
// stats, so you may not always get back what
|
||||
// you put in.
|
||||
|
||||
module.DomCache = instance.web.Class.extend({
|
||||
init: function(options){
|
||||
options = options || {};
|
||||
this.max_size = options.max_size || 2000;
|
||||
|
||||
this.cache = {};
|
||||
this.access_time = {};
|
||||
this.size = 0;
|
||||
},
|
||||
cache_node: function(key,node){
|
||||
var cached = this.cache[key];
|
||||
this.cache[key] = node;
|
||||
this.access_time[key] = new Date().getTime();
|
||||
if(!cached){
|
||||
this.size++;
|
||||
while(this.size >= this.max_size){
|
||||
var oldest_key = null;
|
||||
var oldest_time = new Date().getTime();
|
||||
for(var key in this.cache){
|
||||
var time = this.access_time[key];
|
||||
if(time <= oldest_time){
|
||||
oldest_time = time;
|
||||
oldest_key = key;
|
||||
}
|
||||
}
|
||||
if(oldest_key){
|
||||
delete this.cache[oldest_key];
|
||||
delete this.access_time[oldest_key];
|
||||
}
|
||||
this.size--;
|
||||
}
|
||||
}
|
||||
return node;
|
||||
},
|
||||
get_node: function(key){
|
||||
var cached = this.cache[key];
|
||||
if(cached){
|
||||
this.access_time[key] = new Date().getTime();
|
||||
}
|
||||
return cached;
|
||||
},
|
||||
});
|
||||
|
||||
/*--------------------------------------*\
|
||||
| THE SCALE SCREEN |
|
||||
\*======================================*/
|
||||
|
||||
// The scale screen displays the weight of
|
||||
// a product on the electronic scale.
|
||||
|
||||
module.ScaleScreenWidget = module.ScreenWidget.extend({
|
||||
template:'ScaleScreenWidget',
|
||||
@@ -249,6 +319,505 @@ openerp.point_of_sale.load_screens = function load_screens(instance, module){ //
|
||||
},
|
||||
});
|
||||
|
||||
/*--------------------------------------*\
|
||||
| THE PRODUCT SCREEN |
|
||||
\*======================================*/
|
||||
|
||||
// The product screen contains the list of products,
|
||||
// The category selector and the order display.
|
||||
// It is the default screen for orders and the
|
||||
// startup screen for shops.
|
||||
//
|
||||
// There product screens uses many sub-widgets,
|
||||
// the code follows.
|
||||
|
||||
|
||||
/* ------------ The Numpad ------------ */
|
||||
|
||||
// The numpad that edits the order lines.
|
||||
|
||||
module.NumpadWidget = module.PosBaseWidget.extend({
|
||||
template:'NumpadWidget',
|
||||
init: function(parent, options) {
|
||||
this._super(parent);
|
||||
this.state = new module.NumpadState();
|
||||
window.numpadstate = this.state;
|
||||
var self = this;
|
||||
},
|
||||
start: function() {
|
||||
this.state.bind('change:mode', this.changedMode, this);
|
||||
this.changedMode();
|
||||
this.$el.find('.numpad-backspace').click(_.bind(this.clickDeleteLastChar, this));
|
||||
this.$el.find('.numpad-minus').click(_.bind(this.clickSwitchSign, this));
|
||||
this.$el.find('.number-char').click(_.bind(this.clickAppendNewChar, this));
|
||||
this.$el.find('.mode-button').click(_.bind(this.clickChangeMode, this));
|
||||
},
|
||||
clickDeleteLastChar: function() {
|
||||
return this.state.deleteLastChar();
|
||||
},
|
||||
clickSwitchSign: function() {
|
||||
return this.state.switchSign();
|
||||
},
|
||||
clickAppendNewChar: function(event) {
|
||||
var newChar;
|
||||
newChar = event.currentTarget.innerText || event.currentTarget.textContent;
|
||||
return this.state.appendNewChar(newChar);
|
||||
},
|
||||
clickChangeMode: function(event) {
|
||||
var newMode = event.currentTarget.attributes['data-mode'].nodeValue;
|
||||
return this.state.changeMode(newMode);
|
||||
},
|
||||
changedMode: function() {
|
||||
var mode = this.state.get('mode');
|
||||
$('.selected-mode').removeClass('selected-mode');
|
||||
$(_.str.sprintf('.mode-button[data-mode="%s"]', mode), this.$el).addClass('selected-mode');
|
||||
},
|
||||
});
|
||||
|
||||
/* ---------- The Action Pad ---------- */
|
||||
|
||||
// The action pad contains the payment button and the
|
||||
// customer selection button
|
||||
|
||||
module.ActionpadWidget = module.PosBaseWidget.extend({
|
||||
template: 'ActionpadWidget',
|
||||
renderElement: function() {
|
||||
var self = this;
|
||||
this._super();
|
||||
this.$('.pay').click(function(){
|
||||
self.gui.show_screen('payment');
|
||||
});
|
||||
this.$('.set-customer').click(function(){
|
||||
self.gui.show_screen('clientlist');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/* --------- The Order Widget --------- */
|
||||
|
||||
// Displays the current Order.
|
||||
|
||||
module.OrderWidget = module.PosBaseWidget.extend({
|
||||
template:'OrderWidget',
|
||||
init: function(parent, options) {
|
||||
var self = this;
|
||||
this._super(parent,options);
|
||||
this.editable = false;
|
||||
this.pos.bind('change:selectedOrder', this.change_selected_order, this);
|
||||
this.line_click_handler = function(event){
|
||||
if(!self.editable){
|
||||
return;
|
||||
}
|
||||
self.pos.get_order().select_orderline(this.orderline);
|
||||
self.chrome.widget.numpad.state.reset();
|
||||
};
|
||||
this.client_change_handler = function(event){
|
||||
self.update_summary();
|
||||
}
|
||||
if (this.pos.get_order()) {
|
||||
this.bind_order_events();
|
||||
}
|
||||
},
|
||||
enable_numpad: function(){
|
||||
this.disable_numpad(); //ensure we don't register the callbacks twice
|
||||
this.numpad_state = this.chrome.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_value: function(val) {
|
||||
var order = this.pos.get_order();
|
||||
if (this.editable && order.get_selected_orderline()) {
|
||||
var mode = this.numpad_state.get('mode');
|
||||
if( mode === 'quantity'){
|
||||
order.get_selected_orderline().set_quantity(val);
|
||||
}else if( mode === 'discount'){
|
||||
order.get_selected_orderline().set_discount(val);
|
||||
}else if( mode === 'price'){
|
||||
order.get_selected_orderline().set_unit_price(val);
|
||||
}
|
||||
}
|
||||
},
|
||||
change_selected_order: function() {
|
||||
if (this.pos.get_order()) {
|
||||
this.bind_order_events();
|
||||
this.renderElement();
|
||||
}
|
||||
},
|
||||
orderline_add: function(){
|
||||
this.numpad_state.reset();
|
||||
this.renderElement('and_scroll_to_bottom');
|
||||
},
|
||||
orderline_remove: function(line){
|
||||
this.remove_orderline(line);
|
||||
this.numpad_state.reset();
|
||||
this.update_summary();
|
||||
},
|
||||
orderline_change: function(line){
|
||||
this.rerender_orderline(line);
|
||||
this.update_summary();
|
||||
},
|
||||
bind_order_events: function() {
|
||||
var order = this.pos.get_order();
|
||||
order.unbind('change:client', this.client_change_handler);
|
||||
order.bind('change:client', this.client_change_handler);
|
||||
|
||||
var lines = order.orderlines;
|
||||
lines.unbind('add', this.orderline_add, this);
|
||||
lines.bind('add', this.orderline_add, this);
|
||||
lines.unbind('remove', this.orderline_remove, this);
|
||||
lines.bind('remove', this.orderline_remove, this);
|
||||
lines.unbind('change', this.orderline_change, this);
|
||||
lines.bind('change', this.orderline_change, this);
|
||||
|
||||
},
|
||||
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);
|
||||
|
||||
orderline.node = el_node;
|
||||
return el_node;
|
||||
},
|
||||
remove_orderline: function(order_line){
|
||||
if(this.pos.get_order().get_orderlines().length === 0){
|
||||
this.renderElement();
|
||||
}else{
|
||||
order_line.node.parentNode.removeChild(order_line.node);
|
||||
}
|
||||
},
|
||||
rerender_orderline: function(order_line){
|
||||
var node = order_line.node;
|
||||
var replacement_line = this.render_orderline(order_line);
|
||||
node.parentNode.replaceChild(replacement_line,node);
|
||||
},
|
||||
// overriding the openerp framework replace method for performance reasons
|
||||
replace: function($target){
|
||||
this.renderElement();
|
||||
var target = $target[0];
|
||||
target.parentNode.replaceChild(this.el,target);
|
||||
},
|
||||
renderElement: function(scrollbottom){
|
||||
this.chrome.widget.numpad.state.reset(); //FIXME WTF
|
||||
|
||||
var order = this.pos.get_order();
|
||||
if (!order) {
|
||||
return;
|
||||
}
|
||||
var orderlines = order.get_orderlines();
|
||||
|
||||
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();
|
||||
|
||||
if(scrollbottom){
|
||||
this.el.querySelector('.order-scroller').scrollTop = 100 * orderlines.length;
|
||||
}
|
||||
},
|
||||
update_summary: function(){
|
||||
var order = this.pos.get_order();
|
||||
var total = order ? order.get_total_with_tax() : 0;
|
||||
var taxes = order ? total - order.get_total_without_tax() : 0;
|
||||
|
||||
this.el.querySelector('.summary .total > .value').textContent = this.format_currency(total);
|
||||
this.el.querySelector('.summary .total .subentry .value').textContent = this.format_currency(taxes);
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
/* ------ The Product Categories ------ */
|
||||
|
||||
// Display and navigate the product categories.
|
||||
// Also handles searches.
|
||||
// - set_category() to change the displayed category
|
||||
// - reset_category() to go to the root category
|
||||
// - perform_search() to search for products
|
||||
// - clear_search() does what it says.
|
||||
|
||||
module.ProductCategoriesWidget = module.PosBaseWidget.extend({
|
||||
template: 'ProductCategoriesWidget',
|
||||
init: function(parent, options){
|
||||
var self = this;
|
||||
this._super(parent,options);
|
||||
this.product_type = options.product_type || 'all'; // 'all' | 'weightable'
|
||||
this.onlyWeightable = options.onlyWeightable || false;
|
||||
this.category = this.pos.root_category;
|
||||
this.breadcrumb = [];
|
||||
this.subcategories = [];
|
||||
this.product_list_widget = options.product_list_widget || null;
|
||||
this.category_cache = new module.DomCache();
|
||||
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();
|
||||
};
|
||||
|
||||
this.clear_search_handler = function(event){
|
||||
self.clear_search();
|
||||
};
|
||||
|
||||
var search_timeout = null;
|
||||
this.search_handler = function(event){
|
||||
clearTimeout(search_timeout);
|
||||
|
||||
var query = this.value;
|
||||
|
||||
search_timeout = setTimeout(function(){
|
||||
self.perform_search(self.category, query, event.which === 13);
|
||||
},70);
|
||||
};
|
||||
},
|
||||
|
||||
// changes the category. if undefined, sets to root category
|
||||
set_category : function(category){
|
||||
var db = this.pos.db;
|
||||
if(!category){
|
||||
this.category = db.get_category_by_id(db.root_category_id);
|
||||
}else{
|
||||
this.category = category;
|
||||
}
|
||||
this.breadcrumb = [];
|
||||
var ancestors_ids = db.get_category_ancestors_ids(this.category.id);
|
||||
for(var i = 1; i < ancestors_ids.length; i++){
|
||||
this.breadcrumb.push(db.get_category_by_id(ancestors_ids[i]));
|
||||
}
|
||||
if(this.category.id !== db.root_category_id){
|
||||
this.breadcrumb.push(this.category);
|
||||
}
|
||||
this.subcategories = db.get_category_by_id(db.get_category_childs_ids(this.category.id));
|
||||
},
|
||||
|
||||
get_image_url: function(category){
|
||||
return window.location.origin + '/web/binary/image?model=pos.category&field=image_medium&id='+category.id;
|
||||
},
|
||||
|
||||
render_category: function( category, with_image ){
|
||||
var cached = this.category_cache.get_node(category.id);
|
||||
if(!cached){
|
||||
if(with_image){
|
||||
var image_url = this.get_image_url(category);
|
||||
var category_html = QWeb.render('CategoryButton',{
|
||||
widget: this,
|
||||
category: category,
|
||||
image_url: this.get_image_url(category),
|
||||
});
|
||||
category_html = _.str.trim(category_html);
|
||||
var category_node = document.createElement('div');
|
||||
category_node.innerHTML = category_html;
|
||||
category_node = category_node.childNodes[0];
|
||||
}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.cache_node(category.id,category_node);
|
||||
return category_node;
|
||||
}
|
||||
return cached;
|
||||
},
|
||||
|
||||
replace: function($target){
|
||||
this.renderElement();
|
||||
var target = $target[0];
|
||||
target.parentNode.replaceChild(this.el,target);
|
||||
},
|
||||
|
||||
renderElement: function(){
|
||||
var self = this;
|
||||
|
||||
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
|
||||
for(var i = 0; i < this.subcategories.length; i++){
|
||||
if(this.subcategories[i].image){
|
||||
hasimages = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var list_container = el_node.querySelector('.category-list');
|
||||
if (list_container) {
|
||||
if (!hasimages) {
|
||||
list_container.classList.add('simple');
|
||||
} else {
|
||||
list_container.classList.remove('simple');
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
var products = this.pos.db.get_product_by_category(this.category.id);
|
||||
this.product_list_widget.set_product_list(products); // FIXME: this should be moved elsewhere ...
|
||||
|
||||
this.el.querySelector('.searchbox input').addEventListener('keyup',this.search_handler);
|
||||
|
||||
this.el.querySelector('.search-clear').addEventListener('click',this.clear_search_handler);
|
||||
|
||||
if(this.pos.config.iface_vkeyboard && this.chrome.widget.keyboard){
|
||||
this.chrome.widget.keyboard.connect($(this.el.querySelector('.searchbox input')));
|
||||
}
|
||||
},
|
||||
|
||||
// resets the current category to the root category
|
||||
reset_category: function(){
|
||||
this.set_category();
|
||||
this.renderElement();
|
||||
},
|
||||
|
||||
// empties the content of the search box
|
||||
clear_search: function(){
|
||||
var products = this.pos.db.get_product_by_category(this.category.id);
|
||||
this.product_list_widget.set_product_list(products);
|
||||
var input = this.el.querySelector('.searchbox input');
|
||||
input.value = '';
|
||||
input.focus();
|
||||
},
|
||||
perform_search: function(category, query, buy_result){
|
||||
if(query){
|
||||
var products = this.pos.db.search_product_in_category(category.id,query)
|
||||
if(buy_result && products.length === 1){
|
||||
this.pos.get_order().add_product(products[0]);
|
||||
this.clear_search();
|
||||
}else{
|
||||
this.product_list_widget.set_product_list(products);
|
||||
}
|
||||
}else{
|
||||
var products = this.pos.db.get_product_by_category(this.category.id);
|
||||
this.product_list_widget.set_product_list(products);
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
/* --------- The Product List --------- */
|
||||
|
||||
// Display the list of products.
|
||||
// - change the list with .set_product_list()
|
||||
// - click_product_action(), passed as an option, tells
|
||||
// what to do when a product is clicked.
|
||||
|
||||
module.ProductListWidget = module.PosBaseWidget.extend({
|
||||
template:'ProductListWidget',
|
||||
init: function(parent, options) {
|
||||
var self = this;
|
||||
this._super(parent,options);
|
||||
this.model = options.model;
|
||||
this.productwidgets = [];
|
||||
this.weight = options.weight || 0;
|
||||
this.show_scale = options.show_scale || false;
|
||||
this.next_screen = options.next_screen || false;
|
||||
|
||||
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 = new module.DomCache();
|
||||
},
|
||||
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){
|
||||
var cached = this.product_cache.get_node(product.id);
|
||||
if(!cached){
|
||||
var image_url = this.get_product_image_url(product);
|
||||
var product_html = QWeb.render('Product',{
|
||||
widget: this,
|
||||
product: product,
|
||||
image_url: this.get_product_image_url(product),
|
||||
});
|
||||
var product_node = document.createElement('div');
|
||||
product_node.innerHTML = product_html;
|
||||
product_node = product_node.childNodes[1];
|
||||
this.product_cache.cache_node(product.id,product_node);
|
||||
return product_node;
|
||||
}
|
||||
return cached;
|
||||
},
|
||||
|
||||
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 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);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/* -------- The Product Screen -------- */
|
||||
|
||||
module.ProductScreenWidget = module.ScreenWidget.extend({
|
||||
template:'ProductScreenWidget',
|
||||
|
||||
@@ -290,6 +859,14 @@ openerp.point_of_sale.load_screens = function load_screens(instance, module){ //
|
||||
},
|
||||
});
|
||||
|
||||
/*--------------------------------------*\
|
||||
| THE CLIENT LIST |
|
||||
\*======================================*/
|
||||
|
||||
// The clientlist displays the list of customer,
|
||||
// and allows the cashier to create, edit and assign
|
||||
// customers.
|
||||
|
||||
module.ClientListScreenWidget = module.ScreenWidget.extend({
|
||||
template: 'ClientListScreenWidget',
|
||||
|
||||
@@ -669,6 +1246,16 @@ openerp.point_of_sale.load_screens = function load_screens(instance, module){ //
|
||||
},
|
||||
});
|
||||
|
||||
/*--------------------------------------*\
|
||||
| THE RECEIPT SCREEN |
|
||||
\*======================================*/
|
||||
|
||||
// The receipt screen displays the order's
|
||||
// receipt and allows it to be printed in a web browser.
|
||||
// The receipt screen is not shown if the point of sale
|
||||
// is set up to print with the proxy. Altough it could
|
||||
// be useful to do so...
|
||||
|
||||
module.ReceiptScreenWidget = module.ScreenWidget.extend({
|
||||
template: 'ReceiptScreenWidget',
|
||||
show_numpad: false,
|
||||
@@ -744,6 +1331,13 @@ openerp.point_of_sale.load_screens = function load_screens(instance, module){ //
|
||||
},
|
||||
});
|
||||
|
||||
/*--------------------------------------*\
|
||||
| THE PAYMENT SCREEN |
|
||||
\*======================================*/
|
||||
|
||||
// The Payment Screen handles the payments, and
|
||||
// it is unfortunately quite complicated.
|
||||
|
||||
module.PaymentScreenWidget = module.ScreenWidget.extend({
|
||||
template: 'PaymentScreenWidget',
|
||||
back_screen: 'product',
|
||||
|
||||
Reference in New Issue
Block a user