diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-09-14 16:00:17 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-09-14 16:00:17 +0200 |
| commit | 9c77066f345360429a0920ca4d17af1f84a7ebea (patch) | |
| tree | 469494018b2da134a8ddb5f51930566c1d39bf8e | |
| parent | 5c11e0d6f9dcc2161735ce3fb93430b767d54cf9 (diff) | |
| parent | 6c103589c15dcf97603f3e2909871cb1fefd6484 (diff) | |
[MERGE] forward port branch saas-11.4 up to 6c103589c15
65 files changed, 524 insertions, 354 deletions
diff --git a/addons/account/models/account.py b/addons/account/models/account.py index bb86b8a56ac..6b57858b6b8 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -311,6 +311,12 @@ class AccountAccount(models.Model): self.filtered(lambda r: not r.reconcile)._toggle_reconcile_to_true() else: self.filtered(lambda r: r.reconcile)._toggle_reconcile_to_false() + + if vals.get('currency_id'): + for account in self: + if self.env['account.move.line'].search_count([('account_id', '=', account.id), ('currency_id', 'not in', (False, vals['currency_id']))]): + raise UserError(_('You cannot set a currency on this account as it already has some journal entries having a different foreign currency.')) + return super(AccountAccount, self).write(vals) @api.multi @@ -1128,6 +1134,7 @@ class AccountTax(models.Model): 'refund_account_id': tax.refund_account_id.id, 'analytic': tax.analytic, 'price_include': tax.price_include, + 'tax_exigibility': tax.tax_exigibility, }) return { diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 23dccf27b36..156cf51a172 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -1441,18 +1441,7 @@ class AccountInvoice(models.Model): new_invoices += refund_invoice return new_invoices - @api.multi - def pay_and_reconcile(self, pay_journal, pay_amount=None, date=None, writeoff_acc=None): - """ Create and post an account.payment for the invoice self, which creates a journal entry that reconciles the invoice. - - :param pay_journal: journal in which the payment entry will be created - :param pay_amount: amount of the payment to register, defaults to the residual of the invoice - :param date: payment date, defaults to fields.Date.context_today(self) - :param writeoff_acc: account in which to create a writeoff if pay_amount < self.residual, so that the invoice is fully paid - """ - if isinstance(pay_journal, pycompat.integer_types): - pay_journal = self.env['account.journal'].browse([pay_journal]) - assert len(self) == 1, "Can only pay one invoice at a time." + def _prepare_payment_vals(self, pay_journal, pay_amount=None, date=None, writeoff_acc=None, communication=None): payment_type = self.type in ('out_invoice', 'in_refund') and 'inbound' or 'outbound' if payment_type == 'inbound': payment_method = self.env.ref('account.account_payment_method_manual_in') @@ -1461,9 +1450,10 @@ class AccountInvoice(models.Model): payment_method = self.env.ref('account.account_payment_method_manual_out') journal_payment_methods = pay_journal.outbound_payment_method_ids - communication = self.type in ('in_invoice', 'in_refund') and self.reference or self.number - if self.origin: - communication = '%s (%s)' % (communication, self.origin) + if not communication: + communication = self.type in ('in_invoice', 'in_refund') and self.reference or self.number + if self.origin: + communication = '%s (%s)' % (communication, self.origin) payment_vals = { 'invoice_ids': [(6, 0, self.ids)], @@ -1478,7 +1468,22 @@ class AccountInvoice(models.Model): 'payment_difference_handling': writeoff_acc and 'reconcile' or 'open', 'writeoff_account_id': writeoff_acc and writeoff_acc.id or False, } + return payment_vals + + @api.multi + def pay_and_reconcile(self, pay_journal, pay_amount=None, date=None, writeoff_acc=None): + """ Create and post an account.payment for the invoice self, which creates a journal entry that reconciles the invoice. + + :param pay_journal: journal in which the payment entry will be created + :param pay_amount: amount of the payment to register, defaults to the residual of the invoice + :param date: payment date, defaults to fields.Date.context_today(self) + :param writeoff_acc: account in which to create a writeoff if pay_amount < self.residual, so that the invoice is fully paid + """ + if isinstance(pay_journal, pycompat.integer_types): + pay_journal = self.env['account.journal'].browse([pay_journal]) + assert len(self) == 1, "Can only pay one invoice at a time." + payment_vals = self._prepare_payment_vals(pay_journal, pay_amount=pay_amount, date=date, writeoff_acc=writeoff_acc) payment = self.env['account.payment'].create(payment_vals) payment.post() diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 765b7a9c6a5..e8a6838a139 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -793,7 +793,7 @@ var StatementModel = BasicModel.extend({ }).then(function (result) { if (result.length > 0) { var line = self.getLine(handle); - self.lines[handle].st_line.open_balance_account_id = line.amount < 0 ? result[0]['property_account_payable_id'][0] : result[0]['property_account_receivable_id'][0]; + self.lines[handle].st_line.open_balance_account_id = line.balance.amount < 0 ? result[0]['property_account_payable_id'][0] : result[0]['property_account_receivable_id'][0]; } }); }, @@ -853,6 +853,7 @@ var StatementModel = BasicModel.extend({ '__focus': false }); + prop.tax_exigible = tax.tax_exigibility === 'on_payment' ? true : undefined prop.amount = tax.base; prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, formatOptions); prop.invalid = !self._isValid(prop); @@ -1204,6 +1205,7 @@ var StatementModel = BasicModel.extend({ name : prop.label, debit : amount > 0 ? amount : 0, credit : amount < 0 ? -amount : 0, + tax_exigible: prop.tax_exigible, analytic_tag_ids: [[6, null, _.pluck(prop.analytic_tag_ids, 'id')]] }; if (!isNaN(prop.id)) { @@ -1266,7 +1268,15 @@ var ManualModel = StatementModel.extend({ self.accounts = _.object(self.account_ids, _.pluck(accounts, 'code')); }); - return def_account.then(function () { + var def_reconcileModel = this._rpc({ + model: 'account.reconcile.model', + method: 'search_read', + }) + .then(function (reconcileModels) { + self.reconcileModels = reconcileModels; + }); + + return $.when(def_reconcileModel, def_account).then(function () { switch(context.mode) { case 'customers': case 'suppliers': @@ -1466,7 +1476,7 @@ var ManualModel = StatementModel.extend({ offset: 0, limitMoveLines: this.limitMoveLines, filter: "", - reconcileModels: [], + reconcileModels: this.reconcileModels, account_id: this._formatNameGet([data.account_id, data.account_name]), st_line: data, visible: true diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js index 92cd9f47405..2e554a05776 100644 --- a/addons/account/static/tests/reconciliation_tests.js +++ b/addons/account/static/tests/reconciliation_tests.js @@ -721,6 +721,9 @@ QUnit.module('account', { }], payment_aml_ids: [], new_aml_dicts: []}]], "Should call process_bank_statement_line with ids"); + var def = $.Deferred(); + def.abort = function () {}; + event.data.callback(def); }); // click on reconcile button @@ -763,6 +766,9 @@ QUnit.module('account', { analytic_tag_ids: [[6, null, []]] }]}]], "Should call process_bank_statement_line with ids"); + var def = $.Deferred(); + def.abort = function () {}; + event.data.callback(def); }); // click on validate button @@ -816,6 +822,9 @@ QUnit.module('account', { name: 'SAJ/2014/002 and SAJ/2014/003 : Open balance' }]}]], "Should call process_bank_statement_line with ids"); + var def = $.Deferred(); + def.abort = function () {}; + event.data.callback(def); }); // click on validate button diff --git a/addons/account/views/account_unit_test.xml b/addons/account/views/account_unit_test.xml deleted file mode 100644 index 5e0828b25b6..00000000000 --- a/addons/account/views/account_unit_test.xml +++ /dev/null @@ -1,62 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<odoo> - <data> - <!-- This file must be loaded _after_ account_demo.xml ! --> - <record id="test_invoice_1" model="account.invoice"> - <field name="currency_id" ref="base.EUR"/> - <field name="company_id" ref="base.main_company"/> - <field name="partner_id" ref="base.res_partner_1"/> - <field name="journal_id" search="[('type', '=', 'purchase')]"/> - <field name="state">draft</field> - <field name="type">in_invoice</field> - <field name="account_id" ref="account.a_pay"/> - <field name="name">Test invoice 1</field> - </record> - <record id="test_tax_line" model="account.invoice.tax"> - <field name="name">Test Tax</field> - <field name="amount">100.00</field> - <field name="account_id" ref="account.ova"/> - <field name="invoice_id" ref="test_invoice_1"/> - </record> - <record id="test_invoice_1_line_1" model="account.invoice.line"> - <field name="name">Four Person Desk</field> - <field name="product_id" ref="product.consu_delivery_03"/> - <field name="invoice_id" ref="test_invoice_1"/> - <field name="price_unit">250</field> - <field name="quantity">1</field> - <field name="account_id" ref="account.a_expense"/> - </record> - <record id="test_invoice_1_line_2" model="account.invoice.line"> - <field name="name">Large Meeting Table</field> - <field name="product_id" ref="product.consu_delivery_02"/> - <field name="invoice_id" ref="test_invoice_1"/> - <field name="price_unit">800</field> - <field name="quantity">2</field> - <field name="account_id" ref="account.a_expense"/> - </record> - - <assert id="test_invoice_1" model="account.invoice" string="The currency unit of Test invoice 1 is EUR"> - <test expr="currency_id.symbol">€</test> - </assert> - - <assert id="test_invoice_1" model="account.invoice" severity="error" string="The amount of Test invoice 1 is correct"> - <test expr="sum([l.price_subtotal for l in invoice_line_ids]) == 1850"/> - <test expr="sum([l.price_subtotal for l in invoice_line_ids]) == amount_untaxed"/> - </assert> - - <function model="account.invoice" name="action_invoice_open" eval="[[ref('test_invoice_1')]]"/> - - <assert id="test_invoice_1" model="account.invoice" string="Test invoice 1 is now open"> - <test expr="state">open</test> - </assert> - <function model="account.invoice" name="pay_and_reconcile"> - <!-- ids = --> <value eval="[ref('test_invoice_1')]"/> - <!-- pay_journal_id = --> <value eval="ref('bank_journal')"/> - <!-- pay_amount = --> <value eval="1950"/> - </function> - - <assert id="test_invoice_1" model="account.invoice" string="Test invoice 1 is now paid"> - <test expr="state">paid</test> - </assert> - </data> -</odoo> diff --git a/addons/base_address_extended/models/base_address_extended.py b/addons/base_address_extended/models/base_address_extended.py index 5d3ee07e342..766fac55a8b 100644 --- a/addons/base_address_extended/models/base_address_extended.py +++ b/addons/base_address_extended/models/base_address_extended.py @@ -33,6 +33,11 @@ class Partner(models.Model): street_number2 = fields.Char('Door', compute='_split_street', help="Door Number", inverse='_set_street', store=True) + @api.model + def _address_fields(self): + """Returns the list of address fields that are synced from the parent.""" + return super(Partner, self)._address_fields() + ['street_name', 'street_number', 'street_number2'] + def get_street_fields(self): """Returns the fields that can be used in a street format. Overwrite this function if you want to add your own fields.""" diff --git a/addons/base_geolocalize/models/res_partner.py b/addons/base_geolocalize/models/res_partner.py index 855b4d8bbd1..3594e1cb61b 100644 --- a/addons/base_geolocalize/models/res_partner.py +++ b/addons/base_geolocalize/models/res_partner.py @@ -1,23 +1,35 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import json +import logging import requests from odoo import api, fields, models, tools, _ from odoo.exceptions import UserError +_logger = logging.getLogger(__name__) -def geo_find(addr): + +def geo_find(addr, apikey=False): if not addr: return None - url = 'https://maps.googleapis.com/maps/api/geocode/json' + + if not apikey: + raise UserError(_('''API key for GeoCoding (Places) required.\n + Save this key in System Parameters with key: google.api_key_geocode, value: <your api key> + Visit https://developers.google.com/maps/documentation/geocoding/get-api-key for more information. + ''')) + + url = "https://maps.googleapis.com/maps/api/geocode/json" try: - result = requests.get(url, params={'sensor': 'false', 'address': addr}).json() + result = requests.get(url, params={'sensor': 'false', 'address': addr, 'key': apikey}).json() except Exception as e: raise UserError(_('Cannot contact geolocation servers. Please make sure that your Internet connection is up and running (%s).') % e) if result['status'] != 'OK': + if result.get('error_message'): + _logger.error(result['error_message']) return None try: @@ -45,22 +57,26 @@ class ResPartner(models.Model): partner_longitude = fields.Float(string='Geo Longitude', digits=(16, 5)) date_localization = fields.Date(string='Geolocation Date') + @classmethod + def _geo_localize(cls, apikey, street='', zip='', city='', state='', country=''): + search = geo_query_address(street=street, zip=zip, city=city, state=state, country=country) + result = geo_find(search, apikey) + if result is None: + search = geo_query_address(city=city, state=state, country=country) + result = geo_find(search, apikey) + return result + @api.multi def geo_localize(self): # We need country names in English below + apikey = self.env['ir.config_parameter'].sudo().get_param('google.api_key_geocode') for partner in self.with_context(lang='en_US'): - result = geo_find(geo_query_address(street=partner.street, - zip=partner.zip, - city=partner.city, - state=partner.state_id.name, - country=partner.country_id.name)) - if result is None: - result = geo_find(geo_query_address( - city=partner.city, - state=partner.state_id.name, - country=partner.country_id.name - )) - + result = partner._geo_localize(apikey, + partner.street, + partner.zip, + partner.city, + partner.state_id.name, + partner.country_id.name) if result: partner.write({ 'partner_latitude': result[0], diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py index 68d2e40123e..e6bad838235 100644 --- a/addons/calendar/models/calendar.py +++ b/addons/calendar/models/calendar.py @@ -634,6 +634,8 @@ class Meeting(models.Model): recurring_date = recurring_date.replace(tzinfo=None) else: recurring_date = todate(meeting.recurrent_id_date) + 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] diff --git a/addons/calendar/tests/test_calendar_recurrent_event_case2.py b/addons/calendar/tests/test_calendar_recurrent_event_case2.py index 1f85dd83853..98b42f248b9 100644 --- a/addons/calendar/tests/test_calendar_recurrent_event_case2.py +++ b/addons/calendar/tests/test_calendar_recurrent_event_case2.py @@ -85,14 +85,23 @@ class TestRecurrentEvent(common.TransactionCase): for meeting in meetings: self.assertEqual(meeting.name, 'Sprint Review for google modules', 'Name not changed for id: %s' % meeting.id) + # I detach first occurrence to check it is not modified by changing recurrent event. + min(meetings, key=lambda m: m.start).detach_recurring_event() + # I change description of my weekly meeting Review code with programmer. idval = '%d-%s' % (self.calendar_event_sprint_review.id, '20110425124700') self.CalendarEvent.browse(idval).write({'description': 'Review code of the module: sync_google_calendar.'}) - # I check whether that all the records of this recurrence has been edited. - meetings = self.CalendarEvent.search([('recurrent_id', '=', self.calendar_event_sprint_review.id)]) - for meeting in meetings: - self.assertEqual(meeting.description, 'Review code of the module: sync_google_calendar.', 'Description not changed for id: %s' % meeting.id) + # I check that detached event has not been edited. + detached_meeting = self.CalendarEvent.search([('recurrent_id', '=', self.calendar_event_sprint_review.id)]) + self.assertEqual(detached_meeting.description, False, 'Detached event description changed for id: %s' % meeting.id) + + # I verify wether I find an event by date range when subsequent to a detached one. + last_meeting = max(meetings, key=lambda m: m.start) + meetings = self.CalendarEvent.with_context({'virtual_id': True}).search([ + ('start', '<=', last_meeting.stop), ('stop', '>=', last_meeting.start) + ]) + self.assertEqual(meetings.id, last_meeting.id, 'Last event should be found searching it by date range') # I update the description of two meetings, and check that both have been updated self.calendar_event_sprint_review.write({'description': "Some description"}) diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index d085d7635d6..bb032368eb9 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -935,7 +935,7 @@ class Lead(models.Model): email = '%s@%s' % (alias_record.alias_name, alias_record.alias_domain) email_link = "<a href='mailto:%s'>%s</a>" % (email, email) sub_title = _('or send an email to %s') % (email_link) - return '<p class="o_view_nocontent_smiling_face">%s</p><p>%s</p>' % (help_title, sub_title) + return '<p class="o_view_nocontent_smiling_face">%s</p><p class="oe_view_nocontent_alias">%s</p>' % (help_title, sub_title) @api.multi def log_meeting(self, meeting_subject, meeting_date, duration): @@ -1126,7 +1126,7 @@ class Lead(models.Model): @api.multi def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None): """ Override to set alias of lead and opportunities to their sales team if any. """ - aliases = self.mapped('team_id')._notify_get_reply_to(default=default, records=None, company=company, doc_names=None) + aliases = self.mapped('team_id').sudo()._notify_get_reply_to(default=default, records=None, company=company, doc_names=None) res = {lead.id: aliases.get(lead.team_id.id) for lead in self} leftover = self.filtered(lambda rec: not rec.team_id) if leftover: diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 1aab6729b8f..b4e26174807 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -147,14 +147,14 @@ class HrExpense(models.Model): @api.model def get_empty_list_help(self, help_message): - if help_message: + if help_message and "oe_view_nocontent_smiling_face" not in help_message: use_mailgateway = self.env['ir.config_parameter'].sudo().get_param('hr_expense.use_mailgateway') alias_record = use_mailgateway and self.env.ref('hr_expense.mail_alias_expense') or False if alias_record and alias_record.alias_domain and alias_record.alias_name: link = "<a id='o_mail_test' href='mailto:%(email)s?subject=Lunch%%20with%%20customer%%3A%%20%%2412.32'>%(email)s</a>" % { 'email': '%s@%s' % (alias_record.alias_name, alias_record.alias_domain) } - return '<p class="oe_view_nocontent_smiling_face">%s</p><p>%s</p>' % ( + return '<p class="oe_view_nocontent_smiling_face">%s</p><p class="oe_view_nocontent_alias">%s</p>' % ( _('Add a new expense,'), _('or send receipts by email to %s.') % (link),) return super(HrExpense, self).get_empty_list_help(help_message) diff --git a/addons/l10n_co/models/res_partner.py b/addons/l10n_co/models/res_partner.py index f0c2ba19b28..986ecf2397e 100644 --- a/addons/l10n_co/models/res_partner.py +++ b/addons/l10n_co/models/res_partner.py @@ -46,7 +46,7 @@ class ResPartner(models.Model): except ValueError: partner.l10n_co_verification_code = False - @api.constrains('vat', 'commercial_partner_country_id', 'l10n_co_document_type') + @api.constrains('vat', 'country_id', 'l10n_co_document_type') def check_vat(self): # check_vat is implemented by base_vat which this localization # doesn't directly depend on. It is however automatically diff --git a/addons/mail/models/ir_actions.py b/addons/mail/models/ir_actions.py index 8c84144a12a..f6fa9d4fff0 100644 --- a/addons/mail/models/ir_actions.py +++ b/addons/mail/models/ir_actions.py @@ -80,7 +80,11 @@ class ServerActions(models.Model): # TDE CLEANME: when going to new api with server action, remove action if not action.template_id or not self._context.get('active_id'): return False - action.template_id.send_mail(self._context.get('active_id'), force_send=False, raise_exception=False) + # Clean context from default_type to avoid making attachment + # with wrong values in subsequent operations + cleaned_ctx = dict(self.env.context) + cleaned_ctx.pop('default_type', None) + action.template_id.with_context(cleaned_ctx).send_mail(self._context.get('active_id'), force_send=False, raise_exception=False) return False @api.model diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index a2c7c543f54..3273c2026eb 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -380,7 +380,9 @@ class MailThread(models.AbstractModel): 'email_link': email_link } } - return "%(static_help)s<p>%(dyn_help)s</p>" % { + # do not add alias two times if it was added previously + if "oe_view_nocontent_alias" not in help: + return "%(static_help)s<p class='oe_view_nocontent_alias'>%(dyn_help)s</p>" % { 'static_help': help, 'dyn_help': _("Create a new %(document)s by sending an email to %(email_link)s") % { 'document': document_name, diff --git a/addons/mail/static/src/js/activity.js b/addons/mail/static/src/js/activity.js index 64af9f20a99..8c72ea3dc86 100644 --- a/addons/mail/static/src/js/activity.js +++ b/addons/mail/static/src/js/activity.js @@ -220,6 +220,28 @@ var BasicActivity = AbstractField.extend({ // Handlers //------------------------------------------------------------ + /** Binds a focusout handler on a bootstrap popover + * Useful to do some operations on the popover's HTML, + * like keeping the user's input for the feedback + * @param {JQuery} $popover_el: the element on which + * the popover() method has been called + */ + _bindPopoverFocusout: function ($popover_el) { + var self = this; + // Retrieve the actual popover's HTML + var $popover = $popover_el.data("bs.popover").tip(); + var activityID = $popover_el.data('activity-id'); + $popover.off('focusout'); + $popover.focusout(function (e) { + // outside click of popover hide the popover + // e.relatedTarget is the element receiving the focus + if (!$popover.is(e.relatedTarget) && !$popover.find(e.relatedTarget).length) { + self._draftFeedback[activityID] = $popover.find('#activity_feedback').val(); + $popover.popover('hide'); + } + }); + }, + /** * @private * @param {MouseEvent} ev @@ -284,15 +306,7 @@ var BasicActivity = AbstractField.extend({ $(".o_mail_activity_feedback.popover").not($popover).popover("hide"); $popover.addClass('o_mail_activity_feedback').attr('tabindex', 0); $popover.find('#activity_feedback').focus(); - $popover.off('focusout'); - $popover.focusout(function (e) { - // outside click of popover hide the popover - // e.relatedTarget is the element receiving the focus - if (!$popover.is(e.relatedTarget) && !$popover.find(e.relatedTarget).length) { - self._draftFeedback[activityID] = $popover.find('#activity_feedback').val(); - $popover.popover('hide'); - } - }); + self._bindPopoverFocusout($(this)); }).popover('show'); } }, diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js index 4e1c782a715..144bba0f75c 100644 --- a/addons/mail/static/tests/chatter_tests.js +++ b/addons/mail/static/tests/chatter_tests.js @@ -13,6 +13,7 @@ var testUtils = require('web.test_utils'); var createAsyncView = testUtils.createAsyncView; var createView = testUtils.createView; +var Activity = require('mail.Activity'); var _t = core._t; QUnit.module('mail', {}, function () { @@ -82,6 +83,7 @@ QUnit.module('Chatter', { type: 'selection', selection: [['overdue', 'Overdue'], ['today', 'Today'], ['planned', 'Planned']], }, + note : { string: "Note", type: "char" }, }, }, 'mail.activity.type': { @@ -222,6 +224,74 @@ QUnit.test('basic rendering', function (assert) { form.destroy(); }); +QUnit.test('Activity Done keep feedback on blur', function (assert) { + assert.expect(3); + var done = assert.async(); + + this.data['mail.activity'].records = [ + {activity_type_id: 1, id: 1, user_id: 2, state: 'today', note: 'But I\'m talkin\' about Shaft'}, + ]; + this.data.partner.records[0].activity_ids = [1]; + + var shownDef = $.Deferred(); + var hiddenDef = $.Deferred(); + testUtils.patch(Activity, { + _bindPopoverFocusout: function () { + this._super.apply(this, arguments); + shownDef.resolve(); + }, + }); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + services: this.services, + res_id: 2, + arch:'<form string="Partners">' + + '<div class="oe_chatter">' + + '<field name="activity_ids" widget="mail_activity"/>' + + '</div>' + + '</form>', + }); + + // sanity checks + var $activityEl = form.$('.o_mail_activity[name=activity_ids]'); + assert.strictEqual($activityEl.find('.o_thread_message').length, 1, + 'There should be one activity'); + assert.strictEqual($activityEl.find('.o_thread_message .o_thread_message_note').text().trim(), + 'But I\'m talkin\' about Shaft', 'The activity should have the right note'); + + var $popoverEl = $activityEl.find('.o_thread_message_tools .o_activity_done'); + $popoverEl.on('hidden.bs.popover', hiddenDef.resolve.bind(hiddenDef)); + + // open popover + $popoverEl.click(); + + shownDef.then(function () { + // write a feedback and focusout + var $feedbackPopover = $popoverEl.data('bs.popover').tip(); + $feedbackPopover.find('#activity_feedback').val('John Shaft').focusout(); + + hiddenDef.then(function () { + shownDef = $.Deferred(); + + // re-open popover + $popoverEl.click(); + + shownDef.then(function () { + var $feedbackPopover = $popoverEl.data('bs.popover').tip(); + assert.strictEqual($feedbackPopover.find('#activity_feedback').val(), 'John Shaft', + "feedback should have been kept"); + + form.destroy(); + testUtils.unpatch(Activity); + done(); + }); + }); + }); +}); + QUnit.test('attachmentBox basic rendering', function (assert) { assert.expect(11); this.data.partner.records.push({ diff --git a/addons/membership/models/partner.py b/addons/membership/models/partner.py index dbb8abb5ae9..4ac0cedb4b0 100644 --- a/addons/membership/models/partner.py +++ b/addons/membership/models/partner.py @@ -164,8 +164,10 @@ class Partner(models.Model): @api.model def _cron_update_membership(self): - # used to recompute 'membership_state'; should no longer be necessary - pass + partners = self.search([('membership_state', 'in', ['invoiced', 'paid'])]) + # mark the field to be recomputed, and recompute it + partners._recompute_todo(self._fields['membership_state']) + self.recompute() @api.multi def create_membership_invoice(self, product_id=None, datas=None): diff --git a/addons/mrp/wizard/change_production_qty.py b/addons/mrp/wizard/change_production_qty.py index 453d551782c..31722c53618 100644 --- a/addons/mrp/wizard/change_production_qty.py +++ b/addons/mrp/wizard/change_production_qty.py @@ -90,6 +90,8 @@ class ChangeProductionQty(models.TransientModel): wo.qty_producing = quantity if wo.qty_produced < wo.qty_production and wo.state == 'done': wo.state = 'progress' + if wo.qty_produced == wo.qty_production and wo.state == 'progress': + wo.state = 'done' # assign moves; last operation receive all unassigned moves # TODO: following could be put in a function as it is similar as code in _workorders_create # TODO: only needed when creating new moves diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index 7f341a9d4ab..b3560e236f4 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -89,7 +89,6 @@ class PaymentAcquirer(models.Model): # Formerly associated to `authorize` option from auto_confirm capture_manually = fields.Boolean(string="Capture Amount Manually", help="Capture the amount from Odoo, when the delivery is completed.") - # Formerly associated to `generate_and_pay_invoice` option from auto_confirm journal_id = fields.Many2one( 'account.journal', 'Payment Journal', domain=[('type', 'in', ['bank', 'cash'])], help="""Payments will be registered into this journal. If you get paid straight on your bank account, @@ -197,9 +196,13 @@ class PaymentAcquirer(models.Model): def _check_required_if_provider(self): """ If the field has 'required_if_provider="<provider>"' attribute, then it required if record.provider is <provider>. """ + empty_field = [] for acquirer in self: - if any(getattr(f, 'required_if_provider', None) == acquirer.provider and not acquirer[k] for k, f in self._fields.items()): - return False + for k, f in acquirer._fields.items(): + if getattr(f, 'required_if_provider', None) == acquirer.provider and not acquirer[k]: + empty_field.append(self.env['ir.model.fields'].search([('name', '=', k), ('model', '=', acquirer._name)]).field_description) + if empty_field: + raise ValidationError((', ').join(empty_field)) return True _constraints = [ diff --git a/addons/point_of_sale/views/pos_config_view.xml b/addons/point_of_sale/views/pos_config_view.xml index b7de301c011..b36cd38f7dc 100644 --- a/addons/point_of_sale/views/pos_config_view.xml +++ b/addons/point_of_sale/views/pos_config_view.xml @@ -435,7 +435,7 @@ Operation type used to record product pickings </div> <div class="content-group mt16"> - <field name="picking_type_id"/> + <field name="picking_type_id" required="1"/> </div> </div> </div> diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 24dd46b016c..47a20f62988 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -387,6 +387,7 @@ class Project(models.Model): action_context = safe_eval(action['context']) if action['context'] else {} action_context.update(self._context) action_context['search_default_parent_res_name'] = self.name + action_context.pop('group_by', None) return dict(action, context=action_context) # --------------------------------------------------- diff --git a/addons/rating/views/rating_view.xml b/addons/rating/views/rating_view.xml index aaca9062c6f..423c96b7e27 100644 --- a/addons/rating/views/rating_view.xml +++ b/addons/rating/views/rating_view.xml @@ -96,7 +96,7 @@ on <field name="create_date" /> </li> </ul> - <i t-if="record.feedback.raw_value" class="fa fa-comment float-right mt4" t-attf-title="Feedback:\n#{record.feedback.raw_value}" t-attf-aria-label="Feedback:\n#{record.feedback.raw_value}" role="img"/> + <i t-if="record.feedback.raw_value" class="fa fa-comment float-right mt4" t-att-title="record.feedback.raw_value" t-att-aria-label="record.feedback.raw_value" role="img"/> </div> </div> </t> diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml index 416201d7dc1..828dda343ee 100644 --- a/addons/sale/views/sale_views.xml +++ b/addons/sale/views/sale_views.xml @@ -540,7 +540,7 @@ <field name="client_order_ref"/> <field name="reference" readonly="1" attrs="{'invisible': [('reference', '=', False)]}"/> <field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/> - <field name="analytic_account_id" context="{'default_partner_id':partner_invoice_id, 'default_name':name}" attrs="{'readonly': [('invoice_count','!=',0),('state','=','sale')]}" groups="analytic.group_analytic_accounting"/> + <field name="analytic_account_id" context="{'default_partner_id':partner_invoice_id, 'default_name':name}" attrs="{'readonly': [('invoice_count','!=',0),('state','=','sale')]}" groups="analytic.group_analytic_accounting" force_save="1"/> </group> <group name="sale_pay" string="Invoicing"> <field name="date_order" attrs="{'invisible': [('state', 'in', ['sale', 'done', 'cancel'])]}"/> diff --git a/addons/sale_management/report/sale_report_templates.xml b/addons/sale_management/report/sale_report_templates.xml index 916c7a5042a..18ad9cf028b 100644 --- a/addons/sale_management/report/sale_report_templates.xml +++ b/addons/sale_management/report/sale_report_templates.xml @@ -3,6 +3,7 @@ <template id="report_saleorder_document_inherit_sale_management" inherit_id="sale.report_saleorder_document"> <xpath expr="//div[hasclass('page')]/p[@id='fiscal_position_remark']" position="after"> <div t-if="doc.sale_order_option_ids and doc.state in ['draft', 'sent']"> + <t t-set="has_option_discount" t-value="any(doc.options.filtered(lambda o: o.discount != 0.0))" /> <h4> <span>Optional Products</span> </h4> @@ -10,7 +11,7 @@ <thead> <tr> <th class="text-left">Description</th> - <th groups="sale.group_discount_per_so_line" class="text-left"></th> + <th t-if="has_option_discount" groups="sale.group_discount_per_so_line" class="text-left"></th> <th class="text-right">Unit Price</th> </tr> </thead> @@ -19,8 +20,8 @@ <td> <span t-field="option.name"/> </td> - <td groups="sale.group_discount_per_so_line"> - <strong class="text-info"> + <td t-if="has_option_discount" groups="sale.group_discount_per_so_line"> + <strong t-if="option.discount != 0.0" class="text-info"> <t t-esc="((option.discount % 1) and '%s' or '%d') % option.discount"/>% discount </strong> </td> diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index 3e612a6dc0c..ab749b59927 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -319,6 +319,13 @@ class SaleOrderLine(models.Model): }) return values + def _get_qty_procurement(self): + self.ensure_one() + qty = 0.0 + for move in self.move_ids.filtered(lambda r: r.state != 'cancel'): + qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP') + return qty + @api.multi def _action_launch_stock_rule(self): """ @@ -331,9 +338,7 @@ class SaleOrderLine(models.Model): for line in self: if line.state != 'sale' or not line.product_id.type in ('consu','product'): continue - qty = 0.0 - for move in line.move_ids.filtered(lambda r: r.state != 'cancel'): - qty += move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom, rounding_method='HALF-UP') + qty = line._get_qty_procurement() if float_compare(qty, line.product_uom_qty, precision_digits=precision) >= 0: continue diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index f1bca879dec..a798926bfdb 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -454,8 +454,9 @@ class Picking(models.Model): else: location_dest_id, supplierloc = self.env['stock.warehouse']._get_partner_locations() - self.location_id = location_id - self.location_dest_id = location_dest_id + if self.state == 'draft': + self.location_id = location_id + self.location_dest_id = location_dest_id # TDE CLEANME move into onchange_partner_id if self.partner_id and self.partner_id.picking_warn: if self.partner_id.picking_warn == 'no-message' and self.partner_id.parent_id: diff --git a/addons/stock/tests/test_packing_neg.py b/addons/stock/tests/test_packing_neg.py index 197711cf053..325f9012f9f 100644 --- a/addons/stock/tests/test_packing_neg.py +++ b/addons/stock/tests/test_packing_neg.py @@ -24,23 +24,24 @@ class TestPackingNeg(TransactionCase): }) # Create an incoming picking for this product of 300 PCE from suppliers to stock - default_get_vals = self.env['stock.picking'].default_get(list(self.env['stock.picking'].fields_get())) - default_get_vals.update({ + vals = { 'name': 'Incoming picking (negative product)', 'partner_id': self.ref('base.res_partner_2'), 'picking_type_id': self.ref('stock.picking_type_in'), + 'location_id': self.ref('stock.stock_location_suppliers'), + 'location_dest_id': self.ref('stock.stock_location_stock'), 'move_lines': [(0, 0, { + 'name': 'NEG', 'product_id': product_neg.id, + 'product_uom': product_neg.uom_id.id, 'product_uom_qty': 300.00, 'location_id': self.ref('stock.stock_location_suppliers'), 'location_dest_id': self.ref('stock.stock_location_stock'), })], - }) - pick_neg = self.env['stock.picking'].new(default_get_vals) + } + pick_neg = self.env['stock.picking'].create(vals) pick_neg.onchange_picking_type() pick_neg.move_lines.onchange_product_id() - vals = pick_neg._convert_to_write(pick_neg._cache) - pick_neg = self.env['stock.picking'].create(vals) # Confirm and assign picking pick_neg.action_confirm() @@ -80,23 +81,24 @@ class TestPackingNeg(TransactionCase): pick_neg.action_done() # Make a delivery order of 300 pieces to the customer - default_get_vals = self.env['stock.picking'].default_get(list(self.env['stock.picking'].fields_get())) - default_get_vals.update({ + vals = { 'name': 'outgoing picking (negative product)', 'partner_id': self.ref('base.res_partner_4'), 'picking_type_id': self.ref('stock.picking_type_out'), + 'location_id': self.ref('stock.stock_location_stock'), + 'location_dest_id': self.ref('stock.stock_location_customers'), 'move_lines': [(0, 0, { + 'name': 'NEG', 'product_id': product_neg.id, + 'product_uom': product_neg.uom_id.id, 'product_uom_qty': 300.00, 'location_id': self.ref('stock.stock_location_stock'), 'location_dest_id': self.ref('stock.stock_location_customers'), })], - }) - delivery_order_neg = self.env['stock.picking'].new(default_get_vals) + } + delivery_order_neg = self.env['stock.picking'].create(vals) delivery_order_neg.onchange_picking_type() delivery_order_neg.move_lines.onchange_product_id() - vals = delivery_order_neg._convert_to_write(delivery_order_neg._cache) - delivery_order_neg = self.env['stock.picking'].create(vals) # Assign and confirm delivery_order_neg.action_confirm() @@ -136,23 +138,24 @@ class TestPackingNeg(TransactionCase): self.assertEqual(pallet_3_stock_qty, 50, "Should have 50 pieces in stock on pallet 3") # Create a picking for reconciling the negative quant - default_get_vals = self.env['stock.picking'].default_get(list(self.env['stock.picking'].fields_get())) - default_get_vals.update({ + vals = { 'name': 'reconciling_delivery', 'partner_id': self.ref('base.res_partner_4'), 'picking_type_id': self.ref('stock.picking_type_in'), + 'location_id': self.ref('stock.stock_location_suppliers'), + 'location_dest_id': self.ref('stock.stock_location_stock'), 'move_lines': [(0, 0, { + 'name': 'NEG', 'product_id': product_neg.id, + 'product_uom': product_neg.uom_id.id, 'product_uom_qty': 20.0, 'location_id': self.ref('stock.stock_location_suppliers'), 'location_dest_id': self.ref('stock.stock_location_stock'), })], - }) - delivery_reconcile = self.env['stock.picking'].new(default_get_vals) + } + delivery_reconcile = self.env['stock.picking'].create(vals) delivery_reconcile.onchange_picking_type() delivery_reconcile.move_lines.onchange_product_id() - vals = delivery_reconcile._convert_to_write(delivery_reconcile._cache) - delivery_reconcile = self.env['stock.picking'].create(vals) # Receive 20 products with lot neg in stock with a new incoming shipment that should be on pallet 2 delivery_reconcile.action_confirm() diff --git a/addons/stock/tests/test_proc_rule.py b/addons/stock/tests/test_proc_rule.py index 535af8a1830..1679d40637f 100644 --- a/addons/stock/tests/test_proc_rule.py +++ b/addons/stock/tests/test_proc_rule.py @@ -27,26 +27,23 @@ class TestProcRule(TransactionCase): 'route_ids': [(4, product_route.id)]}) # Create Delivery Order of 10 `product.product_product_3` from Output -> Customer - default_get_vals = self.env['stock.picking'].default_get(list(self.env['stock.picking'].fields_get())) - default_get_vals.update({ + product = self.env.ref('product.product_product_3') + vals = { 'name': 'Delivery order for procurement', 'partner_id': self.ref('base.res_partner_2'), 'picking_type_id': self.ref('stock.picking_type_out'), + 'location_id': self.ref('stock.stock_location_output'), + 'location_dest_id': self.ref('stock.stock_location_customers'), 'move_lines': [(0, 0, { - 'product_id': self.ref('product.product_product_3'), + 'name': '/', + 'product_id': product.id, + 'product_uom': product.uom_id.id, 'product_uom_qty': 10.00, 'procure_method': 'make_to_order', })], - }) - pick_output = self.env['stock.picking'].new(default_get_vals) - pick_output.onchange_picking_type() - pick_output.move_lines.onchange_product_id() - pick_output.update({ - 'location_id': self.ref('stock.stock_location_output'), - 'location_dest_id': self.ref('stock.stock_location_customers'), - }) - vals = pick_output._convert_to_write(pick_output._cache) + } pick_output = self.env['stock.picking'].create(vals) + pick_output.move_lines.onchange_product_id() # Confirm delivery order. pick_output.action_confirm() diff --git a/addons/stock/tests/test_wise_operator.py b/addons/stock/tests/test_wise_operator.py index ec06ab01fc2..2bfd8c08785 100644 --- a/addons/stock/tests/test_wise_operator.py +++ b/addons/stock/tests/test_wise_operator.py @@ -17,25 +17,25 @@ class TestWiseOperator(TransactionCase): 'uom_po_id': self.ref('uom.product_uom_unit'), }) - picking_default_vals = self.env['stock.picking'].default_get(list(self.env['stock.picking'].fields_get())) - # Create an incoming picking for this product of 10 PCE from suppliers to stock - vals = dict(picking_default_vals, **{ + vals = { 'name': 'Incoming picking (wise unit)', 'partner_id': self.ref('base.res_partner_2'), 'picking_type_id': self.ref('stock.picking_type_in'), + 'location_id': self.ref('stock.stock_location_suppliers'), + 'location_dest_id': self.ref('stock.stock_location_stock'), 'move_lines': [(0, 0, { + 'name': '/', 'product_id': product_wise.id, + 'product_uom': product_wise.uom_id.id, 'product_uom_qty': 10.00, 'location_id': self.ref('stock.stock_location_suppliers'), 'location_dest_id': self.ref('stock.stock_location_stock'), })], - }) - pick1_wise = self.env['stock.picking'].new(vals) + } + pick1_wise = self.env['stock.picking'].create(vals) pick1_wise.onchange_picking_type() pick1_wise.move_lines.onchange_product_id() - vals = pick1_wise._convert_to_write(pick1_wise._cache) - pick1_wise = self.env['stock.picking'].create(vals) # Confirm and assign picking and prepare partial pick1_wise.action_confirm() @@ -65,22 +65,24 @@ class TestWiseOperator(TransactionCase): self.assertEqual(len(records.ids), 3, "The number of quants created is not correct") # Make a delivery order of 5 pieces to the customer - vals = dict(picking_default_vals, **{ + vals = { 'name': 'outgoing picking 1 (wise unit)', 'partner_id': self.ref('base.res_partner_4'), 'picking_type_id': self.ref('stock.picking_type_out'), + 'location_id': self.ref('stock.stock_location_stock'), + 'location_dest_id': self.ref('stock.stock_location_customers'), 'move_lines': [(0, 0, { + 'name': '/', 'product_id': product_wise.id, + 'product_uom': product_wise.uom_id.id, 'product_uom_qty': 5.0, 'location_id': self.ref('stock.stock_location_stock'), 'location_dest_id': self.ref('stock.stock_location_customers'), })], - }) - delivery_order_wise1 = self.env['stock.picking'].new(vals) + } + delivery_order_wise1 = self.env['stock.picking'].create(vals) delivery_order_wise1.onchange_picking_type() delivery_order_wise1.move_lines.onchange_product_id() - vals = delivery_order_wise1._convert_to_write(delivery_order_wise1._cache) - delivery_order_wise1 = self.env['stock.picking'].create(vals) # Assign and confirm delivery_order_wise1.action_confirm() @@ -88,22 +90,24 @@ class TestWiseOperator(TransactionCase): self.assertEqual(delivery_order_wise1.state, 'assigned') # Make a delivery order of 5 pieces to the customer - vals = dict(picking_default_vals, **{ + vals = { 'name': 'outgoing picking 2 (wise unit)', 'partner_id': self.ref('base.res_partner_4'), 'picking_type_id': self.ref('stock.picking_type_out'), + 'location_id': self.ref('stock.stock_location_stock'), + 'location_dest_id': self.ref('stock.stock_location_customers'), 'move_lines': [(0, 0, { + 'name': '/', 'product_id': product_wise.id, + 'product_uom': product_wise.uom_id.id, 'product_uom_qty': 5.0, 'location_id': self.ref('stock.stock_location_stock'), 'location_dest_id': self.ref('stock.stock_location_customers'), })], - }) - delivery_order_wise2 = self.env['stock.picking'].new(vals) + } + delivery_order_wise2 = self.env['stock.picking'].create(vals) delivery_order_wise2.onchange_picking_type() delivery_order_wise2.move_lines.onchange_product_id() - vals = delivery_order_wise2._convert_to_write(delivery_order_wise2._cache) - delivery_order_wise2 = self.env['stock.picking'].create(vals) # Assign and confirm delivery_order_wise2.action_confirm() diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml index 1d852bc3be2..79141bf0b06 100644 --- a/addons/stock/views/stock_picking_views.xml +++ b/addons/stock/views/stock_picking_views.xml @@ -265,8 +265,8 @@ <field name="product_id" required="1" attrs="{'readonly': ['|', ('state', '=', 'done'), ('move_id', '!=', False)]}"/> <field name="move_id" invisible="1"/> <field name="product_uom_id" force_save="1" attrs="{'readonly': [('state', '!=', 'draft')]}" groups="uom.group_uom"/> - <field name="location_id" attrs="{'column_invisible': [('parent.picking_type_code', '=', 'incoming')]}" groups="stock.group_tracking_lot" domain="[('id', 'child_of', parent.location_id)]"/> - <field name="location_dest_id" attrs="{'column_invisible': [('parent.picking_type_code', '=', 'outgoing')]}" groups="stock.group_tracking_lot" domain="[('id', 'child_of', parent.location_dest_id)]"/> + <field name="location_id" attrs="{'column_invisible': [('parent.picking_type_code', '=', 'incoming')]}" groups="stock.group_stock_multi_locations" domain="[('id', 'child_of', parent.location_id)]"/> + <field name="location_dest_id" attrs="{'column_invisible': [('parent.picking_type_code', '=', 'outgoing')]}" groups="stock.group_stock_multi_locations" domain="[('id', 'child_of', parent.location_dest_id)]"/> <field name="package_id" groups="stock.group_tracking_lot" domain="[('location_id', '=', location_id)]"/> <field name="result_package_id" groups="stock.group_tracking_lot" domain="['|', '|', ('location_id', '=', False), ('location_id', '=', location_dest_id), ('id', '=', package_id)]"/> <field name="lots_visible" invisible="1"/> diff --git a/addons/stock_dropshipping/models/__init__.py b/addons/stock_dropshipping/models/__init__.py index c617a28246d..7d6f1be055f 100644 --- a/addons/stock_dropshipping/models/__init__.py +++ b/addons/stock_dropshipping/models/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import purchase
\ No newline at end of file +from . import purchase +from . import sale diff --git a/addons/stock_dropshipping/models/sale.py b/addons/stock_dropshipping/models/sale.py new file mode 100644 index 00000000000..81f975cbce6 --- /dev/null +++ b/addons/stock_dropshipping/models/sale.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models, fields + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + purchase_line_ids = fields.One2many('purchase.order.line', 'sale_line_id') + + @api.multi + def _get_qty_procurement(self): + if not self.move_ids.filtered(lambda r: r.state != 'cancel') and self.purchase_line_ids.filtered(lambda r: r.state != 'cancel'): + qty = 0.0 + for po_line in self.purchase_line_ids.filtered(lambda r: r.state != 'cancel'): + qty += po_line.product_uom._compute_quantity(po_line.product_qty, self.product_uom, rounding_method='HALF-UP') + return qty + else: + return super(SaleOrderLine, self)._get_qty_procurement() + diff --git a/addons/stock_dropshipping/tests/__init__.py b/addons/stock_dropshipping/tests/__init__.py index c060ad7247b..990bd0591e3 100644 --- a/addons/stock_dropshipping/tests/__init__.py +++ b/addons/stock_dropshipping/tests/__init__.py @@ -6,4 +6,3 @@ from . import test_dropship from . import test_lifo_price from . import test_procurement_exception from . import test_stockvaluation - diff --git a/addons/stock_dropshipping/tests/test_dropship.py b/addons/stock_dropshipping/tests/test_dropship.py index 7f06c437fa3..9837d93f0af 100644 --- a/addons/stock_dropshipping/tests/test_dropship.py +++ b/addons/stock_dropshipping/tests/test_dropship.py @@ -6,6 +6,50 @@ from odoo.tools import mute_logger class TestDropship(common.TransactionCase): + def test_change_qty(self): + # enable the dropship and MTO route on the product + prod = self.env.ref('product.product_product_8') + dropshipping_route = self.env.ref('stock_dropshipping.route_drop_shipping') + mto_route = self.env.ref('stock.route_warehouse0_mto') + prod.write({'route_ids': [(6, 0, [dropshipping_route.id, mto_route.id])]}) + + # add a vendor + vendor1 = self.env['res.partner'].create({'name': 'vendor1'}) + seller1 = self.env['product.supplierinfo'].create({ + 'name': vendor1.id, + 'price': 8, + }) + prod.write({'seller_ids': [(6, 0, [seller1.id])]}) + + # sell one unit of this product + cust = self.env['res.partner'].create({'name': 'customer1'}) + so = self.env['sale.order'].create({ + 'partner_id': cust.id, + 'partner_invoice_id': cust.id, + 'partner_shipping_id': cust.id, + 'order_line': [(0, 0, { + 'name': prod.name, + 'product_id': prod.id, + 'product_uom_qty': 1.00, + 'product_uom': prod.uom_id.id, + 'price_unit': 12, + })], + 'pricelist_id': self.env.ref('product.list0').id, + 'picking_policy': 'direct', + }) + so.action_confirm() + po = self.env['purchase.order'].search([('group_id', '=', so.procurement_group_id.id)]) + po_line = po.order_line + + # Check the qty on the P0 + self.assertAlmostEqual(po_line.product_qty, 1.00) + + # Update qty on SO and check PO + so.order_line.product_uom_qty = 2.00 + self.assertAlmostEqual(po_line.product_qty, 2.00) + + +class TestDropship(common.TransactionCase): def test_00_dropship(self): diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js index 7a8b3ce8b01..68f35f272da 100644 --- a/addons/web/static/src/js/chrome/abstract_web_client.js +++ b/addons/web/static/src/js/chrome/abstract_web_client.js @@ -187,7 +187,9 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi // If it is not handled, we should display something clearer than the common crash_manager error dialog // since it won't show anything except "Script error." // This link will probably explain it better: https://blog.sentry.io/2016/05/17/what-is-script-error.html - if (message === "Script error." && !file && !line && !col && !error) { + if (!file && !line && !col) { + // Chrome and Opera set "Script error." on the `message` and hide the `error` + // Firefox handles the "Script error." directly. It sets the error thrown by the CORS file into `error` if (window.onOriginError) { window.onOriginError(); delete window.onOriginError; @@ -195,7 +197,7 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi crash_manager.show_error({ type: _t("Odoo Client Error"), message: _t("Unknown CORS error"), - data: {debug: _t("An unknown CORS error occured. The error probably originates from a JavaScript file served from a different origin.")}, + data: {debug: _t("An unknown CORS error occured. The error probably originates from a JavaScript file served from a different origin. (Opening your browser console might give you a hint on the error.)")}, }); } } else { diff --git a/addons/web/static/src/js/core/ajax.js b/addons/web/static/src/js/core/ajax.js index 45313b99428..f844cbdaed4 100644 --- a/addons/web/static/src/js/core/ajax.js +++ b/addons/web/static/src/js/core/ajax.js @@ -46,7 +46,7 @@ function genericJsonRpc (fct_name, params, settings, fct) { }); // FIXME: jsonp? deferred.abort = function () { - deferred.reject('communication', $.Event(), 'abort', 'abort'); + deferred.reject({message: "XmlHttpRequestError abort"}, $.Event('abort')); if (xhr.abort) { xhr.abort(); } diff --git a/addons/web/static/src/js/core/session.js b/addons/web/static/src/js/core/session.js index fcbc5872be0..694285cccdc 100644 --- a/addons/web/static/src/js/core/session.js +++ b/addons/web/static/src/js/core/session.js @@ -344,9 +344,9 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, { } }; - return deferred.then(function () { + var promise = deferred.then(function () { if (aborted) { - var def = $.Deferred().reject('communication', $.Event(), 'abort', 'abort'); + var def = $.Deferred().reject({message: "XmlHttpRequestError abort"}, $.Event('abort')); def.abort = function () {}; return def; } @@ -376,6 +376,9 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, { xhrDef = fct(url, "call", params, options); return xhrDef; }); + + promise.abort = deferred.abort; + return promise; }, url: function (path, params) { params = _.extend(params || {}); diff --git a/addons/web/static/src/js/services/ajax_service.js b/addons/web/static/src/js/services/ajax_service.js index bf754792932..0cecaae9da3 100644 --- a/addons/web/static/src/js/services/ajax_service.js +++ b/addons/web/static/src/js/services/ajax_service.js @@ -7,17 +7,20 @@ var session = require('web.session'); var AjaxService = AbstractService.extend({ rpc: function (route, args, options, target) { - return $.Deferred(function (def) { - session.rpc(route, args, options).then(function () { - if (!target.isDestroyed()) { - def.resolve.apply(def, arguments); - } - }, function () { - if (!target.isDestroyed()) { - def.reject.apply(def, arguments); - } - }); - }).promise(); + var def = $.Deferred(); + var promise = def.promise(); + var xhrDef = session.rpc(route, args, options); + promise.abort = xhrDef.abort.bind(xhrDef); + xhrDef.then(function () { + if (!target.isDestroyed()) { + def.resolve.apply(def, arguments); + } + }).fail(function () { + if (!target.isDestroyed()) { + def.reject.apply(def, arguments); + } + }); + return promise; }, }); diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js index 9c0bcdf37fe..905ea05ac1e 100644 --- a/addons/web/static/src/js/views/kanban/kanban_model.js +++ b/addons/web/static/src/js/views/kanban/kanban_model.js @@ -61,15 +61,11 @@ var KanbanModel = BasicModel.extend({ if (!groupByField || groupByField.type !== 'many2one') { return $.Deferred().reject(); // only supported when grouped on m2o } - // ignore action default which may be for another model - var context = _.pick(parent.context, function (value, key) { - return key.slice(0,8) !== 'default_'; - }); return this._rpc({ model: groupByField.relation, method: 'name_create', args: [name], - context: context, // todo: combine with view context + context: parent.context, // todo: combine with view context }) .then(function (result) { var newGroup = self._makeDataPoint({ diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js index b343f044bc2..15c3e24116f 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -200,6 +200,7 @@ var ListRenderer = BasicRenderer.extend({ reject = columnInvisibleFields[c.attrs.name]; } if (!reject && c.attrs.widget === 'handle') { + self.hasHandle = true; self.handleField = c.attrs.name; } return reject; @@ -663,8 +664,6 @@ var ListRenderer = BasicRenderer.extend({ this._computeAggregates(); $table.toggleClass('o_list_view_grouped', this.isGrouped); $table.toggleClass('o_list_view_ungrouped', !this.isGrouped); - this.hasHandle = this.state.orderedBy.length === 0 || - this.state.orderedBy[0].name === this.handleField; if (this.isGrouped) { $table .append(this._renderHeader(true)) diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js index 491d7d0f382..5cd9a216abf 100644 --- a/addons/web/static/tests/chrome/action_manager_tests.js +++ b/addons/web/static/tests/chrome/action_manager_tests.js @@ -1980,7 +1980,7 @@ QUnit.module('ActionManager', { start: function () { var self = this; return this._super.apply(this, arguments).then(function () { - self.iframe.src = 'test: ' + self.iframe.getAttribute('src'); + self.iframe.src = 'test ' + self.iframe.getAttribute('src'); }); } }); @@ -2002,7 +2002,7 @@ QUnit.module('ActionManager', { if (route === '/report/check_wkhtmltopdf') { return $.when('broken'); } - if (route === 'test: /report/html/some_report') { + if (route === 'test /report/html/some_report') { return $.when(); } return this._super.apply(this, arguments); @@ -2023,7 +2023,7 @@ QUnit.module('ActionManager', { '/web/action/load', '/report/check_wkhtmltopdf', 'notification', - 'test: /report/html/some_report', // report client action's iframe + 'test /report/html/some_report', // report client action's iframe ]); actionManager.destroy(); diff --git a/addons/web/static/tests/core/widget_tests.js b/addons/web/static/tests/core/widget_tests.js index 19e72cb97b6..9510079e380 100644 --- a/addons/web/static/tests/core/widget_tests.js +++ b/addons/web/static/tests/core/widget_tests.js @@ -1,10 +1,12 @@ odoo.define('web.widget_tests', function (require) { "use strict"; +var AjaxService = require('web.AjaxService'); var concurrency = require('web.concurrency'); var core = require('web.core'); var QWeb = require('web.QWeb'); var Widget = require('web.Widget'); +var testUtils = require('web.test_utils'); QUnit.module('core', {}, function () { @@ -377,16 +379,47 @@ QUnit.module('core', {}, function () { }); QUnit.test("calling _rpc on destroyed widgets", function (assert) { - assert.expect(1); + assert.expect(3); + + var def; + var parent = new Widget(); + testUtils.addMockEnvironment(parent, { + session: { + rpc: function () { + def = $.Deferred(); + def.abort = def.reject; + return def; + }, + }, + services: [AjaxService], + }); + var widget = new Widget(parent); + + widget._rpc({route: '/a/route'}).then(function () { + assert.ok(true, "The ajax call should be resolve"); + }); + def.resolve(); + def = null; - var widget = new Widget(); + widget._rpc({route: '/a/route'}).always(function () { + throw Error("Calling _rpc on a destroyed widget should return a " + + "deferred that is never resolved nor rejected"); + }); widget.destroy(); + def.resolve(); + def = null; + widget._rpc({route: '/a/route'}).always(function () { throw Error("Calling _rpc on a destroyed widget should return a " + "deferred that is never resolved nor rejected"); }); + assert.ok(!def, + "The trigger_up is not performed and the call returns a deferred "+ + "never resolved nor rejected"); + assert.ok(true, "there should be no crash when calling _rpc on a destroyed widget"); + parent.destroy(); }); QUnit.test('start is not called when widget is destroyed', function (assert) { diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index f5ddc0982d0..83bd275d9e0 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -111,7 +111,16 @@ var MockServer = Class.extend({ args = JSON.parse(JSON.stringify(args)); } var def = this._performRpc(route, args); - var abort = def.abort; + + var abort = def.abort || def.reject; + if (abort) { + abort = abort.bind(def); + } else { + abort = function () { + throw new Error("Can't abort this request"); + }; + } + def = def.then(function (result) { var resultString = JSON.stringify(result || false); if (debug) { @@ -125,8 +134,10 @@ var MockServer = Class.extend({ } return $.Deferred().reject(errorString, event || $.Event()); }); - def.abort = abort; - return def; + + var promise = def.promise(); + promise.abort = abort; + return promise; }, //-------------------------------------------------------------------------- diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js index b1a0467254a..7c47bf5574a 100644 --- a/addons/web/static/tests/helpers/test_utils.js +++ b/addons/web/static/tests/helpers/test_utils.js @@ -497,15 +497,15 @@ function addMockEnvironment(widget, params) { var services = {ajax: null}; // mocked ajax service already loaded intercept(widget, 'call_service', function (ev) { var args, result; - if (ev.data.service === 'ajax') { + if (services[ev.data.service]) { + var service = services[ev.data.service]; + args = (ev.data.args || []); + result = service[ev.data.method].apply(service, args); + } else if (ev.data.service === 'ajax') { // ajax service is already mocked by the server var route = ev.data.args[0]; args = ev.data.args[1]; result = mockServer.performRpc(route, args); - } else if (services[ev.data.service]) { - var service = services[ev.data.service]; - args = (ev.data.args || []); - result = service[ev.data.method].apply(service, args); } ev.data.callback(result); }); diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index 399095f9427..c967b42a332 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -1490,43 +1490,6 @@ QUnit.module('Views', { kanban.destroy(); }); - QUnit.test('m2o group record created does not use action defaults', function (assert) { - assert.expect(1); - - var kanban = createView({ - View: KanbanView, - model: 'partner', - data: this.data, - arch: '<kanban class="o_kanban_test" on_create="quick_create">' + - '<templates><t t-name="kanban-box">' + - '<div>' + - '<field name="foo"/>' + - '</div>' + - '</t></templates>' + - '</kanban>', - groupBy: ['product_id'], - mockRPC: function (route, args) { - if (args.method === 'name_create') { - assert.notOk(_.has(args.kwargs.context, 'default_name'), - "default_* should be removed from context"); - } - return this._super.apply(this, arguments); - }, - viewOptions: { - context: { - default_name: 'default name partner', - }, - }, - }); - kanban.renderButtons(); - - kanban.$('.o_column_quick_create input').val('new product'); - kanban.$('.o_column_quick_create button.o_kanban_add').click(); - - kanban.destroy(); - }); - - QUnit.test('quick create record fail in grouped', function (assert) { assert.expect(8); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index cafd529b47c..567e722c135 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -3312,76 +3312,6 @@ QUnit.module('Views', { list.destroy(); }); - QUnit.test('editable list, handle widget locks and unlocks on sort', function (assert) { - assert.expect(6); - - // we need another sortable field to lock/unlock the handle - this.data.foo.fields.amount.sortable = true; - // resequence makes sense on a sequence field, not on arbitrary fields - this.data.foo.records[0].int_field = 0; - this.data.foo.records[1].int_field = 1; - this.data.foo.records[2].int_field = 2; - this.data.foo.records[3].int_field = 3; - - var list = createView({ - View: ListView, - model: 'foo', - data: this.data, - arch: '<tree editable="top" default_order="int_field">' + - '<field name="int_field" widget="handle"/>' + - '<field name="amount" widget="float"/>' + - '</tree>', - }); - - assert.strictEqual(list.$('tbody span[name="amount"]').text(), '1200.00500.00300.000.00', - "default should be sorted by int_field"); - - // Drag and drop the fourth line in second position - testUtils.dragAndDrop( - list.$('.ui-sortable-handle').eq(3), - list.$('tbody tr').first(), - {position: 'bottom'} - ); - - // Handle should be unlocked at this point - assert.strictEqual(list.$('tbody span[name="amount"]').text(), '1200.000.00500.00300.00', - "drag and drop should have succeeded, as the handle is unlocked"); - - // Sorting by a field different for int_field should lock the handle - list.$('.o_column_sortable').eq(1).click(); - - assert.strictEqual(list.$('tbody span[name="amount"]').text(), '0.00300.00500.001200.00', - "should have been sorted by amount"); - - // Drag and drop the fourth line in second position (not) - testUtils.dragAndDrop( - list.$('.ui-sortable-handle').eq(3), - list.$('tbody tr').first(), - {position: 'bottom'} - ); - - assert.strictEqual(list.$('tbody span[name="amount"]').text(), '0.00300.00500.001200.00', - "drag and drop should have failed as the handle is locked"); - - // Sorting by int_field should unlock the handle - list.$('.o_column_sortable').eq(0).click(); - - assert.strictEqual(list.$('tbody span[name="amount"]').text(), '1200.000.00500.00300.00', - "records should be ordered as per the previous resequence"); - - // Drag and drop the fourth line in second position - testUtils.dragAndDrop( - list.$('.ui-sortable-handle').eq(3), - list.$('tbody tr').first(), - {position: 'bottom'} - ); - - assert.strictEqual(list.$('tbody span[name="amount"]').text(), '1200.00300.000.00500.00', - "drag and drop should have worked as the handle is unlocked"); - - list.destroy(); - }); - QUnit.test('editable list with handle widget with slow network', function (assert) { assert.expect(15); diff --git a/addons/web_settings_dashboard/static/src/js/dashboard.js b/addons/web_settings_dashboard/static/src/js/dashboard.js index 4d58da38041..4eaf10aab53 100644 --- a/addons/web_settings_dashboard/static/src/js/dashboard.js +++ b/addons/web_settings_dashboard/static/src/js/dashboard.js @@ -5,6 +5,7 @@ var AbstractAction = require('web.AbstractAction'); var config = require('web.config'); var core = require('web.core'); var framework = require('web.framework'); +var session = require('web.session'); var Widget = require('web.Widget'); var QWeb = core.qweb; diff --git a/addons/web_settings_dashboard/static/tests/dashboard_tests.js b/addons/web_settings_dashboard/static/tests/dashboard_tests.js index ed66ac3ecb2..7b593969967 100644 --- a/addons/web_settings_dashboard/static/tests/dashboard_tests.js +++ b/addons/web_settings_dashboard/static/tests/dashboard_tests.js @@ -11,12 +11,12 @@ var Dashboard = webSettingsDashboard.Dashboard; function createDashboard(params) { var widget = new Widget(); var dashboard = new Dashboard(widget); - dashboard.all_dashboards = ['invitations']; // test only user invitations + dashboard.all_dashboards = params.dashboards || ['invitations']; // test only user invitations testUtils.addMockEnvironment(widget, params); var originalDestroy = Dashboard.prototype.destroy; - Dashboard.destroy = function () { + dashboard.destroy = function () { dashboard.destroy = originalDestroy; widget.destroy(); }; diff --git a/addons/website_crm_partner_assign/models/crm_lead.py b/addons/website_crm_partner_assign/models/crm_lead.py index 14bfb2b002d..215b96e9457 100644 --- a/addons/website_crm_partner_assign/models/crm_lead.py +++ b/addons/website_crm_partner_assign/models/crm_lead.py @@ -86,19 +86,10 @@ class CrmLead(models.Model): if lead.partner_latitude and lead.partner_longitude: continue if lead.country_id: - result = geo_find(geo_query_address(street=lead.street, - zip=lead.zip, - city=lead.city, - state=lead.state_id.name, - country=lead.country_id.name)) - - if result is None: - result = geo_find(geo_query_address( - city=lead.city, - state=lead.state_id.name, - country=lead.country_id.name - )) - + apikey = self.env['ir.config_parameter'].sudo().get_param('google.api_key_geocode') + result = self.env['res.partner']._geo_localize(apikey, + lead.street, lead.zip, lead.city, + lead.state_id.name, lead.country_id.name) if result: lead.write({ 'partner_latitude': result[0], diff --git a/addons/website_crm_partner_assign/tests/test_partner_assign.py b/addons/website_crm_partner_assign/tests/test_partner_assign.py index 61374979bfc..1945e6bd2e4 100644 --- a/addons/website_crm_partner_assign/tests/test_partner_assign.py +++ b/addons/website_crm_partner_assign/tests/test_partner_assign.py @@ -16,7 +16,7 @@ class TestPartnerAssign(TransactionCase): def setUp(self): super(TestPartnerAssign, self).setUp() - def geo_find(addr): + def geo_find(addr, apikey): return { 'Wavre, Belgium': (50.7158956, 4.6128075), 'Cannon Hill Park, B46 3AG Birmingham, United Kingdom': (52.45216, -1.898578), diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 8fb7e3c1435..4c586b404ac 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -98,7 +98,6 @@ class SaleOrder(models.Model): 'order_id': order_id, 'product_uom': product.uom_id.id, 'price_unit': pu, - 'customer_lead': product.sale_delay, } @api.multi @@ -257,4 +256,4 @@ class SaleOrderLine(models.Model): record.name_short = record.product_id.with_context(display_default_code=False).display_name def get_description_following_lines(self): - return self.name.splitlines()[1:] + return self.name.splitlines()[1:]
\ No newline at end of file diff --git a/addons/website_sale_stock/models/sale_order.py b/addons/website_sale_stock/models/sale_order.py index 4ac8a67a871..2896163c673 100644 --- a/addons/website_sale_stock/models/sale_order.py +++ b/addons/website_sale_stock/models/sale_order.py @@ -32,6 +32,13 @@ class SaleOrder(models.Model): return values @api.multi + def _website_product_id_change(self, order_id, product_id, qty=0): + res = super(SaleOrder, self)._website_product_id_change(order_id, product_id, qty=qty) + product = self.env['product.product'].browse(product_id) + res['customer_lead'] = product.sale_delay + return res + + @api.multi def _get_stock_warning(self, clear=True): self.ensure_one() warn = self.warning_stock diff --git a/doc/cla/corporate/camptocamp.md b/doc/cla/corporate/camptocamp.md index 777bb974dc5..206fad26646 100644 --- a/doc/cla/corporate/camptocamp.md +++ b/doc/cla/corporate/camptocamp.md @@ -33,3 +33,4 @@ Akim Juillerat akim.juillerat@camptocamp.com https://github.com/grindtildeath Damien Crier damien.crier@camptocamp.com https://github.com/damdam-s Simone Orsi simahawk@gmail.com https://github.com/simahawk Patrick Tombez patrick.tombez@camptocamp.com https://github.com/p-tombez +Julien Coux julien.coux@camptocamp.com https://github.com/jcoux diff --git a/doc/cla/corporate/initos.md b/doc/cla/corporate/initos.md index 3c95fe2d07f..dd7fb8ea0e5 100644 --- a/doc/cla/corporate/initos.md +++ b/doc/cla/corporate/initos.md @@ -21,3 +21,4 @@ Peter Hahn peter.hahn@initos.com https://github.com/codingforfun Claudia Haida claudia.haida@initos.com Andreas Zöllner andreas.zoellner@initos.com https://github.com/azoellner Rami Alwafaie rami.alwafaie@initos.com https://github.com/rami-wafaie +Florian Kantelberg florian.kantelberg@initos.com https://github.com/fkantelberg diff --git a/doc/cla/individual/ivantodorovich.md b/doc/cla/individual/ivantodorovich.md new file mode 100644 index 00000000000..d3352e4a1f7 --- /dev/null +++ b/doc/cla/individual/ivantodorovich.md @@ -0,0 +1,11 @@ +Argentina, 2018-09-04 + +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, + +Ivan Todorovich <ivan.todorovich@gmail.com> https://github.com/ivantodorovich diff --git a/doc/cla/individual/rocketgithub.md b/doc/cla/individual/rocketgithub.md new file mode 100644 index 00000000000..9b30fbb4d5c --- /dev/null +++ b/doc/cla/individual/rocketgithub.md @@ -0,0 +1,11 @@ +Guatemala, 2018-09-04 + +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, + +Rodrigo Fernandez rodrigo@solucionesprisma.com https://github.com/rocketgithub diff --git a/doc/reference/views.rst b/doc/reference/views.rst index 5dcac794ec2..052c63a97f5 100644 --- a/doc/reference/views.rst +++ b/doc/reference/views.rst @@ -957,6 +957,18 @@ sorted on the string of the field. which means that non-stored function fields can not be used in pivot views +In Pivot view a ``field`` can have a ``widget`` attribute to dictate its format. +The widget should be a field formatter, of which the most interesting are +``date``, ``datetime``, ``float_time``, and ``monetary``. + +For instance a timesheet pivot view could be defined as:: + + <pivot string="Timesheet"> + <field name="employee_id" type="row"/> + <field name="date" interval="month" type="col"/> + <field name="unit_amount" type="measure" widget="float_time"/> + </pivot> + .. _reference/views/kanban: Kanban diff --git a/odoo/addons/base/data/res_country_data.xml b/odoo/addons/base/data/res_country_data.xml index 54497ad8f97..2eaa0d0abf3 100644 --- a/odoo/addons/base/data/res_country_data.xml +++ b/odoo/addons/base/data/res_country_data.xml @@ -651,7 +651,7 @@ <field name="name">Guatemala</field> <field name="code">gt</field> <field file="base/static/img/country_flags/gt.png" name="image" type="base64" /> - <field name="currency_id" ref="QTQ" /> + <field name="currency_id" ref="GTQ" /> <field eval="502" name="phone_code" /> </record> <record id="gu" model="res.country"> diff --git a/odoo/addons/base/models/res_partner.py b/odoo/addons/base/models/res_partner.py index f0cb0ef04df..aeaad1f3968 100644 --- a/odoo/addons/base/models/res_partner.py +++ b/odoo/addons/base/models/res_partner.py @@ -451,7 +451,7 @@ class Partner(models.Model): """ Sync commercial fields and address fields from company and to children after create/update, just as if those were all modeled as fields.related to the parent """ # 1. From UPSTREAM: sync from parent - if values.get('parent_id') or values.get('type', 'contact'): + if values.get('parent_id') or values.get('type') == 'contact': # 1a. Commercial fields: sync if parent changed if values.get('parent_id'): self._commercial_sync_from_company() diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py index 1f88db8d519..2d96b8b9877 100644 --- a/odoo/addons/base/models/res_users.py +++ b/odoo/addons/base/models/res_users.py @@ -1005,7 +1005,10 @@ class GroupsView(models.Model): xml3.append(E.field(name=field_name, **attrs)) xml3.append({'class': "o_label_nowrap"}) - user_type_attrs = {'invisible': [(user_type_field_name, '!=', group_employee.id)]} + if user_type_field_name: + user_type_attrs = {'invisible': [(user_type_field_name, '!=', group_employee.id)]} + else: + user_type_attrs = {} xml = E.field( E.group(*(xml1), col="2"), diff --git a/odoo/addons/test_main_flows/__init__.py b/odoo/addons/test_main_flows/__init__.py index ac45264fad4..ba375620a33 100644 --- a/odoo/addons/test_main_flows/__init__.py +++ b/odoo/addons/test_main_flows/__init__.py @@ -1,4 +1,17 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -#from . import models +from odoo import api, SUPERUSER_ID + +# +# Conditional installation of enterprise modules. +# +# This module is defined in community but some steps (defined with 'edition: "enterprise"') +# are only used to test enterprise. As it's not possible to direcly add enterprise +# modules dependencies, this post install hook will install account_accountant if exists. +# +def _auto_install_enterprise_dependencies(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + module_list = ['account_accountant'] + module_ids = env['ir.module.module'].search([('name', 'in', module_list), ('state', '=', 'uninstalled')]) + module_ids.sudo().button_install() diff --git a/odoo/addons/test_main_flows/__manifest__.py b/odoo/addons/test_main_flows/__manifest__.py index cc88c62cf50..cc8feb4fa59 100644 --- a/odoo/addons/test_main_flows/__manifest__.py +++ b/odoo/addons/test_main_flows/__manifest__.py @@ -13,4 +13,5 @@ It will install some main apps and will try to execute the most important action 'views/templates.xml', ], 'installable': True, + 'post_init_hook': '_auto_install_enterprise_dependencies', } diff --git a/odoo/addons/test_translation_import/i18n/fr.po b/odoo/addons/test_translation_import/i18n/fr.po index 478682cd0a8..3e3abcfe2ab 100644 --- a/odoo/addons/test_translation_import/i18n/fr.po +++ b/odoo/addons/test_translation_import/i18n/fr.po @@ -35,7 +35,7 @@ msgid "test.translation.import" msgstr "test.translation.import in french" #. module: test_translation_import -#: help:test.translation.import,name:0 +#: model:ir.model.fields,help:test_translation_import.field_test_translation_import__name msgid "Efgh" msgstr "Efgh in french" diff --git a/odoo/addons/test_translation_import/i18n/test_translation_import.pot b/odoo/addons/test_translation_import/i18n/test_translation_import.pot index 7d197cd620c..39333d2b363 100644 --- a/odoo/addons/test_translation_import/i18n/test_translation_import.pot +++ b/odoo/addons/test_translation_import/i18n/test_translation_import.pot @@ -41,7 +41,7 @@ msgid "test.translation.import" msgstr "" #. module: test_translation_import -#: help:test.translation.import,name:0 +#: model:ir.model.fields,help:test_translation_import.field_test_translation_import__name msgid "Efgh" msgstr "" diff --git a/odoo/tools/translate.py b/odoo/tools/translate.py index 5d1404af5c0..a930e562687 100644 --- a/odoo/tools/translate.py +++ b/odoo/tools/translate.py @@ -1104,10 +1104,15 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, # discard the target from the POT targets. src = dic['src'] - if src in pot_targets: - target = pot_targets[src] - target.value = dic['value'] - target.targets.discard((dic['type'], dic['name'], dic['type'] != 'code' and dic['res_id'] or 0)) + target_key = (dic['type'], dic['name'], dic['type'] != 'code' and dic['res_id'] or 0) + target = pot_targets.get(src) + if not target or target_key not in target.targets: + _logger.info("Translation '%s' (%s, %s, %s) not found in reference pot, skipping", + src[:60], dic['type'], dic['name'], dic['res_id']) + return + + target.value = dic['value'] + target.targets.discard(target_key) # This would skip terms that fail to specify a res_id res_id = dic['res_id'] @@ -1142,7 +1147,6 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, if target.value: for type, name, res_id in target.targets: pot_rows.append((type, name, res_id, src, target.value, target.comments)) - pot_targets.clear() for row in pot_rows: process_row(row) |
