mirror of
https://github.com/OCA/contract.git
synced 2025-02-13 17:57:24 +02:00
[15.0][ADD] subscription_oca
This commit is contained in:
11
subscription_oca/models/__init__.py
Normal file
11
subscription_oca/models/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from . import account_move
|
||||
from . import product_template
|
||||
from . import res_partner
|
||||
from . import sale_order
|
||||
from . import sale_order_line
|
||||
from . import sale_subscription
|
||||
from . import sale_subscription_close_reason
|
||||
from . import sale_subscription_line
|
||||
from . import sale_subscription_stage
|
||||
from . import sale_subscription_tag
|
||||
from . import sale_subscription_template
|
||||
12
subscription_oca/models/account_move.py
Normal file
12
subscription_oca/models/account_move.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# Copyright 2023 Domatix - Carlos Martínez
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = "account.move"
|
||||
|
||||
subscription_id = fields.Many2one(
|
||||
comodel_name="sale.subscription", string="Subscription"
|
||||
)
|
||||
12
subscription_oca/models/product_template.py
Normal file
12
subscription_oca/models/product_template.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# Copyright 2023 Domatix - Carlos Martínez
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
_inherit = "product.template"
|
||||
|
||||
subscribable = fields.Boolean(string="Subscribable product")
|
||||
subscription_template_id = fields.Many2one(
|
||||
comodel_name="sale.subscription.template", string="Subscription template"
|
||||
)
|
||||
33
subscription_oca/models/res_partner.py
Normal file
33
subscription_oca/models/res_partner.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# Copyright 2023 Domatix - Carlos Martínez
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class Partner(models.Model):
|
||||
_inherit = "res.partner"
|
||||
|
||||
subscription_ids = fields.One2many(
|
||||
comodel_name="sale.subscription",
|
||||
inverse_name="partner_id",
|
||||
string="Subscriptions",
|
||||
)
|
||||
subscription_count = fields.Integer(
|
||||
required=False,
|
||||
compute="_compute_subscription_count",
|
||||
)
|
||||
|
||||
def _compute_subscription_count(self):
|
||||
for record in self:
|
||||
record.subscription_count = len(record.subscription_ids)
|
||||
|
||||
def action_view_subscription_ids(self):
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "sale.subscription",
|
||||
"domain": [("id", "in", self.subscription_ids.ids)],
|
||||
"name": self.name,
|
||||
"view_mode": "tree,form",
|
||||
"context": {
|
||||
"default_partner_id": self.id,
|
||||
},
|
||||
}
|
||||
83
subscription_oca/models/sale_order.py
Normal file
83
subscription_oca/models/sale_order.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# Copyright 2023 Domatix - Carlos Martínez
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from collections import defaultdict
|
||||
from datetime import date
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = "sale.order"
|
||||
|
||||
subscription_ids = fields.One2many(
|
||||
comodel_name="sale.subscription",
|
||||
inverse_name="sale_order_id",
|
||||
string="Subscriptions",
|
||||
)
|
||||
subscriptions_count = fields.Integer(compute="_compute_subscriptions_count")
|
||||
order_subscription_id = fields.Many2one(
|
||||
comodel_name="sale.subscription", string="Subscription"
|
||||
)
|
||||
|
||||
@api.depends("subscription_ids")
|
||||
def _compute_subscriptions_count(self):
|
||||
for record in self:
|
||||
record.subscriptions_count = len(record.subscription_ids)
|
||||
|
||||
def action_view_subscriptions(self):
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "sale.subscription",
|
||||
"domain": [("id", "in", self.subscription_ids.ids)],
|
||||
"name": self.name,
|
||||
"view_mode": "tree,form",
|
||||
}
|
||||
|
||||
def get_next_interval(self, type_interval, interval):
|
||||
date_start = date.today()
|
||||
date_start += relativedelta(**{type_interval: interval})
|
||||
return date_start
|
||||
|
||||
def create_subscription(self, lines, subscription_tmpl):
|
||||
subscription_lines = []
|
||||
for line in lines:
|
||||
subscription_lines.append((0, 0, line.get_subscription_line_values()))
|
||||
|
||||
if subscription_tmpl:
|
||||
rec = self.env["sale.subscription"].create(
|
||||
{
|
||||
"partner_id": self.partner_id.id,
|
||||
"user_id": self._context["uid"],
|
||||
"template_id": subscription_tmpl.id,
|
||||
"pricelist_id": self.partner_id.property_product_pricelist.id,
|
||||
"date_start": date.today(),
|
||||
"sale_order_id": self.id,
|
||||
"sale_subscription_line_ids": subscription_lines,
|
||||
}
|
||||
)
|
||||
rec.action_start_subscription()
|
||||
self.subscription_ids = [(4, rec.id)]
|
||||
rec.recurring_next_date = self.get_next_interval(
|
||||
subscription_tmpl.recurring_rule_type,
|
||||
subscription_tmpl.recurring_interval,
|
||||
)
|
||||
|
||||
def group_subscription_lines(self):
|
||||
grouped = defaultdict(list)
|
||||
for order_line in self.order_line.filtered(
|
||||
lambda line: line.product_id.subscribable
|
||||
):
|
||||
grouped[
|
||||
order_line.product_id.product_tmpl_id.subscription_template_id
|
||||
].append(order_line)
|
||||
return grouped
|
||||
|
||||
def action_confirm(self):
|
||||
res = super(SaleOrder, self).action_confirm()
|
||||
for record in self:
|
||||
grouped = self.group_subscription_lines()
|
||||
for tmpl, lines in grouped.items():
|
||||
record.create_subscription(lines, tmpl)
|
||||
return res
|
||||
17
subscription_oca/models/sale_order_line.py
Normal file
17
subscription_oca/models/sale_order_line.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Copyright 2023 Domatix - Carlos Martínez
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from odoo import models
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = "sale.order.line"
|
||||
|
||||
def get_subscription_line_values(self):
|
||||
return {
|
||||
"product_id": self.product_id.id,
|
||||
"name": self.product_id.name,
|
||||
"product_uom_qty": self.product_uom_qty,
|
||||
"price_unit": self.price_unit,
|
||||
"discount": self.discount,
|
||||
"price_subtotal": self.price_subtotal,
|
||||
}
|
||||
470
subscription_oca/models/sale_subscription.py
Normal file
470
subscription_oca/models/sale_subscription.py
Normal file
@@ -0,0 +1,470 @@
|
||||
# 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 = "{}{}{}".format(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.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"].search(
|
||||
[("id", "=", 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")[-1]
|
||||
.id
|
||||
)
|
||||
return super(SaleSubscription, self).create(values)
|
||||
10
subscription_oca/models/sale_subscription_close_reason.py
Normal file
10
subscription_oca/models/sale_subscription_close_reason.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# Copyright 2023 Domatix - Carlos Martínez
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class SaleSubscriptionCloseReason(models.Model):
|
||||
_name = "sale.subscription.close.reason"
|
||||
_description = "Close reason model"
|
||||
|
||||
name = fields.Char(required=True)
|
||||
322
subscription_oca/models/sale_subscription_line.py
Normal file
322
subscription_oca/models/sale_subscription_line.py
Normal file
@@ -0,0 +1,322 @@
|
||||
# Copyright 2023 Domatix - Carlos Martínez
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools.misc import get_lang
|
||||
|
||||
|
||||
class SaleSubscriptionLine(models.Model):
|
||||
_name = "sale.subscription.line"
|
||||
_description = "Subscription lines added to a given subscription"
|
||||
|
||||
product_id = fields.Many2one(
|
||||
comodel_name="product.product",
|
||||
domain=[("sale_ok", "=", True)],
|
||||
string="Product",
|
||||
)
|
||||
currency_id = fields.Many2one(
|
||||
"res.currency",
|
||||
related="sale_subscription_id.currency_id",
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
name = fields.Char(
|
||||
string="Description", compute="_compute_name", store=True, readonly=False
|
||||
)
|
||||
product_uom_qty = fields.Float(default=1.0, string="Quantity")
|
||||
price_unit = fields.Float(
|
||||
string="Unit price", compute="_compute_price_unit", store=True, readonly=False
|
||||
)
|
||||
discount = fields.Float(
|
||||
string="Discount (%)", compute="_compute_discount", store=True, readonly=False
|
||||
)
|
||||
tax_ids = fields.Many2many(
|
||||
comodel_name="account.tax",
|
||||
relation="subscription_line_tax",
|
||||
column1="subscription_line_id",
|
||||
column2="tax_id",
|
||||
string="Taxes",
|
||||
compute="_compute_tax_ids",
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
@api.depends("product_id", "price_unit", "product_uom_qty", "discount", "tax_ids")
|
||||
def _compute_subtotal(self):
|
||||
for record in self:
|
||||
price = record.price_unit * (1 - (record.discount or 0.0) / 100.0)
|
||||
taxes = record.tax_ids.compute_all(
|
||||
price,
|
||||
record.currency_id,
|
||||
record.product_uom_qty,
|
||||
product=record.product_id,
|
||||
partner=record.sale_subscription_id.partner_id,
|
||||
)
|
||||
record.update(
|
||||
{
|
||||
"amount_tax_line_amount": sum(
|
||||
t.get("amount", 0.0) for t in taxes.get("taxes", [])
|
||||
),
|
||||
"price_total": taxes["total_included"],
|
||||
"price_subtotal": taxes["total_excluded"],
|
||||
}
|
||||
)
|
||||
|
||||
price_subtotal = fields.Monetary(
|
||||
string="Subtotal", readonly="True", compute=_compute_subtotal, store=True
|
||||
)
|
||||
price_total = fields.Monetary(
|
||||
string="Total", readonly="True", compute=_compute_subtotal, store=True
|
||||
)
|
||||
amount_tax_line_amount = fields.Float(
|
||||
string="Taxes Amount", compute="_compute_subtotal", store=True
|
||||
)
|
||||
sale_subscription_id = fields.Many2one(
|
||||
comodel_name="sale.subscription", string="Subscription"
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
related="sale_subscription_id.company_id",
|
||||
string="Company",
|
||||
store=True,
|
||||
index=True,
|
||||
)
|
||||
|
||||
@api.depends("product_id")
|
||||
def _compute_name(self):
|
||||
for record in self:
|
||||
if not record.product_id:
|
||||
record.name = False
|
||||
lang = get_lang(self.env, record.sale_subscription_id.partner_id.lang).code
|
||||
product = record.product_id.with_context(lang=lang)
|
||||
record.name = product.with_context(
|
||||
lang=lang
|
||||
).get_product_multiline_description_sale()
|
||||
|
||||
@api.depends("product_id", "sale_subscription_id.fiscal_position_id")
|
||||
def _compute_tax_ids(self):
|
||||
for line in self:
|
||||
fpos = (
|
||||
line.sale_subscription_id.fiscal_position_id
|
||||
or line.sale_subscription_id.fiscal_position_id.get_fiscal_position(
|
||||
line.sale_subscription_id.partner_id.id
|
||||
)
|
||||
)
|
||||
# If company_id is set, always filter taxes by the company
|
||||
taxes = line.product_id.taxes_id.filtered(
|
||||
lambda t: t.company_id == line.env.company
|
||||
)
|
||||
line.tax_ids = fpos.map_tax(taxes)
|
||||
|
||||
@api.depends(
|
||||
"product_id",
|
||||
"sale_subscription_id.partner_id",
|
||||
"sale_subscription_id.pricelist_id",
|
||||
)
|
||||
def _compute_price_unit(self):
|
||||
for record in self:
|
||||
if not record.product_id:
|
||||
continue
|
||||
if (
|
||||
record.sale_subscription_id.pricelist_id
|
||||
and record.sale_subscription_id.partner_id
|
||||
):
|
||||
product = record.product_id.with_context(
|
||||
partner=record.sale_subscription_id.partner_id,
|
||||
quantity=record.product_uom_qty,
|
||||
date=fields.datetime.now(),
|
||||
pricelist=record.sale_subscription_id.pricelist_id.id,
|
||||
uom=record.product_id.uom_id.id,
|
||||
)
|
||||
record.price_unit = product._get_tax_included_unit_price(
|
||||
record.company_id,
|
||||
record.sale_subscription_id.currency_id,
|
||||
fields.datetime.now(),
|
||||
"sale",
|
||||
fiscal_position=record.sale_subscription_id.fiscal_position_id,
|
||||
product_price_unit=record._get_display_price(product),
|
||||
product_currency=record.sale_subscription_id.currency_id,
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
"product_id",
|
||||
"price_unit",
|
||||
"product_uom_qty",
|
||||
"tax_ids",
|
||||
"sale_subscription_id.partner_id",
|
||||
"sale_subscription_id.pricelist_id",
|
||||
)
|
||||
def _compute_discount(self):
|
||||
for record in self:
|
||||
if not (
|
||||
record.product_id
|
||||
and record.product_id.uom_id
|
||||
and record.sale_subscription_id.partner_id
|
||||
and record.sale_subscription_id.pricelist_id
|
||||
and record.sale_subscription_id.pricelist_id.discount_policy
|
||||
== "without_discount"
|
||||
and self.env.user.has_group("product.group_discount_per_so_line")
|
||||
):
|
||||
record.discount = 0.0
|
||||
continue
|
||||
|
||||
record.discount = 0.0
|
||||
product = record.product_id.with_context(
|
||||
lang=record.sale_subscription_id.partner_id.lang,
|
||||
partner=record.sale_subscription_id.partner_id,
|
||||
quantity=record.product_uom_qty,
|
||||
date=fields.Datetime.now(),
|
||||
pricelist=record.sale_subscription_id.pricelist_id.id,
|
||||
uom=record.product_id.uom_id.id,
|
||||
fiscal_position=record.sale_subscription_id.fiscal_position_id
|
||||
or self.env.context.get("fiscal_position"),
|
||||
)
|
||||
|
||||
price, rule_id = record.sale_subscription_id.pricelist_id.with_context(
|
||||
partner_id=record.sale_subscription_id.partner_id.id,
|
||||
date=fields.Datetime.now(),
|
||||
uom=record.product_id.uom_id.id,
|
||||
).get_product_price_rule(
|
||||
record.product_id,
|
||||
record.product_uom_qty or 1.0,
|
||||
record.sale_subscription_id.partner_id,
|
||||
)
|
||||
new_list_price, currency = record.with_context(
|
||||
partner_id=record.sale_subscription_id.partner_id.id,
|
||||
date=fields.Datetime.now(),
|
||||
uom=record.product_id.uom_id.id,
|
||||
)._get_real_price_currency(
|
||||
product, rule_id, record.product_uom_qty, record.product_id.uom_id
|
||||
)
|
||||
|
||||
if new_list_price != 0:
|
||||
if record.sale_subscription_id.pricelist_id.currency_id != currency:
|
||||
new_list_price = currency._convert(
|
||||
new_list_price,
|
||||
record.sale_subscription_id.pricelist_id.currency_id,
|
||||
record.sale_subscription_id.company_id or self.env.company,
|
||||
fields.Date.today(),
|
||||
)
|
||||
discount = (new_list_price - price) / new_list_price * 100
|
||||
if (discount > 0 and new_list_price > 0) or (
|
||||
discount < 0 and new_list_price < 0
|
||||
):
|
||||
record.discount = discount
|
||||
|
||||
def _get_real_price_currency(self, product, rule_id, qty, uom):
|
||||
PricelistItem = self.env["product.pricelist.item"]
|
||||
field_name = "lst_price"
|
||||
currency_id = None
|
||||
product_currency = product.currency_id
|
||||
if rule_id:
|
||||
pricelist_item = PricelistItem.browse(rule_id)
|
||||
if pricelist_item.pricelist_id.discount_policy == "without_discount":
|
||||
while (
|
||||
pricelist_item.base == "pricelist"
|
||||
and pricelist_item.base_pricelist_id
|
||||
and pricelist_item.base_pricelist_id.discount_policy
|
||||
== "without_discount"
|
||||
):
|
||||
_price, rule_id = pricelist_item.base_pricelist_id.with_context(
|
||||
uom=uom.id
|
||||
).get_product_price_rule(
|
||||
product, qty, self.sale_subscription_id.partner_id
|
||||
)
|
||||
pricelist_item = PricelistItem.browse(rule_id)
|
||||
|
||||
if pricelist_item.base == "standard_price":
|
||||
field_name = "standard_price"
|
||||
product_currency = product.cost_currency_id
|
||||
elif (
|
||||
pricelist_item.base == "pricelist" and pricelist_item.base_pricelist_id
|
||||
):
|
||||
field_name = "price"
|
||||
product = product.with_context(
|
||||
pricelist=pricelist_item.base_pricelist_id.id
|
||||
)
|
||||
product_currency = pricelist_item.base_pricelist_id.currency_id
|
||||
currency_id = pricelist_item.pricelist_id.currency_id
|
||||
|
||||
if not currency_id:
|
||||
currency_id = product_currency
|
||||
cur_factor = 1.0
|
||||
else:
|
||||
if currency_id.id == product_currency.id:
|
||||
cur_factor = 1.0
|
||||
else:
|
||||
cur_factor = currency_id._get_conversion_rate(
|
||||
product_currency,
|
||||
currency_id,
|
||||
self.company_id or self.env.company,
|
||||
fields.Date.today(),
|
||||
)
|
||||
|
||||
product_uom = self.env.context.get("uom") or product.uom_id.id
|
||||
if uom and uom.id != product_uom:
|
||||
# the unit price is in a different uom
|
||||
uom_factor = uom._compute_price(1.0, product.uom_id)
|
||||
else:
|
||||
uom_factor = 1.0
|
||||
|
||||
return product[field_name] * uom_factor * cur_factor, currency_id
|
||||
|
||||
def _get_display_price(self, product):
|
||||
if self.sale_subscription_id.pricelist_id.discount_policy == "with_discount":
|
||||
return product.with_context(
|
||||
pricelist=self.sale_subscription_id.pricelist_id.id,
|
||||
uom=self.product_id.uom_id.id,
|
||||
).price
|
||||
|
||||
final_price, rule_id = self.sale_subscription_id.pricelist_id.with_context(
|
||||
partner_id=self.sale_subscription_id.partner_id.id,
|
||||
date=fields.Datetime.now(),
|
||||
uom=self.product_id.uom_id.id,
|
||||
).get_product_price_rule(
|
||||
product or self.product_id,
|
||||
self.product_uom_qty or 1.0,
|
||||
self.sale_subscription_id.partner_id,
|
||||
)
|
||||
base_price, currency = self.with_context(
|
||||
partner_id=self.sale_subscription_id.partner_id.id,
|
||||
date=fields.Datetime.now(),
|
||||
uom=self.product_id.uom_id.id,
|
||||
)._get_real_price_currency(
|
||||
product, rule_id, self.product_uom_qty, self.product_id.uom_id
|
||||
)
|
||||
if currency != self.sale_subscription_id.pricelist_id.currency_id:
|
||||
base_price = currency._convert(
|
||||
base_price,
|
||||
self.sale_subscription_id.pricelist_id.currency_id,
|
||||
self.sale_subscription_id.company_id or self.env.company,
|
||||
fields.Date.today(),
|
||||
)
|
||||
return max(base_price, final_price)
|
||||
|
||||
def _prepare_sale_order_line(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"product_id": self.product_id.id,
|
||||
"name": self.name,
|
||||
"product_uom_qty": self.product_uom_qty,
|
||||
"price_unit": self.price_unit,
|
||||
"discount": self.discount,
|
||||
"price_subtotal": self.price_subtotal,
|
||||
"tax_id": self.tax_ids,
|
||||
"product_uom": self.product_id.uom_id.id,
|
||||
}
|
||||
|
||||
def _prepare_account_move_line(self):
|
||||
self.ensure_one()
|
||||
account = (
|
||||
self.product_id.property_account_income_id
|
||||
or self.product_id.categ_id.property_account_income_categ_id
|
||||
)
|
||||
return {
|
||||
"product_id": self.product_id.id,
|
||||
"name": self.name,
|
||||
"quantity": self.product_uom_qty,
|
||||
"price_unit": self.price_unit,
|
||||
"discount": self.discount,
|
||||
"price_subtotal": self.price_subtotal,
|
||||
"tax_ids": [(6, 0, self.tax_ids.ids)],
|
||||
"product_uom_id": self.product_id.uom_id.id,
|
||||
"account_id": account.id,
|
||||
}
|
||||
29
subscription_oca/models/sale_subscription_stage.py
Normal file
29
subscription_oca/models/sale_subscription_stage.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Copyright 2023 Domatix - Carlos Martínez
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class SaleSubscriptionStage(models.Model):
|
||||
_name = "sale.subscription.stage"
|
||||
_description = "Subscription stage"
|
||||
_order = "sequence, name, id"
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
sequence = fields.Integer()
|
||||
display_name = fields.Char(string="Display name")
|
||||
in_progress = fields.Boolean(string="In progress", default=False)
|
||||
fold = fields.Boolean(string="Kanban folded")
|
||||
description = fields.Text(translate=True)
|
||||
type = fields.Selection(
|
||||
[("pre", "Ready to start"), ("in_progress", "In progress"), ("post", "Closed")],
|
||||
default="pre",
|
||||
)
|
||||
|
||||
@api.constrains("type")
|
||||
def _check_lot_product(self):
|
||||
post_stages = self.env["sale.subscription.stage"].search(
|
||||
[("type", "=", "post")]
|
||||
)
|
||||
if len(post_stages) > 1:
|
||||
raise ValidationError(_("There is already a Closed-type stage declared"))
|
||||
10
subscription_oca/models/sale_subscription_tag.py
Normal file
10
subscription_oca/models/sale_subscription_tag.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# Copyright 2023 Domatix - Carlos Martínez
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class SaleSubscriptionTag(models.Model):
|
||||
_name = "sale.subscription.tag"
|
||||
_description = "Tags for sale subscription"
|
||||
|
||||
name = fields.Char("Tag name", required=True)
|
||||
102
subscription_oca/models/sale_subscription_template.py
Normal file
102
subscription_oca/models/sale_subscription_template.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# Copyright 2023 Domatix - Carlos Martínez
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class SaleSubscriptionTemplate(models.Model):
|
||||
_name = "sale.subscription.template"
|
||||
_description = "Subscription templates"
|
||||
|
||||
name = fields.Char(required=True)
|
||||
description = fields.Text(string="Terms and conditions")
|
||||
recurring_interval = fields.Integer(string="Repeat every", default=1)
|
||||
recurring_rule_type = fields.Selection(
|
||||
[
|
||||
("days", "Day(s)"),
|
||||
("weeks", "Week(s)"),
|
||||
("months", "Month(s)"),
|
||||
("years", "Year(s)"),
|
||||
],
|
||||
string="Recurrence",
|
||||
default="months",
|
||||
)
|
||||
recurring_rule_boundary = fields.Selection(
|
||||
[("unlimited", "Forever"), ("limited", "Fixed")],
|
||||
string="Duration",
|
||||
default="unlimited",
|
||||
)
|
||||
invoicing_mode = fields.Selection(
|
||||
default="draft",
|
||||
string="Invoicing mode",
|
||||
selection=[
|
||||
("draft", "Draft"),
|
||||
("invoice", "Invoice"),
|
||||
("invoice_send", "Invoice & send"),
|
||||
("sale_and_invoice", "Sale order & Invoice"),
|
||||
],
|
||||
)
|
||||
code = fields.Char()
|
||||
recurring_rule_count = fields.Integer(default=1, string="Rule count")
|
||||
invoice_mail_template_id = fields.Many2one(
|
||||
comodel_name="mail.template",
|
||||
string="Invoice Email",
|
||||
domain="[('model', '=', 'account.move')]",
|
||||
)
|
||||
product_ids = fields.One2many(
|
||||
comodel_name="product.template",
|
||||
inverse_name="subscription_template_id",
|
||||
string="Products",
|
||||
)
|
||||
product_ids_count = fields.Integer(
|
||||
compute="_compute_product_ids_count", string="product_ids"
|
||||
)
|
||||
subscription_ids = fields.One2many(
|
||||
comodel_name="sale.subscription",
|
||||
inverse_name="template_id",
|
||||
string="Subscriptions",
|
||||
)
|
||||
subscription_count = fields.Integer(
|
||||
compute="_compute_subscription_count", string="subscription_ids"
|
||||
)
|
||||
|
||||
def _compute_subscription_count(self):
|
||||
data = self.env["sale.subscription"].read_group(
|
||||
domain=[("template_id", "in", self.ids)],
|
||||
fields=["template_id"],
|
||||
groupby=["template_id"],
|
||||
)
|
||||
count_dict = {
|
||||
item["template_id"][0]: item["template_id_count"] for item in data
|
||||
}
|
||||
for record in self:
|
||||
record.subscription_count = count_dict.get(record.id, 0)
|
||||
|
||||
def action_view_subscription_ids(self):
|
||||
return {
|
||||
"name": self.name,
|
||||
"view_mode": "tree,form",
|
||||
"res_model": "sale.subscription",
|
||||
"type": "ir.actions.act_window",
|
||||
"domain": [("id", "in", self.subscription_ids.ids)],
|
||||
}
|
||||
|
||||
def _get_date(self, date_start):
|
||||
self.ensure_one()
|
||||
return relativedelta(months=+self.recurring_rule_count) + date_start
|
||||
|
||||
@api.depends("product_ids")
|
||||
def _compute_product_ids_count(self):
|
||||
for record in self:
|
||||
record.product_ids_count = len(self.product_ids)
|
||||
|
||||
def action_view_product_ids(self):
|
||||
return {
|
||||
"name": self.name,
|
||||
"view_type": "form",
|
||||
"view_mode": "tree,form",
|
||||
"res_model": "product.template",
|
||||
"type": "ir.actions.act_window",
|
||||
"domain": [("id", "in", self.product_ids.ids)],
|
||||
}
|
||||
Reference in New Issue
Block a user