Files
bank-payment/account_payment_order/models/account_payment_order.py
Pedro M. Baeza b8d818967b [REF+IMP] account_payment_order: Use native payments
The previous approach creates manually the journal entries and does all
the hard work, plus not being 100% compatible with the bank statement
reconciliation widget (requiring a patch on OCB to see blue lines).

That decision made sense on the moment it was done (v9), where the
native payment model (account.payment) was very limited, and wasn't able
to store all the needed information for the bank transaction.

Now that the limitations are gone, we can get rid off this extra model,
and generate instead `account.payment` records, using both the native
model + methods to perform the same operations.

This serves also to workaround the problem found in #966.

All the code, views and tests of main module have been adapted to this
new approach in this commit. Later commits will adapt the rest of the
modules of the suite, and add migration scripts to transit from the
previous approach to this new one.

TT39832
2022-12-25 12:51:03 +01:00

442 lines
16 KiB
Python

# © 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,
)
payment_type = fields.Selection(
selection=[("inbound", "Inbound"), ("outbound", "Outbound")],
string="Payment Type",
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_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 (%s) is not the same as the payment "
"type of the payment mode (%s)"
)
% (order.payment_type, 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 %s, the Payment Execution Date "
"is in the past (%s)."
)
% (order.name, 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("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
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):
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
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 '%s' has the option "
"'Disallow Debit Before Maturity Date'. The "
"payment line %s has a maturity date %s "
"which is after the computed payment date %s."
)
% (
order.payment_mode_id.name,
payline.name,
payline.ml_maturity_date,
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 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 '%s' is negative " "or null (%.2f) !")
% (paydict["paylines"][0].partner_id.name, 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)"""
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):
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