first push message
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# from . import controllers
|
||||
from . import models
|
||||
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -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">-->
|
||||
<!--<!– model.search([('state', '=', 'active'), ('next_invoice_date', '<=', fields.Date.today())]).generate_invoice()–>-->
|
||||
<!-- </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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
from . import subscription
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user