[15.0][ADD] subscription_oca

This commit is contained in:
Carlos
2023-09-20 21:49:04 +02:00
committed by Ilyas
parent 69117ccbf2
commit 1ba6fe33a2
34 changed files with 2636 additions and 0 deletions

View 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

View 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"
)

View 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"
)

View 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,
},
}

View 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

View 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,
}

View 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)

View 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)

View 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,
}

View 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"))

View 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)

View 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)],
}