diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index 212963a767b..d0ea7e96c20 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -538,6 +538,8 @@ class AccountJournal(models.Model):
@api.constrains('currency_id', 'default_credit_account_id', 'default_debit_account_id')
def _check_currency(self):
if self.currency_id:
+ if self.currency_id == self.company_id.currency_id:
+ raise ValidationError(_("Currency field should only be set if the journal's currency is different from the company's. Leave the field blank to use company currency."))
if self.default_credit_account_id and not self.default_credit_account_id.currency_id.id == self.currency_id.id:
raise ValidationError(_('The currency of the journal should be the same than the default credit account.'))
if self.default_debit_account_id and not self.default_debit_account_id.currency_id.id == self.currency_id.id:
diff --git a/addons/account/test/account_minimal_test.xml b/addons/account/test/account_minimal_test.xml
index 5284643f6e5..144f971cf90 100644
--- a/addons/account/test/account_minimal_test.xml
+++ b/addons/account/test/account_minimal_test.xml
@@ -276,7 +276,6 @@
bank
-
diff --git a/addons/account/tests/test_payment.py b/addons/account/tests/test_payment.py
index e62429bc104..8a4cb43e7e7 100644
--- a/addons/account/tests/test_payment.py
+++ b/addons/account/tests/test_payment.py
@@ -456,6 +456,7 @@ class TestPayment(AccountingTestCase):
#check the invoice status
self.assertEqual(invoice.state, 'paid')
+
def test_payment_and_writeoff_in_other_currency_3(self):
# Use case related in revision 20935462a0cabeb45480ce70114ff2f4e91eaf79
# Invoice made in secondary currency for which the rate to the company currency
@@ -536,4 +537,4 @@ class TestPayment(AccountingTestCase):
# Check the move has been posted properly
self.assertEqual(payment.mapped('move_line_ids.move_id.state'), ['posted'], "After bank reconciliation, the payment's account.move should be posted.")
self.assertEqual(payment.mapped('move_line_ids.move_id.date'), stmt_line_date, "After bank reconciliation, the payment's account.move should share the same date as the bank statement.")
- self.assertEqual([payment.payment_date], stmt_line_date, "After bank reconciliation, the payment should share the same date as the bank statement.")
+ self.assertEqual([payment.payment_date], stmt_line_date, "After bank reconciliation, the payment should share the same date as the bank statement.")
\ No newline at end of file
diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py
index bbb083de9c5..53cdc1a6be5 100644
--- a/addons/account/tests/test_reconciliation.py
+++ b/addons/account/tests/test_reconciliation.py
@@ -787,7 +787,7 @@ class TestReconciliation(AccountingTestCase):
currency = self.env.user.company_id.currency_id
invoice = self.create_invoice_partner(currency_id=currency.id, partner_id=partner.id)
- journal = self.env['account.journal'].create({'name': 'Bank', 'type': 'bank', 'code': 'THE', 'currency_id': currency.id})
+ journal = self.env['account.journal'].create({'name': 'Bank', 'type': 'bank', 'code': 'THE'})
statement = self.make_payment(invoice, journal, 50)
diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py
index 45647cd49a5..8124749f75e 100644
--- a/addons/calendar/models/calendar.py
+++ b/addons/calendar/models/calendar.py
@@ -607,13 +607,16 @@ class Meeting(models.Model):
event_date = datetime.datetime.now()
use_naive_datetime = self.allday and self.rrule and 'UNTIL' in self.rrule and 'Z' not in self.rrule
- if use_naive_datetime:
- rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date.replace(tzinfo=None), forceset=True, ignoretz=True)
- else:
+ if not use_naive_datetime:
# Convert the event date to saved timezone (or context tz) as it'll
# define the correct hour/day asked by the user to repeat for recurrence.
- event_date = event_date.astimezone(timezone) # transform "+hh:mm" timezone
- rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date, forceset=True, tzinfos={})
+ event_date = event_date.astimezone(timezone)
+
+ # The start date is naive
+ # the timezone will be applied, if necessary, at the very end of the process
+ # to allow for DST timezone reevaluation
+ rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date.replace(tzinfo=None), forceset=True, ignoretz=True)
+
recurring_meetings = self.search([('recurrent_id', '=', self.id), '|', ('active', '=', False), ('active', '=', True)])
# We handle a maximum of 50,000 meetings at a time, and clear the cache at each step to
@@ -629,12 +632,15 @@ class Meeting(models.Model):
else:
if not recurring_date.tzinfo:
recurring_date = pytz.UTC.localize(recurring_date)
- recurring_date = recurring_date.astimezone(timezone)
+ recurring_date = recurring_date.astimezone(timezone).replace(tzinfo=None)
if date_field == "stop":
recurring_date += timedelta(hours=self.duration)
rset1.exdate(recurring_date)
invalidate = True
- return [d.astimezone(pytz.UTC) if d.tzinfo else d for d in rset1 if d.year < MAXYEAR]
+
+ def naive_tz_to_utc(d):
+ return timezone.localize(d).astimezone(pytz.UTC)
+ return [naive_tz_to_utc(d) if not use_naive_datetime else d for d in rset1 if d.year < MAXYEAR]
@api.multi
def _get_recurrency_end_date(self):
@@ -893,17 +899,17 @@ class Meeting(models.Model):
def _inverse_dates(self):
for meeting in self:
if meeting.allday:
- tz = pytz.timezone(self.env.user.tz) if self.env.user.tz else pytz.utc
+ # Convention break:
+ # stop and start are NOT in UTC in allday event
+ # in this case, they actually represent a date
+ # i.e. Christmas is on 25/12 for everyone
+ # even if people don't celebrate it simultaneously
enddate = fields.Datetime.from_string(meeting.stop_date)
- enddate = tz.localize(enddate)
enddate = enddate.replace(hour=18)
- enddate = enddate.astimezone(pytz.utc)
startdate = fields.Datetime.from_string(meeting.start_date)
- startdate = tz.localize(startdate) # Add "+hh:mm" timezone
- startdate = startdate.replace(hour=8) # Set 8 AM in localtime
- startdate = startdate.astimezone(pytz.utc) # Convert to UTC
+ startdate = startdate.replace(hour=8) # Set 8 AM
meeting.write({
'start': startdate.replace(tzinfo=None),
diff --git a/addons/calendar/tests/test_calendar.py b/addons/calendar/tests/test_calendar.py
index fbf86ace9e1..fe456339eaa 100644
--- a/addons/calendar/tests/test_calendar.py
+++ b/addons/calendar/tests/test_calendar.py
@@ -6,6 +6,7 @@ from datetime import datetime, timedelta, time
from odoo import fields
from odoo.tests.common import TransactionCase
+import pytz
class TestCalendar(TransactionCase):
@@ -275,3 +276,41 @@ class TestCalendar(TransactionCase):
self.assertEqual(test_event.res_model, test_record._name)
self.assertEqual(test_event.res_id, test_record.id)
self.assertEqual(len(test_record.activity_ids), 1)
+
+ def test_event_allday(self):
+ self.env.user.tz = 'Pacific/Honolulu'
+
+ event = self.CalendarEvent.create({
+ 'name': 'All Day',
+ 'start': "2018-10-16 00:00:00",
+ 'start_date': "2018-10-16",
+ 'start_datetime': False,
+ 'stop': "2018-10-18 00:00:00",
+ 'stop_date': "2018-10-18",
+ 'stop_datetime': False,
+ 'allday': True,
+ })
+
+ self.assertEqual(str(event.start), '2018-10-16 08:00:00')
+ self.assertEqual(str(event.stop), '2018-10-18 18:00:00')
+
+ def test_recurring_around_dst(self):
+ m = self.CalendarEvent.create({
+ 'name': "wheee",
+ 'start': '2018-10-27 14:30:00',
+ 'allday': False,
+ 'rrule': u'FREQ=DAILY;INTERVAL=1;COUNT=4',
+ 'duration': 2,
+ 'stop': '2018-10-27 16:30:00',
+ })
+
+ start_recurring_dates = m.with_context({'tz': 'Europe/Brussels'})._get_recurrent_date_by_event()
+ self.assertEqual(len(start_recurring_dates), 4)
+
+ for d in start_recurring_dates:
+ self.assertEqual(d.tzinfo, pytz.UTC)
+ if d.day < 28: # DST switch happens between 2018-10-27 and 2018-10-28
+ self.assertEqual(d.hour, 14)
+ else:
+ self.assertEqual(d.hour, 15)
+ self.assertEqual(d.minute, 30)
diff --git a/addons/hr_attendance/models/hr_attendance.py b/addons/hr_attendance/models/hr_attendance.py
index cadccfcf6d0..6b1550438a2 100644
--- a/addons/hr_attendance/models/hr_attendance.py
+++ b/addons/hr_attendance/models/hr_attendance.py
@@ -77,7 +77,7 @@ class HrAttendance(models.Model):
('employee_id', '=', attendance.employee_id.id),
('check_out', '=', False),
('id', '!=', attendance.id),
- ])
+ ], order='check_in desc', limit=1)
if no_check_out_attendances:
raise exceptions.ValidationError(_("Cannot create new attendance record for %(empl_name)s, the employee hasn't checked out since %(datetime)s") % {
'empl_name': attendance.employee_id.name,
diff --git a/addons/l10n_ch/models/res_bank.py b/addons/l10n_ch/models/res_bank.py
index c4ac741ded2..7fec01e1ddb 100644
--- a/addons/l10n_ch/models/res_bank.py
+++ b/addons/l10n_ch/models/res_bank.py
@@ -11,8 +11,12 @@ import werkzeug.urls
def _is_l10n_ch_postal(account_ref):
""" Returns True iff the string account_ref is a valid postal account number,
i.e. it only contains ciphers and is last cipher is the result of a recursive
- modulo 10 operation ran over the rest of it.
+ modulo 10 operation ran over the rest of it. Shorten form with - is also accepted.
"""
+ if re.match('^[0-9]{2}-[0-9]{1,6}-[0-9]$', account_ref or ''):
+ ref_subparts = account_ref.split('-')
+ account_ref = ref_subparts[0] + ref_subparts[1].rjust(6,'0') + ref_subparts[2]
+
if re.match('\d+$', account_ref or ''):
account_ref_without_check = account_ref[:-1]
return mod10r(account_ref_without_check) == account_ref
diff --git a/addons/l10n_ro/data/l10n_ro_chart_data.xml b/addons/l10n_ro/data/l10n_ro_chart_data.xml
index b576f47c29d..8cb84f71214 100644
--- a/addons/l10n_ro/data/l10n_ro_chart_data.xml
+++ b/addons/l10n_ro/data/l10n_ro_chart_data.xml
@@ -1807,7 +1807,7 @@
- >Datorii din operaţiuni de fiducie
+ Datorii din operaţiuni de fiducie4661
diff --git a/addons/l10n_vn/data/l10n_vn_chart_data.xml b/addons/l10n_vn/data/l10n_vn_chart_data.xml
index 0748dd1531e..c643d8a38e5 100644
--- a/addons/l10n_vn/data/l10n_vn_chart_data.xml
+++ b/addons/l10n_vn/data/l10n_vn_chart_data.xml
@@ -81,8 +81,7 @@
- Thuế GTGT được khấu trừ của hàng hóa, dịch vụ
-
+ Thuế GTGT được khấu trừ của hàng hóa, dịch vụ1332
@@ -140,7 +139,7 @@
1388
-
+ Phải thu khác
@@ -149,7 +148,7 @@
141
-
+ Tạm ứng
@@ -322,8 +321,7 @@
- Cây lâu năm, súc vật làm việc và cho sản phẩm
-
+ Cây lâu năm, súc vật làm việc và cho sản phẩm2118
@@ -449,7 +447,7 @@
- >Đầu tư vào công ty liên doanh
+ Đầu tư vào công ty liên doanh
@@ -459,7 +457,7 @@
- >Đầu tư góp vốn vào đơn vị khác
+ Đầu tư góp vốn vào đơn vị khác
@@ -651,14 +649,14 @@
3341
-
+ Phải trả công nhân viên3348
-
+ Phải trả người lao động khác
@@ -677,7 +675,7 @@
- Phải trả nội bộ về vốn kinh doanh
+ Phải trả nội bộ về vốn kinh doanh
@@ -763,7 +761,7 @@
3388
-
+ Phải trả, phải nộp khác
@@ -790,7 +788,7 @@
- Trái phiếu
+ Trái phiếu34311
diff --git a/addons/mail/models/mail_tracking_value.py b/addons/mail/models/mail_tracking_value.py
index 4edeb10fa65..5c899b957cc 100644
--- a/addons/mail/models/mail_tracking_value.py
+++ b/addons/mail/models/mail_tracking_value.py
@@ -9,6 +9,7 @@ from odoo import api, fields, models, tools
class MailTracking(models.Model):
_name = 'mail.tracking.value'
_description = 'Mail Tracking Value'
+ _rec_name = 'field'
_order = 'track_sequence asc'
# TDE CLEANME: why not a m2o to ir model field ?
diff --git a/addons/mail/static/src/js/composers/basic_composer.js b/addons/mail/static/src/js/composers/basic_composer.js
index a6cdb39e2de..0e4d62232e2 100644
--- a/addons/mail/static/src/js/composers/basic_composer.js
+++ b/addons/mail/static/src/js/composers/basic_composer.js
@@ -390,6 +390,7 @@ var BasicComposer = Widget.extend({
return;
}
+ this.$(".o_composer_button_send").prop("disabled", true);
clearTimeout(this._cannedTimeout);
var self = this;
this._preprocessMessage().then(function (message) {
diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml
index 83d40bc7af1..e5db4a72524 100644
--- a/addons/mail/static/src/xml/thread.xml
+++ b/addons/mail/static/src/xml/thread.xml
@@ -351,6 +351,7 @@
+
@@ -358,9 +359,6 @@
Subject:
-
-
-
diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js
index aff69b58197..d4d10f71746 100644
--- a/addons/mail/static/tests/chatter_tests.js
+++ b/addons/mail/static/tests/chatter_tests.js
@@ -786,6 +786,59 @@ QUnit.test('chatter: post, receive and star messages', function (assert) {
});
});
+QUnit.test('chatter: post a message disable the send button', function(assert) {
+ assert.expect(3);
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ services: this.services,
+ arch: '',
+ res_id: 2,
+ session: {},
+ mockRPC: function (route, args) {
+ if (args.method === 'message_get_suggested_recipients') {
+ return $.when({2: []});
+ }
+ if (args.method === 'message_post') {
+ assert.ok(form.$('.o_composer_button_send').prop("disabled"),
+ "Send button should be disabled when a message is being sent");
+ return $.when(57923);
+ }
+ if (args.method === 'message_format') {
+ return $.when([{
+ author_id: ["42", "Me"],
+ model: 'partner',
+ }]);
+ }
+ return this._super(route, args);
+ },
+ intercepts: {
+ get_messages: function (ev) {
+ ev.stopPropagation();
+ ev.data.callback($.when([]));
+ },
+ },
+ });
+
+ form.$('.o_chatter_button_new_message').click();
+ assert.notOk(form.$('.o_composer_button_send').prop('disabled'),
+ "Send button should be enabled when posting a message");
+ form.$('.oe_chatter .o_composer_text_field:first()').val("My first message");
+ form.$('.oe_chatter .o_composer_button_send').click();
+ form.$('.o_chatter_button_new_message').click();
+ assert.notOk(form.$('.o_composer_button_send').prop('disabled'),
+ "Send button should be enabled when posting another message");
+ form.destroy();
+});
+
QUnit.test('chatter: receive notif when document is open', function (assert) {
assert.expect(2);
diff --git a/addons/mrp/models/mrp_unbuild.py b/addons/mrp/models/mrp_unbuild.py
index 8eeb6ef1923..7ee94a3af71 100644
--- a/addons/mrp/models/mrp_unbuild.py
+++ b/addons/mrp/models/mrp_unbuild.py
@@ -123,7 +123,7 @@ class MrpUnbuild(models.Model):
if produce_move.has_tracking != 'none':
original_move = self.mo_id.move_raw_ids.filtered(lambda move: move.product_id == produce_move.product_id)
needed_quantity = produce_move.product_qty
- for move_lines in original_move.mapped('move_line_ids'):
+ for move_lines in original_move.mapped('move_line_ids').filtered(lambda ml: ml.lot_produced_id == self.lot_id):
# Iterate over all move_lines until we unbuilded the correct quantity.
taken_quantity = min(needed_quantity, move_lines.qty_done)
if taken_quantity:
diff --git a/addons/mrp/views/res_config_settings_views.xml b/addons/mrp/views/res_config_settings_views.xml
index d82b8faa8e4..ae48a0e1bea 100644
--- a/addons/mrp/views/res_config_settings_views.xml
+++ b/addons/mrp/views/res_config_settings_views.xml
@@ -13,7 +13,7 @@
-
+
diff --git a/addons/mrp_bom_cost/models/product.py b/addons/mrp_bom_cost/models/product.py
index 9309d73a6d7..9af9b84d9d6 100644
--- a/addons/mrp_bom_cost/models/product.py
+++ b/addons/mrp_bom_cost/models/product.py
@@ -53,12 +53,11 @@ class ProductProduct(models.Model):
if not boms_to_recompute:
boms_to_recompute = []
total = 0
- quant_quantity = bom.product_uom_id._compute_quantity(bom.product_qty, bom.product_tmpl_id.uom_id)
for opt in bom.routing_id.operation_ids:
duration_expected = (
opt.workcenter_id.time_start +
opt.workcenter_id.time_stop +
- quant_quantity * opt.time_cycle)
+ opt.time_cycle)
total += (duration_expected / 60) * opt.workcenter_id.costs_hour
for line in bom.bom_line_ids:
if line._skip_bom_line(self):
diff --git a/addons/mrp_bom_cost/tests/test_bom_price.py b/addons/mrp_bom_cost/tests/test_bom_price.py
index a347a497e37..c91f6203bca 100644
--- a/addons/mrp_bom_cost/tests/test_bom_price.py
+++ b/addons/mrp_bom_cost/tests/test_bom_price.py
@@ -3,7 +3,7 @@
from odoo.exceptions import UserError
from odoo.tests import common, Form
-from odoo.tools.float_utils import float_round
+from odoo.tools.float_utils import float_round, float_compare
class TestBom(common.TransactionCase):
@@ -166,11 +166,11 @@ class TestBom(common.TransactionCase):
# Table Head Operation Cost (1 Dozen)
# --------------------------------------------------------------------------
# Operation cost calculate for 1 dozens
- # Cutting (20 * 12 / 60) * 100 = 400
- # Drilling (25 * 12 / 60) * 100 = 500
- # Fitting (30 * 12 / 60) * 100 = 600
+ # Cutting (20 * 1 / 60) * 100 = 33,33
+ # Drilling (25 * 1 / 60) * 100 = 41,67
+ # Fitting (30 * 1 / 60) * 100 = 50
# ----------------------------------------
- # Operation Cost 1 dozen (1500 per dozen) and 125 for 1 Unit
+ # Operation Cost 1 dozen (125 per dozen) and 10.42 for 1 Unit
# --------------------------------------------------------------------------
self.bom_2.routing_id = routing_1.id
@@ -180,8 +180,8 @@ class TestBom(common.TransactionCase):
# Total cost of Dining Table = (550) + Total cost of operations (125) = 675.0
self.assertEquals(float_round(self.dining_table.standard_price, precision_digits=2), 675.0, "After computing price from BoM price should be 612.5")
self.Product.browse([self.dining_table.id, self.table_head.id]).action_bom_cost()
- # Total cost of Dining Table = (718.75) + Total cost of all operations (125 + 125) = 968.75
- self.assertEquals(float_round(self.dining_table.standard_price, precision_digits=2), 968.75, "After computing price from BoM price should be 786.46")
+ # Total cost of Dining Table = (718.75) + Total cost of all operations (125 + 10.42) = 854.17
+ self.assertEquals(float_compare(self.dining_table.standard_price, 854.17, precision_digits=2), 0, "After computing price from BoM price should be 786.46")
def test_01_compute_price_inventory_valuation(self):
"""Test update cost from bom in list view when inventory valuation is real time."""
diff --git a/addons/payment_stripe/controllers/main.py b/addons/payment_stripe/controllers/main.py
index a92e3cb2752..f021131fd8c 100644
--- a/addons/payment_stripe/controllers/main.py
+++ b/addons/payment_stripe/controllers/main.py
@@ -28,7 +28,7 @@ class StripeController(http.Controller):
try:
acquirer.s2s_process(post)
except Exception as e:
- error = e.message
+ error = str(e)
return_url = post.get('return_url', '/')
if error:
diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css
index 2e7d5f62238..b991609ca89 100644
--- a/addons/point_of_sale/static/src/css/pos.css
+++ b/addons/point_of_sale/static/src/css/pos.css
@@ -2170,6 +2170,19 @@ td {
overflow: auto;
height: 120px;
}
+.pos .modal-dialog .popup-textinput{
+ display: flex;
+ flex-direction: column;
+}
+.pos .modal-dialog .popup-textinput .title,
+.pos .modal-dialog .popup-textinput .footer{
+ flex: none;
+}
+.pos .modal-dialog .popup-textinput textarea{
+ resize: none;
+ flex: auto;
+ margin: 0 20px 80px;
+}
.pos .popup .title {
background: rgba(255,255,255,0.5);
margin: 0;
diff --git a/addons/point_of_sale/static/src/js/chrome.js b/addons/point_of_sale/static/src/js/chrome.js
index df68208194e..a7ef2d69b86 100644
--- a/addons/point_of_sale/static/src/js/chrome.js
+++ b/addons/point_of_sale/static/src/js/chrome.js
@@ -741,6 +741,9 @@ var Chrome = PosBaseWidget.extend(AbstractAction.prototype, {
if(err.message === 'XmlHttpRequestError '){
title = 'Network Failure (XmlHttpRequestError)';
body = 'The Point of Sale could not be loaded due to a network problem.\n Please check your internet connection.';
+ }else if(err.message === 'TLSError'){
+ title = 'Https connection to IoT Box failed';
+ body = 'Make sure you are using IoT Box v18.10 or higher.\n\n Navigate to ' + err.url + ' to accept the certificate of your IoT Box.';
}else if(err.code === 200){
title = err.data.message;
body = err.data.debug;
diff --git a/addons/point_of_sale/static/src/js/devices.js b/addons/point_of_sale/static/src/js/devices.js
index f45a48319ba..c1db7f61884 100644
--- a/addons/point_of_sale/static/src/js/devices.js
+++ b/addons/point_of_sale/static/src/js/devices.js
@@ -192,11 +192,15 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
// try harder when we remember a good proxy url
found_url = this.try_hard_to_connect(localStorage.hw_proxy_url, options)
.then(null,function(){
- return self.find_proxy(options);
+ if (window.location.protocol != 'https:'){
+ return self.find_proxy(options);
+ }
});
}else{
// just find something quick
- found_url = this.find_proxy(options);
+ if (window.location.protocol != 'https:'){
+ found_url = this.find_proxy(options);
+ }
}
success = found_url.then(function(url){
@@ -248,16 +252,17 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
// try several time to connect to a known proxy url
try_hard_to_connect: function(url,options){
options = options || {};
- var port = ':' + (options.port || '8069');
+ var protocol = window.location.protocol;
+ var port = ( !options.port && protocol == "https:") ? ':443' : ':' + (options.port || '8069');
this.set_connection_status('connecting');
if(url.indexOf('//') < 0){
- url = 'http://'+url;
+ url = protocol + '//' + url;
}
if(url.indexOf(':',5) < 0){
- url = url+port;
+ url = url + port;
}
// try real hard to connect to url, with a 1sec timeout and up to 'retries' retries
@@ -273,11 +278,11 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
.done(function(){
done.resolve(url);
})
- .fail(function(){
+ .fail(function(resp){
if(retries > 0){
try_real_hard_to_connect(url,retries-1,done);
}else{
- done.reject();
+ done.reject(resp.statusText, url);
}
});
return done;
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 914284459c5..396ad060b36 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -127,13 +127,21 @@ exports.PosModel = Backbone.Model.extend({
progress: function(prog){
self.chrome.loading_progress(prog);
},
- }).then(function(){
- if(self.config.iface_scan_via_proxy){
- self.barcode_reader.connect_to_proxy();
- }
- }).always(function(){
- done.resolve();
- });
+ }).then(
+ function(){
+ if(self.config.iface_scan_via_proxy){
+ self.barcode_reader.connect_to_proxy();
+ }
+ done.resolve();
+ },
+ function(statusText, url){
+ if (statusText == 'error' && window.location.protocol == 'https:') {
+ var error = {message: 'TLSError', url: url};
+ self.chrome.loading_error(error);
+ } else {
+ done.resolve();
+ }
+ });
return done;
},
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index 435c0cbe62c..15fca79fc3d 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -1289,6 +1289,9 @@ var ClientListScreenWidget = ScreenWidget.extend({
} else {
fields.property_product_pricelist = false;
}
+ var contents = this.$(".client-details-contents");
+ contents.off("click", ".button.save");
+
rpc.query({
model: 'res.partner',
@@ -1308,6 +1311,7 @@ var ClientListScreenWidget = ScreenWidget.extend({
'title': _t('Error: Could not Save Changes'),
'body': error_body,
});
+ contents.on('click','.button.save',function(){ self.save_client_details(partner); });
});
},
@@ -1325,6 +1329,8 @@ var ClientListScreenWidget = ScreenWidget.extend({
// has created, and reload_partner() must have loaded the newly created partner.
self.display_client_details('hide');
}
+ }).always(function(){
+ $(".client-details-contents").on('click','.button.save',function(){ self.save_client_details(partner); });
});
},
diff --git a/addons/pos_restaurant/static/src/js/floors.js b/addons/pos_restaurant/static/src/js/floors.js
index 4ad760b8d0b..213b0afef09 100644
--- a/addons/pos_restaurant/static/src/js/floors.js
+++ b/addons/pos_restaurant/static/src/js/floors.js
@@ -259,7 +259,8 @@ var TableWidget = PosBaseWidget.extend({
args: [[['id', '=', table_id]], fields],
limit: 1,
})
- .then(function (table){
+ .then(function (result){
+ var table = result[0];
for (var field in table) {
self.table[field] = table[field];
}
diff --git a/addons/purchase_requisition/models/purchase_requisition.py b/addons/purchase_requisition/models/purchase_requisition.py
index 16391b9d373..914732c58aa 100644
--- a/addons/purchase_requisition/models/purchase_requisition.py
+++ b/addons/purchase_requisition/models/purchase_requisition.py
@@ -44,7 +44,7 @@ class PurchaseRequisition(models.Model):
_order = "id desc"
def _get_picking_in(self):
- pick_in = self.env.ref('stock.picking_type_in')
+ pick_in = self.env.ref('stock.picking_type_in', raise_if_not_found=False)
company = self.env['res.company']._company_default_get('purchase.requisition')
if not pick_in or pick_in.sudo().warehouse_id.company_id.id != company.id:
pick_in = self.env['stock.picking.type'].search(
diff --git a/addons/purchase_stock/models/account_invoice.py b/addons/purchase_stock/models/account_invoice.py
index 87431c00ef7..54c9bdc19fb 100644
--- a/addons/purchase_stock/models/account_invoice.py
+++ b/addons/purchase_stock/models/account_invoice.py
@@ -120,7 +120,7 @@ class AccountInvoice(models.Model):
# valuation difference, it means this difference is due to exchange rates,
# so we don't create anything, the exchange rate entries will
# be processed automatically by the rest of the code.
- diff_res.append({
+ diff_line = {
'type': 'src',
'name': i_line.name[:64],
'price_unit': inv.currency_id.round(price_unit_val_dif),
@@ -131,7 +131,11 @@ class AccountInvoice(models.Model):
'uom_id': line['uom_id'],
'account_analytic_id': line['account_analytic_id'],
'tax_ids': tax_ids,
- })
+ }
+ # We update the original line accordingly
+ line['price_unit'] = inv.currency_id.round(line['price_unit'] - diff_line['price_unit'])
+ line['price'] = inv.currency_id.round(line['quantity'] * line['price_unit'])
+ diff_res.append(diff_line)
return diff_res
return []
diff --git a/addons/purchase_stock/tests/test_anglo_saxon_valuation_reconciliation.py b/addons/purchase_stock/tests/test_anglo_saxon_valuation_reconciliation.py
index 089d0e6cd8f..5b466f75078 100644
--- a/addons/purchase_stock/tests/test_anglo_saxon_valuation_reconciliation.py
+++ b/addons/purchase_stock/tests/test_anglo_saxon_valuation_reconciliation.py
@@ -25,7 +25,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
})
self.test_product_category.property_account_creditor_price_difference_categ = self.price_dif_account.id
- def _create_purchase(self, product, quantity=1.0):
+ def _create_purchase(self, product, date, quantity=1.0):
rslt = self.env['purchase.order'].create({
'partner_id': self.test_partner.id,
'currency_id': self.currency_two.id,
@@ -36,14 +36,14 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
'product_qty': quantity,
'product_uom': product.uom_po_id.id,
'price_unit': self.product_price_unit,
- 'date_planned': '2017-10-07',
+ 'date_planned': date,
})],
- 'date_order': '2017-10-01',
+ 'date_order': date,
})
rslt.button_confirm()
return rslt
- def _create_invoice_for_po(self, purchase_order):
+ def _create_invoice_for_po(self, purchase_order, date):
account_receivable = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1)
rslt = self.env['account.invoice'].create({
'purchase_id': purchase_order.id,
@@ -51,8 +51,8 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
'currency_id': self.currency_two.id,
'name': 'vendor bill',
'type': 'in_invoice',
- 'date_invoice': '2017-12-31',
- 'date': '2017-12-31',
+ 'date_invoice': date,
+ 'date': date,
'account_id': account_receivable.id,
})
rslt.purchase_order_change()
@@ -62,11 +62,16 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
""" Tests the case into which we receive the goods first, and then make the invoice.
"""
test_product = self.test_product_delivery
- purchase_order = self._create_purchase(test_product)
+ purchase_order = self._create_purchase(test_product, '2018-01-01')
self._process_pickings(purchase_order.picking_ids)
- invoice = self._create_invoice_for_po(purchase_order)
- self.currency_rate.rate = 7.76435463
+ invoice = self._create_invoice_for_po(purchase_order, '2018-02-02')
+ self.env['res.currency.rate'].create({
+ 'currency_id': self.currency_one.id,
+ 'company_id': self.company.id,
+ 'rate': 7.76435463,
+ 'name': '2018-02-01',
+ })
invoice.action_invoice_open()
picking = self.env['stock.picking'].search([('purchase_id','=',purchase_order.id)])
self.check_reconciliation(invoice, picking)
@@ -77,29 +82,16 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
def test_invoice_shipment(self):
""" Tests the case into which we make the invoice first, and then receive the goods.
"""
- # First, we delete the currency rate defined in demo data for USD on June 6th.
- # This is mandatory to ensure consistency of the data generated by the test,
- # as stock pickings created from a PO are by design always created for
- # the current date (there is no way forcing this), meaning that they
- # always use today's exchange rate for their valuation.
- # Despite the fact we rewrite the date of the valuation moves artificially,
- # we cannot correct the debit and credit values, since the anglosaxon
- # entries get automatically reconciled (and you cannot modify a reconciled entry).
- # So, we have to make sure that "today"'s rate will always be the last rate we
- # created in order to ensure complete control of the test.
- self.env.ref('base.rateUSDbis').unlink()
-
-
# Create a PO and an invoice for it
test_product = self.test_product_order
- purchase_order = self._create_purchase(test_product)
+ purchase_order = self._create_purchase(test_product, '2017-12-01')
- invoice = self._create_invoice_for_po(purchase_order)
+ invoice = self._create_invoice_for_po(purchase_order, '2017-12-23')
invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice.id)])
invoice_line.quantity = 1
# The currency rate changes
- self.currency_rate = self.env['res.currency.rate'].create({
+ self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 13.834739702,
@@ -113,7 +105,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
self.check_reconciliation(invoice, picking)
# The currency rate changes again
- self.currency_rate = self.env['res.currency.rate'].create({
+ self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 10.54739702,
@@ -132,7 +124,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
self._change_pickings_date(return_pick, '2018-01-13')
# The currency rate changes again
- self.currency_rate = self.env['res.currency.rate'].create({
+ self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 9.56564564,
@@ -157,25 +149,40 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
""" Tests the case into which we receive part of the goods first, then 2 invoices at different rates, and finally the remaining quantities
"""
test_product = self.test_product_delivery
- purchase_order = self._create_purchase(test_product, quantity=5.0)
+ purchase_order = self._create_purchase(test_product, '2017-01-01', quantity=5.0)
self._process_pickings(purchase_order.picking_ids, quantity=2.0)
picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)], order="id asc", limit=1)
- invoice = self._create_invoice_for_po(purchase_order)
+ invoice = self._create_invoice_for_po(purchase_order, '2017-01-15')
invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice.id)])
invoice_line.quantity = 3
- self.currency_rate.rate = 7.76435463
+ self.env['res.currency.rate'].create({
+ 'currency_id': self.currency_one.id,
+ 'company_id': self.company.id,
+ 'rate': 7.76435463,
+ 'name': '2017-02-01',
+ })
invoice.action_invoice_open()
self.check_reconciliation(invoice, picking, full_reconcile=False)
- invoice2 = self._create_invoice_for_po(purchase_order)
+ invoice2 = self._create_invoice_for_po(purchase_order, '2017-02-15')
invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice2.id)])
invoice_line.quantity = 2
- self.currency_rate.rate = 13.834739702
+ self.env['res.currency.rate'].create({
+ 'currency_id': self.currency_one.id,
+ 'company_id': self.company.id,
+ 'rate': 13.834739702,
+ 'name': '2017-03-01',
+ })
invoice2.action_invoice_open()
self.check_reconciliation(invoice2, picking, full_reconcile=False)
- self.currency_rate.rate = 12.195747002
+ self.env['res.currency.rate'].create({
+ 'currency_id': self.currency_one.id,
+ 'company_id': self.company.id,
+ 'rate': 12.195747002,
+ 'name': '2017-04-01',
+ })
self._process_pickings(purchase_order.picking_ids.filtered(lambda x: x.state != 'done'), quantity=3.0)
picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)], order='id desc', limit=1)
self.check_reconciliation(invoice2, picking)
diff --git a/addons/purchase_stock/tests/test_replenish_wizard.py b/addons/purchase_stock/tests/test_replenish_wizard.py
index 868b4c9585c..08d4d78310f 100644
--- a/addons/purchase_stock/tests/test_replenish_wizard.py
+++ b/addons/purchase_stock/tests/test_replenish_wizard.py
@@ -1,13 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
from odoo.addons.stock.tests.common import TestStockCommon
class TestReplenishWizard(TestStockCommon):
def setUp(self):
super(TestReplenishWizard, self).setUp()
- self.vendor = self.env['res.partner'].search([('supplier', '=', True)], limit=1)
+ self.vendor = self.env['res.partner'].create(dict(name='The Replenisher', supplier=True))
self.product1_price = 500
# Create a supplier info witch the previous vendor
diff --git a/addons/purchase_stock/tests/test_stockvaluation.py b/addons/purchase_stock/tests/test_stockvaluation.py
index 84ac5aedb37..336cb7ebd7a 100644
--- a/addons/purchase_stock/tests/test_stockvaluation.py
+++ b/addons/purchase_stock/tests/test_stockvaluation.py
@@ -5,6 +5,7 @@ import time
from datetime import datetime
from odoo import fields
+from odoo.tests import Form
from odoo.tests.common import TransactionCase, tagged
from odoo.addons.account.tests.account_test_classes import AccountingTestCase
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
@@ -417,3 +418,52 @@ class TestStockValuationWithCOA(AccountingTestCase):
# check the anglo saxon entries
price_diff_entry = self.env['account.move.line'].search([('account_id', '=', price_diff_account.id)])
self.assertEqual(price_diff_entry.credit, 100)
+
+ def test_anglosaxon_valuation(self):
+ self.env.user.company_id.anglo_saxon_accounting = True
+ self.product1.product_tmpl_id.cost_method = 'fifo'
+ self.product1.product_tmpl_id.valuation = 'real_time'
+ self.product1.product_tmpl_id.invoice_policy = 'delivery'
+ price_diff_account = self.env['account.account'].create({
+ 'name': 'price diff account',
+ 'code': 'price diff account',
+ 'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
+ })
+ self.product1.property_account_creditor_price_difference = price_diff_account
+
+ # Create PO
+ po_form = Form(self.env['purchase.order'])
+ po_form.partner_id = self.partner_id
+ with po_form.order_line.new() as po_line:
+ po_line.product_id = self.product1
+ po_line.product_qty = 1
+ po_line.price_unit = 10.0
+ order = po_form.save()
+ order.button_confirm()
+
+ # Receive the goods
+ receipt = order.picking_ids[0]
+ receipt.move_lines.quantity_done = 1
+ receipt.button_validate()
+
+ # Create an invoice with a different price
+ invoice = self.env['account.invoice'].create({
+ 'partner_id': order.partner_id.id,
+ 'purchase_id': order.id,
+ 'account_id': order.partner_id.property_account_payable_id.id,
+ 'type': 'in_invoice',
+ })
+ invoice.purchase_order_change()
+ invoice.invoice_line_ids[0].price_unit = 15.0
+ invoice.action_invoice_open()
+
+ # Check what was posted in the price difference account
+ price_diff_aml = self.env['account.move.line'].search([('account_id','=',price_diff_account.id)])
+ self.assertEquals(len(price_diff_aml), 1, "Only one line should have been generated in the price difference account.")
+ self.assertAlmostEquals(price_diff_aml.debit, 5, "Price difference should be equal to 5 (15-10)")
+
+ # Check what was posted in stock input account
+ input_aml = self.env['account.move.line'].search([('account_id','=',self.stock_input_account.id)])
+ self.assertEquals(len(input_aml), 2, "Only two lines should have been generated in stock input account: one when receiving the product, one when making the invoice.")
+ self.assertAlmostEquals(sum(input_aml.mapped('debit')), 10, "Total debit value on stock input account should be equal to the original PO price of the product.")
+ self.assertAlmostEquals(sum(input_aml.mapped('credit')), 10, "Total credit value on stock input account should be equal to the original PO price of the product.")
diff --git a/addons/sale/static/src/js/sale.js b/addons/sale/static/src/js/sale.js
index 1121a38501a..374508f721b 100644
--- a/addons/sale/static/src/js/sale.js
+++ b/addons/sale/static/src/js/sale.js
@@ -39,6 +39,8 @@ KanbanRecord.include({
.done(function() {
self.trigger_up('kanban_record_update', {id: self.id});
});
+ // TODO: previous lines can be refactored as follows (in master)
+ // self.trigger_up('kanban_record_update', {invoiced_target: value});
}
});
},
diff --git a/addons/sale/static/tests/sales_team_dashboard_tests.js b/addons/sale/static/tests/sales_team_dashboard_tests.js
new file mode 100644
index 00000000000..b1043dd64e1
--- /dev/null
+++ b/addons/sale/static/tests/sales_team_dashboard_tests.js
@@ -0,0 +1,70 @@
+odoo.define('sale.dashboard_tests', function (require) {
+"use strict";
+
+var KanbanView = require('web.KanbanView');
+var testUtils = require('web.test_utils');
+
+var createView = testUtils.createView;
+
+QUnit.module('Sales Team Dashboard', {
+ beforeEach: function () {
+ this.data = {
+ 'crm.team': {
+ fields: {
+ foo: {string: "Foo", type: 'char'},
+ invoiced_target: {string: "Invoiced_target", type: 'integer'},
+ },
+ records: [
+ {id: 1, foo: "yop"},
+ ],
+ },
+ };
+ }
+});
+
+QUnit.test('edit target with several o_kanban_primary_bottom divs', function (assert) {
+ assert.expect(4);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'crm.team',
+ data: this.data,
+ arch: '' +
+ '' +
+ '' +
+ '
diff --git a/addons/stock_account/tests/test_anglo_saxon_valuation_reconciliation_common.py b/addons/stock_account/tests/test_anglo_saxon_valuation_reconciliation_common.py
index b2e3bd0d095..cb254a392ab 100644
--- a/addons/stock_account/tests/test_anglo_saxon_valuation_reconciliation_common.py
+++ b/addons/stock_account/tests/test_anglo_saxon_valuation_reconciliation_common.py
@@ -58,12 +58,6 @@ class ValuationReconciliationTestCase(AccountingTestCase):
self.currency_one = self.company.currency_id
currency_two_name = 'USD' if self.currency_one.name != 'USD' else 'EUR'
self.currency_two = self.env['res.currency'].search([('name', '=', currency_two_name)])
- self.currency_rate = self.env['res.currency.rate'].create({
- 'currency_id': self.currency_one.id,
- 'company_id': self.company.id,
- 'rate': 1.234343354, # Totally arbitratry value
- 'name': '2017-01-01',
- })
self.input_account = self.env['account.account'].create({
'name': 'Test stock in',
@@ -127,3 +121,15 @@ class ValuationReconciliationTestCase(AccountingTestCase):
})
self.product_price_unit = 66.0
+
+ # We delete the currency rate defined in demo data for USD on June 6th.
+ # This is mandatory to ensure consistency of the data generated by the test,
+ # as stock pickings created from a PO are by design always created for
+ # the current date (there is no way forcing this), meaning that they
+ # always use today's exchange rate for their valuation.
+ # Despite the fact we rewrite the date of the valuation moves artificially,
+ # we cannot correct the debit and credit values, since the anglosaxon
+ # entries get automatically reconciled (and you cannot modify a reconciled entry).
+ # So, we have to make sure that "today"'s rate will always be the last rate we
+ # created in order to ensure complete control of the test.
+ self.env.ref('base.rateUSDbis').unlink()
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index de879018ad5..1e930ccf783 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -2188,7 +2188,7 @@ var BasicModel = AbstractModel.extend({
* @param {Object} [options]
* @param {string[]} [options.fieldNames] the list of fields to fetch. If
* not given, fetch all the fields in record.fieldNames (+ display_name)
- * @param {string} [optinos.viewType] the type of view for which the record
+ * @param {string} [options.viewType] the type of view for which the record
* is fetched (usefull to load the adequate fields), by defaults, uses
* record.viewType
* @returns {Deferred