mirror of
https://github.com/OCA/contract.git
synced 2025-02-13 17:57:24 +02:00
471 lines
18 KiB
Python
471 lines
18 KiB
Python
# Copyright 2023 Domatix - Carlos Martínez
|
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
|
import logging
|
|
from datetime import date, datetime
|
|
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.exceptions import AccessError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SaleSubscription(models.Model):
|
|
_name = "sale.subscription"
|
|
_description = "Subscription"
|
|
_inherit = ["mail.thread", "mail.activity.mixin"]
|
|
_order = "id desc"
|
|
|
|
color = fields.Integer("Color Index")
|
|
name = fields.Char(
|
|
compute="_compute_name",
|
|
store=True,
|
|
)
|
|
sequence = fields.Integer()
|
|
company_id = fields.Many2one(
|
|
"res.company",
|
|
"Company",
|
|
required=True,
|
|
index=True,
|
|
default=lambda self: self.env.company,
|
|
)
|
|
partner_id = fields.Many2one(
|
|
comodel_name="res.partner", required=True, string="Partner", index=True
|
|
)
|
|
fiscal_position_id = fields.Many2one(
|
|
"account.fiscal.position",
|
|
string="Fiscal Position",
|
|
domain="[('company_id', '=', company_id)]",
|
|
check_company=True,
|
|
)
|
|
active = fields.Boolean(default=True)
|
|
template_id = fields.Many2one(
|
|
comodel_name="sale.subscription.template",
|
|
required=True,
|
|
string="Subscription template",
|
|
)
|
|
code = fields.Char(
|
|
string="Reference",
|
|
default=lambda self: self.env["ir.sequence"].next_by_code("sale.subscription"),
|
|
)
|
|
in_progress = fields.Boolean(string="In progress", default=False)
|
|
recurring_rule_boundary = fields.Boolean(
|
|
string="Boundary", compute="_compute_rule_boundary", store=True
|
|
)
|
|
pricelist_id = fields.Many2one(
|
|
comodel_name="product.pricelist", required=True, string="Pricelist"
|
|
)
|
|
recurring_next_date = fields.Date(string="Next invoice date", default=date.today())
|
|
user_id = fields.Many2one(
|
|
comodel_name="res.users",
|
|
string="Commercial agent",
|
|
default=lambda self: self.env.user.id,
|
|
)
|
|
date_start = fields.Date(string="Start date", default=date.today())
|
|
date = fields.Date(
|
|
string="Finish date",
|
|
compute="_compute_rule_boundary",
|
|
store=True,
|
|
readonly=False,
|
|
)
|
|
description = fields.Text()
|
|
sale_order_id = fields.Many2one(
|
|
comodel_name="sale.order", string="Origin sale order"
|
|
)
|
|
terms = fields.Text(
|
|
string="Terms and conditions",
|
|
compute="_compute_terms",
|
|
store=True,
|
|
readonly=False,
|
|
)
|
|
invoice_ids = fields.One2many(
|
|
comodel_name="account.move",
|
|
inverse_name="subscription_id",
|
|
string="Invoices",
|
|
)
|
|
sale_order_ids = fields.One2many(
|
|
comodel_name="sale.order",
|
|
inverse_name="order_subscription_id",
|
|
string="Orders",
|
|
)
|
|
recurring_total = fields.Monetary(
|
|
compute="_compute_total", string="Recurring price", store=True
|
|
)
|
|
amount_tax = fields.Monetary(compute="_compute_total", store=True)
|
|
amount_total = fields.Monetary(compute="_compute_total", store=True)
|
|
tag_ids = fields.Many2many(comodel_name="sale.subscription.tag", string="Tags")
|
|
image = fields.Binary("Image", related="user_id.image_512", store=True)
|
|
journal_id = fields.Many2one(comodel_name="account.journal", string="Journal")
|
|
currency_id = fields.Many2one(
|
|
related="pricelist_id.currency_id",
|
|
depends=["pricelist_id"],
|
|
store=True,
|
|
ondelete="restrict",
|
|
)
|
|
|
|
@api.model
|
|
def _read_group_stage_ids(self, stages, domain, order):
|
|
stage_ids = stages.search([], order=order)
|
|
return stage_ids
|
|
|
|
stage_id = fields.Many2one(
|
|
comodel_name="sale.subscription.stage",
|
|
string="Stage",
|
|
tracking=True,
|
|
group_expand="_read_group_stage_ids",
|
|
store=True,
|
|
)
|
|
stage_str = fields.Char(
|
|
related="stage_id.name",
|
|
string="Etapa",
|
|
store=True,
|
|
)
|
|
sale_subscription_line_ids = fields.One2many(
|
|
comodel_name="sale.subscription.line",
|
|
inverse_name="sale_subscription_id",
|
|
)
|
|
sale_order_ids_count = fields.Integer(
|
|
compute="_compute_sale_order_ids_count", string="Sale orders"
|
|
)
|
|
account_invoice_ids_count = fields.Integer(
|
|
compute="_compute_account_invoice_ids_count", string="Invoice Count"
|
|
)
|
|
close_reason_id = fields.Many2one(
|
|
comodel_name="sale.subscription.close.reason", string="Close Reason"
|
|
)
|
|
crm_team_id = fields.Many2one(comodel_name="crm.team", string="Sale team")
|
|
to_renew = fields.Boolean(default=False, string="To renew")
|
|
|
|
def cron_subscription_management(self):
|
|
today = date.today()
|
|
for subscription in self.search([]):
|
|
if subscription.in_progress:
|
|
if (
|
|
subscription.recurring_next_date == today
|
|
and subscription.sale_subscription_line_ids
|
|
):
|
|
try:
|
|
subscription.generate_invoice()
|
|
except Exception:
|
|
logger.exception("Error on subscription invoice generate")
|
|
if not subscription.recurring_rule_boundary:
|
|
if subscription.date == today:
|
|
subscription.action_close_subscription()
|
|
|
|
else:
|
|
if subscription.date_start == today:
|
|
subscription.action_start_subscription()
|
|
subscription.generate_invoice()
|
|
|
|
@api.depends("sale_subscription_line_ids")
|
|
def _compute_total(self):
|
|
for record in self:
|
|
recurring_total = amount_tax = 0.0
|
|
for order_line in record.sale_subscription_line_ids:
|
|
recurring_total += order_line.price_subtotal
|
|
amount_tax += order_line.amount_tax_line_amount
|
|
record.update(
|
|
{
|
|
"recurring_total": recurring_total,
|
|
"amount_tax": amount_tax,
|
|
"amount_total": recurring_total + amount_tax,
|
|
}
|
|
)
|
|
|
|
@api.depends("template_id", "code")
|
|
def _compute_name(self):
|
|
for record in self:
|
|
template_code = record.template_id.code if record.template_id.code else ""
|
|
code = record.code if record.code else ""
|
|
slash = "/" if template_code and code else ""
|
|
record.name = f"{template_code}{slash}{code}"
|
|
|
|
@api.depends("template_id", "date_start")
|
|
def _compute_rule_boundary(self):
|
|
for record in self:
|
|
if record.template_id.recurring_rule_boundary == "unlimited":
|
|
record.date = False
|
|
record.recurring_rule_boundary = True
|
|
else:
|
|
record.date = (
|
|
relativedelta(months=+record.template_id.recurring_rule_count)
|
|
+ record.date_start
|
|
)
|
|
record.recurring_rule_boundary = False
|
|
|
|
@api.depends("template_id")
|
|
def _compute_terms(self):
|
|
for record in self:
|
|
record.terms = record.template_id.description
|
|
|
|
@api.onchange("template_id", "date_start")
|
|
def _onchange_template_id(self):
|
|
today = date.today()
|
|
if self.date_start:
|
|
today = self.date_start
|
|
if self.template_id and self.account_invoice_ids_count > 0:
|
|
self.calculate_recurring_next_date(self.recurring_next_date)
|
|
else:
|
|
self.calculate_recurring_next_date(today)
|
|
|
|
def calculate_recurring_next_date(self, start_date):
|
|
if self.account_invoice_ids_count == 0:
|
|
self.recurring_next_date = date.today()
|
|
else:
|
|
type_interval = self.template_id.recurring_rule_type
|
|
interval = int(self.template_id.recurring_interval)
|
|
self.recurring_next_date = start_date + relativedelta(
|
|
**{type_interval: interval}
|
|
)
|
|
|
|
@api.onchange("partner_id")
|
|
def onchange_partner_id(self):
|
|
self.pricelist_id = self.partner_id.property_product_pricelist
|
|
|
|
@api.onchange("partner_id", "company_id")
|
|
def onchange_partner_id_fpos(self):
|
|
self.fiscal_position_id = (
|
|
self.env["account.fiscal.position"]
|
|
.with_company(self.company_id)
|
|
._get_fiscal_position(self.partner_id)
|
|
)
|
|
|
|
def action_start_subscription(self):
|
|
self.close_reason_id = False
|
|
in_progress_stage = self.env["sale.subscription.stage"].search(
|
|
[("type", "=", "in_progress")], limit=1
|
|
)
|
|
self.stage_id = in_progress_stage
|
|
|
|
def action_close_subscription(self):
|
|
self.recurring_next_date = False
|
|
return {
|
|
"view_type": "form",
|
|
"view_mode": "form",
|
|
"res_model": "close.reason.wizard",
|
|
"type": "ir.actions.act_window",
|
|
"target": "new",
|
|
"res_id": False,
|
|
}
|
|
|
|
def _prepare_sale_order(self, line_ids=False):
|
|
self.ensure_one()
|
|
return {
|
|
"partner_id": self.partner_id.id,
|
|
"fiscal_position_id": self.fiscal_position_id.id,
|
|
"date_order": datetime.now(),
|
|
"payment_term_id": self.partner_id.property_payment_term_id.id,
|
|
"user_id": self.user_id.id,
|
|
"origin": self.name,
|
|
"order_line": line_ids,
|
|
}
|
|
|
|
def _prepare_account_move(self, line_ids):
|
|
self.ensure_one()
|
|
values = {
|
|
"partner_id": self.partner_id.id,
|
|
"invoice_date": self.recurring_next_date,
|
|
"invoice_payment_term_id": self.partner_id.property_payment_term_id.id,
|
|
"invoice_origin": self.name,
|
|
"invoice_user_id": self.user_id.id,
|
|
"partner_bank_id": self.company_id.partner_id.bank_ids[:1].id,
|
|
"invoice_line_ids": line_ids,
|
|
}
|
|
if self.journal_id:
|
|
values["journal_id"] = self.journal_id.id
|
|
return values
|
|
|
|
def create_invoice(self):
|
|
if not self.env["account.move"].check_access_rights("create", False):
|
|
try:
|
|
self.check_access_rights("write")
|
|
self.check_access_rule("write")
|
|
except AccessError:
|
|
return self.env["account.move"]
|
|
line_ids = []
|
|
for line in self.sale_subscription_line_ids:
|
|
line_values = line._prepare_account_move_line()
|
|
line_ids.append((0, 0, line_values))
|
|
invoice_values = self._prepare_account_move(line_ids)
|
|
invoice_id = (
|
|
self.env["account.move"]
|
|
.sudo()
|
|
.with_context(default_move_type="out_invoice", journal_type="sale")
|
|
.create(invoice_values)
|
|
)
|
|
self.write({"invoice_ids": [(4, invoice_id.id)]})
|
|
return invoice_id
|
|
|
|
def create_sale_order(self):
|
|
if not self.env["sale.order"].check_access_rights("create", False):
|
|
try:
|
|
self.check_access_rights("write")
|
|
self.check_access_rule("write")
|
|
except AccessError:
|
|
return self.env["sale.order"]
|
|
line_ids = []
|
|
for line in self.sale_subscription_line_ids:
|
|
line_values = line._prepare_sale_order_line()
|
|
line_ids.append((0, 0, line_values))
|
|
values = self._prepare_sale_order(line_ids)
|
|
order_id = self.env["sale.order"].sudo().create(values)
|
|
self.write({"sale_order_ids": [(4, order_id.id)]})
|
|
return order_id
|
|
|
|
def generate_invoice(self):
|
|
invoice_number = ""
|
|
msg_static = _("Created invoice with reference")
|
|
if self.template_id.invoicing_mode in ["draft", "invoice", "invoice_send"]:
|
|
invoice = self.create_invoice()
|
|
if self.template_id.invoicing_mode != "draft":
|
|
invoice.action_post()
|
|
if self.template_id.invoicing_mode == "invoice_send":
|
|
mail_template = self.template_id.invoice_mail_template_id
|
|
invoice.with_context(force_send=True).message_post_with_template(
|
|
mail_template.id,
|
|
composition_mode="comment",
|
|
email_layout_xmlid="mail.mail_notification_paynow",
|
|
)
|
|
invoice_number = invoice.name
|
|
message_body = (
|
|
"<b>%s</b> <a href=# data-oe-model=account.move data-oe-id=%d>%s</a>"
|
|
% (msg_static, invoice.id, invoice_number)
|
|
)
|
|
|
|
if self.template_id.invoicing_mode == "sale_and_invoice":
|
|
order_id = self.create_sale_order()
|
|
order_id.action_done()
|
|
new_invoice = order_id._create_invoices()
|
|
new_invoice.action_post()
|
|
new_invoice.invoice_origin = order_id.name + ", " + self.name
|
|
invoice_number = new_invoice.name
|
|
message_body = (
|
|
"<b>%s</b> <a href=# data-oe-model=account.move data-oe-id=%d>%s</a>"
|
|
% (msg_static, new_invoice.id, invoice_number)
|
|
)
|
|
if not invoice_number:
|
|
invoice_number = _("To validate")
|
|
message_body = "<b>%s</b> %s" % (msg_static, invoice_number)
|
|
self.calculate_recurring_next_date(self.recurring_next_date)
|
|
self.message_post(body=message_body)
|
|
|
|
def manual_invoice(self):
|
|
invoice_id = self.create_invoice()
|
|
self.calculate_recurring_next_date(self.recurring_next_date)
|
|
context = dict(self.env.context)
|
|
context["form_view_initial_mode"] = "edit"
|
|
return {
|
|
"name": self.name,
|
|
"views": [
|
|
(self.env.ref("account.view_move_form").id, "form"),
|
|
(self.env.ref("account.view_move_tree").id, "tree"),
|
|
],
|
|
"view_type": "form",
|
|
"view_mode": "form",
|
|
"res_model": "account.move",
|
|
"res_id": invoice_id.id,
|
|
"type": "ir.actions.act_window",
|
|
"context": context,
|
|
}
|
|
|
|
@api.depends("invoice_ids", "sale_order_ids.invoice_ids")
|
|
def _compute_account_invoice_ids_count(self):
|
|
for record in self:
|
|
record.account_invoice_ids_count = len(self.invoice_ids) + len(
|
|
self.sale_order_ids.invoice_ids
|
|
)
|
|
|
|
def action_view_account_invoice_ids(self):
|
|
return {
|
|
"name": self.name,
|
|
"views": [
|
|
(self.env.ref("account.view_move_tree").id, "tree"),
|
|
(self.env.ref("account.view_move_form").id, "form"),
|
|
],
|
|
"view_type": "form",
|
|
"view_mode": "tree,form",
|
|
"res_model": "account.move",
|
|
"type": "ir.actions.act_window",
|
|
"domain": [
|
|
("id", "in", self.invoice_ids.ids + self.sale_order_ids.invoice_ids.ids)
|
|
],
|
|
"context": self.env.context,
|
|
}
|
|
|
|
def _compute_sale_order_ids_count(self):
|
|
data = self.env["sale.order"].read_group(
|
|
domain=[("order_subscription_id", "in", self.ids)],
|
|
fields=["order_subscription_id"],
|
|
groupby=["order_subscription_id"],
|
|
)
|
|
count_dict = {
|
|
item["order_subscription_id"][0]: item["order_subscription_id_count"]
|
|
for item in data
|
|
}
|
|
for record in self:
|
|
record.sale_order_ids_count = count_dict.get(record.id, 0)
|
|
|
|
def action_view_sale_order_ids(self):
|
|
active_ids = self.sale_order_ids.ids
|
|
return {
|
|
"name": self.name,
|
|
"view_type": "form",
|
|
"view_mode": "tree,form",
|
|
"res_model": "sale.order",
|
|
"type": "ir.actions.act_window",
|
|
"domain": [("id", "in", active_ids)],
|
|
"context": self.env.context,
|
|
}
|
|
|
|
def _check_dates(self, start, next_invoice):
|
|
if start and next_invoice:
|
|
date_start = start
|
|
date_next_invoice = next_invoice
|
|
if not isinstance(date_start, date) and not isinstance(
|
|
date_next_invoice, date
|
|
):
|
|
date_start = fields.Date.to_date(start)
|
|
date_next_invoice = fields.Date.to_date(next_invoice)
|
|
if date_start > date_next_invoice:
|
|
return True
|
|
return False
|
|
|
|
def write(self, values):
|
|
res = super().write(values)
|
|
if "stage_id" in values:
|
|
for record in self:
|
|
if record.stage_id:
|
|
if record.stage_id.type == "in_progress":
|
|
record.in_progress = True
|
|
record.date_start = date.today()
|
|
elif record.stage_id.type == "post":
|
|
record.close_reason_id = False
|
|
record.in_progress = False
|
|
else:
|
|
record.in_progress = False
|
|
|
|
return res
|
|
|
|
@api.model
|
|
def create(self, values):
|
|
if "recurring_rule_boundary" in values:
|
|
if not values["recurring_rule_boundary"]:
|
|
template_id = self.env["sale.subscription.template"].browse(
|
|
values["template_id"]
|
|
)
|
|
date_start = values["date_start"]
|
|
if not isinstance(values["date_start"], date):
|
|
date_start = fields.Date.to_date(values["date_start"])
|
|
values["date"] = template_id._get_date(date_start)
|
|
if "date_start" in values and "recurring_next_date" in values:
|
|
res = self._check_dates(values["date_start"], values["recurring_next_date"])
|
|
if res:
|
|
values["date_start"] = values["recurring_next_date"]
|
|
values["stage_id"] = (
|
|
self.env["sale.subscription.stage"]
|
|
.search([("type", "=", "pre")], order="sequence desc", limit=1)
|
|
.id
|
|
)
|
|
return super(SaleSubscription, self).create(values)
|