diff --git a/pos_invoicing/README.rst b/pos_invoicing/README.rst new file mode 100644 index 00000000..c8b17163 --- /dev/null +++ b/pos_invoicing/README.rst @@ -0,0 +1,93 @@ +========================= +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-grap%2Fgrap--odoo--incubator-lightgray.png?logo=github + :target: https://github.com/grap/grap-odoo-incubator/tree/8.0/pos_invoicing + :alt: grap/grap-odoo-incubator + +|badge1| |badge2| |badge3| + +When you pay a pos_order, and then create an invoice : + +* you mustn't register a payment against the invoice as the payment + already exists in POS +* The POS payment will be reconciled with the invoice when the session + is closed +* You mustn't modify the invoice because the amount could become + different from the one registered in POS. Thus we have to + automatically validate the created invoice + +Functionality +------------- +About the invoices created from POS after payment: + +* automatically validate them and don't allow modifications +* Disable the Pay Button +* Don't display them in the Customer Payment tool + +Technically +----------- + +add a ``pos_pending_payment`` field on the ``account.invoice`` to mark the +items that shouldn't be paid. + +.. figure:: https://raw.githubusercontent.com/grap/grap-odoo-incubator/8.0/pos_invoicing/static/description/account_invoice_form.png + +**Table of contents** + +.. contents:: + :local: + +Known issues / Roadmap +====================== + +* This module reconcile invoiced orders only if a customer has one invoice per + session. + +* It should be great to use the OCA module ``pos_autoreconcile``. + +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 part of the `grap/grap-odoo-incubator `_ project on GitHub. + + +You are welcome to 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/__openerp__.py b/pos_invoicing/__openerp__.py new file mode 100644 index 00000000..52e877d7 --- /dev/null +++ b/pos_invoicing/__openerp__.py @@ -0,0 +1,27 @@ +# coding: utf-8 +# 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': '8.0.3.0.0', + 'category': 'Point of Sale', + 'author': 'GRAP', + 'website': 'http://www.grap.coop', + 'license': 'AGPL-3', + 'depends': [ + 'point_of_sale', + ], + 'data': [ + 'views/view_account_invoice.xml', + ], + 'demo': [ + 'demo/res_groups.xml', + ], + 'images': [ + 'static/description/account_invoice_form.png', + ], + 'installable': False, +} diff --git a/pos_invoicing/demo/res_groups.xml b/pos_invoicing/demo/res_groups.xml new file mode 100644 index 00000000..2c5779c4 --- /dev/null +++ b/pos_invoicing/demo/res_groups.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/pos_invoicing/i18n/fr.po b/pos_invoicing/i18n/fr.po new file mode 100644 index 00000000..675f5b7c --- /dev/null +++ b/pos_invoicing/i18n/fr.po @@ -0,0 +1,50 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_invoicing +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-08-02 13:14+0000\n" +"PO-Revision-Date: 2018-08-02 13:14+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,name:pos_invoicing.model_account_voucher +msgid "Accounting Voucher" +msgstr "Justificatif comptable" + +#. module: pos_invoicing +#: help:account.invoice,pos_pending_payment:0 +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 +#: view:account.invoice:pos_invoicing.view_account_invoice_form +#: field:account.invoice,pos_pending_payment:0 +msgid "PoS - Pending Payment" +msgstr "PdV - Paiement en cours" + +#. module: pos_invoicing +#: model:ir.model,name:pos_invoicing.model_pos_order +msgid "Point of Sale" +msgstr "Point de Vente" + +#. module: pos_invoicing +#: code:addons/pos_invoicing/models/account_invoice.py:37 +#, 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." diff --git a/pos_invoicing/models/__init__.py b/pos_invoicing/models/__init__.py new file mode 100644 index 00000000..e64dd454 --- /dev/null +++ b/pos_invoicing/models/__init__.py @@ -0,0 +1,5 @@ +# coding: utf-8 +from . import pos_order +from . import pos_session +from . import account_invoice +from . import account_voucher diff --git a/pos_invoicing/models/account_invoice.py b/pos_invoicing/models/account_invoice.py new file mode 100644 index 00000000..3c0d9285 --- /dev/null +++ b/pos_invoicing/models/account_invoice.py @@ -0,0 +1,39 @@ +# coding: utf-8 +# 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 openerp import _, api, fields, models +from openerp.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 invoice_pay_customer(self): + self._check_pos_pending_payment() + return super(AccountInvoice, self).invoice_pay_customer() + + @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_voucher.py b/pos_invoicing/models/account_voucher.py new file mode 100644 index 00000000..216979d7 --- /dev/null +++ b/pos_invoicing/models/account_voucher.py @@ -0,0 +1,28 @@ +# coding: utf-8 +# 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 openerp import api, models + + +class AccountVoucher(models.Model): + _inherit = 'account.voucher' + + # Override section + @api.multi + def recompute_voucher_lines( + self, partner_id, journal_id, price, currency_id, ttype, date): + + move_line_obj = self.env['account.move.line'] + + res = super(AccountVoucher, self).recompute_voucher_lines( + partner_id, journal_id, price, currency_id, ttype, date) + + for voucher_type in ['line_dr_ids', 'line_cr_ids']: + for voucher_line in res['value'][voucher_type]: + move_line = move_line_obj.browse(voucher_line['move_line_id']) + if move_line.invoice.pos_pending_payment: + res['value'][voucher_type].remove(voucher_line) + return res diff --git a/pos_invoicing/models/pos_order.py b/pos_invoicing/models/pos_order.py new file mode 100644 index 00000000..545f6b55 --- /dev/null +++ b/pos_invoicing/models/pos_order.py @@ -0,0 +1,18 @@ +# coding: utf-8 +# 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 openerp import api, models + + +class PosOrder(models.Model): + _inherit = 'pos.order' + + @api.multi + def action_invoice(self): + res = super(PosOrder, self).action_invoice() + self.mapped('invoice_id').write({'pos_pending_payment': True}) + self.mapped('invoice_id').signal_workflow('invoice_open') + return res diff --git a/pos_invoicing/models/pos_session.py b/pos_invoicing/models/pos_session.py new file mode 100644 index 00000000..8f74195f --- /dev/null +++ b/pos_invoicing/models/pos_session.py @@ -0,0 +1,70 @@ +# coding: utf-8 +# 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 openerp import api, models + +_logger = logging.getLogger(__name__) + + +class PosSession(models.Model): + _inherit = 'pos.session' + + @api.multi + def wkf_action_close(self): + move_line_obj = self.env['account.move.line'] + + res = super(PosSession, self).wkf_action_close() + + # Get All Pos Order invoiced during the current Sessions + orders = self.order_ids.filtered(lambda x: x.invoice_id) + + for order in orders: + # Get accounting partner + partner = order.partner_id.parent_id or order.partner_id + + # Search all Sale Move Lines to reconcile in Sale Journal + sale_move_lines = [] + sale_total = 0 + + for move_line in order.invoice_id.move_id.line_id: + if (move_line.partner_id.id == partner.id and + move_line.account_id.type == 'receivable'): + sale_move_lines.append(move_line) + sale_total += move_line.debit - move_line.credit + + # Search all move Line to reconcile in Payment Journals + payment_move_lines = [] + payment_total = 0 + + statement_ids = order.mapped('statement_ids.statement_id').ids + move_lines = move_line_obj.search([ + ('statement_id', 'in', statement_ids), + ('partner_id', '=', partner.id), + ('reconcile_id', '=', False)]) + for move_line in move_lines: + if (move_line.account_id.type == 'receivable'): + payment_move_lines.append(move_line) + payment_total += move_line.debit - move_line.credit + + # Try to reconcile + if payment_total != - sale_total: + # Unable to reconcile + _logger.warning( + "Unable to reconcile the payment of %s #%d." + "(partner : %s)" % ( + order.name, order.id, partner.name)) + else: + # Reconcile move lines + move_lines = move_line_obj.browse( + [x.id for x in sale_move_lines] + + [x.id for x in payment_move_lines]) + move_lines.reconcile('manual', False, False, False) + # Unflag the invoice as 'PoS Pending Payment' + order.invoice_id.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..6875c12b --- /dev/null +++ b/pos_invoicing/readme/DESCRIPTION.rst @@ -0,0 +1,25 @@ +When you pay a pos_order, and then create an invoice : + +* you mustn't register a payment against the invoice as the payment + already exists in POS +* The POS payment will be reconciled with the invoice when the session + is closed +* You mustn't modify the invoice because the amount could become + different from the one registered in POS. Thus we have to + automatically validate the created invoice + +Functionality +------------- +About the invoices created from POS after payment: + +* automatically validate them and don't allow modifications +* Disable the Pay Button +* Don't display them in the Customer Payment tool + +Technically +----------- + +add a ``pos_pending_payment`` field on the ``account.invoice`` to mark the +items that shouldn't be paid. + +.. figure:: ../static/description/account_invoice_form.png diff --git a/pos_invoicing/readme/ROADMAP.rst b/pos_invoicing/readme/ROADMAP.rst new file mode 100644 index 00000000..e09f972b --- /dev/null +++ b/pos_invoicing/readme/ROADMAP.rst @@ -0,0 +1,4 @@ +* This module reconcile invoiced orders only if a customer has one invoice per + session. + +* It should be great to use the OCA module ``pos_autoreconcile``. 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..1ee85da2 Binary files /dev/null and b/pos_invoicing/static/description/account_invoice_form.png differ diff --git a/pos_invoicing/static/description/icon.png b/pos_invoicing/static/description/icon.png new file mode 100644 index 00000000..7f52ccf0 Binary files /dev/null and b/pos_invoicing/static/description/icon.png differ diff --git a/pos_invoicing/tests/__init__.py b/pos_invoicing/tests/__init__.py new file mode 100644 index 00000000..17b82062 --- /dev/null +++ b/pos_invoicing/tests/__init__.py @@ -0,0 +1,2 @@ +# coding: utf-8 +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..a7dc96ae --- /dev/null +++ b/pos_invoicing/tests/test_module.py @@ -0,0 +1,92 @@ +# coding: utf-8 +# 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 time +from openerp.tests.common import TransactionCase + + +class TestPosInvoicing(TransactionCase): + """Tests for POS Invoicing Module""" + + def setUp(self): + super(TestPosInvoicing, self).setUp() + + # Get Registry + self.session_obj = self.env['pos.session'] + self.order_obj = self.env['pos.order'] + + # Get Object + self.config = self.env.ref('point_of_sale.pos_config_main') + self.partner_A = self.env.ref('base.res_partner_2') + self.partner_B = self.env.ref('base.res_partner_12') + self.product = self.env.ref('product.product_product_48') + self.payment_journal = self.env.ref('account.cash_journal') + + # Test Section + def test_01_invoice_with_payment(self): + """Test the workflow: Draft Order -> Payment -> Invoice""" + # Opening Session + session = self.session_obj.create({'config_id': self.config.id}) + + # TODO FIXME, for the time being, reconciliation is not + # set if a customer make many invoices in the same pos session. + # self._create_order(session, self.partner_A, 100, True) + # self._create_order(session, self.partner_A, 200, True) + self._create_order(session, self.partner_B, 400, True) + + # The Invoice must be unpayable but in 'open' state + # Invoice created by order should be in open state + invoiced_orders = session.mapped('order_ids').filtered( + lambda x: x.state == 'invoiced') + invoices = invoiced_orders.mapped('invoice_id') + + self.assertEquals( + [x for x in invoices.mapped('state') if x != 'open'], [], + "All invoices generated from PoS should be in the 'open' state" + " when session is opened") + + self.assertEquals( + [x for x in invoices.mapped('pos_pending_payment') if not x], + [], + "All invoices generated from PoS should be marked as PoS Pending" + " Payment when session is opened") + + # Close Session + session.signal_workflow('close') + + self.assertEquals( + [x for x in invoices.mapped('state') if x != 'paid'], [], + "All invoices generated from PoS should be in the 'paid' state" + " when session is closed") + + self.assertEquals( + [x for x in invoices.mapped('pos_pending_payment') if x], [], + "Invoices generated from PoS should not be marked as PoS Pending" + " Payment when session is closed") + + # Private Section + def _create_order(self, session, partner, amount, with_invoice): + # create Pos Order + order = self.order_obj.create({ + 'session_id': session.id, + 'partner_id': partner.id, + 'lines': [[0, False, { + 'product_id': self.product.id, + 'qty': 1, + 'price_unit': amount, + }]], + }) + # Finish Payment + self.order_obj.add_payment(order.id, { + 'journal': self.payment_journal.id, + 'payment_date': time.strftime('%Y-%m-%d'), + 'amount': amount, + }) + # Mark as Paid + order.signal_workflow('paid') + if with_invoice: + order.action_invoice() + 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..13a9fba2 --- /dev/null +++ b/pos_invoicing/views/view_account_invoice.xml @@ -0,0 +1,21 @@ + + + + + + account.invoice + + + + + + + + + +