diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 804660ede8d..ea80678840a 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -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'] diff --git a/addons/account_analytic_plans/security/ir.model.access.csv b/addons/account_analytic_plans/security/ir.model.access.csv index 9ba40d5c1c3..7a8d6e29067 100644 --- a/addons/account_analytic_plans/security/ir.model.access.csv +++ b/addons/account_analytic_plans/security/ir.model.access.csv @@ -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 diff --git a/addons/delivery/stock.py b/addons/delivery/stock.py index 69238eee0be..1b028032432 100644 --- a/addons/delivery/stock.py +++ b/addons/delivery/stock.py @@ -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 diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py index 7dffc8c042b..f040bf815d2 100644 --- a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py +++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py @@ -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)) diff --git a/addons/mass_mailing/wizard/test_mailing.py b/addons/mass_mailing/wizard/test_mailing.py index 237e1521c19..c0601ea29d5 100644 --- a/addons/mass_mailing/wizard/test_mailing.py +++ b/addons/mass_mailing/wizard/test_mailing.py @@ -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) diff --git a/addons/payment_ogone/models/ogone.py b/addons/payment_ogone/models/ogone.py index 40bd0e2a974..a70dda7d35f 100644 --- a/addons/payment_ogone/models/ogone.py +++ b/addons/payment_ogone/models/ogone.py @@ -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'), diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/default/keyboard b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/default/keyboard new file mode 100644 index 00000000000..2609b092bcc --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/default/keyboard @@ -0,0 +1,10 @@ +# KEYBOARD CONFIGURATION FILE + +# Consult the keyboard(5) manual page. + +XKBMODEL="pc105" +XKBLAYOUT="us" +XKBVARIANT="" +XKBOPTIONS="" + +BACKSPACE="guess" \ No newline at end of file diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh index 68b64fb94ca..781b599d51f 100755 --- a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh @@ -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" diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/udev/rules.d/99-usb.rules b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/udev/rules.d/99-usb.rules new file mode 100644 index 00000000000..7a282a8e6e1 --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/udev/rules.d/99-usb.rules @@ -0,0 +1,2 @@ +SUBSYSTEM=="usb", GROUP="usbusers", MODE="0660" +SUBSYSTEMS=="usb", GROUP="usbusers", MODE="0660" \ No newline at end of file diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/.inputrc b/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/.inputrc new file mode 100644 index 00000000000..131b24148c2 --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/.inputrc @@ -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 diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/.vimrc b/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/.vimrc new file mode 100644 index 00000000000..9e65af338fa --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/.vimrc @@ -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" diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index 47fd47ef794..d7a0b109a38 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -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") diff --git a/addons/purchase_analytic_plans/purchase_analytic_plans_view.xml b/addons/purchase_analytic_plans/purchase_analytic_plans_view.xml index 28ca76e8430..ea16ae891b3 100644 --- a/addons/purchase_analytic_plans/purchase_analytic_plans_view.xml +++ b/addons/purchase_analytic_plans/purchase_analytic_plans_view.xml @@ -8,7 +8,7 @@ - + @@ -19,7 +19,7 @@ - + diff --git a/addons/purchase_requisition/static/src/js/web_addons.js b/addons/purchase_requisition/static/src/js/web_addons.js index d67c8ec146d..f2ea43423f0 100644 --- a/addons/purchase_requisition/static/src/js/web_addons.js +++ b/addons/purchase_requisition/static/src/js/web_addons.js @@ -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(); }); }, }); diff --git a/addons/sale_contract/models/sale_contract.py b/addons/sale_contract/models/sale_contract.py index 7fcd9f51bc5..f47b4b0673a 100644 --- a/addons/sale_contract/models/sale_contract.py +++ b/addons/sale_contract/models/sale_contract.py @@ -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 diff --git a/addons/stock/product_view.xml b/addons/stock/product_view.xml index 17162086c9f..4c717702d5c 100644 --- a/addons/stock/product_view.xml +++ b/addons/stock/product_view.xml @@ -139,7 +139,7 @@ - {'default_groups_ref': ['base.group_user', 'base.group_sale_manager', 'stock.group_stock_manager']} + {'default_groups_ref': ['base.group_user', 'base.group_sale_manager', 'stock.group_stock_manager']} diff --git a/addons/stock/report/report_stock_forecast.py b/addons/stock/report/report_stock_forecast.py index 61397d5a25c..1161e5fc209 100644 --- a/addons/stock/report/report_stock_forecast.py +++ b/addons/stock/report/report_stock_forecast.py @@ -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 diff --git a/addons/stock/stock.py b/addons/stock/stock.py index dad23b73223..1409ce1e907 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -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'): diff --git a/addons/stock_dropshipping/stock_dropshipping.py b/addons/stock_dropshipping/stock_dropshipping.py index d7f2fc54137..96a22e5dfae 100644 --- a/addons/stock_dropshipping/stock_dropshipping.py +++ b/addons/stock_dropshipping/stock_dropshipping.py @@ -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}) diff --git a/addons/web/static/src/js/views/list_view.js b/addons/web/static/src/js/views/list_view.js index 63940726fca..34a483f8aa9 100644 --- a/addons/web/static/src/js/views/list_view.js +++ b/addons/web/static/src/js/views/list_view.js @@ -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); }, diff --git a/addons/web/static/src/js/views/search_view.js b/addons/web/static/src/js/views/search_view.js index cd0735d24ed..dba0f8045fa 100644 --- a/addons/web/static/src/js/views/search_view.js +++ b/addons/web/static/src/js/views/search_view.js @@ -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); diff --git a/addons/web_editor/static/lib/summernote/src/js/editing/History.js b/addons/web_editor/static/lib/summernote/src/js/editing/History.js index 89834d52cb1..dc301bf957b 100644 --- a/addons/web_editor/static/lib/summernote/src/js/editing/History.js +++ b/addons/web_editor/static/lib/summernote/src/js/editing/History.js @@ -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(), diff --git a/addons/web_editor/static/src/js/editor.js b/addons/web_editor/static/src/js/editor.js index f9255716697..4480101bf6a 100644 --- a/addons/web_editor/static/src/js/editor.js +++ b/addons/web_editor/static/src/js/editor.js @@ -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(); diff --git a/addons/web_editor/static/src/js/snippets.editor.js b/addons/web_editor/static/src/js/snippets.editor.js index cc380833074..3de9b71d5f8 100644 --- a/addons/web_editor/static/src/js/snippets.editor.js +++ b/addons/web_editor/static/src/js/snippets.editor.js @@ -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"); diff --git a/addons/web_editor/static/src/js/snippets.options.js b/addons/web_editor/static/src/js/snippets.options.js index 4dce2e358ed..df4ec303894 100644 --- a/addons/web_editor/static/src/js/snippets.options.js +++ b/addons/web_editor/static/src/js/snippets.options.js @@ -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 = $(''); $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 () { diff --git a/addons/web_editor/static/src/js/summernote.js b/addons/web_editor/static/src/js/summernote.js index 46b21aeec88..235bd6c3500 100644 --- a/addons/web_editor/static/src/js/summernote.js +++ b/addons/web_editor/static/src/js/summernote.js @@ -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($("
")[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 = $('

').append(br).insertAfter(dom.ancestor(r.sc, dom.isList))[0]; + $('

').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 + $('

').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; i0 && 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"; diff --git a/addons/website/static/src/js/website.snippets.editor.js b/addons/website/static/src/js/website.snippets.editor.js index 9cc40247b89..61a7622ecbf 100644 --- a/addons/website/static/src/js/website.snippets.editor.js +++ b/addons/website/static/src/js/website.snippets.editor.js @@ -81,7 +81,7 @@ options.registry.slider = options.Class.extend({ this.$indicators.append('
  • '); // 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"; diff --git a/addons/website/static/src/js/website.snippets.gallery.js b/addons/website/static/src/js/website.snippets.gallery.js index 4eb09a4837b..fd6005456e2 100644 --- a/addons/website/static/src/js/website.snippets.gallery.js +++ b/addons/website/static/src/js/website.snippets.gallery.js @@ -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); diff --git a/addons/website/static/src/js/website.theme.js b/addons/website/static/src/js/website.theme.js index f0d45c169ac..7b10fd7ea13 100644 --- a/addons/website/static/src/js/website.theme.js +++ b/addons/website/static/src/js/website.theme.js @@ -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(); diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py index 4c2e4244608..c4fe2c28e0a 100644 --- a/addons/website_sale/models/product.py +++ b/addons/website_sale/models/product.py @@ -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 = { diff --git a/addons/website_sale/static/src/css/website_sale.css b/addons/website_sale/static/src/css/website_sale.css index cc569925f5e..9f696a920d1 100644 --- a/addons/website_sale/static/src/css/website_sale.css +++ b/addons/website_sale/static/src/css/website_sale.css @@ -29,6 +29,10 @@ } /* ---- Default Styles ---- */ + +.o_alternative_product { + margin: auto; +} .oe_product { position: relative; } diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 804da7e61bf..5768de061f4 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -517,12 +517,12 @@

    Suggested alternatives:

    -
    -
    - +
    +
    + diff --git a/doc/cla/corporate/ITLibertas.md b/doc/cla/corporate/ITLibertas.md new file mode 100644 index 00000000000..06626c6d5ad --- /dev/null +++ b/doc/cla/corporate/ITLibertas.md @@ -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 diff --git a/doc/cla/individual/flh.md b/doc/cla/individual/flh.md new file mode 100644 index 00000000000..90860f3e0c4 --- /dev/null +++ b/doc/cla/individual/flh.md @@ -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 diff --git a/openerp/addons/base/ir/ir_http.py b/openerp/addons/base/ir/ir_http.py index 577bed483be..a30f0da1850 100644 --- a/openerp/addons/base/ir/ir_http.py +++ b/openerp/addons/base/ir/ir_http.py @@ -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 diff --git a/openerp/addons/base/rng/view.rng b/openerp/addons/base/rng/view.rng index 3eee1860024..f2bcd74bc8a 100644 --- a/openerp/addons/base/rng/view.rng +++ b/openerp/addons/base/rng/view.rng @@ -259,6 +259,11 @@ + + + + + diff --git a/openerp/addons/base/tests/test_search.py b/openerp/addons/base/tests/test_search.py index bd9d5089356..6cb8490546d 100644 --- a/openerp/addons/base/tests/test_search.py +++ b/openerp/addons/base/tests/test_search.py @@ -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() diff --git a/openerp/models.py b/openerp/models.py index 93cc537d12f..6aaecba62be 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -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: