diff --git a/addons/account/controllers/mail.py b/addons/account/controllers/mail.py
index f30c69e21ba..564856b909c 100644
--- a/addons/account/controllers/mail.py
+++ b/addons/account/controllers/mail.py
@@ -11,6 +11,7 @@ from odoo.tools.misc import consteq
class MailController(MailController):
+ @classmethod
def _redirect_to_record(cls, model, res_id, access_token=None):
# If the current user doesn't have access to the invoice, but provided
# a valid access token, redirect him to the front-end view.
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index c477ca91d77..30934695d07 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -318,7 +318,7 @@ class AccountMoveLine(models.Model):
if not cr.fetchone():
cr.execute('CREATE INDEX account_move_line_partner_id_ref_idx ON account_move_line (partner_id, ref)')
- @api.depends('debit', 'credit', 'amount_currency', 'currency_id', 'matched_debit_ids', 'matched_credit_ids', 'matched_debit_ids.amount', 'matched_credit_ids.amount', 'account_id.currency_id', 'move_id.state')
+ @api.depends('debit', 'credit', 'amount_currency', 'currency_id', 'matched_debit_ids', 'matched_credit_ids', 'matched_debit_ids.amount', 'matched_credit_ids.amount', 'move_id.state')
def _amount_residual(self):
""" Computes the residual amount of a move line from a reconcilable account in the company currency and the line's currency.
This amount will be 0 for fully reconciled lines or lines from a non-reconcilable account, the original line amount
diff --git a/addons/account/models/company.py b/addons/account/models/company.py
index d73dbf82c64..8ae4230a7b2 100644
--- a/addons/account/models/company.py
+++ b/addons/account/models/company.py
@@ -4,7 +4,7 @@ from datetime import timedelta, datetime
import calendar
from odoo import fields, models, api, _
-from odoo.exceptions import ValidationError
+from odoo.exceptions import ValidationError, UserError
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_round, float_is_zero
@@ -130,6 +130,12 @@ Best Regards,'''))
if values.get('cash_account_code_prefix'):
new_cash_code = values.get('cash_account_code_prefix') or company.cash_account_code_prefix
company.reflect_code_prefix_change(company.cash_account_code_prefix, new_cash_code)
+
+ #forbid the change of currency_id if there are already some accounting entries existing
+ if 'currency_id' in values and values['currency_id'] != company.currency_id.id:
+ if self.env['account.move.line'].search([('company_id', '=', company.id)]):
+ raise UserError(_('You cannot change the currency of the company since some journal items already exist'))
+
return super(ResCompany, self).write(values)
@api.model
diff --git a/addons/account/tests/test_payment.py b/addons/account/tests/test_payment.py
index b527efa6be3..271a588dbe9 100644
--- a/addons/account/tests/test_payment.py
+++ b/addons/account/tests/test_payment.py
@@ -20,7 +20,9 @@ class TestPayment(AccountingTestCase):
self.currency_chf_id = self.env.ref("base.CHF").id
self.currency_usd_id = self.env.ref("base.USD").id
self.currency_eur_id = self.env.ref("base.EUR").id
- self.env.ref('base.main_company').write({'currency_id': self.currency_eur_id})
+
+ company = self.env.ref('base.main_company')
+ self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.currency_eur_id, company.id])
self.product = self.env.ref("product.product_product_4")
self.payment_method_manual_in = self.env.ref("account.account_payment_method_manual_in")
self.payment_method_manual_out = self.env.ref("account.account_payment_method_manual_out")
diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py
index aa4fc950279..1180df172c9 100644
--- a/addons/account/tests/test_reconciliation.py
+++ b/addons/account/tests/test_reconciliation.py
@@ -27,7 +27,8 @@ class TestReconciliation(AccountingTestCase):
self.currency_swiss_id = self.env.ref("base.CHF").id
self.currency_usd_id = self.env.ref("base.USD").id
self.currency_euro_id = self.env.ref("base.EUR").id
- self.env.ref('base.main_company').write({'currency_id': self.currency_euro_id})
+ company = self.env.ref('base.main_company')
+ self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.currency_euro_id, company.id])
self.account_rcv = partner_agrolait.property_account_receivable_id or self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1)
self.account_rsa = partner_agrolait.property_account_payable_id or self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_payable').id)], limit=1)
self.product = self.env.ref("product.product_product_4")
diff --git a/addons/barcodes/static/src/js/barcode_form_view.js b/addons/barcodes/static/src/js/barcode_form_view.js
index fa5b27e2a9c..1730b42290e 100644
--- a/addons/barcodes/static/src/js/barcode_form_view.js
+++ b/addons/barcodes/static/src/js/barcode_form_view.js
@@ -155,7 +155,7 @@ FormController.include({
id: candidate.id,
data: candidateChanges,
};
- return this.model.notifyChanges(this.handle, changes);
+ return this.model.notifyChanges(this.handle, changes, {notifyChange: activeBarcode.notifyChange});
},
/**
* @private
@@ -211,6 +211,8 @@ FormController.include({
* @param {OdooEvent} event
* @param {string} event.data.name: the current field name
* @param {string} [event.data.fieldName] optional for x2many sub field
+ * @param {boolean} [event.data.notifyChange] optional for x2many sub field
+ * do not trigger on change server side if a candidate has been found
* @param {string} [event.data.quantity] optional field to increase quantity
* @param {Object} [event.data.commands] optional added methods
* can use comand with specific barcode (with ReservedBarcodePrefixes)
@@ -225,7 +227,9 @@ FormController.include({
handle: this.handle,
target: event.target,
widget: event.target.attrs && event.target.attrs.widget,
+ setQuantityWithKeypress: !! event.data.setQuantityWithKeypress,
fieldName: event.data.fieldName,
+ notifyChange: (event.data.notifyChange !== undefined) ? event.data.notifyChange : true,
quantity: event.data.quantity,
commands: event.data.commands || {},
candidate: this.activeBarcode[name] && this.activeBarcode[name].handle === this.handle ?
@@ -319,7 +323,7 @@ FormController.include({
// only catch the event if we're not focused in
// another field and it's a number
- if (!$(event.target).is('body') || !/[0-9]/.test(character)) {
+ if (!$(event.target).is('body, .modal') || !/[0-9]/.test(character)) {
return;
}
diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py
index 1679198caf3..fb233a50da6 100644
--- a/addons/crm/models/crm_lead.py
+++ b/addons/crm/models/crm_lead.py
@@ -971,6 +971,8 @@ class Lead(models.Model):
'nb_opportunities': 0,
}
+ today = fields.Date.from_string(fields.Date.context_today(self))
+
opportunities = self.search([('type', '=', 'opportunity'), ('user_id', '=', self._uid)])
for opp in opportunities:
@@ -978,28 +980,28 @@ class Lead(models.Model):
if opp.activity_date_deadline:
if opp.date_deadline:
date_deadline = fields.Date.from_string(opp.date_deadline)
- if date_deadline == date.today():
+ if date_deadline == today:
result['closing']['today'] += 1
- if date.today() <= date_deadline <= date.today() + timedelta(days=7):
+ if today <= date_deadline <= today + timedelta(days=7):
result['closing']['next_7_days'] += 1
- if date_deadline < date.today() and not opp.date_closed:
+ if date_deadline < today and not opp.date_closed:
result['closing']['overdue'] += 1
# Next activities
for activity in opp.activity_ids:
date_deadline = fields.Date.from_string(activity.date_deadline)
- if date_deadline == date.today():
+ if date_deadline == today:
result['activity']['today'] += 1
- if date.today() <= date_deadline <= date.today() + timedelta(days=7):
+ if today <= date_deadline <= today + timedelta(days=7):
result['activity']['next_7_days'] += 1
- if date_deadline < date.today():
+ if date_deadline < today:
result['activity']['overdue'] += 1
# Won in Opportunities
if opp.date_closed and opp.stage_id.probability == 100:
date_closed = fields.Date.from_string(opp.date_closed)
- if date.today().replace(day=1) <= date_closed <= date.today():
+ if today.replace(day=1) <= date_closed <= today:
if opp.planned_revenue:
result['won']['this_month'] += opp.planned_revenue
- elif date.today() + relativedelta(months=-1, day=1) <= date_closed < date.today().replace(day=1):
+ elif today + relativedelta(months=-1, day=1) <= date_closed < today.replace(day=1):
if opp.planned_revenue:
result['won']['last_month'] += opp.planned_revenue
@@ -1024,9 +1026,9 @@ class Lead(models.Model):
for activity in activites_done:
if activity['date']:
date_act = fields.Date.from_string(activity['date'])
- if date.today().replace(day=1) <= date_act <= date.today():
+ if today.replace(day=1) <= date_act <= today:
result['done']['this_month'] += 1
- elif date.today() + relativedelta(months=-1, day=1) <= date_act < date.today().replace(day=1):
+ elif today + relativedelta(months=-1, day=1) <= date_act < today.replace(day=1):
result['done']['last_month'] += 1
# Meetings
@@ -1041,9 +1043,9 @@ class Lead(models.Model):
for meeting in meetings:
if meeting['start']:
start = datetime.strptime(meeting['start'], tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
- if start == date.today():
+ if start == today:
result['meeting']['today'] += 1
- if date.today() <= start <= date.today() + timedelta(days=7):
+ if today <= start <= today + timedelta(days=7):
result['meeting']['next_7_days'] += 1
result['done']['target'] = self.env.user.target_sales_done
diff --git a/addons/delivery/models/__init__.py b/addons/delivery/models/__init__.py
index 5229017a189..9e162e76592 100644
--- a/addons/delivery/models/__init__.py
+++ b/addons/delivery/models/__init__.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from . import res_config_settings
from . import delivery_carrier
from . import delivery_grid
from . import product_template
diff --git a/addons/delivery/models/res_config_settings.py b/addons/delivery/models/res_config_settings.py
new file mode 100644
index 00000000000..070f8dcdb29
--- /dev/null
+++ b/addons/delivery/models/res_config_settings.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = 'res.config.settings'
+
+ def set_values(self):
+ super(ResConfigSettings, self).set_values()
+ rule = self.env.ref('delivery.delivery_carrier_comp_rule', False)
+ if rule:
+ rule.write({'active': not bool(self.company_share_product)})
diff --git a/addons/delivery/security/delivery_carrier_security.xml b/addons/delivery/security/delivery_carrier_security.xml
index 8fad0891676..db718fbf361 100644
--- a/addons/delivery/security/delivery_carrier_security.xml
+++ b/addons/delivery/security/delivery_carrier_security.xml
@@ -6,6 +6,7 @@
['|',('company_id','=',user.company_id.id),('company_id','=',False)]
+
diff --git a/addons/delivery/security/delivery_security.xml b/addons/delivery/security/delivery_security.xml
new file mode 100644
index 00000000000..ae588bc245c
--- /dev/null
+++ b/addons/delivery/security/delivery_security.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ Delivery multi-company
+
+
+ ['|',('product_id.company_id','=',user.company_id.id),('product_id.company_id','=',False)]
+
+
+
+
diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py
index 1cf1ee9b17f..3e0c3ea7ca1 100644
--- a/addons/hr_holidays/models/hr_leave.py
+++ b/addons/hr_holidays/models/hr_leave.py
@@ -88,7 +88,7 @@ class HolidaysRequest(models.Model):
@api.onchange('employee_id')
def _onchange_employee_id(self):
self.manager_id = self.employee_id and self.employee_id.parent_id
- self.department_id = self.employee_id and self.employee_id.department_id
+ self.department_id = self.employee_id.department_id
@api.multi
@api.depends('number_of_days_temp')
diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py
index ee1d5833fb0..ddfd68e0d56 100644
--- a/addons/hr_timesheet/models/hr_timesheet.py
+++ b/addons/hr_timesheet/models/hr_timesheet.py
@@ -14,7 +14,7 @@ class AccountAnalyticLine(models.Model):
result['employee_id'] = self.env['hr.employee'].search([('user_id', '=', result['user_id'])], limit=1).id
return result
- task_id = fields.Many2one('project.task', 'Task')
+ task_id = fields.Many2one('project.task', 'Task', index=True)
project_id = fields.Many2one('project.project', 'Project', domain=[('allow_timesheets', '=', True)])
employee_id = fields.Many2one('hr.employee', "Employee")
diff --git a/addons/im_livechat/controllers/main.py b/addons/im_livechat/controllers/main.py
index 11eabd18423..e228524893a 100644
--- a/addons/im_livechat/controllers/main.py
+++ b/addons/im_livechat/controllers/main.py
@@ -110,7 +110,8 @@ class LivechatController(http.Controller):
@http.route('/im_livechat/history', type="json", auth="public")
def history_pages(self, pid, channel_uuid, page_history=None):
- channel = request.env['mail.channel'].search([('uuid', '=', channel_uuid)])
+ partner_ids = (pid, request.env.user.partner_id.id)
+ channel = request.env['mail.channel'].sudo().search([('uuid', '=', channel_uuid), ('channel_partner_ids', 'in', partner_ids)])
if channel:
channel._send_history_message(pid, page_history)
return True
diff --git a/addons/im_livechat/models/im_livechat_channel.py b/addons/im_livechat/models/im_livechat_channel.py
index c2741fcef92..783fbd417ef 100644
--- a/addons/im_livechat/models/im_livechat_channel.py
+++ b/addons/im_livechat/models/im_livechat_channel.py
@@ -238,7 +238,7 @@ class ImLivechatChannelRule(models.Model):
"""
def _match(rules):
for rule in rules:
- if re.search(rule.regex_url, url):
+ if re.search(rule.regex_url or '', url):
return rule
return False
# first, search the country specific rules (the first match is returned)
diff --git a/addons/l10n_be_hr_payroll_fleet/views/res_config_settings_views.xml b/addons/l10n_be_hr_payroll_fleet/views/res_config_settings_views.xml
index 4c095e2eea5..f4800282ec2 100644
--- a/addons/l10n_be_hr_payroll_fleet/views/res_config_settings_views.xml
+++ b/addons/l10n_be_hr_payroll_fleet/views/res_config_settings_views.xml
@@ -15,9 +15,9 @@
-
+
An employee cannot request a new car if there are more than
-
+
existing vehicles availables.
diff --git a/addons/l10n_in/views/report_invoice.xml b/addons/l10n_in/views/report_invoice.xml
index f5cae06634c..6bde405adc7 100644
--- a/addons/l10n_in/views/report_invoice.xml
+++ b/addons/l10n_in/views/report_invoice.xml
@@ -73,21 +73,21 @@
-
+
|
-
+
|
-
+
|
@@ -95,10 +95,10 @@
-
+
-
+
|
diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py
index 2670d8b25ac..beceb943d21 100644
--- a/addons/mail/models/mail_activity.py
+++ b/addons/mail/models/mail_activity.py
@@ -347,6 +347,15 @@ class MailActivityMixin(models.AbstractModel):
def _search_activity_summary(self, operator, operand):
return [('activity_ids.summary', operator, operand)]
+ @api.multi
+ def write(self, vals):
+ # Delete activities of archived record.
+ if 'active' in vals and vals['active'] is False:
+ self.env['mail.activity'].sudo().search(
+ [('res_model', '=', self._name), ('res_id', 'in', self.ids)]
+ ).unlink()
+ return super(MailActivityMixin, self).write(vals)
+
@api.multi
def unlink(self):
""" Override unlink to delete records activities through (res_model, res_id). """
diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml
index cf80ed32995..b5f8a1c7849 100644
--- a/addons/mail/static/src/xml/thread.xml
+++ b/addons/mail/static/src/xml/thread.xml
@@ -271,7 +271,7 @@
-
+
diff --git a/addons/note_pad/models/note.py b/addons/note_pad/models/note.py
index 080e313a873..49536d79b88 100644
--- a/addons/note_pad/models/note.py
+++ b/addons/note_pad/models/note.py
@@ -11,4 +11,4 @@ class NotePad(models.Model):
_pad_fields = ['note_pad']
- note_pad_url = fields.Char('Pad Url', pad_content_field='memo')
+ note_pad_url = fields.Char('Pad Url', pad_content_field='memo', copy=False)
diff --git a/addons/pad/models/pad.py b/addons/pad/models/pad.py
index a1e31bf485a..deb920ec69e 100644
--- a/addons/pad/models/pad.py
+++ b/addons/pad/models/pad.py
@@ -101,16 +101,19 @@ class PadCommon(models.AbstractModel):
@api.multi
def write(self, vals):
- self._set_pad_value(vals)
+ self._set_field_to_pad(vals)
+ self._set_pad_to_field(vals)
return super(PadCommon, self).write(vals)
@api.model
def create(self, vals):
- self._set_pad_value(vals)
+ # Case of a regular creation: we receive the pad url, so we need to update the
+ # corresponding field
+ self._set_pad_to_field(vals)
pad = super(PadCommon, self).create(vals)
- # In case the pad is created programmatically, the content is not filled in yet since it is
- # normally initialized by the JS layer
+ # Case of a programmatical creation (e.g. copy): we receive the field content, so we need
+ # to create the corresponding pad
if self.env.context.get('pad_no_create', False):
return pad
for k, field in self._fields.items():
@@ -124,9 +127,7 @@ class PadCommon(models.AbstractModel):
pad[k] = pad_info.get('url')
return pad
- # Set the pad content in vals
- def _set_pad_value(self, vals):
-
+ def _set_field_to_pad(self, vals):
# Update the pad if the `pad_content_field` is modified
for k, field in self._fields.items():
if hasattr(field, 'pad_content_field') and vals.get(field.pad_content_field) and self[k]:
@@ -135,22 +136,9 @@ class PadCommon(models.AbstractModel):
path = self[k].split('/p/')[1]
myPad.setHtmlFallbackText(path, vals[field.pad_content_field])
+ def _set_pad_to_field(self, vals):
# Update the `pad_content_field` if the pad is modified
for k, v in list(vals.items()):
field = self._fields.get(k)
if hasattr(field, 'pad_content_field'):
vals[field.pad_content_field] = self.pad_get_content(v)
-
- @api.multi
- def copy(self, default=None):
- self.ensure_one()
- if not default:
- default = {}
- for k, field in self._fields.items():
- if hasattr(field, 'pad_content_field'):
- if self.env.context.get('pad_no_create', False):
- default[k] = ''
- else:
- pad = self.pad_generate_url()
- default[k] = pad.get('url')
- return super(PadCommon, self).copy(default)
diff --git a/addons/pad_project/models/project.py b/addons/pad_project/models/project.py
index eae27a4f6a7..f994b3a5ea7 100644
--- a/addons/pad_project/models/project.py
+++ b/addons/pad_project/models/project.py
@@ -8,7 +8,7 @@ class ProjectTask(models.Model):
_name = "project.task"
_inherit = ["project.task", 'pad.common']
- description_pad = fields.Char('Pad URL', pad_content_field='description')
+ 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")
@api.model
@@ -19,12 +19,6 @@ class ProjectTask(models.Model):
self = self.with_context(pad_no_create=True)
return super(ProjectTask, self).create(vals)
- @api.multi
- def copy(self, default=None):
- if not self.use_pad:
- self = self.with_context(pad_no_create=True)
- return super(ProjectTask, self).copy(default)
-
class ProjectProject(models.Model):
_inherit = "project.project"
diff --git a/addons/payment/controllers/portal.py b/addons/payment/controllers/portal.py
index 444d936429b..7e023e01c7a 100644
--- a/addons/payment/controllers/portal.py
+++ b/addons/payment/controllers/portal.py
@@ -73,7 +73,8 @@ class WebsitePayment(http.Controller):
return tx
- @http.route(['/website_payment/transaction/
//',], type='json', auth='public')
+ @http.route(['/website_payment/transaction///',
+ '/website_payment/transaction/v2///',], type='json', auth='public')
def transaction(self, acquirer_id, reference, amount, currency_id, **kwargs):
partner_id = request.env.user.partner_id.id if not request.env.user._is_public() else False
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
@@ -100,7 +101,8 @@ class WebsitePayment(http.Controller):
return acquirer.sudo().render(reference, float(amount), int(currency_id), values=render_values)
- @http.route(['/website_payment/token///'], type='http', auth='public', website=True)
+ @http.route(['/website_payment/token///',
+ '/website_payment/token/v2///'], type='http', auth='public', website=True)
def payment_token(self, pm_id, reference, amount, currency_id, return_url=None, **kwargs):
token = request.env['payment.token'].browse(int(pm_id))
diff --git a/addons/payment/security/ir.model.access.csv b/addons/payment/security/ir.model.access.csv
index 2982f1ab0c0..76d11991b88 100644
--- a/addons/payment/security/ir.model.access.csv
+++ b/addons/payment/security/ir.model.access.csv
@@ -1,6 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
payment_acquirer_all,payment.acquirer.all,model_payment_acquirer,,1,0,0,0
-payment_acquirer_user,payment.acquirer.user,model_payment_acquirer,base.group_user,1,1,1,0
payment_acquirer_system,payment.acquirer.system,model_payment_acquirer,base.group_system,1,1,1,1
payment_transaction_all,payment.transaction.all,model_payment_transaction,,1,0,0,0
payment_transaction_user,payment.transaction.user,model_payment_transaction,base.group_user,1,1,1,0
diff --git a/addons/payment/views/payment_portal_templates.xml b/addons/payment/views/payment_portal_templates.xml
index 166cd429718..2224eeec179 100644
--- a/addons/payment/views/payment_portal_templates.xml
+++ b/addons/payment/views/payment_portal_templates.xml
@@ -55,8 +55,8 @@
Amount:
-
-
+
+
No payment acquirer found.
diff --git a/addons/payment_stripe/static/src/js/stripe.js b/addons/payment_stripe/static/src/js/stripe.js
index 7ecf25284f6..e4474fe72ce 100644
--- a/addons/payment_stripe/static/src/js/stripe.js
+++ b/addons/payment_stripe/static/src/js/stripe.js
@@ -109,8 +109,8 @@ odoo.define('payment_stripe.stripe', function(require) {
if ($('.o_website_payment').length !== 0) {
var invoice_num = $("input[name='invoice_num']").val();
- var url = _.str.sprintf("/website_payment/transaction/%s/%f/%s",
- invoice_num, amount, currency_id);
+ var url = _.str.sprintf("/website_payment/transaction/v2/%f/%s/%s",
+ amount, currency_id, invoice_num);
var create_tx = ajax.jsonRpc(url, 'call', {
acquirer_id: acquirer_id
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index 704c9084e37..19d0b32d58f 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -160,13 +160,14 @@ class PosOrder(models.Model):
"""
Prepare the dict of values to create the new invoice for a pos order.
"""
+ invoice_type = 'out_invoice' if self.amount_total >= 0 else 'out_refund'
return {
'name': self.name,
'origin': self.name,
'account_id': self.partner_id.property_account_receivable_id.id,
'journal_id': self.session_id.config_id.invoice_journal_id.id,
'company_id': self.company_id.id,
- 'type': 'out_invoice',
+ 'type': invoice_type,
'reference': self.name,
'partner_id': self.partner_id.id,
'comment': self.note or '',
@@ -208,7 +209,7 @@ class PosOrder(models.Model):
inv_line = {
'invoice_id': invoice_id,
'product_id': line.product_id.id,
- 'quantity': line.qty,
+ 'quantity': line.qty if self.amount_total >= 0 else -line.qty,
'account_analytic_id': self._prepare_analytic_account(line),
'name': inv_name,
}
diff --git a/addons/project/models/project.py b/addons/project/models/project.py
index 9f940a8bfeb..054f5ad4b09 100644
--- a/addons/project/models/project.py
+++ b/addons/project/models/project.py
@@ -526,7 +526,7 @@ class Task(models.Model):
legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True, related_sudo=False)
legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True, related_sudo=False)
legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True, related_sudo=False)
- parent_id = fields.Many2one('project.task', string='Parent Task')
+ parent_id = fields.Many2one('project.task', string='Parent Task', index=True)
child_ids = fields.One2many('project.task', 'parent_id', string="Sub-tasks")
subtask_count = fields.Integer("Sub-task count", compute='_compute_subtask_count')
email_from = fields.Char(string='Email', help="These people will receive email.", index=True)
diff --git a/addons/purchase/models/stock.py b/addons/purchase/models/stock.py
index 4f5288486bf..fa38fa44bae 100644
--- a/addons/purchase/models/stock.py
+++ b/addons/purchase/models/stock.py
@@ -34,14 +34,14 @@ class StockMove(models.Model):
@api.model
def _prepare_merge_moves_distinct_fields(self):
distinct_fields = super(StockMove, self)._prepare_merge_moves_distinct_fields()
- distinct_fields.append('purchase_line_id')
+ distinct_fields += ['purchase_line_id', 'created_purchase_line_id']
return distinct_fields
@api.model
def _prepare_merge_move_sort_method(self, move):
move.ensure_one()
keys_sorted = super(StockMove, self)._prepare_merge_move_sort_method(move)
- keys_sorted.append(move.purchase_line_id.id)
+ keys_sorted += [move.purchase_line_id.id, move.created_purchase_line_id.id]
return keys_sorted
@api.multi
diff --git a/addons/purchase/tests/test_create_picking.py b/addons/purchase/tests/test_create_picking.py
index 9cdfe0fb4fd..a056a11a651 100644
--- a/addons/purchase/tests/test_create_picking.py
+++ b/addons/purchase/tests/test_create_picking.py
@@ -224,3 +224,92 @@ class TestCreatePicking(common.TestProductCommon):
# check the delivered quantity
self.assertEqual(po.order_line.qty_received, 3.0)
+ def test_04_mto_multiple_po(self):
+ """ Simulate a mto chain with 2 purchase order.
+ Create a move with qty 1, confirm the RFQ then create a new
+ move that will not be merged in the first one(simulate an increase
+ order quantity on a SO). It should generate a new RFQ, validate
+ and receipt the picking then try to reserve the delivery
+ picking.
+ """
+ stock_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_stock')
+ customer_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_customers')
+ picking_type_out = self.env['ir.model.data'].xmlid_to_object('stock.picking_type_out')
+ # route buy should be there by default
+ partner = self.env['res.partner'].create({
+ 'name': 'Jhon'
+ })
+
+ seller = self.env['product.supplierinfo'].create({
+ 'name': partner.id,
+ 'price': 12.0,
+ })
+
+ product = self.env['product.product'].create({
+ 'name': 'product',
+ 'type': 'product',
+ 'route_ids': [(4, self.ref('stock.route_warehouse0_mto')), (4, self.ref('purchase.route_warehouse0_buy'))],
+ 'seller_ids': [(6, 0, [seller.id])],
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+
+ # A picking is require since only moves inside the same picking are merged.
+ customer_picking = self.env['stock.picking'].create({
+ 'location_id': stock_location.id,
+ 'location_dest_id': customer_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': picking_type_out.id,
+ })
+
+ customer_move = self.env['stock.move'].create({
+ 'name': 'move out',
+ 'location_id': stock_location.id,
+ 'location_dest_id': customer_location.id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 80.0,
+ 'procure_method': 'make_to_order',
+ 'picking_id': customer_picking.id,
+ })
+
+ customer_move._action_confirm()
+
+ purchase_order = self.env['purchase.order'].search([('partner_id', '=', partner.id)])
+ self.assertTrue(purchase_order, 'No purchase order created.')
+
+ # Check purchase order line data.
+ purchase_order_line = purchase_order.order_line
+ self.assertEqual(purchase_order_line.product_id, product, 'The product on the purchase order line is not correct.')
+ self.assertEqual(purchase_order_line.price_unit, seller.price, 'The purchase order line price should be the same than the seller.')
+ self.assertEqual(purchase_order_line.product_qty, customer_move.product_uom_qty, 'The purchase order line qty should be the same than the move.')
+
+ purchase_order.button_confirm()
+
+ customer_move_2 = self.env['stock.move'].create({
+ 'name': 'move out',
+ 'location_id': stock_location.id,
+ 'location_dest_id': customer_location.id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 20.0,
+ 'procure_method': 'make_to_order',
+ 'picking_id': customer_picking.id,
+ })
+
+ customer_move_2._action_confirm()
+
+ self.assertTrue(customer_move_2.exists(), 'The second customer move should not be merged in the first.')
+ self.assertEqual(sum(customer_picking.move_lines.mapped('product_uom_qty')), 100.0)
+
+ purchase_order_2 = self.env['purchase.order'].search([('partner_id', '=', partner.id), ('state', '=', 'draft')])
+ self.assertTrue(purchase_order_2, 'No purchase order created.')
+
+ purchase_order_2.button_confirm()
+
+ purchase_order.picking_ids.move_lines.quantity_done = 80.0
+ purchase_order.picking_ids.button_validate()
+
+ purchase_order_2.picking_ids.move_lines.quantity_done = 20.0
+ purchase_order_2.picking_ids.button_validate()
+
+ self.assertEqual(sum(customer_picking.move_lines.mapped('reserved_availability')), 100.0, 'The total quantity for the customer move should be available and reserved.')
diff --git a/addons/sale/__init__.py b/addons/sale/__init__.py
index 0b45acc7630..17753815848 100644
--- a/addons/sale/__init__.py
+++ b/addons/sale/__init__.py
@@ -16,7 +16,8 @@ def uninstall_hook(cr, registry):
db_registry = odoo.modules.registry.Registry.new(dbname)
with api.Environment.manage(), db_registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
- env['account.invoice.report'].init()
+ if 'account.invoice.report' in env:
+ env['account.invoice.report'].init()
if 'crm.team' in env:
recs = env['crm.team'].search([])
for rec in recs:
diff --git a/addons/sale/controllers/mail.py b/addons/sale/controllers/mail.py
index cd0701e21db..d257bfa2736 100644
--- a/addons/sale/controllers/mail.py
+++ b/addons/sale/controllers/mail.py
@@ -11,6 +11,7 @@ from odoo.tools.misc import consteq
class MailController(MailController):
+ @classmethod
def _redirect_to_record(cls, model, res_id, access_token=None):
# If the current user doesn't have access to the sales order, but provided
# a valid access token, redirect him to the front-end view.
diff --git a/addons/sale_crm/models/crm_lead.py b/addons/sale_crm/models/crm_lead.py
index 7409fdccaa5..b4eeb43f565 100644
--- a/addons/sale_crm/models/crm_lead.py
+++ b/addons/sale_crm/models/crm_lead.py
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from datetime import date
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models
@@ -31,7 +30,7 @@ class CrmLead(models.Model):
@api.model
def retrieve_sales_dashboard(self):
res = super(CrmLead, self).retrieve_sales_dashboard()
- date_today = date.today()
+ date_today = fields.Date.from_string(fields.Date.context_today(self))
res['invoiced'] = {
'this_month': 0,
diff --git a/addons/sale_stock/models/account_invoice.py b/addons/sale_stock/models/account_invoice.py
index 4b9212beb8f..3b4c60cc413 100644
--- a/addons/sale_stock/models/account_invoice.py
+++ b/addons/sale_stock/models/account_invoice.py
@@ -51,7 +51,9 @@ class AccountInvoiceLine(models.Model):
qty_to_consider = invoiced_qty - qty_done
qty_to_consider = min(qty_to_consider, quantity - qty_delivered)
qty_delivered += qty_to_consider
- average_price_unit = (average_price_unit * (qty_delivered - qty_to_consider) + (-1 * move.price_unit) * qty_to_consider) / qty_delivered
+ # `move.price_unit` is negative if the move is out and positive if the move is
+ # dropshipped. Use its absolute value to compute the average price unit.
+ average_price_unit = (average_price_unit * (qty_delivered - qty_to_consider) + abs(move.price_unit) * qty_to_consider) / qty_delivered
if qty_delivered == quantity:
break
return average_price_unit
diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py
index 335a6b657fe..83a0c1b82d2 100644
--- a/addons/sale_timesheet/models/sale_order.py
+++ b/addons/sale_timesheet/models/sale_order.py
@@ -137,7 +137,7 @@ class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
qty_delivered_method = fields.Selection(selection_add=[('timesheet', 'Timesheets')])
- task_id = fields.Many2one('project.task', 'Task', help="Task generated by the sales order item")
+ task_id = fields.Many2one('project.task', 'Task', index=True, help="Task generated by the sales order item")
is_service = fields.Boolean("Is a Service", compute='_compute_is_service', store=True, help="Sales Order item should generate a task and/or a project, depending on the product settings.")
analytic_line_ids = fields.One2many(domain=[('project_id', '=', False)]) # only analytic lines, not timesheets (since this field determine if SO line came from expense)
diff --git a/addons/stock/data/stock_demo2.xml b/addons/stock/data/stock_demo2.xml
index 8b86d919a8e..155d18507c0 100644
--- a/addons/stock/data/stock_demo2.xml
+++ b/addons/stock/data/stock_demo2.xml
@@ -419,13 +419,6 @@
obj().env.ref('stock.incomming_chicago_warehouse3').id]"/>
-
-
-
-
-
+
+
@@ -270,7 +271,7 @@
-
+
@@ -280,7 +281,7 @@
-
+
@@ -295,8 +296,9 @@
+
-
+
diff --git a/addons/stock_account/__init__.py b/addons/stock_account/__init__.py
index 17a52b35e20..e6ecd5c5e46 100644
--- a/addons/stock_account/__init__.py
+++ b/addons/stock_account/__init__.py
@@ -66,13 +66,14 @@ def _configure_journals(cr, registry):
'company_id': company_id.id,
'fields_id': field_id,
'value': value,
- 'res_id': 'product.category,'+str(env.ref('product.product_category_all').id),
}
properties = env['ir.property'].search([
('name', '=', record),
('company_id', '=', company_id.id),
- ('value_reference', '!=', False)])
- if not properties:
+ ])
+ if properties:
+ properties.write(vals)
+ else:
# create the property
env['ir.property'].create(vals)
diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py
index b1e5f2b931d..a680bd4d8c1 100644
--- a/addons/stock_account/models/stock.py
+++ b/addons/stock_account/models/stock.py
@@ -200,6 +200,14 @@ class StockMove(models.Model):
return True
return False
+ def _is_dropshipped(self):
+ """ Check if the move should be considered as a dropshipping move so that the cost method
+ will be able to apply the correct logic.
+
+ :return: True if the move is a dropshipping one else False
+ """
+ return self.location_id.usage == 'supplier' and self.location_dest_id.usage == 'customer'
+
@api.model
def _run_fifo(self, move, quantity=None):
move.ensure_one()
@@ -298,6 +306,21 @@ class StockMove(models.Model):
'value': value if quantity is None else self.value + value,
'price_unit': value / valued_quantity,
})
+ elif self._is_dropshipped():
+ curr_rounding = self.company_id.currency_id.rounding
+ if self.product_id.cost_method in ['fifo']:
+ price_unit = self._get_price_unit()
+ # see test_dropship_fifo_perpetual_anglosaxon_ordered
+ self.product_id.standard_price = price_unit
+ else:
+ price_unit = self.product_id.standard_price
+ value = float_round(self.product_qty * price_unit, precision_rounding=curr_rounding)
+ # In move have a positive value, out move have a negative value, let's arbitrary say
+ # dropship are positive.
+ self.write({
+ 'value': value,
+ 'price_unit': price_unit,
+ })
def _action_done(self):
self.product_price_update_before_done()
@@ -319,7 +342,7 @@ class StockMove(models.Model):
if company_src and company_dst and company_src.id != company_dst.id:
raise UserError(_("The move lines are not in a consistent states: they are doing an intercompany in a single step while they should go through the intercompany transit location."))
move._run_valuation()
- for move in res.filtered(lambda m: m.product_id.valuation == 'real_time' and (m._is_in() or m._is_out())):
+ for move in res.filtered(lambda m: m.product_id.valuation == 'real_time' and (m._is_in() or m._is_out() or m._is_dropshipped())):
move._account_entry_move()
return res
@@ -589,7 +612,7 @@ class StockMove(models.Model):
else:
self.with_context(force_company=company_from.id)._create_account_move_line(acc_valuation, acc_dest, journal_id)
- if self.company_id.anglo_saxon_accounting and self.location_id.usage == 'supplier' and self.location_dest_id.usage == 'customer':
+ if self.company_id.anglo_saxon_accounting and self._is_dropshipped():
# Creates an account entry from stock_input to stock_output on a dropship move. https://github.com/odoo/odoo/issues/12687
journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation()
self.with_context(force_company=self.company_id.id)._create_account_move_line(acc_src, acc_dest, journal_id)
diff --git a/addons/stock_dropshipping/tests/__init__.py b/addons/stock_dropshipping/tests/__init__.py
index f4159ef6e7f..c060ad7247b 100644
--- a/addons/stock_dropshipping/tests/__init__.py
+++ b/addons/stock_dropshipping/tests/__init__.py
@@ -5,3 +5,5 @@ from . import test_crossdock
from . import test_dropship
from . import test_lifo_price
from . import test_procurement_exception
+from . import test_stockvaluation
+
diff --git a/addons/stock_dropshipping/tests/test_stockvaluation.py b/addons/stock_dropshipping/tests/test_stockvaluation.py
new file mode 100644
index 00000000000..2eb1ced2756
--- /dev/null
+++ b/addons/stock_dropshipping/tests/test_stockvaluation.py
@@ -0,0 +1,268 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.account.tests.account_test_classes import AccountingTestCase
+
+
+class TestStockValuation(AccountingTestCase):
+ def setUp(self):
+ super(TestStockValuation, self).setUp()
+ self.supplier_location = self.env.ref('stock.stock_location_suppliers')
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ self.partner_id = self.env.ref('base.res_partner_1')
+ self.product1 = self.env.ref('product.product_product_8')
+
+ self.acc_payable = self.env['account.account'].search([('name', '=', 'Account Payable')]).id
+ self.acc_expense = self.env['account.account'].search([('name', '=', 'Expenses')]).id
+ self.acc_receivable = self.env['account.account'].search([('name', '=', 'Account Receivable')]).id
+ self.acc_sale = self.env['account.account'].search([('name', '=', 'Product Sales')]).id
+ self.acc_stock_in = self.env['account.account'].search([('name', '=', 'Stock Interim Account (Received)')]).id
+ self.acc_stock_out = self.env['account.account'].search([('name', '=', 'Stock Interim Account (Delivered)')]).id
+
+ def _dropship_product1(self):
+ # enable the dropship and MTO route on the product
+ dropshipping_route = self.env.ref('stock_dropshipping.route_drop_shipping')
+ mto_route = self.env.ref('stock.route_warehouse0_mto')
+ self.product1.write({'route_ids': [(6, 0, [dropshipping_route.id, mto_route.id])]})
+
+ # add a vendor
+ vendor1 = self.env['res.partner'].create({'name': 'vendor1'})
+ seller1 = self.env['product.supplierinfo'].create({
+ 'name': vendor1.id,
+ 'price': 8,
+ })
+ self.product1.write({'seller_ids': [(6, 0, [seller1.id])]})
+
+ # sell one unit of this product
+ customer1 = self.env['res.partner'].create({'name': 'customer1'})
+ sale_order1 = self.env['sale.order'].create({
+ 'partner_id': customer1.id,
+ 'partner_invoice_id': customer1.id,
+ 'partner_shipping_id': customer1.id,
+ 'order_line': [(0, 0, {
+ 'name': self.product1.name,
+ 'product_id': self.product1.id,
+ 'product_uom_qty': 1,
+ 'product_uom': self.product1.uom_id.id,
+ 'price_unit': 12,
+ })],
+ 'pricelist_id': self.env.ref('product.list0').id,
+ 'picking_policy': 'direct',
+ })
+ sale_order1.action_confirm()
+
+ # confirm the purchase order
+ purchase_order1 = self.env['purchase.order'].search([('group_id', '=', sale_order1.procurement_group_id.id)])
+ purchase_order1.button_confirm()
+
+ # validate the dropshipping picking
+ self.assertEqual(len(sale_order1.picking_ids), 1)
+ self.assertEqual(sale_order1.picking_ids.move_lines._is_dropshipped(), True)
+ wizard = sale_order1.picking_ids.button_validate()
+ immediate_transfer = self.env[wizard['res_model']].browse(wizard['res_id'])
+ immediate_transfer.process()
+ self.assertEqual(sale_order1.picking_ids.state, 'done')
+
+ # create the vendor bill
+ vendor_bill1 = self.env['account.invoice'].create({
+ 'partner_id': vendor1.id,
+ 'purchase_id': purchase_order1.id,
+ 'account_id': vendor1.property_account_payable_id.id,
+ 'type': 'in_invoice',
+ })
+ vendor_bill1.purchase_order_change()
+ vendor_bill1.action_invoice_open()
+
+ # create the customer invoice
+ customer_invoice1_id = sale_order1.action_invoice_create()
+ customer_invoice1 = self.env['account.invoice'].browse(customer_invoice1_id)
+ customer_invoice1.action_invoice_open()
+
+ all_amls = vendor_bill1.move_id.line_ids + customer_invoice1.move_id.line_ids
+ if sale_order1.picking_ids.move_lines.account_move_ids:
+ all_amls |= sale_order1.picking_ids.move_lines.account_move_ids.line_ids
+ return all_amls
+
+ def _check_results(self, expected_aml, expected_aml_count, all_amls):
+ # Construct a dict similar to `expected_aml` with `all_amls` in order to
+ # compare them.
+ result_aml = {}
+ for aml in all_amls:
+ account_id = aml.account_id.id
+ if result_aml.get(account_id):
+ debit = result_aml[account_id][0]
+ credit = result_aml[account_id][1]
+ result_aml[account_id] = (debit + aml.debit, credit + aml.credit)
+ else:
+ result_aml[account_id] = (aml.debit, aml.credit)
+
+ self.assertEqual(len(all_amls), expected_aml_count)
+
+ for k, v in expected_aml.items():
+ self.assertEqual(result_aml[k], v)
+
+ # -------------------------------------------------------------------------
+ # Continental
+ # -------------------------------------------------------------------------
+ def test_dropship_standard_perpetual_continental_ordered(self):
+ self.env.user.company_id.anglo_saxon_accounting = False
+ self.product1.product_tmpl_id.cost_method = 'standard'
+ self.product1.product_tmpl_id.standard_price = 10
+ self.product1.product_tmpl_id.valuation = 'real_time'
+ self.product1.product_tmpl_id.invoice_policy = 'order'
+
+ all_amls = self._dropship_product1()
+
+ expected_aml = {
+ self.acc_payable: (0.0, 8.0),
+ self.acc_expense: (8.0, 0.0),
+ self.acc_receivable: (12.0, 0.0),
+ self.acc_sale: (0.0, 12.0),
+ }
+
+ self._check_results(expected_aml, 4, all_amls)
+
+ def test_dropship_standard_perpetual_continental_delivered(self):
+ self.env.user.company_id.anglo_saxon_accounting = False
+ self.product1.product_tmpl_id.cost_method = 'standard'
+ self.product1.product_tmpl_id.standard_price = 10
+ self.product1.product_tmpl_id.valuation = 'real_time'
+ self.product1.product_tmpl_id.invoice_policy = 'delivery'
+
+ all_amls = self._dropship_product1()
+
+ expected_aml = {
+ self.acc_payable: (0.0, 8.0),
+ self.acc_expense: (8.0, 0.0),
+ self.acc_receivable: (12.0, 0.0),
+ self.acc_sale: (0.0, 12.0),
+ }
+
+ self._check_results(expected_aml, 4, all_amls)
+
+ def test_dropship_fifo_perpetual_continental_ordered(self):
+ self.env.user.company_id.anglo_saxon_accounting = False
+ self.product1.product_tmpl_id.cost_method = 'fifo'
+ self.product1.product_tmpl_id.standard_price = 10
+ self.product1.product_tmpl_id.valuation = 'real_time'
+ self.product1.product_tmpl_id.invoice_policy = 'order'
+
+ all_amls = self._dropship_product1()
+
+ expected_aml = {
+ self.acc_payable: (0.0, 8.0),
+ self.acc_expense: (8.0, 0.0),
+ self.acc_receivable: (12.0, 0.0),
+ self.acc_sale: (0.0, 12.0),
+ }
+
+ self._check_results(expected_aml, 4, all_amls)
+
+ def test_dropship_fifo_perpetual_continental_delivered(self):
+ self.env.user.company_id.anglo_saxon_accounting = False
+
+ self.product1.product_tmpl_id.cost_method = 'fifo'
+ self.product1.product_tmpl_id.standard_price = 10
+ self.product1.product_tmpl_id.valuation = 'real_time'
+ self.product1.product_tmpl_id.invoice_policy = 'delivery'
+
+ all_amls = self._dropship_product1()
+
+ expected_aml = {
+ self.acc_payable: (0.0, 8.0),
+ self.acc_expense: (8.0, 0.0),
+ self.acc_receivable: (12.0, 0.0),
+ self.acc_sale: (0.0, 12.0),
+ }
+
+ self._check_results(expected_aml, 4, all_amls)
+
+ # -------------------------------------------------------------------------
+ # Anglosaxon
+ # -------------------------------------------------------------------------
+ def test_dropship_standard_perpetual_anglosaxon_ordered(self):
+ self.env.user.company_id.anglo_saxon_accounting = True
+ self.product1.product_tmpl_id.cost_method = 'standard'
+ self.product1.product_tmpl_id.standard_price = 10
+ self.product1.product_tmpl_id.valuation = 'real_time'
+ self.product1.product_tmpl_id.invoice_policy = 'order'
+
+ all_amls = self._dropship_product1()
+
+ expected_aml = {
+ self.acc_payable: (0.0, 8.0),
+ self.acc_expense: (10.0, 0.0),
+ self.acc_receivable: (12.0, 0.0),
+ self.acc_sale: (0.0, 12.0),
+ self.acc_stock_in: (8.0, 10.0),
+ self.acc_stock_out: (10.0, 10.0),
+ }
+ # Interim IN is not balanced because because there's a difference between the po line
+ # price unit and the standard price. We could set a price difference account on the
+ # category to compensate.
+
+ self._check_results(expected_aml, 8, all_amls)
+
+ def test_dropship_standard_perpetual_anglosaxon_delivered(self):
+ self.env.user.company_id.anglo_saxon_accounting = True
+ self.product1.product_tmpl_id.cost_method = 'standard'
+ self.product1.product_tmpl_id.standard_price = 10
+ self.product1.product_tmpl_id.valuation = 'real_time'
+ self.product1.product_tmpl_id.invoice_policy = 'delivery'
+
+ all_amls = self._dropship_product1()
+
+ expected_aml = {
+ self.acc_payable: (0.0, 8.0),
+ self.acc_expense: (10.0, 0.0),
+ self.acc_receivable: (12.0, 0.0),
+ self.acc_sale: (0.0, 12.0),
+ self.acc_stock_in: (8.0, 10.0),
+ self.acc_stock_out: (10.0, 10.0),
+ }
+ # Interim IN is not balanced because because there's a difference between the po line
+ # price unit and the standard price. We could set a price difference account on the
+ # category to compensate.
+
+ self._check_results(expected_aml, 8, all_amls)
+
+ def test_dropship_fifo_perpetual_anglosaxon_ordered(self):
+ self.env.user.company_id.anglo_saxon_accounting = True
+ self.product1.product_tmpl_id.cost_method = 'fifo'
+ self.product1.product_tmpl_id.standard_price = 10
+ self.product1.product_tmpl_id.valuation = 'real_time'
+ self.product1.product_tmpl_id.invoice_policy = 'order'
+
+ all_amls = self._dropship_product1()
+
+ expected_aml = {
+ self.acc_payable: (0.0, 8.0),
+ self.acc_expense: (8.0, 0.0),
+ self.acc_receivable: (12.0, 0.0),
+ self.acc_sale: (0.0, 12.0),
+ self.acc_stock_in: (8.0, 8.0),
+ self.acc_stock_out: (8.0, 8.0),
+ }
+
+ self._check_results(expected_aml, 8, all_amls)
+
+ def test_dropship_fifo_perpetual_anglosaxon_delivered(self):
+ self.env.user.company_id.anglo_saxon_accounting = True
+ self.product1.product_tmpl_id.cost_method = 'fifo'
+ self.product1.product_tmpl_id.standard_price = 10
+ self.product1.product_tmpl_id.valuation = 'real_time'
+ self.product1.product_tmpl_id.invoice_policy = 'delivery'
+
+ all_amls = self._dropship_product1()
+
+ expected_aml = {
+ self.acc_payable: (0.0, 8.0),
+ self.acc_expense: (8.0, 0.0),
+ self.acc_receivable: (12.0, 0.0),
+ self.acc_sale: (0.0, 12.0),
+ self.acc_stock_in: (8.0, 8.0),
+ self.acc_stock_out: (8.0, 8.0),
+ }
+
+ self._check_results(expected_aml, 8, all_amls)
+
diff --git a/addons/stock_landed_costs/views/stock_landed_cost_views.xml b/addons/stock_landed_costs/views/stock_landed_cost_views.xml
index 6ec34e5db11..c2b5b580658 100644
--- a/addons/stock_landed_costs/views/stock_landed_cost_views.xml
+++ b/addons/stock_landed_costs/views/stock_landed_cost_views.xml
@@ -22,7 +22,7 @@
-
+
diff --git a/addons/survey/models/survey.py b/addons/survey/models/survey.py
index 78ea2b17810..60c387fd395 100644
--- a/addons/survey/models/survey.py
+++ b/addons/survey/models/survey.py
@@ -680,6 +680,7 @@ class SurveyLabel(models.Model):
value = fields.Char('Suggested value', translate=True, required=True)
quizz_mark = fields.Float('Score for this choice', help="A positive score indicates a correct choice; a negative or null score indicates a wrong answer")
+ @api.one
@api.constrains('question_id', 'question_id_2')
def _check_question_not_empty(self):
"""Ensure that field question_id XOR field question_id_2 is not null"""
diff --git a/addons/web/static/lib/qweb/qweb2.js b/addons/web/static/lib/qweb/qweb2.js
index c4bb7128e9b..94a0afaa32a 100644
--- a/addons/web/static/lib/qweb/qweb2.js
+++ b/addons/web/static/lib/qweb/qweb2.js
@@ -358,7 +358,12 @@ QWeb2.Engine = (function() {
req.open('GET', s, async);
if (async) {
req.addEventListener("load", function() {
- if (req.status == 200) {
+ // 0, not being a valid HTTP status code, is used by browsers
+ // to indicate success for a non-http xhr response
+ // (for example, using the file:// protocol)
+ // https://developer.mozilla.org/fr/docs/Web/API/XMLHttpRequest
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=331610
+ if (req.status == 200 || req.status == 0) {
callback(null, self._parse_from_request(req));
} else {
callback(new Error("Can't load template " + s + ", http status " + req.status));
diff --git a/addons/web/static/src/js/views/pivot/pivot_controller.js b/addons/web/static/src/js/views/pivot/pivot_controller.js
index 731183e7efe..5f31c7103c7 100644
--- a/addons/web/static/src/js/views/pivot/pivot_controller.js
+++ b/addons/web/static/src/js/views/pivot/pivot_controller.js
@@ -126,6 +126,7 @@ var PivotController = AbstractController.extend({
var table = this.model.exportData();
if(table.measure_row.length + 1 > 256) {
crash_manager.show_message(_t("For Excel compatibility, data cannot be exported if there are more than 256 columns.\n\nTip: try to flip axis, filter further or reduce the number of measures."));
+ framework.unblockUI();
return;
}
framework.blockUI();
diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js
index 99a5e4ee334..1c76c41ab71 100644
--- a/addons/web/static/src/js/views/pivot/pivot_model.js
+++ b/addons/web/static/src/js/views/pivot/pivot_model.js
@@ -135,7 +135,7 @@ var PivotModel = AbstractModel.extend({
exportData: function () {
var measureNbr = this.data.measures.length;
var headers = this._computeHeaders();
- var measureRow = measureNbr > 1 ? _.last(headers) : [];
+ var measureRow = measureNbr >= 1 ? _.last(headers) : [];
var rows = this._computeRows();
var i, j, value;
headers[0].splice(0,1);
diff --git a/addons/web/static/src/js/widgets/data_export.js b/addons/web/static/src/js/widgets/data_export.js
index 4b9f6a29a47..dd0096f269b 100644
--- a/addons/web/static/src/js/widgets/data_export.js
+++ b/addons/web/static/src/js/widgets/data_export.js
@@ -455,7 +455,7 @@ var DataExport = Dialog.extend({
Dialog.alert(this, _t("Please select fields to export..."));
return;
}
- exported_fields.unshift({name: 'id', label: 'External ID'});
+ exported_fields.unshift({name: 'id', label: _t('External ID')});
var export_format = this.$export_format_inputs.filter(':checked').val();
diff --git a/addons/web/static/src/less/form_view.less b/addons/web/static/src/less/form_view.less
index 505f62ac42f..60a3671caa2 100644
--- a/addons/web/static/src/less/form_view.less
+++ b/addons/web/static/src/less/form_view.less
@@ -165,7 +165,9 @@
.o-flex-flow(row, wrap);
> .btn {
- margin-right: 3px;
+ @o-statusbar-buttons-vmargin: 4px;
+ min-height: @odoo-statusbar-height - 2 * @o-statusbar-buttons-vmargin;
+ margin: @o-statusbar-buttons-vmargin 3px @o-statusbar-buttons-vmargin 0;
padding-top: 2px;
padding-bottom: 2px;
}
diff --git a/addons/website_form/controllers/main.py b/addons/website_form/controllers/main.py
index f4fd9716aa2..b37ca97ea79 100644
--- a/addons/website_form/controllers/main.py
+++ b/addons/website_form/controllers/main.py
@@ -171,7 +171,8 @@ class WebsiteForm(http.Controller):
return data
def insert_record(self, request, model, values, custom, meta=None):
- record = request.env[model.model].sudo().with_context(mail_create_nosubscribe=True).create(values)
+ model_name = model.sudo().model
+ record = request.env[model_name].sudo().with_context(mail_create_nosubscribe=True).create(values)
if custom or meta:
default_field = model.website_form_default_field_id
@@ -183,13 +184,13 @@ class WebsiteForm(http.Controller):
# If there is a default field configured for this model, use it.
# If there isn't, put the custom data in a message instead
if default_field.name:
- if default_field.ttype == 'html' or model.model == 'mail.mail':
+ if default_field.ttype == 'html' or model_name == 'mail.mail':
custom_content = nl2br(custom_content)
record.update({default_field.name: custom_content})
else:
values = {
'body': nl2br(custom_content),
- 'model': model.model,
+ 'model': model_name,
'message_type': 'comment',
'no_auto_thread': False,
'res_id': record.id,
@@ -201,7 +202,8 @@ class WebsiteForm(http.Controller):
# Link all files attached on the form
def insert_attachment(self, model, id_record, files):
orphan_attachment_ids = []
- record = model.env[model.model].browse(id_record)
+ model_name = model.sudo().model
+ record = model.env[model_name].browse(id_record)
authorized_fields = model.sudo()._get_form_writable_fields()
for file in files:
custom_field = file.field_name not in authorized_fields
@@ -209,7 +211,7 @@ class WebsiteForm(http.Controller):
'name': file.filename,
'datas': base64.encodestring(file.read()),
'datas_fname': file.filename,
- 'res_model': model.model,
+ 'res_model': model_name,
'res_id': record.id,
}
attachment_id = request.env['ir.attachment'].sudo().create(attachment_value)
@@ -221,10 +223,10 @@ class WebsiteForm(http.Controller):
# If some attachments didn't match a field on the model,
# we create a mail.message to link them to the record
if orphan_attachment_ids:
- if model.model != 'mail.mail':
+ if model_name != 'mail.mail':
values = {
'body': _('Attached files :
'),
- 'model': model.model,
+ 'model': model_name,
'message_type': 'comment',
'no_auto_thread': False,
'res_id': id_record,
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index d3a44363fab..3cf961ada09 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -151,7 +151,7 @@ class WebsiteSale(http.Controller):
else:
price = variant.website_public_price / quantity
visible_attribute_ids = [v.id for v in variant.attribute_value_ids if v.attribute_id.id in visible_attrs_ids]
- attribute_value_ids.append([variant.id, visible_attribute_ids, variant.website_price, price])
+ attribute_value_ids.append([variant.id, visible_attribute_ids, variant.website_price / quantity, price])
return attribute_value_ids
def _get_search_order(self, post):
diff --git a/addons/website_sale/data/demo.xml b/addons/website_sale/data/demo.xml
index e7a9dfafb94..7d6b4ee7524 100644
--- a/addons/website_sale/data/demo.xml
+++ b/addons/website_sale/data/demo.xml
@@ -548,6 +548,16 @@ Weight: 31 grams
formula
+
+ list_price
+ 1_product
+
+
+ 40
+ 2
+ formula
+
+
diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js
index 874b9f698b5..b1fe1d710ce 100644
--- a/addons/website_sale/static/src/js/website_sale.js
+++ b/addons/website_sale/static/src/js/website_sale.js
@@ -88,18 +88,34 @@ odoo.define('website_sale.website_sale', function (require) {
$(oe_website_sale).on("change", 'input[name="add_qty"]', function (event) {
var product_ids = [];
- var product_dom = $(".js_product .js_add_cart_variants[data-attribute_value_ids]");
+ var product_dom = $(event.target).closest(".js_product").find("ul.js_add_cart_variants");
var qty = $(event.target).closest('form').find('input[name="add_qty"]').val();
if (!product_dom.length) {
+ // if variants in list view, update variant price based on quantity
+ var $list_products = $(event.target).closest('.js_product').find('.js_product_change');
+ if ($list_products.length) {
+ ajax.jsonRpc("/shop/get_unit_price", 'call', {
+ 'product_ids': _.map($list_products, function (variant) {
+ return parseInt(variant.value);
+ }),
+ 'add_qty': parseInt(qty)
+ }).then(function (data) {
+ _.each(data, function (value, key) {
+ $list_products.filter('[value="'+key+'"]').data('price', value);
+ });
+ $list_products.filter(':checked').change();
+ });
+ }
return;
}
var attribute_value_ids = product_dom.data("attribute_value_ids");
if(_.isString(attribute_value_ids)) {
attribute_value_ids = JSON.parse(attribute_value_ids.replace(/'/g, '"'));
+ // set permanent reference so modifications are kept
+ product_dom.data('attribute_value_ids', attribute_value_ids);
}
_.each(attribute_value_ids, function(entry) {
product_ids.push(entry[0]);});
- var qty = $(event.target).closest('form').find('input[name="add_qty"]').val();
if ($("#product_detail").length) {
// display the reduction from the pricelist in function of the quantity
@@ -109,7 +125,7 @@ odoo.define('website_sale.website_sale', function (require) {
for(var j=0; j < current.length; j++){
current[j][2] = data[current[j][0]];
}
- product_dom.attr("data-attribute_value_ids", JSON.stringify(current)).trigger("change");
+ product_dom.trigger("change");
});
}
});
@@ -210,8 +226,7 @@ odoo.define('website_sale.website_sale', function (require) {
$('input[name="'+$input.attr("name")+'"]').add($input).filter(function () {
var $prod = $(this).closest('*:has(input[name="product_id"])');
return !$prod.length || +$prod.find('input[name="product_id"]').val() === product_id;
- }).val(new_qty);
- $input.change();
+ }).val(new_qty).change();
return false;
});
@@ -380,7 +395,7 @@ odoo.define('website_sale.website_sale', function (require) {
});
$('div.js_product', oe_website_sale).each(function () {
- $('input.js_product_change', this).first().trigger('change');
+ $('input.js_product_change', this).first().prop('checked', 'checked').trigger('change');
});
$('.js_add_cart_variants', oe_website_sale).each(function () {
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 3595a5fd219..662d2212838 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -458,8 +458,7 @@