[MERGE] forward port of branch 8.0 up to 5e6ec89
This commit is contained in:
@@ -305,20 +305,22 @@ class account_analytic_account(osv.osv):
|
||||
args=[]
|
||||
if context is None:
|
||||
context={}
|
||||
account_ids = []
|
||||
if name:
|
||||
account_ids = self.search(cr, uid, [('code', '=', name)] + args, limit=limit, context=context)
|
||||
if not account_ids:
|
||||
dom = []
|
||||
for name2 in name.split('/'):
|
||||
name = name2.strip()
|
||||
account_ids = self.search(cr, uid, dom + [('name', operator, name)], limit=limit, context=context)
|
||||
if not account_ids: break
|
||||
dom = [('parent_id','in',account_ids)]
|
||||
if account_ids and args:
|
||||
# final filtering according to domain (args)
|
||||
account_ids = self.search(cr, uid, [('id', 'in', account_ids)] + args, limit=limit, context=context)
|
||||
else:
|
||||
account_ids = self.search(cr, uid, args, limit=limit, context=context)
|
||||
if '/' in name:
|
||||
for name2 in name.split('/'):
|
||||
# intermediate search without limit and args - could be expensive for large tables if `name` is not selective
|
||||
account_ids = self.search(cr, uid, dom + [('name', operator, name2.strip())], limit=None, context=context)
|
||||
if not account_ids: break
|
||||
dom = [('parent_id','in',account_ids)]
|
||||
if account_ids and args:
|
||||
# final filtering according to domain (args)4
|
||||
account_ids = self.search(cr, uid, [('id', 'in', account_ids)] + args, limit=limit, context=context)
|
||||
if not account_ids:
|
||||
return super(account_analytic_account, self).name_search(cr, uid, name, args, operator=operator, context=context, limit=limit)
|
||||
return self.name_get(cr, uid, account_ids, context=context)
|
||||
|
||||
class account_analytic_line(osv.osv):
|
||||
|
||||
@@ -126,15 +126,24 @@ class base_action_rule(osv.osv):
|
||||
ir_filter = self.pool['ir.filters'].browse(cr, uid, filter_id, context=context)
|
||||
return {'value': {'filter_domain': ir_filter.domain}}
|
||||
|
||||
def _get_eval_context(self, cr, uid, context=None):
|
||||
""" Prepare the context used when evaluating python code
|
||||
:returns: dict -- evaluation context given to (safe_)eval """
|
||||
return {
|
||||
'time': time,
|
||||
'user': self.pool['res.users'].browse(cr, uid, uid, context=context),
|
||||
}
|
||||
|
||||
def _filter(self, cr, uid, action, action_filter, record_ids, domain=False, context=None):
|
||||
""" Filter the list record_ids that satisfy the domain or the action filter. """
|
||||
if record_ids and (domain is not False or action_filter):
|
||||
eval_context = self._get_eval_context(cr, uid, context=context)
|
||||
if domain is not False:
|
||||
new_domain = [('id', 'in', record_ids)] + eval(domain)
|
||||
new_domain = [('id', 'in', record_ids)] + eval(domain, eval_context)
|
||||
ctx = context
|
||||
elif action_filter:
|
||||
assert action.model == action_filter.model_id, "Filter model different from action rule model"
|
||||
new_domain = [('id', 'in', record_ids)] + eval(action_filter.domain)
|
||||
new_domain = [('id', 'in', record_ids)] + eval(action_filter.domain, eval_context)
|
||||
ctx = dict(context or {})
|
||||
ctx.update(eval(action_filter.context))
|
||||
record_ids = self.pool[action.model].search(cr, uid, new_domain, context=ctx)
|
||||
@@ -309,6 +318,7 @@ class base_action_rule(osv.osv):
|
||||
# retrieve all the action rules to run based on a timed condition
|
||||
action_dom = [('kind', '=', 'on_time')]
|
||||
action_ids = self.search(cr, uid, action_dom, context=dict(context, active_test=True))
|
||||
eval_context = self._get_eval_context(cr, uid, context=context)
|
||||
for action in self.browse(cr, uid, action_ids, context=context):
|
||||
now = datetime.now()
|
||||
if action.last_run:
|
||||
@@ -320,9 +330,9 @@ class base_action_rule(osv.osv):
|
||||
domain = []
|
||||
ctx = dict(context)
|
||||
if action.filter_domain is not False:
|
||||
domain = eval(action.filter_domain)
|
||||
domain = eval(action.filter_domain, eval_context)
|
||||
elif action.filter_id:
|
||||
domain = eval(action.filter_id.domain)
|
||||
domain = eval(action.filter_id.domain, eval_context)
|
||||
ctx.update(eval(action.filter_id.context))
|
||||
if 'lang' not in ctx:
|
||||
# Filters might be language-sensitive, attempt to reuse creator lang
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<field name="name">crm.claim.from_delivery.form</field>
|
||||
<field name="model">stock.picking</field>
|
||||
<field name="inherit_id" ref="stock.view_picking_form"/>
|
||||
<field name="groups_id" eval="[(4, ref('base.group_sale_salesman')), (4, ref('base.group_partner_manager'))]"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="/form/sheet/h1" position="before">
|
||||
<div class="oe_right oe_button_box">
|
||||
|
||||
@@ -327,7 +327,7 @@
|
||||
<field name="partner_id"/>
|
||||
</div>
|
||||
<div class="oe_deadline">
|
||||
<t t-if="record.date_deadline.raw_value and record.date_deadline.raw_value lt (new Date())" t-set="red">oe_kanban_text_red</t>
|
||||
<t t-if="record.date_action.raw_value and record.date_action.raw_value lt (new Date())" t-set="red">oe_kanban_text_red</t>
|
||||
<span t-attf-class="#{red || ''}"><field name="date_action"/></span>
|
||||
<t t-if="record.date_action.raw_value"> : </t>
|
||||
<field name="title_action"/>
|
||||
|
||||
@@ -41,8 +41,12 @@ class res_partner(osv.osv):
|
||||
}
|
||||
except:
|
||||
pass
|
||||
return res
|
||||
|
||||
def _phonecall_count(self, cr, uid, ids, field_name, arg, context=None):
|
||||
res = {}
|
||||
for partner in self.browse(cr, uid, ids, context):
|
||||
res[partner.id]['phonecall_count'] = len(partner.phonecall_ids)
|
||||
res[partner.id] = len(partner.phonecall_ids)
|
||||
return res
|
||||
|
||||
_columns = {
|
||||
@@ -55,7 +59,7 @@ class res_partner(osv.osv):
|
||||
'Phonecalls'),
|
||||
'opportunity_count': fields.function(_opportunity_meeting_phonecall_count, string="Opportunity", type='integer', multi='opp_meet'),
|
||||
'meeting_count': fields.function(_opportunity_meeting_phonecall_count, string="# Meetings", type='integer', multi='opp_meet'),
|
||||
'phonecall_count': fields.function(_opportunity_meeting_phonecall_count, string="Phonecalls", type="integer", multi='opp_meet'),
|
||||
'phonecall_count': fields.function(_phonecall_count, string="Phonecalls", type="integer"),
|
||||
}
|
||||
|
||||
def redirect_partner_form(self, cr, uid, partner_id, context=None):
|
||||
|
||||
@@ -103,6 +103,21 @@
|
||||
context="{'partner_id': active_id}">
|
||||
<field string="Meetings" name="meeting_count" widget="statinfo"/>
|
||||
</button>
|
||||
</xpath>
|
||||
</data>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- second view due to access rights required to compute phonecall_count -->
|
||||
<record id="view_partners_form_crm_calls" model="ir.ui.view">
|
||||
<field name="name">view.res.partner.form.crm.inherited.calls</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="groups_id" eval="[(4, ref('base.group_sale_manager')), (4, ref('base.group_partner_manager'))]"/>
|
||||
<field eval="18" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<data>
|
||||
<xpath expr="//div[@name='buttons']" position="inside">
|
||||
<button class="oe_inline oe_stat_button" type="action"
|
||||
name="%(crm.crm_phonecall_category_phone_incoming0)d"
|
||||
icon="fa-phone"
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
<group colspan="2" col="2" groups="base.group_no_one">
|
||||
<separator colspan="2" string="Dates"/>
|
||||
<field name="create_date"/>
|
||||
<field name="date_closed"/>
|
||||
<field name="date_closed" invisible="1"/>
|
||||
<field name="write_date"/>
|
||||
</group>
|
||||
|
||||
@@ -220,7 +220,7 @@
|
||||
<filter string="Type" icon="terp-stock_symbol-selection" domain="[]" context="{'group_by':'categ_id'}"/>
|
||||
<filter string="Claim Month" icon="terp-go-month" domain="[]" help="Claim Date by Month" context="{'group_by':'date'}"/>
|
||||
<filter string="Deadline" icon="terp-go-month" domain="[]" context="{'group_by':'date_deadline'}"/>
|
||||
<filter string="Closure" icon="terp-go-month" domain="[]" help="Date Closed" context="{'group_by':'date_closed'}" groups="base.group_no_one"/>
|
||||
<filter string="Closure" icon="terp-go-month" domain="[]" help="Date Closed" context="{'group_by':'date_closed'}" groups="base.group_no_one" invisible="1"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
|
||||
@@ -44,7 +44,7 @@ class crm_claim_report(osv.osv):
|
||||
'create_date': fields.datetime('Create Date', readonly=True, select=True),
|
||||
'claim_date': fields.datetime('Claim Date', readonly=True),
|
||||
'delay_close': fields.float('Delay to close', digits=(16,2),readonly=True, group_operator="avg",help="Number of Days to close the case"),
|
||||
'stage_id': fields.many2one ('crm.stage', 'Stage', readonly=True,domain="[('team_ids','=',team_id)]"),
|
||||
'stage_id': fields.many2one ('crm.claim.stage', 'Stage', readonly=True,domain="[('team_ids','=',team_id)]"),
|
||||
'categ_id': fields.many2one('crm.claim.category', 'Category',readonly=True),
|
||||
'partner_id': fields.many2one('res.partner', 'Partner', readonly=True),
|
||||
'company_id': fields.many2one('res.company', 'Company', readonly=True),
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<field name="name">res.partner.claim.info.form</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="groups_id" eval="[(4, ref('base.group_sale_salesman')), (4, ref('base.group_partner_manager'))]"/>
|
||||
<field name="priority" eval="50"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='buttons']" position="inside">
|
||||
|
||||
@@ -100,7 +100,7 @@ class crm_helpdesk(osv.osv):
|
||||
if values.get('state'):
|
||||
if values.get('state') in ['draft', 'open'] and not values.get('date_open'):
|
||||
values['date_open'] = fields.datetime.now()
|
||||
elif values.get('state') == 'close' and not values.get('date_closed'):
|
||||
elif values.get('state') == 'done' and not values.get('date_closed'):
|
||||
values['date_closed'] = fields.datetime.now()
|
||||
return super(crm_helpdesk, self).write(cr, uid, ids, values, context=context)
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
});
|
||||
|
||||
im_livechat.ChatButton = openerp.Widget.extend({
|
||||
className: "openerp_style oe_chat_button",
|
||||
className: "openerp_style oe_chat_button hidden-print",
|
||||
events: {
|
||||
"click": "click"
|
||||
},
|
||||
|
||||
@@ -1007,6 +1007,8 @@ class mail_thread(osv.AbstractModel):
|
||||
(model, thread_id, custom_values, uid, None),
|
||||
update_author=True, assert_model=True, create_fallback=True, context=context)
|
||||
if route:
|
||||
# parent is invalid for a compat-reply
|
||||
message_dict.pop('parent_id', None)
|
||||
_logger.info(
|
||||
'Routing mail from %s to %s with Message-Id %s: direct thread reply (compat-mode) to model: %s, thread_id: %s, custom_values: %s, uid: %s',
|
||||
email_from, email_to, message_id, model, thread_id, custom_values, uid)
|
||||
@@ -1033,6 +1035,9 @@ class mail_thread(osv.AbstractModel):
|
||||
elif route is False:
|
||||
return []
|
||||
|
||||
# no route found for a matching reference (or reply), so parent is invalid
|
||||
message_dict.pop('parent_id', None)
|
||||
|
||||
# 4. Look for a matching mail.alias entry
|
||||
# Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
|
||||
# for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
|
||||
@@ -1115,6 +1120,8 @@ class mail_thread(osv.AbstractModel):
|
||||
if thread_id and hasattr(model_pool, 'message_update'):
|
||||
model_pool.message_update(cr, user_id, [thread_id], message_dict, context=nosub_ctx)
|
||||
else:
|
||||
# if a new thread is created, parent is irrelevant
|
||||
message_dict.pop('parent_id', None)
|
||||
thread_id = model_pool.message_new(cr, user_id, message_dict, custom_values, context=nosub_ctx)
|
||||
else:
|
||||
if thread_id:
|
||||
|
||||
@@ -736,3 +736,31 @@ class TestMailgateway(TestMail):
|
||||
'message_post: private discussion: incorrect recipients when replying')
|
||||
self.assertEqual(set(msg_nids), set(test_nids),
|
||||
'message_post: private discussion: incorrect notified recipients when replying')
|
||||
|
||||
# Do bert forward it to an alias
|
||||
mail_group_model_id = self.ir_model.search(cr, uid, [('model', '=', 'mail.group')])[0]
|
||||
self.mail_alias.create(cr, uid, {
|
||||
'alias_name': 'groups',
|
||||
'alias_user_id': False,
|
||||
'alias_model_id': mail_group_model_id,
|
||||
'alias_parent_model_id': mail_group_model_id,
|
||||
'alias_parent_thread_id': self.group_pigs_id,
|
||||
'alias_contact': 'everyone'})
|
||||
|
||||
msg = self.mail_message.browse(cr, uid, msg1_id)
|
||||
# forward it to a new thread AND an existing thread
|
||||
for i, to in enumerate(['groups', 'group+pigs']):
|
||||
fw_msg_id = '<THIS.IS.A.FW.MESSAGE.%d@bert.fr>' % (i,)
|
||||
fw_message = format(MAIL_TEMPLATE, to='%s@whatever.tld' % (to,),
|
||||
subject='FW: Re: 1',
|
||||
email_from='bert@bert.fr',
|
||||
extra='References: %s' % msg.message_id,
|
||||
msg_id=fw_msg_id)
|
||||
self.mail_thread.message_process(cr, uid, None, fw_message)
|
||||
|
||||
msg_ids = self.mail_message.search(cr, uid, [('message_id', '=', fw_msg_id)])
|
||||
self.assertEqual(len(msg_ids), 1)
|
||||
msg_fw = self.mail_message.browse(cr, uid, msg_ids[0])
|
||||
|
||||
self.assertEqual(msg_fw.model, 'mail.group')
|
||||
self.assertFalse(msg_fw.parent_id)
|
||||
|
||||
@@ -92,6 +92,6 @@ class product_product(osv.osv):
|
||||
('product_id', '=', False),
|
||||
('product_tmpl_id', 'in', list(products)),
|
||||
]
|
||||
result['context'] = "{}"
|
||||
result['context'] = "{'default_product_id': active_id, 'search_default_product_id': active_id, 'default_product_tmpl_id': %s}" % (len(products) and products.pop() or 'False')
|
||||
result['domain'] = str(domain)
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
import models
|
||||
import controllers
|
||||
@@ -0,0 +1,37 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright Eezee-It
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
{
|
||||
'name': 'Worldline SIPS Payment Acquiring for online payments',
|
||||
'version': '1.0',
|
||||
'author': 'Eezee-It',
|
||||
'category': 'Hidden',
|
||||
'description': """
|
||||
Worldline SIPS Payment Acquirer for online payments
|
||||
|
||||
Works with Worldline keys version 2.0, contains implementation of
|
||||
payments acquirer using Worldline SIPS.""",
|
||||
'depends': ['payment'],
|
||||
'data': [
|
||||
'views/sips.xml',
|
||||
'views/payment_acquirer.xml',
|
||||
'data/sips.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import main
|
||||
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
import json
|
||||
import logging
|
||||
import werkzeug
|
||||
|
||||
from openerp import http
|
||||
from openerp.http import request
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SipsController(http.Controller):
|
||||
_notify_url = '/payment/sips/ipn/'
|
||||
_return_url = '/payment/sips/dpn/'
|
||||
|
||||
def _get_return_url(self, **post):
|
||||
""" Extract the return URL from the data coming from sips. """
|
||||
return_url = post.pop('return_url', '')
|
||||
if not return_url:
|
||||
tx_obj = request.registry['payment.transaction']
|
||||
data = tx_obj._sips_data_to_object(post.get('Data'))
|
||||
custom = json.loads(data.pop('returnContext', False) or '{}')
|
||||
return_url = custom.get('return_url', '/')
|
||||
return return_url
|
||||
|
||||
def sips_validate_data(self, **post):
|
||||
res = False
|
||||
env = request.env
|
||||
tx_obj = env['payment.transaction']
|
||||
acquirer_obj = env['payment.acquirer']
|
||||
|
||||
sips = acquirer_obj.search([('provider', '=', 'sips')], limit=1)
|
||||
|
||||
security = sips._sips_generate_shasign(post)
|
||||
if security == post['Seal']:
|
||||
_logger.debug('Sips: validated data')
|
||||
res = tx_obj.sudo().form_feedback(post, 'sips')
|
||||
else:
|
||||
_logger.warning('Sips: data are corrupted')
|
||||
return res
|
||||
|
||||
@http.route([
|
||||
'/payment/sips/ipn/'],
|
||||
type='http', auth='none', methods=['POST'])
|
||||
def sips_ipn(self, **post):
|
||||
""" Sips IPN. """
|
||||
self.sips_validate_data(**post)
|
||||
return ''
|
||||
|
||||
@http.route([
|
||||
'/payment/sips/dpn'], type='http', auth="none", methods=['POST'])
|
||||
def sips_dpn(self, **post):
|
||||
""" Sips DPN """
|
||||
return_url = self._get_return_url(**post)
|
||||
self.sips_validate_data(**post)
|
||||
return werkzeug.utils.redirect(return_url)
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data noupdate="0">
|
||||
|
||||
<record id="payment_acquirer_sips" model="payment.acquirer">
|
||||
<field name="name">Sips</field>
|
||||
<field name="provider">sips</field>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
<field name="view_template_id" ref="payment_sips.sips_acquirer_button"/>
|
||||
<field name="environment">test</field>
|
||||
<field name="pre_msg"><![CDATA[
|
||||
<p>You will be redirected to the Sips website after clicking on payment button.</p>]]></field>
|
||||
</record>
|
||||
|
||||
<record id="sequence_payment_transaction_type_sips" model="ir.sequence.type">
|
||||
<field name="name">Sips Payment Transaction</field>
|
||||
<field name="code">sips.payment.transaction</field>
|
||||
</record>
|
||||
|
||||
<record id="sequence_payment_transaction_sips" model="ir.sequence">
|
||||
<field name="name">Sips Payment Transaction</field>
|
||||
<field name="code">sips.payment.transaction</field>
|
||||
<field name="prefix"></field>
|
||||
<field eval="1" name="number_next"/>
|
||||
<field eval="1" name="number_increment"/>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sips
|
||||
@@ -0,0 +1,232 @@
|
||||
# -*- coding: utf-'8' "-*-"
|
||||
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
import json
|
||||
import logging
|
||||
from hashlib import sha256
|
||||
import urlparse
|
||||
import unicodedata
|
||||
|
||||
from openerp import models, fields, api
|
||||
from openerp.tools.float_utils import float_compare
|
||||
from openerp.tools.translate import _
|
||||
from openerp.addons.payment.models.payment_acquirer import ValidationError
|
||||
from openerp.addons.payment_sips.controllers.main import SipsController
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
CURRENCY_CODES = {
|
||||
'EUR': '978',
|
||||
'USD': '840',
|
||||
'CHF': '756',
|
||||
'GBP': '826',
|
||||
'CAD': '124',
|
||||
'JPY': '392',
|
||||
'MXN': '484',
|
||||
'TRY': '949',
|
||||
'AUD': '036',
|
||||
'NZD': '554',
|
||||
'NOK': '578',
|
||||
'BRL': '986',
|
||||
'ARS': '032',
|
||||
'KHR': '116',
|
||||
'TWD': '901',
|
||||
}
|
||||
|
||||
|
||||
class AcquirerSips(models.Model):
|
||||
_inherit = 'payment.acquirer'
|
||||
# Fields
|
||||
sips_merchant_id = fields.Char('SIPS API User Password',
|
||||
required_if_provider='sips')
|
||||
sips_secret = fields.Char('SIPS Secret', size=64, required_if_provider='sips')
|
||||
|
||||
# Methods
|
||||
def _get_sips_urls(self, environment):
|
||||
""" Worldline SIPS URLS """
|
||||
url = {
|
||||
'prod': 'https://payment-webinit.sips-atos.com/paymentInit',
|
||||
'test': 'https://payment-webinit.simu.sips-atos.com/paymentInit', }
|
||||
|
||||
return {'sips_form_url': url.get(environment, url['test']), }
|
||||
|
||||
@api.model
|
||||
def _get_providers(self):
|
||||
providers = super(AcquirerSips, self)._get_providers()
|
||||
providers.append(['sips', 'Sips'])
|
||||
return providers
|
||||
|
||||
def _sips_generate_shasign(self, values):
|
||||
""" Generate the shasign for incoming or outgoing communications.
|
||||
:param dict values: transaction values
|
||||
:return string: shasign
|
||||
"""
|
||||
if self.provider != 'sips':
|
||||
raise ValidationError(_('Incorrect payment acquirer provider'))
|
||||
data = values['Data']
|
||||
|
||||
# Test key provided by Worldine
|
||||
key = u'002001000000001_KEY1'
|
||||
|
||||
if self.environment == 'prod':
|
||||
key = getattr(self, 'sips_secret')
|
||||
|
||||
shasign = sha256(data + key)
|
||||
return shasign.hexdigest()
|
||||
|
||||
@api.multi
|
||||
def sips_form_generate_values(self, partner_values, tx_values):
|
||||
self.ensure_one()
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
currency = self.env['res.currency'].sudo().browse(tx_values['currency_id'])
|
||||
currency_code = CURRENCY_CODES.get(currency.name, False)
|
||||
if not currency_code:
|
||||
raise ValidationError(_('Currency not supported by Wordline'))
|
||||
amount = int(tx_values.get('amount') * 100)
|
||||
if self.environment == 'prod':
|
||||
# For production environment, key version 2 is required
|
||||
merchant_id = getattr(self, 'sips_merchant_id')
|
||||
key_version = '2'
|
||||
else:
|
||||
# Test key provided by Atos Wordline works only with version 1
|
||||
merchant_id = '002001000000001'
|
||||
key_version = '1'
|
||||
|
||||
sips_tx_values = dict(tx_values)
|
||||
sips_tx_values.update({
|
||||
'Data': u'amount=%s|' % amount +
|
||||
u'currencyCode=%s|' % currency_code +
|
||||
u'merchantId=%s|' % merchant_id +
|
||||
u'normalReturnUrl=%s|' % urlparse.urljoin(base_url, SipsController._return_url) +
|
||||
u'automaticResponseUrl=%s|' % urlparse.urljoin(base_url, SipsController._return_url) +
|
||||
u'transactionReference=%s|' % tx_values['reference'] +
|
||||
u'statementReference=%s|' % tx_values['reference'] +
|
||||
u'keyVersion=%s' % key_version,
|
||||
'InterfaceVersion': 'HP_2.3',
|
||||
})
|
||||
|
||||
return_context = {}
|
||||
if sips_tx_values.get('return_url'):
|
||||
return_context[u'return_url'] = u'%s' % sips_tx_values.pop('return_url')
|
||||
return_context[u'reference'] = u'%s' % sips_tx_values['reference']
|
||||
sips_tx_values['Data'] += u'|returnContext=%s' % (json.dumps(return_context))
|
||||
|
||||
shasign = self._sips_generate_shasign(sips_tx_values)
|
||||
sips_tx_values['Seal'] = shasign
|
||||
return partner_values, sips_tx_values
|
||||
|
||||
@api.multi
|
||||
def sips_get_form_action_url(self):
|
||||
self.ensure_one()
|
||||
return self._get_sips_urls(self.environment)['sips_form_url']
|
||||
|
||||
|
||||
class TxSips(models.Model):
|
||||
_inherit = 'payment.transaction'
|
||||
|
||||
# sips status
|
||||
_sips_valid_tx_status = ['00']
|
||||
_sips_wait_tx_status = ['90', '99']
|
||||
_sips_refused_tx_status = ['05', '14', '34', '54', '75', '97']
|
||||
_sips_error_tx_status = ['03', '12', '24', '25', '30', '40', '51', '63', '94']
|
||||
_sips_pending_tx_status = ['60']
|
||||
_sips_cancel_tx_status = ['17']
|
||||
|
||||
# --------------------------------------------------
|
||||
# FORM RELATED METHODS
|
||||
# --------------------------------------------------
|
||||
def _sips_data_to_object(self, data):
|
||||
res = {}
|
||||
for element in data.split('|'):
|
||||
element_split = element.split('=')
|
||||
res[element_split[0]] = element_split[1]
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _sips_form_get_tx_from_data(self, data):
|
||||
""" Given a data dict coming from sips, verify it and find the related
|
||||
transaction record. """
|
||||
|
||||
data = self._sips_data_to_object(data.get('Data'))
|
||||
reference = data.get('transactionReference')
|
||||
|
||||
if not reference:
|
||||
custom = json.loads(data.pop('returnContext', False) or '{}')
|
||||
reference = custom.get('reference')
|
||||
|
||||
payment_tx = self.search([('reference', '=', reference)])
|
||||
if not payment_tx or len(payment_tx) > 1:
|
||||
error_msg = _('Sips: received data for reference %s') % reference
|
||||
if not payment_tx:
|
||||
error_msg += _('; no order found')
|
||||
else:
|
||||
error_msg += _('; multiple order found')
|
||||
_logger.error(error_msg)
|
||||
raise ValidationError(error_msg)
|
||||
return payment_tx
|
||||
|
||||
@api.model
|
||||
def _sips_form_get_invalid_parameters(self, tx, data):
|
||||
invalid_parameters = []
|
||||
|
||||
data = self._sips_data_to_object(data.get('Data'))
|
||||
|
||||
# TODO: txn_id: should be false at draft, set afterwards, and verified with txn details
|
||||
if tx.acquirer_reference and data.get('transactionReference') != tx.acquirer_reference:
|
||||
invalid_parameters.append(('transactionReference', data.get('transactionReference'), tx.acquirer_reference))
|
||||
# check what is bought
|
||||
if float_compare(float(data.get('amount', '0.0')) / 100, tx.amount, 2) != 0:
|
||||
invalid_parameters.append(('amount', data.get('amount'), '%.2f' % tx.amount))
|
||||
if tx.partner_reference and data.get('customerId') != tx.partner_reference:
|
||||
invalid_parameters.append(('customerId', data.get('customerId'), tx.partner_reference))
|
||||
|
||||
return invalid_parameters
|
||||
|
||||
@api.model
|
||||
def _sips_form_validate(self, tx, data):
|
||||
data = self._sips_data_to_object(data.get('Data'))
|
||||
status = data.get('responseCode')
|
||||
data = {
|
||||
'acquirer_reference': data.get('transactionReference'),
|
||||
'partner_reference': data.get('customerId'),
|
||||
'date_validate': data.get('transactionDateTime',
|
||||
fields.Datetime.now())
|
||||
}
|
||||
res = False
|
||||
if status in self._sips_valid_tx_status:
|
||||
msg = 'Payment for tx ref: %s, got response [%s], set as done.' % \
|
||||
(tx.reference, status)
|
||||
_logger.info(msg)
|
||||
data.update(state='done', state_message=msg)
|
||||
res = True
|
||||
elif status in self._sips_error_tx_status:
|
||||
msg = 'Payment for tx ref: %s, got response [%s], set as ' \
|
||||
'error.' % (tx.reference, status)
|
||||
data.update(state='error', state_message=msg)
|
||||
elif status in self._sips_wait_tx_status:
|
||||
msg = 'Received wait status for payment ref: %s, got response ' \
|
||||
'[%s], set as error.' % (tx.reference, status)
|
||||
data.update(state='error', state_message=msg)
|
||||
elif status in self._sips_refused_tx_status:
|
||||
msg = 'Received refused status for payment ref: %s, got response' \
|
||||
' [%s], set as error.' % (tx.reference, status)
|
||||
data.update(state='error', state_message=msg)
|
||||
elif status in self._sips_pending_tx_status:
|
||||
msg = 'Payment ref: %s, got response [%s] set as pending.' \
|
||||
% (tx.reference, status)
|
||||
data.update(state='pending', state_message=msg)
|
||||
elif status in self._sips_cancel_tx_status:
|
||||
msg = 'Received notification for payment ref: %s, got response ' \
|
||||
'[%s], set as cancel.' % (tx.reference, status)
|
||||
data.update(state='cancel', state_message=msg)
|
||||
else:
|
||||
msg = 'Received unrecognized status for payment ref: %s, got ' \
|
||||
'response [%s], set as error.' % (tx.reference, status)
|
||||
data.update(state='error', state_message=msg)
|
||||
|
||||
_logger.info(msg)
|
||||
tx.write(data)
|
||||
return res
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="acquirer_form_sips" model="ir.ui.view">
|
||||
<field name="name">acquirer.form.sips</field>
|
||||
<field name="model">payment.acquirer</field>
|
||||
<field name="inherit_id" ref="payment.acquirer_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr='//group[@name="acquirer_display"]' position='after'>
|
||||
<group attrs="{'invisible': [('provider', '!=', 'sips')]}">
|
||||
<field name="sips_secret"/>
|
||||
<field name="sips_merchant_id"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data noupdate="0">
|
||||
|
||||
<template id="sips_acquirer_button">
|
||||
<form t-att-action="tx_url" method="post" target="_self">
|
||||
<input type="hidden" name="Data" t-att-value="tx_values['Data']"/>
|
||||
<input type="hidden" name="InterfaceVersion" t-att-value="tx_values['InterfaceVersion']"/>
|
||||
<input type="hidden" name="Seal" t-att-value="tx_values['Seal']"/>
|
||||
<!-- submit -->
|
||||
<button type="submit" width="100px"
|
||||
t-att-class="submit_class">
|
||||
<img t-if="not submit_txt" src="/payment_sips/static/src/img/sips_icon.png"/>
|
||||
<span t-if="submit_txt"><t t-esc="submit_txt"/> <span class="fa fa-long-arrow-right"/></span>
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -1189,19 +1189,22 @@ class pos_order(osv.osv):
|
||||
# Create an move for each order line
|
||||
|
||||
cur = order.pricelist_id.currency_id
|
||||
round_per_line = True
|
||||
if order.company_id.tax_calculation_rounding_method == 'round_globally':
|
||||
round_per_line = False
|
||||
for line in order.lines:
|
||||
tax_amount = 0
|
||||
computed_taxes = account_tax_obj.compute_all(cr, uid, line.tax_ids, line.price_unit * (100.0-line.discount) / 100.0, line.qty)['taxes']
|
||||
|
||||
for tax in computed_taxes:
|
||||
tax_amount += cur_obj.round(cr, uid, cur, tax['amount'])
|
||||
tax_amount += cur_obj.round(cr, uid, cur, tax['amount']) if round_per_line else tax['amount']
|
||||
if tax_amount < 0:
|
||||
group_key = (tax['ref_tax_code_id'], tax['base_code_id'], tax['account_collected_id'], tax['id'])
|
||||
else:
|
||||
group_key = (tax['tax_code_id'], tax['base_code_id'], tax['account_collected_id'], tax['id'])
|
||||
|
||||
group_tax.setdefault(group_key, 0)
|
||||
group_tax[group_key] += cur_obj.round(cr, uid, cur, tax['amount'])
|
||||
group_tax[group_key] += cur_obj.round(cr, uid, cur, tax['amount']) if round_per_line else tax['amount']
|
||||
|
||||
amount = line.price_subtotal
|
||||
|
||||
|
||||
@@ -151,6 +151,7 @@ openerp.point_of_sale.load_models = function load_models(instance, module){ //mo
|
||||
model: 'product.uom',
|
||||
fields: [],
|
||||
domain: null,
|
||||
context: function(self){ return { active_test: false }; },
|
||||
loaded: function(self,units){
|
||||
self.units = units;
|
||||
var units_by_id = {};
|
||||
|
||||
@@ -44,15 +44,11 @@ class sale_report(osv.osv):
|
||||
'categ_id': fields.many2one('product.category','Product Category', readonly=True),
|
||||
'nbr': fields.integer('# of Lines', readonly=True), # TDE FIXME master: rename into nbr_lines
|
||||
'state': fields.selection([
|
||||
('draft', 'Quotation'),
|
||||
('sent', 'Quotation Sent'),
|
||||
('waiting_date', 'Waiting Schedule'),
|
||||
('manual', 'Manual In Progress'),
|
||||
('progress', 'In Progress'),
|
||||
('invoice_except', 'Invoice Exception'),
|
||||
('done', 'Done'),
|
||||
('cancel', 'Cancelled')
|
||||
], 'Order Status', readonly=True),
|
||||
('cancel', 'Cancelled'),
|
||||
('draft', 'Draft'),
|
||||
('confirmed', 'Confirmed'),
|
||||
('exception', 'Exception'),
|
||||
('done', 'Done')], 'Order Status', readonly=True),
|
||||
'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', readonly=True),
|
||||
'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True),
|
||||
'invoiced': fields.boolean('Paid', readonly=True),
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<record id="filter_sale_report_sales_funnel" model="ir.filters">
|
||||
<field name="name">Sales Funnel</field>
|
||||
<field name="model_id">sale.report</field>
|
||||
<field name="domain">['&', ('date','<=', time.strftime('%%Y-12-31')), '&', ('date','>=',time.strftime('%%Y-01-01')), '|', ('state','in',('draft','sent')), ('state','not in',('draft','sent','cancel'))]</field>
|
||||
<field name="domain">['&', ('date','<=', time.strftime('%%Y-12-31')), '&', ('date','>=',time.strftime('%%Y-01-01')), ('state','not in',('cancel',))]</field>
|
||||
<field name="user_id" eval="False"/>
|
||||
<field name="context">{'group_by': ['state'], 'measures': ['price_total']}</field>
|
||||
</record>
|
||||
@@ -60,8 +60,8 @@
|
||||
<field name="date"/>
|
||||
<field name="date_confirm"/>
|
||||
<filter string="This Year" name="year" invisible="1" domain="[('date','<=', time.strftime('%%Y-12-31')),('date','>=',time.strftime('%%Y-01-01'))]"/>
|
||||
<filter name="Quotations" domain="[('state','in',('draft','sent'))]"/>
|
||||
<filter name="Sales" string="Sales" domain="[('state','not in',('draft','sent','cancel'))]"/>
|
||||
<filter name="Quotations" domain="[('state','in',('draft'))]"/>
|
||||
<filter name="Sales" string="Sales" domain="[('state','not in',('draft', 'cancel'))]"/>
|
||||
<separator/>
|
||||
<field name="partner_id"/>
|
||||
<field name="product_id"/>
|
||||
|
||||
+2
-2
@@ -1250,10 +1250,10 @@ class product_product(osv.Model):
|
||||
def _sales_count(self, cr, uid, ids, field_name, arg, context=None):
|
||||
r = dict.fromkeys(ids, 0)
|
||||
domain = [
|
||||
('state', 'in', ['waiting_date','progress','manual', 'shipping_except', 'invoice_except', 'done']),
|
||||
('state', 'in', ['confirmed', 'done']),
|
||||
('product_id', 'in', ids),
|
||||
]
|
||||
for group in self.pool['sale.report'].read_group(cr, uid, domain, ['product_id','product_uom_qty'], ['product_id'], context=context):
|
||||
for group in self.pool['sale.report'].read_group(cr, uid, domain, ['product_id', 'product_uom_qty'], ['product_id'], context=context):
|
||||
r[group['product_id'][0]] = group['product_uom_qty']
|
||||
return r
|
||||
|
||||
|
||||
@@ -699,7 +699,7 @@
|
||||
<field name="name">Sales Analysis</field>
|
||||
<field name="res_model">sale.report</field>
|
||||
<field name="view_mode">graph</field>
|
||||
<field name="domain">[('state','not in',('draft','sent','cancel')),('team_id', '=', active_id)]</field>
|
||||
<field name="domain">[('state','not in',('draft','cancel')),('team_id', '=', active_id)]</field>
|
||||
<field name="context">{'search_default_order_month':1}</field>
|
||||
<field name="help">This report performs analysis on your sales orders. Analysis check your sales revenues and sort it by different group criteria (salesman, partner, product, etc.) Use this report to perform analysis on sales not having invoiced yet. If you want to analyse your turnover, you should use the Invoice Analysis report in the Accounting application.</field>
|
||||
</record>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
@media print {
|
||||
.js_surveyform {
|
||||
font-size: 13px;
|
||||
}
|
||||
.js_surveyform textarea.form-control {
|
||||
height: 10em;
|
||||
}
|
||||
.js_surveyform h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
.js_surveyform h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
.js_question-wrapper {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
@@ -172,7 +172,7 @@
|
||||
</template>
|
||||
|
||||
<template id="simple_choice" name="Simple choice">
|
||||
<div t-if="question.display_mode == 'dropdown'" class="js_drop row">
|
||||
<div t-if="question.display_mode == 'dropdown' and token" class="js_drop row">
|
||||
<div class="col-md-12">
|
||||
<select class="form-control" t-att-name="prefix">
|
||||
<option disabled="1" selected="1" value="">Choose...</option>
|
||||
@@ -192,7 +192,7 @@
|
||||
<input type="text" class="form-control" t-att-name="'%s_%s' % (prefix, 'comment')"/>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="question.display_mode == 'columns' " class="row js_radio">
|
||||
<div t-if="question.display_mode == 'columns' or not token" class="row js_radio">
|
||||
<div t-foreach='question.labels_ids' t-as='label' t-attf-class="col-md-#{question.column_nb}">
|
||||
<label t-att-class="' bg-success ' if quizz_correction and label.quizz_mark > 0.0 else ''">
|
||||
<input type="radio" t-att-name="prefix" t-att-value='label.id' />
|
||||
@@ -259,6 +259,12 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="assets_frontend_survey" inherit_id="website.assets_frontend" name="Printing Survey assets">
|
||||
<xpath expr="." position="inside">
|
||||
<link href="/survey/static/src/css/survey_print.css" rel="stylesheet" type="text/css"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- Printable view of a survey (all pages) -->
|
||||
<template id="survey_print" name="Survey">
|
||||
<t t-call="website.layout">
|
||||
|
||||
@@ -530,6 +530,8 @@ instance.web_view_editor.ViewEditor = instance.web.Widget.extend({
|
||||
var tr = self.get_object_by_id(this.one_object.clicked_tr_id, this.one_object['main_object'], [])[0].att_list[0];
|
||||
var parent_tr = ($(side).prevAll("tr[level=" + String(this.one_object.clicked_tr_level - 1) + "]"))[0];
|
||||
var field_dataset = new instance.web.DataSetSearch(this, this.model, null, null);
|
||||
if(_.isUndefined(parent_tr))
|
||||
return;
|
||||
parent_tr = self.get_object_by_id(parseInt($(parent_tr).attr('id').replace(/[^0-9]+/g, '')), this.one_object['main_object'], [])[0].att_list[0];
|
||||
_.each([tr, parent_tr],function(element) {
|
||||
var value = _.has(_CHILDREN, element) ? element : _.str.include(html_tag, element)?"html_tag":false;
|
||||
@@ -994,7 +996,7 @@ instance.web_view_editor.ViewEditor = instance.web.Widget.extend({
|
||||
type_widget.set_value(node.value);
|
||||
self.add_widget.push(type_widget);
|
||||
});
|
||||
table_selector.find("td[id^=]").attr("width","100px");
|
||||
table_selector.find("td[id^='']").attr("width","100px");
|
||||
self.add_node_dialog.$el.find('#new_field').click(function() {
|
||||
model_data = new instance.web.DataSetSearch(self,'ir.model', null, null);
|
||||
model_data.read_slice([], {domain: [['model','=', self.model]]}).done(function(result) {
|
||||
|
||||
@@ -16,3 +16,5 @@ access_product_attribute_price_public,product.attribute price public,product.mod
|
||||
access_product_attribute_line_public,product.attribute line public,product.model_product_attribute_line,,1,0,0,0
|
||||
access_fiscal_position_public,fiscal position public,account.model_account_fiscal_position,base.group_portal,1,0,0,0
|
||||
access_payment_term,payment term public,account.model_account_payment_term,base.group_portal,1,0,0,0
|
||||
access_product_supplierinfo_user,product.supplierinfo.user,product.model_product_supplierinfo,base.group_public,1,0,0,0
|
||||
access_pricelist_partnerinfo_user,pricelist.partnerinfo.user,product.model_pricelist_partnerinfo,base.group_public,1,0,0,0
|
||||
|
||||
|
@@ -229,6 +229,11 @@ $('.oe_website_sale').each(function () {
|
||||
$parent.find(".js_check_product").attr("disabled", "disabled");
|
||||
}
|
||||
});
|
||||
|
||||
$('div.js_product', oe_website_sale).each(function () {
|
||||
$('input.js_product_change', this).first().trigger('change');
|
||||
});
|
||||
|
||||
$('ul.js_add_cart_variants', oe_website_sale).each(function () {
|
||||
$('input.js_variant_change, select.js_variant_change', this).first().trigger('change');
|
||||
});
|
||||
|
||||
+13
-13
@@ -9,7 +9,7 @@ Theme Tutorial
|
||||
Odoo celebrates freedom. Freedom for the designer to go further and
|
||||
freedom for the user to customize everything according to their needs.
|
||||
|
||||
Ready to create your own theme? Great. Here some things you should know before you begin. This tutorial is a guide to creating an Odoo theme.
|
||||
Ready to create your own theme? Great. Here are some things you should know before you begin. This tutorial is a guide to creating an Odoo theme.
|
||||
|
||||
.. image:: theme_tutorial_assets/img/Intro.jpg
|
||||
|
||||
@@ -219,8 +219,8 @@ Odoo’s themes are packaged like modules. Even if you are designing a very simp
|
||||
Create another empty file and name it ``__init__.py``. It's a
|
||||
mandatory system file. Create and leave it blank.
|
||||
|
||||
``view`` and ``static`` folders
|
||||
Create them in the main folder. In ``view`` you'll place your xml
|
||||
``views`` and ``static`` folders
|
||||
Create them in the main folder. In ``views`` you'll place your xml
|
||||
files that define your snippets, your pages and your
|
||||
options. ``static`` folder is the right place for your style ,
|
||||
images and custom js code.
|
||||
@@ -276,7 +276,6 @@ The ``data`` property will contain the xml files list. Right now it’s empty, b
|
||||
Installing your theme
|
||||
---------------------
|
||||
|
||||
To install your theme you just place your theme folder inside "addons"
|
||||
To install your theme, you just place your theme folder inside addons in your Odoo installation.
|
||||
|
||||
After that, navigate to the Settings page, look for your theme and click on the install button.
|
||||
@@ -310,7 +309,7 @@ Extend the default Header
|
||||
|
||||
By default, Odoo header contains a responsive navigation menu and the company’s logo. You can easily add new elements or style the existing one.
|
||||
|
||||
To do so, create a **layout.xml** file in your **views** folder folder and add the default Odoo xml markup.
|
||||
To do so, create a **layout.xml** file in your **views** folder and add the default Odoo xml markup.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
@@ -429,7 +428,7 @@ default page layout with our code.
|
||||
As you can see, we wrapped our code into two ``<div>``, one with ID ``wrap`` and the other one with class ``container``. This is to provide a minimal layout.
|
||||
|
||||
The next step is to add an empty area that the user
|
||||
can fill with snippets. To archive this, just create a ``div`` with
|
||||
can fill with snippets. To achieve this, just create a ``div`` with
|
||||
``oe_structure`` class just before closing the ``div#wrap`` element.
|
||||
|
||||
.. code-block:: xml
|
||||
@@ -463,7 +462,7 @@ can fill with snippets. To archive this, just create a ``div`` with
|
||||
|
||||
You can create as many snippet areas as you like and place them anywhere in your pages.
|
||||
|
||||
Our page is almost ready. Now all we have to do is add **pages.xml** in our **__opererp_.py** file
|
||||
Our page is almost ready. Now all we have to do is add **pages.xml** in our **__openerp__.py** file
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -555,8 +554,8 @@ order to operate.
|
||||
|
||||
In this case, we are referring to ``assets_frontend`` template,
|
||||
located in the ``website`` module. ``assets_frontend`` specifies the
|
||||
list of assets loaded by the the website builder and our goal is to
|
||||
add our less file to this list.
|
||||
list of assets loaded by the website builder and our goal is to add
|
||||
our less file to this list.
|
||||
|
||||
This can be achieved using xpath with the attributes
|
||||
``expr="link[last()]"`` and ``position="after"``, which means "*take my
|
||||
@@ -615,12 +614,12 @@ The template contains the HTML markup that will be displayed by the snippet.
|
||||
|
||||
As you can see, we used Bootstrap default classes for our three columns. It’s not just about layout, these classes **will be triggered by the Website Builder to make them resizable by the user**.
|
||||
|
||||
The previous code will create the snippet’s content, but we still need to place it into the editor bar, so the user will be able to drag&drop it into the page. Copy/paste this template in your **snippet.xml** file.
|
||||
The previous code will create the snippet’s content, but we still need to place it into the editor bar, so the user will be able to drag&drop it into the page. Copy/paste this template in your **snippets.xml** file.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<template id="place_into_bar" inherit_id="website.snippets" name="Place into bar">
|
||||
<xpath expr="//div[@id='snippet_structure']//div[@class='o_panel_body']" position="inside">
|
||||
<xpath expr="//div[@id='snippet_structure']" position="inside">
|
||||
<t t-snippet="theme_tutorial.snippet_testimonial"
|
||||
t-thumbnail="/theme_tutorial/static/src/img/ui/snippet_thumb.jpg"/>
|
||||
</xpath>
|
||||
@@ -666,7 +665,7 @@ Options group properties
|
||||
Options are wrapped in groups. Groups can have properties that define how the included options will interact with the user interface.
|
||||
|
||||
``data-selector=" css selector(s) "``
|
||||
Bind the all the options included into the group to a particular element.
|
||||
Bind all the options included into the group to a particular element.
|
||||
``data-js=" custom method name "``
|
||||
Is used to bind custom Javascript methods.
|
||||
``data-drop-in=" css selector(s) "``
|
||||
@@ -709,7 +708,7 @@ We start by adding a new file in our views folder - name it **options.xml** and
|
||||
|
||||
.. note::
|
||||
|
||||
The previous template will inherit the default **snippet_option template** adding our options after the **background** options (xpath expr attribute).
|
||||
The previous template will inherit the default **snippet_options template** adding our options after the **background** options (xpath expr attribute).
|
||||
To place your options in a particular order, inspect the **snippet_options template** from the **website module** and add your options before/after the desired position.
|
||||
|
||||
As you can see, we wrapped all our options inside a DIV tag that will
|
||||
@@ -724,6 +723,7 @@ Since ``select_class`` method avoids multiple selections, the last "empty"
|
||||
option will reset the snippet to default.
|
||||
|
||||
Add **options.xml** to ``__openerp__.py`` and update your theme.
|
||||
|
||||
.. image:: theme_tutorial_assets/img/restart.png
|
||||
|
||||
Dropping our snippet onto the page, you will notice that our new options are automatically added to the customize menu. Inspecting the page, you will also notice that the class will be applied to the element when selecting an option.
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp.exceptions import AccessError
|
||||
from openerp.osv import osv, fields
|
||||
|
||||
class res_partner(osv.Model):
|
||||
@@ -49,6 +50,8 @@ class Category(models.Model):
|
||||
name = fields.Char(required=True)
|
||||
parent = fields.Many2one('test_new_api.category')
|
||||
display_name = fields.Char(compute='_compute_display_name', inverse='_inverse_display_name')
|
||||
discussions = fields.Many2many('test_new_api.discussion', 'test_new_api_discussion_category',
|
||||
'category', 'discussion')
|
||||
|
||||
@api.one
|
||||
@api.depends('name', 'parent.display_name') # this definition is recursive
|
||||
@@ -74,6 +77,10 @@ class Category(models.Model):
|
||||
# assign name of last category, and reassign display_name (to normalize it)
|
||||
self.name = names[-1].strip()
|
||||
|
||||
def read(self, fields=None, load='_classic_read'):
|
||||
if self.search_count([('id', 'in', self._ids), ('name', '=', 'NOACCESS')]):
|
||||
raise AccessError('Sorry')
|
||||
return super(Category, self).read(fields, load)
|
||||
|
||||
class Discussion(models.Model):
|
||||
_name = 'test_new_api.discussion'
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
from datetime import date, datetime
|
||||
|
||||
from openerp.exceptions import AccessError
|
||||
from openerp.tests import common
|
||||
|
||||
|
||||
@@ -337,6 +338,24 @@ class TestNewFields(common.TransactionCase):
|
||||
self.assertEqual(data['display_name'], display_name)
|
||||
self.assertEqual(data['size'], size)
|
||||
|
||||
def test_31_prefetch(self):
|
||||
""" test prefetch of records handle AccessError """
|
||||
Category = self.env['test_new_api.category']
|
||||
cat_1 = Category.create({'name': 'NOACCESS'}).id
|
||||
cat_2 = Category.create({'name': 'ACCESS', 'parent': cat_1}).id
|
||||
|
||||
self.env.clear()
|
||||
|
||||
cat = Category.browse(cat_2)
|
||||
self.assertEqual(cat.name, 'ACCESS')
|
||||
# both categories should be in prefetch ids
|
||||
self.assertSetEqual(self.env.prefetch[Category._name], set([cat_1, cat_2]))
|
||||
# but due to our (lame) overwrite of `read`, it should not forbid us to read records we have access to
|
||||
self.assertFalse(len(cat.discussions))
|
||||
self.assertEqual(cat.parent.id, cat_1)
|
||||
with self.assertRaises(AccessError):
|
||||
Category.browse(cat_1).name
|
||||
|
||||
def test_40_new(self):
|
||||
""" test new records. """
|
||||
discussion = self.env.ref('test_new_api.discussion_0')
|
||||
|
||||
+2
-1
@@ -3240,7 +3240,8 @@ class BaseModel(object):
|
||||
try:
|
||||
result = records.read(list(fnames), load='_classic_write')
|
||||
except AccessError:
|
||||
pass
|
||||
# not all records may be accessible, try with only current record
|
||||
result = self.read(list(fnames), load='_classic_write')
|
||||
|
||||
# check the cache, and update it if necessary
|
||||
if not self._cache.contains(field):
|
||||
|
||||
Reference in New Issue
Block a user