[ADD] website_sale_slides: add default course product

Purpose
=======
This commit adds a default product called "Access Course" to the
database when you have the `website_sale_slides` installed.

This commit also starts the transition for courses products
into `event ticket`-like products

Specifications
==============
This product is already created in order to help the users start to sell
their courses.

We also added a new type of product named `Courses`.
This will be used to create courses and easily work with courses products.

The commit adds the list of all the courses that the will gain access to
when he will buy the product inside of the cart and in the right column
whe it appears.

It also adds the same list when the product is inserted into a sales order.

The commit adds the choice of enroll policies in the front-end when
`website_sales_slides` is installed and of the corresponding product
if we are creating a paid course.

Task-2636092

closes odoo/odoo#77990

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Thomas Josse
2021-11-09 15:35:15 +00:00
committed by std-odoo
parent a79ab876af
commit cbec2409a2
18 changed files with 209 additions and 27 deletions
@@ -10,6 +10,7 @@
'depends': ['website_slides', 'website_sale'],
'installable': True,
'data': [
'data/product_data.xml',
'report/sale_report_views.xml',
'views/website_slides_menu_views.xml',
'views/slide_channel_views.xml',
@@ -25,6 +26,9 @@
'web.assets_frontend': [
'website_sale_slides/static/src/js/**/*',
],
'web.assets_qweb': [
'website_sale_slides/static/src/xml/website_slides_channel.xml',
]
},
'license': 'LGPL-3',
}
@@ -2,11 +2,22 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.website_slides.controllers.main import WebsiteSlides
from odoo.http import request
from odoo.http import request, route
from odoo.tools import format_amount
class WebsiteSaleSlides(WebsiteSlides):
@route('/slides/get_course_products', type='json', auth='user')
def get_course_products(self):
"""Return a list of the course products values with formatted price."""
products = request.env['product.product'].search([('detailed_type', '=', 'course')])
return [{
'id': product.id,
'name': f'{product.name} ({format_amount(request.env, product.list_price, product.currency_id)})',
} for product in products]
def _prepare_additional_channel_values(self, values, **kwargs):
values = super(WebsiteSaleSlides, self)._prepare_additional_channel_values(values, **kwargs)
channel = values['channel']
@@ -15,3 +26,13 @@ class WebsiteSaleSlides(WebsiteSlides):
values['product_info'] = channel.product_id.product_tmpl_id._get_combination_info(product_id=channel.product_id.id, pricelist=pricelist)
values['product_info']['currency_id'] = request.website.currency_id
return values
def _slide_channel_prepare_values(self, **kwargs):
"""Parse the values posted when we create a new course
from website to add the selected product.
"""
channel = super(WebsiteSaleSlides, self)._slide_channel_prepare_values(**kwargs)
channel['enroll'] = kwargs.get('enroll')
if channel['enroll'] == 'payment' and 'product_id' in kwargs:
channel['product_id'] = int(kwargs['product_id'])
return channel
@@ -0,0 +1,17 @@
<odoo><data>
<record id="product_category_courses" model="product.category">
<field name="parent_id" ref="product.product_category_1"/>
<field name="name">Courses</field>
</record>
<record id="default_product_course" model="product.product">
<field name="name">Course Access</field>
<field name="standard_price">99.99</field>
<field name="list_price">99.99</field>
<field name="detailed_type">course</field>
<field name="invoice_policy">order</field>
<field name="is_published" eval="True"/>
<field name="image_1920" type="base64" file="website_sale_slides/static/img/default_course_product.svg"/>
<field name="categ_id" ref="product_category_courses"/>
</record>
</data></odoo>
@@ -6,7 +6,7 @@
<field name="name">Taking care of Trees Course</field>
<field name="standard_price">150.0</field>
<field name="list_price">150.0</field>
<field name="detailed_type">service</field>
<field name="detailed_type">course</field>
<field name="invoice_policy">order</field>
<field name="is_published" eval="True"/>
<field name="image_1920" type="base64" file="website_sale_slides/static/img/channel_demo_tree_1.jpg"/>
@@ -18,7 +18,7 @@
<field name="name">DIY Furniture Course</field>
<field name="standard_price">100.0</field>
<field name="list_price">100.0</field>
<field name="detailed_type">service</field>
<field name="detailed_type">course</field>
<field name="invoice_policy">order</field>
<field name="is_published" eval="True"/>
<field name="image_1920" type="base64" file="website_sale_slides/static/img/product_course.png"/>
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
from . import product_product
from . import slide_channel
from . import product_template
from . import sale_order
from . import slide_channel
from . import website
@@ -1,10 +1,18 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo import _, fields, models
class Product(models.Model):
_inherit = "product.product"
channel_ids = fields.One2many('slide.channel', 'product_id', string='Courses')
def get_product_multiline_description_sale(self):
payment_channels = self.channel_ids.filtered(lambda course: course.enroll == 'payment')
if not payment_channels:
return super(Product, self).get_product_multiline_description_sale()
return _('Access to:\n%s', '\n'.join(payment_channels.mapped('name')))
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ProductTemplate(models.Model):
_inherit = 'product.template'
detailed_type = fields.Selection(selection_add=[
('course', 'Course'),
], ondelete={'course': 'set service'})
def _detailed_type_mapping(self):
type_mapping = super(ProductTemplate, self)._detailed_type_mapping()
type_mapping['course'] = 'service'
return type_mapping
@@ -17,7 +17,7 @@ class SaleOrder(models.Model):
)
products = so_lines.mapped('product_id')
related_channels = self.env['slide.channel'].search(
[('product_id', 'in', products.ids)]
[('product_id', 'in', products.ids), ('enroll', '=', 'payment')],
)
channel_products = related_channels.mapped('product_id')
@@ -7,10 +7,16 @@ from odoo import api, fields, models
class Channel(models.Model):
_inherit = 'slide.channel'
def _get_default_product_id(self):
product_courses = self.env['product.product'].search(
[('detailed_type', '=', 'course')], limit=2)
return product_courses.id if len(product_courses) == 1 else False
enroll = fields.Selection(selection_add=[
('payment', 'On payment')
], ondelete={'payment': lambda recs: recs.write({'enroll': 'invite'})})
product_id = fields.Many2one('product.product', 'Product', index=True)
product_id = fields.Many2one('product.product', 'Product', domain=[('detailed_type', '=', 'course')],
default=_get_default_product_id)
product_sale_revenues = fields.Monetary(
string='Total revenues', compute='_compute_product_sale_revenues',
groups="sales_team.group_sale_salesman")
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo.osv import expression
class Website(models.Model):
_inherit = 'website'
def sale_product_domain(self):
return expression.AND([
super(Website, self).sale_product_domain(),
[('detailed_type', '!=', 'course')],
])
@@ -0,0 +1 @@
<?xml version="1.0" ?><svg id="solid" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><title/><path d="M407.951,343.116a7.2,7.2,0,0,1,.048.733c.121,13.4-15.891,29.5-41.725,41.981C345.344,395.944,309.222,408,256,408s-89.344-12.056-110.274-22.17C119.892,373.348,103.88,357.246,104,343.849a7.2,7.2,0,0,1,.048-.733L115.1,243.624a7,7,0,0,1,9.82-5.615L242.911,290.9a32.043,32.043,0,0,0,26.179,0l117.986-52.89a7,7,0,0,1,9.82,5.615ZM474.988,153.4l-205.9-92.3a32.037,32.037,0,0,0-26.179,0l-205.9,92.3a16,16,0,0,0,0,29.2l205.9,92.3a32.043,32.043,0,0,0,26.179,0L440,198.284v59.874A15.98,15.98,0,0,0,432,272V376h32V272a15.98,15.98,0,0,0-8-13.842V191.111l18.987-8.511a16,16,0,0,0,0-29.2Z" style="fill:#3b3b3b"/></svg>

After

Width:  |  Height:  |  Size: 713 B

@@ -0,0 +1,26 @@
/** @odoo-module **/
import {ChannelCreateDialog} from '@website_slides/js/website_slides.editor';
ChannelCreateDialog.include({
xmlDependencies: ChannelCreateDialog.prototype.xmlDependencies.concat(
['/website_sale_slides/static/src/xml/website_slides_channel.xml']
),
events: _.extend({}, ChannelCreateDialog.prototype.events, {
'change input[name="enroll"]': '_onEnrollChanged',
}),
willStart: async function () {
await this._super();
this.products = await this._rpc({route: '/slides/get_course_products'});
},
/**
* @private
*/
_onEnrollChanged: function (ev) {
this.$('.o_wslides_course_products').toggleClass('d-none', ev.target.value !== 'payment');
},
});
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-extend="website.slide.channel.create">
<t t-jquery="form > div:last" t-operation="after">
<label for="enroll">Enroll Policy</label>
<div class="form-row" id="enroll_policies">
<div class="form-group col-3">
<div class="form-check mx-0">
<input class="form-check-input"
type="radio" name="enroll"
id="enroll_public" value="public"
checked="checked"/>
<label class="form-check-label font-weight-normal"
for="enroll_public">
Public
</label>
</div>
</div>
<div class="form-group col-3">
<div class="form-check mx-0">
<input class="form-check-input"
type="radio" name="enroll"
id="enroll_invitation" value="invite"/>
<label class="form-check-label font-weight-normal"
for="enroll_invitation">
On Invitation
</label>
</div>
</div>
<div class="form-group col-3">
<div class="form-check mx-0">
<input class="form-check-input"
type="radio" name="enroll"
id="enroll_payment" value="payment"/>
<label class="form-check-label font-weight-normal"
for="enroll_payment">
On Payment
</label>
</div>
</div>
</div>
<label class="o_wslides_course_products d-none">Product</label>
<div class="form-group o_wslides_course_products d-none">
<select class="form-control" name="product_id" id="product_id">
<option t-foreach="widget.products" t-as="product"
t-att-value="product.id" t-esc="product.name"/>
</select>
</div>
</t>
</t>
</templates>
@@ -8,7 +8,7 @@
<xpath expr="//field[@name='enroll']" position="after">
<field name="product_id"
attrs="{'invisible': [('enroll', '!=', 'payment')], 'required': [('enroll', '=', 'payment')]}"
context="{'default_detailed_type': 'service', 'default_invoice_policy': 'order', 'default_purchase_ok': False, 'default_sale_ok': True, 'default_website_published': True}"/>
context="{'default_detailed_type': 'course', 'default_invoice_policy': 'order', 'default_purchase_ok': False, 'default_sale_ok': True, 'default_website_published': True}"/>
</xpath>
<xpath expr="//button[@name='action_redirect_to_members']" position="after">
<button name="action_view_sales"
@@ -34,4 +34,15 @@
</div>
</template>
<template id="cart_summary_inherit_website_sale_slides"
inherit_id="website_sale.cart_summary"
name="Course Cart right column">
<xpath expr="//td[hasclass('td-product_name')]/div/strong" position="after">
<div t-if="line.product_id.channel_ids"
t-foreach="line.product_id.channel_ids.filtered(lambda course: course.enroll == 'payment')"
t-as="course"
t-esc="course.name"/>
</xpath>
</template>
</data></odoo>
@@ -7,7 +7,7 @@ import SlidesCourseTagAdd from '@website_slides/js/slides_course_tag_add';
const TagCourseDialog = SlidesCourseTagAdd.TagCourseDialog;
var ChannelCreateDialog = Dialog.extend({
export const ChannelCreateDialog = Dialog.extend({
template: 'website.slide.channel.create',
xmlDependencies: Dialog.prototype.xmlDependencies.concat(
['/website_slides/static/src/xml/website_slides_channel.xml',
@@ -573,6 +573,13 @@ $truncate-limits: 2, 3, 10;
box-shadow: 3px 3px 5px gray;
}
#slide_channel_add_form label img {
// make the layout preview images size depending on the screen size
// so we do not need to scroll on most resolutions
cursor: pointer;
height: 20vh;
}
// Embed PDFViewer
// **************************************************
#PDFViewer.o_wslides_fs_pdf_viewer {
@@ -16,30 +16,25 @@
<input type="text" class="form-control" name="tag_ids" id="tag_ids" placeholder="Tags"/>
</div>
<label for="channel_type">Choose a layout</label>
<div class="form-row">
<div class="form-group col-6">
<div class="form-check px-0">
<input class="form-check-input d-none" type="radio" name="channel_type" id="channel_type1" value="training" checked="checked"/>
<label for="channel_type1">
<img class="w-100" src="/website_slides/static/src/img/channel-training-layout.png" alt="Training Layout"/>
</label>
</div>
</div>
<div class="form-group col-6">
<div class="form-check px-0">
<input class="form-check-input d-none" type="radio" name="channel_type" id="channel_type2" value="documentation"/>
<label for="channel_type2">
<img class="w-100" src="/website_slides/static/src/img/channel-documentation-layout.png" alt="Documentation Layout"/>
</label>
</div>
</div>
<div class="d-flex flex-row justify-content-around">
<input class="form-check-input d-none" type="radio" name="channel_type" id="channel_type1" value="training" checked="checked"/>
<label for="channel_type1">
<img src="/website_slides/static/src/img/channel-training-layout.png"
alt="Training Layout"/>
</label>
<input class="form-check-input d-none" type="radio" name="channel_type" id="channel_type2" value="documentation"/>
<label for="channel_type2">
<img src="/website_slides/static/src/img/channel-documentation-layout.png"
alt="Documentation Layout"/>
</label>
</div>
<div class="form-group">
<label for="title">Description</label>
<textarea rows="2" class="form-control" name="description" id="description"
<textarea rows="1" class="form-control" name="description" id="description"
placeholder="Common tasks for a computer scientist is asking the right questions and answering
questions. In this course, you'll study those topics with activities about mathematics, science and logic." />
</div>
<label id="communication-label">Review</label>
<div class="form-group">
<div class="o_wslide_channel_communication_type">
<div class="form-check">