[MERGE] forward port branch 12.0 up to cd21c016b0

This commit is contained in:
Christophe Simonis
2019-06-27 18:01:07 +02:00
38 changed files with 225 additions and 44 deletions
+2
View File
@@ -879,6 +879,8 @@ class AccountInvoice(models.Model):
bank_id = bank_ids[0].id if bank_ids else False
self.partner_bank_id = bank_id
domain = {'partner_bank_id': [('id', 'in', bank_ids.ids)]}
elif type == 'out_invoice':
domain = {'partner_bank_id': [('partner_id.ref_company_ids', 'in', [self.company_id.id])]}
res = {}
if warning:
+2 -1
View File
@@ -1597,7 +1597,8 @@ class AccountPartialReconcile(models.Model):
move_date = self.debit_move_id.date
newly_created_move = self.env['account.move']
with self.env.norecompute():
for move in (self.debit_move_id.move_id, self.credit_move_id.move_id):
# We use a set here in case the reconciled lines belong to the same move (it happens with POS)
for move in {self.debit_move_id.move_id, self.credit_move_id.move_id}:
#move_date is the max of the 2 reconciled items
if move_date < move.date:
move_date = move.date
@@ -609,7 +609,10 @@
<field name="fiscal_position_id" options="{'no_create': True}" />
<field name="move_id" groups="account.group_account_user" attrs="{'invisible': [('move_id', '=', False)]}"/>
<field name="origin" groups="base.group_user" attrs="{'invisible': [('origin', '=', False)]}"/>
<field name="partner_bank_id" domain="[('partner_id.ref_company_ids', 'in', [company_id])]" context="{'default_partner_id':company_id}"/>
<!-- Since the same form view is used for customer invoices and credit notes,
we can't set a domain or a default value context on partner_bank_id. We must
rely on the onchange -->
<field name="partner_bank_id"/>
</group>
</group>
<field name="tax_line_ids">
@@ -82,6 +82,7 @@
<div class="o_setting_left_pane"/>
<div class="o_setting_right_pane">
<span class="o_form_label">Rounding Method</span>
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." aria-label="Values set here are company-specific." groups="base.group_multi_company" role="img"/>
<div class="text-muted">
How total tax amount is computed in orders and invoices
</div>
+17
View File
@@ -39,6 +39,23 @@ class FinancialYearOpeningWizard(models.TransientModel):
(wiz.fiscalyear_last_month, wiz.fiscalyear_last_day)
)
@api.multi
def write(self, vals):
# Amazing workaround: non-stored related fields on company are a BAD idea since the 3 fields
# must follow the constraint '_check_fiscalyear_last_day'. The thing is, in case of related
# fields, the inverse write is done one value at a time, and thus the constraint is verified
# one value at a time... so it is likely to fail.
for wiz in self:
wiz.company_id.write({
'account_opening_date': vals.get('opening_date') or wiz.company_id.account_opening_date,
'fiscalyear_last_day': vals.get('fiscalyear_last_day') or wiz.company_id.fiscalyear_last_day,
'fiscalyear_last_month': vals.get('fiscalyear_last_month') or wiz.company_id.fiscalyear_last_month,
})
vals.pop('opening_date', None)
vals.pop('fiscalyear_last_day', None)
vals.pop('fiscalyear_last_month', None)
return super().write(vals)
@api.multi
def action_save_onboarding_fiscal_year(self):
self.env.user.company_id.set_onboarding_step_done('account_setup_fy_data_state')
+3
View File
@@ -733,6 +733,9 @@
help="Opportunities that are assigned to me"/>
<filter string="Unassigned" name="unassigned"
domain="[('user_id','=', False)]" help="No salesperson"/>
<filter string="Open Opportunities" name="open_opportunities"
domain="[('probability', '&lt;', 100), ('type', '=', 'opportunity')]"
help="Open Opportunities"/>
<separator/>
<filter string="Unread Messages" name="message_needaction" domain="[('message_needaction','=',True)]"/>
<separator/>
+1 -1
View File
@@ -206,7 +206,7 @@
</div>
<div class="row" t-if="record.opportunities_count.raw_value">
<div class="col-8">
<a name="%(crm_case_form_view_salesteams_opportunity)d" type="action"> <!-- context="{'search_default_probability': NOT or < 100}" -->
<a name="%(crm_case_form_view_salesteams_opportunity)d" type="action" context="{'search_default_open_opportunities': True}"> <!-- context="{'search_default_probability': NOT or < 100}" -->
<field name="opportunities_count"/>
<t t-if="record.opportunities_count.raw_value == 1">Open Opportunity</t>
<t t-else="">Open Opportunities</t>
+17 -5
View File
@@ -189,12 +189,24 @@ class GoogleService(models.TransientModel):
status = error.response.status_code
response = ""
else:
req = json.loads(error.request.body or 'null')
res = error.response.json()
_logger.exception("Error while requesting Google Services\nRequest:\n%s\nResponse:\n%s", pformat(req), pformat(res))
try:
exception_req = pformat(json.loads(error.request.body))
except:
exception_req = error.request.body
try:
exception_res = pformat(error.response.json())
error_msg = _("Error while requesting Google Services: %s") % res['error']['message']
warning_msg = _("Something went wrong with your request to google: %s") % res['error']['message']
except:
exception_res = error.response.text
error_msg = _("Error while requesting Google Services")
warning_msg = _("Something went wrong with your request to google")
_logger.exception("Error while requesting Google Services\nRequest:\n%s\nResponse:\n%s", exception_req, exception_res)
if error.response.status_code in (400, 401, 410):
raise UserError(_("Error while requesting Google Services: %s") % res['error']['message'])
raise self.env['res.config.settings'].get_config_warning(_("Something went wrong with your request to google: %s") % res['error']['message'])
raise UserError(error_msg)
raise self.env['res.config.settings'].get_config_warning(warning_msg)
return (status, response, ask_time)
# TODO : remove me, it is only used in google calendar. Make google_calendar use the constants
@@ -6,6 +6,7 @@ var basicFields = require('web.basic_fields');
var fieldUtils = require('web.field_utils');
var fieldRegistry = require('web.field_registry');
var _registry = require('web._field_registry'); // wait registry to be filled...
var session = require('web.session');
/**
@@ -75,7 +76,6 @@ var FieldTimesheetToggle = basicFields.FieldFloatToggle.extend({
* implementation (float_time, float_toggle, ...). The default
* value will be 'float_factor'.
**/
var FieldTimesheetUom = FieldTimesheetFactor;
var widgetName = 'timesheet_uom' in session ?
session.timesheet_uom.timesheet_widget : 'float_factor';
var FieldTimesheetUom = widgetName === 'float_toggle' ?
@@ -90,7 +90,7 @@ var _tweak_options = function(options) {
options.factor = session.timesheet_uom_factor;
}
return options;
}
};
fieldUtils.format.timesheet_uom = function(value, field, options) {
options = _tweak_options(options || {});
@@ -106,4 +106,3 @@ fieldUtils.parse.timesheet_uom = function(value, field, options) {
return FieldTimesheetUom;
});
+1 -1
View File
@@ -864,7 +864,7 @@
<field name="account_id" ref="pcg_44566"/>
<field name="sequence" eval="10"/>
<field name="type_tax_use">purchase</field>
<field name="tag_ids" eval="[(6,0,[ref('l10n_fr.tag_fr_08'),ref('l10n_fr.tag_fr_17')])]"/>
<field name="tag_ids" eval="[(6,0,[ref('l10n_fr.tag_fr_27'),ref('l10n_fr.tag_fr_36')])]"/>
<field name="tax_group_id" ref="tax_group_tva_55"/>
</record>
+3 -1
View File
@@ -305,7 +305,9 @@ class AccountFrFec(models.TransientModel):
END
AS PieceRef,
TO_CHAR(am.date, 'YYYYMMDD') AS PieceDate,
CASE WHEN aml.name IS NULL OR aml.name = '' THEN '/' ELSE replace(replace(aml.name, '|', '/'), '\t', '') END AS EcritureLib,
CASE WHEN aml.name IS NULL OR aml.name = '' THEN '/'
WHEN aml.name SIMILAR TO '[\t|\s|\n]*' THEN '/'
ELSE replace(replace(replace(aml.name, '|', '/'), '\t', ''), '\n', '') END AS EcritureLib,
replace(CASE WHEN aml.debit = 0 THEN '0,00' ELSE to_char(aml.debit, '000000000000000D99') END, '.', ',') AS Debit,
replace(CASE WHEN aml.credit = 0 THEN '0,00' ELSE to_char(aml.credit, '000000000000000D99') END, '.', ',') AS Credit,
CASE WHEN rec.name IS NULL THEN '' ELSE rec.name END AS EcritureLet,
@@ -134,10 +134,6 @@ MailManager.include({
}).always(function () {
thread.isCreatingWindow = false;
});
} else if (!options.passively) {
if (threadWindow.isHidden()) {
this._makeThreadWindowVisible(threadWindow);
}
}
def.then(function () {
threadWindow.updateVisualFoldState();
@@ -222,6 +222,91 @@ QUnit.test('hidden thread windows dropdown when not enough horizontal space (ver
testUtils.mock.unpatch(this.services.mail_service);
});
QUnit.test('receive message from hidden thread window', function (assert) {
// This is almost the same test as the one before, except there are
// at most 3 thread windows visible, and 2 thread windows are visible
// when it shows the 'hidden thread window' button.
// This case occurs when the amount of available slots depends on
// whether the hidden button should be displayed or not
// Example:
// - global width of 400px
// - button width of 100px
// - thread width of 250px
//
// Without button: 1 thread window (250px < 800px)
// With button: 1 thread window (250px + 100px = 350px < 400px)
assert.expect(7);
testUtils.patch(this.services.mail_service, {
HIDDEN_THREAD_WINDOW_DROPDOWN_BUTTON_WIDTH: 100,
THREAD_WINDOW_WIDTH: 250,
_getGlobalWidth: function () { return 400; },
});
var channels = [{
id: 1,
channel_type: 'channel',
name: "channel" + 1,
}, {
id: 2,
channel_type: 'channel',
name: "channel" + 2,
}];
this.data.initMessaging = {
channel_slots: {
channel_channel: channels,
},
};
var parent = this.createParent({
data: this.data,
services: this.services,
});
// detach channel 2 first, so that chanel 1 is open and channel 2 is hidden
parent.call('mail_service', 'getChannel', 2).detach();
parent.call('mail_service', 'getChannel', 1).detach();
var $visibleThreadWindows = $('.o_thread_window:not(.o_thread_window_dropdown, .o_hidden)');
assert.strictEqual($visibleThreadWindows.length, 1,
"should have 1 thread windows visible (as many as available slots)");
assert.strictEqual($visibleThreadWindows.filter('[data-thread-id="1"]').length, 1,
"the thread window with ID 1 should be visible");
assert.strictEqual($('.o_thread_window.o_hidden[data-thread-id="2"]').length, 1,
"the thread window with ID 2 should be hidden");
assert.strictEqual(
$('.o_thread_window_dropdown .o_total_unread_counter').text().trim(),
"",
"should have no unread counter on hidden dropup menu");
// simulate receiving a message in channel ID 2
var messageData = {
author_id: [5, "Someone else"],
body: "<p>Test message</p>",
id: 1,
model: 'mail.channel',
res_id: 2,
channel_ids: [2],
};
var notification = [[false, 'mail.channel', 2], messageData];
parent.call('bus_service', 'trigger', 'notification', [notification]);
$visibleThreadWindows = $('.o_thread_window:not(.o_thread_window_dropdown, .o_hidden)');
assert.strictEqual($visibleThreadWindows.filter('[data-thread-id="1"]').length, 1,
"the thread window with ID 1 should stay visible");
assert.strictEqual($('.o_thread_window.o_hidden[data-thread-id="2"]').length, 1,
"the thread window with ID 2 should stay hidden");
assert.strictEqual(
$('.o_thread_window_dropdown .o_total_unread_counter').text().trim(),
"1",
"should have unread counter of 1 on hidden dropup menu");
parent.destroy();
testUtils.unpatch(this.services.mail_service);
});
});
});
});
+3 -1
View File
@@ -9,7 +9,7 @@ import math
from odoo import api, fields, models, _
from odoo.addons import decimal_precision as dp
from odoo.exceptions import AccessError, UserError
from odoo.tools import float_compare, float_round, DEFAULT_SERVER_DATETIME_FORMAT
from odoo.tools import float_compare, float_round, float_is_zero, DEFAULT_SERVER_DATETIME_FORMAT
class MrpProduction(models.Model):
""" Manufacturing Orders """
@@ -820,6 +820,8 @@ class MrpProduction(models.Model):
moves_to_do = order.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
for move in moves_to_do.filtered(lambda m: m.product_qty == 0.0 and m.quantity_done > 0):
move.product_uom_qty = move.quantity_done
for move in moves_to_do.filtered(lambda m: float_is_zero(m.quantity_done, precision_rounding=m.product_uom.rounding)):
move._action_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)
@@ -14,8 +14,7 @@ class ReportBomStructure(models.AbstractModel):
docs = []
for bom_id in docids:
bom = self.env['mrp.bom'].browse(bom_id)
variant = data and data.get('variant')
candidates = variant and self.env['product.product'].browse(variant) or bom.product_tmpl_id.product_variant_ids
candidates = bom.product_id or bom.product_tmpl_id.product_variant_ids
for product_variant_id in candidates:
if data and data.get('childs'):
doc = self._get_pdf_line(bom_id, product_id=product_variant_id, qty=float(data.get('quantity')), child_bom_ids=json.loads(data.get('childs')))
@@ -184,6 +184,12 @@ msgstr ""
msgid "Stripe: no order found for reference %s"
msgstr ""
#. module: payment_stripe
#: code:addons/payment_stripe/models/payment.py:260
#, python-format
msgid "Stripe: no payment token was provided or the token creation failed."
msgstr ""
#. module: payment_stripe
#: code:addons/payment_stripe/models/payment.py:170
#, python-format
+1 -1
View File
@@ -257,7 +257,7 @@ class PaymentTokenStripe(models.Model):
description = 'Partner: %s (id: %s)' % (partner_id.name, partner_id.id)
if not token:
raise Exception('stripe_create: No token provided!')
raise UserError(_("Stripe: no payment token was provided or the token creation failed."))
res = self._stripe_create_customer(token, description, payment_acquirer.id)
@@ -105,9 +105,17 @@ odoo.define('payment_stripe.stripe', function(require) {
var invoice_num = get_input_value("invoice_num");
var merchant = get_input_value("merchant");
// Search if the user wants to save the credit card information
var form_save_token = false;
var acquirer_form = $('#o_payment_form_acq_' + acquirer_id);
if (acquirer_form.length) {
form_save_token = acquirer_form.find('input[name="o_payment_form_save_token"]').prop('checked');
}
ajax.jsonRpc(payment_tx_url, 'call', {
acquirer_id: acquirer_id,
access_token: access_token,
save_token: form_save_token,
}).then(function(data) {
var $pay_stripe = $('#pay_stripe').detach();
try { provider_form[0].innerHTML = data; } catch (e) {}
@@ -42,10 +42,16 @@ var DiscountButton = screens.ActionButtonWidget.extend({
}
// Add discount
// We add the price as manually set to avoid recomputation when changing customer.
var discount = - pc / 100.0 * order.get_total_with_tax();
if( discount < 0 ){
order.add_product(product, { price: discount });
order.add_product(product, {
price: discount,
extras: {
price_manually_set: true,
},
});
}
},
});
+10
View File
@@ -552,6 +552,16 @@ class ProductTemplate(models.Model):
if variants_to_create:
Product.create(variants_to_create)
# Avoid access errors in case the products is shared amongst companies but the underlying
# objects are not. If unlink fails because of an AccessError (e.g. while recomputing
# fields), the 'write' call will fail as well for the same reason since the field has
# been set to recompute.
if variants_to_unlink:
variants_to_unlink.check_access_rights('unlink')
variants_to_unlink.check_access_rule('unlink')
variants_to_unlink.check_access_rights('write')
variants_to_unlink.check_access_rule('write')
variants_to_unlink = variants_to_unlink.sudo()
# unlink or inactive product
# try in batch first because it is much faster
try:
+2 -2
View File
@@ -1441,7 +1441,7 @@ class SaleOrderLine(models.Model):
if currency != self.order_id.pricelist_id.currency_id:
base_price = currency._convert(
base_price, self.order_id.pricelist_id.currency_id,
self.order_id.company_id, self.order_id.date_order or fields.Date.today())
self.order_id.company_id or self.env.user.company_id, self.order_id.date_order or fields.Date.today())
# negative discounts (= surcharge) are included in the display price
return max(base_price, final_price)
@@ -1624,7 +1624,7 @@ class SaleOrderLine(models.Model):
# we need new_list_price in the same currency as price, which is in the SO's pricelist's currency
new_list_price = currency._convert(
new_list_price, self.order_id.pricelist_id.currency_id,
self.order_id.company_id, self.order_id.date_order or fields.Date.today())
self.order_id.company_id or self.env.user.company_id, self.order_id.date_order or fields.Date.today())
discount = (new_list_price - price) / new_list_price * 100
if (discount > 0 and new_list_price > 0) or (discount < 0 and new_list_price < 0):
self.discount = discount
+1 -1
View File
@@ -57,7 +57,7 @@ class CrmTeam(models.Model):
def _compute_sales_to_invoice(self):
sale_order_data = self.env['sale.order'].read_group([
('team_id', 'in', self.ids),
('order_line.qty_to_invoice', '>', 0),
('invoice_status','=','to invoice'),
], ['team_id'], ['team_id'])
data_map = {datum['team_id'][0]: datum['team_id_count'] for datum in sale_order_data }
for team in self:
+1 -1
View File
@@ -1186,7 +1186,7 @@
<field name="view_type">form</field>
<field name="view_mode">tree,form,calendar,graph,kanban,pivot</field>
<field name="search_view_id" ref="sale.sale_order_view_search_inherit_sale"/>
<field name="domain">[('state', '=', 'sale'),('invoice_status','=','to invoice')]</field>
<field name="domain">[('invoice_status','=','to invoice')]</field>
<field name="context">{
'search_default_team_id': [active_id],
'default_team_id': active_id,
+2 -1
View File
@@ -18,7 +18,8 @@ class SaleOrderLine(models.Model):
if product_uom_id != product_id.uom_id:
purchase_price = product_id.uom_id._compute_price(purchase_price, product_uom_id)
price = frm_cur._convert(
purchase_price, to_cur, order_id.company_id, order_id.date_order or fields.Date.today(), round=False)
purchase_price, to_cur, order_id.company_id or self.env.user.company_id,
order_id.date_order or fields.Date.today(), round=False)
return price
@api.model
@@ -324,7 +324,7 @@ class Project(models.Model):
def _table_get_empty_so_lines(self):
""" get the Sale Order Lines having no timesheet but having generated a task or a project """
so_lines = self.sudo().mapped('sale_line_id.order_id.order_line').filtered(lambda sol: sol.is_service and not sol.is_expense)
so_lines = self.sudo().mapped('sale_line_id.order_id.order_line').filtered(lambda sol: sol.is_service and not sol.is_expense and not sol.is_downpayment)
return set(so_lines.ids), set(so_lines.mapped('order_id').ids)
# --------------------------------------------------
+4 -2
View File
@@ -396,10 +396,12 @@ class StockMove(models.Model):
receipt_moves_to_reassign = self.env['stock.move']
if 'product_uom_qty' in vals:
for move in self.filtered(lambda m: m.state not in ('done', 'draft') and m.picking_id):
if vals['product_uom_qty'] != move.product_uom_qty:
if float_compare(vals['product_uom_qty'], move.product_uom_qty, precision_rounding=move.product_uom.rounding):
self.env['stock.move.line']._log_message(move.picking_id, move, 'stock.track_move_template', vals)
if self.env.context.get('do_not_unreserve') is None:
move_to_unreserve = self.filtered(lambda m: m.state not in ['draft', 'done', 'cancel'] and m.reserved_availability > vals.get('product_uom_qty'))
move_to_unreserve = self.filtered(
lambda m: m.state not in ['draft', 'done', 'cancel'] and float_compare(m.reserved_availability, vals.get('product_uom_qty'), precision_rounding=m.product_uom.rounding) == 1
)
move_to_unreserve._do_unreserve()
(self - move_to_unreserve).filtered(lambda m: m.state == 'assigned').write({'state': 'partially_available'})
# When editing the initial demand, directly run again action assign on receipt moves.
+4 -1
View File
@@ -278,7 +278,10 @@ class StockMoveLine(models.Model):
# When editing a done move line, the reserved availability of a potential chained move is impacted. Take care of running again `_action_assign` on the concerned moves.
next_moves = self.env['stock.move']
if updates or 'qty_done' in vals:
for ml in self.filtered(lambda ml: ml.move_id.state == 'done' and ml.product_id.type == 'product'):
mls = self.filtered(lambda ml: ml.move_id.state == 'done' and ml.product_id.type == 'product')
if not updates: # we can skip those where qty_done is already good up to UoM rounding
mls = mls.filtered(lambda ml: not float_is_zero(ml.qty_done - vals['qty_done'], precision_rounding=ml.product_uom_id.rounding))
for ml in mls:
# undo the original move line
qty_done_orig = ml.move_id.product_uom._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP')
in_date = Quant._update_available_quantity(ml.product_id, ml.location_dest_id, -qty_done_orig, lot_id=ml.lot_id,
+4 -1
View File
@@ -89,7 +89,10 @@ class StockMoveLine(models.Model):
if 'qty_done' in vals:
moves_to_update = {}
for move_line in self.filtered(lambda ml: ml.state == 'done' and (ml.move_id._is_in() or ml.move_id._is_out())):
moves_to_update[move_line.move_id] = vals['qty_done'] - move_line.qty_done
rounding = move_line.product_uom_id.rounding
qty_difference = float_round(vals['qty_done'] - move_line.qty_done, precision_rounding=rounding)
if not float_is_zero(qty_difference, precision_rounding=rounding):
moves_to_update[move_line.move_id] = qty_difference
for move_id, qty_difference in moves_to_update.items():
move_vals = {}
@@ -1655,7 +1655,7 @@ var FieldBinaryFile = AbstractFieldBinary.extend({
'id': this.res_id,
'field': this.name,
'filename_field': filename_fieldname,
'filename': this.recordData[filename_fieldname] || null,
'filename': this.recordData[filename_fieldname] || "",
'download': true,
'data': utils.is_bin_size(this.value) ? null : this.value,
},
@@ -271,7 +271,6 @@ return AbstractModel.extend({
this.data.highlight_date = this.data.target_date = start.clone();
// set dates in UTC with timezone applied manually
this.data.start_date = this.data.end_date = start;
this.data.start_date.utc().add(this.getSession().getTZOffset(this.data.start_date), 'minutes');
switch (this.data.scale) {
case 'month':
@@ -286,6 +285,9 @@ return AbstractModel.extend({
this.data.start_date = this.data.start_date.clone().startOf('day');
this.data.end_date = this.data.end_date.clone().endOf('day');
}
this.data.start_date.utc();
this.data.end_date.utc();
},
/**
* @param {string} scale the scale to set
@@ -950,8 +950,8 @@ var PivotModel = AbstractModel.extend({
}
if (measureComparisonValue instanceof Array) {
dataPoints[groupIdentifier][measureName + 'Aggregate'].comparisonData = 1;
dataPoints[groupIdentifier][measureName].variation = computeVariation(
dataPoints[groupIdentifier][measureName].data,
dataPoints[groupIdentifier][measureName + 'Aggregate'].variation = computeVariation(
dataPoints[groupIdentifier][measureName + 'Aggregate'].data,
1
);
+1 -1
View File
@@ -1654,7 +1654,7 @@ QUnit.module('Views', {
results = [
"2", "0", "100%", "0", "1", "-100%", "2", "1", "100%" ,
"1", "0", "100%", "1", "0" , "100%" ,
"1" , "0", "100%", "0", "1", "-100%", "1" , "1" , "100%"
"1" , "0", "100%", "0", "1", "-100%", "1" , "1" , "0%"
];
checkCellValues(results);
+1
View File
@@ -146,6 +146,7 @@ var Tip = Widget.extend({
},
_reposition: function () {
if (this.tip_opened) return;
if (!this.$el) return;
this.$el.removeClass("o_animated");
// Reverse left/right position if direction is right to left
@@ -114,7 +114,7 @@ sAnimations.registry.websiteForum = sAnimations.Class.extend({
_.each($('textarea.load_editor'), function (textarea) {
var $textarea = $(textarea);
var editorKarma = $textarea.data('karma') || 30; // default value for backward compatibility
var editorKarma = $textarea.data('karma') || 0; // default value for backward compatibility
if (!$textarea.val().match(/\S/)) {
$textarea.val('<p><br/></p>');
}
@@ -14,7 +14,7 @@ class ProductTemplate(models.Model):
('custom', 'Show product-specific notifications'),
], string='Inventory Availability', help='Adds an inventory availability status on the web product page.', default='never')
available_threshold = fields.Float(string='Availability Threshold', default=5.0)
custom_message = fields.Text(string='Custom Message', default='')
custom_message = fields.Text(string='Custom Message', default='', translate=True)
@api.multi
def _get_combination_info(self, combination=False, product_id=False, add_qty=1, pricelist=False, reference_product=False, only_template=False):
+6 -1
View File
@@ -329,12 +329,17 @@ class IrHttp(models.AbstractModel):
if not filename:
if filename_field in record:
filename = record[filename_field]
else:
if not filename:
filename = "%s-%s-%s" % (record._name, record.id, field)
if not mimetype:
mimetype = guess_mimetype(base64.b64decode(content), default=default_mimetype)
# extension
extension = mimetypes.guess_extension(mimetype)
if extension and not filename.endswith(extension):
filename = "%s%s" % (filename, extension)
if not filehash:
filehash = '"%s"' % hashlib.md5(pycompat.to_text(content).encode('utf-8')).hexdigest()
+1 -1
View File
@@ -855,7 +855,7 @@ class IrModelFields(models.Model):
keys = [key for key in new_vals if old_vals[key] != new_vals[key]]
self.pool.post_init(record.modified, keys)
old_vals.update(new_vals)
if module and (module == model._original_module or module in field._modules):
if module and not field.manual and (module == model._original_module or module in field._modules):
to_xmlids.append(name)
if to_insert:
+15 -3
View File
@@ -36,6 +36,8 @@ class MigrationManager(object):
Python file names must start by `pre-` or `post-` and will be executed, respectively,
before and after the module initialisation. `end-` scripts are run after all modules have
been updated.
A special folder named `0.0.0` can contain scripts that will be run on any version change.
In `pre` stage, `0.0.0` scripts are run first, while in `post` and `end`, they are run last.
Example:
<moduledir>
`-- migrations
@@ -48,6 +50,8 @@ class MigrationManager(object):
|-- 9.0.1.1 # processed only on a 9.0 server
| |-- pre-delete_table_z.py
| `-- post-clean-data.py
|-- 0.0.0
| `-- end-invariants.py # processed on all version update
`-- foo.py # not processed
"""
@@ -94,13 +98,20 @@ class MigrationManager(object):
return version # the version number already containt the server version
return "%s.%s" % (release.major_version, version)
def _get_migration_versions(pkg):
def _get_migration_versions(pkg, stage):
versions = sorted({
ver
for lv in self.migrations[pkg.name].values()
for ver, lf in lv.items()
if lf
}, key=lambda k: parse_version(convert_version(k)))
if "0.0.0" in versions:
# reorder versions
versions.remove("0.0.0")
if stage == "pre":
versions.insert(0, "0.0.0")
else:
versions.append("0.0.0")
return versions
def _get_migration_files(pkg, version, stage):
@@ -127,10 +138,11 @@ class MigrationManager(object):
parsed_installed_version = parse_version(installed_version)
current_version = parse_version(convert_version(pkg.data['version']))
versions = _get_migration_versions(pkg)
versions = _get_migration_versions(pkg, stage)
for version in versions:
if parsed_installed_version < parse_version(convert_version(version)) <= current_version:
if ((version == "0.0.0" and parsed_installed_version < current_version)
or parsed_installed_version < parse_version(convert_version(version)) <= current_version):
strfmt = {'addon': pkg.name,
'stage': stage,