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, +)