[IMP] pos_self_order: various modifications
Previously, QR codes could only be obtained by printing an A4 page. The problem The problem was that some customers would want to create a specific design and get the raw image of the QR code directly. A button has now been added to the settings to download a zip file containing all the QR codes in PNG format. --- A change has been made to the restaurant table form to enable editing of the access ID. editing of the access ID. This is useful if a customer has reset his QR codes and wishes to put them back. --- Previously, the "close" button in the table selection popup didn't work. This has been corrected with this PR. --- Small design change to the display of products that don't have an image in the selfOrder. Part-of: odoo/odoo#139287
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
"views/pos_config_view.xml",
|
||||
"views/pos_session_view.xml",
|
||||
"views/custom_link_views.xml",
|
||||
"views/pos_restaurant_views.xml",
|
||||
"views/product_views.xml",
|
||||
"data/init_access.xml",
|
||||
"data/custom_link_data.xml",
|
||||
|
||||
@@ -14,7 +14,6 @@ class RestaurantTable(models.Model):
|
||||
"Security Token",
|
||||
copy=False,
|
||||
required=True,
|
||||
readonly=True,
|
||||
default=lambda self: self._get_identifier(),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# -*- codingimport base64
|
||||
import qrcode
|
||||
import base64
|
||||
import zipfile
|
||||
from io import BytesIO
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
@@ -74,6 +78,68 @@ class ResConfigSettings(models.TransientModel):
|
||||
"domain": ['|', ['pos_config_ids', 'in', self.pos_config_id.id], ["pos_config_ids", "=", False]],
|
||||
}
|
||||
|
||||
def _generate_single_qr_code(self, url):
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||
box_size=10,
|
||||
border=4,
|
||||
)
|
||||
qr.add_data(url)
|
||||
qr.make(fit=True)
|
||||
return qr.make_image(fill_color="black", back_color="white")
|
||||
|
||||
def generate_qr_codes_zip(self):
|
||||
if not self.pos_self_ordering_mode in ['mobile', 'consultation']:
|
||||
raise ValidationError(_("QR codes can only be generated in mobile or consultation mode."))
|
||||
|
||||
qr_images = []
|
||||
|
||||
if self.pos_module_pos_restaurant:
|
||||
table_ids = self.pos_config_id.floor_ids.table_ids
|
||||
|
||||
if not table_ids:
|
||||
raise ValidationError(_("In Self-Order mode, you must have at least one table to generate QR codes"))
|
||||
|
||||
for table in table_ids:
|
||||
qr_images.append({
|
||||
'image': self._generate_single_qr_code(self.pos_config_id._get_self_order_url(table.id)),
|
||||
'name': f"{table.floor_id.name} - {table.name}",
|
||||
})
|
||||
else:
|
||||
qr_images.append({
|
||||
'image': self._generate_single_qr_code(self.pos_config_id._get_self_order_url()),
|
||||
'name': "generic",
|
||||
})
|
||||
|
||||
# Create a zip with all images in qr_images
|
||||
zip_buffer = BytesIO()
|
||||
with zipfile.ZipFile(zip_buffer, "w", 0) as zip_file:
|
||||
for index, qr_image in enumerate(qr_images):
|
||||
with zip_file.open(f"{qr_image['name']} ({index + 1}).png", "w") as buf:
|
||||
qr_image['image'].save(buf, format="PNG")
|
||||
zip_buffer.seek(0)
|
||||
|
||||
# Delete previous attachments
|
||||
self.env["ir.attachment"].search([
|
||||
("name", "=", "self_order_qr_code.zip"),
|
||||
]).unlink()
|
||||
|
||||
# Create an attachment with the zip
|
||||
attachment_id = self.env["ir.attachment"].create({
|
||||
"name": "self_order_qr_code.zip",
|
||||
"type": "binary",
|
||||
"raw": zip_buffer.read(),
|
||||
"res_model": self._name,
|
||||
"res_id": self.id,
|
||||
})
|
||||
|
||||
return {
|
||||
"type": "ir.actions.act_url",
|
||||
"url": f"/web/content/{attachment_id.id}",
|
||||
"target": "new",
|
||||
}
|
||||
|
||||
def generate_qr_codes_page(self):
|
||||
"""
|
||||
Generate the data needed to print the QR codes page
|
||||
|
||||
@@ -33,17 +33,23 @@ export class ProductCard extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
const pic = this.selfRef.el.querySelector(".o_self_order_item_card_image");
|
||||
let pic = this.selfRef.el.querySelector(".o_self_order_item_card_image");
|
||||
if (!pic) {
|
||||
return;
|
||||
pic = this.selfRef.el.querySelector(".o_self_order_item_card_no_image");
|
||||
}
|
||||
|
||||
const picRect = pic.getBoundingClientRect();
|
||||
const clonedPic = pic.cloneNode(true);
|
||||
const toOrderRect = toOrder.getBoundingClientRect();
|
||||
|
||||
clonedPic.classList.remove('w-100', 'h-100');
|
||||
clonedPic.classList.add('position-fixed', 'border', 'border-white', 'border-4', 'z-index-1');
|
||||
clonedPic.classList.remove("w-100", "h-100");
|
||||
clonedPic.classList.add(
|
||||
"position-fixed",
|
||||
"border",
|
||||
"border-white",
|
||||
"border-4",
|
||||
"z-index-1"
|
||||
);
|
||||
clonedPic.style.top = `${picRect.top}px`;
|
||||
clonedPic.style.left = `${picRect.left}px`;
|
||||
clonedPic.style.width = `${picRect.width}px`;
|
||||
@@ -53,9 +59,10 @@ export class ProductCard extends Component {
|
||||
document.body.appendChild(clonedPic);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
let offsetTop = (toOrderRect.top - picRect.top) - picRect.height * 0.5;
|
||||
let offsetLeft = (toOrderRect.left - picRect.left) - picRect.width * 0.25;
|
||||
clonedPic.style.transform = 'translateY(' + offsetTop +'px) translateX(' + offsetLeft +'px) scale(0.5)';
|
||||
const offsetTop = toOrderRect.top - picRect.top - picRect.height * 0.5;
|
||||
const offsetLeft = toOrderRect.left - picRect.left - picRect.width * 0.25;
|
||||
clonedPic.style.transform =
|
||||
"translateY(" + offsetTop + "px) translateX(" + offsetLeft + "px) scale(0.5)";
|
||||
clonedPic.style.opacity = "0"; // Fading out the card
|
||||
});
|
||||
|
||||
|
||||
@@ -26,3 +26,9 @@
|
||||
left: 22px;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.o_self_order_item_card_no_image {
|
||||
span {
|
||||
color: #DFDFDF;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,15 @@
|
||||
<div
|
||||
class="ratio ratio-1x1 w-25 w-sm-50 w-md-100"
|
||||
t-att-class="{
|
||||
'd-none d-md-block': !props.product.has_image
|
||||
'd-md-block': !props.product.has_image
|
||||
}">
|
||||
<div class="placeholder-glow">
|
||||
<div class="placeholder w-100 h-100 bg-300 rounded"/>
|
||||
<div class="placeholder-glow o_self_order_item_card_no_image">
|
||||
<div t-attf-class="{{ props.product.has_image ? 'placeholder' : 'd-flex align-items-center justify-content-center h-100' }} bg-200 w-100 h-100 rounded">
|
||||
<span t-if="!props.product.has_image" t-esc="props.product.name" class="text-center text-white fs-2 fw-bold mb-1 mb-sm-2"/>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
t-if="props.product.has_image"
|
||||
class="o_self_order_item_card_image w-100 rounded"
|
||||
t-attf-src="/menu/get-image/{{ props.product.id }}/512?unique={{props.product.write_date}}"
|
||||
alt="Product image"
|
||||
|
||||
@@ -97,10 +97,10 @@ export class CartPage extends Component {
|
||||
if (table) {
|
||||
this.selfOrder.table = table;
|
||||
this.router.addTableIdentifier(table);
|
||||
this.pay();
|
||||
}
|
||||
|
||||
this.state.selectTable = false;
|
||||
this.pay();
|
||||
}
|
||||
|
||||
getChildLines(line) {
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
&.active {
|
||||
color: $headings-color;
|
||||
|
||||
&.active-no-image{
|
||||
outline: 1px solid $o-action;
|
||||
}
|
||||
|
||||
img {
|
||||
outline: 1px solid $o-action;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
t-key="category.id"
|
||||
t-ref="category_{{category.id}}"
|
||||
t-attf-class="nav-link category-item flex-shrink-0 p-0"
|
||||
t-att-class="{
|
||||
'active-no-image border border-sm rounded p-1 d-flex align-items-center': !category.has_image
|
||||
}"
|
||||
t-attf-href="#scrollspy_{{category.id}}">
|
||||
<div t-if="category.has_image" class="ratio ratio-1x1 mb-1">
|
||||
<div class="placeholder-glow">
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="pos_self_order_table_form_view" model="ir.ui.view">
|
||||
<field name="name">Restaurant Table</field>
|
||||
<field name="model">restaurant.table</field>
|
||||
<field name="inherit_id" ref="pos_restaurant.view_restaurant_table_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@col='2']" position="inside">
|
||||
<field name="identifier" groups="base.group_no_one" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -18,6 +18,7 @@
|
||||
<button class="btn-link p-0" icon="oi-arrow-right" name="preview_self_order_app" type="object" string="Preview Web interface"/>
|
||||
<button class="btn-link p-0" icon="oi-arrow-right" name="custom_link_action" type="object" string="Home buttons"/>
|
||||
<button class="btn-link p-0" icon="oi-arrow-right" name="generate_qr_codes_page" type="object" string="Print QR Codes" invisible="not pos_self_ordering_mode in ['consultation', 'mobile']"/>
|
||||
<button class="btn-link p-0" icon="oi-arrow-right" name="generate_qr_codes_zip" type="object" string="Download QR Codes" invisible="not pos_self_ordering_mode in ['consultation', 'mobile']"/>
|
||||
<button groups="base.group_no_one" class="btn-link p-0" icon="oi-arrow-right" name="update_access_tokens" type="object" string="Reset QR Codes" invisible="not pos_self_ordering_mode in ['consultation', 'mobile']"/>
|
||||
</div>
|
||||
</setting>
|
||||
|
||||
Reference in New Issue
Block a user