diff --git a/addons/point_of_sale/static/src/app/screens/receipt_screen/receipt/receipt_header/receipt_header.xml b/addons/point_of_sale/static/src/app/screens/receipt_screen/receipt/receipt_header/receipt_header.xml index 5e9bd9bd689..fe4b91f3486 100644 --- a/addons/point_of_sale/static/src/app/screens/receipt_screen/receipt/receipt_header/receipt_header.xml +++ b/addons/point_of_sale/static/src/app/screens/receipt_screen/receipt/receipt_header/receipt_header.xml @@ -3,6 +3,7 @@
+

diff --git a/addons/point_of_sale/static/src/app/store/models.js b/addons/point_of_sale/static/src/app/store/models.js index 8f2ea2289d5..b02d3239cfb 100644 --- a/addons/point_of_sale/static/src/app/store/models.js +++ b/addons/point_of_sale/static/src/app/store/models.js @@ -1,6 +1,6 @@ /** @odoo-module */ -import { qrCodeSrc, uuidv4, constructFullProductName } from "@point_of_sale/utils"; +import { random5Chars, uuidv4, qrCodeSrc, 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 { @@ -2683,15 +2683,7 @@ export class Order extends PosModel { return true; } } - /** - * Returns a random 5 digits alphanumeric code - * @returns {string} - */ _generateTicketCode() { - let code = ""; - while (code.length != 5) { - code = Math.random().toString(36).slice(2, 7); - } - return code; + return random5Chars(); } } diff --git a/addons/point_of_sale/static/src/utils.js b/addons/point_of_sale/static/src/utils.js index 56545f7e57f..d00254c19c3 100644 --- a/addons/point_of_sale/static/src/utils.js +++ b/addons/point_of_sale/static/src/utils.js @@ -53,6 +53,17 @@ export function constructFullProductName(line, attribute_value_by_id, display_na return attributeString !== "" ? `${display_name} ${attributeString}` : display_name; } +/** + * Returns a random 5 digits alphanumeric code + * @returns {string} + */ +export function random5Chars() { + let code = ""; + while (code.length != 5) { + code = Math.random().toString(36).slice(2, 7); + } + return code; +} export function qrCodeSrc(url, { size = 200 } = {}) { return `/report/barcode/QR/${encodeURIComponent(url)}?width=${size}&height=${size}`; diff --git a/addons/pos_self_order/__manifest__.py b/addons/pos_self_order/__manifest__.py index 0bb4c48177a..81d33643111 100644 --- a/addons/pos_self_order/__manifest__.py +++ b/addons/pos_self_order/__manifest__.py @@ -61,11 +61,18 @@ 'web/static/lib/bootstrap/js/dist/base-component.js', "web/static/lib/bootstrap/js/dist/carousel.js", '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', + "point_of_sale/static/src/app/generic_components/**/*", + "point_of_sale/static/src/css/pos_receipts.css", + "point_of_sale/static/src/app/screens/receipt_screen/receipt/**/*", + "point_of_sale/static/src/app/printer/base_printer.js", + "point_of_sale/static/src/app/printer/printer_service.js", + 'web_editor/static/lib/html2canvas.js', + "point_of_sale/static/src/app/printer/render_service.js", + "pos_self_order/static/src/app/**/*", ], # Assets tests "pos_self_order.assets_tests": [ diff --git a/addons/pos_self_order/controllers/orders.py b/addons/pos_self_order/controllers/orders.py index 76386f5dddf..59020a99245 100644 --- a/addons/pos_self_order/controllers/orders.py +++ b/addons/pos_self_order/controllers/orders.py @@ -46,6 +46,7 @@ class PosSelfOrderController(http.Controller): 'amount_paid': 0, 'amount_return': 0, 'table_stand_number': order.get('table_stand_number'), + 'ticket_code': order.get('ticket_code'), }, 'to_invoice': False, 'session_id': pos_session.id, diff --git a/addons/pos_self_order/models/pos_config.py b/addons/pos_self_order/models/pos_config.py index f54f9cd4802..2159de3239d 100644 --- a/addons/pos_self_order/models/pos_config.py +++ b/addons/pos_self_order/models/pos_config.py @@ -339,8 +339,12 @@ class PosConfig(models.Model): return { "pos_config_id": self.id, "pos_session": self.current_session_id.read(["id", "access_token"])[0] if self.current_session_id and self.current_session_id.state == 'opened' else False, - "company_name": self.company_id.name, - "company_color": self.company_id.color, + "company": { + **self.company_id.read(["name", "color", "email", "website", "vat", "name", "phone", "point_of_sale_use_ticket_qr_code", "point_of_sale_ticket_unique_code"])[0], + "partner_id": [None, self.company_id.partner_id.contact_address], + "country": self.company_id.country_id.read(["vat_label"])[0], + }, + "base_url": self.get_base_url(), "custom_links": self._get_self_order_custom_links(), "currency_id": self.currency_id.id, "pos_payment_methods": payment_methods if self.self_ordering_mode == "kiosk" else [], @@ -358,6 +362,8 @@ class PosConfig(models.Model): "self_ordering_image_home_ids": self._get_self_ordering_attachment(self.self_ordering_image_home_ids), "self_ordering_image_brand": self._get_self_ordering_image(self.self_ordering_image_brand), "self_ordering_pay_after": self.self_ordering_pay_after, + "receipt_header": self.receipt_header, + "receipt_footer": self.receipt_footer, }, } diff --git a/addons/pos_self_order/models/pos_order.py b/addons/pos_self_order/models/pos_order.py index 0199f9b02bc..0846dcbe204 100644 --- a/addons/pos_self_order/models/pos_order.py +++ b/addons/pos_self_order/models/pos_order.py @@ -45,6 +45,28 @@ class PosOrder(models.Model): table_stand_number = fields.Char(string="Table Stand Number") take_away = fields.Boolean(string="Take Away", default=False) + def _compute_tax_details(self): + self.ensure_one() + taxes = sum([line.tax_ids.compute_all(line.price_unit, quantity=line.qty, product=line.product_id)['taxes'] + for line in self.lines], []) + tax_percetanges = {tax['id']: tax['amount'] for tax in self.env['account.tax'].search([]).read(['amount'])} + merged_tax_details = {} + for tax_obj in taxes: + tax_id = tax_obj['id'] + if tax_id not in merged_tax_details: + merged_tax_details[tax_id] = { + 'tax': { + 'id': tax_id, + 'amount': tax_percetanges[tax_id] + }, + 'name': tax_obj['name'], + 'amount': 0.0, + 'base': 0.0, + } + merged_tax_details[tax_id]['amount'] += tax_obj['amount'] + merged_tax_details[tax_id]['base'] += tax_obj['base'] + return list(merged_tax_details.values()) + @api.model def create_from_ui(self, orders, draft=False): orders = super().create_from_ui(orders, draft) @@ -112,6 +134,7 @@ class PosOrder(models.Model): } for line in self.lines ], + "tax_details": self._compute_tax_details(), } def _send_order(self): diff --git a/addons/pos_self_order/static/src/app/models/line.js b/addons/pos_self_order/static/src/app/models/line.js index b299e074994..02e13342d4f 100644 --- a/addons/pos_self_order/static/src/app/models/line.js +++ b/addons/pos_self_order/static/src/app/models/line.js @@ -43,7 +43,6 @@ export class Line extends Reactive { this.initCustomAttribute(); } - initCustomAttribute() { this.custom_attribute_value_ids = this.custom_attribute_value_ids.map( (customAttribute) => new ProductCustomAttribute(customAttribute) diff --git a/addons/pos_self_order/static/src/app/models/order.js b/addons/pos_self_order/static/src/app/models/order.js index 5f6a5908825..d47ef919b47 100644 --- a/addons/pos_self_order/static/src/app/models/order.js +++ b/addons/pos_self_order/static/src/app/models/order.js @@ -1,6 +1,7 @@ /** @odoo-module **/ import { Reactive } from "@web/core/utils/reactive"; import { Line } from "./line"; +import { random5Chars } from "@point_of_sale/utils"; export class Order extends Reactive { constructor({ @@ -15,6 +16,7 @@ export class Order extends Reactive { amount_tax, lastChangesSent, take_away, + tax_details, }) { super(); this.setup(...arguments); @@ -32,6 +34,8 @@ export class Order extends Reactive { this.amount_tax = order.amount_tax || 0; this.lines = order.lines || []; this.take_away = typeof order.take_away === "boolean" ? order.take_away : null; + this.ticket_code = random5Chars(); // 5-digits alphanum code shown on the receipt + this.tax_details = order.tax_details || []; // data this.lastChangesSent = order.lastChangesSent || {}; diff --git a/addons/pos_self_order/static/src/app/pages/cart_page/cart_page.js b/addons/pos_self_order/static/src/app/pages/cart_page/cart_page.js index 1d6275759a6..3909dbe850b 100644 --- a/addons/pos_self_order/static/src/app/pages/cart_page/cart_page.js +++ b/addons/pos_self_order/static/src/app/pages/cart_page/cart_page.js @@ -6,7 +6,6 @@ import { useSelfOrder } from "@pos_self_order/app/self_order_service"; import { PopupTable } from "@pos_self_order/app/components/popup_table/popup_table"; import { _t } from "@web/core/l10n/translation"; import { OrderWidget } from "@pos_self_order/app/components/order_widget/order_widget"; -import { attributeFormatter } from "@pos_self_order/app/utils"; export class CartPage extends Component { static template = "pos_self_order.CartPage"; @@ -190,10 +189,4 @@ export class CartPage extends Component { }); } } - - getSelectedAttributes(line) { - const attributeValues = line.attribute_value_ids; - const customAttr = line.custom_attribute_value_ids; - return attributeFormatter(this.selfOrder.attributeById, attributeValues, customAttr); - } } diff --git a/addons/pos_self_order/static/src/app/pages/cart_page/cart_page.xml b/addons/pos_self_order/static/src/app/pages/cart_page/cart_page.xml index 534a055721b..434b2d3abe3 100644 --- a/addons/pos_self_order/static/src/app/pages/cart_page/cart_page.xml +++ b/addons/pos_self_order/static/src/app/pages/cart_page/cart_page.xml @@ -28,14 +28,14 @@
-
+
- :
- ()
-
+
- :
diff --git a/addons/pos_self_order/static/src/app/pages/confirmation_page/confirmation_page.js b/addons/pos_self_order/static/src/app/pages/confirmation_page/confirmation_page.js index 8aaa56f4f9f..664ce24123e 100644 --- a/addons/pos_self_order/static/src/app/pages/confirmation_page/confirmation_page.js +++ b/addons/pos_self_order/static/src/app/pages/confirmation_page/confirmation_page.js @@ -4,6 +4,7 @@ import { Component, onMounted, useState } from "@odoo/owl"; import { useSelfOrder } from "@pos_self_order/app/self_order_service"; import { cookie } from "@web/core/browser/cookie"; import { useService } from "@web/core/utils/hooks"; +import { OrderReceipt } from "@point_of_sale/app/screens/receipt_screen/receipt/order_receipt"; export class ConfirmationPage extends Component { static template = "pos_self_order.ConfirmationPage"; @@ -12,6 +13,7 @@ export class ConfirmationPage extends Component { setup() { this.selfOrder = useSelfOrder(); this.router = useService("router"); + this.printer = useService("printer"); this.confirmedOrder = {}; this.changeToDisplay = []; this.state = useState({ @@ -25,6 +27,12 @@ export class ConfirmationPage extends Component { this.setDefautLanguage(); }, 5000); } + setTimeout(() => { + this.printer.print(OrderReceipt, { + data: this.selfOrder.export_for_printing(this.selfOrder.currentOrder), + formatCurrency: this.selfOrder.formatMonetary, + }); + }, 500); }); this.initOrder(); diff --git a/addons/pos_self_order/static/src/app/pages/order_history_page/order_history_page.js b/addons/pos_self_order/static/src/app/pages/order_history_page/order_history_page.js index aefa913c88a..e69f1749eec 100644 --- a/addons/pos_self_order/static/src/app/pages/order_history_page/order_history_page.js +++ b/addons/pos_self_order/static/src/app/pages/order_history_page/order_history_page.js @@ -4,7 +4,6 @@ import { Component, useState } from "@odoo/owl"; import { useSelfOrder } from "@pos_self_order/app/self_order_service"; import { _t } from "@web/core/l10n/translation"; import { useService } from "@web/core/utils/hooks"; - export class OrdersHistoryPage extends Component { static template = "pos_self_order.OrdersHistoryPage"; @@ -32,7 +31,7 @@ export class OrdersHistoryPage extends Component { } getPrice(line) { - return this.selfOrder.show_prices_with_tax_included + return this.selfOrder.config.iface_tax_included ? line.price_subtotal_incl : line.price_subtotal; } diff --git a/addons/pos_self_order/static/src/app/pages/order_history_page/order_history_page.xml b/addons/pos_self_order/static/src/app/pages/order_history_page/order_history_page.xml index 2494155c16b..9a0c4b3c0d5 100644 --- a/addons/pos_self_order/static/src/app/pages/order_history_page/order_history_page.xml +++ b/addons/pos_self_order/static/src/app/pages/order_history_page/order_history_page.xml @@ -99,6 +99,7 @@
+
diff --git a/addons/pos_self_order/static/src/app/self_order_index.scss b/addons/pos_self_order/static/src/app/self_order_index.scss index c0e1a9f633a..fd4d842ba77 100644 --- a/addons/pos_self_order/static/src/app/self_order_index.scss +++ b/addons/pos_self_order/static/src/app/self_order_index.scss @@ -67,4 +67,11 @@ small { @include row-cols(4); } } -} \ No newline at end of file +} +ul, ol { + padding: 0; + margin: 0; +} +li { + list-style-type: none; +} diff --git a/addons/pos_self_order/static/src/app/self_order_service.js b/addons/pos_self_order/static/src/app/self_order_service.js index 5a7072b750c..c13110d480e 100644 --- a/addons/pos_self_order/static/src/app/self_order_service.js +++ b/addons/pos_self_order/static/src/app/self_order_service.js @@ -7,13 +7,16 @@ import { Product } from "@pos_self_order/app/models/product"; import { Combo } from "@pos_self_order/app/models/combo"; import { session } from "@web/session"; import { getColor } from "@web/core/colors/colors"; -import { categorySorter } from "@pos_self_order/app/utils"; +import { categorySorter, attributeFormatter } from "@pos_self_order/app/utils"; import { Order } from "@pos_self_order/app/models/order"; import { batched } from "@web/core/utils/timing"; import { useState, markup } from "@odoo/owl"; import { useService } from "@web/core/utils/hooks"; import { registry } from "@web/core/registry"; import { cookie } from "@web/core/browser/cookie"; +import { formatDateTime } from "@web/core/l10n/dates"; +import { printerService } from "@point_of_sale/app/printer/printer_service"; +import { qrCodeSrc } from "@point_of_sale/utils"; export class SelfOrder extends Reactive { constructor(...args) { @@ -21,12 +24,13 @@ export class SelfOrder extends Reactive { this.ready = this.setup(...args).then(() => this); } - async setup(env, { rpc, notification, router }) { + async setup(env, { rpc, notification, router, printer }) { // services this.notification = notification; this.router = router; this.env = env; this.rpc = rpc; + this.printer = printer; // data Object.assign(this, { @@ -48,7 +52,7 @@ export class SelfOrder extends Reactive { this.comboByIds = {}; this.ordering = false; this.orders = []; - this.color = getColor(this.company_color); + this.color = getColor(this.company.color); this.initData(); if (this.config.self_ordering_mode === "kiosk") { @@ -390,7 +394,7 @@ export class SelfOrder extends Reactive { } handleProductChanges(payload) { - const product = new Product(payload.product, this.show_prices_with_tax_included); + const product = new Product(payload.product, this.config.iface_tax_included); this.productByIds[payload.product.id] = product; for (const categ of payload.product.pos_categ_ids) { if (!this.pos_category.map((c) => c.id).includes(categ.id)) { @@ -448,15 +452,70 @@ export class SelfOrder extends Reactive { return product.price_info.display_price_default; } } + getLinePrice(line) { + return this.config.iface_tax_included ? line.price_subtotal_incl : line.price_subtotal; + } + getSelectedAttributes(line) { + const attributeValues = line.attribute_value_ids; + const customAttr = line.custom_attribute_value_ids; + return attributeFormatter(this.attributeById, attributeValues, customAttr); + } + getFullProductName(line) { + const attrs = this.getSelectedAttributes(line); + const attrsStr = " (" + attrs.map((a) => a.value).join(", ") + ")"; + return line.full_product_name + (attrs.length ? attrsStr : ""); + } + getLineDisplayData(line) { + return { + productName: this.getFullProductName(line), + price: this.formatMonetary(this.getLinePrice(line)), + qty: line.qty.toString(), + unitPrice: this.formatMonetary(this.getLinePrice(line) / line.qty), + comboParent: line.combo_parent_uuid + ? this.currentOrder.lines.find((l) => l.uuid === line.combo_parent_uuid) + .full_product_name + : "", + customerNote: line.customer_note, + }; + } + export_for_printing(order) { + order.receiptDate ||= formatDateTime(luxon.DateTime.now()); + return { + orderlines: order.lines.map(this.getLineDisplayData.bind(this)), + amount_total: order.amount_total, + amount_tax: order.amount_tax, + total_without_tax: order.amount_total - order.amount_tax, + total_paid: order.amount_paid, + paymentlines: [{ name: "Bank", amount: order.amount_total }], + change: 0, + tax_details: order.tax_details, + name: order.pos_reference, + date: order.receiptDate, + ticket_code: this.company.point_of_sale_ticket_unique_code && order.ticketCode, + base_url: this.base_url, + footer: this.config.receipt_footer, + headerData: { + header: this.config.receipt_header, + company: this.company, + trackingNumber: order.trackingNumber, + }, + pos_qr_code: + this.company.point_of_sale_use_ticket_qr_code && + qrCodeSrc( + `${this.base_url}/pos/ticket/validate?access_token=${order.access_token}` + ), + }; + } } export const selfOrderService = { - dependencies: ["rpc", "notification", "router"], - async start(env, { rpc, notification, router }) { - return new SelfOrder(env, { rpc, notification, router }).ready; + dependencies: ["rpc", "notification", "router", "printer"], + async start(env, { rpc, notification, router, printer }) { + return new SelfOrder(env, { rpc, notification, router, printer }).ready; }, }; +registry.category("services").add("printer", printerService); registry.category("services").add("self_order", selfOrderService); export function useSelfOrder() { diff --git a/addons/pos_self_order_epson_printer/__init__.py b/addons/pos_self_order_epson_printer/__init__.py new file mode 100644 index 00000000000..dc5e6b693d1 --- /dev/null +++ b/addons/pos_self_order_epson_printer/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/addons/pos_self_order_epson_printer/__manifest__.py b/addons/pos_self_order_epson_printer/__manifest__.py new file mode 100644 index 00000000000..c87ea0cc718 --- /dev/null +++ b/addons/pos_self_order_epson_printer/__manifest__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +{ + 'name': 'POS Self Order Epson Printer', + 'version': '1.0', + 'category': 'Sales/Point of Sale', + 'sequence': 6, + 'summary': 'Epson ePOS Printers in PoS Kiosk', + 'description': "Use Epson ePOS Printers without the IoT Box in the PoS Kiosk", + 'depends': ['pos_epson_printer', 'pos_self_order'], + 'installable': True, + 'auto_install': True, + 'assets': { + 'pos_self_order.assets': [ + 'pos_epson_printer/static/src/app/epson_printer.js', + 'pos_epson_printer/static/src/app/components/epos_templates.xml', + 'pos_self_order_epson_printer/static/src/**/*', + ], + }, + 'license': 'LGPL-3', +} diff --git a/addons/pos_self_order_epson_printer/models/__init__.py b/addons/pos_self_order_epson_printer/models/__init__.py new file mode 100644 index 00000000000..eaa81f34b8a --- /dev/null +++ b/addons/pos_self_order_epson_printer/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import pos_config diff --git a/addons/pos_self_order_epson_printer/models/pos_config.py b/addons/pos_self_order_epson_printer/models/pos_config.py new file mode 100644 index 00000000000..b8a75480bd4 --- /dev/null +++ b/addons/pos_self_order_epson_printer/models/pos_config.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + +class PosConfig(models.Model): + _inherit = 'pos.config' + + + def _get_self_ordering_data(self): + data = super()._get_self_ordering_data() + data["config"]["epson_printer_ip"] = self.epson_printer_ip + data["config"]["other_devices"] = self.other_devices + return data diff --git a/addons/pos_self_order_epson_printer/static/src/overrides/models/self_order_service.js b/addons/pos_self_order_epson_printer/static/src/overrides/models/self_order_service.js new file mode 100644 index 00000000000..50ea3e091a6 --- /dev/null +++ b/addons/pos_self_order_epson_printer/static/src/overrides/models/self_order_service.js @@ -0,0 +1,19 @@ +/** @odoo-module */ + +import { EpsonPrinter } from "@pos_epson_printer/app/epson_printer"; +import { SelfOrder } from "@pos_self_order/app/self_order_service"; +import { patch } from "@web/core/utils/patch"; + +patch(SelfOrder.prototype, { + async setup() { + await super.setup(...arguments); + if (!this.config.epson_printer_ip || !this.config.other_devices) { + return; + } + this.printer.setPrinter( + new EpsonPrinter({ + ip: this.config.epson_printer_ip, + }) + ); + }, +});