diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-06-27 11:37:52 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-06-27 11:37:52 +0200 |
| commit | 12ddec3a7674c71028080f97adf8c7fec3176b65 (patch) | |
| tree | cfc76999bd7988c804417def7e409ccb1d60d607 | |
| parent | eb67d830166fdd87ea6f350dac60c95a9ae96198 (diff) | |
| parent | 152519cd6585df92926806983d7ebae7237d2376 (diff) | |
[MERGE] forward port branch 11.0 up to 152519cd658
46 files changed, 607 insertions, 57 deletions
diff --git a/addons/account/models/product.py b/addons/account/models/product.py index b2b0026eea8..b72bc31d7bd 100644 --- a/addons/account/models/product.py +++ b/addons/account/models/product.py @@ -36,22 +36,6 @@ class ProductTemplate(models.Model): help="The expense is accounted for when a vendor bill is validated, except in anglo-saxon accounting with perpetual inventory valuation in which case the expense (Cost of Goods Sold account) is recognized at the customer invoice validation. If the field is empty, it uses the one defined in the product category.") @api.multi - def write(self, vals): - #TODO: really? i don't see the reason we'd need that constraint.. - check = self.ids and 'uom_po_id' in vals - if check: - self._cr.execute("SELECT id, uom_po_id FROM product_template WHERE id IN %s", [tuple(self.ids)]) - uoms = dict(self._cr.fetchall()) - res = super(ProductTemplate, self).write(vals) - if check: - self._cr.execute("SELECT id, uom_po_id FROM product_template WHERE id IN %s", [tuple(self.ids)]) - if dict(self._cr.fetchall()) != uoms: - products = self.env['product.product'].search([('product_tmpl_id', 'in', self.ids)]) - if self.env['account.move.line'].search_count([('product_id', 'in', products.ids)]): - raise UserError(_('You can not change the unit of measure of a product that has been already used in an account journal item. If you need to change the unit of measure, you may deactivate this product.')) - return res - - @api.multi def _get_product_accounts(self): return { 'income': self.property_account_income_id or self.categ_id.property_account_income_categ_id, diff --git a/addons/auth_signup/models/res_users.py b/addons/auth_signup/models/res_users.py index c91cf78a3fc..90b161662cc 100644 --- a/addons/auth_signup/models/res_users.py +++ b/addons/auth_signup/models/res_users.py @@ -155,5 +155,5 @@ class ResUsers(models.Model): sup = super(ResUsers, self) if not default or not default.get('email'): # avoid sending email to the user we are duplicating - sup = super(ResUsers, self.with_context(reset_password=False)) + sup = super(ResUsers, self.with_context(no_reset_password=True)) return sup.copy(default=default) diff --git a/addons/crm/__init__.py b/addons/crm/__init__.py index 4e9b019bd7a..3398a4b470d 100644 --- a/addons/crm/__init__.py +++ b/addons/crm/__init__.py @@ -5,3 +5,11 @@ from . import controllers from . import models from . import report from . import wizard + +from odoo import api, SUPERUSER_ID + + +def uninstall_hook(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + teams = env['crm.team'].search([('dashboard_graph_model', '=', 'crm.opportunity.report')]) + teams.update({'dashboard_graph_model': None}) diff --git a/addons/crm/__manifest__.py b/addons/crm/__manifest__.py index 8e1a36c89ea..7ac544a6c75 100644 --- a/addons/crm/__manifest__.py +++ b/addons/crm/__manifest__.py @@ -53,4 +53,5 @@ 'installable': True, 'application': True, 'auto_install': False, + 'uninstall_hook': 'uninstall_hook', } diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 53c1094f997..653a1a4e4ed 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -442,7 +442,8 @@ class Lead(models.Model): 'res_id': self.id, 'views': [(form_view.id, 'form'),], 'type': 'ir.actions.act_window', - 'target': 'inline' + 'target': 'inline', + 'context': {'default_type': 'opportunity'} } # ---------------------------------------- diff --git a/addons/crm/wizard/crm_lead_to_opportunity_views.xml b/addons/crm/wizard/crm_lead_to_opportunity_views.xml index 201f2957636..5b29d25b934 100644 --- a/addons/crm/wizard/crm_lead_to_opportunity_views.xml +++ b/addons/crm/wizard/crm_lead_to_opportunity_views.xml @@ -56,8 +56,7 @@ <group string="Assign these opportunities to"> <field name="team_id"/> <field name="user_ids" widget="many2many_tags" domain="[('share', '=', False)]"/> - <!-- Uncomment me in trunk --> - <!-- <field name="force_assignation" /> --> + <field name="force_assignation"/> </group> <label for="opportunity_ids" string="Leads with existing duplicates (for information)" help="Leads that you selected that have duplicates. If the list is empty, it means that no duplicates were found" attrs="{'invisible': [('deduplicate', '=', False)]}"/> <group attrs="{'invisible': [('deduplicate', '=', False)]}"> diff --git a/addons/delivery/models/sale_order.py b/addons/delivery/models/sale_order.py index 4402cb5a127..e08f37b135e 100644 --- a/addons/delivery/models/sale_order.py +++ b/addons/delivery/models/sale_order.py @@ -124,3 +124,7 @@ class SaleOrderLine(models.Model): if not line.product_id or not line.product_uom or not line.product_uom_qty: return 0.0 line.product_qty = line.product_uom._compute_quantity(line.product_uom_qty, line.product_id.uom_id) + + def _is_delivery(self): + self.ensure_one() + return self.is_delivery diff --git a/addons/event/security/event_security.xml b/addons/event/security/event_security.xml index 110262928b6..7664085fc0e 100644 --- a/addons/event/security/event_security.xml +++ b/addons/event/security/event_security.xml @@ -41,5 +41,12 @@ ] </field> </record> + <record model="ir.rule" id="event_registration_portal"> + <field name="name">Event/Registration: Portal</field> + <field name="model_id" ref="model_event_registration"/> + <field name="groups" eval="[(4, ref('base.group_portal'))]"/> + <field name="domain_force">['|', ('email', '=', user.partner_id.email), ('partner_id', '=', user.partner_id.id)] + </field> + </record> </data> </odoo> diff --git a/addons/event/security/ir.model.access.csv b/addons/event/security/ir.model.access.csv index ed7ec0e0cc6..8a49f01f191 100644 --- a/addons/event/security/ir.model.access.csv +++ b/addons/event/security/ir.model.access.csv @@ -5,7 +5,8 @@ access_event_event_portal,event.event.portal,model_event_event,,1,0,0,0 access_event_event_user,event.event.user,model_event_event,event.group_event_user,1,0,0,0
access_event_event_manager,event.event.manager,model_event_event,event.group_event_manager,1,1,1,1
access_event_registration,event.registration,model_event_registration,event.group_event_user,1,1,1,1
-access_event_registration_portal,event.registration,model_event_registration,,0,0,0,0
+access_event_registration_employee,event.registration,model_event_registration,base.group_user,1,0,0,0
+access_event_registration_portal,event.registration,model_event_registration,base.group_portal,1,0,0,0
access_event_mail,event.mail,model_event_mail,event.group_event_user,1,0,0,0
access_event_mail_manager,event.mail manager,model_event_mail,event.group_event_manager,1,1,1,1
access_event_mail_registration,event.mail.registration,model_event_mail_registration,event.group_event_user,1,0,0,0
diff --git a/addons/event_sale/__manifest__.py b/addons/event_sale/__manifest__.py index 97140bd471f..2293b52dfb5 100644 --- a/addons/event_sale/__manifest__.py +++ b/addons/event_sale/__manifest__.py @@ -26,6 +26,7 @@ this event. 'data/event_sale_data.xml', 'report/event_event_templates.xml', 'security/ir.model.access.csv', + 'security/event_security.xml', 'wizard/event_edit_registration.xml', ], 'demo': ['data/event_demo.xml'], diff --git a/addons/event_sale/security/event_security.xml b/addons/event_sale/security/event_security.xml new file mode 100644 index 00000000000..a5bc0885b9b --- /dev/null +++ b/addons/event_sale/security/event_security.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data noupdate="0"> + <record id="sales_team.group_sale_salesman" model="res.groups"> + <field name="implied_ids" eval="[(4, ref('event.group_event_user'))]"/> + </record> + </data> +</odoo> diff --git a/addons/hr_payroll/models/hr_salary_rule.py b/addons/hr_payroll/models/hr_salary_rule.py index af9d6c342a5..f8a342a354b 100644 --- a/addons/hr_payroll/models/hr_salary_rule.py +++ b/addons/hr_payroll/models/hr_salary_rule.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models, _ -from odoo.exceptions import UserError +from odoo.exceptions import UserError, ValidationError from odoo.tools.safe_eval import safe_eval from odoo.addons import decimal_precision as dp diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index 6a6735f3d66..f3aeddf85a7 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -5,6 +5,7 @@ import logging import re from email.utils import formataddr +from openerp.http import request from odoo import _, api, fields, models, modules, SUPERUSER_ID, tools from odoo.exceptions import UserError, AccessError @@ -288,11 +289,12 @@ class Message(models.Model): # 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see attachments_data = attachments.sudo().read(['id', 'datas_fname', 'name', 'mimetype']) + safari = request and request.httprequest.user_agent.browser == 'safari' attachments_tree = dict((attachment['id'], { 'id': attachment['id'], 'filename': attachment['datas_fname'], 'name': attachment['name'], - 'mimetype': attachment['mimetype'], + 'mimetype': 'application/octet-stream' if safari and 'video' in attachment['mimetype'] else attachment['mimetype'], }) for attachment in attachments_data) # 3. Tracking values diff --git a/addons/mass_mailing_event/views/event_views.xml b/addons/mass_mailing_event/views/event_views.xml index e3105fad8eb..480c0dddf21 100644 --- a/addons/mass_mailing_event/views/event_views.xml +++ b/addons/mass_mailing_event/views/event_views.xml @@ -10,7 +10,9 @@ name="action_mass_mailing_attendees" icon="fa-envelope-o" attrs="{'invisible': [('seats_expected', '=', 0)]}"> - Mail Attendees + <div class="o_field_widget o_stat_info"> + <span class="o_stat_text">Mail Attendees</span> + </div> </button> </button> </field> diff --git a/addons/mrp/report/mrp_bom_cost_report_templates.xml b/addons/mrp/report/mrp_bom_cost_report_templates.xml index 85d3b442a93..225c3dca5d8 100644 --- a/addons/mrp/report/mrp_bom_cost_report_templates.xml +++ b/addons/mrp/report/mrp_bom_cost_report_templates.xml @@ -36,7 +36,7 @@ <span t-att-res-id="bom_line['product_id'].id" res-model="product.product" view-type="form" t-esc="bom_line['product_id'].name"/> </td> <td class="text-right"> - <span t-esc="bom_line['product_uom_qty']"/> <span t-esc="bom_line['product_uom'].name" groups="product.group_uom"/> + <span t-esc="bom_line['product_uom_qty']" t-esc-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/> <span t-esc="bom_line['product_uom'].name" groups="product.group_uom"/> </td> <td class="text-right"> <span t-esc="bom_line['price_unit']" t-options='{"widget": "monetary", "display_currency": currency}'/> diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py index 2ef110a6dae..44ebafa3242 100644 --- a/addons/point_of_sale/models/pos_config.py +++ b/addons/point_of_sale/models/pos_config.py @@ -332,10 +332,11 @@ class PosConfig(models.Model): def name_get(self): result = [] for config in self: - if (not config.session_ids) or (config.session_ids[0].state == 'closed'): + last_session = self.env['pos.session'].search([('config_id', '=', config.id)], limit=1) + if (not last_session) or (last_session.state == 'closed'): result.append((config.id, config.name + ' (' + _('not used') + ')')) continue - result.append((config.id, config.name + ' (' + config.session_ids[0].user_id.name + ')')) + result.append((config.id, config.name + ' (' + last_session.user_id.name + ')')) return result @api.model diff --git a/addons/point_of_sale/static/src/js/devices.js b/addons/point_of_sale/static/src/js/devices.js index f06abc15c66..f45a48319ba 100644 --- a/addons/point_of_sale/static/src/js/devices.js +++ b/addons/point_of_sale/static/src/js/devices.js @@ -215,7 +215,7 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ var self = this; function status(){ - self.connection.rpc('/hw_proxy/status_json',{},{timeout:2500}) + self.connection.rpc('/hw_proxy/status_json',{},{shadow: true, timeout:2500}) .then(function(driver_status){ self.set_connection_status('connected',driver_status); },function(){ @@ -239,7 +239,7 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ callbacks[i](params); } if(this.get('status').status !== 'disconnected'){ - return this.connection.rpc('/hw_proxy/' + name, params || {}); + return this.connection.rpc('/hw_proxy/' + name, params || {}, {shadow: true}); }else{ return (new $.Deferred()).reject(); } @@ -588,7 +588,7 @@ var BarcodeReader = core.Class.extend({ this.remote_active = 1; function waitforbarcode(){ - return self.proxy.connection.rpc('/hw_proxy/scanner',{},{timeout:7500}) + return self.proxy.connection.rpc('/hw_proxy/scanner',{},{shadow: true, timeout:7500}) .then(function(barcode){ if(!self.remote_scanning){ self.remote_active = 0; diff --git a/addons/point_of_sale/views/point_of_sale_dashboard.xml b/addons/point_of_sale/views/point_of_sale_dashboard.xml index 8b70e02c9ec..49f4da04fe2 100644 --- a/addons/point_of_sale/views/point_of_sale_dashboard.xml +++ b/addons/point_of_sale/views/point_of_sale_dashboard.xml @@ -141,7 +141,7 @@ </div> </div> - <div t-if="widget.editable" class="o_kanban_card_manage_settings row"> + <div t-if="widget.editable" class="o_kanban_card_manage_settings row" groups="point_of_sale.group_pos_manager"> <div class="col-xs-12 text-right"> <a type="edit">Settings</a> </div> diff --git a/addons/pos_restaurant/static/src/js/multiprint.js b/addons/pos_restaurant/static/src/js/multiprint.js index 1a135bdfe03..738b13d9362 100644 --- a/addons/pos_restaurant/static/src/js/multiprint.js +++ b/addons/pos_restaurant/static/src/js/multiprint.js @@ -27,11 +27,14 @@ var Printer = core.Class.extend(mixins.PropertiesMixin,{ function send_printing_job(){ if(self.receipt_queue.length > 0){ var r = self.receipt_queue.shift(); - self.connection.rpc('/hw_proxy/print_xml_receipt',{receipt: r},{timeout: 5000}) + var options = {shadow: true, timeout: 5000}; + self.connection.rpc('/hw_proxy/print_xml_receipt', {receipt: r}, options) .then(function(){ send_printing_job(); - },function(){ + },function(error, event){ self.receipt_queue.unshift(r); + console.log('There was an error while trying to print the order:'); + console.log(error); }); } } @@ -55,7 +58,7 @@ models.load_models({ for(var i = 0; i < printers.length; i++){ if(active_printers[printers[i].id]){ - var url = printers[i].proxy_ip; + var url = printers[i].proxy_ip || ''; if(url.indexOf('//') < 0){ url = 'http://'+url; } diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 70fad5e6152..40077d86acc 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -964,12 +964,12 @@ class Task(models.Model): @api.multi def message_get_suggested_recipients(self): recipients = super(Task, self).message_get_suggested_recipients() - for task in self.filtered('partner_id'): - reason = _('Customer Email') if task.partner_id.email else _('Customer') + for task in self: if task.partner_id: + reason = _('Customer Email') if task.partner_id.email else _('Customer') task._message_add_suggested_recipient(recipients, partner=task.partner_id, reason=reason) elif task.email_from: - task._message_add_suggested_recipient(recipients, partner=task.email_from, reason=reason) + task._message_add_suggested_recipient(recipients, email=task.email_from, reason=_('Customer Email')) return recipients @api.multi diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index 397952619d1..789b37b95d3 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -402,6 +402,8 @@ class PurchaseOrder(models.Model): for pick in order.picking_ids.filtered(lambda r: r.state != 'cancel'): pick.action_cancel() + order.order_line.write({'move_dest_ids':[(5,0,0)]}) + self.write({'state': 'cancel'}) @api.multi @@ -947,7 +949,8 @@ class ProcurementRule(models.Model): cache[domain] = po if not po: vals = self._prepare_purchase_order(product_id, product_qty, product_uom, origin, values, partner) - po = self.env['purchase.order'].sudo().create(vals) + company_id = values.get('company_id') and values['company_id'].id or self.env.user.company_id.id + po = self.env['purchase.order'].with_context(force_company=company_id).sudo().create(vals) cache[domain] = po elif not po.origin or origin not in po.origin.split(', '): if po.origin: diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 657dccf4c63..23d1e4dccf0 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -1239,3 +1239,8 @@ class SaleOrderLine(models.Model): discount = (new_list_price - price) / new_list_price * 100 if discount > 0: self.discount = discount + + + def _is_delivery(self): + self.ensure_one() + return False
\ No newline at end of file diff --git a/addons/sale/views/res_config_settings_views.xml b/addons/sale/views/res_config_settings_views.xml index ed515fe7b45..002a57eb8ab 100644 --- a/addons/sale/views/res_config_settings_views.xml +++ b/addons/sale/views/res_config_settings_views.xml @@ -347,7 +347,7 @@ </div> <h2>Invoicing</h2> <div class="row mt16 o_settings_container"> - <div class="col-xs-12 col-md-6 o_setting_box" title="This default value is applied to any new product created. This can be changed in the product detail form."> + <div class="col-xs-12 col-md-6 o_setting_box hidden" title="This default value is applied to any new product created. This can be changed in the product detail form."> <div class="o_setting_right_pane"> <label for="default_invoice_policy"/> <div class="text-muted"> diff --git a/addons/sale_management/static/src/js/tour.js b/addons/sale_management/static/src/js/tour.js index 89c792e104a..b379a0526d3 100644 --- a/addons/sale_management/static/src/js/tour.js +++ b/addons/sale_management/static/src/js/tour.js @@ -48,6 +48,8 @@ tour.register('sale_tour', { in_modal: false, run: function (actions) { actions.auto(); + // There might be a modal because of the view: + // sale.order.form.editable.list, enabled by some groups if ($(".modal-footer .btn-primary").length) { actions.auto(".modal-footer .btn-primary"); } diff --git a/addons/sale_order_dates/models/sale_order.py b/addons/sale_order_dates/models/sale_order.py index 0a01683040d..d206e4c8f3d 100644 --- a/addons/sale_order_dates/models/sale_order.py +++ b/addons/sale_order_dates/models/sale_order.py @@ -33,7 +33,7 @@ class SaleOrder(models.Model): for order in self: dates_list = [] order_datetime = fields.Datetime.from_string(order.date_order) - for line in order.order_line.filtered(lambda x: x.state != 'cancel'): + for line in order.order_line.filtered(lambda x: x.state != 'cancel' and not x._is_delivery()): dt = order_datetime + timedelta(days=line.customer_lead or 0.0) dates_list.append(dt) if dates_list: diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index 39958e6e616..592acae3de3 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -585,3 +585,23 @@ class ProductCategory(models.Model): category = category.parent_id routes |= category.route_ids self.total_route_ids = routes + + +class ProductUoM(models.Model): + _inherit = 'product.uom' + + def write(self, values): + # Users can not update the factor if open stock moves are based on it + if 'factor' in values or 'factor_inv' in values or 'category_id' in values: + stock_move_lines = self.env['stock.move.line'].search_count([ + ('product_uom_id.category_id', '=', self.category_id.id), + ('state', '!=', 'cancel'), + ]) + + if stock_move_lines: + raise UserError(_( + "You cannot change the ratio of this unit of mesure as some" + " products with this UoM have already been moved or are " + "currently reserved." + )) + return super(ProductUoM, self).write(values) diff --git a/addons/stock/models/stock_location.py b/addons/stock/models/stock_location.py index 05e4bb2b60d..5a06cc2425e 100644 --- a/addons/stock/models/stock_location.py +++ b/addons/stock/models/stock_location.py @@ -83,6 +83,21 @@ class Location(models.Model): if 'usage' in values and values['usage'] == 'view': if self.mapped('quant_ids'): raise UserError(_("This location's usage cannot be changed to view as it contains products.")) + if 'usage' in values or 'scrap_location' in values: + modified_locations = self.filtered( + lambda l: l.usage != values.get('usage') or + l.scrap_location != values.get('scrap_location') + ) + reserved_quantities = self.env['stock.move.line'].search_count([ + ('location_id', 'in', modified_locations.ids), + ('product_qty', '>', 0), + ]) + if reserved_quantities: + raise UserError(_( + "You cannot change the location type or its use as a scrap" + " location as there are products reserved in this location." + " Please unreserve the products first." + )) return super(Location, self).write(values) def name_get(self): diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 53111d925fe..1feda768342 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -787,7 +787,7 @@ class StockMove(models.Model): group_id = False return { 'company_id': self.company_id, - 'date_planned': self.date, + 'date_planned': self.date_expected, 'move_dest_ids': self, 'group_id': group_id, 'route_ids': self.route_ids, diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py index 0b73b7815e6..0439e8d7ccb 100644 --- a/addons/stock/models/stock_quant.py +++ b/addons/stock/models/stock_quant.py @@ -439,6 +439,12 @@ class QuantPackage(models.Model): if move_lines_to_remove: move_lines_to_remove.write({'result_package_id': False}) else: + move_line_to_modify = self.env['stock.move.line'].search([ + ('package_id', '=', package.id), + ('state', 'in', ('assigned', 'partially_available')), + ('product_qty', '!=', 0), + ]) + move_line_to_modify.write({'package_id': False}) package.mapped('quant_ids').write({'package_id': False}) def action_view_picking(self): diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py index 661628b2132..07916d95fa6 100644 --- a/addons/stock/models/stock_warehouse.py +++ b/addons/stock/models/stock_warehouse.py @@ -397,7 +397,7 @@ class Warehouse(models.Model): pull_rules_list = supplier_wh._get_supply_pull_rules_values( [self.Routing(output_location, transit_location, supplier_wh.out_type_id)], - values={'route_id': inter_wh_route.id, 'propagate_warehouse_id': self.id}) + values={'route_id': inter_wh_route.id}) pull_rules_list += self._get_supply_pull_rules_values( [self.Routing(transit_location, input_location, self.in_type_id)], values={'route_id': inter_wh_route.id, 'propagate_warehouse_id': supplier_wh.id}) diff --git a/addons/stock/tests/__init__.py b/addons/stock/tests/__init__.py index 52f5e2c53b0..25ae6888c7c 100644 --- a/addons/stock/tests/__init__.py +++ b/addons/stock/tests/__init__.py @@ -9,6 +9,7 @@ from . import test_quant from . import test_inventory from . import test_move from . import test_move2 +from . import test_robustness from . import test_packing from . import test_packing_neg from . import test_proc_rule diff --git a/addons/stock/tests/test_robustness.py b/addons/stock/tests/test_robustness.py new file mode 100644 index 00000000000..bd3dc6db96e --- /dev/null +++ b/addons/stock/tests/test_robustness.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.exceptions import UserError +from odoo.tests.common import TransactionCase + + +class TestRobustness(TransactionCase): + def setUp(self): + super(TestRobustness, self).setUp() + self.stock_location = self.env.ref('stock.stock_location_stock') + self.customer_location = self.env.ref('stock.stock_location_customers') + self.uom_unit = self.env.ref('product.product_uom_unit') + self.uom_dozen = self.env.ref('product.product_uom_dozen') + self.product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + 'categ_id': self.env.ref('product.product_category_all').id, + }) + + def test_uom_factor(self): + """ Changing the factor of a unit of measure shouldn't be allowed while + quantities are reserved, else the existing move lines won't be consistent + with the `reserved_quantity` on quants. + """ + # make some stock + self.env['stock.quant']._update_available_quantity( + self.product1, + self.stock_location, + 12, + ) + + # reserve a dozen + move1 = self.env['stock.move'].create({ + 'name': 'test_uom_rounding', + 'location_id': self.stock_location.id, + 'location_dest_id': self.customer_location.id, + 'product_id': self.product1.id, + 'product_uom': self.uom_dozen.id, + 'product_uom_qty': 1, + }) + move1._action_confirm() + move1._action_assign() + self.assertEqual(move1.state, 'assigned') + quant = self.env['stock.quant']._gather( + self.product1, + self.stock_location, + ) + + # assert the reservation + self.assertEqual(quant.reserved_quantity, 12) + self.assertEqual(move1.product_qty, 12) + + # change the factor + with self.assertRaises(UserError): + with self.cr.savepoint(): + move1.product_uom.factor = 0.05 + + # assert the reservation + self.assertEqual(quant.reserved_quantity, 12) + self.assertEqual(move1.state, 'assigned') + self.assertEqual(move1.product_qty, 12) + + # unreserve + move1._do_unreserve() + + def test_location_usage(self): + """ Changing the usage of a location shouldn't be allowed while + quantities are reserved, else the existing move lines won't be + consistent with the `reserved_quantity` on the quants. + """ + # change stock usage + self.stock_location.scrap_location = True + + # make some stock + self.env['stock.quant']._update_available_quantity( + self.product1, + self.stock_location, + 1, + ) + + # reserve a unit + move1 = self.env['stock.move'].create({ + 'name': 'test_location_archive', + 'location_id': self.stock_location.id, + 'location_dest_id': self.customer_location.id, + 'product_id': self.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1, + }) + move1._action_confirm() + move1._action_assign() + self.assertEqual(move1.state, 'assigned') + quant = self.env['stock.quant']._gather( + self.product1, + self.stock_location, + ) + + # assert the reservation + self.assertEqual(quant.reserved_quantity, 0) # reservation is bypassed in scrap location + self.assertEqual(move1.product_qty, 1) + + # change the stock usage + with self.assertRaises(UserError): + with self.cr.savepoint(): + self.stock_location.scrap_location = False + + # unreserve + move1._do_unreserve() + + def test_package_unpack(self): + """ Unpack a package that contains quants with a reservation + should also remove the package on the reserved move lines. + """ + package = self.env['stock.quant.package'].create({ + 'name': 'Shell Helix HX7 10W30', + }) + + self.env['stock.quant']._update_available_quantity( + self.product1, + self.stock_location, + 10, + package_id=package + ) + + # reserve a dozen + move1 = self.env['stock.move'].create({ + 'name': 'test_uom_rounding', + 'location_id': self.stock_location.id, + 'location_dest_id': self.customer_location.id, + 'product_id': self.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 10, + }) + move1._action_confirm() + move1._action_assign() + + move1.result_package_id = False + + package.unpack() + + # unreserve + move1._do_unreserve() + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 1) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location, package_id=package)), 0) + + self.assertEqual(self.env['stock.quant']._gather(self.product1, self.stock_location).reserved_quantity, 0) diff --git a/addons/stock/tests/test_warehouse.py b/addons/stock/tests/test_warehouse.py index e793b323b98..b79a5ab768c 100644 --- a/addons/stock/tests/test_warehouse.py +++ b/addons/stock/tests/test_warehouse.py @@ -228,6 +228,215 @@ class TestWarehouse(TestStockCommon): quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', location_loss.id)]) self.assertEqual(len(quant), 1) + def test_resupply_route(self): + """ Simulate a resupply chain between warehouses. + Stock -> transit -> Dist. -> transit -> Shop -> Customer + Create the move from Shop to Customer and ensure that all the pull + rules are triggered in order to complete the move chain to Stock. + """ + warehouse_stock = self.env['stock.warehouse'].create({ + 'name': 'Stock.', + 'code': 'STK', + }) + + warehouse_distribution = self.env['stock.warehouse'].create({ + 'name': 'Dist.', + 'code': 'DIST', + 'default_resupply_wh_id': warehouse_stock.id, + 'resupply_wh_ids': [(6, 0, [warehouse_stock.id])] + }) + + warehouse_shop = self.env['stock.warehouse'].create({ + 'name': 'Shop', + 'code': 'SHOP', + 'default_resupply_wh_id': warehouse_distribution.id, + 'resupply_wh_ids': [(6, 0, [warehouse_distribution.id])] + }) + + route_stock_to_dist = warehouse_distribution.resupply_route_ids + route_dist_to_shop = warehouse_shop.resupply_route_ids + + # Change the procure_method on the pull rules between dist and shop + # warehouses. Since mto and resupply routes are both on product it will + # select one randomly between them and if it select the resupply it is + # 'make to stock' and it will not create the picking between stock and + # dist warehouses. + route_dist_to_shop.pull_ids.write({'procure_method': 'make_to_order'}) + + product = self.env['product.product'].create({ + 'name': 'Fakir', + 'type': 'product', + 'route_ids': [(4, route_id) for route_id in [route_stock_to_dist.id, route_dist_to_shop.id, self.env.ref('stock.route_warehouse0_mto').id]], + }) + + picking_out = self.env['stock.picking'].create({ + 'partner_id': self.env.ref('base.res_partner_2').id, + 'picking_type_id': self.env.ref('stock.picking_type_out').id, + 'location_id': warehouse_shop.lot_stock_id.id, + 'location_dest_id': self.env.ref('stock.stock_location_customers').id, + }) + self.env['stock.move'].create({ + 'name': product.name, + 'product_id': product.id, + 'product_uom_qty': 1, + 'product_uom': product.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': warehouse_shop.lot_stock_id.id, + 'location_dest_id': self.env.ref('stock.stock_location_customers').id, + 'warehouse_id': warehouse_shop.id, + 'procure_method': 'make_to_order', + }) + picking_out.action_confirm() + + moves = self.env['stock.move'].search([('product_id', '=', product.id)]) + # Shop/Stock -> Customer + # Transit -> Shop/Stock + # Dist/Stock -> Transit + # Transit -> Dist/Stock + # Stock/Stock -> Transit + self.assertEqual(len(moves), 5, 'Invalid moves number.') + self.assertTrue(self.env['stock.move'].search([('location_id', '=', warehouse_stock.lot_stock_id.id)])) + self.assertTrue(self.env['stock.move'].search([('location_dest_id', '=', warehouse_distribution.lot_stock_id.id)])) + self.assertTrue(self.env['stock.move'].search([('location_id', '=', warehouse_distribution.lot_stock_id.id)])) + self.assertTrue(self.env['stock.move'].search([('location_dest_id', '=', warehouse_shop.lot_stock_id.id)])) + self.assertTrue(self.env['stock.move'].search([('location_id', '=', warehouse_shop.lot_stock_id.id)])) + + def test_mutiple_resupply_warehouse(self): + """ Simulate the following situation: + - 2 shops with stock are resupply by 2 distinct warehouses + - Shop Namur is resupply by the warehouse stock Namur + - Shop Wavre is resupply by the warehouse stock Wavre + - Simulate 2 moves for the same product but in different shop. + This test ensure that the move are supplied by the correct distribution + warehouse. + """ + customer_location = self.env.ref('stock.stock_location_customers') + + warehouse_distribution_wavre = self.env['stock.warehouse'].create({ + 'name': 'Stock Wavre.', + 'code': 'WV', + }) + + warehouse_shop_wavre = self.env['stock.warehouse'].create({ + 'name': 'Shop Wavre', + 'code': 'SHWV', + 'default_resupply_wh_id': warehouse_distribution_wavre.id, + 'resupply_wh_ids': [(6, 0, [warehouse_distribution_wavre.id])] + }) + + warehouse_distribution_namur = self.env['stock.warehouse'].create({ + 'name': 'Stock Namur.', + 'code': 'NM', + }) + + warehouse_shop_namur = self.env['stock.warehouse'].create({ + 'name': 'Shop Namur', + 'code': 'SHNM', + 'default_resupply_wh_id': warehouse_distribution_namur.id, + 'resupply_wh_ids': [(6, 0, [warehouse_distribution_namur.id])] + }) + + route_shop_namur = warehouse_shop_namur.resupply_route_ids + route_shop_wavre = warehouse_shop_wavre.resupply_route_ids + + # The product contains the 2 resupply routes. + product = self.env['product.product'].create({ + 'name': 'Fakir', + 'type': 'product', + 'route_ids': [(4, route_id) for route_id in [route_shop_namur.id, route_shop_wavre.id, self.env.ref('stock.route_warehouse0_mto').id]], + }) + + # Add 1 quant in each distribution warehouse. + self.env['stock.quant']._update_available_quantity(product, warehouse_distribution_wavre.lot_stock_id, 1.0) + self.env['stock.quant']._update_available_quantity(product, warehouse_distribution_namur.lot_stock_id, 1.0) + + # Create the move for the shop Namur. Should create a resupply from + # distribution warehouse Namur. + picking_out_namur = self.env['stock.picking'].create({ + 'partner_id': self.env.ref('base.res_partner_2').id, + 'picking_type_id': self.env.ref('stock.picking_type_out').id, + 'location_id': warehouse_shop_namur.lot_stock_id.id, + 'location_dest_id': customer_location.id, + }) + self.env['stock.move'].create({ + 'name': product.name, + 'product_id': product.id, + 'product_uom_qty': 1, + 'product_uom': product.uom_id.id, + 'picking_id': picking_out_namur.id, + 'location_id': warehouse_shop_namur.lot_stock_id.id, + 'location_dest_id': customer_location.id, + 'warehouse_id': warehouse_shop_namur.id, + 'procure_method': 'make_to_order', + }) + picking_out_namur.action_confirm() + + # Validate the picking + # Dist. warehouse Namur -> transit Location -> Shop Namur + picking_stock_transit = self.env['stock.picking'].search([('location_id', '=', warehouse_distribution_namur.lot_stock_id.id)]) + self.assertTrue(picking_stock_transit) + picking_stock_transit.action_assign() + picking_stock_transit.move_lines[0].quantity_done = 1.0 + picking_stock_transit.action_done() + + picking_transit_shop_namur = self.env['stock.picking'].search([('location_dest_id', '=', warehouse_shop_namur.lot_stock_id.id)]) + self.assertTrue(picking_transit_shop_namur) + picking_transit_shop_namur.action_assign() + picking_transit_shop_namur.move_lines[0].quantity_done = 1.0 + picking_transit_shop_namur.action_done() + + picking_out_namur.action_assign() + picking_out_namur.move_lines[0].quantity_done = 1.0 + picking_out_namur.action_done() + + # Check that the correct quantity has been provided to customer + self.assertEqual(self.env['stock.quant']._gather(product, customer_location).quantity, 1) + # Ensure there still no quants in distribution warehouse + self.assertEqual(len(self.env['stock.quant']._gather(product, warehouse_distribution_namur.lot_stock_id)), 0) + + # Create the move for the shop Wavre. Should create a resupply from + # distribution warehouse Wavre. + picking_out_wavre = self.env['stock.picking'].create({ + 'partner_id': self.env.ref('base.res_partner_2').id, + 'picking_type_id': self.env.ref('stock.picking_type_out').id, + 'location_id': warehouse_shop_wavre.lot_stock_id.id, + 'location_dest_id': customer_location.id, + }) + self.env['stock.move'].create({ + 'name': product.name, + 'product_id': product.id, + 'product_uom_qty': 1, + 'product_uom': product.uom_id.id, + 'picking_id': picking_out_wavre.id, + 'location_id': warehouse_shop_wavre.lot_stock_id.id, + 'location_dest_id': customer_location.id, + 'warehouse_id': warehouse_shop_wavre.id, + 'procure_method': 'make_to_order', + }) + picking_out_wavre.action_confirm() + + # Validate the picking + # Dist. warehouse Wavre -> transit Location -> Shop Wavre + picking_stock_transit = self.env['stock.picking'].search([('location_id', '=', warehouse_distribution_wavre.lot_stock_id.id)]) + self.assertTrue(picking_stock_transit) + picking_stock_transit.action_assign() + picking_stock_transit.move_lines[0].quantity_done = 1.0 + picking_stock_transit.action_done() + + picking_transit_shop_wavre = self.env['stock.picking'].search([('location_dest_id', '=', warehouse_shop_wavre.lot_stock_id.id)]) + self.assertTrue(picking_transit_shop_wavre) + picking_transit_shop_wavre.action_assign() + picking_transit_shop_wavre.move_lines[0].quantity_done = 1.0 + picking_transit_shop_wavre.action_done() + + picking_out_wavre.action_assign() + picking_out_wavre.move_lines[0].quantity_done = 1.0 + picking_out_wavre.action_done() + + # Check that the correct quantity has been provided to customer + self.assertEqual(self.env['stock.quant']._gather(product, customer_location).quantity, 2) + # Ensure there still no quants in distribution warehouse + self.assertEqual(len(self.env['stock.quant']._gather(product, warehouse_distribution_wavre.lot_stock_id)), 0) class TestResupply(TestStockCommon): def setUp(self): diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py index 27630eb1011..6e3189ed168 100644 --- a/addons/stock_account/models/product.py +++ b/addons/stock_account/models/product.py @@ -146,11 +146,13 @@ class ProductProduct(models.Model): 'account_id': debit_account_id, 'debit': abs(diff * qty_available), 'credit': 0, + 'product_id': product.id, }), (0, 0, { 'name': _('Standard Price changed - %s') % (product.display_name), 'account_id': credit_account_id, 'debit': 0, 'credit': abs(diff * qty_available), + 'product_id': product.id, })], } move = AccountMove.create(move_vals) diff --git a/addons/web/static/src/js/services/crash_manager.js b/addons/web/static/src/js/services/crash_manager.js index ccc7e089e65..d903b5870c5 100644 --- a/addons/web/static/src/js/services/crash_manager.js +++ b/addons/web/static/src/js/services/crash_manager.js @@ -41,7 +41,8 @@ var CrashManager = core.Class.extend({ core.bus.trigger('connection_lost'); this.connection_lost = true; var timeinterval = setInterval(function() { - ajax.jsonRpc('/web/webclient/version_info').then(function() { + var options = {shadow: true}; + ajax.jsonRpc('/web/webclient/version_info', 'call', {}, options).then(function () { clearInterval(timeinterval); core.bus.trigger('connection_restored'); self.connection_lost = false; diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 8c6b6308609..919bb179c2c 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -2579,6 +2579,11 @@ var BasicModel = AbstractModel.extend({ relationField: field.relation_field, viewType: view ? view.type : fieldInfo.viewType, }); + // set existing changes to the list + if (record._changes && record._changes[fieldName]) { + list._changes = self.localData[record._changes[fieldName]]._changes; + record._changes[fieldName] = list.id; + } record.data[fieldName] = list.id; if (!fieldInfo.__no_fetch) { var def = self._readUngroupedList(list).then(function () { diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 0d8e5dc56d4..f98ac1724d7 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -143,10 +143,12 @@ QUnit.module('relational_fields', { user: { fields: { name: {string: "Name", type: "char"}, + partner_ids: {string: "one2many partners field", type: "one2many", relation: 'partner', relation_field: 'user_id'}, }, records: [{ id: 17, name: "Aline", + partner_ids: [1, 2], }, { id: 19, name: "Christine", @@ -9474,6 +9476,94 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('onchange affecting inline unopened list view', function (assert) { + // when we got onchange result for fields of record that were not + // already available because they were in a inline view not already + // opened, in a given configuration the change were applied ignoring + // existing data, thus a line of a one2many field inside a one2many + // field could be duplicated unexplectedly + assert.expect(5); + + var numUserOnchange = 0; + + this.data.user.onchanges = { + partner_ids: function (obj) { + if (numUserOnchange === 0) { + // simulate proper server onchange after save of modal with new record + obj.partner_ids = [ + [5], + [1, 1, { + display_name: 'first record', + turtles: [ + [5], + [1, 2, {'display_name': 'donatello'}], + ], + }], + [1, 2, { + display_name: 'second record', + turtles: [ + [5], + obj.partner_ids[1][2].turtles[0], + ], + }], + ]; + } + numUserOnchange++; + }, + }; + + var form = createView({ + View: FormView, + model: 'user', + data: this.data, + arch: '<form><sheet><group>' + + '<field name="partner_ids">' + + '<form>'+ + '<field name="turtles">' + + '<tree editable="bottom">' + + '<field name="display_name"/>' + + '</tree>' + + '</field>' + + '</form>' + + '<tree>' + + '<field name="display_name"/>' + + '</tree>' + + '</field>' + + '</group></sheet></form>', + res_id: 17, + }); + + // add a turtle on second partner + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_data_row:eq(1)').click(); + $('.modal .o_field_x2many_list_row_add a').click(); + $('.modal input[name="display_name"]').val('michelangelo').change(); + $('.modal .btn-primary').click(); + // open first partner so changes from previous action are applied + form.$('.o_data_row:eq(0)').click(); + $('.modal .btn-primary').click(); + form.$buttons.find('.o_form_button_save').click(); + + assert.strictEqual(numUserOnchange, 2, + 'there should 2 and only 2 onchange from closing the partner modal'); + + form.$('.o_data_row:eq(0)').click(); + assert.strictEqual($('.modal .o_data_row').length, 1, + 'only 1 turtle for first partner'); + assert.strictEqual($('.modal .o_data_row').text(), 'donatello', + 'first partner turtle is donatello'); + $('.modal .o_form_button_cancel').click(); + + form.$('.o_data_row:eq(1)').click(); + assert.strictEqual($('.modal .o_data_row').length, 1, + 'only 1 turtle for second partner'); + assert.strictEqual($('.modal .o_data_row').text(), 'michelangelo', + 'second partner turtle is michelangelo'); + $('.modal .o_form_button_cancel').click(); + + form.destroy(); + }); + QUnit.module('FieldMany2Many'); QUnit.test('many2many kanban: edition', function (assert) { diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py index cc25b86d862..0aa3dd24753 100644 --- a/addons/web_editor/controllers/main.py +++ b/addons/web_editor/controllers/main.py @@ -253,7 +253,7 @@ class Web_Editor(http.Controller): ## @param bundles - True if the bundles views must be fetched (default to False) ## @param bundles_restriction - Names of the bundle in which to look for less files (if empty, search in all of them) ## @returns a dictionary with views info in the views key and style info in the less key - @http.route("/web_editor/get_assets_editor_resources", type="json", auth="user") + @http.route("/web_editor/get_assets_editor_resources", type="json", auth="user", website=True) def get_assets_editor_resources(self, key, get_views=True, get_less=True, bundles=False, bundles_restriction=[]): # Related views must be fetched if the user wants the views and/or the style views = request.env["ir.ui.view"].get_related_views(key, bundles=bundles) diff --git a/addons/web_editor/static/src/js/editor/transcoder.js b/addons/web_editor/static/src/js/editor/transcoder.js index ebe6fea5dbb..84fe3cf8c93 100644 --- a/addons/web_editor/static/src/js/editor/transcoder.js +++ b/addons/web_editor/static/src/js/editor/transcoder.js @@ -108,9 +108,14 @@ function getMatchedCSSRules(a) { // The css generates all the attributes separately and not in simplified form. // In order to have a better compatibility (outlook for example) we simplify the css tags. // e.g. border-left-style: none; border-bottom-s .... will be simplified in border-style = none - _.each([['margin'], ['padding'], ['border', 'style']], function (attr) { - var p = attr[0]; - var e = attr[1] ? '-' + attr[1] : ''; + _.each([ + {property: 'margin'}, + {property: 'padding'}, + {property: 'border', propertyEnd: '-style', defaultValue: 'none'}, + ], function (propertyInfo) { + var p = propertyInfo.property; + var e = propertyInfo.propertyEnd || ''; + var defVal = propertyInfo.defaultValue || 0; if (style[p+'-top'+e] || style[p+'-right'+e] || style[p+'-bottom'+e] || style[p+'-left'+e]) { if (style[p+'-top'+e] === style[p+'-right'+e] && style[p+'-top'+e] === style[p+'-bottom'+e] && style[p+'-top'+e] === style[p+'-left'+e]) { @@ -119,7 +124,7 @@ function getMatchedCSSRules(a) { } else { // keep => property: [top value] [right value] [bottom value] [left value]; - style[p+e] = (style[p+'-top'+e] || 0) + ' ' + (style[p+'-right'+e] || 0) + ' ' + (style[p+'-bottom'+e] || 0) + ' ' + (style[p+'-left'+e] || 0); + style[p+e] = (style[p+'-top'+e] || defVal) + ' ' + (style[p+'-right'+e] || defVal) + ' ' + (style[p+'-bottom'+e] || defVal) + ' ' + (style[p+'-left'+e] || defVal); if (style[p+e].indexOf('inherit') !== -1 || style[p+e].indexOf('initial') !== -1) { // keep => property-top: [top value]; property-right: [right value]; property-bottom: [bottom value]; property-left: [left value]; delete style[p+e]; diff --git a/addons/website_event/controllers/main.py b/addons/website_event/controllers/main.py index f3a8041d54e..a4fb8659326 100644 --- a/addons/website_event/controllers/main.py +++ b/addons/website_event/controllers/main.py @@ -175,7 +175,7 @@ class WebsiteEventController(http.Controller): 'event': event, 'main_object': event, 'range': range, - 'registrable': event._is_event_registrable() + 'registrable': event.sudo()._is_event_registrable() } return request.render("website_event.event_description_full", values) diff --git a/addons/website_event/views/event_templates.xml b/addons/website_event/views/event_templates.xml index 9ae221bc2fd..752d227620e 100644 --- a/addons/website_event/views/event_templates.xml +++ b/addons/website_event/views/event_templates.xml @@ -480,7 +480,7 @@ <t t-if="event.state != 'draft'" itemprop="availability" content="http://schema.org/Discontinued"> Event registration is closed. </t> - <t t-if="uid"> + <t t-if="request.env.user.has_group('event.group_event_manager')"> <a t-attf-href="/web#id=#{event.id}&view_type=form&model=event.event"> <i class="fa fa-plus-circle"></i> <em>Configure and Launch Event Registration</em> diff --git a/addons/website_mail_channel/static/src/js/website_mail_channel.snippet.js b/addons/website_mail_channel/static/src/js/website_mail_channel.snippet.js index 9daba4bf666..e48cae79df4 100644 --- a/addons/website_mail_channel/static/src/js/website_mail_channel.snippet.js +++ b/addons/website_mail_channel/static/src/js/website_mail_channel.snippet.js @@ -8,6 +8,7 @@ sAnimation.registry.follow_alias = sAnimation.Class.extend({ start: function () { var self = this; this.is_user = false; + var unsubscribePage = window.location.search.slice(1).split('&').indexOf("unsubscribe") >= 0; this._rpc({ route: '/groups/is_member', params: { @@ -19,6 +20,9 @@ sAnimation.registry.follow_alias = sAnimation.Class.extend({ self.is_user = data.is_user; self.email = data.email; self.$target.find('.js_mg_link').attr('href', '/groups/' + self.$target.data('id')); + if (unsubscribePage && self.is_user) { + self.$target.find(".js_mg_follow_form").remove(); + } self.toggle_subscription(data.is_member ? 'on' : 'off', data.email); self.$target.removeClass("hidden"); }); diff --git a/addons/website_sale/models/res_config_settings.py b/addons/website_sale/models/res_config_settings.py index 91dbebf9f2f..cae080f20aa 100644 --- a/addons/website_sale/models/res_config_settings.py +++ b/addons/website_sale/models/res_config_settings.py @@ -43,7 +43,7 @@ class ResConfigSettings(models.TransientModel): order_mail_template = fields.Many2one('mail.template', string='Order Confirmation Email', default=_default_order_mail_template, domain="[('model', '=', 'sale.order')]", - help="Email sent to customer at the end of the checkout process") + help="Email sent to customer at the end of the checkout process", readonly=True) automatic_invoice = fields.Boolean("Automatic Invoice", help="The invoice is generated automatically and available in the customer portal " diff --git a/addons/website_sale_options/static/src/js/website_sale.js b/addons/website_sale_options/static/src/js/website_sale.js index da7025d65bb..27da88ae866 100644 --- a/addons/website_sale_options/static/src/js/website_sale.js +++ b/addons/website_sale_options/static/src/js/website_sale.js @@ -9,7 +9,7 @@ require('website_sale.website_sale'); $('.oe_website_sale #add_to_cart, .oe_website_sale #products_grid .a-submit') .off('click') .removeClass('a-submit') - .click(function (event) { + .click(_.debounce(function (event) { var $form = $(this).closest('form'); var quantity = parseFloat($form.find('input[name="add_qty"]').val() || 1); var product_id = parseInt($form.find('input[type="hidden"][name="product_id"], input[type="radio"][name="product_id"]:checked').first().val(),10); @@ -96,6 +96,6 @@ $('.oe_website_sale #add_to_cart, .oe_website_sale #products_grid .a-submit') }); }); return false; - }); + }, 200, true)); }); diff --git a/odoo/addons/base/models/ir_property.py b/odoo/addons/base/models/ir_property.py index 99b63a95887..63ca6d2d6d0 100644 --- a/odoo/addons/base/models/ir_property.py +++ b/odoo/addons/base/models/ir_property.py @@ -189,7 +189,7 @@ class Property(models.Model): if not values: return - if not default_value: + if default_value is None: domain = self._get_domain(name, model) if domain is None: raise Exception() diff --git a/odoo/addons/base/models/ir_qweb.py b/odoo/addons/base/models/ir_qweb.py index 4e3a7eb679a..921f3ce2ee6 100644 --- a/odoo/addons/base/models/ir_qweb.py +++ b/odoo/addons/base/models/ir_qweb.py @@ -174,7 +174,7 @@ class IrQWeb(models.AbstractModel, QWeb): value=ast.Call( func=ast.Attribute( value=ast.Name(id='self', ctx=ast.Load()), - attr='_get_asset', + attr='_get_asset_nodes', ctx=ast.Load() ), args=[ @@ -286,15 +286,28 @@ class IrQWeb(models.AbstractModel, QWeb): # method called by computing code + def get_asset_bundle(self, xmlid, files, remains=None, env=None): + return AssetsBundle(xmlid, files, remains=remains, env=env) + + # compatibility to remove after v11 - DEPRECATED + @tools.conditional( + 'xml' not in tools.config['dev_mode'], + tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'async', keys=("website_id",)), + ) + def _get_asset(self, xmlid, options, css=True, js=True, debug=False, async=False, values=None): + files, remains = self._get_asset_content(xmlid, options) + asset = self.get_asset_bundle(xmlid, files, remains, env=self.env) + return asset.to_html(css=css, js=js, debug=debug, async=async, url_for=(values or {}).get('url_for', lambda url: url)) + @tools.conditional( # in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear # by restarting the server after updating the source code (or using the "Clear server cache" in debug tools) 'xml' not in tools.config['dev_mode'], tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'async', keys=("website_id",)), ) - def _get_asset(self, xmlid, options, css=True, js=True, debug=False, async=False, values=None): + def _get_asset_nodes(self, xmlid, options, css=True, js=True, debug=False, async=False, values=None): files, remains = self._get_asset_content(xmlid, options) - asset = AssetsBundle(xmlid, files, env=self.env) + asset = self.get_asset_bundle(xmlid, files, env=self.env) remains = [node for node in remains if (css and node[0] == 'link') or (js and node[0] != 'link')] return remains + asset.to_node(css=css, js=js, debug=debug, async=async) |
