[MERGE] auth_signup, web, website, portal: clean signin/signup process and customer portal breadcrumb

This merge eases the signin / signup process by rellabelling / adding
options / cleaning support of auth tokens. It also improves customer
portal by cleaning the breadcrumb now using a simple bootstrap menu.
Finally people can now see their sale orders using an access token
without having to rely on website_quote.

Closes 17022.
This commit is contained in:
Thibault Delavallée
2017-07-06 13:21:08 +02:00
39 changed files with 1247 additions and 1146 deletions
@@ -19,7 +19,7 @@
<p>Dear ${object.partner_id.name}
% set access_action = object.with_context(force_website=True).get_access_action()
% set is_online = access_action and access_action['type'] == 'ir.actions.act_url'
% set access_url = is_online and '/mail/view?model=account.invoice&res_id=%d' % (object.id) or '/report/pdf/account.report_invoice/' + str(object.id)
% set access_url = is_online and object.get_mail_url() or '/report/pdf/account.report_invoice/' + str(object.id)
% if object.partner_id.parent_id:
(<i>${object.partner_id.parent_id.name}</i>)
+2 -2
View File
@@ -6,7 +6,7 @@ import json
import requests
from odoo import api, fields, models
from odoo.exceptions import AccessDenied
from odoo.exceptions import AccessDenied, UserError
from odoo.addons.auth_signup.models.res_users import SignupError
from odoo.addons import base
@@ -82,7 +82,7 @@ class ResUsers(models.Model):
try:
_, login, _ = self.signup(values, token)
return login
except SignupError:
except (SignupError, UserError):
raise access_denied_exception
@api.model
+21 -3
View File
@@ -6,11 +6,13 @@ import werkzeug
from odoo import http, _
from odoo.addons.auth_signup.models.res_users import SignupError
from odoo.addons.web.controllers.main import ensure_db, Home
from odoo.exceptions import UserError
from odoo.http import request
from odoo.tools import pycompat
_logger = logging.getLogger(__name__)
class AuthSignupHome(Home):
@http.route()
@@ -33,7 +35,19 @@ class AuthSignupHome(Home):
if 'error' not in qcontext and request.httprequest.method == 'POST':
try:
self.do_signup(qcontext)
# Send an account creation confirmation email
if qcontext.get('token'):
user_sudo = request.env['res.users'].sudo().search([('login', '=', qcontext.get('login'))])
template = request.env.ref('auth_signup.mail_template_user_signup_account_created', raise_if_not_found=False)
if user_sudo and template:
template.sudo().with_context(
lang=user_sudo.lang,
auth_login=werkzeug.url_encode({'auth_login': user_sudo.email}),
password=request.params.get('password')
).send_mail(user_sudo.id, force_send=True)
return super(AuthSignupHome, self).web_login(*args, **kw)
except UserError as e:
qcontext['error'] = str(e)
except (SignupError, AssertionError) as e:
if request.env["res.users"].sudo().search([("login", "=", qcontext.get("login"))]):
qcontext["error"] = _("Another user is already registered using this email address.")
@@ -57,7 +71,7 @@ class AuthSignupHome(Home):
return super(AuthSignupHome, self).web_login(*args, **kw)
else:
login = qcontext.get('login')
assert login, "No login provided."
assert login, _("No login provided.")
_logger.info(
"Password reset attempt for <%s> by user <%s> from %s",
login, request.env.user.login, request.httprequest.remote_addr)
@@ -86,6 +100,8 @@ class AuthSignupHome(Home):
""" Shared helper returning the rendering context for signup and reset password """
qcontext = request.params.copy()
qcontext.update(self.get_auth_signup_config())
if not qcontext.get('token') and request.session.get('auth_signup_token'):
qcontext['token'] = request.session.get('auth_signup_token')
if qcontext.get('token'):
try:
# retrieve the user info (name, login or email) corresponding to a signup token
@@ -100,8 +116,10 @@ class AuthSignupHome(Home):
def do_signup(self, qcontext):
""" Shared helper that creates a res.partner out of a token """
values = { key: qcontext.get(key) for key in ('login', 'name', 'password') }
assert values, "The form was not properly filled in."
assert values.get('password') == qcontext.get('confirm_password'), "Passwords do not match; please retype them."
if not values:
raise UserError(_("The form was not properly filled in."))
if values.get('password') != qcontext.get('confirm_password'):
raise UserError(_("Passwords do not match; please retype them."))
supported_langs = [lang['code'] for lang in request.env['res.lang'].sudo().search_read([], ['code'])]
if request.lang in supported_langs:
values['lang'] = request.lang
@@ -91,5 +91,49 @@
<field name="user_signature" eval="False"/>
</record>
<!-- Email template for new users that used a signup token -->
<record id="mail_template_user_signup_account_created" model="mail.template">
<field name="name">Auth Signup: Odoo Account Created</field>
<field name="model_id" ref="base.model_res_users"/>
<field name="email_from">"${object.company_id.name|safe}" &lt;${(object.company_id.email or user.email)|safe}&gt;</field>
<field name="email_to">${object.email|safe}</field>
<field name="subject">Welcome to ${object.company_id.name}!</field>
<field name="body_html" type="html">
<div style="padding:0px;width:600px;margin:auto;background: #FFFFFF repeat top /100%;color:#777777">
<table cellspacing="0" cellpadding="0" style="width:600px;border-collapse:collapse;background:inherit;color:inherit">
<tbody><tr>
<td valign="center" width="200" style="padding:10px 10px 10px 5px;font-size: 12px">
<img src="/logo.png" style="padding: 0px; margin: 0px; height: auto; width: 80px;" alt="${user.company_id.name}" />
</td>
</tr></tbody>
</table>
</div>
<div style="padding:0px;width:600px;margin:auto;background: #FFFFFF repeat top /100%;color:#777777">
<p>Dear ${object.name},</p>
<p>
Your account has been successfully created!
</p>
<p>
Your login: ${object.email}
<br/>
Your password: ${ctx['password']}
</p>
<p>
To gain access to your account, you can use the following link:
</p>
<div style="text-align: center; margin-top: 16px;">
<a href="/web/login?${ctx['auth_login']}" style="padding: 5px 10px; font-size: 12px; line-height: 18px; color: #FFFFFF; border-color:#875A7B; text-decoration: none; display: inline-block; margin-bottom: 0px; font-weight: 400; text-align: center; vertical-align: middle; cursor: pointer; white-space: nowrap; background-image: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius:3px">Go to My Account</a>
</div>
<p>Best regards,</p>
</div>
<div style="padding:0px;width:600px;margin:auto; margin-top: 10px; background: #fff repeat top /100%;color:#777777">
${user.signature | safe}
<p style="font-size: 11px; margin-top: 10px;">
<strong>Sent by ${user.company_id.name} using <a href="www.odoo.com" style="text-decoration:none; color: #875A7B;">Odoo</a></strong>
</p>
</div></field>
<field name="user_signature" eval="False"/>
</record>
</data>
</odoo>
@@ -2,24 +2,27 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.tools.safe_eval import safe_eval
class BaseConfigSettings(models.TransientModel):
_inherit = 'base.config.settings'
auth_signup_reset_password = fields.Boolean(string='Enable password reset from Login page')
auth_signup_uninvited = fields.Boolean(string='Allow external users to sign up')
auth_signup_uninvited = fields.Selection([
('b2b', 'On invitation (B2B)'),
('b2c', 'Free sign up (B2C)'),
], string='Customer Account')
auth_signup_template_user_id = fields.Many2one('res.users', string='Template user for new users created through signup')
@api.model
def get_values(self):
res = super(BaseConfigSettings, self).get_values()
get_param = self.env['ir.config_parameter'].sudo().get_param
# we use safe_eval on the result, since the value of the parameter is a nonempty string
# the value of the parameter is a nonempty string
res.update(
auth_signup_reset_password=safe_eval(get_param('auth_signup.reset_password', 'False')),
auth_signup_uninvited=safe_eval(get_param('auth_signup.allow_uninvited', 'False')),
auth_signup_template_user_id=safe_eval(get_param('auth_signup.template_user_id', 'False')),
auth_signup_reset_password=get_param('auth_signup.reset_password', 'False').lower() == 'true',
auth_signup_uninvited='b2c' if get_param('auth_signup.allow_uninvited', 'False').lower() == 'true' else 'b2b',
auth_signup_template_user_id=get_param('auth_signup.template_user_id', 'False').lower() == 'true',
)
return res
@@ -29,7 +32,7 @@ class BaseConfigSettings(models.TransientModel):
set_param = self.env['ir.config_parameter'].sudo().set_param
# we store the repr of the values, since the value of the parameter is a required string
set_param('auth_signup.reset_password', repr(self.auth_signup_reset_password))
set_param('auth_signup.allow_uninvited', repr(self.auth_signup_uninvited))
set_param('auth_signup.allow_uninvited', repr(self.auth_signup_uninvited == 'b2c'))
set_param('auth_signup.template_user_id', repr(self.auth_signup_template_user_id.id))
@api.multi
+19 -3
View File
@@ -4,9 +4,10 @@
import random
import werkzeug.urls
from collections import defaultdict
from datetime import datetime, timedelta
from odoo import api, fields, models, _
from odoo import api, exceptions, fields, models, _
from odoo.tools import pycompat
@@ -97,6 +98,21 @@ class ResPartner(models.Model):
def action_signup_prepare(self):
return self.signup_prepare()
def signup_get_auth_param(self):
""" Get a signup token related to the partner if signup is enabled.
If the partner already has a user, get the login parameter.
"""
res = defaultdict(dict)
allow_signup = self.env['ir.config_parameter'].get_param('auth_signup.allow_uninvited', 'False').lower() == 'true'
for partner in self:
if allow_signup and not partner.user_ids:
partner.signup_prepare()
res[partner.id]['auth_signup_token'] = partner.signup_token
elif partner.user_ids:
res[partner.id]['auth_login'] = partner.user_ids[0].login
return res
@api.multi
def signup_cancel(self):
return self.write({'signup_token': False, 'signup_type': False, 'signup_expiration': False})
@@ -125,11 +141,11 @@ class ResPartner(models.Model):
partner = self.search([('signup_token', '=', token)], limit=1)
if not partner:
if raise_exception:
raise SignupError("Signup token '%s' is not valid" % token)
raise exceptions.UserError(_("Signup token '%s' is not valid") % token)
return False
if check_validity and not partner.signup_valid:
if raise_exception:
raise SignupError("Signup token '%s' is no longer valid" % token)
raise exceptions.UserError(_("Signup token '%s' is no longer valid") % token)
return False
return partner
+1 -1
View File
@@ -85,7 +85,7 @@ class ResUsers(models.Model):
# check that uninvited users may sign up
if 'partner_id' not in values:
if not literal_eval(get_param('auth_signup.allow_uninvited', 'False')):
raise SignupError('Signup is not allowed for uninvited users')
raise SignupError(_('Signup is not allowed for uninvited users'))
assert values.get('login'), "Signup: no login given for new user"
assert values.get('partner_id') or values.get('name'), "Signup: no name or partner given for new user"
@@ -2,7 +2,7 @@
<odoo>
<template id="auth_signup.login" inherit_id="web.login" name="Sign up - Reset Password">
<xpath expr="//button[@type='submit']" position="before">
<a t-if="signup_enabled" t-attf-href="/web/signup?{{ keep_query() }}" class="btn btn-link pull-right">Sign up</a>
<a t-if="signup_enabled" t-attf-href="/web/signup?{{ keep_query() }}" class="btn btn-link pull-right">Don't have an account?</a>
<a t-if="reset_password_enabled" t-attf-href="/web/reset_password?{{ keep_query() }}" class="btn btn-link pull-right">Reset Password</a>
</xpath>
</template>
@@ -40,7 +40,7 @@
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<t t-call="auth_signup.fields">
<t t-set="only_passwords" t-value="bool(token)"/>
<t t-set="only_passwords" t-value="bool(token and not invalid_token)"/>
</t>
<p class="alert alert-danger" t-if="error">
@@ -49,7 +49,7 @@
<input type="hidden" name="redirect" t-att-value="redirect"/>
<input type="hidden" name="token" t-att-value="token"/>
<div class="clearfix oe_login_buttons">
<a t-attf-href="/web/login?{{ keep_query() }}" class="btn btn-link pull-right">Back to Login</a>
<a t-attf-href="/web/login?{{ keep_query() }}" class="btn btn-link pull-right">Already have an account?</a>
<button type="submit" class="btn btn-primary pull-left">Sign up</button>
</div>
@@ -9,14 +9,16 @@
<xpath expr="//div[@id='access_rights']" position="after">
<div class="col-xs-12 col-md-6 o_setting_box" title="If unchecked, only invited users may sign up.">
<div class="o_setting_left_pane">
<field name="auth_signup_uninvited"/>
</div>
<div class="o_setting_right_pane">
<label string="User Signup" for="auth_signup_uninvited"/>
<label for="auth_signup_uninvited"/>
<div class="text-muted">
Allow external users to sign up from Login page
Let your customers log in to see their documents
</div>
<div class="content-group" attrs="{'invisible': [('auth_signup_uninvited','=',False)]}">
<div class="mt8">
<field name="auth_signup_uninvited" widget="radio" required="True"/>
</div>
<div class="content-group" attrs="{'invisible': [('auth_signup_uninvited','=','b2b')]}">
<div class="mt16">
<button type="object" name="open_template_user" string="Default Access Rights" icon="fa-arrow-right" class="btn-link"/>
</div>
@@ -6,7 +6,7 @@
<p>Dear ${object.partner_id.name}
% set access_action = object.with_context(force_website=True).get_access_action()
% set is_online = access_action and access_action['type'] == 'ir.actions.act_url'
% set access_url = is_online and '/mail/view?model=account.invoice&res_id=%d' % (object.id) or '/report/pdf/account.report_invoice/' + str(object.id)
% set access_url = is_online and object.get_mail_url() or '/report/pdf/account.report_invoice/' + str(object.id)
% if object.partner_id.parent_id:
(<i>${object.partner_id.parent_id.name}</i>)
+7 -6
View File
@@ -52,7 +52,7 @@ class MailController(http.Controller):
return comparison, record, redirect
@classmethod
def _redirect_to_record(cls, model, res_id):
def _redirect_to_record(cls, model, res_id, access_token=None):
uid = request.session.uid
# no model / res_id, meaning no possible record -> redirect to login
@@ -68,7 +68,6 @@ class MailController(http.Controller):
# the record has a window redirection: check access rights
if uid is not None:
record = record_sudo.sudo(uid)
if not RecordModel.sudo(uid).check_access_rights('read', raise_exception=False):
return cls._redirect_to_messaging()
try:
@@ -76,7 +75,7 @@ class MailController(http.Controller):
except AccessError:
return cls._redirect_to_messaging()
else:
record_action = record.get_access_action()
record_action = record_sudo.get_access_action(access_uid=uid)
else:
record_action = record_sudo.get_access_action()
@@ -162,15 +161,17 @@ class MailController(http.Controller):
return subtypes_list
@http.route('/mail/view', type='http', auth='none')
def mail_action_view(self, model=None, res_id=None, message_id=None):
def mail_action_view(self, model=None, res_id=None, message_id=None, access_token=None, **kwargs):
""" Generic access point from notification emails. The heuristic to
choose where to redirect the user is the following :
choose where to redirect the user is the following :
- find a public URL
- if none found
- users with a read access are redirected to the document
- users without read access are redirected to the Messaging
- not logged users are redirected to the login page
models that have an access_token may apply variations on this.
"""
if message_id:
try:
@@ -185,7 +186,7 @@ class MailController(http.Controller):
elif res_id and isinstance(res_id, basestring):
res_id = int(res_id)
return self._redirect_to_record(model, res_id)
return self._redirect_to_record(model, res_id, access_token)
@http.route('/mail/follow', type='http', auth='user', methods=['GET'])
def mail_action_follow(self, model, res_id, token=None):
+1 -1
View File
@@ -20,7 +20,7 @@
% set doc_name = 'quotation' if object.state in ('draft', 'sent') else 'order confirmation'
% set pay_sign_name = ('require_payment' in object and object.require_payment and 'pay') or 'sign'
% set access_name = is_online and object.template_id and object.state in ('draft', 'sent') and 'Accept and %s online' % pay_sign_name or 'View %s' % doc_name
% set access_url = is_online and access_action['url'] or ''
% set access_url = is_online and object.get_mail_url() or ''
% if object.partner_id.parent_id:
(<i>${object.partner_id.parent_id.name}</i>)
+10 -3
View File
@@ -293,12 +293,19 @@ class SaleOrder(models.Model):
@api.model_cr_context
def _init_column(self, column_name):
""" Initialize the value of the given column for existing rows.
Overridden here because we skip generating unique access tokens
for potentially tons of existing sale orders, should they be needed,
they will be generated on the fly.
Overridden here because we need to generate different access tokens
and by default _init_column calls the default method once and applies
it for every record.
"""
if column_name != 'access_token':
super(SaleOrder, self)._init_column(column_name)
else:
query = """UPDATE %(table_name)s
SET %(column_name)s = md5(md5(random()::varchar || id::varchar) || clock_timestamp()::varchar)::uuid::varchar
WHERE %(column_name)s IS NULL
""" % {'table_name': self._name, 'column_name': column_name}
self.env.cr.execute(query)
def _generate_access_token(self):
for order in self:
+3
View File
@@ -488,6 +488,9 @@ class Home(http.Controller):
if 'error' in request.params and request.params.get('error') == 'access':
values['error'] = _('Only employee can access this database. Please contact the administrator.')
if 'login' not in values and request.session.get('auth_login'):
values['login'] = request.session.get('auth_login')
response = request.render('web.login', values)
response.headers['X-Frame-Options'] = 'DENY'
return response
+7
View File
@@ -134,6 +134,13 @@ class Http(models.AbstractModel):
first_pass = not hasattr(request, 'website')
request.website = None
func = None
# add signup token or login to the session if given
if 'auth_signup_token' in request.params:
request.session['auth_signup_token'] = request.params['auth_signup_token']
if 'auth_login' in request.params:
request.session['auth_login'] = request.params['auth_login']
try:
if request.httprequest.method == 'GET' and '//' in request.httprequest.path:
new_url = request.httprequest.path.replace('//', '/') + '?' + request.httprequest.query_string
@@ -53,6 +53,10 @@ class WebsiteConfigSettings(models.TransientModel):
has_google_analytics = fields.Boolean("Google Analytics")
has_google_analytics_dashboard = fields.Boolean("Google Analytics in Dashboard")
has_google_maps = fields.Boolean("Google Maps")
auth_signup_uninvited = fields.Selection([
('b2b', 'On invitation (B2B)'),
('b2c', 'Free sign up (B2C)'),
], string='Customer Account')
@api.onchange('has_google_analytics')
def onchange_has_google_analytics(self):
@@ -77,12 +81,13 @@ class WebsiteConfigSettings(models.TransientModel):
if not self.user_has_groups('website.group_website_designer'):
raise AccessDenied()
res = super(WebsiteConfigSettings, self).get_values()
params = self.env['ir.config_parameter'].sudo()
get_param = self.env['ir.config_parameter'].sudo().get_param
res.update(
has_google_analytics=params.get_param('website.has_google_analytics'),
has_google_analytics_dashboard=params.get_param('website.has_google_analytics_dashboard'),
has_google_maps=params.get_param('website.has_google_maps'),
google_maps_api_key=params.get_param('google_maps_api_key', default=''),
auth_signup_uninvited='b2c' if get_param('auth_signup.allow_uninvited', 'False').lower() == 'true' else 'b2b',
has_google_analytics=get_param('website.has_google_analytics'),
has_google_analytics_dashboard=get_param('website.has_google_analytics_dashboard'),
has_google_maps=get_param('website.has_google_maps'),
google_maps_api_key=get_param('google_maps_api_key', default=''),
)
return res
@@ -90,7 +95,9 @@ class WebsiteConfigSettings(models.TransientModel):
if not self.user_has_groups('website.group_website_designer'):
raise AccessDenied()
super(WebsiteConfigSettings, self).set_values()
self.env['ir.config_parameter'].sudo().set_param('website.has_google_analytics', self.has_google_analytics)
self.env['ir.config_parameter'].sudo().set_param('website.has_google_analytics_dashboard', self.has_google_analytics_dashboard)
self.env['ir.config_parameter'].sudo().set_param('website.has_google_maps', self.has_google_maps)
self.env['ir.config_parameter'].sudo().set_param('google_maps_api_key', (self.google_maps_api_key or '').strip())
set_param = self.env['ir.config_parameter'].sudo().set_param
set_param('auth_signup.allow_uninvited', repr(self.auth_signup_uninvited == 'b2c'))
set_param('website.has_google_analytics', self.has_google_analytics)
set_param('website.has_google_analytics_dashboard', self.has_google_analytics_dashboard)
set_param('website.has_google_maps', self.has_google_maps)
set_param('google_maps_api_key', (self.google_maps_api_key or '').strip())
@@ -147,6 +147,19 @@
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane">
</div>
<div class="o_setting_right_pane">
<label for="auth_signup_uninvited"/>
<div class="text-muted">
Let your customers log in to see their documents
</div>
<div class="mt8">
<field name="auth_signup_uninvited" widget="radio" required="True"/>
</div>
</div>
</div>
</div>
<h2>Social Media</h2>
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, exceptions, models
from werkzeug.urls import url_encode
class AccountInvoice(models.Model):
@@ -17,22 +18,36 @@ class AccountInvoice(models.Model):
return groups
@api.multi
def get_access_action(self):
def get_access_action(self, access_uid=None):
""" Instead of the classic form view, redirect to the online invoice for portal users. """
self.ensure_one()
if self.env.user.share or self.env.context.get('force_website'):
user, record = self.env.user, self
if access_uid:
user = self.env['res.users'].sudo().browse(access_uid)
record = self.sudo(user)
if user.share or self.env.context.get('force_website'):
try:
self.check_access_rule('read')
record.check_access_rule('read')
except exceptions.AccessError:
pass
else:
return {
'type': 'ir.actions.act_url',
'url': '/my/invoices', # No controller /my/invoices/<int>, only a report pdf
'url': '/my/invoices?', # No controller /my/invoices/<int>, only a report pdf
'target': 'self',
'res_id': self.id,
}
return super(AccountInvoice, self).get_access_action()
return super(AccountInvoice, self).get_access_action(access_uid)
def get_mail_url(self):
self.ensure_one()
params = {
'model': self._name,
'res_id': self.id,
}
params.update(self.partner_id.signup_get_auth_param()[self.partner_id.id])
return '/mail/view?' + url_encode(params)
@api.multi
def get_signup_url(self):
@@ -1,19 +1,17 @@
<odoo>
<template id="portal_my_home_menu_invoice" name="Portal layout : invoice menu entries" inherit_id="website_portal.portal_layout" priority="30">
<xpath expr="//ul[contains(@class,'o_portal_submenu')]" position="inside">
<li t-if="invoice_count" t-att-class="page_name == 'invoice' and 'active'">
<a href="/my/invoices">Invoices</a>
</li>
</xpath>
<xpath expr="//ol[contains(@class,'o_portal_submenu')]" position="inside">
<li t-if="page_name == 'invoice'" class="active">
Invoices
</li>
</xpath>
</template>
<template id="portal_my_home_invoice" name="Portal My Home : invoice entries" inherit_id="website_portal.portal_my_home" priority="30">
<xpath expr="//div[contains(@class,'o_my_home_content')]" position="inside">
<h3 t-if="invoice_count" class="page-header">
<a href="/my/invoices">Your Invoices and Payments
<small class="ml8">
<span class='badge'><t t-esc="invoice_count"/></span>
</small>
<a href="/my/invoices">My Invoices
<span class="badge ml8" t-esc="invoice_count"/>
</a>
</h3>
</xpath>
@@ -22,7 +20,7 @@
<template id="portal_my_invoices" name="My Invoices and Payments">
<t t-call="website_portal.portal_layout">
<div class="page-header">
<span class="h3">Your Invoices</span>
<span class="h3">My Invoices</span>
<t t-call="website_portal.portal_searchbar"/>
</div>
<t t-if="not invoices">
+4 -3
View File
@@ -216,12 +216,13 @@ class BlogPost(models.Model):
return result
@api.multi
def get_access_action(self):
def get_access_action(self, access_uid=None):
""" Instead of the classic form view, redirect to the post on website
directly if user is an employee or if the post is published. """
self.ensure_one()
if self.env.user.share and not self.sudo().website_published:
return super(BlogPost, self).get_access_action()
user = access_uid and self.env['res.users'].sudo().browse(access_uid) or self.env.user
if user.share and not self.sudo().website_published:
return super(BlogPost, self).get_access_action(access_uid)
return {
'type': 'ir.actions.act_url',
'url': self.url,
@@ -193,17 +193,29 @@
</xpath>
</template>
<template id="portal_my_home_menu_lead" name="Portal layout : lead menu entry" inherit_id="website_portal.portal_layout" priority="15">
<xpath expr="//ul[contains(@class,'o_portal_submenu')]" position="inside">
<t t-if="user.commercial_partner_id.grade_id and lead_count">
<li t-att-class="page_name == 'lead' and 'active'">
<a href="/my/leads">Leads</a>
</li>
</t>
<t t-if="user.commercial_partner_id.grade_id">
<li t-att-class="page_name == 'opportunity' and 'active'">
<a href="/my/opportunities">Opportunities</a>
</li>
</t>
<xpath expr="//ol[contains(@class,'o_portal_submenu')]" position="inside">
<li t-if="page_name == 'lead' or lead">
<a t-if="lead" t-attf-href="/my/leads?{{ keep_query() }}">
My Leads
</a>
<t t-else="">
My Leads
</t>
</li>
<li t-if="lead">
<span t-field="lead.name"/>
</li>
<li t-if="page_name == 'opportunity' or opportunity">
<a t-if="opportunity" t-attf-href="/my/opportunities?{{ keep_query() }}">
My Opportunities
</a>
<t t-else="">
My Opportunities
</t>
</li>
<li t-if="opportunity">
<span t-field="opportunity.name"/>
</li>
</xpath>
</template>
@@ -211,21 +223,15 @@
<xpath expr="//div[contains(@class,'o_my_home_content')]" position="inside">
<t t-if="user.commercial_partner_id.grade_id">
<h3 t-if="lead_count" class="page-header">
<a href="/my/leads">Your Leads
<small class="ml8">
<span class='badge'><t t-esc="lead_count"/></span>
</small>
<a href="/my/leads">My Leads
<span class="badge ml8" t-esc="lead_count"/>
</a>
</h3>
<h3 class="page-header">
<a href="/my/opportunities">Your Opportunities
<small class="ml8">
<t t-if="opp_count">
<span class='badge'><t t-esc="opp_count"/></span>
</t>
<t t-if="not opp_count">
There are currently no opportunities.
</t>
<a href="/my/opportunities">My Opportunities
<span t-if="opp_count" class="badge ml8" t-esc="opp_count"/>
<small t-if="not opp_count" class="ml8">
There are currently no opportunities.
</small>
</a>
</h3>
@@ -236,7 +242,7 @@
<template id="portal_my_leads" name="My Leads">
<t t-call="website_portal.portal_layout">
<h3 class="page-header">
Your Leads
My Leads
<t t-call="website_portal.portal_searchbar"/>
</h3>
<div t-if="not leads" class="alert alert-warning mt8" role="alert">
@@ -271,7 +277,7 @@
<template id="portal_my_opportunities" name="My Opportunities">
<t t-call="website_portal.portal_layout">
<h3 class="page-header">
Your Opportunities
My Opportunities
<t t-call="website_portal.portal_searchbar"/>
<button name='new_opp' data-toggle="modal" data-target=".modal_new_opp" class="btn btn-default pull-right mr8"><i class="fa fa-plus fa-lg" /></button>
</h3>
@@ -344,465 +350,431 @@
</template>
<template id="portal_my_lead" name="My Lead">
<t t-call="website.layout">
<div id="wrap">
<div class="container">
<div class="oe_structure">
<div class="row">
<div class="col-sm-6">
<ol class="breadcrumb mt8">
<li><a href="/my/home"><i class="fa fa-home"/></a></li>
<li><a t-attf-href="/my/leads?#{keep_query()}">My Leads</a></li>
<li><span t-field="lead.name"/></li>
</ol>
</div>
</div>
</div>
<div class="container">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-12">
<span class="pull-right" title="Rating">
<t t-foreach="range(1, 4)" t-as="i">
<span t-attf-class="fa fa-lg fa-star#{'' if i &lt;= int(lead.priority) else '-o'}"/>
</t>
</span>
<h4>
Lead - <span t-field="lead.name"/>
</h4>
</div>
</div>
</div>
<div class="panel-body row">
<div class="col-md-6">
<div class="row" t-if="lead.partner_name or lead.email_from or lead.partner_id">
<label class="col-xs-4">Customer</label>
<address class="col-xs-8">
<div>
<span t-if="lead.partner_name" itemprop="name" t-field="lead.partner_name" />
<span t-if="not lead.partner_name" itemprop="name" t-field="lead.contact_name"/>
</div>
<div t-if="lead.phone">
<span class="fa fa-phone" /> <span itemprop="telephone" t-field="lead.phone" />
</div>
<div t-if="lead.mobile">
<span class="fa fa-mobile" /> <span itemprop="telephone" t-field="lead.mobile" />
</div>
<div t-if="lead.email_from">
<span class="fa fa-envelope"/> <span itemprop="email" t-field="lead.email_from" />
</div>
</address>
</div>
<div class="row" t-if="lead.street or lead.street2 or lead.city or lead.state_id or lead.country_id">
<label class="col-xs-4">Address</label>
<address class="col-xs-8">
<div t-if="lead.street"><span t-field="lead.street"/></div>
<div t-if="lead.street2"><span t-field="lead.street2"/></div>
<div t-if="lead.city or lead.zip">
<span t-field="lead.city"/> <span t-field="lead.zip"/>
</div>
<div t-if="lead.state_id or country_id">
<span t-field="lead.state_id"/> <span t-field="lead.country_id"/>
</div>
</address>
</div>
<div class="row" t-if="lead.user_id">
<label class="col-xs-4">Salesperson</label>
<span class="col-xs-8" t-field="lead.user_id" />
</div>
<div class="row" t-if="lead.team_id">
<label class="col-xs-4">Sales Channel</label>
<span class="col-xs-8" t-field="lead.team_id" />
</div>
</div>
<div class="col-md-6">
<div class="row" t-if="lead.date_deadline">
<label class="col-xs-4">Expected Closing</label>
<span class="col-xs-8" t-field="lead.date_deadline" />
</div>
<div class="row" groups="!base.group_portal" t-if="lead.tag_ids">
<label class="col-xs-4">Tags</label>
<span class="col-xs-8">
<t t-foreach="lead.tag_ids" t-as="tag">
<span class="label label-info" t-esc="tag.name" />
</t>
</span>
</div>
<div class="row mt16" t-if="lead.partner_assigned_id">
<label class="col-xs-4">Assigned Partner</label>
<address class="col-xs-8" t-field="lead.partner_assigned_id" t-options='{"widget": "contact", "fields": ["name", "email", "phone"], "no_marker": True}'/>
</div>
<div class="row mt16" t-if="lead.campaign_id">
<label class="col-xs-4">Campaign</label>
<span class="col-xs-8" t-field="lead.campaign_id" />
</div>
<div class="row" t-if="lead.medium_id">
<label class="col-xs-4">Medium</label>
<span class="col-xs-8" t-field="lead.medium_id" />
</div>
</div>
</div>
</div>
<div>
<a class="btn btn-primary btn" data-toggle="modal" data-target=".modal_partner_assign_interested"><i class="fa fa-file-text-o"/> I'm interested</a>
<a class="btn btn-primary btn" data-toggle="modal" data-target=".modal_partner_assign_desinterested"><i class="fa fa-fw fa-times"/> I'm not interested</a>
<div class="modal fade modal_partner_assign_interested" role="form" aria-hidden="true">
<div class="modal-dialog">
<form method="POST" class="js_accept_json modal-content js_website_submit_form interested_partner_assign_form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="lead_id" class="assign_lead_id" t-att-value="lead.id"/>
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&amp;times;</button>
<h4 class="modal-title">Lead Feedback</h4>
</div>
<div class="modal-body" id="sign-dialog">
<div class="form-group">
<label class="control-label" for="comment">What is the next action? When? What is the expected revenue?</label>
<input type="text" name="comment" id="comment" class="form-control comment_interested"/>
</div>
<div class="form-group">
<label class="control-label" for="customer_contacted">Do you have contacted the customer?</label>
<input type="checkbox" name="customer_contacted" id="customer_contacted" class="contacted_interested"/>
</div>
<div>
<span class="text-danger error_partner_assign_interested" style="display:none;">You need to fill up the next action and contact the customer before accepting the lead</span>
</div>
</div>
<div class="modal-footer">
<button t-attf-class="btn btn-primary interested_partner_assign_confirm">Confirm</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
<div class="modal fade modal_partner_assign_desinterested" role="form" aria-hidden="true">
<div class="modal-dialog">
<form method="POST" class="js_accept_json modal-content js_website_submit_form desinterested_partner_assign_form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="lead_id" class="assign_lead_id" t-att-value="lead.id"/>
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&amp;times;</button>
<h4 class="modal-title">Lead Feedback</h4>
</div>
<div class="modal-body" id="sign-dialog">
<div class="form-group">
<label class="control-label" for="comment">Why aren't you interested by this lead?</label>
<input type="text" name="comment" id="comment" class="form-control comment_desinterested"/>
</div>
<div class="form-group">
<label class="control-label" for="contacted_desinterested">Do you have contacted the customer?</label>
<input type="checkbox" name="contacted_desinterested" id="contacted_desinterested" class="contacted_desinterested"/>
</div>
<div class="form-group">
<label class="control-label" for="customer_mark_spam">This lead is a spam</label>
<input type="checkbox" name="customer_mark_spam" id="customer_mark_spam" class="customer_mark_spam"/>
</div>
<div>
<span class="text-danger error_partner_assign_desinterested" style="display:none;">You need to fill up the next action and contact the customer before accepting the lead</span>
</div>
</div>
<div class="modal-footer">
<button t-attf-class="btn btn-primary desinterested_partner_assign_confirm">Confirm</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<div class="row mt32">
<div class="col-md-12">
<h4><strong>Message and communication history</strong></h4>
</div>
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="lead"/>
<t t-call="website_portal.portal_layout">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-12">
<span class="pull-right" title="Rating">
<t t-foreach="range(1, 4)" t-as="i">
<span t-attf-class="fa fa-lg fa-star#{'' if i &lt;= int(lead.priority) else '-o'}"/>
</t>
</div>
</span>
<h4>
Lead - <span t-field="lead.name"/>
</h4>
</div>
</div>
</div>
<div class="panel-body row">
<div class="col-md-6">
<div class="row" t-if="lead.partner_name or lead.email_from or lead.partner_id">
<label class="col-xs-4">Customer</label>
<address class="col-xs-8">
<div>
<span t-if="lead.partner_name" itemprop="name" t-field="lead.partner_name" />
<span t-if="not lead.partner_name" itemprop="name" t-field="lead.contact_name"/>
</div>
<div t-if="lead.phone">
<span class="fa fa-phone" /> <span itemprop="telephone" t-field="lead.phone" />
</div>
<div t-if="lead.mobile">
<span class="fa fa-mobile" /> <span itemprop="telephone" t-field="lead.mobile" />
</div>
<div t-if="lead.email_from">
<span class="fa fa-envelope"/> <span itemprop="email" t-field="lead.email_from" />
</div>
</address>
</div>
<div class="row" t-if="lead.street or lead.street2 or lead.city or lead.state_id or lead.country_id">
<label class="col-xs-4">Address</label>
<address class="col-xs-8">
<div t-if="lead.street"><span t-field="lead.street"/></div>
<div t-if="lead.street2"><span t-field="lead.street2"/></div>
<div t-if="lead.city or lead.zip">
<span t-field="lead.city"/> <span t-field="lead.zip"/>
</div>
<div t-if="lead.state_id or country_id">
<span t-field="lead.state_id"/> <span t-field="lead.country_id"/>
</div>
</address>
</div>
<div class="row" t-if="lead.user_id">
<label class="col-xs-4">Salesperson</label>
<span class="col-xs-8" t-field="lead.user_id" />
</div>
<div class="row" t-if="lead.team_id">
<label class="col-xs-4">Sales Channel</label>
<span class="col-xs-8" t-field="lead.team_id" />
</div>
</div>
<div class="col-md-6">
<div class="row" t-if="lead.date_deadline">
<label class="col-xs-4">Expected Closing</label>
<span class="col-xs-8" t-field="lead.date_deadline" />
</div>
<div class="row" groups="!base.group_portal" t-if="lead.tag_ids">
<label class="col-xs-4">Tags</label>
<span class="col-xs-8">
<t t-foreach="lead.tag_ids" t-as="tag">
<span class="label label-info" t-esc="tag.name" />
</t>
</span>
</div>
<div class="row mt16" t-if="lead.partner_assigned_id">
<label class="col-xs-4">Assigned Partner</label>
<address class="col-xs-8" t-field="lead.partner_assigned_id" t-options='{"widget": "contact", "fields": ["name", "email", "phone"], "no_marker": True}'/>
</div>
<div class="row mt16" t-if="lead.campaign_id">
<label class="col-xs-4">Campaign</label>
<span class="col-xs-8" t-field="lead.campaign_id" />
</div>
<div class="row" t-if="lead.medium_id">
<label class="col-xs-4">Medium</label>
<span class="col-xs-8" t-field="lead.medium_id" />
</div>
</div>
</div>
</div>
<div>
<a class="btn btn-primary btn" data-toggle="modal" data-target=".modal_partner_assign_interested"><i class="fa fa-file-text-o"/> I'm interested</a>
<a class="btn btn-primary btn" data-toggle="modal" data-target=".modal_partner_assign_desinterested"><i class="fa fa-fw fa-times"/> I'm not interested</a>
<div class="modal fade modal_partner_assign_interested" role="form" aria-hidden="true">
<div class="modal-dialog">
<form method="POST" class="js_accept_json modal-content js_website_submit_form interested_partner_assign_form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="lead_id" class="assign_lead_id" t-att-value="lead.id"/>
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&amp;times;</button>
<h4 class="modal-title">Lead Feedback</h4>
</div>
<div class="modal-body" id="sign-dialog">
<div class="form-group">
<label class="control-label" for="comment">What is the next action? When? What is the expected revenue?</label>
<input type="text" name="comment" id="comment" class="form-control comment_interested"/>
</div>
<div class="form-group">
<label class="control-label" for="customer_contacted">Do you have contacted the customer?</label>
<input type="checkbox" name="customer_contacted" id="customer_contacted" class="contacted_interested"/>
</div>
<div>
<span class="text-danger error_partner_assign_interested" style="display:none;">You need to fill up the next action and contact the customer before accepting the lead</span>
</div>
</div>
<div class="modal-footer">
<button t-attf-class="btn btn-primary interested_partner_assign_confirm">Confirm</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
<div class="modal fade modal_partner_assign_desinterested" role="form" aria-hidden="true">
<div class="modal-dialog">
<form method="POST" class="js_accept_json modal-content js_website_submit_form desinterested_partner_assign_form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="lead_id" class="assign_lead_id" t-att-value="lead.id"/>
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&amp;times;</button>
<h4 class="modal-title">Lead Feedback</h4>
</div>
<div class="modal-body" id="sign-dialog">
<div class="form-group">
<label class="control-label" for="comment">Why aren't you interested by this lead?</label>
<input type="text" name="comment" id="comment" class="form-control comment_desinterested"/>
</div>
<div class="form-group">
<label class="control-label" for="contacted_desinterested">Do you have contacted the customer?</label>
<input type="checkbox" name="contacted_desinterested" id="contacted_desinterested" class="contacted_desinterested"/>
</div>
<div class="form-group">
<label class="control-label" for="customer_mark_spam">This lead is a spam</label>
<input type="checkbox" name="customer_mark_spam" id="customer_mark_spam" class="customer_mark_spam"/>
</div>
<div>
<span class="text-danger error_partner_assign_desinterested" style="display:none;">You need to fill up the next action and contact the customer before accepting the lead</span>
</div>
</div>
<div class="modal-footer">
<button t-attf-class="btn btn-primary desinterested_partner_assign_confirm">Confirm</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<div class="row mt32">
<div class="col-md-12">
<h4><strong>Message and communication history</strong></h4>
</div>
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="lead"/>
</t>
</div>
</div>
</t>
</template>
<template id="portal_my_opportunity" name="My Opportunity">
<t t-call="website.layout">
<div id="wrap">
<div class="container">
<div class="oe_structure">
<div class="row">
<div class="col-sm-6">
<ol class="breadcrumb mt8">
<li><a href="/my/home"><i class="fa fa-home"/></a></li>
<li><a t-attf-href="/my/opportunities?#{keep_query()}">My Opportunities</a></li>
<li><span t-field="opportunity.name"/></li>
</ol>
</div>
</div>
</div>
<div class="container">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-12">
<h4 style="display:inline-block">
Opportunity - <span t-field="opportunity.name"/>
</h4>
<span class="btn-group pull-right" role="group">
<t t-foreach="stages" t-as="stage">
<button type="button" t-att-data="stage.id" t-att-opp="opportunity.id" t-attf-class="pull-right btn btn-sm opp-stage-button #{'btn-default' if opportunity.stage_id.name != stage.name else 'disabled btn-primary'}">
<span t-field="stage.name"/>
</button>
</t>
</span>
</div>
</div>
<div class="row">
<div class="col-md-4">
<strong><span class="text-nowrap" t-esc="opportunity.planned_revenue" t-options="{'widget': 'monetary', 'display_currency': opportunity.company_currency}"/> at <span t-field="opportunity.probability"/>%</strong>
</div>
<div class="col-md-4">
<strong>Expected Closing: <span t-field="opportunity.date_deadline" /></strong>
</div>
<div class="col-md-4">
<span class="pull-right" title="Rating">
<t t-foreach="range(1, 4)" t-as="i">
<span t-attf-class="fa fa-lg fa-star#{'' if i &lt;= int(opportunity.priority) else '-o'}"/>
</t>
</span>
</div>
</div>
</div>
<div class="panel-body">
<div class="col-md-6">
<div>
<button type="button" data-toggle="modal" data-target=".modal_edit_contact" class="btn btn-sm btn-primary" style="margin-bottom: 10px;">Edit Contact</button>
<div class="modal fade modal_edit_contact" role="form" aria-hidden="true">
<div class="modal-dialog">
<form method="POST" class="js_accept_json modal-content js_website_submit_form edit_contact_form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="opportunity_id" class="opportunity_id" t-att-value="opportunity.id"/>
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&amp;times;</button>
<h4 class="modal-title">Edit Contact</h4>
</div>
<div class="modal-body" id="sign-dialog">
<t t-if="opportunity.partner_name">
<div class="form-group">
<label class="control-label" for="partner_name">Customer Name</label>
<input type="text" name="partner_name" class="form-control partner_name" t-att-value="opportunity.partner_name"/>
</div>
</t>
<t t-if="not opportunity.partner_name">
<div class="form-group">
<label class="control-label" for="partner_name">Customer Name</label>
<input type="text" name="partner_name" class="form-control partner_name" t-att-value="opportunity.contact_name"/>
</div>
</t>
<div class="form-group">
<label class="control-label" for="phone">Phone</label>
<input type="text" name="phone" class="form-control phone" t-att-value="opportunity.phone"/>
</div>
<div class="form-group">
<label class="control-label" for="mobile">Mobile</label>
<input type="text" name="mobile" class="form-control mobile" t-att-value="opportunity.mobile"/>
</div>
<div class="form-group">
<label class="control-label" for="email_from">Email</label>
<input type="text" name="email_from" class="form-control email_from" t-att-value="opportunity.email_from"/>
</div>
<div class="form-group">
<label class="control-label" for="street">Address</label>
<input type="text" name="street" class="form-control street" t-att-value="opportunity.street" placeholder="Street"/>
</div>
<div class="form-group">
<input type="text" name="street2" class="form-control street2" t-att-value="opportunity.street2" placeholder="Street2"/>
</div>
<div class="form-group">
<div class="row">
<div class="col-sm-5">
<input type="text" name="city" class="form-control city" t-att-value="opportunity.city" placeholder="City"/>
</div>
<div class="col-sm-5">
<select name="state_id" class="form-control state_id">
<option>States...</option>
<t t-foreach="states or []" t-as="state">
<option class="state" t-att-value="state.id" t-att-country="state.country_id.id" t-att-selected="state.id == opportunity.state_id.id">
<t t-esc="state.name" />
</option>
</t>
</select>
</div>
<div class="col-sm-2">
<input type="text" name="zip" class="form-control zip" t-att-value="opportunity.zip" placeholder="ZIP"/>
</div>
</div>
</div>
<div class="form-group">
<select name="country_id" class="form-control country_id">
<option>Countries...</option>
<t t-foreach="countries or []" t-as="country">
<option t-att-value="country.id" t-att-selected="country.id == opportunity.country_id.id">
<t t-esc="country.name" />
</option>
</t>
</select>
</div>
</div>
<div class="modal-footer">
<button t-attf-class="btn btn-primary edit_contact_confirm">Confirm</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<div class="row" t-if="opportunity.partner_name or opportunity.email_from or opportunity.contact_name">
<label class="col-xs-4">Customer</label>
<address class="col-xs-8">
<div>
<span t-if="opportunity.partner_name" itemprop="name" t-field="opportunity.partner_name" />
<span t-if="not opportunity.partner_name" itemprop="name" t-field="opportunity.contact_name" />
</div>
<div t-if="opportunity.phone">
<span class="fa fa-phone" /> <span itemprop="telephone" t-field="opportunity.phone" />
</div>
<div t-if="opportunity.mobile">
<span class="fa fa-mobile" /> <span itemprop="mobile" t-field="opportunity.mobile" />
</div>
<div t-if="opportunity.email_from">
<span class="fa fa-envelope"/> <span itemprop="email" t-field="opportunity.email_from" />
</div>
</address>
</div>
<div class="row" t-if="opportunity.street or opportunity.street2 or opportunity.city or opportunity.state_id or opportunity.country_id">
<label class="col-xs-4">Address</label>
<address class="col-xs-8">
<div t-if="opportunity.street"><span t-field="opportunity.street"/></div>
<div t-if="opportunity.street2"><span t-field="opportunity.street2"/></div>
<div t-if="opportunity.city or opportunity.zip">
<span t-field="opportunity.city"/> <span t-field="opportunity.zip"/>
</div>
<div t-if="opportunity.state_id or country_id">
<span t-field="opportunity.state_id"/> <span t-field="opportunity.country_id"/>
</div>
</address>
</div>
</div>
<div class="col-md-6">
<div>
<button type="button" data-toggle="modal" data-target=".modal_edit_opp" class="btn btn-sm btn-primary" style="margin-bottom: 10px;">Edit Opportunity</button>
<div class="modal fade modal_edit_opp" role="form" aria-hidden="true">
<div class="modal-dialog">
<form method="POST" class="js_accept_json modal-content js_website_submit_form edit_opp_form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="opportunity_id" class="opportunity_id" t-att-value="opportunity.id"/>
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&amp;times;</button>
<h4 class="modal-title">Edit Opportunity</h4>
</div>
<div class="modal-body" id="sign-dialog">
<div class="form-group">
<div class="row">
<div class="col-sm-6">
<div class="input-group">
<div class="input-group-addon"><span class="text-nowrap" t-esc="opportunity.company_currency.symbol"/></div>
<input type="text" name="planned_revenue" class="form-control planned_revenue" t-att-value="opportunity.planned_revenue" placeholder="Planned Revenue"/>
</div>
</div>
<div class="col-sm-1">
<span>at </span>
</div>
<div class="col-sm-5">
<div class="input-group">
<input type="text" name="probability" class="form-control probability" t-att-value="opportunity.probability" placeholder="Probability"/>
<div class="input-group-addon">%</div>
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="row">
<div class="col-sm-6">
<label>Priority: </label>
<div class="input-group">
<label class="radio-inline">
<input type="radio" name="PriorityRadioOptions" value="0" t-att-checked="opportunity.priority not in ['1','2','3']"/><i class="fa fa-star-o"></i>
</label>
<label class="radio-inline">
<input type="radio" name="PriorityRadioOptions" value="1" t-att-checked="opportunity.priority == '1'"/><i class="fa fa-star"></i>
</label>
<label class="radio-inline">
<input type="radio" name="PriorityRadioOptions" value="2" t-att-checked="opportunity.priority == '2'"/><i class="fa fa-star"></i><i class="fa fa-star"></i>
</label>
<label class="radio-inline">
<input type="radio" name="PriorityRadioOptions" value="3" t-att-checked="opportunity.priority == '3'"/><i class="fa fa-star"></i><i class="fa fa-star"></i><i class="fa fa-star"></i>
</label>
</div>
</div>
<div class="col-sm-6">
<label>Deadline: </label>
<div class='input-group date'>
<input type="text" name="date_deadline" t-att-value="opportunity.date_deadline" class="form-control date_deadline" data-date-format="YYYY-MM-DD" t-att-name="prefix" placeholder="YYYY-MM-DD" />
<span class="input-group-addon"><span class="fa fa-calendar"></span></span>
</div>
</div>
</div>
</div>
<div class="form-group">
<label class="control-label" for="next_activity">Next Activity</label>
<select class="form-control next_activity" name="next_activity">
<t t-foreach="activity_types" t-as="activity_type">
<option t-att-data="activity_type.id" t-att-selected="activity_type.id == user_activity.activity_type_id.id" t-att-name="activity_type.name" t-att-days="activity_type.days" t-att-summary="activity_type.summary"><t t-esc="activity_type.name"/></option>
</t>
</select>
</div>
<div class="form-group">
<label class="control-label" for="activity_summary">Details Next Activity</label>
<textarea row="3" name="activity_summary" class="form-control activity_summary"><t t-esc="user_activity.summary"/></textarea>
</div>
<div class="form-group">
<label class="control-label" for="activity_date_deadline">Next Activity Date</label>
<div class='input-group date'>
<input type="text" name="activity_date_deadline" t-att-value="user_activity.date_deadline" class="form-control activity_date_deadline" data-date-format="YYYY-MM-DD" t-att-name="prefix" placeholder="YYYY-MM-DD" />
<span class="input-group-addon"><span class="fa fa-calendar"></span></span>
</div>
</div>
</div>
<div class="modal-footer">
<button t-attf-class="btn btn-primary edit_opp_confirm">Confirm</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<div class="row" t-if="user_activity">
<label class="col-xs-4">Next Activity</label>
<span class="col-xs-8">
<span t-field="user_activity.activity_type_id"/>
<span t-if="user_activity.date_deadline">&#160;on&#160;</span>
<span t-field="user_activity.date_deadline"/>
</span>
</div>
<div class="row" t-if="user_activity">
<span class="col-xs-8 col-xs-offset-4" t-field="user_activity.summary"/>
</div>
</div>
</div>
</div>
<div class="row mt32">
<div class="col-md-12">
<h4><strong>Message and communication history</strong></h4>
</div>
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="opportunity"/>
<t t-call="website_portal.portal_layout">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-12">
<h4 style="display:inline-block">
Opportunity - <span t-field="opportunity.name"/>
</h4>
<span class="btn-group pull-right" role="group">
<t t-foreach="stages" t-as="stage">
<button type="button" t-att-data="stage.id" t-att-opp="opportunity.id" t-attf-class="pull-right btn btn-sm opp-stage-button #{'btn-default' if opportunity.stage_id.name != stage.name else 'disabled btn-primary'}">
<span t-field="stage.name"/>
</button>
</t>
</div>
</span>
</div>
</div>
<div class="row">
<div class="col-md-4">
<strong><span class="text-nowrap" t-esc="opportunity.planned_revenue" t-options="{'widget': 'monetary', 'display_currency': opportunity.company_currency}"/> at <span t-field="opportunity.probability"/>%</strong>
</div>
<div class="col-md-4">
<strong>Expected Closing: <span t-field="opportunity.date_deadline" /></strong>
</div>
<div class="col-md-4">
<span class="pull-right" title="Rating">
<t t-foreach="range(1, 4)" t-as="i">
<span t-attf-class="fa fa-lg fa-star#{'' if i &lt;= int(opportunity.priority) else '-o'}"/>
</t>
</span>
</div>
</div>
</div>
<div class="panel-body">
<div class="col-md-6">
<div>
<button type="button" data-toggle="modal" data-target=".modal_edit_contact" class="btn btn-sm btn-primary" style="margin-bottom: 10px;">Edit Contact</button>
<div class="modal fade modal_edit_contact" role="form" aria-hidden="true">
<div class="modal-dialog">
<form method="POST" class="js_accept_json modal-content js_website_submit_form edit_contact_form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="opportunity_id" class="opportunity_id" t-att-value="opportunity.id"/>
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&amp;times;</button>
<h4 class="modal-title">Edit Contact</h4>
</div>
<div class="modal-body" id="sign-dialog">
<t t-if="opportunity.partner_name">
<div class="form-group">
<label class="control-label" for="partner_name">Customer Name</label>
<input type="text" name="partner_name" class="form-control partner_name" t-att-value="opportunity.partner_name"/>
</div>
</t>
<t t-if="not opportunity.partner_name">
<div class="form-group">
<label class="control-label" for="partner_name">Customer Name</label>
<input type="text" name="partner_name" class="form-control partner_name" t-att-value="opportunity.contact_name"/>
</div>
</t>
<div class="form-group">
<label class="control-label" for="phone">Phone</label>
<input type="text" name="phone" class="form-control phone" t-att-value="opportunity.phone"/>
</div>
<div class="form-group">
<label class="control-label" for="mobile">Mobile</label>
<input type="text" name="mobile" class="form-control mobile" t-att-value="opportunity.mobile"/>
</div>
<div class="form-group">
<label class="control-label" for="email_from">Email</label>
<input type="text" name="email_from" class="form-control email_from" t-att-value="opportunity.email_from"/>
</div>
<div class="form-group">
<label class="control-label" for="street">Address</label>
<input type="text" name="street" class="form-control street" t-att-value="opportunity.street" placeholder="Street"/>
</div>
<div class="form-group">
<input type="text" name="street2" class="form-control street2" t-att-value="opportunity.street2" placeholder="Street2"/>
</div>
<div class="form-group">
<div class="row">
<div class="col-sm-5">
<input type="text" name="city" class="form-control city" t-att-value="opportunity.city" placeholder="City"/>
</div>
<div class="col-sm-5">
<select name="state_id" class="form-control state_id">
<option>States...</option>
<t t-foreach="states or []" t-as="state">
<option class="state" t-att-value="state.id" t-att-country="state.country_id.id" t-att-selected="state.id == opportunity.state_id.id">
<t t-esc="state.name" />
</option>
</t>
</select>
</div>
<div class="col-sm-2">
<input type="text" name="zip" class="form-control zip" t-att-value="opportunity.zip" placeholder="ZIP"/>
</div>
</div>
</div>
<div class="form-group">
<select name="country_id" class="form-control country_id">
<option>Countries...</option>
<t t-foreach="countries or []" t-as="country">
<option t-att-value="country.id" t-att-selected="country.id == opportunity.country_id.id">
<t t-esc="country.name" />
</option>
</t>
</select>
</div>
</div>
<div class="modal-footer">
<button t-attf-class="btn btn-primary edit_contact_confirm">Confirm</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<div class="row" t-if="opportunity.partner_name or opportunity.email_from or opportunity.contact_name">
<label class="col-xs-4">Customer</label>
<address class="col-xs-8">
<div>
<span t-if="opportunity.partner_name" itemprop="name" t-field="opportunity.partner_name" />
<span t-if="not opportunity.partner_name" itemprop="name" t-field="opportunity.contact_name" />
</div>
<div t-if="opportunity.phone">
<span class="fa fa-phone" /> <span itemprop="telephone" t-field="opportunity.phone" />
</div>
<div t-if="opportunity.mobile">
<span class="fa fa-mobile" /> <span itemprop="mobile" t-field="opportunity.mobile" />
</div>
<div t-if="opportunity.email_from">
<span class="fa fa-envelope"/> <span itemprop="email" t-field="opportunity.email_from" />
</div>
</address>
</div>
<div class="row" t-if="opportunity.street or opportunity.street2 or opportunity.city or opportunity.state_id or opportunity.country_id">
<label class="col-xs-4">Address</label>
<address class="col-xs-8">
<div t-if="opportunity.street"><span t-field="opportunity.street"/></div>
<div t-if="opportunity.street2"><span t-field="opportunity.street2"/></div>
<div t-if="opportunity.city or opportunity.zip">
<span t-field="opportunity.city"/> <span t-field="opportunity.zip"/>
</div>
<div t-if="opportunity.state_id or country_id">
<span t-field="opportunity.state_id"/> <span t-field="opportunity.country_id"/>
</div>
</address>
</div>
</div>
<div class="col-md-6">
<div>
<button type="button" data-toggle="modal" data-target=".modal_edit_opp" class="btn btn-sm btn-primary" style="margin-bottom: 10px;">Edit Opportunity</button>
<div class="modal fade modal_edit_opp" role="form" aria-hidden="true">
<div class="modal-dialog">
<form method="POST" class="js_accept_json modal-content js_website_submit_form edit_opp_form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="opportunity_id" class="opportunity_id" t-att-value="opportunity.id"/>
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&amp;times;</button>
<h4 class="modal-title">Edit Opportunity</h4>
</div>
<div class="modal-body" id="sign-dialog">
<div class="form-group">
<div class="row">
<div class="col-sm-6">
<div class="input-group">
<div class="input-group-addon"><span class="text-nowrap" t-esc="opportunity.company_currency.symbol"/></div>
<input type="text" name="planned_revenue" class="form-control planned_revenue" t-att-value="opportunity.planned_revenue" placeholder="Planned Revenue"/>
</div>
</div>
<div class="col-sm-1">
<span>at </span>
</div>
<div class="col-sm-5">
<div class="input-group">
<input type="text" name="probability" class="form-control probability" t-att-value="opportunity.probability" placeholder="Probability"/>
<div class="input-group-addon">%</div>
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="row">
<div class="col-sm-6">
<label>Priority: </label>
<div class="input-group">
<label class="radio-inline">
<input type="radio" name="PriorityRadioOptions" value="0" t-att-checked="opportunity.priority not in ['1','2','3']"/><i class="fa fa-star-o"></i>
</label>
<label class="radio-inline">
<input type="radio" name="PriorityRadioOptions" value="1" t-att-checked="opportunity.priority == '1'"/><i class="fa fa-star"></i>
</label>
<label class="radio-inline">
<input type="radio" name="PriorityRadioOptions" value="2" t-att-checked="opportunity.priority == '2'"/><i class="fa fa-star"></i><i class="fa fa-star"></i>
</label>
<label class="radio-inline">
<input type="radio" name="PriorityRadioOptions" value="3" t-att-checked="opportunity.priority == '3'"/><i class="fa fa-star"></i><i class="fa fa-star"></i><i class="fa fa-star"></i>
</label>
</div>
</div>
<div class="col-sm-6">
<label>Deadline: </label>
<div class='input-group date'>
<input type="text" name="date_deadline" t-att-value="opportunity.date_deadline" class="form-control date_deadline" data-date-format="YYYY-MM-DD" t-att-name="prefix" placeholder="YYYY-MM-DD" />
<span class="input-group-addon"><span class="fa fa-calendar"></span></span>
</div>
</div>
</div>
</div>
<div class="form-group">
<label class="control-label" for="next_activity">Next Activity</label>
<select class="form-control next_activity" name="next_activity">
<t t-foreach="activity_types" t-as="activity_type">
<option t-att-data="activity_type.id" t-att-selected="activity_type.id == user_activity.activity_type_id.id" t-att-name="activity_type.name" t-att-days="activity_type.days" t-att-summary="activity_type.summary"><t t-esc="activity_type.name"/></option>
</t>
</select>
</div>
<div class="form-group">
<label class="control-label" for="activity_summary">Details Next Activity</label>
<textarea row="3" name="activity_summary" class="form-control activity_summary"><t t-esc="user_activity.summary"/></textarea>
</div>
<div class="form-group">
<label class="control-label" for="activity_date_deadline">Next Activity Date</label>
<div class='input-group date'>
<input type="text" name="activity_date_deadline" t-att-value="user_activity.date_deadline" class="form-control activity_date_deadline" data-date-format="YYYY-MM-DD" t-att-name="prefix" placeholder="YYYY-MM-DD" />
<span class="input-group-addon"><span class="fa fa-calendar"></span></span>
</div>
</div>
</div>
<div class="modal-footer">
<button t-attf-class="btn btn-primary edit_opp_confirm">Confirm</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<div class="row" t-if="user_activity">
<label class="col-xs-4">Next Activity</label>
<span class="col-xs-8">
<span t-field="user_activity.activity_type_id"/>
<span t-if="user_activity.date_deadline">&#160;on&#160;</span>
<span t-field="user_activity.date_deadline"/>
</span>
</div>
<div class="row" t-if="user_activity">
<span class="col-xs-8 col-xs-offset-4" t-field="user_activity.summary"/>
</div>
</div>
</div>
</div>
<div class="row mt32">
<div class="col-md-12">
<h4><strong>Message and communication history</strong></h4>
</div>
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="opportunity"/>
</t>
</div>
</div>
</t>
+1 -1
View File
@@ -776,7 +776,7 @@ class Post(models.Model):
return True
@api.multi
def get_access_action(self):
def get_access_action(self, access_uid=None):
""" Instead of the classic form view, redirect to the post on the website directly """
self.ensure_one()
return {
@@ -28,3 +28,7 @@
height: 40px;
width: 50px;
}
.breadcrumb {
display: inline-block;
}
@@ -99,42 +99,40 @@
<template id="portal_layout" name="Portal Layout">
<t t-call="website.layout">
<div class="container mt16 o_website_portal">
<div class="navbar navbar-default">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#oe-help-navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/my/home">
My Account
</a>
</div>
<div class="collapse navbar-collapse" id="oe-help-navbar-collapse">
<ul class="nav navbar-nav o_portal_submenu">
</ul>
<div class="oe_structure">
<div t-if="not no_breadcrumbs" class="row">
<div class="col-sm-12">
<ol class="breadcrumb mt8 o_portal_submenu">
<li><a href="/my/home"><i class="fa fa-home"/></a></li>
</ol>
</div>
</div>
</div>
</div>
<div id="wrap" class='portal-wrap'>
<div class="container mb64">
<div class="row">
<div class="col-md-9 col-lg-8">
<t t-raw="0"/>
</div>
<div id="o_my_sidebar" class="col-md-3 col-lg-offset-1 o_my_sidebar">
<div class="o_my_contact" t-if="sales_rep">
<t t-call="website_portal.contact"/>
<t t-if="my_details">
<div class="row">
<div t-attf-class="col-md-9 col-lg-8">
<t t-raw="0"/>
</div>
<div class="o_my_details">
<h3 class="page-header">Your Details <a href="/my/account" class="btn btn-default btn-xs">Change</a></h3>
<div t-field="user.partner_id" t-options='{"widget": "contact", "fields": ["email", "phone", "address", "name"]}'/>
</div>
<div class="o_my_archives" t-if="archive_groups">
<t t-call="website_portal.portal_archive_groups"/>
<div id="o_my_sidebar" class="col-md-3 col-lg-offset-1 o_my_sidebar">
<div class="o_my_contact" t-if="sales_rep">
<t t-call="website_portal.contact"/>
</div>
<div class="o_my_details">
<h3 class="page-header">Your Details <a href="/my/account" class="btn btn-default btn-xs">Change</a></h3>
<div t-field="user.partner_id" t-options='{"widget": "contact", "fields": ["email", "phone", "address", "name"]}'/>
</div>
<div class="o_my_archives" t-if="archive_groups">
<t t-call="website_portal.portal_archive_groups"/>
</div>
</div>
</div>
</div>
</t>
<t t-else="">
<t t-raw="0"/>
</t>
</div>
</div>
</t>
@@ -142,6 +140,7 @@
<template id="portal_my_home" name="My Portal">
<t t-call="website_portal.portal_layout">
<t t-set="my_details" t-value="True"/>
<div class="o_my_home_content">
<div class="oe_structure"></div>
</div>
@@ -2,22 +2,26 @@
<odoo>
<template id="portal_my_home_menu_purchase" name="Portal layout : purchase menu entries" inherit_id="website_portal.portal_layout" priority="25">
<xpath expr="//ul[contains(@class,'o_portal_submenu')]" position="inside">
<t t-if="purchase_count">
<li t-att-class="page_name == 'purchase' and 'active'">
<a href="/my/purchase">Purchase Orders</a>
</li>
</t>
<xpath expr="//ol[contains(@class,'o_portal_submenu')]" position="inside">
<li t-if="page_name == 'purchase' or purchase_order">
<a t-if="purchase_order" t-attf-href="/my/purchase?{{ keep_query() }}">
My Purchase Orders
</a>
<t t-else="">
My Purchase Orders
</t>
</li>
<li t-if="purchase_order">
<t t-esc="purchase_order.name"/>
</li>
</xpath>
</template>
<template id="portal_my_home_purchase" name="Portal My Home : purchase entry" inherit_id="website_portal.portal_my_home" priority="25">
<xpath expr="//div[contains(@class,'o_my_home_content')]" position="inside">
<h3 t-if="purchase_count" class="page-header">
<a href="/my/purchase">Your Purchase Orders
<small class="ml8">
<span class='badge'><t t-esc="purchase_count"/></span>
</small>
<a href="/my/purchase">My Purchase Orders
<span class="badge ml8" t-esc="purchase_count"/>
</a>
</h3>
</xpath>
@@ -26,7 +30,7 @@
<template id="portal_my_purchase_orders" name="Portal: My Purchase Orders">
<t t-call="website_portal.portal_layout">
<h3 class="page-header">
Your Purchase Orders
My Purchase Orders
<t t-call="website_portal.portal_searchbar"/>
</h3>
<table class="table table-hover o_my_status_table">
@@ -61,114 +65,94 @@
</template>
<template id="portal_my_purchase_order" name="Portal: My Purchase Order">
<t t-call="website.layout">
<div id="wrap">
<div class="container">
<div class="oe_structure">
<div class="row">
<div class="col-sm-4">
<ol class="breadcrumb mt8">
<li><a href="/my/home"><i class="fa fa-home"/></a></li>
<li>
<a t-attf-href="/my/purchase?{{ keep_query()}}">
My Purchase Orders
</a>
</li>
<li>
<t t-esc="order.name"/>
</li>
</ol>
<t t-call="website_portal.portal_layout">
<t t-set="purchase_order" t-value="order"/>
<div id="optional_placeholder"></div>
<div class="container">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-12">
<h4>
<t t-call='website_portal.record_pager'/>
Purchase Order
<span t-esc="order.name"/>
</h4>
</div>
</div>
</div>
</div>
<div id="optional_placeholder"></div>
<div class="container">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-12">
<h4>
<t t-call='website_portal.record_pager'/>
Purchase Order
<span t-esc="order.name"/>
</h4>
</div>
<div class="panel-body">
<div class="mb8">
<strong>Date:</strong> <span t-field="order.date_order" t-options='{"widget": "date"}'/>
</div>
<div class="row">
<div class="col-md-6">
<strong>Product</strong>
</div>
<div class="col-md-2 text-right">
<strong>Unit Price</strong>
</div>
<div class="col-md-2 text-right">
<strong>Quantity</strong>
</div>
<div class="col-md-2 text-right">
<strong>Subtotal</strong>
</div>
</div>
<t t-foreach="order.order_line" t-as="ol">
<div class="row purchases_vertical_align">
<div class="col-md-1 text-center">
<img t-att-src="'/web/image/purchase.order.line/%s/product_image/48x48' % ol.id"/>
</div>
<div id='product_name' class="col-md-5">
<span t-esc="ol.product_id.name"/>
</div>
<div class="col-md-2 text-right">
<span t-field="ol.price_unit" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</div>
<div class="col-md-2 text-right">
<span t-esc="ol.product_qty"/>
</div>
<div class="col-md-2 text-right">
<span t-field="ol.price_subtotal" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</div>
</div>
<div class="panel-body">
<div class="mb8">
<strong>Date:</strong> <span t-field="order.date_order" t-options='{"widget": "date"}'/>
</t>
<hr/>
<div class="row">
<div class="col-md-12 text-right">
<div class="row">
<div class="col-md-10 text-right">
Untaxed Amount:
</div>
<div class="col-md-2 text-right">
<span t-field="order.amount_untaxed" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<strong>Product</strong>
<div class="col-md-10 text-right">
Taxes:
</div>
<div class="col-md-2 text-right">
<strong>Unit Price</strong>
</div>
<div class="col-md-2 text-right">
<strong>Quantity</strong>
</div>
<div class="col-md-2 text-right">
<strong>Subtotal</strong>
<span t-field="order.amount_tax" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</div>
</div>
<t t-foreach="order.order_line" t-as="ol">
<div class="row purchases_vertical_align">
<div class="col-md-1 text-center">
<img t-att-src="'/web/image/purchase.order.line/%s/product_image/48x48' % ol.id"/>
</div>
<div id='product_name' class="col-md-5">
<span t-esc="ol.product_id.name"/>
</div>
<div class="col-md-2 text-right">
<span t-field="ol.price_unit" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</div>
<div class="col-md-2 text-right">
<span t-esc="ol.product_qty"/>
</div>
<div class="col-md-2 text-right">
<span t-field="ol.price_subtotal" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</div>
</div>
</t>
<hr/>
<div class="row">
<div class="col-md-12 text-right">
<div class="row">
<div class="col-md-10 text-right">
Untaxed Amount:
</div>
<div class="col-md-2 text-right">
<span t-field="order.amount_untaxed" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</div>
</div>
<div class="row">
<div class="col-md-10 text-right">
Taxes:
</div>
<div class="col-md-2 text-right">
<span t-field="order.amount_tax" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</div>
</div>
<div class="row">
<div class="col-md-10 text-right">
<strong>Total:</strong>
</div>
<div class="col-md-2 text-right">
<strong><span t-field="order.amount_total" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/></strong>
</div>
</div>
<div class="col-md-10 text-right">
<strong>Total:</strong>
</div>
<div class="col-md-2 text-right">
<strong><span t-field="order.amount_total" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/></strong>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="oe_structure mb32"/>
</div>
</div>
<div class="oe_structure mb32"/>
</t>
</template>
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mail
from . import main
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import werkzeug
from odoo.addons.mail.controllers.main import MailController
from odoo.exceptions import AccessError
from odoo.http import request
from odoo.tools.misc import consteq
class MailController(MailController):
def _redirect_to_record(cls, model, res_id, access_token=None):
# If the current user doesn't have access to the sales order, but provided
# a valid access token, redirect him to the front-end view.
if model == 'sale.order' and res_id and access_token:
uid = request.session.uid or request.env.ref('base.public_user').id
record_sudo = request.env[model].sudo().browse(res_id).exists()
try:
record_sudo.sudo(uid).check_access_rights('read')
record_sudo.sudo(uid).check_access_rule('read')
except AccessError:
if record_sudo.access_token and consteq(record_sudo.access_token, access_token):
record_action = record_sudo.with_context(
force_website=True).get_access_action(uid)
if record_action['type'] == 'ir.actions.act_url':
return werkzeug.utils.redirect(record_action['url'])
return super(MailController, cls)._redirect_to_record(model, res_id, access_token=None)
+12 -7
View File
@@ -4,6 +4,7 @@
from odoo import http, _
from odoo.exceptions import AccessError
from odoo.http import request
from odoo.tools import consteq
from odoo.addons.website_portal.controllers.main import website_account, get_records_pager
@@ -135,21 +136,25 @@ class website_account(website_account):
})
return request.render("website_portal_sale.portal_my_orders", values)
@http.route(['/my/orders/<int:order>'], type='http', auth="user", website=True)
def orders_followup(self, order=None, **kw):
@http.route(['/my/orders/<int:order>'], type='http', auth="public", website=True)
def orders_followup(self, order=None, access_token=None, **kw):
order = request.env['sale.order'].browse([order])
order_sudo = order.sudo()
values = {}
try:
order.check_access_rights('read')
order.check_access_rule('read')
except AccessError:
return request.render("website.403")
order_sudo = order.sudo()
if not access_token or not consteq(order_sudo.access_token, access_token):
return request.render("website.403")
values.update({'no_breadcrumbs': True})
order_invoice_lines = {il.product_id.id: il.invoice_id for il in order_sudo.invoice_ids.mapped('invoice_line_ids')}
history = request.session.get('my_orders_history', [])
values = {
values.update({
'order': order_sudo,
'order_invoice_lines': order_invoice_lines,
}
values.update(get_records_pager(history, order))
})
values.update(get_records_pager(history, order_sudo))
return request.render("website_portal_sale.orders_followup", values)
+27 -19
View File
@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, exceptions, fields, models, _
from odoo import api, fields, models
from odoo.exceptions import ValidationError
from werkzeug.urls import url_encode
import uuid
class SaleOrder(models.Model):
@@ -24,26 +25,33 @@ class SaleOrder(models.Model):
order.payment_transaction_count = mapped_data.get(order.id, 0)
@api.multi
def get_access_action(self):
""" Instead of the classic form view, redirect to the online quote for
portal users that have access to a confirmed order. """
def get_access_action(self, access_uid=None):
""" Instead of the classic form view, redirect to the online order for
portal users or if force_website=True in the context. """
# TDE note: read access on sales order to portal users granted to followed sales orders
self.ensure_one()
if self.state == 'cancel' or (self.state == 'draft' and not self.env.context.get('mark_so_as_sent')):
return super(SaleOrder, self).get_access_action()
if self.env.user.share or self.env.context.get('force_website'):
try:
self.check_access_rule('read')
except exceptions.AccessError:
pass
else:
return {
'type': 'ir.actions.act_url',
'url': '/my/orders/%s' % self.id,
'target': 'self',
'res_id': self.id,
}
return super(SaleOrder, self).get_access_action()
return super(SaleOrder, self).get_access_action(access_uid)
user = self.env['res.users'].sudo().browse(access_uid) if access_uid else self.env.user
if user.share or self.env.context.get('force_website'):
return {
'type': 'ir.actions.act_url',
'url': '/my/orders/%s?access_token=%s' % (self.id, self.access_token),
'target': 'self',
'res_id': self.id,
}
return super(SaleOrder, self).get_access_action(access_uid)
def get_mail_url(self):
self.ensure_one()
params = {
'model': self._name,
'res_id': self.id,
'access_token': self.access_token,
}
params.update(self.partner_id.signup_get_auth_param()[self.partner_id.id])
return '/mail/view?' + url_encode(params)
@api.multi
def _notification_recipients(self, message, groups):
@@ -7,12 +7,31 @@
</template>
<template id="portal_my_home_menu_sale" name="Portal layout : sales menu entries" inherit_id="website_portal.portal_layout" priority="20">
<xpath expr="//ul[contains(@class,'o_portal_submenu')]" position="inside">
<li t-if="quotation_count" t-att-class="page_name == 'quote' and 'active'">
<a href="/my/quotes">Quotations</a>
<xpath expr="//ol[contains(@class,'o_portal_submenu')]" position="inside">
<li t-if="page_name == 'quote' or sales_order and sales_order.state == 'sent'">
<a t-if="sales_order" t-attf-href="/my/quotes?{{ keep_query() }}">
My Quotations
</a>
<t t-else="">
My Quotations
</t>
</li>
<li t-att-class="page_name == 'order' and 'active'">
<a href="/my/orders">Sales Orders</a>
<li t-if="page_name == 'order' or sales_order and sales_order.state != 'sent'">
<a t-if="sales_order" t-attf-href="/my/orders?{{ keep_query() }}">
My Sales Orders
</a>
<t t-else="">
My Sales Orders
</t>
</li>
<li t-if="sales_order">
<t t-if="sales_order.state == 'sent'">
Quotation
</t>
<t t-if="sales_order.state != 'sent'">
Order
</t>
<t t-esc="sales_order.name"/>
</li>
</xpath>
</template>
@@ -20,21 +39,15 @@
<template id="portal_my_home_sale" name="Portal My Home : sales entries" inherit_id="website_portal.portal_my_home" priority="20">
<xpath expr="//div[contains(@class,'o_my_home_content')]" position="inside">
<h3 t-if="quotation_count" class="page-header">
<a href="/my/quotes">Your Quotations
<small class="ml8">
<span class='badge'><t t-esc="quotation_count"/></span>
</small>
<a href="/my/quotes">My Quotations
<span class="badge ml8" t-esc="quotation_count"/>
</a>
</h3>
<h3 class="page-header">
<a href="/my/orders">Your Sales Orders
<small class="ml8">
<t t-if="order_count">
<span class='badge'><t t-esc="order_count"/></span>
</t>
<t t-if="not order_count">
There are currently no orders.
</t>
<a href="/my/orders">My Sales Orders
<span t-if="order_count" class="badge ml8" t-esc="order_count"/>
<small t-if="not order_count" class="ml8">
There are currently no orders.
</small>
</a>
</h3>
@@ -44,7 +57,7 @@
<template id="portal_my_quotations" name="My Quotations">
<t t-call="website_portal.portal_layout">
<div class="page-header">
<span class="h3">Your Quotations</span>
<span class="h3">My Quotations</span>
<t t-call="website_portal.portal_searchbar"/>
</div>
<t t-if="not quotations">
@@ -91,7 +104,7 @@
<template id="portal_my_orders" name="My Sales Orders">
<t t-call="website_portal.portal_layout">
<div class="page-header">
<span class="h3">Your Sales Orders</span>
<span class="h3">My Sales Orders</span>
<t t-call="website_portal.portal_searchbar"/>
</div>
<t t-if="not orders">
@@ -142,188 +155,157 @@
</template>
<template id="orders_followup" name="Sales Order">
<t t-call="website.layout">
<div id="wrap">
<div class="container">
<div class="oe_structure">
<div class="row">
<div class="col-sm-4">
<ol class="breadcrumb mt8">
<li><a href="/my/home"><i class="fa fa-home"/></a></li>
<li>
<a t-attf-href="/my/#{'quotes' if order.state == 'sent' else 'orders'}?{{ keep_query()}}">
<t t-if="order.state == 'sent'">
My Quotations
</t>
<t t-if="order.state != 'sent'">
My Sales Orders
</t>
</a>
</li>
<li>
<t t-if="order.state == 'sent'">
Quotation
</t>
<t t-if="order.state != 'sent'">
Order
</t>
<t t-esc="order.name"/>
</li>
</ol>
</div>
<t t-call="website_portal.portal_layout">
<t t-set="sales_order" t-value="order"/> <!-- rename to ensure breadcrumb compatibility (differentiate with purchase orders) -->
<div id="optional_placeholder"></div>
<div class="container">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-12">
<h4>
<t t-call='website_portal.record_pager'/>
<t t-if="order.state == 'sent'">
Quotation
</t>
<t t-if="order.state != 'sent'">
Order
</t>
<span t-esc="order.name"/>
</h4>
</div>
</div>
<div id="optional_placeholder"></div>
<div class="container">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-12">
<h4>
<t t-call='website_portal.record_pager'/>
<t t-if="order.state == 'sent'">
Quotation
</t>
<t t-if="order.state != 'sent'">
Order
</t>
<span t-esc="order.name"/>
</h4>
</div>
</div>
</div>
<div class="panel-body">
<!-- <hr/> -->
<div class="mb8">
<strong>Date:</strong> <span t-field="order.create_date" t-options='{"widget": "date"}'/>
</div>
<div class='row'>
<div class="col-md-6">
<div>
<strong>Invoicing Address</strong>
</div>
<div>
<address t-field="order.partner_invoice_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/>
</div>
<t t-set="invoices" t-value="[i for i in order.invoice_ids if i.state not in ['draft', 'cancel']]"/>
<t t-if="invoices">
<div>
<strong>Invoices</strong>
</div>
<div class="panel-body">
<!-- <hr/> -->
<div class="mb8">
<strong>Date:</strong> <span t-field="order.create_date" t-options='{"widget": "date"}'/>
</div>
<div class='row'>
<div class="col-md-6">
<div>
<strong>Invoicing Address</strong>
</div>
<div>
<address t-field="order.partner_invoice_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/>
</div>
<t t-set="invoices" t-value="[i for i in order.invoice_ids if i.state not in ['draft', 'cancel']]"/>
<t t-if="invoices">
<div>
<strong>Invoices</strong>
</div>
<div>
<t t-foreach="invoices" t-as="i">
<t t-set="report_url" t-value="'/my/invoices/%s' % i.id"/>
<div>
<a t-att-href="report_url"><span class="fa fa-download"/></a>
<a t-att-href="report_url"><span t-esc="i.number"/></a>
<span class="text-muted" t-field="i.date_invoice"/>
<t t-if="i.state == 'paid'">
<span class="label label-success orders_label_text_align"><i class="fa fa-fw fa-check"/> Paid</span>
</t>
<t t-if="i.state != 'paid'">
<span class="label label-info orders_label_text_align"><i class="fa fa-fw fa-clock-o"/> Waiting</span>
</t>
</div>
</t>
</div>
<div>
<t t-foreach="invoices" t-as="i">
<t t-set="report_url" t-value="'/my/invoices/%s' % i.id"/>
<div>
<a t-att-href="report_url"><span class="fa fa-download"/></a>
<a t-att-href="report_url"><span t-esc="i.number"/></a>
<span class="text-muted" t-field="i.date_invoice"/>
<t t-if="i.state == 'paid'">
<span class="label label-success orders_label_text_align"><i class="fa fa-fw fa-check"/> Paid</span>
</t>
<t t-if="i.state != 'paid'">
<span class="label label-info orders_label_text_align"><i class="fa fa-fw fa-clock-o"/> Waiting</span>
</t>
</div>
<div id="shipping_address" class="col-md-6">
<div>
<strong>Shipping Address</strong>
</div>
<div>
<address t-field="order.partner_shipping_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/>
</div>
</div>
</div>
<hr/>
<div class="row">
<div class="col-md-6">
<strong>Product</strong>
</div>
<div class="hidden-sm hidden-xs col-md-2 text-right">
<strong>Unit Price</strong>
</div>
<div class="hidden-sm hidden-xs col-md-2 text-right">
<strong>Quantity</strong>
</div>
<div class="hidden-sm hidden-xs col-md-2 text-right">
<strong>Subtotal</strong>
</div>
</div>
<t t-foreach="order.order_line" t-as="ol">
<div class="row orders_vertical_align">
<div class="visible-lg col-lg-1 text-center">
<span t-field="ol.product_image" t-options="{'widget': 'image', 'class': 'img-rounded', 'style':'width: 48px;height:48px'}" />
</div>
<div id='product_name' class="col-lg-4 col-sm-6 col-xs-4">
<span t-esc="ol.product_id.name"/>
</div>
<div class="col-xs-3 text-right">
<span t-field="ol.price_unit" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/>
</div>
<div class="col-xs-2 text-right">
<t t-if="ol._name == 'sale.order.line'">
<span t-esc="ol.product_uom_qty"/>
</t>
<t t-if="ol._name == 'account.invoice.line'">
<span t-esc="ol.quantity"/>
</t>
</div>
<div class="col-sm-2 col-xs-3 text-right">
<span t-field="ol.price_subtotal" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/>
</div>
</div>
</t>
</div>
</t>
</div>
<div id="shipping_address" class="col-md-6">
<div>
<strong>Shipping Address</strong>
</div>
<div>
<address t-field="order.partner_shipping_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/>
</div>
</div>
</div>
<hr/>
<hr/>
<div class="row">
<div class="col-md-6">
<div>
<strong>Contact</strong>
</div>
<div t-field="order.user_id.partner_id" t-options='{"widget": "contact", "fields": ["email", "phone"]}'/>
<br/>
</div>
<div class="col-md-6">
<div class="row">
<div class="col-md-9 col-xs-8 text-right">
Untaxed Amount:
</div>
<div class="col-md-3 col-xs-4 text-right">
<span t-field="order.amount_untaxed" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/>
</div>
</div>
<div class="row">
<div class="col-md-9 col-xs-8 text-right">
Taxes:
</div>
<div class="col-md-3 col-xs-4 text-right">
<span t-field="order.amount_tax" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/>
</div>
</div>
<div class="row">
<div class="col-md-9 col-xs-8 text-right">
<strong>Total:</strong>
</div>
<div class="col-md-3 col-xs-4 text-right">
<strong><span t-field="order.amount_total" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/></strong>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<strong>Product</strong>
</div>
<div class="hidden-sm hidden-xs col-md-2 text-right">
<strong>Unit Price</strong>
</div>
<div class="hidden-sm hidden-xs col-md-2 text-right">
<strong>Quantity</strong>
</div>
<div class="hidden-sm hidden-xs col-md-2 text-right">
<strong>Subtotal</strong>
</div>
</div>
<t t-foreach="order.order_line" t-as="ol">
<div class="row orders_vertical_align">
<div class="visible-lg col-lg-1 text-center">
<span t-field="ol.product_image" t-options="{'widget': 'image', 'class': 'img-rounded', 'style':'width: 48px;height:48px'}" />
</div>
<div id='product_name' class="col-lg-4 col-sm-6 col-xs-4">
<span t-esc="ol.product_id.name"/>
</div>
<div class="col-xs-3 text-right">
<span t-field="ol.price_unit" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/>
</div>
<div class="col-xs-2 text-right">
<t t-if="ol._name == 'sale.order.line'">
<span t-esc="ol.product_uom_qty"/>
</t>
<t t-if="ol._name == 'account.invoice.line'">
<span t-esc="ol.quantity"/>
</t>
</div>
<div class="col-sm-2 col-xs-3 text-right">
<span t-field="ol.price_subtotal" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/>
</div>
</div>
</t>
<hr/>
<div class="row">
<div class="col-md-6">
<div>
<strong>Contact</strong>
</div>
<div t-field="order.user_id.partner_id" t-options='{"widget": "contact", "fields": ["email", "phone"]}'/>
<br/>
</div>
<div class="col-md-6">
<div class="row">
<div class="col-md-9 col-xs-8 text-right">
Untaxed Amount:
</div>
<div class="col-md-3 col-xs-4 text-right">
<span t-field="order.amount_untaxed" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/>
</div>
</div>
<div class="row">
<div class="col-md-9 col-xs-8 text-right">
Taxes:
</div>
<div class="col-md-3 col-xs-4 text-right">
<span t-field="order.amount_tax" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/>
</div>
</div>
<div class="row">
<div class="col-md-9 col-xs-8 text-right">
<strong>Total:</strong>
</div>
<div class="col-md-3 col-xs-4 text-right">
<strong><span t-field="order.amount_total" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/></strong>
</div>
</div>
</div>
</div>
<div class="oe_structure mb32"/>
</div>
</div>
</div>
<div class="oe_structure mb32"/>
</t>
</template>
+18 -8
View File
@@ -9,13 +9,18 @@ class Project(models.Model):
_inherit = ['project.project']
@api.multi
def get_access_action(self):
def get_access_action(self, access_uid=None):
""" Instead of the classic form view, redirect to website for portal users
that can read the project. """
self.ensure_one()
if self.env.user.share:
user, record = self.env.user, self
if access_uid:
user = self.env['res.users'].sudo().browse(access_uid)
record = self.sudo(user)
if user.share:
try:
self.check_access_rule('read')
record.check_access_rule('read')
except exceptions.AccessError:
pass
else:
@@ -25,7 +30,7 @@ class Project(models.Model):
'target': 'self',
'res_id': self.id,
}
return super(Project, self).get_access_action()
return super(Project, self).get_access_action(access_uid)
@api.multi
def _notification_recipients(self, message, groups):
@@ -47,13 +52,18 @@ class Task(models.Model):
task.website_url = '/my/task/%s' % task.id
@api.multi
def get_access_action(self):
def get_access_action(self, access_uid=None):
""" Instead of the classic form view, redirect to website for portal users
that can read the task. """
self.ensure_one()
if self.env.user.share:
user, record = self.env.user, self
if access_uid:
user = self.env['res.users'].sudo().browse(access_uid)
record = self.sudo(user)
if user.share:
try:
self.check_access_rule('read')
record.check_access_rule('read')
except exceptions.AccessError:
pass
else:
@@ -63,7 +73,7 @@ class Task(models.Model):
'target': 'self',
'res_id': self.id,
}
return super(Task, self).get_access_action()
return super(Task, self).get_access_action(access_uid)
@api.multi
def _notification_recipients(self, message, groups):
+125 -147
View File
@@ -1,12 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="portal_layout" name="Portal layout: project menu entry" inherit_id="website_portal.portal_layout" priority="40">
<xpath expr="//ul[contains(@class,'o_portal_submenu')]" position="inside">
<li t-if="project_count" t-att-class="page_name == 'project' and 'active'">
<a href="/my/projects">Projects</a>
<xpath expr="//ol[contains(@class,'o_portal_submenu')]" position="inside">
<li t-if="page_name == 'project' or project">
<a t-if="project" t-attf-href="/my/projects?{{ keep_query() }}">
Projects
</a>
<t t-else="">
Projects
</t>
</li>
<li t-if="task_count" t-att-class="page_name == 'task' and 'active'">
<a href="/my/tasks">Tasks</a>
<li t-if="project">
<t t-esc="project.name"/>
</li>
<li t-if="page_name == 'task' or task">
<a t-if="task" t-attf-href="/my/tasks?{{ keep_query() }}">
My Tasks
</a>
<t t-else="">
My Tasks
</t>
</li>
<li t-if="task">
<span t-field="task.name"/>
</li>
</xpath>
</template>
@@ -14,17 +30,13 @@
<template id="portal_my_home" name="Portal My Home: project entries" inherit_id="website_portal.portal_my_home" priority="40">
<xpath expr="//div[contains(@class,'o_my_home_content')]" position="inside">
<h3 t-if="project_count" class="page-header">
<a href="/my/projects">Your Projects
<small class="ml8">
<span class='badge'><t t-esc="project_count"/></span>
</small>
<a href="/my/projects">My Projects
<span class="badge ml8" t-esc="project_count"/>
</a>
</h3>
<h3 t-if="task_count" class="page-header">
<a href="/my/tasks">Your Tasks
<small class="ml8">
<span class='badge'><t t-esc="task_count"/></span>
</small>
<a href="/my/tasks">My Tasks
<span class="badge ml8" t-esc="task_count"/>
</a>
</h3>
</xpath>
@@ -34,7 +46,7 @@
<t t-call="website_portal.portal_layout">
<div class="row">
<h3 class="page-header">
Your Projects
My Projects
<t t-call="website_portal.portal_searchbar"/>
</h3>
</div>
@@ -73,80 +85,63 @@
</template>
<template id="my_project" name="My Project">
<t t-call="website.layout">
<div id="wrap">
<div class="container">
<div class="oe_structure">
<div class="row">
<div class="col-sm-4">
<ol class="breadcrumb mt8">
<li><a href="/my/home"><i class="fa fa-home"/></a></li>
<li><a t-attf-href="/my/projects?#{keep_query()}">My Projects</a></li>
<li><span t-field="project.name"/></li>
</ol>
</div>
</div>
</div>
<div class="container">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-12">
<h4>
<t t-call='website_portal.record_pager'/>
Project - <span t-field="project.name"/>
</h4>
</div>
</div>
</div>
<div class="panel-body">
<div class='row'>
<div class="col-md-6">
<t t-if="project.partner_id">
<strong>Customer</strong>
<div t-if="project.partner_id">
<address t-field="project.partner_id" t-field-options='{"widget": "contact", "fields": ["name", "email"]}'/>
</div>
</t>
<t t-if="project.user_id">
<strong>Project Manager</strong>
<div>
<address t-field="project.user_id" t-field-options='{"widget": "contact", "fields": ["name", "email", "phone"]}'/>
</div>
</t>
</div>
<div class="col-md-6">
<div class="pull-right">
<a t-attf-href="/my/tasks?filterby=#{project.id}" class="btn btn-default btn-lg">
<span class="fa fa-tasks" />
<span t-esc="project.task_count" />
<span t-field="project.label_tasks" />
</a>
</div>
</div>
</div>
</div>
</div>
<div class="row mt32">
<div class="col-md-12">
<h4><strong>Message and communication history</strong></h4>
</div>
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="project"/>
</t>
<t t-call="website_portal.portal_layout">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-12">
<h4>
<t t-call='website_portal.record_pager'/>
Project - <span t-field="project.name"/>
</h4>
</div>
</div>
</div>
<div class="panel-body">
<div class='row'>
<div class="col-md-6">
<t t-if="project.partner_id">
<strong>Customer</strong>
<div t-if="project.partner_id">
<address t-field="project.partner_id" t-field-options='{"widget": "contact", "fields": ["name", "email"]}'/>
</div>
</t>
<t t-if="project.user_id">
<strong>Project Manager</strong>
<div>
<address t-field="project.user_id" t-field-options='{"widget": "contact", "fields": ["name", "email", "phone"]}'/>
</div>
</t>
</div>
<div class="col-md-6">
<div class="pull-right">
<a t-attf-href="/my/tasks?filterby=#{project.id}" class="btn btn-default btn-lg">
<span class="fa fa-tasks" />
<span t-esc="project.task_count" />
<span t-field="project.label_tasks" />
</a>
</div>
</div>
</div>
</div>
</div>
<div class="row mt32">
<div class="col-md-12">
<h4><strong>Message and communication history</strong></h4>
</div>
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="project"/>
</t>
</div>
</div>
</t>
</template>
<template id="my_tasks" name="My Tasks">
<t t-call="website_portal.portal_layout">
<h3 class="page-header">
Your Tasks
My Tasks
<t t-call="website_portal.portal_searchbar"/>
</h3>
<t t-if="not tasks">
@@ -183,82 +178,65 @@
</template>
<template id="my_task" name="My Task">
<t t-call="website.layout">
<div id="wrap">
<div class="container">
<div class="oe_structure">
<div class="row">
<div class="col-sm-6">
<ol class="breadcrumb mt8">
<li><a href="/my/home"><i class="fa fa-home"/></a></li>
<li><a t-attf-href="/my/tasks?#{keep_query()}">My Tasks</a></li>
<li><span t-field="task.name"/></li>
</ol>
<t t-call="website_portal.portal_layout">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-12">
<h4>
<t t-call='website_portal.record_pager'/>
Task - <span t-field="task.name"/>
<span t-field="task.stage_id.name" class="pull-right label label-info" title="Current stage of this task"/>
<a class="btn btn-info" t-att-href="'/web#return_label=Website&amp;model=project.task&amp;id=%s&amp;view_type=form' % (task.id)" groups="project.group_project_user">Edit Task</a>
</h4>
</div>
</div>
</div>
<div class="panel-body">
<div class="mb8" t-if="user.partner_id.id in task.sudo().project_id.message_partner_ids.ids">
<strong>Project:</strong> <a t-attf-href="/my/project/#{task.project_id.id}" t-field="task.project_id.name"/>
</div>
<div class='row'>
<div class="col-md-6">
<div class="mb8">
<strong>Date:</strong> <span t-field="task.create_date" t-field-options='{"widget": "date"}'/>
</div>
<div t-if="task.user_id">
<strong>Assigned to</strong>
<div>
<address t-field="task.user_id" t-field-options='{"widget": "contact", "fields": ["name", "email", "phone"]}'/>
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb8" t-if="task.date_deadline">
<strong>Deadline:</strong> <span t-field="task.date_deadline" t-field-options='{"widget": "date"}'/>
</div>
<div t-if="task.partner_id">
<strong>Reported by</strong>
<div>
<address t-field="task.partner_id" t-field-options='{"widget": "contact", "fields": ["name", "email"]}'/>
</div>
</div>
</div>
</div>
<div class="container">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-12">
<h4>
<t t-call='website_portal.record_pager'/>
Task - <span t-field="task.name"/>
<span t-field="task.stage_id.name" class="pull-right label label-info" title="Current stage of this task"/>
<a class="btn btn-info" t-att-href="'/web#return_label=Website&amp;model=project.task&amp;id=%s&amp;view_type=form' % (task.id)" groups="project.group_project_user">Edit Task</a>
</h4>
</div>
</div>
</div>
<div class="panel-body">
<div class="mb8" t-if="user.partner_id.id in task.sudo().project_id.message_partner_ids.ids">
<strong>Project:</strong> <a t-attf-href="/my/project/#{task.project_id.id}" t-field="task.project_id.name"/>
</div>
<div class='row'>
<div class="col-md-6">
<div class="mb8">
<strong>Date:</strong> <span t-field="task.create_date" t-field-options='{"widget": "date"}'/>
</div>
<div t-if="task.user_id">
<strong>Assigned to</strong>
<div>
<address t-field="task.user_id" t-field-options='{"widget": "contact", "fields": ["name", "email", "phone"]}'/>
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb8" t-if="task.date_deadline">
<strong>Deadline:</strong> <span t-field="task.date_deadline" t-field-options='{"widget": "date"}'/>
</div>
<div t-if="task.partner_id">
<strong>Reported by</strong>
<div>
<address t-field="task.partner_id" t-field-options='{"widget": "contact", "fields": ["name", "email"]}'/>
</div>
</div>
</div>
</div>
<div class="container" t-if="task.description">
<p t-field="task.description" />
</div>
</div>
</div>
<div class="row mt32">
<div class="col-md-12">
<h4><strong>Message and communication history</strong></h4>
</div>
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="task"/>
</t>
</div>
</div>
<div class="container" t-if="task.description">
<p t-field="task.description" />
</div>
</div>
</div>
<div class="row mt32">
<div class="col-md-12">
<h4><strong>Message and communication history</strong></h4>
</div>
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="task"/>
</t>
</div>
</div>
</t>
</template>
</odoo>
@@ -22,13 +22,18 @@ class Issue(models.Model):
issue.attachment_ids = list(set(attachment_ids) - set(message_attachment_ids))
@api.multi
def get_access_action(self):
def get_access_action(self, access_uid=None):
""" Instead of the classic form view, redirect to website for portal users
that can read the issue. """
self.ensure_one()
if self.env.user.share:
user, record = self.env.user, self
if access_uid:
user = self.env['res.users'].sudo().browse(access_uid)
record = self.sudo(user)
if user.share:
try:
self.check_access_rule('read')
record.check_access_rule('read')
except exceptions.AccessError:
pass
else:
@@ -38,7 +43,7 @@ class Issue(models.Model):
'target': 'self',
'res_id': self.id,
}
return super(Issue, self).get_access_action()
return super(Issue, self).get_access_action(access_uid)
@api.multi
def _notification_recipients(self, message, groups):
@@ -2,9 +2,17 @@
<odoo>
<template id="portal_layout" name="Portal layout: Issues menu entry" inherit_id="website_portal.portal_layout" priority="45">
<xpath expr="//ul[contains(@class,'o_portal_submenu')]" position="inside">
<li t-if="issue_count" t-att-class="page_name == 'issue' and 'active'">
<a href="/my/issues">Issues</a>
<xpath expr="//ol[contains(@class,'o_portal_submenu')]" position="inside">
<li t-if="page_name == 'issue' or issue">
<a t-if="issue" t-attf-href="/my/issues?{{ keep_query() }}">
My Issues
</a>
<t t-else="">
My Issues
</t>
</li>
<li t-if="issue">
Issue <span t-esc="issue.id"/>
</li>
</xpath>
</template>
@@ -12,10 +20,8 @@
<template id="portal_my_home" name="Portal My Home: Issue entries" inherit_id="website_portal.portal_my_home" priority="45">
<xpath expr="//div[contains(@class,'o_my_home_content')]" position="inside">
<h3 t-if="issue_count" class="page-header">
<a href="/my/issues">Your Issues
<small class="ml8">
<span class='badge'><t t-esc="issue_count"/></span>
</small>
<a href="/my/issues">My Issues
<span class="badge ml8" t-esc="issue_count"/>
</a>
</h3>
</xpath>
@@ -24,7 +30,7 @@
<template id="my_issues" name="My Issues">
<t t-call="website_portal.portal_layout">
<h3 class="page-header">
Your Issues
My Issues
<t t-call="website_portal.portal_searchbar"/>
</h3>
<t t-if="not issues">
@@ -61,87 +67,70 @@
</template>
<template id="my_issues_issue" name="My Issue">
<t t-call="website.layout">
<div id="wrap">
<div class="container">
<div class="oe_structure">
<div class="row">
<div class="col-sm-4">
<ol class="breadcrumb mt8">
<li><a href="/my/home"><i class="fa fa-home"/></a></li>
<li><a t-attf-href="/my/issues?#{keep_query()}">My Issues</a></li>
<li>Issue <span t-esc="issue.id"/></li>
</ol>
</div>
<t t-call="website_portal.portal_layout">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-12">
<h4>
<t t-call='website_portal.record_pager'/>
Issue <span t-esc="issue.id"/> - <span t-field="issue.name"/>
<a class="btn btn-info" t-att-href="'/web#return_label=Website&amp;model=project.issue&amp;id=%s&amp;view_type=form' % (issue.id)" groups="project.group_project_user">Edit Issue</a>
<span t-field="issue.stage_id.name" class="pull-right label label-info" title="Current stage of this issue"/>
</h4>
</div>
</div>
<div class="container">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-12">
<h4>
<t t-call='website_portal.record_pager'/>
Issue <span t-esc="issue.id"/> - <span t-field="issue.name"/>
<a class="btn btn-info" t-att-href="'/web#return_label=Website&amp;model=project.issue&amp;id=%s&amp;view_type=form' % (issue.id)" groups="project.group_project_user">Edit Issue</a>
<span t-field="issue.stage_id.name" class="pull-right label label-info" title="Current stage of this issue"/>
</h4>
</div>
</div>
</div>
<div class="panel-body">
<div class="mb8">
<strong>Date:</strong> <span t-field="issue.create_date" t-options='{"widget": "date"}'/>
</div>
<div class='row'>
<div class="col-md-6" t-if="issue.user_id">
<strong>Assigned to</strong>
<div>
<address t-field="issue.user_id" t-options='{"widget": "contact", "fields": ["name", "email", "phone"], "no_marker": True}'/>
</div>
</div>
<div class="col-md-6">
<strong>Reported by</strong>
<div t-if="issue.partner_id">
<address t-field="issue.partner_id" t-options='{"widget": "contact", "fields": ["name", "email"], "no_marker": True}'/>
</div>
</div>
<t t-if="issue.description">
<div class="col-md-12">
<strong>Description</strong>
<div t-field="issue.description"/>
</div>
</t>
</div>
</div>
<div t-if="issue.attachment_ids" class='panel-footer'>
<strong>
<t t-if='len(issue.attachment_ids)&gt;1'>Attachments</t>
<t t-else="">Attachment</t>
</strong>
<div class='row'>
<div class="col-md-2 col-sm-3 text-center oe_attachments" t-foreach='issue.attachment_ids' t-as='attachment'>
<a t-attf-href="/web/content/#{attachment.id}?download=true" target="_blank">
<div class='oe_attachment_embedded o_image o_image_small' t-att-title="attachment.name" t-att-data-mimetype="attachment.mimetype" t-attf-data-src="/web/image/#{attachment.id}/50x40"/>
<div class='oe_attachment_name'><t t-esc='attachment.name' /></div>
</a>
</div>
</div>
</div>
<div class="panel-body">
<div class="mb8">
<strong>Date:</strong> <span t-field="issue.create_date" t-options='{"widget": "date"}'/>
</div>
<div class='row'>
<div class="col-md-6" t-if="issue.user_id">
<strong>Assigned to</strong>
<div>
<address t-field="issue.user_id" t-options='{"widget": "contact", "fields": ["name", "email", "phone"], "no_marker": True}'/>
</div>
</div>
<div class="row mt32">
<div class="col-md-6">
<strong>Reported by</strong>
<div t-if="issue.partner_id">
<address t-field="issue.partner_id" t-options='{"widget": "contact", "fields": ["name", "email"], "no_marker": True}'/>
</div>
</div>
<t t-if="issue.description">
<div class="col-md-12">
<h4><strong>Message and communication history</strong></h4>
</div>
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="issue"/>
</t>
<strong>Description</strong>
<div t-field="issue.description"/>
</div>
</t>
</div>
</div>
<div t-if="issue.attachment_ids" class='panel-footer'>
<strong>
<t t-if='len(issue.attachment_ids)&gt;1'>Attachments</t>
<t t-else="">Attachment</t>
</strong>
<div class='row'>
<div class="col-md-2 col-sm-3 text-center oe_attachments" t-foreach='issue.attachment_ids' t-as='attachment'>
<a t-attf-href="/web/content/#{attachment.id}?download=true" target="_blank">
<div class='oe_attachment_embedded o_image o_image_small' t-att-title="attachment.name" t-att-data-mimetype="attachment.mimetype" t-attf-data-src="/web/image/#{attachment.id}/50x40"/>
<div class='oe_attachment_name'><t t-esc='attachment.name' /></div>
</a>
</div>
</div>
</div>
</div>
<div class="row mt32">
<div class="col-md-12">
<h4><strong>Message and communication history</strong></h4>
</div>
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="issue"/>
</t>
</div>
</div>
</t>
</template>
+1 -1
View File
@@ -54,7 +54,7 @@ class sale_quote(http.Controller):
'order_valid': (not order_sudo.validity_date) or (now <= order_sudo.validity_date),
'days_valid': days,
'action': request.env.ref('sale.action_quotations').id,
'breadcrumb': request.env.user.partner_id == order_sudo.partner_id,
'no_breadcrumbs': request.env.user.partner_id.commercial_partner_id not in order_sudo.message_partner_ids,
'tx_id': Transaction.id if Transaction else False,
'tx_state': Transaction.state if Transaction else False,
'tx_post_msg': Transaction.acquirer_id.post_msg if Transaction else False,
+14 -9
View File
@@ -7,6 +7,8 @@ from odoo import api, fields, models, _
from odoo.tools.translate import html_translate
from odoo.addons import decimal_precision as dp
from werkzeug.urls import url_encode
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
@@ -45,12 +47,6 @@ class SaleOrderLine(models.Model):
class SaleOrder(models.Model):
_inherit = 'sale.order'
def _website_url(self):
super(SaleOrder, self)._website_url()
for so in self:
if so.state not in ['sale', 'done']:
so.website_url = '/quote/%s' % (so.id)
template_id = fields.Many2one(
'sale.quote.template', 'Quotation Template',
readonly=True,
@@ -167,11 +163,13 @@ class SaleOrder(models.Model):
}
@api.multi
def get_access_action(self):
def get_access_action(self, access_uid=None):
""" Instead of the classic form view, redirect to the online quote if it exists. """
self.ensure_one()
if not self.template_id or (not self.env.user.share and not self.env.context.get('force_website')):
return super(SaleOrder, self).get_access_action()
user = access_uid and self.env['res.users'].sudo().browse(access_uid) or self.env.user
if not self.template_id or (not user.share and not self.env.context.get('force_website')):
return super(SaleOrder, self).get_access_action(access_uid)
return {
'type': 'ir.actions.act_url',
'url': '/quote/%s/%s' % (self.id, self.access_token),
@@ -179,6 +177,13 @@ class SaleOrder(models.Model):
'res_id': self.id,
}
def get_mail_url(self):
self.ensure_one()
if self.state not in ['sale', 'done']:
auth_param = url_encode(self.partner_id.signup_get_auth_param()[self.partner_id.id])
return '/quote/%s/%s?' % (self.id, self.access_token) + auth_param
return super(SaleOrder, self).get_mail_url()
@api.multi
def _confirm_online_quote(self, transaction):
""" Payment callback: validate the order and write transaction details in chatter """
@@ -166,20 +166,10 @@
<!-- Complete page of the quotation -->
<template id="so_quotation" name="Product Quotation">
<t t-call="website.layout">
<t t-call="website_portal.portal_layout">
<t t-set="sales_order" t-value="quotation"/> <!-- rename to ensure breadcrumb compatibility with website_portal_sale's portal_layout inheritance -->
<body data-spy="scroll" data-target=".navspy" data-offset="50">
<div class="container o_website_quote">
<div t-if="breadcrumb" class="row">
<div class="col-sm-4">
<ol class="breadcrumb mt8">
<li><a href="/my/home"><i class="fa fa-home"/></a></li>
<li><a t-attf-href="/my/quotes?#{keep_query()}">My Quotations</a></li>
<li>
Quotation <t t-esc="quotation.name"/>
</li>
</ol>
</div>
</div>
<div class="row mt16">
<!-- Sidebar -->
<div class="col-md-3 hidden-print">
+2 -2
View File
@@ -434,7 +434,7 @@ class Slide(models.Model):
return fields
@api.multi
def get_access_action(self):
def get_access_action(self, access_uid=None):
""" Instead of the classic form view, redirect to website if it is published. """
self.ensure_one()
if self.website_published:
@@ -444,7 +444,7 @@ class Slide(models.Model):
'target': 'self',
'res_id': self.id,
}
return super(Slide, self).get_access_action()
return super(Slide, self).get_access_action(access_uid)
@api.multi
def _notification_recipients(self, message, groups):
+10 -5
View File
@@ -1339,12 +1339,14 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
return False
@api.multi
def get_formview_action(self):
def get_formview_action(self, access_uid=None):
""" Return an action to open the document ``self``. This method is meant
to be overridden in addons that want to give specific view ids for
example.
"""
view_id = self.sudo().get_formview_id(access_uid=self.env.uid)
An optional access_uid holds the user that will access the document
that could be different from the current user. """
view_id = self.sudo().get_formview_id(access_uid=access_uid)
return {
'type': 'ir.actions.act_window',
'res_model': self._name,
@@ -1357,12 +1359,15 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
}
@api.multi
def get_access_action(self):
def get_access_action(self, access_uid=None):
""" Return an action to open the document. This method is meant to be
overridden in addons that want to give specific access to the document.
By default it opens the formview of the document.
An optional access_uid holds the user that will access the document
that could be different from the current user.
"""
return self[0].get_formview_action()
return self[0].get_formview_action(access_uid=access_uid)
@api.model
def search_count(self, args):