[IMP] pos_self_order: send kiosk order to preparation display

This commit consist of adaptation of the pos_self_order module to send
orders to preparation display. The main changes consist of sending the
table stand number that the client put in to the backend to be shown
in the preparation display. It also consist of moving the tracking
number from the pos_self_order module to the point_of_sale module so
that every number has a tracking number.

closes odoo/odoo#134980

Task-id: 3495477
Related: odoo/enterprise#47203
Signed-off-by: David Monnom (moda) <moda@odoo.com>
This commit is contained in:
Adrien Guilliams (adgu)
2023-10-12 16:11:56 +00:00
parent 28824a07c5
commit adc58cdafe
27 changed files with 124 additions and 94 deletions
+1 -1
View File
@@ -392,7 +392,7 @@ class PosConfig(models.Model):
return pos_configs
def _reset_default_on_vals(self, vals):
if 'tip_product_id' in vals and vals['iface_tipproduct'] and not vals['tip_product_id']:
if 'tip_product_id' in vals and not vals['tip_product_id'] and 'iface_tipproduct' in vals and vals['iface_tipproduct']:
default_product = self.env.ref('point_of_sale.product_product_tip', False)
if default_product:
vals['tip_product_id'] = default_product.id
+7 -4
View File
@@ -59,7 +59,7 @@ class PosOrder(models.Model):
'tip_amount': ui_order.get('tip_amount', 0),
'access_token': ui_order.get('access_token', ''),
'ticket_code': ui_order.get('ticket_code', ''),
'last_order_preparation_change': ui_order.get('last_order_preparation_change', False),
'last_order_preparation_change': ui_order.get('last_order_preparation_change', '{}'),
}
@api.model
@@ -113,9 +113,10 @@ class PosOrder(models.Model):
return new_session
def _get_tracking_ref(self):
self.ensure_one()
return str(self.session_id.id)[-1] + str(self.sequence_number)[-2:]
@api.depends('sequence_number', 'session_id')
def _compute_tracking_number(self):
for record in self:
record.tracking_number = str(record.session_id.id)[-1] + str(record.sequence_number)[-2:]
@api.model
def _process_order(self, order, draft, existing_order):
@@ -327,6 +328,7 @@ class PosOrder(models.Model):
has_refundable_lines = fields.Boolean('Has Refundable Lines', compute='_compute_has_refundable_lines')
refunded_orders_count = fields.Integer(compute='_compute_refund_related_fields')
ticket_code = fields.Char(help='5 digits alphanumeric code to be used by portal user to request an invoice')
tracking_number = fields.Char(string="Tracking Number", compute='_compute_tracking_number')
@api.depends('lines.refund_orderline_ids', 'lines.refunded_orderline_id')
def _compute_refund_related_fields(self):
@@ -1132,6 +1134,7 @@ class PosOrder(models.Model):
'access_token': order.access_token,
'ticket_code': order.ticket_code,
'last_order_preparation_change': order.last_order_preparation_change,
'tracking_number':order.tracking_number,
}
@api.model
@@ -229,6 +229,18 @@ class PosSession(models.Model):
sessions = super().create(vals_list)
sessions.action_pos_session_open()
date_string = fields.Date.today().isoformat()
ir_sequence = self.env['ir.sequence'].sudo().search([('code', '=', f'pos.order_{date_string}')])
if not ir_sequence:
self.env['ir.sequence'].sudo().create({
'name': _("PoS Order"),
'padding': 0,
'code': f'pos.order_{date_string}',
'number_next': 1,
'number_increment': 1,
'company_id': self.env.company.id,
})
return sessions
def unlink(self):
@@ -1,6 +1,6 @@
/** @odoo-module */
import { uuidv4 } from "@point_of_sale/utils";
import { uuidv4, constructFullProductName } from "@point_of_sale/utils";
// FIXME POSREF - unify use of native parseFloat and web's parseFloat. We probably don't need the native version.
import { parseFloat as oParseFloat } from "@web/views/fields/parsers";
import {
@@ -567,27 +567,11 @@ export class Orderline extends PosModel {
this.price_extra = parseFloat(price_extra) || 0.0;
}
set_full_product_name() {
let attributeString = "";
if (this.attribute_value_ids && this.attribute_value_ids.length > 0) {
for (const valId of this.attribute_value_ids) {
const value = this.pos.db.attribute_value_by_id[valId];
if (value.is_custom) {
const customValue = this.custom_attribute_value_ids.find(
(cus) => cus.custom_product_template_attribute_value_id == parseInt(valId)
);
attributeString += customValue
? `${value.name}: ${customValue.custom_value}, `
: `${value.name}, `;
} else {
attributeString += `${value.name}, `;
}
}
attributeString = attributeString.slice(0, -2);
attributeString = `(${attributeString})`;
}
this.full_product_name = `${this.product.display_name} ${attributeString}`;
this.full_product_name = constructFullProductName(
this,
this.pos.db.attribute_value_by_id,
this.product.display_name
);
}
get_price_extra() {
return this.price_extra;
+24
View File
@@ -29,3 +29,27 @@ export function deduceUrl(url) {
}
return url;
}
export function constructFullProductName(line, attribute_value_by_id, display_name) {
let attributeString = "";
if (line.attribute_value_ids && line.attribute_value_ids.length > 0) {
for (const valId of line.attribute_value_ids) {
const value = attribute_value_by_id[valId];
if (value.is_custom) {
const customValue = line.custom_attribute_value_ids.find(
(cus) => cus.custom_product_template_attribute_value_id == parseInt(valId)
);
attributeString += customValue
? `${value.name}: ${customValue.custom_value}, `
: `${value.name}, `;
} else {
attributeString += `${value.name}, `;
}
}
attributeString = attributeString.slice(0, -2);
attributeString = `(${attributeString})`;
}
return `${display_name} ${attributeString}`;
}
+1 -1
View File
@@ -20,7 +20,7 @@ class PosAdyenController(http.Controller):
_logger.info('notification received from adyen:\n%s', pprint.pformat(data))
terminal_identifier = data['SaleToPOIResponse']['MessageHeader']['POIID']
payment_method_sudo = request.env['pos.payment.method'].sudo().search([('adyen_terminal_identifier', '=', terminal_identifier)], limit=1)
pos_session_id = int(data["SaleToPOIResponse"]["PaymentResponse"]["SaleData"]["SaleTransactionID"]["TransactionID"].split("--")[1])
pos_session_id = int(data["SaleToPOIResponse"]["PaymentResponse"]["SaleData"]["SaleTransactionID"]["TransactionID"].split("-")[1])
pos_session_sudo = request.env["pos.session"].sudo().browse(pos_session_id)
if payment_method_sudo:
@@ -8,25 +8,13 @@ from odoo.exceptions import ValidationError
class PosConfig(models.Model):
_inherit = 'pos.config'
self_order_online_payment_method_id = fields.Many2one('pos.payment.method', string='Self Online Payment', help="The online payment method to use when a customer pays a self-order online.", domain=[('is_online_payment', '=', True)], compute='_compute_self_order_online_payment_method_id', store=True, readonly=False)
self_order_online_payment_method_id = fields.Many2one('pos.payment.method', string='Self Online Payment', help="The online payment method to use when a customer pays a self-order online.", domain=[('is_online_payment', '=', True)], store=True, readonly=False)
@api.constrains('self_order_online_payment_method_id')
def _check_self_order_online_payment_method_id(self):
if any(config.self_order_online_payment_method_id and not config.self_order_online_payment_method_id._get_online_payment_providers(config.id, error_if_invalid=True) for config in self):
raise ValidationError(_("The online payment method used for self-order in a POS config must have at least one published payment provider supporting the currency of that POS config."))
@api.constrains('self_ordering_mode', 'self_ordering_pay_after', 'self_order_online_payment_method_id')
def _check_self_order_pay_after_each(self):
if any(config.self_ordering_mode == 'mobile' and config.self_ordering_pay_after == 'each' and not config.self_order_online_payment_method_id for config in self):
raise ValidationError(_("The POS self-order mode with payment after each order requires an online payment method to be configured."))
@api.depends('company_id', 'self_ordering_mode', 'self_ordering_pay_after', 'self_order_online_payment_method_id')
def _compute_self_order_online_payment_method_id(self):
for config in self:
if not config.self_ordering_mode == 'mobile':
config.self_order_online_payment_method_id = False
elif config.self_ordering_pay_after == 'each' and (not config.self_order_online_payment_method_id or not config.self_order_online_payment_method_id.is_online_payment):
config.self_order_online_payment_method_id = self.env['pos.payment.method'].sudo()._get_or_create_online_payment_method(config.company_id.id, config.id)
if config.self_ordering_mode == 'mobile' and config.self_ordering_service_mode == 'each' and config.self_order_online_payment_method_id and not config.self_order_online_payment_method_id._get_online_payment_providers(config.id, error_if_invalid=True):
raise ValidationError(_("The online payment method used for self-order in a POS config must have at least one published payment provider supporting the currency of that POS config."))
def _get_self_ordering_data(self):
res = super()._get_self_ordering_data()
@@ -95,5 +95,6 @@ class PosSession(models.Model):
"note": "",
"product_id": orderline.product_id.id,
"quantity": orderline.qty,
"attribute_value_ids": orderline.attribute_value_ids.ids,
}
order.write({'last_order_preparation_change': json.dumps(last_order_preparation_change)})
+1
View File
@@ -63,6 +63,7 @@
'web/static/lib/bootstrap/js/dist/scrollspy.js',
"pos_self_order/static/src/app/**/*",
"point_of_sale/static/src/app/store/models/product_custom_attribute.js",
"point_of_sale/static/src/utils.js",
'web_editor/static/src/js/editor/odoo-editor/src/base_style.scss',
'web_editor/static/src/scss/web_editor.common.scss',
],
+2 -5
View File
@@ -13,14 +13,10 @@ class PosSelfOrderController(http.Controller):
is_take_away = order.get('take_away')
pos_config, table = self._verify_authorization(access_token, table_identifier, is_take_away)
pos_session = pos_config.current_session_id
date_string = fields.Date.today().isoformat()
ir_sequence_session = pos_config.env['ir.sequence'].with_context(company_id=pos_config.company_id.id).next_by_code(f'pos.order_{pos_session.id}')
ir_sequence_tracking = pos_config.env['ir.sequence'].with_context(company_id=pos_config.company_id.id).next_by_code(f'pos.order_{date_string}')
sequence_number = re.findall(r'\d+', ir_sequence_session)[0]
order_reference = self._generate_unique_id(pos_session.id, pos_config.id, sequence_number, device_type)
tracking_number = f"{'A' if device_type == 'kiosk' else 'B'}{ir_sequence_tracking}"
fiscal_position = (
pos_config.self_ordering_alternative_fp_id
@@ -49,7 +45,7 @@ class PosSelfOrderController(http.Controller):
'amount_total': 0,
'amount_paid': 0,
'amount_return': 0,
'tracking_number': tracking_number,
'table_stand_number': order.get('table_stand_number'),
},
'to_invoice': False,
'session_id': pos_session.id,
@@ -143,6 +139,7 @@ class PosSelfOrderController(http.Controller):
'amount_tax': amount_total - amount_untaxed,
'amount_total': amount_total,
'table_id': table if table else False,
'table_stand_number': order.get('table_stand_number'),
})
pos_order.send_table_count_notification(pos_order.table_id)
+6 -1
View File
@@ -147,7 +147,7 @@ class PosConfig(models.Model):
def write(self, vals):
for record in self:
if vals.get('self_ordering_mode') == 'kiosk' or record.self_ordering_mode == 'kiosk':
if vals.get('self_ordering_mode') == 'kiosk' or (vals.get('pos_self_ordering_mode') == 'mobile' and vals.get('pos_self_ordering_service_mode') == 'counter'):
vals['self_ordering_pay_after'] = 'each'
if (not vals.get('module_pos_restaurant') and not record.module_pos_restaurant) and vals.get('self_ordering_mode') == 'mobile':
@@ -170,6 +170,11 @@ class PosConfig(models.Model):
vals['self_ordering_image_home_ids'] = unlink_commands + vals['self_ordering_image_home_ids']
if vals.get('self_ordering_mode') == 'mobile' and vals.get('self_ordering_pay_after') == 'meal':
vals['self_ordering_service_mode'] = 'table'
if (vals.get('self_ordering_service_mode') == 'counter' or record.self_ordering_service_mode == 'counter') and vals.get('self_ordering_mode') == 'mobile':
vals['self_ordering_pay_after'] = 'each'
return super().write(vals)
@api.depends("module_pos_restaurant")
+7 -3
View File
@@ -42,7 +42,7 @@ class PosOrderLine(models.Model):
class PosOrder(models.Model):
_inherit = "pos.order"
tracking_number = fields.Char(string="Tracking Number")
table_stand_number = fields.Char(string="Table Stand Number")
take_away = fields.Boolean(string="Take Away", default=False)
@api.model
@@ -66,8 +66,8 @@ class PosOrder(models.Model):
def _order_fields(self, ui_order):
fields = super()._order_fields(ui_order)
fields.update({
'tracking_number': ui_order.get('tracking_number'),
'take_away': ui_order.get('take_away'),
'table_stand_number': ui_order.get('table_stand_number'),
})
return fields
@@ -92,7 +92,7 @@ class PosOrder(models.Model):
"take_away": self.take_away,
"pos_reference": self.pos_reference,
"access_token": self.access_token,
"tracking_number": self.tracking_number,
"table_stand_number": self.table_stand_number,
"state": self.state,
"date_order": str(self.date_order),
"amount_total": self.amount_total,
@@ -113,3 +113,7 @@ class PosOrder(models.Model):
for line in self.lines
],
}
def _send_order(self):
#This function is made to be overriden by pos_self_order_preparation_display
pass
@@ -15,8 +15,6 @@ class PosSession(models.Model):
@api.model
def _create_pos_self_sessions_sequence(self, sessions):
date_string = fields.Date.today().isoformat()
ir_sequence = self.env['ir.sequence'].sudo().search([('code', '=', f'pos.order_{date_string}')])
company_id = self.env.company.id
for session in sessions:
@@ -29,16 +27,6 @@ class PosSession(models.Model):
'company_id': company_id,
})
if not ir_sequence:
self.env['ir.sequence'].sudo().create({
'name': _("PoS Order"),
'padding': 0,
'code': f'pos.order_{date_string}',
'number_next': 1,
'number_increment': 1,
'company_id': company_id,
})
return sessions
def _loader_params_product_product(self):
@@ -5,6 +5,7 @@ import { useSelfOrder } from "@pos_self_order/app/self_order_service";
import { useService, useForwardRefToParent } from "@web/core/utils/hooks";
import { Line } from "@pos_self_order/app/models/line";
import { ProductInfoPopup } from "@pos_self_order/app/components/product_info_popup/product_info_popup";
import { constructFullProductName } from "@point_of_sale/utils";
export class ProductCard extends Component {
static template = "pos_self_order.ProductCard";
@@ -100,16 +101,18 @@ export class ProductCard extends Component {
isProductInCart.qty++;
} else {
const lines = this.selfOrder.currentOrder.lines;
lines.push(
new Line({
id: null,
uuid: null,
qty: 1,
product_id: product.id,
full_product_name: product.name,
})
const line = new Line({
id: null,
uuid: null,
qty: 1,
product_id: product.id,
});
line.full_product_name = constructFullProductName(
line,
this.selfOrder.attributeValueById,
product.name
);
lines.push(line);
}
await this.selfOrder.getPricesFromServer();
}
@@ -28,7 +28,7 @@ export class Line extends Reactive {
// server data
this.id = line.id || null;
this.uuid = line.uuid || uuidv4();
this.full_product_name = line.full_product_name;
this.full_product_name = line.full_product_name || "";
this.product_id = line.product_id;
this.qty = line.qty ? line.qty : 0;
this.customer_note = line.customer_note;
@@ -13,7 +13,6 @@ export class Order extends Reactive {
date,
amount_total,
amount_tax,
tracking_number,
lastChangesSent,
take_away,
}) {
@@ -32,7 +31,6 @@ export class Order extends Reactive {
this.amount_total = order.amount_total || 0;
this.amount_tax = order.amount_tax || 0;
this.lines = order.lines || [];
this.tracking_number = order.tracking_number || null;
this.take_away = typeof order.take_away === "boolean" ? order.take_away : null;
// data
@@ -53,6 +51,18 @@ export class Order extends Reactive {
return this.isAlreadySent && this.hasNotAllLinesSent().length === 0;
}
get trackingNumber() {
if (this.pos_reference) {
const reference = this.pos_reference;
const arrRef = reference.split(" ")[1].split("-");
const sessionID = parseInt(arrRef[0]).toString();
const sequence = parseInt(arrRef[2]).toString();
const trackingNumber = sessionID + sequence;
return trackingNumber;
}
return null;
}
initLines() {
this.lines = this.lines.map((line) => new Line(line));
}
@@ -63,6 +63,11 @@ export class CartPage extends Component {
return;
}
if (type === "mobile" && orderingMode === "table" && !takeAway && !this.selfOrder.table) {
this.state.selectTable = true;
return;
}
if (mode === "meal" && !order.isSavedOnServer) {
this.sendInProgress = true;
await this.selfOrder.sendDraftOrderToServer();
@@ -72,13 +77,9 @@ export class CartPage extends Component {
return;
}
}
if (orderingMode === "table" && !takeAway) {
if (type === "kiosk") {
this.router.navigate("stand_number");
} else if (type === "mobile" && !this.selfOrder.table) {
this.state.selectTable = true;
return;
} else {
this.router.navigate("payment");
}
@@ -19,7 +19,7 @@
<div class="product-info d-flex flex-column gap-2 flex-grow-1">
<div>
<span t-if="selfOrder.currentOrder.isSavedOnServer"><t t-esc="line.qty" />x </span>
<strong t-esc="line.full_product_name"/>
<strong t-esc="selfOrder.productByIds[line.product_id].name"/>
</div>
<div t-if="this.selfOrder.productByIds[line.product_id]?.attributes.length > 0 || childLines.length > 0" class="d-flex align-items-start gap-2 gap-md-3 my-2">
<button
@@ -37,7 +37,7 @@
</div>
</div>
<div t-foreach="childLines" t-as="cline" t-key="cline.uuid" class="text-muted">
- <span t-esc="cline.full_product_name" /> <span t-if="cline.price_subtotal_incl">(<t t-esc="selfOrder.formatMonetary(cline.price_subtotal_incl)"/>)</span>
- <span t-esc="selfOrder.productByIds[cline.product_id].name" /> <span t-if="cline.price_subtotal_incl">(<t t-esc="selfOrder.formatMonetary(cline.price_subtotal_incl)"/>)</span>
<div class="product-info d-flex flex-column flex-grow-1 ms-3">
<div t-foreach="getSelectedAttributes(cline)" t-as="attribute" t-key="attribute.name">
- <span t-esc="attribute.name" /> : <span t-esc="attribute.value" />
@@ -6,6 +6,7 @@ import { ComboSelection } from "@pos_self_order/app/components/combo_selection/c
import { useService } from "@web/core/utils/hooks";
import { Line } from "@pos_self_order/app/models/line";
import { attributeFlatter, attributeFormatter } from "@pos_self_order/app/utils";
import { constructFullProductName } from "@point_of_sale/utils";
export class ComboPage extends Component {
static template = "pos_self_order.ComboPage";
@@ -151,12 +152,16 @@ export class ComboPage extends Component {
uuid: null,
qty: this.state.qty,
product_id: combo.product.id,
full_product_name: combo.product.name,
attribute_value_ids: attributeFlatter(combo.product.variants),
custom_attribute_value_ids: Object.values(combo.product.customValues),
combo_parent_uuid: parent_line.uuid,
combo_id: combo.id,
});
child_line.full_product_name = constructFullProductName(
child_line,
this.selfOrder.attributeValueById,
combo.product.name
);
lines.push(child_line);
parent_line.child_lines.push(child_line);
}
@@ -22,8 +22,6 @@ export class LandingPage extends Component {
this.selfOrder.orders = [];
this.selfOrder.editedOrder = null;
}
this.tablePadNumber = null;
});
onMounted(() => {
@@ -55,7 +55,7 @@
<t t-set="comboParent" t-value="line.combo_parent_uuid and order.lines.find(l => l.uuid === line.combo_parent_uuid)" />
<div t-if="comboParent" class="info ms-2 combo-parent-name">
<i class="fa fa-th-large me-2" role="img" aria-label="Combo" title="Combo"/>
<t t-esc="comboParent.full_product_name" />
<t t-esc="selfOrder.productByIds[comboParent.product_id].name" />
</div>
<div t-if="line.customer_note" class="d-inline-block m-0 text-muted small break-line">
<i class="fa fa-pencil-square-o" aria-hidden="true" />
@@ -13,13 +13,13 @@
<div class="d-inline-flex flex-column border rounded py-4 px-5 bg-view mb-3">
<t t-if="selfOrder.currentOrder.take_away || selfOrder.config.self_ordering_service_mode !== 'table'">
<span class="fs-2 text-muted">Your order number</span>
<span class="number lh-1" t-esc="selfOrder.currentOrder.tracking_number" />
<span class="number lh-1" t-esc="selfOrder.currentOrder.trackingNumber" />
</t>
<t t-else="">
<span class="fs-2 text-muted">Your table number:</span>
<span class="number lh-1 mb-4" t-esc="selfOrder.tablePadNumber" />
<span class="number lh-1 mb-4" t-esc="selfOrder.currentOrder.table_stand_number" />
<span class="fs-2 text-muted">Order number:</span>
<span class="number lh-1" t-esc="selfOrder.currentOrder.tracking_number" />
<span class="number lh-1" t-esc="selfOrder.currentOrder.trackingNumber" />
</t>
</div>
<h2 class="m-0 text-muted">Get your ticket below</h2>
@@ -6,6 +6,7 @@ import { AttributeSelection } from "@pos_self_order/app/components/attribute_sel
import { Line } from "@pos_self_order/app/models/line";
import { useService } from "@web/core/utils/hooks";
import { attributeFlatter } from "@pos_self_order/app/utils";
import { constructFullProductName } from "@point_of_sale/utils";
export class ProductPage extends Component {
static template = "pos_self_order.ProductPage";
@@ -124,12 +125,17 @@ export class ProductPage extends Component {
uuid: lineToMerge ? lineToMerge.uuid : null,
qty: this.state.qty,
product_id: this.product.id,
full_product_name: this.product.name,
customer_note: this.state.customer_note,
custom_attribute_value_ids: Object.values(this.env.customValues),
attribute_value_ids: attributeFlatter(this.env.selectedValues),
});
mainLine.full_product_name = constructFullProductName(
mainLine,
this.selfOrder.attributeValueById,
this.product.name
);
lines.push(mainLine);
}
@@ -34,7 +34,7 @@ export class StandNumberPage extends Component {
confirm() {
if (this.state.standNumber.length > 0) {
this.selfOrder.tablePadNumber = this.state.standNumber;
this.selfOrder.currentOrder.table_stand_number = this.state.standNumber;
this.router.navigate("payment");
}
}
@@ -39,7 +39,6 @@ export class SelfOrder extends Reactive {
this.lastEditedProductId = null;
this.attributeValueById = {};
this.eatingLocation = "in"; // (in, out) in by default because out can be disabled in the config
this.tablePadNumber = null;
this.currentProduct = 0;
this.attributeById = {};
this.priceLoading = false;
@@ -54,7 +54,7 @@
</div>
<div id="self-payment-after" class="content-group row" invisible="not pos_self_ordering_mode in ['kiosk', 'mobile']">
<label string="Pay after" for="pos_self_ordering_pay_after" class="col-lg-4"/>
<field name="pos_self_ordering_pay_after" readonly="pos_self_ordering_mode == 'kiosk' or pos_self_ordering_service_mode == 'counter' and pos_self_ordering_mode == 'mobile'" widget="upgrade_selection"/>
<field name="pos_self_ordering_pay_after" readonly="pos_self_ordering_mode == 'kiosk' or (pos_self_ordering_service_mode == 'counter' and pos_self_ordering_mode == 'mobile')" widget="upgrade_selection"/>
</div>
</setting>
<setting string="Eat in / Take out" help="Adjust the tax rate based on whether customers are dining in or opting for takeout." invisible="not pos_self_ordering_mode in ['mobile', 'kiosk']">
@@ -4,7 +4,7 @@ from odoo import http, fields
from odoo.http import request
class PosSelfAdyenContoller(PosAdyenController):
class PosSelfAdyenController(PosAdyenController):
@http.route()
def notification(self):
super().notification()
@@ -33,6 +33,7 @@ class PosSelfAdyenContoller(PosAdyenController):
'pos_order_id': order_sudo.id
})
order_sudo.action_pos_order_paid()
order_sudo._send_order()
if order_sudo.config_id.self_ordering_mode == 'kiosk':
request.env['bus.bus']._sendone(f'pos_config-{order_sudo.config_id.access_token}', 'PAYMENT_STATUS', {