diff options
| author | Christophe Simonis <chs@odoo.com> | 2015-07-27 14:56:49 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2015-07-27 14:56:49 +0200 |
| commit | 9d6786bc4324fa94d4c73db70ccc83dec58c18c8 (patch) | |
| tree | 71ae5b4f4b6ef4f476625bc29accfa6ffa21fb22 | |
| parent | 809f19c9b795964c0ea3821feb8d4ee82d1c1cbc (diff) | |
| parent | 5e6ec89659b639942c4ca7774b0598a0b85a67be (diff) | |
[MERGE] forward port of branch 8.0 up to 5e6ec89
41 files changed, 589 insertions, 57 deletions
diff --git a/addons/analytic/models/analytic.py b/addons/analytic/models/analytic.py index 925f628efcc..199ef9da00c 100644 --- a/addons/analytic/models/analytic.py +++ b/addons/analytic/models/analytic.py @@ -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): diff --git a/addons/base_action_rule/base_action_rule.py b/addons/base_action_rule/base_action_rule.py index cc5b5da7a8f..7180d305e29 100644 --- a/addons/base_action_rule/base_action_rule.py +++ b/addons/base_action_rule/base_action_rule.py @@ -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 diff --git a/addons/claim_from_delivery/claim_delivery_view.xml b/addons/claim_from_delivery/claim_delivery_view.xml index f676b03ac47..58bc536db36 100644 --- a/addons/claim_from_delivery/claim_delivery_view.xml +++ b/addons/claim_from_delivery/claim_delivery_view.xml @@ -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"> diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml index 3fad50e1595..37b4a8e84b1 100644 --- a/addons/crm/crm_lead_view.xml +++ b/addons/crm/crm_lead_view.xml @@ -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"/> diff --git a/addons/crm/res_partner.py b/addons/crm/res_partner.py index 844e755f953..b5740c310e8 100644 --- a/addons/crm/res_partner.py +++ b/addons/crm/res_partner.py @@ -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): diff --git a/addons/crm/res_partner_view.xml b/addons/crm/res_partner_view.xml index cb9536a8097..72e2407179d 100644 --- a/addons/crm/res_partner_view.xml +++ b/addons/crm/res_partner_view.xml @@ -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" diff --git a/addons/crm_claim/crm_claim_view.xml b/addons/crm_claim/crm_claim_view.xml index 5a02912a5d5..a4144637aeb 100644 --- a/addons/crm_claim/crm_claim_view.xml +++ b/addons/crm_claim/crm_claim_view.xml @@ -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> diff --git a/addons/crm_claim/report/crm_claim_report.py b/addons/crm_claim/report/crm_claim_report.py index ec53b66ea45..86d5e65c83a 100644 --- a/addons/crm_claim/report/crm_claim_report.py +++ b/addons/crm_claim/report/crm_claim_report.py @@ -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), diff --git a/addons/crm_claim/res_partner_view.xml b/addons/crm_claim/res_partner_view.xml index 11bf9e5a3ba..97e16cc3fdd 100644 --- a/addons/crm_claim/res_partner_view.xml +++ b/addons/crm_claim/res_partner_view.xml @@ -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"> diff --git a/addons/crm_helpdesk/crm_helpdesk.py b/addons/crm_helpdesk/crm_helpdesk.py index 8d7c85bf7a7..0ffc18c2ad3 100644 --- a/addons/crm_helpdesk/crm_helpdesk.py +++ b/addons/crm_helpdesk/crm_helpdesk.py @@ -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) diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js index e4a4510b58f..161df944d72 100644 --- a/addons/im_livechat/static/src/js/im_livechat.js +++ b/addons/im_livechat/static/src/js/im_livechat.js @@ -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" }, diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 9c4c2491b76..3c05b8b1183 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -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: diff --git a/addons/mail/tests/test_mail_gateway.py b/addons/mail/tests/test_mail_gateway.py index ba13b5ebacb..7f16c4f0f5f 100644 --- a/addons/mail/tests/test_mail_gateway.py +++ b/addons/mail/tests/test_mail_gateway.py @@ -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) diff --git a/addons/mrp/product.py b/addons/mrp/product.py index e7eb807e87f..0c461ae8421 100644 --- a/addons/mrp/product.py +++ b/addons/mrp/product.py @@ -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 diff --git a/addons/payment_sips/__init__.py b/addons/payment_sips/__init__.py new file mode 100644 index 00000000000..396c76fe87a --- /dev/null +++ b/addons/payment_sips/__init__.py @@ -0,0 +1,2 @@ +import models +import controllers diff --git a/addons/payment_sips/__openerp__.py b/addons/payment_sips/__openerp__.py new file mode 100644 index 00000000000..89c36a757e9 --- /dev/null +++ b/addons/payment_sips/__openerp__.py @@ -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, +} diff --git a/addons/payment_sips/controllers/__init__.py b/addons/payment_sips/controllers/__init__.py new file mode 100644 index 00000000000..bbd183e955b --- /dev/null +++ b/addons/payment_sips/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +import main diff --git a/addons/payment_sips/controllers/main.py b/addons/payment_sips/controllers/main.py new file mode 100644 index 00000000000..47c9c8d0f0e --- /dev/null +++ b/addons/payment_sips/controllers/main.py @@ -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) diff --git a/addons/payment_sips/data/sips.xml b/addons/payment_sips/data/sips.xml new file mode 100644 index 00000000000..69a5819d41e --- /dev/null +++ b/addons/payment_sips/data/sips.xml @@ -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> diff --git a/addons/payment_sips/models/__init__.py b/addons/payment_sips/models/__init__.py new file mode 100644 index 00000000000..365abd07d65 --- /dev/null +++ b/addons/payment_sips/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +import sips diff --git a/addons/payment_sips/models/sips.py b/addons/payment_sips/models/sips.py new file mode 100644 index 00000000000..980873178a9 --- /dev/null +++ b/addons/payment_sips/models/sips.py @@ -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 diff --git a/addons/payment_sips/static/description/icon.png b/addons/payment_sips/static/description/icon.png Binary files differnew file mode 100644 index 00000000000..f04253c3bec --- /dev/null +++ b/addons/payment_sips/static/description/icon.png diff --git a/addons/payment_sips/static/src/img/sips_icon.png b/addons/payment_sips/static/src/img/sips_icon.png Binary files differnew file mode 100644 index 00000000000..f04253c3bec --- /dev/null +++ b/addons/payment_sips/static/src/img/sips_icon.png diff --git a/addons/payment_sips/static/src/img/sips_logo.png b/addons/payment_sips/static/src/img/sips_logo.png Binary files differnew file mode 100644 index 00000000000..f04253c3bec --- /dev/null +++ b/addons/payment_sips/static/src/img/sips_logo.png diff --git a/addons/payment_sips/views/payment_acquirer.xml b/addons/payment_sips/views/payment_acquirer.xml new file mode 100644 index 00000000000..3e93859027b --- /dev/null +++ b/addons/payment_sips/views/payment_acquirer.xml @@ -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> diff --git a/addons/payment_sips/views/sips.xml b/addons/payment_sips/views/sips.xml new file mode 100644 index 00000000000..02553d2c754 --- /dev/null +++ b/addons/payment_sips/views/sips.xml @@ -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> diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index 60a84e6d2bc..9cac67f0634 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -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 diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 153eb61ce4b..d81fc2bbcb6 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -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 = {}; diff --git a/addons/sale/report/sale_report.py b/addons/sale/report/sale_report.py index b5de3054dd9..febbd8d4356 100644 --- a/addons/sale/report/sale_report.py +++ b/addons/sale/report/sale_report.py @@ -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), diff --git a/addons/sale/report/sale_report_view.xml b/addons/sale/report/sale_report_view.xml index 08401d70b2e..7e36d48d7ac 100644 --- a/addons/sale/report/sale_report_view.xml +++ b/addons/sale/report/sale_report_view.xml @@ -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"/> diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 109d3a6d72f..ff773b0ad87 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -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 diff --git a/addons/sale/sale_view.xml b/addons/sale/sale_view.xml index 59136e20948..4e054ad1bdc 100644 --- a/addons/sale/sale_view.xml +++ b/addons/sale/sale_view.xml @@ -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> diff --git a/addons/survey/static/src/css/survey_print.css b/addons/survey/static/src/css/survey_print.css new file mode 100644 index 00000000000..a28f672b147 --- /dev/null +++ b/addons/survey/static/src/css/survey_print.css @@ -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; + } +} diff --git a/addons/survey/views/survey_templates.xml b/addons/survey/views/survey_templates.xml index 29fb3de8892..cb629686d3c 100644 --- a/addons/survey/views/survey_templates.xml +++ b/addons/survey/views/survey_templates.xml @@ -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"> diff --git a/addons/web_view_editor/static/src/js/view_editor.js b/addons/web_view_editor/static/src/js/view_editor.js index 6471e090f85..f3c46796e10 100644 --- a/addons/web_view_editor/static/src/js/view_editor.js +++ b/addons/web_view_editor/static/src/js/view_editor.js @@ -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) { diff --git a/addons/website_sale/security/ir.model.access.csv b/addons/website_sale/security/ir.model.access.csv index 4f96b77911d..65e8c12c270 100644 --- a/addons/website_sale/security/ir.model.access.csv +++ b/addons/website_sale/security/ir.model.access.csv @@ -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 diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index e3a7d833e2f..15965a2d8da 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -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'); }); diff --git a/doc/howtos/themes.rst b/doc/howtos/themes.rst index 9aa83478ba9..cd59db73d5d 100644 --- a/doc/howtos/themes.rst +++ b/doc/howtos/themes.rst @@ -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. diff --git a/openerp/addons/test_new_api/models.py b/openerp/addons/test_new_api/models.py index 55611f6029d..4399cb0ed49 100644 --- a/openerp/addons/test_new_api/models.py +++ b/openerp/addons/test_new_api/models.py @@ -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' diff --git a/openerp/addons/test_new_api/tests/test_new_fields.py b/openerp/addons/test_new_api/tests/test_new_fields.py index af42443fbc0..3aba853c4f8 100644 --- a/openerp/addons/test_new_api/tests/test_new_fields.py +++ b/openerp/addons/test_new_api/tests/test_new_fields.py @@ -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') diff --git a/openerp/models.py b/openerp/models.py index 01cfa58de76..d410e95a738 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -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): |
