[MERGE] forward port of branch 8.0 up to 5e6ec89

This commit is contained in:
Christophe Simonis
2015-07-27 14:56:49 +02:00
41 changed files with 589 additions and 57 deletions
+12 -10
View File
@@ -305,20 +305,22 @@ class account_analytic_account(osv.osv):
args=[]
if context is None:
context={}
account_ids = []
if name:
account_ids = self.search(cr, uid, [('code', '=', name)] + args, limit=limit, context=context)
if not account_ids:
dom = []
for name2 in name.split('/'):
name = name2.strip()
account_ids = self.search(cr, uid, dom + [('name', operator, name)], limit=limit, context=context)
if not account_ids: break
dom = [('parent_id','in',account_ids)]
if account_ids and args:
# final filtering according to domain (args)
account_ids = self.search(cr, uid, [('id', 'in', account_ids)] + args, limit=limit, context=context)
else:
account_ids = self.search(cr, uid, args, limit=limit, context=context)
if '/' in name:
for name2 in name.split('/'):
# intermediate search without limit and args - could be expensive for large tables if `name` is not selective
account_ids = self.search(cr, uid, dom + [('name', operator, name2.strip())], limit=None, context=context)
if not account_ids: break
dom = [('parent_id','in',account_ids)]
if account_ids and args:
# final filtering according to domain (args)4
account_ids = self.search(cr, uid, [('id', 'in', account_ids)] + args, limit=limit, context=context)
if not account_ids:
return super(account_analytic_account, self).name_search(cr, uid, name, args, operator=operator, context=context, limit=limit)
return self.name_get(cr, uid, account_ids, context=context)
class account_analytic_line(osv.osv):
+14 -4
View File
@@ -126,15 +126,24 @@ class base_action_rule(osv.osv):
ir_filter = self.pool['ir.filters'].browse(cr, uid, filter_id, context=context)
return {'value': {'filter_domain': ir_filter.domain}}
def _get_eval_context(self, cr, uid, context=None):
""" Prepare the context used when evaluating python code
:returns: dict -- evaluation context given to (safe_)eval """
return {
'time': time,
'user': self.pool['res.users'].browse(cr, uid, uid, context=context),
}
def _filter(self, cr, uid, action, action_filter, record_ids, domain=False, context=None):
""" Filter the list record_ids that satisfy the domain or the action filter. """
if record_ids and (domain is not False or action_filter):
eval_context = self._get_eval_context(cr, uid, context=context)
if domain is not False:
new_domain = [('id', 'in', record_ids)] + eval(domain)
new_domain = [('id', 'in', record_ids)] + eval(domain, eval_context)
ctx = context
elif action_filter:
assert action.model == action_filter.model_id, "Filter model different from action rule model"
new_domain = [('id', 'in', record_ids)] + eval(action_filter.domain)
new_domain = [('id', 'in', record_ids)] + eval(action_filter.domain, eval_context)
ctx = dict(context or {})
ctx.update(eval(action_filter.context))
record_ids = self.pool[action.model].search(cr, uid, new_domain, context=ctx)
@@ -309,6 +318,7 @@ class base_action_rule(osv.osv):
# retrieve all the action rules to run based on a timed condition
action_dom = [('kind', '=', 'on_time')]
action_ids = self.search(cr, uid, action_dom, context=dict(context, active_test=True))
eval_context = self._get_eval_context(cr, uid, context=context)
for action in self.browse(cr, uid, action_ids, context=context):
now = datetime.now()
if action.last_run:
@@ -320,9 +330,9 @@ class base_action_rule(osv.osv):
domain = []
ctx = dict(context)
if action.filter_domain is not False:
domain = eval(action.filter_domain)
domain = eval(action.filter_domain, eval_context)
elif action.filter_id:
domain = eval(action.filter_id.domain)
domain = eval(action.filter_id.domain, eval_context)
ctx.update(eval(action.filter_id.context))
if 'lang' not in ctx:
# Filters might be language-sensitive, attempt to reuse creator lang
@@ -13,6 +13,7 @@
<field name="name">crm.claim.from_delivery.form</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form"/>
<field name="groups_id" eval="[(4, ref('base.group_sale_salesman')), (4, ref('base.group_partner_manager'))]"/>
<field name="arch" type="xml">
<xpath expr="/form/sheet/h1" position="before">
<div class="oe_right oe_button_box">
+1 -1
View File
@@ -327,7 +327,7 @@
<field name="partner_id"/>
</div>
<div class="oe_deadline">
<t t-if="record.date_deadline.raw_value and record.date_deadline.raw_value lt (new Date())" t-set="red">oe_kanban_text_red</t>
<t t-if="record.date_action.raw_value and record.date_action.raw_value lt (new Date())" t-set="red">oe_kanban_text_red</t>
<span t-attf-class="#{red || ''}"><field name="date_action"/></span>
<t t-if="record.date_action.raw_value"> : </t>
<field name="title_action"/>
+6 -2
View File
@@ -41,8 +41,12 @@ class res_partner(osv.osv):
}
except:
pass
return res
def _phonecall_count(self, cr, uid, ids, field_name, arg, context=None):
res = {}
for partner in self.browse(cr, uid, ids, context):
res[partner.id]['phonecall_count'] = len(partner.phonecall_ids)
res[partner.id] = len(partner.phonecall_ids)
return res
_columns = {
@@ -55,7 +59,7 @@ class res_partner(osv.osv):
'Phonecalls'),
'opportunity_count': fields.function(_opportunity_meeting_phonecall_count, string="Opportunity", type='integer', multi='opp_meet'),
'meeting_count': fields.function(_opportunity_meeting_phonecall_count, string="# Meetings", type='integer', multi='opp_meet'),
'phonecall_count': fields.function(_opportunity_meeting_phonecall_count, string="Phonecalls", type="integer", multi='opp_meet'),
'phonecall_count': fields.function(_phonecall_count, string="Phonecalls", type="integer"),
}
def redirect_partner_form(self, cr, uid, partner_id, context=None):
+15
View File
@@ -103,6 +103,21 @@
context="{'partner_id': active_id}">
<field string="Meetings" name="meeting_count" widget="statinfo"/>
</button>
</xpath>
</data>
</field>
</record>
<!-- second view due to access rights required to compute phonecall_count -->
<record id="view_partners_form_crm_calls" model="ir.ui.view">
<field name="name">view.res.partner.form.crm.inherited.calls</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="groups_id" eval="[(4, ref('base.group_sale_manager')), (4, ref('base.group_partner_manager'))]"/>
<field eval="18" name="priority"/>
<field name="arch" type="xml">
<data>
<xpath expr="//div[@name='buttons']" position="inside">
<button class="oe_inline oe_stat_button" type="action"
name="%(crm.crm_phonecall_category_phone_incoming0)d"
icon="fa-phone"
+2 -2
View File
@@ -162,7 +162,7 @@
<group colspan="2" col="2" groups="base.group_no_one">
<separator colspan="2" string="Dates"/>
<field name="create_date"/>
<field name="date_closed"/>
<field name="date_closed" invisible="1"/>
<field name="write_date"/>
</group>
@@ -220,7 +220,7 @@
<filter string="Type" icon="terp-stock_symbol-selection" domain="[]" context="{'group_by':'categ_id'}"/>
<filter string="Claim Month" icon="terp-go-month" domain="[]" help="Claim Date by Month" context="{'group_by':'date'}"/>
<filter string="Deadline" icon="terp-go-month" domain="[]" context="{'group_by':'date_deadline'}"/>
<filter string="Closure" icon="terp-go-month" domain="[]" help="Date Closed" context="{'group_by':'date_closed'}" groups="base.group_no_one"/>
<filter string="Closure" icon="terp-go-month" domain="[]" help="Date Closed" context="{'group_by':'date_closed'}" groups="base.group_no_one" invisible="1"/>
</group>
</search>
</field>
+1 -1
View File
@@ -44,7 +44,7 @@ class crm_claim_report(osv.osv):
'create_date': fields.datetime('Create Date', readonly=True, select=True),
'claim_date': fields.datetime('Claim Date', readonly=True),
'delay_close': fields.float('Delay to close', digits=(16,2),readonly=True, group_operator="avg",help="Number of Days to close the case"),
'stage_id': fields.many2one ('crm.stage', 'Stage', readonly=True,domain="[('team_ids','=',team_id)]"),
'stage_id': fields.many2one ('crm.claim.stage', 'Stage', readonly=True,domain="[('team_ids','=',team_id)]"),
'categ_id': fields.many2one('crm.claim.category', 'Category',readonly=True),
'partner_id': fields.many2one('res.partner', 'Partner', readonly=True),
'company_id': fields.many2one('res.company', 'Company', readonly=True),
+1
View File
@@ -6,6 +6,7 @@
<field name="name">res.partner.claim.info.form</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="groups_id" eval="[(4, ref('base.group_sale_salesman')), (4, ref('base.group_partner_manager'))]"/>
<field name="priority" eval="50"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='buttons']" position="inside">
+1 -1
View File
@@ -100,7 +100,7 @@ class crm_helpdesk(osv.osv):
if values.get('state'):
if values.get('state') in ['draft', 'open'] and not values.get('date_open'):
values['date_open'] = fields.datetime.now()
elif values.get('state') == 'close' and not values.get('date_closed'):
elif values.get('state') == 'done' and not values.get('date_closed'):
values['date_closed'] = fields.datetime.now()
return super(crm_helpdesk, self).write(cr, uid, ids, values, context=context)
@@ -143,7 +143,7 @@
});
im_livechat.ChatButton = openerp.Widget.extend({
className: "openerp_style oe_chat_button",
className: "openerp_style oe_chat_button hidden-print",
events: {
"click": "click"
},
+7
View File
@@ -1007,6 +1007,8 @@ class mail_thread(osv.AbstractModel):
(model, thread_id, custom_values, uid, None),
update_author=True, assert_model=True, create_fallback=True, context=context)
if route:
# parent is invalid for a compat-reply
message_dict.pop('parent_id', None)
_logger.info(
'Routing mail from %s to %s with Message-Id %s: direct thread reply (compat-mode) to model: %s, thread_id: %s, custom_values: %s, uid: %s',
email_from, email_to, message_id, model, thread_id, custom_values, uid)
@@ -1033,6 +1035,9 @@ class mail_thread(osv.AbstractModel):
elif route is False:
return []
# no route found for a matching reference (or reply), so parent is invalid
message_dict.pop('parent_id', None)
# 4. Look for a matching mail.alias entry
# Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
# for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
@@ -1115,6 +1120,8 @@ class mail_thread(osv.AbstractModel):
if thread_id and hasattr(model_pool, 'message_update'):
model_pool.message_update(cr, user_id, [thread_id], message_dict, context=nosub_ctx)
else:
# if a new thread is created, parent is irrelevant
message_dict.pop('parent_id', None)
thread_id = model_pool.message_new(cr, user_id, message_dict, custom_values, context=nosub_ctx)
else:
if thread_id:
+28
View File
@@ -736,3 +736,31 @@ class TestMailgateway(TestMail):
'message_post: private discussion: incorrect recipients when replying')
self.assertEqual(set(msg_nids), set(test_nids),
'message_post: private discussion: incorrect notified recipients when replying')
# Do bert forward it to an alias
mail_group_model_id = self.ir_model.search(cr, uid, [('model', '=', 'mail.group')])[0]
self.mail_alias.create(cr, uid, {
'alias_name': 'groups',
'alias_user_id': False,
'alias_model_id': mail_group_model_id,
'alias_parent_model_id': mail_group_model_id,
'alias_parent_thread_id': self.group_pigs_id,
'alias_contact': 'everyone'})
msg = self.mail_message.browse(cr, uid, msg1_id)
# forward it to a new thread AND an existing thread
for i, to in enumerate(['groups', 'group+pigs']):
fw_msg_id = '<THIS.IS.A.FW.MESSAGE.%d@bert.fr>' % (i,)
fw_message = format(MAIL_TEMPLATE, to='%s@whatever.tld' % (to,),
subject='FW: Re: 1',
email_from='bert@bert.fr',
extra='References: %s' % msg.message_id,
msg_id=fw_msg_id)
self.mail_thread.message_process(cr, uid, None, fw_message)
msg_ids = self.mail_message.search(cr, uid, [('message_id', '=', fw_msg_id)])
self.assertEqual(len(msg_ids), 1)
msg_fw = self.mail_message.browse(cr, uid, msg_ids[0])
self.assertEqual(msg_fw.model, 'mail.group')
self.assertFalse(msg_fw.parent_id)
+1 -1
View File
@@ -92,6 +92,6 @@ class product_product(osv.osv):
('product_id', '=', False),
('product_tmpl_id', 'in', list(products)),
]
result['context'] = "{}"
result['context'] = "{'default_product_id': active_id, 'search_default_product_id': active_id, 'default_product_tmpl_id': %s}" % (len(products) and products.pop() or 'False')
result['domain'] = str(domain)
return result
+2
View File
@@ -0,0 +1,2 @@
import models
import controllers
+37
View File
@@ -0,0 +1,37 @@
# -*- encoding: utf-8 -*-
##############################################################################
#
# Copyright Eezee-It
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'Worldline SIPS Payment Acquiring for online payments',
'version': '1.0',
'author': 'Eezee-It',
'category': 'Hidden',
'description': """
Worldline SIPS Payment Acquirer for online payments
Works with Worldline keys version 2.0, contains implementation of
payments acquirer using Worldline SIPS.""",
'depends': ['payment'],
'data': [
'views/sips.xml',
'views/payment_acquirer.xml',
'data/sips.xml',
],
'installable': True,
}
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import main
+60
View File
@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
try:
import simplejson as json
except ImportError:
import json
import logging
import werkzeug
from openerp import http
from openerp.http import request
_logger = logging.getLogger(__name__)
class SipsController(http.Controller):
_notify_url = '/payment/sips/ipn/'
_return_url = '/payment/sips/dpn/'
def _get_return_url(self, **post):
""" Extract the return URL from the data coming from sips. """
return_url = post.pop('return_url', '')
if not return_url:
tx_obj = request.registry['payment.transaction']
data = tx_obj._sips_data_to_object(post.get('Data'))
custom = json.loads(data.pop('returnContext', False) or '{}')
return_url = custom.get('return_url', '/')
return return_url
def sips_validate_data(self, **post):
res = False
env = request.env
tx_obj = env['payment.transaction']
acquirer_obj = env['payment.acquirer']
sips = acquirer_obj.search([('provider', '=', 'sips')], limit=1)
security = sips._sips_generate_shasign(post)
if security == post['Seal']:
_logger.debug('Sips: validated data')
res = tx_obj.sudo().form_feedback(post, 'sips')
else:
_logger.warning('Sips: data are corrupted')
return res
@http.route([
'/payment/sips/ipn/'],
type='http', auth='none', methods=['POST'])
def sips_ipn(self, **post):
""" Sips IPN. """
self.sips_validate_data(**post)
return ''
@http.route([
'/payment/sips/dpn'], type='http', auth="none", methods=['POST'])
def sips_dpn(self, **post):
""" Sips DPN """
return_url = self._get_return_url(**post)
self.sips_validate_data(**post)
return werkzeug.utils.redirect(return_url)
+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<record id="payment_acquirer_sips" model="payment.acquirer">
<field name="name">Sips</field>
<field name="provider">sips</field>
<field name="company_id" ref="base.main_company"/>
<field name="view_template_id" ref="payment_sips.sips_acquirer_button"/>
<field name="environment">test</field>
<field name="pre_msg"><![CDATA[
<p>You will be redirected to the Sips website after clicking on payment button.</p>]]></field>
</record>
<record id="sequence_payment_transaction_type_sips" model="ir.sequence.type">
<field name="name">Sips Payment Transaction</field>
<field name="code">sips.payment.transaction</field>
</record>
<record id="sequence_payment_transaction_sips" model="ir.sequence">
<field name="name">Sips Payment Transaction</field>
<field name="code">sips.payment.transaction</field>
<field name="prefix"></field>
<field eval="1" name="number_next"/>
<field eval="1" name="number_increment"/>
</record>
</data>
</openerp>
+3
View File
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import sips
+232
View File
@@ -0,0 +1,232 @@
# -*- coding: utf-'8' "-*-"
try:
import simplejson as json
except ImportError:
import json
import logging
from hashlib import sha256
import urlparse
import unicodedata
from openerp import models, fields, api
from openerp.tools.float_utils import float_compare
from openerp.tools.translate import _
from openerp.addons.payment.models.payment_acquirer import ValidationError
from openerp.addons.payment_sips.controllers.main import SipsController
_logger = logging.getLogger(__name__)
CURRENCY_CODES = {
'EUR': '978',
'USD': '840',
'CHF': '756',
'GBP': '826',
'CAD': '124',
'JPY': '392',
'MXN': '484',
'TRY': '949',
'AUD': '036',
'NZD': '554',
'NOK': '578',
'BRL': '986',
'ARS': '032',
'KHR': '116',
'TWD': '901',
}
class AcquirerSips(models.Model):
_inherit = 'payment.acquirer'
# Fields
sips_merchant_id = fields.Char('SIPS API User Password',
required_if_provider='sips')
sips_secret = fields.Char('SIPS Secret', size=64, required_if_provider='sips')
# Methods
def _get_sips_urls(self, environment):
""" Worldline SIPS URLS """
url = {
'prod': 'https://payment-webinit.sips-atos.com/paymentInit',
'test': 'https://payment-webinit.simu.sips-atos.com/paymentInit', }
return {'sips_form_url': url.get(environment, url['test']), }
@api.model
def _get_providers(self):
providers = super(AcquirerSips, self)._get_providers()
providers.append(['sips', 'Sips'])
return providers
def _sips_generate_shasign(self, values):
""" Generate the shasign for incoming or outgoing communications.
:param dict values: transaction values
:return string: shasign
"""
if self.provider != 'sips':
raise ValidationError(_('Incorrect payment acquirer provider'))
data = values['Data']
# Test key provided by Worldine
key = u'002001000000001_KEY1'
if self.environment == 'prod':
key = getattr(self, 'sips_secret')
shasign = sha256(data + key)
return shasign.hexdigest()
@api.multi
def sips_form_generate_values(self, partner_values, tx_values):
self.ensure_one()
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
currency = self.env['res.currency'].sudo().browse(tx_values['currency_id'])
currency_code = CURRENCY_CODES.get(currency.name, False)
if not currency_code:
raise ValidationError(_('Currency not supported by Wordline'))
amount = int(tx_values.get('amount') * 100)
if self.environment == 'prod':
# For production environment, key version 2 is required
merchant_id = getattr(self, 'sips_merchant_id')
key_version = '2'
else:
# Test key provided by Atos Wordline works only with version 1
merchant_id = '002001000000001'
key_version = '1'
sips_tx_values = dict(tx_values)
sips_tx_values.update({
'Data': u'amount=%s|' % amount +
u'currencyCode=%s|' % currency_code +
u'merchantId=%s|' % merchant_id +
u'normalReturnUrl=%s|' % urlparse.urljoin(base_url, SipsController._return_url) +
u'automaticResponseUrl=%s|' % urlparse.urljoin(base_url, SipsController._return_url) +
u'transactionReference=%s|' % tx_values['reference'] +
u'statementReference=%s|' % tx_values['reference'] +
u'keyVersion=%s' % key_version,
'InterfaceVersion': 'HP_2.3',
})
return_context = {}
if sips_tx_values.get('return_url'):
return_context[u'return_url'] = u'%s' % sips_tx_values.pop('return_url')
return_context[u'reference'] = u'%s' % sips_tx_values['reference']
sips_tx_values['Data'] += u'|returnContext=%s' % (json.dumps(return_context))
shasign = self._sips_generate_shasign(sips_tx_values)
sips_tx_values['Seal'] = shasign
return partner_values, sips_tx_values
@api.multi
def sips_get_form_action_url(self):
self.ensure_one()
return self._get_sips_urls(self.environment)['sips_form_url']
class TxSips(models.Model):
_inherit = 'payment.transaction'
# sips status
_sips_valid_tx_status = ['00']
_sips_wait_tx_status = ['90', '99']
_sips_refused_tx_status = ['05', '14', '34', '54', '75', '97']
_sips_error_tx_status = ['03', '12', '24', '25', '30', '40', '51', '63', '94']
_sips_pending_tx_status = ['60']
_sips_cancel_tx_status = ['17']
# --------------------------------------------------
# FORM RELATED METHODS
# --------------------------------------------------
def _sips_data_to_object(self, data):
res = {}
for element in data.split('|'):
element_split = element.split('=')
res[element_split[0]] = element_split[1]
return res
@api.model
def _sips_form_get_tx_from_data(self, data):
""" Given a data dict coming from sips, verify it and find the related
transaction record. """
data = self._sips_data_to_object(data.get('Data'))
reference = data.get('transactionReference')
if not reference:
custom = json.loads(data.pop('returnContext', False) or '{}')
reference = custom.get('reference')
payment_tx = self.search([('reference', '=', reference)])
if not payment_tx or len(payment_tx) > 1:
error_msg = _('Sips: received data for reference %s') % reference
if not payment_tx:
error_msg += _('; no order found')
else:
error_msg += _('; multiple order found')
_logger.error(error_msg)
raise ValidationError(error_msg)
return payment_tx
@api.model
def _sips_form_get_invalid_parameters(self, tx, data):
invalid_parameters = []
data = self._sips_data_to_object(data.get('Data'))
# TODO: txn_id: should be false at draft, set afterwards, and verified with txn details
if tx.acquirer_reference and data.get('transactionReference') != tx.acquirer_reference:
invalid_parameters.append(('transactionReference', data.get('transactionReference'), tx.acquirer_reference))
# check what is bought
if float_compare(float(data.get('amount', '0.0')) / 100, tx.amount, 2) != 0:
invalid_parameters.append(('amount', data.get('amount'), '%.2f' % tx.amount))
if tx.partner_reference and data.get('customerId') != tx.partner_reference:
invalid_parameters.append(('customerId', data.get('customerId'), tx.partner_reference))
return invalid_parameters
@api.model
def _sips_form_validate(self, tx, data):
data = self._sips_data_to_object(data.get('Data'))
status = data.get('responseCode')
data = {
'acquirer_reference': data.get('transactionReference'),
'partner_reference': data.get('customerId'),
'date_validate': data.get('transactionDateTime',
fields.Datetime.now())
}
res = False
if status in self._sips_valid_tx_status:
msg = 'Payment for tx ref: %s, got response [%s], set as done.' % \
(tx.reference, status)
_logger.info(msg)
data.update(state='done', state_message=msg)
res = True
elif status in self._sips_error_tx_status:
msg = 'Payment for tx ref: %s, got response [%s], set as ' \
'error.' % (tx.reference, status)
data.update(state='error', state_message=msg)
elif status in self._sips_wait_tx_status:
msg = 'Received wait status for payment ref: %s, got response ' \
'[%s], set as error.' % (tx.reference, status)
data.update(state='error', state_message=msg)
elif status in self._sips_refused_tx_status:
msg = 'Received refused status for payment ref: %s, got response' \
' [%s], set as error.' % (tx.reference, status)
data.update(state='error', state_message=msg)
elif status in self._sips_pending_tx_status:
msg = 'Payment ref: %s, got response [%s] set as pending.' \
% (tx.reference, status)
data.update(state='pending', state_message=msg)
elif status in self._sips_cancel_tx_status:
msg = 'Received notification for payment ref: %s, got response ' \
'[%s], set as cancel.' % (tx.reference, status)
data.update(state='cancel', state_message=msg)
else:
msg = 'Received unrecognized status for payment ref: %s, got ' \
'response [%s], set as error.' % (tx.reference, status)
data.update(state='error', state_message=msg)
_logger.info(msg)
tx.write(data)
return res
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="acquirer_form_sips" model="ir.ui.view">
<field name="name">acquirer.form.sips</field>
<field name="model">payment.acquirer</field>
<field name="inherit_id" ref="payment.acquirer_form"/>
<field name="arch" type="xml">
<xpath expr='//group[@name="acquirer_display"]' position='after'>
<group attrs="{'invisible': [('provider', '!=', 'sips')]}">
<field name="sips_secret"/>
<field name="sips_merchant_id"/>
</group>
</xpath>
</field>
</record>
</data>
</openerp>
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<template id="sips_acquirer_button">
<form t-att-action="tx_url" method="post" target="_self">
<input type="hidden" name="Data" t-att-value="tx_values['Data']"/>
<input type="hidden" name="InterfaceVersion" t-att-value="tx_values['InterfaceVersion']"/>
<input type="hidden" name="Seal" t-att-value="tx_values['Seal']"/>
<!-- submit -->
<button type="submit" width="100px"
t-att-class="submit_class">
<img t-if="not submit_txt" src="/payment_sips/static/src/img/sips_icon.png"/>
<span t-if="submit_txt"><t t-esc="submit_txt"/> <span class="fa fa-long-arrow-right"/></span>
</button>
</form>
</template>
</data>
</openerp>
+5 -2
View File
@@ -1189,19 +1189,22 @@ class pos_order(osv.osv):
# Create an move for each order line
cur = order.pricelist_id.currency_id
round_per_line = True
if order.company_id.tax_calculation_rounding_method == 'round_globally':
round_per_line = False
for line in order.lines:
tax_amount = 0
computed_taxes = account_tax_obj.compute_all(cr, uid, line.tax_ids, line.price_unit * (100.0-line.discount) / 100.0, line.qty)['taxes']
for tax in computed_taxes:
tax_amount += cur_obj.round(cr, uid, cur, tax['amount'])
tax_amount += cur_obj.round(cr, uid, cur, tax['amount']) if round_per_line else tax['amount']
if tax_amount < 0:
group_key = (tax['ref_tax_code_id'], tax['base_code_id'], tax['account_collected_id'], tax['id'])
else:
group_key = (tax['tax_code_id'], tax['base_code_id'], tax['account_collected_id'], tax['id'])
group_tax.setdefault(group_key, 0)
group_tax[group_key] += cur_obj.round(cr, uid, cur, tax['amount'])
group_tax[group_key] += cur_obj.round(cr, uid, cur, tax['amount']) if round_per_line else tax['amount']
amount = line.price_subtotal
@@ -151,6 +151,7 @@ openerp.point_of_sale.load_models = function load_models(instance, module){ //mo
model: 'product.uom',
fields: [],
domain: null,
context: function(self){ return { active_test: false }; },
loaded: function(self,units){
self.units = units;
var units_by_id = {};
+5 -9
View File
@@ -44,15 +44,11 @@ class sale_report(osv.osv):
'categ_id': fields.many2one('product.category','Product Category', readonly=True),
'nbr': fields.integer('# of Lines', readonly=True), # TDE FIXME master: rename into nbr_lines
'state': fields.selection([
('draft', 'Quotation'),
('sent', 'Quotation Sent'),
('waiting_date', 'Waiting Schedule'),
('manual', 'Manual In Progress'),
('progress', 'In Progress'),
('invoice_except', 'Invoice Exception'),
('done', 'Done'),
('cancel', 'Cancelled')
], 'Order Status', readonly=True),
('cancel', 'Cancelled'),
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('exception', 'Exception'),
('done', 'Done')], 'Order Status', readonly=True),
'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', readonly=True),
'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True),
'invoiced': fields.boolean('Paid', readonly=True),
+3 -3
View File
@@ -29,7 +29,7 @@
<record id="filter_sale_report_sales_funnel" model="ir.filters">
<field name="name">Sales Funnel</field>
<field name="model_id">sale.report</field>
<field name="domain">['&amp;', ('date','&lt;=', time.strftime('%%Y-12-31')), '&amp;', ('date','&gt;=',time.strftime('%%Y-01-01')), '|', ('state','in',('draft','sent')), ('state','not in',('draft','sent','cancel'))]</field>
<field name="domain">['&amp;', ('date','&lt;=', time.strftime('%%Y-12-31')), '&amp;', ('date','&gt;=',time.strftime('%%Y-01-01')), ('state','not in',('cancel',))]</field>
<field name="user_id" eval="False"/>
<field name="context">{'group_by': ['state'], 'measures': ['price_total']}</field>
</record>
@@ -60,8 +60,8 @@
<field name="date"/>
<field name="date_confirm"/>
<filter string="This Year" name="year" invisible="1" domain="[('date','&lt;=', time.strftime('%%Y-12-31')),('date','&gt;=',time.strftime('%%Y-01-01'))]"/>
<filter name="Quotations" domain="[('state','in',('draft','sent'))]"/>
<filter name="Sales" string="Sales" domain="[('state','not in',('draft','sent','cancel'))]"/>
<filter name="Quotations" domain="[('state','in',('draft'))]"/>
<filter name="Sales" string="Sales" domain="[('state','not in',('draft', 'cancel'))]"/>
<separator/>
<field name="partner_id"/>
<field name="product_id"/>
+2 -2
View File
@@ -1250,10 +1250,10 @@ class product_product(osv.Model):
def _sales_count(self, cr, uid, ids, field_name, arg, context=None):
r = dict.fromkeys(ids, 0)
domain = [
('state', 'in', ['waiting_date','progress','manual', 'shipping_except', 'invoice_except', 'done']),
('state', 'in', ['confirmed', 'done']),
('product_id', 'in', ids),
]
for group in self.pool['sale.report'].read_group(cr, uid, domain, ['product_id','product_uom_qty'], ['product_id'], context=context):
for group in self.pool['sale.report'].read_group(cr, uid, domain, ['product_id', 'product_uom_qty'], ['product_id'], context=context):
r[group['product_id'][0]] = group['product_uom_qty']
return r
+1 -1
View File
@@ -699,7 +699,7 @@
<field name="name">Sales Analysis</field>
<field name="res_model">sale.report</field>
<field name="view_mode">graph</field>
<field name="domain">[('state','not in',('draft','sent','cancel')),('team_id', '=', active_id)]</field>
<field name="domain">[('state','not in',('draft','cancel')),('team_id', '=', active_id)]</field>
<field name="context">{'search_default_order_month':1}</field>
<field name="help">This report performs analysis on your sales orders. Analysis check your sales revenues and sort it by different group criteria (salesman, partner, product, etc.) Use this report to perform analysis on sales not having invoiced yet. If you want to analyse your turnover, you should use the Invoice Analysis report in the Accounting application.</field>
</record>
@@ -0,0 +1,17 @@
@media print {
.js_surveyform {
font-size: 13px;
}
.js_surveyform textarea.form-control {
height: 10em;
}
.js_surveyform h1 {
font-size: 28px;
}
.js_surveyform h2 {
font-size: 20px;
}
.js_question-wrapper {
page-break-inside: avoid;
}
}
+8 -2
View File
@@ -172,7 +172,7 @@
</template>
<template id="simple_choice" name="Simple choice">
<div t-if="question.display_mode == 'dropdown'" class="js_drop row">
<div t-if="question.display_mode == 'dropdown' and token" class="js_drop row">
<div class="col-md-12">
<select class="form-control" t-att-name="prefix">
<option disabled="1" selected="1" value="">Choose...</option>
@@ -192,7 +192,7 @@
<input type="text" class="form-control" t-att-name="'%s_%s' % (prefix, 'comment')"/>
</div>
</div>
<div t-if="question.display_mode == 'columns' " class="row js_radio">
<div t-if="question.display_mode == 'columns' or not token" class="row js_radio">
<div t-foreach='question.labels_ids' t-as='label' t-attf-class="col-md-#{question.column_nb}">
<label t-att-class="' bg-success ' if quizz_correction and label.quizz_mark > 0.0 else ''">
<input type="radio" t-att-name="prefix" t-att-value='label.id' />
@@ -259,6 +259,12 @@
</div>
</template>
<template id="assets_frontend_survey" inherit_id="website.assets_frontend" name="Printing Survey assets">
<xpath expr="." position="inside">
<link href="/survey/static/src/css/survey_print.css" rel="stylesheet" type="text/css"/>
</xpath>
</template>
<!-- Printable view of a survey (all pages) -->
<template id="survey_print" name="Survey">
<t t-call="website.layout">
@@ -530,6 +530,8 @@ instance.web_view_editor.ViewEditor = instance.web.Widget.extend({
var tr = self.get_object_by_id(this.one_object.clicked_tr_id, this.one_object['main_object'], [])[0].att_list[0];
var parent_tr = ($(side).prevAll("tr[level=" + String(this.one_object.clicked_tr_level - 1) + "]"))[0];
var field_dataset = new instance.web.DataSetSearch(this, this.model, null, null);
if(_.isUndefined(parent_tr))
return;
parent_tr = self.get_object_by_id(parseInt($(parent_tr).attr('id').replace(/[^0-9]+/g, '')), this.one_object['main_object'], [])[0].att_list[0];
_.each([tr, parent_tr],function(element) {
var value = _.has(_CHILDREN, element) ? element : _.str.include(html_tag, element)?"html_tag":false;
@@ -994,7 +996,7 @@ instance.web_view_editor.ViewEditor = instance.web.Widget.extend({
type_widget.set_value(node.value);
self.add_widget.push(type_widget);
});
table_selector.find("td[id^=]").attr("width","100px");
table_selector.find("td[id^='']").attr("width","100px");
self.add_node_dialog.$el.find('#new_field').click(function() {
model_data = new instance.web.DataSetSearch(self,'ir.model', null, null);
model_data.read_slice([], {domain: [['model','=', self.model]]}).done(function(result) {
@@ -16,3 +16,5 @@ access_product_attribute_price_public,product.attribute price public,product.mod
access_product_attribute_line_public,product.attribute line public,product.model_product_attribute_line,,1,0,0,0
access_fiscal_position_public,fiscal position public,account.model_account_fiscal_position,base.group_portal,1,0,0,0
access_payment_term,payment term public,account.model_account_payment_term,base.group_portal,1,0,0,0
access_product_supplierinfo_user,product.supplierinfo.user,product.model_product_supplierinfo,base.group_public,1,0,0,0
access_pricelist_partnerinfo_user,pricelist.partnerinfo.user,product.model_pricelist_partnerinfo,base.group_public,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
16 access_product_attribute_line_public product.attribute line public product.model_product_attribute_line 1 0 0 0
17 access_fiscal_position_public fiscal position public account.model_account_fiscal_position base.group_portal 1 0 0 0
18 access_payment_term payment term public account.model_account_payment_term base.group_portal 1 0 0 0
19 access_product_supplierinfo_user product.supplierinfo.user product.model_product_supplierinfo base.group_public 1 0 0 0
20 access_pricelist_partnerinfo_user pricelist.partnerinfo.user product.model_pricelist_partnerinfo base.group_public 1 0 0 0
@@ -229,6 +229,11 @@ $('.oe_website_sale').each(function () {
$parent.find(".js_check_product").attr("disabled", "disabled");
}
});
$('div.js_product', oe_website_sale).each(function () {
$('input.js_product_change', this).first().trigger('change');
});
$('ul.js_add_cart_variants', oe_website_sale).each(function () {
$('input.js_variant_change, select.js_variant_change', this).first().trigger('change');
});
+13 -13
View File
@@ -9,7 +9,7 @@ Theme Tutorial
Odoo celebrates freedom. Freedom for the designer to go further and
freedom for the user to customize everything according to their needs.
Ready to create your own theme? Great. Here some things you should know before you begin. This tutorial is a guide to creating an Odoo theme.
Ready to create your own theme? Great. Here are some things you should know before you begin. This tutorial is a guide to creating an Odoo theme.
.. image:: theme_tutorial_assets/img/Intro.jpg
@@ -219,8 +219,8 @@ Odoo’s themes are packaged like modules. Even if you are designing a very simp
Create another empty file and name it ``__init__.py``. It's a
mandatory system file. Create and leave it blank.
``view`` and ``static`` folders
Create them in the main folder. In ``view`` you'll place your xml
``views`` and ``static`` folders
Create them in the main folder. In ``views`` you'll place your xml
files that define your snippets, your pages and your
options. ``static`` folder is the right place for your style ,
images and custom js code.
@@ -276,7 +276,6 @@ The ``data`` property will contain the xml files list. Right now it’s empty, b
Installing your theme
---------------------
To install your theme you just place your theme folder inside "addons"
To install your theme, you just place your theme folder inside addons in your Odoo installation.
After that, navigate to the Settings page, look for your theme and click on the install button.
@@ -310,7 +309,7 @@ Extend the default Header
By default, Odoo header contains a responsive navigation menu and the company’s logo. You can easily add new elements or style the existing one.
To do so, create a **layout.xml** file in your **views** folder folder and add the default Odoo xml markup.
To do so, create a **layout.xml** file in your **views** folder and add the default Odoo xml markup.
.. code-block:: xml
@@ -429,7 +428,7 @@ default page layout with our code.
As you can see, we wrapped our code into two ``<div>``, one with ID ``wrap`` and the other one with class ``container``. This is to provide a minimal layout.
The next step is to add an empty area that the user
can fill with snippets. To archive this, just create a ``div`` with
can fill with snippets. To achieve this, just create a ``div`` with
``oe_structure`` class just before closing the ``div#wrap`` element.
.. code-block:: xml
@@ -463,7 +462,7 @@ can fill with snippets. To archive this, just create a ``div`` with
You can create as many snippet areas as you like and place them anywhere in your pages.
Our page is almost ready. Now all we have to do is add **pages.xml** in our **__opererp_.py** file
Our page is almost ready. Now all we have to do is add **pages.xml** in our **__openerp__.py** file
.. code-block:: python
@@ -555,8 +554,8 @@ order to operate.
In this case, we are referring to ``assets_frontend`` template,
located in the ``website`` module. ``assets_frontend`` specifies the
list of assets loaded by the the website builder and our goal is to
add our less file to this list.
list of assets loaded by the website builder and our goal is to add
our less file to this list.
This can be achieved using xpath with the attributes
``expr="link[last()]"`` and ``position="after"``, which means "*take my
@@ -615,12 +614,12 @@ The template contains the HTML markup that will be displayed by the snippet.
As you can see, we used Bootstrap default classes for our three columns. It’s not just about layout, these classes **will be triggered by the Website Builder to make them resizable by the user**.
The previous code will create the snippet’s content, but we still need to place it into the editor bar, so the user will be able to drag&drop it into the page. Copy/paste this template in your **snippet.xml** file.
The previous code will create the snippet’s content, but we still need to place it into the editor bar, so the user will be able to drag&drop it into the page. Copy/paste this template in your **snippets.xml** file.
.. code-block:: xml
<template id="place_into_bar" inherit_id="website.snippets" name="Place into bar">
<xpath expr="//div[@id='snippet_structure']//div[@class='o_panel_body']" position="inside">
<xpath expr="//div[@id='snippet_structure']" position="inside">
<t t-snippet="theme_tutorial.snippet_testimonial"
t-thumbnail="/theme_tutorial/static/src/img/ui/snippet_thumb.jpg"/>
</xpath>
@@ -666,7 +665,7 @@ Options group properties
Options are wrapped in groups. Groups can have properties that define how the included options will interact with the user interface.
``data-selector=" css selector(s) "``
Bind the all the options included into the group to a particular element.
Bind all the options included into the group to a particular element.
``data-js=" custom method name "``
Is used to bind custom Javascript methods.
``data-drop-in=" css selector(s) "``
@@ -709,7 +708,7 @@ We start by adding a new file in our views folder - name it **options.xml** and
.. note::
The previous template will inherit the default **snippet_option template** adding our options after the **background** options (xpath expr attribute).
The previous template will inherit the default **snippet_options template** adding our options after the **background** options (xpath expr attribute).
To place your options in a particular order, inspect the **snippet_options template** from the **website module** and add your options before/after the desired position.
As you can see, we wrapped all our options inside a DIV tag that will
@@ -724,6 +723,7 @@ Since ``select_class`` method avoids multiple selections, the last "empty"
option will reset the snippet to default.
Add **options.xml** to ``__openerp__.py`` and update your theme.
.. image:: theme_tutorial_assets/img/restart.png
Dropping our snippet onto the page, you will notice that our new options are automatically added to the customize menu. Inspecting the page, you will also notice that the class will be applied to the element when selecting an option.
+7
View File
@@ -19,6 +19,7 @@
#
##############################################################################
from openerp.exceptions import AccessError
from openerp.osv import osv, fields
class res_partner(osv.Model):
@@ -49,6 +50,8 @@ class Category(models.Model):
name = fields.Char(required=True)
parent = fields.Many2one('test_new_api.category')
display_name = fields.Char(compute='_compute_display_name', inverse='_inverse_display_name')
discussions = fields.Many2many('test_new_api.discussion', 'test_new_api_discussion_category',
'category', 'discussion')
@api.one
@api.depends('name', 'parent.display_name') # this definition is recursive
@@ -74,6 +77,10 @@ class Category(models.Model):
# assign name of last category, and reassign display_name (to normalize it)
self.name = names[-1].strip()
def read(self, fields=None, load='_classic_read'):
if self.search_count([('id', 'in', self._ids), ('name', '=', 'NOACCESS')]):
raise AccessError('Sorry')
return super(Category, self).read(fields, load)
class Discussion(models.Model):
_name = 'test_new_api.discussion'
@@ -3,6 +3,7 @@
#
from datetime import date, datetime
from openerp.exceptions import AccessError
from openerp.tests import common
@@ -337,6 +338,24 @@ class TestNewFields(common.TransactionCase):
self.assertEqual(data['display_name'], display_name)
self.assertEqual(data['size'], size)
def test_31_prefetch(self):
""" test prefetch of records handle AccessError """
Category = self.env['test_new_api.category']
cat_1 = Category.create({'name': 'NOACCESS'}).id
cat_2 = Category.create({'name': 'ACCESS', 'parent': cat_1}).id
self.env.clear()
cat = Category.browse(cat_2)
self.assertEqual(cat.name, 'ACCESS')
# both categories should be in prefetch ids
self.assertSetEqual(self.env.prefetch[Category._name], set([cat_1, cat_2]))
# but due to our (lame) overwrite of `read`, it should not forbid us to read records we have access to
self.assertFalse(len(cat.discussions))
self.assertEqual(cat.parent.id, cat_1)
with self.assertRaises(AccessError):
Category.browse(cat_1).name
def test_40_new(self):
""" test new records. """
discussion = self.env.ref('test_new_api.discussion_0')
+2 -1
View File
@@ -3240,7 +3240,8 @@ class BaseModel(object):
try:
result = records.read(list(fnames), load='_classic_write')
except AccessError:
pass
# not all records may be accessible, try with only current record
result = self.read(list(fnames), load='_classic_write')
# check the cache, and update it if necessary
if not self._cache.contains(field):