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 fiducie 4661 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ẩm 2118 @@ -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ên 3348 - + 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ếu 34311 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: '' + + '' + + '' + + '
' + + '' + + 'Click to define a target' + + '
' + + '
' + + '
' + + '' + + '' + + '', + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.strictEqual(args.args[1].invoiced_target, 123, + "new value is correctly saved"); + } + return this._super.apply(this, arguments); + }, + + }); + + assert.strictEqual(kanban.$('.o_kanban_view .sales_team_target_definition').length, 1, + "should have classname 'sales_team_target_definition'"); + assert.strictEqual(kanban.$('.o_kanban_primary_bottom').length, 2, + "should have two divs with classname 'o_kanban_primary_bottom'"); + + kanban.$('a.sales_team_target_definition').click(); + assert.strictEqual(kanban.$('.o_kanban_primary_bottom:last input').length, 1, + "should have rendered an input in the last o_kanban_primary_bottom div"); + + kanban.$('.o_kanban_primary_bottom:last input').focus(); + kanban.$('.o_kanban_primary_bottom:last input').val('123'); + kanban.$('.o_kanban_primary_bottom:last input').blur(); + + kanban.destroy(); +}); + +}); diff --git a/addons/sale/views/assets.xml b/addons/sale/views/assets.xml index c441839d102..458b884f9d3 100644 --- a/addons/sale/views/assets.xml +++ b/addons/sale/views/assets.xml @@ -28,4 +28,10 @@ + + diff --git a/addons/sale_stock/tests/test_anglo_saxon_valuation_reconciliation.py b/addons/sale_stock/tests/test_anglo_saxon_valuation_reconciliation.py index a89a64423f0..f80c4bf758c 100644 --- a/addons/sale_stock/tests/test_anglo_saxon_valuation_reconciliation.py +++ b/addons/sale_stock/tests/test_anglo_saxon_valuation_reconciliation.py @@ -30,7 +30,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): #set the invoice_policy to delivery to have an accurate COGS entry self.test_product_delivery.invoice_policy = "delivery" - def _create_sale(self, product, quantity=1.0): + def _create_sale(self, product, date, quantity=1.0): rslt = self.env['sale.order'].create({ 'partner_id': self.test_partner.id, 'currency_id': self.currency_two.id, @@ -42,17 +42,18 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): 'product_uom': product.uom_po_id.id, 'price_unit': self.product_price_unit, })], + 'date_order': date, }) rslt.action_confirm() return rslt - def _create_invoice_for_so(self, sale_order, product): + def _create_invoice_for_so(self, sale_order, product, date): rslt = self.env['account.invoice'].create({ 'partner_id': self.test_partner.id, 'currency_id': self.currency_two.id, 'name': 'customer invoice', 'type': 'out_invoice', - 'date_invoice': time.strftime('%Y') + '-12-22', + 'date_invoice': date, 'account_id': self.account_receivable.id, 'invoice_line_ids': [(0, 0, { 'name': 'test line', @@ -92,11 +93,16 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): test_product = self.test_product_delivery self._set_initial_stock_for_product(test_product) - sale_order = self._create_sale(test_product) + sale_order = self._create_sale(test_product, '2108-01-01') self._process_pickings(sale_order.picking_ids) - invoice = self._create_invoice_for_so(sale_order, test_product) - self.currency_rate.rate = 9.87366352 + invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-12') + self.env['res.currency.rate'].create({ + 'currency_id': self.currency_one.id, + 'company_id': self.company.id, + 'rate': 9.87366352, + 'name': '2018-02-01', + }) invoice.action_invoice_open() picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)]) self.check_reconciliation(invoice, picking, operation='sale') @@ -110,10 +116,15 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): self.test_product_delivery.standard_price = 13 self._set_initial_stock_for_product(test_product) - sale_order = self._create_sale(test_product) + sale_order = self._create_sale(test_product, '2018-01-01') - invoice = self._create_invoice_for_so(sale_order, test_product) - self.currency_rate.rate = 0.974784 + invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-03') + self.env['res.currency.rate'].create({ + 'currency_id': self.currency_one.id, + 'company_id': self.company.id, + 'rate': 0.974784, + 'name': '2018-02-01', + }) invoice.action_invoice_open() self._process_pickings(sale_order.picking_ids) @@ -122,7 +133,12 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): self.check_reconciliation(invoice, picking, operation='sale') #return the goods and refund the invoice - self.currency_rate.rate = 10.54739702 + self.env['res.currency.rate'].create({ + 'currency_id': self.currency_one.id, + 'company_id': self.company.id, + 'rate': 10.54739702, + 'name': '2018-03-01', + }) stock_return_picking = self.env['stock.return.picking']\ .with_context(active_ids=[picking.id], active_id=picking.id).create({}) stock_return_picking.product_return_moves.quantity = 1.0 @@ -131,7 +147,12 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): return_pick.action_assign() return_pick.move_lines.quantity_done = 1 return_pick.action_done() - self.currency_rate.rate = 9.56564564 + self.env['res.currency.rate'].create({ + 'currency_id': self.currency_one.id, + 'company_id': self.company.id, + 'rate': 9.56564564, + 'name': '2018-04-01', + }) refund_invoice_wiz = self.env['account.invoice.refund'].with_context(active_ids=[invoice.id]).create({ 'description': 'test_invoice_shipment_refund', 'filter_refund': 'cancel', @@ -147,26 +168,41 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): test_product = self.test_product_delivery self._set_initial_stock_for_product(test_product) - sale_order = self._create_sale(test_product, quantity=5) + sale_order = self._create_sale(test_product, '2018-01-01', quantity=5) self._process_pickings(sale_order.picking_ids, quantity=2.0) picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)], order="id asc", limit=1) - invoice = self._create_invoice_for_so(sale_order, test_product) + invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-03') 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': '2018-02-01', + }) invoice.action_invoice_open() self.check_reconciliation(invoice, picking, full_reconcile=False, operation='sale') - invoice2 = self._create_invoice_for_so(sale_order, test_product) + invoice2 = self._create_invoice_for_so(sale_order, test_product, '2018-03-12') 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': '2018-03-01', + }) invoice2.action_invoice_open() self.check_reconciliation(invoice2, picking, full_reconcile=False, operation='sale') - 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': '2018-04-01', + }) self._process_pickings(sale_order.picking_ids.filtered(lambda x: x.state != 'done'), quantity=3.0) picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)], order='id desc', limit=1) self.check_reconciliation(invoice2, picking, operation='sale') diff --git a/addons/stock/models/stock_rule.py b/addons/stock/models/stock_rule.py index 2ffaf454b57..1ccda04f406 100644 --- a/addons/stock/models/stock_rule.py +++ b/addons/stock/models/stock_rule.py @@ -361,8 +361,10 @@ class ProcurementGroup(models.Model): self.sudo()._procure_orderpoint_confirm(use_new_cursor=use_new_cursor, company_id=company_id) # Search all confirmed stock_moves and try to assign them - confirmed_moves = self.env['stock.move'].search([('state', '=', 'confirmed'), ('product_uom_qty', '!=', 0.0)], limit=None, order='priority desc, date_expected asc') - for moves_chunk in split_every(100, confirmed_moves.ids): + moves_to_assign = self.env['stock.move'].search([ + ('state', 'in', ['confirmed', 'partially_available']), ('product_uom_qty', '!=', 0.0) + ], limit=None, order='priority desc, date_expected asc') + for moves_chunk in split_every(100, moves_to_assign.ids): self.env['stock.move'].browse(moves_chunk)._action_assign() if use_new_cursor: self._cr.commit() diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml index f98e9507650..f527cbbf01e 100644 --- a/addons/stock/views/stock_picking_views.xml +++ b/addons/stock/views/stock_picking_views.xml @@ -332,9 +332,11 @@ - + + +
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} resolves to the record or is rejected in @@ -3785,11 +3785,12 @@ var BasicModel = AbstractModel.extend({ */ _postprocess: function (record, options) { var self = this; + var viewType = options && options.viewType || record.viewType; var defs = []; _.each(record.getFieldNames(options), function (name) { var field = record.fields[name]; - var fieldInfo = record.fieldsInfo[record.viewType][name] || {}; + var fieldInfo = record.fieldsInfo[viewType][name] || {}; var options = fieldInfo.options || {}; if (options.always_reload) { if (record.fields[name].type === 'many2one' && record.data[name]) { @@ -3798,7 +3799,7 @@ var BasicModel = AbstractModel.extend({ model: field.relation, method: 'name_get', args: [element.data.id], - context: self._getContext(record, {fieldName: name}), + context: self._getContext(record, {fieldName: name, viewType: viewType}), }) .then(function (result) { element.data.display_name = result[0][1]; diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js index 9e1cde2aada..eb3efe7001d 100644 --- a/addons/web/static/src/js/views/basic/basic_view.js +++ b/addons/web/static/src/js/views/basic/basic_view.js @@ -150,6 +150,14 @@ var BasicView = AbstractView.extend({ } } } + // Many2one: context is not the same between the different views + // this means the result of a name_get could differ + if (fieldType === 'many2one') { + if (JSON.stringify(record.data[name].context) !== + JSON.stringify(fieldInfo.context)) { + fieldNames.push(name); + } + } } }); diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js index e195b33068c..6ffbad9612a 100644 --- a/addons/web/static/src/js/views/kanban/kanban_controller.js +++ b/addons/web/static/src/js/views/kanban/kanban_controller.js @@ -273,7 +273,7 @@ var KanbanController = BasicController.extend({ _onButtonNew: function () { var state = this.model.get(this.handle, {raw: true}); var quickCreateEnabled = this.quickCreateEnabled && viewUtils.isQuickCreateEnabled(state); - if (this.on_create === 'quick_create' && quickCreateEnabled) { + if (this.on_create === 'quick_create' && quickCreateEnabled && state.data.length) { // Activate the quick create in the first column this.renderer.addQuickCreate(); } else if (this.on_create && this.on_create !== 'quick_create') { diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 81c21cf6052..70effe45e72 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -2332,7 +2332,7 @@ QUnit.module('basic_fields', { var evt = document.createEvent("MouseEvents"); //taken ref from https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/initMouseEvent evt.initMouseEvent("mouseover", true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null); $('.discreteBar')[0].dispatchEvent(evt); - var tooltip = $('.nvtooltip').find('table').find('.key')[0].innerText; + var tooltip = $('.nvtooltip').find('table').find('.key')[0].innerText.trim(); assert.equal(tooltip, graph_key, "graph tooltip should be generated "); $('.nvtooltip').remove(); diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index e1e91863448..7a96ede57ee 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -380,6 +380,93 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('show_address works in a view embedded in a view of another type', function (assert) { + assert.expect(1); + + this.data.turtle.records[1].turtle_trululu = 2; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '', + res_id: 1, + archs: { + "turtle,false,form": '
' + + '' + + '' + + '', + "turtle,false,list": '' + + '' + + '', + }, + mockRPC: function (route, args) { + if (args.method === 'name_get') { + return this._super(route, args).then(function (result) { + if (args.model === 'partner' && args.kwargs.context.show_address) { + result[0][1] += '\nrue morgue\nparis 75013'; + } + return result; + }); + } + return this._super(route, args); + }, + }); + // click the turtle field, opens a modal with the turtle form view + form.$('.o_data_row:first td.o_data_cell').click(); + + assert.strictEqual($('[name="turtle_trululu"]').text(), "second recordrue morgueparis 75013", + "The partner's address should be displayed"); + form.destroy(); + }); + + QUnit.test('many2one data is reloaded if there is a context to take into account', function (assert) { + assert.expect(1); + + this.data.turtle.records[1].turtle_trululu = 2; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '', + res_id: 1, + archs: { + "turtle,false,form": '
' + + '' + + '' + + '', + "turtle,false,list": '' + + '' + + '' + + '', + }, + mockRPC: function (route, args) { + if (args.method === 'name_get') { + return this._super(route, args).then(function (result) { + if (args.model === 'partner' && args.kwargs.context.show_address) { + result[0][1] += '\nrue morgue\nparis 75013'; + } + return result; + }); + } + return this._super(route, args); + }, + }); + // click the turtle field, opens a modal with the turtle form view + form.$('.o_data_row:first').click(); + + assert.strictEqual($('.modal [name=turtle_trululu]').text(), "second recordrue morgueparis 75013", + "The partner's address should be displayed"); + form.destroy(); + }); + QUnit.test('many2ones in form views with search more', function (assert) { assert.expect(3); this.data.partner.records.push({ diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index 17c0c022e85..ba8113e7889 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -4737,6 +4737,30 @@ QUnit.module('Views', { kanban.destroy(); }); + QUnit.test('quick_create on grouped kanban without column', function (assert) { + assert.expect(1); + this.data.partner.records = []; + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '' + + '
' + + '' + + '
' + + '
', + groupBy: ['product_id'], + + intercepts: { + switch_view: function (event) { + assert.ok(true, "switch_view was called instead of quick_create"); + }, + }, + }); + kanban.$buttons.find('.o-kanban-button-new').click(); + kanban.destroy(); + }); + QUnit.test('keyboard navigation on kanban basic rendering', function (assert) { assert.expect(3); diff --git a/addons/web_editor/static/src/js/editor/rte.js b/addons/web_editor/static/src/js/editor/rte.js index bf16cc090e5..5725bc4eb0f 100644 --- a/addons/web_editor/static/src/js/editor/rte.js +++ b/addons/web_editor/static/src/js/editor/rte.js @@ -611,6 +611,14 @@ var RTEWidget = Widget.extend({ return; } + // Removes strange _moz_abspos attribute when it appears. Cannot + // find another solution which works in all cases. A grabber still + // appears at the same time which I did not manage to remove. + // TODO find a complete and better solution + _.defer(function () { + $editable.find('[_moz_abspos]').removeAttr('_moz_abspos'); + }); + if ($target.is('a')) { /** * Remove content editable everywhere and add it on the link only so that characters can be added diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js index 5df08ee801d..97d3777b95f 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -280,15 +280,36 @@ var SnippetOption = Widget.extend({ */ _setActive: function () { var self = this; - this.$el.find('[data-toggle-class], [data-select-class]') - .addBack('[data-toggle-class], [data-select-class]') + this.$el.find('[data-toggle-class]') + .addBack('[data-toggle-class]') .removeClass('active') .filter(function () { - var $elem = $(this); - var className = $elem.data('toggleClass') || $elem.data('selectClass'); - return self.$target.hasClass(className); + var className = $(this).data('toggleClass'); + return !className || self.$target.hasClass(className); }) .addClass('active'); + + _processSelectClassElements(this.$el); + _.each(this.$el.find('.dropdown-menu'), function (group) { + _processSelectClassElements($(group).children()); + }); + + function _processSelectClassElements($elements) { + var maxNbClasses = -1; + $elements.filter('[data-select-class]') + .removeClass('active') + .filter(function () { + var className = $(this).data('selectClass'); + var nbClasses = className ? className.split(' ').length : 0; + if (nbClasses >= maxNbClasses && (!className || self.$target.hasClass(className))) { + maxNbClasses = nbClasses; + return true; + } + return false; + }) + .last() + .addClass('active'); + } }, //-------------------------------------------------------------------------- @@ -749,7 +770,7 @@ registry.colorpicker = SnippetOption.extend({ */ _onColorResetButtonClick: function () { this.$target.removeClass(this.classes).css('background-color', ''); - self.$target.trigger('content_changed'); + this.$target.trigger('content_changed'); this.$el.find('.colorpicker button.selected').removeClass('selected'); }, }); @@ -852,7 +873,9 @@ registry.background = SnippetOption.extend({ */ setTarget: function () { this._super.apply(this, arguments); + // TODO should be automatic for all options as equal to the start method this.bindBackgroundEvents(); + this.__customImageSrc = this._getSrcFromCssValue(); }, //-------------------------------------------------------------------------- diff --git a/addons/website/models/website.py b/addons/website/models/website.py index cec3ceeb246..c592ad32b57 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -111,6 +111,11 @@ class Website(models.Model): ('b2c', 'Free sign up'), ], string='Customer Account', default='b2b') + @api.onchange('language_ids') + def _onchange_language_ids(self): + if self.language_ids and self.default_lang_id not in self.language_ids: + self.default_lang_id = self.language_ids[0] + @api.multi def _compute_menu(self): Menu = self.env['website.menu'] diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js index f50b653ab2e..3d79951cf70 100644 --- a/addons/website/static/src/js/editor/snippets.options.js +++ b/addons/website/static/src/js/editor/snippets.options.js @@ -271,6 +271,7 @@ options.registry.carousel = options.Class.extend({ * @override */ _setActive: function () { + this._super.apply(this, arguments); this.$el.find('[data-interval]').removeClass('active') .filter('[data-interval=' + this.$target.attr('data-interval') + ']').addClass('active'); }, diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 79b4618b09b..df17f5636ea 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -392,11 +392,16 @@ class WebsiteSale(ProductConfiguratorController): @http.route(['/shop/cart/update'], type='http', auth="public", methods=['POST'], website=True, csrf=False) def cart_update(self, product_id, add_qty=1, set_qty=0, **kw): + sale_order = request.website.sale_get_order(force_create=1) + if sale_order.state != 'draft': + request.session['sale_order_id'] = None + sale_order = request.website.sale_get_order(force_create=True) + product_custom_attribute_values = None if kw.get('product_custom_attribute_values'): product_custom_attribute_values = json.loads(kw.get('product_custom_attribute_values')) - request.website.sale_get_order(force_create=1)._cart_update( + sale_order._cart_update( product_id=int(product_id), add_qty=add_qty, set_qty=set_qty, @@ -569,6 +574,7 @@ class WebsiteSale(ProductConfiguratorController): new_values['customer'] = True new_values['team_id'] = request.website.salesteam_id and request.website.salesteam_id.id + new_values['user_id'] = request.website.salesperson_id and request.website.salesperson_id.id new_values['website_id'] = request.website.id lang = request.lang if request.lang in request.website.mapped('language_ids.code') else None diff --git a/doc/_extensions/autojsdoc/parser/types.py b/doc/_extensions/autojsdoc/parser/types.py index 8737b26ecc9..69836f46ac4 100644 --- a/doc/_extensions/autojsdoc/parser/types.py +++ b/doc/_extensions/autojsdoc/parser/types.py @@ -44,6 +44,8 @@ def tokenize(typespec): yield (LITERAL, ast.literal_eval(string)) elif toktype == token.ENDMARKER: return + elif toktype == token.NEWLINE: + pass else: raise ValueError("Unknown typespec token %s" % token.tok_name[toktype]) diff --git a/doc/_extensions/odoo_ext/translator.py b/doc/_extensions/odoo_ext/translator.py index cf6c149d063..491f02a2ad3 100644 --- a/doc/_extensions/odoo_ext/translator.py +++ b/doc/_extensions/odoo_ext/translator.py @@ -220,7 +220,7 @@ class BootstrapTranslator(nodes.NodeVisitor, object): else: opts = {} - def warner(msg): + def warner(msg, **kw): self.builder.warn(msg, (self.builder.current_docname, node.line)) highlighted = self.builder.highlighter.highlight_block( node.rawsource, lang, opts=opts, warn=warner, linenos=linenos, diff --git a/doc/cla/corporate/brahoo.md b/doc/cla/corporate/brahoo.md new file mode 100644 index 00000000000..04e4d39cb31 --- /dev/null +++ b/doc/cla/corporate/brahoo.md @@ -0,0 +1,15 @@ +The Netherlands, 2018-10-24 + +Brahoo B.V. agrees to the terms of the Odoo Corporate Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Cas Vissers c.vissers@brahoo.nl https://github.com/CasVissers + +List of contributors: + +Cas Vissers c.vissers@brahoo.nl https://github.com/CasVissers \ No newline at end of file diff --git a/doc/cla/individual/anilkesariya.md b/doc/cla/individual/anilkesariya.md new file mode 100644 index 00000000000..2b4158360eb --- /dev/null +++ b/doc/cla/individual/anilkesariya.md @@ -0,0 +1,12 @@ +India, 17, Sept - 2018 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Anil Kesariya anil.r.kesariya@gmail.com https://github.com/AnilKesariya + diff --git a/doc/cla/individual/ark74.md b/doc/cla/individual/ark74.md new file mode 100644 index 00000000000..e729859d95f --- /dev/null +++ b/doc/cla/individual/ark74.md @@ -0,0 +1,11 @@ +Mexico, 2018-10-24 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Luis Guzman ark@switnet.org https://github.com/ark74 diff --git a/doc/cla/individual/mhamra.md b/doc/cla/individual/mhamra.md new file mode 100644 index 00000000000..260fee70284 --- /dev/null +++ b/doc/cla/individual/mhamra.md @@ -0,0 +1,11 @@ +Argentina, 2018-10-18 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Marcelo Ariel Hamra marcelo.hamra@gmail.com https://github.com/mhamra diff --git a/doc/reference/orm.rst b/doc/reference/orm.rst index 73b96552c21..d2c8f812f74 100644 --- a/doc/reference/orm.rst +++ b/doc/reference/orm.rst @@ -1010,17 +1010,17 @@ them (e.g. to change their default sort order): .. literalinclude:: ../../odoo/addons/test_documentation_examples/extension.py :language: python - :lines: 5- + :lines: 7- .. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_extension.py :language: python - :lines: 8,13 + :lines: 10,15 will yield: .. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_extension.py :language: text - :lines: 11 + :lines: 13 .. note:: it will also yield the various :ref:`automatic fields ` unless they've been disabled diff --git a/doc/webservices/iap.rst b/doc/webservices/iap.rst index 23467c267c3..003fe2cf07f 100644 --- a/doc/webservices/iap.rst +++ b/doc/webservices/iap.rst @@ -81,8 +81,11 @@ Overview The Credits +.. note:: The credits went from integer to float value starting **October 2018** + Integer values are still supported. + Every service provided through the In-App platform can be used by the - clients with tokens or *credits*. The credits are an integer unit and + clients with tokens or *credits*. The credits are an float unit and their monetary value depends on the service and is decided by the provider. This could be: @@ -157,7 +160,8 @@ The first step is to register your service on the IAP endpoint (production and/or test) before you can actually query user accounts. To create a service, go to your *Portal Account* on the IAP endpoint (https://iap.odoo.com for production, https://iap-sandbox.odoo.com for testing, the endpoints are -*independent* and *not synchronized*). +*independent* and *not synchronized*). Alternatively, you can go to your portal +on Odoo (https://iap.odoo.com/my/home) and select *In-App Services*. .. note:: @@ -166,33 +170,45 @@ production, https://iap-sandbox.odoo.com for testing, the endpoints are on sandbox to ease the tests. Log in then go to :menuselection:`My Account --> Your In-App Services`, click -Create and provide the name of your service. +Create and provide the informations of your service. -The now created service has *two* important fields: +The service has *four* important fields: * :samp:`name` - :class:`ServiceName`: this will identify your service in the - client's :ref:`app ` communicates directly with IAP. -* :samp:`key` - :class:`ServiceKey`: the developer key that identifies you in - IAP (see :ref:`your service `) and allows to draw credits from - the client's account. - + client's :ref:`app ` communicates directly with IAP, choose it + carefully! .. warning:: The :class:`ServiceName` is unique and should usually match the name of your - Odoo App. + Odoo Client App. +* :samp:`Icon` - :class:`Icon`: A generic icon that will serve as default for your + :ref:`packages ` + +* :samp:`key` - :class:`ServiceKey`: the developer key that identifies you in + IAP (see :ref:`your service `) and allows to draw credits from + the client's account. It will be shown only once upon creation of the service + and can be regenerated at will. .. danger:: Your :class:`ServiceKey` *is a secret*, leaking your service key allows other application developers to draw credits bought for your service(s). -.. image:: images/service_select.png +* :samp:`privacy policy` - :class:`PrivacyPolicy`: This is an url to the privacy + policy of your service. This should explicitly mention the **information you collect**, + How you **use it, its relevance** to make your service work and inform the + client on how they can **access, update or delete their personal information**. + +.. image:: images/menu.png :align: center -.. image:: images/service_create.png +.. image:: images/service_list.png :align: center -.. image:: images/service_packs.png +.. image:: images/creating_service.png + :align: center + +.. image:: images/service_created.png :align: center You can then create *credit packs* which clients can purchase in order to @@ -203,9 +219,10 @@ use your service. Packages -------- -The credit packages are essentially a product with 4 characteristics. +The credit packages are essentially a product with 5 characteristics. * Name: the name of the package, +* Icon: A specific icon for the package. If not provided, it will fallback on the service icon * Description: details on the package that will appear on the shop page as well as the invoice, * Credits: the amount of credits the client is entitled to when buying the package, @@ -362,7 +379,6 @@ parameters we can use to make things clearer to the end-user: .. patch:: - .. TODO:: how do you test your service? JSON-RPC2_ Transaction API @@ -394,13 +410,13 @@ Authorize :param ServiceKey key: :param UserToken account_token: - :param int credit: + :param float credit: :param str description: optional, helps users identify the reason for charges on their accounts. :returns: :class:`TransactionToken` if the authorization succeeded. :raises: :class:`~odoo.exceptions.AccessError` if the service token is invalid :raises: :class:`~odoo.addons.iap.models.iap.InsufficientCreditError` if the account does - :raises: ``TypeError`` if the ``credit`` value is not an integer + :raises: ``TypeError`` if the ``credit`` value is not an integer or a float .. code-block:: python @@ -434,7 +450,7 @@ Capture :param TransactionToken token: :param ServiceKey key: - :param int credit_to_capture: (new - 15 Jan 2018) optional parameter to capture a smaller amount of credits than authorized + :param float credit_to_capture: (new - 15 Jan 2018) optional parameter to capture a smaller amount of credits than authorized :raises: :class:`~odoo.exceptions.AccessError` .. code-block:: python @@ -534,6 +550,33 @@ care how they are implemented Raised by any unexpeted behaviour at the discretion of the App developer (*you*). + +Test the API +------------ + +In order to test the developped app, we propose a sandbox platform that allows you to: + +1. Test the whole flow from the client's point of view - Actual services and transactions + that can be consulted. (again this requires to change the endpoint, see the danger note + in :ref:`Service `) +2. Test the API. + +The latter consists in specific tokens that will work on **IAP-Sandbox only**. + +* token ``000000``: represents a non-existing account. Returns + an :class:`~odoo.addons.iap.models.iap.InsufficientCreditError` on authorize attempt. +* token ``000111``: Represents an account without sufficient credits to perform any service. + Returns an :class:`~odoo.addons.iap.models.iap.InsufficientCreditError` on authorize attempt. +* token ``111111``: Represents an account with enough credits to perform any service. + An authorize attempt will return a dummy transacion token that is processed by the capture + and cancel routes. + +.. note:: + + * Those tokens are only active on the IAP-Sanbox server. + * The service key is completely ignored with this flow, If you want to run a robust test + of your service, you should ignore these tokens. + Odoo Helpers ============ @@ -548,7 +591,7 @@ Charging .. note:: A new functionality was introduced to capture a different amount of credits than reserved. - As this patch was added on the 15th of January 2018, you will need to upgrade your ``iap`` module in order to use it. + As this patch was added on the **15th of January 2018**, you will need to upgrade your ``iap`` module in order to use it. The specifics of the new functionality are highlighted in the code. .. class:: odoo.addons.iap.models.iap.charge(env, key, account_token, credit[, description, credit_template]) @@ -567,12 +610,12 @@ Charging configuration key :param ServiceKey key: :param UserToken token: - :param int credit: + :param float credit: :param str description: :param Qweb template credit_template: .. code-block:: python - :emphasize-lines: 10,13,14,15 + :emphasize-lines: 11,13,14,15 @route('/deathstar/superlaser', type='json') def superlaser(self, user_account, @@ -583,7 +626,8 @@ Charging 0.0 is none, 1.0 is full power """ credits = int(MAXIMUM_POWER * factor) - with charge(request.env, SERVICE_KEY, user_account, credits) as transaction: + description = "We will demonstrate the power of this station on your home planet of Alderaan." + with charge(request.env, SERVICE_KEY, user_account, credits, description) as transaction: # TODO: allow other targets transaction.credit = max(credits, 2) # Sales ongoing one the energy price, diff --git a/doc/webservices/images/creating_service.png b/doc/webservices/images/creating_service.png new file mode 100644 index 00000000000..dec807ade45 Binary files /dev/null and b/doc/webservices/images/creating_service.png differ diff --git a/doc/webservices/images/menu.png b/doc/webservices/images/menu.png new file mode 100644 index 00000000000..50e1ed0267a Binary files /dev/null and b/doc/webservices/images/menu.png differ diff --git a/doc/webservices/images/package.png b/doc/webservices/images/package.png index 0cc934caa2f..aca5959f7db 100644 Binary files a/doc/webservices/images/package.png and b/doc/webservices/images/package.png differ diff --git a/doc/webservices/images/service_create.png b/doc/webservices/images/service_create.png deleted file mode 100644 index 93c673467d6..00000000000 Binary files a/doc/webservices/images/service_create.png and /dev/null differ diff --git a/doc/webservices/images/service_created.png b/doc/webservices/images/service_created.png new file mode 100644 index 00000000000..1cb7a78c921 Binary files /dev/null and b/doc/webservices/images/service_created.png differ diff --git a/doc/webservices/images/service_list.png b/doc/webservices/images/service_list.png new file mode 100644 index 00000000000..d2b37f6255c Binary files /dev/null and b/doc/webservices/images/service_list.png differ diff --git a/doc/webservices/images/service_packs.png b/doc/webservices/images/service_packs.png deleted file mode 100644 index 3086d698ea4..00000000000 Binary files a/doc/webservices/images/service_packs.png and /dev/null differ diff --git a/doc/webservices/images/service_select.png b/doc/webservices/images/service_select.png deleted file mode 100644 index e3ceda4e339..00000000000 Binary files a/doc/webservices/images/service_select.png and /dev/null differ diff --git a/odoo/addons/base/data/res.lang.csv b/odoo/addons/base/data/res.lang.csv index 2c86f47f72b..3a0ccc3e07d 100644 --- a/odoo/addons/base/data/res.lang.csv +++ b/odoo/addons/base/data/res.lang.csv @@ -61,7 +61,7 @@ base.lang_my,"Burmese / ဗမာစာ",my_MM,my,"Left-to-Right","[3,3]",".",", "base.lang_es_CL","Spanish (CL) / Español (CL)","es_CL","es_CL","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True","1" "base.lang_es_CO","Spanish (CO) / Español (CO)","es_CO","es_CO","Left-to-Right","[3,0]",",",".","%d-%m-%Y","%H:%M:%S","True","7" "base.lang_es_CR","Spanish (CR) / Español (CR)","es_CR","es_CR","Left-to-Right","[3,0]",","," ","%d/%m/%Y","%H:%M:%S","True","1" -"base.lang_es_DO","Spanish (DO) / Español (DO)","es_DO","es_DO","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","True","1" +"base.lang_es_DO","Spanish (DO) / Español (DO)","es_DO","es_DO","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%I:%M:%S %p","True","1" "base.lang_es_EC","Spanish (EC) / Español (EC)","es_EC","es_EC","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True","1" "base.lang_es_GT","Spanish (GT) / Español (GT)","es_GT","es_GT","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","True","7" "base.lang_es_MX","Spanish (MX) / Español (MX)","es_MX","es_MX","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","True","7" diff --git a/odoo/addons/base/data/res_currency_data.xml b/odoo/addons/base/data/res_currency_data.xml index 9966ae87ae6..f4028c0b698 100644 --- a/odoo/addons/base/data/res_currency_data.xml +++ b/odoo/addons/base/data/res_currency_data.xml @@ -670,8 +670,9 @@ RD$ 0.01 - Peso + Pesos Centavos + before diff --git a/odoo/addons/base/models/ir_module.py b/odoo/addons/base/models/ir_module.py index 627edf53c50..350b4d7a489 100644 --- a/odoo/addons/base/models/ir_module.py +++ b/odoo/addons/base/models/ir_module.py @@ -695,11 +695,10 @@ class Module(models.Model): mod.write(updated_values) else: mod_path = modules.get_module_path(mod_name) - if not mod_path: + if not mod_path or not terp: continue - if not terp or not terp.get('installable', True): - continue - mod = self.create(dict(name=mod_name, state='uninstalled', **values)) + state = "uninstalled" if terp.get('installable', True) else "uninstallable" + mod = self.create(dict(name=mod_name, state=state, **values)) res[1] += 1 mod._update_dependencies(terp.get('depends', [])) diff --git a/odoo/addons/base/models/res_config.py b/odoo/addons/base/models/res_config.py index 7712904fcc0..32beb3b5742 100644 --- a/odoo/addons/base/models/res_config.py +++ b/odoo/addons/base/models/res_config.py @@ -387,8 +387,6 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin) def copy(self, values): raise UserError(_("Cannot duplicate configuration!"), "") - # TODO: Find replacement for 'onchange' attribute in view with dynamic - # api.onchange(...) and migrate the onchange_module(...) accordingly. @api.model def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): @@ -410,9 +408,6 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin) modifiers = json.loads(node.get("modifiers")) modifiers['readonly'] = True node.set("modifiers", json.dumps(modifiers)) - if 'on_change' not in node.attrib: - node.set("on_change", - "onchange_module(%s, '%s')" % (field, field)) ret_val['arch'] = etree.tostring(doc, encoding='unicode') return ret_val @@ -436,6 +431,16 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin) } return {} + def _register_hook(self): + """ Add an onchange method for each module field. """ + def make_method(name): + return lambda self: self.onchange_module(self[name], name) + + for name in self._fields: + if name.startswith('module_'): + method = make_method(name) + self._onchange_methods[name].append(method) + @api.model def _get_classified_fields(self): """ return a dictionary with the fields classified by category:: diff --git a/odoo/models.py b/odoo/models.py index 087845db552..dae750654f5 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -2822,7 +2822,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): record._cache.update(record._convert_to_cache(values, validate=False)) if not self.env.cache.contains(self, field): exc = AccessError("No value found for %s.%s" % (self, field.name)) - self.env.cache.set_failed(self, field, exc) + self.env.cache.set_failed(self, [field], exc) @api.multi def _read_from_database(self, field_names, inherited_field_names=[]): diff --git a/odoo/modules/migration.py b/odoo/modules/migration.py index 28a9c1c170d..57d217e770d 100644 --- a/odoo/modules/migration.py +++ b/odoo/modules/migration.py @@ -56,8 +56,8 @@ class MigrationManager(object): function. Theses files must respect a directory tree structure: A 'migrations' folder which containt a folder by version. Version can be 'module' version or 'server.module' version (in this case, the files will only be processed by this version of the server). - Python file names must start by `pre` or `post` and will be executed, respectively, - before and after the module initialisation. `end` scripts are run after all modules have + Python file names must start by `pre-` or `post-` and will be executed, respectively, + before and after the module initialisation. `end-` scripts are run after all modules have been updated. Example: diff --git a/odoo/tests/common.py b/odoo/tests/common.py index bd1a4f0eda2..55fad1ceb73 100644 --- a/odoo/tests/common.py +++ b/odoo/tests/common.py @@ -530,7 +530,8 @@ class ChromeBrowser(): '--user-data-dir': self.user_data_dir, '--disable-translate': '', '--window-size': '1366x768', - '--remote-debugging-port': str(self.devtools_port) + '--remote-debugging-port': str(self.devtools_port), + '--no-sandbox': '', } cmd = [self.executable] cmd += ['%s=%s' % (k, v) if v else k for k, v in switches.items()] diff --git a/requirements.txt b/requirements.txt index 775f6917f93..3328dcabebb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,7 +33,7 @@ pytz==2016.7 pyusb==1.0.0 qrcode==5.3 reportlab==3.3.0 -requests==2.11.1 +requests==2.20.0 suds-jurko==0.6 vatnumber==1.2 vobject==0.9.3 diff --git a/setup/win32/winpy_requirements.txt b/setup/win32/winpy_requirements.txt index 3d1c5f44b2c..b2978d32134 100644 --- a/setup/win32/winpy_requirements.txt +++ b/setup/win32/winpy_requirements.txt @@ -27,7 +27,7 @@ pytz==2016.7 pyusb==1.0.0 qrcode==5.3 reportlab>=3.3.0 -requests==2.11.1 +requests==2.20.0 six==1.10.0 suds-jurko==0.6 vatnumber==1.2