1 function openerp_restaurant_floors(instance,module){
2 var QWeb = instance.web.qweb;
3 var _t = instance.web._t;
5 // At POS Startup, load the floors, and add them to the pos model
6 module.PosModel.prototype.models.push({
7 model: 'restaurant.floor',
8 fields: ['name','background_image','table_ids','sequence'],
9 domain: function(self){ return [['pos_config_id','=',self.config.id]] },
10 loaded: function(self,floors){
12 self.floors_by_id = {};
13 for (var i = 0; i < floors.length; i++) {
14 floors[i].tables = [];
15 self.floors_by_id[floors[i].id] = floors[i];
18 // Make sure they display in the correct order
19 self.floors = self.floors.sort(function(a,b){ return a.sequence - b.sequence; });
21 // Ignore floorplan features if no floor specified.
22 self.config.iface_floorplan = !!self.floors.length;
26 // At POS Startup, after the floors are loaded, load the tables, and associate
27 // them with their floor.
28 module.PosModel.prototype.models.push({
29 model: 'restaurant.table',
30 fields: ['name','width','height','position_h','position_v','shape','floor_id','color'],
31 loaded: function(self,tables){
32 self.tables_by_id = {};
33 for (var i = 0; i < tables.length; i++) {
34 self.tables_by_id[tables[i].id] = tables[i];
35 var floor = self.floors_by_id[tables[i].floor_id[0]];
37 floor.tables.push(tables[i]);
38 tables[i].floor = floor;
44 // The Table GUI element, should always be a child of the FloorScreenWidget
45 module.TableWidget = module.PosBaseWidget.extend({
46 template: 'TableWidget',
47 init: function(parent, options){
48 this._super(parent, options)
49 this.table = options.table;
50 this.selected = false;
52 this.dragpos = {x:0, y:0};
53 this.handle_dragging = false;
56 // computes the absolute position of a DOM mouse event, used
57 // when resizing tables
58 event_position: function(event){
59 if(event.touches && event.touches[0]){
60 return {x: event.touches[0].screenX, y: event.touches[0].screenY};
62 return {x: event.screenX, y: event.screenY};
65 // when a table is clicked, go to the table's orders
66 // but if we're editing, we select/deselect it.
67 click_handler: function(){
69 var floorplan = this.getParent();
70 if (floorplan.editing) {
71 setTimeout(function(){ // in a setTimeout to debounce with drag&drop start
75 } else if (!self.selected) {
76 self.getParent().select_table(self);
78 self.getParent().deselect_tables();
83 floorplan.pos.set_table(this.table);
86 // drag and drop for moving the table, at drag start
87 dragstart_handler: function(event,$el,drag){
88 if (this.selected && !this.handle_dragging) {
90 this.dragpos = { x: drag.offsetX, y: drag.offsetY };
93 // drag and drop for moving the table, at drag end
94 dragend_handler: function(event,$el){
95 this.dragging = false;
97 // drag and drop for moving the table, at every drop movement.
98 dragmove_handler: function(event,$el,drag){
100 var dx = drag.offsetX - this.dragpos.x;
101 var dy = drag.offsetY - this.dragpos.y;
103 this.dragpos = { x: drag.offsetX, y: drag.offsetY };
106 this.table.position_v += dy;
107 this.table.position_h += dx;
109 $el.css(this.table_style());
112 // drag and dropping the resizing handles
113 handle_dragstart_handler: function(event, $el, drag) {
114 if (this.selected && !this.dragging) {
115 this.handle_dragging = true;
116 this.handle_dragpos = this.event_position(event);
117 this.handle = drag.target;
120 handle_dragend_handler: function(event, $el, drag) {
121 this.handle_dragging = false;
123 handle_dragmove_handler: function(event, $el, drag) {
124 if (this.handle_dragging) {
125 var pos = this.event_position(event);
126 var dx = pos.x - this.handle_dragpos.x;
127 var dy = pos.y - this.handle_dragpos.y;
129 this.handle_dragpos = pos;
132 var cl = this.handle.classList;
134 var MIN_SIZE = 40; // smaller than this, and it becomes impossible to edit.
136 var tw = Math.max(MIN_SIZE, this.table.width);
137 var th = Math.max(MIN_SIZE, this.table.height);
138 var tx = this.table.position_h;
139 var ty = this.table.position_v;
141 if (cl.contains('left') && tw - dx >= MIN_SIZE) {
144 } else if (cl.contains('right') && tw + dx >= MIN_SIZE) {
148 if (cl.contains('top') && th - dy >= MIN_SIZE) {
151 } else if (cl.contains('bottom') && th + dy >= MIN_SIZE) {
155 this.table.width = tw;
156 this.table.height = th;
157 this.table.position_h = tx;
158 this.table.position_v = ty;
160 this.$el.css(this.table_style());
163 set_table_color: function(color){
164 this.table.color = color;
165 this.renderElement();
167 set_table_name: function(name){
169 this.table.name = name;
170 this.renderElement();
173 // The table's positioning is handled via css absolute positioning,
174 // which is handled here.
175 table_style: function(){
176 var table = this.table;
177 function unit(val){ return '' + val + 'px'; }
179 'width': unit(table.width),
180 'height': unit(table.height),
181 'line-height': unit(table.height),
182 'margin-left': unit(-table.width/2),
183 'margin-top': unit(-table.height/2),
184 'top': unit(table.position_v + table.height/2),
185 'left': unit(table.position_h + table.width/2),
186 'border-radius': table.shape === 'round' ?
187 unit(Math.max(table.width,table.height)/2) : '3px',
190 style['background-color'] = table.color;
192 if (table.height >= 150 && table.width >= 150) {
193 style['font-size'] = '32px';
198 // convert the style dictionary to a ; separated string for inclusion in templates
199 table_style_str: function(){
200 var style = this.table_style();
203 str += s + ":" + style[s] + "; ";
207 // select the table (should be called via the floorplan)
209 this.selected = true;
210 this.renderElement();
212 // deselect the table (should be called via the floorplan)
213 deselect: function() {
214 this.selected = false;
215 this.renderElement();
218 // sends the table's modification to the server
219 save_changes: function(){
221 var model = new instance.web.Model('restaurant.table');
222 var fields = _.find(this.pos.models,function(model){ return model.model === 'restaurant.table'; }).fields;
224 // we need a serializable copy of the table, containing only the fields defined on the server
225 var serializable_table = {};
226 for (var i = 0; i < fields.length; i++) {
227 if (typeof this.table[fields[i]] !== 'undefined') {
228 serializable_table[fields[i]] = this.table[fields[i]];
232 serializable_table.id = this.table.id
234 model.call('create_from_ui',[serializable_table]).then(function(table_id){
235 model.query(fields).filter([['id','=',table_id]]).first().then(function(table){
236 for (field in table) {
237 self.table[field] = table[field];
239 self.renderElement();
243 // destroy the table. We do not really destroy it, we set it
244 // to inactive so that it doesn't show up anymore, but it still
245 // available on the database for the orders that depend on it.
248 var model = new instance.web.Model('restaurant.table');
249 return model.call('create_from_ui',[{'active':false,'id':this.table.id}]).then(function(table_id){
250 // Removing all references from the table and the table_widget in in the UI ...
251 for (var i = 0; i < self.pos.floors.length; i++) {
252 var floor = self.pos.floors[i];
253 for (var j = 0; j < floor.tables.length; j++) {
254 if (floor.tables[j].id === table_id) {
255 floor.tables.splice(j,1);
260 var floorplan = self.getParent();
261 for (var i = 0; i < floorplan.table_widgets.length; i++) {
262 if (floorplan.table_widgets[i] === self) {
263 floorplan.table_widgets.splice(i,1);
266 if (floorplan.selected_table === self) {
267 floorplan.selected_table = null;
269 floorplan.update_toolbar();
273 get_notifications: function(){ //FIXME : Make this faster
274 var orders = this.pos.get_table_orders(this.table);
275 var notifications = {};
276 for (var i = 0; i < orders.length; i++) {
277 if (orders[i].hasChangesToPrint()) {
278 notifications['printing'] = true;
284 renderElement: function(){
286 this.order_count = this.pos.get_table_orders(this.table).length;
287 this.notifications = this.get_notifications();
290 this.$el.on('mouseup', function(event){ self.click_handler(event,$(this)); });
291 this.$el.on('touchend', function(event){ self.click_handler(event,$(this)); });
292 this.$el.on('touchcancel', function(event){ self.click_handler(event,$(this)); });
293 this.$el.on('dragstart', function(event,drag){ self.dragstart_handler(event,$(this),drag); });
294 this.$el.on('drag', function(event,drag){ self.dragmove_handler(event,$(this),drag); });
295 this.$el.on('dragend', function(event,drag){ self.dragend_handler(event,$(this),drag); });
297 var handles = this.$el.find('.table-handle');
298 handles.on('dragstart', function(event,drag){ self.handle_dragstart_handler(event,$(this),drag); });
299 handles.on('drag', function(event,drag){ self.handle_dragmove_handler(event,$(this),drag); });
300 handles.on('dragend', function(event,drag){ self.handle_dragend_handler(event,$(this),drag); });
304 // The screen that allows you to select the floor, see and select the table,
305 // as well as edit them.
306 module.FloorScreenWidget = module.ScreenWidget.extend({
307 template: 'FloorScreenWidget',
308 show_leftpane: false,
310 // Ignore products, discounts, and client barcodes
311 barcode_product_action: function(code){},
312 barcode_discount_action: function(code){},
313 barcode_client_action: function(code){},
315 init: function(parent, options) {
316 this._super(parent, options);
317 this.floor = this.pos.floors[0];
318 this.table_widgets = [];
319 this.selected_table = null;
320 this.editing = false;
325 this.toggle_editing();
327 this.pos_widget.order_selector.show();
331 this.pos_widget.order_selector.hide();
332 for (var i = 0; i < this.table_widgets.length; i++) {
333 this.table_widgets[i].renderElement();
336 click_floor_button: function(event,$el){
337 var floor = this.pos.floors_by_id[$el.data('id')];
338 if (floor !== this.floor) {
340 this.toggle_editing();
343 this.selected_table = null;
344 this.renderElement();
347 background_image_url: function(floor) {
348 return '/web/binary/image?model=restaurant.floor&id='+floor.id+'&field=background_image';
350 deselect_tables: function(){
351 for (var i = 0; i < this.table_widgets.length; i++) {
352 var table = this.table_widgets[i];
353 if (table.selected) {
357 this.selected_table = null;
358 this.update_toolbar();
360 select_table: function(table_widget){
361 if (!table_widget.selected) {
362 this.deselect_tables();
363 table_widget.select();
364 this.selected_table = table_widget;
365 this.update_toolbar();
368 tool_shape_action: function(){
369 if (this.selected_table) {
370 var table = this.selected_table.table;
371 if (table.shape === 'square') {
372 table.shape = 'round';
374 table.shape = 'square';
376 this.selected_table.renderElement();
377 this.update_toolbar();
380 tool_colorpicker_open: function(){
381 if (this.selected_table) {
382 this.$('.color-picker').removeClass('oe_hidden');
385 tool_colorpicker_pick: function(event,$el){
386 if (this.selected_table) {
387 this.selected_table.set_table_color($el[0].style['background-color']);
390 tool_colorpicker_close: function(){
391 this.$('.color-picker').addClass('oe_hidden');
393 tool_rename_table: function(){
395 if (this.selected_table) {
396 this.pos_widget.screen_selector.show_popup('textinput',{
397 'message':_t('Table Name ?'),
398 'value': this.selected_table.table.name,
399 'confirm': function(value) {
400 self.selected_table.set_table_name(value);
405 tool_duplicate_table: function(){
406 if (this.selected_table) {
407 var tw = this.create_table(this.selected_table.table);
408 tw.table.position_h += 10;
409 tw.table.position_v += 10;
411 this.select_table(tw);
414 tool_new_table: function(){
415 var tw = this.create_table({
423 this.select_table(tw);
425 create_table: function(params) {
427 for (var p in params) {
428 table[p] = params[p];
432 table.floor_id = [this.floor.id,''];
433 table.floor = this.floor;
435 this.floor.tables.push(table);
436 var tw = new module.TableWidget(this,{table: table});
437 tw.appendTo('.floor-map');
438 this.table_widgets.push(tw);
441 tool_trash_table: function(){
443 if (this.selected_table) {
444 this.pos_widget.screen_selector.show_popup('confirm',{
445 'message':_t('Are you sure ?'),
446 'comment':_t('Removing a table cannot be undone'),
447 'confirm': function(){
448 self.selected_table.trash();
453 toggle_editing: function(){
454 this.editing = !this.editing;
455 this.update_toolbar();
458 this.deselect_tables();
461 update_toolbar: function(){
464 this.$('.edit-bar').removeClass('oe_hidden');
465 this.$('.edit-button.editing').addClass('active');
467 this.$('.edit-bar').addClass('oe_hidden');
468 this.$('.edit-button.editing').removeClass('active');
471 if (this.selected_table) {
472 this.$('.needs-selection').removeClass('disabled');
473 var table = this.selected_table.table;
474 if (table.shape === 'square') {
475 this.$('.button-option.square').addClass('oe_hidden');
476 this.$('.button-option.round').removeClass('oe_hidden');
478 this.$('.button-option.square').removeClass('oe_hidden');
479 this.$('.button-option.round').addClass('oe_hidden');
482 this.$('.needs-selection').addClass('disabled');
484 this.tool_colorpicker_close();
486 renderElement: function(){
489 // cleanup table widgets from previous renders
490 for (var i = 0; i < this.table_widgets.length; i++) {
491 this.table_widgets[i].destroy();
494 this.table_widgets = [];
498 for (var i = 0; i < this.floor.tables.length; i++) {
499 var tw = new module.TableWidget(this,{
500 table: this.floor.tables[i],
502 tw.appendTo(this.$('.floor-map'));
503 this.table_widgets.push(tw);
506 this.$('.floor-selector .button').click(function(event){
507 self.click_floor_button(event,$(this));
510 this.$('.edit-button.shape').click(function(event){
511 self.tool_shape_action();
514 this.$('.edit-button.color').click(function(event){
515 self.tool_colorpicker_open();
518 this.$('.edit-button.dup-table').click(function(event){
519 self.tool_duplicate_table();
522 this.$('.edit-button.new-table').click(function(event){
523 self.tool_new_table();
526 this.$('.edit-button.rename').click(function(event){
527 self.tool_rename_table();
530 this.$('.edit-button.trash').click(function(event){
531 self.tool_trash_table();
534 this.$('.color-picker .close-picker').click(function(event){
535 self.tool_colorpicker_close();
536 event.stopPropagation();
539 this.$('.color-picker .color').click(function(event){
540 self.tool_colorpicker_pick(event,$(this));
541 self.tool_colorpicker_close();
542 event.stopPropagation();
545 this.$('.edit-button.editing').click(function(){
546 self.toggle_editing();
549 this.$('.floor-map').click(function(event){
550 if (event.target === self.$('.floor-map')[0]) {
551 self.deselect_tables();
555 this.$('.color-picker .close-picker').click(function(event){
556 self.tool_colorpicker_close();
557 event.stopPropagation();
560 this.update_toolbar();
565 // Add the FloorScreen to the GUI, and set it as the default screen
566 module.PosWidget.include({
567 build_widgets: function(){
570 if (this.pos.config.iface_floorplan) {
571 this.floors_screen = new module.FloorScreenWidget(this,{});
572 this.floors_screen.appendTo(this.$('.screens'));
573 this.screen_selector.add_screen('floors',this.floors_screen);
574 this.screen_selector.change_default_screen('floors');
579 // when the floor plan is activated, we need to go back to the floor plan
580 // when an order is completed. Usually on order completion, a new order is
581 // set as the current one. Now, we set the new order to null.
582 // load_saved_screen() is called whenever the current order is changed, and
583 // will detect this, and set the current screen to the default_screen,
584 // which is the floor plan.
585 module.ScreenSelector.include({
586 load_saved_screen: function(){
587 if (this.pos.config.iface_floorplan) {
588 if (!this.pos.get_order()) {
589 this.set_current_screen(this.default_screen,null,'refresh');
591 this._super({default_screen:'products'});
594 this._super.apply(this,arguments);
599 // New orders are now associated with the current table, if any.
600 var _super_order = module.Order.prototype;
601 module.Order = module.Order.extend({
602 initialize: function(attr) {
603 _super_order.initialize.apply(this,arguments);
605 this.table = this.pos.table;
609 export_as_JSON: function() {
610 var json = _super_order.export_as_JSON.apply(this,arguments);
611 json.table = this.table ? this.table.name : undefined;
612 json.table_id = this.table ? this.table.id : false;
613 json.floor = this.table ? this.table.floor.name : false;
614 json.floor_id = this.table ? this.table.floor.id : false;
617 init_from_JSON: function(json) {
618 _super_order.init_from_JSON.apply(this,arguments);
619 this.table = this.pos.tables_by_id[json.table_id];
620 this.floor = this.table ? this.pos.floors_by_id[json.floor_id] : undefined;
622 export_for_printing: function() {
623 var json = _super_order.export_for_printing.apply(this,arguments);
624 json.table = this.table ? this.table.name : undefined;
625 json.floor = this.table ? this.table.floor.name : undefined;
630 // We need to modify the OrderSelector to hide itself when we're on
632 module.OrderSelectorWidget.include({
633 floor_button_click_handler: function(){
634 this.pos.set_table(null);
637 this.$el.addClass('oe_invisible');
640 this.$el.removeClass('oe_invisible');
642 renderElement: function(){
645 if (this.pos.config.iface_floorplan) {
646 if (this.pos.get_order()) {
647 if (this.pos.table && this.pos.table.floor) {
648 this.$('.orders').prepend(QWeb.render('BackToFloorButton',{table: this.pos.table, floor:this.pos.table.floor}));
649 this.$('.floor-button').click(function(){
650 self.floor_button_click_handler();
653 this.$el.removeClass('oe_invisible');
655 this.$el.addClass('oe_invisible');
661 // We need to change the way the regular UI sees the orders, it
662 // needs to only see the orders associated with the current table,
663 // and when an order is validated, it needs to go back to the floor map.
665 // And when we change the table, we must create an order for that table
667 var _super_posmodel = module.PosModel.prototype;
668 module.PosModel = module.PosModel.extend({
669 initialize: function(session, attributes) {
671 return _super_posmodel.initialize.call(this,session,attributes);
674 // changes the current table.
675 set_table: function(table) {
676 if (!table) { // no table ? go back to the floor plan, see ScreenSelector
677 this.set_order(null);
678 } else { // table ? load the associated orders ...
680 var orders = this.get_order_list();
682 this.set_order(orders[0]); // and go to the first one ...
684 this.add_new_order(); // or create a new order with the current table
689 // if we have tables, we do not load a default order, as the default order will be
690 // set when the user selects a table.
691 set_start_order: function() {
692 if (!this.config.iface_floorplan) {
693 _super_posmodel.set_start_order.apply(this,arguments);
697 // we need to prevent the creation of orders when there is no
699 add_new_order: function() {
700 if (this.config.iface_floorplan) {
702 _super_posmodel.add_new_order.call(this);
704 console.warn("WARNING: orders cannot be created when there is no active table in restaurant mode");
707 _super_posmodel.add_new_order.apply(this,arguments);
712 // get the list of unpaid orders (associated to the current table)
713 get_order_list: function() {
714 var orders = _super_posmodel.get_order_list.call(this);
715 if (!this.config.iface_floorplan) {
717 } else if (!this.table) {
721 for (var i = 0; i < orders.length; i++) {
722 if ( orders[i].table === this.table) {
723 t_orders.push(orders[i]);
730 // get the list of orders associated to a table. FIXME: should be O(1)
731 get_table_orders: function(table) {
732 var orders = _super_posmodel.get_order_list.call(this);
734 for (var i = 0; i < orders.length; i++) {
735 if (orders[i].table === table) {
736 t_orders.push(orders[i]);
742 // When we validate an order we go back to the floor plan.
743 // When we cancel an order and there is multiple orders
744 // on the table, stay on the table.
745 on_removed_order: function(removed_order,index,reason){
746 if (this.config.iface_floorplan) {
747 var order_list = this.get_order_list();
748 if( (reason === 'abandon' || removed_order.temporary) && order_list.length > 0){
749 this.set_order(order_list[index] || order_list[order_list.length -1]);
751 // back to the floor plan
752 this.set_table(null);
755 _super_posmodel.on_removed_order.apply(this,arguments);