When the user looks for open payables or receivables, in the

context of payment orders, she should ocus on the amount that
is due to be paid. In this method we are forcing to display both
the amount due in company and in the invoice currency.

We then hide the fields debit and credit, because they add no value.
This commit is contained in:
Jordi Ballester
2017-10-27 19:04:05 +02:00
committed by Enric Tobella
parent 4102166a47
commit ee3c27b747
26 changed files with 332 additions and 79 deletions

View File

@@ -26,13 +26,13 @@ This module adds several options on Payment Modes, cf Invoicing/Accounting > Con
Usage
=====
You can create a Payment Order via the menu Invoicing/Accounting > Payments > Payment Orders and then select the move lines to pay.
You can create a Payment order via the menu Invoicing/Accounting > Payments > Payment Orders and then select the move lines to pay.
You can create a Debit Order via the menu Invoicing/Accounting > Payments > Debit Orders and then select the move lines to debit.
You can create a Debit order via the menu Invoicing/Accounting > Payments > Debit Orders and then select the move lines to debit.
This module also adds a button *Add to Payment Order* on supplier invoices and a button *Add to Debit Order* on customer invoices.
You can print a Payment Order via the menu Invoicing/Accounting > Payments > Payment Orders and then select the payment oder to print.
You can print a Payment order via the menu Invoicing/Accounting > Payments > Payment Orders and then select the payment oder to print.
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from . import models
from . import report
from . import wizard

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# © 2009 EduSense BV (<http://www.edusense.nl>)
# © 2011-2013 Therp BV (<https://therp.nl>)
# © 2013-2014 ACSONE SA (<https://acsone.eu>).

View File

@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from . import account_payment_mode
from . import account_payment_order
from . import account_payment_line

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# © 2013-2014 ACSONE SA (<https://acsone.eu>).
# © 2014 Serv. Tecnol. Avanzados - Pedro M. Baeza
# © 2016 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>)
@@ -57,7 +56,9 @@ class AccountInvoice(models.Model):
if not inv.payment_order_ok:
raise UserError(_(
"The invoice %s has a payment mode '%s' "
"which is not selectable in payment orders."))
"which is not selectable in payment orders." % (
inv.number, inv.payment_mode_id.display_name))
)
payorders = apoo.search([
('payment_mode_id', '=', inv.payment_mode_id.id),
('state', '=', 'draft')])

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# © 2016 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).

View File

@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
# © 2014-2016 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>)
# © 2014 Serv. Tecnol. Avanzados - Pedro M. Baeza
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from lxml import etree
from odoo import models, fields, api
from odoo.osv import orm
class AccountMoveLine(models.Model):
@@ -73,3 +74,57 @@ class AccountMoveLine(models.Model):
for mline in self:
aplo.create(mline._prepare_payment_line_vals(payment_order))
return
@api.model
def fields_view_get(self, view_id=None, view_type='form', toolbar=False,
submenu=False):
# When the user looks for open payables or receivables, in the
# context of payment orders, she should focus primarily on amount that
# is due to be paid, and secondarily on the total amount. In this
# method we are forcing to display both the amount due in company and
# in the invoice currency.
# We then hide the fields debit and credit, because they add no value.
result = super(AccountMoveLine, self).fields_view_get(view_id,
view_type,
toolbar=toolbar,
submenu=submenu)
doc = etree.XML(result['arch'])
if view_type == 'tree' and self._module == 'account_payment_order':
if not doc.xpath("//field[@name='balance']"):
for placeholder in doc.xpath(
"//field[@name='amount_currency']"):
elem = etree.Element(
'field', {
'name': 'balance',
'readonly': 'True'
})
orm.setup_modifiers(elem)
placeholder.addprevious(elem)
if not doc.xpath("//field[@name='amount_residual_currency']"):
for placeholder in doc.xpath(
"//field[@name='amount_currency']"):
elem = etree.Element(
'field', {
'name': 'amount_residual_currency',
'readonly': 'True'
})
orm.setup_modifiers(elem)
placeholder.addnext(elem)
if not doc.xpath("//field[@name='amount_residual']"):
for placeholder in doc.xpath(
"//field[@name='amount_currency']"):
elem = etree.Element(
'field', {
'name': 'amount_residual',
'readonly': 'True'
})
orm.setup_modifiers(elem)
placeholder.addnext(elem)
# Remove credit and debit data - which is irrelevant in this case
for elem in doc.xpath("//field[@name='debit']"):
doc.remove(elem)
for elem in doc.xpath("//field[@name='credit']"):
doc.remove(elem)
result['arch'] = etree.tostring(doc)
return result

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# © 2015-2016 Akretion - Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# © 2009 EduSense BV (<http://www.edusense.nl>)
# © 2011-2013 Therp BV (<https://therp.nl>)
# © 2014-2016 Serv. Tecnol. Avanzados - Pedro M. Baeza

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# © 2009 EduSense BV (<http://www.edusense.nl>)
# © 2011-2013 Therp BV (<https://therp.nl>)
# © 2016 Serv. Tecnol. Avanzados - Pedro M. Baeza
@@ -55,12 +54,14 @@ class AccountPaymentOrder(models.Model):
company_partner_bank_id = fields.Many2one(
related='journal_id.bank_account_id', string='Company Bank Account',
readonly=True)
state = fields.Selection([
('draft', 'Draft'),
('open', 'Confirmed'),
('generated', 'File Generated'),
('uploaded', 'File Uploaded'),
('cancel', 'Cancel'),
state = fields.Selection(
[
('draft', 'Draft'),
('open', 'Confirmed'),
('generated', 'File Generated'),
('uploaded', 'File Uploaded'),
('done', 'Done'),
('cancel', 'Cancel'),
], string='Status', readonly=True, copy=False, default='draft',
track_visibility='onchange')
date_prefered = fields.Selection([
@@ -77,6 +78,7 @@ class AccountPaymentOrder(models.Model):
"as the Payment Execution Date Type.")
date_generated = fields.Date(string='File Generation Date', readonly=True)
date_uploaded = fields.Date(string='File Upload Date', readonly=True)
date_done = fields.Date(string='Done Date', readonly=True)
generated_user_id = fields.Many2one(
'res.users', string='Generated by', readonly=True, ondelete='restrict',
copy=False)

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# © 2015-2016 Akretion - Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
@@ -15,7 +14,7 @@ class BankPaymentLine(models.Model):
readonly=True)
order_id = fields.Many2one(
'account.payment.order', string='Order', ondelete='cascade',
index=True)
index=True, readonly=True)
payment_type = fields.Selection(
related='order_id.payment_type', string="Payment Type",
readonly=True, store=True)
@@ -79,9 +78,6 @@ class BankPaymentLine(models.Model):
for bline in self:
amount_currency = sum(
bline.mapped('payment_line_ids.amount_currency'))
import logging
logging.info(bline.company_id)
logging.info(bline.company_currency_id)
amount_company_currency = bline.currency_id.with_context(
date=bline.date).compute(
amount_currency, bline.company_currency_id)

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# © 2015-2016 Akretion - Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# © 2017 Acsone SA/NV (<https://www.acsone.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).

View File

@@ -1,7 +1,4 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import test_payment_mode
from . import test_bank
from . import test_payment_order
from . import test_payment_order_inbound
from . import test_payment_order_outbound

View File

@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Creu Blanca
# © 2017 Creu Blanca
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo.tests.common import TransactionCase

View File

@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Creu Blanca
# © 2017 Creu Blanca
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo.tests.common import TransactionCase
@@ -29,6 +28,15 @@ class TestPaymentMode(TransactionCase):
self.manual_out = self.env.ref(
'account.account_payment_method_manual_out')
self.manual_in = self.env.ref(
'account.account_payment_method_manual_in')
self.electronic_out = self.env['account.payment.method'].create({
'name': 'Electronic Out',
'code': 'electronic_out',
'payment_type': 'outbound',
})
self.payment_mode_c1 = self.env['account.payment.mode'].create({
'name': 'Direct Debit of suppliers from Bank 1',
'bank_account_link': 'variable',
@@ -63,3 +71,32 @@ class TestPaymentMode(TransactionCase):
'transfer_account_id': self.account.id,
'transfer_journal_id': False
})
def test_onchange_generate_move(self):
self.payment_mode_c1.generate_move = True
self.payment_mode_c1.generate_move_change()
self.assertEqual(self.payment_mode_c1.move_option, 'date')
self.payment_mode_c1.generate_move = False
self.payment_mode_c1.generate_move_change()
self.assertFalse(self.payment_mode_c1.move_option)
def test_onchange_offsetting_account(self):
self.payment_mode_c1.offsetting = 'bank_account'
self.payment_mode_c1.offsetting_account_change()
self.assertFalse(self.payment_mode_c1.transfer_account_id)
def test_onchange_payment_type(self):
self.payment_mode_c1.payment_method_id = self.manual_in
self.payment_mode_c1.payment_method_id_change()
self.assertTrue(all([
journal.type in [
'sale_refund', 'sale'
] for journal in self.payment_mode_c1.default_journal_ids
]))
self.payment_mode_c1.payment_method_id = self.manual_out
self.payment_mode_c1.payment_method_id_change()
self.assertTrue(all([
journal.type in [
'purchase_refund', 'purchase'
] for journal in self.payment_mode_c1.default_journal_ids
]))

View File

@@ -0,0 +1,111 @@
# © 2017 Camptocamp SA
# © 2017 Creu Blanca
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo.tests.common import TransactionCase
from odoo.exceptions import ValidationError, UserError
from datetime import date, timedelta
class TestPaymentOrderInbound(TransactionCase):
def setUp(self):
super(TestPaymentOrderInbound, self).setUp()
self.inbound_mode = self.browse_ref(
'account_payment_mode.payment_mode_inbound_dd1'
)
self.invoice_line_account = self.env['account.account'].search(
[('user_type_id', '=', self.env.ref(
'account.data_account_type_revenue').id)],
limit=1).id
self.journal = self.env['account.journal'].search(
[('type', '=', 'bank')], limit=1
)
self.inbound_mode.variable_journal_ids = self.journal
self.inbound_order = self.env['account.payment.order'].create({
'payment_type': 'inbound',
'payment_mode_id': self.inbound_mode.id,
'journal_id': self.journal.id,
})
self.invoice = self._create_customer_invoice()
def _create_customer_invoice(self):
invoice_account = self.env['account.account'].search(
[('user_type_id', '=', self.env.ref(
'account.data_account_type_receivable').id)],
limit=1).id
invoice = self.env['account.invoice'].create({
'partner_id': self.env.ref('base.res_partner_4').id,
'account_id': invoice_account,
'type': 'out_invoice',
'payment_mode_id': self.inbound_mode.id
})
self.env['account.invoice.line'].create({
'product_id': self.env.ref('product.product_product_4').id,
'quantity': 1.0,
'price_unit': 100.0,
'invoice_id': invoice.id,
'name': 'product that cost 100',
'account_id': self.invoice_line_account,
})
return invoice
def test_constrains_type(self):
with self.assertRaises(ValidationError):
order = self.env['account.payment.order'].create({
'payment_mode_id': self.inbound_mode.id,
'journal_id': self.journal.id,
})
order.payment_type = 'outbound'
def test_constrains_date(self):
with self.assertRaises(ValidationError):
self.inbound_order.date_scheduled = date.today() - timedelta(
days=1)
def test_creation(self):
# Open invoice
self.invoice.action_invoice_open()
# Add to payment order using the wizard
self.env['account.invoice.payment.line.multi'].with_context(
active_model='account.invoice',
active_ids=self.invoice.ids
).create({}).run()
payment_order = self.env['account.payment.order'].search([])
self.assertEqual(len(payment_order.ids), 1)
bank_journal = self.env['account.journal'].search(
[('type', '=', 'bank')], limit=1)
# Set journal to allow cancelling entries
bank_journal.update_posted = True
payment_order.write({
'journal_id': bank_journal.id,
})
self.assertEqual(len(payment_order.payment_line_ids), 1)
self.assertEqual(len(payment_order.bank_line_ids), 0)
# Open payment order
payment_order.draft2open()
self.assertEqual(payment_order.bank_line_count, 1)
# Generate and upload
payment_order.open2generated()
payment_order.generated2uploaded()
self.assertEqual(payment_order.state, 'uploaded')
with self.assertRaises(UserError):
payment_order.unlink()
bank_line = payment_order.bank_line_ids
with self.assertRaises(UserError):
bank_line.unlink()
payment_order.action_done_cancel()
self.assertEqual(payment_order.state, 'cancel')
payment_order.cancel2draft()
payment_order.unlink()
self.assertEqual(len(self.env['account.payment.order'].search([])), 0)

View File

@@ -1,28 +1,37 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Camptocamp SA
# © 2017 Camptocamp SA
# © 2017 Creu Blanca
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from datetime import date, datetime, timedelta
from odoo.exceptions import UserError, ValidationError
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError
from datetime import datetime
class TestPaymentOrder(TransactionCase):
class TestPaymentOrderOutbound(TransactionCase):
def setUp(self):
super(TestPaymentOrder, self).setUp()
super(TestPaymentOrderOutbound, self).setUp()
self.journal = self.env['account.journal'].search(
[('type', '=', 'bank')], limit=1
)
self.invoice_line_account = self.env['account.account'].search(
[('user_type_id', '=', self.env.ref(
'account.data_account_type_expenses').id)],
limit=1).id
self.invoice = self._create_supplier_invoice()
self.invoice_02 = self._create_supplier_invoice()
self.mode = self.env.ref(
'account_payment_mode.payment_mode_outbound_ct1')
self.creation_mode = self.env.ref(
'account_payment_mode.payment_mode_outbound_dd1')
self.bank_journal = self.env['account.journal'].search(
[('type', '=', 'bank')], limit=1)
def _create_supplier_invoice(self):
invoice_account = self.env['account.account'].search(
[('user_type_id', '=', self.env.ref(
'account.data_account_type_payable').id)],
limit=1).id
self.invoice_line_account = self.env['account.account'].search(
[('user_type_id', '=', self.env.ref(
'account.data_account_type_expenses').id)],
limit=1).id
invoice = self.env['account.invoice'].create({
'partner_id': self.env.ref('base.res_partner_4').id,
'account_id': invoice_account,
@@ -42,42 +51,81 @@ class TestPaymentOrder(TransactionCase):
return invoice
def test_creation(self):
def test_creation_due_date(self):
self.mode.variable_journal_ids = self.bank_journal
self.mode.group_lines = False
self.order_creation('due')
def test_creation_no_date(self):
self.mode.group_lines = True
self.creation_mode.write({
'group_lines': False,
'bank_account_link': 'fixed',
'default_date_prefered': 'due',
'fixed_journal_id': self.bank_journal.id,
})
self.mode.variable_journal_ids = self.bank_journal
self.order_creation(False)
def test_creation_fixed_date(self):
self.mode.write({
'bank_account_link': 'fixed',
'default_date_prefered': 'fixed',
'fixed_journal_id': self.bank_journal.id,
})
self.invoice_02.action_invoice_open()
self.order_creation('fixed')
def order_creation(self, date_prefered):
# Open invoice
self.invoice.action_invoice_open()
mode = self.env.ref('account_payment_mode.payment_mode_outbound_ct1')
order = self.env['account.payment.order'].create({
order_vals = {
'payment_type': 'outbound',
'payment_mode_id': self.env.ref(
'account_payment_mode.payment_mode_outbound_dd1').id
})
bank_journal = self.env['account.journal'].search(
[('type', '=', 'bank')], limit=1)
mode.variable_journal_ids = bank_journal
order.payment_mode_id = mode.id
'payment_mode_id': self.creation_mode.id,
}
if date_prefered:
order_vals['date_prefered'] = date_prefered
order = self.env['account.payment.order'].create(order_vals)
with self.assertRaises(UserError):
order.draft2open()
order.payment_mode_id = self.mode.id
order.payment_mode_id_change()
self.assertEqual(order.journal_id.id, bank_journal.id)
self.assertEqual(order.journal_id.id, self.bank_journal.id)
self.assertEqual(len(order.payment_line_ids), 0)
if date_prefered:
self.assertEqual(order.date_prefered, date_prefered)
with self.assertRaises(UserError):
order.draft2open()
line_create = self.env['account.payment.line.create'].with_context(
active_model='account.payment.order',
active_id=order.id
).create({})
line_create.date_type = 'move'
line_create.move_date = datetime.now()
).create({
'date_type': 'move',
'move_date': datetime.now()
})
line_create.payment_mode = 'any'
line_create.move_line_filters_change()
line_create.populate()
line_create.create_payment_lines()
line_create_due = self.env['account.payment.line.create'].with_context(
line_created_due = self.env[
'account.payment.line.create'].with_context(
active_model='account.payment.order',
active_id=order.id
).create({})
line_create_due.date_type = 'due'
line_create_due.due_date = datetime.now()
line_create_due.populate()
line_create_due.create_payment_lines()
).create({
'date_type': 'due',
'due_date': datetime.now()
})
line_created_due.populate()
line_created_due.create_payment_lines()
self.assertGreater(len(order.payment_line_ids), 0)
order.draft2open()
order.open2generated()
order.generated2uploaded()
order.action_done()
self.assertEqual(order.state, 'done')
def test_cancel_payment_order(self):
# Open invoice
@@ -96,7 +144,7 @@ class TestPaymentOrder(TransactionCase):
bank_journal.update_posted = True
payment_order.write({
'journal_id': bank_journal.id
'journal_id': bank_journal.id,
})
self.assertEqual(len(payment_order.payment_line_ids), 1)
@@ -122,6 +170,15 @@ class TestPaymentOrder(TransactionCase):
payment_order.action_done_cancel()
self.assertEqual(payment_order.state, 'cancel')
payment_order.cancel2draft()
payment_order.unlink()
self.assertEqual(len(self.env['account.payment.order'].search([])), 0)
def test_constrains(self):
outbound_order = self.env['account.payment.order'].create({
'payment_type': 'outbound',
'payment_mode_id': self.mode.id,
'journal_id': self.journal.id,
})
with self.assertRaises(ValidationError):
outbound_order.date_scheduled = date.today() - timedelta(
days=1)

View File

@@ -14,6 +14,7 @@
<field name="move_line_id"
domain="[('reconciled','=', False), ('account_id.reconcile', '=', True)] "/> <!-- we removed the filter on amount_to_pay, because we want to be able to select refunds -->
<field name="date"/>
<field name="ml_maturity_date" readonly="1"/>
<field name="amount_currency"/>
<field name="currency_id"/>
<field name="partner_id"/>
@@ -46,6 +47,7 @@
<field name="partner_id"/>
<field name="communication"/>
<field name="partner_bank_id"/>
<field name="move_line_id" invisible="1"/>
<field name="ml_maturity_date"/>
<field name="date"/>
<field name="amount_currency" string="Amount"/>

View File

@@ -10,7 +10,7 @@
<field name="name">bank.payment.line.form</field>
<field name="model">bank.payment.line</field>
<field name="arch" type="xml">
<form string="Bank Payment Line">
<form string="Bank Payment Line" create="false">
<group name="main">
<field name="order_id"
invisible="not context.get('bank_payment_line_main_view')"/>
@@ -36,7 +36,7 @@
<field name="name">bank.payment.line.tree</field>
<field name="model">bank.payment.line</field>
<field name="arch" type="xml">
<tree string="Bank Payment Lines">
<tree string="Bank Payment Lines" create="false">
<field name="order_id"
invisible="not context.get('bank_payment_line_main_view')"/>
<field name="partner_id"/>

View File

@@ -101,17 +101,11 @@ class AccountPaymentLineCreate(models.TransientModel):
# will not be refunded with a payment.
domain += [
('credit', '>', 0),
# '|',
('account_id.internal_type', '=', 'payable'),
# '&',
# ('account_id.internal_type', '=', 'receivable'),
# ('reconcile_partial_id', '=', False), # TODO uncomment
]
('account_id.internal_type', 'in', ['payable', 'receivable'])]
elif self.order_id.payment_type == 'inbound':
domain += [
('debit', '>', 0),
('account_id.internal_type', '=', 'receivable'),
]
('account_id.internal_type', 'in', ['receivable', 'payable'])]
# Exclude lines that are already in a non-cancelled
# and non-uploaded payment order; lines that are in a
# uploaded payment order are proposed if they are not reconciled,

View File

@@ -0,0 +1 @@
__import__('pkg_resources').declare_namespace(__name__)

View File

@@ -0,0 +1 @@
__import__('pkg_resources').declare_namespace(__name__)

View File

@@ -0,0 +1 @@
../../../../account_payment_order

View File

@@ -0,0 +1,2 @@
[bdist_wheel]
universal=1

View File

@@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)