[MERGE] from trunk

This commit is contained in:
Christophe Matthieu 2014-05-27 18:28:40 +02:00
commit abab5c2e94
63 changed files with 1409 additions and 321 deletions

View File

@ -956,7 +956,6 @@
<field name="ref_tax_sign"/>
</group>
<group string="Children/Sub Taxes" colspan="2">
<field name="child_depend" class="oe_inline"/>
<field name="child_ids" nolabel="1" colspan="2">
<tree string="Account Tax">
<field name="sequence"/>

View File

@ -13,7 +13,7 @@ for customization purpose.
'depends': ['web'],
'installable': True,
'auto_install': False,
'data': [],
'data': ['views/base_import_module.xml'],
'qweb': [],
'test': [],
}

View File

@ -1,14 +1,8 @@
# -*- coding: utf-8 -*-
import functools
import os
import zipfile
from os.path import join as opj
import openerp
from openerp.http import Controller, route, request, Response
MAX_FILE_SIZE = 100 * 1024 * 1024 # in megabytes
def webservice(f):
@functools.wraps(f)
def wrap(*args, **kw):
@ -42,32 +36,4 @@ class ImportModule(Controller):
@webservice
def upload(self, mod_file=None, **kw):
self.check_user()
imm = request.registry['ir.module.module']
if not mod_file:
raise Exception("No file sent.")
if not zipfile.is_zipfile(mod_file):
raise Exception("Not a zipfile.")
success = []
errors = dict()
with zipfile.ZipFile(mod_file, "r") as z:
for zf in z.filelist:
if zf.file_size > MAX_FILE_SIZE:
raise Exception("File '%s' exceed maximum allowed file size" % zf.filename)
with openerp.tools.osutil.tempdir() as module_dir:
z.extractall(module_dir)
dirs = [d for d in os.listdir(module_dir) if os.path.isdir(opj(module_dir, d))]
for mod_name in dirs:
try:
# assert mod_name.startswith('theme_')
path = opj(module_dir, mod_name)
imm.import_module(request.cr, request.uid, mod_name, path, context=request.context)
success.append(mod_name)
except Exception, e:
errors[mod_name] = str(e)
r = ["Successfully imported module '%s'" % mod for mod in success]
for mod, error in errors.items():
r.append("Error while importing module '%s': %r" % (mod, error))
return '\n'.join(r)
return request.registry['ir.module.module'].import_zipfile(request.cr, request.uid, mod_file, context=request.context)[0]

View File

@ -1,2 +1,3 @@
# -*- coding: utf-8 -*-
import ir_module
import base_import_module

View File

@ -0,0 +1,53 @@
import base64
from StringIO import StringIO
from io import BytesIO
from openerp.osv import osv, fields
class base_import_module(osv.TransientModel):
""" Import Module """
_name = "base.import.module"
_description = "Import Module"
_columns = {
'module_file': fields.binary('Module .ZIP file', required=True),
'state':fields.selection([('init','init'),('done','done')], 'Status', readonly=True),
'import_message': fields.char('Import message'),
}
_defaults = {
'state': 'init',
}
def import_module(self, cr, uid, ids, context=None):
module_obj = self.pool.get('ir.module.module')
data = self.browse(cr, uid, ids[0] , context=context)
zip_data = base64.decodestring(data.module_file)
fp = BytesIO()
fp.write(zip_data)
res = module_obj.import_zipfile(cr, uid, fp, context=context)
self.write(cr, uid, ids, {'state': 'done', 'import_message': res[0]}, context=context)
context = dict(context, module_name=res[1])
# Return wizard otherwise it will close wizard and will not show result message to user.
return {
'name': 'Import Module',
'view_type': 'form',
'view_mode': 'form',
'target': 'new',
'res_id': ids[0],
'res_model': 'base.import.module',
'type': 'ir.actions.act_window',
'context': context,
}
def action_module_open(self, cr, uid, ids, context):
return {
'domain': [('name', 'in', context.get('module_name',[]))],
'name': 'Modules',
'view_type': 'form',
'view_mode': 'tree,form',
'res_model': 'ir.module.module',
'view_id': False,
'type': 'ir.actions.act_window',
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -1,14 +1,18 @@
import logging
import os
import sys
import zipfile
from os.path import join as opj
import openerp
from openerp.osv import osv
from openerp.tools import convert_file
from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
MAX_FILE_SIZE = 100 * 1024 * 1024 # in megabytes
class view(osv.osv):
_inherit = "ir.module.module"
@ -22,7 +26,7 @@ class view(osv.osv):
unmet_dependencies = set(terp['depends']).difference(known_mods_names.keys())
if unmet_dependencies:
raise Exception("Unmet module dependencies: %s" % ', '.join(unmet_dependencies))
raise osv.except_osv(_('Error !'), _("Unmet module dependencies: %s" % ', '.join(unmet_dependencies)))
if mod:
self.write(cr, uid, mod.id, values)
@ -69,3 +73,33 @@ class view(osv.osv):
return True
def import_zipfile(self, cr, uid, module_file, context=None):
if not module_file:
raise Exception("No file sent.")
if not zipfile.is_zipfile(module_file):
raise osv.except_osv(_('Error !'), _('File is not a zip file!'))
success = []
errors = dict()
module_names = []
with zipfile.ZipFile(module_file, "r") as z:
for zf in z.filelist:
if zf.file_size > MAX_FILE_SIZE:
raise osv.except_osv(_('Error !'), _("File '%s' exceed maximum allowed file size" % zf.filename))
with openerp.tools.osutil.tempdir() as module_dir:
z.extractall(module_dir)
dirs = [d for d in os.listdir(module_dir) if os.path.isdir(opj(module_dir, d))]
for mod_name in dirs:
module_names.append(mod_name)
try:
# assert mod_name.startswith('theme_')
path = opj(module_dir, mod_name)
self.import_module(cr, uid, mod_name, path, context=context)
success.append(mod_name)
except Exception, e:
errors[mod_name] = str(e)
r = ["Successfully imported module '%s'" % mod for mod in success]
for mod, error in errors.items():
r.append("Error while importing module '%s': %r" % (mod, error))
return '\n'.join(r), module_names

View File

@ -3,49 +3,51 @@
<data>
<record id="view_base_module_import" model="ir.ui.view">
<field name="name">Module Import</field>
<field name="model">base.module.import</field>
<field name="name">Import Module</field>
<field name="model">base.import.module</field>
<field name="arch" type="xml">
<form string="Import module" version="7.0">
<field name="state" invisible="1"/>
<separator string="Module Import" colspan="4"/>
<separator string="Import Module" colspan="4"/>
<group states="init" col="4">
<label string="Select module package to import (.zip file):" colspan="4"/>
<field name="module_file" colspan="4"/>
</group>
<group states="done" col="4">
<label string="Module file successfully imported!" colspan="4"/>
<field name="import_message" colspan="4" nolabel="1" readonly="1"/>
</group>
<footer>
<button name="importzip" string="Import module" type="object" states="init" class="oe_highlight"/>
<label string="or" states="init"/>
<button name="action_module_open" string="Open Modules" type="object" states="done" class="oe_highlight"/>
<label string="or" states="done"/>
<button string="Cancel" class="oe_link" special="cancel"/>
<div states="init">
<button name="import_module" string="Import Module" type="object" class="oe_highlight"/> or
<button special="cancel" string="Cancel" class="oe_link"/>
</div>
<div states="done">
<button name="action_module_open" string="Open Modules" type="object" class="oe_highlight"/> or
<button special="cancel" string="Close" class="oe_link"/>
</div>
</footer>
</form>
</field>
</record>
<record id="action_view_base_module_import" model="ir.actions.act_window">
<field name="name">Module Import</field>
<field name="name">Import Module</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">base.module.import</field>
<field name="res_model">base.import.module</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<!-- This feature is now deprecated, but may come back later.
<menuitem
name="Import Module"
action="action_view_base_module_import"
id="menu_view_base_module_import"
parent="menu_management"
parent="base.menu_management"
groups="base.group_no_one"
sequence="6"/>
-->
sequence="100"/>
</data>
</openerp>

View File

@ -16,4 +16,7 @@ class AccountPaymentConfig(osv.TransientModel):
'module_payment_adyen': fields.boolean(
'Manage Payments Using Adyen',
help='-It installs the module payment_adyen.'),
'module_payment_buckaroo': fields.boolean(
'Manage Payments Using Buckaroo',
help='-It installs the module payment_buckaroo.'),
}

View File

@ -20,6 +20,10 @@
<field name="module_payment_adyen" class="oe_inline"/>
<label for="module_payment_adyen"/>
</div>
<div>
<field name="module_payment_buckaroo" class="oe_inline"/>
<label for="module_payment_buckaroo"/>
</div>
</xpath>
</field>
</record>

View File

@ -20,7 +20,7 @@
<input t-if="tx_values.get('merchantReturnData')" type='hidden' name='merchantReturnData'
t-att-value="tx_values.get('merchantReturnData')"/>
<!-- submit -->
<button type="image" name="submit" width="100px"
<button type="submit" width="100px"
t-att-class="submit_class">
<img t-if="not submit_txt" src="/payment_adyen/static/src/img/adyen_icon.png"/>
<span t-if="submit_txt"><t t-esc="submit_txt"/> <span class="fa fa-long-arrow-right"/></span>

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2014-Today OpenERP SA (<http://www.openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import models
import controllers

View File

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
{
'name': 'Buckaroo Payment Acquirer',
'category': 'Hidden',
'summary': 'Payment Acquirer: Buckaroo Implementation',
'version': '1.0',
'description': """Buckaroo Payment Acquirer""",
'author': 'OpenERP SA',
'depends': ['payment'],
'data': [
'views/buckaroo.xml',
'views/payment_acquirer.xml',
'data/buckaroo.xml',
],
'installable': True,
}

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import main

View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
try:
import simplejson as json
except ImportError:
import json
import logging
import pprint
import werkzeug
from openerp import http, SUPERUSER_ID
from openerp.http import request
_logger = logging.getLogger(__name__)
class BuckarooController(http.Controller):
_return_url = '/payment/buckaroo/return'
_cancel_url = '/payment/buckaroo/cancel'
_exception_url = '/payment/buckaroo/error'
_reject_url = '/payment/buckaroo/reject'
@http.route([
'/payment/buckaroo/return',
'/payment/buckaroo/cancel',
'/payment/buckaroo/error',
'/payment/buckaroo/reject',
], type='http', auth='none')
def buckaroo_return(self, **post):
""" Buckaroo."""
_logger.info('Buckaroo: entering form_feedback with post data %s', pprint.pformat(post)) # debug
request.registry['payment.transaction'].form_feedback(request.cr, SUPERUSER_ID, post, 'buckaroo', context=request.context)
return_url = post.pop('return_url', '')
if not return_url:
data ='' + post.pop('ADD_RETURNDATA', '{}').replace("'", "\"")
custom = json.loads(data)
return_url = custom.pop('return_url', '/')
return werkzeug.utils.redirect(return_url)

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<record id="payment_acquirer_buckaroo" model="payment.acquirer">
<field name="name">Buckaroo</field>
<field name="provider">buckaroo</field>
<field name="company_id" ref="base.main_company"/>
<field name="view_template_id" ref="buckaroo_acquirer_button"/>
<field name="environment">test</field>
<field name="pre_msg"><![CDATA[
<p>You will be redirected to the Buckaroo website after cliking on the payment button.</p>]]></field>
<field name="brq_websitekey">dummy</field>
<field name="brq_secretkey">dummy</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import buckaroo

View File

@ -0,0 +1,191 @@
# -*- coding: utf-'8' "-*-"
from hashlib import sha1
import logging
import urlparse
from openerp.addons.payment.models.payment_acquirer import ValidationError
from openerp.addons.payment_buckaroo.controllers.main import BuckarooController
from openerp.osv import osv, fields
from openerp.tools.float_utils import float_compare
_logger = logging.getLogger(__name__)
class AcquirerBuckaroo(osv.Model):
_inherit = 'payment.acquirer'
def _get_buckaroo_urls(self, cr, uid, environment, context=None):
""" Buckaroo URLs
"""
if environment == 'prod':
return {
'buckaroo_form_url': 'https://checkout.buckaroo.nl/html/',
}
else:
return {
'buckaroo_form_url': 'https://testcheckout.buckaroo.nl/html/',
}
def _get_providers(self, cr, uid, context=None):
providers = super(AcquirerBuckaroo, self)._get_providers(cr, uid, context=context)
providers.append(['buckaroo', 'Buckaroo'])
return providers
_columns = {
'brq_websitekey': fields.char('WebsiteKey', required_if_provider='buckaroo'),
'brq_secretkey': fields.char('SecretKey', required_if_provider='buckaroo'),
}
def _buckaroo_generate_digital_sign(self, acquirer, inout, values):
""" Generate the shasign for incoming or outgoing communications.
:param browse acquirer: the payment.acquirer browse record. It should
have a shakey in shaky out
:param string inout: 'in' (openerp contacting buckaroo) or 'out' (buckaroo
contacting openerp).
:param dict values: transaction values
:return string: shasign
"""
assert inout in ('in', 'out')
assert acquirer.provider == 'buckaroo'
keys = "add_returndata Brq_amount Brq_culture Brq_currency Brq_invoicenumber Brq_return Brq_returncancel Brq_returnerror Brq_returnreject brq_test Brq_websitekey".split()
def get_value(key):
if values.get(key):
return values[key]
return ''
if inout == 'out':
if 'BRQ_SIGNATURE' in values:
del values['BRQ_SIGNATURE']
items = sorted((k.upper(), v) for k, v in values.items())
sign = ''.join('%s=%s' % (k, v) for k, v in items)
else:
sign = ''.join('%s=%s' % (k,get_value(k)) for k in keys)
#Add the pre-shared secret key at the end of the signature
sign = sign + acquirer.brq_secretkey
if isinstance(sign, str):
sign = urlparse.parse_qsl(sign)
shasign = sha1(sign).hexdigest()
return shasign
def buckaroo_form_generate_values(self, cr, uid, id, partner_values, tx_values, context=None):
base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
acquirer = self.browse(cr, uid, id, context=context)
buckaroo_tx_values = dict(tx_values)
buckaroo_tx_values.update({
'Brq_websitekey': acquirer.brq_websitekey,
'Brq_amount': tx_values['amount'],
'Brq_currency': tx_values['currency'] and tx_values['currency'].name or '',
'Brq_invoicenumber': tx_values['reference'],
'brq_test' : True,
'Brq_return': '%s' % urlparse.urljoin(base_url, BuckarooController._return_url),
'Brq_returncancel': '%s' % urlparse.urljoin(base_url, BuckarooController._cancel_url),
'Brq_returnerror': '%s' % urlparse.urljoin(base_url, BuckarooController._exception_url),
'Brq_returnreject': '%s' % urlparse.urljoin(base_url, BuckarooController._reject_url),
'Brq_culture': 'en-US',
})
if buckaroo_tx_values.get('return_url'):
buckaroo_tx_values['add_returndata'] = {'return_url': '%s' % buckaroo_tx_values.pop('return_url')}
else:
buckaroo_tx_values['add_returndata'] = ''
buckaroo_tx_values['Brq_signature'] = self._buckaroo_generate_digital_sign(acquirer, 'in', buckaroo_tx_values)
return partner_values, buckaroo_tx_values
def buckaroo_get_form_action_url(self, cr, uid, id, context=None):
acquirer = self.browse(cr, uid, id, context=context)
return self._get_buckaroo_urls(cr, uid, acquirer.environment, context=context)['buckaroo_form_url']
class TxBuckaroo(osv.Model):
_inherit = 'payment.transaction'
# buckaroo status
_buckaroo_valid_tx_status = [190]
_buckaroo_pending_tx_status = [790, 791, 792, 793]
_buckaroo_cancel_tx_status = [890, 891]
_buckaroo_error_tx_status = [490, 491, 492]
_buckaroo_reject_tx_status = [690]
_columns = {
'buckaroo_txnid': fields.char('Transaction ID'),
}
# --------------------------------------------------
# FORM RELATED METHODS
# --------------------------------------------------
def _buckaroo_form_get_tx_from_data(self, cr, uid, data, context=None):
""" Given a data dict coming from buckaroo, verify it and find the related
transaction record. """
reference, pay_id, shasign = data.get('BRQ_INVOICENUMBER'), data.get('BRQ_PAYMENT'), data.get('BRQ_SIGNATURE')
if not reference or not pay_id or not shasign:
error_msg = 'Buckaroo: received data with missing reference (%s) or pay_id (%s) or shashign (%s)' % (reference, pay_id, shasign)
_logger.error(error_msg)
raise ValidationError(error_msg)
tx_ids = self.search(cr, uid, [('reference', '=', reference)], context=context)
if not tx_ids or len(tx_ids) > 1:
error_msg = 'Buckaroo: received data for reference %s' % (reference)
if not tx_ids:
error_msg += '; no order found'
else:
error_msg += '; multiple order found'
_logger.error(error_msg)
raise ValidationError(error_msg)
tx = self.pool['payment.transaction'].browse(cr, uid, tx_ids[0], context=context)
#verify shasign
shasign_check = self.pool['payment.acquirer']._buckaroo_generate_digital_sign(tx.acquirer_id, 'out' ,data)
if shasign_check.upper() != shasign.upper():
error_msg = 'Buckaroo: invalid shasign, received %s, computed %s, for data %s' % (shasign, shasign_check, data)
_logger.error(error_msg)
raise ValidationError(error_msg)
return tx
def _buckaroo_form_get_invalid_parameters(self, cr, uid, tx, data, context=None):
invalid_parameters = []
if tx.acquirer_reference and data.get('BRQ_TRANSACTIONS') != tx.acquirer_reference:
invalid_parameters.append(('Transaction Id', data.get('BRQ_TRANSACTIONS'), tx.acquirer_reference))
# check what is buyed
if float_compare(float(data.get('BRQ_AMOUNT', '0.0')), tx.amount, 2) != 0:
invalid_parameters.append(('Amount', data.get('BRQ_AMOUNT'), '%.2f' % tx.amount))
if data.get('BRQ_CURRENCY') != tx.currency_id.name:
invalid_parameters.append(('Currency', data.get('BRQ_CURRENCY'), tx.currency_id.name))
return invalid_parameters
def _buckaroo_form_validate(self, cr, uid, tx, data, context=None):
status_code = int(data.get('BRQ_STATUSCODE','0'))
if status_code in self._buckaroo_valid_tx_status:
tx.write({
'state': 'done',
'buckaroo_txnid': data.get('BRQ_TRANSACTIONS'),
})
return True
elif status_code in self._buckaroo_pending_tx_status:
tx.write({
'state': 'pending',
'buckaroo_txnid': data.get('BRQ_TRANSACTIONS'),
})
return True
elif status_code in self._buckaroo_cancel_tx_status:
tx.write({
'state': 'cancel',
'buckaroo_txnid': data.get('BRQ_TRANSACTIONS'),
})
return True
else:
error = 'Buckaroo: feedback error'
_logger.info(error)
tx.write({
'state': 'error',
'state_message': error,
'buckaroo_txnid': data.get('BRQ_TRANSACTIONS'),
})
return False

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
from openerp.addons.payment_buckaroo.tests import test_buckaroo
checks = [
test_buckaroo,
]

View File

@ -0,0 +1,178 @@
# -*- coding: utf-8 -*-
from lxml import objectify
import urlparse
import openerp
from openerp.addons.payment.models.payment_acquirer import ValidationError
from openerp.addons.payment.tests.common import PaymentAcquirerCommon
from openerp.addons.payment_buckaroo.controllers.main import BuckarooController
from openerp.tools import mute_logger
@openerp.tests.common.at_install(False)
@openerp.tests.common.post_install(False)
class BuckarooCommon(PaymentAcquirerCommon):
def setUp(self):
super(BuckarooCommon, self).setUp()
cr, uid = self.cr, self.uid
self.base_url = self.registry('ir.config_parameter').get_param(cr, uid, 'web.base.url')
# get the buckaroo account
model, self.buckaroo_id = self.registry('ir.model.data').get_object_reference(cr, uid, 'payment_buckaroo', 'payment_acquirer_buckaroo')
@openerp.tests.common.at_install(False)
@openerp.tests.common.post_install(False)
class BuckarooForm(BuckarooCommon):
def test_10_Buckaroo_form_render(self):
cr, uid, context = self.cr, self.uid, {}
# be sure not to do stupid things
buckaroo = self.payment_acquirer.browse(self.cr, self.uid, self.buckaroo_id, None)
self.assertEqual(buckaroo.environment, 'test', 'test without test environment')
# ----------------------------------------
# Test: button direct rendering
# ----------------------------------------
form_values = {
'add_returndata': None,
'Brq_websitekey': buckaroo.brq_websitekey,
'Brq_amount': '2240.0',
'Brq_currency': 'EUR',
'Brq_invoicenumber': 'SO004',
'Brq_signature': '1b8c10074c622d965272a91a9e88b5b3777d2474', # update me
'brq_test': 'True',
'Brq_return': '%s' % urlparse.urljoin(self.base_url, BuckarooController._return_url),
'Brq_returncancel': '%s' % urlparse.urljoin(self.base_url, BuckarooController._cancel_url),
'Brq_returnerror': '%s' % urlparse.urljoin(self.base_url, BuckarooController._exception_url),
'Brq_returnreject': '%s' % urlparse.urljoin(self.base_url, BuckarooController._reject_url),
'Brq_culture': 'en-US',
}
# render the button
res = self.payment_acquirer.render(
cr, uid, self.buckaroo_id,
'SO004', 2240.0, self.currency_euro_id,
partner_id=None,
partner_values=self.buyer_values,
context=context)
# check form result
tree = objectify.fromstring(res)
self.assertEqual(tree.get('action'), 'https://testcheckout.buckaroo.nl/html/', 'Buckaroo: wrong form POST url')
for form_input in tree.input:
if form_input.get('name') in ['submit']:
continue
self.assertEqual(
form_input.get('value'),
form_values[form_input.get('name')],
'Buckaroo: wrong value for input %s: received %s instead of %s' % (form_input.get('name'), form_input.get('value'), form_values[form_input.get('name')])
)
# ----------------------------------------
# Test2: button using tx + validation
# ----------------------------------------
# create a new draft tx
tx_id = self.payment_transaction.create(
cr, uid, {
'amount': 2240.0,
'acquirer_id': self.buckaroo_id,
'currency_id': self.currency_euro_id,
'reference': 'SO004',
'partner_id': self.buyer_id,
}, context=context
)
# render the button
res = self.payment_acquirer.render(
cr, uid, self.buckaroo_id,
'should_be_erased', 2240.0, self.currency_euro,
tx_id=tx_id,
partner_id=None,
partner_values=self.buyer_values,
context=context)
# check form result
tree = objectify.fromstring(res)
self.assertEqual(tree.get('action'), 'https://testcheckout.buckaroo.nl/html/', 'Buckaroo: wrong form POST url')
for form_input in tree.input:
if form_input.get('name') in ['submit']:
continue
self.assertEqual(
form_input.get('value'),
form_values[form_input.get('name')],
'Buckaroo: wrong value for form input %s: received %s instead of %s' % (form_input.get('name'), form_input.get('value'), form_values[form_input.get('name')])
)
@mute_logger('openerp.addons.payment_buckaroo.models.buckaroo', 'ValidationError')
def test_20_buckaroo_form_management(self):
cr, uid, context = self.cr, self.uid, {}
# be sure not to do stupid thing
buckaroo = self.payment_acquirer.browse(self.cr, self.uid, self.buckaroo_id, None)
self.assertEqual(buckaroo.environment, 'test', 'test without test environment')
# typical data posted by buckaroo after client has successfully paid
buckaroo_post_data = {
'BRQ_RETURNDATA': u'',
'BRQ_AMOUNT': u'2240.00',
'BRQ_CURRENCY': u'EUR',
'BRQ_CUSTOMER_NAME': u'Jan de Tester',
'BRQ_INVOICENUMBER': u'SO004',
'BRQ_PAYMENT': u'573311D081B04069BD6336001611DBD4',
'BRQ_PAYMENT_METHOD': u'paypal',
'BRQ_SERVICE_PAYPAL_PAYERCOUNTRY': u'NL',
'BRQ_SERVICE_PAYPAL_PAYEREMAIL': u'fhe@openerp.com',
'BRQ_SERVICE_PAYPAL_PAYERFIRSTNAME': u'Jan',
'BRQ_SERVICE_PAYPAL_PAYERLASTNAME': u'Tester',
'BRQ_SERVICE_PAYPAL_PAYERMIDDLENAME': u'de',
'BRQ_SERVICE_PAYPAL_PAYERSTATUS': u'verified',
'BRQ_SIGNATURE': u'175d82dd53a02bad393fee32cb1eafa3b6fbbd91',
'BRQ_STATUSCODE': u'190',
'BRQ_STATUSCODE_DETAIL': u'S001',
'BRQ_STATUSMESSAGE': u'Transaction successfully processed',
'BRQ_TEST': u'true',
'BRQ_TIMESTAMP': u'2014-05-08 12:41:21',
'BRQ_TRANSACTIONS': u'D6106678E1D54EEB8093F5B3AC42EA7B',
'BRQ_WEBSITEKEY': u'5xTGyGyPyl',
}
# should raise error about unknown tx
with self.assertRaises(ValidationError):
self.payment_transaction.form_feedback(cr, uid, buckaroo_post_data, 'buckaroo', context=context)
tx_id = self.payment_transaction.create(
cr, uid, {
'amount': 2240.0,
'acquirer_id': self.buckaroo_id,
'currency_id': self.currency_euro_id,
'reference': 'SO004',
'partner_name': 'Norbert Buyer',
'partner_country_id': self.country_france_id,
}, context=context
)
# validate it
self.payment_transaction.form_feedback(cr, uid, buckaroo_post_data, 'buckaroo', context=context)
# check state
tx = self.payment_transaction.browse(cr, uid, tx_id, context=context)
self.assertEqual(tx.state, 'done', 'Buckaroo: validation did not put tx into done state')
self.assertEqual(tx.buckaroo_txnid, buckaroo_post_data.get('BRQ_TRANSACTIONS'), 'Buckaroo: validation did not update tx payid')
# reset tx
tx.write({'state': 'draft', 'date_validate': False, 'buckaroo_txnid': False})
# now buckaroo post is ok: try to modify the SHASIGN
buckaroo_post_data['BRQ_SIGNATURE'] = '54d928810e343acf5fb0c3ee75fd747ff159ef7a'
with self.assertRaises(ValidationError):
self.payment_transaction.form_feedback(cr, uid, buckaroo_post_data, 'buckaroo', context=context)
# simulate an error
buckaroo_post_data['BRQ_STATUSCODE'] = 2
buckaroo_post_data['BRQ_SIGNATURE'] = '4164b52adb1e6a2221d3d8a39d8c3e18a9ecb90b'
self.payment_transaction.form_feedback(cr, uid, buckaroo_post_data, 'buckaroo', context=context)
# check state
tx = self.payment_transaction.browse(cr, uid, tx_id, context=context)
self.assertEqual(tx.state, 'error', 'Buckaroo: erroneous validation did not put tx into error state')

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<template id="buckaroo_acquirer_button">
<form t-if="acquirer.brq_websitekey" t-att-action="tx_url" method="post" target="_self">
<input type="hidden" name="Brq_websitekey" t-att-value="tx_values['Brq_websitekey']"/>
<input type="hidden" name="Brq_amount" t-att-value="tx_values['Brq_amount'] or '0.0'"/>
<input type="hidden" name="Brq_currency" t-att-value="tx_values['Brq_currency']"/>
<input type="hidden" name="Brq_invoicenumber" t-att-value="tx_values['Brq_invoicenumber']"/>
<input type="hidden" name="Brq_signature" t-att-value="tx_values['Brq_signature']"/>
<input type="hidden" name="brq_test" t-att-value="tx_values['brq_test']"/>
<input type="hidden" name="Brq_culture" t-att-value="tx_values['Brq_culture']"/>
<!-- URLs -->
<input t-if="tx_values.get('Brq_return')" type='hidden' name='Brq_return'
t-att-value="tx_values.get('Brq_return')"/>
<input t-if="tx_values.get('Brq_returncancel')" type='hidden' name='Brq_returncancel'
t-att-value="tx_values.get('Brq_returncancel')"/>
<input t-if="tx_values.get('Brq_returnerror')" type='hidden' name='Brq_returnerror'
t-att-value="tx_values.get('Brq_returnerror')"/>
<input t-if="tx_values.get('Brq_returnreject')" type='hidden' name='Brq_returnreject'
t-att-value="tx_values.get('Brq_returnreject')"/>
<input type='hidden' name='add_returndata' t-att-value="tx_values.get('add_returndata')"/>
<!-- submit -->
<button type="submit" width="100px"
t-att-class="submit_class">
<img t-if="not submit_txt" src="/payment_buckaroo/static/src/img/buckaroo_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>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="acquirer_form_buckaroo" model="ir.ui.view">
<field name="name">acquirer.form.buckaroo</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', '!=', 'buckaroo')]}">
<field name="brq_websitekey"/>
<field name="brq_secretkey"/>
</group>
</xpath>
</field>
</record>
<record id="transaction_form_buckaroo" model="ir.ui.view">
<field name="name">acquirer.transaction.form.buckaroo</field>
<field name="model">payment.transaction</field>
<field name="inherit_id" ref="payment.transaction_form"/>
<field name="arch" type="xml">
<xpath expr='//notebook' position='inside'>
<page string="Buckaroo TX Details">
<group>
<field name="buckaroo_txnid"/>
</group>
</page>
</xpath>
</field>
</record>
</data>
</openerp>

View File

@ -41,7 +41,7 @@
<input type='hidden' name='EXCEPTIONURL' t-att-value='tx_values["EXCEPTIONURL"]'/>
<input type='hidden' name='CANCELURL' t-att-value='tx_values["CANCELURL"]'/>
<!-- submit -->
<button type="image" name="submit" width="100px"
<button type="submit" width="100px"
t-att-class="submit_class">
<img t-if="not submit_txt" src="/payment_ogone/static/src/img/ogone_icon.png"/>
<span t-if="submit_txt"><t t-esc="submit_txt"/> <span class="fa fa-long-arrow-right"/></span>

View File

@ -31,7 +31,7 @@
<input t-if="tx_values.get('cancel_return')" type="hidden" name="cancel_return"
t-att-value="tx_values.get('cancel_return')"/>
<!-- submit -->
<button type="image" name="submit" width="100px"
<button type="submit" width="100px"
t-att-class="submit_class">
<img t-if="not submit_txt" src="/payment_paypal/static/src/img/paypal_icon.png"/>
<span t-if="submit_txt"><t t-esc="submit_txt"/> <span class="fa fa-long-arrow-right"/></span>

View File

@ -11,7 +11,7 @@
<input type='hidden' name='amount' t-att-value='amount or "0.0"'/>
<input type='hidden' name='currency' t-att-value='currency.name'/>
<!-- submit -->
<button name="submit" width="100px"
<button type="submit" width="100px"
t-att-class="submit_class">
<img t-if="not submit_txt" src="/payment_transfer/static/src/img/transfer_icon.png"/>
<span t-if="submit_txt"><t t-esc="submit_txt"/> <span class="fa fa-long-arrow-right"/></span>

View File

@ -3394,6 +3394,12 @@ instance.web.form.CompletionFieldMixin = {
classname: 'oe_m2o_dropdown_option'
});
}
else if (values.length == 0)
values.push({
label: _t("No results to show..."),
action: function() {},
classname: 'oe_m2o_dropdown_option'
});
return values;
});

View File

@ -108,8 +108,11 @@ class Website(openerp.addons.web.controllers.main.Home):
locs = request.website.enumerate_pages()
while True:
start = pages * LOC_PER_SITEMAP
loc_slice = islice(locs, start, start + LOC_PER_SITEMAP)
urls = iuv.render(cr, uid, 'website.sitemap_locs', dict(locs=loc_slice), context=context)
values = {
'locs': islice(locs, start, start + LOC_PER_SITEMAP),
'url_root': request.httprequest.url_root[:-1],
}
urls = iuv.render(cr, uid, 'website.sitemap_locs', values, context=context)
if urls.strip():
page = iuv.render(cr, uid, 'website.sitemap_xml', dict(content=urls), context=context)
if not first_page:
@ -155,22 +158,25 @@ class Website(openerp.addons.web.controllers.main.Home):
@http.route('/website/theme_change', type='http', auth="user", website=True)
def theme_change(self, theme_id=False, **kwargs):
imd = request.registry['ir.model.data']
view = request.registry['ir.ui.view']
Views = request.registry['ir.ui.view']
view_model, view_option_id = imd.get_object_reference(
_, theme_template_id = imd.get_object_reference(
request.cr, request.uid, 'website', 'theme')
views = view.search(
request.cr, request.uid, [('inherit_id', '=', view_option_id)],
context=request.context)
view.write(request.cr, request.uid, views, {'inherit_id': False},
context=request.context)
views = Views.search(request.cr, request.uid, [
('inherit_id', '=', theme_template_id),
('application', '=', 'enabled'),
], context=request.context)
Views.write(request.cr, request.uid, views, {
'application': 'disabled',
}, context=request.context)
if theme_id:
module, xml_id = theme_id.split('.')
view_model, view_id = imd.get_object_reference(
_, view_id = imd.get_object_reference(
request.cr, request.uid, module, xml_id)
view.write(request.cr, request.uid, [view_id],
{'inherit_id': view_option_id}, context=request.context)
Views.write(request.cr, request.uid, [view_id], {
'application': 'enabled'
}, context=request.context)
return request.render('website.themes', {'theme_changed': True})
@ -194,54 +200,45 @@ class Website(openerp.addons.web.controllers.main.Home):
module_obj.button_immediate_upgrade(request.cr, request.uid, module_ids, context=request.context)
return request.redirect(redirect)
@http.route('/website/customize_template_toggle', type='json', auth='user', website=True)
def customize_template_set(self, view_id):
view_obj = request.registry.get("ir.ui.view")
view = view_obj.browse(request.cr, request.uid, int(view_id),
context=request.context)
if view.inherit_id:
value = False
else:
value = view.inherit_option_id and view.inherit_option_id.id or False
view_obj.write(request.cr, request.uid, [view_id], {
'inherit_id': value
}, context=request.context)
return True
@http.route('/website/customize_template_get', type='json', auth='user', website=True)
def customize_template_get(self, xml_id, optional=True):
def customize_template_get(self, xml_id, full=False):
""" Lists the templates customizing ``xml_id``. By default, only
returns optional templates (which can be toggled on and off), if
``full=True`` returns all templates customizing ``xml_id``
"""
imd = request.registry['ir.model.data']
view_model, view_theme_id = imd.get_object_reference(
request.cr, request.uid, 'website', 'theme')
user = request.registry['res.users'].browse(request.cr, request.uid, request.uid, request.context)
group_ids = [g.id for g in user.groups_id]
user = request.registry['res.users']\
.browse(request.cr, request.uid, request.uid, request.context)
user_groups = set(user.groups_id)
view = request.registry.get("ir.ui.view")
views = view._views_get(request.cr, request.uid, xml_id, context=request.context)
done = {}
views = request.registry["ir.ui.view"]\
._views_get(request.cr, request.uid, xml_id, context=request.context)
done = set()
result = []
for v in views:
if v.groups_id and [g for g in v.groups_id if g.id not in group_ids]:
if not user_groups.issuperset(v.groups_id):
continue
if v.inherit_option_id and v.inherit_option_id.id != view_theme_id or not optional:
if v.inherit_option_id.id not in done:
if full or (v.application != 'always' and v.inherit_id.id != view_theme_id):
if v.inherit_id not in done:
result.append({
'name': v.inherit_option_id.name,
'name': v.inherit_id.name,
'id': v.id,
'xml_id': v.xml_id,
'inherit_id': v.inherit_id.id,
'header': True,
'active': False
})
done[v.inherit_option_id.id] = True
done.add(v.inherit_id)
result.append({
'name': v.name,
'id': v.id,
'xml_id': v.xml_id,
'inherit_id': v.inherit_id.id,
'header': False,
'active': (v.inherit_id.id == v.inherit_option_id.id) or (not optional and v.inherit_id.id)
'active': v.application in ('always', 'enabled'),
})
return result

View File

@ -13,8 +13,6 @@ from openerp.osv import osv, fields
class view(osv.osv):
_inherit = "ir.ui.view"
_columns = {
'inherit_option_id': fields.many2one('ir.ui.view','Optional Inheritancy'),
'inherited_option_ids': fields.one2many('ir.ui.view','inherit_option_id','Optional Inheritancies'),
'page': fields.boolean("Whether this view is a web page template (complete)"),
'website_meta_title': fields.char("Website meta title", size=70, translate=True),
'website_meta_description': fields.text("Website meta description", size=160, translate=True),
@ -24,25 +22,30 @@ class view(osv.osv):
'page': False,
}
def _view_obj(self, cr, uid, view_id, context=None):
if isinstance(view_id, basestring):
return self.pool['ir.model.data'].xmlid_to_object(
cr, uid, view_id, raise_if_not_found=True, context=context
)
elif isinstance(view_id, (int, long)):
return self.browse(cr, uid, view_id, context=context)
# assume it's already a view object (WTF?)
return view_id
# Returns all views (called and inherited) related to a view
# Used by translation mechanism, SEO and optional templates
def _views_get(self, cr, uid, view, options=True, context=None, root=True, stack_result=None):
if not context:
context = {}
if not stack_result:
stack_result = []
def view_obj(view):
if isinstance(view, basestring):
mod_obj = self.pool.get("ir.model.data")
m, n = view.split('.')
view = mod_obj.get_object(cr, uid, m, n, context=context)
elif isinstance(view, (int, long)):
view = self.pool.get("ir.ui.view").browse(cr, uid, view, context=context)
return view
def _views_get(self, cr, uid, view_id, options=True, context=None, root=True):
""" For a given view ``view_id``, should return:
* the view itself
* all views inheriting from it, enabled or not
- but not the optional children of a non-enabled child
* all views called from it (via t-call)
"""
try:
view = view_obj(view)
view = self._view_obj(cr, uid, view_id, context=context)
except ValueError:
# Shall we log that ?
return []
@ -55,19 +58,25 @@ class view(osv.osv):
node = etree.fromstring(view.arch)
for child in node.xpath("//t[@t-call]"):
try:
call_view = view_obj(child.get('t-call'))
called_view = self._view_obj(cr, uid, child.get('t-call'), context=context)
except ValueError:
continue
if call_view not in result:
result += self._views_get(cr, uid, call_view, options=options, context=context, stack_result=result)
if called_view not in result:
result += self._views_get(cr, uid, called_view, options=options, context=context)
todo = view.inherit_children_ids
if options:
todo += filter(lambda x: not x.inherit_id, view.inherited_option_ids)
# Keep options in a determinitic order whatever their enabled disabled status
todo.sort(lambda x,y:cmp(x.id,y.id))
for child_view in todo:
for r in self._views_get(cr, uid, child_view, options=bool(child_view.inherit_id), context=context, root=False, stack_result=result):
extensions = view.inherit_children_ids
if not options:
# only active children
extensions = (v for v in view.inherit_children_ids
if v.application in ('always', 'enabled'))
# Keep options in a deterministic order regardless of their applicability
for extension in sorted(extensions, key=lambda v: v.id):
for r in self._views_get(
cr, uid, extension,
# only return optional grandchildren if this child is enabled
options=extension.application in ('always', 'enabled'),
context=context, root=False):
if r not in result:
result.append(r)
return result

View File

@ -97,7 +97,7 @@
var viewId = $(document.documentElement).data('view-xmlid');
openerp.jsonRpc('/website/customize_template_get', 'call', {
'xml_id': viewId,
'optional': false,
'full': true,
}).then(function (views) {
self.loadViews.call(self, views);
self.open.call(self);

View File

@ -470,8 +470,14 @@
});
menu.on('click', 'a[data-action!=ace]', function (event) {
var view_id = $(event.currentTarget).data('view-id');
openerp.jsonRpc('/website/customize_template_toggle', 'call', {
'view_id': view_id
return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
model: 'ir.ui.view',
method: 'toggle',
args: [],
kwargs: {
ids: [parseInt(view_id, 10)],
context: website.get_context()
}
}).then( function() {
window.location.reload();
});

View File

@ -203,82 +203,82 @@
All Default Themes
-->
<template id="website.theme_amelia" name="Amelia" inherit_option_id="website.theme">
<template id="website.theme_amelia" name="Amelia" inherit_id="website.theme" optional="disabled">
<xpath expr="//link[@id='bootstrap_css']" position="replace">
<link rel='stylesheet' href='/website/static/src/css/bootswatch/amelia.min.css' t-ignore="true"/>
<link rel='stylesheet' href='/website/static/src/css/bootswatch/amelia.fix.css' t-ignore="true"/>
</xpath>
</template>
<template id="website.theme_cerulean" name="Cerulean" inherit_option_id="website.theme">
<template id="website.theme_cerulean" name="Cerulean" inherit_id="website.theme" optional="disabled">
<xpath expr="//link[@id='bootstrap_css']" position="replace">
<link rel='stylesheet' href='/website/static/src/css/bootswatch/cerulean.min.css' t-ignore="true"/>
</xpath>
</template>
<template id="website.theme_cosmo" name="Cosmo" inherit_option_id="website.theme">
<template id="website.theme_cosmo" name="Cosmo" inherit_id="website.theme" optional="disabled">
<xpath expr="//link[@id='bootstrap_css']" position="replace">
<link rel='stylesheet' href='/website/static/src/css/bootswatch/cosmo.min.css' t-ignore="true"/>
<link rel='stylesheet' href='/website/static/src/css/bootswatch/cosmo.fix.css' t-ignore="true"/>
</xpath>
</template>
<template id="website.theme_cyborg" name="Cyborg" inherit_option_id="website.theme">
<template id="website.theme_cyborg" name="Cyborg" inherit_id="website.theme" optional="disabled">
<xpath expr="//link[@id='bootstrap_css']" position="replace">
<link rel='stylesheet' href='/website/static/src/css/bootswatch/cyborg.min.css' t-ignore="true"/>
<link rel='stylesheet' href='/website/static/src/css/bootswatch/cyborg.fix.css' t-ignore="true"/>
</xpath>
</template>
<template id="website.theme_flatly" name="Flatly" inherit_option_id="website.theme">
<template id="website.theme_flatly" name="Flatly" inherit_id="website.theme" optional="disabled">
<xpath expr="//link[@id='bootstrap_css']" position="replace">
<link rel='stylesheet' href='/website/static/src/css/bootswatch/flatly.min.css' t-ignore="true"/>
<link rel='stylesheet' href='/website/static/src/css/bootswatch/flatly.fix.css' t-ignore="true"/>
</xpath>
</template>
<template id="website.theme_journal" name="Journal" inherit_option_id="website.theme">
<template id="website.theme_journal" name="Journal" inherit_id="website.theme" optional="disabled">
<xpath expr="//link[@id='bootstrap_css']" position="replace">
<link rel='stylesheet' href='/website/static/src/css/bootswatch/journal.min.css' t-ignore="true"/>
<link rel='stylesheet' href='/website/static/src/css/bootswatch/journal.fix.css' t-ignore="true"/>
</xpath>
</template>
<template id="website.theme_readable" name="Readable" inherit_option_id="website.theme">
<template id="website.theme_readable" name="Readable" inherit_id="website.theme" optional="disabled">
<xpath expr="//link[@id='bootstrap_css']" position="replace">
<link rel='stylesheet' href='/website/static/src/css/bootswatch/readable.min.css' t-ignore="true"/>
<link rel='stylesheet' href='/website/static/src/css/bootswatch/readable.fix.css' t-ignore="true"/>
</xpath>
</template>
<template id="website.theme_simplex" name="Simplex" inherit_option_id="website.theme">
<template id="website.theme_simplex" name="Simplex" inherit_id="website.theme" optional="disabled">
<xpath expr="//link[@id='bootstrap_css']" position="replace">
<link rel='stylesheet' href='/website/static/src/css/bootswatch/simplex.min.css' t-ignore="true"/>
<link rel='stylesheet' href='/website/static/src/css/bootswatch/simplex.fix.css' t-ignore="true"/>
</xpath>
</template>
<template id="website.theme_slate" name="Slate" inherit_option_id="website.theme">
<template id="website.theme_slate" name="Slate" inherit_id="website.theme" optional="disabled">
<xpath expr="//link[@id='bootstrap_css']" position="replace">
<link rel='stylesheet' href='/website/static/src/css/bootswatch/slate.min.css' t-ignore="true"/>
<link rel='stylesheet' href='/website/static/src/css/bootswatch/slate.fix.css' t-ignore="true"/>
</xpath>
</template>
<template id="website.theme_spacelab" name="Spacelab" inherit_option_id="website.theme">
<template id="website.theme_spacelab" name="Spacelab" inherit_id="website.theme" optional="disabled">
<xpath expr="//link[@id='bootstrap_css']" position="replace">
<link rel='stylesheet' href='/website/static/src/css/bootswatch/spacelab.min.css' t-ignore="true"/>
<link rel='stylesheet' href='/website/static/src/css/bootswatch/spacelab.fix.css' t-ignore="true"/>
</xpath>
</template>
<template id="website.theme_united" name="United" inherit_option_id="website.theme">
<template id="website.theme_united" name="United" inherit_id="website.theme" optional="disabled">
<xpath expr="//link[@id='bootstrap_css']" position="replace">
<link rel='stylesheet' href='/website/static/src/css/bootswatch/united.min.css' t-ignore="true"/>
</xpath>
</template>
<template id="website.theme_yeti" name="Yeti" inherit_option_id="website.theme">
<template id="website.theme_yeti" name="Yeti" inherit_id="website.theme" optional="disabled">
<xpath expr="//link[@id='bootstrap_css']" position="replace">
<link rel='stylesheet' href='/website/static/src/css/bootswatch/yeti.min.css' t-ignore="true"/>
<link rel='stylesheet' href='/website/static/src/css/bootswatch/yeti.fix.css' t-ignore="true"/>

View File

@ -220,7 +220,7 @@
</html>
</template>
<template id="layout_logo_show" inherit_id="website.layout" inherit_option_id="website.layout" name="Show Logo">
<template id="layout_logo_show" inherit_id="website.layout" optional="enabled" name="Show Logo">
<xpath expr="//header//a[@class='navbar-brand']" position="replace">
<a href="/" class="navbar-brand logo">
<img src="/logo.png"/>
@ -304,7 +304,7 @@
<script type="text/javascript" src="/website/static/src/js/jQuery.transfo.js"></script>
</template>
<template id="debugger" inherit_option_id="website.layout" name="Debugger &amp; Tests">
<template id="debugger" inherit_id="website.layout" optional="disabled" name="Debugger &amp; Tests">
<xpath expr='//t[@name="layout_head"]' position="after">
<script type="text/javascript" src="/website/static/src/js/website.tour.js"></script>
</xpath>
@ -318,7 +318,7 @@
</xpath>
</template>
<template id="show_sign_in" inherit_option_id="website.layout" inherit_id="website.layout" name="Show Sign In" groups="base.group_public">
<template id="show_sign_in" optional="enabled" inherit_id="website.layout" name="Show Sign In" groups="base.group_public">
<xpath expr="//ul[@id='top_menu']" position="inside">
<li class="divider"/>
<li>
@ -329,7 +329,7 @@
</xpath>
</template>
<template id="footer_custom" inherit_option_id="website.layout" name="Custom Footer">
<template id="footer_custom" inherit_id="website.layout" optional="disabled" name="Custom Footer">
<xpath expr="//div[@id='footer_container']" position="before">
<div class="oe_structure">
<section data-snippet-id='three-columns' class="mt16 mb16">

View File

@ -161,7 +161,7 @@
<!-- Option: Blog Post List: show tags -->
<template id="opt_blog_post_short_tags" name="Tags"
inherit_option_id="website_blog.blog_post_short" inherit_id="website_blog.blog_post_short">
optional="enabled" inherit_id="website_blog.blog_post_short">
<xpath expr="//div[@name='blog_post_data']" position="inside">
<p class="post-meta text-muted text-center" t-if="len(blog_post.tag_ids)">
<span class="fa fa-tags"/>
@ -265,7 +265,7 @@
<!-- Options: Blog Post: breadcrumb -->
<template id="blog_breadcrumb" name="Breadcrumb"
inherit_option_id="website_blog.blog_post_complete">
inherit_id="website_blog.blog_post_complete" optional="disabled">
<xpath expr="//div[@id='title']" position="before">
<div class="container">
<div class="row">
@ -285,7 +285,7 @@
<!-- Options: Blog Post: user can reply -->
<template id="opt_blog_post_complete_comment" name="Allow blog post comment"
inherit_option_id="website_blog.blog_post_complete"
inherit_id="website_blog.blog_post_complete" optional="disabled"
groups="website_mail.group_comment">
<xpath expr="//ul[@id='comments-list']" position="before">
<section class="mb32 read_width css_editable_mode_hidden">
@ -307,7 +307,7 @@
<!-- Options: Blog Post: user can select text for tweet -->
<template id="opt_blog_post_select_to_tweet" name="Select to Tweet"
inherit_option_id="website_blog.blog_post_complete">
inherit_id="website_blog.blog_post_complete" optional="disabled">
<xpath expr="//div[@id='blog_content']" position="attributes">
<attribute name="class">js_tweet mt32</attribute>
</xpath>
@ -318,7 +318,7 @@
<!-- Options: Blog Post: user can add Inline Discussion -->
<template id="opt_blog_post_inline_discussion" name="Allow comment in text"
inherit_option_id="website_blog.blog_post_complete">
inherit_id="website_blog.blog_post_complete" optional="disabled">
<xpath expr="//div[@id='blog_content']" position="attributes">
<attribute name="enable_chatter_discuss">True</attribute>
</xpath>
@ -326,7 +326,7 @@
<!-- Options: Blog Post: show tags -->
<template id="opt_blog_post_complete_tags" name="Tags"
inherit_option_id="website_blog.blog_post_complete" inherit_id="website_blog.blog_post_complete">
optional="enabled" inherit_id="website_blog.blog_post_complete">
<xpath expr="//p[@name='blog_post_data']" position="after">
<p class="post-meta text-muted text-center" t-if="len(blog_post.tag_ids)">
<span class="fa fa-tags"/>
@ -355,7 +355,7 @@
<!-- Option:Right Column for extra info -->
<template id="index_right" name="Right Column"
inherit_option_id="website_blog.blog_post_short">
inherit_id="website_blog.blog_post_short" optional="disabled">
<xpath expr="//div[@id='main_column']" position="attributes">
<attribute name="class">col-sm-8</attribute>
</xpath>
@ -366,7 +366,7 @@
<!-- Option:Right Column: tags -->
<template id="opt_blog_rc_tags" name="Tags"
inherit_option_id="website_blog.index_right">
inherit_id="website_blog.index_right" optional="disabled">
<xpath expr="//div[@id='blog_right_column']" position="inside">
<section class="mt32">
<h4>Tags</h4>
@ -383,7 +383,7 @@
<!-- Option:Right Column: archives -->
<template id="opt_blog_rc_history" name="Archives"
inherit_option_id="website_blog.index_right">
inherit_id="website_blog.index_right" optional="disabled">
<xpath expr="//div[@id='blog_right_column']" position="inside">
<section class="mt32">
<h4>Archives</h4>
@ -400,7 +400,7 @@
<!-- Option:Right Column: about us -->
<template id="opt_blog_rc_about_us" name="About Us" priority="2"
inherit_option_id="website_blog.index_right">
inherit_id="website_blog.index_right" optional="disabled">
<xpath expr="//div[@id='blog_right_column']" position="inside">
<section class="mt32">
<h4>About us</h4>
@ -417,7 +417,7 @@
<!-- Option:Right Column: follow us -->
<template id="opt_blog_rc_follow_us" name="Follow us" priority="4"
inherit_option_id="website_blog.index_right">
inherit_id="website_blog.index_right" optional="disabled">
<xpath expr="//div[@id='blog_right_column']" position="inside">
<section class="mt32">
<h4>Follow us<small t-if="blog">: <t t-esc="blog.name"/></small></h4>
@ -444,7 +444,7 @@
<!-- Option:Right Column: blogs -->
<template id="opt_blog_rc_blogs" name="Our Blogs" priority="6"
inherit_option_id="website_blog.index_right">
inherit_id="website_blog.index_right" optional="disabled">
<xpath expr="//div[@id='blog_right_column']" position="inside">
<section class="mt32 mb32">
<h4>Our Blogs</h4>

View File

@ -2,7 +2,7 @@
<openerp>
<data>
<template id="contactus_form" name="Contact Form" inherit_id="website.contactus" inherit_option_id="website.contactus">
<template id="contactus_form" name="Contact Form" inherit_id="website.contactus" optional="enabled">
<xpath expr="//div[@name='mail_button']" position="replace">
<form action="/crm/contactus" method="post" class="form-horizontal mt32" enctype="multipart/form-data">
<t t-foreach="kwargs" t-as="kwarg">
@ -47,7 +47,7 @@
</xpath>
</template>
<template id="contactus_form_company_name" name="Company Name" inherit_id="website_crm.contactus_form" inherit_option_id="website_crm.contactus_form">
<template id="contactus_form_company_name" name="Company Name" inherit_id="website_crm.contactus_form" optional="enabled">
<xpath expr="//div[@name='email_from_container']" position="after">
<div t-attf-class="form-group #{error and 'partner_name' in error and 'has-error' or ''}">
<label class="col-md-3 col-sm-4 control-label" for="partner_name">Your Company</label>

View File

@ -113,7 +113,7 @@
</t>
</template>
<template id="ref_country" inherit_id="website_crm_partner_assign.index" inherit_option_id="website_crm_partner_assign.index" name="Left World Map">
<template id="ref_country" inherit_id="website_crm_partner_assign.index" optional="enabled" name="Left World Map">
<xpath expr="//ul[@id='reseller_countries']" position="after">
<h3>World Map</h3>
<ul class="nav">

View File

@ -66,7 +66,7 @@
</template>
<!-- Option: left column: World Map -->
<template id="opt_country" inherit_option_id="website_customer.index" name="Show Map">
<template id="opt_country" inherit_id="website_customer.index" optional="disabled" name="Show Map">
<xpath expr="//div[@id='ref_left_column']" position="inside">
<iframe t-attf-src="/google_map/?partner_ids=#{ google_map_partner_ids }&amp;partner_url=/customers/&amp;output=embed"
@ -74,7 +74,7 @@
</xpath>
</template>
<template id="opt_country_list" inherit_id="website_customer.index" inherit_option_id="website_customer.index" name="Filter on Countries">
<template id="opt_country_list" inherit_id="website_customer.index" optional="enabled" name="Filter on Countries">
<xpath expr="//div[@id='ref_left_column']" position="inside">
<h3>References by Country</h3>
<ul class="nav nav-pills nav-stacked mt16 mb32">

View File

@ -87,7 +87,7 @@
</t>
</template>
<template id="event_right_photos" inherit_option_id="website_event.index" name="Photos">
<template id="event_right_photos" inherit_id="website_event.index" optional="disabled" name="Photos">
<xpath expr="//div[@id='right_column']" position="inside">
<div class="row">
<div class="col-md-12 mb16">
@ -106,7 +106,7 @@
</xpath>
</template>
<template id="event_right_quotes" inherit_option_id="website_event.index" name="Quotes">
<template id="event_right_quotes" inherit_id="website_event.index" optional="disabled" name="Quotes">
<xpath expr="//div[@id='right_column']" position="inside">
<div class="row">
<div class="col-md-12 mb16">
@ -123,7 +123,7 @@
</xpath>
</template>
<template id="event_right_country_event" inherit_option_id="website_event.index" name="Country Events">
<template id="event_right_country_event" inherit_id="website_event.index" optional="disabled" name="Country Events">
<xpath expr="//div[@id='right_column']" position="inside">
<div class="row">
<div class="col-md-12 mb16 mt16 country_events">
@ -140,7 +140,7 @@
</xpath>
</template>
<template id="event_left_column" inherit_option_id="website_event.index" inherit_id="website_event.index" name="Filters">
<template id="event_left_column" optional="enabled" inherit_id="website_event.index" name="Filters">
<xpath expr="//div[@id='middle_column']" position="attributes">
<attribute name="class">col-md-6</attribute>
</xpath>
@ -159,7 +159,7 @@
</xpath>
</template>
<template id="event_category" inherit_option_id="website_event.event_left_column" name="Filter by Category">
<template id="event_category" inherit_id="website_event.event_left_column" optional="disabled" name="Filter by Category">
<xpath expr="//div[@id='left_column']" position="inside">
<ul class="nav nav-pills nav-stacked mt32">
<t t-foreach="types">
@ -173,7 +173,7 @@
</xpath>
</template>
<template id="event_location" inherit_option_id="website_event.event_left_column" name="Filter by Country">
<template id="event_location" inherit_id="website_event.event_left_column" optional="disabled" name="Filter by Country">
<xpath expr="//div[@id='left_column']" position="inside">
<ul class="nav nav-pills nav-stacked mt32">
<t t-foreach="countries">

View File

@ -53,7 +53,7 @@
{
title: "Pay Now",
waitFor: '#payment_method label:has(input:checked):has(img[title="Wire Transfer"])',
element: '.oe_sale_acquirer_button .btn[name="submit"]:visible',
element: '.oe_sale_acquirer_button .btn[type="submit"]:visible',
},
{
title: "finish",

View File

@ -26,7 +26,7 @@
</xpath>
</template>
<template id="event_description_full" inherit_id="website_event.event_description_full" inherit_option_id="website_event.event_description_full" name="Event's Ticket form">
<template id="event_description_full" inherit_id="website_event.event_description_full" optional="enabled" name="Event's Ticket form">
<xpath expr="//div[@t-field='event.description']" position="before">
<form t-attf-action="/event/cart/update?event_id=#{ event.id }" method="post" t-if="event.event_ticket_ids">
<table itemprop="offers" class="table table-striped">

View File

@ -2,7 +2,7 @@
<openerp>
<data>
<template name="Sponsors" id="event_sponsor" inherit_option_id="website_event.layout" inherit_id="website_event.layout">
<template name="Sponsors" id="event_sponsor" optional="enabled" inherit_id="website_event.layout">
<xpath expr="//t[@t-call='website.layout']" position="inside">
<t t-set="head">
<link rel='stylesheet' href='/website_event_track/static/src/css/website_event_track.css'/>
@ -157,7 +157,7 @@
</t>
</template>
<template id="tracks_filter" inherit_id="website_event_track.tracks" inherit_option_id="website_event_track.tracks" name="Filter on Tags">
<template id="tracks_filter" inherit_id="website_event_track.tracks" optional="enabled" name="Filter on Tags">
<xpath expr="//div[@id='left_column']" position="inside">
<ul class="nav nav-pills nav-stacked">
<li t-att-class="'' if searches.get('tag') else 'active'"><a t-attf-href="/event/#{ slug(event) }/track">All Tags</a></li>
@ -246,7 +246,7 @@
</t>
</template>
<template id="event_track_social" name="Social Widgets" inherit_option_id="website_event_track.track_view">
<template id="event_track_social" name="Social Widgets" inherit_id="website_event_track.track_view" optional="disabled">
<xpath expr="//div[@id='right_column']" position="inside">
<div class="panel panel-default">
<div class="panel-heading">

View File

@ -449,7 +449,10 @@ class WebsiteForum(http.Controller):
pager = request.website.pager(url="/forum/%s/users" % slug(forum), total=tag_count, page=page, step=step, scope=30)
obj_ids = User.search(cr, SUPERUSER_ID, [('karma', '>', 1)], limit=step, offset=pager['offset'], order='karma DESC', context=context)
users = User.browse(cr, SUPERUSER_ID, obj_ids, context=context)
# put the users in block of 3 to display them as a table
users = [[] for i in range(len(obj_ids)/3+1)]
for index, user in enumerate(User.browse(cr, SUPERUSER_ID, obj_ids, context=context)):
users[index/3].append(user)
searches['users'] = 'True'
values = self._prepare_forum_values(forum=forum, searches=searches)

View File

@ -725,11 +725,8 @@
<template id="users">
<t t-call="website_forum.header">
<t t-foreach="users" t-as="user">
<t t-if="user_index % 3 == 0">
<div class="row"></div>
</t>
<div class="col-sm-4">
<div t-foreach="users" t-as="row_users" class="row mt16">
<div t-foreach="row_users" t-as="user" class="col-sm-4">
<img class="pull-left img img-circle img-avatar" t-attf-src="/forum/user/#{user.id}/avatar"/>
<div>
<a t-attf-href="/forum/#{slug(forum)}/user/#{user.id}" t-field="user.name"/>
@ -750,7 +747,7 @@
<t t-raw="0"/>
</div>
</div>
</t>
</div>
<div class="pull-left">
<t t-call="website.pager"/>
</div>

View File

@ -3,7 +3,7 @@
<data>
<!-- Page -->
<template id="aboutus" inherit_id="website.aboutus" inherit_option_id="website.aboutus" name="Our Team">
<template id="aboutus" inherit_id="website.aboutus" optional="enabled" name="Our Team">
<xpath expr="//div[@class='oe_structure']" position="after">
<section class="container">
<div class="col-sm-12 text-center" t-if="len(employee_ids)">

View File

@ -230,7 +230,7 @@
</t>
</template>
<template id="job_departments" inherit_option_id="website_hr_recruitment.index" name="Filter by Departments">
<template id="job_departments" inherit_id="website_hr_recruitment.index" optional="disabled" name="Filter by Departments">
<xpath expr="//div[@id='jobs_grid_left']" position="inside">
<ul class="nav nav-pills nav-stacked mb32">
<li t-att-class=" '' if department_id else 'active' "><a href="/jobs">All Departments</a></li>
@ -249,7 +249,7 @@
</xpath>
</template>
<template id="job_offices" inherit_option_id="website_hr_recruitment.index" name="Filter by Offices">
<template id="job_offices" inherit_id="website_hr_recruitment.index" optional="disabled" name="Filter by Offices">
<xpath expr="//div[@id='jobs_grid_left']" position="inside">
<ul class="nav nav-pills nav-stacked mb32">
<li t-att-class=" '' if office_id else 'active' "><a href="/jobs">All Offices</a></li>

View File

@ -84,7 +84,7 @@
</template>
<template id="opt_index_country" name="Location"
inherit_option_id="website_membership.index" inherit_id="website_membership.index">
optional="enabled" inherit_id="website_membership.index">
<xpath expr="//div[@id='left_column']/ul[last()]" position="after">
<ul class="nav nav-pills nav-stacked mt16">
<li class="nav-header"><h3>Location</h3></li>
@ -101,7 +101,7 @@
<!-- Option: index: Left Google Map -->
<template id="opt_index_google_map" name="Left World Map"
inherit_option_id="website_membership.index" inherit_id="website_membership.index">
optional="enabled" inherit_id="website_membership.index">
<xpath expr="//div[@id='left_column']/ul[1]" position="before">
<ul class="nav nav-pills nav-stacked mt16">
<li class="nav-header"><h3>World Map</h3></li>

View File

@ -92,7 +92,7 @@
</section>
</template>
<template id="change_quantity" inherit_option_id="website_quote.pricing" name="Change Quantity">
<template id="change_quantity" inherit_id="website_quote.pricing" optional="disabled" name="Change Quantity">
<xpath expr="//div[@id='quote_qty']" position="replace">
<div class="input-group">
<span class="input-group-addon hidden-print">
@ -130,7 +130,7 @@
</template>
<!-- Options:Quotation Chatter: user can reply -->
<template id="opt_quotation_chatter_post_complete_comment" name="Allow Comments" inherit_option_id="website_quote.chatter" inherit_id="website_quote.chatter">
<template id="opt_quotation_chatter_post_complete_comment" name="Allow Comments" optional="enabled" inherit_id="website_quote.chatter">
<xpath expr="//h1" position="after">
<section class="mb32 css_editable_mode_hidden hidden-print">
<form id="comment" t-attf-action="/quote/#{quotation.id}/#{quotation.access_token}/post" method="POST">
@ -388,7 +388,7 @@
</template>
<!-- Options:Quotation Signature -->
<template id="opt_quotation_signature" name="Ask Signature" inherit_option_id="website_quote.so_quotation" inherit_id="website_quote.so_quotation">
<template id="opt_quotation_signature" name="Ask Signature" optional="enabled" inherit_id="website_quote.so_quotation">
<xpath expr="//div[@id='sign-dialog']" position="inside">
<div class="panel panel-default mt16 mb0" id="drawsign">
<div class="panel-heading">

View File

@ -509,7 +509,6 @@ class website_sale(http.Controller):
values['acquirers'] = payment_obj.browse(cr, uid, acquirer_ids, context=context)
render_ctx = dict(context, submit_class='btn btn-primary', submit_txt='Pay Now')
for acquirer in values['acquirers']:
render_ctx['tx_url'] = '/shop/payment/transaction/%s' % acquirer.id
acquirer.button = payment_obj.render(
cr, SUPERUSER_ID, acquirer.id,
order.name,
@ -523,20 +522,17 @@ class website_sale(http.Controller):
return request.website.render("website_sale.payment", values)
@http.route(['/shop/payment/transaction/<int:acquirer_id>'], type='http', methods=['POST'], auth="public", website=True)
def payment_transaction(self, acquirer_id, **post):
""" Hook method that creates a payment.transaction and redirect to the
acquirer, using post values to re-create the post action.
@http.route(['/shop/payment/transaction/<int:acquirer_id>'], type='json', auth="public", website=True)
def payment_transaction(self, acquirer_id):
""" Json method that creates a payment.transaction, used to create a
transaction when the user clicks on 'pay now' button. After having
created the transaction, the event continues and the user is redirected
to the acquirer website.
:param int acquirer_id: id of a payment.acquirer record. If not set the
user is redirected to the checkout page
:param dict post: should coutain all post data for the acquirer
"""
# @TDEFIXME: don't know why we received those data, but should not be send to the acquirer
post.pop('submit.x', None)
post.pop('submit.y', None)
cr, uid, context = request.cr, request.uid, request.context
payment_obj = request.registry.get('payment.acquirer')
transaction_obj = request.registry.get('payment.transaction')
sale_order_obj = request.registry['sale.order']
order = request.website.sale_get_order(context=context)
@ -548,7 +544,13 @@ class website_sale(http.Controller):
# find an already existing transaction
tx = request.website.sale_get_transaction()
if not tx:
if tx:
if tx.state == 'draft': # button cliked but no more info -> rewrite on tx or create a new one ?
tx.write({
'acquirer_id': acquirer_id,
})
tx_id = tx.id
else:
tx_id = transaction_obj.create(cr, SUPERUSER_ID, {
'acquirer_id': acquirer_id,
'type': 'form',
@ -560,10 +562,6 @@ class website_sale(http.Controller):
'sale_order_id': order.id,
}, context=context)
request.session['sale_transaction_id'] = tx_id
elif tx and tx.state == 'draft': # button cliked but no more info -> rewrite on tx or create a new one ?
tx.write({
'acquirer_id': acquirer_id,
})
# update quotation
sale_order_obj.write(
@ -574,9 +572,7 @@ class website_sale(http.Controller):
# confirm the quotation
sale_order_obj.action_button_confirm(cr, SUPERUSER_ID, [order.id], context=request.context)
acquirer_form_post_url = payment_obj.get_form_action_url(cr, uid, acquirer_id, context=context)
acquirer_total_url = '%s?%s' % (acquirer_form_post_url, werkzeug.url_encode(post))
return request.redirect(acquirer_total_url)
return tx_id
@http.route('/shop/payment/get_status/<int:sale_order_id>', type='json', auth="public", website=True)
def payment_get_status(self, sale_order_id, **post):

View File

@ -113,7 +113,7 @@
{
title: "Pay Now",
waitFor: '#payment_method label:has(input:checked):has(img[title="Wire Transfer"])',
element: '.oe_sale_acquirer_button .btn[name="submit"]:visible',
element: '.oe_sale_acquirer_button .btn[type="submit"]:visible',
},
{
title: "finish",

View File

@ -9,4 +9,18 @@ $(document).ready(function () {
})
.find("input[name='acquirer']:checked").click();
// When clicking on payment button: create the tx using json then continue to the acquirer
$('button[type="submit"]').on("click", function (ev) {
ev.preventDefault();
ev.stopPropagation();
var $form = $(ev.currentTarget).parents('form');
var acquirer_id = $(ev.currentTarget).parents('div.oe_sale_acquirer_button').first().data('id');
if (! acquirer_id) {
return false;
}
openerp.jsonRpc('/shop/payment/transaction/' + acquirer_id, 'call', {}).then(function (data) {
$form.submit();
});
});
});

View File

@ -89,7 +89,7 @@
</div>
</template>
<template id="products_description" inherit_option_id="website_sale.products_item" name="Product Description">
<template id="products_description" inherit_id="website_sale.products_item" optional="disabled" name="Product Description">
<xpath expr="//div[@class='product_price']" position="before">
<div class="text-info oe_subdescription" contenteditable="false">
<div itemprop="description" t-field="product.description_sale"></div>
@ -97,7 +97,7 @@
</xpath>
</template>
<template id="products_add_to_cart" inherit_option_id="website_sale.products_item" name="Add to Cart">
<template id="products_add_to_cart" inherit_id="website_sale.products_item" optional="disabled" name="Add to Cart">
<xpath expr="//div[@class='product_price']" position="inside">
<form action="/shop/cart/update" method="post" style="display: inline-block;">
<input name="product_id" t-att-value="product.product_variant_ids[0].id" type="hidden"/>
@ -250,7 +250,7 @@
</li>
</template>
<template id="products_categories" inherit_option_id="website_sale.products" name="Product Categories">
<template id="products_categories" inherit_id="website_sale.products" optional="disabled" name="Product Categories">
<xpath expr="//div[@id='products_grid_before']" position="inside">
<ul class="nav nav-pills nav-stacked mt16">
<li t-att-class=" '' if category else 'active' "><a t-att-href="keep('/shop',category=0)">All Products</a></li>
@ -267,7 +267,7 @@
</xpath>
</template>
<template id="products_attributes" inherit_option_id="website_sale.products" name="Product Attribute's Filters">
<template id="products_attributes" inherit_id="website_sale.products" optional="disabled" name="Product Attribute's Filters">
<xpath expr="//div[@id='products_grid_before']" position="inside">
<form class="js_attributes" method="get">
<input type="hidden" name="search" t-att-value="search"/>
@ -320,7 +320,7 @@
</xpath>
</template>
<template id="products_list_view" inherit_option_id="website_sale.products" name="List View">
<template id="products_list_view" inherit_id="website_sale.products" optional="disabled" name="List View">
<xpath expr="//div[@id='products_grid']//table" position="replace">
<t t-foreach="products" t-as="product">
<div class="oe_product oe_list oe_product_cart" t-att-data-publish="product.website_published and 'on' or 'off'">
@ -505,7 +505,7 @@
</xpath>
</template>
<template id="recommended_products" inherit_id="website_sale.product" inherit_option_id="website_sale.product" name="Alternative Products">
<template id="recommended_products" inherit_id="website_sale.product" optional="enabled" name="Alternative Products">
<xpath expr="//div[@id='product_full_description']" position="after">
<div class="container mt32" t-if="product.alternative_product_ids">
<h3>Suggested alternatives:</h3>
@ -527,7 +527,7 @@
</xpath>
</template>
<template id="product_attributes" inherit_id="website_sale.product" inherit_option_id="website_sale.product" name="Product attributes">
<template id="product_attributes" inherit_id="website_sale.product" optional="enabled" name="Product attributes">
<xpath expr="//p[@t-field='product.description_sale']" position="after">
<hr t-if="product.variant_ids"/>
<p class="text-muted">
@ -543,7 +543,7 @@
<!-- Product options: OpenChatter -->
<template id="product_comment" inherit_option_id="website_sale.product" name="Discussion">
<template id="product_comment" inherit_id="website_sale.product" optional="disabled" name="Discussion">
<xpath expr="//div[@t-field='product.website_description']" position="after">
<hr class="mb32"/>
<section class="container">
@ -709,7 +709,7 @@
</t>
</template>
<template id="suggested_products_list" inherit_id="website_sale.cart" inherit_option_id="website_sale.cart" name="Suggested Products in my cart">
<template id="suggested_products_list" inherit_id="website_sale.cart" optional="enabled" name="Suggested Products in my cart">
<xpath expr="//table[@id='cart_products']" position="after">
<table t-if="suggested_products" class='table table-striped table-condensed'>
<colgroup>
@ -765,13 +765,13 @@
</xpath>
</template>
<template id="continue_shopping" inherit_id="website_sale.cart" inherit_option_id="website_sale.cart" name="Continue Shopping Button">
<template id="continue_shopping" inherit_id="website_sale.cart" optional="enabled" name="Continue Shopping Button">
<xpath expr="//a[@href='/shop/checkout']" position="before">
<a href="/shop" class="btn btn-default mb32"><span class="fa fa-long-arrow-left"/> Continue Shopping</a>
</xpath>
</template>
<template id="reduction_code" inherit_option_id="website_sale.cart" name="Reduction Code">
<template id="reduction_code" inherit_id="website_sale.cart" optional="disabled" name="Reduction Code">
<xpath expr="//div[@id='right_column']" position="inside">
<h4>Coupon Code</h4>
<p>

68
contributing.md Normal file
View File

@ -0,0 +1,68 @@
Contributing to Odoo
====================
Reporting Issues
----------------
If possible, always attach a pull request when creating an issue (GitHub will automatically create an issue when submitting the changes). The issues not linked to a pull request or an internal request on odoo.com will be handled with a lower priority.
If later on you create a pull request solving an opened issue, do not forget to reference to it in your pull request (e.g.: "This patch fixes issue #42").
When reporting an issue or creating a pull request, use the following structure:
> **Quantity field is ignored in a sale order**
>
> Impacted versions:
>
> - 7.0 and above
>
> Steps to reproduce:
>
> 1. create a new sale order
> 2. add a line with product 'Service', quantity 2, unit price 10.0
> 3. validate the sale order
>
> Current behavior:
>
> - Total price of 10.0
>
> Expected behavior:
>
> - Total price of 20.0 (2 * 10 = 20)
For web or rendering issues, do not forget to specify the operating system and browser you are using.
Against which version should I submit a patch?
----------------------------------------------
Periodically, we forward port the fixes realized in the latest stable version to master and intermediate saas repositories. This means that you should submit your pull request against the lowest supported version. If applying, you should always submit your code against `odoo/7.0`. The `saas-x` versions are intermediate versions between stable versions.
![Submiting against the right version](/doc/_static/pull-request-version.png)
However your change **must** be submitted against `odoo/master` if
* it modifies the database structure (e.g.: adding a new field)
* it adds a new feature
Why was my fix labeled as blocked?
----------------------------------
The label *blocked* is used when an action is required from the submitter. The typical reasons are:
* the fix is incomplete/incorrect and a change is required
* more information is required
Pull requests with the blocked label will not be processed as long as the label remains. Once the correction done, we will review it and eventually remove the label.
Why was my issue closed without merging?
----------------------------------------
A pull request is closed when it will not be merged into odoo. This will typically happens if the fix/issue:
* is not relevant to odoo development (label *invalid*)
* is not considered as a bug or we have no plan to change the current behavior (label *wontfix*)
* is a duplicated of another opened issue (label *duplicate*)
* the pull request should be resubmitted against another version
What is this odoo-dev repository? Should I use it?
--------------------------------------------------
The `odoo-dev/odoo` repository is an internal repository used by the R&D of Odoo to keep the main repository clean. If you are coming from Launchpad, this is the equivalent of the `~openerp-dev` repository.
When forking odoo to submit a patch, always use the `github.com/odoo/odoo` repository. Be also careful of the version you are branching as it will determine the history once the pull request will be realized (e.g.: `git checkout -b 7.0-my-branch odoo/7.0`).

BIN
doc/_static/pull-request-version.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -63,7 +63,6 @@ The kernel of OpenERP, needed for all installation.
'module/module_view.xml',
'module/module_data.xml',
'module/module_report.xml',
'module/wizard/base_module_import_view.xml',
'module/wizard/base_module_update_view.xml',
'module/wizard/base_language_install_view.xml',
'module/wizard/base_import_language_view.xml',

View File

@ -612,7 +612,7 @@ class FloatConverter(osv.AbstractModel):
# it to switch to scientific notation starting at a million *and* to
# strip decimals. So use %f and if no precision was specified manually
# strip trailing 0.
if not precision:
if precision is None:
formatted = re.sub(r'(?:(0|\d+?)0+)$', r'\1', formatted)
return formatted

View File

@ -141,8 +141,35 @@ class view(osv.osv):
'model_ids': fields.one2many('ir.model.data', 'res_id', domain=[('model','=','ir.ui.view')], auto_join=True),
'create_date': fields.datetime('Create Date', readonly=True),
'write_date': fields.datetime('Last Modification Date', readonly=True),
'mode': fields.selection(
[('primary', "Base view"), ('extension', "Extension View")],
string="View inheritance mode", required=True,
help="""Only applies if this view inherits from an other one (inherit_id is not False/Null).
* if extension (default), if this view is requested the closest primary view
is looked up (via inherit_id), then all views inheriting from it with this
view's model are applied
* if primary, the closest primary view is fully resolved (even if it uses a
different model than this one), then this view's inheritance specs
(<xpath/>) are applied, and the result is used as if it were this view's
actual arch.
"""),
'application': fields.selection([
('always', "Always applied"),
('enabled', "Optional, enabled"),
('disabled', "Optional, disabled"),
],
required=True, string="Application status",
help="""If this view is inherited,
* if always, the view always extends its parent
* if enabled, the view currently extends its parent but can be disabled
* if disabled, the view currently does not extend its parent but can be enabled
"""),
}
_defaults = {
'mode': 'primary',
'application': 'always',
'priority': 16,
}
_order = "priority,name"
@ -191,8 +218,14 @@ class view(osv.osv):
return False
return True
_sql_constraints = [
('inheritance_mode',
"CHECK (mode != 'extension' OR inherit_id IS NOT NULL)",
"Invalid inheritance mode: if the mode is 'extension', the view must"
" extend an other view"),
]
_constraints = [
(_check_xml, 'Invalid view definition', ['arch'])
(_check_xml, 'Invalid view definition', ['arch']),
]
def _auto_init(self, cr, context=None):
@ -201,6 +234,12 @@ class view(osv.osv):
if not cr.fetchone():
cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)')
def _compute_defaults(self, cr, uid, values, context=None):
if 'inherit_id' in values:
values.setdefault(
'mode', 'extension' if values['inherit_id'] else 'primary')
return values
def create(self, cr, uid, values, context=None):
if 'type' not in values:
if values.get('inherit_id'):
@ -209,10 +248,13 @@ class view(osv.osv):
values['type'] = etree.fromstring(values['arch']).tag
if not values.get('name'):
values['name'] = "%s %s" % (values['model'], values['type'])
values['name'] = "%s %s" % (values.get('model'), values['type'])
self.read_template.clear_cache(self)
return super(view, self).create(cr, uid, values, context)
return super(view, self).create(
cr, uid,
self._compute_defaults(cr, uid, values, context=context),
context=context)
def write(self, cr, uid, ids, vals, context=None):
if not isinstance(ids, (list, tuple)):
@ -226,10 +268,36 @@ class view(osv.osv):
if custom_view_ids:
self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids)
if vals.get('application') == 'disabled':
from_always = self.search(
cr, uid, [('id', 'in', ids), ('application', '=', 'always')], context=context)
if from_always:
raise ValueError(
"Can't disable views %s marked as always applied" % (
', '.join(map(str, from_always))))
self.read_template.clear_cache(self)
ret = super(view, self).write(cr, uid, ids, vals, context)
ret = super(view, self).write(
cr, uid, ids,
self._compute_defaults(cr, uid, vals, context=context),
context)
return ret
def toggle(self, cr, uid, ids, context=None):
""" Switches between enabled and disabled application statuses
"""
for view in self.browse(cr, uid, ids, context=context):
if view.application == 'enabled':
view.write({'application': 'disabled'})
elif view.application == 'disabled':
view.write({'application': 'enabled'})
else:
raise ValueError(_("Can't toggle view %d with application %r") % (
view.id,
view.application,
))
def copy(self, cr, uid, id, default=None, context=None):
if not default:
default = {}
@ -241,7 +309,7 @@ class view(osv.osv):
# default view selection
def default_view(self, cr, uid, model, view_type, context=None):
""" Fetches the default view for the provided (model, view_type) pair:
view with no parent (inherit_id=Fase) with the lowest priority.
primary view with the lowest priority.
:param str model:
:param int view_type:
@ -251,7 +319,7 @@ class view(osv.osv):
domain = [
['model', '=', model],
['type', '=', view_type],
['inherit_id', '=', False],
['mode', '=', 'primary'],
]
ids = self.search(cr, uid, domain, limit=1, context=context)
if not ids:
@ -278,15 +346,19 @@ class view(osv.osv):
user = self.pool['res.users'].browse(cr, 1, uid, context=context)
user_groups = frozenset(user.groups_id or ())
check_view_ids = context and context.get('check_view_ids') or (0,)
conditions = [['inherit_id', '=', view_id], ['model', '=', model]]
conditions = [
['inherit_id', '=', view_id],
['model', '=', model],
['mode', '=', 'extension'],
['application', 'in', ['always', 'enabled']],
]
if self.pool._init:
# Module init currently in progress, only consider views from
# modules whose code is already loaded
conditions.extend([
'|',
['model_ids.module', 'in', tuple(self.pool._init_modules)],
['id', 'in', check_view_ids],
['id', 'in', context and context.get('check_view_ids') or (0,)],
])
view_ids = self.search(cr, uid, conditions, context=context)
@ -442,7 +514,7 @@ class view(osv.osv):
if context is None: context = {}
if root_id is None:
root_id = source_id
sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, uid, source_id, model, context=context)
sql_inherit = self.pool['ir.ui.view'].get_inheriting_views_arch(cr, uid, source_id, model, context=context)
for (specs, view_id) in sql_inherit:
specs_tree = etree.fromstring(specs.encode('utf-8'))
if context.get('inherit_branding'):
@ -465,7 +537,7 @@ class view(osv.osv):
# if view_id is not a root view, climb back to the top.
base = v = self.browse(cr, uid, view_id, context=context)
while v.inherit_id:
while v.mode != 'primary':
v = v.inherit_id
root_id = v.id
@ -475,7 +547,16 @@ class view(osv.osv):
# read the view arch
[view] = self.read(cr, uid, [root_id], fields=fields, context=context)
arch_tree = etree.fromstring(view['arch'].encode('utf-8'))
view_arch = etree.fromstring(view['arch'].encode('utf-8'))
if not v.inherit_id:
arch_tree = view_arch
else:
parent_view = self.read_combined(
cr, uid, v.inherit_id.id, fields=fields, context=context)
arch_tree = etree.fromstring(parent_view['arch'])
self.apply_inheritance_specs(
cr, uid, arch_tree, view_arch, parent_view['id'], context=context)
if context.get('inherit_branding'):
arch_tree.attrib.update({

View File

@ -19,7 +19,6 @@
#
##############################################################################
import base_module_import
import base_module_update
import base_language_install
import base_import_language

View File

@ -1,63 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import base64
import os
from StringIO import StringIO
import zipfile
from openerp import tools
from openerp.osv import osv, fields
from openerp.tools.translate import _
class base_module_import(osv.osv_memory):
""" Import Module """
_name = "base.module.import"
_description = "Import Module"
_columns = {
'module_file': fields.binary('Module .ZIP file', required=True),
'state':fields.selection([('init','init'),('done','done')],
'Status', readonly=True),
'module_name': fields.char('Module Name', size=128),
}
_defaults = {
'state': 'init',
}
def importzip(self, cr, uid, ids, context):
#TODO: drop this model and the corresponding view/action in trunk
raise NotImplementedError('This feature is not available')
def action_module_open(self, cr, uid, ids, context):
(data,) = self.browse(cr, uid, ids , context=context)
return {
'domain': str([('name', '=', data.module_name)]),
'name': 'Modules',
'view_type': 'form',
'view_mode': 'tree,form',
'res_model': 'ir.module.module',
'type': 'ir.actions.act_window',
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -1,12 +1,16 @@
# -*- encoding: utf-8 -*-
from functools import partial
import itertools
import unittest2
from lxml import etree as ET
from lxml.builder import E
from psycopg2 import IntegrityError
from openerp.tests import common
import openerp.tools
Field = E.field
@ -14,9 +18,22 @@ class ViewCase(common.TransactionCase):
def setUp(self):
super(ViewCase, self).setUp()
self.addTypeEqualityFunc(ET._Element, self.assertTreesEqual)
self.Views = self.registry('ir.ui.view')
def browse(self, id, context=None):
return self.Views.browse(self.cr, self.uid, id, context=context)
def create(self, value, context=None):
return self.Views.create(self.cr, self.uid, value, context=context)
def read_combined(self, id):
return self.Views.read_combined(
self.cr, self.uid,
id, ['arch'],
context={'check_view_ids': self.Views.search(self.cr, self.uid, [])}
)
def assertTreesEqual(self, n1, n2, msg=None):
self.assertEqual(n1.tag, n2.tag)
self.assertEqual(n1.tag, n2.tag, msg)
self.assertEqual((n1.text or '').strip(), (n2.text or '').strip(), msg)
self.assertEqual((n1.tail or '').strip(), (n2.tail or '').strip(), msg)
@ -24,8 +41,8 @@ class ViewCase(common.TransactionCase):
# equality (!?!?!?!)
self.assertEqual(dict(n1.attrib), dict(n2.attrib), msg)
for c1, c2 in zip(n1, n2):
self.assertTreesEqual(c1, c2, msg)
for c1, c2 in itertools.izip_longest(n1, n2):
self.assertEqual(c1, c2, msg)
class TestNodeLocator(common.TransactionCase):
@ -374,13 +391,6 @@ class TestApplyInheritedArchs(ViewCase):
""" Applies a sequence of modificator archs to a base view
"""
class TestViewCombined(ViewCase):
"""
Test fallback operations of View.read_combined:
* defaults mapping
* ?
"""
class TestNoModel(ViewCase):
def test_create_view_nomodel(self):
View = self.registry('ir.ui.view')
@ -628,6 +638,8 @@ class test_views(ViewCase):
def _insert_view(self, **kw):
"""Insert view into database via a query to passtrough validation"""
kw.pop('id', None)
kw.setdefault('mode', 'extension' if kw.get('inherit_id') else 'primary')
kw.setdefault('application', 'always')
keys = sorted(kw.keys())
fields = ','.join('"%s"' % (k.replace('"', r'\"'),) for k in keys)
@ -805,6 +817,354 @@ class test_views(ViewCase):
string="Replacement title", version="7.0"
))
class ViewModeField(ViewCase):
"""
This should probably, eventually, be folded back into other test case
classes, integrating the test (or not) of the mode field to regular cases
"""
def testModeImplicitValue(self):
""" mode is auto-generated from inherit_id:
* inherit_id -> mode=extension
* not inherit_id -> mode=primary
"""
view = self.browse(self.create({
'inherit_id': None,
'arch': '<qweb/>'
}))
self.assertEqual(view.mode, 'primary')
view2 = self.browse(self.create({
'inherit_id': view.id,
'arch': '<qweb/>'
}))
self.assertEqual(view2.mode, 'extension')
@openerp.tools.mute_logger('openerp.sql_db')
def testModeExplicit(self):
view = self.browse(self.create({
'inherit_id': None,
'arch': '<qweb/>'
}))
view2 = self.browse(self.create({
'inherit_id': view.id,
'mode': 'primary',
'arch': '<qweb/>'
}))
self.assertEqual(view.mode, 'primary')
with self.assertRaises(IntegrityError):
self.create({
'inherit_id': None,
'mode': 'extension',
'arch': '<qweb/>'
})
@openerp.tools.mute_logger('openerp.sql_db')
def testPurePrimaryToExtension(self):
"""
A primary view with inherit_id=None can't be converted to extension
"""
view_pure_primary = self.browse(self.create({
'inherit_id': None,
'arch': '<qweb/>'
}))
with self.assertRaises(IntegrityError):
view_pure_primary.write({'mode': 'extension'})
def testInheritPrimaryToExtension(self):
"""
A primary view with an inherit_id can be converted to extension
"""
base = self.create({'inherit_id': None, 'arch': '<qweb/>'})
view = self.browse(self.create({
'inherit_id': base,
'mode': 'primary',
'arch': '<qweb/>'
}))
view.write({'mode': 'extension'})
def testDefaultExtensionToPrimary(self):
"""
An extension view can be converted to primary
"""
base = self.create({'inherit_id': None, 'arch': '<qweb/>'})
view = self.browse(self.create({
'inherit_id': base,
'arch': '<qweb/>'
}))
view.write({'mode': 'primary'})
class TestDefaultView(ViewCase):
def testDefaultViewBase(self):
self.create({
'inherit_id': False,
'priority': 10,
'mode': 'primary',
'arch': '<qweb/>',
})
v2 = self.create({
'inherit_id': False,
'priority': 1,
'mode': 'primary',
'arch': '<qweb/>',
})
default = self.Views.default_view(self.cr, self.uid, False, 'qweb')
self.assertEqual(
default, v2,
"default_view should get the view with the lowest priority for "
"a (model, view_type) pair"
)
def testDefaultViewPrimary(self):
v1 = self.create({
'inherit_id': False,
'priority': 10,
'mode': 'primary',
'arch': '<qweb/>',
})
self.create({
'inherit_id': False,
'priority': 5,
'mode': 'primary',
'arch': '<qweb/>',
})
v3 = self.create({
'inherit_id': v1,
'priority': 1,
'mode': 'primary',
'arch': '<qweb/>',
})
default = self.Views.default_view(self.cr, self.uid, False, 'qweb')
self.assertEqual(
default, v3,
"default_view should get the view with the lowest priority for "
"a (model, view_type) pair in all the primary tables"
)
class TestViewCombined(ViewCase):
"""
* When asked for a view, instead of looking for the closest parent with
inherit_id=False look for mode=primary
* If root.inherit_id, resolve the arch for root.inherit_id (?using which
model?), then apply root's inheritance specs to it
* Apply inheriting views on top
"""
def setUp(self):
super(TestViewCombined, self).setUp()
self.a1 = self.create({
'model': 'a',
'arch': '<qweb><a1/></qweb>'
})
self.a2 = self.create({
'model': 'a',
'inherit_id': self.a1,
'priority': 5,
'arch': '<xpath expr="//a1" position="after"><a2/></xpath>'
})
self.a3 = self.create({
'model': 'a',
'inherit_id': self.a1,
'arch': '<xpath expr="//a1" position="after"><a3/></xpath>'
})
# mode=primary should be an inheritance boundary in both direction,
# even within a model it should not extend the parent
self.a4 = self.create({
'model': 'a',
'inherit_id': self.a1,
'mode': 'primary',
'arch': '<xpath expr="//a1" position="after"><a4/></xpath>',
})
self.b1 = self.create({
'model': 'b',
'inherit_id': self.a3,
'mode': 'primary',
'arch': '<xpath expr="//a1" position="after"><b1/></xpath>'
})
self.b2 = self.create({
'model': 'b',
'inherit_id': self.b1,
'arch': '<xpath expr="//a1" position="after"><b2/></xpath>'
})
self.c1 = self.create({
'model': 'c',
'inherit_id': self.a1,
'mode': 'primary',
'arch': '<xpath expr="//a1" position="after"><c1/></xpath>'
})
self.c2 = self.create({
'model': 'c',
'inherit_id': self.c1,
'priority': 5,
'arch': '<xpath expr="//a1" position="after"><c2/></xpath>'
})
self.c3 = self.create({
'model': 'c',
'inherit_id': self.c2,
'priority': 10,
'arch': '<xpath expr="//a1" position="after"><c3/></xpath>'
})
self.d1 = self.create({
'model': 'd',
'inherit_id': self.b1,
'mode': 'primary',
'arch': '<xpath expr="//a1" position="after"><d1/></xpath>'
})
def test_basic_read(self):
arch = self.read_combined(self.a1)['arch']
self.assertEqual(
ET.fromstring(arch),
E.qweb(
E.a1(),
E.a3(),
E.a2(),
), arch)
def test_read_from_child(self):
arch = self.read_combined(self.a3)['arch']
self.assertEqual(
ET.fromstring(arch),
E.qweb(
E.a1(),
E.a3(),
E.a2(),
), arch)
def test_read_from_child_primary(self):
arch = self.read_combined(self.a4)['arch']
self.assertEqual(
ET.fromstring(arch),
E.qweb(
E.a1(),
E.a4(),
E.a3(),
E.a2(),
), arch)
def test_cross_model_simple(self):
arch = self.read_combined(self.c2)['arch']
self.assertEqual(
ET.fromstring(arch),
E.qweb(
E.a1(),
E.c3(),
E.c2(),
E.c1(),
E.a3(),
E.a2(),
), arch)
def test_cross_model_double(self):
arch = self.read_combined(self.d1)['arch']
self.assertEqual(
ET.fromstring(arch),
E.qweb(
E.a1(),
E.d1(),
E.b2(),
E.b1(),
E.a3(),
E.a2(),
), arch)
class TestOptionalViews(ViewCase):
"""
Tests ability to enable/disable inherited views, formerly known as
inherit_option_id
"""
def setUp(self):
super(TestOptionalViews, self).setUp()
self.v0 = self.create({
'model': 'a',
'arch': '<qweb><base/></qweb>',
})
self.v1 = self.create({
'model': 'a',
'inherit_id': self.v0,
'application': 'always',
'priority': 10,
'arch': '<xpath expr="//base" position="after"><v1/></xpath>',
})
self.v2 = self.create({
'model': 'a',
'inherit_id': self.v0,
'application': 'enabled',
'priority': 9,
'arch': '<xpath expr="//base" position="after"><v2/></xpath>',
})
self.v3 = self.create({
'model': 'a',
'inherit_id': self.v0,
'application': 'disabled',
'priority': 8,
'arch': '<xpath expr="//base" position="after"><v3/></xpath>'
})
def test_applied(self):
""" mandatory and enabled views should be applied
"""
arch = self.read_combined(self.v0)['arch']
self.assertEqual(
ET.fromstring(arch),
E.qweb(
E.base(),
E.v1(),
E.v2(),
)
)
def test_applied_state_toggle(self):
""" Change application states of v2 and v3, check that the results
are as expected
"""
self.browse(self.v2).write({'application': 'disabled'})
arch = self.read_combined(self.v0)['arch']
self.assertEqual(
ET.fromstring(arch),
E.qweb(
E.base(),
E.v1(),
)
)
self.browse(self.v3).write({'application': 'enabled'})
arch = self.read_combined(self.v0)['arch']
self.assertEqual(
ET.fromstring(arch),
E.qweb(
E.base(),
E.v1(),
E.v3(),
)
)
self.browse(self.v2).write({'application': 'enabled'})
arch = self.read_combined(self.v0)['arch']
self.assertEqual(
ET.fromstring(arch),
E.qweb(
E.base(),
E.v1(),
E.v2(),
E.v3(),
)
)
def test_mandatory_no_disabled(self):
with self.assertRaises(Exception):
self.browse(self.v1).write({'application': 'disabled'})
class TestXPathExtentions(common.BaseCase):
def test_hasclass(self):
tree = E.node(

View File

@ -217,9 +217,23 @@
<rng:optional><rng:attribute name="priority"/></rng:optional>
<rng:choice>
<rng:group>
<rng:optional><rng:attribute name="inherit_id"/></rng:optional>
<rng:optional><rng:attribute name="inherit_option_id"/></rng:optional>
<rng:optional>
<rng:attribute name="inherit_id"/>
<rng:optional>
<rng:attribute name="primary">
<rng:value>True</rng:value>
</rng:attribute>
</rng:optional>
</rng:optional>
<rng:optional><rng:attribute name="groups"/></rng:optional>
<rng:optional>
<rng:attribute name="optional">
<rng:choice>
<rng:value>enabled</rng:value>
<rng:value>disabled</rng:value>
</rng:choice>
</rng:attribute>
</rng:optional>
</rng:group>
<rng:optional>
<rng:attribute name="page"><rng:value>True</rng:value></rng:attribute>

View File

@ -729,7 +729,6 @@ class BaseModel(object):
_all_columns = {}
_table = None
_invalids = set()
_log_create = False
_sql_constraints = []
_protected = ['read', 'write', 'create', 'default_get', 'perm_read', 'unlink', 'fields_get', 'fields_view_get', 'search', 'name_get', 'distinct_field_get', 'name_search', 'copy', 'import_data', 'search_count', 'exists']
@ -1543,9 +1542,6 @@ class BaseModel(object):
yield dbid, xid, converted, dict(extras, record=stream.index)
def get_invalid_fields(self, cr, uid):
return list(self._invalids)
def _validate(self, cr, uid, ids, context=None):
context = context or {}
lng = context.get('lang')
@ -1566,12 +1562,9 @@ class BaseModel(object):
# Check presence of __call__ directly instead of using
# callable() because it will be deprecated as of Python 3.0
if hasattr(msg, '__call__'):
tmp_msg = msg(self, cr, uid, ids, context=context)
if isinstance(tmp_msg, tuple):
tmp_msg, params = tmp_msg
translated_msg = tmp_msg % params
else:
translated_msg = tmp_msg
translated_msg = msg(self, cr, uid, ids, context=context)
if isinstance(translated_msg, tuple):
translated_msg = translated_msg[0] % translated_msg[1]
else:
translated_msg = trans._get_source(cr, uid, self._name, 'constraint', lng, msg)
if extra_error:
@ -1579,11 +1572,8 @@ class BaseModel(object):
error_msgs.append(
_("The field(s) `%s` failed against a constraint: %s") % (', '.join(fields), translated_msg)
)
self._invalids.update(fields)
if error_msgs:
raise except_orm('ValidateError', '\n'.join(error_msgs))
else:
self._invalids.clear()
def default_get(self, cr, uid, fields_list, context=None):
"""

View File

@ -866,7 +866,7 @@ form: module.record_id""" % (xml_id,)
if '.' not in full_tpl_id:
full_tpl_id = '%s.%s' % (self.module, tpl_id)
# set the full template name for qweb <module>.<id>
if not (el.get('inherit_id') or el.get('inherit_option_id')):
if not el.get('inherit_id'):
el.set('t-name', full_tpl_id)
el.tag = 't'
else:
@ -889,15 +889,18 @@ form: module.record_id""" % (xml_id,)
record.append(Field("qweb", name='type'))
record.append(Field(el.get('priority', "16"), name='priority'))
record.append(Field(el, name="arch", type="xml"))
for field_name in ('inherit_id','inherit_option_id'):
value = el.attrib.pop(field_name, None)
if value: record.append(Field(name=field_name, ref=value))
if 'inherit_id' in el.attrib:
record.append(Field(name='inherit_id', ref=el.get('inherit_id')))
groups = el.attrib.pop('groups', None)
if groups:
grp_lst = map(lambda x: "ref('%s')" % x, groups.split(','))
record.append(Field(name="groups_id", eval="[(6, 0, ["+', '.join(grp_lst)+"])]"))
if el.attrib.pop('page', None) == 'True':
record.append(Field(name="page", eval="True"))
if el.get('primary') == 'True':
record.append(Field('primary', name='mode'))
if el.get('optional'):
record.append(Field(el.get('optional'), name='application'))
return self._tag_record(cr, record, data_node)