Merge PR #876 into 15.0

Signed-off-by HaraldPanten
This commit is contained in:
OCA-git-bot
2022-03-16 16:28:05 +00:00
92 changed files with 57560 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
=====================
Account Payment Order
=====================
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! 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%2Fbank--payment-lightgray.png?logo=github
:target: https://github.com/OCA/bank-payment/tree/14.0/account_payment_order
:alt: OCA/bank-payment
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/bank-payment-14-0/bank-payment-14-0-account_payment_order
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/173/14.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
This module adds support for payment orders and debit orders.
**Table of contents**
.. contents::
:local:
Installation
============
This module depends on:
* account_payment_partner
* base_iban
* document
This modules is part of the OCA/bank-payment suite.
Configuration
=============
This module adds several options on Payment Modes, cf Invoicing/Accounting >
Configuration > Management > Payment Modes.
Usage
=====
You can create a Payment order via the menu Invoicing/Accounting > Vendors > Payment Orders and then select the move lines to pay.
You can create a Debit order via the menu Invoicing/Accounting > Customers > Debit Orders and then select the move lines to debit.
This module also adds an action *Add to Payment Order* on supplier invoices and *Add to Debit Order* on customer invoices.
You can print a Payment order via the menu Invoicing/Accounting > Vendors > Payment Orders and then select the payment oder to print.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/bank-payment/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 <https://github.com/OCA/bank-payment/issues/new?body=module:%20account_payment_order%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* ACSONE SA/NV
* Therp BV
* Tecnativa
* Akretion
Contributors
~~~~~~~~~~~~
* Stéphane Bidoul <stephane.bidoul@acsone.eu>
* Alexis de Lattre <alexis.delattre@akretion.com>
* Adrien Peiffer <adrien.peiffer@acsone.eu>
* Stefan Rijnhart
* Laurent Mignon <laurent.mignon@acsone.eu>
* Alexandre Fayolle
* Danimar Ribeiro
* Erwin van der Ploeg
* Raphaël Valyi
* Sandy Carter
* Angel Moya <angel.moya@domatix.com>
* Jose María Alzaga <jose.alzaga@aselcis.com>
* Meyomesse Gilles <meyomesse.gilles@gmail.com>
* `DynApps <https://www.dynapps.be>`_:
* Raf Ven <raf.ven@dynapps.be>
* Andrea Stirpe <a.stirpe@onestein.nl>
* `Jarsa <https://www.jarsa.com.mx>`_:
* Alan Ramos <alan.ramos@jarsa.com.mx>
* `Tecnativa <https://www.tecnativa.com>`_:
* Pedro M. Baeza
* Carlos Dauden
* Carlos Roca
* `Open Source Integrators <https://www.opensourceintegrators.com>`_:
* Ammar Officewala <aofficewala@opensourceintegrators.com>
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/bank-payment <https://github.com/OCA/bank-payment/tree/14.0/account_payment_order>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@@ -0,0 +1,3 @@
from . import models
from . import report
from . import wizard

View File

@@ -0,0 +1,42 @@
# © 2009 EduSense BV (<http://www.edusense.nl>)
# © 2011-2013 Therp BV (<https://therp.nl>)
# © 2013-2014 ACSONE SA (<https://acsone.eu>).
# © 2014-2016 Tecnativa - Pedro M. Baeza
# © 2016 Akretion (<https://www.akretion.com>).
# © 2016 Aselcis (<https://www.aselcis.com>).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Account Payment Order",
"version": "15.0.1.0.0",
"license": "AGPL-3",
"author": "ACSONE SA/NV, "
"Therp BV, "
"Tecnativa, "
"Akretion, "
"Odoo Community Association (OCA)",
"website": "https://github.com/OCA/bank-payment",
"category": "Banking addons",
"external_dependencies": {"python": ["lxml"]},
"depends": ["account_payment_partner", "base_iban"], # for manual_bank_tranfer
"data": [
"views/account_payment_method.xml",
"security/payment_security.xml",
"security/ir.model.access.csv",
"wizard/account_payment_line_create_view.xml",
"wizard/account_invoice_payment_line_multi_view.xml",
"views/account_payment_mode.xml",
"views/account_payment_order.xml",
"views/account_payment_line.xml",
"views/bank_payment_line.xml",
"views/account_move_line.xml",
"views/ir_attachment.xml",
"views/account_invoice_view.xml",
"data/payment_seq.xml",
"report/print_account_payment_order.xml",
"report/account_payment_order.xml",
],
"demo": ["demo/payment_demo.xml"],
"installable": True,
}

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
© 2015-2016 Akretion (https://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
Copyright 2019 Tecnativa - Pedro M. Baeza
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo noupdate="1">
<record id="bank_payment_line_seq" model="ir.sequence">
<field name="name">Bank Payment Line</field>
<field name="code">bank.payment.line</field>
<field name="prefix">L</field>
<field name="padding">5</field>
<field name="company_id" eval="False" />
</record>
<record id="account_payment_line_seq" model="ir.sequence">
<field name="name">Payment Line</field>
<field name="code">account.payment.line</field>
<field name="prefix">P</field>
<field name="padding">5</field>
<field name="company_id" eval="False" />
</record>
<record id="account_payment_order_seq" model="ir.sequence">
<field name="name">Payment Order</field>
<field name="code">account.payment.order</field>
<field name="prefix">PAY</field>
<field name="padding">4</field>
<field name="company_id" eval="False" />
</record>
</odoo>

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record
id="account_payment_mode.payment_mode_outbound_dd1"
model="account.payment.mode"
>
<field name="payment_order_ok" eval="False" />
</record>
<record
id="account_payment_mode.payment_mode_outbound_dd2"
model="account.payment.mode"
>
<field name="payment_order_ok" eval="False" />
</record>
<record
id="account_payment_mode.payment_mode_inbound_ct1"
model="account.payment.mode"
>
<field name="payment_order_ok" eval="False" />
</record>
<record
id="account_payment_mode.payment_mode_inbound_ct2"
model="account.payment.mode"
>
<field name="payment_order_ok" eval="False" />
</record>
<record
id="account_payment_mode.payment_mode_outbound_ct1"
model="account.payment.mode"
>
<!-- Credit Transfer to Suppliers -->
<field
name="default_journal_ids"
search="[('type', 'in', ('purchase', 'purchase_refund'))]"
/>
</record>
<record
id="account_payment_mode.payment_mode_inbound_dd1"
model="account.payment.mode"
>
<!-- Direct Debit of customers -->
<field
name="default_journal_ids"
search="[('type', 'in', ('sale', 'sale_refund'))]"
/>
</record>
</odoo>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
# Copyright 2021 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
def migrate(cr, version):
if not version:
return
cr.execute("UPDATE account_payment_order SET state='uploaded' WHERE state='done'")

View File

@@ -0,0 +1,10 @@
from . import account_payment_mode
from . import account_payment_order
from . import account_payment_line
from . import bank_payment_line
from . import account_move
from . import account_move_line
from . import res_bank
from . import account_payment_method
from . import account_journal
from . import account_payment

View File

@@ -0,0 +1,33 @@
# Copyright 2019 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class AccountJournal(models.Model):
_inherit = "account.journal"
inbound_payment_order_only = fields.Boolean(
compute="_compute_inbound_payment_order_only", readonly=True, store=True
)
outbound_payment_order_only = fields.Boolean(
compute="_compute_outbound_payment_order_only", readonly=True, store=True
)
@api.depends("inbound_payment_method_line_ids.payment_method_id.payment_order_only")
def _compute_inbound_payment_order_only(self):
for rec in self:
rec.inbound_payment_order_only = all(
p.payment_order_only
for p in rec.inbound_payment_method_line_ids.payment_method_id
)
@api.depends(
"outbound_payment_method_line_ids.payment_method_id.payment_order_only"
)
def _compute_outbound_payment_order_only(self):
for rec in self:
rec.outbound_payment_order_only = all(
p.payment_order_only
for p in rec.outbound_payment_method_line_ids.payment_method_id
)

View File

@@ -0,0 +1,139 @@
# © 2013-2014 ACSONE SA (<https://acsone.eu>).
# © 2014 Serv. Tecnol. Avanzados - Pedro M. Baeza
# © 2016 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>)
# 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 AccountMove(models.Model):
_inherit = "account.move"
payment_order_id = fields.Many2one(
comodel_name="account.payment.order",
string="Payment Order",
copy=False,
readonly=True,
check_company=True,
)
payment_order_ok = fields.Boolean(compute="_compute_payment_order_ok")
# we restore this field from <=v11 for now for preserving behavior
# TODO: Check if we can remove it and base everything in something at
# payment mode or company level
reference_type = fields.Selection(
selection=[("none", "Free Reference"), ("structured", "Structured Reference")],
readonly=True,
states={"draft": [("readonly", False)]},
default="none",
)
@api.depends("payment_mode_id", "line_ids", "line_ids.payment_mode_id")
def _compute_payment_order_ok(self):
for move in self:
payment_mode = move.line_ids.filtered(lambda x: not x.reconciled).mapped(
"payment_mode_id"
)[:1]
if not payment_mode:
payment_mode = move.payment_mode_id
move.payment_order_ok = payment_mode.payment_order_ok
def _prepare_new_payment_order(self, payment_mode=None):
self.ensure_one()
if payment_mode is None:
payment_mode = self.env["account.payment.mode"]
vals = {"payment_mode_id": payment_mode.id or self.payment_mode_id.id}
# other important fields are set by the inherit of create
# in account_payment_order.py
return vals
def get_account_payment_domain(self, payment_mode):
return [("payment_mode_id", "=", payment_mode.id), ("state", "=", "draft")]
def create_account_payment_line(self):
apoo = self.env["account.payment.order"]
result_payorder_ids = []
action_payment_type = "debit"
for move in self:
if move.state != "posted":
raise UserError(_("The invoice %s is not in Posted state") % move.name)
applicable_lines = move.line_ids.filtered(
lambda x: (
not x.reconciled
and x.payment_mode_id.payment_order_ok
and x.account_id.internal_type in ("receivable", "payable")
and not any(
p_state in ("draft", "open", "generated")
for p_state in x.payment_line_ids.mapped("state")
)
)
)
if not applicable_lines:
raise UserError(
_(
"No Payment Line created for invoice %s because "
"it already exists or because this invoice is "
"already paid."
)
% move.name
)
payment_modes = applicable_lines.mapped("payment_mode_id")
if not payment_modes:
raise UserError(_("No Payment Mode on invoice %s") % move.name)
for payment_mode in payment_modes:
payorder = apoo.search(
move.get_account_payment_domain(payment_mode), limit=1
)
new_payorder = False
if not payorder:
payorder = apoo.create(
move._prepare_new_payment_order(payment_mode)
)
new_payorder = True
result_payorder_ids.append(payorder.id)
action_payment_type = payorder.payment_type
count = 0
for line in applicable_lines.filtered(
lambda x: x.payment_mode_id == payment_mode
):
line.create_payment_line_from_move_line(payorder)
count += 1
if new_payorder:
move.message_post(
body=_(
"%(count)d payment lines added to the new draft payment "
"order %(name)s which has been automatically created.",
count=count,
name=payorder.name,
)
)
else:
move.message_post(
body=_(
"%(count)d payment lines added to the existing draft "
"payment order %(name)s.",
count=count,
name=payorder.name,
)
)
action = self.env["ir.actions.act_window"]._for_xml_id(
"account_payment_order.account_payment_order_%s_action"
% action_payment_type,
)
if len(result_payorder_ids) == 1:
action.update(
{
"view_mode": "form,tree,pivot,graph",
"res_id": payorder.id,
"views": False,
}
)
else:
action.update(
{
"view_mode": "tree,form,pivot,graph",
"domain": "[('id', 'in', %s)]" % result_payorder_ids,
"views": False,
}
)
return action

View File

@@ -0,0 +1,102 @@
# © 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 odoo import api, fields, models
from odoo.fields import first
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
partner_bank_id = fields.Many2one(
comodel_name="res.partner.bank",
string="Partner Bank Account",
compute="_compute_partner_bank_id",
readonly=False,
store=True,
help="Bank account on which we should pay the supplier",
check_company=True,
)
bank_payment_line_id = fields.Many2one(
comodel_name="bank.payment.line",
readonly=True,
index=True,
check_company=True,
)
payment_line_ids = fields.One2many(
comodel_name="account.payment.line",
inverse_name="move_line_id",
string="Payment lines",
check_company=True,
)
@api.depends("move_id", "move_id.partner_bank_id", "move_id.payment_mode_id")
def _compute_partner_bank_id(self):
for ml in self:
if (
ml.move_id.move_type in ("in_invoice", "in_refund")
and not ml.reconciled
and ml.payment_mode_id.payment_order_ok
and ml.account_id.internal_type in ("receivable", "payable")
and not any(
p_state in ("draft", "open", "generated")
for p_state in ml.payment_line_ids.mapped("state")
)
):
ml.partner_bank_id = ml.move_id.partner_bank_id.id
else:
ml.partner_bank_id = ml.partner_bank_id
def _prepare_payment_line_vals(self, payment_order):
self.ensure_one()
assert payment_order, "Missing payment order"
aplo = self.env["account.payment.line"]
# default values for communication_type and communication
communication_type = "normal"
communication = self.ref or self.name
# change these default values if move line is linked to an invoice
if self.move_id.is_invoice():
if (self.move_id.reference_type or "none") != "none":
communication = self.move_id.ref
ref2comm_type = aplo.invoice_reference_type2communication_type()
communication_type = ref2comm_type[self.move_id.reference_type]
else:
if (
self.move_id.move_type in ("in_invoice", "in_refund")
and self.move_id.ref
):
communication = self.move_id.ref
elif "out" in self.move_id.move_type:
# Force to only put invoice number here
communication = self.move_id.name
if self.currency_id:
currency_id = self.currency_id.id
amount_currency = self.amount_residual_currency
else:
currency_id = self.company_id.currency_id.id
amount_currency = self.amount_residual
# TODO : check that self.amount_residual_currency is 0
# in this case
if payment_order.payment_type == "outbound":
amount_currency *= -1
partner_bank_id = self.partner_bank_id.id or first(self.partner_id.bank_ids).id
vals = {
"order_id": payment_order.id,
"partner_bank_id": partner_bank_id,
"partner_id": self.partner_id.id,
"move_line_id": self.id,
"communication": communication,
"communication_type": communication_type,
"currency_id": currency_id,
"amount_currency": amount_currency,
"date": False,
# date is set when the user confirms the payment order
}
return vals
def create_payment_line_from_move_line(self, payment_order):
vals_list = []
for mline in self:
vals_list.append(mline._prepare_payment_line_vals(payment_order))
return self.env["account.payment.line"].create(vals_list)

View File

@@ -0,0 +1,43 @@
# Copyright 2019 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, models
class AccountPayment(models.Model):
_inherit = "account.payment"
def _get_default_journal(self):
res = super()._get_default_journal()
return res.filtered(lambda journal: not journal.inbound_payment_order_only)
@api.depends("payment_type", "journal_id")
def _compute_payment_method_line_fields(self):
res = super()._compute_payment_method_line_fields()
for pay in self:
pay.available_payment_method_line_ids = (
pay.journal_id._get_available_payment_method_lines(
pay.payment_type
).filtered(lambda x: not x.payment_method_id.payment_order_only)
)
to_exclude = self._get_payment_method_codes_to_exclude()
if to_exclude:
pay.available_payment_method_line_ids = (
pay.available_payment_method_line_ids.filtered(
lambda x: x.code not in to_exclude
)
)
if (
pay.payment_method_line_id.id
not in pay.available_payment_method_line_ids.ids
):
# In some cases, we could be linked to a payment method
# line that has been unlinked from the journal.
# In such cases, we want to show it on the payment.
pay.hide_payment_method_line = False
else:
pay.hide_payment_method_line = (
len(pay.available_payment_method_line_ids) == 1
and pay.available_payment_method_line_ids.code == "manual"
)
return res

View File

@@ -0,0 +1,170 @@
# © 2015-2016 Akretion - Alexis de Lattre <alexis.delattre@akretion.com>
# 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 AccountPaymentLine(models.Model):
_name = "account.payment.line"
_description = "Payment Lines"
_check_company_auto = True
name = fields.Char(string="Payment Reference", readonly=True, copy=False)
order_id = fields.Many2one(
comodel_name="account.payment.order",
string="Payment Order",
ondelete="cascade",
index=True,
check_company=True,
)
company_id = fields.Many2one(
related="order_id.company_id", store=True, readonly=True
)
company_currency_id = fields.Many2one(
related="order_id.company_currency_id", store=True, readonly=True
)
payment_type = fields.Selection(
related="order_id.payment_type", store=True, readonly=True
)
bank_account_required = fields.Boolean(
related="order_id.payment_method_id.bank_account_required", readonly=True
)
state = fields.Selection(
related="order_id.state", string="State", readonly=True, store=True
)
move_line_id = fields.Many2one(
comodel_name="account.move.line",
string="Journal Item",
ondelete="restrict",
check_company=True,
)
ml_maturity_date = fields.Date(related="move_line_id.date_maturity", readonly=True)
currency_id = fields.Many2one(
comodel_name="res.currency",
string="Currency of the Payment Transaction",
required=True,
default=lambda self: self.env.user.company_id.currency_id,
)
amount_currency = fields.Monetary(string="Amount", currency_field="currency_id")
amount_company_currency = fields.Monetary(
compute="_compute_amount_company_currency",
string="Amount in Company Currency",
currency_field="company_currency_id",
)
partner_id = fields.Many2one(
comodel_name="res.partner",
string="Partner",
required=True,
domain=[("parent_id", "=", False)],
check_company=True,
)
partner_bank_id = fields.Many2one(
comodel_name="res.partner.bank",
string="Partner Bank Account",
required=False,
ondelete="restrict",
check_company=True,
)
date = fields.Date(string="Payment Date")
# communication field is required=False because we don't want to block
# the creation of lines from move/invoices when communication is empty
# This field is required in the form view and there is an error message
# when going from draft to confirm if the field is empty
communication = fields.Char(
required=False, help="Label of the payment that will be seen by the destinee"
)
communication_type = fields.Selection(
selection=[("normal", "Free")], required=True, default="normal"
)
bank_line_id = fields.Many2one(
comodel_name="bank.payment.line",
string="Bank Payment Line",
readonly=True,
index=True,
check_company=True,
)
_sql_constraints = [
(
"name_company_unique",
"unique(name, company_id)",
"A payment line already exists with this reference " "in the same company!",
)
]
@api.model
def create(self, vals):
if vals.get("name", "New") == "New":
vals["name"] = (
self.env["ir.sequence"].next_by_code("account.payment.line") or "New"
)
return super(AccountPaymentLine, self).create(vals)
@api.depends("amount_currency", "currency_id", "company_currency_id", "date")
def _compute_amount_company_currency(self):
for line in self:
if line.currency_id and line.company_currency_id:
line.amount_company_currency = line.currency_id._convert(
line.amount_currency,
line.company_currency_id,
line.company_id,
line.date or fields.Date.today(),
)
else:
line.amount_company_currency = 0
def payment_line_hashcode(self):
self.ensure_one()
bplo = self.env["bank.payment.line"]
values = []
for field in bplo.same_fields_payment_line_and_bank_payment_line():
values.append(str(self[field]))
# Don't group the payment lines that are attached to the same supplier
# but to move lines with different accounts (very unlikely),
# for easier generation/comprehension of the transfer move
values.append(str(self.move_line_id.account_id or False))
# Don't group the payment lines that use a structured communication
# otherwise it would break the structured communication system !
if self.communication_type != "normal":
values.append(str(self.id))
hashcode = "-".join(values)
return hashcode
@api.onchange("partner_id")
def partner_id_change(self):
partner_bank = False
if self.partner_id.bank_ids:
partner_bank = self.partner_id.bank_ids[0]
self.partner_bank_id = partner_bank
@api.onchange("move_line_id")
def move_line_id_change(self):
if self.move_line_id:
vals = self.move_line_id._prepare_payment_line_vals(self.order_id)
vals.pop("order_id")
for field, value in vals.items():
self[field] = value
else:
self.partner_id = False
self.partner_bank_id = False
self.amount_currency = 0.0
self.currency_id = self.env.user.company_id.currency_id
self.communication = False
def invoice_reference_type2communication_type(self):
"""This method is designed to be inherited by
localization modules"""
# key = value of 'reference_type' field on account_invoice
# value = value of 'communication_type' field on account_payment_line
res = {"none": "normal", "structured": "structured"}
return res
def draft2open_payment_line_check(self):
self.ensure_one()
if self.bank_account_required and not self.partner_bank_id:
raise UserError(
_("Missing Partner Bank Account on payment line %s") % self.name
)
if not self.communication:
raise UserError(_("Communication is empty on payment line %s.") % self.name)

View File

@@ -0,0 +1,15 @@
# Copyright 2019 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class AccountPaymentMethod(models.Model):
_inherit = "account.payment.method"
payment_order_only = fields.Boolean(
string="Only for payment orders",
help="This option helps enforcing the use of payment orders for "
"some payment methods.",
default=False,
)

View File

@@ -0,0 +1,126 @@
# © 2009 EduSense BV (<http://www.edusense.nl>)
# © 2011-2013 Therp BV (<https://therp.nl>)
# © 2014-2016 Serv. Tecnol. Avanzados - Pedro M. Baeza
# © 2016 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>)
# 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 AccountPaymentMode(models.Model):
"""This corresponds to the object payment.mode of v8 with some
important changes"""
_inherit = "account.payment.mode"
payment_order_ok = fields.Boolean(
string="Selectable in Payment Orders", default=True
)
no_debit_before_maturity = fields.Boolean(
string="Disallow Debit Before Maturity Date",
help="If you activate this option on an Inbound payment mode, "
"you will have an error message when you confirm a debit order "
"that has a payment line with a payment date before the maturity "
"date.",
)
# Default options for the "payment.order.create" wizard
default_payment_mode = fields.Selection(
selection=[("same", "Same"), ("same_or_null", "Same or empty"), ("any", "Any")],
string="Payment Mode on Invoice",
default="same",
)
default_journal_ids = fields.Many2many(
comodel_name="account.journal",
string="Journals Filter",
domain="[('company_id', '=', company_id)]",
)
default_invoice = fields.Boolean(
string="Linked to an Invoice or Refund", default=False
)
default_target_move = fields.Selection(
selection=[("posted", "All Posted Entries"), ("all", "All Entries")],
string="Target Moves",
default="posted",
)
default_date_type = fields.Selection(
selection=[("due", "Due"), ("move", "Move")],
default="due",
string="Type of Date Filter",
)
# default option for account.payment.order
default_date_prefered = fields.Selection(
selection=[
("now", "Immediately"),
("due", "Due Date"),
("fixed", "Fixed Date"),
],
string="Default Payment Execution Date",
)
group_lines = fields.Boolean(
string="Group Transactions in Payment Orders",
default=True,
help="If this mark is checked, the transaction lines of the "
"payment order will be grouped upon confirmation of the payment "
"order.The grouping will be done only if the following "
"fields matches:\n"
"* Partner\n"
"* Currency\n"
"* Destination Bank Account\n"
"* Payment Date\n"
"and if the 'Communication Type' is 'Free'\n"
"(other modules can set additional fields to restrict the "
"grouping.)",
)
generate_move = fields.Boolean(
string="Generate Accounting Entries On File Upload", default=True
)
move_option = fields.Selection(
selection=[
("date", "One move per payment date"),
("line", "One move per payment line"),
],
default="date",
)
post_move = fields.Boolean(default=True)
@api.constrains("generate_move", "move_option")
def transfer_move_constrains(self):
for mode in self:
if mode.generate_move and not mode.move_option:
raise ValidationError(
_(
"On the payment mode '%s', you must "
"choose an option for the 'Move Option' parameter."
)
% mode.name
)
@api.onchange("payment_method_id")
def payment_method_id_change(self):
if self.payment_method_id:
ajo = self.env["account.journal"]
aj_ids = []
if self.payment_method_id.payment_type == "outbound":
aj_ids = ajo.search(
[
("type", "in", ("purchase_refund", "purchase")),
("company_id", "=", self.company_id.id),
]
).ids
elif self.payment_method_id.payment_type == "inbound":
aj_ids = ajo.search(
[
("type", "in", ("sale_refund", "sale")),
("company_id", "=", self.company_id.id),
]
).ids
self.default_journal_ids = [(6, 0, aj_ids)]
@api.onchange("generate_move")
def generate_move_change(self):
if self.generate_move:
# default values
self.move_option = "date"
else:
self.move_option = False

View File

@@ -0,0 +1,613 @@
# © 2009 EduSense BV (<http://www.edusense.nl>)
# © 2011-2013 Therp BV (<https://therp.nl>)
# © 2016 Serv. Tecnol. Avanzados - Pedro M. Baeza
# © 2016 Akretion (Alexis de Lattre - alexis.delattre@akretion.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import base64
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
class AccountPaymentOrder(models.Model):
_name = "account.payment.order"
_description = "Payment Order"
_inherit = ["mail.thread"]
_order = "id desc"
_check_company_auto = True
name = fields.Char(string="Number", readonly=True, copy=False)
payment_mode_id = fields.Many2one(
comodel_name="account.payment.mode",
required=True,
ondelete="restrict",
tracking=True,
readonly=True,
states={"draft": [("readonly", False)]},
check_company=True,
)
payment_type = fields.Selection(
selection=[("inbound", "Inbound"), ("outbound", "Outbound")],
readonly=True,
required=True,
)
payment_method_id = fields.Many2one(
comodel_name="account.payment.method",
related="payment_mode_id.payment_method_id",
readonly=True,
store=True,
)
company_id = fields.Many2one(
related="payment_mode_id.company_id", store=True, readonly=True
)
company_currency_id = fields.Many2one(
related="payment_mode_id.company_id.currency_id", store=True, readonly=True
)
bank_account_link = fields.Selection(
related="payment_mode_id.bank_account_link", readonly=True
)
allowed_journal_ids = fields.Many2many(
comodel_name="account.journal",
compute="_compute_allowed_journal_ids",
string="Allowed journals",
)
journal_id = fields.Many2one(
comodel_name="account.journal",
string="Bank Journal",
ondelete="restrict",
readonly=True,
states={"draft": [("readonly", False)]},
tracking=True,
check_company=True,
)
# The journal_id field is only required at confirm step, to
# allow auto-creation of payment order from invoice
company_partner_bank_id = fields.Many2one(
related="journal_id.bank_account_id",
string="Company Bank Account",
readonly=True,
)
state = fields.Selection(
selection=[
("draft", "Draft"),
("open", "Confirmed"),
("generated", "File Generated"),
("uploaded", "File Uploaded"),
("cancel", "Cancel"),
],
string="Status",
readonly=True,
copy=False,
default="draft",
tracking=True,
)
date_prefered = fields.Selection(
selection=[
("now", "Immediately"),
("due", "Due Date"),
("fixed", "Fixed Date"),
],
string="Payment Execution Date Type",
required=True,
default="due",
tracking=True,
readonly=True,
states={"draft": [("readonly", False)]},
)
date_scheduled = fields.Date(
string="Payment Execution Date",
readonly=True,
states={"draft": [("readonly", False)]},
tracking=True,
help="Select a requested date of execution if you selected 'Due Date' "
"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)
generated_user_id = fields.Many2one(
comodel_name="res.users",
string="Generated by",
readonly=True,
ondelete="restrict",
copy=False,
check_company=True,
)
payment_line_ids = fields.One2many(
comodel_name="account.payment.line",
inverse_name="order_id",
string="Transactions",
readonly=True,
states={"draft": [("readonly", False)]},
)
bank_line_ids = fields.One2many(
comodel_name="bank.payment.line",
inverse_name="order_id",
string="Bank Transactions",
readonly=True,
help="The bank payment lines are used to generate the payment file. "
"They are automatically created from transaction lines upon "
"confirmation of the payment order: one bank payment line can "
"group several transaction lines if the option "
"'Group Transactions in Payment Orders' is active on the payment "
"mode.",
)
total_company_currency = fields.Monetary(
compute="_compute_total", store=True, currency_field="company_currency_id"
)
bank_line_count = fields.Integer(
compute="_compute_bank_line_count", string="Number of Bank Transactions"
)
move_ids = fields.One2many(
comodel_name="account.move",
inverse_name="payment_order_id",
string="Journal Entries",
readonly=True,
)
move_count = fields.Integer(
compute="_compute_move_count", string="Number of Journal Entries"
)
description = fields.Char()
@api.depends("payment_mode_id")
def _compute_allowed_journal_ids(self):
for record in self:
if record.payment_mode_id.bank_account_link == "fixed":
record.allowed_journal_ids = record.payment_mode_id.fixed_journal_id
elif record.payment_mode_id.bank_account_link == "variable":
record.allowed_journal_ids = record.payment_mode_id.variable_journal_ids
else:
record.allowed_journal_ids = False
def unlink(self):
for order in self:
if order.state == "uploaded":
raise UserError(
_(
"You cannot delete an uploaded payment order. You can "
"cancel it in order to do so."
)
)
return super(AccountPaymentOrder, self).unlink()
@api.constrains("payment_type", "payment_mode_id")
def payment_order_constraints(self):
for order in self:
if (
order.payment_mode_id.payment_type
and order.payment_mode_id.payment_type != order.payment_type
):
raise ValidationError(
_(
"The payment type (%(ptype)s) is not the same as the payment "
"type of the payment mode (%(pmode)s)",
ptype=order.payment_type,
pmode=order.payment_mode_id.payment_type,
)
)
@api.constrains("date_scheduled")
def check_date_scheduled(self):
today = fields.Date.context_today(self)
for order in self:
if order.date_scheduled:
if order.date_scheduled < today:
raise ValidationError(
_(
"On payment order %(porder)s, the Payment Execution Date "
"is in the past (%(exedate)s).",
porder=order.name,
exedate=order.date_scheduled,
)
)
@api.depends("payment_line_ids", "payment_line_ids.amount_company_currency")
def _compute_total(self):
for rec in self:
rec.total_company_currency = sum(
rec.mapped("payment_line_ids.amount_company_currency") or [0.0]
)
@api.depends("bank_line_ids")
def _compute_bank_line_count(self):
for order in self:
order.bank_line_count = len(order.bank_line_ids)
@api.depends("move_ids")
def _compute_move_count(self):
rg_res = self.env["account.move"].read_group(
[("payment_order_id", "in", self.ids)],
["payment_order_id"],
["payment_order_id"],
)
mapped_data = {
x["payment_order_id"][0]: x["payment_order_id_count"] for x in rg_res
}
for order in self:
order.move_count = mapped_data.get(order.id, 0)
@api.model
def create(self, vals):
if vals.get("name", "New") == "New":
vals["name"] = (
self.env["ir.sequence"].next_by_code("account.payment.order") or "New"
)
if vals.get("payment_mode_id"):
payment_mode = self.env["account.payment.mode"].browse(
vals["payment_mode_id"]
)
vals["payment_type"] = payment_mode.payment_type
if payment_mode.bank_account_link == "fixed":
vals["journal_id"] = payment_mode.fixed_journal_id.id
if not vals.get("date_prefered") and payment_mode.default_date_prefered:
vals["date_prefered"] = payment_mode.default_date_prefered
return super(AccountPaymentOrder, self).create(vals)
@api.onchange("payment_mode_id")
def payment_mode_id_change(self):
if len(self.allowed_journal_ids) == 1:
self.journal_id = self.allowed_journal_ids
if self.payment_mode_id.default_date_prefered:
self.date_prefered = self.payment_mode_id.default_date_prefered
def action_uploaded_cancel(self):
for move in self.move_ids:
move.button_cancel()
for move_line in move.line_ids:
move_line.remove_move_reconcile()
move.with_context(force_delete=True).unlink()
self.action_cancel()
return True
def cancel2draft(self):
self.write({"state": "draft"})
return True
def action_cancel(self):
for order in self:
order.write({"state": "cancel"})
order.bank_line_ids.unlink()
return True
@api.model
def _prepare_bank_payment_line(self, paylines):
return {
"order_id": paylines[0].order_id.id,
"payment_line_ids": [(6, 0, paylines.ids)],
"communication": "-".join([line.communication for line in paylines]),
}
def draft2open(self):
"""
Called when you click on the 'Confirm' button
Set the 'date' on payment line depending on the 'date_prefered'
setting of the payment.order
Re-generate the bank payment lines
"""
bplo = self.env["bank.payment.line"]
today = fields.Date.context_today(self)
for order in self:
if not order.journal_id:
raise UserError(
_("Missing Bank Journal on payment order %s.") % order.name
)
if (
order.payment_method_id.bank_account_required
and not order.journal_id.bank_account_id
):
raise UserError(
_("Missing bank account on bank journal '%s'.")
% order.journal_id.display_name
)
if not order.payment_line_ids:
raise UserError(
_("There are no transactions on payment order %s.") % order.name
)
# Delete existing bank payment lines
order.bank_line_ids.unlink()
# Create the bank payment lines from the payment lines
group_paylines = {} # key = hashcode
for payline in order.payment_line_ids:
payline.draft2open_payment_line_check()
# Compute requested payment date
if order.date_prefered == "due":
requested_date = payline.ml_maturity_date or payline.date or today
elif order.date_prefered == "fixed":
requested_date = order.date_scheduled or today
else:
requested_date = today
# No payment date in the past
if requested_date < today:
requested_date = today
# inbound: check option no_debit_before_maturity
if (
order.payment_type == "inbound"
and order.payment_mode_id.no_debit_before_maturity
and payline.ml_maturity_date
and requested_date < payline.ml_maturity_date
):
raise UserError(
_(
"The payment mode '%(pmode)s' has the option "
"'Disallow Debit Before Maturity Date'. The "
"payment line %(pline)s has a maturity date %(mdate)s "
"which is after the computed payment date %(pdate)s.",
pmode=order.payment_mode_id.name,
pline=payline.name,
mdate=payline.ml_maturity_date,
pdate=requested_date,
)
)
# Write requested_date on 'date' field of payment line
# norecompute is for avoiding a chained recomputation
# payment_line_ids.date
# > payment_line_ids.amount_company_currency
# > total_company_currency
with self.env.norecompute():
payline.date = requested_date
# Group options
if order.payment_mode_id.group_lines:
hashcode = payline.payment_line_hashcode()
else:
# Use line ID as hascode, which actually means no grouping
hashcode = payline.id
if hashcode in group_paylines:
group_paylines[hashcode]["paylines"] += payline
group_paylines[hashcode]["total"] += payline.amount_currency
else:
group_paylines[hashcode] = {
"paylines": payline,
"total": payline.amount_currency,
}
order.recompute()
# Create bank payment lines
for paydict in list(group_paylines.values()):
# Block if a bank payment line is <= 0
if paydict["total"] <= 0:
raise UserError(
_(
"The amount for Partner '%(partner)s' is negative "
"or null (%(amount).2f) !",
partner=paydict["paylines"][0].partner_id.name,
amount=paydict["total"],
)
)
vals = self._prepare_bank_payment_line(paydict["paylines"])
bplo.create(vals)
self.write({"state": "open"})
return True
def generate_payment_file(self):
"""Returns (payment file as string, filename)"""
self.ensure_one()
if self.payment_method_id.code == "manual":
return (False, False)
else:
raise UserError(
_(
"No handler for this payment method. Maybe you haven't "
"installed the related Odoo module."
)
)
def open2generated(self):
self.ensure_one()
payment_file_str, filename = self.generate_payment_file()
action = {}
if payment_file_str and filename:
attachment = self.env["ir.attachment"].create(
{
"res_model": "account.payment.order",
"res_id": self.id,
"name": filename,
"datas": base64.b64encode(payment_file_str),
}
)
simplified_form_view = self.env.ref(
"account_payment_order.view_attachment_simplified_form"
)
action = {
"name": _("Payment File"),
"view_mode": "form",
"view_id": simplified_form_view.id,
"res_model": "ir.attachment",
"type": "ir.actions.act_window",
"target": "current",
"res_id": attachment.id,
}
self.write(
{
"date_generated": fields.Date.context_today(self),
"state": "generated",
"generated_user_id": self._uid,
}
)
return action
def generated2uploaded(self):
for order in self:
if order.payment_mode_id.generate_move:
order.generate_move()
self.write(
{"state": "uploaded", "date_uploaded": fields.Date.context_today(self)}
)
return True
def _prepare_move(self, bank_lines=None):
if self.payment_type == "outbound":
ref = _("Payment order %s") % self.name
else:
ref = _("Debit order %s") % self.name
if bank_lines and len(bank_lines) == 1:
ref += " - " + bank_lines.name
vals = {
"date": bank_lines[0].date,
"journal_id": self.journal_id.id,
"ref": ref,
"payment_order_id": self.id,
"line_ids": [],
}
total_company_currency = total_payment_currency = 0
for bline in bank_lines:
total_company_currency += bline.amount_company_currency
total_payment_currency += bline.amount_currency
partner_ml_vals = self._prepare_move_line_partner_account(bline)
vals["line_ids"].append((0, 0, partner_ml_vals))
trf_ml_vals = self._prepare_move_line_offsetting_account(
total_company_currency, total_payment_currency, bank_lines
)
vals["line_ids"].append((0, 0, trf_ml_vals))
return vals
def _prepare_move_line_offsetting_account(
self, amount_company_currency, amount_payment_currency, bank_lines
):
vals = {}
payment_method = self.payment_mode_id.payment_method_id
account_id = False
if self.payment_type == "inbound":
account_id = (
self.journal_id.inbound_payment_method_line_ids.filtered(
lambda x: x.payment_method_id == payment_method
).payment_account_id.id
or self.journal_id.company_id.account_journal_payment_debit_account_id.id
)
elif self.payment_type == "outbound":
account_id = (
self.journal_id.outbound_payment_method_line_ids.filtered(
lambda x: x.payment_method_id == payment_method
).payment_account_id.id
or self.journal_id.company_id.account_journal_payment_credit_account_id.id
)
partner_id = False
for index, bank_line in enumerate(bank_lines):
if index == 0:
partner_id = bank_line.payment_line_ids[0].partner_id.id
elif bank_line.payment_line_ids[0].partner_id.id != partner_id:
# we have different partners in the grouped move
partner_id = False
break
vals.update(
{
"partner_id": partner_id,
"account_id": account_id,
"credit": (
self.payment_type == "outbound" and amount_company_currency or 0.0
),
"debit": (
self.payment_type == "inbound" and amount_company_currency or 0.0
),
}
)
if bank_lines[0].currency_id != bank_lines[0].company_currency_id:
sign = self.payment_type == "outbound" and -1 or 1
vals.update(
{
"currency_id": bank_lines[0].currency_id.id,
"amount_currency": amount_payment_currency * sign,
}
)
return vals
def _prepare_move_line_partner_account(self, bank_line):
if bank_line.payment_line_ids[0].move_line_id:
account_id = bank_line.payment_line_ids[0].move_line_id.account_id.id
else:
if self.payment_type == "inbound":
account_id = bank_line.partner_id.property_account_receivable_id.id
else:
account_id = bank_line.partner_id.property_account_payable_id.id
if self.payment_type == "outbound":
name = _("Payment bank line %s") % bank_line.name
else:
name = _("Debit bank line %s") % bank_line.name
vals = {
"name": name,
"bank_payment_line_id": bank_line.id,
"partner_id": bank_line.partner_id.id,
"account_id": account_id,
"credit": (
self.payment_type == "inbound"
and bank_line.amount_company_currency
or 0.0
),
"debit": (
self.payment_type == "outbound"
and bank_line.amount_company_currency
or 0.0
),
}
if bank_line.currency_id != bank_line.company_currency_id:
sign = self.payment_type == "inbound" and -1 or 1
vals.update(
{
"currency_id": bank_line.currency_id.id,
"amount_currency": bank_line.amount_currency * sign,
}
)
return vals
def _create_reconcile_move(self, hashcode, blines):
self.ensure_one()
post_move = self.payment_mode_id.post_move
am_obj = self.env["account.move"]
mvals = self._prepare_move(blines)
move = am_obj.create(mvals)
if post_move:
move.action_post()
blines.reconcile_payment_lines()
def _prepare_trf_moves(self):
"""
prepare a dict "trfmoves" that can be used when
self.payment_mode_id.move_option = date or line
key = unique identifier (date or True or line.id)
value = bank_pay_lines (recordset that can have several entries)
"""
self.ensure_one()
trfmoves = {}
for bline in self.bank_line_ids:
hashcode = bline.move_line_offsetting_account_hashcode()
if hashcode in trfmoves:
trfmoves[hashcode] += bline
else:
trfmoves[hashcode] = bline
return trfmoves
def generate_move(self):
"""
Create the moves that pay off the move lines from
the payment/debit order.
"""
self.ensure_one()
trfmoves = self._prepare_trf_moves()
for hashcode, blines in trfmoves.items():
self._create_reconcile_move(hashcode, blines)
def action_bank_payment_line(self):
self.ensure_one()
action = self.env.ref("account_payment_order.bank_payment_line_action")
action_dict = action.read()[0]
action_dict["domain"] = [("id", "in", self.bank_line_ids.ids)]
return action_dict
def action_move_journal_line(self):
self.ensure_one()
action = self.env.ref("account.action_move_journal_line").sudo().read()[0]
if self.move_count == 1:
action.update(
{
"view_mode": "form,tree,kanban",
"views": False,
"view_id": False,
"res_id": self.move_ids[0].id,
}
)
else:
action["domain"] = [("id", "in", self.move_ids.ids)]
ctx = self.env.context.copy()
ctx.update({"search_default_misc_filter": 0})
action["context"] = ctx
return action

View File

@@ -0,0 +1,204 @@
# Copyright 2015-2016 Akretion - Alexis de Lattre
# Copyright 2018 Tecnativa - Pedro M. Baeza
# 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 BankPaymentLine(models.Model):
_name = "bank.payment.line"
_description = "Bank Payment Lines"
_check_company_auto = True
name = fields.Char(string="Bank Payment Line Ref", required=True, readonly=True)
order_id = fields.Many2one(
comodel_name="account.payment.order",
ondelete="cascade",
index=True,
readonly=True,
check_company=True,
)
payment_type = fields.Selection(
related="order_id.payment_type", readonly=True, store=True
)
state = fields.Selection(related="order_id.state", readonly=True, store=True)
payment_line_ids = fields.One2many(
comodel_name="account.payment.line",
inverse_name="bank_line_id",
string="Payment Lines",
readonly=True,
)
partner_id = fields.Many2one(
comodel_name="res.partner",
related="payment_line_ids.partner_id",
readonly=True,
store=True,
check_company=True,
) # store=True for groupby
# Function Float fields are sometimes badly displayed in tree view,
# see bug report https://github.com/odoo/odoo/issues/8632
# But is it still true in v9 ?
amount_currency = fields.Monetary(
string="Amount",
currency_field="currency_id",
compute="_compute_amount",
store=True,
readonly=True,
)
amount_company_currency = fields.Monetary(
string="Amount in Company Currency",
currency_field="company_currency_id",
compute="_compute_amount",
store=True,
readonly=True,
)
currency_id = fields.Many2one(
comodel_name="res.currency",
required=True,
readonly=True,
related="payment_line_ids.currency_id",
)
partner_bank_id = fields.Many2one(
comodel_name="res.partner.bank",
string="Bank Account",
readonly=True,
related="payment_line_ids.partner_bank_id",
check_company=True,
)
date = fields.Date(related="payment_line_ids.date", readonly=True)
communication_type = fields.Selection(
related="payment_line_ids.communication_type", readonly=True
)
communication = fields.Char(required=True, readonly=True)
company_id = fields.Many2one(
comodel_name="res.company",
related="order_id.payment_mode_id.company_id",
store=True,
readonly=True,
)
company_currency_id = fields.Many2one(
comodel_name="res.currency",
related="order_id.payment_mode_id.company_id.currency_id",
readonly=True,
store=True,
)
@api.model
def same_fields_payment_line_and_bank_payment_line(self):
"""
This list of fields is used both to compute the grouping
hashcode and to copy the values from payment line
to bank payment line
The fields must have the same name on the 2 objects
"""
same_fields = [
"currency_id",
"partner_id",
"partner_bank_id",
"date",
"communication_type",
]
return same_fields
@api.depends("payment_line_ids", "payment_line_ids.amount_currency")
def _compute_amount(self):
for bline in self:
amount_currency = sum(bline.mapped("payment_line_ids.amount_currency"))
amount_company_currency = bline.currency_id._convert(
amount_currency,
bline.company_currency_id,
bline.company_id,
bline.date or fields.Date.today(),
)
bline.amount_currency = amount_currency
bline.amount_company_currency = amount_company_currency
@api.model
@api.returns("self")
def create(self, vals):
if vals.get("name", "New") == "New":
vals["name"] = (
self.env["ir.sequence"].next_by_code("bank.payment.line") or "New"
)
return super(BankPaymentLine, self).create(vals)
def move_line_offsetting_account_hashcode(self):
"""
This method is inherited in the module
account_banking_sepa_direct_debit
"""
self.ensure_one()
if self.order_id.payment_mode_id.move_option == "date":
hashcode = fields.Date.to_string(self.date)
else:
hashcode = str(self.id)
return hashcode
def reconcile_payment_lines(self):
for bline in self:
if all([pline.move_line_id for pline in bline.payment_line_ids]):
bline.reconcile()
else:
bline.no_reconcile_hook()
def no_reconcile_hook(self):
"""This method is designed to be inherited if needed"""
return
def reconcile(self):
self.ensure_one()
amlo = self.env["account.move.line"]
transit_mlines = amlo.search([("bank_payment_line_id", "=", self.id)])
assert len(transit_mlines) == 1, "We should have only 1 move"
transit_mline = transit_mlines[0]
assert not transit_mline.reconciled, "Transit move should not be reconciled"
lines_to_rec = transit_mline
for payment_line in self.payment_line_ids:
if not payment_line.move_line_id:
raise UserError(
_(
"Can not reconcile: no move line for "
"payment line %(line)s of partner '%(partner)s'.",
line=payment_line.name,
partner=payment_line.partner_id.name,
)
)
if payment_line.move_line_id.reconciled:
raise UserError(
_(
"Move line '%(line)s' of partner '%(partner)s' has already "
"been reconciled",
line=payment_line.move_line_id.name,
partner=payment_line.partner_id.name,
)
)
if payment_line.move_line_id.account_id != transit_mline.account_id:
raise UserError(
_(
"For partner '%(partner)s', the account of the account "
"move line to pay (%(line1)s) is different from the "
"account of of the transit move line (%(line2)s).",
partner=payment_line.move_line_id.partner_id.name,
line1=payment_line.move_line_id.account_id.code,
line2=transit_mline.account_id.code,
)
)
lines_to_rec += payment_line.move_line_id
lines_to_rec.reconcile()
def unlink(self):
for line in self:
order_state = line.order_id.state
if order_state == "uploaded":
raise UserError(
_(
"Cannot delete a payment order line whose payment order is"
" in state '%(state)s'. You need to cancel it first.",
state=order_state,
)
)
return super(BankPaymentLine, self).unlink()

View File

@@ -0,0 +1,25 @@
# © 2015-2016 Akretion - Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import _, api, models
from odoo.exceptions import ValidationError
class ResBank(models.Model):
_inherit = "res.bank"
@api.constrains("bic")
def check_bic_length(self):
for bank in self:
if bank.bic and len(bank.bic) not in (8, 11):
raise ValidationError(
_(
"A valid BIC contains 8 or 11 characters. The BIC '%(bic)s' "
"contains %(num)d characters, so it is not valid.",
bic=bank.bic,
num=len(bank.bic),
)
)
# starting from v9, on res.partner.bank bank_bic is a related of bank_id.bic

View File

@@ -0,0 +1,2 @@
This module adds several options on Payment Modes, cf Invoicing/Accounting >
Configuration > Management > Payment Modes.

View File

@@ -0,0 +1,30 @@
* Stéphane Bidoul <stephane.bidoul@acsone.eu>
* Alexis de Lattre <alexis.delattre@akretion.com>
* Adrien Peiffer <adrien.peiffer@acsone.eu>
* Stefan Rijnhart
* Laurent Mignon <laurent.mignon@acsone.eu>
* Alexandre Fayolle
* Danimar Ribeiro
* Erwin van der Ploeg
* Raphaël Valyi
* Sandy Carter
* Angel Moya <angel.moya@domatix.com>
* Jose María Alzaga <jose.alzaga@aselcis.com>
* Meyomesse Gilles <meyomesse.gilles@gmail.com>
* `DynApps <https://www.dynapps.be>`_:
* Raf Ven <raf.ven@dynapps.be>
* Andrea Stirpe <a.stirpe@onestein.nl>
* `Jarsa <https://www.jarsa.com.mx>`_:
* Alan Ramos <alan.ramos@jarsa.com.mx>
* `Tecnativa <https://www.tecnativa.com>`_:
* Pedro M. Baeza
* Carlos Dauden
* Carlos Roca
* `Open Source Integrators <https://www.opensourceintegrators.com>`_:
* Ammar Officewala <aofficewala@opensourceintegrators.com>
* Marçal Isern <marsal.isern@qubiq.es>

View File

@@ -0,0 +1 @@
This module adds support for payment orders and debit orders.

View File

@@ -0,0 +1,7 @@
This module depends on:
* account_payment_partner
* base_iban
* document
This modules is part of the OCA/bank-payment suite.

View File

@@ -0,0 +1,7 @@
You can create a Payment order via the menu Invoicing/Accounting > Vendors > Payment Orders and then select the move lines to pay.
You can create a Debit order via the menu Invoicing/Accounting > Customers > Debit Orders and then select the move lines to debit.
This module also adds an action *Add to Payment Order* on supplier invoices and *Add to Debit Order* on customer invoices.
You can print a Payment order via the menu Invoicing/Accounting > Vendors > Payment Orders and then select the payment oder to print.

View File

@@ -0,0 +1 @@
from . import account_payment_order

View File

@@ -0,0 +1,46 @@
# © 2017 Acsone SA/NV (<https://www.acsone.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import api, models
from odoo.tools.misc import formatLang
class AccountPaymentOrderReport(models.AbstractModel):
_name = "report.account_payment_order.print_account_payment_order_main"
_description = "Technical model for printing payment order"
@api.model
def _get_report_values(self, docids, data=None):
AccountPaymentOrderObj = self.env["account.payment.order"]
docs = AccountPaymentOrderObj.browse(docids)
return {
"doc_ids": docids,
"doc_model": "account.payment.order",
"docs": docs,
"data": data,
"env": self.env,
"get_bank_account_name": self.get_bank_account_name,
"formatLang": formatLang,
}
@api.model
def get_bank_account_name(self, partner_bank):
"""
:param partner_bank:
:return:
"""
if partner_bank:
name = ""
if partner_bank.bank_name:
name = "%s: " % partner_bank.bank_id.name
if partner_bank.acc_number:
name = "{} {}".format(name, partner_bank.acc_number)
if partner_bank.bank_bic:
name = "%s - " % (name)
if partner_bank.bank_bic:
name = "{} BIC {}".format(name, partner_bank.bank_bic)
return name
else:
return False

View File

@@ -0,0 +1,158 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2017 ACSONE SA/NV
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<template id="print_account_payment_order_document">
<t
t-set="doc"
t-value="doc.with_context({'lang': doc.generated_user_id and doc.generated_user_id.lang or user.lang})"
/>
<t t-call="web.external_layout">
<div class="page">
<div class="oe_structure" />
<div class="row">
<div class="col-4 offset-8">
<span
t-raw="'%s &lt;br&gt;' % doc.journal_id.bank_id.name if doc.journal_id.bank_id.name else ''"
/>
<span
t-raw="'%s &lt;br&gt;' % doc.journal_id.bank_id.street if doc.journal_id.bank_id.street else ''"
/>
<span
t-raw="'%s &lt;br&gt;' % doc.journal_id.bank_id.street2 if doc.journal_id.bank_id.street2 else ''"
/>
<span
t-raw="'%s &lt;br&gt;' % doc.journal_id.bank_id.zip if doc.journal_id.bank_id.zip else ''"
/>
<span
t-raw="'%s &lt;br&gt;' % doc.journal_id.bank_id.city if doc.journal_id.bank_id.city else ''"
/>
<span
t-raw="'%s &lt;br&gt;' % doc.journal_id.bank_id.state.name if doc.journal_id.bank_id.state.name else ''"
/>
<span
t-raw="'%s &lt;br&gt;' % doc.journal_id.bank_id.country.name if doc.journal_id.bank_id.country.name else ''"
/>
</div>
</div>
<t t-if="doc.payment_type == 'inbound'">
<h2>Debit Order</h2>
</t>
<t t-else="">
<h2>Payment Order</h2>
</t>
<div class="row mt32 mb32">
<div t-if="doc.payment_mode_id.name" class="col-2">
<strong>Payment Type:</strong>
<p t-field="doc.payment_mode_id.name" />
</div>
<div t-if="doc.name" class="col-2">
<strong>Reference</strong>
<p t-field="doc.name" />
</div>
<div t-if="doc.company_partner_bank_id.bank_id.id" class="col-2">
<strong>Used Account:</strong>
<p>
<span
t-esc="get_bank_account_name(doc.company_partner_bank_id)"
/>
</p>
</div>
<div t-if="doc.date_prefered" class="col-2">
<strong>Execution:</strong>
<p t-field="doc.date_prefered" />
</div>
<div t-if="doc.company_id.currency_id.name" class="col-2">
<strong>Company Currency:</strong>
<p t-field="doc.company_id.currency_id.name" />
</div>
</div>
<table class="table table-condensed">
<thead>
<tr>
<th>Partner</th>
<th class="text-center">Bank Account</th>
<th class="text-center">Invoice Ref</th>
<th class="text-center">Value Date</th>
<th class="text-right">Amount</th>
<th class="text-right">Currency</th>
</tr>
</thead>
<tbody>
<!-- Total amount on lines
see _get_amount_total v8
-->
<t t-set="total_amount" t-value="0" />
<tr t-foreach="doc.payment_line_ids" t-as="line">
<!-- compute total amount -->
<t
t-set="total_amount"
t-value="total_amount+line.amount_currency"
/>
<td>
<span t-field="line.partner_id.name" />
</td>
<td class="text-center">
<span
t-esc="get_bank_account_name(line.partner_bank_id)"
/>
</td>
<td class="text-center">
<t
t-if="line.move_line_id.move_id and 'in_' in line.move_line_id.move_id.move_type"
>
<span t-field="line.move_line_id.move_id.ref" />
</t>
<t t-else="">
<span t-esc="line.move_line_id.move_id.name" />
</t>
</td>
<td class="text-center">
<span t-field="line.date" />
</td>
<td class="text-right">
<span t-field="line.amount_currency" />
</td>
<td class="text-right">
<span t-field="line.amount_company_currency" />
</td>
</tr>
</tbody>
</table>
<div class="row pull-right">
<div class="col-4">
<table class="table table-condensed">
<tr class="border-black">
<td>
<strong>Total</strong>
</td>
<td class="text-right">
<span
t-esc="formatLang(env, total_amount, currency_obj=doc.company_currency_id)"
/>
</td>
</tr>
<tr>
<td>Total (Currency)</td>
<td class="text-right">
<span t-field="doc.total_company_currency" />
</td>
</tr>
</table>
</div>
</div>
<div class="oe_structure" />
</div>
</t>
</template>
<template id="print_account_payment_order_main">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t
t-call="account_payment_order.print_account_payment_order_document"
t-lang="doc.generated_user_id.lang"
/>
</t>
</t>
</template>
</odoo>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2017 ACSONE SA/NV
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<!-- QWeb Report -->
<record id="action_print_payment_order" model="ir.actions.report">
<field name="name">Payment Order</field>
<field name="model">account.payment.order</field>
<field name="report_type">qweb-pdf</field>
<field
name="report_name"
>account_payment_order.print_account_payment_order_main</field>
<field
name="report_file"
>account_payment_order.print_account_payment_order_main</field>
<field
name="binding_model_id"
ref="account_payment_order.model_account_payment_order"
/>
<field name="binding_type">report</field>
</record>
</odoo>

View File

@@ -0,0 +1,8 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_account_payment_order,Full access on account.payment.order to Payment Manager,model_account_payment_order,group_account_payment,1,1,1,1
access_account_payment_line,Full access on account.payment.line to Payment Manager,model_account_payment_line,group_account_payment,1,1,1,1
access_bank_payment_line,Full access on bank.payment.line to Payment Manager,model_bank_payment_line,group_account_payment,1,1,1,1
base.access_res_partner_bank_group_partner_manager,Full access on res.partner.bank to Account Payment group,base.model_res_partner_bank,group_account_payment,1,1,1,1
base.access_res_bank_group_partner_manager,Full access on res.bank to Account Payment group,base.model_res_bank,group_account_payment,1,1,1,1
access_account_payment_line_create,access_account_payment_line_create,model_account_payment_line_create,group_account_payment,1,1,1,1
access_account_invoice_payment_line_multi,access_account_invoice_payment_line_multi,model_account_invoice_payment_line_multi,group_account_payment,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_account_payment_order Full access on account.payment.order to Payment Manager model_account_payment_order group_account_payment 1 1 1 1
3 access_account_payment_line Full access on account.payment.line to Payment Manager model_account_payment_line group_account_payment 1 1 1 1
4 access_bank_payment_line Full access on bank.payment.line to Payment Manager model_bank_payment_line group_account_payment 1 1 1 1
5 base.access_res_partner_bank_group_partner_manager Full access on res.partner.bank to Account Payment group base.model_res_partner_bank group_account_payment 1 1 1 1
6 base.access_res_bank_group_partner_manager Full access on res.bank to Account Payment group base.model_res_bank group_account_payment 1 1 1 1
7 access_account_payment_line_create access_account_payment_line_create model_account_payment_line_create group_account_payment 1 1 1 1
8 access_account_invoice_payment_line_multi access_account_invoice_payment_line_multi model_account_invoice_payment_line_multi group_account_payment 1 1 1 1

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<data noupdate="0">
<record id="group_account_payment" model="res.groups">
<field name="name">Accounting / Payments</field>
<field
name="users"
eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"
/>
<field name="category_id" ref="base.module_category_usability" />
</record>
</data>
<data noupdate="1">
<record id="account_payment_order_company_rule" model="ir.rule">
<field name="name">Payment order multi-company rule</field>
<field name="model_id" ref="model_account_payment_order" />
<field
name="domain_force"
>['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="account_payment_line_company_rule" model="ir.rule">
<field name="name">Payment line multi-company rule</field>
<field name="model_id" ref="model_account_payment_line" />
<field
name="domain_force"
>['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="bank_payment_line_company_rule" model="ir.rule">
<field name="name">Bank payment line multi-company rule</field>
<field name="model_id" ref="model_bank_payment_line" />
<field
name="domain_force"
>['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
</data>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,478 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
<title>Account Payment Order</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="account-payment-order">
<h1 class="title">Account Payment Order</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/bank-payment/tree/14.0/account_payment_order"><img alt="OCA/bank-payment" src="https://img.shields.io/badge/github-OCA%2Fbank--payment-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/bank-payment-14-0/bank-payment-14-0-account_payment_order"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/173/14.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>This module adds support for payment orders and debit orders.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#installation" id="id1">Installation</a></li>
<li><a class="reference internal" href="#configuration" id="id2">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="id3">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id4">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id5">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id6">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id7">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id8">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="installation">
<h1><a class="toc-backref" href="#id1">Installation</a></h1>
<p>This module depends on:</p>
<ul class="simple">
<li>account_payment_partner</li>
<li>base_iban</li>
<li>document</li>
</ul>
<p>This modules is part of the OCA/bank-payment suite.</p>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#id2">Configuration</a></h1>
<p>This module adds several options on Payment Modes, cf Invoicing/Accounting &gt;
Configuration &gt; Management &gt; Payment Modes.</p>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id3">Usage</a></h1>
<p>You can create a Payment order via the menu Invoicing/Accounting &gt; Vendors &gt; Payment Orders and then select the move lines to pay.</p>
<p>You can create a Debit order via the menu Invoicing/Accounting &gt; Customers &gt; Debit Orders and then select the move lines to debit.</p>
<p>This module also adds an action <em>Add to Payment Order</em> on supplier invoices and <em>Add to Debit Order</em> on customer invoices.</p>
<p>You can print a Payment order via the menu Invoicing/Accounting &gt; Vendors &gt; Payment Orders and then select the payment oder to print.</p>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id4">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/bank-payment/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/OCA/bank-payment/issues/new?body=module:%20account_payment_order%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id5">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id6">Authors</a></h2>
<ul class="simple">
<li>ACSONE SA/NV</li>
<li>Therp BV</li>
<li>Tecnativa</li>
<li>Akretion</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id7">Contributors</a></h2>
<ul class="simple">
<li>Stéphane Bidoul &lt;<a class="reference external" href="mailto:stephane.bidoul&#64;acsone.eu">stephane.bidoul&#64;acsone.eu</a>&gt;</li>
<li>Alexis de Lattre &lt;<a class="reference external" href="mailto:alexis.delattre&#64;akretion.com">alexis.delattre&#64;akretion.com</a>&gt;</li>
<li>Adrien Peiffer &lt;<a class="reference external" href="mailto:adrien.peiffer&#64;acsone.eu">adrien.peiffer&#64;acsone.eu</a>&gt;</li>
<li>Stefan Rijnhart</li>
<li>Laurent Mignon &lt;<a class="reference external" href="mailto:laurent.mignon&#64;acsone.eu">laurent.mignon&#64;acsone.eu</a>&gt;</li>
<li>Alexandre Fayolle</li>
<li>Danimar Ribeiro</li>
<li>Erwin van der Ploeg</li>
<li>Raphaël Valyi</li>
<li>Sandy Carter</li>
<li>Angel Moya &lt;<a class="reference external" href="mailto:angel.moya&#64;domatix.com">angel.moya&#64;domatix.com</a>&gt;</li>
<li>Jose María Alzaga &lt;<a class="reference external" href="mailto:jose.alzaga&#64;aselcis.com">jose.alzaga&#64;aselcis.com</a>&gt;</li>
<li>Meyomesse Gilles &lt;<a class="reference external" href="mailto:meyomesse.gilles&#64;gmail.com">meyomesse.gilles&#64;gmail.com</a>&gt;</li>
<li><a class="reference external" href="https://www.dynapps.be">DynApps</a>:<ul>
<li>Raf Ven &lt;<a class="reference external" href="mailto:raf.ven&#64;dynapps.be">raf.ven&#64;dynapps.be</a>&gt;</li>
</ul>
</li>
<li>Andrea Stirpe &lt;<a class="reference external" href="mailto:a.stirpe&#64;onestein.nl">a.stirpe&#64;onestein.nl</a>&gt;</li>
<li><a class="reference external" href="https://www.jarsa.com.mx">Jarsa</a>:<ul>
<li>Alan Ramos &lt;<a class="reference external" href="mailto:alan.ramos&#64;jarsa.com.mx">alan.ramos&#64;jarsa.com.mx</a>&gt;</li>
</ul>
</li>
<li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:<ul>
<li>Pedro M. Baeza</li>
<li>Carlos Dauden</li>
<li>Carlos Roca</li>
</ul>
</li>
<li><a class="reference external" href="https://www.opensourceintegrators.com">Open Source Integrators</a>:<ul>
<li>Ammar Officewala &lt;<a class="reference external" href="mailto:aofficewala&#64;opensourceintegrators.com">aofficewala&#64;opensourceintegrators.com</a>&gt;</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id8">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>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.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/bank-payment/tree/14.0/account_payment_order">OCA/bank-payment</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,5 @@
from . import test_payment_mode
from . import test_bank
from . import test_payment_order_inbound
from . import test_payment_order_outbound
from . import test_account_payment

View File

@@ -0,0 +1,167 @@
# Copyright 2019 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from unittest.mock import patch
from odoo.addons.account.models.account_payment_method import AccountPaymentMethod
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
class TestAccountPayment(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
Method_get_payment_method_information = (
AccountPaymentMethod._get_payment_method_information
)
def _get_payment_method_information(self):
res = Method_get_payment_method_information(self)
res["IN"] = {"mode": "multi", "domain": [("type", "=", "bank")]}
res["IN2"] = {"mode": "multi", "domain": [("type", "=", "bank")]}
res["OUT"] = {"mode": "multi", "domain": [("type", "=", "bank")]}
return res
cls.company = cls.company_data["company"]
cls.env.user.company_ids += cls.company
# MODELS
cls.account_payment_model = cls.env["account.payment"]
cls.account_journal_model = cls.env["account.journal"]
cls.payment_method_model = cls.env["account.payment.method"]
# INSTANCES
# Payment methods
with patch.object(
AccountPaymentMethod,
"_get_payment_method_information",
_get_payment_method_information,
):
cls.inbound_payment_method_01 = cls.payment_method_model.create(
{
"name": "inbound",
"code": "IN",
"payment_type": "inbound",
}
)
cls.inbound_payment_method_02 = cls.inbound_payment_method_01.copy(
{
"name": "inbound 2",
"code": "IN2",
"payment_type": "inbound",
}
)
cls.outbound_payment_method_01 = cls.payment_method_model.create(
{
"name": "outbound",
"code": "OUT",
"payment_type": "outbound",
}
)
# Journals
cls.manual_in = cls.env.ref("account.account_payment_method_manual_in")
cls.manual_out = cls.env.ref("account.account_payment_method_manual_out")
cls.bank_journal = cls.company_data["default_journal_bank"]
def test_account_payment_01(self):
self.assertFalse(self.inbound_payment_method_01.payment_order_only)
self.assertFalse(self.inbound_payment_method_02.payment_order_only)
self.assertFalse(self.bank_journal.inbound_payment_order_only)
self.inbound_payment_method_01.payment_order_only = True
self.assertTrue(self.inbound_payment_method_01.payment_order_only)
self.assertFalse(self.inbound_payment_method_02.payment_order_only)
self.assertFalse(self.bank_journal.inbound_payment_order_only)
self.inbound_payment_method_02.payment_order_only = True
self.assertTrue(self.inbound_payment_method_01.payment_order_only)
self.assertTrue(self.inbound_payment_method_02.payment_order_only)
self.manual_in.payment_order_only = True
self.assertTrue(self.bank_journal.inbound_payment_order_only)
def test_account_payment_02(self):
self.assertFalse(self.outbound_payment_method_01.payment_order_only)
self.assertFalse(self.bank_journal.outbound_payment_order_only)
self.outbound_payment_method_01.payment_order_only = True
self.assertTrue(self.outbound_payment_method_01.payment_order_only)
payment_method_id = (
self.bank_journal.outbound_payment_method_line_ids.payment_method_id
)
payment_method_id.payment_order_only = True
self.assertTrue(self.bank_journal.outbound_payment_order_only)
def test_account_payment_03(self):
self.assertFalse(self.inbound_payment_method_01.payment_order_only)
self.assertFalse(self.inbound_payment_method_02.payment_order_only)
self.assertFalse(self.bank_journal.inbound_payment_order_only)
new_account_payment = self.account_payment_model.with_context(
default_company_id=self.company.id
).new(
{
"journal_id": self.bank_journal.id,
"payment_type": "inbound",
"amount": 1,
"company_id": self.company.id,
}
)
# check journals
journals = new_account_payment._get_default_journal()
self.assertIn(self.bank_journal, journals)
# check payment methods
payment_methods = (
new_account_payment.available_payment_method_line_ids.filtered(
lambda x: x.payment_type == "inbound"
)
.mapped("payment_method_id")
.ids
)
self.assertIn(self.inbound_payment_method_01.id, payment_methods)
self.assertIn(self.inbound_payment_method_02.id, payment_methods)
# Set one payment method of the bank journal 'payment order only'
self.inbound_payment_method_01.payment_order_only = True
# check journals
journals = new_account_payment._get_default_journal()
self.assertIn(self.bank_journal, journals)
# check payment methods
new_account_payment2 = self.account_payment_model.with_context(
default_company_id=self.company.id
).new(
{
"journal_id": self.bank_journal.id,
"payment_type": "inbound",
"amount": 1,
"company_id": self.company.id,
}
)
payment_methods = new_account_payment2.available_payment_method_line_ids.mapped(
"payment_method_id"
).ids
self.assertNotIn(self.inbound_payment_method_01.id, payment_methods)
self.assertIn(self.inbound_payment_method_02.id, payment_methods)
# Set all payment methods of the bank journal 'payment order only'
self.inbound_payment_method_02.payment_order_only = True
self.assertTrue(self.inbound_payment_method_01.payment_order_only)
self.assertTrue(self.inbound_payment_method_02.payment_order_only)
self.manual_in.payment_order_only = True
self.assertTrue(self.bank_journal.inbound_payment_order_only)
# check journals
journals = new_account_payment._get_default_journal()
self.assertNotIn(self.bank_journal, journals)
# check payment methods
new_account_payment3 = self.account_payment_model.with_context(
default_company_id=self.company.id
).new(
{
"journal_id": self.bank_journal.id,
"payment_type": "inbound",
"amount": 1,
"company_id": self.company.id,
}
)
payment_methods = new_account_payment3.available_payment_method_line_ids.mapped(
"payment_method_id"
).ids
self.assertNotIn(self.inbound_payment_method_01.id, payment_methods)
self.assertNotIn(self.inbound_payment_method_02.id, payment_methods)

View File

@@ -0,0 +1,13 @@
# © 2017 Creu Blanca
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo.exceptions import ValidationError
from odoo.tests.common import TransactionCase
class TestBank(TransactionCase):
def test_bank(self):
bank = self.env["res.bank"].search([], limit=1)
self.assertTrue(bank)
with self.assertRaises(ValidationError):
bank.bic = "TEST"

View File

@@ -0,0 +1,99 @@
# © 2017 Creu Blanca
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from unittest.mock import patch
from odoo.tests.common import TransactionCase
from odoo.addons.account.models.account_payment_method import AccountPaymentMethod
class TestPaymentMode(TransactionCase):
def setUp(self):
super(TestPaymentMode, self).setUp()
Method_get_payment_method_information = (
AccountPaymentMethod._get_payment_method_information
)
def _get_payment_method_information(self):
res = Method_get_payment_method_information(self)
res["IN"] = {"mode": "multi", "domain": [("type", "=", "bank")]}
res["IN2"] = {"mode": "multi", "domain": [("type", "=", "bank")]}
res["electronic_out"] = {"mode": "multi", "domain": [("type", "=", "bank")]}
return res
# Company
self.company = self.env.ref("base.main_company")
self.journal_c1 = self.env["account.journal"].create(
{
"name": "Journal 1",
"code": "J1",
"type": "bank",
"company_id": self.company.id,
}
)
self.account = self.env["account.account"].search(
[("reconcile", "=", True), ("company_id", "=", self.company.id)], limit=1
)
self.manual_out = self.env.ref("account.account_payment_method_manual_out")
self.manual_in = self.env.ref("account.account_payment_method_manual_in")
with patch.object(
AccountPaymentMethod,
"_get_payment_method_information",
_get_payment_method_information,
):
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",
"payment_method_id": self.manual_out.id,
"company_id": self.company.id,
"fixed_journal_id": self.journal_c1.id,
"variable_journal_ids": [(6, 0, [self.journal_c1.id])],
}
)
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_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,122 @@
# Copyright 2017 Camptocamp SA
# Copyright 2017 Creu Blanca
# Copyright 2019 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from datetime import date, timedelta
from odoo.exceptions import UserError, ValidationError
from odoo.tests.common import Form
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
class TestPaymentOrderInboundBase(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.company = cls.company_data["company"]
cls.env.user.company_id = cls.company.id
cls.partner = cls.env["res.partner"].create(
{
"name": "Test Partner",
}
)
cls.inbound_mode = cls.env["account.payment.mode"].create(
{
"name": "Test Direct Debit of customers",
"bank_account_link": "variable",
"payment_method_id": cls.env.ref(
"account.account_payment_method_manual_in"
).id,
"company_id": cls.company.id,
}
)
cls.invoice_line_account = cls.company_data["default_account_revenue"]
cls.journal = cls.company_data["default_journal_bank"]
cls.inbound_mode.variable_journal_ids = cls.journal
# Make sure no others orders are present
cls.domain = [
("state", "=", "draft"),
("payment_type", "=", "inbound"),
("company_id", "=", cls.env.user.company_id.id),
]
cls.payment_order_obj = cls.env["account.payment.order"]
cls.payment_order_obj.search(cls.domain).unlink()
# Create payment order
cls.inbound_order = cls.env["account.payment.order"].create(
{
"payment_type": "inbound",
"payment_mode_id": cls.inbound_mode.id,
"journal_id": cls.journal.id,
}
)
# Open invoice
cls.invoice = cls._create_customer_invoice(cls)
cls.invoice.action_post()
# Add to payment order using the wizard
cls.env["account.invoice.payment.line.multi"].with_context(
active_model="account.move", active_ids=cls.invoice.ids
).create({}).run()
def _create_customer_invoice(self):
with Form(
self.env["account.move"].with_context(default_move_type="out_invoice")
) as invoice_form:
invoice_form.partner_id = self.partner
with invoice_form.invoice_line_ids.new() as invoice_line_form:
invoice_line_form.product_id = self.env.ref("product.product_product_4")
invoice_line_form.name = "product that cost 100"
invoice_line_form.quantity = 1
invoice_line_form.price_unit = 100.0
invoice_line_form.account_id = self.invoice_line_account
invoice_line_form.tax_ids.clear()
invoice = invoice_form.save()
invoice_form = Form(invoice)
invoice_form.payment_mode_id = self.inbound_mode
return invoice_form.save()
class TestPaymentOrderInbound(TestPaymentOrderInboundBase):
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):
payment_order = self.inbound_order
self.assertEqual(len(payment_order.ids), 1)
payment_order.write({"journal_id": self.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_uploaded_cancel()
self.assertEqual(payment_order.state, "cancel")
payment_order.cancel2draft()
payment_order.unlink()
self.assertEqual(len(self.payment_order_obj.search(self.domain)), 0)

View File

@@ -0,0 +1,290 @@
# © 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 import fields
from odoo.exceptions import UserError, ValidationError
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
class TestPaymentOrderOutbound(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.company = cls.company_data["company"]
cls.env.user.company_id = cls.company.id
cls.partner = cls.env["res.partner"].create(
{
"name": "Test Partner",
}
)
cls.invoice_line_account = cls.env["account.account"].create(
{
"name": "Test account",
"code": "TEST1",
"user_type_id": cls.env.ref("account.data_account_type_expenses").id,
}
)
cls.mode = cls.env["account.payment.mode"].create(
{
"name": "Test Credit Transfer to Suppliers",
"company_id": cls.company.id,
"bank_account_link": "variable",
"payment_method_id": cls.env.ref(
"account.account_payment_method_manual_out"
).id,
}
)
cls.creation_mode = cls.env["account.payment.mode"].create(
{
"name": "Test Direct Debit of suppliers from Société Générale",
"company_id": cls.company.id,
"bank_account_link": "variable",
"payment_method_id": cls.env.ref(
"account.account_payment_method_manual_out"
).id,
}
)
cls.invoice = cls._create_supplier_invoice(cls, "F1242")
cls.invoice_02 = cls._create_supplier_invoice(cls, "F1243")
cls.bank_journal = cls.company_data["default_journal_bank"]
# Make sure no other payment orders are in the DB
cls.domain = [
("state", "=", "draft"),
("payment_type", "=", "outbound"),
("company_id", "=", cls.env.user.company_id.id),
]
cls.env["account.payment.order"].search(cls.domain).unlink()
def _create_supplier_invoice(self, ref):
invoice = self.env["account.move"].create(
{
"partner_id": self.partner.id,
"move_type": "in_invoice",
"ref": ref,
"payment_mode_id": self.mode.id,
"invoice_date": fields.Date.today(),
"invoice_line_ids": [
(
0,
None,
{
"product_id": self.env.ref("product.product_product_4").id,
"quantity": 1.0,
"price_unit": 100.0,
"name": "product that cost 100",
"account_id": self.invoice_line_account.id,
},
)
],
}
)
return invoice
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_post()
self.order_creation("fixed")
def order_creation(self, date_prefered):
# Open invoice
self.invoice.action_post()
order_vals = {
"payment_type": "outbound",
"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, 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({"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_created_due = (
self.env["account.payment.line.create"]
.with_context(active_model="account.payment.order", active_id=order.id)
.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()
self.assertEqual(order.move_ids[0].date, order.bank_line_ids[0].date)
self.assertEqual(order.state, "uploaded")
def test_cancel_payment_order(self):
# Open invoice
self.invoice.action_post()
# Add to payment order using the wizard
self.env["account.invoice.payment.line.multi"].with_context(
active_model="account.move", active_ids=self.invoice.ids
).create({}).run()
payment_order = self.env["account.payment.order"].search(self.domain)
self.assertEqual(len(payment_order), 1)
payment_order.write({"journal_id": self.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_uploaded_cancel()
self.assertEqual(payment_order.state, "cancel")
payment_order.cancel2draft()
payment_order.unlink()
self.assertEqual(len(self.env["account.payment.order"].search(self.domain)), 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.bank_journal.id,
}
)
with self.assertRaises(ValidationError):
outbound_order.date_scheduled = date.today() - timedelta(days=1)
def test_manual_line_and_manual_date(self):
# Create payment order
outbound_order = self.env["account.payment.order"].create(
{
"date_prefered": "due",
"payment_type": "outbound",
"payment_mode_id": self.mode.id,
"journal_id": self.journal.id,
"description": "order with manual line",
}
)
self.assertEqual(len(outbound_order.payment_line_ids), 0)
# Create a manual payment order line with custom date
vals = {
"order_id": outbound_order.id,
"partner_id": self.env.ref("base.res_partner_4").id,
"partner_bank_id": self.env.ref("base.res_partner_4").bank_ids[0].id,
"communication": "manual line and manual date",
"currency_id": outbound_order.payment_mode_id.company_id.currency_id.id,
"amount_currency": 192.38,
"date": date.today() + timedelta(days=8),
}
self.env["account.payment.line"].create(vals)
self.assertEqual(len(outbound_order.payment_line_ids), 1)
self.assertEqual(
outbound_order.payment_line_ids[0].date, date.today() + timedelta(days=8)
)
# Create a manual payment order line with normal date
vals = {
"order_id": outbound_order.id,
"partner_id": self.env.ref("base.res_partner_4").id,
"partner_bank_id": self.env.ref("base.res_partner_4").bank_ids[0].id,
"communication": "manual line",
"currency_id": outbound_order.payment_mode_id.company_id.currency_id.id,
"amount_currency": 200.38,
}
self.env["account.payment.line"].create(vals)
self.assertEqual(len(outbound_order.payment_line_ids), 2)
self.assertEqual(outbound_order.payment_line_ids[1].date, False)
# Open payment order
self.assertEqual(len(outbound_order.bank_line_ids), 0)
outbound_order.draft2open()
self.assertEqual(outbound_order.bank_line_count, 2)
self.assertEqual(
outbound_order.payment_line_ids[0].date,
outbound_order.payment_line_ids[0].bank_line_id.date,
)
self.assertEqual(outbound_order.payment_line_ids[1].date, date.today())
self.assertEqual(outbound_order.payment_line_ids[1].bank_line_id.date, date.today())
# Generate and upload
outbound_order.open2generated()
outbound_order.generated2uploaded()
self.assertEqual(outbound_order.state, "uploaded")
with self.assertRaises(UserError):
outbound_order.unlink()
bank_line = outbound_order.bank_line_ids
with self.assertRaises(UserError):
bank_line.unlink()
outbound_order.action_done_cancel()
self.assertEqual(outbound_order.state, "cancel")
outbound_order.cancel2draft()
outbound_order.unlink()
self.assertEqual(
len(
self.env["account.payment.order"].search(
[("description", "=", "order with manual line")]
)
),
0,
)

View File

@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
© 2016 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>)
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_move_form" model="ir.ui.view">
<field name="name">account_payment_order.view_move_form</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account_payment_partner.view_move_form" />
<field name="arch" type="xml">
<button id="account_invoice_payment_btn" position="after">
<!-- For customer refunds:
'Add to Direct Debit Order' will deduct the refund from a customer invoice
We could also need a button 'Add to Payment Order' to reimburse
a customer via wire transfer... but I prefer to keep things
simple ; to do that, the user should manually create a payment order
and select the move lines -->
<button
name="create_account_payment_line"
type="object"
string="Add to Debit Order"
groups="account_payment_order.group_account_payment"
attrs="{'invisible': ['|', '|',
('payment_order_ok', '=', False),
('state', '!=', 'posted'),
('move_type', 'not in', ('out_invoice', 'out_refund'))
]}"
/>
<button
name="create_account_payment_line"
type="object"
string="Add to Payment Order"
groups="account_payment_order.group_account_payment"
attrs="{'invisible': ['|', '|',
('payment_order_ok', '=', False),
('state', '!=', 'posted'),
('move_type', 'not in', ('in_invoice', 'in_refund'))
]}"
/>
</button>
<field name="payment_mode_id" position="after">
<field name="payment_order_ok" invisible="1" />
</field>
<field name="payment_reference" position="before">
<field
name="reference_type"
attrs="{'readonly':[('state','!=','draft')],
'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund'))],
'required': [('move_type', 'in', ('out_invoice', 'out_refund'))]}"
/>
</field>
</field>
</record>
<record
id="account_invoice_create_account_payment_line_action"
model="ir.actions.act_window"
>
<field name="name">Add to Payment/Debit Order</field>
<field name="res_model">account.invoice.payment.line.multi</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="binding_model_id" ref="account.model_account_move" />
</record>
<record id="view_invoice_tree" model="ir.ui.view">
<field name="name">account_payment_order.view_invoice_tree</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_invoice_tree" />
<field name="arch" type="xml">
<button name="action_register_payment" position="after">
<button
name="%(account_invoice_create_account_payment_line_action)d"
type="action"
string="Add to Payment/Debit Order"
groups="account_payment_order.group_account_payment"
invisible="context.get('default_move_type') not in ('out_invoice', 'out_refund', 'in_invoice', 'in_refund')"
/>
</button>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
© 2016 Akretion (https://www.akretion.com/)
@author Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_move_line_form" model="ir.ui.view">
<field name="name">account_payment_order.move_line_form</field>
<field name="model">account.move.line</field>
<field name="inherit_id" ref="account_payment_partner.view_move_line_form" />
<field name="arch" type="xml">
<group name="payments" position="inside">
<field
name="partner_bank_id"
domain="[('partner_id', '=', partner_id), '|', ('company_id', '=', company_id), ('company_id', '=', False)]"
/>
<field name="bank_payment_line_id" />
</group>
</field>
</record>
<record id="view_move_line_tree" model="ir.ui.view">
<field name="name">account_payment_order.add.move_line_tree</field>
<field name="model">account.move.line</field>
<field name="inherit_id" ref="account.view_move_line_tree" />
<field name="mode">primary</field>
<field name="priority">99</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='amount_currency']" position="after">
<field name="balance" readonly="1" />
<field name="amount_residual_currency" readonly="1" />
<field name="amount_residual" readonly="1" />
</xpath>
<xpath expr="//field[@name='debit']" position="replace" />
<xpath expr="//field[@name='credit']" position="replace" />
<xpath expr="//field[@name='tax_ids']" position="replace" />
</field>
</record>
</odoo>

View File

@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="account_payment_line_form" model="ir.ui.view">
<field name="name">account.payment.line.form</field>
<field name="model">account.payment.line</field>
<field name="arch" type="xml">
<form string="Payment Lines">
<group name="main" col="2">
<group name="left">
<field
name="order_id"
invisible="not context.get('account_payment_line_main_view')"
/>
<field name="name" />
<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" />
<field
name="partner_bank_id"
context="{'default_partner_id': partner_id}"
domain="[('partner_id', '=', partner_id), '|', ('company_id', '=', company_id), ('company_id', '=', False)]"
attrs="{'required': [('bank_account_required', '=', True)]}"
/>
<field name="bank_account_required" invisible="1" />
<field name="communication_type" />
<field name="communication" required="1" />
</group>
<group name="right">
<field
name="company_id"
widget="selection"
groups="base.group_multi_company"
/>
<field name="amount_company_currency" />
<field name="company_currency_id" invisible="1" />
<field name="bank_line_id" />
<field name="payment_type" invisible="1" />
</group>
</group>
</form>
</field>
</record>
<record id="account_payment_line_tree" model="ir.ui.view">
<field name="name">account.payment.line.tree</field>
<field name="model">account.payment.line</field>
<field name="arch" type="xml">
<tree>
<field
name="order_id"
invisible="not context.get('account_payment_line_main_view')"
/>
<field name="partner_id" />
<field name="communication" />
<field name="partner_bank_id" />
<field name="move_line_id" optional="hide" />
<field name="ml_maturity_date" optional="show" />
<field name="date" />
<field name="amount_currency" string="Amount" />
<field name="currency_id" invisible="1" />
<field name="name" optional="show" />
<field
name="amount_company_currency"
sum="Total in Company Currency"
invisible="1"
/>
<field name="payment_type" invisible="1" />
</tree>
</field>
</record>
<record id="account_payment_line_action" model="ir.actions.act_window">
<field name="name">Payment Lines</field>
<field name="res_model">account.payment.line</field>
<field name="view_mode">tree,form</field>
<field name="context">{'account_payment_line_main_view': True}</field>
</record>
</odoo>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2019 ACSONE SA/NV
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="account_payment_method_form_view">
<field
name="name"
>account.payment.method.form (in account_payment_order)</field>
<field name="model">account.payment.method</field>
<field
name="inherit_id"
ref="account_payment_mode.account_payment_method_form"
/>
<field name="arch" type="xml">
<field name="active" position="before">
<field name="payment_order_only" />
</field>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="account_payment_mode_form" model="ir.ui.view">
<field name="name">account_payment_order.account.payment.mode.form</field>
<field name="model">account.payment.mode</field>
<field name="inherit_id" ref="account_payment_mode.account_payment_mode_form" />
<field name="arch" type="xml">
<field name="payment_type" position="after">
<field name="payment_order_ok" />
</field>
<group name="main" position="after">
<group
name="payment_order_options"
string="Options for Payment Orders"
attrs="{'invisible': [('payment_order_ok', '=', False)]}"
>
<field
name="no_debit_before_maturity"
attrs="{'invisible': ['|', ('payment_type', '!=', 'inbound'), ('payment_order_ok', '!=', True)]}"
/>
<field name="default_date_prefered" />
<field name="group_lines" />
</group>
<group
name="payment_order_create_defaults"
string="Select Move Lines to Pay - Default Values"
attrs="{'invisible': [('payment_order_ok', '=', False)]}"
>
<field name="default_journal_ids" widget="many2many_tags" />
<field name="default_payment_mode" />
<field name="default_target_move" widget="radio" />
<field name="default_invoice" />
<field name="default_date_type" />
</group>
<group
name="accounting-config"
string="Accounting Entries Options"
attrs="{'invisible': [('payment_order_ok', '=', False)]}"
>
<field name="generate_move" />
<field
name="move_option"
attrs="{'invisible': [('generate_move', '=', False)], 'required': [('generate_move', '=', True)]}"
/>
<field
name="post_move"
attrs="{'invisible': [('generate_move', '=', False)]}"
/>
</group>
</group>
</field>
</record>
<record id="account_payment_mode_tree" model="ir.ui.view">
<field name="name">account_payment_order.account.payment.mode.tree</field>
<field name="model">account.payment.mode</field>
<field name="inherit_id" ref="account_payment_mode.account_payment_mode_tree" />
<field name="arch" type="xml">
<field name="payment_type" position="after">
<field name="payment_order_ok" />
</field>
</field>
</record>
<record id="account_payment_mode_search" model="ir.ui.view">
<field name="name">account_payment_order.account.payment.mode.search</field>
<field name="model">account.payment.mode</field>
<field
name="inherit_id"
ref="account_payment_mode.account_payment_mode_search"
/>
<field name="arch" type="xml">
<filter name="outbound" position="after">
<filter
name="payment_order_ok"
string="Selectable in Payment Orders"
domain="[('payment_order_ok', '=', 1)]"
/>
</filter>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,277 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="account_payment_order_form" model="ir.ui.view">
<field name="name">account.payment.order.form</field>
<field name="model">account.payment.order</field>
<field name="arch" type="xml">
<form string="Payment Order">
<header>
<button
name="%(account_payment_line_create_action)d"
type="action"
string="Create Payment Lines from Journal Items"
states="draft"
class="oe_highlight"
/>
<button
name="draft2open"
type="object"
states="draft"
string="Confirm Payments"
class="oe_highlight"
/>
<button
name="open2generated"
type="object"
states="open"
string="Generate Payment File"
class="oe_highlight"
/>
<button
name="generated2uploaded"
type="object"
states="generated"
string="File Successfully Uploaded"
class="oe_highlight"
/>
<button
name="cancel2draft"
type="object"
states="cancel"
string="Back to Draft"
/>
<button
name="action_cancel"
type="object"
states="draft,open,generated"
string="Cancel Payments"
/>
<button
name="action_uploaded_cancel"
type="object"
states="uploaded"
string="Cancel Payments"
/>
<field
name="state"
widget="statusbar"
statusbar_visible="draft,open,generated,uploaded"
/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button
class="oe_stat_button"
name="action_move_journal_line"
type="object"
icon="fa-bars"
>
<field
string="Journal Entries"
name="move_count"
widget="statinfo"
/>
</button>
</div>
<div class="oe_title">
<label for="name" class="oe_edit_only" />
<h1>
<field name="name" />
</h1>
</div>
<group name="head" col="2">
<group name="head-left">
<field
name="payment_mode_id"
domain="[('payment_order_ok', '=', True), ('payment_type', '=', payment_type)]"
/>
<field name="allowed_journal_ids" invisible="1" />
<field
name="journal_id"
domain="[('id', 'in', allowed_journal_ids)]"
/>
<field name="bank_account_link" invisible="1" />
<field name="company_partner_bank_id" />
<field
name="company_id"
groups="base.group_multi_company"
/>
<field name="payment_type" invisible="0" />
<field
name="bank_line_count"
attrs="{'invisible': [('state', 'in', ('draft', 'cancel'))]}"
/>
</group>
<group name="head-right">
<field name="date_prefered" />
<field
name="date_scheduled"
attrs="{'invisible': [('date_prefered', '!=', 'fixed')], 'required': [('date_prefered', '=', 'fixed')]}"
/>
<field name="date_generated" />
<field name="generated_user_id" />
<field name="date_uploaded" />
<field name="description" />
</group>
</group>
<notebook>
<page name="payment-lines" string="Transactions">
<field
name="payment_line_ids"
context="{'default_payment_type': payment_type}"
/>
</page>
<page
name="bank-lines"
string="Bank Transactions"
attrs="{'invisible': [('state', 'in', ('draft', 'cancel'))]}"
>
<field name="bank_line_ids" edit="0" create="0" />
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers" />
<field name="message_ids" widget="mail_thread" />
</div>
</form>
</field>
</record>
<record id="account_payment_order_tree" model="ir.ui.view">
<field name="name">account.payment.order.tree</field>
<field name="model">account.payment.order</field>
<field name="arch" type="xml">
<tree>
<field name="name" decoration-bf="1" />
<field name="payment_mode_id" />
<field name="journal_id" />
<field name="company_id" groups="base.group_multi_company" />
<field name="date_uploaded" />
<field name="description" optional="show" />
<field
name="bank_line_count"
optional="hide"
string="Bank Transactions"
/>
<field name="total_company_currency" sum="Total Company Currency" />
<field name="company_currency_id" invisible="1" />
<field
name="state"
decoration-info="state == 'draft'"
decoration-success="state == 'uploaded'"
decoration-warning="state == 'open'"
decoration-danger="state == 'generated'"
decoration-muted="state == 'cancel'"
widget="badge"
/>
</tree>
</field>
</record>
<record id="account_payment_order_search" model="ir.ui.view">
<field name="name">account.payment.order.search</field>
<field name="model">account.payment.order</field>
<field name="arch" type="xml">
<search string="Search Payment Orders">
<field
name="description"
filter_domain="['|', ('name', 'ilike', self), ('description', 'ilike', self)]"
string="Name or Description"
/>
<field name="journal_id" />
<filter
name="draft"
string="Draft"
domain="[('state', '=', 'draft')]"
/>
<filter
name="open"
string="Confirmed"
domain="[('state', '=', 'open')]"
/>
<filter
name="generated"
string="File Generated"
domain="[('state', '=', 'generated')]"
/>
<filter
name="uploaded"
string="File Uploaded"
domain="[('state', '=', 'uploaded')]"
/>
<group string="Group By" name="groupby">
<filter
name="payment_mode_groupby"
string="Payment Mode"
context="{'group_by': 'payment_mode_id'}"
/>
<filter
name="journal_groupby"
string="Bank Journal"
context="{'group_by': 'journal_id'}"
/>
<filter
name="date_generated_groupby"
string="File Generation Date"
context="{'group_by': 'date_generated'}"
/>
<filter
name="date_uploaded_groupby"
string="File Upload Date"
context="{'group_by': 'date_uploaded'}"
/>
<filter
name="state_groupby"
string="State"
context="{'group_by': 'state'}"
/>
</group>
</search>
</field>
</record>
<record id="account_payment_order_graph" model="ir.ui.view">
<field name="name">account.payment.order.graph</field>
<field name="model">account.payment.order</field>
<field name="arch" type="xml">
<graph string="Payment Orders">
<field name="date_uploaded" type="row" interval="month" />
<field name="total_company_currency" type="measure" />
</graph>
</field>
</record>
<record id="account_payment_order_pivot" model="ir.ui.view">
<field name="name">account.payment.order.pivot</field>
<field name="model">account.payment.order</field>
<field name="arch" type="xml">
<pivot string="Payment Orders">
<field name="date_uploaded" type="row" interval="month" />
<field name="total_company_currency" type="measure" />
</pivot>
</field>
</record>
<record id="account_payment_order_outbound_action" model="ir.actions.act_window">
<field name="name">Payment Orders</field>
<field name="res_model">account.payment.order</field>
<field name="view_mode">tree,form,pivot,graph</field>
<field name="domain">[('payment_type', '=', 'outbound')]</field>
<field name="context">{'default_payment_type': 'outbound'}</field>
</record>
<record id="account_payment_order_inbound_action" model="ir.actions.act_window">
<field name="name">Debit Orders</field>
<field name="res_model">account.payment.order</field>
<field name="view_mode">tree,form,pivot,graph</field>
<field name="domain">[('payment_type', '=', 'inbound')]</field>
<field name="context">{'default_payment_type': 'inbound'}</field>
</record>
<menuitem
id="account_payment_order_outbound_menu"
action="account_payment_order_outbound_action"
parent="account.menu_finance_payables"
sequence="21"
/>
<menuitem
id="account_payment_order_inbound_menu"
action="account_payment_order_inbound_action"
parent="account.menu_finance_receivables"
sequence="18"
/>
</odoo>

View File

@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!--
© 2015-2016 Akretion (https://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<record id="bank_payment_line_form" model="ir.ui.view">
<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" create="false">
<group name="main">
<field
name="order_id"
invisible="not context.get('bank_payment_line_main_view')"
/>
<field name="name" />
<field
name="company_id"
groups="base.group_multi_company"
invisible="not context.get('bank_payment_line_main_view')"
/>
<field name="partner_id" />
<field name="date" />
<field name="amount_currency" />
<field name="currency_id" invisible="1" />
<field name="partner_bank_id" />
<field name="communication_type" />
<field name="communication" />
</group>
<group string="Related Payment Lines" name="payment-lines">
<field name="payment_line_ids" nolabel="1" />
</group>
</form>
</field>
</record>
<record id="bank_payment_line_tree" model="ir.ui.view">
<field name="name">bank.payment.line.tree</field>
<field name="model">bank.payment.line</field>
<field name="arch" type="xml">
<tree create="false">
<field
name="order_id"
invisible="not context.get('bank_payment_line_main_view')"
/>
<field name="partner_id" />
<field name="communication" />
<field name="partner_bank_id" />
<field name="date" />
<field name="amount_currency" sum="Total Amount" />
<field name="currency_id" invisible="1" />
<field name="name" optional="show" />
<field
name="company_id"
groups="base.group_multi_company"
invisible="not context.get('bank_payment_line_main_view')"
/>
</tree>
</field>
</record>
<record id="bank_payment_line_search" model="ir.ui.view">
<field name="name">bank.payment.line.search</field>
<field name="model">bank.payment.line</field>
<field name="arch" type="xml">
<search string="Search Bank Payment Lines">
<field name="partner_id" />
<filter
name="inbound"
string="Inbound"
domain="[('payment_type', '=', 'inbound')]"
/>
<filter
name="outbound"
string="Outbound"
domain="[('payment_type', '=', 'outbound')]"
/>
<group string="Group By" name="groupby">
<filter
name="state_groupby"
string="State"
context="{'group_by': 'state'}"
/>
<filter
name="partner_groupby"
string="Partner"
context="{'group_by': 'partner_id'}"
/>
</group>
</search>
</field>
</record>
<record id="bank_payment_line_action" model="ir.actions.act_window">
<field name="name">Bank Payment Lines</field>
<field name="res_model">bank.payment.line</field>
<field name="view_mode">tree,form</field>
<field name="context">{'bank_payment_line_main_view': True}</field>
</record>
<menuitem
id="bank_payment_line_menu"
action="bank_payment_line_action"
parent="account.menu_finance_payables"
sequence="50"
groups="group_account_payment"
/>
</odoo>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- © 2016 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>) -->
<odoo>
<record id="view_attachment_simplified_form" model="ir.ui.view">
<field name="name">ir.attachment.simplified.form</field>
<field name="model">ir.attachment</field>
<field name="priority" eval="25" />
<field name="arch" type="xml">
<form string="Attachments">
<sheet>
<label for="name" class="oe_edit_only" />
<h1>
<field name="name" />
</h1>
<group name="main">
<field name="datas" filename="name" string="Generated File" />
<label for="create_uid" string="Created by" />
<div name="creation_div">
<field name="create_uid" readonly="1" class="oe_inline" />
on
<field name="create_date" readonly="1" class="oe_inline" />
</div>
</group>
</sheet>
</form>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,2 @@
from . import account_payment_line_create
from . import account_invoice_payment_line_multi

View File

@@ -0,0 +1,18 @@
# © 2016 Akretion (<https://www.akretion.com>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import models
class AccountInvoicePaymentLineMulti(models.TransientModel):
_name = "account.invoice.payment.line.multi"
_description = "Create payment lines from invoice tree view"
def run(self):
self.ensure_one()
assert (
self._context["active_model"] == "account.move"
), "Active model should be account.move"
invoices = self.env["account.move"].browse(self._context["active_ids"])
action = invoices.create_account_payment_line()
return action

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
© 2016 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>)
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="account_invoice_payment_line_multi_form" model="ir.ui.view">
<field name="name">account_invoice_payment_line_multi.form</field>
<field name="model">account.invoice.payment.line.multi</field>
<field name="arch" type="xml">
<form string="Create Payment Lines">
<p>This wizard will create payment lines for the selected invoices:</p>
<ul>
<li
>if there are existing draft payment orders for the payment modes of the invoices, the payment lines will be added to those payment orders</li>
<li
>otherwise, new payment orders will be created (one per payment mode).</li>
</ul>
<footer>
<button
type="object"
name="run"
string="Create"
class="oe_highlight"
/>
<button special="cancel" string="Cancel" class="oe_link" />
</footer>
</form>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,177 @@
# © 2009 EduSense BV (<http://www.edusense.nl>)
# © 2011-2013 Therp BV (<https://therp.nl>)
# © 2014-2015 ACSONE SA/NV (<https://acsone.eu>)
# © 2015-2016 Akretion (<https://www.akretion.com>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import _, api, fields, models
class AccountPaymentLineCreate(models.TransientModel):
_name = "account.payment.line.create"
_description = "Wizard to create payment lines"
order_id = fields.Many2one(
comodel_name="account.payment.order", string="Payment Order"
)
journal_ids = fields.Many2many(
comodel_name="account.journal", string="Journals Filter"
)
partner_ids = fields.Many2many(
comodel_name="res.partner",
string="Partners",
domain=[("parent_id", "=", False)],
)
target_move = fields.Selection(
selection=[("posted", "All Posted Entries"), ("all", "All Entries")],
string="Target Moves",
)
allow_blocked = fields.Boolean(string="Allow Litigation Move Lines")
invoice = fields.Boolean(string="Linked to an Invoice or Refund")
date_type = fields.Selection(
selection=[("due", "Due Date"), ("move", "Move Date")],
string="Type of Date Filter",
required=True,
)
due_date = fields.Date()
move_date = fields.Date(default=fields.Date.context_today)
payment_mode = fields.Selection(
selection=[("same", "Same"), ("same_or_null", "Same or Empty"), ("any", "Any")],
)
move_line_ids = fields.Many2many(
comodel_name="account.move.line", string="Move Lines"
)
@api.model
def default_get(self, field_list):
res = super(AccountPaymentLineCreate, self).default_get(field_list)
context = self.env.context
assert (
context.get("active_model") == "account.payment.order"
), "active_model should be payment.order"
assert context.get("active_id"), "Missing active_id in context !"
order = self.env["account.payment.order"].browse(context["active_id"])
mode = order.payment_mode_id
res.update(
{
"journal_ids": mode.default_journal_ids.ids or False,
"target_move": mode.default_target_move,
"invoice": mode.default_invoice,
"date_type": mode.default_date_type,
"payment_mode": mode.default_payment_mode,
"order_id": order.id,
}
)
return res
def _prepare_move_line_domain(self):
self.ensure_one()
domain = [
("reconciled", "=", False),
("company_id", "=", self.order_id.company_id.id),
]
if self.journal_ids:
domain += [("journal_id", "in", self.journal_ids.ids)]
if self.partner_ids:
domain += [("partner_id", "in", self.partner_ids.ids)]
if self.target_move == "posted":
domain += [("move_id.state", "=", "posted")]
if not self.allow_blocked:
domain += [("blocked", "!=", True)]
if self.date_type == "due":
domain += [
"|",
("date_maturity", "<=", self.due_date),
("date_maturity", "=", False),
]
elif self.date_type == "move":
domain.append(("date", "<=", self.move_date))
if self.invoice:
domain.append(
(
"move_id.move_type",
"in",
("in_invoice", "out_invoice", "in_refund", "out_refund"),
)
)
if self.payment_mode:
if self.payment_mode == "same":
domain.append(
("payment_mode_id", "=", self.order_id.payment_mode_id.id)
)
elif self.payment_mode == "same_or_null":
domain += [
"|",
("payment_mode_id", "=", False),
("payment_mode_id", "=", self.order_id.payment_mode_id.id),
]
if self.order_id.payment_type == "outbound":
# For payables, propose all unreconciled credit lines,
# including partially reconciled ones.
# If they are partially reconciled with a supplier refund,
# the residual will be added to the payment order.
#
# For receivables, propose all unreconciled credit lines.
# (ie customer refunds): they can be refunded with a payment.
# Do not propose partially reconciled credit lines,
# as they are deducted from a customer invoice, and
# will not be refunded with a payment.
domain += [
("credit", ">", 0),
("account_id.internal_type", "in", ["payable", "receivable"]),
]
elif self.order_id.payment_type == "inbound":
domain += [
("debit", ">", 0),
("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,
paylines = self.env["account.payment.line"].search(
[
("state", "in", ("draft", "open", "generated")),
("move_line_id", "!=", False),
]
)
if paylines:
move_lines_ids = [payline.move_line_id.id for payline in paylines]
domain += [("id", "not in", move_lines_ids)]
return domain
def populate(self):
domain = self._prepare_move_line_domain()
lines = self.env["account.move.line"].search(domain)
self.move_line_ids = lines
action = {
"name": _("Select Move Lines to Create Transactions"),
"type": "ir.actions.act_window",
"res_model": "account.payment.line.create",
"view_mode": "form",
"target": "new",
"res_id": self.id,
"context": self._context,
}
return action
@api.onchange(
"date_type",
"move_date",
"due_date",
"journal_ids",
"invoice",
"target_move",
"allow_blocked",
"payment_mode",
"partner_ids",
)
def move_line_filters_change(self):
domain = self._prepare_move_line_domain()
res = {"domain": {"move_line_ids": domain}}
return res
def create_payment_lines(self):
if self.move_line_ids:
self.move_line_ids.create_payment_line_from_move_line(self.order_id)
return True

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
© 2013-2016 Akretion (https://www.akretion.com)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="account_payment_line_create_form" model="ir.ui.view">
<field name="name">account_payment_line_create.form</field>
<field name="model">account.payment.line.create</field>
<field name="arch" type="xml">
<form string="Choose Move Lines Filter Options">
<group name="main">
<field name="order_id" invisible="1" />
<field name="date_type" />
<field
name="move_date"
attrs="{'required': [('date_type', '=', 'move')], 'invisible': [('date_type', '!=', 'move')]}"
/>
<field
name="due_date"
attrs="{'required': [('date_type', '=', 'due')], 'invisible': [('date_type', '!=', 'due')]}"
/>
<field
name="journal_ids"
widget="many2many_tags"
placeholder="Keep empty for using all journals"
/>
<field
name="partner_ids"
widget="many2many_tags"
placeholder="Keep empty to use all partners"
/>
<field name="payment_mode" />
<field name="target_move" widget="radio" />
<field name="invoice" />
<field name="allow_blocked" />
<label
for="populate"
string="Click on Add All Move Lines to auto-select the move lines matching the above criteria or click on Add an item to manually select the move lines filtered by the above criteria."
colspan="2"
/>
<button name="populate" type="object" string="Add All Move Lines" />
</group>
<group
name="move_lines"
string="Selected Move Lines to Create Transactions"
>
<field
name="move_line_ids"
nolabel="1"
context="{'tree_view_ref': 'account_payment_order.view_move_line_tree'}"
>
<tree>
<field name="date" />
<field name="move_id" required="0" />
<field name="journal_id" />
<field name="partner_id" />
<field name="account_id" />
<field name="date_maturity" />
<field name="debit" />
<field name="credit" />
<field name="amount_residual" sum="Total Residual" />
<field name="amount_currency" />
<field name="amount_residual_currency" />
<field name="company_currency_id" invisible="1" />
</tree>
</field>
</group>
<footer>
<button
name="create_payment_lines"
type="object"
string="Create Transactions"
class="oe_highlight"
/>
<button string="Cancel" special="cancel" class="oe_link" />
</footer>
</form>
</field>
</record>
<record id="account_payment_line_create_action" model="ir.actions.act_window">
<field name="name">Create Transactions from Move Lines</field>
<field name="res_model">account.payment.line.create</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
# generated from manifests external_dependencies
lxml

View File

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

View File

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