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'],
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];
17 // Ignore floorplan features if no floor specified, or feature deactivated
18 self.config.iface_floorplan = self.config.iface_floorplan && !!self.floors.length;
22 // At POS Startup, after the floors are loaded, load the tables, and associate
23 // them with their floor.
24 module.PosModel.prototype.models.push({
25 model: 'restaurant.table',
26 fields: ['name','width','height','position_h','position_v','shape','floor_id','color'],
27 loaded: function(self,tables){
28 self.tables_by_id = {};
29 for (var i = 0; i < tables.length; i++) {
30 self.tables_by_id[tables[i].id] = tables[i];
31 var floor = self.floors_by_id[tables[i].floor_id[0]];
33 floor.tables.push(tables[i]);
34 tables[i].floor = floor;
40 // The Table GUI element, should always be a child of the FloorScreenWidget
41 module.TableWidget = module.PosBaseWidget.extend({
42 template: 'TableWidget',
43 init: function(parent, options){
44 this._super(parent, options)
45 this.table = options.table;
46 this.selected = false;
48 this.dragpos = {x:0, y:0};
49 this.handle_dragging = false;
52 // computes the absolute position of a DOM mouse event, used
53 // when resizing tables
54 event_position: function(event){
55 if(event.touches && event.touches[0]){
56 return {x: event.touches[0].screenX, y: event.touches[0].screenY};
58 return {x: event.screenX, y: event.screenY};
61 // when a table is clicked, go to the table's orders
62 // but if we're editing, we select/deselect it.
63 click_handler: function(){
65 var floorplan = this.getParent();
66 if (floorplan.editing) {
67 setTimeout(function(){ // in a setTimeout to debounce with drag&drop start
71 } else if (!self.selected) {
72 self.getParent().select_table(self);
74 self.getParent().deselect_tables();
79 floorplan.pos.set_table(this.table);
82 // drag and drop for moving the table, at drag start
83 dragstart_handler: function(event,$el,drag){
84 if (this.selected && !this.handle_dragging) {
86 this.dragpos = { x: drag.offsetX, y: drag.offsetY };
89 // drag and drop for moving the table, at drag end
90 dragend_handler: function(event,$el){
91 this.dragging = false;
93 // drag and drop for moving the table, at every drop movement.
94 dragmove_handler: function(event,$el,drag){
96 var dx = drag.offsetX - this.dragpos.x;
97 var dy = drag.offsetY - this.dragpos.y;
99 this.dragpos = { x: drag.offsetX, y: drag.offsetY };
102 this.table.position_v += dy;
103 this.table.position_h += dx;
105 $el.css(this.table_style());
108 // drag and dropping the resizing handles
109 handle_dragstart_handler: function(event, $el, drag) {
110 if (this.selected && !this.dragging) {
111 this.handle_dragging = true;
112 this.handle_dragpos = this.event_position(event);
113 this.handle = drag.target;
116 handle_dragend_handler: function(event, $el, drag) {
117 this.handle_dragging = false;
119 handle_dragmove_handler: function(event, $el, drag) {
120 if (this.handle_dragging) {
121 var pos = this.event_position(event);
122 var dx = pos.x - this.handle_dragpos.x;
123 var dy = pos.y - this.handle_dragpos.y;
125 this.handle_dragpos = pos;
128 var cl = this.handle.classList;
130 var MIN_SIZE = 40; // smaller than this, and it becomes impossible to edit.
132 var tw = Math.max(MIN_SIZE, this.table.width);
133 var th = Math.max(MIN_SIZE, this.table.height);
134 var tx = this.table.position_h;
135 var ty = this.table.position_v;
137 if (cl.contains('left') && tw - dx >= MIN_SIZE) {
140 } else if (cl.contains('right') && tw + dx >= MIN_SIZE) {
144 if (cl.contains('top') && th - dy >= MIN_SIZE) {
147 } else if (cl.contains('bottom') && th + dy >= MIN_SIZE) {
151 this.table.width = tw;
152 this.table.height = th;
153 this.table.position_h = tx;
154 this.table.position_v = ty;
156 this.$el.css(this.table_style());
159 set_table_color: function(color){
160 this.table.color = color;
161 this.renderElement();
163 set_table_name: function(name){
165 this.table.name = name;
166 this.renderElement();
169 // The table's positioning is handled via css absolute positioning,
170 // which is handled here.
171 table_style: function(){
172 var table = this.table;
173 function unit(val){ return '' + val + 'px'; }
175 'width': unit(table.width),
176 'height': unit(table.height),
177 'line-height': unit(table.height),
178 'margin-left': unit(-table.width/2),
179 'margin-top': unit(-table.height/2),
180 'top': unit(table.position_v + table.height/2),
181 'left': unit(table.position_h + table.width/2),
182 'border-radius': table.shape === 'round' ?
183 unit(Math.max(table.width,table.height)/2) : '3px',
186 style['background-color'] = table.color;
188 if (table.height >= 150 && table.width >= 150) {
189 style['font-size'] = '32px';
194 // convert the style dictionary to a ; separated string for inclusion in templates
195 table_style_str: function(){
196 var style = this.table_style();
199 str += s + ":" + style[s] + "; ";
203 // select the table (should be called via the floorplan)
205 this.selected = true;
206 this.renderElement();
208 // deselect the table (should be called via the floorplan)
209 deselect: function() {
210 this.selected = false;
211 this.renderElement();
214 // sends the table's modification to the server
215 save_changes: function(){
217 var model = new instance.web.Model('restaurant.table');
218 var fields = _.find(this.pos.models,function(model){ return model.model === 'restaurant.table'; }).fields;
220 // we need a serializable copy of the table, containing only the fields defined on the server
221 var serializable_table = {};
222 for (var i = 0; i < fields.length; i++) {
223 if (typeof this.table[fields[i]] !== 'undefined') {
224 serializable_table[fields[i]] = this.table[fields[i]];
228 serializable_table.id = this.table.id
230 model.call('create_from_ui',[serializable_table]).then(function(table_id){
231 model.query(fields).filter([['id','=',table_id]]).first().then(function(table){
232 for (field in table) {
233 self.table[field] = table[field];
235 self.renderElement();
239 // destroy the table. We do not really destroy it, we set it
240 // to inactive so that it doesn't show up anymore, but it still
241 // available on the database for the orders that depend on it.
244 var model = new instance.web.Model('restaurant.table');
245 return model.call('create_from_ui',[{'active':false,'id':this.table.id}]).then(function(table_id){
246 // Removing all references from the table and the table_widget in in the UI ...
247 for (var i = 0; i < self.pos.floors.length; i++) {
248 var floor = self.pos.floors[i];
249 for (var j = 0; j < floor.tables.length; j++) {
250 if (floor.tables[j].id === table_id) {
251 floor.tables.splice(j,1);
256 var floorplan = self.getParent();
257 for (var i = 0; i < floorplan.table_widgets.length; i++) {
258 if (floorplan.table_widgets[i] === self) {
259 floorplan.table_widgets.splice(i,1);
262 if (floorplan.selected_table === self) {
263 floorplan.selected_table = null;
265 floorplan.update_toolbar();
269 get_notifications: function(){ //FIXME : Make this faster
270 var orders = this.pos.get_table_orders(this.table);
271 var notifications = {};
272 for (var i = 0; i < orders.length; i++) {
273 if (orders[i].hasChangesToPrint()) {
274 notifications['printing'] = true;
280 renderElement: function(){
282 this.order_count = this.pos.get_table_orders(this.table).length;
283 this.notifications = this.get_notifications();
286 this.$el.on('mouseup', function(event){ self.click_handler(event,$(this)); });
287 this.$el.on('touchend', function(event){ self.click_handler(event,$(this)); });
288 this.$el.on('touchcancel', function(event){ self.click_handler(event,$(this)); });
289 this.$el.on('dragstart', function(event,drag){ self.dragstart_handler(event,$(this),drag); });
290 this.$el.on('drag', function(event,drag){ self.dragmove_handler(event,$(this),drag); });
291 this.$el.on('dragend', function(event,drag){ self.dragend_handler(event,$(this),drag); });
293 var handles = this.$el.find('.table-handle');
294 handles.on('dragstart', function(event,drag){ self.handle_dragstart_handler(event,$(this),drag); });
295 handles.on('drag', function(event,drag){ self.handle_dragmove_handler(event,$(this),drag); });
296 handles.on('dragend', function(event,drag){ self.handle_dragend_handler(event,$(this),drag); });
300 // The screen that allows you to select the floor, see and select the table,
301 // as well as edit them.
302 module.FloorScreenWidget = module.ScreenWidget.extend({
303 template: 'FloorScreenWidget',
304 show_leftpane: false,
306 // Ignore products, discounts, and client barcodes
307 barcode_product_action: function(code){},
308 barcode_discount_action: function(code){},
309 barcode_client_action: function(code){},
311 init: function(parent, options) {
312 this._super(parent, options);
313 this.floor = this.pos.floors[0];
314 this.table_widgets = [];
315 this.selected_table = null;
316 this.editing = false;
321 this.toggle_editing();
323 this.pos_widget.order_selector.show();
327 this.pos_widget.order_selector.hide();
328 for (var i = 0; i < this.table_widgets.length; i++) {
329 this.table_widgets[i].renderElement();
332 click_floor_button: function(event,$el){
333 var floor = this.pos.floors_by_id[$el.data('id')];
334 if (floor !== this.floor) {
336 this.toggle_editing();
339 this.selected_table = null;
340 this.renderElement();
343 background_image_url: function(floor) {
344 return '/web/binary/image?model=restaurant.floor&id='+floor.id+'&field=background_image';
346 deselect_tables: function(){
347 for (var i = 0; i < this.table_widgets.length; i++) {
348 var table = this.table_widgets[i];
349 if (table.selected) {
353 this.selected_table = null;
354 this.update_toolbar();
356 select_table: function(table_widget){
357 if (!table_widget.selected) {
358 this.deselect_tables();
359 table_widget.select();
360 this.selected_table = table_widget;
361 this.update_toolbar();
364 tool_shape_action: function(){
365 if (this.selected_table) {
366 var table = this.selected_table.table;
367 if (table.shape === 'square') {
368 table.shape = 'round';
370 table.shape = 'square';
372 this.selected_table.renderElement();
373 this.update_toolbar();
376 tool_colorpicker_open: function(){
377 if (this.selected_table) {
378 this.$('.color-picker').removeClass('oe_hidden');
381 tool_colorpicker_pick: function(event,$el){
382 if (this.selected_table) {
383 this.selected_table.set_table_color($el[0].style['background-color']);
386 tool_colorpicker_close: function(){
387 this.$('.color-picker').addClass('oe_hidden');
389 tool_rename_table: function(){
391 if (this.selected_table) {
392 this.pos_widget.screen_selector.show_popup('textinput',{
393 'message':_t('Table Name ?'),
394 'value': this.selected_table.table.name,
395 'confirm': function(value) {
396 self.selected_table.set_table_name(value);
401 tool_duplicate_table: function(){
402 if (this.selected_table) {
403 var tw = this.create_table(this.selected_table.table);
404 tw.table.position_h += 10;
405 tw.table.position_v += 10;
407 this.select_table(tw);
410 tool_new_table: function(){
411 var tw = this.create_table({
419 this.select_table(tw);
421 create_table: function(params) {
423 for (var p in params) {
424 table[p] = params[p];
428 table.floor_id = [this.floor.id,''];
429 table.floor = this.floor;
431 this.floor.tables.push(table);
432 var tw = new module.TableWidget(this,{table: table});
433 tw.appendTo('.floor-map');
434 this.table_widgets.push(tw);
437 tool_trash_table: function(){
439 if (this.selected_table) {
440 this.pos_widget.screen_selector.show_popup('confirm',{
441 'message':_t('Are you sure ?'),
442 'comment':_t('Removing a table cannot be undone'),
443 'confirm': function(){
444 self.selected_table.trash();
449 toggle_editing: function(){
450 this.editing = !this.editing;
451 this.update_toolbar();
454 this.deselect_tables();
457 update_toolbar: function(){
460 this.$('.edit-bar').removeClass('oe_hidden');
461 this.$('.edit-button.editing').addClass('active');
463 this.$('.edit-bar').addClass('oe_hidden');
464 this.$('.edit-button.editing').removeClass('active');
467 if (this.selected_table) {
468 this.$('.needs-selection').removeClass('disabled');
469 var table = this.selected_table.table;
470 if (table.shape === 'square') {
471 this.$('.button-option.square').addClass('oe_hidden');
472 this.$('.button-option.round').removeClass('oe_hidden');
474 this.$('.button-option.square').removeClass('oe_hidden');
475 this.$('.button-option.round').addClass('oe_hidden');
478 this.$('.needs-selection').addClass('disabled');
480 this.tool_colorpicker_close();
482 renderElement: function(){
485 // cleanup table widgets from previous renders
486 for (var i = 0; i < this.table_widgets.length; i++) {
487 this.table_widgets[i].destroy();
490 this.table_widgets = [];
494 for (var i = 0; i < this.floor.tables.length; i++) {
495 var tw = new module.TableWidget(this,{
496 table: this.floor.tables[i],
498 tw.appendTo(this.$('.floor-map'));
499 this.table_widgets.push(tw);
502 this.$('.floor-selector .button').click(function(event){
503 self.click_floor_button(event,$(this));
506 this.$('.edit-button.shape').click(function(event){
507 self.tool_shape_action();
510 this.$('.edit-button.color').click(function(event){
511 self.tool_colorpicker_open();
514 this.$('.edit-button.dup-table').click(function(event){
515 self.tool_duplicate_table();
518 this.$('.edit-button.new-table').click(function(event){
519 self.tool_new_table();
522 this.$('.edit-button.rename').click(function(event){
523 self.tool_rename_table();
526 this.$('.edit-button.trash').click(function(event){
527 self.tool_trash_table();
530 this.$('.color-picker .close-picker').click(function(event){
531 self.tool_colorpicker_close();
532 event.stopPropagation();
535 this.$('.color-picker .color').click(function(event){
536 self.tool_colorpicker_pick(event,$(this));
537 self.tool_colorpicker_close();
538 event.stopPropagation();
541 this.$('.edit-button.editing').click(function(){
542 self.toggle_editing();
545 this.$('.floor-map').click(function(event){
546 if (event.target === self.$('.floor-map')[0]) {
547 self.deselect_tables();
551 this.$('.color-picker .close-picker').click(function(event){
552 self.tool_colorpicker_close();
553 event.stopPropagation();
556 this.update_toolbar();
561 // Add the FloorScreen to the GUI, and set it as the default screen
562 module.PosWidget.include({
563 build_widgets: function(){
566 if (this.pos.config.iface_floorplan) {
567 this.floors_screen = new module.FloorScreenWidget(this,{});
568 this.floors_screen.appendTo(this.$('.screens'));
569 this.screen_selector.add_screen('floors',this.floors_screen);
570 this.screen_selector.change_default_screen('floors');
575 // when the floor plan is activated, we need to go back to the floor plan
576 // when an order is completed. Usually on order completion, a new order is
577 // set as the current one. Now, we set the new order to null.
578 // load_saved_screen() is called whenever the current order is changed, and
579 // will detect this, and set the current screen to the default_screen,
580 // which is the floor plan.
581 module.ScreenSelector.include({
582 load_saved_screen: function(){
583 if (this.pos.config.iface_floorplan) {
584 if (!this.pos.get_order()) {
585 this.set_current_screen(this.default_screen,null,'refresh');
587 this._super({default_screen:'products'});
590 this._super.apply(this,arguments);
595 // New orders are now associated with the current table, if any.
596 var _super_order = module.Order.prototype;
597 module.Order = module.Order.extend({
598 initialize: function(attr) {
599 _super_order.initialize.apply(this,arguments);
601 this.table = this.pos.table;
605 export_as_JSON: function() {
606 var json = _super_order.export_as_JSON.apply(this,arguments);
607 json.table = this.table ? this.table.name : undefined;
608 json.table_id = this.table ? this.table.id : false;
609 json.floor = this.table ? this.table.floor.name : false;
610 json.floor_id = this.table ? this.table.floor.id : false;
613 init_from_JSON: function(json) {
614 _super_order.init_from_JSON.apply(this,arguments);
615 this.table = this.pos.tables_by_id[json.table_id];
616 this.floor = this.table ? this.pos.floors_by_id[json.floor_id] : undefined;
618 export_for_printing: function() {
619 var json = _super_order.export_for_printing.apply(this,arguments);
620 json.table = this.table ? this.table.name : undefined;
621 json.floor = this.table ? this.table.floor.name : undefined;
626 // We need to modify the OrderSelector to hide itself when we're on
628 module.OrderSelectorWidget.include({
629 floor_button_click_handler: function(){
630 this.pos.set_table(null);
633 this.$el.addClass('oe_invisible');
636 this.$el.removeClass('oe_invisible');
638 renderElement: function(){
641 if (this.pos.config.iface_floorplan) {
642 if (this.pos.get_order()) {
643 if (this.pos.table && this.pos.table.floor) {
644 this.$('.orders').prepend(QWeb.render('BackToFloorButton',{table: this.pos.table, floor:this.pos.table.floor}));
645 this.$('.floor-button').click(function(){
646 self.floor_button_click_handler();
649 this.$el.removeClass('oe_invisible');
651 this.$el.addClass('oe_invisible');
657 // We need to change the way the regular UI sees the orders, it
658 // needs to only see the orders associated with the current table,
659 // and when an order is validated, it needs to go back to the floor map.
661 // And when we change the table, we must create an order for that table
663 var _super_posmodel = module.PosModel.prototype;
664 module.PosModel = module.PosModel.extend({
665 initialize: function(session, attributes) {
667 return _super_posmodel.initialize.call(this,session,attributes);
670 // changes the current table.
671 set_table: function(table) {
672 if (!table) { // no table ? go back to the floor plan, see ScreenSelector
673 this.set_order(null);
674 } else { // table ? load the associated orders ...
676 var orders = this.get_order_list();
678 this.set_order(orders[0]); // and go to the first one ...
680 this.add_new_order(); // or create a new order with the current table
685 // if we have tables, we do not load a default order, as the default order will be
686 // set when the user selects a table.
687 set_start_order: function() {
688 if (!this.config.iface_floorplan) {
689 _super_posmodel.set_start_order.apply(this,arguments);
693 // we need to prevent the creation of orders when there is no
695 add_new_order: function() {
696 if (this.config.iface_floorplan) {
698 _super_posmodel.add_new_order.call(this);
700 console.warn("WARNING: orders cannot be created when there is no active table in restaurant mode");
703 _super_posmodel.add_new_order.apply(this,arguments);
708 // get the list of unpaid orders (associated to the current table)
709 get_order_list: function() {
710 var orders = _super_posmodel.get_order_list.call(this);
711 if (!this.config.iface_floorplan) {
713 } else if (!this.table) {
717 for (var i = 0; i < orders.length; i++) {
718 if ( orders[i].table === this.table) {
719 t_orders.push(orders[i]);
726 // get the list of orders associated to a table. FIXME: should be O(1)
727 get_table_orders: function(table) {
728 var orders = _super_posmodel.get_order_list.call(this);
730 for (var i = 0; i < orders.length; i++) {
731 if (orders[i].table === table) {
732 t_orders.push(orders[i]);
738 // When we validate an order we go back to the floor plan.
739 // When we cancel an order and there is multiple orders
740 // on the table, stay on the table.
741 on_removed_order: function(removed_order,index,reason){
742 if (this.config.iface_floorplan) {
743 var order_list = this.get_order_list();
744 if( (reason === 'abandon' || removed_order.temporary) && order_list.length > 0){
745 this.set_order(order_list[index] || order_list[order_list.length -1]);
747 // back to the floor plan
748 this.set_table(null);
751 _super_posmodel.on_removed_order.apply(this,arguments);