first push message

This commit is contained in:
2026-07-01 14:41:49 +07:00
parent 6667dec2bf
commit 58b5f46cc4
2951 changed files with 316619 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
# from . import controllers
from . import models
+48
View File
@@ -0,0 +1,48 @@
{
'name': "custom_subscriptions",
'summary': "Short (1 phrase/line) summary of the module's purpose",
'description': """
Long description of module's purpose
""",
'author': "My Company",
'website': "https://www.yourcompany.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Uncategorized',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base',
'sale',
'account',
'product',],
# always loaded
'data': [
'security/ir.model.access.csv',
'data/sequence_data.xml',
'data/cron_data.xml',
'views/subscription_views.xml',
'views/menu_views.xml',
],
# only loaded in demonstration mode
'demo': [
'demo/demo.xml',
],
# 'assets': {
# 'web.assets_backend': [
# 'custom_subscriptions/static/src/scss/app_selector.scss',
# 'custom_subscriptions/static/src/js/app_selector.js',
# 'custom_subscriptions/static/src/xml/app_selector.xml',
# ],
# },
'installable': True,
'application': True,
'auto_install': False,
}
@@ -0,0 +1 @@
from . import main
+31
View File
@@ -0,0 +1,31 @@
from odoo import http
from odoo.http import request
class AppSelectorController(http.Controller):
@http.route('/custom_subscriptions/get_available_apps', type='json', auth='user', methods=['POST'])
def get_available_apps(self):
"""
Securely fetches uninstalled apps for the selector.
auth='user' ensures only logged-in users can access.
"""
# Security: Use sudo() to read module data, but filter safely
modules = request.env['ir.module.module'].sudo().search([
('state', '=', 'uninstalled'),
('application', '=', True),
('name', 'not in', ['web', 'base', 'custom_subscriptions', 'mail']),
], order='name')
categories = {}
for mod in modules:
cat = mod.category_id.name or 'Other'
if cat not in categories:
categories[cat] = []
categories[cat].append({
'id': mod.id,
'name': mod.name,
'shortdesc': mod.shortdesc or '',
'icon': mod.icon_image and f'/web/image/ir.module.module/{mod.id}/icon_image' or '/web/static/img/placeholder.png',
})
return categories
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="ir_cron_generate_subscription_invoices" model="ir.cron">
<field name="name">Subscriptions: Generate Invoices</field>
<field name="model_id" ref="model_subscription_subscription"/>
<field name="state">code</field>
<!-- FIX: Used fields.Date.today() instead of context_today -->
<!-- <field name="code">-->
<!--&lt;!&ndash; model.search([('state', '=', 'active'), ('next_invoice_date', '&lt;=', fields.Date.today())]).generate_invoice()&ndash;&gt;-->
<!-- </field>-->
<!-- <field name="interval_number">1</field>-->
<!-- <field name="interval_type">days</field>-->
<!-- <field name="numbercall">-1</field>-->
<!-- <field name="active">True</field>-->
</record>
</data>
</odoo>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Sequence for Subscriptions -->
<record id="seq_subscription_subscription" model="ir.sequence">
<field name="name">Subscription Sequence</field>
<field name="code">subscription.subscription</field>
<field name="prefix">SUB/</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
</odoo>
+30
View File
@@ -0,0 +1,30 @@
<odoo>
<data>
<!--
<record id="object0" model="custom_subscriptions.custom_subscriptions">
<field name="name">Object 0</field>
<field name="value">0</field>
</record>
<record id="object1" model="custom_subscriptions.custom_subscriptions">
<field name="name">Object 1</field>
<field name="value">10</field>
</record>
<record id="object2" model="custom_subscriptions.custom_subscriptions">
<field name="name">Object 2</field>
<field name="value">20</field>
</record>
<record id="object3" model="custom_subscriptions.custom_subscriptions">
<field name="name">Object 3</field>
<field name="value">30</field>
</record>
<record id="object4" model="custom_subscriptions.custom_subscriptions">
<field name="name">Object 4</field>
<field name="value">40</field>
</record>
-->
</data>
</odoo>
+1
View File
@@ -0,0 +1 @@
from . import subscription
+181
View File
@@ -0,0 +1,181 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
class SubscriptionTemplate(models.Model):
_name = 'subscription.template'
_description = 'Subscription Template'
_order = 'sequence, name'
name = fields.Char(string='Template Name', required=True)
sequence = fields.Integer(string='Sequence', default=10)
interval_number = fields.Integer(string='Invoice Every', default=1, required=True)
interval_type = fields.Selection([
('days', 'Days'),
('weeks', 'Weeks'),
('months', 'Months'),
('years', 'Years'),
], string='Interval Type', default='months', required=True)
active = fields.Boolean(string='Active', default=True)
product_id = fields.Many2one('product.product', string='Product', required=True)
quantity = fields.Float(string='Quantity', default=1.0)
# FIX: Changed to Float because list_price is Float
price_unit = fields.Float(string='Price', related='product_id.list_price')
currency_id = fields.Many2one(related='product_id.currency_id')
description = fields.Text(string='Description')
def name_get(self):
result = []
for record in self:
name = f"{record.name} ({record.interval_number} {record.interval_type})"
result.append((record.id, name))
return result
class Subscription(models.Model):
_name = 'subscription.subscription'
_description = 'Subscription'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'create_date desc'
name = fields.Char(string='Reference', required=True, copy=False, readonly=True, default='New')
template_id = fields.Many2one('subscription.template', string='Subscription Template', required=True)
partner_id = fields.Many2one('res.partner', string='Customer', required=True)
company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company)
# Dates
start_date = fields.Date(string='Start Date', required=True, default=fields.Date.context_today)
next_invoice_date = fields.Date(string='Next Invoice Date', required=True, copy=False)
end_date = fields.Date(string='End Date')
last_invoice_date = fields.Date(string='Last Invoice Date', copy=False)
# Status
state = fields.Selection([
('draft', 'Draft'),
('active', 'Active'),
('closed', 'Closed'),
('cancelled', 'Cancelled'),
], string='Status', default='draft', tracking=True, copy=False)
# Pricing
currency_id = fields.Many2one(related='template_id.currency_id')
# FIX: Changed to Float to match template
price_unit = fields.Float(related='template_id.price_unit')
# FIX: Changed to Float
quantity = fields.Float(related='template_id.quantity')
recurring_total = fields.Float(string='Recurring Price', compute='_compute_recurring_total', store=True)
# Invoicing
invoice_count = fields.Integer(string='Invoice Count', compute='_compute_invoice_count')
invoice_ids = fields.One2many('account.move', 'subscription_id', string='Invoices')
# Additional info
note = fields.Text(string='Notes')
active = fields.Boolean(string='Active', default=True)
@api.model
def create(self, vals):
if vals.get('name', 'New') == 'New':
vals['name'] = self.env['ir.sequence'].next_by_code('subscription.subscription') or 'New'
return super().create(vals)
def _compute_recurring_total(self):
for sub in self:
sub.recurring_total = sub.price_unit * sub.quantity
def _compute_invoice_count(self):
for sub in self:
sub.invoice_count = len(sub.invoice_ids)
def action_start_subscription(self):
self.write({
'state': 'active',
'next_invoice_date': self.start_date
})
return True
def action_close_subscription(self):
self.write({'state': 'closed'})
return True
def action_cancel_subscription(self):
self.write({'state': 'cancelled'})
return True
def action_draft_subscription(self):
self.write({'state': 'draft'})
return True
def action_view_invoices(self):
self.ensure_one()
return {
'name': _('Invoices'),
'type': 'ir.actions.act_window',
'res_model': 'account.move',
'view_mode': 'tree,form',
'domain': [('subscription_id', '=', self.id)],
'context': {'default_subscription_id': self.id}
}
def generate_invoice(self):
"""Generate invoice for active subscriptions"""
for sub in self:
if sub.state != 'active':
continue
if sub.end_date and sub.next_invoice_date > sub.end_date:
sub.write({'state': 'closed'})
continue
# Create invoice
invoice_vals = {
'move_type': 'out_invoice',
'partner_id': sub.partner_id.id,
'company_id': sub.company_id.id,
'currency_id': sub.currency_id.id,
'subscription_id': sub.id,
'invoice_date': sub.next_invoice_date,
'invoice_origin': f"Subscription: {sub.name}",
'invoice_line_ids': [(0, 0, {
'product_id': sub.template_id.product_id.id,
'name': sub.template_id.description or sub.template_id.product_id.name,
'quantity': sub.quantity,
'price_unit': sub.price_unit,
'account_id': sub.template_id.product_id.property_account_income_id.id or
sub.template_id.product_id.categ_id.property_account_income_categ_id.id,
})]
}
invoice = self.env['account.move'].create(invoice_vals)
# Update subscription
next_date = sub.next_invoice_date
if sub.template_id.interval_type == 'days':
next_date += timedelta(days=sub.template_id.interval_number)
elif sub.template_id.interval_type == 'weeks':
next_date += timedelta(weeks=sub.template_id.interval_number)
elif sub.template_id.interval_type == 'months':
next_date += relativedelta(months=sub.template_id.interval_number)
elif sub.template_id.interval_type == 'years':
next_date += relativedelta(years=sub.template_id.interval_number)
sub.write({
'last_invoice_date': sub.next_invoice_date,
'next_invoice_date': next_date,
})
return True
class AccountMove(models.Model):
_inherit = 'account.move'
subscription_id = fields.Many2one('subscription.subscription', string='Subscription', ondelete='set null',
copy=False)
@@ -0,0 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_subscription_template_user,subscription.template.user,model_subscription_template,base.group_user,1,1,1,0
access_subscription_subscription_user,subscription.subscription.user,model_subscription_subscription,base.group_user,1,1,1,0
access_subscription_template_salesman,subscription.template.salesman,model_subscription_template,base.group_user,1,1,1,0
access_subscription_subscription_salesman,subscription.subscription.salesman,model_subscription_subscription,base.group_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_subscription_template_user subscription.template.user model_subscription_template base.group_user 1 1 1 0
3 access_subscription_subscription_user subscription.subscription.user model_subscription_subscription base.group_user 1 1 1 0
4 access_subscription_template_salesman subscription.template.salesman model_subscription_template base.group_user 1 1 1 0
5 access_subscription_subscription_salesman subscription.subscription.salesman model_subscription_subscription base.group_user 1 1 1 0
@@ -0,0 +1,70 @@
/** @odoo-module **/
// ^^^ MUST be first line, no comments/spaces before
import { Component, useState, onWillStart } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { jsonrpc } from "@web/core/network/rpc_service";
import { useNotification } from "@web/core/notifications/notification_hook";
export class AppSelector extends Component {
static template = "custom_subscriptions.AppSelectorTemplate";
setup() {
this.orm = useService("orm");
this.notification = useNotification();
this.state = useState({
categories: {},
selectedIds: [],
loading: true
});
onWillStart(async () => {
await this.fetchApps();
});
}
async fetchApps() {
try {
const result = await jsonrpc("/custom_subscriptions/get_available_apps");
this.state.categories = result;
this.state.loading = false;
} catch (error) {
console.error("Failed to load apps:", error);
this.notification.add("Error loading apps", { type: "danger" });
}
}
toggleSelection(id) {
const idx = this.state.selectedIds.indexOf(id);
if (idx === -1) {
this.state.selectedIds.push(id);
} else {
this.state.selectedIds.splice(idx, 1);
}
}
isSelected(id) {
return this.state.selectedIds.includes(id);
}
async installApps() {
if (this.state.selectedIds.length === 0) {
this.notification.add("Please select at least one app.", { type: "warning" });
return;
}
if (!confirm(`Install ${this.state.selectedIds.length} app(s)?`)) return;
try {
await this.orm.call("ir.module.module", "button_immediate_install", [this.state.selectedIds]);
this.notification.add("Installing... Reloading soon", { type: "success" });
setTimeout(() => window.location.reload(), 2000);
} catch (error) {
this.notification.add(error.data?.message || "Install failed", { type: "danger" });
}
}
}
// ⚠️ CRITICAL: This key MUST match the <field name="tag"> in XML exactly
registry.category("actions").add("custom_app_selector", AppSelector);
@@ -0,0 +1,193 @@
.app-selector-wrapper {
background-color: #f8f9fa;
min-height: 100vh;
padding-bottom: 40px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.app-selector-header {
text-align: center;
padding: 30px 20px;
background: white;
margin-bottom: 20px;
border-bottom: 1px solid #eee;
h1 {
font-size: 2.2rem;
margin: 0 0 8px 0;
color: #212529;
}
p {
color: #717b84;
margin: 0;
}
}
.app-selector-content {
display: flex;
max-width: 1200px;
margin: 0 auto;
gap: 30px;
padding: 0 20px;
}
.app-grid {
flex: 1;
min-width: 0;
}
.category-title {
font-style: italic;
font-weight: 400;
font-size: 1.3rem;
margin: 25px 0 15px 0;
color: #000;
border-bottom: 1px solid #eee;
padding-bottom: 8px;
}
.app-cards-row {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
gap: 12px;
}
.app-card {
background: white;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 16px 12px;
position: relative;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
&:hover {
border-color: #adb5bd;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
transform: translateY(-2px);
}
&.selected {
border: 2px solid #714B67;
background-color: #f9f5fa;
}
}
.card-check {
display: none;
position: absolute;
top: -8px;
right: -8px;
width: 22px;
height: 22px;
background: #714B67;
color: white;
border-radius: 50%;
align-items: center;
justify-content: center;
font-size: 11px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.app-card.selected .card-check {
display: flex;
}
.app-icon {
width: 42px;
height: 42px;
margin-bottom: 10px;
object-fit: contain;
}
.app-name {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 4px;
color: #212529;
line-height: 1.3;
}
.app-desc {
font-size: 0.75rem;
color: #717b84;
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
max-height: 2.6em;
}
/* Sidebar */
.app-sidebar {
width: 280px;
flex-shrink: 0;
}
.sidebar-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
position: sticky;
top: 20px;
}
.sidebar-card h3 {
margin: 0 0 15px 0;
padding-bottom: 12px;
border-bottom: 1px solid #eee;
font-size: 1.1rem;
}
.trial-info {
background-color: #e6f4f9;
padding: 14px;
border-radius: 6px;
margin: 15px 0;
font-size: 0.85rem;
color: #0c5460;
line-height: 1.5;
p { margin: 4px 0; }
}
.btn-continue {
width: 100%;
background-color: #714B67;
color: white;
border: none;
padding: 12px 16px;
font-weight: 600;
font-size: 1rem;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
&:hover:not(:disabled) {
background-color: #5a3c53;
}
&:disabled {
background-color: #adb5bd;
cursor: not-allowed;
}
}
/* Responsive */
@media (max-width: 992px) {
.app-selector-content {
flex-direction: column;
}
.app-sidebar {
width: 100%;
}
.sidebar-card {
position: static;
}
}
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="custom_subscriptions.AppSelectorTemplate" owl="1">
<div class="app-selector-wrapper">
<div class="app-selector-header">
<h1>Choose your Apps</h1>
<p>Free instant access. No credit card required.</p>
</div>
<div class="app-selector-content">
<!-- App Grid -->
<div class="app-grid">
<t t-if="state.loading">
<div class="text-center p-5 text-muted">Loading apps...</div>
</t>
<t t-else="">
<t t-foreach="Object.keys(state.categories)" t-as="category" t-key="category">
<div class="category-section">
<h2 class="category-title" t-esc="category"/>
<div class="app-cards-row">
<t t-foreach="state.categories[category]" t-as="app" t-key="app.id">
<div class="app-card"
t-att-class="isSelected(app.id) ? 'selected' : ''"
t-on-click="() => this.toggleSelection(app.id)">
<div class="card-check">
<i class="fa fa-check"/>
</div>
<div class="card-body">
<img t-att-src="app.icon" class="app-icon" alt="" onerror="this.src='/web/static/img/placeholder.png'"/>
<div class="app-name" t-esc="app.name"/>
<div class="app-desc" t-esc="app.shortdesc"/>
</div>
</div>
</t>
</div>
</div>
</t>
</t>
</div>
<!-- Sidebar -->
<div class="app-sidebar">
<div class="sidebar-card">
<h3><t t-esc="state.selectedIds.length"/> Apps selected</h3>
<div class="trial-info">
<p><strong>Community Edition</strong></p>
<p>Install modules instantly.</p>
<p>No credit card required.</p>
</div>
<button class="btn-continue" t-on-click="installApps" t-att-disabled="state.selectedIds.length === 0">
Continue
</button>
</div>
</div>
</div>
</div>
</t>
</templates>
+32
View File
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Main Menu -->
<menuitem id="menu_subscription_root"
name="Subscriptions"
web_icon="custom_subscriptions,static/description/icon.png"
sequence="90"/>
<!-- Subscriptions Menu -->
<menuitem id="menu_subscription_main"
name="Subscriptions"
parent="menu_subscription_root"
sequence="10"/>
<menuitem id="menu_subscription_subscription"
name="Subscriptions"
parent="menu_subscription_main"
action="subscription_action"
sequence="10"/>
<menuitem id="menu_subscription_template"
name="Subscription Templates"
parent="menu_subscription_main"
action="subscription_template_action"
sequence="20"/>
<!-- Reporting Menu -->
<menuitem id="menu_subscription_reporting"
name="Reporting"
parent="menu_subscription_root"
sequence="50"/>
</odoo>
@@ -0,0 +1,183 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Subscription Template Tree View -->
<record id="subscription_template_view_tree" model="ir.ui.view">
<field name="name">subscription.template.view.tree</field>
<field name="model">subscription.template</field>
<field name="arch" type="xml">
<list>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="product_id"/>
<field name="interval_number"/>
<field name="interval_type"/>
<field name="price_unit"/>
</list>
</field>
</record>
<!-- Subscription Template Form View -->
<record id="subscription_template_view_form" model="ir.ui.view">
<field name="name">subscription.template.view.form</field>
<field name="model">subscription.template</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
</button>
</div>
<div class="oe_title">
<label for="name"/>
<h1>
<field name="name" placeholder="Template Name"/>
</h1>
</div>
<group>
<group>
<field name="product_id"/>
<field name="quantity"/>
<field name="price_unit"/>
</group>
<group>
<field name="interval_number"/>
<field name="interval_type"/>
</group>
</group>
<group>
<field name="description"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- Subscription Template Action -->
<record id="subscription_template_action" model="ir.actions.act_window">
<field name="name">Subscription Templates</field>
<field name="res_model">subscription.template</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first subscription template
</p>
</field>
</record>
<!-- Subscription Tree View -->
<record id="subscription_view_tree" model="ir.ui.view">
<field name="name">subscription.subscription.view.tree</field>
<field name="model">subscription.subscription</field>
<field name="arch" type="xml">
<list decoration-success="state == 'active'" decoration-warning="state == 'draft'">
<field name="name"/>
<field name="partner_id"/>
<field name="template_id"/>
<field name="start_date"/>
<field name="next_invoice_date"/>
<field name="recurring_total" widget="monetary"/>
<field name="state" widget="badge" decoration-success="state == 'active'" decoration-warning="state == 'draft'"/>
</list>
</field>
</record>
<!-- Subscription Form View -->
<record id="subscription_view_form" model="ir.ui.view">
<field name="name">subscription.subscription.view.form</field>
<field name="model">subscription.subscription</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_start_subscription" string="Start Subscription" type="object" class="btn-primary" invisible="state != 'draft'"/>
<button name="action_close_subscription" string="Close" type="object" invisible="state != 'active'"/>
<button name="action_cancel_subscription" string="Cancel" type="object" invisible="state not in ('active', 'draft')"/>
<button name="action_draft_subscription" string="Set to Draft" type="object" invisible="state != 'cancelled'"/>
<field name="state" widget="statusbar" statusbar_visible="draft,active,closed"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_view_invoices" type="object" class="oe_stat_button" icon="fa-usd">
<field name="invoice_count" widget="statinfo" string="Invoices"/>
</button>
</div>
<div class="oe_title">
<h1>
<field name="name" readonly="1"/>
</h1>
</div>
<group>
<group>
<field name="template_id" options="{'no_create': True}"/>
<field name="partner_id" options="{'no_create': True}"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<group>
<field name="start_date"/>
<field name="next_invoice_date"/>
<field name="end_date"/>
<field name="last_invoice_date"/>
</group>
</group>
<group>
<group>
<field name="currency_id" invisible="1"/>
<field name="price_unit"/>
<field name="quantity"/>
<field name="recurring_total"/>
</group>
</group>
<group>
<field name="note" placeholder="Internal notes..."/>
</group>
</sheet>
</form>
</field>
</record>
<!-- Subscription Search View -->
<record id="subscription_view_search" model="ir.ui.view">
<field name="name">subscription.subscription.view.search</field>
<field name="model">subscription.subscription</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="partner_id"/>
<field name="template_id"/>
<filter string="Active" name="active" domain="[('state', '=', 'active')]"/>
<filter string="Draft" name="draft" domain="[('state', '=', 'draft')]"/>
<filter string="Closed" name="closed" domain="[('state', '=', 'closed')]"/>
<group>
<filter string="Status" name="group_state" context="{'group_by': 'state'}"/>
<filter string="Customer" name="group_partner" context="{'group_by': 'partner_id'}"/>
<filter string="Template" name="group_template" context="{'group_by': 'template_id'}"/>
</group>
</search>
</field>
</record>
<!-- Subscription Action -->
<record id="subscription_action" model="ir.actions.act_window">
<field name="name">Subscriptions</field>
<field name="res_model">subscription.subscription</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="subscription_view_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first subscription
</p>
<p>
Manage customer subscriptions and recurring billing.
</p>
</field>
</record>
<!-- Dashboard Action -->
<record id="subscription_dashboard_action" model="ir.actions.act_window">
<field name="name">Subscriptions Dashboard</field>
<field name="res_model">subscription.subscription</field>
<field name="view_mode">list,form</field>
<field name="domain">[('state', '=', 'active')]</field>
<field name="context">{'search_default_active': 1}</field>
</record>
</odoo>