You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
471 lines
23 KiB
471 lines
23 KiB
# -*- coding: utf-8 -*-
|
|
#############################################################################
|
|
#
|
|
# Cybrosys Technologies Pvt. Ltd.
|
|
#
|
|
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
|
|
# Author: SREERAG PM (<https://www.cybrosys.com>)
|
|
#
|
|
# You can modify it under the terms of the GNU AFFERO
|
|
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
|
|
#
|
|
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
|
|
# (AGPL v3) along with this program.
|
|
# If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
#############################################################################
|
|
from dateutil.relativedelta import relativedelta
|
|
from odoo import api, fields, models, SUPERUSER_ID, _
|
|
from odoo.exceptions import UserError
|
|
|
|
|
|
class SubscriptionPackage(models.Model):
|
|
"""Subscription Package Model"""
|
|
_name = 'subscription.package'
|
|
_description = 'Subscription Package'
|
|
_rec_name = 'name'
|
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
|
|
|
@api.model
|
|
def _read_group_stage_ids(self, stages, domain):
|
|
""" Read all the stages and display it in the kanban view,
|
|
even if it is empty."""
|
|
stages_ids = stages.sudo()._search([], order=stages._order)
|
|
return stages.browse(stages_ids)
|
|
|
|
def _default_stage_id(self):
|
|
"""Setting default stage"""
|
|
rec = self.env['subscription.package.stage'].search([], limit=1,
|
|
order='sequence ASC')
|
|
return rec.id if rec else None
|
|
|
|
name = fields.Char(string='Name', default="New", compute='_compute_name',
|
|
store=True, required=True,
|
|
help='Choose the name for the subscription package.')
|
|
partner_id = fields.Many2one('res.partner', string='Customer',
|
|
help='Select the customer associated with '
|
|
'this record.')
|
|
partner_invoice_id = fields.Many2one('res.partner',
|
|
help='Select the invoice address '
|
|
'associated with this record.',
|
|
string='Invoice Address',
|
|
related='partner_id')
|
|
partner_shipping_id = fields.Many2one('res.partner',
|
|
help="Add shipping/service address",
|
|
string='Shipping/Service Address',
|
|
related='partner_id')
|
|
plan_id = fields.Many2one('subscription.package.plan',
|
|
string='Subscription Plan',
|
|
help="Choose the subscription package plan")
|
|
start_date = fields.Date(string='Period Start Date',
|
|
help='Add the period start date',
|
|
ondelete='restrict')
|
|
date_started = fields.Date(string='Subsciption Start date',
|
|
help='Add the Subscription package start date',
|
|
ondelete='restrict', readonly=True)
|
|
next_invoice_date = fields.Date(string='Next Invoice Date',
|
|
store=True, help='Add next invoice date',
|
|
compute="_compute_next_invoice_date",
|
|
inverse="_inverse_next_invoice_date")
|
|
company_id = fields.Many2one('res.company', string='Company',
|
|
help='Select the company',
|
|
default=lambda self: self.env.company,
|
|
required=True)
|
|
user_id = fields.Many2one('res.users', string='Sales Person',
|
|
help='Add the Sales person',
|
|
default=lambda self: self.env.user)
|
|
sale_order_id = fields.Many2one('sale.order', string="Sale Order",
|
|
help='Select the sale order', copy=False)
|
|
is_to_renew = fields.Boolean(string='To Renew', copy=True,
|
|
help='Is subscription package is renew')
|
|
tag_ids = fields.Many2many('account.account.tag', string='Tags',
|
|
help='Add the tags')
|
|
stage_id = fields.Many2one('subscription.package.stage', string='Stage',
|
|
default=lambda self: self._default_stage_id(),
|
|
index=True,
|
|
group_expand='_read_group_stage_ids',
|
|
help='Subscription Package stage', copy=False)
|
|
invoice_count = fields.Integer(string='Invoices',
|
|
help='Subscription package invoice count',
|
|
compute='_compute_invoice_count')
|
|
so_count = fields.Integer(string='Sales',
|
|
help='subscription package sales count',
|
|
compute='_compute_sale_count')
|
|
description = fields.Text(string='Description',
|
|
help='Subscription package description')
|
|
analytic_account_id = fields.Many2one('account.analytic.account',
|
|
help='Choose the analytic account',
|
|
string='Analytic Account')
|
|
product_line_ids = fields.One2many('subscription.package.product.line',
|
|
'subscription_id', ondelete='restrict',
|
|
string='Products Line',
|
|
help='Subscription package product line')
|
|
currency_id = fields.Many2one('res.currency', string='Currency',
|
|
readonly=True, default=lambda
|
|
self: self.env.company.currency_id, help='Add Currency')
|
|
current_stage = fields.Char(string='Current Stage', default='Draft',
|
|
help='Current stage of the '
|
|
'subscription package. '
|
|
'This field is computed based on '
|
|
'the associated stage_id.',
|
|
store=True, compute='_compute_current_stage')
|
|
reference_code = fields.Char(string='Reference',
|
|
help='This field represents the '
|
|
'reference code associated '
|
|
'with the record.')
|
|
is_closed = fields.Boolean(string="Closed", default=False,
|
|
help='Is Closed')
|
|
close_reason_id = fields.Many2one('subscription.package.stop',
|
|
help='The reason for c'
|
|
'losing the subscription package.',
|
|
string='Close Reason')
|
|
closed_by = fields.Many2one('res.users', string='Closed By',
|
|
help="The user responsible "
|
|
"for closing the record")
|
|
close_date = fields.Date(string='Closed on',
|
|
help="The date on which the record was closed")
|
|
stage_category = fields.Selection(related='stage_id.category',
|
|
help="The category associated with "
|
|
"the current stage of the record. ",
|
|
store=True)
|
|
invoice_mode = fields.Selection(related="plan_id.invoice_mode",
|
|
help="The invoice mode "
|
|
"associated with the plan.")
|
|
total_recurring_price = fields.Float(string='Untaxed Amount',
|
|
help="The total recurring "
|
|
"price excluding taxes.",
|
|
compute='_compute_total_recurring_price',
|
|
store=True)
|
|
tax_total = fields.Float("Taxes", readonly=True,
|
|
help="The total amount of "
|
|
"taxes associated with the record")
|
|
total_with_tax = fields.Monetary("Total Recurring Price", readonly=True,
|
|
help="The total recurring "
|
|
"price including taxes")
|
|
recurrence_period_id = fields.Many2one("recurrence.period",
|
|
string="Recurrence Period")
|
|
sale_order_count = fields.Integer(string='Sale Order Count',
|
|
help="The count of associated "
|
|
"sale orders for this record.")
|
|
|
|
def _valid_field_parameter(self, field, name):
|
|
"""Check the validity of a field parameter for a specific field."""
|
|
if name == 'ondelete':
|
|
return True
|
|
return super(SubscriptionPackage,
|
|
self)._valid_field_parameter(field, name)
|
|
|
|
@api.depends('invoice_count')
|
|
def _compute_invoice_count(self):
|
|
""" Calculate Invoice count based on subscription package """
|
|
sale_id = self.env['sale.order'].search(
|
|
[('id', '=', self.sale_order_id.id)])
|
|
invoices = sale_id.order_line.invoice_lines.move_id.filtered(
|
|
lambda r: r.move_type in ('out_invoice', 'out_refund'))
|
|
invoices.write({'subscription_id': self.id})
|
|
invoice_count = self.env['account.move'].search_count(
|
|
[('subscription_id', '=', self.id)])
|
|
if invoice_count > 0:
|
|
self.invoice_count = invoice_count
|
|
else:
|
|
self.invoice_count = 0
|
|
|
|
@api.depends('so_count')
|
|
def _compute_sale_count(self):
|
|
""" Calculate sale order count based on subscription package """
|
|
self.so_count = self.env['sale.order'].search_count(
|
|
[('id', '=', self.sale_order_id.id)])
|
|
|
|
@api.depends('stage_id')
|
|
def _compute_current_stage(self):
|
|
""" It displays current stage for subscription package """
|
|
for rec in self:
|
|
rec.current_stage = rec.env['subscription.package.stage'].search(
|
|
[('id', '=', rec.stage_id.id)]).category
|
|
|
|
@api.depends('start_date')
|
|
def _compute_next_invoice_date(self):
|
|
"""The compute function is the next invoice date for subscription
|
|
packages based on the start date and renewal time."""
|
|
for sub in self.env['subscription.package'].search([]):
|
|
if sub.start_date:
|
|
sub.next_invoice_date = sub.start_date + relativedelta(
|
|
days=sub.plan_id.renewal_time)
|
|
|
|
def _inverse_next_invoice_date(self):
|
|
"""Inverse function for next invoice date"""
|
|
for sub in self.env['subscription.package'].search([]):
|
|
if sub.start_date:
|
|
return
|
|
|
|
def button_invoice_count(self):
|
|
""" It displays invoice based on subscription package """
|
|
return {
|
|
'name': 'Invoices',
|
|
'domain': [('subscription_id', '=', self.id)],
|
|
'view_type': 'form',
|
|
'res_model': 'account.move',
|
|
'view_mode': 'list,form',
|
|
'type': 'ir.actions.act_window',
|
|
'context': {
|
|
"create": False
|
|
}
|
|
}
|
|
|
|
def button_sale_count(self):
|
|
""" It displays sale order based on subscription package """
|
|
return {
|
|
'name': 'Products',
|
|
'domain': [('id', '=', self.sale_order_id.id)],
|
|
'view_type': 'form',
|
|
'res_model': 'sale.order',
|
|
'view_mode': 'list,form',
|
|
'type': 'ir.actions.act_window',
|
|
'context': {
|
|
"create": False
|
|
}
|
|
}
|
|
|
|
def button_close(self):
|
|
""" Button for subscription close wizard """
|
|
return {
|
|
'name': "Subscription Close Reason",
|
|
'type': 'ir.actions.act_window',
|
|
'view_type': 'form',
|
|
'view_mode': 'form',
|
|
'res_model': 'subscription.close',
|
|
'target': 'new'
|
|
}
|
|
|
|
def button_start_date(self):
|
|
"""Button to start subscription package"""
|
|
stage_id = (self.env['subscription.package.stage'].search([
|
|
('category', '=', 'progress')], limit=1).id)
|
|
for rec in self:
|
|
if len(rec.env['subscription.package.stage'].search(
|
|
[('category', '=', 'draft')])) > 1:
|
|
raise UserError(
|
|
_('More than one stage is having category "Draft". '
|
|
'Please change category of stage to "In Progress", '
|
|
'only one stage is allowed to have category "Draft"'))
|
|
else:
|
|
if not rec.product_line_ids:
|
|
raise UserError("Empty order lines !! Please add the "
|
|
"subscription product.")
|
|
else:
|
|
if rec.sale_order_id:
|
|
rec.sale_order_id.write({'subscription_id': rec.id,
|
|
'is_subscription': True})
|
|
for line in rec.sale_order_id.order_line.filtered(
|
|
lambda x: x.product_template_id.is_subscription == True):
|
|
line.qty_to_invoice = line.product_uom_qty
|
|
rec.write(
|
|
{'stage_id': stage_id,
|
|
'date_started': fields.Date.today(),
|
|
'start_date': fields.Date.today()})
|
|
|
|
def button_sale_order(self):
|
|
"""Button to create sale order"""
|
|
this_products_line = []
|
|
for rec in self.product_line_ids:
|
|
rec_list = [0, 0, {'product_id': rec.product_id.id,
|
|
'product_uom_qty': rec.product_qty,
|
|
'discount': rec.discount}]
|
|
this_products_line.append(rec_list)
|
|
orders = self.env['sale.order'].search(
|
|
[('id', '=', self.sale_order_count),
|
|
('invoice_status', '=', 'no')])
|
|
if orders:
|
|
for order in orders:
|
|
order.action_confirm()
|
|
so_id = self.env['sale.order'].create({
|
|
'id': self.sale_order_count,
|
|
'partner_id': self.partner_id.id,
|
|
'partner_invoice_id': self.partner_id.id,
|
|
'partner_shipping_id': self.partner_id.id,
|
|
'is_subscription': True,
|
|
'subscription_id': self.id,
|
|
'order_line': this_products_line
|
|
})
|
|
self.sale_order_id = so_id
|
|
return {
|
|
'name': _('Sales Orders'),
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'sale.order',
|
|
'domain': [('id', '=', so_id.id)],
|
|
'view_mode': 'list,form',
|
|
'context': {
|
|
"create": False
|
|
}
|
|
}
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
"""It displays subscription product in partner and generate sequence"""
|
|
for vals in vals_list:
|
|
partner = self.env['res.partner'].search(
|
|
[('id', '=', vals.get('partner_id'))])
|
|
partner.is_active_subscription = True
|
|
if vals.get('reference_code', 'New') is False:
|
|
vals['reference_code'] = self.env['ir.sequence'].next_by_code(
|
|
'sequence.reference.code') or 'New'
|
|
create_id = super().create(vals)
|
|
return create_id
|
|
|
|
@api.depends('reference_code')
|
|
def _compute_name(self):
|
|
"""It displays record name as combination of short code, reference
|
|
code and partner name """
|
|
for rec in self:
|
|
plan_id = self.env['subscription.package.plan'].search(
|
|
[('id', '=', rec.plan_id.id)])
|
|
if plan_id.short_code and rec.reference_code:
|
|
rec.name = plan_id.short_code + '/' + rec.reference_code + '-' + rec.partner_id.name
|
|
|
|
def set_close(self):
|
|
""" Button to close subscription package """
|
|
stage = self.env['subscription.package.stage'].search(
|
|
[('category', '=', 'closed')], limit=1).id
|
|
for sub in self:
|
|
values = {'stage_id': stage, 'is_to_renew': False}
|
|
sub.write(values)
|
|
return True
|
|
|
|
def send_renew_alert_mail(self, today, renew_date, sub_id):
|
|
"""The function is used to send a renewal alert email and mark the
|
|
subscription for renewal if today is the renewal date."""
|
|
if today == renew_date:
|
|
self.env.ref(
|
|
'subscription_package'
|
|
'.mail_template_subscription_renew').send_mail(
|
|
sub_id, force_send=True)
|
|
subscription = self.env['subscription.package'].browse(sub_id)
|
|
subscription.write({'is_to_renew': True})
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def find_renew_date(self, next_invoice, date_started, end):
|
|
"""The function is used to calculate the renewal date, end date,
|
|
and close date based on subscription details."""
|
|
if end == 0:
|
|
end_date = next_invoice
|
|
difference = (next_invoice - date_started).days / 10
|
|
renew_date = next_invoice - relativedelta(
|
|
days=difference)
|
|
close_date = next_invoice
|
|
else:
|
|
end_date = fields.Date.add(date_started,
|
|
days=end)
|
|
close = date_started + relativedelta(days=end)
|
|
difference = (close - date_started).days / 10
|
|
renew_date = close - relativedelta(
|
|
days=difference)
|
|
close_date = close
|
|
|
|
data = {'renew_date': renew_date,
|
|
'end_date': end_date,
|
|
'close_date': close_date}
|
|
return data
|
|
|
|
def close_limit_cron(self):
|
|
""" It Checks renew date, close date. It will send mail when renew
|
|
date and also generates invoices based on the plan. It wil close the
|
|
subscription automatically if renewal limit is exceeded"""
|
|
pending_subscriptions = self.env['subscription.package'].search(
|
|
[('stage_category', '=', 'progress')])
|
|
today_date = fields.Date.today()
|
|
pending_subscription = False
|
|
for pending_subscription in pending_subscriptions:
|
|
get_dates = self.find_renew_date(
|
|
pending_subscription.next_invoice_date,
|
|
pending_subscription.date_started,
|
|
pending_subscription.plan_id.days_to_end)
|
|
renew_date = get_dates['renew_date']
|
|
end_date = get_dates['end_date']
|
|
pending_subscription.close_date = get_dates['close_date']
|
|
if today_date == pending_subscription.next_invoice_date:
|
|
if pending_subscription.plan_id.invoice_mode == 'draft_invoice':
|
|
this_products_line = []
|
|
for rec in pending_subscription.product_line_ids:
|
|
rec_list = [0, 0, {'product_id': rec.product_id.id,
|
|
'quantity': rec.product_qty,
|
|
'price_unit': rec.unit_price,
|
|
'discount': rec.discount,
|
|
'tax_ids': rec.tax_ids
|
|
}]
|
|
this_products_line.append(rec_list)
|
|
self.env['account.move'].create(
|
|
{
|
|
'move_type': 'out_invoice',
|
|
'invoice_date_due': today_date,
|
|
'invoice_payment_term_id': False,
|
|
'invoice_date': today_date,
|
|
'state': 'draft',
|
|
'subscription_id': pending_subscription.id,
|
|
'partner_id': pending_subscription.partner_invoice_id.id,
|
|
'currency_id': pending_subscription.partner_invoice_id.currency_id.id,
|
|
'invoice_line_ids': this_products_line
|
|
})
|
|
pending_subscription.write({
|
|
'is_to_renew': False,
|
|
'start_date': pending_subscription.next_invoice_date})
|
|
new_date = self.find_renew_date(
|
|
pending_subscription.next_invoice_date,
|
|
pending_subscription.date_started,
|
|
pending_subscription.plan_id.days_to_end)
|
|
pending_subscription.write(
|
|
{'close_date': new_date['close_date']})
|
|
self.send_renew_alert_mail(today_date,
|
|
new_date['renew_date'],
|
|
pending_subscription.id)
|
|
|
|
if (today_date == end_date) and (
|
|
pending_subscription.plan_id.limit_choice != 'manual'):
|
|
display_msg = ("<h5><i>The renewal limit has been exceeded "
|
|
"today for this subscription based on the "
|
|
"current subscription plan.</i></h5>")
|
|
pending_subscription.message_post(body=display_msg)
|
|
pending_subscription.is_closed = True
|
|
reason = (self.env['subscription.package.stop'].search([
|
|
('name', '=', 'Renewal Limit Exceeded')]).id)
|
|
pending_subscription.close_reason_id = reason
|
|
pending_subscription.closed_by = self.user_id
|
|
pending_subscription.close_date = fields.Date.today()
|
|
stage = (self.env['subscription.package.stage'].search([
|
|
('category', '=', 'closed')]).id)
|
|
values = {'stage_id': stage, 'is_to_renew': False,
|
|
'next_invoice_date': False}
|
|
pending_subscription.write(values)
|
|
|
|
self.send_renew_alert_mail(today_date, renew_date,
|
|
pending_subscription.id)
|
|
|
|
return dict(pending=pending_subscription)
|
|
|
|
@api.depends('product_line_ids.total_amount',
|
|
'product_line_ids.price_total', 'product_line_ids.tax_ids')
|
|
def _compute_total_recurring_price(self):
|
|
""" The compute function used to calculate recurring price """
|
|
for record in self:
|
|
total_recurring = 0
|
|
total_tax = 0.0
|
|
for line in record.product_line_ids:
|
|
if line.total_amount != line.price_total:
|
|
line_tax = line.price_total - line.total_amount
|
|
total_tax += line_tax
|
|
total_recurring += line.total_amount
|
|
record['total_recurring_price'] = total_recurring
|
|
record['tax_total'] = total_tax
|
|
total_with_tax = total_recurring + total_tax
|
|
record['total_with_tax'] = total_with_tax
|
|
|
|
def action_renew(self):
|
|
""" The function is used to perform the renewal
|
|
action for the subscription package."""
|
|
return self.button_sale_order()
|
|
|