summaryrefslogtreecommitdiff
path: root/addons/web
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web')
-rw-r--r--addons/web/controllers/main.py10
-rw-r--r--addons/web/i18n/nl.po2
-rw-r--r--addons/web/i18n/tr.po154
-rw-r--r--addons/web/static/src/js/chrome/action_manager.js10
-rw-r--r--addons/web/static/src/js/chrome/keyboard_navigation_mixin.js22
-rw-r--r--addons/web/static/src/js/core/ajax.js16
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js2
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js9
-rw-r--r--addons/web/static/src/js/views/abstract_controller.js7
-rw-r--r--addons/web/static/src/js/views/abstract_renderer.js5
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js109
-rw-r--r--addons/web/static/src/js/views/basic/basic_renderer.js7
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_view.js16
-rw-r--r--addons/web/static/src/js/views/form/form_controller.js10
-rw-r--r--addons/web/static/src/js/views/form/form_view.js8
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_model.js9
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_record.js30
-rw-r--r--addons/web/static/src/js/views/list/list_editable_renderer.js7
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_model.js6
-rw-r--r--addons/web/static/src/scss/list_view.scss8
-rw-r--r--addons/web/static/tests/chrome/action_manager_tests.js63
-rw-r--r--addons/web/static/tests/chrome/keyboard_navigation_mixin_tests.js31
-rw-r--r--addons/web/static/tests/core/pyeval_tests.js2
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js310
-rw-r--r--addons/web/static/tests/helpers/mock_server.js6
-rw-r--r--addons/web/static/tests/helpers/test_utils.js48
-rw-r--r--addons/web/static/tests/views/calendar_tests.js95
-rw-r--r--addons/web/static/tests/views/graph_tests.js16
-rw-r--r--addons/web/static/tests/views/kanban_tests.js156
-rw-r--r--addons/web/static/tests/views/list_tests.js43
-rw-r--r--addons/web/static/tests/views/pivot_tests.js33
-rw-r--r--addons/web/tests/test_js.py4
-rw-r--r--addons/web/views/webclient_templates.xml1
33 files changed, 1040 insertions, 215 deletions
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index 709fbb61d9c..f4cddcc1ee2 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -1047,8 +1047,12 @@ class Binary(http.Controller):
elif status != 200 and download:
return request.not_found()
- height = int(height or 0)
- width = int(width or 0)
+ if headers and dict(headers).get('Content-Type', '') == 'image/svg+xml': # we shan't resize svg images
+ height = 0
+ width = 0
+ else:
+ height = int(height or 0)
+ width = int(width or 0)
if crop and (width or height):
content = crop_image(content, type='center', size=(width, height), ratio=(1, 1))
@@ -1116,7 +1120,7 @@ class Binary(http.Controller):
if request.httprequest.user_agent.browser == 'safari':
# Safari sends NFD UTF-8 (where é is composed by 'e' and [accent])
# we need to send it the same stuff, otherwise it'll fail
- filename = unicodedata.normalize('NFD', ufile.filename).encode('UTF-8')
+ filename = unicodedata.normalize('NFD', ufile.filename)
try:
attachment = Model.create({
diff --git a/addons/web/i18n/nl.po b/addons/web/i18n/nl.po
index 9b960e441ac..068c20c10e5 100644
--- a/addons/web/i18n/nl.po
+++ b/addons/web/i18n/nl.po
@@ -333,7 +333,7 @@ msgstr "Conditie toevoegen"
#: code:addons/web/static/src/xml/kanban.xml:44
#, python-format
msgid "Add a new Column"
-msgstr ""
+msgstr "Nieuwe kolom toevoegen"
#. module: web
#. openerp-web
diff --git a/addons/web/i18n/tr.po b/addons/web/i18n/tr.po
index fca1a956f29..3b883a5c214 100644
--- a/addons/web/i18n/tr.po
+++ b/addons/web/i18n/tr.po
@@ -52,7 +52,7 @@ msgstr " [Bana]"
#: code:addons/web/static/src/js/chrome/search_filters.js:226
#, python-format
msgid " and "
-msgstr ""
+msgstr "ve"
#. module: web
#. openerp-web
@@ -218,7 +218,7 @@ msgstr "(etiketyok)"
#: code:addons/web/static/src/js/fields/field_utils.js:256
#, python-format
msgid "1 record"
-msgstr ""
+msgstr "1 kayıt"
#. module: web
#: model:ir.ui.view,arch_db:web.menu_secondary
@@ -244,14 +244,14 @@ msgstr ""
#: code:addons/web/static/src/xml/base.xml:604
#, python-format
msgid "ALL"
-msgstr ""
+msgstr "Tümü"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:605
#, python-format
msgid "ANY"
-msgstr ""
+msgstr "Hiçbiri"
#. module: web
#. openerp-web
@@ -300,7 +300,7 @@ msgstr "Etkinleştir"
#: code:addons/web/static/src/xml/base.xml:228
#, python-format
msgid "Activate Assets Debugging"
-msgstr ""
+msgstr "Varlıklar Ayıklamasını Etkinleştir"
#. module: web
#. openerp-web
@@ -361,21 +361,21 @@ msgstr "Özelleşmiş grup ekle"
#: code:addons/web/static/src/xml/base.xml:612
#, python-format
msgid "Add filter"
-msgstr ""
+msgstr "Filtre ekle"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:672
#, python-format
msgid "Add new value"
-msgstr ""
+msgstr "Yeni değer ekle"
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/fields/basic_fields.js:1578
#, python-format
msgid "Add to Favorites"
-msgstr ""
+msgstr "Favorilerinize Ekleyin"
#. module: web
#. openerp-web
@@ -433,7 +433,7 @@ msgstr "Tüm kullanıcılar"
#: code:addons/web/static/src/xml/base.xml:145
#, python-format
msgid "An error occurred"
-msgstr ""
+msgstr "Bir hata oluştu"
#. module: web
#. openerp-web
@@ -443,6 +443,8 @@ msgid ""
"An unknown CORS error occured. The error probably originates from a "
"JavaScript file served from a different origin."
msgstr ""
+"Bilinmeyen bir CORS hatası oluştur. Hata muhtemelen farklı orijinlerden "
+"hizmet veren JavaScript dosyalarından dolayı oluştu."
#. module: web
#. openerp-web
@@ -595,7 +597,7 @@ msgstr "İptal"
#: code:addons/web/static/src/js/views/graph/graph_renderer.js:70
#, python-format
msgid "Cannot render chart with mode : "
-msgstr ""
+msgstr "Mod ile çizelge oluşturulamaz: "
#. module: web
#. openerp-web
@@ -713,7 +715,7 @@ msgstr "Kopyalandı!"
#: code:addons/web/static/src/xml/base.xml:143
#, python-format
msgid "Copy the full error to clipboard"
-msgstr ""
+msgstr "Tüm hataları panele kopyala"
#. module: web
#. openerp-web
@@ -760,7 +762,7 @@ msgstr "Oluştur"
#: code:addons/web/static/src/js/views/form/form_controller.js:502
#, python-format
msgid "Create "
-msgstr ""
+msgstr "Oluştur"
#. module: web
#. openerp-web
@@ -895,7 +897,7 @@ msgstr "Vazgeç"
#: code:addons/web/static/src/js/widgets/data_export.js:336
#, python-format
msgid "Do you really want to delete this export template?"
-msgstr ""
+msgstr "Bu dışa aktarma şablonunu gerçekten silmek istiyor musunuz?"
#. module: web
#. openerp-web
@@ -923,7 +925,7 @@ msgstr "Domain"
#: code:addons/web/static/src/js/widgets/domain_selector.js:520
#, python-format
msgid "Domain error"
-msgstr ""
+msgstr "Domain hatası"
#. module: web
#. openerp-web
@@ -977,7 +979,7 @@ msgstr "İşlemi Düzenle"
#: code:addons/web/static/src/xml/base.xml:273
#, python-format
msgid "Edit Calendar view"
-msgstr ""
+msgstr "Takvim düzenleme görüntüsü"
#. module: web
#. openerp-web
@@ -991,49 +993,49 @@ msgstr "Kolonu Düzenle"
#: code:addons/web/static/src/xml/base.xml:580
#, python-format
msgid "Edit Domain"
-msgstr ""
+msgstr "Domain Düzenle"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:267
#, python-format
msgid "Edit Form view"
-msgstr ""
+msgstr "Form görünümünü düzenle"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:271
#, python-format
msgid "Edit Gantt view"
-msgstr ""
+msgstr "Gantt görünümünü düzenle"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:270
#, python-format
msgid "Edit Graph view"
-msgstr ""
+msgstr "Grafik görünümünü düzenle"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:269
#, python-format
msgid "Edit Kanban view"
-msgstr ""
+msgstr "Kanban görünümünü düzenle"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:268
#, python-format
msgid "Edit List view"
-msgstr ""
+msgstr "Liste görünümünü düzenle"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:272
#, python-format
msgid "Edit Pivot view"
-msgstr ""
+msgstr "Pivot görünümünü düzenle"
#. module: web
#. openerp-web
@@ -1054,7 +1056,7 @@ msgstr "Aşamayı Düzenle"
#: code:addons/web/static/src/xml/base.xml:274
#, python-format
msgid "Edit view, type:"
-msgstr ""
+msgstr "görünümü düzenle, tür:"
#. module: web
#: model:ir.ui.view,arch_db:web.login
@@ -1100,7 +1102,7 @@ msgstr "Herkesin takvimi"
#: code:addons/web/static/src/js/views/calendar/calendar_model.js:507
#, python-format
msgid "Everything"
-msgstr ""
+msgstr "Herşey"
#. module: web
#. openerp-web
@@ -1287,6 +1289,9 @@ msgid ""
"\n"
"Tip: try to flip axis, filter further or reduce the number of measures."
msgstr ""
+"Excel uyumluluğu için, 256 sütundan daha fazla ise dışa aktarılamaz.\n"
+"\n"
+"İpucu: ekseni çevirmeye, daha fazla filtrelemeye veya önlem sayısını azaltmaya çalışın."
#. module: web
#. openerp-web
@@ -1356,7 +1361,7 @@ msgstr ""
#: code:addons/web/static/src/js/core/dialog.js:250
#, python-format
msgid "I am sure about this."
-msgstr ""
+msgstr "Bu konu hakkında eminim."
#. module: web
#. openerp-web
@@ -1402,13 +1407,15 @@ msgid ""
"Invalid database name. Only alphanumerical characters, underscore, hyphen "
"and dot are allowed."
msgstr ""
+"Geçersiz veritabanı adı. Yalnızca alfasayısal karakterler, alt çizgi, tire "
+"ve nokta izin verilir."
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:578
#, python-format
msgid "Invalid domain"
-msgstr ""
+msgstr "Geçersiz Domain"
#. module: web
#. openerp-web
@@ -1416,21 +1423,21 @@ msgstr ""
#: code:addons/web/static/src/xml/base.xml:709
#, python-format
msgid "Invalid field chain"
-msgstr ""
+msgstr "Geçersiz alan zinciri"
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/views/graph/graph_renderer.js:69
#, python-format
msgid "Invalid mode for chart"
-msgstr ""
+msgstr "Çizelge için geçersiz mod"
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/widgets/debug_manager.js:207
#, python-format
msgid "JS Mobile Tests"
-msgstr ""
+msgstr "JS Mobil Testleri"
#. module: web
#. openerp-web
@@ -1537,7 +1544,7 @@ msgstr "M2O arama alanları henüz çok öntanımlı değerleri desteklemiyor"
#. module: web
#: model:ir.ui.view,arch_db:web.external_layout_boxed
msgid "Mail:"
-msgstr ""
+msgstr "Mail:"
#. module: web
#: model:ir.ui.view,arch_db:web.login_layout
@@ -1557,21 +1564,21 @@ msgstr "Filtreleri Yönet"
#: code:addons/web/static/src/xml/base.xml:611
#, python-format
msgid "Match"
-msgstr ""
+msgstr "Eşleştir"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:618
#, python-format
msgid "Match records with"
-msgstr ""
+msgstr "Kayıtları eşleştir"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:616
#, python-format
msgid "Match records with the following rule:"
-msgstr ""
+msgstr "Kayıtları takip eden kuralla birlikte eşleştir:"
#. module: web
#. openerp-web
@@ -1678,7 +1685,7 @@ msgstr "Odoo.com hesabım"
#: code:addons/web/static/src/xml/base.xml:606
#, python-format
msgid "NONE"
-msgstr ""
+msgstr "Hiç"
#. module: web
#. openerp-web
@@ -1735,7 +1742,7 @@ msgstr "Gösterilecek veri yok"
#: code:addons/web/static/src/js/widgets/debug_manager.js:355
#, python-format
msgid "No metadata available"
-msgstr ""
+msgstr "Kılavuz bilgi yok"
#. module: web
#. openerp-web
@@ -1794,7 +1801,7 @@ msgstr "Odoo Apps yakında hazır olacak"
#: code:addons/web/static/src/js/services/crash_manager.js:156
#, python-format
msgid "Odoo Client Error"
-msgstr ""
+msgstr "Odoo Müşteri Hatası"
#. module: web
#. openerp-web
@@ -1808,14 +1815,14 @@ msgstr "Odoo Enterprise"
#: code:addons/web/static/src/js/services/crash_manager.js:118
#, python-format
msgid "Odoo Error"
-msgstr ""
+msgstr "Odoo Hatası"
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/services/crash_manager.js:58
#, python-format
msgid "Odoo Session Expired"
-msgstr ""
+msgstr "Odoo Oturumu Sonlandı"
#. module: web
#. openerp-web
@@ -1823,7 +1830,7 @@ msgstr ""
#: code:addons/web/static/src/js/services/crash_manager.js:197
#, python-format
msgid "Odoo Warning"
-msgstr ""
+msgstr "Odoo Uyarısı"
#. module: web
#. openerp-web
@@ -1878,6 +1885,8 @@ msgstr "Sadece tamsayı değeri geçerli olmalıdır."
msgid ""
"Only employee can access this database. Please contact the administrator."
msgstr ""
+"Bu veri tabanına sadece personel erişebilir. Lütfen yöneticinizle bağlantıya"
+" geçin."
#. module: web
#. openerp-web
@@ -1983,7 +1992,7 @@ msgstr "Pivot"
#: code:addons/web/static/src/js/views/list/list_renderer.js:333
#, python-format
msgid "Please click on the \"save\" button first"
-msgstr ""
+msgstr "Lütfen ilk önce 'kaydet' butonuna basın."
#. module: web
#. openerp-web
@@ -2038,6 +2047,7 @@ msgstr ""
msgid ""
"Please use the copy button to report the error to your support service."
msgstr ""
+"Lütfen destek serisine hatayı raporlamak için kopyala butonunu kullanın."
#. module: web
#: model:ir.ui.view,arch_db:web.menu_secondary
@@ -2093,7 +2103,7 @@ msgstr ""
#: code:addons/web/static/src/js/widgets/model_field_selector.js:455
#, python-format
msgid "Relation not allowed"
-msgstr ""
+msgstr "Bağlantıya izin verilmiyor"
#. module: web
#. openerp-web
@@ -2149,7 +2159,7 @@ msgstr "Geriyükle"
#: code:addons/web/static/src/xml/base.xml:220
#, python-format
msgid "Run JS Mobile Tests"
-msgstr ""
+msgstr "JS Mobil Testlerini Başlat"
#. module: web
#. openerp-web
@@ -2261,7 +2271,7 @@ msgstr "Ara: "
#: code:addons/web/static/src/xml/base.xml:150
#, python-format
msgid "See details"
-msgstr ""
+msgstr "Detayları gör."
#. module: web
#. openerp-web
@@ -2282,7 +2292,7 @@ msgstr "Seç <i class=\"fa fa-database\"/>"
#: code:addons/web/static/src/xml/base.xml:582
#, python-format
msgid "Select a model to add a filter."
-msgstr ""
+msgstr "Filtre eklemek için bir model seçin."
#. module: web
#. openerp-web
@@ -2400,7 +2410,7 @@ msgstr "Aç"
#: code:addons/web/static/src/js/widgets/domain_selector.js:567
#, python-format
msgid "Syntax error"
-msgstr ""
+msgstr "Söz dizimi hatası"
#. module: web
#. openerp-web
@@ -2414,7 +2424,7 @@ msgstr "Çay içmek için bir dakika mola verin,<br /> çünkü yükleniyor..."
#: code:addons/web/static/src/js/widgets/switch_company_menu.js:44
#, python-format
msgid "Tap on the list to change company"
-msgstr ""
+msgstr "Şirket değiştirmek için listeye dokunun."
#. module: web
#. openerp-web
@@ -2435,7 +2445,7 @@ msgstr "Tel:"
#: code:addons/web/static/src/js/widgets/domain_selector.js:567
#, python-format
msgid "The domain you entered is not properly formed"
-msgstr ""
+msgstr "Girdiğiniz domain düzgünce kurulamadı."
#. module: web
#. openerp-web
@@ -2445,6 +2455,8 @@ msgid ""
"The field chain is not valid. Did you maybe use a non-existing field name or"
" followed a non-relational field?"
msgstr ""
+"Alan zinciri geçersizdir. Belki var olmayan alan adı kullandınız yada "
+"ilişkisi olmayan alanı takip ettiniz."
#. module: web
#. openerp-web
@@ -2481,6 +2493,8 @@ msgid ""
"The record has been modified, your changes will be discarded. Do you want to"
" proceed?"
msgstr ""
+"Kayıt değiştirildi, değişikliklerinizi kaybedeceksiniz. Devam etmek istiyor "
+"musunuz?"
#. module: web
#. openerp-web
@@ -2519,7 +2533,7 @@ msgstr "Dosyanızı yüklerken bir problem oluştu"
#: code:addons/web/static/src/js/widgets/domain_selector.js:459
#, python-format
msgid "This domain is not supported."
-msgstr ""
+msgstr "Bu domain alanı desteklenmiyor."
#. module: web
#. openerp-web
@@ -2595,7 +2609,7 @@ msgstr "Silsile takibi:"
#: code:addons/web/static/src/js/fields/field_utils.js:62
#, python-format
msgid "True"
-msgstr ""
+msgstr "Doğru"
#. module: web
#. openerp-web
@@ -2662,7 +2676,7 @@ msgstr "Tanımlanmamış"
#: code:addons/web/static/src/js/chrome/abstract_web_client.js:191
#, python-format
msgid "Unknown CORS error"
-msgstr ""
+msgstr "Bilinmeyen CORS hatası"
#. module: web
#. openerp-web
@@ -2683,7 +2697,7 @@ msgstr "Adsız"
#: code:addons/web/static/src/js/views/pivot/pivot_view.js:93
#, python-format
msgid "Untitled"
-msgstr ""
+msgstr "Başlıksız"
#. module: web
#. openerp-web
@@ -2764,23 +2778,23 @@ msgstr "Uyarı"
#. module: web
#: model:ir.ui.view,arch_db:web.benchmark_suite
msgid "Web Benchmarks"
-msgstr ""
+msgstr "Web Karşılaştırması"
#. module: web
#: model:ir.ui.view,arch_db:web.qunit_mobile_suite
msgid "Web Mobile Tests"
-msgstr ""
+msgstr "Web Mobil Testleri"
#. module: web
#: model:ir.ui.view,arch_db:web.qunit_suite
msgid "Web Tests"
-msgstr ""
+msgstr "Web Testleri"
#. module: web
#: model:ir.ui.view,arch_db:web.external_layout_boxed
#: model:ir.ui.view,arch_db:web.external_layout_standard
msgid "Web:"
-msgstr ""
+msgstr "Web:"
#. module: web
#. openerp-web
@@ -2866,7 +2880,7 @@ msgstr ""
#: code:addons/web/static/src/js/widgets/model_field_selector.js:455
#, python-format
msgid "You cannot follow relations for this field chain construction"
-msgstr ""
+msgstr "Bu alan zinciri yapısı ilişkilerini takip edemezsiniz."
#. module: web
#: code:addons/web/controllers/main.py:793
@@ -2964,7 +2978,7 @@ msgstr "yaklaşık bir saat önce"
#: code:addons/web/static/src/xml/base.xml:611
#, python-format
msgid "all records"
-msgstr ""
+msgstr "tüm kayıtlar"
#. module: web
#: model:ir.model,name:web.model_base
@@ -3021,7 +3035,7 @@ msgstr "büyük ya da eşittir"
#: code:addons/web/static/src/js/widgets/domain_selector.js:25
#, python-format
msgid "in"
-msgstr ""
+msgstr "içinde"
#. module: web
#: model:ir.model,name:web.model_ir_qweb_field_image
@@ -3043,14 +3057,14 @@ msgstr "olan"
#: code:addons/web/static/src/js/chrome/search_filters.js:183
#, python-format
msgid "is after"
-msgstr ""
+msgstr "sonrası"
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/chrome/search_filters.js:184
#, python-format
msgid "is before"
-msgstr ""
+msgstr "öncesi"
#. module: web
#. openerp-web
@@ -3137,7 +3151,7 @@ msgstr "doğrudur"
#: code:addons/web/static/src/js/core/utils.js:127
#, python-format
msgid "kMGTPE"
-msgstr ""
+msgstr "kMGTPE"
#. module: web
#. openerp-web
@@ -3167,7 +3181,7 @@ msgstr "küçük ya da eşittir"
#: code:addons/web/static/src/xml/base.xml:701
#, python-format
msgid "not"
-msgstr ""
+msgstr "değildir"
#. module: web
#. openerp-web
@@ -3188,28 +3202,28 @@ msgstr "geçerli bir sayı değil"
#: code:addons/web/static/src/js/widgets/domain_selector.js:26
#, python-format
msgid "not in"
-msgstr ""
+msgstr "içinde değil"
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/widgets/domain_selector.js:658
#, python-format
msgid "not set (false)"
-msgstr ""
+msgstr "ayarlanmaz (yanlış)"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:620
#, python-format
msgid "of the following rules:"
-msgstr ""
+msgstr "takip eden kuralların:"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:635
#, python-format
msgid "of:"
-msgstr ""
+msgstr "ile ilgili:"
#. module: web
#. openerp-web
@@ -3224,14 +3238,14 @@ msgstr "veya"
#: code:addons/web/static/src/js/widgets/domain_selector.js:29
#, python-format
msgid "parent of"
-msgstr ""
+msgstr "üstü olarak"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:576
#, python-format
msgid "record(s)"
-msgstr ""
+msgstr "kayıtlar"
#. module: web
#. openerp-web
@@ -3245,11 +3259,11 @@ msgstr "kalan)"
#: code:addons/web/static/src/xml/base.xml:701
#, python-format
msgid "set"
-msgstr ""
+msgstr "ayar"
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/widgets/domain_selector.js:658
#, python-format
msgid "set (true)"
-msgstr ""
+msgstr "ayarla (doğru)"
diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js
index 8ae6d93bc58..9885d684dec 100644
--- a/addons/web/static/src/js/chrome/action_manager.js
+++ b/addons/web/static/src/js/chrome/action_manager.js
@@ -775,17 +775,17 @@ var ActionManager = Widget.extend({
* @param {Object} options see @doAction options
*/
_preprocessAction: function (action, options) {
- action._originalAction = JSON.stringify(action);
-
- action.jsID = _.uniqueId('action_');
- action.pushState = options.pushState;
-
// ensure that the context and domain are evaluated
var context = new Context(this.userContext, options.additional_context, action.context);
action.context = pyeval.eval('context', context);
if (action.domain) {
action.domain = pyeval.eval('domain', action.domain, action.context);
}
+
+ action._originalAction = JSON.stringify(action);
+
+ action.jsID = _.uniqueId('action_');
+ action.pushState = options.pushState;
},
/**
* Unlinks the given action and its controller from the internal structures
diff --git a/addons/web/static/src/js/chrome/keyboard_navigation_mixin.js b/addons/web/static/src/js/chrome/keyboard_navigation_mixin.js
index dc1b5247cb1..d0eed176d9c 100644
--- a/addons/web/static/src/js/chrome/keyboard_navigation_mixin.js
+++ b/addons/web/static/src/js/chrome/keyboard_navigation_mixin.js
@@ -26,7 +26,6 @@ odoo.define('web.KeyboardNavigationMixin', function (require) {
var KeyboardNavigationMixin = {
events: {
'keydown': '_onKeyDown',
- 'keypress': '_onKeyPress',
'keyup': '_onKeyUp',
},
@@ -127,6 +126,11 @@ odoo.define('web.KeyboardNavigationMixin', function (require) {
}
}
});
+
+ var elementsWithoutAriaKeyshortcut = this.$el.find('[accesskey]').not('[aria-keyshortcuts]');
+ _.each(elementsWithoutAriaKeyshortcut, function (elem) {
+ elem.setAttribute('aria-keyshortcuts', 'Alt+Shift+' + elem.accessKey);
+ });
this._addAccessKeyOverlays();
}
if (keyDownEvent.altKey && !keyDownEvent.ctrlKey && keyDownEvent.key.length === 1) { // we don't want to catch the Alt key down, only the characters A to Z and number keys
@@ -138,6 +142,7 @@ odoo.define('web.KeyboardNavigationMixin', function (require) {
if (elementWithAccessKey.length) {
if (this.BrowserDetection.isOsMac() ||
!this.BrowserDetection.isBrowserChrome()) { // on windows and linux, chrome does not prevent the default of the accesskeys
+ elementWithAccessKey[0].focus();
elementWithAccessKey[0].click();
if (keyDownEvent.preventDefault) keyDownEvent.preventDefault(); else keyDownEvent.returnValue = false;
if (keyDownEvent.stopPropagation) keyDownEvent.stopPropagation();
@@ -178,21 +183,6 @@ odoo.define('web.KeyboardNavigationMixin', function (require) {
}
},
/**
- * hides the shortcut overlays when key press event is triggered on the ALT key
- *
- * @private
- * @param keyPressEvent {jQueryKeyboardEvent} the keyboard event triggered
- * @return {undefined|false}
- */
- _onKeyPress: function (keyPressEvent) {
- if ((keyPressEvent.altKey || keyPressEvent.key === 'Alt') && !keyPressEvent.ctrlKey) {
- if (keyPressEvent.preventDefault) keyPressEvent.preventDefault(); else keyPressEvent.returnValue = false;
- if (keyPressEvent.stopPropagation) keyPressEvent.stopPropagation();
- if (keyPressEvent.cancelBubble) keyPressEvent.cancelBubble = true;
- return false;
- }
- },
- /**
* hides the shortcut overlays when keyup event is triggered on the ALT key
*
* @private
diff --git a/addons/web/static/src/js/core/ajax.js b/addons/web/static/src/js/core/ajax.js
index 0ee6d2ad1ed..d6c67eacaec 100644
--- a/addons/web/static/src/js/core/ajax.js
+++ b/addons/web/static/src/js/core/ajax.js
@@ -260,8 +260,20 @@ function get_file(options) {
if (options.error) {
var body = this.contentDocument.body;
var nodes = body.children.length === 0 ? body.childNodes : body.children;
- var node = nodes[1] || nodes[0];
- options.error(JSON.parse(node.textContent));
+ var errorParams = {};
+
+ try { // Case of a serialized Odoo Exception: It is Json Parsable
+ var node = nodes[1] || nodes[0];
+ errorParams = JSON.parse(node.textContent);
+ } catch (e) { // Arbitrary uncaught python side exception
+ errorParams = {
+ message: nodes.length > 1 ? nodes[1].textContent : '',
+ data: {
+ title: nodes.length > 0 ? nodes[0].textContent : '',
+ }
+ }
+ }
+ options.error(errorParams);
}
} finally {
complete();
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 10057d44984..31b0c53dd64 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -622,7 +622,7 @@ var FieldMonetary = InputField.extend({
this.$el.empty();
// Prepare and add the input
- this._prepareInput().appendTo(this.$el);
+ this._prepareInput(this.$input).appendTo(this.$el);
if (this.currency) {
// Prepare and add the currency symbol
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 7044a79a535..62cc965cc15 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -340,8 +340,8 @@ var FieldMany2One = AbstractField.extend({
_renderReadonly: function () {
var value = _.escape((this.m2o_value || "").trim()).split("\n").join("<br/>");
this.$el.html(value);
- if (!this.nodeOptions.no_open) {
- this.$el.attr('href', '#');
+ if (!this.nodeOptions.no_open && this.value) {
+ this.$el.attr('href', _.str.sprintf('#id=%s&model=%s', this.value.res_id, this.field.relation));
this.$el.addClass('o_form_uri');
}
},
@@ -1334,10 +1334,7 @@ var FieldOne2Many = FieldX2Many.extend({
var self = this;
var id = ev.data.id;
- var onSaved = function (record, hasChanged) {
- if (!hasChanged) {
- return;
- }
+ var onSaved = function (record) {
if (_.some(self.value.data, {id: record.id})) {
// the record already exists in the relation, so trigger an
// empty 'UPDATE' operation when the user clicks on 'Save' in
diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js
index fe4ba14de71..2f9957737ae 100644
--- a/addons/web/static/src/js/views/abstract_controller.js
+++ b/addons/web/static/src/js/views/abstract_controller.js
@@ -12,6 +12,7 @@ odoo.define('web.AbstractController', function (require) {
* reading localstorage, ...) has to go through the controller.
*/
+var concurrency = require('web.concurrency');
var ControlPanelMixin = require('web.ControlPanelMixin');
var core = require('web.core');
var AbstractAction = require('web.AbstractAction');
@@ -54,6 +55,8 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
this.controllerID = params.controllerID;
this.initialState = params.initialState;
+ // use a DropPrevious to correctly handle concurrent updates
+ this.dp = new concurrency.DropPrevious();
// those arguments are temporary, they won't be necessary as soon as the
// ControlPanel will be handled by the View
this.displayName = params.displayName;
@@ -232,11 +235,11 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
var self = this;
var shouldReload = (options && 'reload' in options) ? options.reload : true;
var def = shouldReload ? this.model.reload(this.handle, params) : $.when();
- return def.then(function (handle) {
+ return this.dp.add(def).then(function (handle) {
self.handle = handle || self.handle; // update handle if we reloaded
var state = self.model.get(self.handle);
var localState = self.renderer.getLocalState();
- return self.renderer.updateState(state, params).then(function () {
+ return self.dp.add(self.renderer.updateState(state, params)).then(function () {
self.renderer.setLocalState(localState);
self._update(state);
});
diff --git a/addons/web/static/src/js/views/abstract_renderer.js b/addons/web/static/src/js/views/abstract_renderer.js
index 2394fb31a64..059380f87bd 100644
--- a/addons/web/static/src/js/views/abstract_renderer.js
+++ b/addons/web/static/src/js/views/abstract_renderer.js
@@ -67,6 +67,11 @@ return Widget.extend({
getLocalState: function () {
},
/**
+ * Order to focus to be given to the content of the current view
+ */
+ giveFocus: function () {
+ },
+ /**
* This is the reverse operation from getLocalState. With this method, we
* expect the renderer to restore all DOM state, if it is relevant.
*
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 ec0fad66a06..cc3dbd67595 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -1068,7 +1068,13 @@ var BasicModel = AbstractModel.extend({
addFieldsInfo: function (recordID, viewInfo) {
var record = this.localData[recordID];
record.fields = _.extend({}, record.fields, viewInfo.fields);
- record.fieldsInfo = _.extend({}, record.fieldsInfo, viewInfo.fieldsInfo);
+ // complete the given fieldsInfo with the fields of the main view, so
+ // that those field will be reloaded if a reload is triggered by the
+ // secondary view
+ var fieldsInfo = _.mapObject(viewInfo.fieldsInfo, function (fieldsInfo) {
+ return _.defaults({}, fieldsInfo, record.fieldsInfo[record.viewType]);
+ });
+ record.fieldsInfo = _.extend({}, record.fieldsInfo, fieldsInfo);
},
/**
* For list resources, this freezes the current records order.
@@ -1553,8 +1559,10 @@ var BasicModel = AbstractModel.extend({
rec = self._makeDataPoint(params);
list._cache[rec.res_id] = rec.id;
}
-
- rec._noAbandon = true;
+ // Do not abandon the record if it has been created
+ // from `default_get`. The list has a savepoint only
+ // after having fully executed `default_get`.
+ rec._noAbandon = !list._savePoint;
list._changes.push({operation: 'ADD', id: rec.id});
if (command[0] === 1) {
list._changes.push({operation: 'UPDATE', id: rec.id});
@@ -1973,6 +1981,9 @@ var BasicModel = AbstractModel.extend({
var records = [];
var ids = [];
list = this._applyX2ManyOperations(list);
+ if (_.isEmpty(list.data)) {
+ return $.when();
+ }
_.each(list.data, function (localId) {
var record = self.localData[localId];
var data = record._changes || record.data;
@@ -1986,13 +1997,16 @@ var BasicModel = AbstractModel.extend({
return this._rpc({
model: model,
method: 'name_get',
- args: [ids],
+ args: [_.uniq(ids)],
context: list.context,
})
.then(function (name_gets) {
- for (var i = 0; i < name_gets.length; i++) {
- records[i].data.display_name = name_gets[i][1];
- }
+ _.each(records, function (record) {
+ var nameGet = _.find(name_gets, function (nameGet) {
+ return nameGet[0] === record.data.id;
+ });
+ record.data.display_name = nameGet[1];
+ });
});
},
/**
@@ -2010,7 +2024,8 @@ var BasicModel = AbstractModel.extend({
*/
_fetchRecord: function (record, options) {
var self = this;
- var fieldNames = options && options.fieldNames || record.getFieldNames();
+ options = options || {};
+ var fieldNames = options.fieldNames || record.getFieldNames(options);
fieldNames = _.uniq(fieldNames.concat(['display_name']));
return this._rpc({
model: record.model,
@@ -2477,7 +2492,16 @@ var BasicModel = AbstractModel.extend({
var self = this;
var def;
if (list.static) {
- def = this._readUngroupedList(list);
+ def = this._readUngroupedList(list).then(function () {
+ if (list.parentID && self.isNew(list.parentID)) {
+ // list from a default_get, so fetch display_name for many2one fields
+ var many2ones = self._getMany2OneFieldNames(list);
+ var defs = _.map(many2ones, function (name) {
+ return self._fetchNameGets(list, name);
+ });
+ return $.when.apply($, defs);
+ }
+ });
} else {
def = this._searchReadUngroupedList(list);
}
@@ -2510,8 +2534,9 @@ var BasicModel = AbstractModel.extend({
_fetchX2Manys: function (record, options) {
var self = this;
var defs = [];
- var fieldNames = options && options.fieldNames || record.getFieldNames();
- var viewType = options && options.viewType || record.viewType;
+ options = options || {};
+ var fieldNames = options.fieldNames || record.getFieldNames(options);
+ var viewType = options.viewType || record.viewType;
_.each(fieldNames, function (fieldName) {
var field = record.fields[fieldName];
if (field.type === 'one2many' || field.type === 'many2many') {
@@ -3034,11 +3059,32 @@ var BasicModel = AbstractModel.extend({
* default view type.
*
* @param {Object} element an element from the localData
+ * @param {Object} [options]
+ * @param {Object} [options.viewType] current viewType. If not set, we will
+ * assume main viewType from the record
* @returns {string[]} the list of field names
*/
- _getFieldNames: function (element) {
+ _getFieldNames: function (element, options) {
var fieldsInfo = element.fieldsInfo;
- return Object.keys(fieldsInfo && fieldsInfo[element.viewType] || {});
+ var viewType = options && options.viewType || element.viewType;
+ return Object.keys(fieldsInfo && fieldsInfo[viewType] || {});
+ },
+ /**
+ * Get many2one fields names in a datapoint. This is useful in order to
+ * fetch their names in the case of a default_get.
+ *
+ * @private
+ * @param {Object} datapoint a valid resource object
+ * @returns {string[]} list of field names that are many2one
+ */
+ _getMany2OneFieldNames: function (datapoint) {
+ var many2ones = [];
+ _.each(datapoint.fields, function (field, name) {
+ if (field.type === 'many2one') {
+ many2ones.push(name);
+ }
+ });
+ return many2ones;
},
/**
* Evaluate the record evaluation context. This method is supposed to be
@@ -3533,13 +3579,16 @@ var BasicModel = AbstractModel.extend({
* @see _fetchRecord @see _makeDefaultRecord
*
* @param {Object} record
- * @param {Object} record
+ * @param {Object} [options]
+ * @param {Object} [options.viewType] current viewType. If not set, we will
+ * assume main viewType from the record
* @returns {Deferred<Object>} resolves to the finished resource
*/
_postprocess: function (record, options) {
var self = this;
var defs = [];
- _.each(record.getFieldNames(), function (name) {
+
+ _.each(record.getFieldNames(options), function (name) {
var field = record.fields[name];
var fieldInfo = record.fieldsInfo[record.viewType][name] || {};
var options = fieldInfo.options || {};
@@ -3879,14 +3928,20 @@ var BasicModel = AbstractModel.extend({
var emptyGroupsIDs = _.difference(_.pluck(previousGroups, 'id'), list.data);
_.each(emptyGroupsIDs, function (groupID) {
list.data.push(groupID);
+ var emptyGroup = self.localData[groupID];
+ // this attribute hasn't been updated in the previous
+ // loop for empty groups
+ emptyGroup.aggregateValues = {};
});
}
return $.when.apply($, defs).then(function () {
- // generate the res_ids of the main list, being the concatenation
- // of the fetched res_ids in each group
- list.res_ids = _.flatten(_.map(arguments, function (group) {
- return group ? group.res_ids : [];
- }));
+ if (!options || !options.onlyGroups) {
+ // generate the res_ids of the main list, being the concatenation
+ // of the fetched res_ids in each group
+ list.res_ids = _.flatten(_.map(arguments, function (group) {
+ return group ? group.res_ids : [];
+ }));
+ }
return list;
});
});
@@ -4116,10 +4171,20 @@ var BasicModel = AbstractModel.extend({
var r2 = self.localData[record2ID];
var data1 = _.extend({}, r1.data, r1._changes);
var data2 = _.extend({}, r2.data, r2._changes);
- if (data1[order.name] < data2[order.name]) {
+
+ // Default value to sort against: the value of the field
+ var orderData1 = data1[order.name];
+ var orderData2 = data2[order.name];
+
+ // If the field is a relation, sort on the display_name of those records
+ if (list.fields[order.name].type === 'many2one') {
+ orderData1 = orderData1 ? self.localData[orderData1].data.display_name : "";
+ orderData2 = orderData2 ? self.localData[orderData2].data.display_name : "";
+ }
+ if (orderData1 < orderData2) {
return order.asc ? -1 : 1;
}
- if (data1[order.name] > data2[order.name]) {
+ if (orderData1 > orderData2) {
return order.asc ? 1 : -1;
}
return compareRecords(record1ID, record2ID, level + 1);
diff --git a/addons/web/static/src/js/views/basic/basic_renderer.js b/addons/web/static/src/js/views/basic/basic_renderer.js
index 16507184915..24b053a7bbe 100644
--- a/addons/web/static/src/js/views/basic/basic_renderer.js
+++ b/addons/web/static/src/js/views/basic/basic_renderer.js
@@ -135,13 +135,6 @@ var BasicRenderer = AbstractRenderer.extend({
dom.setSelectionRange(field.getFocusableElement().get(0), {start: offset, end: offset});
}
},
-
- /**
- * Order to focus to be given to the content of the current view
- */
- giveFocus:function() {
- },
-
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
diff --git a/addons/web/static/src/js/views/calendar/calendar_view.js b/addons/web/static/src/js/views/calendar/calendar_view.js
index a813a626565..6c12d508937 100644
--- a/addons/web/static/src/js/views/calendar/calendar_view.js
+++ b/addons/web/static/src/js/views/calendar/calendar_view.js
@@ -118,8 +118,20 @@ var CalendarView = AbstractView.extend({
//if quick_add = is NOT False and IS specified in view, we this one for widgets.QuickCreate'
this.controllerParams.quickAddPop = (!('quick_add' in attrs) || utils.toBoolElse(attrs.quick_add+'', true));
this.controllerParams.disableQuickCreate = params.disable_quick_create || !this.controllerParams.quickAddPop;
- // If this field is set ot true, we don't open the event in form view, but in a popup with the view_id passed by this parameter
- this.controllerParams.formViewId = !attrs.form_view_id || !utils.toBoolElse(attrs.form_view_id, true) ? false : attrs.form_view_id;
+
+ // If form_view_id is set, then the calendar view will open a form view
+ // with this id, when it needs to edit or create an event.
+ this.controllerParams.formViewId =
+ attrs.form_view_id ? parseInt(attrs.form_view_id, 10) : false;
+ if (!this.controllerParams.formViewId && params.action) {
+ var formViewDescr = _.find(params.action.views, function (v) {
+ return v[1] === 'form';
+ });
+ if (formViewDescr) {
+ this.controllerParams.formViewId = formViewDescr[0];
+ }
+ }
+
this.controllerParams.readonlyFormViewId = !attrs.readonly_form_view_id || !utils.toBoolElse(attrs.readonly_form_view_id, true) ? false : attrs.readonly_form_view_id;
this.controllerParams.eventOpenPopup = utils.toBoolElse(attrs.event_open_popup || '', false);
this.controllerParams.mapping = mapping;
diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js
index dbb64dc3682..be49c12e768 100644
--- a/addons/web/static/src/js/views/form/form_controller.js
+++ b/addons/web/static/src/js/views/form/form_controller.js
@@ -233,6 +233,16 @@ var FormController = BasicController.extend({
return changedFields;
});
},
+ /**
+ * Overrides to force the viewType to 'form', so that we ensure that the
+ * correct fields are reloaded (this is only useful for one2many form views).
+ *
+ * @override
+ */
+ update: function (params, options) {
+ params = _.extend({viewType: 'form'}, params);
+ return this._super(params, options);
+ },
//--------------------------------------------------------------------------
// Private
diff --git a/addons/web/static/src/js/views/form/form_view.js b/addons/web/static/src/js/views/form/form_view.js
index 607f9f54a89..f0ea360fed0 100644
--- a/addons/web/static/src/js/views/form/form_view.js
+++ b/addons/web/static/src/js/views/form/form_view.js
@@ -73,6 +73,14 @@ var FormView = BasicView.extend({
_.each(this.loadParams.fieldsInfo.form, function (attrs, fieldName) {
var field = fields[fieldName];
+ if (!field) {
+ // when a one2many record is opened in a form view, the fields
+ // of the main one2many view (list or kanban) are added to the
+ // fieldsInfo of its form view, but those fields aren't in the
+ // loadParams.fields, as they are not displayed in the view, so
+ // we can ignore them.
+ return;
+ }
if (field.type !== 'one2many' && field.type !== 'many2many') {
return;
}
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 411d4cc7b14..6c4ea1f133c 100644
--- a/addons/web/static/src/js/views/kanban/kanban_model.js
+++ b/addons/web/static/src/js/views/kanban/kanban_model.js
@@ -398,9 +398,8 @@ var KanbanModel = BasicModel.extend({
return $.when();
},
/**
- * Reloads all progressbar data if the given id is a record's one. This is
- * done after given deferred and insures that the given deferred's result is
- * not lost.
+ * Reloads all progressbar data. This is done after given deferred and
+ * insures that the given deferred's result is not lost.
*
* @private
* @param {string} recordID
@@ -409,7 +408,9 @@ var KanbanModel = BasicModel.extend({
*/
_reloadProgressBarGroupFromRecord: function (recordID, def) {
var element = this.localData[recordID];
- if (element.type !== 'record') {
+ if (element.type === 'list' && !element.parentID) {
+ // we are reloading the whole view, so there is no need to manually
+ // reload the progressbars
return def;
}
diff --git a/addons/web/static/src/js/views/kanban/kanban_record.js b/addons/web/static/src/js/views/kanban/kanban_record.js
index 072f4d41f27..30bafaab117 100644
--- a/addons/web/static/src/js/views/kanban/kanban_record.js
+++ b/addons/web/static/src/js/views/kanban/kanban_record.js
@@ -23,7 +23,6 @@ var KanbanRecord = Widget.extend({
events: {
'click .oe_kanban_action': '_onKanbanActionClicked',
'click .o_kanban_manage_toggle_button': '_onManageTogglerClicked',
- 'keydown' : '_onKanbanKeyDown',
},
/**
* @override
@@ -289,6 +288,9 @@ var KanbanRecord = Widget.extend({
if (this.$el.hasClass('oe_kanban_global_click') ||
this.$el.hasClass('oe_kanban_global_click_edit')) {
this.$el.on('click', this._onGlobalClick.bind(this));
+ this.$el.on('keydown', this._onKeyDownCard.bind(this));
+ } else {
+ this.$el.on('keydown', this._onKeyDownOpenFirstLink.bind(this));
}
this._processFields();
this._processWidgets();
@@ -505,11 +507,33 @@ var KanbanRecord = Widget.extend({
this.do_warn("Kanban: no action for type : " + type);
}
},
- _onKanbanKeyDown: function (event) {
+ /**
+ * This event is linked to the kanban card when there is a global_click
+ * class on this card
+ *
+ * @private
+ * @param {KeyDownEvent} event
+ */
+ _onKeyDownCard: function (event) {
+ switch (event.keyCode) {
+ case $.ui.keyCode.ENTER:
+ event.preventDefault();
+ this._onGlobalClick(event);
+ break;
+ }
+ },
+ /**
+ * This event is linked ot the kanban card when there is no global_click
+ * class on the card
+ *
+ * @private
+ * @param {KeyDownEvent} event
+ */
+ _onKeyDownOpenFirstLink: function (event) {
switch (event.keyCode) {
case $.ui.keyCode.ENTER:
event.preventDefault();
- this._openRecord();
+ $(event.target).find('a, button').first().click();
break;
}
},
diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js
index 4d26ba3627e..49fe72cc4b2 100644
--- a/addons/web/static/src/js/views/list/list_editable_renderer.js
+++ b/addons/web/static/src/js/views/list/list_editable_renderer.js
@@ -539,8 +539,13 @@ ListRenderer.include({
* @returns {Deferred} this deferred is resolved immediately
*/
_renderView: function () {
+ var self = this;
this.currentRow = null;
- return this._super.apply(this, arguments);
+ return this._super.apply(this, arguments).then(function () {
+ if (self._isEditable()) {
+ self.$('table').addClass('o_editable_list');
+ }
+ });
},
/**
* Force the resequencing of the items in the list.
diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js
index 4a5d9b45b4a..b45f7a9ffdf 100644
--- a/addons/web/static/src/js/views/pivot/pivot_model.js
+++ b/addons/web/static/src/js/views/pivot/pivot_model.js
@@ -17,6 +17,7 @@ odoo.define('web.PivotModel', function (require) {
*/
var AbstractModel = require('web.AbstractModel');
+var concurrency = require('web.concurrency');
var core = require('web.core');
var session = require('web.session');
var utils = require('web.utils');
@@ -32,6 +33,7 @@ var PivotModel = AbstractModel.extend({
this._super.apply(this, arguments);
this.numbering = {};
this.data = null;
+ this._loadDataDropPrevious = new concurrency.DropPrevious();
},
//--------------------------------------------------------------------------
@@ -556,7 +558,7 @@ var PivotModel = AbstractModel.extend({
}
}
- return $.when.apply(null, groupBys.map(function (groupBy) {
+ return this._loadDataDropPrevious.add($.when.apply(null, groupBys.map(function (groupBy) {
return self._rpc({
model: self.modelName,
method: 'read_group',
@@ -566,7 +568,7 @@ var PivotModel = AbstractModel.extend({
groupBy: groupBy,
lazy: false,
});
- })).then(function () {
+ }))).then(function () {
var data = Array.prototype.slice.call(arguments);
if (data[0][0].__count === 0) {
self.data.has_data = false;
diff --git a/addons/web/static/src/scss/list_view.scss b/addons/web/static/src/scss/list_view.scss
index 36bee4439c6..49cba9409cc 100644
--- a/addons/web/static/src/scss/list_view.scss
+++ b/addons/web/static/src/scss/list_view.scss
@@ -5,6 +5,14 @@
background-color: $o-view-background-color;
margin-bottom: 0px;
+ // Checkbox in editable list
+ // should be clickable and activate the row
+ &.o_editable_list .o_data_row:not(.o_selected_row) .o_data_cell {
+ .o_checkbox:not(.o_readonly_modifier) {
+ pointer-events: none;
+ }
+ }
+
&.table {
td, th {
vertical-align: middle;
diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js
index dfafdc85801..fd29e91c73c 100644
--- a/addons/web/static/tests/chrome/action_manager_tests.js
+++ b/addons/web/static/tests/chrome/action_manager_tests.js
@@ -12,6 +12,7 @@ var ListController = require('web.ListController');
var StandaloneFieldManagerMixin = require('web.StandaloneFieldManagerMixin');
var RamStorage = require('web.RamStorage');
var ReportService = require('web.ReportService');
+var SessionStorageService = require('web.SessionStorageService');
var testUtils = require('web.test_utils');
var Widget = require('web.Widget');
var createActionManager = testUtils.createActionManager;
@@ -2860,6 +2861,68 @@ QUnit.module('ActionManager', {
actionManager.destroy();
});
+ QUnit.test("current act_window action is stored in session_storage", function (assert) {
+ assert.expect(1);
+
+ var expectedAction = _.extend({}, _.findWhere(this.actions, {id: 3}), {
+ context: {},
+ });
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ services: [SessionStorageService.extend({
+ setItem: function (key, value) {
+ assert.strictEqual(value, JSON.stringify(expectedAction),
+ "should store the executed action in the sessionStorage");
+ },
+ })],
+ });
+
+ actionManager.doAction(3);
+
+ actionManager.destroy();
+ });
+
+ QUnit.test("store evaluated context of current action in session_storage", function (assert) {
+ // this test ensures that we don't store stringified instances of
+ // CompoundContext in the session_storage, as they would be meaningless
+ // once restored
+ assert.expect(1);
+
+ var expectedAction = _.extend({}, _.findWhere(this.actions, {id: 4}), {
+ context: {
+ active_model: 'partner',
+ active_id: 1,
+ active_ids: [1],
+ },
+ });
+ var checkSessionStorage = false;
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ services: [SessionStorageService.extend({
+ setItem: function (key, value) {
+ if (checkSessionStorage) {
+ assert.strictEqual(value, JSON.stringify(expectedAction),
+ "should correctly store the executed action in the sessionStorage");
+ }
+ },
+ })],
+ });
+
+ // execute an action and open a record in form view
+ actionManager.doAction(3);
+ actionManager.$('.o_list_view .o_data_row:first').click();
+
+ // click on 'Execute action' button (it executes an action with a CompoundContext as context)
+ checkSessionStorage = true;
+ actionManager.$('.o_form_view button:contains(Execute action)').click();
+
+ actionManager.destroy();
+ });
+
QUnit.module('Actions in target="new"');
QUnit.test('can execute act_window actions in target="new"', function (assert) {
diff --git a/addons/web/static/tests/chrome/keyboard_navigation_mixin_tests.js b/addons/web/static/tests/chrome/keyboard_navigation_mixin_tests.js
new file mode 100644
index 00000000000..38f13e7a117
--- /dev/null
+++ b/addons/web/static/tests/chrome/keyboard_navigation_mixin_tests.js
@@ -0,0 +1,31 @@
+odoo.define('web.keyboard_navigation_mixin_tests', function (require) {
+"use strict";
+
+var KeyboardNavigationMixin = require('web.KeyboardNavigationMixin');
+var Widget = require('web.Widget');
+
+QUnit.module('KeyboardNavigationMixin', function () {
+ QUnit.test('aria-keyshortcuts is added on elements with accesskey', function (assert) {
+ assert.expect(1);
+ var $target = $('#qunit-fixture');
+ var KeyboardWidget = Widget.extend(KeyboardNavigationMixin, {
+ start: function () {
+ var $button = $('<button>').text('Click Me!').attr('accesskey', 'o');
+ // we need to define the accesskey because it will not be assigned on invisible buttons
+ this.$el.append($button);
+ return this._super.apply(this, arguments);
+ },
+ });
+ var w = new KeyboardWidget();
+ w.appendTo($target);
+
+ // minimum set of attribute to generate a native event that works with the mixin
+ var e = new Event("keydown");
+ e.key = '';
+ e.altKey = true;
+ w.$el[0].dispatchEvent(e);
+
+ assert.ok(w.$el.find('button[aria-keyshortcuts]')[0], 'the aria-keyshortcuts is set on the button');
+ });
+});
+});
diff --git a/addons/web/static/tests/core/pyeval_tests.js b/addons/web/static/tests/core/pyeval_tests.js
index 89bfa32872b..737466c8c93 100644
--- a/addons/web/static/tests/core/pyeval_tests.js
+++ b/addons/web/static/tests/core/pyeval_tests.js
@@ -508,7 +508,6 @@ QUnit.module('core', function () {
{
"default_attachment_ids": [],
"default_body": "",
- "default_content_subtype": "html",
"default_model": "res.users",
"default_parent_id": false,
"default_res_id": 1
@@ -526,7 +525,6 @@ QUnit.module('core', function () {
assert.deepEqual(result.context, {
"default_attachment_ids": [],
"default_body": "",
- "default_content_subtype": "html",
"default_model": "res.users",
"default_parent_id": false,
"default_res_id": 1,
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index b16d812993e..1012974bb58 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -599,6 +599,35 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('many2one in non edit mode', function (assert) {
+ assert.expect(3);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="trululu"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('a.o_form_uri').length, 1,
+ "should display 1 m2o link in form");
+ assert.strictEqual(form.$('a.o_form_uri').attr('href'), "#id=4&model=partner",
+ "href should contain id and model");
+
+ // Remove value from many2one and then save, there should not have href with id and model on m2o anchor
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_field_many2one input').val('').trigger('keyup').trigger('focusout');
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.strictEqual(form.$('a.o_form_uri').attr('href'), "#",
+ "href should have #");
+
+ form.destroy();
+ });
+
QUnit.test('many2one searches with correct value', function (assert) {
var done = assert.async();
assert.expect(6);
@@ -1208,8 +1237,167 @@ QUnit.module('relational_fields', {
form.destroy();
});
- QUnit.test('list in form: discard newly added element with empty required field (onchange)', function (assert) {
- assert.expect(8);
+ QUnit.test('list in form: name_get with unique ids (default_get)', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].display_name = "MyTrululu";
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'default_get') {
+ return $.when({
+ p: [
+ [0, 0, { trululu: 1 }],
+ [0, 0, { trululu: 1 }]
+ ]
+ });
+ }
+ if (args.method === 'name_get') {
+ assert.deepEqual(args.args[0], _.uniq(args.args[0]),
+ "should not have duplicates in name_get rpc");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('td.o_data_cell').text(), "MyTrululuMyTrululu",
+ "both records should have the correct display_name for trululu field");
+
+ form.destroy();
+ });
+
+ QUnit.test('list in form: show name of many2one fields in multi-page (default_get)', function (assert) {
+ assert.expect(4);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="p">' +
+ '<tree editable="bottom" limit="1">' +
+ '<field name="display_name"/>' +
+ '<field name="trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'default_get') {
+ return $.when({
+ p: [
+ [0, 0, { display_name: 'record1', trululu: 1 }],
+ [0, 0, { display_name: 'record2', trululu: 2 }]
+ ]
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('td.o_data_cell').first().text(),
+ "record1", "should show display_name of 1st record");
+ assert.strictEqual(form.$('td.o_data_cell').first().next().text(),
+ "first record", "should show display_name of trululu of 1st record");
+
+ form.$('button.o_pager_next').click();
+
+ assert.strictEqual(form.$('td.o_data_cell').first().text(),
+ "record2", "should show display_name of 2nd record");
+ assert.strictEqual(form.$('td.o_data_cell').first().next().text(),
+ "second record", "should show display_name of trululu of 2nd record");
+
+ form.destroy();
+ });
+
+ QUnit.test('list in form: discard newly added element with empty required field (onchange in default_get)', function (assert) {
+ // variant of the test "list in form: discard newly added element with
+ // empty required field (default_get)", in which the `default_get`
+ // performs an `onchange` at the same time. This `onchange` may create
+ // some records, which should not be abandoned on discard, similarly
+ // to records created directly by `default_get`
+ assert.expect(7);
+
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+
+ this.data.partner.onchanges = {
+ product_id: function (obj) {
+ if (obj.product_id === 37) {
+ obj.p = [[0, 0, { display_name: "entry", trululu: false }]];
+ }
+ },
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="product_id"/>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '<field name="trululu" required="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'default_get') {
+ return $.when({
+ product_id: 37,
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // check that there is a record in the editable list with empty string as required field
+ assert.strictEqual(form.$('.o_data_row').length, 1,
+ "should have a row in the editable list");
+ assert.strictEqual($('td.o_data_cell').first().text(), "entry",
+ "should have the correct displayed name");
+ var requiredField = $('td.o_data_cell.o_required_modifier');
+ assert.strictEqual(requiredField.length, 1,
+ "should have a required field on this record");
+ assert.strictEqual(requiredField.text(), "",
+ "should have empty string in the required field on this record");
+
+ // click on empty required field in editable list record
+ requiredField.click();
+ // click off so that the required field still stay empty
+ $('body').click();
+
+ // record should not be dropped
+ assert.strictEqual(form.$('.o_data_row').length, 1,
+ "should not have dropped record in the editable list");
+ assert.strictEqual($('td.o_data_cell').first().text(), "entry",
+ "should still have the correct displayed name");
+ assert.strictEqual($('td.o_data_cell.o_required_modifier').text(), "",
+ "should still have empty string in the required field");
+
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ form.destroy();
+ });
+
+ QUnit.test('list in form: discard newly added element with empty required field (onchange after default_get)', function (assert) {
+ // discarding a record from an `onchange` in a `default_get` should not
+ // abandon the record. However, any `onchange` after `default_get`
+ // should be abandoned on discard
+ assert.expect(6);
var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
@@ -1220,7 +1408,6 @@ QUnit.module('relational_fields', {
obj.p = [[0, 0, { display_name: "entry", trululu: false }]];
}
},
- trululu: function () {},
};
var form = createView({
@@ -1262,13 +1449,9 @@ QUnit.module('relational_fields', {
// click off so that the required field still stay empty
$('body').click();
- // record should not be dropped
- assert.strictEqual(form.$('.o_data_row').length, 1,
- "should not have droppped record in the editable list");
- assert.strictEqual($('td.o_data_cell').first().text(), "entry",
- "should still have the correct displayed name");
- assert.strictEqual($('td.o_data_cell.o_required_modifier').text(), "",
- "should still have empty string in the required field");
+ // record should be dropped
+ assert.strictEqual(form.$('.o_data_row').length, 0,
+ "should have dropped record in the editable list");
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
form.destroy();
@@ -2243,6 +2426,43 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('x2many list sorted by many2one', function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[0].p = [1, 2, 4];
+ this.data.partner.fields.trululu.sortable = true;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="id"/>' +
+ '<field name="trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_data_row .o_list_number').text(), '124',
+ "should have correct order initially");
+
+ form.$('.o_list_view thead th:nth(1)').click();
+
+ assert.strictEqual(form.$('.o_data_row .o_list_number').text(), '412',
+ "should have correct order (ASC)");
+
+ form.$('.o_list_view thead th:nth(1)').click();
+
+ assert.strictEqual(form.$('.o_data_row .o_list_number').text(), '214',
+ "should have correct order (DESC)");
+
+ form.destroy();
+ });
+
QUnit.test('many2many list add *many* records, remove, re-add', function (assert) {
assert.expect(5);
@@ -5763,7 +5983,7 @@ QUnit.module('relational_fields', {
rpcCount++;
if (args.method === 'write') {
assert.deepEqual(args.args[1].p, [[0, args.args[1].p[0][1], {
- int_field: 123, product_id: 41
+ foo: false, int_field: 123, product_id: 41,
}]]);
}
return this._super(route, args);
@@ -8921,6 +9141,74 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('one2many form view with action button', function (assert) {
+ // once the action button is clicked, the record is reloaded (via the
+ // on_close handler, executed because the python method does not return
+ // any action, or an ir.action.act_window_close) ; this test ensures that
+ // it reloads the fields of the opened view (i.e. the form in this case).
+ // See https://github.com/odoo/odoo/issues/24189
+ assert.expect(7);
+
+ var data = this.data;
+ data.partner.records[0].p = [2];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: data,
+ res_id: 1,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree><field name="display_name"/></tree>' +
+ '<form>' +
+ '<button type="action" string="Set Timmy"/>' +
+ '<field name="timmy"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'partner_type,false,list': '<tree><field name="display_name"/></tree>',
+ },
+ intercepts: {
+ execute_action: function (ev) {
+ data.partner.records[1].display_name = 'new name';
+ data.partner.records[1].timmy = [12];
+ ev.data.on_closed();
+ },
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual(form.$('.o_data_row').length, 1,
+ "there should be one record in the one2many");
+ assert.strictEqual(form.$('.o_data_cell').text(), 'second record',
+ "initial display_name of o2m record should be correct");
+
+ // open one2many record in form view
+ form.$('.o_data_cell:first').click();
+ assert.strictEqual($('.modal .o_form_view').length, 1,
+ "should have opened the form view in a dialog");
+ assert.strictEqual($('.modal .o_form_view .o_data_row').length, 0,
+ "there should be no record in the many2many");
+
+ // click on the action button
+ $('.modal .o_form_view button').click();
+ assert.strictEqual($('.modal .o_data_row').length, 1,
+ "fields in the o2m form view should have been read");
+ assert.strictEqual($('.modal .o_data_cell').text(), 'gold',
+ "many2many subrecord should have been fetched");
+
+ // save the dialog
+ $('.modal .modal-footer .btn-primary').click();
+
+ assert.strictEqual(form.$('.o_data_cell').text(), 'new name',
+ "fields in the o2m list view should have been read as well");
+
+ form.destroy();
+ });
+
QUnit.module('FieldMany2Many');
QUnit.test('many2many kanban: edition', function (assert) {
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js
index b57b59eb2a7..5afdc3dd447 100644
--- a/addons/web/static/tests/helpers/mock_server.js
+++ b/addons/web/static/tests/helpers/mock_server.js
@@ -116,12 +116,12 @@ var MockServer = Class.extend({
console.log('%c[rpc] response' + route, 'color: blue; font-weight: bold;', JSON.parse(resultString));
}
return JSON.parse(resultString);
- }).fail(function (result) {
+ }, function (result, event) {
var errorString = JSON.stringify(result || false);
if (debug) {
console.log('%c[rpc] response (error) ' + route, 'color: orange; font-weight: bold;', JSON.parse(errorString));
}
- return JSON.parse(errorString);
+ return $.Deferred().reject(errorString, event || $.Event());
});
},
@@ -854,7 +854,7 @@ var MockServer = Class.extend({
*/
_mockReadProgressBar: function (model, kwargs) {
var domain = kwargs.domain;
- var groupBy = kwargs.groupBy;
+ var groupBy = kwargs.group_by;
var progress_bar = kwargs.progress_bar;
var records = this._getRecords(model, domain || []);
diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js
index 2da3eb5ddd9..6c05076ef98 100644
--- a/addons/web/static/tests/helpers/test_utils.js
+++ b/addons/web/static/tests/helpers/test_utils.js
@@ -733,53 +733,6 @@ function patch (target, props) {
});
}
}
-/**
- * In Phantomjs, there is a crash when calling window.getSelection
- * in order for the tests to work, for the specific test that uses it, replace
- * the default window.getSelection by a mock
- *
- * usage:
- * QUnit.test('...',function(done){
- * var unpatchWindowGetSelection = testUtils.patchWindowGetSelection();
- *
- * // do something that needs to use window.getSelection()
- * assert.strictEqual(....);
- *
- * // restore the original function
- * unpatchWindowGetSelection();
- *
- * // finish the test
- * done();
- * })
- *
- * @returns {function} the unpatch function
- */
-function patchWindowGetSelection() {
- var originalWindowGetSelection = window.getSelection;
- window.getSelection = function () {
- return {
- removeAllRanges: function () {},
- addRange: function (range) {},
- getRangeAt: function (index) {
- return {
- startOffset : 0
- };
- },
- anchorNode: {
- parentNode: {
- childNodes: [{
- outerHTML: "@",
- nodeType: 3,
- textContent: '@',
- }],
- },
- },
- };
- };
- return function () {
- window.getSelection = originalWindowGetSelection;
- };
-}
/**
* Unpatches a given Class or Object.
@@ -831,7 +784,6 @@ return $.when(
intercept: intercept,
observe: observe,
patch: patch,
- patchWindowGetSelection: patchWindowGetSelection,
removeSrcAttribute: removeSrcAttribute,
triggerKeypressEvent: triggerKeypressEvent,
triggerMouseEvent: triggerMouseEvent,
diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js
index f1356558a23..9728d8a7c74 100644
--- a/addons/web/static/tests/views/calendar_tests.js
+++ b/addons/web/static/tests/views/calendar_tests.js
@@ -1895,6 +1895,101 @@ QUnit.module('Views', {
calendar.destroy();
});
+
+ QUnit.test('form_view_id attribute works (for creating events)', function (assert) {
+ assert.expect(1);
+
+ var calendar = createView({
+ View: CalendarView,
+ model: 'event',
+ data: this.data,
+ arch: '<calendar class="o_calendar_test" '+
+ 'date_start="start" '+
+ 'date_stop="stop" '+
+ 'mode="month" '+
+ 'form_view_id="42">'+
+ '<field name="name"/>'+
+ '</calendar>',
+ archs: archs,
+ viewOptions: {
+ initialDate: initialDate,
+ },
+ mockRPC: function (route, args) {
+ if (args.method === "create") {
+ // we simulate here the case where a create call with just
+ // the field name fails. This is a normal flow, the server
+ // reject the create rpc (quick create), then the web client
+ // fall back to a form view. This happens typically when a
+ // model has required fields
+ return $.Deferred().reject('None shall pass!');
+ }
+ return this._super(route, args);
+ },
+ intercepts: {
+ do_action: function (event) {
+ assert.strictEqual(event.data.action.views[0][0], 42,
+ "should do a do_action with view id 42");
+ },
+ },
+ });
+
+ var $cell = calendar.$('.fc-day-grid .fc-row:eq(2) .fc-day:eq(2)');
+ testUtils.triggerMouseEvent($cell, "mousedown");
+ testUtils.triggerMouseEvent($cell, "mouseup");
+ var $input = $('.modal-body input:first');
+ $input.val("It's just a fleshwound").trigger('input');
+ $('.modal button.btn:contains(Create)').trigger('click');
+
+ calendar.destroy();
+ });
+
+ QUnit.test('calendar fallback to form view id in action if necessary', function (assert) {
+ assert.expect(1);
+
+ var calendar = createView({
+ View: CalendarView,
+ model: 'event',
+ data: this.data,
+ arch: '<calendar class="o_calendar_test" '+
+ 'date_start="start" '+
+ 'date_stop="stop" '+
+ 'mode="month"> '+
+ '<field name="name"/>'+
+ '</calendar>',
+ archs: archs,
+ viewOptions: {
+ initialDate: initialDate,
+ action: {views: [[1, 'kanban'], [43, 'form']]}
+ },
+ mockRPC: function (route, args) {
+ if (args.method === "create") {
+ // we simulate here the case where a create call with just
+ // the field name fails. This is a normal flow, the server
+ // reject the create rpc (quick create), then the web client
+ // fall back to a form view. This happens typically when a
+ // model has required fields
+ return $.Deferred().reject('None shall pass!');
+ }
+ return this._super(route, args);
+ },
+ intercepts: {
+ do_action: function (event) {
+ assert.strictEqual(event.data.action.views[0][0], 43,
+ "should do a do_action with view id 43");
+ },
+ },
+ });
+
+ var $cell = calendar.$('.fc-day-grid .fc-row:eq(2) .fc-day:eq(2)');
+ testUtils.triggerMouseEvent($cell, "mousedown");
+ testUtils.triggerMouseEvent($cell, "mouseup");
+ var $input = $('.modal-body input:first');
+ $input.val("It's just a fleshwound").trigger('input');
+ $('.modal button.btn:contains(Create)').trigger('click');
+
+ calendar.destroy();
+ });
+
});
});
diff --git a/addons/web/static/tests/views/graph_tests.js b/addons/web/static/tests/views/graph_tests.js
index a13130e4b27..028eb4cb3a3 100644
--- a/addons/web/static/tests/views/graph_tests.js
+++ b/addons/web/static/tests/views/graph_tests.js
@@ -602,6 +602,22 @@ QUnit.module('Views', {
done();
});
});
+
+ QUnit.test('graph view crash when moving from search view using Down key', function (assert) {
+ assert.expect(1);
+
+ var graph = createView({
+ View: GraphView,
+ model: "foo",
+ data: this.data,
+ arch: '<graph string="Partners" type="pie">' +
+ '<field name="bar"/>' +
+ '</graph>',
+ });
+ graph.renderer.giveFocus();
+ assert.ok(true,"should not generate any error");
+ graph.destroy();
+ });
});
});
diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js
index 04003c568a3..04c1c603118 100644
--- a/addons/web/static/tests/views/kanban_tests.js
+++ b/addons/web/static/tests/views/kanban_tests.js
@@ -3273,6 +3273,132 @@ QUnit.module('Views', {
kanban.destroy();
});
+ // XXX test deactivated in saas~11.2 as kanban archiving has been removed (and readded in saas~11.4)
+ QUnit.skip('column progressbars on archiving records update counter', function (assert) {
+ assert.expect(4);
+
+ // add active field on partner model and make all records active
+ this.data.partner.fields.active = {string: 'Active', type: 'char', default: true};
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<kanban>' +
+ '<field name="active"/>' +
+ '<field name="bar"/>' +
+ '<field name="int_field"/>' +
+ '<progressbar field="foo" colors=\'{"yop": "success", "gnap": "warning", "blip": "danger"}\' sum_field="int_field"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div>' +
+ '<field name="name"/>' +
+ '</div>' +
+ '</t></templates>' +
+ '</kanban>',
+ groupBy: ['bar'],
+ });
+
+ assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_counter_side').text(), "36",
+ "counter should contain the correct value");
+ assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_counter_progress > .progress-bar:first').data('originalTitle'), "1 yop",
+ "the counter progressbars should be correctly displayed");
+
+ // archive all records of the second columns
+ kanban.$('.o_kanban_group:eq(1) .o_column_archive').click();
+
+ assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_counter_side').text(), "0",
+ "counter should contain the correct value");
+ assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_counter_progress > .progress-bar:first').data('originalTitle'), "0 yop",
+ "the counter progressbars should have been correctly updated");
+
+ kanban.destroy();
+ });
+
+ QUnit.skip('kanban with progressbars: correctly update env when archiving records', function (assert) {
+ assert.expect(3);
+
+ // add active field on partner model and make all records active
+ this.data.partner.fields.active = {string: 'Active', type: 'char', default: true};
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<kanban>' +
+ '<field name="active"/>' +
+ '<field name="bar"/>' +
+ '<field name="int_field"/>' +
+ '<progressbar field="foo" colors=\'{"yop": "success", "gnap": "warning", "blip": "danger"}\' sum_field="int_field"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div>' +
+ '<field name="name"/>' +
+ '</div>' +
+ '</t></templates>' +
+ '</kanban>',
+ groupBy: ['bar'],
+ intercepts: {
+ env_updated: function (ev) {
+ assert.step(ev.data.ids);
+ },
+ },
+ });
+
+ // archive all records of the first column
+ kanban.$('.o_kanban_group:first .o_column_archive').click();
+
+ assert.verifySteps([
+ [1, 2, 3, 4],
+ [1, 2, 3],
+ ]);
+
+ kanban.destroy();
+ });
+
+ QUnit.test('RPCs when (re)loading kanban view progressbars', function (assert) {
+ assert.expect(9);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<kanban>' +
+ '<field name="bar"/>' +
+ '<field name="int_field"/>' +
+ '<progressbar field="foo" colors=\'{"yop": "success", "gnap": "warning", "blip": "danger"}\' sum_field="int_field"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div>' +
+ '<field name="name"/>' +
+ '</div>' +
+ '</t></templates>' +
+ '</kanban>',
+ groupBy: ['bar'],
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ kanban.reload();
+
+ assert.verifySteps([
+ // initial load
+ 'read_group',
+ '/web/dataset/search_read',
+ '/web/dataset/search_read',
+ 'read_progress_bar',
+ // reload
+ 'read_group',
+ '/web/dataset/search_read',
+ '/web/dataset/search_read',
+ 'read_progress_bar',
+ ]);
+
+ kanban.destroy();
+ });
+
QUnit.test('drag & drop records grouped by m2o with progressbar', function (assert) {
assert.expect(4);
@@ -3342,7 +3468,7 @@ QUnit.module('Views', {
"Initial count should be Three");
kanban.$('.bg-success-full').click();
var lastCount = parseInt(kanban.$('.o_kanban_counter_side').eq(1).text());
- assert.strictEqual(lastCount, 0,
+ assert.strictEqual(lastCount, 1,
"kanban counters should vary according to what subgroup is selected");
kanban.destroy();
@@ -3638,6 +3764,34 @@ QUnit.module('Views', {
kanban.destroy();
});
+
+ QUnit.test('keyboard navigation on kanban when the focus is on a link that' +
+ 'has an action and the kanban has no oe_kanban_global_... class', function(assert){
+ assert.expect(1)
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test"><templates><t t-name="kanban-box">' +
+ '<div><a type="edit">Edit</a></div>' +
+ '</t></templates></kanban>',
+ });
+
+ testUtils.intercept(kanban, 'switch_view', function (event) {
+ assert.deepEqual(event.data, {
+ view_type: 'form',
+ res_id: 1,
+ mode: 'edit',
+ model: 'partner',
+ },'When selecting focusing a card and hitting ENTER, the first link or button is clicked');
+ });
+ kanban.$('.o_kanban_record').first().focus().trigger($.Event('keydown', {
+ keyCode: $.ui.keyCode.ENTER,
+ which: $.ui.keyCode.ENTER,
+ }));
+
+ kanban.destroy();
+ });
});
});
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index c948528dcc6..fb4ccee9f8b 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -2414,7 +2414,7 @@ QUnit.module('Views', {
// Press 'Tab' -> should go to next line
// add a value in the cell because the Tab on an empty first cell would activate the next widget in the view
- list.$('.o_selected_row input').val(11).trigger('input');
+ list.$('.o_selected_row input').val(11).trigger('input');
list.$('.o_selected_row input').trigger({type: 'keydown', which: 9});
assert.ok(list.$('.o_data_row:nth(3)').hasClass('o_selected_row'),
"fourth row should be in edition");
@@ -3431,6 +3431,47 @@ QUnit.module('Views', {
testUtils.unpatch(mixins.ParentedMixin);
});
+ QUnit.test('concurrent reloads finishing in inverse order', function (assert) {
+ assert.expect(3);
+
+ var blockSearchRead = false;
+ var def = $.Deferred();
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree><field name="foo"/></tree>',
+ mockRPC: function (route) {
+ var result = this._super.apply(this, arguments);
+ if (route === '/web/dataset/search_read' && blockSearchRead) {
+ return $.when(def).then(_.constant(result));
+ }
+ return result;
+ },
+ });
+
+ assert.strictEqual(list.$('.o_list_view .o_data_row').length, 4,
+ "list view should contain 4 records");
+
+ // reload with a domain (this request is blocked)
+ blockSearchRead = true;
+ list.reload({domain: [['foo', '=', 'yop']]});
+
+ assert.strictEqual(list.$('.o_list_view .o_data_row').length, 4,
+ "list view should still contain 4 records (search_read being blocked)");
+
+ // reload without the domain
+ blockSearchRead = false;
+ list.reload({domain: []});
+
+ // unblock the RPC
+ def.resolve();
+ assert.strictEqual(list.$('.o_list_view .o_data_row').length, 4,
+ "list view should still contain 4 records");
+
+ list.destroy();
+ });
+
QUnit.test('list view on a "noCache" model', function (assert) {
assert.expect(8);
diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js
index a1d9e1fae1f..4d7358f2574 100644
--- a/addons/web/static/tests/views/pivot_tests.js
+++ b/addons/web/static/tests/views/pivot_tests.js
@@ -1145,4 +1145,37 @@ QUnit.module('Views', {
pivot.destroy();
});
+
+ QUnit.test('parallel data loading should discard all but the last one', function (assert) {
+ assert.expect(2);
+
+ var def;
+
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot>' +
+ '<field name="foo" type="measure"/>' +
+ '</pivot>',
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'read_group') {
+ return $.when(def).then(_.constant(result));
+ }
+ return result;
+ },
+ });
+
+ def = $.Deferred();
+ pivot.update({groupBy: ['product_id']});
+ pivot.update({groupBy: ['product_id', 'customer']});
+ def.resolve();
+
+ assert.strictEqual(pivot.$('.o_pivot_cell_value').length, 6,
+ "should have 6 cells");
+ assert.strictEqual(pivot.$('tbody tr').length, 6,
+ "should have 6 rows");
+ pivot.destroy();
+ });
});});
diff --git a/addons/web/tests/test_js.py b/addons/web/tests/test_js.py
index f903fa704c5..04df8be91c1 100644
--- a/addons/web/tests/test_js.py
+++ b/addons/web/tests/test_js.py
@@ -32,6 +32,6 @@ class WebSuite(odoo.tests.HttpCase):
filename = asset['filename']
if not filename or asset['atype'] != 'text/javascript':
continue
- with open(filename, 'r') as fp:
- if RE_ONLY.search(fp.read()):
+ with open(filename, 'rb') as fp:
+ if RE_ONLY.search(fp.read().decode('utf-8')):
self.fail("`QUnit.only()` used in file %r" % asset['url'])
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index 8e13b294952..2cc10572543 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -534,6 +534,7 @@
<script type="text/javascript" src="/web/static/tests/chrome/action_manager_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/chrome/search_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/chrome/pager_tests.js"></script>
+ <script type="text/javascript" src="/web/static/tests/chrome/keyboard_navigation_mixin_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/widgets/data_export_tests.js"/>
<script type="text/javascript" src="/web/static/tests/widgets/debug_manager_tests.js"/>