mirror of
https://github.com/bringout/oca-payment.git
synced 2026-04-23 13:21:59 +02:00
Restructure: move packages from packages/ subdirectory to root
Flattened directory structure by moving payment packages from redundant packages/ subdirectory to the root level of oca-payment repository. Changes: - Moved odoo-bringout-oca-payment-* from packages/ to root - Updated CLAUDE.md to reflect new flat structure - Removed redundant packages/ directory 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
99c650f4f5
commit
7f7e88ab3d
202 changed files with 1 additions and 1 deletions
|
|
@ -0,0 +1,10 @@
|
|||
from . import account_payment_mode
|
||||
from . import account_payment_order
|
||||
from . import account_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
|
||||
from . import res_company
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
# © 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",
|
||||
)
|
||||
payment_line_count = fields.Integer(compute="_compute_payment_line_count")
|
||||
|
||||
@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 _compute_payment_line_count(self):
|
||||
for move in self:
|
||||
move.payment_line_count = len(
|
||||
self.env["account.payment.line"]._search(
|
||||
[("move_line_id", "in", self.line_ids.ids)]
|
||||
)
|
||||
)
|
||||
|
||||
def _get_payment_order_communication_direct(self):
|
||||
"""Retrieve the communication string for this direct item."""
|
||||
communication = self.payment_reference or self.ref or self.name
|
||||
if self.is_invoice():
|
||||
if self.is_purchase_document():
|
||||
communication = self.payment_reference or self.ref
|
||||
else:
|
||||
communication = self.payment_reference or self.name
|
||||
return communication or ""
|
||||
|
||||
def _get_payment_order_communication_full(self):
|
||||
"""Retrieve the full communication string for the payment order.
|
||||
Reversal moves and partial payments references added.
|
||||
Avoid having everything in the same method to avoid infinite recursion
|
||||
with partial payments.
|
||||
"""
|
||||
communication = self._get_payment_order_communication_direct()
|
||||
references = []
|
||||
# Build a recordset to gather moves from which references have already
|
||||
# taken in order to avoid duplicates
|
||||
reference_moves = self.env["account.move"].browse()
|
||||
# If we have credit note(s) - reversal_move_id is a one2many
|
||||
if self.reversal_move_id:
|
||||
references.extend(
|
||||
[
|
||||
move._get_payment_order_communication_direct()
|
||||
for move in self.reversal_move_id
|
||||
]
|
||||
)
|
||||
reference_moves |= self.reversal_move_id
|
||||
# Retrieve partial payments - e.g.: manual credit notes
|
||||
(
|
||||
invoice_partials,
|
||||
exchange_diff_moves,
|
||||
) = self._get_reconciled_invoices_partials()
|
||||
for (_x, _y, payment_move_line,) in (
|
||||
invoice_partials + exchange_diff_moves
|
||||
):
|
||||
payment_move = payment_move_line.move_id
|
||||
if payment_move not in reference_moves:
|
||||
references.append(
|
||||
payment_move._get_payment_order_communication_direct()
|
||||
)
|
||||
# Add references to communication from lines move
|
||||
if references:
|
||||
communication += " " + " ".join(references)
|
||||
return communication
|
||||
|
||||
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 = set()
|
||||
action_payment_type = "debit"
|
||||
for move in self:
|
||||
if move.state != "posted":
|
||||
raise UserError(_("The invoice %s is not in Posted state") % move.name)
|
||||
pre_applicable_lines = move.line_ids.filtered(
|
||||
lambda x: (
|
||||
not x.reconciled
|
||||
and x.account_id.account_type
|
||||
in ("asset_receivable", "liability_payable")
|
||||
)
|
||||
)
|
||||
if not pre_applicable_lines:
|
||||
raise UserError(_("No pending AR/AP lines to add on %s") % move.name)
|
||||
payment_modes = pre_applicable_lines.mapped("payment_mode_id")
|
||||
if not payment_modes:
|
||||
raise UserError(_("No Payment Mode on invoice %s") % move.name)
|
||||
applicable_lines = pre_applicable_lines.filtered(
|
||||
lambda x: x.payment_mode_id.payment_order_ok
|
||||
)
|
||||
if not applicable_lines:
|
||||
raise UserError(
|
||||
_(
|
||||
"No Payment Line created for invoice %s because "
|
||||
"its payment mode is not intended for payment orders."
|
||||
)
|
||||
% move.name
|
||||
)
|
||||
payment_lines = applicable_lines.payment_line_ids.filtered(
|
||||
lambda l: l.state in ("draft", "open", "generated")
|
||||
)
|
||||
if payment_lines:
|
||||
raise UserError(
|
||||
_(
|
||||
"The invoice %(move)s is already added in the payment "
|
||||
"order(s) %(order)s."
|
||||
)
|
||||
% {
|
||||
"move": move.name,
|
||||
"order": payment_lines.order_id.mapped("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.add(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 <a href=# data-oe-model=account.payment.order "
|
||||
"data-oe-id=%(order_id)d>%(name)s</a>, which has been "
|
||||
"automatically created.",
|
||||
count=count,
|
||||
order_id=payorder.id,
|
||||
name=payorder.name,
|
||||
)
|
||||
)
|
||||
else:
|
||||
move.message_post(
|
||||
body=_(
|
||||
"%(count)d payment lines added to the existing draft "
|
||||
"payment order "
|
||||
"<a href=# data-oe-model=account.payment.order "
|
||||
"data-oe-id=%(order_id)d>%(name)s</a>.",
|
||||
count=count,
|
||||
order_id=payorder.id,
|
||||
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)]" % list(result_payorder_ids),
|
||||
"views": False,
|
||||
}
|
||||
)
|
||||
return action
|
||||
|
||||
def action_payment_lines(self):
|
||||
self.ensure_one()
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"account_payment_order.account_payment_line_action"
|
||||
)
|
||||
action.update(
|
||||
{
|
||||
"domain": [("move_line_id", "in", self.line_ids.ids)],
|
||||
"context": dict(
|
||||
self.env.context,
|
||||
account_payment_line_main_view=1,
|
||||
form_view_ref="account_payment_order.account_payment_line_form_readonly",
|
||||
),
|
||||
}
|
||||
)
|
||||
return action
|
||||
|
||||
@api.model
|
||||
def _get_invoice_in_payment_state(self):
|
||||
"""Called from _compute_payment_state method.
|
||||
Consider in_payment all the moves that are included in a payment order.
|
||||
"""
|
||||
if self.line_ids.payment_line_ids:
|
||||
return "in_payment"
|
||||
return super()._get_invoice_in_payment_state()
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
# © 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,
|
||||
)
|
||||
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 or not ml.payment_mode_id)
|
||||
and ml.account_id.account_type
|
||||
in ("asset_receivable", "liability_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 _get_communication(self):
|
||||
"""
|
||||
Retrieve the communication string for the payment order
|
||||
"""
|
||||
aplo = self.env["account.payment.line"]
|
||||
# default values for communication_type and communication
|
||||
communication_type = "normal"
|
||||
communication = self.move_id._get_payment_order_communication_full()
|
||||
# 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":
|
||||
ref2comm_type = aplo.invoice_reference_type2communication_type()
|
||||
communication_type = ref2comm_type[self.move_id.reference_type]
|
||||
return communication_type, communication
|
||||
|
||||
def _prepare_payment_line_vals(self, payment_order):
|
||||
self.ensure_one()
|
||||
communication_type, communication = self._get_communication()
|
||||
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)
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
# Copyright 2019 ACSONE SA/NV
|
||||
# Copyright 2022 Tecnativa - Pedro M. Baeza
|
||||
# Copyright 2023 Noviat
|
||||
# Copyright 2024 Tecnativa - Víctor Martínez
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class AccountPayment(models.Model):
|
||||
_inherit = "account.payment"
|
||||
|
||||
payment_order_id = fields.Many2one(comodel_name="account.payment.order")
|
||||
payment_line_ids = fields.Many2many(comodel_name="account.payment.line")
|
||||
order_state = fields.Selection(
|
||||
related="payment_order_id.state", string="Payment Order State"
|
||||
)
|
||||
payment_line_date = fields.Date(compute="_compute_payment_line_date")
|
||||
|
||||
@api.depends("payment_type", "journal_id")
|
||||
def _compute_payment_method_line_fields(self):
|
||||
res = super()._compute_payment_method_line_fields()
|
||||
for pay in self:
|
||||
if pay.payment_order_id:
|
||||
pay.available_payment_method_line_ids = (
|
||||
pay.payment_order_id.journal_id._get_available_payment_method_lines(
|
||||
pay.payment_type
|
||||
)
|
||||
)
|
||||
else:
|
||||
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 = pay._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
|
||||
)
|
||||
)
|
||||
return res
|
||||
|
||||
@api.depends("payment_line_ids", "payment_line_ids.date")
|
||||
def _compute_payment_line_date(self):
|
||||
for item in self:
|
||||
item.payment_line_date = item.payment_line_ids[:1].date
|
||||
|
||||
@api.depends("payment_line_ids")
|
||||
def _compute_partner_bank_id(self):
|
||||
# Force the payment line bank account. The grouping function has already
|
||||
# assured that there's no more than one bank account in the group
|
||||
order_pays = self.filtered("payment_line_ids")
|
||||
for pay in order_pays:
|
||||
pay.partner_bank_id = pay.payment_line_ids.partner_bank_id
|
||||
return super(AccountPayment, self - order_pays)._compute_partner_bank_id()
|
||||
|
||||
@api.constrains("payment_method_line_id")
|
||||
def _check_payment_method_line_id(self):
|
||||
for pay in self:
|
||||
transfer_journal = (
|
||||
pay.payment_order_id.payment_mode_id.transfer_journal_id
|
||||
or pay.company_id.transfer_journal_id
|
||||
)
|
||||
if pay.journal_id == transfer_journal:
|
||||
continue
|
||||
else:
|
||||
super(AccountPayment, pay)._check_payment_method_line_id()
|
||||
return
|
||||
|
||||
def update_payment_reference(self):
|
||||
view = self.env.ref("account_payment_order.account_payment_update_view_form")
|
||||
return {
|
||||
"name": _("Update Payment Reference"),
|
||||
"view_type": "form",
|
||||
"view_mode": "form",
|
||||
"res_model": "account.payment.update",
|
||||
"view_id": view.id,
|
||||
"target": "new",
|
||||
"type": "ir.actions.act_window",
|
||||
"context": dict(
|
||||
self.env.context, default_payment_reference=self.payment_reference
|
||||
),
|
||||
}
|
||||
|
||||
def _prepare_move_line_default_vals(self, write_off_line_vals=None):
|
||||
"""Overwrite date_maturity of the move_lines that are generated when related
|
||||
to a payment order.
|
||||
"""
|
||||
vals_list = super()._prepare_move_line_default_vals(
|
||||
write_off_line_vals=write_off_line_vals
|
||||
)
|
||||
if not self.payment_order_id:
|
||||
return vals_list
|
||||
for vals in vals_list:
|
||||
vals["date_maturity"] = self.payment_line_ids[0].date
|
||||
return vals_list
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
# © 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,
|
||||
)
|
||||
partner_bank_acc_type = fields.Selection(
|
||||
related="partner_bank_id.acc_type", string="Bank Account Type"
|
||||
)
|
||||
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"), ("structured", "Structured")],
|
||||
required=True,
|
||||
default="normal",
|
||||
)
|
||||
payment_ids = fields.Many2many(
|
||||
comodel_name="account.payment",
|
||||
string="Payment transaction",
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"name_company_unique",
|
||||
"unique(name, company_id)",
|
||||
"A payment line already exists with this reference in the same company!",
|
||||
)
|
||||
]
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get("name", "New") == "New":
|
||||
vals["name"] = (
|
||||
self.env["ir.sequence"].next_by_code("account.payment.line")
|
||||
or "New"
|
||||
)
|
||||
return super().create(vals_list)
|
||||
|
||||
@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
|
||||
|
||||
@api.model
|
||||
def _get_payment_line_grouping_fields(self):
|
||||
"""This list of fields is used o compute the grouping hashcode."""
|
||||
return [
|
||||
"currency_id",
|
||||
"partner_id",
|
||||
"partner_bank_id",
|
||||
"date",
|
||||
"communication_type",
|
||||
]
|
||||
|
||||
def payment_line_hashcode(self):
|
||||
self.ensure_one()
|
||||
values = []
|
||||
for field in self._get_payment_line_grouping_fields():
|
||||
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))
|
||||
return "-".join(values)
|
||||
|
||||
@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)
|
||||
|
||||
def _prepare_account_payment_vals(self):
|
||||
"""Prepare the dictionary to create an account payment record from a set of
|
||||
payment lines.
|
||||
"""
|
||||
journal = self.order_id.journal_id
|
||||
payment_mode = self.order_id.payment_mode_id
|
||||
vals = {
|
||||
"payment_type": self.order_id.payment_type,
|
||||
"partner_id": self.partner_id.id,
|
||||
"destination_account_id": self.move_line_id.account_id.id,
|
||||
"company_id": self.order_id.company_id.id,
|
||||
"amount": sum(self.mapped("amount_currency")),
|
||||
"date": fields.Date.context_today(self),
|
||||
"currency_id": self.currency_id.id,
|
||||
"ref": self.order_id.name,
|
||||
# Put the name as the wildcard for forcing a unique name. If not, Odoo gets
|
||||
# the sequence for all the payment at the same time
|
||||
"name": "/",
|
||||
"payment_reference": " - ".join([line.communication for line in self]),
|
||||
"journal_id": journal.id,
|
||||
"partner_bank_id": self.partner_bank_id.id,
|
||||
"payment_order_id": self.order_id.id,
|
||||
"payment_line_ids": [(6, 0, self.ids)],
|
||||
}
|
||||
# Determine payment method line according payment method and journal
|
||||
line = self.env["account.payment.method.line"].search(
|
||||
[
|
||||
("payment_method_id", "=", payment_mode.payment_method_id.id),
|
||||
("journal_id", "=", journal.id),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
if line:
|
||||
vals["payment_method_line_id"] = line.id
|
||||
# Determine partner_type
|
||||
move_type = self[:1].move_line_id.move_id.move_type
|
||||
if move_type in {"out_invoice", "out_refund"}:
|
||||
vals["partner_type"] = "customer"
|
||||
elif move_type in {"in_invoice", "in_refund"}:
|
||||
vals["partner_type"] = "supplier"
|
||||
else:
|
||||
p_type = "customer" if vals["payment_type"] == "inbound" else "supplier"
|
||||
vals["partner_type"] = p_type
|
||||
# Fill destination account if manual payment line with no linked journal item
|
||||
if not vals["destination_account_id"]:
|
||||
if vals["partner_type"] == "customer":
|
||||
vals[
|
||||
"destination_account_id"
|
||||
] = self.partner_id.property_account_receivable_id.id
|
||||
else:
|
||||
vals[
|
||||
"destination_account_id"
|
||||
] = self.partner_id.property_account_payable_id.id
|
||||
|
||||
transfer_journal = (
|
||||
self.order_id.payment_mode_id.transfer_journal_id
|
||||
or self.company_id.transfer_journal_id
|
||||
)
|
||||
if transfer_journal:
|
||||
vals["journal_id"] = transfer_journal.id
|
||||
return vals
|
||||
|
||||
def action_open_business_doc(self):
|
||||
if not self.move_line_id:
|
||||
return False
|
||||
return self.move_line_id.action_open_business_doc()
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
# © 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
|
||||
|
||||
|
||||
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.)",
|
||||
)
|
||||
transfer_journal_id = fields.Many2one(
|
||||
comodel_name="account.journal",
|
||||
string="Transfer journal on payment/debit orders",
|
||||
domain="[('type', '=', 'general')]",
|
||||
help="Journal to write payment entries when confirming payment/debit orders",
|
||||
)
|
||||
|
||||
@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)]
|
||||
|
|
@ -0,0 +1,494 @@
|
|||
# © 2009 EduSense BV (<http://www.edusense.nl>)
|
||||
# © 2011-2013 Therp BV (<https://therp.nl>)
|
||||
# © 2016 Akretion (Alexis de Lattre - alexis.delattre@akretion.com)
|
||||
# Copyright 2016-2022 Tecnativa - Pedro M. Baeza
|
||||
# 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", "mail.activity.mixin"]
|
||||
_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,
|
||||
)
|
||||
partner_banks_archive_msg = fields.Html(
|
||||
compute="_compute_partner_banks_archive_msg",
|
||||
)
|
||||
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)]},
|
||||
)
|
||||
payment_ids = fields.One2many(
|
||||
comodel_name="account.payment",
|
||||
inverse_name="payment_order_id",
|
||||
string="Payment Transactions",
|
||||
readonly=True,
|
||||
)
|
||||
payment_count = fields.Integer(
|
||||
compute="_compute_payment_count",
|
||||
string="Number of Payment Transactions",
|
||||
)
|
||||
total_company_currency = fields.Monetary(
|
||||
compute="_compute_total", store=True, currency_field="company_currency_id"
|
||||
)
|
||||
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_line_ids.partner_bank_id", "payment_line_ids.partner_bank_id.active"
|
||||
)
|
||||
def _compute_partner_banks_archive_msg(self):
|
||||
"""Information message to show archived bank accounts and to be able
|
||||
to act on them before confirmation (avoid duplicates)."""
|
||||
for item in self:
|
||||
msg_lines = []
|
||||
for partner_bank in item.payment_line_ids.filtered(
|
||||
lambda x: x.partner_bank_id and not x.partner_bank_id.active
|
||||
).mapped("partner_bank_id"):
|
||||
msg_line = _(
|
||||
"<b>Account Number</b>: %(number)s - <b>Partner</b>: %(name)s"
|
||||
) % {
|
||||
"number": partner_bank.acc_number,
|
||||
"name": partner_bank.partner_id.display_name,
|
||||
}
|
||||
msg_lines.append(msg_line)
|
||||
item.partner_banks_archive_msg = (
|
||||
"<br/>".join(msg_lines) if len(msg_lines) > 0 else False
|
||||
)
|
||||
|
||||
@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.constrains("payment_line_ids")
|
||||
def _check_payment_lines(self):
|
||||
for order in self:
|
||||
move_line_ids = [
|
||||
x.move_line_id.id for x in order.payment_line_ids if x.move_line_id
|
||||
]
|
||||
if len(move_line_ids) != len(set(move_line_ids)):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"There are several lines pointing to the same pending "
|
||||
"balance. This is probably caused by a manual line creation. "
|
||||
"Please remove this duplication for being able to save the "
|
||||
"order."
|
||||
)
|
||||
)
|
||||
|
||||
@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("payment_ids")
|
||||
def _compute_payment_count(self):
|
||||
for order in self:
|
||||
order.payment_count = len(order.payment_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_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
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_list)
|
||||
|
||||
@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):
|
||||
self.action_cancel()
|
||||
return True
|
||||
|
||||
def cancel2draft(self):
|
||||
self.write({"state": "draft"})
|
||||
return True
|
||||
|
||||
def action_cancel(self):
|
||||
# Unreconcile and cancel payments
|
||||
self.payment_ids.action_draft()
|
||||
self.payment_ids.action_cancel()
|
||||
self.write({"state": "cancel"})
|
||||
return True
|
||||
|
||||
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 account payments.
|
||||
"""
|
||||
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
|
||||
)
|
||||
# Unreconcile, cancel and delete existing account payments
|
||||
order.payment_ids.action_draft()
|
||||
order.payment_ids.action_cancel()
|
||||
order.payment_ids.unlink()
|
||||
# Prepare account payments from the payment lines
|
||||
payline_err_text = []
|
||||
group_paylines = {} # key = hashcode
|
||||
for payline in order.payment_line_ids:
|
||||
try:
|
||||
payline.draft2open_payment_line_check()
|
||||
except UserError as e:
|
||||
payline_err_text.append(e.args[0])
|
||||
# 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
|
||||
requested_date = max(today, requested_date)
|
||||
# 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
|
||||
):
|
||||
payline_err_text.append(
|
||||
_(
|
||||
"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
|
||||
hashcode = (
|
||||
payline.payment_line_hashcode()
|
||||
if order.payment_mode_id.group_lines
|
||||
else 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,
|
||||
}
|
||||
# Raise errors that happened on the validation process
|
||||
if payline_err_text:
|
||||
raise UserError(
|
||||
_("There's at least one validation error:\n")
|
||||
+ "\n".join(payline_err_text)
|
||||
)
|
||||
|
||||
order.env.flush_all()
|
||||
|
||||
# Create account payments
|
||||
payment_vals = []
|
||||
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"],
|
||||
)
|
||||
)
|
||||
payment_vals.append(paydict["paylines"]._prepare_account_payment_vals())
|
||||
self.env["account.payment"].create(payment_vals)
|
||||
self.write({"state": "open"})
|
||||
return True
|
||||
|
||||
def generate_payment_file(self):
|
||||
"""Returns (payment file as string, filename).
|
||||
|
||||
By default, any method not specifically intercepted by extra modules will do
|
||||
nothing, including the existing manual one.
|
||||
"""
|
||||
self.ensure_one()
|
||||
return (False, False)
|
||||
|
||||
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):
|
||||
self.payment_ids.action_post()
|
||||
# Perform the reconciliation of payments and source journal items
|
||||
for payment in self.payment_ids:
|
||||
(
|
||||
payment.payment_line_ids.move_line_id
|
||||
+ payment.move_id.line_ids.filtered(
|
||||
lambda x: x.account_id == payment.destination_account_id
|
||||
)
|
||||
).reconcile()
|
||||
self.write(
|
||||
{"state": "uploaded", "date_uploaded": fields.Date.context_today(self)}
|
||||
)
|
||||
return True
|
||||
|
||||
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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# Copyright 2023 Noviat
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
transfer_journal_id = fields.Many2one(
|
||||
comodel_name="account.journal",
|
||||
string="Transfer journal on payment/debit orders",
|
||||
domain="[('type', '=', 'general')]",
|
||||
help="Journal to write payment entries when confirming payment/debit orders",
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue