[MERGE] forward port of branch saas-6 up to e16d495

This commit is contained in:
Christophe Simonis
2015-08-24 18:19:19 +02:00
39 changed files with 446 additions and 167 deletions
+1
View File
@@ -574,6 +574,7 @@ class AccountInvoice(models.Model):
else:
line['currency_id'] = False
line['amount_currency'] = False
line['price'] = self.currency_id.round(line['price'])
if self.type in ('out_invoice', 'in_refund'):
total += line['price']
total_currency += line['amount_currency'] or line['price']
@@ -2,5 +2,7 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_account_analytic_plan,account.analytic.plan,model_account_analytic_plan,account.group_account_user,1,1,1,1
access_account_analytic_plan_line,account.analytic.plan.line,model_account_analytic_plan_line,account.group_account_user,1,1,1,1
access_account_analytic_plan_instance,account.analytic.plan.instance,model_account_analytic_plan_instance,account.group_account_user,1,1,1,1
access_account_analytic_plan_instance_group_user,account.analytic.plan.instance,model_account_analytic_plan_instance,base.group_user,1,0,0,0
access_account_analytic_plan_instance_line,account.analytic.plan.instance.line,model_account_analytic_plan_instance_line,account.group_account_user,1,1,1,1
access_account_analytic_plan_instance_line_group_user,account.analytic.plan.instance.line,model_account_analytic_plan_instance_line,base.group_user,1,0,0,0
access_account_analytic_plan_line_invoice,account.analytic.plan.line.invoice,model_account_analytic_plan_line,account.group_account_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_account_analytic_plan account.analytic.plan model_account_analytic_plan account.group_account_user 1 1 1 1
3 access_account_analytic_plan_line account.analytic.plan.line model_account_analytic_plan_line account.group_account_user 1 1 1 1
4 access_account_analytic_plan_instance account.analytic.plan.instance model_account_analytic_plan_instance account.group_account_user 1 1 1 1
5 access_account_analytic_plan_instance_group_user account.analytic.plan.instance model_account_analytic_plan_instance base.group_user 1 0 0 0
6 access_account_analytic_plan_instance_line account.analytic.plan.instance.line model_account_analytic_plan_instance_line account.group_account_user 1 1 1 1
7 access_account_analytic_plan_instance_line_group_user account.analytic.plan.instance.line model_account_analytic_plan_instance_line base.group_user 1 0 0 0
8 access_account_analytic_plan_line_invoice account.analytic.plan.line.invoice model_account_analytic_plan_line account.group_account_user 1 1 1 1
+1
View File
@@ -48,6 +48,7 @@ class stock_picking(osv.osv):
"""
carrier_obj = self.pool.get('delivery.carrier')
grid_obj = self.pool.get('delivery.grid')
fpos_obj = self.pool['account.fiscal.position']
currency_obj = self.pool.get('res.currency')
if not picking.carrier_id or \
any(inv_line.product_id.id == picking.carrier_id.product_id.id
@@ -249,8 +249,10 @@ class account_analytic_line(osv.osv):
# key_id is either an account.analytic.account, either a res.partner
# don't really care, what's important is the analytic lines that
# will be used to create the invoice lines
partner = analytic_line_ids[0].account_id.partner_id # will be the same for every line
account = analytic_lines[0].account_id
partner = account.partner_id # will be the same for every line
if (not partner) or not (currency_id):
raise UserError(_('Contract incomplete. Please fill in the Customer and Pricelist fields for %s.') % (account.name))
curr_invoice = self._prepare_cost_invoice(cr, uid, partner, company_id, currency_id, analytic_line_ids, group_by_partner, context=context)
invoice_context = dict(context,
@@ -265,8 +267,6 @@ class account_analytic_line(osv.osv):
invoice_lines_grouping = {}
for analytic_line in analytic_line_ids:
account = analytic_line.account_id
if (not partner) or not (account.pricelist_id):
raise UserError(_('Contract incomplete. Please fill in the Customer and Pricelist fields for %s.') % (account.name))
if not analytic_line.to_invoice:
raise UserError(_('Trying to invoice non invoiceable line for %s.') % (analytic_line.product_id.name))
@@ -33,6 +33,7 @@ class TestMassMailing(osv.TransientModel):
'body_html': '',
'notification': True,
'mailing_id': mailing.id,
'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids],
}
mail_mail_obj = Mail.browse(cr, uid, Mail.create(cr, uid, mail_values, context=context), context=context)
unsubscribe_url = Mail._get_unsubscribe_url(cr, uid, mail_mail_obj, test_mail, context=context)
+7 -4
View File
@@ -249,6 +249,9 @@ class PaymentTxOgone(osv.Model):
_logger.info(error_msg)
raise ValidationError(error_msg)
if not tx.acquirer_reference:
tx.acquirer_reference = pay_id
# alias was created on ogone server, store it
if alias:
method_obj = self.pool['payment.method']
@@ -380,7 +383,7 @@ class PaymentTxOgone(osv.Model):
_logger.info('Ogone: trying to validate an already validated tx (ref %s)', tx.reference)
return True
status = int(tree.get('STATUS', '0'))
status = int(tree.get('STATUS') or 0)
if status in self._ogone_valid_tx_status:
tx.write({
'state': 'done',
@@ -401,11 +404,11 @@ class PaymentTxOgone(osv.Model):
'acquirer_reference': tree.get('PAYID'),
'html_3ds': str(tree.HTML_ANSWER).decode('base64')
})
elif status in self._ogone_wait_tx_status and tries > 0:
time.sleep(1500)
elif (not status or status in self._ogone_wait_tx_status) and tries > 0:
time.sleep(500)
tx.write({'acquirer_reference': tree.get('PAYID')})
tree = self._ogone_s2s_get_tx_status(tx)
return self.ogone_s2s_validate(tx, tree, tries - 1)
return self._ogone_s2s_validate_tree(tx, tree, tries - 1)
else:
error = 'Ogone: feedback error: %(error_str)s\n\n%(error_code)s: %(error_msg)s' % {
'error_str': tree.get('NCERRORPLUS'),
@@ -0,0 +1,10 @@
# KEYBOARD CONFIGURATION FILE
# Consult the keyboard(5) manual page.
XKBMODEL="pc105"
XKBLAYOUT="us"
XKBVARIANT=""
XKBOPTIONS=""
BACKSPACE="guess"
@@ -31,7 +31,7 @@ apt-get -y autoremove
apt-get update
apt-get -y dist-upgrade
PKGS_TO_INSTALL="adduser postgresql-client python python-dateutil python-decorator python-docutils python-feedparser python-imaging python-jinja2 python-ldap python-libxslt1 python-lxml python-mako python-mock python-openid python-passlib python-psutil python-psycopg2 python-pybabel python-pychart python-pydot python-pyparsing python-pypdf python-reportlab python-requests python-simplejson python-tz python-unittest2 python-vatnumber python-vobject python-werkzeug python-xlwt python-yaml postgresql python-gevent python-serial python-pip python-dev localepurge vim mc mg"
PKGS_TO_INSTALL="adduser postgresql-client python python-dateutil python-decorator python-docutils python-feedparser python-imaging python-jinja2 python-ldap python-libxslt1 python-lxml python-mako python-mock python-openid python-passlib python-psutil python-psycopg2 python-pybabel python-pychart python-pydot python-pyparsing python-pypdf python-reportlab python-requests python-simplejson python-tz python-unittest2 python-vatnumber python-vobject python-werkzeug python-xlwt python-yaml postgresql python-gevent python-serial python-pip python-dev localepurge vim mc mg screen"
apt-get -y install ${PKGS_TO_INSTALL}
@@ -48,14 +48,22 @@ pip install pyusb==1.0.0b1
pip install qrcode
pip install evdev
groupadd usbusers
usermod -a -G usbusers pi
usermod -a -G lp pi
sudo -u postgres createuser -s pi
mkdir /var/log/odoo
chown pi:pi /var/log/odoo
echo "* * * * * rm /var/run/odoo/sessions/*" | crontab -
update-rc.d odoo defaults
# https://www.raspberrypi.org/forums/viewtopic.php?p=79249
# to not have "setting up console font and keymap" during boot take ages
setupcon
# create dirs for ramdisks
create_ramdisk_dir () {
mkdir "${1}_ram"
@@ -0,0 +1,2 @@
SUBSYSTEM=="usb", GROUP="usbusers", MODE="0660"
SUBSYSTEMS=="usb", GROUP="usbusers", MODE="0660"
@@ -0,0 +1,53 @@
#
set meta-flag on
set convert-meta off
set input-meta on
set output-meta on
set show-all-if-ambiguous on
set visible-stats on
set bell-style none
# set bell-style visible
#"\M-k": kill-whole-line
#----------------------------------------------------------
# arrows left/right/up/down
"\e[C": forward-char
"\e[D": backward-char
"\e[A": previous-history
"\e[B": next-history
# ---------------------------------------------------------
# pgup/pgdn
"\e[6~": history-search-forward
"\e[5~": history-search-backward
#----------------------------------------------------------
# insert,delete
#"\e[2~": quoted-insert
"\e[3~": delete-char
# esc+delete, esc+backspace
"\e\e[3~": kill-word
"\e\C-h": backward-kill-word
#----------------------------------------------------------
# ctrl left/right (rxvt)
"\eOc": forward-word
"\eOd": backward-word
# ctrl left/right (xterm)
"\e[1;5C": forward-word
"\e[1;5D": backward-word
# ctrl left/right (unknown)
#"\e[5C": forward-word
#"\e[5D": backward-word
#"\e\e[C": forward-word
#"\e\e[D": backward-word
# ---------------------------------------------------------
# home/end (linux console)
"\e[1~": beginning-of-line
"\e[4~": end-of-line
# home/end (rxvt)
"\e[7~": beginning-of-line
"\e[8~": end-of-line
# home/end (xterm, freebsd console)
"\e[H": beginning-of-line
"\e[F": end-of-line
# home/end (non RH/Debian xterm, unknown)
"\eOH": beginning-of-line
"\eOF": end-of-line
"\eOw": end-of-line
@@ -0,0 +1,27 @@
set all&
set autoindent
set backspace=2
set nobackup
set nocompatible
set noerrorbells
set noexpandtab
set expandtab
set number
set hidden
set history=500
set hlsearch
set ignorecase
set modeline
set ruler
set shiftwidth=4
set scrolloff=5
set showcmd
set showmode
set tabstop=4
set textwidth=0
set visualbell
set t_vb=
set wrap
set list
set listchars=tab:~.,trail:.,extends:>,precedes:<
set viminfo="NONE"
+11 -1
View File
@@ -746,8 +746,12 @@ class purchase_order(osv.osv):
#we don't round the price_unit, as we may want to store the standard price with more digits than allowed by the currency
price_unit = self.pool.get('res.currency').compute(cr, uid, order.currency_id.id, order.company_id.currency_id.id, price_unit, round=False, context=context)
res = []
if order.location_id.usage == 'customer':
name = order_line.product_id.with_context(dict(context or {}, lang=order.dest_address_id.lang)).name
else:
name = order_line.name or ''
move_template = {
'name': order_line.name or '',
'name': name,
'product_id': order_line.product_id.id,
'product_uom': order_line.product_uom.id,
'product_uos': order_line.product_uom.id,
@@ -1478,6 +1482,9 @@ class procurement_order(osv.osv):
return qty, price
def update_origin_po(self, cr, uid, po, proc, context=None):
pass
def _get_grouping_dicts(self, cr, uid, ids, context=None):
"""
It will group the procurements according to the pos they should go into. That way, lines going to the same
@@ -1610,6 +1617,9 @@ class procurement_order(osv.osv):
po_obj.message_post(cr, uid, [add_purchase], body=msg, context=context)
po_obj.write(cr, uid, [add_purchase], {'order_line': line_values},context=context)
for procurement in procurements:
self.update_origin_po(cr, uid, po, procurement, context=context)
# Create new purchase orders
partner_obj = self.pool.get("res.partner")
@@ -8,7 +8,7 @@
<field name="inherit_id" ref="purchase.purchase_order_line_form"/>
<field name="arch" type="xml">
<field name="account_analytic_id" position="replace">
<field name="analytics_id" context="{'journal_id':parent.journal_id, 'model': 'purchase_order'}" domain="[('plan_id','&lt;&gt;',False)]"/>
<field name="analytics_id" groups="purchase.group_analytic_accounting" context="{'journal_id':parent.journal_id, 'model': 'purchase_order'}" domain="[('plan_id','&lt;&gt;',False)]"/>
</field>
</field>
</record>
@@ -19,7 +19,7 @@
<field name="inherit_id" ref="purchase.purchase_order_form"/>
<field name="arch" type="xml">
<field name="account_analytic_id" position="replace">
<field name="analytics_id" context="{'journal_id':parent.journal_id}" domain="[('plan_id','&lt;&gt;',False)]"/>
<field name="analytics_id" groups="purchase.group_analytic_accounting" context="{'journal_id':parent.journal_id}" domain="[('plan_id','&lt;&gt;',False)]"/>
</field>
</field>
</record>
@@ -24,7 +24,7 @@ var CompareListView = ListView.extend({
generate_purchase_order: function () {
var self = this;
new Model(self.dataset.model).call("generate_po",[self.dataset.context.tender_id,self.dataset.context]).then(function(result) {
self.ViewManager.ActionManager.history_back();
self.ViewManager.action_manager.history_back();
});
},
});
@@ -673,6 +673,7 @@ class account_analytic_account(osv.osv):
'fiscal_position_id': fpos_id,
'payment_term_id': partner_payment_term,
'company_id': contract.company_id.id or False,
'user_id': contract.manager_id.id or uid,
'comment': _("This invoice covers the following period: %s - %s") % (next_date.date(), new_date.date()),
}
return invoice
+1 -1
View File
@@ -139,7 +139,7 @@
</group>
</group>
<field name="product_manager" position="attributes">
<attribute name="context">{'default_groups_ref': ['base.group_user', 'base.group_sale_manager', 'stock.group_stock_manager']}</attribute>
<attribute name="context" translation="off">{'default_groups_ref': ['base.group_user', 'base.group_sale_manager', 'stock.group_stock_manager']}</attribute>
</field>
<group name="procurement" position="after" >
<group string="Supply Chain Information" attrs="{'invisible': [('type', '=', 'service')]}" groups="base.group_user">
+20 -26
View File
@@ -17,36 +17,24 @@ class report_stock_forecast(models.Model):
def init(self, cr):
tools.drop_view_if_exists(cr, 'report_stock_forecast')
cr.execute("""CREATE or REPLACE VIEW report_stock_forecast AS (SELECT
max(id) as id,
MIN(id) as id,
product_id as product_id,
date as date,
sum(product_qty) AS quantity,
sum(sum(product_qty)) OVER(PARTITION BY product_id ORDER BY date ) AS cumulative_quantity
sum(sum(product_qty)) OVER (PARTITION BY product_id ORDER BY date) AS cumulative_quantity
FROM
(SELECT
max(id) as id,
MIN(id) as id,
MAIN.product_id as product_id,
SUB.date as date,
CASE WHEN MAIN.date = SUB.date THEN sum(MAIN.product_qty) ELSE 0 END as product_qty
FROM
(SELECT
max(sq.id) as id,
MIN(sq.id) as id,
sq.product_id,
CASE WHEN sm.date < CURRENT_DATE
THEN to_date(to_char(sm.date, 'YYYY/MM/DD'), 'YYYY/MM/DD')
ELSE to_date(to_char(CURRENT_DATE, 'YYYY/MM/DD'), 'YYYY/MM/DD')
END AS date,
date_trunc('week', to_date(to_char(CURRENT_DATE, 'YYYY/MM/DD'), 'YYYY/MM/DD')) as date,
SUM(sq.qty) AS product_qty
FROM
(SELECT min(date_expected) AS date FROM stock_move AS sm
LEFT JOIN
stock_location source_location ON sm.location_id = source_location.id
LEFT JOIN
stock_location dest_location ON sm.location_dest_id = dest_location.id
WHERE
sm.state IN ('confirmed','assigned','waiting') AND
(dest_location.usage = 'internal' AND source_location.usage != 'internal')
or (source_location.usage = 'internal' AND dest_location.usage != 'internal')) AS sm,
stock_quant as sq
LEFT JOIN
product_product ON product_product.id = sq.product_id
@@ -57,9 +45,12 @@ class report_stock_forecast(models.Model):
GROUP BY date, sq.product_id
UNION ALL
SELECT
max(sm.id) as id,
MIN(-sm.id) as id,
sm.product_id,
to_date(to_char(sm.date_expected, 'YYYY/MM/DD'), 'YYYY/MM/DD') AS date,
CASE WHEN sm.date_expected > CURRENT_DATE
THEN date_trunc('week', to_date(to_char(sm.date_expected, 'YYYY/MM/DD'), 'YYYY/MM/DD'))
ELSE date_trunc('week', to_date(to_char(CURRENT_DATE, 'YYYY/MM/DD'), 'YYYY/MM/DD')) END
AS date,
SUM(sm.product_qty) AS product_qty
FROM
stock_move as sm
@@ -75,9 +66,12 @@ class report_stock_forecast(models.Model):
GROUP BY sm.date_expected,sm.product_id
UNION ALL
SELECT
max(sm.id) as id,
MIN(-sm.id) as id,
sm.product_id,
to_date(to_char(sm.date_expected, 'YYYY/MM/DD'), 'YYYY/MM/DD') AS date,
CASE WHEN sm.date_expected > CURRENT_DATE
THEN date_trunc('week', to_date(to_char(sm.date_expected, 'YYYY/MM/DD'), 'YYYY/MM/DD'))
ELSE date_trunc('week', to_date(to_char(CURRENT_DATE, 'YYYY/MM/DD'), 'YYYY/MM/DD')) END
AS date,
SUM(-(sm.product_qty)) AS product_qty
FROM
stock_move as sm
@@ -96,18 +90,18 @@ class report_stock_forecast(models.Model):
(SELECT DISTINCT date
FROM
(
SELECT CURRENT_DATE AS DATE
SELECT date_trunc('week', CURRENT_DATE) AS DATE
UNION ALL
SELECT to_date(to_char(sm.date_expected, 'YYYY/MM/DD'), 'YYYY/MM/DD') AS date
SELECT date_trunc('week', to_date(to_char(sm.date_expected, 'YYYY/MM/DD'), 'YYYY/MM/DD')) AS date
FROM stock_move sm
LEFT JOIN
stock_location source_location ON sm.location_id = source_location.id
LEFT JOIN
stock_location dest_location ON sm.location_dest_id = dest_location.id
WHERE
sm.state IN ('confirmed','assigned','waiting') and
(dest_location.usage = 'internal' AND source_location.usage != 'internal')
or (source_location.usage = 'internal' AND dest_location.usage != 'internal')) AS DATE_SEARCH)
sm.state IN ('confirmed','assigned','waiting') and sm.date_expected > CURRENT_DATE and
((dest_location.usage = 'internal' AND source_location.usage != 'internal')
or (source_location.usage = 'internal' AND dest_location.usage != 'internal'))) AS DATE_SEARCH)
SUB ON (SUB.date IS NOT NULL)
GROUP BY MAIN.product_id,SUB.date, MAIN.date
) AS FINAL
+4
View File
@@ -3711,9 +3711,13 @@ class stock_warehouse(osv.osv):
self._handle_renaming(cr, uid, warehouse, name, vals.get('code', warehouse.code), context=context_with_inactive)
if warehouse.in_type_id:
seq_obj.write(cr, uid, warehouse.in_type_id.sequence_id.id, {'name': name + _(' Sequence in'), 'prefix': vals.get('code', warehouse.code) + '\IN\\'}, context=context)
if warehouse.out_type_id:
seq_obj.write(cr, uid, warehouse.out_type_id.sequence_id.id, {'name': name + _(' Sequence out'), 'prefix': vals.get('code', warehouse.code) + '\OUT\\'}, context=context)
if warehouse.pack_type_id:
seq_obj.write(cr, uid, warehouse.pack_type_id.sequence_id.id, {'name': name + _(' Sequence packing'), 'prefix': vals.get('code', warehouse.code) + '\PACK\\'}, context=context)
if warehouse.pick_type_id:
seq_obj.write(cr, uid, warehouse.pick_type_id.sequence_id.id, {'name': name + _(' Sequence picking'), 'prefix': vals.get('code', warehouse.code) + '\PICK\\'}, context=context)
if warehouse.int_type_id:
seq_obj.write(cr, uid, warehouse.int_type_id.sequence_id.id, {'name': name + _(' Sequence internal'), 'prefix': vals.get('code', warehouse.code) + '\INT\\'}, context=context)
if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'):
for cmd in vals.get('resupply_wh_ids'):
@@ -43,3 +43,12 @@ class purchase_order(models.Model):
if not self.env.context.get('no_invoice_policy_check'):
self._check_invoice_policy()
super(purchase_order, self).wkf_confirm_order()
class procurement_order(models.Model):
_inherit = 'procurement.order'
@api.model
def update_origin_po(self, po, proc):
super(procurement_order, self).update_origin_po(po, proc)
if proc.sale_line_id and not (proc.origin in po.origin):
po.sudo().write({'origin': po.origin+', '+proc.origin})
@@ -128,6 +128,7 @@ var ListView = View.extend( /** @lends instance.web.ListView# */ {
// the view's number of records per page (|| section)
this._limit = (this.options.limit ||
this.defaults.limit ||
parseInt(this.fields_view.arch.attrs.limit, 10) ||
(this.getParent().action || {}).limit ||
80);
},
@@ -423,7 +423,7 @@ var SearchView = Widget.extend(/** @lends instance.web.SearchView# */{
if (this.$buttons) {
var fields_def = new Model(this.dataset.model).call('fields_get', {
context: this.dataset.context
context: this.dataset.get_context()
});
this.groupby_menu = new GroupByMenu(this, this.groupbys, fields_def);
@@ -9,7 +9,7 @@ define(['summernote/core/range', 'summernote/core/dom'], function (range, dom) {
var makeSnapshot = function () {
var rng = range.create();
var emptyBookmark = {s: {path: [0], offset: 0}, e: {path: [0], offset: 0}};
var emptyBookmark = {s: {path: [], offset: 0}, e: {path: [], offset: 0}};
return {
contents: $editable.html(),
@@ -144,6 +144,8 @@ editor.Class = Widget.extend({
d.resolve();
}).on('hidden.bs.modal', function () {
d.reject();
}).on('keydown.dismiss.bs.modal', function (event) {
event.stopImmediatePropagation();
});
d.always(function () {
$dialog.remove();
@@ -531,7 +531,8 @@ var BuildingBlock = Widget.extend({
this.snippets.push($snippet.get(0));
}
this.$active_snipped_id = $snippet;
this.create_overlay(this.$active_snipped_id);
this.create_snippet_editor(this.$active_snipped_id);
this.cover_target($snippet.data('overlay'), $snippet);
this.snippet_focus($snippet);
}
this.$snippet.trigger('snippet-activated', $snippet);
@@ -539,13 +540,11 @@ var BuildingBlock = Widget.extend({
$snippet.trigger('snippet-activated', $snippet);
}
},
create_overlay: function ($snippet) {
create_snippet_editor: function ($snippet) {
if (typeof $snippet.data("snippet-editor") === 'undefined') {
var $targets = this.activate_overlay_zones($snippet);
if (!$targets.length) return;
if (!this.activate_overlay_zones($snippet).length) return;
$snippet.data("snippet-editor", new Editor(this, $snippet));
}
this.cover_target($snippet.data('overlay'), $snippet);
},
// activate drag and drop for the snippets in the snippet toolbar
@@ -657,22 +656,12 @@ var BuildingBlock = Widget.extend({
animation.start(true, $target);
// drop_and_build_snippet
self.create_overlay($target);
if ($target.data("snippet-editor")) {
$target.data("snippet-editor").drop_and_build_snippet();
}
for (var k in self.templateOptions) {
self.templateOptions[k].selector.all($target).each(function () {
var $snippet = $(this);
self.create_overlay($snippet);
if ($snippet.data("snippet-editor")) {
$snippet.data("snippet-editor").drop_and_build_snippet();
}
});
}
self.call_for_all_snippets($target, function (editor, $snippet) {
editor.drop_and_build_snippet();
});
self.create_snippet_editor($target);
self.cover_target($target.data('overlay'), $target);
$target.closest(".o_editable").trigger("content_changed");
// end
self.make_active($target);
},0);
@@ -683,6 +672,20 @@ var BuildingBlock = Widget.extend({
});
},
// call a method on a snippet and all his children
call_for_all_snippets: function ($snippet, callback) {
var self = this;
$snippet.add(globalSelector.all($snippet)).each(function () {
var $snippet = $(this);
setTimeout(function () {
self.create_snippet_editor($snippet);
if ($snippet.data("snippet-editor")) {
callback.call(self, $snippet.data("snippet-editor"), $snippet);
}
});
});
},
// return the original snippet in the editor bar from a snippet id (string)
get_snippet_from_id: function(id){
return $('.oe_snippet').filter(function(){
@@ -845,14 +848,12 @@ var BuildingBlock = Widget.extend({
var timer;
$target.closest('.o_editable').on("content_changed", function (event) {
if ($target.data('overlay') && $target.data('overlay').hasClass("oe_active")) {
clearTimeout(timer);
timer = setTimeout(function () {
if ($target.data('overlay')) {
self.cover_target($target.data('overlay'), $target);
}
},50);
}
clearTimeout(timer);
timer = setTimeout(function () {
if ($target.data('overlay') && $target.data('overlay').hasClass("oe_active")) {
self.cover_target($target.data('overlay'), $target);
}
},50);
});
var resize = function () {
@@ -985,7 +986,9 @@ var Editor = Class.extend({
self.buildingBlock.editor_busy = false;
self.get_parent_block();
setTimeout(function () {self.buildingBlock.create_overlay(self.$target);},0);
setTimeout(function () {
self.BuildingBlock.cover_target(self.$target.data('overlay'), self.$target);
},0);
},
load_style_options: function () {
@@ -1022,7 +1025,10 @@ var Editor = Class.extend({
get_parent_block: function () {
var self = this;
var $button = this.$overlay.find('.oe_snippet_parent');
var $parent = globalSelector.closest(this.$target.parent());
var $parent = this.$target.parents().filter(function () { return $(this).data("snippet-editor"); });
if (!$parent.length) {
$parent = globalSelector.closest(this.$target.parent());
}
if ($parent.length) {
$button.removeClass("hidden");
$button.off("click").on('click', function (event) {
@@ -1056,12 +1062,13 @@ var Editor = Class.extend({
var $clone = this.$target.clone(false);
this.buildingBlock.parent.rte.historyRecordUndo(this.$target);
this.$target.after($clone);
for (var i in this.styles){
this.styles[i].on_clone($clone);
}
this.buildingBlock.create_overlay(this.$target);
this.BuildingBlock.call_for_all_snippets($clone, function (editor, $snippet) {
for (var i in editor.styles){
editor.styles[i].on_clone($snippet);
}
});
return false;
},
@@ -1072,9 +1079,11 @@ var Editor = Class.extend({
this.buildingBlock.parent.rte.historyRecordUndo(this.$target);
var index = _.indexOf(this.buildingBlock.snippets, this.$target.get(0));
for (var i in this.styles){
this.styles[i].on_remove();
}
this.BuildingBlock.call_for_all_snippets(this.$target, function (editor, $snippet) {
for (var i in editor.styles){
editor.styles[i].on_remove();
}
});
delete this.buildingBlock.snippets[index];
var $editable = this.$target.closest(".o_editable");
@@ -319,20 +319,20 @@ var colorpicker = Option.extend({
var background = Option.extend({
start: function ($change_target) {
this.$bg = $change_target || this.$target;
this.$target = $change_target || this.$target;
this._super();
var src = this.$bg.css("background-image").replace(/url\(['"]*|['"]*\)|^none$/g, "");
if (this.$bg.hasClass('oe_custom_bg')) {
var src = this.$target.css("background-image").replace(/url\(['"]*|['"]*\)|^none$/g, "");
if (this.$target.hasClass('oe_custom_bg')) {
this.$el.find('li[data-choose_image]').data("background", src).attr("data-background", src);
}
},
background: function(type, value, $li) {
if (value && value.length) {
this.$bg.attr("style", 'background-image: url(' + value + '); ' + (this.$bg.attr("style") || '').replace(/background-image:[^;]+/, '') );
this.$bg.addClass("oe_img_bg");
this.$target.attr("style", 'background-image: url(' + value + '); ' + (this.$target.attr("style") || '').replace(/background-image:[^;]+/, '') );
this.$target.addClass("oe_img_bg");
} else {
this.$bg.css("background-image", "");
this.$bg.removeClass("oe_img_bg").removeClass("oe_custom_bg");
this.$target.css("background-image", "");
this.$target.removeClass("oe_img_bg").removeClass("oe_custom_bg");
}
},
select_class : function(type, value, $li) {
@@ -345,7 +345,7 @@ var background = Option.extend({
var self = this;
var $image = $('<img class="hidden"/>');
$image.attr("src", value);
$image.appendTo(self.$bg);
$image.appendTo(self.$target);
var _editor = new widget.MediaDialog(null, $image[0]);
_editor.appendTo(document.body);
@@ -356,8 +356,8 @@ var background = Option.extend({
$image.remove();
self.$el.find('li[data-choose_image]').data("background", value).attr("data-background", value);
self.background(type, value,$li);
self.$bg.addClass('oe_custom_bg');
self.$bg.trigger("snippet-option-change", [self]);
self.$target.addClass('oe_custom_bg');
self.$target.trigger("snippet-option-change", [self]);
self.set_active();
});
_editor.on('cancel', self, function () {
@@ -366,9 +366,13 @@ var background = Option.extend({
},
set_active: function () {
var self = this;
var src = this.$bg.css("background-image").replace(/url\(['"]*|['"]*\)|^none$/g, "");
var src = this.$target.css("background-image").replace(/url\(['"]*|['"]*\)|^none$/g, "");
this._super();
if (this.$target.hasClass('oe_custom_bg')) {
this.$el.find('li[data-choose_image]').data("background", src).attr("data-background", src);
}
this.$el.find('li[data-background]:not([data-background=""])')
.removeClass("active")
.each(function () {
+19 -10
View File
@@ -26,7 +26,7 @@ dom.hasContentAfter = function (node) {
var next;
while (node.nextSibling) {
next = node.nextSibling;
if (next.tagName || next.textContent.match(/\S|\u00A0/) || dom.isBR(next)) return next;
if (next.tagName || dom.isVisibleText(next) || dom.isBR(next)) return next;
node = next;
}
};
@@ -34,7 +34,7 @@ dom.hasContentBefore = function (node) {
var prev;
while (node.previousSibling) {
prev = node.previousSibling;
if (prev.tagName || prev.textContent.match(/\S|\u00A0/) || dom.isBR(prev)) return prev;
if (prev.tagName || dom.isVisibleText(prev) || dom.isBR(prev)) return prev;
node = prev;
}
};
@@ -138,7 +138,7 @@ dom.mergeFilter = function (prev, cur, parent) {
if (prev && prev.tagName === "P" && dom.isText(cur)) {
return true;
}
if (prev && dom.isText(cur) && !cur.textContent.match(/\S|\u00A0/) && (dom.isText(prev) || prev.textContent.match(/\S|\u00A0/))) {
if (prev && dom.isText(cur) && !dom.isVisibleText(cur) && (dom.isText(prev) || dom.isVisibleText(prev))) {
return true;
}
if (prev && !dom.isBR(prev) && dom.isEqual(prev, cur) &&
@@ -382,7 +382,7 @@ dom.removeSpace = function (node, begin, so, end, eo) {
if (cur === end) add = false;
// remove begin empty text node
if (node.childNodes.length > 1 && dom.isText(cur) && !cur.textContent.match(/\S|\u00A0/)) {
if (node.childNodes.length > 1 && dom.isText(cur) && !dom.isVisibleText(cur)) {
removed = true;
if (cur === begin) {
so = 0;
@@ -579,7 +579,7 @@ dom.removeBetween = function (sc, so, ec, eo, towrite) {
}
eo = so;
if(!dom.isBR(sc) && !sc.textContent.match(/\S|\u00A0/) && !dom.isText(dom.hasContentBefore(sc)) && !dom.isText(dom.hasContentAfter(sc))) {
if(!dom.isBR(sc) && !dom.isVisibleText(sc) && !dom.isText(dom.hasContentBefore(sc)) && !dom.isText(dom.hasContentAfter(sc))) {
ancestor = dom.node(sc);
var text = document.createTextNode('\u00A0');
$(sc).before(text);
@@ -699,6 +699,9 @@ dom.isContentEditable = function (node) {
return $(node).closest('[contenteditable]').is('[contenteditable="true"]');
};
dom.isPre = function (node) {
return node.tagName === "PRE";
};
dom.isFont = function (node) {
var nodeName = node && node.nodeName.toUpperCase();
return node && (nodeName === "FONT" ||
@@ -1025,6 +1028,8 @@ $.summernote.pluginEvents.enter = function (event, editor, layoutInfo) {
}
var node = dom.node(r.sc);
var exist = r.sc.childNodes[r.so] || r.sc;
exist = dom.isVisibleText(exist) || dom.isBR(exist) ? exist : dom.hasContentAfter(exist) || (dom.hasContentBefore(exist) || exist);
var last = node;
while (node && dom.isSplitable(node) && !dom.isList(node)) {
last = node;
@@ -1042,7 +1047,7 @@ $.summernote.pluginEvents.enter = function (event, editor, layoutInfo) {
} while (node && dom.isBR(node));
// create an other br because the user can't see the new line with only br in a block
if (!node) {
if (!node && (!br.nextElementSibling || !dom.isBR(br.nextElementSibling))) {
$(br).before($("<br/>")[0]);
}
node = br.nextSibling || br;
@@ -1051,7 +1056,11 @@ $.summernote.pluginEvents.enter = function (event, editor, layoutInfo) {
node = br;
} else if (!r.so && r.isOnList() && !r.sc.textContent.length && !dom.ancestor(r.sc, dom.isLi).nextElementSibling) {
// double enter on the end of a list = new line out of the list
node = $('<p></p>').append(br).insertAfter(dom.ancestor(r.sc, dom.isList))[0];
$('<p></p>').append(br).insertAfter(dom.ancestor(r.sc, dom.isList));
node = br;
} else if (dom.isBR(exist) && $(r.sc).closest('blockquote, pre').length && !dom.hasContentAfter($(exist.parentNode).closest('blockquote *, pre *').length ? exist.parentNode : exist)) {
// double enter on the end of a blockquote & pre = new line out of the list
$('<p></p>').append(br).insertAfter($(r.sc).closest('blockquote, pre'));
node = br;
} else if (last === r.sc) {
if (dom.isBR(last)) {
@@ -1071,7 +1080,7 @@ $.summernote.pluginEvents.enter = function (event, editor, layoutInfo) {
$(cur).html(br);
}
}
if (!node.textContent.match(/\S|\u00A0/)) {
if (!dom.isVisibleText(node)) {
node = dom.firstChild(node);
$(dom.node( dom.isBR(node) ? node.parentNode : node )).html(br);
node = br;
@@ -1105,7 +1114,7 @@ $.summernote.pluginEvents.visible = function (event, editor, layoutInfo) {
while (node.parentNode) {
if (dom.isForbiddenNode(node)) {
var text = node.previousSibling;
if (text && dom.isText(text) && text.textContent.match(/\S|\u00A0/)) {
if (text && dom.isText(text) && dom.isVisibleText(text)) {
range.create(text, text.textContent.length, text, text.textContent.length).select();
} else {
text = node.parentNode.insertBefore(document.createTextNode( "." ), node);
@@ -1715,7 +1724,7 @@ $.summernote.pluginEvents.formatBlock = function (event, editor, layoutInfo, sTa
// fix by odoo because if you select a style in a li with no p tag all the ul is wrapped by the style tag
var nodes = dom.listBetween(r.sc, r.ec);
for (var i=0; i<nodes.length; i++) {
if (dom.isBR(nodes[i]) || (dom.isText(nodes[i]) && nodes[i].textContent.match(/\S|\u00A0/)) || dom.isB(nodes[i]) || dom.isU(nodes[i]) || dom.isS(nodes[i]) || dom.isI(nodes[i]) || dom.isFont(nodes[i])) {
if (dom.isBR(nodes[i]) || (dom.isText(nodes[i]) && dom.isVisibleText(nodes[i])) || dom.isB(nodes[i]) || dom.isU(nodes[i]) || dom.isS(nodes[i]) || dom.isI(nodes[i]) || dom.isFont(nodes[i])) {
var ancestor = dom.ancestor(nodes[i], isFormatNode);
if (!ancestor) {
dom.wrap(nodes[i], sTagName);
+14 -4
View File
@@ -16,6 +16,7 @@ var dom = $.summernote.core.dom;
var Dialog = Widget.extend({
events: {
'hidden.bs.modal': 'destroy',
'keydown.dismiss.bs.modal': 'stop_escape',
'click button.save': 'save',
'click button[data-dismiss="modal"]': 'cancel',
},
@@ -38,6 +39,11 @@ var Dialog = Widget.extend({
close: function () {
this.$el.modal('hide');
},
stop_escape: function(event) {
if($(".modal.in").length>0 && event.which == 27){
event.stopPropagation();
}
}
});
/**
@@ -251,6 +257,7 @@ var ImageDialog = Widget.extend({
//'change select.image-style': 'preview_image',
'click .existing-attachments [data-src]': 'select_existing',
'click .existing-attachment-remove': 'try_remove',
'keydown.dismiss.bs.modal': function(){},
}),
init: function (parent, media, options) {
this._super();
@@ -339,7 +346,7 @@ var ImageDialog = Widget.extend({
return this.media;
},
clear: function () {
this.media.className = this.media.className.replace(/(^|\s)(img(\s|$)|img-[^\s]*)/g, ' ');
this.media.className = this.media.className.replace(/(^|\s+)(img(\s|$)|img-(?!circle|rounded|thumbnail)[^\s]*)/g, ' ');
},
cancel: function () {
this.trigger('cancel');
@@ -609,6 +616,7 @@ var fontIconsDialog = Widget.extend({
$(".font-icons-icon").removeClass("font-icons-selected");
$(event.target).addClass("font-icons-selected");
},
'keydown.dismiss.bs.modal': function(){},
}),
// extract list of font (like awsome) from the cheatsheet.
@@ -662,12 +670,14 @@ var fontIconsDialog = Widget.extend({
var icons = this.icons;
var style = this.media.attributes.style ? this.media.attributes.style.value : '';
var classes = (this.media.className||"").split(/\s+/);
var custom_classes = /^fa(-[1-5]x|spin|rotate-(9|18|27)0|flip-(horizont|vertic)al|fw|border)?$/;
var non_fa_classes = _.reject(classes, function (cls) {
return self.getFont(cls);
return self.getFont(cls) || custom_classes.test(cls);
});
var final_classes = non_fa_classes.concat(this.get_fa_classes());
if (this.media.tagName !== "SPAN") {
var media = document.createElement('span');
$(media).data($(this.media).data());
$(this.media).replaceWith(media);
this.media = media;
style = style.replace(/\s*width:[^;]+/, '');
@@ -828,7 +838,8 @@ var VideoDialog = Widget.extend({
'change input#urlvideo': 'change_input',
'keyup input#urlvideo': 'change_input',
'change input#embedvideo': 'change_input',
'keyup input#embedvideo': 'change_input'
'keyup input#embedvideo': 'change_input',
'keydown.dismiss.bs.modal': function(){},
}),
init: function (parent, media) {
this._super();
@@ -842,7 +853,6 @@ var VideoDialog = Widget.extend({
if ($media.hasClass("media_iframe_video")) {
var src = $media.data('src');
this.$("input#urlvideo").val(src);
this.$("#autoplay").attr("checked", src.indexOf('autoplay=1') != -1);
this.get_video();
} else {
this.add_class = "pull-left";
@@ -81,7 +81,7 @@ options.registry.slider = options.Class.extend({
this.$indicators.append('<li data-target="#' + this.id + '" data-slide-to="' + cycle + '"></li>');
// clone the best candidate from template to use new features
var $snippets = this.BuildingBlock.$snippets;
var $snippets = this.buildingBlock.$snippets;
//since saas-6, all snippets must start by s_
var selection = this.$target.closest('[class*="s_"');
if (_.isUndefined(selection)) {
@@ -174,7 +174,7 @@ options.registry.carousel = options.registry.slider.extend({
};
this.$target.on('slid.bs.carousel', function () {
if(self.editor && self.editor.styles.background) {
self.editor.styles.background.$bg = self.$target.find(".item.active");
self.editor.styles.background.$target = self.$target.find(".item.active");
self.editor.styles.background.set_active();
}
self.$target.carousel("pause");
@@ -264,9 +264,13 @@ options.registry.marginAndResize = options.Class.extend({
} else {
var xy = event['page'+XY];
var current = resize[2] || 0;
var margin_dir = {s:'bottom', n: 'top', w: 'left', e: 'right'}[compass];
var real_margin = parseInt(self.$target.css('margin-'+margin_dir));
_.each(resize[0], function (val, key) {
if (self.$target.hasClass(val)) {
current = key;
} else if (resize[1][key] === real_margin) {
current = key;
}
});
var begin = current;
@@ -362,12 +366,12 @@ options.registry.marginAndResize = options.Class.extend({
if (offset === 0) overlay_class+= " block-w-left";
var mb = _class.match(/mb([0-9-]+)/i);
mb = mb ? +mb[1] : 0;
mb = mb ? +mb[1] : parseInt(this.$target.css('margin-bottom'));
if (mb >= 128) overlay_class+= " block-s-bottom";
else if (!mb) overlay_class+= " block-s-top";
var mt = _class.match(/mt([0-9-]+)/i);
mt = mt ? +mt[1] : 0;
mt = mt ? +mt[1] : parseInt(this.$target.css('margin-top'));
if (mt >= 128) overlay_class+= " block-n-top";
else if (!mt) overlay_class+= " block-n-bottom";
@@ -38,13 +38,9 @@ options.registry.gallery = options.Class.extend({
this._temp_col = this.$el.find("data-columns").data("columns");
},
drop_and_build_snippet: function() {
var uuid = 0;
$(".carousel").each(function () {
var id = parseInt(($(this).attr('id') || '0').replace(/[^0-9]/, ''));
if (id > uuid) uuid = id;
});
this.$target.find('.carousel').attr('id', 'slideshow_'+ (uuid+1));
this.$target.find('[data-target]').attr('data-target', '#slideshow_'+ (uuid+1));
var uuid = new Date().getTime();
this.$target.find('.carousel').attr('id', 'slideshow_' + uuid);
this.$target.find('[data-target]').attr('data-target', '#slideshow_' + uuid);
},
styling : function(type, value) {
var classes = this.$el.find('li[data-styling]').map(function () {
@@ -194,18 +190,13 @@ options.registry.gallery = options.Class.extend({
var self = this,
$imgs = $(this.get_imgs()),
urls = $imgs.map(function() { return $(this).attr("src"); } ).get(),
uuid = 0;
$(".carousel").each(function () {
var id = parseInt(($(this).attr('id') || '0').replace(/[^0-9]/, ''));
if (id > uuid) uuid = id;
});
urls = $imgs.map(function() { return $(this).attr("src"); } ).get();
var params = {
srcs : urls,
index: 1,
index: 0,
title: "",
interval : this.$target.data("interval") || false,
id: "slideshow_" + (uuid+1)
id: "slideshow_" + new Date().getTime()
},
$slideshow = $(qweb.render('website.gallery.slideshow', params));
this.replace($slideshow);
+40 -14
View File
@@ -29,6 +29,12 @@ var Theme = Widget.extend({
template: 'website.theme_customize',
events: {
'change input[data-xmlid],input[data-enable],input[data-disable]': 'change_selection',
'click label:has(input[data-xmlid],input[data-enable],input[data-disable])': function (event) {
var self = this;
this.time_select = _.defer(function () {
self.on_select($(event.target).find('input'));
});
},
'click .close': 'close',
'click': 'click',
},
@@ -39,7 +45,7 @@ var Theme = Widget.extend({
this.flag = false;
this.$el.addClass("theme_customize_modal");
this.active_select_tags();
this.$inputs = $("input[data-xmlid],input[data-enable],input[data-disable]");
this.$inputs = this.$("input[data-xmlid],input[data-enable],input[data-disable]");
setTimeout(function () {self.$el.addClass('in');}, 0);
this.keydown_escape = function (event) {
if (event.keyCode === 27) {
@@ -92,10 +98,10 @@ var Theme = Widget.extend({
self.$inputs.filter('[data-xmlid=""]').prop("checked", true).change();
self.$inputs.filter('[data-xmlid]:not([data-xmlid=""])').each(function () {
if (!_.difference(self.get_xml_ids($(this)), data[1]).length) {
$(this).prop("checked", false).change();
$(this).prop("checked", false).trigger("change", true);
}
if (!_.difference(self.get_xml_ids($(this)), data[0]).length) {
$(this).prop("checked", true).change();
$(this).prop("checked", true).trigger("change", true);
}
});
}).fail(function (d, error) {
@@ -174,9 +180,8 @@ var Theme = Widget.extend({
return $.Deferred();
}
},
enable_disable: function (data, enable) {
if (!data) return;
this.$('#'+data.split(",").join(", #")).each(function () {
enable_disable: function ($inputs, enable) {
$inputs.each(function () {
var check = $(this).prop("checked");
var $label = $(this).closest("label");
$(this).prop("checked", enable);
@@ -187,21 +192,31 @@ var Theme = Widget.extend({
}
});
},
change_selection: function (event) {
change_selection: function (event, init_mode) {
var self = this;
if (this.$el.hasClass("loading")) return;
clearTimeout(this.time_select);
if (this.$el.hasClass("loading")) return; // prevent to change selection when css is loading
var $option = $(event.target),
$options = $option,
checked = $option.prop("checked");
if (checked) {
this.enable_disable($option.data('enable'), true);
this.enable_disable($option.data('disable'), false);
if($option.data('enable')) {
var $inputs = this.get_inputs($option.data('enable'));
$options = $options.add($inputs.filter(':not(:checked)'));
this.enable_disable($inputs, true);
}
if($option.data('disable')) {
var $inputs = this.get_inputs($option.data('disable'));
$options = $options.add($inputs.filter(':checked'));
this.enable_disable($inputs, false);
}
$option.closest("label").addClass("checked");
} else {
$option.closest("label").removeClass("checked");
}
$option.prop("checked", checked);
var $enable = this.$inputs.filter('[data-xmlid]:checked');
$enable.closest("label").addClass("checked");
@@ -236,14 +251,25 @@ var Theme = Widget.extend({
clearTimeout(this.timer);
if (this.flag) {
this.timer = setTimeout(function () {
this.timer = _.defer(function () {
if (!init_mode) self.on_select($options);
self.update_style(self.get_xml_ids($enable), self.get_xml_ids($disable), self.reload);
self.reload = false;
},0);
});
} else {
this.timer = setTimeout(function () { self.reload = false; },0);
this.timer = _.defer(function () {
if (!init_mode) self.on_select($options);
self.reload = false;
});
}
},
/* Method call when the user change the selection or click on an input
* @values: all changed inputs
*/
on_select: function ($inputs) {
clearTimeout(this.time_select);
console.log($inputs.get());
},
click: function (event) {
if (!$(event.target).closest("#theme_customize_modal > *").length) {
this.close();
+5 -1
View File
@@ -161,7 +161,7 @@ class product_template(osv.Model):
class product_product(osv.Model):
_inherit = "product.product"
# Wrapper for call_kw with inherits
# Wrappers for call_kw with inherits
def open_website_url(self, cr, uid, ids, context=None):
template_id = self.browse(cr, uid, ids, context=context).product_tmpl_id.id
return self.pool['product.template'].open_website_url(cr, uid, [template_id], context=context)
@@ -170,6 +170,10 @@ class product_product(osv.Model):
template_id = self.browse(cr, uid, ids, context=context).product_tmpl_id.id
return self.pool['product.template'].website_publish_button(cr, uid, [template_id], context=context)
def website_publish_button(self, cr, uid, ids, context=None):
template_id = self.browse(cr, uid, ids, context=context).product_tmpl_id.id
return self.pool['product.template'].website_publish_button(cr, uid, [template_id], context=context)
class product_attribute(osv.Model):
_inherit = "product.attribute"
_columns = {
@@ -29,6 +29,10 @@
}
/* ---- Default Styles ---- */
.o_alternative_product {
margin: auto;
}
.oe_product {
position: relative;
}
+4 -4
View File
@@ -517,12 +517,12 @@
<h3>Suggested alternatives:</h3>
<div class='row mt16' style="">
<t t-foreach="product.alternative_product_ids" t-as="product">
<div class='col-md-2 thumbnail' style='width: 170px; height:130px; float:left; display:inline; margin-right: 10px;'>
<div class='mt16 text-center'>
<span t-field="product.image_small" t-field-options='{"widget": "image", "class": "img-rounded shadow" }'/>
<div class='col-md-2 thumbnail' style='width: 170px; height:130px; float:left; display:inline; margin-right: 10px; overflow:hidden;'>
<div class='mt16 text-center' style="height: 100%;">
<div t-field="product.image_small" t-field-options='{"widget": "image", "class": "img-rounded shadow o_alternative_product" }'/>
<h5>
<a t-attf-href="/shop/product/#{ slug(product) }" style="display: block">
<span t-field='product.name' style="display: block"/>
<span t-att-title='product.name' t-field='product.name' style="display: block;"/>
</a>
</h5>
</div>
+17
View File
@@ -0,0 +1,17 @@
Russia, 2015-08-14
IT Libertas agrees to the terms of the Odoo Corporate Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
IT Libertas LLC info@itlibertas.com https://github.com/itlibertas
List of contributors:
Denis Baranov baranov@itlibertas.com https://github.com/barachka
Tatiana Deribina deribina@itlibertas.com https://github.com/Sapfiriana
Aleksandr Shevelev shevelev@itlibertas.com https://github.com/alexalv
+11
View File
@@ -0,0 +1,11 @@
France, 2015-08-18
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Florian Hatat github@hatat.me https://github.com/flh
+3 -5
View File
@@ -107,11 +107,9 @@ class ir_http(osv.AbstractModel):
datas = attach[0]['datas'] or ''
name = attach[0]['name']
if not datas:
if name.startswith(('http://', 'https://', '/')):
return werkzeug.utils.redirect(name, 301)
else:
return werkzeug.wrappers.Response(status=204) # NO CONTENT
if (not datas and name != request.httprequest.path and
name.startswith(('http://', 'https://', '/'))):
return werkzeug.utils.redirect(name, 301)
response = werkzeug.wrappers.Response()
server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
+5
View File
@@ -259,6 +259,11 @@
<rng:optional><rng:attribute name="decoration-primary"/></rng:optional>
<rng:optional><rng:attribute name="decoration-success"/></rng:optional>
<rng:optional><rng:attribute name="decoration-warning"/></rng:optional>
<rng:optional>
<rng:attribute name="limit">
<rng:data type="int"/>
</rng:attribute>
</rng:optional>
<rng:optional>
<rng:attribute name="editable">
<rng:choice>
+56 -6
View File
@@ -5,6 +5,18 @@ import openerp.tests.common as common
class test_search(common.TransactionCase):
def patch_order(self, model, order):
m_e = self.env[model]
m_r = self.registry(model)
old_order = m_e._order
@self.addCleanup
def cleanup():
m_r._order = type(m_e)._order = old_order
m_r._order = type(m_e)._order = order
def test_00_search_order(self):
registry, cr, uid = self.registry, self.cr, self.uid
@@ -96,17 +108,13 @@ class test_search(common.TransactionCase):
self.assertEqual(test_user_ids, expected_ids, 'search on res_users did not provide expected ids or expected order')
# Do: order on many2one, but not by specifying in order parameter of search, but by overriding _order of res_users
users_class = type(users_obj)
users_order = users_class._order
users_class._order = 'country_id desc, name asc, login desc'
self.addCleanup(setattr, users_class, '_order', users_order)
self.patch_order('res.users', 'country_id desc, name asc, login desc')
user_ids = users_obj.search(cr, search_user, [])
expected_ids = [search_user, c, b, a]
test_user_ids = filter(lambda x: x in expected_ids, user_ids)
self.assertEqual(test_user_ids, expected_ids, 'search on res_users did not provide expected ids or expected order')
def test_11_indirect_inherits_m2order(self):
def test_11_indirect_inherits_m2o_order(self):
registry, cr, uid = self.registry, self.cr, self.uid
Cron = registry('ir.cron')
Users = registry('res.users')
@@ -121,6 +129,48 @@ class test_search(common.TransactionCase):
expected_ids = [cron_ids[l] for l in 'ABC']
self.assertEqual(ids, expected_ids)
def test_12_m2o_order_loop_self(self):
registry, cr, uid = self.registry, self.cr, self.uid
Cats = registry('ir.module.category')
ids = {}
def create(name, **kw):
ids[name] = Cats.create(cr, uid, dict(kw, name=name))
self.patch_order('ir.module.category', 'parent_id desc, name')
create('A')
create('B', parent_id=ids['A'])
create('C', parent_id=ids['A'])
create('D')
create('E', parent_id=ids['D'])
create('F', parent_id=ids['D'])
expected_order = [ids[x] for x in 'ADEFBC']
domain = [('id', 'in', ids.values())]
search_result = Cats.search(cr, uid, domain)
self.assertEqual(search_result, expected_order)
def test_13_m2o_order_loop_multi(self):
Users = self.env['res.users']
# will sort by login desc of the creator, then by name
self.patch_order('res.partner', 'create_uid, name')
self.patch_order('res.users', 'partner_id, login desc')
kw = dict(groups_id=[(6, 0, [self.ref('base.group_system'),
self.ref('base.group_partner_manager')])])
u1 = Users.create(dict(name='Q', login='m', **kw)).id
u2 = Users.sudo(user=u1).create(dict(name='B', login='f', **kw)).id
u3 = Users.create(dict(name='C', login='c', **kw)).id
u4 = Users.sudo(user=u2).create(dict(name='D', login='z', **kw)).id
expected_order = [u2, u4, u3, u1]
domain = [('id', 'in', [u1, u2, u3, u4])]
search_result = list(Users.search(domain)._ids)
self.assertEqual(search_result, expected_order)
if __name__ == '__main__':
unittest2.main()
+21 -13
View File
@@ -4541,7 +4541,7 @@ class BaseModel(object):
return '"%s"."%s"' % (table_alias, field)
@api.model
def _generate_m2o_order_by(self, alias, order_field, query, reverse_direction):
def _generate_m2o_order_by(self, alias, order_field, query, reverse_direction, seen):
"""
Add possibly missing JOIN to ``query`` and generate the ORDER BY clause for m2o fields,
either native m2o fields or function/related fields that are stored, including
@@ -4551,18 +4551,18 @@ class BaseModel(object):
"""
if order_field not in self._columns and order_field in self._inherit_fields:
# also add missing joins for reaching the table containing the m2o field
qualified_field = self._inherits_join_calc(alias, order_field, query)
order_field_column = self._inherit_fields[order_field][2]
qualified_field = self._inherits_join_calc(alias, order_field, query)
alias, order_field = qualified_field.replace('"', '').split('.', 1)
else:
qualified_field = '"%s"."%s"' % (alias, order_field)
order_field_column = self._columns[order_field]
assert order_field_column._type == 'many2one', 'Invalid field passed to _generate_m2o_order_by()'
if not order_field_column._classic_write and not getattr(order_field_column, 'store', False):
_logger.debug("Many2one function/related fields must be stored " \
"to be used as ordering fields! Ignoring sorting for %s.%s",
self._name, order_field)
return
_logger.debug("Many2one function/related fields must be stored "
"to be used as ordering fields! Ignoring sorting for %s.%s",
self._name, order_field)
return []
# figure out the applicable order_by for the m2o
dest_model = self.env[order_field_column._obj]
@@ -4573,13 +4573,15 @@ class BaseModel(object):
# Join the dest m2o table if it's not joined yet. We use [LEFT] OUTER join here
# as we don't want to exclude results that have NULL values for the m2o
src_table, src_field = qualified_field.replace('"', '').split('.', 1)
dst_alias, dst_alias_statement = query.add_join((src_table, dest_model._table, src_field, 'id', src_field), implicit=False, outer=True)
join = (alias, dest_model._table, order_field, 'id', order_field)
dst_alias, dst_alias_statement = query.add_join(join, implicit=False, outer=True)
return dest_model._generate_order_by_inner(dst_alias, m2o_order, query,
reverse_direction=reverse_direction)
reverse_direction=reverse_direction, seen=seen)
@api.model
def _generate_order_by_inner(self, alias, order_spec, query, reverse_direction=False):
def _generate_order_by_inner(self, alias, order_spec, query, reverse_direction=False, seen=None):
if seen is None:
seen = set()
order_by_elements = []
self._check_qorder(order_spec)
for order_part in order_spec.split(','):
@@ -4603,7 +4605,10 @@ class BaseModel(object):
inner_clauses = ['"%s"."%s"' % (alias, order_field)]
add_dir = True
elif order_column._type == 'many2one':
inner_clauses = self._generate_m2o_order_by(alias, order_field, query, do_reverse)
key = (self._name, order_column._obj, order_field)
if key not in seen:
seen.add(key)
inner_clauses = self._generate_m2o_order_by(alias, order_field, query, do_reverse, seen)
else:
continue # ignore non-readable or "non-joinable" fields
elif order_field in self._inherit_fields:
@@ -4613,7 +4618,10 @@ class BaseModel(object):
inner_clauses = [self._inherits_join_calc(alias, order_field, query)]
add_dir = True
elif order_column._type == 'many2one':
inner_clauses = self._generate_m2o_order_by(alias, order_field, query, do_reverse)
key = (parent_obj._name, order_column._obj, order_field)
if key not in seen:
seen.add(key)
inner_clauses = self._generate_m2o_order_by(alias, order_field, query, do_reverse, seen)
else:
continue # ignore non-readable or "non-joinable" fields
else: