Sylvain LE GAL
6 years ago
17 changed files with 235 additions and 231 deletions
-
15pos_invoicing/__manifest__.py
-
13pos_invoicing/demo/res_groups.xml
-
38pos_invoicing/i18n/fr.po
-
56pos_invoicing/i18n/pos_invoicing.pot
-
3pos_invoicing/models/__init__.py
-
14pos_invoicing/models/account_invoice.py
-
22pos_invoicing/models/account_payment.py
-
28pos_invoicing/models/account_voucher.py
-
13pos_invoicing/models/pos_order.py
-
59pos_invoicing/models/pos_session.py
-
43pos_invoicing/readme/DESCRIPTION.rst
-
4pos_invoicing/readme/ROADMAP.rst
-
BINpos_invoicing/static/description/account_invoice_form.png
-
BINpos_invoicing/static/description/icon.png
-
1pos_invoicing/tests/__init__.py
-
148pos_invoicing/tests/test_module.py
-
9pos_invoicing/views/view_account_invoice.xml
@ -1,13 +0,0 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!-- |
|||
Copyright (C) 2015 - 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). |
|||
--> |
|||
<openerp><data> |
|||
|
|||
<record id="account.group_account_manager" model="res.groups"> |
|||
<field name="users" eval="[(4, ref('base.user_root'))]"/> |
|||
</record> |
|||
|
|||
</data></openerp> |
@ -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 "" |
|||
|
@ -1,5 +1,4 @@ |
|||
# coding: utf-8 |
|||
from . import pos_order |
|||
from . import pos_session |
|||
from . import account_invoice |
|||
from . import account_voucher |
|||
from . import account_payment |
@ -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() |
@ -1,28 +0,0 @@ |
|||
# 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 |
@ -1,18 +1,17 @@ |
|||
# 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 |
|||
from odoo import 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') |
|||
def _prepare_invoice(self): |
|||
res = super()._prepare_invoice() |
|||
res.update({ |
|||
'pos_pending_payment': True, |
|||
}) |
|||
return res |
@ -1,25 +1,38 @@ |
|||
When you pay a pos_order, and then create an invoice : |
|||
This module extend the Point of Sale Odoo module, regarding invoicing. |
|||
|
|||
* 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 |
|||
This module prevent to make some mistakes in Odoo Point of Sale |
|||
regarding invoices generated via Point of Sale. |
|||
|
|||
Functionality |
|||
------------- |
|||
About the invoices created from POS after payment: |
|||
Without this module |
|||
~~~~~~~~~~~~~~~~~~~ |
|||
|
|||
* automatically validate them and don't allow modifications |
|||
* Disable the Pay Button |
|||
* Don't display them in the Customer Payment tool |
|||
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. |
|||
|
|||
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 |
|||
* 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 |
@ -1,4 +0,0 @@ |
|||
* 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``. |
Before Width: 852 | Height: 430 | Size: 32 KiB After Width: 1196 | Height: 385 | Size: 38 KiB |
Before Width: 64 | Height: 64 | Size: 4.0 KiB |
@ -1,2 +1 @@ |
|||
# coding: utf-8 |
|||
from . import test_module |
@ -1,92 +1,102 @@ |
|||
# 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 |
|||
from odoo import fields |
|||
from odoo.tests.common import TransactionCase |
|||
from odoo.exceptions import Warning as UserError |
|||
|
|||
|
|||
class TestPosInvoicing(TransactionCase): |
|||
"""Tests for POS Invoicing Module""" |
|||
class TestModule(TransactionCase): |
|||
|
|||
def setUp(self): |
|||
super(TestPosInvoicing, self).setUp() |
|||
super(TestModule, self).setUp() |
|||
|
|||
# Get Registry |
|||
self.session_obj = self.env['pos.session'] |
|||
self.order_obj = self.env['pos.order'] |
|||
self.PosOrder = self.env['pos.order'] |
|||
self.AccountPayment = self.env['account.payment'] |
|||
|
|||
# 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') |
|||
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') |
|||
|
|||
# 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) |
|||
# 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() |
|||
|
|||
# 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") |
|||
# Test Section |
|||
def test_order_invoice(self): |
|||
order = self._create_order() |
|||
|
|||
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") |
|||
# Check if invoice is correctly set |
|||
self.assertEquals(order.invoice_id.pos_pending_payment, True) |
|||
|
|||
# Close Session |
|||
session.signal_workflow('close') |
|||
# Try to register payment should fail on this invoice should fail |
|||
with self.assertRaises(UserError): |
|||
payment = self.register_payment(order.invoice_id) |
|||
payment.post() |
|||
|
|||
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") |
|||
# Try to register a payment not linked to this invoice should be ok |
|||
payment = self.register_payment() |
|||
payment.post() |
|||
|
|||
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") |
|||
# 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 _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, |
|||
}]], |
|||
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, |
|||
}) |
|||
# 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() |
|||
|
|||
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 |
Write
Preview
Loading…
Cancel
Save
Reference in new issue