diff --git a/pos_statement_closing_balance/__init__.py b/pos_statement_closing_balance/__init__.py new file mode 100644 index 00000000..976591c9 --- /dev/null +++ b/pos_statement_closing_balance/__init__.py @@ -0,0 +1,2 @@ +from . import wizards +from . import models diff --git a/pos_statement_closing_balance/__manifest__.py b/pos_statement_closing_balance/__manifest__.py new file mode 100644 index 00000000..84cb0a00 --- /dev/null +++ b/pos_statement_closing_balance/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2020 ForgeFlow, S.L. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + 'name': 'POS Statement Closing Balance', + 'version': '12.0.1.0.1', + 'category': 'Point Of Sale', + 'summary': 'Allows to set a closing balance in your statements and ' + 'auto-post the difference between theoretical and actual.', + 'author': 'ForgeFlow, Odoo Community Association (OCA)', + 'website': 'http://www.github.com/OCA/pos', + 'license': 'AGPL-3', + 'depends': [ + 'pos_cash_box_journal', + ], + 'data': [ + 'wizards/pos_update_statement_closing_balance.xml', + 'views/pos_session_views.xml', + 'views/account_journal_views.xml', + ], + 'installable': True, +} diff --git a/pos_statement_closing_balance/models/__init__.py b/pos_statement_closing_balance/models/__init__.py new file mode 100644 index 00000000..7107db85 --- /dev/null +++ b/pos_statement_closing_balance/models/__init__.py @@ -0,0 +1,2 @@ +from . import pos_session +from . import account_journal diff --git a/pos_statement_closing_balance/models/account_journal.py b/pos_statement_closing_balance/models/account_journal.py new file mode 100644 index 00000000..42d46017 --- /dev/null +++ b/pos_statement_closing_balance/models/account_journal.py @@ -0,0 +1,10 @@ +# Copyright 2020 ForgeFlow, S.L. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class AccountJournal(models.Model): + _inherit = 'account.journal' + + pos_control_ending_balance = fields.Boolean( + 'Control ending balance in POS') diff --git a/pos_statement_closing_balance/models/pos_session.py b/pos_statement_closing_balance/models/pos_session.py new file mode 100644 index 00000000..c7703825 --- /dev/null +++ b/pos_statement_closing_balance/models/pos_session.py @@ -0,0 +1,48 @@ +# Copyright 2020 ForgeFlow, S.L. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError + + +class PosSession(models.Model): + _inherit = "pos.session" + + ending_balances_to_update = fields.Boolean( + compute='_compute_ending_balances_to_update') + + @api.multi + def _compute_ending_balances_to_update(self): + for rec in self: + rec.ending_balances_to_update = False + for statement in rec.statement_ids: + journal = statement.journal_id + if journal.pos_control_ending_balance and \ + journal.type != 'cash' or (journal.type == 'cash' and + not rec.cash_control): + rec.ending_balances_to_update = True + + @api.multi + def button_update_statement_ending_balance(self): + self.ensure_one() + action = self.env.ref( + "pos_statement_closing_balance." + "action_pos_update_bank_statement_closing_balance") + result = action.read()[0] + return result + + @api.multi + def _check_pos_session_bank_balance(self): + for session in self: + for statement in session.statement_ids: + if statement.journal_id.pos_control_ending_balance and \ + (statement != session.cash_register_id) and ( + statement.balance_end != statement.balance_end_real): + raise ValidationError(_( + 'Mismatch in the closing balance ' + 'for a non-cash statement.')) + return True + + @api.multi + def action_pos_session_closing_control(self): + self._check_pos_session_bank_balance() + return super(PosSession, self).action_pos_session_closing_control() diff --git a/pos_statement_closing_balance/readme/CONFIGURATION.rst b/pos_statement_closing_balance/readme/CONFIGURATION.rst new file mode 100644 index 00000000..e305b792 --- /dev/null +++ b/pos_statement_closing_balance/readme/CONFIGURATION.rst @@ -0,0 +1,3 @@ +* Go to *Invoicing > Configuration > Journals* and set, for each Journal + that will be used in the POS, the flag 'Control ending balance in POS' if + you expect users to enter the ending balance. diff --git a/pos_statement_closing_balance/readme/CONTRIBUTORS.rst b/pos_statement_closing_balance/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..38b52533 --- /dev/null +++ b/pos_statement_closing_balance/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* ForgeFlow + * Jordi Ballester Alomar + diff --git a/pos_statement_closing_balance/readme/DESCRIPTION.rst b/pos_statement_closing_balance/readme/DESCRIPTION.rst new file mode 100644 index 00000000..572f9550 --- /dev/null +++ b/pos_statement_closing_balance/readme/DESCRIPTION.rst @@ -0,0 +1,8 @@ +This module extends the functionality of point of sale to allow users to input +the ending balances of the payment methods when they close a POS Session. +The system will then post the difference to the 'Liquidity Transfers' account. + +Currently it's only possible to define the ending balance for the cash +if you configure "Cash Control" in the POS Configuration. But that setting +will not allow users to enter the ending balances for credit card transactions +, for example. diff --git a/pos_statement_closing_balance/readme/USAGE.rst b/pos_statement_closing_balance/readme/USAGE.rst new file mode 100644 index 00000000..bd0392f2 --- /dev/null +++ b/pos_statement_closing_balance/readme/USAGE.rst @@ -0,0 +1,7 @@ +* Go to *Point of Sale*, then to a session and go to *Settings*. +* Define the payment methods that you want to use. +* Start the Session and create some POS orders. +* Go back to the POS Session and press "Close". +* Press the button "Update Ending Balances" in the POS Session, and enter + the final balance in the column "Balance End Real". + diff --git a/pos_statement_closing_balance/tests/__init__.py b/pos_statement_closing_balance/tests/__init__.py new file mode 100644 index 00000000..13c5ff1e --- /dev/null +++ b/pos_statement_closing_balance/tests/__init__.py @@ -0,0 +1 @@ +from . import test_pos_statement_closing_balance diff --git a/pos_statement_closing_balance/tests/test_pos_statement_closing_balance.py b/pos_statement_closing_balance/tests/test_pos_statement_closing_balance.py new file mode 100644 index 00000000..e9d551f6 --- /dev/null +++ b/pos_statement_closing_balance/tests/test_pos_statement_closing_balance.py @@ -0,0 +1,65 @@ +# Copyright 2020 ForgeFlow, S.L. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo.tests.common import TransactionCase +from odoo.exceptions import ValidationError + + +class TestPosStatementClosingBalance(TransactionCase): + def setUp(self): + super(TestPosStatementClosingBalance, self).setUp() + self.pos_config = self.env["pos.config"].create({"name": "PoS config"}) + bank_journal = self.env["account.journal"].create({ + "name": "Test bank", + "code": "TB1", + "type": "bank", + "pos_control_ending_balance": True, + }) + self.pos_config.journal_ids += bank_journal + self.pos_config.open_session_cb() + self.session = self.pos_config.current_session_id + self.session.action_pos_session_open() + + def test_wizard(self): + journal = self.session.journal_ids.filtered(lambda j: j.code == 'TB1') + wizard = ( + self.env["cash.box.journal.in"] + .with_context( + active_model="pos.session", active_ids=self.session.ids + ) + .create({"amount": 10, "name": "Out"}) + ) + wizard.journal_id = journal + wizard.run() + self.assertEqual( + self.session.statement_ids.filtered( + lambda r: r.journal_id.id == journal.id + ).difference, + -10.0, + ) + with self.assertRaises(ValidationError): + self.session.action_pos_session_closing_control() + + wizard = ( + self.env["pos.update.bank.statement.closing.balance"] + .with_context( + active_model="pos.session", active_ids=self.session.ids + ) + .create({}) + ) + for item in wizard.item_ids: + item.balance_end_real = 2.0 + wizard.action_confirm() + self.assertEqual( + self.session.statement_ids.filtered( + lambda r: r.journal_id.id == journal.id + ).balance_end, + 2.0, + ) + self.assertEqual( + self.session.statement_ids.filtered( + lambda r: r.journal_id.id == journal.id + ).difference, + 0, + ) + self.session.action_pos_session_closing_control() + self.assertEqual(self.session.state, 'closed') diff --git a/pos_statement_closing_balance/views/account_journal_views.xml b/pos_statement_closing_balance/views/account_journal_views.xml new file mode 100644 index 00000000..675b8417 --- /dev/null +++ b/pos_statement_closing_balance/views/account_journal_views.xml @@ -0,0 +1,13 @@ + + + + account.journal.form + account.journal + + + + + + + + diff --git a/pos_statement_closing_balance/views/pos_session_views.xml b/pos_statement_closing_balance/views/pos_session_views.xml new file mode 100644 index 00000000..fecc9a90 --- /dev/null +++ b/pos_statement_closing_balance/views/pos_session_views.xml @@ -0,0 +1,19 @@ + + + + pos.session.form.view + pos.session + + + + + + + diff --git a/pos_statement_closing_balance/wizards/__init__.py b/pos_statement_closing_balance/wizards/__init__.py new file mode 100644 index 00000000..8905d18d --- /dev/null +++ b/pos_statement_closing_balance/wizards/__init__.py @@ -0,0 +1 @@ +from . import pos_update_statement_closing_balance diff --git a/pos_statement_closing_balance/wizards/pos_update_statement_closing_balance.py b/pos_statement_closing_balance/wizards/pos_update_statement_closing_balance.py new file mode 100644 index 00000000..05f32dd4 --- /dev/null +++ b/pos_statement_closing_balance/wizards/pos_update_statement_closing_balance.py @@ -0,0 +1,121 @@ +# Copyright 2020 ForgeFlow, S.L. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + + +class POSBankStatementUpdateClosingBalance(models.TransientModel): + _name = "pos.update.bank.statement.closing.balance" + _description = 'POS Update Bank Statement Closing Balance' + + session_id = fields.Many2one( + comodel_name='pos.session', + ) + item_ids = fields.One2many( + comodel_name="pos.update.bank.statement.closing.balance.line", + inverse_name="wiz_id", + string="Items", + ) + + @api.model + def _prepare_item(self, session, statement): + return { + "statement_id": statement.id, + "name": statement.name, + "journal_id": statement.journal_id.id, + "balance_start": statement.balance_start, + "total_entry_encoding": statement.total_entry_encoding, + "currency_id": statement.currency_id.id, + } + + @api.model + def default_get(self, flds): + res = super().default_get(flds) + session_obj = self.env["pos.session"] + active_ids = self.env.context["active_ids"] or [] + active_model = self.env.context["active_model"] + + if not active_ids: + return res + assert active_model == "pos.session", \ + "Bad context propagation" + + items = [] + if len(active_ids) > 1: + raise UserError(_('You cannot start the closing ' + 'balance for multiple POS sessions')) + session = session_obj.browse(active_ids[0]) + for statement in session.statement_ids: + if statement.journal_id.type != 'cash' or \ + (statement.journal_id.type == 'cash' and + not session.cash_control): + items.append([0, 0, self._prepare_item(session, statement)]) + res["session_id"] = session.id + res["item_ids"] = items + return res + + @api.model + def _prepare_cash_box_journal(self, item): + return { + 'amount': abs(item.difference), + 'name': _('Out'), + "journal_id": item.journal_id.id, + } + + @api.multi + def action_confirm(self): + self.ensure_one() + for item in self.item_ids: + if item.difference: + if item.difference > 0.0: + model = "cash.box.journal.in" + else: + model = "cash.box.journal.out" + wizard = ( + self.env[model] + .with_context( + active_model="pos.session", + active_ids=self.session_id.ids + ).create(self._prepare_cash_box_journal(item)) + ) + wizard.run() + item.statement_id.balance_end_real = item.balance_end_real + return True + + +class BankStatementLineUpdateEndingBalanceLine(models.TransientModel): + _name = "pos.update.bank.statement.closing.balance.line" + _description = 'POS Update Bank Statement Closing Balance Line' + + wiz_id = fields.Many2one( + comodel_name='pos.update.bank.statement.closing.balance', + required=True, + ) + statement_id = fields.Many2one( + comodel_name='account.bank.statement', + ) + name = fields.Char( + related='statement_id.name' + ) + journal_id = fields.Many2one( + comodel_name='account.journal', + related='statement_id.journal_id', + ) + balance_start = fields.Monetary( + related='statement_id.balance_start', + ) + total_entry_encoding = fields.Monetary( + related='statement_id.total_entry_encoding', + ) + balance_end = fields.Monetary(compute='_compute_balance_end') + balance_end_real = fields.Monetary(default=0.0) + difference = fields.Monetary(compute='_compute_balance_end') + currency_id = fields.Many2one( + comodel_name='res.currency', + related='statement_id.currency_id' + ) + + def _compute_balance_end(self): + for rec in self: + rec.balance_end = rec.balance_start + rec.total_entry_encoding + rec.difference = rec.balance_end_real - rec.balance_end diff --git a/pos_statement_closing_balance/wizards/pos_update_statement_closing_balance.xml b/pos_statement_closing_balance/wizards/pos_update_statement_closing_balance.xml new file mode 100644 index 00000000..74c79c9f --- /dev/null +++ b/pos_statement_closing_balance/wizards/pos_update_statement_closing_balance.xml @@ -0,0 +1,33 @@ + + + + pos.update.bank.statement.closing.balance.form + pos.update.bank.statement.closing.balance + form + +
+ + + + + + + + + + +
+
+
+
+
+ + + Update Ending Balances + pos.update.bank.statement.closing.balance + form + new + +