# Part of Odoo. See LICENSE file for full copyright and licensing details. import json from collections import defaultdict from odoo import fields, models from odoo.fields import Domain class ProjectProject(models.Model): _inherit = 'project.project' # ------------------------------------------- # Actions # ------------------------------------------- def _get_subscription_action(self, domain=None, subscription_ids=None): if not domain and not subscription_ids: return {} action = self.env["ir.actions.actions"]._for_xml_id("sale_subscription.sale_subscription_action") action_context = {'default_project_id': self.id} if self.partner_id.commercial_partner_id: action_context['default_partner_id'] = self.partner_id.commercial_partner_id.id action.update({ 'views': [[False, 'list'], [False, 'kanban'], [False, 'form'], [False, 'pivot'], [False, 'graph'], [False, 'cohort']], 'context': action_context, 'domain': domain or [('id', 'in', subscription_ids)] }) if subscription_ids and len(subscription_ids) == 1: action["views"] = [[False, 'form']] action["res_id"] = subscription_ids[0] return action def action_open_project_subscriptions(self): self.ensure_one() subscription_ids = self.env['sale.order']._search([('is_subscription', '=', True), ('project_id', '=', self.id)]) return self._get_subscription_action(subscription_ids=list(subscription_ids)) def action_profitability_items(self, section_name, domain=None, res_id=False): if section_name == 'subscriptions': return self._get_subscription_action(domain, [res_id] if res_id else []) return super().action_profitability_items(section_name, domain, res_id) # ------------------------------------------- # Project Update # ------------------------------------------- def _get_profitability_labels(self): labels = super()._get_profitability_labels() labels['subscriptions'] = self.env._('Subscriptions') return labels def _get_profitability_sequence_per_invoice_type(self): sequence_per_invoice_type = super()._get_profitability_sequence_per_invoice_type() sequence_per_invoice_type['subscriptions'] = 8 return sequence_per_invoice_type def _get_profitability_aal_domain(self): return Domain.AND([ super()._get_profitability_aal_domain(), ['|', ('move_line_id', '=', False), ('move_line_id.subscription_id', '=', False)], ]) def _get_profitability_items(self, with_action=True): profitability_items = super()._get_profitability_items(with_action) subscription_read_group = self.env['sale.order'].sudo()._read_group( [('project_id.account_id', 'in', self.account_id.ids), ('subscription_state', '!=', '1_draft'), ('is_subscription', '=', True), ], ['sale_order_template_id', 'subscription_state', 'currency_id', 'plan_id'], ['recurring_monthly:sum', 'id:array_agg'], ) if not subscription_read_group: return profitability_items all_subscription_ids = [] amount_to_invoice = 0.0 set_currency_ids = {self.currency_id.id} amount_to_invoice_per_currency = defaultdict(lambda: 0.0) recurring_per_currency_per_template = defaultdict(lambda: defaultdict(lambda: 0.0)) for sale_order_template_id, subscription_state, currency, subscription_plan, recurring_monthly, ids in subscription_read_group: all_subscription_ids.extend(ids) set_currency_ids.add(currency.id) if subscription_state != '3_progress': # then the subscriptions are closed and so nothing is to invoice. continue if not sale_order_template_id: # then we will take the recurring monthly amount that we will invoice in the next invoice(s). target = amount_to_invoice_per_currency else: target = recurring_per_currency_per_template[sale_order_template_id.id] unit = subscription_plan.billing_period_unit value = subscription_plan.billing_period_value if unit == 'month': periodicity = value else: # unit in ['week', 'year'] periodicity = value * 12 if unit == 'week': periodicity = periodicity / 365.25 * 7 target[currency.id] += recurring_monthly * periodicity # fetch the data needed for the 'invoiced' section before handling the 'to invoice' one, in order to have all the currencies available and make only one fetch on the rates aal_read_group = self.env['account.analytic.line'].sudo()._read_group( [('move_line_id.subscription_id', 'in', all_subscription_ids), ('account_id', 'in', self.account_id.ids)], ['currency_id'], ['amount:sum'], ) set_currency_ids.union({currency.id for currency, dummy in aal_read_group}) rates_per_currency_id = self.env['res.currency'].browse(set_currency_ids)._get_rates(self.company_id or self.env.company, fields.Date.context_today(self)) project_currency_rate = rates_per_currency_id[self.currency_id.id] for currency_id in amount_to_invoice_per_currency: rate = project_currency_rate / rates_per_currency_id[currency_id] amount_to_invoice += amount_to_invoice_per_currency[currency_id] * rate subscription_template_dict = {} if recurring_per_currency_per_template.keys(): subscription_template_dict = { res['id']: res['duration_value'] for res in self.env['sale.order.template'].sudo().search_read( [('id', 'in', list(recurring_per_currency_per_template.keys())), ('is_unlimited', '=', False)], ['id', 'duration_value'], ) } for subcription_template_id in recurring_per_currency_per_template: nb_period = subscription_template_dict.get(subcription_template_id, 1) for currency_id in recurring_per_currency_per_template[subcription_template_id]: rate = project_currency_rate / rates_per_currency_id[currency_id] amount_to_invoice += recurring_per_currency_per_template[subcription_template_id][currency_id] * nb_period * rate amount_invoiced = 0.0 for currency, amount in aal_read_group: rate = project_currency_rate / rates_per_currency_id[currency.id] amount_invoiced += amount * rate revenues = profitability_items['revenues'] section_id = 'subscriptions' subscription_revenue = { 'id': section_id, 'sequence': self._get_profitability_sequence_per_invoice_type()[section_id], 'invoiced': amount_invoiced, 'to_invoice': amount_to_invoice, } if with_action and all_subscription_ids and self.env.user.has_group('sales_team.group_sale_salesman'): args = [section_id, [('id', 'in', all_subscription_ids)]] if len(all_subscription_ids) == 1: args.append(all_subscription_ids[0]) action = {'name': 'action_profitability_items', 'type': 'object', 'args': json.dumps(args)} subscription_revenue['action'] = action revenues['data'].append(subscription_revenue) revenues['total']['invoiced'] += amount_invoiced revenues['total']['to_invoice'] += amount_to_invoice return profitability_items def get_subscription_items_data(self, offset, limit): all_subscription_ids = self.env['sale.order']._search([ ('project_id.account_id', 'in', self.account_id.ids), ('subscription_state', 'not in', ['1_draft', '5_renewed']), ('is_subscription', '=', True), ]) display_load_more = False subscriptions_lines = self.env['sale.order.line'].search(offset=offset, limit=limit + 1, domain=[('order_id', 'in', all_subscription_ids), ('display_type', '=', False)]) if len(subscriptions_lines) > limit: subscriptions_lines = subscriptions_lines - subscriptions_lines[limit] display_load_more = True action_id = self.env.ref('sale_subscription.sale_subscription_action').id new_items = [] for subscription_line in subscriptions_lines.with_context(with_price_unit=True)._read_format(['display_name', 'product_uom_qty', 'qty_delivered', 'qty_invoiced', 'product_uom_id', 'product_id', 'order_id']): action_dict = {} if self.env.user.has_group('sales_team.group_sale_salesman'): action_dict = {'action': {'name': action_id, 'resId': subscription_line['order_id'][0]}} new_items.append({**subscription_line, **action_dict}) return {'sol_items': new_items, 'displayLoadMore': display_load_more} def _get_foldable_section(self): foldable_section = super()._get_foldable_section() return foldable_section + [ 'subscriptions', ] def _get_profitability_sale_order_items_domain(self, domain=None): return Domain.AND([ super()._get_profitability_sale_order_items_domain(domain), [('order_id.is_subscription', '=', False)], ]) def _get_items_from_invoices_domain(self, domain=None): return Domain.AND([ super()._get_items_from_invoices_domain(domain), [('subscription_id', '=', False)], ]) def _get_sale_items_domain(self, additional_domain=None): return Domain.AND([ super()._get_sale_items_domain(additional_domain), [('order_id.is_subscription', '=', False)], ])