diff --git a/pos_invoicing/README.rst b/pos_invoicing/README.rst new file mode 100644 index 00000000..852113aa --- /dev/null +++ b/pos_invoicing/README.rst @@ -0,0 +1,112 @@ +========================= +Point Of Sale - Invoicing +========================= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpos-lightgray.png?logo=github + :target: https://github.com/OCA/pos/tree/12.0/pos_invoicing + :alt: OCA/pos +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/pos-12-0/pos-12-0-pos_invoicing + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/184/12.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extend the Point of Sale Odoo module, regarding invoicing. + +This module prevent to make some mistakes in Odoo Point of Sale +regarding invoices generated via Point of Sale. + +Without this module +~~~~~~~~~~~~~~~~~~~ + +When an invoice generated from Point of Sale is confirmed +it is in a 'open' state, until the session is closed, and the entries are +generated. At this step, invoice will be marked as 'paid' and the related +accounting moves will be reconcilied. +So, as long as the session is not closed, any user can: + +* cancel the invoice; +* register a payment; +* reconcile the invoice with an existing payment; + +All that action should be prohibited because the payment exists. + +With that module +~~~~~~~~~~~~~~~~ + +All those actions will not be possible anymore. + + +Note that the changes only impact the opened invoice coming from point of sale, +before the session is closed. + +Technically +----------- + +* add a ``pos_pending_payment`` field on the ``account.invoice`` to mark the + items that shouldn't be paid. + +This field is checked when the invoice is created from point of sale, +and is unchecked, when the session is closed. + +.. figure:: https://raw.githubusercontent.com/OCA/pos/12.0/pos_invoicing/static/description/account_invoice_form.png + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* GRAP + +Contributors +~~~~~~~~~~~~ + +* Sylvain LE GAL +* Julien WESTE + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/pos `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/pos_invoicing/__init__.py b/pos_invoicing/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/pos_invoicing/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/pos_invoicing/__manifest__.py b/pos_invoicing/__manifest__.py new file mode 100644 index 00000000..ca80289e --- /dev/null +++ b/pos_invoicing/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright (C) 2013 - Today: GRAP (http://www.grap.coop) +# @author: Julien WESTE +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + 'name': 'Point Of Sale - Invoicing', + 'summary': 'Handle invoicing from Point Of Sale', + 'version': '12.0.3.0.0', + 'category': 'Point of Sale', + 'author': 'GRAP, Odoo Community Association (OCA)', + 'website': 'http://www.github.com/OCA/pos', + 'license': 'AGPL-3', + 'depends': [ + 'point_of_sale', + ], + 'data': [ + 'views/view_account_invoice.xml', + ], + 'installable': True, +} diff --git a/pos_invoicing/i18n/fr.po b/pos_invoicing/i18n/fr.po new file mode 100644 index 00000000..a79f6952 --- /dev/null +++ b/pos_invoicing/i18n/fr.po @@ -0,0 +1,56 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_invoicing +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-11 16:18+0000\n" +"PO-Revision-Date: 2019-07-11 16:18+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_invoicing +#: model:ir.model.fields,help:pos_invoicing.field_account_invoice__pos_pending_payment +msgid "Indicates an invoice for which there are pending payments in the Point of Sale. \n" +"The invoice will be marked as paid when the session will be closed." +msgstr "La case est cochée si il y a des paiements en cours dans le point de vente. \n" +"La facture sera marquée comme payée quand la session sera fermée." + +#. module: pos_invoicing +#: model:ir.model,name:pos_invoicing.model_account_invoice +msgid "Invoice" +msgstr "Facture" + +#. module: pos_invoicing +#: model:ir.model,name:pos_invoicing.model_account_payment +msgid "Payments" +msgstr "Paiements" + +#. module: pos_invoicing +#: model:ir.model.fields,field_description:pos_invoicing.field_account_invoice__pos_pending_payment +msgid "PoS - Pending Payment" +msgstr "PdV - Paiement en cours" + +#. module: pos_invoicing +#: model:ir.model,name:pos_invoicing.model_pos_session +msgid "Point of Sale Session" +msgstr "Session du point de vente" + +#. module: pos_invoicing +#: code:addons/pos_invoicing/models/account_invoice.py:36 +#: code:addons/pos_invoicing/models/account_invoice.py:38 +#, python-format +msgid "You can not realize this action on the invoice(s) %s because there are pending payments in the Point of Sale." +msgstr "Vous ne pouvez pas réaliser cette action sur la / les facture(s) %s car il y a des paiements en cours dans le point de vente." + +#. module: pos_invoicing +#: code:addons/pos_invoicing/models/account_payment.py:17 +#, python-format +msgid "You can not realize this action on the payments(s) %s because there are pending payments in the Point of Sale." +msgstr "Vous ne pouvez pas réaliser cette action sur la / les paiement(s) %s car il y a des paiements en cours dans le point de vente." diff --git a/pos_invoicing/i18n/pos_invoicing.pot b/pos_invoicing/i18n/pos_invoicing.pot new file mode 100644 index 00000000..51cc170a --- /dev/null +++ b/pos_invoicing/i18n/pos_invoicing.pot @@ -0,0 +1,56 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_invoicing +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-11 16:17+0000\n" +"PO-Revision-Date: 2019-07-11 16:17+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_invoicing +#: model:ir.model.fields,help:pos_invoicing.field_account_invoice__pos_pending_payment +msgid "Indicates an invoice for which there are pending payments in the Point of Sale. \n" +"The invoice will be marked as paid when the session will be closed." +msgstr "" + +#. module: pos_invoicing +#: model:ir.model,name:pos_invoicing.model_account_invoice +msgid "Invoice" +msgstr "" + +#. module: pos_invoicing +#: model:ir.model,name:pos_invoicing.model_account_payment +msgid "Payments" +msgstr "" + +#. module: pos_invoicing +#: model:ir.model.fields,field_description:pos_invoicing.field_account_invoice__pos_pending_payment +msgid "PoS - Pending Payment" +msgstr "" + +#. module: pos_invoicing +#: model:ir.model,name:pos_invoicing.model_pos_session +msgid "Point of Sale Session" +msgstr "" + +#. module: pos_invoicing +#: code:addons/pos_invoicing/models/account_invoice.py:36 +#: code:addons/pos_invoicing/models/account_invoice.py:38 +#, python-format +msgid "You can not realize this action on the invoice(s) %s because there are pending payments in the Point of Sale." +msgstr "" + +#. module: pos_invoicing +#: code:addons/pos_invoicing/models/account_payment.py:17 +#, python-format +msgid "You can not realize this action on the payments(s) %s because there are pending payments in the Point of Sale." +msgstr "" + diff --git a/pos_invoicing/models/__init__.py b/pos_invoicing/models/__init__.py new file mode 100644 index 00000000..10c66764 --- /dev/null +++ b/pos_invoicing/models/__init__.py @@ -0,0 +1,4 @@ +from . import pos_order +from . import pos_session +from . import account_invoice +from . import account_payment diff --git a/pos_invoicing/models/account_invoice.py b/pos_invoicing/models/account_invoice.py new file mode 100644 index 00000000..40da0a1b --- /dev/null +++ b/pos_invoicing/models/account_invoice.py @@ -0,0 +1,41 @@ +# Copyright (C) 2018 - Today: GRAP (http://www.grap.coop) +# @author: Julien WESTE +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import _, api, fields, models +from odoo.exceptions import Warning as UserError + + +class AccountInvoice(models.Model): + _inherit = 'account.invoice' + + pos_pending_payment = fields.Boolean( + string='PoS - Pending Payment', readonly=True, + oldname='forbid_payment', + help="Indicates an invoice for which there are pending payments in the" + " Point of Sale. \nThe invoice will be marked as paid when the session" + " will be closed.") + + # Overload Section + @api.multi + def action_cancel(self): + self._check_pos_pending_payment() + return super(AccountInvoice, self).action_cancel() + + @api.multi + def _get_outstanding_info_JSON(self): + self.ensure_one() + if self.pos_pending_payment: + return + else: + return super()._get_outstanding_info_JSON() + + @api.multi + def _check_pos_pending_payment(self): + invoices = self.filtered(lambda x: x.pos_pending_payment) + if invoices: + raise UserError(_( + "You can not realize this action on the invoice(s) %s because" + " there are pending payments in the Point of Sale.") % ( + ', '.join(invoices.mapped('name')))) diff --git a/pos_invoicing/models/account_payment.py b/pos_invoicing/models/account_payment.py new file mode 100644 index 00000000..c414fe02 --- /dev/null +++ b/pos_invoicing/models/account_payment.py @@ -0,0 +1,22 @@ +# Copyright (C) 2019 - Today: GRAP (http://www.grap.coop) +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import _, api, models +from odoo.exceptions import Warning as UserError + + +class AccountPayment(models.Model): + _inherit = 'account.payment' + + @api.multi + def post(self): + payments = self.filtered( + lambda x: any(x.mapped('invoice_ids.pos_pending_payment'))) + if payments: + raise UserError(_( + "You can not realize this action on the payments(s) %s because" + " there are pending payments in the Point of Sale.") % ( + ', '.join( + [x for x in payments.mapped('communication') if x]))) + return super().post() diff --git a/pos_invoicing/models/pos_order.py b/pos_invoicing/models/pos_order.py new file mode 100644 index 00000000..8d52330b --- /dev/null +++ b/pos_invoicing/models/pos_order.py @@ -0,0 +1,17 @@ +# Copyright (C) 2013 - Today: GRAP (http://www.grap.coop) +# @author: Julien WESTE +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import models + + +class PosOrder(models.Model): + _inherit = 'pos.order' + + def _prepare_invoice(self): + res = super()._prepare_invoice() + res.update({ + 'pos_pending_payment': True, + }) + return res diff --git a/pos_invoicing/models/pos_session.py b/pos_invoicing/models/pos_session.py new file mode 100644 index 00000000..3485181d --- /dev/null +++ b/pos_invoicing/models/pos_session.py @@ -0,0 +1,21 @@ +# Copyright (C) 2013 - Today: GRAP (http://www.grap.coop) +# @author: Julien WESTE +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import logging + +from odoo import api, models + +_logger = logging.getLogger(__name__) + + +class PosSession(models.Model): + _inherit = 'pos.session' + + @api.multi + def action_pos_session_close(self): + res = super().action_pos_session_close() + orders = self.mapped('order_ids').filtered(lambda x: x.invoice_id) + orders.mapped('invoice_id').write({'pos_pending_payment': False}) + return res diff --git a/pos_invoicing/readme/CONTRIBUTORS.rst b/pos_invoicing/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..7754037b --- /dev/null +++ b/pos_invoicing/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Sylvain LE GAL +* Julien WESTE diff --git a/pos_invoicing/readme/DESCRIPTION.rst b/pos_invoicing/readme/DESCRIPTION.rst new file mode 100644 index 00000000..8c1500ba --- /dev/null +++ b/pos_invoicing/readme/DESCRIPTION.rst @@ -0,0 +1,39 @@ +This module extend the Point of Sale Odoo module, regarding invoicing. + +This module prevent to make some mistakes in Odoo Point of Sale +regarding invoices generated via Point of Sale. + +Without this module +~~~~~~~~~~~~~~~~~~~ + +When an invoice generated from Point of Sale is confirmed +it is in a 'open' state, until the session is closed, and the entries are +generated. At this step, invoice will be marked as 'paid' and the related +accounting moves will be reconcilied. +So, as long as the session is not closed, any user can: + +* cancel the invoice; +* register a payment; +* reconcile the invoice with an existing payment; + +All that action should be prohibited because the payment exists. + +With that module +~~~~~~~~~~~~~~~~ + +All those actions will not be possible anymore. + + +Note that the changes only impact the opened invoice coming from point of sale, +before the session is closed. + +Technically +----------- + +* add a ``pos_pending_payment`` field on the ``account.invoice`` to mark the + items that shouldn't be paid. + +This field is checked when the invoice is created from point of sale, +and is unchecked, when the session is closed. + +.. figure:: ../static/description/account_invoice_form.png diff --git a/pos_invoicing/static/description/account_invoice_form.png b/pos_invoicing/static/description/account_invoice_form.png new file mode 100644 index 00000000..9bf2c6cf Binary files /dev/null and b/pos_invoicing/static/description/account_invoice_form.png differ diff --git a/pos_invoicing/tests/__init__.py b/pos_invoicing/tests/__init__.py new file mode 100644 index 00000000..d9b96c4f --- /dev/null +++ b/pos_invoicing/tests/__init__.py @@ -0,0 +1 @@ +from . import test_module diff --git a/pos_invoicing/tests/test_module.py b/pos_invoicing/tests/test_module.py new file mode 100644 index 00000000..cb106689 --- /dev/null +++ b/pos_invoicing/tests/test_module.py @@ -0,0 +1,102 @@ +# Copyright (C) 2013 - Today: GRAP (http://www.grap.coop) +# @author: Julien WESTE +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields +from odoo.tests.common import TransactionCase +from odoo.exceptions import Warning as UserError + + +class TestModule(TransactionCase): + + def setUp(self): + super(TestModule, self).setUp() + + # Get Registry + self.PosOrder = self.env['pos.order'] + self.AccountPayment = self.env['account.payment'] + + # Get Object + self.pos_product = self.env.ref('point_of_sale.whiteboard_pen') + self.pricelist = self.env.ref('product.list0') + self.partner = self.env.ref('base.res_partner_12') + + # Create a new pos config and open it + self.pos_config = self.env.ref('point_of_sale.pos_config_main').copy() + self.pos_config.open_session_cb() + + # Test Section + def test_order_invoice(self): + order = self._create_order() + + # Check if invoice is correctly set + self.assertEquals(order.invoice_id.pos_pending_payment, True) + + # Try to register payment should fail on this invoice should fail + with self.assertRaises(UserError): + payment = self.register_payment(order.invoice_id) + payment.post() + + # Try to register a payment not linked to this invoice should be ok + payment = self.register_payment() + payment.post() + + # Once closed check if the invoice is correctly set + self.pos_config.current_session_id.action_pos_session_closing_control() + self.assertEquals(order.invoice_id.pos_pending_payment, False) + + # Private Section + def register_payment(self, invoice_id=False): + journal = self.pos_config.journal_ids[0] + return self.AccountPayment.create({ + 'invoice_ids': invoice_id and [(4, invoice_id.id, None)] or False, + 'payment_type': 'inbound', + 'partner_type': 'customer', + 'payment_date': fields.Datetime.now(), + 'partner_id': self.partner.id, + 'amount': 0.9, + 'journal_id': journal.id, + 'payment_method_id': journal.inbound_payment_method_ids[0].id, + }) + + def _create_order(self): + # Create order + order_data = { + 'id': u'0006-001-0010', + 'to_invoice': True, + 'data': { + 'pricelist_id': self.pricelist.id, + 'user_id': 1, + 'name': 'Order 0006-001-0010', + 'partner_id': self.partner.id, + 'amount_paid': 0.9, + 'pos_session_id': self.pos_config.current_session_id.id, + 'lines': [[0, 0, { + 'product_id': self.pos_product.id, + 'price_unit': 0.9, + 'qty': 1, + 'price_subtotal': 0.9, + 'price_subtotal_incl': 0.9, + }]], + 'statement_ids': [[0, 0, { + 'journal_id': self.pos_config.journal_ids[0].id, + 'amount': 0.9, + 'name': fields.Datetime.now(), + 'account_id': + self.env.user.partner_id.property_account_receivable_id.id, + 'statement_id': + self.pos_config.current_session_id.statement_ids[0].id, + }]], + 'creation_date': u'2018-09-27 15:51:03', + 'amount_tax': 0, + 'fiscal_position_id': False, + 'uid': u'00001-001-0001', + 'amount_return': 0, + 'sequence_number': 1, + 'amount_total': 0.9, + }} + + result = self.PosOrder.create_from_ui([order_data]) + order = self.PosOrder.browse(result[0]) + return order diff --git a/pos_invoicing/views/view_account_invoice.xml b/pos_invoicing/views/view_account_invoice.xml new file mode 100644 index 00000000..2beabcbe --- /dev/null +++ b/pos_invoicing/views/view_account_invoice.xml @@ -0,0 +1,20 @@ + + + + + + account.invoice + + + + + + + + +