diff --git a/pos_safe_box/README.rst b/pos_safe_box/README.rst
new file mode 100644
index 000000000..f8194777b
--- /dev/null
+++ b/pos_safe_box/README.rst
@@ -0,0 +1,7 @@
+.. image:: https://img.shields.io/badge/licence-LGPL--3-blue.svg
+ :target: https://www.gnu.org/licenses/lgpl-3.0-standalone.html
+ :alt: License: LGPL-3
+
+============
+POS safe box
+============
diff --git a/pos_safe_box/__init__.py b/pos_safe_box/__init__.py
new file mode 100644
index 000000000..56b694d7b
--- /dev/null
+++ b/pos_safe_box/__init__.py
@@ -0,0 +1,5 @@
+# Copyright 2017 Creu Blanca
+# Copyright 2017 Eficent Business and IT Consulting Services, S.L.
+# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
+
+from . import models
diff --git a/pos_safe_box/__manifest__.py b/pos_safe_box/__manifest__.py
new file mode 100644
index 000000000..6f99c6ec7
--- /dev/null
+++ b/pos_safe_box/__manifest__.py
@@ -0,0 +1,23 @@
+# Copyright 2017 Creu Blanca
+# Copyright 2017 Eficent Business and IT Consulting Services, S.L.
+# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
+
+{
+ "name": "Safe Box with PoS",
+ "version": "16.0.1.0.0",
+ "author": "Eficent, CreuBlanca",
+ "depends": ["safe_box", "pos_close_approval", "pos_session_pay_invoice"],
+ "data": [
+ "security/ir.model.access.csv",
+ "data/ir_sequence_data.xml",
+ "views/pos_session_validation_views.xml",
+ "views/pos_session_views.xml",
+ "views/res_config_settings.xml",
+ "views/safe_box_group_views.xml",
+ "views/safe_box_coin_views.xml",
+ ],
+ "website": "https://github.com/tegin/cb-addons",
+ "license": "AGPL-3",
+ "installable": True,
+ "auto_install": False,
+}
diff --git a/pos_safe_box/data/ir_sequence_data.xml b/pos_safe_box/data/ir_sequence_data.xml
new file mode 100644
index 000000000..a93838593
--- /dev/null
+++ b/pos_safe_box/data/ir_sequence_data.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ Point of Sale session validation
+ pos.session.validation
+ SES/%(range_year)s/
+
+
+
+
+ 4
+
+
diff --git a/pos_safe_box/i18n/es.po b/pos_safe_box/i18n/es.po
new file mode 100644
index 000000000..6aac7eabf
--- /dev/null
+++ b/pos_safe_box/i18n/es.po
@@ -0,0 +1,319 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * pos_safe_box
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-11-14 08:31+0000\n"
+"PO-Revision-Date: 2018-11-14 08:31+0000\n"
+"Last-Translator: <>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: pos_safe_box
+#: code:addons/pos_safe_box/models/pos_session_validation.py:128
+#, python-format
+msgid "Account cannot be found for this company"
+msgstr "No se puede encontrar una cuenta para esta empresa"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_amount
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_line_amount
+msgid "Amount"
+msgstr "Importe"
+
+#. module: pos_safe_box
+#: model:ir.ui.view,arch_db:pos_safe_box.view_pos_session_validation_form
+msgid "Approve"
+msgstr "Aprobar"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_approve_date
+msgid "Approve Date"
+msgstr "Fecha de aprobación"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_approve_move_id
+msgid "Approve move"
+msgstr "Aprobar mudanza"
+
+#. module: pos_safe_box
+#: selection:pos.session.validation,state:0
+msgid "Approved"
+msgstr "Aprobado"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_cash_amount
+msgid "Cash Amount"
+msgstr "Importe en efectivo"
+
+#. module: pos_safe_box
+#: model:ir.ui.view,arch_db:pos_safe_box.view_pos_session_validation_form
+msgid "Close"
+msgstr "Cerrar"
+
+#. module: pos_safe_box
+#: selection:pos.session.validation,state:0
+msgid "Closed"
+msgstr "Cerrado"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_closing_date
+msgid "Closing Date"
+msgstr "Fecha de cierre"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_closing_move_id
+msgid "Closing move"
+msgstr "Movimiento de cierre"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_line_safe_box_coin_id
+#: selection:safe.box.coin,type:0
+msgid "Coin"
+msgstr "Moneda"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_coin_amount
+msgid "Coin Amount"
+msgstr "Importe de monedas"
+
+#. module: pos_safe_box
+#: model:ir.ui.view,arch_db:pos_safe_box.view_pos_session_validation_form
+msgid "Coins"
+msgstr "Monedas"
+
+#. module: pos_safe_box
+#: code:addons/pos_safe_box/models/pos_session_validation.py:156
+#, python-format
+msgid "Coins and Notes must match cash value"
+msgstr "Las monedas y los billetes deben coincidir con el valor en efectivo"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_create_uid
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_line_create_uid
+msgid "Created by"
+msgstr "Creado por"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_create_date
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_line_create_date
+msgid "Created on"
+msgstr "Creado en"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_currency_id
+msgid "Currency"
+msgstr "Moneda"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_date
+msgid "Date"
+msgstr "Fecha"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_display_name
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_line_display_name
+msgid "Display Name"
+msgstr "Nombre mostrado"
+
+#. module: pos_safe_box
+#: selection:pos.session.validation,state:0
+msgid "Draft"
+msgstr "Borrador"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_id
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_line_id
+msgid "ID"
+msgstr "ID"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_issue_statement_line_ids
+msgid "Issue Statement Line"
+msgstr "Línea de estado de emisión"
+
+#. module: pos_safe_box
+#: model:ir.ui.view,arch_db:pos_safe_box.view_pos_session_validation_form
+msgid "Issues"
+msgstr "Cuestiones"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation___last_update
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_line___last_update
+msgid "Last Modified on"
+msgstr "Última modificación el"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_line_write_uid
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_write_uid
+msgid "Last Updated by"
+msgstr "Última actualización por"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_line_write_date
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_write_date
+msgid "Last Updated on"
+msgstr "Última actualización el"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_line_ids
+msgid "Line"
+msgstr "Linea"
+
+#. module: pos_safe_box
+#: model:ir.ui.view,arch_db:pos_safe_box.view_pos_session_validation_line_tree
+msgid "Lines"
+msgstr "Lines"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_name
+msgid "Name"
+msgstr "Nombre"
+
+#. module: pos_safe_box
+#: selection:safe.box.coin,type:0
+msgid "Note"
+msgstr "Nota"
+
+#. module: pos_safe_box
+#: code:addons/pos_safe_box/models/safe_box_group.py:56
+#, python-format
+msgid "Only one validation session is allowed"
+msgstr "Solo se permite una sesión de validación"
+
+#. module: pos_safe_box
+#: model:ir.ui.menu,name:pos_safe_box.pos_session_validation_menu
+msgid "PoS Session validation"
+msgstr "Validación de poS Session"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_pos_session_validation_id
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_line_pos_session_validation_id
+msgid "Pos Session Validation"
+msgstr "Validación Session Validation"
+
+#. module: pos_safe_box
+#: model:ir.ui.view,arch_db:pos_safe_box.view_pos_config_form
+msgid "Safe box"
+msgstr "Caja fuerte"
+
+#. module: pos_safe_box
+#: model:ir.model,name:pos_safe_box.model_safe_box_coin
+msgid "Safe box coin"
+msgstr "Moneda caja fuerte"
+
+#. module: pos_safe_box
+#: model:ir.model,name:pos_safe_box.model_safe_box_group
+msgid "Safe box group"
+msgstr "Grupo de caja de seguridad"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_config_safe_box_group_id
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_safe_box_group_id
+msgid "Safe box system"
+msgstr "Sistema de caja de seguridad"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_safe_box_group_approve_coin_safe_box_id
+msgid "Safe box where coins are stored on approval"
+msgstr "Caja de seguridad donde las monedas se almacenan"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_safe_box_group_coin_safe_box_id
+msgid "Safe box where coins are stored on closure"
+msgstr "Caja de seguridad donde se guardan las monedas del cierre"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_safe_box_group_approve_note_safe_box_id
+msgid "Safe box where notes are stored on approval"
+msgstr "Caja de seguridad donde las notas se almacenan"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_safe_box_group_note_safe_box_id
+msgid "Safe box where notes are stored on closure"
+msgstr "Caja de seguridad donde se guardan las notas al cierre"
+
+#. module: pos_safe_box
+#: code:addons/pos_safe_box/models/pos_session_validation.py:171
+#, python-format
+msgid "Safe boxes are not configured"
+msgstr "Las cajas de seguridad no están configuradas"
+
+#. module: pos_safe_box
+#: model:ir.actions.act_window,name:pos_safe_box.pos_session_validation_action
+#: model:ir.ui.view,arch_db:pos_safe_box.view_pos_session_validation_form
+msgid "Session Validation"
+msgstr "Validacion de sesion"
+
+#. module: pos_safe_box
+#: model:ir.model,name:pos_safe_box.model_pos_session_validation
+msgid "Session validation"
+msgstr "Validacion de sesion"
+
+#. module: pos_safe_box
+#: model:ir.ui.view,arch_db:pos_safe_box.view_pos_session_validation_tree
+msgid "Session validations"
+msgstr "Validaciones de sesion"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_pos_session_ids
+#: model:ir.ui.view,arch_db:pos_safe_box.view_pos_session_validation_form
+msgid "Sessions"
+msgstr "Sesiones"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_state
+msgid "State"
+msgstr "Estado"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_statement_ids
+msgid "Statement"
+msgstr "Extracto"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_statement_line_ids
+msgid "Statement Line"
+msgstr "Línea de declaración"
+
+#. module: pos_safe_box
+#: model:ir.ui.view,arch_db:pos_safe_box.view_pos_session_validation_form
+msgid "Statements"
+msgstr "Extractos"
+
+#. module: pos_safe_box
+#: model:ir.ui.view,arch_db:pos_safe_box.view_pos_session_validation_line_tree
+msgid "Total amount"
+msgstr "Importe total"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_safe_box_coin_type
+msgid "Type"
+msgstr "Tipo"
+
+#. module: pos_safe_box
+#: model:ir.model.fields,field_description:pos_safe_box.field_pos_session_validation_line_value
+msgid "Value"
+msgstr "Valor"
+
+#. module: pos_safe_box
+#: model:ir.model,name:pos_safe_box.model_pos_config
+msgid "pos.config"
+msgstr "pos.config"
+
+#. module: pos_safe_box
+#: model:ir.model,name:pos_safe_box.model_pos_session
+msgid "pos.session"
+msgstr "pos.session"
+
+#. module: pos_safe_box
+#: model:ir.model,name:pos_safe_box.model_pos_session_validation_line
+msgid "pos.session.validation.line"
+msgstr "pos.session.validation.line"
+
diff --git a/pos_safe_box/models/__init__.py b/pos_safe_box/models/__init__.py
new file mode 100644
index 000000000..d4f49dfe8
--- /dev/null
+++ b/pos_safe_box/models/__init__.py
@@ -0,0 +1,10 @@
+# Copyright 2017 Creu Blanca
+# Copyright 2017 Eficent Business and IT Consulting Services, S.L.
+# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
+
+from . import pos_session
+from . import pos_config
+from . import pos_session_validation
+from . import safe_box_group
+from . import safe_box_coin
+from . import res_config_settings
diff --git a/pos_safe_box/models/pos_config.py b/pos_safe_box/models/pos_config.py
new file mode 100644
index 000000000..d7052ade7
--- /dev/null
+++ b/pos_safe_box/models/pos_config.py
@@ -0,0 +1,11 @@
+# Copyright 2017 Creu Blanca
+# Copyright 2017 Eficent Business and IT Consulting Services, S.L.
+# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
+
+from odoo import fields, models
+
+
+class PosConfig(models.Model):
+ _inherit = "pos.config"
+
+ safe_box_group_id = fields.Many2one("safe.box.group", string="Safe box system")
diff --git a/pos_safe_box/models/pos_session.py b/pos_safe_box/models/pos_session.py
new file mode 100644
index 000000000..cdf3bcf66
--- /dev/null
+++ b/pos_safe_box/models/pos_session.py
@@ -0,0 +1,19 @@
+# Copyright 2017 Creu Blanca
+# Copyright 2017 Eficent Business and IT Consulting Services, S.L.
+# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
+
+from odoo import fields, models
+
+
+class PosSession(models.Model):
+ _inherit = "pos.session"
+
+ pos_session_validation_id = fields.Many2one("pos.session.validation", readonly=True)
+
+ def action_pos_session_close(self, *args, **kwargs):
+ res = super(PosSession, self).action_pos_session_close(*args, **kwargs)
+ for session in self.filtered(lambda r: r.state == "closed"):
+ sbg = session.config_id.safe_box_group_id
+ if sbg:
+ self.pos_session_validation_id = sbg.get_current_session_validation()
+ return res
diff --git a/pos_safe_box/models/pos_session_validation.py b/pos_safe_box/models/pos_session_validation.py
new file mode 100644
index 000000000..cd04e2709
--- /dev/null
+++ b/pos_safe_box/models/pos_session_validation.py
@@ -0,0 +1,240 @@
+# Copyright 2017 Creu Blanca
+# Copyright 2017 Eficent Business and IT Consulting Services, S.L.
+# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
+
+from odoo import _, api, fields, models, tools
+from odoo.exceptions import ValidationError
+
+
+class PosSessionValidation(models.Model):
+ _name = "pos.session.validation"
+ _description = "Session validation"
+ _order = "id desc"
+
+ name = fields.Char(default="/", required=True, readonly=True)
+ date = fields.Date(default=lambda self: fields.Date.today(), readonly=True)
+ safe_box_group_id = fields.Many2one(
+ "safe.box.group",
+ string="Safe box system",
+ required=True,
+ readonly=True,
+ )
+ pos_session_ids = fields.One2many(
+ "pos.session",
+ inverse_name="pos_session_validation_id",
+ string="Sessions",
+ readonly=True,
+ )
+ line_ids = fields.One2many(
+ comodel_name="pos.session.validation.line",
+ inverse_name="pos_session_validation_id",
+ )
+ state = fields.Selection(
+ [("draft", "Draft"), ("closed", "Closed"), ("approved", "Approved")]
+ )
+ statement_line_ids = fields.One2many(
+ comodel_name="account.bank.statement.line",
+ compute="_compute_statement_line_ids",
+ readonly=True,
+ )
+ issue_statement_line_ids = fields.One2many(
+ comodel_name="account.bank.statement.line",
+ compute="_compute_statement_values",
+ )
+ currency_id = fields.Many2one(
+ "res.currency", related="safe_box_group_id.currency_id", readonly=True
+ )
+ amount = fields.Monetary(
+ currency_field="currency_id", compute="_compute_statement_values"
+ )
+ coin_amount = fields.Monetary(
+ currency_field="currency_id", compute="_compute_amount"
+ )
+ cash_amount = fields.Monetary(
+ currency_field="currency_id", compute="_compute_statement_values"
+ )
+ closing_move_id = fields.Many2one("safe.box.move", "Closing move", readonly=True)
+ closing_date = fields.Datetime(readonly=True)
+ approve_move_id = fields.Many2one("safe.box.move", "Approve move", readonly=True)
+ approve_date = fields.Datetime(readonly=True)
+
+ @api.depends("pos_session_ids")
+ def _compute_statement_line_ids(self):
+ for record in self:
+ record.statement_line_ids = record.pos_session_ids.mapped(
+ "statement_line_ids"
+ )
+
+ @api.depends("line_ids")
+ def _compute_amount(self):
+ for record in self:
+ record.coin_amount = sum(record.line_ids.mapped("amount"))
+
+ def _compute_statement_amount(self):
+ lines = self.pos_session_ids.mapped("statement_line_ids")
+ lines_not_computed = lines.filtered(
+ lambda r: r.account_id
+ not in r.pos_session_id.payment_method_ids.mapped("receivable_account_id")
+ )
+ payments = self.pos_session_ids.mapped("order_ids.payment_ids")
+ amount = sum(lines_not_computed.mapped("amount")) + sum(
+ payments.mapped("amount")
+ )
+ return amount
+
+ @api.depends("statement_line_ids", "pos_session_ids")
+ def _compute_statement_values(self):
+ for record in self:
+ record.amount = sum(record.statement_line_ids.mapped("amount"))
+ record.amount = record._compute_statement_amount()
+ record.cash_amount = sum(
+ record.statement_line_ids.filtered(
+ lambda r: r.journal_id.type == "cash"
+ ).mapped("amount")
+ )
+ lines = record.statement_line_ids
+ record.issue_statement_line_ids = lines.filtered(lambda r: not r.invoice_id)
+
+ def safe_box_move_vals(self):
+ return {"safe_box_group_id": self.safe_box_group_id.id}
+
+ def safe_box_move_line_vals(self, move, safe_box, value):
+ return {
+ "safe_box_move_id": move.id,
+ "safe_box_id": safe_box,
+ "amount": value,
+ }
+
+ def account_move_vals(self, journal, amount):
+ account = self.safe_box_group_id.account_ids.filtered(
+ lambda r: r.company_id.id == journal.company_id.id
+ )
+ if not account:
+ raise ValidationError(_("Account cannot be found for this company"))
+ statement_account = journal.default_account_id
+ return {
+ "journal_id": journal.id,
+ "safe_box_move_id": self.closing_move_id.id,
+ "line_ids": [
+ (
+ 0,
+ 0,
+ {
+ "account_id": statement_account.id,
+ "credit": amount if amount > 0 else 0,
+ "debit": -amount if amount < 0 else 0,
+ },
+ ),
+ (
+ 0,
+ 0,
+ {
+ "account_id": account.id,
+ "credit": -amount if amount < 0 else 0,
+ "debit": amount if amount > 0 else 0,
+ },
+ ),
+ ],
+ }
+
+ def close(self):
+ self.ensure_one()
+ if self.state != "draft":
+ raise ValidationError(_("You can only approve draft moves"))
+ if self.coin_amount != self.cash_amount:
+ raise ValidationError(_("Coins and Notes must match cash value"))
+ self.closing_move_id = self.env["safe.box.move"].create(
+ self.safe_box_move_vals()
+ )
+ lines = {}
+ for line in self.line_ids:
+ if not lines.get(line.safe_box_coin_id.type):
+ lines[line.safe_box_coin_id.type] = 0
+ lines[line.safe_box_coin_id.type] += line.amount
+ for key in lines.keys():
+ safe_box = False
+ if key == "note":
+ safe_box = self.safe_box_group_id.note_safe_box_id.id
+ elif key == "coin":
+ safe_box = self.safe_box_group_id.coin_safe_box_id.id
+ if not safe_box:
+ raise ValidationError(_("Safe boxes are not configured"))
+ self.env["safe.box.move.line"].create(
+ self.safe_box_move_line_vals(self.closing_move_id, safe_box, lines[key])
+ )
+ for journal in self.statement_line_ids.journal_id.filtered(
+ lambda r: (r.type == "cash")
+ ):
+ amount = sum(
+ self.statement_line_ids.filtered(
+ lambda r: r.journal_id == journal
+ ).mapped("amount")
+ )
+ if tools.float_is_zero(
+ amount,
+ precision_rounding=(
+ journal.currency_id or journal.company_id.currency_id
+ ).rounding,
+ ):
+ continue
+ move = self.env["account.move"].create(
+ self.account_move_vals(journal, amount)
+ )
+ move.action_post()
+ self.closing_move_id.close()
+ self.write({"state": "closed", "closing_date": fields.Datetime.now()})
+
+ def approve(self):
+ self.ensure_one()
+ if self.state != "closed":
+ raise ValidationError(_("You can only approve closed moves"))
+ sbg = self.safe_box_group_id
+ lines = []
+ for initial_safe_box, end_safe_box in [
+ (sbg.coin_safe_box_id, sbg.approve_coin_safe_box_id),
+ (sbg.note_safe_box_id, sbg.approve_note_safe_box_id),
+ ]:
+ if end_safe_box:
+ value = self.closing_move_id.line_ids.filtered(
+ lambda r: r.safe_box_id.id == initial_safe_box.id
+ ).amount
+ lines.append({"safe_box_id": end_safe_box.id, "amount": value})
+ lines.append({"safe_box_id": initial_safe_box.id, "amount": -value})
+ if len(lines) > 0:
+ self.approve_move_id = self.env["safe.box.move"].create(
+ {
+ "safe_box_group_id": self.safe_box_group_id.id,
+ "line_ids": [(0, 0, line) for line in lines],
+ }
+ )
+ self.approve_move_id.close()
+ self.write({"state": "approved", "approve_date": fields.Datetime.now()})
+
+ @api.model
+ def get_name(self, vals):
+ return self.env["ir.sequence"].next_by_code("pos.session.validation") or "/"
+
+ @api.model
+ def create(self, vals):
+ if vals.get("name", "/") == "/":
+ vals.update({"name": self.get_name(vals)})
+ return super(PosSessionValidation, self).create(vals)
+
+
+class PosSessionValidationLine(models.Model):
+ _name = "pos.session.validation.line"
+ _description = "Add amount on validation"
+
+ pos_session_validation_id = fields.Many2one(
+ "pos.session.validation", required=True, readonly=True
+ )
+ safe_box_coin_id = fields.Many2one(
+ "safe.box.coin", required=True, string="Coin", readonly=True
+ )
+ value = fields.Integer(required=True, default=0)
+ amount = fields.Float(compute="_compute_amount")
+
+ @api.depends("value", "safe_box_coin_id")
+ def _compute_amount(self):
+ for record in self:
+ record.amount = record.safe_box_coin_id.rate * record.value
diff --git a/pos_safe_box/models/res_config_settings.py b/pos_safe_box/models/res_config_settings.py
new file mode 100644
index 000000000..1a087e7fb
--- /dev/null
+++ b/pos_safe_box/models/res_config_settings.py
@@ -0,0 +1,15 @@
+# Copyright 2025 Dixmit
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+
+class ResConfigSettings(models.TransientModel):
+
+ _inherit = "res.config.settings"
+
+ pos_safe_box_group_id = fields.Many2one(
+ related="pos_config_id.safe_box_group_id",
+ string="Safe box system",
+ readonly=False,
+ )
diff --git a/pos_safe_box/models/safe_box_coin.py b/pos_safe_box/models/safe_box_coin.py
new file mode 100644
index 000000000..b42c8c3ea
--- /dev/null
+++ b/pos_safe_box/models/safe_box_coin.py
@@ -0,0 +1,13 @@
+# Copyright 2017 Creu Blanca
+# Copyright 2017 Eficent Business and IT Consulting Services, S.L.
+# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
+
+from odoo import fields, models
+
+
+class SafeBoxCoin(models.Model):
+ _inherit = "safe.box.coin"
+
+ type = fields.Selection(
+ [("coin", "Coin"), ("note", "Note")], default="coin", required=True
+ )
diff --git a/pos_safe_box/models/safe_box_group.py b/pos_safe_box/models/safe_box_group.py
new file mode 100644
index 000000000..f6ce5ac4e
--- /dev/null
+++ b/pos_safe_box/models/safe_box_group.py
@@ -0,0 +1,52 @@
+# Copyright 2017 Creu Blanca
+# Copyright 2017 Eficent Business and IT Consulting Services, S.L.
+# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
+
+from odoo import _, exceptions, fields, models
+
+
+class SafeBoxGroup(models.Model):
+ _inherit = "safe.box.group"
+
+ coin_safe_box_id = fields.Many2one(
+ "safe.box",
+ domain="[('id', 'in', safe_box_ids)]",
+ string="Safe box where coins are stored on closure",
+ )
+ approve_coin_safe_box_id = fields.Many2one(
+ "safe.box",
+ domain="[('id', 'in', safe_box_ids)]",
+ string="Safe box where coins are stored on approval",
+ )
+ note_safe_box_id = fields.Many2one(
+ "safe.box",
+ domain="[('id', 'in', safe_box_ids)]",
+ string="Safe box where notes are stored on closure",
+ )
+ approve_note_safe_box_id = fields.Many2one(
+ "safe.box",
+ domain="[('id', 'in', safe_box_ids)]",
+ string="Safe box where notes are stored on approval",
+ )
+
+ def session_validation_vals(self):
+ return {
+ "safe_box_group_id": self.id,
+ "state": "draft",
+ "line_ids": [
+ (0, 0, {"safe_box_coin_id": coin.id}) for coin in self.coin_ids
+ ],
+ }
+
+ def get_current_session_validation(self):
+ self.ensure_one()
+ validation = self.env["pos.session.validation"].search(
+ [("safe_box_group_id", "=", self.id), ("state", "=", "draft")]
+ )
+ if not validation:
+ return self.env["pos.session.validation"].create(
+ self.session_validation_vals()
+ )
+ if len(validation.ids) > 1:
+ raise exceptions.Warning(_("Only one validation session is allowed"))
+ return validation
diff --git a/pos_safe_box/security/ir.model.access.csv b/pos_safe_box/security/ir.model.access.csv
new file mode 100644
index 000000000..496567e4f
--- /dev/null
+++ b/pos_safe_box/security/ir.model.access.csv
@@ -0,0 +1,5 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_pos_session_validation,access_pos_session_validation,model_pos_session_validation,point_of_sale.group_pos_user,1,1,1,0
+access_pos_session_validation_safe_box,access_pos_session_validation_safe_box,model_pos_session_validation,safe_box.group_safe_box_user,1,1,1,0
+access_pos_session_validation_line,access_pos_session_validation_line,model_pos_session_validation_line,point_of_sale.group_pos_user,1,1,1,0
+access_pos_session_validation_line_safe_box,access_pos_session_validation_line_safe_box,model_pos_session_validation_line,safe_box.group_safe_box_user,1,1,1,0
diff --git a/pos_safe_box/static/description/icon.png b/pos_safe_box/static/description/icon.png
new file mode 100644
index 000000000..270165a99
Binary files /dev/null and b/pos_safe_box/static/description/icon.png differ
diff --git a/pos_safe_box/tests/__init__.py b/pos_safe_box/tests/__init__.py
new file mode 100644
index 000000000..6b9a34f0e
--- /dev/null
+++ b/pos_safe_box/tests/__init__.py
@@ -0,0 +1,5 @@
+# Copyright 2017 Creu Blanca
+# Copyright 2017 Eficent Business and IT Consulting Services, S.L.
+# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
+
+from . import test_pos_safe_box
diff --git a/pos_safe_box/tests/test_pos_safe_box.py b/pos_safe_box/tests/test_pos_safe_box.py
new file mode 100644
index 000000000..dade47a8d
--- /dev/null
+++ b/pos_safe_box/tests/test_pos_safe_box.py
@@ -0,0 +1,142 @@
+# Copyright 2017 Creu Blanca
+# Copyright 2017 Eficent Business and IT Consulting Services, S.L.
+# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
+import odoo
+from odoo.exceptions import ValidationError
+from odoo.tests import Form
+
+from odoo.addons.base.tests.common import DISABLED_MAIL_CONTEXT
+from odoo.addons.point_of_sale.tests.common import TestPointOfSaleCommon
+
+
+@odoo.tests.tagged("post_install", "-at_install")
+class TestPosSafeBox(TestPointOfSaleCommon):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.env = cls.env(context=dict(cls.env.context, **DISABLED_MAIL_CONTEXT))
+ cls.pos_config.cash_control = True
+ cls.safe_box_group = cls.env["safe.box.group"].create(
+ {
+ "name": "Group",
+ "code": "SB",
+ "currency_id": cls.env.ref("base.USD").id,
+ }
+ )
+ cls.coin_01 = cls.env["safe.box.coin"].create(
+ {
+ "safe_box_group_id": cls.safe_box_group.id,
+ "name": "Coin",
+ "type": "coin",
+ "rate": 1,
+ }
+ )
+ cls.coin_02 = cls.env["safe.box.coin"].create(
+ {
+ "safe_box_group_id": cls.safe_box_group.id,
+ "name": "Note",
+ "type": "note",
+ "rate": 1,
+ }
+ )
+ cls.safe_box_01 = cls.env["safe.box"].create(
+ {"safe_box_group_id": cls.safe_box_group.id, "name": "SB 01"}
+ )
+ cls.safe_box_02 = cls.env["safe.box"].create(
+ {"safe_box_group_id": cls.safe_box_group.id, "name": "SB 02"}
+ )
+ cls.safe_box_03 = cls.env["safe.box"].create(
+ {"safe_box_group_id": cls.safe_box_group.id, "name": "SB 03"}
+ )
+ cls.account_01 = (
+ cls.env["account.account"]
+ .sudo()
+ .create(
+ {
+ "name": "Account 01",
+ "code": "001",
+ "company_id": cls.company.id,
+ "account_type": "asset_cash",
+ "safe_box_group_id": cls.safe_box_group.id,
+ }
+ )
+ )
+ cls.journal = cls.env["account.journal"].search(
+ [("type", "=", "cash"), ("company_id", "=", cls.company.id)],
+ limit=1,
+ )
+ cls.pos_config.safe_box_group_id = cls.safe_box_group
+ cls.invoice_out = cls.env["account.move"].create(
+ {
+ "partner_id": cls.partner4.id,
+ "company_id": cls.company.id,
+ "move_type": "out_invoice",
+ "date": "2016-03-12",
+ "invoice_date": "2016-03-12",
+ "invoice_line_ids": [
+ (
+ 0,
+ 0,
+ {
+ "product_id": cls.product3.id,
+ "name": "Producto de prueba",
+ "quantity": 1.0,
+ "price_unit": 100.0,
+ "tax_ids": [],
+ },
+ )
+ ],
+ }
+ )
+ cls.invoice_out.action_post()
+
+ def test_pos_safe_box(self):
+ self.pos_config._action_to_open_ui()
+ session = self.pos_config.current_session_id
+ session.action_pos_session_open()
+ wizard_context = session.button_show_wizard_pay_out_invoice()["context"]
+ cash_in = self.env["cash.pay.invoice"].with_context(**wizard_context)
+ with Form(cash_in) as form:
+ form.invoice_id = self.invoice_out
+ self.assertEqual(form.amount, 100)
+ cash_in.browse(form.id).action_pay_invoice()
+
+ # Set it as rescue in order to avoid extra moves
+ session.rescue = True
+ session.cash_register_balance_end_real = session.cash_register_balance_end
+ session.cash_register_difference = 0.0
+
+ session.action_pos_session_closing_control()
+ session.flush_recordset()
+ self.assertEqual(session.state, "closed")
+ self.assertTrue(session.pos_session_validation_id)
+ validation = session.pos_session_validation_id
+ with self.assertRaises(ValidationError):
+ validation.close()
+ validation.line_ids.filtered(
+ lambda r: r.safe_box_coin_id == self.coin_01
+ ).value = 50
+ validation.line_ids.filtered(
+ lambda r: r.safe_box_coin_id == self.coin_02
+ ).value = 50
+ with self.assertRaises(ValidationError):
+ validation.close()
+ self.safe_box_group.write(
+ {
+ "coin_safe_box_id": self.safe_box_01.id,
+ "note_safe_box_id": self.safe_box_02.id,
+ "approve_note_safe_box_id": self.safe_box_03.id,
+ }
+ )
+ validation.close()
+ self.assertEqual(self.safe_box_01.amount, 50)
+ self.assertEqual(self.safe_box_02.amount, 50)
+ self.assertEqual(self.safe_box_03.amount, 0)
+ with self.assertRaises(ValidationError):
+ validation.close()
+ validation.approve()
+ self.assertEqual(self.safe_box_01.amount, 50)
+ self.assertEqual(self.safe_box_02.amount, 0)
+ self.assertEqual(self.safe_box_03.amount, 50)
+ with self.assertRaises(ValidationError):
+ validation.approve()
diff --git a/pos_safe_box/views/pos_session_validation_views.xml b/pos_safe_box/views/pos_session_validation_views.xml
new file mode 100644
index 000000000..4156e7594
--- /dev/null
+++ b/pos_safe_box/views/pos_session_validation_views.xml
@@ -0,0 +1,105 @@
+
+
+
+ pos.session.validation.line.tree
+ pos.session.validation.line
+
+
+
+
+
+
+
+
+
+
+ pos.session.validation.tree
+ pos.session.validation
+
+
+
+
+
+
+
+
+
+
+
+
+ pos.session.validation.form
+ pos.session.validation
+
+
+
+
+
+ Session Validation
+ ir.actions.act_window
+ pos.session.validation
+ tree,form
+
+
+
diff --git a/pos_safe_box/views/pos_session_views.xml b/pos_safe_box/views/pos_session_views.xml
new file mode 100644
index 000000000..306bb2e1a
--- /dev/null
+++ b/pos_safe_box/views/pos_session_views.xml
@@ -0,0 +1,13 @@
+
+
+
+ pos.session.form.view
+ pos.session
+
+
+
+
+
+
+
+
diff --git a/pos_safe_box/views/res_config_settings.xml b/pos_safe_box/views/res_config_settings.xml
new file mode 100644
index 000000000..b9f8f09e9
--- /dev/null
+++ b/pos_safe_box/views/res_config_settings.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+ res.config.settings
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pos_safe_box/views/safe_box_coin_views.xml b/pos_safe_box/views/safe_box_coin_views.xml
new file mode 100644
index 000000000..60536bfd1
--- /dev/null
+++ b/pos_safe_box/views/safe_box_coin_views.xml
@@ -0,0 +1,23 @@
+
+
+
+ safe.box.coin.form
+ safe.box.coin
+
+
+
+
+
+
+
+
+ safe.box.coin.tree
+ safe.box.coin
+
+
+
+
+
+
+
+
diff --git a/pos_safe_box/views/safe_box_group_views.xml b/pos_safe_box/views/safe_box_group_views.xml
new file mode 100644
index 000000000..22215f070
--- /dev/null
+++ b/pos_safe_box/views/safe_box_group_views.xml
@@ -0,0 +1,16 @@
+
+
+
+ safe.box.group.form
+ safe.box.group
+
+
+
+
+
+
+
+
+
+
+
diff --git a/setup/pos_safe_box/odoo/addons/pos_safe_box b/setup/pos_safe_box/odoo/addons/pos_safe_box
new file mode 120000
index 000000000..15a3bac65
--- /dev/null
+++ b/setup/pos_safe_box/odoo/addons/pos_safe_box
@@ -0,0 +1 @@
+../../../../pos_safe_box
\ No newline at end of file
diff --git a/setup/pos_safe_box/setup.py b/setup/pos_safe_box/setup.py
new file mode 100644
index 000000000..28c57bb64
--- /dev/null
+++ b/setup/pos_safe_box/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)