mirror of
https://github.com/OCA/pms.git
synced 2025-01-29 00:17:45 +02:00
395 lines
15 KiB
Python
395 lines
15 KiB
Python
# Copyright 2017 Dario Lodeiros
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
import itertools as it
|
|
import json
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.exceptions import UserError, ValidationError
|
|
|
|
|
|
class AccountMove(models.Model):
|
|
_inherit = "account.move"
|
|
_check_pms_properties_auto = True
|
|
|
|
folio_ids = fields.Many2many(
|
|
string="Folios",
|
|
help="Folios where the account move are included",
|
|
comodel_name="pms.folio",
|
|
compute="_compute_folio_origin",
|
|
relation="account_move_folio_ids_rel",
|
|
column1="account_move_id",
|
|
column2="folio_ids_id",
|
|
store=True,
|
|
readonly=False,
|
|
)
|
|
pms_property_id = fields.Many2one(
|
|
string="Property",
|
|
help="The property associated to the account move",
|
|
comodel_name="pms.property",
|
|
compute="_compute_pms_property_id",
|
|
store=True,
|
|
readonly=False,
|
|
# check_pms_properties=True,
|
|
)
|
|
# journal_id = fields.Many2one(check_pms_properties=True)
|
|
is_simplified_invoice = fields.Boolean(
|
|
help="Technical field to know if the invoice is simplified",
|
|
related="journal_id.is_simplified_invoice",
|
|
store=True,
|
|
)
|
|
origin_agency_id = fields.Many2one(
|
|
string="Origin Agency",
|
|
help="The agency where the folio account move originates",
|
|
comodel_name="res.partner",
|
|
domain="[('is_agency', '=', True)]",
|
|
compute="_compute_origin_agency_id",
|
|
store=True,
|
|
readonly=False,
|
|
)
|
|
|
|
@api.onchange("pms_property_id")
|
|
def _onchange_pms_property_id(self):
|
|
for move in self:
|
|
journals = self.env["account.journal"].search(
|
|
[
|
|
("pms_property_ids", "=", move.pms_property_id.id),
|
|
]
|
|
)
|
|
if journals:
|
|
move.journal_id = journals[0]
|
|
else:
|
|
move.journal_id = False
|
|
|
|
@api.depends("journal_id", "folio_ids")
|
|
def _compute_pms_property_id(self):
|
|
for move in self:
|
|
if move.folio_ids:
|
|
move.pms_property_id = move.folio_ids.mapped("pms_property_id")
|
|
elif len(move.journal_id.mapped("pms_property_ids")) == 1:
|
|
move.pms_property_id = move.journal_id.mapped("pms_property_ids")[0]
|
|
elif not move.journal_id.pms_property_ids:
|
|
move.pms_property_id = False
|
|
elif not move.pms_property_id:
|
|
move.pms_property_id = False
|
|
|
|
@api.depends("line_ids", "line_ids.folio_ids")
|
|
def _compute_folio_origin(self):
|
|
for move in self:
|
|
move.folio_ids = False
|
|
move.folio_ids = move.mapped("line_ids.folio_ids.id")
|
|
|
|
@api.depends("line_ids", "line_ids.origin_agency_id")
|
|
def _compute_origin_agency_id(self):
|
|
"""
|
|
Compute the origin agency of the account move
|
|
if the move has multiple agencies in origin,
|
|
the first one is returned (REVIEW: is this correct?)
|
|
"""
|
|
self.origin_agency_id = False
|
|
for move in self:
|
|
agencies = move.mapped("line_ids.origin_agency_id")
|
|
if agencies:
|
|
move.origin_agency_id = agencies[0]
|
|
|
|
def _compute_payments_widget_to_reconcile_info(self):
|
|
for move in self:
|
|
if not move.line_ids.folio_line_ids:
|
|
super(AccountMove, move)._compute_payments_widget_to_reconcile_info()
|
|
else:
|
|
move.invoice_outstanding_credits_debits_widget = json.dumps(False)
|
|
move.invoice_has_outstanding = False
|
|
|
|
if (
|
|
move.state != "posted"
|
|
or move.payment_state not in ("not_paid", "partial")
|
|
or not move.is_invoice(include_receipts=True)
|
|
):
|
|
continue
|
|
|
|
pay_term_lines = move.line_ids.filtered(
|
|
lambda line: line.account_id.user_type_id.type
|
|
in ("receivable", "payable")
|
|
)
|
|
|
|
payments_widget_vals = {
|
|
"outstanding": True,
|
|
"content": [],
|
|
"move_id": move.id,
|
|
}
|
|
|
|
if move.is_inbound():
|
|
domain = [("balance", "<", 0.0)]
|
|
payments_widget_vals["title"] = _("Outstanding credits")
|
|
else:
|
|
domain = [("balance", ">", 0.0)]
|
|
payments_widget_vals["title"] = _("Outstanding debits")
|
|
|
|
domain.extend(
|
|
[
|
|
("account_id", "in", pay_term_lines.account_id.ids),
|
|
("parent_state", "=", "posted"),
|
|
("reconciled", "=", False),
|
|
"|",
|
|
("amount_residual", "!=", 0.0),
|
|
("amount_residual_currency", "!=", 0.0),
|
|
"|",
|
|
(
|
|
"folio_ids",
|
|
"in",
|
|
move.line_ids.mapped("folio_line_ids.folio_id.id"),
|
|
),
|
|
("partner_id", "=", move.commercial_partner_id.id),
|
|
]
|
|
)
|
|
|
|
for line in self.env["account.move.line"].search(domain):
|
|
if line.currency_id == move.currency_id:
|
|
# Same foreign currency.
|
|
amount = abs(line.amount_residual_currency)
|
|
else:
|
|
# Different foreign currencies.
|
|
amount = move.company_currency_id._convert(
|
|
abs(line.amount_residual),
|
|
move.currency_id,
|
|
move.company_id,
|
|
line.date,
|
|
)
|
|
|
|
if move.currency_id.is_zero(amount):
|
|
continue
|
|
|
|
payments_widget_vals["content"].append(
|
|
{
|
|
"journal_name": line.ref or line.move_id.name,
|
|
"amount": amount,
|
|
"currency": move.currency_id.symbol,
|
|
"id": line.id,
|
|
"move_id": line.move_id.id,
|
|
"position": move.currency_id.position,
|
|
"digits": [69, move.currency_id.decimal_places],
|
|
"payment_date": fields.Date.to_string(line.date),
|
|
}
|
|
)
|
|
|
|
if not payments_widget_vals["content"]:
|
|
continue
|
|
|
|
move.invoice_outstanding_credits_debits_widget = json.dumps(
|
|
payments_widget_vals
|
|
)
|
|
move.invoice_has_outstanding = True
|
|
|
|
def _search_default_journal(self, journal_types):
|
|
"""
|
|
Search for the default journal based on the journal type and property,
|
|
the parent method is overwritten to add the property filter if
|
|
default_pms_property_id is set in context
|
|
"""
|
|
journal = super(AccountMove, self)._search_default_journal(journal_types)
|
|
company_id = self._context.get("default_company_id", self.env.company.id)
|
|
company = self.env["res.company"].browse(company_id)
|
|
pms_property_id = self.env.context.get(
|
|
"default_pms_property_id", self.pms_property_id.id
|
|
) or (
|
|
self.env.user.get_active_property_ids()
|
|
and self.env.user.get_active_property_ids()[0]
|
|
)
|
|
pms_property = self.env["pms.property"].browse(pms_property_id)
|
|
if pms_property:
|
|
domain = [
|
|
("company_id", "=", pms_property.company_id.id),
|
|
("type", "in", journal_types),
|
|
("pms_property_ids", "in", pms_property.id),
|
|
]
|
|
journal = self.env["account.journal"].search(domain, limit=1)
|
|
if not journal:
|
|
domain = [
|
|
("company_id", "=", pms_property.company_id.id),
|
|
("type", "in", journal_types),
|
|
("pms_property_ids", "=", False),
|
|
]
|
|
journal = self.env["account.journal"].search(domain, limit=1)
|
|
else:
|
|
domain = [
|
|
("company_id", "=", company_id),
|
|
("type", "in", journal_types),
|
|
("pms_property_ids", "=", False),
|
|
]
|
|
journal = self.env["account.journal"].search(domain, limit=1)
|
|
if not journal:
|
|
if pms_property:
|
|
error_msg = _(
|
|
"""No journal could be found in property %(property_name)s
|
|
for any of those types: %(journal_types)s""",
|
|
property_name=pms_property.display_name,
|
|
journal_types=", ".join(journal_types),
|
|
)
|
|
else:
|
|
error_msg = _(
|
|
"""No journal could be found in company %(company_name)s
|
|
for any of those types: %(journal_types)s""",
|
|
company_name=company.display_name,
|
|
journal_types=", ".join(journal_types),
|
|
)
|
|
raise UserError(error_msg)
|
|
return journal
|
|
|
|
@api.depends("pms_property_id")
|
|
def _compute_suitable_journal_ids(self):
|
|
super(AccountMove, self)._compute_suitable_journal_ids()
|
|
for move in self:
|
|
if move.pms_property_id:
|
|
move.suitable_journal_ids = move.suitable_journal_ids.filtered(
|
|
lambda j: not j.pms_property_ids
|
|
or move.pms_property_id.id in j.pms_property_ids.ids
|
|
)
|
|
|
|
def _autoreconcile_folio_payments(self):
|
|
"""
|
|
Reconcile payments with the invoice
|
|
"""
|
|
# TODO: Add setting option to enable automatic payment reconciliation
|
|
for move in self.filtered(lambda m: m.state == "posted"):
|
|
if move.is_invoice(include_receipts=True) and move.folio_ids:
|
|
to_reconcile_payments_widget_vals = json.loads(
|
|
move.invoice_outstanding_credits_debits_widget
|
|
)
|
|
if not to_reconcile_payments_widget_vals:
|
|
continue
|
|
current_amounts = {
|
|
vals["move_id"]: vals["amount"]
|
|
for vals in to_reconcile_payments_widget_vals["content"]
|
|
}
|
|
pay_term_lines = move.line_ids.filtered(
|
|
lambda line: line.account_id.user_type_id.type
|
|
in ("receivable", "payable")
|
|
)
|
|
to_propose = (
|
|
self.env["account.move"]
|
|
.browse(list(current_amounts.keys()))
|
|
.line_ids.filtered(
|
|
lambda line: line.account_id == pay_term_lines.account_id
|
|
and line.folio_ids in move.folio_ids
|
|
and (
|
|
line.move_id.partner_id == move.partner_id
|
|
or not line.move_id.partner_id
|
|
)
|
|
)
|
|
)
|
|
to_reconcile = self.match_pays_by_amount(
|
|
payments=to_propose, invoice=move
|
|
)
|
|
if to_reconcile:
|
|
(pay_term_lines + to_reconcile).reconcile()
|
|
# Set partner in payment
|
|
for record in to_reconcile:
|
|
if record.payment_id and not record.payment_id.partner_id:
|
|
record.payment_id.partner_id = move.partner_id
|
|
if (
|
|
record.statement_line_id
|
|
and not record.statement_line_id.partner_id
|
|
):
|
|
record.statement_line_id.partner_id = move.partner_id
|
|
return True
|
|
|
|
def _post(self, soft=True):
|
|
"""
|
|
Overwrite the original method to add the folio_ids to the invoice
|
|
"""
|
|
for record in self:
|
|
record._check_pms_valid_invoice(record)
|
|
res = super(AccountMove, self)._post(soft)
|
|
self._autoreconcile_folio_payments()
|
|
return res
|
|
|
|
def match_pays_by_amount(self, payments, invoice):
|
|
"""
|
|
Match payments by amount
|
|
"""
|
|
for i in range(len(payments)):
|
|
combinations = list(it.combinations(payments, i + 1))
|
|
for combi in combinations:
|
|
# TODO: compare with currency differences
|
|
if sum(abs(item.balance) for item in combi) == invoice.amount_residual:
|
|
return payments.filtered(
|
|
lambda p: p.id in [item.id for item in combi]
|
|
)
|
|
return []
|
|
|
|
@api.model
|
|
def _check_pms_valid_invoice(self, move):
|
|
"""
|
|
Check invoice and receipts legal status
|
|
"""
|
|
if (
|
|
move.company_id.check_min_partner_data_invoice
|
|
and move.is_invoice(include_receipts=True)
|
|
and not move.journal_id.is_simplified_invoice
|
|
and (
|
|
not move.partner_id or not move.partner_id._check_enought_invoice_data()
|
|
)
|
|
):
|
|
raise UserError(
|
|
_(
|
|
"You cannot validate this invoice. Please check the "
|
|
" partner has the complete information required."
|
|
)
|
|
)
|
|
if move.journal_id.is_simplified_invoice:
|
|
move._check_simplified_restrictions()
|
|
return True
|
|
|
|
def _check_simplified_restrictions(self):
|
|
self.ensure_one()
|
|
if (
|
|
self.pms_property_id
|
|
and self.amount_total > self.pms_property_id.max_amount_simplified_invoice
|
|
and (
|
|
not self.pms_property_id.avoid_simplified_max_amount_downpayment
|
|
or not self._is_downpayment()
|
|
)
|
|
):
|
|
mens = _(
|
|
"The total amount of the simplified invoice is higher than the "
|
|
"maximum amount allowed for simplified invoices."
|
|
)
|
|
if self.folio_ids:
|
|
self.folio_ids.message_post(body=mens)
|
|
raise ValidationError(mens)
|
|
return True
|
|
|
|
def _proforma_access_url(self):
|
|
self.ensure_one()
|
|
if self.is_invoice(include_receipts=True):
|
|
return "/my/invoices/proforma/%s" % (self.id)
|
|
else:
|
|
return False
|
|
|
|
def get_proforma_portal_url(
|
|
self,
|
|
suffix=None,
|
|
report_type=None,
|
|
download=None,
|
|
query_string=None,
|
|
anchor=None,
|
|
):
|
|
"""
|
|
Get a proforma portal url for this model, including access_token.
|
|
The associated route must handle the flags for them to have any effect.
|
|
- suffix: string to append to the url, before the query string
|
|
- report_type: report_type query string, often one of: html, pdf, text
|
|
- download: set the download query string to true
|
|
- query_string: additional query string
|
|
- anchor: string to append after the anchor #
|
|
"""
|
|
self.ensure_one()
|
|
url = self._proforma_access_url() + "%s?access_token=%s%s%s%s%s" % (
|
|
suffix if suffix else "",
|
|
self._portal_ensure_token(),
|
|
"&report_type=%s" % report_type if report_type else "",
|
|
"&download=true" if download else "",
|
|
query_string if query_string else "",
|
|
"#%s" % anchor if anchor else "",
|
|
)
|
|
return url
|