#2 #3 add membership_payment_date: membership start date -> payment date. Don't overlap membership periods on renewals
This commit is contained in:
parent
54965d483f
commit
d8cd83bdaf
12 changed files with 511 additions and 0 deletions
1
membership_payment_date/models/__init__.py
Normal file
1
membership_payment_date/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import account_move
|
||||
122
membership_payment_date/models/account_move.py
Normal file
122
membership_payment_date/models/account_move.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
# Copyright 2025 Criptomart
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
import math
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = "account.move"
|
||||
|
||||
def _invoice_paid_hook(self):
|
||||
"""Override: update membership_lines dates upon invoice payment.
|
||||
|
||||
RF-01: Use the payment date instead of the invoice date for date_from.
|
||||
RF-02: Renewals: if the member has an active membership, the new one
|
||||
starts the day after the current one expires, avoiding overlap.
|
||||
RF-03: For variable products, recalculate date_to from the new date_from.
|
||||
"""
|
||||
res = super()._invoice_paid_hook()
|
||||
for invoice in self.filtered(lambda m: m.move_type == "out_invoice"):
|
||||
membership_lines = self.env["membership.membership_line"].search(
|
||||
[
|
||||
("account_invoice_line", "in", invoice.invoice_line_ids.ids),
|
||||
("date_cancel", "=", False),
|
||||
]
|
||||
)
|
||||
if not membership_lines:
|
||||
continue
|
||||
payment_date = self._get_membership_payment_date(invoice)
|
||||
for mline in membership_lines:
|
||||
partner = mline.partner
|
||||
product = mline.membership_id
|
||||
quantity = (
|
||||
mline.account_invoice_line.quantity
|
||||
if mline.account_invoice_line
|
||||
else 1.0
|
||||
)
|
||||
date_from, date_to = self._compute_membership_dates(
|
||||
partner,
|
||||
product,
|
||||
payment_date,
|
||||
quantity,
|
||||
exclude_line_ids=membership_lines.ids,
|
||||
)
|
||||
mline.write(
|
||||
{
|
||||
"date_from": date_from,
|
||||
"date_to": date_to,
|
||||
}
|
||||
)
|
||||
return res
|
||||
|
||||
def _get_membership_payment_date(self, invoice):
|
||||
"""Get the date of the most recent reconciled payment.
|
||||
|
||||
If there are no payments (e.g. zero-amount invoice), fallback to today.
|
||||
"""
|
||||
self.ensure_one()
|
||||
payments = invoice.reconciled_payment_ids
|
||||
if payments:
|
||||
return max(payments.mapped("date"))
|
||||
return fields.Date.today()
|
||||
|
||||
def _compute_membership_dates(
|
||||
self, partner, product, payment_date, quantity=1.0,
|
||||
exclude_line_ids=None,
|
||||
):
|
||||
"""Calculate date_from and date_to for a membership line.
|
||||
|
||||
:param partner: res.partner (member)
|
||||
:param product: product.product (membership product)
|
||||
:param payment_date: effective payment date
|
||||
:param quantity: product quantity on the invoice line
|
||||
:param exclude_line_ids: membership line IDs to exclude from the
|
||||
active membership search (to avoid matching the lines we are
|
||||
currently updating)
|
||||
:return: tuple (date_from, date_to)
|
||||
"""
|
||||
domain = [
|
||||
("partner", "=", partner.id),
|
||||
("date_cancel", "=", False),
|
||||
("state", "in", ("paid", "invoiced")),
|
||||
("date_to", ">=", fields.Date.today()),
|
||||
]
|
||||
if exclude_line_ids:
|
||||
domain.append(("id", "not in", exclude_line_ids))
|
||||
# RF-02: Look for an active membership (not cancelled, not expired)
|
||||
active_membership = self.env["membership.membership_line"].search(
|
||||
domain,
|
||||
limit=1,
|
||||
order="date_to desc",
|
||||
)
|
||||
|
||||
if active_membership:
|
||||
# RF-02: Renewal — start the day after the current one expires
|
||||
date_from = active_membership.date_to + timedelta(days=1)
|
||||
else:
|
||||
# RF-01: New membership — use the payment date
|
||||
date_from = payment_date
|
||||
|
||||
# RF-03: Calculate date_to according to membership type
|
||||
if product.membership_type == "variable":
|
||||
qty = int(math.ceil(quantity))
|
||||
next_date = product._get_next_date(date_from, qty=qty)
|
||||
date_to = next_date and (next_date - timedelta(days=1)) or False
|
||||
else:
|
||||
date_to = product.membership_date_to
|
||||
|
||||
# Safety: respect the product's lower bound
|
||||
if product.membership_date_from and date_from < product.membership_date_from:
|
||||
date_from = product.membership_date_from
|
||||
|
||||
# Safety: ensure date_from does not exceed date_to
|
||||
if date_to and date_from > date_to:
|
||||
if active_membership:
|
||||
date_from = date_to
|
||||
else:
|
||||
date_from = min(payment_date, date_to)
|
||||
|
||||
return date_from, date_to
|
||||
Loading…
Add table
Add a link
Reference in a new issue