Skip to content

Commit b42b0f8

Browse files
committed
[IMP] pos_restaurant: floor background picture
Before this PR: It was not possible to use a custom image for the background of a floor plan. Now: It is now possible to add an image to the floor plan in order to use it as the background. If a colour is selected when a custom image is used, the image will be removed from the record. The custom background image cannot be used in kanban view of the floor plan. Task-3616978 closes odoo#156460 Signed-off-by: Joseph Caburnay (jcb) <jcb@odoo.com>
1 parent 096f5b1 commit b42b0f8

File tree

15 files changed

+199
-67
lines changed

15 files changed

+199
-67
lines changed

addons/point_of_sale/static/src/app/main.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,7 @@ whenReady(() => {
4242
}
4343
if (hasTouch()) {
4444
classList.add("o_touch_device");
45+
classList.add("o_mobile_overscroll");
46+
document.documentElement.classList.add("o_mobile_overscroll");
4547
}
4648
})();

addons/point_of_sale/static/src/app/models/data_service.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ export class PosData extends Reactive {
197197

198198
const dataToUpdate = {};
199199
const keysToUpdate = Object.keys(vals);
200-
const serializedRecords = record.serialize();
200+
const serializedRecords = record.serialize(true);
201201

202202
for (const key of keysToUpdate) {
203203
dataToUpdate[key] = serializedRecords[key];

addons/point_of_sale/static/src/app/models/related_models.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -583,11 +583,11 @@ export function createRelatedModels(modelDefs, modelClasses = {}, indexes = {})
583583
for (const name in fields) {
584584
const field = fields[name];
585585
if (field.type === "many2one") {
586-
result[name] = record[name] ? record[name].id : undefined;
586+
result[name] = record[name] ? record[name].id : null;
587587
} else if (X2MANY_TYPES.has(field.type)) {
588588
result[name] = [...record[name]].map((record) => record.id);
589589
} else {
590-
result[name] = record[name];
590+
result[name] = record[name] || null;
591591
}
592592
}
593593
return result;

addons/point_of_sale/static/src/app/screens/partner_list/partner_editor/partner_editor.js

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,17 +130,15 @@ export class PartnerEditor extends Component {
130130
});
131131
} else {
132132
const imageUrl = await getDataURLFromFile(file);
133-
const loadedImage = await loadImage(imageUrl, {
134-
onError: () => {
135-
this.dialog.add(AlertDialog, {
136-
title: _t("Loading Image Error"),
137-
body: _t("Encountered error when loading image. Please try again."),
138-
});
139-
},
140-
});
133+
const loadedImage = await loadImage(imageUrl);
141134
if (loadedImage) {
142135
const resizedImage = await this._resizeImage(loadedImage, 800, 600);
143136
this.changes.image_1920 = resizedImage.toDataURL();
137+
} else {
138+
this.dialog.add(AlertDialog, {
139+
title: _t("Loading Image Error"),
140+
body: _t("Encountered error when loading image. Please try again."),
141+
});
144142
}
145143
}
146144
}

addons/point_of_sale/static/src/app/screens/product_screen/product_screen.js

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,26 +95,20 @@ export class ProductScreen extends Component {
9595
? [...selectedCategory.child_id]
9696
: this.pos.models["pos.category"].filter((category) => !category.parent_id);
9797
}
98-
getChildCategoriesInfo(selectedCategory) {
99-
return this.getCategoriesInfo(this.getChildCategories(selectedCategory), selectedCategory);
100-
}
10198
getCategoriesAndSub() {
10299
return this.getAncestorsAndCurrent().flatMap((category) =>
103100
this.getChildCategoriesInfo(category)
104101
);
105102
}
106103

107-
getCategoriesInfo(categories, selectedCategory) {
108-
return categories.map((category) => ({
104+
getChildCategoriesInfo(selectedCategory) {
105+
return this.getChildCategories(selectedCategory).map((category) => ({
109106
...pick(category, "id", "name", "color"),
110107
imgSrc:
111108
this.pos.config.show_category_images && category.has_image
112109
? `/web/image?model=pos.category&field=image_128&id=${category.id}`
113110
: undefined,
114-
isSelected:
115-
selectedCategory &&
116-
this.getAncestorsAndCurrent().includes(category) &&
117-
!this.ui.isSmall,
111+
isSelected: this.getAncestorsAndCurrent().includes(category) && !this.ui.isSmall,
118112
isChildren: this.getChildCategories(this.pos.selectedCategory).includes(category),
119113
}));
120114
}

addons/point_of_sale/static/src/scss/pos.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ button {
7070
/*rtl:ignore*/
7171
direction: ltr;
7272
}
73+
.o_mobile_overscroll {
74+
overscroll-behavior: none;
75+
}
7376

7477
.pos .button.validation.load-order-button {
7578
height: calc(var(--btn-height-size) * 2);

addons/pos_restaurant/models/pos_restaurant.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class RestaurantFloor(models.Model):
1818
table_ids = fields.One2many('restaurant.table', 'floor_id', string='Tables')
1919
sequence = fields.Integer('Sequence', default=1)
2020
active = fields.Boolean(default=True)
21+
floor_background_image = fields.Image(string='Floor Background Image')
2122

2223
@api.ondelete(at_uninstall=False)
2324
def _unlink_except_active_pos_session(self):

addons/pos_restaurant/models/pos_session.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def _load_data_params(self, config_id):
1616
params.update({
1717
'restaurant.floor': {
1818
'domain': [('pos_config_ids', '=', self.config_id.id)],
19-
'fields': ['name', 'background_color', 'table_ids', 'sequence'],
19+
'fields': ['name', 'background_color', 'table_ids', 'sequence', 'floor_background_image'],
2020
},
2121
'restaurant.table': {
2222
'domain': lambda data: [('active', '=', True), ('floor_id', 'in', [floor['id'] for floor in data['restaurant.floor']])],

addons/pos_restaurant/static/src/app/floor_screen/floor_screen.js

Lines changed: 109 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
1212
import { Table } from "@pos_restaurant/app/floor_screen/table";
1313
import { usePos } from "@point_of_sale/app/store/pos_hook";
1414
import { useService } from "@web/core/utils/hooks";
15-
import { Component, onMounted, useRef, useState, onWillStart } from "@odoo/owl";
15+
import { Component, onMounted, useRef, useState, onWillStart, useEffect } from "@odoo/owl";
1616
import { ask } from "@point_of_sale/app/store/make_awaitable_dialog";
1717
import { unique } from "@web/core/utils/arrays";
18+
import { loadImage } from "@point_of_sale/utils";
19+
import { getDataURLFromFile } from "@web/core/utils/urls";
20+
import { hasTouch } from "@web/core/browser/feature_detection";
1821

1922
export class FloorScreen extends Component {
2023
static components = { Table };
@@ -28,16 +31,73 @@ export class FloorScreen extends Component {
2831
const floor = this.pos.currentFloor;
2932
this.state = useState({
3033
selectedFloorId: floor ? floor.id : null,
34+
floorHeight: "100%",
35+
floorWidth: "100%",
3136
selectedTableIds: this.getTablesSelectedByDefault(),
3237
isColorPicker: false,
3338
});
3439
this.floorMapRef = useRef("floor-map-ref");
40+
this.floorScrollBox = useRef("floor-map-scroll");
3541
const ui = useState(useService("ui"));
3642
const mode = localStorage.getItem("floorPlanStyle");
3743
this.pos.floorPlanStyle = ui.isSmall || mode == "kanban" ? "kanban" : "default";
3844
this.map = useRef("map");
39-
onMounted(() => this.pos.openCashControl());
45+
onMounted(() => {
46+
this.pos.openCashControl();
47+
});
4048
onWillStart(this.onWillStart);
49+
useEffect(
50+
() => {
51+
this.computeFloorSize();
52+
},
53+
() => [this.activeFloor, this.pos.floorPlanStyle]
54+
);
55+
useEffect(
56+
(tableL) => {
57+
if (hasTouch()) {
58+
if (tableL) {
59+
this.floorScrollBox.el.classList.remove("overflow-scroll");
60+
this.floorScrollBox.el.classList.add("overflow-hidden");
61+
} else {
62+
this.floorScrollBox.el.classList.remove("overflow-hidden");
63+
this.floorScrollBox.el.classList.add("overflow-scroll");
64+
}
65+
}
66+
},
67+
() => [this.state.selectedTableIds.length]
68+
);
69+
}
70+
computeFloorSize() {
71+
if (this.pos.floorPlanStyle === "kanban") {
72+
this.state.floorHeight = "100%";
73+
this.state.floorWidth = window.innerWidth + "px";
74+
return;
75+
}
76+
const tables = this.activeFloor.table_ids;
77+
const floorV = this.floorMapRef.el.clientHeight;
78+
const floorH = this.floorMapRef.el.offsetWidth;
79+
const positionH = Math.max(
80+
...tables.map((table) => table.position_h + table.width),
81+
floorH
82+
);
83+
const positionV = Math.max(
84+
...tables.map((table) => table.position_v + table.height),
85+
floorV
86+
);
87+
88+
if (this.activeFloor.floor_background_image) {
89+
const img = new Image();
90+
img.onload = () => {
91+
const height = Math.max(img.height, positionV);
92+
const width = Math.max(img.width, positionH);
93+
this.state.floorHeight = `${height}px`;
94+
this.state.floorWidth = `${width}px`;
95+
};
96+
img.src = "data:image/png;base64," + this.activeFloor.floor_background_image;
97+
} else {
98+
this.state.floorHeight = `${positionV}px`;
99+
this.state.floorWidth = `${positionH}px`;
100+
}
41101
}
42102
getTablesSelectedByDefault() {
43103
return this.pos.orderToTransfer ? [this.pos.orderToTransfer.tableId] : [];
@@ -67,6 +127,11 @@ export class FloorScreen extends Component {
67127
}
68128
await this.pos.unsetTable();
69129
}
130+
get floorBackround() {
131+
return this.activeFloor.floor_background_image
132+
? "data:image/png;base64," + this.activeFloor.floor_background_image
133+
: "none";
134+
}
70135
onClickFloorMap() {
71136
this.state.selectedTableIds = this.getTablesSelectedByDefault();
72137
this.state.isColorPicker = false;
@@ -303,7 +368,9 @@ export class FloorScreen extends Component {
303368
}
304369
unselectTables() {
305370
if (this.selectedTables.length) {
306-
this.pos.updateTables(...this.selectedTables);
371+
for (const table of this.selectedTables) {
372+
this.pos.data.write("restaurant.table", [table.id], table.serialize(true));
373+
}
307374
}
308375
this.state.selectedTableIds = [];
309376
}
@@ -386,8 +453,9 @@ export class FloorScreen extends Component {
386453
title: _t("Table Name ?"),
387454
getPayload: (newName) => {
388455
if (newName !== this.selectedTables[0].name) {
389-
this.selectedTables[0].name = newName;
390-
this.pos.updateTables(this.selectedTables[0]);
456+
this.pos.data.write("restaurant.table", [this.selectedTables[0].id], {
457+
name: newName,
458+
});
391459
}
392460
},
393461
}
@@ -419,8 +487,9 @@ export class FloorScreen extends Component {
419487
const newSeatsNum = parseInt(num, 10);
420488
selectedTables.forEach((selectedTable) => {
421489
if (newSeatsNum !== selectedTable.seats) {
422-
selectedTable.seats = newSeatsNum;
423-
this.pos.updateTables(selectedTable);
490+
this.pos.data.write("restaurant.table", [selectedTable.id], {
491+
seats: newSeatsNum,
492+
});
424493
}
425494
});
426495
},
@@ -434,15 +503,13 @@ export class FloorScreen extends Component {
434503
}
435504
changeShape(form) {
436505
for (const table of this.selectedTables) {
437-
table.shape = form;
506+
this.pos.data.write("restaurant.table", [table.id], { shape: form });
438507
}
439-
this.pos.updateTables(...this.selectedTables);
440508
}
441509
unlinkTables() {
442510
for (const table of this.selectedTables) {
443-
table.update({ parent_id: null });
511+
this.pos.data.write("restaurant.table", [table.id], { parent_id: false });
444512
}
445-
this.pos.updateTables(...this.selectedTables);
446513
}
447514
linkTables() {
448515
const parentTable =
@@ -465,14 +532,14 @@ export class FloorScreen extends Component {
465532
}
466533
setColor(color) {
467534
if (this.selectedTables.length > 0) {
468-
this.selectedTables.forEach((selectedTable) => {
469-
selectedTable.color = color;
470-
});
471-
this.pos.updateTables(...this.selectedTables);
535+
for (const table of this.selectedTables) {
536+
this.pos.data.write("restaurant.table", [table.id], { color: color });
537+
}
472538
} else {
473539
this.activeFloor.background_color = color;
474540
this.pos.data.write("restaurant.floor", [this.activeFloor.id], {
475541
background_color: color,
542+
floor_background_image: false,
476543
});
477544
}
478545
this.state.isColorPicker = false;
@@ -622,6 +689,33 @@ export class FloorScreen extends Component {
622689
getChildren(table) {
623690
return this.pos.models["restaurant.table"].filter((t) => t.parent_id?.id === table.id);
624691
}
692+
async uploadImage(event) {
693+
const file = event.target.files[0];
694+
if (!file.type.match(/image.*/)) {
695+
this.dialog.add(AlertDialog, {
696+
title: _t("Unsupported File Format"),
697+
body: _t("Only web-compatible Image formats such as .png or .jpeg are supported."),
698+
});
699+
} else {
700+
const imageUrl = await getDataURLFromFile(file);
701+
const loadedImage = await loadImage(imageUrl);
702+
if (loadedImage) {
703+
this.env.services.ui.block();
704+
await this.pos.data.ormWrite("restaurant.floor", [this.activeFloor.id], {
705+
floor_background_image: imageUrl.split(",")[1],
706+
});
707+
// A read is added to be sure that we have the same image as the one in backend
708+
await this.pos.data.read("restaurant.floor", [this.activeFloor.id]);
709+
this.env.services.ui.unblock();
710+
} else {
711+
this.dialog.add(AlertDialog, {
712+
title: _t("Loading Image Error"),
713+
body: _t("Encountered error when loading image. Please try again."),
714+
});
715+
}
716+
this.state.isColorPicker = false;
717+
}
718+
}
625719
}
626720

627721
registry.category("pos_screens").add("FloorScreen", FloorScreen);

addons/pos_restaurant/static/src/app/floor_screen/floor_screen.scss

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,26 @@
1919
}
2020
}
2121
}
22+
.floor-picture {
23+
height: 48px;
24+
width: 48px;
25+
& > img {
26+
position: absolute;
27+
top: -9999px;
28+
bottom: -9999px;
29+
right: -9999px;
30+
left: -9999px;
31+
max-height: 64px;
32+
margin: auto;
33+
}
34+
.image-uploader {
35+
position: absolute;
36+
z-index: 1000;
37+
top: 0;
38+
left: 0;
39+
right: 0;
40+
bottom: 0;
41+
opacity: 0;
42+
cursor: pointer;
43+
}
44+
}

0 commit comments

Comments
 (0)