[MERGE] forward port branch 12.0 up to cd21c016b0
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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', '<', 100), ('type', '=', 'opportunity')]"
|
||||
help="Open Opportunities"/>
|
||||
<separator/>
|
||||
<filter string="Unread Messages" name="message_needaction" domain="[('message_needaction','=',True)]"/>
|
||||
<separator/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
# --------------------------------------------------
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user