diff --git a/addons/l10n_es_pos/__init__.py b/addons/l10n_es_pos/__init__.py
new file mode 100644
index 00000000000..0ee8b5073e2
--- /dev/null
+++ b/addons/l10n_es_pos/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import tests
diff --git a/addons/l10n_es_pos/__manifest__.py b/addons/l10n_es_pos/__manifest__.py
new file mode 100644
index 00000000000..799a833f7af
--- /dev/null
+++ b/addons/l10n_es_pos/__manifest__.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+{
+ 'name': 'Spain - Point of Sale',
+ 'countries': ['es'],
+ 'category': 'Accounting/Localizations/Point of Sale',
+ 'summary': """Spanish localization for Point of Sale""",
+ 'depends': ['point_of_sale', 'l10n_es'],
+ 'auto_install': True,
+ 'license': 'LGPL-3',
+ 'data': [
+ 'views/res_config_settings_views.xml',
+ 'views/pos_order_views.xml',
+ ],
+ 'assets': {
+ 'point_of_sale._assets_pos': [
+ 'l10n_es_pos/static/src/**/*',
+ ],
+ 'web.assets_tests': [
+ 'l10n_es_pos/static/tests/**/*',
+ ],
+ },
+}
diff --git a/addons/l10n_es_pos/models/__init__.py b/addons/l10n_es_pos/models/__init__.py
new file mode 100644
index 00000000000..a6113f350d1
--- /dev/null
+++ b/addons/l10n_es_pos/models/__init__.py
@@ -0,0 +1,5 @@
+from . import pos_config
+from . import pos_order
+from . import pos_session
+from . import res_config_settings
+from . import account_move
diff --git a/addons/l10n_es_pos/models/account_move.py b/addons/l10n_es_pos/models/account_move.py
new file mode 100644
index 00000000000..62c502ee8bd
--- /dev/null
+++ b/addons/l10n_es_pos/models/account_move.py
@@ -0,0 +1,12 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo import models
+
+
+class AccountMove(models.Model):
+ _inherit = 'account.move'
+
+ def _compute_l10n_es_is_simplified(self):
+ super()._compute_l10n_es_is_simplified()
+ for move in self:
+ if move.pos_order_ids:
+ move.l10n_es_is_simplified = move.pos_order_ids[0].is_l10n_es_simplified_invoice
diff --git a/addons/l10n_es_pos/models/pos_config.py b/addons/l10n_es_pos/models/pos_config.py
new file mode 100644
index 00000000000..59c0181e8b2
--- /dev/null
+++ b/addons/l10n_es_pos/models/pos_config.py
@@ -0,0 +1,34 @@
+from odoo import api, fields, models
+
+
+class PosConfig(models.Model):
+ _inherit = "pos.config"
+
+ is_spanish = fields.Boolean(string="Company located in Spain", compute="_is_company_spanish")
+
+ def _is_company_spanish(self):
+ for pos in self:
+ pos.is_spanish = pos.company_id.country_id.code == "ES"
+
+ l10n_es_simplified_invoice_limit = fields.Float(
+ string="Simplified Invoice limit amount",
+ help="Over this amount is not legally possible to create a simplified invoice",
+ default=400,
+ )
+ l10n_es_simplified_invoice_journal_id = fields.Many2one('account.journal')
+ simplified_partner_id = fields.Many2one(
+ "res.partner", string="Simplified invoice partner", compute="_get_simplified_partner"
+ )
+
+ @api.depends("company_id")
+ def _get_simplified_partner(self):
+ for config in self:
+ config.simplified_partner_id = self.env.ref("l10n_es.partner_simplified").id
+
+ def get_limited_partners_loading(self):
+ # this function normally returns 100 partners, but we have to make sure that
+ # the simplified partner is also loaded
+ res = super().get_limited_partners_loading()
+ if (self.simplified_partner_id.id,) not in res:
+ res.append((self.simplified_partner_id.id,))
+ return res
diff --git a/addons/l10n_es_pos/models/pos_order.py b/addons/l10n_es_pos/models/pos_order.py
new file mode 100644
index 00000000000..3de986362f4
--- /dev/null
+++ b/addons/l10n_es_pos/models/pos_order.py
@@ -0,0 +1,28 @@
+from odoo import api, fields, models
+
+class PosOrder(models.Model):
+ _inherit = "pos.order"
+
+ is_l10n_es_simplified_invoice = fields.Boolean("Simplified invoice")
+ l10n_es_simplified_invoice_number = fields.Char("Simplified invoice number", compute="_compute_l10n_es_simplified_invoice_number")
+
+ @api.depends("account_move")
+ def _compute_l10n_es_simplified_invoice_number(self):
+ for order in self:
+ if order.is_l10n_es_simplified_invoice:
+ order.l10n_es_simplified_invoice_number = order.account_move.name
+ else:
+ order.l10n_es_simplified_invoice_number = False
+
+ @api.model
+ def _order_fields(self, ui_order):
+ res = super(PosOrder, self)._order_fields(ui_order)
+ if ui_order.get("is_l10n_es_simplified_invoice"):
+ res.update({"is_l10n_es_simplified_invoice": ui_order["is_l10n_es_simplified_invoice"]})
+ return res
+
+ def _prepare_invoice_vals(self):
+ res = super()._prepare_invoice_vals()
+ if self.config_id.is_spanish and self.is_l10n_es_simplified_invoice:
+ res["journal_id"] = self.config_id.l10n_es_simplified_invoice_journal_id.id
+ return res
diff --git a/addons/l10n_es_pos/models/pos_session.py b/addons/l10n_es_pos/models/pos_session.py
new file mode 100644
index 00000000000..5b156392df4
--- /dev/null
+++ b/addons/l10n_es_pos/models/pos_session.py
@@ -0,0 +1,12 @@
+from odoo import models
+
+
+class PosSession(models.Model):
+ _inherit = "pos.session"
+
+ def _loader_params_res_company(self):
+ res = super()._loader_params_res_company()
+ if not self.config_id.is_spanish:
+ return res
+ res["search_params"]["fields"] += ["street", "city", "zip"]
+ return res
diff --git a/addons/l10n_es_pos/models/res_config_settings.py b/addons/l10n_es_pos/models/res_config_settings.py
new file mode 100644
index 00000000000..d572bb5e3f4
--- /dev/null
+++ b/addons/l10n_es_pos/models/res_config_settings.py
@@ -0,0 +1,18 @@
+from odoo import fields, models
+
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = "res.config.settings"
+
+ pos_is_spanish = fields.Boolean(
+ string="Consider the specific spanish legislation, such as the use of simplified invoices",
+ related="pos_config_id.is_spanish",
+ )
+
+ pos_l10n_es_simplified_invoice_limit = fields.Float(
+ related="pos_config_id.l10n_es_simplified_invoice_limit",
+ readonly=False,
+ )
+ pos_l10n_es_simplified_invoice_journal_id = fields.Many2one(
+ related="pos_config_id.l10n_es_simplified_invoice_journal_id", readonly=False
+ )
diff --git a/addons/l10n_es_pos/static/src/overrides/components/order_receipt/order_receipt.xml b/addons/l10n_es_pos/static/src/overrides/components/order_receipt/order_receipt.xml
new file mode 100644
index 00000000000..af60dd4c78a
--- /dev/null
+++ b/addons/l10n_es_pos/static/src/overrides/components/order_receipt/order_receipt.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
Simplified invoice
+
+
+
+
+
+
()
+
+
+
+
+
Customer:
+
+ :
+
+
+
+
+
+
+
diff --git a/addons/l10n_es_pos/static/src/overrides/components/payment_screen/payment_screen.js b/addons/l10n_es_pos/static/src/overrides/components/payment_screen/payment_screen.js
new file mode 100644
index 00000000000..cdae11bc97a
--- /dev/null
+++ b/addons/l10n_es_pos/static/src/overrides/components/payment_screen/payment_screen.js
@@ -0,0 +1,51 @@
+/** @odoo-module */
+import { patch } from "@web/core/utils/patch";
+import { PaymentScreen } from "@point_of_sale/app/screens/payment_screen/payment_screen";
+
+patch(PaymentScreen.prototype, {
+ async validateOrder(isForceValidate) {
+ if (!this.pos.config.is_spanish) {
+ return await super.validateOrder(...arguments);
+ }
+ const order = this.currentOrder;
+ this.currentOrder.to_invoice = true;
+ const simplified_partner =
+ this.pos.db.partner_by_id[this.pos.config.simplified_partner_id[0]];
+ if (order.canBeSimplifiedInvoiced()) {
+ order.is_l10n_es_simplified_invoice ??= true;
+ } else {
+ order.is_l10n_es_simplified_invoice = false;
+ }
+ order.partner ||= simplified_partner;
+ if (!order.is_l10n_es_simplified_invoice && order.partner.id === simplified_partner.id) {
+ order.partner = null;
+ }
+ return await super.validateOrder(...arguments);
+ },
+ toggleIsToInvoice() {
+ super.toggleIsToInvoice(...arguments);
+ if (
+ this.currentOrder.to_invoice &&
+ this.pos.config.is_spanish &&
+ this.currentOrder.canBeSimplifiedInvoiced()
+ ) {
+ this.currentOrder.is_l10n_es_simplified_invoice = false;
+ }
+ },
+ shouldDownloadInvoice() {
+ return this.pos.config.is_spanish
+ ? !this.pos.selectedOrder.is_l10n_es_simplified_invoice
+ : super.shouldDownloadInvoice();
+ },
+ async _postPushOrderResolve(order, order_server_ids) {
+ if (this.pos.config.is_spanish) {
+ const savedOrder = await this.orm.searchRead(
+ "pos.order",
+ [["id", "in", order_server_ids]],
+ ["account_move"]
+ );
+ order.invoice_name = savedOrder[0].account_move[1];
+ }
+ return super._postPushOrderResolve(...arguments);
+ },
+});
diff --git a/addons/l10n_es_pos/static/src/overrides/models/order.js b/addons/l10n_es_pos/static/src/overrides/models/order.js
new file mode 100644
index 00000000000..66ed52379cd
--- /dev/null
+++ b/addons/l10n_es_pos/static/src/overrides/models/order.js
@@ -0,0 +1,23 @@
+/** @odoo-module */
+import { patch } from "@web/core/utils/patch";
+import { Order } from "@point_of_sale/app/store/models";
+
+patch(Order.prototype, {
+ canBeSimplifiedInvoiced() {
+ return (
+ this.pos.config.is_spanish &&
+ this.env.utils.roundCurrency(this.get_total_with_tax()) <
+ this.pos.config.l10n_es_simplified_invoice_limit
+ );
+ },
+ wait_for_push_order() {
+ return this.pos.config.is_spanish ? true : super.wait_for_push_order(...arguments);
+ },
+ export_as_JSON() {
+ const json = super.export_as_JSON(...arguments);
+ if (this.pos.config.is_spanish) {
+ json.is_l10n_es_simplified_invoice = this.is_l10n_es_simplified_invoice;
+ }
+ return json;
+ },
+});
diff --git a/addons/l10n_es_pos/static/tests/tours/helpers/receipt_helpers.js b/addons/l10n_es_pos/static/tests/tours/helpers/receipt_helpers.js
new file mode 100644
index 00000000000..34db755c0c5
--- /dev/null
+++ b/addons/l10n_es_pos/static/tests/tours/helpers/receipt_helpers.js
@@ -0,0 +1,22 @@
+/** @odoo-module */
+
+import { ProductScreen } from "@point_of_sale/../tests/tours/helpers/ProductScreenTourMethods";
+import { PaymentScreen } from "@point_of_sale/../tests/tours/helpers/PaymentScreenTourMethods";
+
+export function checkSimplifiedInvoiceNumber(number) {
+ return [
+ {
+ content: "verify that the simplified invoice number appears correctly on the receipt",
+ trigger: `.receipt-screen .simplified-invoice-number:contains('${number}')`,
+ isCheck: true,
+ },
+ ];
+}
+
+export function pay() {
+ return [
+ ...ProductScreen.do.clickPayButton(),
+ ...PaymentScreen.do.clickPaymentMethod("Bank"),
+ ...PaymentScreen.do.clickValidate(),
+ ];
+}
diff --git a/addons/l10n_es_pos/static/tests/tours/spanish_pos_tour.js b/addons/l10n_es_pos/static/tests/tours/spanish_pos_tour.js
new file mode 100644
index 00000000000..1c3d53c8df6
--- /dev/null
+++ b/addons/l10n_es_pos/static/tests/tours/spanish_pos_tour.js
@@ -0,0 +1,51 @@
+/** @odoo-module */
+
+import { ProductScreen } from "@point_of_sale/../tests/tours/helpers/ProductScreenTourMethods";
+import { ReceiptScreen } from "@point_of_sale/../tests/tours/helpers/ReceiptScreenTourMethods";
+import { PaymentScreen } from "@point_of_sale/../tests/tours/helpers/PaymentScreenTourMethods";
+import { PartnerListScreen } from "@point_of_sale/../tests/tours/helpers/PartnerListScreenTourMethods";
+import { registry } from "@web/core/registry";
+import { checkSimplifiedInvoiceNumber, pay } from "./helpers/receipt_helpers";
+
+const SIMPLIFIED_INVOICE_LIMIT = 1000;
+
+registry.category("web_tour.tours").add("spanish_pos_tour", {
+ test: true,
+ steps: () => [
+ ...ProductScreen.do.confirmOpeningPopup(),
+
+ ...ProductScreen.exec.addOrderline("Desk Pad", "1"),
+ ...pay(),
+ ...checkSimplifiedInvoiceNumber("0001"),
+ ...ReceiptScreen.do.clickNextOrder(),
+
+ ...ProductScreen.exec.addOrderline("Desk Pad", "1", SIMPLIFIED_INVOICE_LIMIT - 1),
+ ...pay(),
+ ...checkSimplifiedInvoiceNumber("0002"),
+ ...ReceiptScreen.do.clickNextOrder(),
+
+ ...ProductScreen.exec.addOrderline("Desk Pad", "1", SIMPLIFIED_INVOICE_LIMIT + 1),
+ ...pay(),
+
+ {
+ content: "verify that the pos requires the selection of a partner",
+ trigger: `div.popup.popup-confirm .modal-header:contains('Please select the Customer') ~ footer div.button.confirm`,
+ },
+
+ ...PartnerListScreen.do.clickPartner(""),
+
+ ...PaymentScreen.check.isInvoiceOptionSelected(),
+ ...PaymentScreen.do.clickValidate(),
+ {
+ content:
+ "verify that the simplified invoice number does not appear on the receipt, because this order is invoiced, so it does not have a simplified invoice number",
+ trigger: ".receipt-screen:not(:has(.simplified-invoice-number))",
+ isCheck: true,
+ },
+ ...ReceiptScreen.do.clickNextOrder(),
+
+ ...ProductScreen.exec.addOrderline("Desk Pad", "1"),
+ ...pay(),
+ ...checkSimplifiedInvoiceNumber("0003"),
+ ],
+});
diff --git a/addons/l10n_es_pos/tests/__init__.py b/addons/l10n_es_pos/tests/__init__.py
new file mode 100644
index 00000000000..6f4bae0c7a2
--- /dev/null
+++ b/addons/l10n_es_pos/tests/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import test_frontend
diff --git a/addons/l10n_es_pos/tests/test_frontend.py b/addons/l10n_es_pos/tests/test_frontend.py
new file mode 100644
index 00000000000..7e4309ce2ba
--- /dev/null
+++ b/addons/l10n_es_pos/tests/test_frontend.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import odoo.tests
+from odoo.addons.point_of_sale.tests.test_frontend import TestPointOfSaleHttpCommon
+
+
+@odoo.tests.tagged('post_install_l10n', 'post_install', '-at_install')
+class TestUi(TestPointOfSaleHttpCommon):
+ @classmethod
+ def _get_main_company(cls):
+ cls.company_data["company"].country_id = cls.env.ref("base.es").id
+ return cls.company_data["company"]
+
+ def test_spanish_pos(self):
+ simp = self.env['account.journal'].create({
+ 'name': 'Simplified Invoice Journal',
+ 'type': 'sale',
+ 'company_id': self._get_main_company().id,
+ 'code': 'SIMP',
+ })
+ def get_number_of_regular_invoices():
+ return self.env['account.move'].search_count([('journal_id', '=', self.main_pos_config.invoice_journal_id.id), ('l10n_es_is_simplified', '=', False), ('pos_order_ids', '!=', False)])
+ initial_number_of_regular_invoices = get_number_of_regular_invoices()
+ self.main_pos_config.l10n_es_simplified_invoice_journal_id = simp
+ # this `limit` value is linked to the `SIMPLIFIED_INVOICE_LIMIT` const in the tour
+ self.main_pos_config.l10n_es_simplified_invoice_limit = 1000
+ self.main_pos_config.with_user(self.pos_user).open_ui()
+ self.start_tour(f"/pos/ui?config_id={self.main_pos_config.id}", "spanish_pos_tour", login="pos_user")
+ num_of_simp_invoices = self.env['account.move'].search_count([('journal_id', '=', simp.id), ('l10n_es_is_simplified', '=', True)])
+ num_of_regular_invoices = get_number_of_regular_invoices() - initial_number_of_regular_invoices
+ self.assertEqual(num_of_simp_invoices, 3)
+ self.assertEqual(num_of_regular_invoices, 1)
diff --git a/addons/l10n_es_pos/views/pos_order_views.xml b/addons/l10n_es_pos/views/pos_order_views.xml
new file mode 100644
index 00000000000..fda3d4687ac
--- /dev/null
+++ b/addons/l10n_es_pos/views/pos_order_views.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ pos.order.form
+ pos.order
+
+
+
+
+
+
+
+
+
+ pos.order
+
+
+
+
+
+
+
+
diff --git a/addons/l10n_es_pos/views/res_config_settings_views.xml b/addons/l10n_es_pos/views/res_config_settings_views.xml
new file mode 100644
index 00000000000..3a53ec62b75
--- /dev/null
+++ b/addons/l10n_es_pos/views/res_config_settings_views.xml
@@ -0,0 +1,29 @@
+
+
+
+ res.config.settings.view.form
+ res.config.settings
+
+
+
+
+
+
+ Above this limit the simplified invoice won't be made
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py
index 6ee5e005daf..1248dc5634d 100644
--- a/addons/point_of_sale/controllers/main.py
+++ b/addons/point_of_sale/controllers/main.py
@@ -53,7 +53,7 @@ class PosController(PortalAccount):
pos_session = request.env['pos.session'].sudo().search(domain, limit=1)
if not pos_session or config_id and not pos_config.active:
return request.redirect('/web#action=point_of_sale.action_client_pos_menu')
- # The POS only work in one company, so we enforce the one of the session in the context
+ # The POS only works in one company, so we enforce the one of the session in the context
company = pos_session.company_id
session_info = request.env['ir.http'].session_info()
session_info['user_context']['allowed_company_ids'] = company.ids
diff --git a/addons/point_of_sale/static/src/app/navbar/cash_move_popup/cash_move_receipt_template.xml b/addons/point_of_sale/static/src/app/navbar/cash_move_popup/cash_move_receipt_template.xml
index 51ccd4f3c32..7a08fa906d8 100644
--- a/addons/point_of_sale/static/src/app/navbar/cash_move_popup/cash_move_receipt_template.xml
+++ b/addons/point_of_sale/static/src/app/navbar/cash_move_popup/cash_move_receipt_template.xml
@@ -14,11 +14,9 @@