summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorOlivier Dony <odo@openerp.com>2017-05-16 17:09:17 +0200
committerOlivier Dony <odo@openerp.com>2017-05-16 17:09:45 +0200
commit5afe894f440eff3b58134d14fa7c9b43b19fe88b (patch)
tree2b89e105c3389187dc9f1707e88a0acdc531a1a1 /addons
parent8dac9cfc93cb4196007bd08dd8b1304cd522e615 (diff)
parent878fbc75ff9ebade92f43748500e64350367a824 (diff)
[MERGE] Forward-port saas-15 up to 878fbc75ff9ebade92f43748500e64350367a824
Diffstat (limited to 'addons')
-rw-r--r--addons/account/models/account_bank_statement.py4
-rw-r--r--addons/account/models/company.py9
-rw-r--r--addons/account/report/account_invoice_report.py2
-rw-r--r--addons/auth_signup/controllers/main.py3
-rw-r--r--addons/auth_signup/models/res_users.py3
-rw-r--r--addons/base_automation/views/base_automation_view.xml7
-rw-r--r--addons/google_spreadsheet/views/base_config_settings_views.xml11
-rw-r--r--addons/im_livechat/models/im_livechat_channel.py2
-rw-r--r--addons/l10n_fr_hr_payroll/report/report_l10n_fr_fiche_paye.xml2
-rw-r--r--addons/mass_mailing/models/mass_mailing.py2
-rw-r--r--addons/mass_mailing/static/src/js/mass_mailing_editor.js20
-rw-r--r--addons/mass_mailing/static/src/less/themes/theme_default.less39
-rw-r--r--addons/mrp/models/mrp_production.py3
-rw-r--r--addons/payment/models/payment_acquirer.py10
-rw-r--r--addons/point_of_sale/models/pos_order.py2
-rw-r--r--addons/point_of_sale/static/src/xml/pos.xml24
-rw-r--r--addons/pos_mercury/models/pos_mercury_transaction.py3
-rw-r--r--addons/pos_restaurant/static/src/js/floors.js7
-rw-r--r--addons/product/models/res_company.py5
-rw-r--r--addons/report/models/report.py30
-rw-r--r--addons/sale/report/sale_report.xml12
-rw-r--r--addons/sale/report/sale_report_templates.xml4
-rw-r--r--addons/sale/security/sale_security.xml1
-rw-r--r--addons/sale/wizard/sale_make_invoice_advance.py14
-rw-r--r--addons/sale_mrp/sale_mrp.py24
-rw-r--r--addons/sale_timesheet/models/procurement.py2
-rw-r--r--addons/stock/models/procurement.py9
-rw-r--r--addons/stock/models/res_company.py7
-rw-r--r--addons/stock/models/stock_move.py5
-rw-r--r--addons/stock/views/stock_picking_views.xml2
-rw-r--r--addons/stock_account/models/stock.py6
-rw-r--r--addons/web/static/src/js/widgets/debug_manager.js1
-rw-r--r--addons/web_tour/static/src/less/tip.less24
-rw-r--r--addons/website/static/src/js/website.snippets.animation.js93
-rw-r--r--addons/website/static/src/js/website.snippets.editor.js3
-rw-r--r--addons/website/static/src/less/website.snippets.less7
-rw-r--r--addons/website_event/models/event.py19
-rw-r--r--addons/website_event_track/models/event.py15
-rw-r--r--addons/website_form/controllers/main.py2
-rw-r--r--addons/website_mail_channel/controllers/main.py11
-rw-r--r--addons/website_sale/views/templates.xml4
41 files changed, 313 insertions, 140 deletions
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index fc2a4bd0c36..71ed7b11a7e 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -426,7 +426,7 @@ class AccountBankStatementLine(models.Model):
payment_to_unreconcile |= line.payment_id
if st_line.move_name and line.payment_id.payment_reference == st_line.move_name:
#there can be several moves linked to a statement line but maximum one created by the line itself
- aml_to_cancel |= st_line.journal_entry_ids
+ aml_to_cancel |= line
payment_to_cancel |= line.payment_id
aml_to_unbind = aml_to_unbind - aml_to_cancel
@@ -887,6 +887,8 @@ class AccountBankStatementLine(models.Model):
if aml_dict.get('tax_ids') and isinstance(aml_dict['tax_ids'][0], pycompat.integer_types):
# Transform the value in the format required for One2many and Many2many fields
aml_dict['tax_ids'] = map(lambda id: (4, id, None), aml_dict['tax_ids'])
+ if any(line.journal_entry_ids for line in self):
+ raise UserError(_('A selected statement line was already reconciled with an account move.'))
# Fully reconciled moves are just linked to the bank statement
total = self.amount
diff --git a/addons/account/models/company.py b/addons/account/models/company.py
index b15cb4afe0b..44eee5432ae 100644
--- a/addons/account/models/company.py
+++ b/addons/account/models/company.py
@@ -79,8 +79,7 @@ Best Regards,''')
account.write({'code': account.code.rstrip('0').ljust(digits, '0')})
@api.multi
- def write(self, values):
- #restrict the closing of FY if there are still unposted entries
+ def _validate_fiscalyear_lock(self, values):
if values.get('fiscalyear_lock_date'):
nb_draft_entries = self.env['account.move'].search([
('company_id', 'in', [c.id for c in self]),
@@ -88,6 +87,12 @@ Best Regards,''')
('date', '<=', values['fiscalyear_lock_date'])])
if nb_draft_entries:
raise ValidationError(_('There are still unposted entries in the period you want to lock. You should either post or delete them.'))
+
+ @api.multi
+ def write(self, values):
+ #restrict the closing of FY if there are still unposted entries
+ self._validate_fiscalyear_lock(values)
+
# Reflect the change on accounts
for company in self:
digits = values.get('accounts_code_digits') or company.accounts_code_digits
diff --git a/addons/account/report/account_invoice_report.py b/addons/account/report/account_invoice_report.py
index 042455837ce..b028301af6f 100644
--- a/addons/account/report/account_invoice_report.py
+++ b/addons/account/report/account_invoice_report.py
@@ -108,7 +108,7 @@ class AccountInvoiceReport(models.Model):
1 AS nbr,
ai.type, ai.state, pt.categ_id, ai.date_due, ai.account_id, ail.account_id AS account_line_id,
ai.partner_bank_id,
- SUM ((invoice_type.sign * ail.quantity) / (u.factor * u2.factor)) AS product_qty,
+ SUM ((invoice_type.sign * ail.quantity) / u.factor * u2.factor) AS product_qty,
SUM(ail.price_subtotal_signed) AS price_total,
SUM(ABS(ail.price_subtotal_signed)) / CASE
WHEN SUM(ail.quantity / u.factor * u2.factor) <> 0::numeric
diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py
index 6c1b0facad3..dcaba89bc61 100644
--- a/addons/auth_signup/controllers/main.py
+++ b/addons/auth_signup/controllers/main.py
@@ -57,6 +57,9 @@ class AuthSignupHome(Home):
else:
login = qcontext.get('login')
assert login, "No login provided."
+ _logger.info(
+ "Password reset attempt for <%s> by user <%s> from %s",
+ login, request.env.user.login, request.httprequest.remote_addr)
request.env['res.users'].sudo().reset_password(login)
qcontext['message'] = _("An email has been sent with credentials to reset your password")
except SignupError:
diff --git a/addons/auth_signup/models/res_users.py b/addons/auth_signup/models/res_users.py
index d99961076be..05d52596c85 100644
--- a/addons/auth_signup/models/res_users.py
+++ b/addons/auth_signup/models/res_users.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import logging
from ast import literal_eval
@@ -11,6 +12,7 @@ from odoo.tools.misc import ustr
from odoo.addons.base.ir.ir_mail_server import MailDeliveryException
from odoo.addons.auth_signup.models.res_partner import SignupError, now
+_logger = logging.getLogger(__name__)
class ResUsers(models.Model):
_inherit = 'res.users'
@@ -134,6 +136,7 @@ class ResUsers(models.Model):
if not user.email:
raise UserError(_("Cannot send email: user %s has no email address.") % user.name)
template.with_context(lang=user.lang).send_mail(user.id, force_send=True, raise_exception=True)
+ _logger.info("Password reset email sent for user <%s> to <%s>", user.login, user.email)
@api.model
def create(self, values):
diff --git a/addons/base_automation/views/base_automation_view.xml b/addons/base_automation/views/base_automation_view.xml
index fa8c4b5eab2..a81dd04d75b 100644
--- a/addons/base_automation/views/base_automation_view.xml
+++ b/addons/base_automation/views/base_automation_view.xml
@@ -15,6 +15,13 @@
attrs="{'invisible':[('menu_ir_values_id','=',False)]}"
help="Remove the contextual action related to this server action"/>
</xpath>
+ <div class="oe_title" position="before">
+ <div class="oe_button_box" name="button_box">
+ <button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
+ <field name="active" widget="boolean_button"/>
+ </button>
+ </div>
+ </div>
<xpath expr="//group[@name='action_wrapper']" position="inside">
<group>
<field name="trigger"/>
diff --git a/addons/google_spreadsheet/views/base_config_settings_views.xml b/addons/google_spreadsheet/views/base_config_settings_views.xml
index 14bda91e373..6afd8ddacda 100644
--- a/addons/google_spreadsheet/views/base_config_settings_views.xml
+++ b/addons/google_spreadsheet/views/base_config_settings_views.xml
@@ -8,13 +8,10 @@
<field name="arch" type="xml">
<xpath expr="//div[@id='msg_module_google_spreadsheet']" position="replace">
<div class="content-group" attrs="{'invisible': [('module_google_spreadsheet','=',False)]}">
- <div class="row mt16">
- <label for="google_drive_authorization_code" class="col-md-3 o_light_label"/>
- <field name="google_drive_authorization_code"/>
- </div>
- <a type="button" class="btn-link" target="_blank"><i class="fa fa-fw fa-arrow-right"/>
- <field name="google_drive_uri_copy" widget="url" text="Generate Code"/>
- </a>
+ <div class="content-group mt16">
+ Please use the settings of Google Drive
+ on the left or above.
+ </div>
</div>
</xpath>
</field>
diff --git a/addons/im_livechat/models/im_livechat_channel.py b/addons/im_livechat/models/im_livechat_channel.py
index c2f0e8cb3c2..20dc2939d00 100644
--- a/addons/im_livechat/models/im_livechat_channel.py
+++ b/addons/im_livechat/models/im_livechat_channel.py
@@ -166,7 +166,7 @@ class ImLivechatChannel(models.Model):
operator_partner_id = user.partner_id.id
# partner to add to the mail.channel
channel_partner_to_add = [(4, operator_partner_id)]
- if self.env.uid: # if the user if logged (portal user), he can be identify
+ if self.env.user and self.env.user.active: # valid session user (not public)
channel_partner_to_add.append((4, self.env.user.partner_id.id))
# create the session, and add the link with the given channel
mail_channel = self.env["mail.channel"].with_context(mail_create_nosubscribe=False).sudo().create({
diff --git a/addons/l10n_fr_hr_payroll/report/report_l10n_fr_fiche_paye.xml b/addons/l10n_fr_hr_payroll/report/report_l10n_fr_fiche_paye.xml
index 883a7c75f1e..85ae97c2248 100644
--- a/addons/l10n_fr_hr_payroll/report/report_l10n_fr_fiche_paye.xml
+++ b/addons/l10n_fr_hr_payroll/report/report_l10n_fr_fiche_paye.xml
@@ -48,7 +48,7 @@
</td>
<td>
<span t-esc="o.employee_id.id or '_'"/><br/>
- <span t-esc="o.employee_id.ssnid or '_'"/><br/>
+ <span t-esc="o.employee_id.identification_id or '_'"/><br/>
<span t-esc="o.contract_id.job_id.name or '_'"/><br/>
<span t-esc="o.contract_id.qualif or '_'"/><br/>
<span t-esc="o.contract_id.niveau or '_'"/><br/>
diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py
index cc3519ef737..4c3bab77de1 100644
--- a/addons/mass_mailing/models/mass_mailing.py
+++ b/addons/mass_mailing/models/mass_mailing.py
@@ -507,7 +507,7 @@ class MassMailing(models.Model):
return super(MassMailing, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby)
def update_opt_out(self, email, res_ids, value):
- model = self.env[self.mailing_model_real]
+ model = self.env[self.mailing_model_real].with_context(active_test=False)
if 'opt_out' in model._fields:
email_fname = 'email_from'
if 'email' in model._fields:
diff --git a/addons/mass_mailing/static/src/js/mass_mailing_editor.js b/addons/mass_mailing/static/src/js/mass_mailing_editor.js
index 4475d001e5d..5d475808589 100644
--- a/addons/mass_mailing/static/src/js/mass_mailing_editor.js
+++ b/addons/mass_mailing/static/src/js/mass_mailing_editor.js
@@ -273,9 +273,13 @@ snippets_editor.Class.include({
function editable_area_is_empty($layout) {
$layout = $layout || $editable_area.find(".o_layout");
var $mail_wrapper = $layout.children(".o_mail_wrapper");
+ var $mail_wrapper_content = $mail_wrapper.find('.o_mail_wrapper_td');
+ if (!$mail_wrapper_content.length) { // compatibility
+ $mail_wrapper_content = $mail_wrapper;
+ }
return (
$editable_area.html().trim() === ""
- || ($layout.length > 0 && ($layout.html().trim() === "" || $mail_wrapper.length > 0 && $mail_wrapper.html().trim() === ""))
+ || ($layout.length > 0 && ($layout.html().trim() === "" || $mail_wrapper_content.length > 0 && $mail_wrapper_content.html().trim() === ""))
);
}
@@ -325,7 +329,15 @@ snippets_editor.Class.include({
switch_images(theme_params, $editable_area);
var $old_layout = $editable_area.find(".o_layout");
- var $new_wrapper = $("<div/>", {"class": "o_mail_wrapper oe_structure"});
+ // This wrapper structure is the only way to have a responsive and
+ // centered fixed-width content column on all mail clients
+ var $new_wrapper = $('<table/>', {class: 'o_mail_wrapper'});
+ var $new_wrapper_content = $("<td/>", {class: 'o_mail_no_resize o_mail_wrapper_td oe_structure'});
+ $new_wrapper.append($('<tr/>').append(
+ $("<td/>", {class: 'o_mail_no_resize'}),
+ $new_wrapper_content,
+ $("<td/>", {class: 'o_mail_no_resize'})
+ ));
var $new_layout = $("<div/>", {"class": "o_layout " + theme_params.className}).append($new_wrapper);
var $contents;
@@ -338,11 +350,11 @@ snippets_editor.Class.include({
}
$editable_area.empty().append($new_layout);
- $new_wrapper.append($contents);
+ $new_wrapper_content.append($contents);
$old_layout.remove();
if (first_choice) {
- self.add_default_snippet_text_classes($new_wrapper);
+ self.add_default_snippet_text_classes($new_wrapper_content);
}
self.show_blocks();
}
diff --git a/addons/mass_mailing/static/src/less/themes/theme_default.less b/addons/mass_mailing/static/src/less/themes/theme_default.less
index 98f86421000..74f71191db2 100644
--- a/addons/mass_mailing/static/src/less/themes/theme_default.less
+++ b/addons/mass_mailing/static/src/less/themes/theme_default.less
@@ -49,27 +49,36 @@
// ===== Layout =====
.o_layout {
- font-family: @o_mm_def_font;
- color: lighten(@o_mm_def_text_color, 20%);
overflow: hidden;
- width: 100%;
+ width: 94%;
min-height: 100%;
background-color: @o_mm_def_color_alpha;
- padding: 24px 0;
+ padding: 24px 3%;
+
+ // Forces on <p/> elements as several mail clients does not correctly
+ // inherit font properties
+ &, p {
+ font-family: @o_mm_def_font;
+ color: lighten(@o_mm_def_text_color, 20%);
+ }
> .o_mail_wrapper {
width: 100%;
- max-width: @o_mm_def_body_width;
- margin: auto;
- border: 1px solid darken(@o_mm_def_color_alpha, 2%);
- background-color: @o_mm_def_color_beta;
+ border-collapse: separate; // Allow the first and last td to have the
+ // same width
+
+ .o_mail_wrapper_td {
+ width: @o_mm_def_body_width;
+ border: 1px solid darken(@o_mm_def_color_alpha, 2%);
+ background-color: @o_mm_def_color_beta;
+ }
}
}
// ===== Snippets (general) =====
.o_mail_snippet_general {
width: 100%;
- max-width: @o_mm_def_body_width;
+ max-width: @o_mm_def_body_width; // should not be necessary thanks to mail wrapper
margin: 0 auto;
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
@@ -157,7 +166,11 @@
}
.o_mail_block_two_cols, .o_mail_block_three_cols {
- font-size: small;
+ // Forces on <p/> elements as several mail clients does not correctly
+ // inherit font properties
+ &, p {
+ font-size: small;
+ }
.o_mail_col_container {
padding: 10px 20px;
@@ -169,7 +182,11 @@
}
.o_mail_block_image_text, .o_mail_block_text_image {
- font-size: small;
+ // Forces on <p/> elements as several mail clients does not correctly
+ // inherit font properties
+ &, p {
+ font-size: small;
+ }
}
.o_mail_block_event {
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index 5fd40f455c7..ac799bd0538 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -499,11 +499,14 @@ class MrpProduction(models.Model):
@api.multi
def post_inventory(self):
for order in self:
+ moves_not_to_do = order.move_raw_ids.filtered(lambda x: x.state == 'done')
moves_to_do = order.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
moves_to_do.action_done()
+ moves_to_do = order.move_raw_ids.filtered(lambda x: x.state == 'done') - moves_not_to_do
order._cal_price(moves_to_do)
moves_to_finish = order.move_finished_ids.filtered(lambda x: x.state not in ('done','cancel'))
moves_to_finish.action_done()
+
for move in moves_to_finish:
#Group quants by lots
lot_quants = {}
diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py
index bb98284f9f2..b2fd5ea0ab6 100644
--- a/addons/payment/models/payment_acquirer.py
+++ b/addons/payment/models/payment_acquirer.py
@@ -507,7 +507,7 @@ class PaymentTransaction(models.Model):
tx.write({'reference': str(tx.id)})
# Generate callback hash if it is configured on the tx; avoid generating unnecessary stuff
- if tx.callback_model_id and tx.callback_res_id and tx.callback_method:
+ if tx.callback_model_id and tx.callback_res_id and tx.sudo().callback_method:
tx.write({'callback_hash': tx._generate_callback_hash()})
return tx
@@ -547,7 +547,9 @@ class PaymentTransaction(models.Model):
def _generate_callback_hash(self):
self.ensure_one()
secret = self.env['ir.config_parameter'].sudo().get_param('database.secret')
- token = '%s%s%s' % (self.callback_model_id.model, self.callback_res_id, self.callback_method)
+ token = '%s%s%s' % (self.callback_model_id.model,
+ self.callback_res_id,
+ self.sudo().callback_method)
return hmac.new(str(secret), token, hashlib.sha256).hexdigest()
# --------------------------------------------------
@@ -635,7 +637,7 @@ class PaymentTransaction(models.Model):
@api.multi
def execute_callback(self):
res = None
- for transaction in self.filtered(lambda tx: tx.callback_model_id and tx.callback_res_id and tx.callback_method):
+ for transaction in self.filtered(lambda tx: tx.callback_model_id and tx.callback_res_id and tx.sudo().callback_method):
valid_token = transaction._generate_callback_hash()
if not consteq(ustr(valid_token), transaction.callback_hash):
_logger.warning("Invalid callback signature for transaction %d" % (transaction.id))
@@ -643,7 +645,7 @@ class PaymentTransaction(models.Model):
record = self.env[transaction.callback_model_id.model].browse(transaction.callback_res_id).exists()
if record:
- res = getattr(record, transaction.callback_method)(transaction)
+ res = getattr(record, transaction.sudo().callback_method)(transaction)
else:
_logger.warning("Did not found record %s.%s for callback of transaction %d" % (transaction.callback_model_id.model, transaction.callback_res_id, transaction.id))
return res
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index ed5335c1c46..2bb4fb23dd9 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -677,7 +677,7 @@ class PosOrder(models.Model):
qty = 0
qty_done = 0
pack_lots = []
- pos_pack_lots = PosPackOperationLot.search([('order_id', '=', order.id), ('product_id', '=', pack_operation.product_id.id)])
+ pos_pack_lots = PosPackOperationLot.search([('order_id', '=', order.id), ('product_id', '=', pack_operation.product_id.id)])
pack_lot_names = [pos_pack.lot_name for pos_pack in pos_pack_lots]
if pack_lot_names and lots_necessary:
diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml
index 04c59ac13fb..71e4ca2c1c8 100644
--- a/addons/point_of_sale/static/src/xml/pos.xml
+++ b/addons/point_of_sale/static/src/xml/pos.xml
@@ -753,7 +753,7 @@
<t t-if='simple'>
<line>
<left><t t-esc='line.product_name_wrapped[0]' /></left>
- <right><value><t t-esc='line.price_display' /></value></right>
+ <right><value t-att-value-decimals='pos.currency.decimals'>><t t-esc='line.price_display' /></value></right>
</line>
<t t-call="XmlReceiptWrappedProductNameLines"/>
</t>
@@ -765,19 +765,19 @@
</t>
<line indent='1'>
<left>
- <value value-decimals='3' value-autoint='on'>
+ <value t-att-value-decimals='pos.dp["Product Unit of Measure"]' value-autoint='on'>
<t t-esc='line.quantity' />
</value>
<t t-if='line.unit_name !== "Unit(s)"'>
<t t-esc='line.unit_name' />
</t>
x
- <value value-decimals='2'>
+ <value t-att-value-decimals='pos.dp["Product Price"]'>
<t t-esc='line.price' />
</value>
</left>
<right>
- <value><t t-esc='line.price_display' /></value>
+ <value t-att-value-decimals='pos.currency.decimals'><t t-esc='line.price_display' /></value>
</right>
</line>
</t>
@@ -789,11 +789,11 @@
<t t-set='taxincluded' t-value='Math.abs(receipt.subtotal - receipt.total_with_tax) &lt;= 0.000001' />
<t t-if='!taxincluded'>
<line><right>--------</right></line>
- <line><left>Subtotal</left><right> <value><t t-esc="receipt.subtotal" /></value></right></line>
+ <line><left>Subtotal</left><right><value t-att-value-decimals='pos.currency.decimals'><t t-esc="receipt.subtotal" /></value></right></line>
<t t-foreach='receipt.tax_details' t-as='tax'>
<line>
<left><t t-esc='tax.name' /></left>
- <right><value><t t-esc='tax.amount' /></value></right>
+ <right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='tax.amount' /></value></right>
</line>
</t>
</t>
@@ -803,7 +803,7 @@
<line><right>--------</right></line>
<line class='total' size='double-height'>
<left><pre> TOTAL</pre></left>
- <right><value><t t-esc='receipt.total_with_tax' /></value></right>
+ <right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='receipt.total_with_tax' /></value></right>
</line>
<br/><br/>
@@ -812,14 +812,14 @@
<t t-foreach='paymentlines' t-as='line'>
<line>
<left><t t-esc='line.name' /></left>
- <right><value><t t-esc='line.get_amount()'/></value></right>
+ <right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='line.get_amount()'/></value></right>
</line>
</t>
<br/>
<line size='double-height'>
<left><pre> CHANGE</pre></left>
- <right><value><t t-esc='receipt.change' /></value></right>
+ <right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='receipt.change' /></value></right>
</line>
<br/>
@@ -828,19 +828,19 @@
<t t-if='receipt.total_discount'>
<line>
<left>Discounts</left>
- <right><value><t t-esc='receipt.total_discount'/></value></right>
+ <right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='receipt.total_discount'/></value></right>
</line>
</t>
<t t-if='taxincluded'>
<t t-foreach='receipt.tax_details' t-as='tax'>
<line>
<left><t t-esc='tax.name' /></left>
- <right><value><t t-esc='tax.amount' /></value></right>
+ <right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='tax.amount' /></value></right>
</line>
</t>
<line>
<left>Total Taxes</left>
- <right><value><t t-esc='receipt.total_tax'/></value></right>
+ <right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='receipt.total_tax'/></value></right>
</line>
</t>
diff --git a/addons/pos_mercury/models/pos_mercury_transaction.py b/addons/pos_mercury/models/pos_mercury_transaction.py
index 36748c5ea0c..ca392c4d895 100644
--- a/addons/pos_mercury/models/pos_mercury_transaction.py
+++ b/addons/pos_mercury/models/pos_mercury_transaction.py
@@ -17,7 +17,7 @@ class MercuryTransaction(models.Model):
_name = 'pos_mercury.mercury_transaction'
def _get_pos_session(self):
- pos_session = self.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', self.env.uid)])
+ pos_session = self.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', self.env.uid)], limit=1)
if not pos_session:
raise UserError("No POS session")
@@ -34,6 +34,7 @@ class MercuryTransaction(models.Model):
raise UserError("No Mercury configuration associated with the journal.")
def _setup_request(self, data):
+ # todo: in master make the client include the pos.session id and use that
pos_session = self._get_pos_session()
config = pos_session.config_id
diff --git a/addons/pos_restaurant/static/src/js/floors.js b/addons/pos_restaurant/static/src/js/floors.js
index 2494401ff00..67307794fbc 100644
--- a/addons/pos_restaurant/static/src/js/floors.js
+++ b/addons/pos_restaurant/static/src/js/floors.js
@@ -979,4 +979,11 @@ screens.define_action_button({
},
});
+return {
+ TableGuestsButton: TableGuestsButton,
+ TransferOrderButton:TransferOrderButton,
+ TableWidget: TableWidget,
+ FloorScreenWidget: FloorScreenWidget,
+};
+
});
diff --git a/addons/product/models/res_company.py b/addons/product/models/res_company.py
index 3d8d0dcfc70..d94e8d2cda2 100644
--- a/addons/product/models/res_company.py
+++ b/addons/product/models/res_company.py
@@ -18,12 +18,13 @@ class ResCompany(models.Model):
'currency_id': new_company.currency_id.id,
})
field = self.env['ir.model.fields']._get('res.partner', 'property_product_pricelist')
- self.env['ir.property'].create({
+ product_property = self.env['ir.property'].create({
'name': 'property_product_pricelist',
- 'company_id': new_company.id,
'value_reference': 'product.pricelist,%s' % pricelist.id,
'fields_id': field.id
})
+ # multi-company security rules prevents access
+ product_property.sudo().write({'company_id': new_company.id})
return new_company
@api.multi
diff --git a/addons/report/models/report.py b/addons/report/models/report.py
index 55abb28a638..3ff68a9052f 100644
--- a/addons/report/models/report.py
+++ b/addons/report/models/report.py
@@ -576,19 +576,23 @@ class Report(models.Model):
"""
writer = PdfFileWriter()
streams = [] # We have to close the streams *after* PdfFilWriter's call to write()
- for document in documents:
- pdfreport = open(document, 'rb')
- streams.append(pdfreport)
- reader = PdfFileReader(pdfreport)
- for page in range(0, reader.getNumPages()):
- writer.addPage(reader.getPage(page))
-
- merged_file_fd, merged_file_path = tempfile.mkstemp(suffix='.html', prefix='report.merged.tmp.')
- with closing(os.fdopen(merged_file_fd, 'w')) as merged_file:
- writer.write(merged_file)
-
- for stream in streams:
- stream.close()
+ try:
+ for document in documents:
+ pdfreport = open(document, 'rb')
+ streams.append(pdfreport)
+ reader = PdfFileReader(pdfreport)
+ for page in range(0, reader.getNumPages()):
+ writer.addPage(reader.getPage(page))
+
+ merged_file_fd, merged_file_path = tempfile.mkstemp(suffix='.pdf', prefix='report.merged.tmp.')
+ with closing(os.fdopen(merged_file_fd, 'w')) as merged_file:
+ writer.write(merged_file)
+ finally:
+ for stream in streams:
+ try:
+ stream.close()
+ except Exception:
+ pass
return merged_file_path
diff --git a/addons/sale/report/sale_report.xml b/addons/sale/report/sale_report.xml
index 3e4996dffe2..e0bad531aea 100644
--- a/addons/sale/report/sale_report.xml
+++ b/addons/sale/report/sale_report.xml
@@ -12,17 +12,5 @@
<record id="report_sale_order" model="ir.actions.report.xml">
<field name="print_report_name">(object.state in ('draft', 'sent') and 'Quotation'+'-'+(object.name) or 'Order'+'-'+(object.name))</field>
</record>
-
- <report
- string="Pro-Forma"
- id="report_proforma_quotation"
- model="sale.order"
- report_type="qweb-pdf"
- name="sale.report_saleproforma"
- file="sale.report_saleorder"
- />
- <record id="report_proforma_quotation" model="ir.actions.report.xml">
- <field name="print_report_name">'Pro-Forma'+'-'+(object.name)</field>
- </record>
</data>
</odoo>
diff --git a/addons/sale/report/sale_report_templates.xml b/addons/sale/report/sale_report_templates.xml
index b16c4d98192..71a6bbd144d 100644
--- a/addons/sale/report/sale_report_templates.xml
+++ b/addons/sale/report/sale_report_templates.xml
@@ -29,11 +29,11 @@
</div>
<h2>
- <t t-if="not proforma">
+ <t t-if="not env.context.get('proforma', False)">
<span t-if="doc.state not in ['draft','sent']">Order # </span>
<span t-if="doc.state in ['draft','sent']">Quotation # </span>
</t>
- <t t-if="proforma">
+ <t t-if="env.context.get('proforma', False)">
<span>Pro-Forma # </span>
</t>
<span t-field="doc.name"/>
diff --git a/addons/sale/security/sale_security.xml b/addons/sale/security/sale_security.xml
index 32c1d36917e..0a269ab44bf 100644
--- a/addons/sale/security/sale_security.xml
+++ b/addons/sale/security/sale_security.xml
@@ -4,7 +4,6 @@
<record id="sale.group_sale_layout" model="res.groups">
<field name="name">Personalize sales order and invoice report</field>
<field name="category_id" ref="base.module_category_hidden"/>
- <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="group_delivery_invoice_address" model="res.groups">
diff --git a/addons/sale/wizard/sale_make_invoice_advance.py b/addons/sale/wizard/sale_make_invoice_advance.py
index 3e28cbb9575..886d80d19bd 100644
--- a/addons/sale/wizard/sale_make_invoice_advance.py
+++ b/addons/sale/wizard/sale_make_invoice_advance.py
@@ -82,10 +82,11 @@ class SaleAdvancePaymentInv(models.TransientModel):
else:
amount = self.amount
name = _('Down Payment')
- if order.fiscal_position_id and self.product_id.taxes_id:
- tax_ids = order.fiscal_position_id.map_tax(self.product_id.taxes_id).ids
+ taxes = self.product_id.taxes_id.filtered(lambda r: not order.company_id or r.company_id == order.company_id)
+ if order.fiscal_position_id and taxes:
+ tax_ids = order.fiscal_position_id.map_tax(taxes).ids
else:
- tax_ids = self.product_id.taxes_id.ids
+ tax_ids = taxes.ids
invoice = inv_obj.create({
'name': order.client_order_ref or order.name,
@@ -145,10 +146,11 @@ class SaleAdvancePaymentInv(models.TransientModel):
raise UserError(_('The product used to invoice a down payment should have an invoice policy set to "Ordered quantities". Please update your deposit product to be able to create a deposit invoice.'))
if self.product_id.type != 'service':
raise UserError(_("The product used to invoice a down payment should be of type 'Service'. Please use another product or update this product."))
- if order.fiscal_position_id and self.product_id.taxes_id:
- tax_ids = order.fiscal_position_id.map_tax(self.product_id.taxes_id).ids
+ taxes = self.product_id.taxes_id.filtered(lambda r: not order.company_id or r.company_id == order.company_id)
+ if order.fiscal_position_id and taxes:
+ tax_ids = order.fiscal_position_id.map_tax(taxes).ids
else:
- tax_ids = self.product_id.taxes_id.ids
+ tax_ids = taxes.ids
so_line = sale_line_obj.create({
'name': _('Advance: %s') % (time.strftime('%m %Y'),),
'price_unit': amount,
diff --git a/addons/sale_mrp/sale_mrp.py b/addons/sale_mrp/sale_mrp.py
index 8aa4074c8bb..03aec004f03 100644
--- a/addons/sale_mrp/sale_mrp.py
+++ b/addons/sale_mrp/sale_mrp.py
@@ -42,6 +42,30 @@ class SaleOrderLine(models.Model):
return 0.0
return super(SaleOrderLine, self)._get_delivered_qty()
+ @api.multi
+ def _get_bom_component_qty(self, bom):
+ bom_quantity = self.product_uom._compute_quantity(self.product_uom_qty, bom.product_uom_id)
+ boms, lines = bom.explode(self.product_id, bom_quantity)
+ components = {}
+ for line, line_data in lines:
+ product = line.product_id.id
+ uom = line.product_uom_id
+ qty = line.product_qty
+ if components.get(product, False):
+ if uom.id != components[product]['uom']:
+ from_uom = uom
+ to_uom = self.env['product.uom'].browse(components[product]['uom'])
+ qty = from_uom._compute_quantity(qty, to_uom_id=to_uom)
+ components[product]['qty'] += qty
+ else:
+ # To be in the uom reference of the product
+ to_uom = self.env['product.product'].browse(product).uom_id
+ if uom.id != to_uom.id:
+ from_uom = uom
+ qty = from_uom._compute_quantity(qty, to_uom_id=to_uom)
+ components[product] = {'qty': qty, 'uom': to_uom.id}
+ return components
+
class AccountInvoiceLine(models.Model):
# TDE FIXME: what is this code ??
diff --git a/addons/sale_timesheet/models/procurement.py b/addons/sale_timesheet/models/procurement.py
index bd7e4c85329..437a6f7398c 100644
--- a/addons/sale_timesheet/models/procurement.py
+++ b/addons/sale_timesheet/models/procurement.py
@@ -71,7 +71,7 @@ class ProcurementOrder(models.Model):
'partner_id': self.sale_line_id.order_id.partner_id.id or self.partner_dest_id.id,
'user_id': self.env.uid,
'procurement_id': self.id,
- 'description': self.name + '\n',
+ 'description': self.name + '<br/>',
'project_id': project.id,
'company_id': self.company_id.id,
}
diff --git a/addons/stock/models/procurement.py b/addons/stock/models/procurement.py
index 7ce3bb2abed..902e0517ef5 100644
--- a/addons/stock/models/procurement.py
+++ b/addons/stock/models/procurement.py
@@ -273,6 +273,11 @@ class ProcurementOrder(models.Model):
def _procurement_from_orderpoint_post_process(self, orderpoint_ids):
return True
+ def _get_orderpoint_domain(self, company_id=False):
+ domain = [('company_id', '=', company_id)] if company_id else []
+ domain += [('product_id.active', '=', True)]
+ return domain
+
@api.model
def _procure_orderpoint_confirm(self, use_new_cursor=False, company_id=False):
""" Create procurements based on orderpoints.
@@ -282,9 +287,7 @@ class ProcurementOrder(models.Model):
"""
OrderPoint = self.env['stock.warehouse.orderpoint']
-
- domain = [('company_id', '=', company_id)] if company_id else []
- domain += [('product_id.active', '=', True)]
+ domain = self._get_orderpoint_domain(company_id=company_id)
orderpoints_noprefetch = OrderPoint.with_context(prefetch_fields=False).search(domain,
order=self._procurement_from_orderpoint_get_order()).ids
while orderpoints_noprefetch:
diff --git a/addons/stock/models/res_company.py b/addons/stock/models/res_company.py
index c1fd7aff521..6860ce7496b 100644
--- a/addons/stock/models/res_company.py
+++ b/addons/stock/models/res_company.py
@@ -23,14 +23,17 @@ class Company(models.Model):
location = self.env['stock.location'].create({
'name': _('%s: Transit Location') % self.name,
'usage': 'transit',
- 'company_id': self.id,
'location_id': parent_location and parent_location.id or False,
})
+ location.sudo().write({'company_id': self.id})
self.write({'internal_transit_location_id': location.id})
@api.model
def create(self, vals):
company = super(Company, self).create(vals)
- self.env['stock.warehouse'].create({'name': company.name, 'code': company.name[:5], 'company_id': company.id})
+
+ # multi-company rules prevents creating warehouse and sub-locations
+ self.env['stock.warehouse'].check_access_rights('create')
+ self.env['stock.warehouse'].sudo().create({'name': company.name, 'code': company.name[:5], 'company_id': company.id})
company.create_transit_location()
return company
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index 591842e4225..b7ec53666ae 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -495,9 +495,12 @@ class StockMove(models.Model):
self._push_apply()
return self
+ def _set_default_price_moves(self):
+ return self.filtered(lambda move: not move.price_unit)
+
def set_default_price_unit_from_product(self):
""" Set price to move, important in inter-company moves or receipts with only one partner """
- for move in self.filtered(lambda move: not move.price_unit):
+ for move in self._set_default_price_moves():
move.write({'price_unit': move.product_id.standard_price})
attribute_price = set_default_price_unit_from_product
diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml
index 5d5f2b2b4e6..ff42bf3095a 100644
--- a/addons/stock/views/stock_picking_views.xml
+++ b/addons/stock/views/stock_picking_views.xml
@@ -226,7 +226,7 @@
<h1 class="hidden-xs">
<field name="name" attrs="{'invisible': [('name','=','/')]}" readonly="1"/>
</h1>
- <group class="hidden-xs">
+ <group>
<group>
<field name="partner_id" attrs="{'invisible': [('picking_type_code', '=', 'internal')]}"/>
<field name="location_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'outgoing')]}"/>
diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py
index fa373b6c9eb..ca8e2e840dc 100644
--- a/addons/stock_account/models/stock.py
+++ b/addons/stock_account/models/stock.py
@@ -179,6 +179,12 @@ class StockMove(models.Model):
to_refund = fields.Boolean(string="To Refund (update SO/PO)",
help='Trigger a decrease of the delivered/received quantity in the associated Sale Order/Purchase Order')
+ def _set_default_price_moves(self):
+ # When the cost method is in real or average price, the price can be set to 0.0 on the PO
+ # So the price doesn't have to be updated
+ moves = super(StockMove, self)._set_default_price_moves()
+ return moves.filtered(lambda m: m.product_id.cost_method not in ('real', 'average'))
+
@api.multi
def action_done(self):
self.product_price_update_before_done()
diff --git a/addons/web/static/src/js/widgets/debug_manager.js b/addons/web/static/src/js/widgets/debug_manager.js
index cdb3236aa64..badff89c846 100644
--- a/addons/web/static/src/js/widgets/debug_manager.js
+++ b/addons/web/static/src/js/widgets/debug_manager.js
@@ -168,6 +168,7 @@ var DebugManager = Widget.extend({
res_model: 'ir.ui.view',
title: _t('Select a view'),
disable_multiple_selection: true,
+ domain: [['type', '!=', 'qweb'], ['type', '!=', 'search']],
on_selected: function (element_ids) {
self._rpc({
model: 'ir.ui.view',
diff --git a/addons/web_tour/static/src/less/tip.less b/addons/web_tour/static/src/less/tip.less
index c53ad943ff1..8f994e64ee7 100644
--- a/addons/web_tour/static/src/less/tip.less
+++ b/addons/web_tour/static/src/less/tip.less
@@ -1,6 +1,28 @@
.o_tooltip_parent {
position: relative!important;
+
+ // Tooltips are placed in the <body/> element when opened because this is
+ // the only way to position them above everything else thanks to a 1051
+ // z-index. However, for scrolling performance, the tooltip is placed in its
+ // ideal location (see Tip._get_ideal_location). When in this location, the
+ // tooltip were sometimes overlapping unwanted elements (e.g. chat windows).
+ //
+ // The z-index of the ideal locations cannot be determined safely as they
+ // are different from one tip to another. So, in the appswitcher for
+ // example, the tooltips will act as root-level z-index 1051. As the chat
+ // windows are root-level z-index 3, the tooltips will overlap in that case.
+ //
+ // Changing the opacity of the tooltip parents forces the creation of a
+ // stacking context; the appswitcher tooltips are thus now considered to be
+ // root-level z-index auto (or the default appswitcher one) and should so
+ // act like their parent (e.g. the appswitcher is below the chat windows so
+ // the inner tooltips will be too). The tips will be above all elements of
+ // the appswitcher as they still have a high z-index, but relative to the
+ // appswitcher (this is especially useful in the website where most tooltips
+ // are placed in the body and need to be placed above elements with z-index
+ // like the navbar).
+ opacity: 0.999;
}
.o_tooltip {
@@ -39,7 +61,7 @@
border: @o-tip-border-width solid @o-tip-color;
border-radius: 50%;
padding: 0; // ! Cannot be changed ! Javascript rely on this.
- z-index: @zindex-modal+1;
+ z-index: @zindex-modal + 1; // See comment on 'o_tooltip_parent' class
background-color: @o-tip-color;
box-shadow: 0 2px 2px 1px rgba(150, 150, 150, 0.3);
diff --git a/addons/website/static/src/js/website.snippets.animation.js b/addons/website/static/src/js/website.snippets.animation.js
index e4dd19e03db..ed4b810179c 100644
--- a/addons/website/static/src/js/website.snippets.animation.js
+++ b/addons/website/static/src/js/website.snippets.animation.js
@@ -520,52 +520,71 @@ animation.registry.gallery = animation.Class.extend({
animation.registry.gallery_slider = animation.Class.extend({
selector: ".o_slideshow",
- start: function () {
- var $carousel = this.$target.is(".carousel") ? this.$target : this.$target.find(".carousel");
- var $indicator = $carousel.find('.carousel-indicators');
- var $lis = $indicator.find('li:not(.fa)');
- var $prev = $indicator.find('li.fa:first');
- var $next = $indicator.find('li.fa:last');
- var index = ($lis.filter('.active').index() || 1) -1;
- var page = Math.floor(index / 10);
- var nb = Math.ceil($lis.length / 10);
-
- // fix bootstrap use index insead of data-slide-to
- $carousel.on('slide.bs.carousel', function () {
+ start: function (editable_mode) {
+ var self = this;
+ this.$carousel = this.$target.is(".carousel") ? this.$target : this.$target.find(".carousel");
+ this.$indicator = this.$carousel.find('.carousel-indicators');
+ this.$prev = this.$indicator.find('li.fa:first').css('visibility', ''); // force visibility as some databases have it hidden
+ this.$next = this.$indicator.find('li.fa:last').css('visibility', '');
+ var $lis = this.$indicator.find('li:not(.fa)');
+ var nbPerPage = Math.floor(this.$indicator.width() / $lis.first().outerWidth(true)) - 3; // - navigator - 1 to leave some space
+ var realNbPerPage = nbPerPage || 1;
+ var nbPages = Math.ceil($lis.length / realNbPerPage);
+
+ var index;
+ var page;
+ update();
+
+ function hide() {
+ $lis.each(function (i) {
+ $(this).toggleClass('hidden', !(i >= page*nbPerPage && i < (page+1)*nbPerPage));
+ });
+ if (editable_mode) { // do not remove DOM in edit mode
+ return;
+ }
+ if (page <= 0) {
+ self.$prev.detach();
+ } else {
+ self.$prev.prependTo(self.$indicator);
+ }
+ if (page >= nbPages - 1) {
+ self.$next.detach();
+ } else {
+ self.$next.appendTo(self.$indicator);
+ }
+ }
+
+ function update() {
+ index = $lis.index($lis.filter('.active')) || 0;
+ page = Math.floor(index / realNbPerPage);
+ hide();
+ }
+
+ this.$carousel.on('slide.bs.carousel.gallery_slider', function () {
setTimeout(function () {
- var $item = $carousel.find('.carousel-inner .prev, .carousel-inner .next');
+ var $item = self.$carousel.find('.carousel-inner .prev, .carousel-inner .next');
var index = $item.index();
$lis.removeClass("active")
.filter('[data-slide-to="'+index+'"]')
.addClass("active");
- },0);
- });
-
- function hide () {
- $lis.addClass('hidden').each(function (i) {
- if (i >= page*10 && i < (page+1)*10) {
- $(this).removeClass('hidden');
- }
- });
- $prev.css('visibility', page === 0 ? 'hidden' : '');
- $next.css('visibility', (page+1) >= nb ? 'hidden' : '');
- }
-
- $indicator.find('li.fa').on('click', function () {
- page = (page + ($(this).hasClass('o_indicators_left')?-1:1)) % nb;
- $carousel.carousel(page*10);
- hide();
+ }, 0);
});
- hide();
-
- $carousel.on('slid.bs.carousel', function () {
- var index = ($lis.filter('.active').index() || 1) -1;
- page = Math.floor(index / 10);
+ this.$indicator.on('click.gallery_slider', '> li.fa', function () {
+ page += ($(this).hasClass('o_indicators_left') ? -1 : 1);
+ page = Math.max(0, Math.min(nbPages - 1, page)); // should not be necessary
+ self.$carousel.carousel(page * realNbPerPage);
hide();
});
+ this.$carousel.on('slid.bs.carousel.gallery_slider', update);
+ },
+ stop: function () {
+ this._super.apply(this, arguments);
- return this._super.apply(this, arguments);
- }
+ this.$prev.prependTo(this.$indicator);
+ this.$next.appendTo(this.$indicator);
+ this.$carousel.off('.gallery_slider');
+ this.$indicator.off('.gallery_slider');
+ },
});
});
diff --git a/addons/website/static/src/js/website.snippets.editor.js b/addons/website/static/src/js/website.snippets.editor.js
index 40be63e3b9d..083ed195905 100644
--- a/addons/website/static/src/js/website.snippets.editor.js
+++ b/addons/website/static/src/js/website.snippets.editor.js
@@ -223,9 +223,6 @@ options.registry.carousel = options.registry.slider.extend({
// set background and prepare to clean for save
this.$target.on('slid.bs.carousel', function () {
- self.editor.styles.background_position.$target = self.editor.styles.background.$target;
- self.editor.styles.background_position.set_active();
- self.editor.styles.background.$target.trigger("snippet-option-change", [self.editor.styles.background]);
self.$target.carousel("pause");
if (!self.editor) return;
diff --git a/addons/website/static/src/less/website.snippets.less b/addons/website/static/src/less/website.snippets.less
index 4fce9f14246..5f96bbac0e6 100644
--- a/addons/website/static/src/less/website.snippets.less
+++ b/addons/website/static/src/less/website.snippets.less
@@ -324,9 +324,6 @@ div.carousel .container {
max-height: 100%;
max-width: 100%;
margin: auto;
- position: relative;
- top: 50%;
- .translate(0, -50%);
}
ul.carousel-indicators {
@@ -357,6 +354,10 @@ div.carousel .container {
filter: gray;
filter: url("data:image/svg+xml;utf8,<svg version='1.1' xmlns='http://www.w3.org/2000/svg' height='0'><filter id='greyscale'><feColorMatrix type='matrix' values='0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0' /></filter></svg>#greyscale");
}
+
+ &:first-child {
+ margin-left: 0;
+ }
}
}
diff --git a/addons/website_event/models/event.py b/addons/website_event/models/event.py
index 640eb9a3121..8fab469c5c8 100644
--- a/addons/website_event/models/event.py
+++ b/addons/website_event/models/event.py
@@ -57,7 +57,14 @@ class Event(models.Model):
if self.event_type_id:
self.website_menu = self.event_type_id.website_menu
+ def _get_standard_menu_entries_names(self):
+ """ Method returning menu entries possibly generated by event modules.
+ It is used to distinguish module-generated pages from custom pages. """
+ return [_('Introduction'), _('Location'), _('Register')]
+
def _get_menu_entries(self):
+ """ Method returning menu entries to display on the website view of the
+ event, possibly depending on some options in inheriting modules. """
self.ensure_one()
return [
(_('Introduction'), False, 'website_event.template_intro'),
@@ -75,9 +82,17 @@ class Event(models.Model):
root_menu = self.env['website.menu'].create({'name': event.name})
event.menu_id = root_menu
+ existing_page_names = event.menu_id.child_id.mapped('name')
+ required_page_names = [entry[0] for entry in self._get_menu_entries()]
+ standard_page_names = self._get_standard_menu_entries_names()
+
+ # remove entries that should not exist anymore
+ submenu_to_delete = event.menu_id.child_id.filtered(lambda menu: menu.name not in required_page_names and menu.name in standard_page_names)
+ submenu_to_delete.unlink()
+
+ # create missing entries
for sequence, (name, url, xml_id) in enumerate(self._get_menu_entries()):
- existing_pages = event.menu_id.child_id.mapped('name')
- if name not in existing_pages:
+ if name not in existing_page_names:
if not url:
newpath = self.env['website'].new_page(name + ' ' + self.name, xml_id, ispage=False)
url = "/event/" + slug(self) + "/page/" + newpath
diff --git a/addons/website_event_track/models/event.py b/addons/website_event_track/models/event.py
index 3acd8e8279f..baae681bc7b 100644
--- a/addons/website_event_track/models/event.py
+++ b/addons/website_event_track/models/event.py
@@ -80,6 +80,21 @@ class Event(models.Model):
self.website_track = False
self.website_track_proposal = False
+ @api.onchange('website_track')
+ def _onchange_website_track(self):
+ if not self.website_track:
+ self.website_track_proposal = False
+
+ @api.onchange('website_track_proposal')
+ def _onchange_website_track_proposal(self):
+ if self.website_track_proposal:
+ self.website_track = True
+
+ def _get_standard_menu_entries_names(self):
+ res = super(Event, self)._get_standard_menu_entries_names()
+ res += [_('Talks'), _('Agenda'), _('Talk Proposals')]
+ return res
+
def _get_menu_entries(self):
self.ensure_one()
res = super(Event, self)._get_menu_entries()
diff --git a/addons/website_form/controllers/main.py b/addons/website_form/controllers/main.py
index 6a93f387ec1..11abd44ef76 100644
--- a/addons/website_form/controllers/main.py
+++ b/addons/website_form/controllers/main.py
@@ -171,7 +171,7 @@ class WebsiteForm(http.Controller):
return data
def insert_record(self, request, model, values, custom, meta=None):
- record = request.env[model.model].sudo().create(values)
+ record = request.env[model.model].sudo().with_context(mail_create_nosubscribe=True).create(values)
if custom or meta:
default_field = model.website_form_default_field_id
diff --git a/addons/website_mail_channel/controllers/main.py b/addons/website_mail_channel/controllers/main.py
index 9a64504e309..c9b3deb268e 100644
--- a/addons/website_mail_channel/controllers/main.py
+++ b/addons/website_mail_channel/controllers/main.py
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import werkzeug
+
from datetime import datetime
from dateutil import relativedelta
@@ -119,6 +121,9 @@ class MailGroup(http.Controller):
"/groups/<model('mail.channel'):group>/page/<int:page>"
], type='http', auth="public", website=True)
def thread_headers(self, group, page=1, mode='thread', date_begin=None, date_end=None, **post):
+ if group.channel_type != 'channel':
+ raise werkzeug.exceptions.NotFound()
+
Message = request.env['mail.message']
domain = [('model', '=', 'mail.channel'), ('res_id', '=', group.id), ('message_type', '!=', 'notification')]
@@ -151,6 +156,9 @@ class MailGroup(http.Controller):
'''/groups/<model('mail.channel'):group>/<model('mail.message', "[('model','=','mail.channel'), ('res_id','=',group[0])]"):message>''',
], type='http', auth="public", website=True)
def thread_discussion(self, group, message, mode='thread', date_begin=None, date_end=None, **post):
+ if group.channel_type != 'channel':
+ raise werkzeug.exceptions.NotFound()
+
Message = request.env['mail.message']
if mode == 'thread':
base_domain = [('model', '=', 'mail.channel'), ('res_id', '=', group.id), ('parent_id', '=', message.parent_id and message.parent_id.id or False)]
@@ -175,6 +183,9 @@ class MailGroup(http.Controller):
'''/groups/<model('mail.channel'):group>/<model('mail.message', "[('model','=','mail.channel'), ('res_id','=',group[0])]"):message>/get_replies''',
type='json', auth="public", methods=['POST'], website=True)
def render_messages(self, group, message, **post):
+ if group.channel_type != 'channel':
+ return False
+
last_displayed_id = post.get('last_displayed_id')
if not last_displayed_id:
return False
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 63bf22b2a8b..917fb4258da 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -465,7 +465,7 @@
<a id="add_to_cart" class="btn btn-primary btn-lg mt8 js_check_product a-submit" href="#">Add to Cart</a>
</div>
</form>
- <hr />
+ <hr t-if="product.availability != 'empty'"/>
<div t-if="product.availability == 'in_stock'" class="alert alert-success">
<span class="fa fa-check" />
<span>In Stock</span>
@@ -1431,7 +1431,7 @@
<div class="col-sm-12">
<form target="_self" action="/shop/payment/validate" method="post" class="pull-right">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()" />
- <a style="width:127px;" class="btn btn-primary a-submit">
+ <a class="btn btn-primary a-submit">
<span t-if="order.amount_total &gt; 0">Pay Now <span class="fa fa-long-arrow-right"></span></span>
<span t-if="order.amount_total == 0">Confirm Order <span class="fa fa-long-arrow-right"></span></span>
</a>