summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2018-08-09 15:06:31 +0200
committerChristophe Simonis <chs@odoo.com>2018-08-09 15:06:31 +0200
commit9cb97a8c2daec720a2ccb8220f07b04a8786876c (patch)
tree8875292477a40d9f5d134d743c811231960ce525 /addons
parent33e7479865763e8c8255db7e057ef4703bef6093 (diff)
parent499af66727e46272efcd7f6785176097642cb096 (diff)
[MERGE] forward port branch saas-11.2 up to 499af66727e
Diffstat (limited to 'addons')
-rw-r--r--addons/account/models/account_move.py3
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_model.js1
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_renderer.js8
-rw-r--r--addons/account_asset/report/account_asset_report.py1
-rw-r--r--addons/auth_signup/models/res_partner.py13
-rw-r--r--addons/auth_signup/models/res_users.py12
-rw-r--r--addons/barcodes/static/src/js/barcode_events.js2
-rw-r--r--addons/crm/tests/test_lead2opportunity.py2
-rw-r--r--addons/event_sale/models/sale_order.py11
-rw-r--r--addons/hr_gamification/security/ir.model.access.csv2
-rw-r--r--addons/hr_holidays/report/hr_leave_report.py3
-rw-r--r--addons/mail/models/mail_channel.py2
-rw-r--r--addons/mail/models/mail_template.py6
-rw-r--r--addons/mail/views/mail_template_views.xml7
-rw-r--r--addons/pad_project/models/project.py2
-rw-r--r--addons/payment_stripe/static/src/js/stripe.js5
-rw-r--r--addons/point_of_sale/static/src/js/screens.js3
-rw-r--r--addons/point_of_sale/wizard/pos_payment.py7
-rw-r--r--addons/sale/models/product_template.py3
-rw-r--r--addons/sale/models/sale.py2
-rw-r--r--addons/sale/views/res_config_settings_views.xml2
-rw-r--r--addons/sale_stock/tests/test_sale_stock.py73
-rw-r--r--addons/sale_timesheet/models/product.py2
-rw-r--r--addons/stock/models/stock_picking.py7
-rw-r--r--addons/web/controllers/main.py5
-rw-r--r--addons/website/models/ir_attachment.py6
-rw-r--r--addons/website/models/website.py1
27 files changed, 155 insertions, 36 deletions
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index 3369007386e..b6f2fee7fe4 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -1123,8 +1123,9 @@ class AccountMoveLine(models.Model):
an analytic account. This method is intended to be extended in other modules.
"""
amount = (self.credit or 0.0) - (self.debit or 0.0)
+ default_name = self.name or (self.ref or '/' + ' -- ' + (self.partner_id and self.partner_id.name or '/'))
return {
- 'name': self.name,
+ 'name': default_name,
'date': self.date,
'account_id': self.analytic_account_id.id,
'tag_ids': [(6, 0, self._get_analytic_tag_ids())],
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js
index cf87b0a782c..27ad4066f76 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_model.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js
@@ -874,6 +874,7 @@ var StatementModel = BasicModel.extend({
}
});
total = Math.round(total*1000)/1000 || 0;
+ amount_currency = Math.round(amount_currency);
line.balance = {
amount: total,
amount_str: field_utils.format.monetary(Math.abs(total), {}, formatOptions),
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
index b9cb5f70fb7..0e5c7e2cad8 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
@@ -304,8 +304,12 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
self.fields = {
partner_id : new relational_fields.FieldMany2One(self,
'partner_id',
- self.model.get(recordID),
- {mode: 'edit'}
+ self.model.get(recordID), {
+ mode: 'edit',
+ attrs: {
+ placeholder: self._initialState.st_line.communication_partner_name || '',
+ }
+ }
)
};
self.fields.partner_id.appendTo(self.$('.accounting_view caption'));
diff --git a/addons/account_asset/report/account_asset_report.py b/addons/account_asset/report/account_asset_report.py
index 0b577215f56..eb3e08287c2 100644
--- a/addons/account_asset/report/account_asset_report.py
+++ b/addons/account_asset/report/account_asset_report.py
@@ -60,6 +60,7 @@ class AssetAssetReport(models.Model):
from account_asset_depreciation_line dl
left join account_asset_asset a on (dl.asset_id=a.id)
left join (select min(d.id) as id,ac.id as ac_id from account_asset_depreciation_line as d inner join account_asset_asset as ac ON (ac.id=d.asset_id) group by ac_id) as dlmin on dlmin.ac_id=a.id
+ where a.active is true
group by
dl.amount,dl.asset_id,dl.depreciation_date,dl.name,
a.date, dl.move_check, a.state, a.category_id, a.partner_id, a.company_id,
diff --git a/addons/auth_signup/models/res_partner.py b/addons/auth_signup/models/res_partner.py
index 17171c645f6..c0da1d85f83 100644
--- a/addons/auth_signup/models/res_partner.py
+++ b/addons/auth_signup/models/res_partner.py
@@ -25,9 +25,9 @@ def now(**kwargs):
class ResPartner(models.Model):
_inherit = 'res.partner'
- signup_token = fields.Char(copy=False)
- signup_type = fields.Char(string='Signup Token Type', copy=False)
- signup_expiration = fields.Datetime(copy=False)
+ signup_token = fields.Char(copy=False, groups="base.group_erp_manager")
+ signup_type = fields.Char(string='Signup Token Type', copy=False, groups="base.group_erp_manager")
+ signup_expiration = fields.Datetime(copy=False, groups="base.group_erp_manager")
signup_valid = fields.Boolean(compute='_compute_signup_valid', string='Signup Token is Valid')
signup_url = fields.Char(compute='_compute_signup_url', string='Signup URL')
@@ -35,15 +35,17 @@ class ResPartner(models.Model):
@api.depends('signup_token', 'signup_expiration')
def _compute_signup_valid(self):
dt = now()
- for partner in self:
+ for partner in self.sudo():
partner.signup_valid = bool(partner.signup_token) and \
(not partner.signup_expiration or dt <= partner.signup_expiration)
@api.multi
def _compute_signup_url(self):
""" proxy for function field towards actual implementation """
- result = self._get_signup_url_for_action()
+ result = self.sudo()._get_signup_url_for_action()
for partner in self:
+ if any(u.has_group('base.group_user') for u in partner.user_ids if u != self.env.user):
+ self.env['res.users'].check_access_rights('write')
partner.signup_url = result.get(partner.id, False)
@api.multi
@@ -106,6 +108,7 @@ class ResPartner(models.Model):
allow_signup = self.env['ir.config_parameter'].sudo().get_param('auth_signup.invitation_scope', 'b2b') == 'b2c'
for partner in self:
if allow_signup and not partner.user_ids:
+ partner = partner.sudo()
partner.signup_prepare()
res[partner.id]['auth_signup_token'] = partner.signup_token
elif partner.user_ids:
diff --git a/addons/auth_signup/models/res_users.py b/addons/auth_signup/models/res_users.py
index cc222498155..8792bad9489 100644
--- a/addons/auth_signup/models/res_users.py
+++ b/addons/auth_signup/models/res_users.py
@@ -137,10 +137,20 @@ class ResUsers(models.Model):
template = self.env.ref('auth_signup.reset_password_email')
assert template._name == 'mail.template'
+ template_values = {
+ 'email_to': '${object.email|safe}',
+ 'email_cc': False,
+ 'auto_delete': True,
+ 'partner_to': False,
+ 'scheduled_date': False,
+ }
+ template.write(template_values)
+
for user in self:
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)
+ with self.env.cr.savepoint():
+ 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
diff --git a/addons/barcodes/static/src/js/barcode_events.js b/addons/barcodes/static/src/js/barcode_events.js
index 2d602a36b97..0f2849525de 100644
--- a/addons/barcodes/static/src/js/barcode_events.js
+++ b/addons/barcodes/static/src/js/barcode_events.js
@@ -143,6 +143,8 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, {
e.key === "ArrowUp" || e.key === "ArrowDown" ||
e.key === "Escape" || e.key === "Tab" ||
e.key === "Backspace" || e.key === "Delete" ||
+ e.key === "Home" || e.key === "End" ||
+ e.key === "PageUp" || e.key === "PageDown" ||
e.key === "Unidentified" || /F\d\d?/.test(e.key)) {
return true;
} else {
diff --git a/addons/crm/tests/test_lead2opportunity.py b/addons/crm/tests/test_lead2opportunity.py
index 2bc62921a59..4d7279ea9de 100644
--- a/addons/crm/tests/test_lead2opportunity.py
+++ b/addons/crm/tests/test_lead2opportunity.py
@@ -38,7 +38,7 @@ class TestLead2opportunity2win(TestCrmCases):
crm_case_3.message_post(subject='Test note', body='Détails envoyés par le client sur ​​le FAX pour la qualité')
# I convert mass lead into opportunity customer.
- mass = CrmLead2OpportunityPartnerMass.with_context({'active_model': 'crm.lead', 'active_ids': [crm_case_13.id, crm_case_2.id], 'active_id': crm_case_13.id}).create({
+ mass = CrmLead2OpportunityPartnerMass.sudo(self.crm_salemanager.id).with_context({'active_model': 'crm.lead', 'active_ids': [crm_case_13.id, crm_case_2.id], 'active_id': crm_case_13.id}).create({
'user_ids': [(6, 0, self.env.ref('base.user_root').ids)],
'team_id': self.env.ref("sales_team.team_sales_department").id
})
diff --git a/addons/event_sale/models/sale_order.py b/addons/event_sale/models/sale_order.py
index 6902d8e4743..2ac4deaed5f 100644
--- a/addons/event_sale/models/sale_order.py
+++ b/addons/event_sale/models/sale_order.py
@@ -8,12 +8,13 @@ class SaleOrder(models.Model):
@api.multi
def action_confirm(self):
- self.ensure_one()
res = super(SaleOrder, self).action_confirm()
- # confirm registration if it was free (otherwise it will be confirmed once invoice fully paid)
- self.order_line._update_registrations(confirm=self.amount_total == 0, cancel_to_draft=False)
- if any(self.order_line.filtered(lambda line: line.event_id)):
- return self.env['ir.actions.act_window'].with_context(default_sale_order_id=self.id).for_xml_id('event_sale', 'action_sale_order_event_registration')
+ for order in self:
+ # confirm registration if it was free (otherwise it will be confirmed once invoice fully paid)
+ order.order_line._update_registrations(confirm=order.amount_total == 0, cancel_to_draft=False)
+ if any(order.order_line.filtered(lambda line: line.event_id)):
+ return self.env['ir.actions.act_window'].with_context(default_sale_order_id=order.id).for_xml_id(
+ 'event_sale', 'action_sale_order_event_registration')
return res
diff --git a/addons/hr_gamification/security/ir.model.access.csv b/addons/hr_gamification/security/ir.model.access.csv
index 9ace96f7139..78979d89de5 100644
--- a/addons/hr_gamification/security/ir.model.access.csv
+++ b/addons/hr_gamification/security/ir.model.access.csv
@@ -2,4 +2,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
challenge_officer,"Challenge Officer",gamification.model_gamification_challenge,hr.group_hr_user,1,1,1,1
challenge_line_officer,"Challenge Line Officer",gamification.model_gamification_challenge_line,hr.group_hr_user,1,1,1,1
badge_officer,"Badge Officer",gamification.model_gamification_badge,hr.group_hr_user,1,1,1,1
-badge_user_officer,"Badge-user Officer",gamification.model_gamification_badge_user,hr.group_hr_user,1,1,1,1
+badge_user_officer,"Badge-user Officer",gamification.model_gamification_badge_user,hr.group_hr_user,1,1,1,1
diff --git a/addons/hr_holidays/report/hr_leave_report.py b/addons/hr_holidays/report/hr_leave_report.py
index 5df7a3e4636..3e918920c52 100644
--- a/addons/hr_holidays/report/hr_leave_report.py
+++ b/addons/hr_holidays/report/hr_leave_report.py
@@ -38,6 +38,7 @@ class LeaveReport(models.Model):
def init(self):
tools.drop_view_if_exists(self._cr, 'hr_leave_report')
+
self._cr.execute("""
CREATE or REPLACE view hr_leave_report as (
SELECT row_number() over(ORDER BY leaves.employee_id) as id,
@@ -61,7 +62,7 @@ class LeaveReport(models.Model):
FALSE as payslip_status,
'allocation' as type
from hr_leave_allocation as allocation
- union select
+ union all select
request.employee_id as employee_id,
request.name as name,
request.number_of_days as number_of_days,
diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py
index f93449fbe21..3584222035e 100644
--- a/addons/mail/models/mail_channel.py
+++ b/addons/mail/models/mail_channel.py
@@ -469,7 +469,7 @@ class Channel(models.Model):
'is_minimized': minimized
}
domain = [('partner_id', '=', self.env.user.partner_id.id), ('channel_id.uuid', '=', uuid)]
- channel_partners = self.env['mail.channel.partner'].search(domain)
+ channel_partners = self.env['mail.channel.partner'].search(domain, limit=1)
channel_partners.write(values)
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_partners.channel_id.channel_info()[0])
diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py
index 55c0d1beba5..36f7fa3bbb9 100644
--- a/addons/mail/models/mail_template.py
+++ b/addons/mail/models/mail_template.py
@@ -253,17 +253,17 @@ class MailTemplate(models.Model):
def unlink_action(self):
for template in self:
if template.ref_ir_act_window:
- template.ref_ir_act_window.sudo().unlink()
+ template.ref_ir_act_window.unlink()
return True
@api.multi
def create_action(self):
- ActWindowSudo = self.env['ir.actions.act_window'].sudo()
+ ActWindow = self.env['ir.actions.act_window']
view = self.env.ref('mail.email_compose_message_wizard_form')
for template in self:
button_name = _('Send Mail (%s)') % template.name
- action = ActWindowSudo.create({
+ action = ActWindow.create({
'name': button_name,
'type': 'ir.actions.act_window',
'res_model': 'mail.compose.message',
diff --git a/addons/mail/views/mail_template_views.xml b/addons/mail/views/mail_template_views.xml
index 80f1058a4f2..81def4a35ae 100644
--- a/addons/mail/views/mail_template_views.xml
+++ b/addons/mail/views/mail_template_views.xml
@@ -9,7 +9,9 @@
<sheet>
<div class="oe_button_box" name="button_box">
<field name="ref_ir_act_window" invisible="1"/>
- <button class="oe_stat_button" name="create_action" type="object"
+ <button class="oe_stat_button"
+ groups="base.group_system"
+ name="create_action" type="object"
attrs="{'invisible':[('ref_ir_act_window','!=',False)]}" icon="fa-plus"
help="Display an option on related documents to open a composition wizard with this template">
<div class="o_field_widget o_stat_info">
@@ -17,7 +19,8 @@
<span class="o_stat_text">Context Action</span>
</div>
</button>
- <button name="unlink_action" type="object"
+ <button name="unlink_action" type="object"
+ groups="base.group_system"
class="oe_stat_button" icon="fa-minus"
attrs="{'invisible':[('ref_ir_act_window','=',False)]}"
help="Remove the contextual action to use this template on related documents" widget="statinfo">
diff --git a/addons/pad_project/models/project.py b/addons/pad_project/models/project.py
index f994b3a5ea7..6d2273b15dd 100644
--- a/addons/pad_project/models/project.py
+++ b/addons/pad_project/models/project.py
@@ -9,7 +9,7 @@ class ProjectTask(models.Model):
_inherit = ["project.task", 'pad.common']
description_pad = fields.Char('Pad URL', pad_content_field='description', copy=False)
- use_pad = fields.Boolean(related="project_id.use_pads", string="Use collaborative pad")
+ use_pad = fields.Boolean(related="project_id.use_pads", string="Use collaborative pad", readonly=True)
@api.model
def create(self, vals):
diff --git a/addons/payment_stripe/static/src/js/stripe.js b/addons/payment_stripe/static/src/js/stripe.js
index 529e0dc43d2..262068fd12d 100644
--- a/addons/payment_stripe/static/src/js/stripe.js
+++ b/addons/payment_stripe/static/src/js/stripe.js
@@ -105,7 +105,10 @@ odoo.define('payment_stripe.stripe', function(require) {
acquirer_id: acquirer_id,
access_token: access_token,
}).then(function(data) {
- try { provider_form[0].innerHTML = data; } catch (e) {};
+ var $pay_stripe = $('#pay_stripe').detach();
+ try { provider_form[0].innerHTML = data; } catch (e) {}
+ // Restore 'Pay Now' button HTML since data might have changed it.
+ $(provider_form[0]).find('#pay_stripe').replaceWith($pay_stripe);
}).done(function () {
getStripeHandler().open({
name: merchant,
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index 172bd53a152..95a7555e5d2 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -1372,6 +1372,9 @@ var ClientListScreenWidget = ScreenWidget.extend({
reload_partners: function(){
var self = this;
return this.pos.load_new_partners().then(function(){
+ // partners may have changed in the backend
+ self.partner_cache = new DomCache();
+
self.render_list(self.pos.db.get_partners_sorted(1000));
// update the currently assigned client if it has been changed in db.
diff --git a/addons/point_of_sale/wizard/pos_payment.py b/addons/point_of_sale/wizard/pos_payment.py
index e4b494badde..a2578ffef9c 100644
--- a/addons/point_of_sale/wizard/pos_payment.py
+++ b/addons/point_of_sale/wizard/pos_payment.py
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
+from odoo.tools import float_is_zero
class PosMakePayment(models.TransientModel):
@@ -30,7 +31,7 @@ class PosMakePayment(models.TransientModel):
session_id = fields.Many2one('pos.session', required=True, default=_default_session)
journal_id = fields.Many2one('account.journal', string='Payment Mode', required=True, default=_default_journal)
- amount = fields.Float(digits=(16, 2), required=True, default=_default_amount)
+ amount = fields.Float(digits=0, required=True, default=_default_amount)
payment_name = fields.Char(string='Payment Reference')
payment_date = fields.Date(string='Payment Date', required=True, default=lambda *a: fields.Datetime.now())
@@ -49,11 +50,13 @@ class PosMakePayment(models.TransientModel):
"""
self.ensure_one()
order = self.env['pos.order'].browse(self.env.context.get('active_id', False))
+ currency = order.pricelist_id.currency_id
amount = order.amount_total - order.amount_paid
data = self.read()[0]
# add_payment expect a journal key
data['journal'] = data['journal_id'][0]
- if amount != 0.0:
+ data['amount'] = currency.round(data['amount']) if currency else data['amount']
+ if not float_is_zero(amount, precision_rounding=currency.rounding or 0.01):
order.add_payment(data)
if order.test_paid():
order.action_pos_order_paid()
diff --git a/addons/sale/models/product_template.py b/addons/sale/models/product_template.py
index 79dffdd719b..6a3ceb62cd9 100644
--- a/addons/sale/models/product_template.py
+++ b/addons/sale/models/product_template.py
@@ -57,5 +57,6 @@ class ProductTemplate(models.Model):
def _onchange_type(self):
""" Force values to stay consistent with integrity constraints """
if self.type == 'consu':
- self.invoice_policy = 'order'
+ if not self.invoice_policy:
+ self.invoice_policy = 'order'
self.service_type = 'manual'
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 7708f0e217f..bc9fcd8bd76 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -1247,13 +1247,13 @@ class SaleOrderLine(models.Model):
@api.onchange('product_id', 'price_unit', 'product_uom', 'product_uom_qty', 'tax_id')
def _onchange_discount(self):
- self.discount = 0.0
if not (self.product_id and self.product_uom and
self.order_id.partner_id and self.order_id.pricelist_id and
self.order_id.pricelist_id.discount_policy == 'without_discount' and
self.env.user.has_group('sale.group_discount_per_so_line')):
return
+ self.discount = 0.0
product = self.product_id.with_context(
lang=self.order_id.partner_id.lang,
partner=self.order_id.partner_id.id,
diff --git a/addons/sale/views/res_config_settings_views.xml b/addons/sale/views/res_config_settings_views.xml
index a973243ed39..534064d84c2 100644
--- a/addons/sale/views/res_config_settings_views.xml
+++ b/addons/sale/views/res_config_settings_views.xml
@@ -347,7 +347,7 @@
</div>
<h2>Invoicing</h2>
<div class="row mt16 o_settings_container">
- <div class="col-xs-12 col-md-6 o_setting_box hidden" title="This default value is applied to any new product created. This can be changed in the product detail form.">
+ <div class="col-xs-12 col-md-6 o_setting_box" title="This default value is applied to any new product created. This can be changed in the product detail form.">
<div class="o_setting_right_pane">
<label for="default_invoice_policy"/>
<div class="text-muted">
diff --git a/addons/sale_stock/tests/test_sale_stock.py b/addons/sale_stock/tests/test_sale_stock.py
index 23cff0c7f8e..69ff5a37323 100644
--- a/addons/sale_stock/tests/test_sale_stock.py
+++ b/addons/sale_stock/tests/test_sale_stock.py
@@ -423,3 +423,76 @@ class TestSaleStock(TestSale):
# check the delivered quantity
self.assertEqual(so1.order_line.qty_delivered, 3.0)
+ def test_07_forced_qties(self):
+ """ Make multiple sale order lines of the same product which isn't available in stock. On
+ the picking, create new move lines (through the detailed operations view). See that the move
+ lines are correctly dispatched through the moves.
+ """
+ uom_unit = self.env.ref('product.product_uom_unit')
+ uom_dozen = self.env.ref('product.product_uom_dozen')
+ item1 = self.products['prod_order']
+
+ self.assertEqual(item1.uom_id.id, uom_unit.id)
+
+ # sell a dozen
+ so1 = self.env['sale.order'].create({
+ 'partner_id': self.partner.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': item1.name,
+ 'product_id': item1.id,
+ 'product_uom_qty': 1,
+ 'product_uom': uom_dozen.id,
+ 'price_unit': item1.list_price,
+ }),
+ (0, 0, {
+ 'name': item1.name,
+ 'product_id': item1.id,
+ 'product_uom_qty': 1,
+ 'product_uom': uom_dozen.id,
+ 'price_unit': item1.list_price,
+ }),
+ (0, 0, {
+ 'name': item1.name,
+ 'product_id': item1.id,
+ 'product_uom_qty': 1,
+ 'product_uom': uom_dozen.id,
+ 'price_unit': item1.list_price,
+ }),
+ ],
+ })
+ so1.action_confirm()
+
+ self.assertEqual(len(so1.picking_ids.move_lines), 3)
+ so1.picking_ids.write({
+ 'move_line_ids': [
+ (0, 0, {
+ 'product_id': item1.id,
+ 'product_uom_qty': 0,
+ 'qty_done': 1,
+ 'product_uom_id': uom_dozen.id,
+ 'location_id': so1.picking_ids.location_id.id,
+ 'location_dest_id': so1.picking_ids.location_dest_id.id,
+ }),
+ (0, 0, {
+ 'product_id': item1.id,
+ 'product_uom_qty': 0,
+ 'qty_done': 1,
+ 'product_uom_id': uom_dozen.id,
+ 'location_id': so1.picking_ids.location_id.id,
+ 'location_dest_id': so1.picking_ids.location_dest_id.id,
+ }),
+ (0, 0, {
+ 'product_id': item1.id,
+ 'product_uom_qty': 0,
+ 'qty_done': 1,
+ 'product_uom_id': uom_dozen.id,
+ 'location_id': so1.picking_ids.location_id.id,
+ 'location_dest_id': so1.picking_ids.location_dest_id.id,
+ }),
+ ],
+ })
+ so1.picking_ids.button_validate()
+ self.assertEqual(so1.picking_ids.state, 'done')
+ self.assertEqual(so1.order_line.mapped('qty_delivered'), [1, 1, 1])
+
diff --git a/addons/sale_timesheet/models/product.py b/addons/sale_timesheet/models/product.py
index ceb8273549b..202491b2e3f 100644
--- a/addons/sale_timesheet/models/product.py
+++ b/addons/sale_timesheet/models/product.py
@@ -83,3 +83,5 @@ class ProductTemplate(models.Model):
if self.type == 'service':
self.invoice_policy = 'order'
self.service_type = 'timesheet'
+ elif self.type == 'consu' and self.service_policy == 'ordered_timesheet':
+ self.invoice_policy = 'order'
diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py
index 660c68f27d4..03516c551bf 100644
--- a/addons/stock/models/stock_picking.py
+++ b/addons/stock/models/stock_picking.py
@@ -633,8 +633,9 @@ class Picking(models.Model):
# # Link existing moves or add moves when no one is related
for ops in pick.move_line_ids.filtered(lambda x: not x.move_id):
# Search move with this product
- moves = pick.move_lines.filtered(lambda x: x.product_id == ops.product_id)
- if moves: #could search move that needs it the most (that has some quantities left)
+ moves = pick.move_lines.filtered(lambda x: x.product_id == ops.product_id)
+ moves = sorted(moves, key=lambda m: m.quantity_done < m.product_qty, reverse=True)
+ if moves:
ops.move_id = moves[0].id
else:
new_move = self.env['stock.move'].create({
@@ -797,7 +798,7 @@ class Picking(models.Model):
quantity_done[quant.product_id.id] += quant.qty
for pack in self.mapped('move_line_ids').filtered(lambda x: x.product_id and not x.move_id):
quantity_done.setdefault(pack.product_id.id, 0)
- quantity_done[pack.product_id.id] += pack.qty_done
+ quantity_done[pack.product_id.id] += pack.product_uom_id._compute_quantity(pack.qty_done, pack.product_id.uom_id)
return any(quantity_done[x] < quantity_todo.get(x, 0) for x in quantity_done)
@api.multi
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index 41d5d229772..c4fba082be2 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -744,6 +744,8 @@ class Database(http.Controller):
@http.route('/web/database/restore', type='http', auth="none", methods=['POST'], csrf=False)
def restore(self, master_pwd, backup_file, name, copy=False):
try:
+ data_file = None
+ db.check_super(master_pwd)
with tempfile.NamedTemporaryFile(delete=False) as data_file:
backup_file.save(data_file)
db.restore_db(name, data_file.name, str2bool(copy))
@@ -752,7 +754,8 @@ class Database(http.Controller):
error = "Database restore error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
finally:
- os.unlink(data_file.name)
+ if data_file:
+ os.unlink(data_file.name)
@http.route('/web/database/change_password', type='http', auth="none", methods=['POST'], csrf=False)
def change_password(self, master_pwd, master_pwd_new):
diff --git a/addons/website/models/ir_attachment.py b/addons/website/models/ir_attachment.py
index a71038adbf5..a1f9df5dad1 100644
--- a/addons/website/models/ir_attachment.py
+++ b/addons/website/models/ir_attachment.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import fields, models
+from odoo import api, fields, models
class Attachment(models.Model):
@@ -10,3 +10,7 @@ class Attachment(models.Model):
# related for backward compatibility with saas-6
website_url = fields.Char(string="Website URL", related='local_url', deprecated=True)
+
+ @api.model
+ def get_serving_groups(self):
+ return super(Attachment, self).get_serving_groups() + ['website.group_website_designer']
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index a733e1aad14..c32dd8fc239 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -791,7 +791,6 @@ class Page(models.Model):
@api.multi
def write(self, vals):
- self.ensure_one()
if 'url' in vals and not vals['url'].startswith('/'):
vals['url'] = '/' + vals['url']
result = super(Page, self).write(vals)