diff options
| author | Vidhin Mehta (OpenERP) <vme@tinyerp.com> | 2011-09-14 11:07:46 +0530 |
|---|---|---|
| committer | Vidhin Mehta (OpenERP) <vme@tinyerp.com> | 2011-09-14 11:07:46 +0530 |
| commit | b6076c3cc3ca5141349b99821268b8fd5e148b77 (patch) | |
| tree | bb875d3ef5127cf5b8c35de6475de14e5477dbbd /addons/web/static/src | |
| parent | 243e1bdb0079ea9f214696ead8228d464ecb2cfc (diff) | |
| parent | c7c2b5846d3775780bbaa9a84a93ad755bdb8756 (diff) | |
[Merge] upto revno 1024.
bzr revid: vme@tinyerp.com-20110914053746-ri6d5s341mm6tc7n
Diffstat (limited to 'addons/web/static/src')
317 files changed, 13295 insertions, 0 deletions
diff --git a/addons/web/static/src/css/base-ie7.css b/addons/web/static/src/css/base-ie7.css new file mode 100644 index 00000000000..35599dbdd4b --- /dev/null +++ b/addons/web/static/src/css/base-ie7.css @@ -0,0 +1,4 @@ + +.openerp .menu span { + min-width: 0px; +} diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css new file mode 100644 index 00000000000..cc8b596e6f5 --- /dev/null +++ b/addons/web/static/src/css/base.css @@ -0,0 +1,1317 @@ +/* TODO: separate openerp web client page css from openerp views css */ +body.openerp { + padding: 0; + margin: 0; + height: 100%; + min-width: 1000px; + overflow-y: scroll; + font-size: 80%; +} + +body.openerp, .openerp textarea, .openerp input, .openerp select, .openerp option, .openerp button, .openerp .ui-widget { + font-family: Ubuntu, Helvetica, sans-serif; +} + +.oe_box { + border: 1px solid #aaf; + padding: 2px; + margin: 2px; +} + +#oe_header h2 { + margin: 2px 0; +} + +#oe_errors pre { + margin: 0; +} + +.openerp .oe-number { + text-align: right !important; +} +.openerp .oe_hide { + display: none !important; +} + +/* STATES */ +.openerp .on_logged, +.openerp .db_options_row { + display: none; +} + +/* Loading */ +.openerp .loading { + display: none; + z-index: 100; + position: fixed; + top: 0; + right: 50%; + padding: 4px 12px; + background: #A61300; + color: white; + text-align: center; + border: 1px solid #900; + border-top: none; + -moz-border-radius-bottomright: 8px; + -moz-border-radius-bottomleft: 8px; + border-bottom-right-radius: 8px; + border-bottom-left-radius: 8px; +} +.openerp .oe_notification { + z-index: 1001; + display: none; +} +.openerp .oe_notification * { + color: white; +} + +/* Login */ +.openerp .login { + display: none; +} +.openerp .login form { + float: left; + width: 420px; + margin-left: 40px; + margin-bottom: 60px; +} +.openerp .login fieldset { + padding-bottom: 5px; + min-width: 100px; + margin-top: 60px; + border-radius: 10px; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; +} +.openerp .login fieldset legend { + padding: 4px; +} +.openerp .login .oe_box2 { + padding: 5px 5px 20px 5px; +} +.openerp .login .oe_box2 table { + width: 100%; + border:none; +} +.openerp .login .oe_box2 td { + padding: 3px; + text-align: right; +} +.openerp .login .oe_box2 td input, +.openerp .login .oe_box2 td select { + width: 100%; +} +.openerp .login .oe_box2 td.oe_remember { + text-align:left; +} +.openerp .login .oe_box2 td.oe_remember input { + width: inherit; +} +.openerp .login .oe_login_right_pane { + padding:70px 35px 5px 10px; + min-width: 200px; + margin-left: 500px; +} +.openerp .login .login_error_message { + display: none; + background-color: #9A0404; + border-radius: 3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + color: white; + font-size: 16px; + font-weight: bold; + padding: 5px; + margin-top: 5px; + text-align: center; +} +.openerp .login.login_invalid .login_error_message { + display: block; +} + +.openerp.login-mode .login-container { + height: 100%; +} +.openerp.login-mode .login { + display: block; +} +.openerp.login-mode .menu, +.openerp.login-mode .secondary_menu, +.openerp.login-mode .oe-application, +.openerp.login-mode .db_options_row { + display: none; +} + +/* Database */ +.openerp.database_block .db_options_row { + height: 100%; + display: table-row; +} + +.openerp.database_block .menu, +.openerp.database_block .secondary_menu, +.openerp.database_block .oe-application, +.openerp.database_block .login-container { + display: none; +} + +.db_container { + width: 15%; + background: #666666; +} + +ul.db_options li { + padding: 5px 0 10px 5px; + background: #949292; /* Old browsers */ + background: -moz-linear-gradient(top, #949292 30%, #6d6b6b 95%, #282828 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(30%,#949292), color-stop(95%,#6d6b6b), color-stop(100%,#282828)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #949292 30%,#6d6b6b 95%,#282828 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #949292 30%,#6d6b6b 95%,#282828 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #949292 30%,#6d6b6b 95%,#282828 100%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#949292', endColorstr='#282828',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #949292 30%,#6d6b6b 95%,#282828 100%); /* W3C */ + /* for ie9 */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#949292', endColorstr='#5B5A5A',GradientType=0 ); /* IE6-9 */ + border: none; + /* overriding jquery ui */ + -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; + display: block; + font-weight: bold; + text-transform: uppercase; + margin: 1px; + color: #EEEEEE; + cursor: pointer; + width: 196px; + font-size: 12px; +} + +.db_option_table { + border: 1px solid #5A5858; + padding: 5px; + -moz-border-radius: 10px; +} + +table.db_option_table input.required { + background-color: #D2D2FF !important; +} + +.db_option_table input[type="text"], .db_option_table input[type="password"], .db_option_table select { + width: 300px; +} + +.option_string { + font-weight: bold; + color: #555; + width: 100%; + text-align: center; + padding: 10px 0; + font-size: large; +} + +label.error { + float: none; + color: red; + padding-left: .5em; + vertical-align: top; +} + +/* Main*/ +.openerp .main_table { + width: 100%; + height: 100%; + background: #f0eeee; +} + +/* Menu */ +.openerp .sf-menu { + margin-bottom: 0; +} +/* +.sf-menu a { + padding: 5px 5px; +} +*/ + +.openerp .menu { + height: 34px; + background: #cc4e45; /* Old browsers */ + background: -moz-linear-gradient(top, #cc4e45 0%, #b52d20 8%, #7a211a 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#cc4e45), color-stop(8%,#b52d20), color-stop(100%,#7a211a)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #cc4e45 0%,#b52d20 8%,#7a211a 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #cc4e45 0%,#b52d20 8%,#7a211a 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #cc4e45 0%,#b52d20 8%,#7a211a 100%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#CC4E45', endColorstr='#7A211A',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #cc4e45 0%,#b52d20 8%,#7a211a 100%); /* W3C */ +} +.openerp .menu td { + text-align: center; + padding:0; +} +.openerp .menu a { + display:block; + min-width: 60px; + height: 20px; + margin: 3px 2px; + padding: 0 8px; + + background: #bd5e54; /* Old browsers */ + background: -moz-linear-gradient(top, #bd5e54 0%, #90322a 60%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#bd5e54), color-stop(60%,#90322a)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #bd5e54 0%,#90322a 60%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #bd5e54 0%,#90322a 60%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #bd5e54 0%,#90322a 60%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#BD5E54', endColorstr='#90322A',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #bd5e54 0%,#90322a 60%); /* W3C */ + + border: 1px solid #6E2A24; + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + + color: #eee; + text-shadow: #222 0 1px 0; + text-decoration: none; + text-transform: uppercase; + line-height: 20px; + font-weight: bold; + font-size: 85%; + + white-space: nowrap; +} +.openerp .menu a:hover, +.openerp .menu a:focus, +.openerp .menu a.active { + background: #c6c6c6; /* Old browsers */ + background: -moz-linear-gradient(top, #c6c6c6 0%, #5c5c5c 7%, #969595 86%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#c6c6c6), color-stop(7%,#5c5c5c), color-stop(86%,#969595)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #c6c6c6 0%,#5c5c5c 7%,#969595 86%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #c6c6c6 0%,#5c5c5c 7%,#969595 86%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #c6c6c6 0%,#5c5c5c 7%,#969595 86%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#C6C6C6', endColorstr='#969595',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #c6c6c6 0%,#5c5c5c 7%,#969595 86%); /* W3C */ + /* for ie */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#5c5c5c', endColorstr='#969595',GradientType=0 ); /* IE6-9 */ + + color: #fff; +} +.openerp .oe-application-container { + height: 100%; +} +/* Secondary Menu */ +.openerp .secondary_menu { + width: 200px; + min-width: 200px; + border-right: 1px solid #3C3C3C; + border-bottom: 1px solid #5A5858; + background: #5A5858; + vertical-align: top; + height: 100%; +} +.openerp .secondary_menu .menu_content { + padding: 0; + border: none; + background: none; + overflow: hidden; +} +.openerp .secondary_menu h3 { + padding: 0 0 2px; + background: #949292; /* Old browsers */ + background: -moz-linear-gradient(top, #949292 0%, #6d6b6b 87%, #282828 99%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#949292), color-stop(87%,#6d6b6b), color-stop(99%,#282828)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #949292 0%,#6d6b6b 87%,#282828 99%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #949292 0%,#6d6b6b 87%,#282828 99%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #949292 0%,#6d6b6b 87%,#282828 99%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#949292', endColorstr='#282828',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #949292 0%,#6d6b6b 87%,#282828 99%); /* W3C */ + /* for ie9 */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#949292', endColorstr='#5B5A5A',GradientType=0 ); /* IE6-9 */ + border: none; + /* overriding jquery ui */ + -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; +} +.openerp .secondary_menu h4 { + padding: 0 0 2px 10px; + border: none; + background: none; +} +.openerp .secondary_menu h3 span, .openerp .secondary_menu h4 span { + left: 0 !important; +} +.openerp .secondary_menu a { + display: block; + height: 20px; + padding: 0 5px; + line-height: 20px; + white-space: nowrap; + color: white; + text-decoration: none; + text-shadow: 0 1px 0 #333; +} +.openerp .secondary_menu a.leaf:hover, +.openerp .secondary_menu a.leaf:active, +.openerp .secondary_menu a.leaf.active, +.openerp .secondary_menu h4:hover, +.openerp .secondary_menu h4:active, +.openerp .secondary_menu h4.active { + background: #ffffff; /* Old browsers */ + background: -moz-linear-gradient(top, #ffffff 0%, #d8d8d8 11%, #afafaf 86%, #333333 91%, #5a5858 96%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(11%,#d8d8d8), color-stop(86%,#afafaf), color-stop(91%,#333333), color-stop(96%,#5a5858)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%,#5a5858 96%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%,#5a5858 96%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%,#5a5858 96%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#FFFFFF', endColorstr='#5A5858',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%,#5a5858 96%); /* W3C */ + /* overriding jquery ui */ + -moz-border-radius-topright: 0; -webkit-border-top-right-radius: 0; border-top-right-radius: 0; + color: #3f3d3d; + text-shadow: #fff 0 1px 0; + border: none !important; +} + +.openerp .secondary_menu h4:hover a, +.openerp .secondary_menu h4:active a, +.openerp .secondary_menu h4.active a { + color: #3f3d3d; + text-shadow: #fff 0 1px 0; + border: none !important; +} +.openerp div.submenu_accordion div.menu_content a span { + padding-left: 20px; +} + +/* Header */ +.openerp .header { + height: 65px; + background: url("/web/static/src/img/header-background.png") repeat-x scroll left top transparent; + color: #FFFFFF; + letter-spacing: 0.5px; + text-shadow: 0 1px 0 #333333; +} +.openerp .company_logo_link { + display: block; + float: left; + height: 63px; + width: 200px; + border: 1px solid white; + border-right-color: black; + border-bottom-color: black; + background: #FFFFFF; + background: -moz-linear-gradient(top, #FFFFFF 0%, #CECECE 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFFFFF), color-stop(100%,#CECECE)); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#FFFFFF', endColorstr='#CECECE',GradientType=0 ); +} +.openerp .company_logo { + margin-top: 7px; + margin-left: 10px; + display: block; + background: url(/web/static/src/img/logo.png); + width:180px; + height:46px; +} +.openerp .header_title { + float: left; + font-size: 100%; + margin: 0; + padding: 4px 10px; + text-shadow: 0 1px 0 #111111; +} +.openerp .header_title small { + color: #ccc; + font-size: 90%; + font-weight: normal; +} +.openerp .header_corner { + float: right; +} +.openerp .header_corner .block { + float: left; + height: 34px; + line-height: 34px; + /*background: url(../images/top-sep-a.png) no-repeat;*/ + border-left: 1px solid #6a6a6a; + background: #828282; + background: -moz-linear-gradient(top, #828282 0%, #4D4D4D 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#828282), color-stop(100%,#4D4D4D)); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#828282', endColorstr='#4D4D4D',GradientType=0 ); +} +.openerp .header_corner .block a { + display: block; + color: white; + text-decoration: none; + padding: 0 10px; +} +.openerp .header_corner .block a:hover { + background: #929292; + background: -moz-linear-gradient(top, #929292 0%, #4D4D4D 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#929292), color-stop(100%,#4D4D4D)); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#929292', endColorstr='#4D4D4D',GradientType=0 ); +} +.openerp .header_corner ul.block { + /*float: left;*/ + list-style: none; + height: 34px; + margin: 0; + padding: 0 0 0 2px; + /*background: url(../images/top-sep-a.png) no-repeat;*/ + line-height: 33px; + /*font-size: 1em;*/ + /*text-transform: uppercase;*/ +} +.openerp .header_corner ul.block li { + float: left; +} +.openerp .header_corner ul.block li a { + padding: 0 5px; + position: relative; + line-height: 32px; +} +.openerp .header_corner ul.block li a img { + vertical-align: middle; +} +.openerp .header_corner ul.block li a small { + position: absolute; + right: 0; + top: 5px; + padding: 1px 4px 2px; + background: rgba(0, 0, 0, 0.75); + border-radius: 7px; + -moz-border-radius: 7px; + -webkit-border-radius: 7px; + line-height: 1em; + font-weight: bold; +} + +/* Footer */ +.openerp div.oe_footer { + background: none repeat scroll 0 0 #CCCCCC; + overflow: hidden; + padding: 5px 0; + position: relative; + -moz-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.4); + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.4); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.4); +} +.openerp div.oe_footer p.oe_footer_powered { + left: 50%; + margin: 0; + padding: 0 15px; + color: #666666; + font-weight: bold; + font-size: 0.8em; + text-align: center; +} +.openerp div.oe_footer p.oe_footer_powered a { + text-decoration: none; + color: #666666; +} + +/* Main Application */ +.openerp .oe-main-content, +.openerp .oe-application { + padding: 0; + height: 100%; +} + +.openerp h2.oe_view_title { + font-size: 175%; + font-weight: normal; + margin: 2px 0; + color: #252424; + text-shadow: white 0 1px 0; +} + +/* View Manager */ +.openerp .oe_vm_switch { + float: right; +} + +/* SearchView */ +.openerp .filter_label, .openerp .filter_icon { + border: 1px solid #666; + border-left-width: 0; + background: #F0F0F0; + background: -moz-linear-gradient(top, #F0F0F0 0%, #C0C0C0 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#F0F0F0), color-stop(100%,#C0C0C0)); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#F0F0F0', endColorstr='#C0C0C0',GradientType=0 ); +} +.openerp .filter_label:hover, .openerp .filter_icon:hover { + background: #F0F0F0; + background: -moz-linear-gradient(top, #F0F0F0 0%, #A1A7CE 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#F0F0F0), color-stop(100%,#A1A7CE)); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#F0F0F0', endColorstr='#A1A7CE',GradientType=0 ); +} +.openerp .filter_label:active, .openerp .filter_icon:active { + background: #AAAAAA; + background: -moz-linear-gradient(top, #999999 0%, #EEEEEE 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#999999), color-stop(100%,#EEEEEE)); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#999999', endColorstr='#EEEEEE',GradientType=0 ); +} +.openerp .filter_label.enabled, .openerp .filter_icon.enabled { + background: #AAAAAA; +} +.openerp .filter_icon { + height: 22px; + padding: 1px 2px 0 2px; + margin: 0; + vertical-align: bottom; +} +.openerp .filter_label { + font-weight: bold; + text-transform: uppercase; + text-shadow: #EEE 0 1px 0; + color: #4C4C4C; + white-space: nowrap; + min-height: 40px; + min-width: 75px; + padding: 2px 4px; + margin: 0; +} +.openerp .filter_label_group { + padding-right: 0.4em; + white-space: nowrap; +} +.openerp .filter_label_group button:first-child { + border-left: 1px solid #666; + -webkit-border-top-left-radius: 7px; + -webkit-border-bottom-left-radius: 7px; + -moz-border-radius-topleft: 7px; + -moz-border-radius-bottomleft: 7px; + border-top-left-radius: 7px; + border-bottom-left-radius: 7px; +} +.openerp .filter_label_group button:last-child { + -webkit-border-top-right-radius: 7px; + -webkit-border-bottom-right-radius: 7px; + -moz-border-radius-topright: 7px; + -moz-border-radius-bottomright: 7px; + border-top-right-radius: 7px; + border-bottom-right-radius: 7px; +} + +.openerp .searchview_group_string { + display: block; + color: #7D7979; + font-weight: bold; + padding: 2px 0 2px 10px; + margin-top: 4px; + text-decoration: none; +} +.openerp .searchview_group_string:hover { + background-color: #ccc; +} +.openerp .searchview_group.folded .searchview_group_string { + background: url("/web/static/src/img/ui/group-folded.png") no-repeat scroll 0 50%; +} +.openerp .searchview_group.folded .searchview_group_content { + display: none; +} +.openerp .searchview_group.expanded .searchview_group_string { + background: url("/web/static/src/img/ui/group-expanded.png") no-repeat scroll 0 50%; +} +.openerp .searchview_group.expanded .searchview_group_content { + display: block; +} +.openerp .searchview_group_content { + padding-left: 10px; +} + +.openerp .searchview_extended_group { + padding: 3px; + margin: 2px; +} + +.openerp .searchview_extended_group .oe_adv_filters_and { + border-bottom: 1px solid #8E8E8E; + text-align: center; + margin-top: -10px; +} +.openerp .searchview_extended_group .oe_adv_filters_and span { + background: #F0EEEE; + position: relative; + top: 0.5em; + padding: 0 1em 0 1em; + color: #8E8E8E; +} + +.openerp .searchview_extended_group.last_group .oe_adv_filters_and { + display: none; +} + +.openerp .oe_search-view-custom-filter-btn span { + background: url(/web/static/src/img/icons/gtk-add.png) repeat-y; + padding-left: 18px; +} + +.openerp .searchview_extended_add_proposition span { + font-size: 0.9em; + background: url(/web/static/src/img/icons/gtk-add.png) repeat-y; + padding-left: 18px; +} + +.openerp .searchview_extended_delete_group { + float:right; + display: none; +} + +.openerp .searchview_extended_delete_prop { + text-decoration: none; +} + +.openerp .searchview_extended_delete_group span, +.openerp .searchview_extended_delete_prop span { + font-size: 0.9em; + background: url(/web/static/src/img/icons/gtk-close.png) repeat-y; + padding-left: 18px; +} +/* List */ +.openerp .oe-listview table { + clear: right; + width: 100%; + border-spacing: 0; + border: 1px solid silver; +} + +.openerp .oe-listview thead table { + width: 100%; + border: none; +} +.openerp .oe-listview tr.odd { + background-color: #f3f3f3; +} +.openerp .oe-listview tbody tr:hover { + background-color: #ecebf2; +} +.openerp .oe-listview tbody tr:hover { + background-color: #eae9f0; +} +.openerp .oe-listview thead table tr, +.openerp .oe-listview thead table tr:hover { + background: none; +} + +.openerp .oe-listview td, +.openerp .oe-listview th { + vertical-align: middle; + text-align: left; +} +.openerp .oe-listview td.oe-record-delete { + text-align: right; +} +.openerp .oe-listview th.oe-sortable { + cursor: pointer; +} +.openerp .oe-listview th.oe-sortable .ui-icon { + height: 1em; + display: inline; + display: inline-block; +} + +.openerp .oe-listview .oe-field-cell { + cursor: pointer; +} +.openerp .oe-listview .oe-field-cell button { + padding: 0; + border: none; + background: none; + width: 100%; +} +.openerp .oe-listview .oe-field-cell button:active { + opacity: 0.5; +} +.openerp .oe-listview .oe-field-cell button img { + cursor: pointer; +} +.openerp .oe-listview .oe-field-cell button img:hover { + opacity: 0.75; +} + +.openerp .oe-listview th.oe-actions { + text-align: left; + white-space: nowrap; +} +.openerp .oe-listview th.oe-list-pager { + text-align: right; + white-space: nowrap; +} +.openerp .oe-list-pager .oe-pager-state { + cursor: pointer; +} + +.openerp .oe-listview .oe-group-name { + padding-right: 1em; +} +.openerp .oe-listview .oe-group-name, +.openerp .oe-listview .oe-group-pagination { + white-space: nowrap; +} + +.openerp .oe-listview tfoot td { + padding: 3px 3px 0; +} +.openerp .oe-listview .oe-list-footer { + text-align: center; + white-space: nowrap; +} +.openerp .oe-listview .oe-list-footer span { + margin: 0 1em; +} + +/** list rounded corners + + rounded corners are a pain on tables: need to round not only table, but + also on the first and last children of the first and last row + */ +.openerp .oe-listview table.oe-listview-content { + -webkit-border-radius: 7px; + -moz-border-radius: 7px; + border-radius: 7px; +} +.openerp .oe-listview table.oe-listview-content thead tr:first-child th:first-child { + -webkit-border-top-left-radius: 7px; + -moz-border-radius-topleft: 7px; + border-top-left-radius: 7px; +} +.openerp .oe-listview table.oe-listview-content thead tr:first-child th:last-child { + -webkit-border-top-right-radius: 7px; + -moz-border-radius-topright: 7px; + border-top-right-radius: 7px; +} +.openerp .oe-listview table.oe-listview-content tfoot td:first-child, +.openerp .oe-listview table.oe-listview-content tbody:last-child tr:last-child th:first-child { + -webkit-border-bottom-left-radius: 7px; + -moz-border-radius-bottomleft: 7px; + border-bottom-left-radius: 7px; +} +.openerp .oe-listview table.oe-listview-content tfoot td:last-child, +.openerp .oe-listview table.oe-listview-content tbody:last-child tr:last-child td:last-child { + -webkit-border-bottom-right-radius: 7px; + -moz-border-radius-bottomright: 7px; + border-bottom-right-radius: 7px; +} + +/* Notebook */ +.openerp .oe_form_notebook { + padding: 0; + background: none; + border-width: 0; +} +.openerp .oe_form_notebook ul.ui-tabs-nav { + padding-left: 0; + background: transparent; + border-width: 0 0 1px 0; + border-radius: 0; + -moz-border-radius: 0; + -webkit-border-radius: 0; + line-height: 0.5em; +} +.openerp .oe_form_notebook ul.ui-tabs-nav li { + font-weight: bold; +} +.openerp .oe_form_notebook .ui-tabs-panel { + background: #f9f9f9; + border-width: 0 1px 1px 1px; +} +.openerp .oe_form_notebook .ui-tabs-selected { + background: #f9f9f9; +} + +/* Form */ +.openerp table.oe_frame td { + color: #4c4c4c; +} +.openerp td.oe_form_frame_cell { + padding: 2px; +} +.openerp td.oe_form_frame_cell.oe_form_group { + padding: 0; +} +.openerp .required.error { + border: 1px solid #900; +} +.openerp .oe_form_buttons, .openerp .oe_list_buttons { + float: left; +} +.openerp .oe_form_pager, .openerp .oe_list_pager { + float: right; +} + +.openerp label.oe_label_help, .openerp label.oe_label { + display: block; +} +.openerp label.oe_label_help { + cursor: help; +} +.openerp label.oe_label, +.openerp label.oe_label_help { + text-align: right; + margin: 3px 0 0 10px; +} +.openerp label.oe_label_help span { + font-size: 80%; + color: darkgreen; + vertical-align:top; + position: relative; + top: -4px; + padding: 0 2px; +} + +/* Inputs */ +.openerp .oe_forms input[type="text"], .openerp .oe_forms input[type="password"], .openerp .oe_forms select, .openerp .oe_forms textarea { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + padding: 0 2px 0 2px; + border: 1px solid #999; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + background: white; + min-width: 90px; + color: #1f1f1f; +} +.openerp .oe_forms textarea { + resize:vertical; +} +.openerp .oe_forms input[type="text"], .openerp .oe_forms input[type="password"], .openerp .oe_forms select, .openerp .oe_forms .button { + height: 22px; +} +.openerp .oe_forms input.field_datetime { + min-width: 11em; +} +.openerp .oe_forms .button { + color: #4c4c4c; + white-space: nowrap; +} +.openerp .oe_forms .button span { + position: relative; + vertical-align: top; +} +.openerp img.ui-datepicker-trigger { + margin-left: -20px; + vertical-align: middle; + cursor: pointer; + position: relative; + top: -1px; +} +.openerp img.oe_field_translate { + margin-left: -21px; + vertical-align: top; + cursor: pointer; + position: relative; + top: 4px; +} +.openerp .oe_trad_field.touched { + border: 1px solid green !important; +} + +/* http://www.quirksmode.org/dom/inputfile.html + * http://stackoverflow.com/questions/2855589/replace-input-type-file-by-an-image + */ +.openerp .oe-binary-file-set { + overflow: hidden; + position: relative; +} +.openerp input.oe-binary-file { + z-index: 2; + line-height: 0; + font-size: 50px; + position: absolute; + /* Should be adjusted for all browsers */ + top: -2px; + left: -700px; + opacity: 0; + filter: alpha(opacity = 0); + -ms-filter: "alpha(opacity=0)"; + margin: 0; + padding:0; +} + +/* Widgets */ +.openerp .separator { + border: 0 solid #666; +} +.openerp .separator.horizontal { + font-weight: bold; + border-bottom-width: 1px; + margin: 3px 4px 3px 1px; + height: 15px; +} +.openerp .oe_form_frame_cell.oe_form_separator_vertical { + border-left: 1px solid #666; +} +.openerp td.required input, .openerp td.required select { + background-color: #D2D2FF !important; +} +.openerp td.invalid input, .openerp td.invalid select, .openerp td.invalid textarea { + background-color: #F66 !important; + border: 1px solid #D00 !important; +} +.openerp div.oe-progressbar span { + position: absolute; + margin-left: 10px; + margin-top: 5px; + font-weight: bold; +} + +/* jQuery UI override */ +.openerp .ui-widget { + font-size: 1em; +} +.openerp .oe_form_field_progressbar .ui-progressbar { + height: 22px; + font-size: 10px; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + border: 1px solid #999; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + background: white; + min-width: 90px; +} + +/* Sidebar */ +.openerp .view-manager-main-table { + margin: 0; + width:100%; + border-collapse:collapse; + height:100%; +} + +.openerp .view-manager-main-table tbody { + vertical-align: top; +} + +.openerp .view-manager-main-content { + width: 100%; + padding: 6px; +} + +.openerp .view-manager-main-sidebar { + width: 180px; + padding: 0; + margin: 0; +} + +.openerp .sidebar-main-div { + height: 100%; + border-left: 1px solid #D2CFCF; +} + +.openerp .sidebar-content { + padding: 0; + margin: 0; + width: 180px; + height: 100%; + font-size: 0.9em; +} + +.openerp .closed-sidebar .sidebar-content { + width: 22px; +} + +.openerp .closed-sidebar .sidebar-content { + display: none; +} + +.openerp .sidebar-main-div a { + color: #555; + text-decoration: none; +} + +.openerp .sidebar-main-div a:hover { + color: black; +} + +.openerp .oe-sidebar-attachments-toolbar { + margin: 4px 0 0 4px; +} +.openerp .oe-sidebar-attachments-items { + clear: both; + padding-top: 5px !important; +} +.openerp .oe-sidebar-attachments-items li { + position: relative; + padding: 0 0 3px 10px !important; +} +.openerp .oe-sidebar-attachments-items li:hover { + background: #ddd; +} +.openerp .oe-sidebar-attachments-link { + display: block; + margin-right: 15px; + overflow: hidden; +} +.openerp .oe-sidebar-attachment-delete { + position: absolute; + right: 2px; + top: 1px; + overflow: hidden; + width: 15px; + height: 15px; + padding: 1px; + border-radius: 7px; + -moz-border-radius: 7px; + -webkit-border-radius: 7px; +} +.openerp .oe-sidebar-attachment-delete:hover { + background-color: white; +} + +.openerp .view-manager-main-sidebar h2 { + margin:0; + font-size: 1.15em; + color: #8E8E8E; + text-shadow: white 0 1px 0; + padding-left: 10px; + padding-right: 21px; + height: 21px; + + background: #ffffff; /* Old browsers */ + background: -moz-linear-gradient(top, #ffffff 0%, #ebe9e9 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(100%,#ebe9e9)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#FFFFFF', endColorstr='#EBE9E9',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* W3C */ + + border: 1px solid #D2CFCF; + border-right-width: 0; + border-left-width: 0; +} + +.openerp .view-manager-main-sidebar ul { + list-style-type: none; + margin: 0; + padding: 0; + display: block; +} + +.openerp .view-manager-main-sidebar li { + display: block; + padding: 3px 3px 3px 10px; +} + +.openerp .toggle-sidebar { + cursor: pointer; + border: 1px solid #D2CFCF; + display: block; + background: url(/web/static/src/img/toggle-a-bg.png); + width: 21px; + height: 21px; + z-index: 10; +} +.openerp .open-sidebar .toggle-sidebar { + margin-left: 158px; + background-position: 21px 0; + position: absolute; +} +.openerp .closed-sidebar .toggle-sidebar { + border-left: none; +} +.openerp li.oe_sidebar_print { + padding-left: 20px; + background: 1px 3px url(/web/static/src/img/icons/gtk-print.png) no-repeat; +} + +.openerp.kitten-mode-activated .main_table { + background: url(http://placekitten.com/g/1500/800) repeat; +} + +.openerp.kitten-mode-activated .header { + background: url(http://placekitten.com/g/211/65) repeat; +} + +.openerp.kitten-mode-activated .secondary_menu { + background: url(http://placekitten.com/g/212/100) repeat; +} + +.openerp.kitten-mode-activated .menu { + background: #828282; + background: -moz-linear-gradient(top, #828282 0%, #4D4D4D 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#828282), color-stop(100%,#4D4D4D)); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#828282', endColorstr='#4D4D4D',GradientType=0 ); +} +.openerp.kitten-mode-activated .menu a { + background: none; +} +.openerp.kitten-mode-activated .menu span { + background: none; +} +.openerp.kitten-mode-activated .sidebar-content li a, +.openerp.kitten-mode-activated .oe-application .view-manager-main-content h2.oe_view_title, +.openerp.kitten-mode-activated .oe-application .view-manager-main-content a.searchview_group_string, +.openerp.kitten-mode-activated .oe-application .view-manager-main-content label { + color: white; +} +.openerp.kitten-mode-activated .menu, +.openerp.kitten-mode-activated .header_corner, +.openerp.kitten-mode-activated .header_title, +.openerp.kitten-mode-activated .secondary_menu div, +.openerp.kitten-mode-activated .oe-application, +.openerp.kitten-mode-activated .oe_footer, +.openerp.kitten-mode-activated .loading, +.openerp.kitten-mode-activated .ui-dialog { + opacity:0.8; + filter:alpha(opacity=80); +} +.openerp.kitten-mode-activated .header .company_logo { + background: url(http://placekitten.com/g/180/46); +} +.openerp.kitten-mode-activated .loading { + background: #828282; + border-color: #828282; +} + +.openerp .oe-m2o { + padding-right: 20px; + white-space: nowrap; +} +.openerp .oe_forms .oe-m2o input[type="text"] { + padding-right: 20px; +} +.openerp .oe-m2o-drop-down-button { + margin-left: -23px; +} +.openerp .oe-m2o-drop-down-button img, +.openerp .oe-m2o-cm-button img { + margin-bottom: -4px; + cursor: pointer; +} +.openerp .oe-m2o-cm-button img { + margin-left: 4px; +} +.openerp .oe-m2o-disabled-cm { + color: grey; +} +.parent_top { + vertical-align: text-top; +} + +.openerp .oe-dialog-warning p { + padding-left: 1em; + font-size: 1.2em; + font-weight: bold; +} + +.openerp .dhx_mini_calendar { + -moz-box-shadow: none; + -khtml-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} +.openerp .oe-treeview-table { + width: 100%; +} +.treeview-tr, .treeview-td { + cursor: pointer; + vertical-align: top; + text-align: left; +} +.treeview-tr span, .treeview-td span { + display: block; +} +.treeview-tr:first-of-type { + background: transparent url(/web/static/src/img/expand.gif) 0 50% no-repeat; +} +.oe-open .treeview-tr:first-of-type { + background-image: url(/web/static/src/img/collapse.gif); +} +.treeview-tr:first-of-type span, +.treeview-td:first-of-type span { + margin-left: 16px; +} + +.treeview-header { + text-align: left; + vertical-align: top; +} +/* Shortcuts*/ +.oe-shortcut-toggle { + height: 20px; + padding: 0; + width: 24px; + cursor: pointer; + display: block; + background: url(/web/static/src/img/add-shortcut.png) no-repeat bottom; + float: left; +} +.oe-shortcut-remove{ + background: url(/web/static/src/img/remove-shortcut.png) no-repeat bottom; +} +/* ================ */ +.oe-shortcuts { + position: absolute; + + margin: 0; + padding: 6px 15px; + top: 37px; + left: 197px; + right: 0; + height: 17px; + line-height: 1.2; +} +.oe-shortcuts ul { + display: block; + overflow: hidden; + list-style: none; + white-space: nowrap; + padding: 0; + margin: 0; +} +.oe-shortcuts li { + cursor: pointer; + display: -moz-inline-stack; + display: inline-block; + display: inline; /*IE7 */ + color: #fff; + text-align: center; + border-left: 1px solid #909090; + padding: 4px; + font-size: 90%; + font-weight: normal; +} +.oe-shortcuts li:first-child { + border-left: none; + padding-left: 0; +} + +ul.oe-arrow-list { + padding-left: 0.5em; + margin: 0; +} +ul.oe-arrow-list li { + display: inline-block; + margin-left: -0.5em; +} +ul.oe-arrow-list li span { + vertical-align: top; + display: inline-block; + border-width:1em; + border-style:solid; + border-color: white; + line-height:0em; +} +ul.oe-arrow-list .oe-arrow-list-before { + border-left-color: rgba(0,0,0,0); + border-right-width:0; +} +ul.oe-arrow-list .oe-arrow-list-after { + border-color: rgba(0,0,0,0); + border-left-color: white; + border-right-width:0; +} +ul.oe-arrow-list li.oe-arrow-list-selected span { + border-color: #CFCCCC; +} +ul.oe-arrow-list li.oe-arrow-list-selected .oe-arrow-list-before { + border-left-color: rgba(0,0,0,0); +} +ul.oe-arrow-list li.oe-arrow-list-selected .oe-arrow-list-after { + border-color: rgba(0,0,0,0); + border-left-color: #CFCCCC; +} diff --git a/addons/web/static/src/css/data_export.css b/addons/web/static/src/css/data_export.css new file mode 100644 index 00000000000..149fcea81e6 --- /dev/null +++ b/addons/web/static/src/css/data_export.css @@ -0,0 +1,84 @@ +.openerp .oe-export { + width: 100%; +} +.openerp .oe_export_row tr{ + background-color: #FFFFFF; + font-size: 0.8em; + height: 22px; +} + +.openerp tr.ui-selected td { + background-color: #CCCCCC; +} + +.openerp .oe_export_requiredfield { + background-color: #D2D2FF; +} + +.openerp .oe_export_readonlyfield{ + background-color: #999999; +} + +.openerp .oe_export_row:hover{ + background-color: #F3F3F3; +} + +.openerp .oe_export_fields_selector_left { + width: 50%; +} + +.openerp div#left_field_panel { + overflow: scroll; + width: 100%; + height: 400px; + border: solid #999999 1px; +} +.openerp div#left_field_panel table { + width: 100%; +} + +.openerp .oe_export_fields_selector_center { + text-align: center; +} +.openerp .oe_export_fields_selector_center button { + white-space: nowrap; +} + +.openerp .oe_export_fields_selector_right { + width: 45%; + height: 400px; +} + +.openerp .oe_export_fields_selector_right select{ + width: 100%; + height: 100%; +} + +.openerp .oe_export_tree_header{ + border: 0.5px solid #E3E3E3; + text-align: left; + white-space: nowrap; + padding: 4px 5px; + background: url(/web/static/src/img/header.gif); +} + + +.openerp table.tree_grid{ + border: 1px solid #E3E3E3; + text-align: left; + white-space: nowrap; + background-color:#E3E3E3; + border-collapse: collapse; + width: 100%; +} + +.openerp table.tree_grid a:hover { + color: blue; + border: none; +} + +.openerp table.tree_grid a { + color: #5F5C5C; + border: none; + display: block; +} diff --git a/addons/web/static/src/img/accessories-archiver.png b/addons/web/static/src/img/accessories-archiver.png Binary files differnew file mode 100644 index 00000000000..8bcbe4566bc --- /dev/null +++ b/addons/web/static/src/img/accessories-archiver.png diff --git a/addons/web/static/src/img/add-shortcut.png b/addons/web/static/src/img/add-shortcut.png Binary files differnew file mode 100644 index 00000000000..bcc0a09eb45 --- /dev/null +++ b/addons/web/static/src/img/add-shortcut.png diff --git a/addons/web/static/src/img/attachments-close.png b/addons/web/static/src/img/attachments-close.png Binary files differnew file mode 100644 index 00000000000..14d645534b8 --- /dev/null +++ b/addons/web/static/src/img/attachments-close.png diff --git a/addons/web/static/src/img/collapse.gif b/addons/web/static/src/img/collapse.gif Binary files differnew file mode 100644 index 00000000000..759e72ebcfe --- /dev/null +++ b/addons/web/static/src/img/collapse.gif diff --git a/addons/web/static/src/img/db.png b/addons/web/static/src/img/db.png Binary files differnew file mode 100644 index 00000000000..83e42df26b3 --- /dev/null +++ b/addons/web/static/src/img/db.png diff --git a/addons/web/static/src/img/down-arrow.png b/addons/web/static/src/img/down-arrow.png Binary files differnew file mode 100644 index 00000000000..b1288e52682 --- /dev/null +++ b/addons/web/static/src/img/down-arrow.png diff --git a/addons/web/static/src/img/expand.gif b/addons/web/static/src/img/expand.gif Binary files differnew file mode 100644 index 00000000000..ca98d0b5f2b --- /dev/null +++ b/addons/web/static/src/img/expand.gif diff --git a/addons/web/static/src/img/favicon.ico b/addons/web/static/src/img/favicon.ico Binary files differnew file mode 100644 index 00000000000..a323cc21c00 --- /dev/null +++ b/addons/web/static/src/img/favicon.ico diff --git a/addons/web/static/src/img/header-about.png b/addons/web/static/src/img/header-about.png Binary files differnew file mode 100644 index 00000000000..b53e9a86ebe --- /dev/null +++ b/addons/web/static/src/img/header-about.png diff --git a/addons/web/static/src/img/header-background.png b/addons/web/static/src/img/header-background.png Binary files differnew file mode 100644 index 00000000000..774b3954124 --- /dev/null +++ b/addons/web/static/src/img/header-background.png diff --git a/addons/web/static/src/img/header-help.png b/addons/web/static/src/img/header-help.png Binary files differnew file mode 100644 index 00000000000..19e9906fdef --- /dev/null +++ b/addons/web/static/src/img/header-help.png diff --git a/addons/web/static/src/img/header-home.png b/addons/web/static/src/img/header-home.png Binary files differnew file mode 100644 index 00000000000..fd96742d15a --- /dev/null +++ b/addons/web/static/src/img/header-home.png diff --git a/addons/web/static/src/img/header-preferences.png b/addons/web/static/src/img/header-preferences.png Binary files differnew file mode 100644 index 00000000000..6c978b49f55 --- /dev/null +++ b/addons/web/static/src/img/header-preferences.png diff --git a/addons/web/static/src/img/header-requests.png b/addons/web/static/src/img/header-requests.png Binary files differnew file mode 100644 index 00000000000..6577e7e6448 --- /dev/null +++ b/addons/web/static/src/img/header-requests.png diff --git a/addons/web/static/src/img/header.gif b/addons/web/static/src/img/header.gif Binary files differnew file mode 100644 index 00000000000..f40bd9c0f6a --- /dev/null +++ b/addons/web/static/src/img/header.gif diff --git a/addons/web/static/src/img/icons/STOCK_ABOUT.png b/addons/web/static/src/img/icons/STOCK_ABOUT.png Binary files differnew file mode 100644 index 00000000000..1b430a35758 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_ABOUT.png diff --git a/addons/web/static/src/img/icons/STOCK_ADD.png b/addons/web/static/src/img/icons/STOCK_ADD.png Binary files differnew file mode 100644 index 00000000000..70612c181c6 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_ADD.png diff --git a/addons/web/static/src/img/icons/STOCK_APPLY.png b/addons/web/static/src/img/icons/STOCK_APPLY.png Binary files differnew file mode 100644 index 00000000000..116f2e433bb --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_APPLY.png diff --git a/addons/web/static/src/img/icons/STOCK_BOLD.png b/addons/web/static/src/img/icons/STOCK_BOLD.png Binary files differnew file mode 100644 index 00000000000..408f25aa20b --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_BOLD.png diff --git a/addons/web/static/src/img/icons/STOCK_CANCEL.png b/addons/web/static/src/img/icons/STOCK_CANCEL.png Binary files differnew file mode 100644 index 00000000000..8cf6273281b --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_CANCEL.png diff --git a/addons/web/static/src/img/icons/STOCK_CDROM.png b/addons/web/static/src/img/icons/STOCK_CDROM.png Binary files differnew file mode 100644 index 00000000000..27e8edd8a0b --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_CDROM.png diff --git a/addons/web/static/src/img/icons/STOCK_CLEAR.png b/addons/web/static/src/img/icons/STOCK_CLEAR.png Binary files differnew file mode 100644 index 00000000000..b2874fb84fa --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_CLEAR.png diff --git a/addons/web/static/src/img/icons/STOCK_CLOSE.png b/addons/web/static/src/img/icons/STOCK_CLOSE.png Binary files differnew file mode 100644 index 00000000000..4966974e178 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_CLOSE.png diff --git a/addons/web/static/src/img/icons/STOCK_COLOR_PICKER.png b/addons/web/static/src/img/icons/STOCK_COLOR_PICKER.png Binary files differnew file mode 100644 index 00000000000..232d54f8f3b --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_COLOR_PICKER.png diff --git a/addons/web/static/src/img/icons/STOCK_CONNECT.png b/addons/web/static/src/img/icons/STOCK_CONNECT.png Binary files differnew file mode 100644 index 00000000000..c820b54abcc --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_CONNECT.png diff --git a/addons/web/static/src/img/icons/STOCK_CONVERT.png b/addons/web/static/src/img/icons/STOCK_CONVERT.png Binary files differnew file mode 100644 index 00000000000..6288b964cc0 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_CONVERT.png diff --git a/addons/web/static/src/img/icons/STOCK_COPY.png b/addons/web/static/src/img/icons/STOCK_COPY.png Binary files differnew file mode 100644 index 00000000000..3d68da606d2 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_COPY.png diff --git a/addons/web/static/src/img/icons/STOCK_CUT.png b/addons/web/static/src/img/icons/STOCK_CUT.png Binary files differnew file mode 100644 index 00000000000..96681d26968 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_CUT.png diff --git a/addons/web/static/src/img/icons/STOCK_DELETE.png b/addons/web/static/src/img/icons/STOCK_DELETE.png Binary files differnew file mode 100644 index 00000000000..879d6fd1cbe --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_DELETE.png diff --git a/addons/web/static/src/img/icons/STOCK_DIALOG_AUTHENTICATION.png b/addons/web/static/src/img/icons/STOCK_DIALOG_AUTHENTICATION.png Binary files differnew file mode 100644 index 00000000000..eb411112f83 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_DIALOG_AUTHENTICATION.png diff --git a/addons/web/static/src/img/icons/STOCK_DIALOG_ERROR.png b/addons/web/static/src/img/icons/STOCK_DIALOG_ERROR.png Binary files differnew file mode 100644 index 00000000000..e97bc43504b --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_DIALOG_ERROR.png diff --git a/addons/web/static/src/img/icons/STOCK_DIALOG_INFO.png b/addons/web/static/src/img/icons/STOCK_DIALOG_INFO.png Binary files differnew file mode 100644 index 00000000000..ed46c3ecdb3 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_DIALOG_INFO.png diff --git a/addons/web/static/src/img/icons/STOCK_DIALOG_QUESTION.png b/addons/web/static/src/img/icons/STOCK_DIALOG_QUESTION.png Binary files differnew file mode 100644 index 00000000000..ef68f43f43f --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_DIALOG_QUESTION.png diff --git a/addons/web/static/src/img/icons/STOCK_DIALOG_WARNING.png b/addons/web/static/src/img/icons/STOCK_DIALOG_WARNING.png Binary files differnew file mode 100644 index 00000000000..65a4d2c7220 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_DIALOG_WARNING.png diff --git a/addons/web/static/src/img/icons/STOCK_DIRECTORY.png b/addons/web/static/src/img/icons/STOCK_DIRECTORY.png Binary files differnew file mode 100644 index 00000000000..8e41b7878a9 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_DIRECTORY.png diff --git a/addons/web/static/src/img/icons/STOCK_DISCONNECT.png b/addons/web/static/src/img/icons/STOCK_DISCONNECT.png Binary files differnew file mode 100644 index 00000000000..fd188701fc2 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_DISCONNECT.png diff --git a/addons/web/static/src/img/icons/STOCK_DND.png b/addons/web/static/src/img/icons/STOCK_DND.png Binary files differnew file mode 100644 index 00000000000..bf3b8068397 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_DND.png diff --git a/addons/web/static/src/img/icons/STOCK_DND_MULTIPLE.png b/addons/web/static/src/img/icons/STOCK_DND_MULTIPLE.png Binary files differnew file mode 100644 index 00000000000..aa4c65ad5b1 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_DND_MULTIPLE.png diff --git a/addons/web/static/src/img/icons/STOCK_EDIT.png b/addons/web/static/src/img/icons/STOCK_EDIT.png Binary files differnew file mode 100644 index 00000000000..53b7d0855ed --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_EDIT.png diff --git a/addons/web/static/src/img/icons/STOCK_EXECUTE.png b/addons/web/static/src/img/icons/STOCK_EXECUTE.png Binary files differnew file mode 100644 index 00000000000..eddc0f86b25 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_EXECUTE.png diff --git a/addons/web/static/src/img/icons/STOCK_FILE.png b/addons/web/static/src/img/icons/STOCK_FILE.png Binary files differnew file mode 100644 index 00000000000..fa157156bcc --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_FILE.png diff --git a/addons/web/static/src/img/icons/STOCK_FIND.png b/addons/web/static/src/img/icons/STOCK_FIND.png Binary files differnew file mode 100644 index 00000000000..7a23b0125b5 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_FIND.png diff --git a/addons/web/static/src/img/icons/STOCK_FIND_AND_REPLACE.png b/addons/web/static/src/img/icons/STOCK_FIND_AND_REPLACE.png Binary files differnew file mode 100644 index 00000000000..b8d8eeec1b2 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_FIND_AND_REPLACE.png diff --git a/addons/web/static/src/img/icons/STOCK_FLOPPY.png b/addons/web/static/src/img/icons/STOCK_FLOPPY.png Binary files differnew file mode 100644 index 00000000000..7d8209cfb6f --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_FLOPPY.png diff --git a/addons/web/static/src/img/icons/STOCK_FULLSCREEN.png b/addons/web/static/src/img/icons/STOCK_FULLSCREEN.png Binary files differnew file mode 100644 index 00000000000..8ddc31e7481 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_FULLSCREEN.png diff --git a/addons/web/static/src/img/icons/STOCK_GOTO_BOTTOM.png b/addons/web/static/src/img/icons/STOCK_GOTO_BOTTOM.png Binary files differnew file mode 100644 index 00000000000..be52840dee1 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_GOTO_BOTTOM.png diff --git a/addons/web/static/src/img/icons/STOCK_GOTO_FIRST.png b/addons/web/static/src/img/icons/STOCK_GOTO_FIRST.png Binary files differnew file mode 100644 index 00000000000..3d00e36d696 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_GOTO_FIRST.png diff --git a/addons/web/static/src/img/icons/STOCK_GOTO_LAST.png b/addons/web/static/src/img/icons/STOCK_GOTO_LAST.png Binary files differnew file mode 100644 index 00000000000..2028226996d --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_GOTO_LAST.png diff --git a/addons/web/static/src/img/icons/STOCK_GOTO_TOP.png b/addons/web/static/src/img/icons/STOCK_GOTO_TOP.png Binary files differnew file mode 100644 index 00000000000..227a91e5157 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_GOTO_TOP.png diff --git a/addons/web/static/src/img/icons/STOCK_GO_BACK.png b/addons/web/static/src/img/icons/STOCK_GO_BACK.png Binary files differnew file mode 100644 index 00000000000..568117dc170 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_GO_BACK.png diff --git a/addons/web/static/src/img/icons/STOCK_GO_DOWN.png b/addons/web/static/src/img/icons/STOCK_GO_DOWN.png Binary files differnew file mode 100644 index 00000000000..5677cffe6a3 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_GO_DOWN.png diff --git a/addons/web/static/src/img/icons/STOCK_GO_FORWARD.png b/addons/web/static/src/img/icons/STOCK_GO_FORWARD.png Binary files differnew file mode 100644 index 00000000000..fa89d31b734 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_GO_FORWARD.png diff --git a/addons/web/static/src/img/icons/STOCK_GO_UP.png b/addons/web/static/src/img/icons/STOCK_GO_UP.png Binary files differnew file mode 100644 index 00000000000..84f96fdfca2 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_GO_UP.png diff --git a/addons/web/static/src/img/icons/STOCK_HARDDISK.png b/addons/web/static/src/img/icons/STOCK_HARDDISK.png Binary files differnew file mode 100644 index 00000000000..73e5ef0a82e --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_HARDDISK.png diff --git a/addons/web/static/src/img/icons/STOCK_HELP.png b/addons/web/static/src/img/icons/STOCK_HELP.png Binary files differnew file mode 100644 index 00000000000..f8e17009dd6 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_HELP.png diff --git a/addons/web/static/src/img/icons/STOCK_HOME.png b/addons/web/static/src/img/icons/STOCK_HOME.png Binary files differnew file mode 100644 index 00000000000..9d3d50d0fcb --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_HOME.png diff --git a/addons/web/static/src/img/icons/STOCK_INDENT.png b/addons/web/static/src/img/icons/STOCK_INDENT.png Binary files differnew file mode 100644 index 00000000000..19f2669447d --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_INDENT.png diff --git a/addons/web/static/src/img/icons/STOCK_INDEX.png b/addons/web/static/src/img/icons/STOCK_INDEX.png Binary files differnew file mode 100644 index 00000000000..0b63cc25fa8 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_INDEX.png diff --git a/addons/web/static/src/img/icons/STOCK_INFO.png b/addons/web/static/src/img/icons/STOCK_INFO.png Binary files differnew file mode 100644 index 00000000000..272019f5c99 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_INFO.png diff --git a/addons/web/static/src/img/icons/STOCK_ITALIC.png b/addons/web/static/src/img/icons/STOCK_ITALIC.png Binary files differnew file mode 100644 index 00000000000..26c57226a31 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_ITALIC.png diff --git a/addons/web/static/src/img/icons/STOCK_JUMP_TO.png b/addons/web/static/src/img/icons/STOCK_JUMP_TO.png Binary files differnew file mode 100644 index 00000000000..82d7896bdac --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_JUMP_TO.png diff --git a/addons/web/static/src/img/icons/STOCK_JUSTIFY_CENTER.png b/addons/web/static/src/img/icons/STOCK_JUSTIFY_CENTER.png Binary files differnew file mode 100644 index 00000000000..d5f514298eb --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_JUSTIFY_CENTER.png diff --git a/addons/web/static/src/img/icons/STOCK_JUSTIFY_FILL.png b/addons/web/static/src/img/icons/STOCK_JUSTIFY_FILL.png Binary files differnew file mode 100644 index 00000000000..5e2f5c73f30 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_JUSTIFY_FILL.png diff --git a/addons/web/static/src/img/icons/STOCK_JUSTIFY_LEFT.png b/addons/web/static/src/img/icons/STOCK_JUSTIFY_LEFT.png Binary files differnew file mode 100644 index 00000000000..ecb76b117eb --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_JUSTIFY_LEFT.png diff --git a/addons/web/static/src/img/icons/STOCK_JUSTIFY_RIGHT.png b/addons/web/static/src/img/icons/STOCK_JUSTIFY_RIGHT.png Binary files differnew file mode 100644 index 00000000000..3cd4d161420 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_JUSTIFY_RIGHT.png diff --git a/addons/web/static/src/img/icons/STOCK_LEAVE_FULLSCREEN.png b/addons/web/static/src/img/icons/STOCK_LEAVE_FULLSCREEN.png Binary files differnew file mode 100644 index 00000000000..15bc8ab7d7b --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_LEAVE_FULLSCREEN.png diff --git a/addons/web/static/src/img/icons/STOCK_MEDIA_FORWARD.png b/addons/web/static/src/img/icons/STOCK_MEDIA_FORWARD.png Binary files differnew file mode 100644 index 00000000000..e5e198fbb79 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_MEDIA_FORWARD.png diff --git a/addons/web/static/src/img/icons/STOCK_MEDIA_NEXT.png b/addons/web/static/src/img/icons/STOCK_MEDIA_NEXT.png Binary files differnew file mode 100644 index 00000000000..e0aa2431cf0 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_MEDIA_NEXT.png diff --git a/addons/web/static/src/img/icons/STOCK_MEDIA_PAUSE.png b/addons/web/static/src/img/icons/STOCK_MEDIA_PAUSE.png Binary files differnew file mode 100644 index 00000000000..6721994eb8d --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_MEDIA_PAUSE.png diff --git a/addons/web/static/src/img/icons/STOCK_MEDIA_PLAY.png b/addons/web/static/src/img/icons/STOCK_MEDIA_PLAY.png Binary files differnew file mode 100644 index 00000000000..b0832712c81 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_MEDIA_PLAY.png diff --git a/addons/web/static/src/img/icons/STOCK_MEDIA_PREVIOUS.png b/addons/web/static/src/img/icons/STOCK_MEDIA_PREVIOUS.png Binary files differnew file mode 100644 index 00000000000..756899e0bc1 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_MEDIA_PREVIOUS.png diff --git a/addons/web/static/src/img/icons/STOCK_MEDIA_RECORD.png b/addons/web/static/src/img/icons/STOCK_MEDIA_RECORD.png Binary files differnew file mode 100644 index 00000000000..904974c81e5 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_MEDIA_RECORD.png diff --git a/addons/web/static/src/img/icons/STOCK_MEDIA_REWIND.png b/addons/web/static/src/img/icons/STOCK_MEDIA_REWIND.png Binary files differnew file mode 100644 index 00000000000..9c22d9e2778 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_MEDIA_REWIND.png diff --git a/addons/web/static/src/img/icons/STOCK_MEDIA_STOP.png b/addons/web/static/src/img/icons/STOCK_MEDIA_STOP.png Binary files differnew file mode 100644 index 00000000000..128cc2313ec --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_MEDIA_STOP.png diff --git a/addons/web/static/src/img/icons/STOCK_MISSING_IMAGE.png b/addons/web/static/src/img/icons/STOCK_MISSING_IMAGE.png Binary files differnew file mode 100644 index 00000000000..cc69ed33e4b --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_MISSING_IMAGE.png diff --git a/addons/web/static/src/img/icons/STOCK_NETWORK.png b/addons/web/static/src/img/icons/STOCK_NETWORK.png Binary files differnew file mode 100644 index 00000000000..df94a515fec --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_NETWORK.png diff --git a/addons/web/static/src/img/icons/STOCK_NEW.png b/addons/web/static/src/img/icons/STOCK_NEW.png Binary files differnew file mode 100644 index 00000000000..04a290fac7d --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_NEW.png diff --git a/addons/web/static/src/img/icons/STOCK_NO.png b/addons/web/static/src/img/icons/STOCK_NO.png Binary files differnew file mode 100644 index 00000000000..047ddcd9289 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_NO.png diff --git a/addons/web/static/src/img/icons/STOCK_OK.png b/addons/web/static/src/img/icons/STOCK_OK.png Binary files differnew file mode 100644 index 00000000000..7755b8769f0 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_OK.png diff --git a/addons/web/static/src/img/icons/STOCK_OPEN.png b/addons/web/static/src/img/icons/STOCK_OPEN.png Binary files differnew file mode 100644 index 00000000000..9cdd8c66770 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_OPEN.png diff --git a/addons/web/static/src/img/icons/STOCK_PASTE.png b/addons/web/static/src/img/icons/STOCK_PASTE.png Binary files differnew file mode 100644 index 00000000000..840f26c8b38 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_PASTE.png diff --git a/addons/web/static/src/img/icons/STOCK_PREFERENCES.png b/addons/web/static/src/img/icons/STOCK_PREFERENCES.png Binary files differnew file mode 100644 index 00000000000..9ba17a0ab20 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_PREFERENCES.png diff --git a/addons/web/static/src/img/icons/STOCK_PRINT.png b/addons/web/static/src/img/icons/STOCK_PRINT.png Binary files differnew file mode 100644 index 00000000000..be0ed4efb44 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_PRINT.png diff --git a/addons/web/static/src/img/icons/STOCK_PRINT_PREVIEW.png b/addons/web/static/src/img/icons/STOCK_PRINT_PREVIEW.png Binary files differnew file mode 100644 index 00000000000..ab15fcbd307 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_PRINT_PREVIEW.png diff --git a/addons/web/static/src/img/icons/STOCK_PROPERTIES.png b/addons/web/static/src/img/icons/STOCK_PROPERTIES.png Binary files differnew file mode 100644 index 00000000000..894415ec1ce --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_PROPERTIES.png diff --git a/addons/web/static/src/img/icons/STOCK_QUIT.png b/addons/web/static/src/img/icons/STOCK_QUIT.png Binary files differnew file mode 100644 index 00000000000..50b5144660b --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_QUIT.png diff --git a/addons/web/static/src/img/icons/STOCK_REDO.png b/addons/web/static/src/img/icons/STOCK_REDO.png Binary files differnew file mode 100644 index 00000000000..54055b3966d --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_REDO.png diff --git a/addons/web/static/src/img/icons/STOCK_REFRESH.png b/addons/web/static/src/img/icons/STOCK_REFRESH.png Binary files differnew file mode 100644 index 00000000000..7474728b4c0 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_REFRESH.png diff --git a/addons/web/static/src/img/icons/STOCK_REMOVE.png b/addons/web/static/src/img/icons/STOCK_REMOVE.png Binary files differnew file mode 100644 index 00000000000..c2b6e915f77 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_REMOVE.png diff --git a/addons/web/static/src/img/icons/STOCK_REVERT_TO_SAVED.png b/addons/web/static/src/img/icons/STOCK_REVERT_TO_SAVED.png Binary files differnew file mode 100644 index 00000000000..eaf54584edf --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_REVERT_TO_SAVED.png diff --git a/addons/web/static/src/img/icons/STOCK_SAVE.png b/addons/web/static/src/img/icons/STOCK_SAVE.png Binary files differnew file mode 100644 index 00000000000..c61836548a7 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_SAVE.png diff --git a/addons/web/static/src/img/icons/STOCK_SAVE_AS.png b/addons/web/static/src/img/icons/STOCK_SAVE_AS.png Binary files differnew file mode 100644 index 00000000000..1831e4e0a4b --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_SAVE_AS.png diff --git a/addons/web/static/src/img/icons/STOCK_SELECT_COLOR.png b/addons/web/static/src/img/icons/STOCK_SELECT_COLOR.png Binary files differnew file mode 100644 index 00000000000..12b015df107 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_SELECT_COLOR.png diff --git a/addons/web/static/src/img/icons/STOCK_SELECT_FONT.png b/addons/web/static/src/img/icons/STOCK_SELECT_FONT.png Binary files differnew file mode 100644 index 00000000000..66e606313a5 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_SELECT_FONT.png diff --git a/addons/web/static/src/img/icons/STOCK_SORT_ASCENDING.png b/addons/web/static/src/img/icons/STOCK_SORT_ASCENDING.png Binary files differnew file mode 100644 index 00000000000..a648fa0d359 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_SORT_ASCENDING.png diff --git a/addons/web/static/src/img/icons/STOCK_SORT_DESCENDING.png b/addons/web/static/src/img/icons/STOCK_SORT_DESCENDING.png Binary files differnew file mode 100644 index 00000000000..a90b588d797 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_SORT_DESCENDING.png diff --git a/addons/web/static/src/img/icons/STOCK_SPELL_CHECK.png b/addons/web/static/src/img/icons/STOCK_SPELL_CHECK.png Binary files differnew file mode 100644 index 00000000000..698f7c24a43 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_SPELL_CHECK.png diff --git a/addons/web/static/src/img/icons/STOCK_STOP.png b/addons/web/static/src/img/icons/STOCK_STOP.png Binary files differnew file mode 100644 index 00000000000..2ee93ed6415 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_STOP.png diff --git a/addons/web/static/src/img/icons/STOCK_STRIKETHROUGH.png b/addons/web/static/src/img/icons/STOCK_STRIKETHROUGH.png Binary files differnew file mode 100644 index 00000000000..1c11f214622 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_STRIKETHROUGH.png diff --git a/addons/web/static/src/img/icons/STOCK_UNDELETE.png b/addons/web/static/src/img/icons/STOCK_UNDELETE.png Binary files differnew file mode 100644 index 00000000000..c44b940c03b --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_UNDELETE.png diff --git a/addons/web/static/src/img/icons/STOCK_UNDERLINE.png b/addons/web/static/src/img/icons/STOCK_UNDERLINE.png Binary files differnew file mode 100644 index 00000000000..7dbb5d7e60a --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_UNDERLINE.png diff --git a/addons/web/static/src/img/icons/STOCK_UNDO.png b/addons/web/static/src/img/icons/STOCK_UNDO.png Binary files differnew file mode 100644 index 00000000000..7a50de0c61e --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_UNDO.png diff --git a/addons/web/static/src/img/icons/STOCK_UNINDENT.png b/addons/web/static/src/img/icons/STOCK_UNINDENT.png Binary files differnew file mode 100644 index 00000000000..8ace914c1bd --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_UNINDENT.png diff --git a/addons/web/static/src/img/icons/STOCK_YES.png b/addons/web/static/src/img/icons/STOCK_YES.png Binary files differnew file mode 100644 index 00000000000..01fb373c251 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_YES.png diff --git a/addons/web/static/src/img/icons/STOCK_ZOOM_100.png b/addons/web/static/src/img/icons/STOCK_ZOOM_100.png Binary files differnew file mode 100644 index 00000000000..5193406b787 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_ZOOM_100.png diff --git a/addons/web/static/src/img/icons/STOCK_ZOOM_FIT.png b/addons/web/static/src/img/icons/STOCK_ZOOM_FIT.png Binary files differnew file mode 100644 index 00000000000..1e70e619466 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_ZOOM_FIT.png diff --git a/addons/web/static/src/img/icons/STOCK_ZOOM_IN.png b/addons/web/static/src/img/icons/STOCK_ZOOM_IN.png Binary files differnew file mode 100644 index 00000000000..29e50e45d4a --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_ZOOM_IN.png diff --git a/addons/web/static/src/img/icons/STOCK_ZOOM_OUT.png b/addons/web/static/src/img/icons/STOCK_ZOOM_OUT.png Binary files differnew file mode 100644 index 00000000000..0da5ee7ec13 --- /dev/null +++ b/addons/web/static/src/img/icons/STOCK_ZOOM_OUT.png diff --git a/addons/web/static/src/img/icons/color-picker.png b/addons/web/static/src/img/icons/color-picker.png Binary files differnew file mode 100644 index 00000000000..765294c6bdd --- /dev/null +++ b/addons/web/static/src/img/icons/color-picker.png diff --git a/addons/web/static/src/img/icons/gtk-about.png b/addons/web/static/src/img/icons/gtk-about.png Binary files differnew file mode 100644 index 00000000000..1b430a35758 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-about.png diff --git a/addons/web/static/src/img/icons/gtk-add.png b/addons/web/static/src/img/icons/gtk-add.png Binary files differnew file mode 100644 index 00000000000..70612c181c6 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-add.png diff --git a/addons/web/static/src/img/icons/gtk-apply.png b/addons/web/static/src/img/icons/gtk-apply.png Binary files differnew file mode 100644 index 00000000000..116f2e433bb --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-apply.png diff --git a/addons/web/static/src/img/icons/gtk-bold.png b/addons/web/static/src/img/icons/gtk-bold.png Binary files differnew file mode 100644 index 00000000000..408f25aa20b --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-bold.png diff --git a/addons/web/static/src/img/icons/gtk-cancel.png b/addons/web/static/src/img/icons/gtk-cancel.png Binary files differnew file mode 100644 index 00000000000..8cf6273281b --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-cancel.png diff --git a/addons/web/static/src/img/icons/gtk-cdrom.png b/addons/web/static/src/img/icons/gtk-cdrom.png Binary files differnew file mode 100644 index 00000000000..27e8edd8a0b --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-cdrom.png diff --git a/addons/web/static/src/img/icons/gtk-clear.png b/addons/web/static/src/img/icons/gtk-clear.png Binary files differnew file mode 100644 index 00000000000..b2874fb84fa --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-clear.png diff --git a/addons/web/static/src/img/icons/gtk-close.png b/addons/web/static/src/img/icons/gtk-close.png Binary files differnew file mode 100644 index 00000000000..4966974e178 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-close.png diff --git a/addons/web/static/src/img/icons/gtk-color-picker.png b/addons/web/static/src/img/icons/gtk-color-picker.png Binary files differnew file mode 100644 index 00000000000..232d54f8f3b --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-color-picker.png diff --git a/addons/web/static/src/img/icons/gtk-connect.png b/addons/web/static/src/img/icons/gtk-connect.png Binary files differnew file mode 100644 index 00000000000..c820b54abcc --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-connect.png diff --git a/addons/web/static/src/img/icons/gtk-convert.png b/addons/web/static/src/img/icons/gtk-convert.png Binary files differnew file mode 100644 index 00000000000..6288b964cc0 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-convert.png diff --git a/addons/web/static/src/img/icons/gtk-copy.png b/addons/web/static/src/img/icons/gtk-copy.png Binary files differnew file mode 100644 index 00000000000..3d68da606d2 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-copy.png diff --git a/addons/web/static/src/img/icons/gtk-cut.png b/addons/web/static/src/img/icons/gtk-cut.png Binary files differnew file mode 100644 index 00000000000..96681d26968 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-cut.png diff --git a/addons/web/static/src/img/icons/gtk-delete.png b/addons/web/static/src/img/icons/gtk-delete.png Binary files differnew file mode 100644 index 00000000000..879d6fd1cbe --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-delete.png diff --git a/addons/web/static/src/img/icons/gtk-dialog-authentication.png b/addons/web/static/src/img/icons/gtk-dialog-authentication.png Binary files differnew file mode 100644 index 00000000000..eb411112f83 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-dialog-authentication.png diff --git a/addons/web/static/src/img/icons/gtk-dialog-error.png b/addons/web/static/src/img/icons/gtk-dialog-error.png Binary files differnew file mode 100644 index 00000000000..e97bc43504b --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-dialog-error.png diff --git a/addons/web/static/src/img/icons/gtk-dialog-info.png b/addons/web/static/src/img/icons/gtk-dialog-info.png Binary files differnew file mode 100644 index 00000000000..ed46c3ecdb3 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-dialog-info.png diff --git a/addons/web/static/src/img/icons/gtk-dialog-question.png b/addons/web/static/src/img/icons/gtk-dialog-question.png Binary files differnew file mode 100644 index 00000000000..ef68f43f43f --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-dialog-question.png diff --git a/addons/web/static/src/img/icons/gtk-dialog-warning.png b/addons/web/static/src/img/icons/gtk-dialog-warning.png Binary files differnew file mode 100644 index 00000000000..65a4d2c7220 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-dialog-warning.png diff --git a/addons/web/static/src/img/icons/gtk-directory-remote.png b/addons/web/static/src/img/icons/gtk-directory-remote.png Binary files differnew file mode 100644 index 00000000000..34e737d7c07 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-directory-remote.png diff --git a/addons/web/static/src/img/icons/gtk-directory.png b/addons/web/static/src/img/icons/gtk-directory.png Binary files differnew file mode 100644 index 00000000000..8e41b7878a9 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-directory.png diff --git a/addons/web/static/src/img/icons/gtk-disconnect.png b/addons/web/static/src/img/icons/gtk-disconnect.png Binary files differnew file mode 100644 index 00000000000..fd188701fc2 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-disconnect.png diff --git a/addons/web/static/src/img/icons/gtk-dnd-multiple.png b/addons/web/static/src/img/icons/gtk-dnd-multiple.png Binary files differnew file mode 100644 index 00000000000..aa4c65ad5b1 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-dnd-multiple.png diff --git a/addons/web/static/src/img/icons/gtk-dnd.png b/addons/web/static/src/img/icons/gtk-dnd.png Binary files differnew file mode 100644 index 00000000000..bf3b8068397 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-dnd.png diff --git a/addons/web/static/src/img/icons/gtk-edit.png b/addons/web/static/src/img/icons/gtk-edit.png Binary files differnew file mode 100644 index 00000000000..53b7d0855ed --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-edit.png diff --git a/addons/web/static/src/img/icons/gtk-execute.png b/addons/web/static/src/img/icons/gtk-execute.png Binary files differnew file mode 100644 index 00000000000..eddc0f86b25 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-execute.png diff --git a/addons/web/static/src/img/icons/gtk-file.png b/addons/web/static/src/img/icons/gtk-file.png Binary files differnew file mode 100644 index 00000000000..fa157156bcc --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-file.png diff --git a/addons/web/static/src/img/icons/gtk-find-and-replace.png b/addons/web/static/src/img/icons/gtk-find-and-replace.png Binary files differnew file mode 100644 index 00000000000..b8d8eeec1b2 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-find-and-replace.png diff --git a/addons/web/static/src/img/icons/gtk-find.png b/addons/web/static/src/img/icons/gtk-find.png Binary files differnew file mode 100644 index 00000000000..7a23b0125b5 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-find.png diff --git a/addons/web/static/src/img/icons/gtk-floppy.png b/addons/web/static/src/img/icons/gtk-floppy.png Binary files differnew file mode 100644 index 00000000000..7d8209cfb6f --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-floppy.png diff --git a/addons/web/static/src/img/icons/gtk-fullscreen.png b/addons/web/static/src/img/icons/gtk-fullscreen.png Binary files differnew file mode 100644 index 00000000000..8ddc31e7481 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-fullscreen.png diff --git a/addons/web/static/src/img/icons/gtk-go-back.png b/addons/web/static/src/img/icons/gtk-go-back.png Binary files differnew file mode 100644 index 00000000000..568117dc170 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-go-back.png diff --git a/addons/web/static/src/img/icons/gtk-go-down.png b/addons/web/static/src/img/icons/gtk-go-down.png Binary files differnew file mode 100644 index 00000000000..5677cffe6a3 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-go-down.png diff --git a/addons/web/static/src/img/icons/gtk-go-forward.png b/addons/web/static/src/img/icons/gtk-go-forward.png Binary files differnew file mode 100644 index 00000000000..fa89d31b734 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-go-forward.png diff --git a/addons/web/static/src/img/icons/gtk-go-up.png b/addons/web/static/src/img/icons/gtk-go-up.png Binary files differnew file mode 100644 index 00000000000..84f96fdfca2 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-go-up.png diff --git a/addons/web/static/src/img/icons/gtk-goto-bottom.png b/addons/web/static/src/img/icons/gtk-goto-bottom.png Binary files differnew file mode 100644 index 00000000000..be52840dee1 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-goto-bottom.png diff --git a/addons/web/static/src/img/icons/gtk-goto-first.png b/addons/web/static/src/img/icons/gtk-goto-first.png Binary files differnew file mode 100644 index 00000000000..3d00e36d696 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-goto-first.png diff --git a/addons/web/static/src/img/icons/gtk-goto-last.png b/addons/web/static/src/img/icons/gtk-goto-last.png Binary files differnew file mode 100644 index 00000000000..2028226996d --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-goto-last.png diff --git a/addons/web/static/src/img/icons/gtk-goto-top.png b/addons/web/static/src/img/icons/gtk-goto-top.png Binary files differnew file mode 100644 index 00000000000..227a91e5157 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-goto-top.png diff --git a/addons/web/static/src/img/icons/gtk-harddisk.png b/addons/web/static/src/img/icons/gtk-harddisk.png Binary files differnew file mode 100644 index 00000000000..73e5ef0a82e --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-harddisk.png diff --git a/addons/web/static/src/img/icons/gtk-help.png b/addons/web/static/src/img/icons/gtk-help.png Binary files differnew file mode 100644 index 00000000000..f8e17009dd6 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-help.png diff --git a/addons/web/static/src/img/icons/gtk-home.png b/addons/web/static/src/img/icons/gtk-home.png Binary files differnew file mode 100644 index 00000000000..9d3d50d0fcb --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-home.png diff --git a/addons/web/static/src/img/icons/gtk-indent.png b/addons/web/static/src/img/icons/gtk-indent.png Binary files differnew file mode 100644 index 00000000000..19f2669447d --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-indent.png diff --git a/addons/web/static/src/img/icons/gtk-index.png b/addons/web/static/src/img/icons/gtk-index.png Binary files differnew file mode 100644 index 00000000000..0b63cc25fa8 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-index.png diff --git a/addons/web/static/src/img/icons/gtk-info.png b/addons/web/static/src/img/icons/gtk-info.png Binary files differnew file mode 100644 index 00000000000..272019f5c99 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-info.png diff --git a/addons/web/static/src/img/icons/gtk-italic.png b/addons/web/static/src/img/icons/gtk-italic.png Binary files differnew file mode 100644 index 00000000000..26c57226a31 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-italic.png diff --git a/addons/web/static/src/img/icons/gtk-jump-to.png b/addons/web/static/src/img/icons/gtk-jump-to.png Binary files differnew file mode 100644 index 00000000000..82d7896bdac --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-jump-to.png diff --git a/addons/web/static/src/img/icons/gtk-justify-center.png b/addons/web/static/src/img/icons/gtk-justify-center.png Binary files differnew file mode 100644 index 00000000000..d5f514298eb --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-justify-center.png diff --git a/addons/web/static/src/img/icons/gtk-justify-fill.png b/addons/web/static/src/img/icons/gtk-justify-fill.png Binary files differnew file mode 100644 index 00000000000..5e2f5c73f30 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-justify-fill.png diff --git a/addons/web/static/src/img/icons/gtk-justify-left.png b/addons/web/static/src/img/icons/gtk-justify-left.png Binary files differnew file mode 100644 index 00000000000..ecb76b117eb --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-justify-left.png diff --git a/addons/web/static/src/img/icons/gtk-justify-right.png b/addons/web/static/src/img/icons/gtk-justify-right.png Binary files differnew file mode 100644 index 00000000000..3cd4d161420 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-justify-right.png diff --git a/addons/web/static/src/img/icons/gtk-leave-fullscreen.png b/addons/web/static/src/img/icons/gtk-leave-fullscreen.png Binary files differnew file mode 100644 index 00000000000..15bc8ab7d7b --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-leave-fullscreen.png diff --git a/addons/web/static/src/img/icons/gtk-media-forward.png b/addons/web/static/src/img/icons/gtk-media-forward.png Binary files differnew file mode 100644 index 00000000000..e5e198fbb79 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-media-forward.png diff --git a/addons/web/static/src/img/icons/gtk-media-next.png b/addons/web/static/src/img/icons/gtk-media-next.png Binary files differnew file mode 100644 index 00000000000..e0aa2431cf0 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-media-next.png diff --git a/addons/web/static/src/img/icons/gtk-media-pause.png b/addons/web/static/src/img/icons/gtk-media-pause.png Binary files differnew file mode 100644 index 00000000000..6721994eb8d --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-media-pause.png diff --git a/addons/web/static/src/img/icons/gtk-media-play.png b/addons/web/static/src/img/icons/gtk-media-play.png Binary files differnew file mode 100644 index 00000000000..b0832712c81 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-media-play.png diff --git a/addons/web/static/src/img/icons/gtk-media-previous.png b/addons/web/static/src/img/icons/gtk-media-previous.png Binary files differnew file mode 100644 index 00000000000..756899e0bc1 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-media-previous.png diff --git a/addons/web/static/src/img/icons/gtk-media-record.png b/addons/web/static/src/img/icons/gtk-media-record.png Binary files differnew file mode 100644 index 00000000000..904974c81e5 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-media-record.png diff --git a/addons/web/static/src/img/icons/gtk-media-rewind.png b/addons/web/static/src/img/icons/gtk-media-rewind.png Binary files differnew file mode 100644 index 00000000000..9c22d9e2778 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-media-rewind.png diff --git a/addons/web/static/src/img/icons/gtk-media-stop.png b/addons/web/static/src/img/icons/gtk-media-stop.png Binary files differnew file mode 100644 index 00000000000..128cc2313ec --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-media-stop.png diff --git a/addons/web/static/src/img/icons/gtk-missing-image.png b/addons/web/static/src/img/icons/gtk-missing-image.png Binary files differnew file mode 100644 index 00000000000..cc69ed33e4b --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-missing-image.png diff --git a/addons/web/static/src/img/icons/gtk-network.png b/addons/web/static/src/img/icons/gtk-network.png Binary files differnew file mode 100644 index 00000000000..df94a515fec --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-network.png diff --git a/addons/web/static/src/img/icons/gtk-new.png b/addons/web/static/src/img/icons/gtk-new.png Binary files differnew file mode 100644 index 00000000000..04a290fac7d --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-new.png diff --git a/addons/web/static/src/img/icons/gtk-no.png b/addons/web/static/src/img/icons/gtk-no.png Binary files differnew file mode 100644 index 00000000000..047ddcd9289 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-no.png diff --git a/addons/web/static/src/img/icons/gtk-ok.png b/addons/web/static/src/img/icons/gtk-ok.png Binary files differnew file mode 100644 index 00000000000..7755b8769f0 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-ok.png diff --git a/addons/web/static/src/img/icons/gtk-open.png b/addons/web/static/src/img/icons/gtk-open.png Binary files differnew file mode 100644 index 00000000000..9cdd8c66770 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-open.png diff --git a/addons/web/static/src/img/icons/gtk-paste-v.png b/addons/web/static/src/img/icons/gtk-paste-v.png Binary files differnew file mode 100644 index 00000000000..76ec896f70e --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-paste-v.png diff --git a/addons/web/static/src/img/icons/gtk-paste.png b/addons/web/static/src/img/icons/gtk-paste.png Binary files differnew file mode 100644 index 00000000000..840f26c8b38 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-paste.png diff --git a/addons/web/static/src/img/icons/gtk-preferences.png b/addons/web/static/src/img/icons/gtk-preferences.png Binary files differnew file mode 100644 index 00000000000..9ba17a0ab20 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-preferences.png diff --git a/addons/web/static/src/img/icons/gtk-print-preview.png b/addons/web/static/src/img/icons/gtk-print-preview.png Binary files differnew file mode 100644 index 00000000000..ab15fcbd307 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-print-preview.png diff --git a/addons/web/static/src/img/icons/gtk-print.png b/addons/web/static/src/img/icons/gtk-print.png Binary files differnew file mode 100644 index 00000000000..be0ed4efb44 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-print.png diff --git a/addons/web/static/src/img/icons/gtk-properties.png b/addons/web/static/src/img/icons/gtk-properties.png Binary files differnew file mode 100644 index 00000000000..894415ec1ce --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-properties.png diff --git a/addons/web/static/src/img/icons/gtk-quit.png b/addons/web/static/src/img/icons/gtk-quit.png Binary files differnew file mode 100644 index 00000000000..50b5144660b --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-quit.png diff --git a/addons/web/static/src/img/icons/gtk-redo.png b/addons/web/static/src/img/icons/gtk-redo.png Binary files differnew file mode 100644 index 00000000000..54055b3966d --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-redo.png diff --git a/addons/web/static/src/img/icons/gtk-refresh.png b/addons/web/static/src/img/icons/gtk-refresh.png Binary files differnew file mode 100644 index 00000000000..7474728b4c0 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-refresh.png diff --git a/addons/web/static/src/img/icons/gtk-remove.png b/addons/web/static/src/img/icons/gtk-remove.png Binary files differnew file mode 100644 index 00000000000..c2b6e915f77 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-remove.png diff --git a/addons/web/static/src/img/icons/gtk-revert-to-saved.png b/addons/web/static/src/img/icons/gtk-revert-to-saved.png Binary files differnew file mode 100644 index 00000000000..eaf54584edf --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-revert-to-saved.png diff --git a/addons/web/static/src/img/icons/gtk-save-as.png b/addons/web/static/src/img/icons/gtk-save-as.png Binary files differnew file mode 100644 index 00000000000..1831e4e0a4b --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-save-as.png diff --git a/addons/web/static/src/img/icons/gtk-save.png b/addons/web/static/src/img/icons/gtk-save.png Binary files differnew file mode 100644 index 00000000000..c61836548a7 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-save.png diff --git a/addons/web/static/src/img/icons/gtk-select-color.png b/addons/web/static/src/img/icons/gtk-select-color.png Binary files differnew file mode 100644 index 00000000000..12b015df107 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-select-color.png diff --git a/addons/web/static/src/img/icons/gtk-select-font.png b/addons/web/static/src/img/icons/gtk-select-font.png Binary files differnew file mode 100644 index 00000000000..66e606313a5 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-select-font.png diff --git a/addons/web/static/src/img/icons/gtk-sort-ascending.png b/addons/web/static/src/img/icons/gtk-sort-ascending.png Binary files differnew file mode 100644 index 00000000000..a648fa0d359 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-sort-ascending.png diff --git a/addons/web/static/src/img/icons/gtk-sort-descending.png b/addons/web/static/src/img/icons/gtk-sort-descending.png Binary files differnew file mode 100644 index 00000000000..a90b588d797 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-sort-descending.png diff --git a/addons/web/static/src/img/icons/gtk-spell-check.png b/addons/web/static/src/img/icons/gtk-spell-check.png Binary files differnew file mode 100644 index 00000000000..698f7c24a43 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-spell-check.png diff --git a/addons/web/static/src/img/icons/gtk-stop.png b/addons/web/static/src/img/icons/gtk-stop.png Binary files differnew file mode 100644 index 00000000000..2ee93ed6415 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-stop.png diff --git a/addons/web/static/src/img/icons/gtk-strikethrough.png b/addons/web/static/src/img/icons/gtk-strikethrough.png Binary files differnew file mode 100644 index 00000000000..1c11f214622 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-strikethrough.png diff --git a/addons/web/static/src/img/icons/gtk-undelete.png b/addons/web/static/src/img/icons/gtk-undelete.png Binary files differnew file mode 100644 index 00000000000..c44b940c03b --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-undelete.png diff --git a/addons/web/static/src/img/icons/gtk-underline.png b/addons/web/static/src/img/icons/gtk-underline.png Binary files differnew file mode 100644 index 00000000000..7dbb5d7e60a --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-underline.png diff --git a/addons/web/static/src/img/icons/gtk-undo.png b/addons/web/static/src/img/icons/gtk-undo.png Binary files differnew file mode 100644 index 00000000000..7a50de0c61e --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-undo.png diff --git a/addons/web/static/src/img/icons/gtk-unindent.png b/addons/web/static/src/img/icons/gtk-unindent.png Binary files differnew file mode 100644 index 00000000000..8ace914c1bd --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-unindent.png diff --git a/addons/web/static/src/img/icons/gtk-yes.png b/addons/web/static/src/img/icons/gtk-yes.png Binary files differnew file mode 100644 index 00000000000..01fb373c251 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-yes.png diff --git a/addons/web/static/src/img/icons/gtk-zoom-100.png b/addons/web/static/src/img/icons/gtk-zoom-100.png Binary files differnew file mode 100644 index 00000000000..5193406b787 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-zoom-100.png diff --git a/addons/web/static/src/img/icons/gtk-zoom-fit.png b/addons/web/static/src/img/icons/gtk-zoom-fit.png Binary files differnew file mode 100644 index 00000000000..1e70e619466 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-zoom-fit.png diff --git a/addons/web/static/src/img/icons/gtk-zoom-in.png b/addons/web/static/src/img/icons/gtk-zoom-in.png Binary files differnew file mode 100644 index 00000000000..29e50e45d4a --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-zoom-in.png diff --git a/addons/web/static/src/img/icons/gtk-zoom-out.png b/addons/web/static/src/img/icons/gtk-zoom-out.png Binary files differnew file mode 100644 index 00000000000..0da5ee7ec13 --- /dev/null +++ b/addons/web/static/src/img/icons/gtk-zoom-out.png diff --git a/addons/web/static/src/img/icons/index1.jpeg b/addons/web/static/src/img/icons/index1.jpeg Binary files differnew file mode 100644 index 00000000000..f43c68d79ec --- /dev/null +++ b/addons/web/static/src/img/icons/index1.jpeg diff --git a/addons/web/static/src/img/icons/index2.jpeg b/addons/web/static/src/img/icons/index2.jpeg Binary files differnew file mode 100644 index 00000000000..ec9d1eb6d3c --- /dev/null +++ b/addons/web/static/src/img/icons/index2.jpeg diff --git a/addons/web/static/src/img/icons/lead-stage-lost.png b/addons/web/static/src/img/icons/lead-stage-lost.png Binary files differnew file mode 100644 index 00000000000..e1f98a26b59 --- /dev/null +++ b/addons/web/static/src/img/icons/lead-stage-lost.png diff --git a/addons/web/static/src/img/icons/lead-stage-pending.png b/addons/web/static/src/img/icons/lead-stage-pending.png Binary files differnew file mode 100644 index 00000000000..9e9544ee178 --- /dev/null +++ b/addons/web/static/src/img/icons/lead-stage-pending.png diff --git a/addons/web/static/src/img/icons/lead-stage-won.png b/addons/web/static/src/img/icons/lead-stage-won.png Binary files differnew file mode 100644 index 00000000000..02effd5f14c --- /dev/null +++ b/addons/web/static/src/img/icons/lead-stage-won.png diff --git a/addons/web/static/src/img/icons/star-off.png b/addons/web/static/src/img/icons/star-off.png Binary files differnew file mode 100644 index 00000000000..7cd0afd4f03 --- /dev/null +++ b/addons/web/static/src/img/icons/star-off.png diff --git a/addons/web/static/src/img/icons/star-on.png b/addons/web/static/src/img/icons/star-on.png Binary files differnew file mode 100644 index 00000000000..4f2295e1395 --- /dev/null +++ b/addons/web/static/src/img/icons/star-on.png diff --git a/addons/web/static/src/img/icons/stock.png b/addons/web/static/src/img/icons/stock.png Binary files differnew file mode 100644 index 00000000000..ca38181db90 --- /dev/null +++ b/addons/web/static/src/img/icons/stock.png diff --git a/addons/web/static/src/img/icons/stock_calendar.png b/addons/web/static/src/img/icons/stock_calendar.png Binary files differnew file mode 100644 index 00000000000..99a1f3f9ef1 --- /dev/null +++ b/addons/web/static/src/img/icons/stock_calendar.png diff --git a/addons/web/static/src/img/icons/stock_gantt.png b/addons/web/static/src/img/icons/stock_gantt.png Binary files differnew file mode 100644 index 00000000000..585761b9220 --- /dev/null +++ b/addons/web/static/src/img/icons/stock_gantt.png diff --git a/addons/web/static/src/img/icons/stock_graph.png b/addons/web/static/src/img/icons/stock_graph.png Binary files differnew file mode 100644 index 00000000000..4d8310621ea --- /dev/null +++ b/addons/web/static/src/img/icons/stock_graph.png diff --git a/addons/web/static/src/img/icons/stock_log.png b/addons/web/static/src/img/icons/stock_log.png Binary files differnew file mode 100644 index 00000000000..32af4dd6901 --- /dev/null +++ b/addons/web/static/src/img/icons/stock_log.png diff --git a/addons/web/static/src/img/icons/stock_person.png b/addons/web/static/src/img/icons/stock_person.png Binary files differnew file mode 100644 index 00000000000..202cf0ee6c5 --- /dev/null +++ b/addons/web/static/src/img/icons/stock_person.png diff --git a/addons/web/static/src/img/icons/stock_translate.png b/addons/web/static/src/img/icons/stock_translate.png Binary files differnew file mode 100644 index 00000000000..5ab41d49343 --- /dev/null +++ b/addons/web/static/src/img/icons/stock_translate.png diff --git a/addons/web/static/src/img/icons/terp-accessories-archiver+.png b/addons/web/static/src/img/icons/terp-accessories-archiver+.png Binary files differnew file mode 100644 index 00000000000..14d6d02536e --- /dev/null +++ b/addons/web/static/src/img/icons/terp-accessories-archiver+.png diff --git a/addons/web/static/src/img/icons/terp-accessories-archiver-minus.png b/addons/web/static/src/img/icons/terp-accessories-archiver-minus.png Binary files differnew file mode 100644 index 00000000000..024c8f692d8 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-accessories-archiver-minus.png diff --git a/addons/web/static/src/img/icons/terp-accessories-archiver.png b/addons/web/static/src/img/icons/terp-accessories-archiver.png Binary files differnew file mode 100644 index 00000000000..8bcbe4566bc --- /dev/null +++ b/addons/web/static/src/img/icons/terp-accessories-archiver.png diff --git a/addons/web/static/src/img/icons/terp-account.png b/addons/web/static/src/img/icons/terp-account.png Binary files differnew file mode 100644 index 00000000000..30dea82a105 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-account.png diff --git a/addons/web/static/src/img/icons/terp-administration.png b/addons/web/static/src/img/icons/terp-administration.png Binary files differnew file mode 100644 index 00000000000..ab129b2e4b3 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-administration.png diff --git a/addons/web/static/src/img/icons/terp-calendar.png b/addons/web/static/src/img/icons/terp-calendar.png Binary files differnew file mode 100644 index 00000000000..37206565792 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-calendar.png diff --git a/addons/web/static/src/img/icons/terp-call-start.png b/addons/web/static/src/img/icons/terp-call-start.png Binary files differnew file mode 100644 index 00000000000..602a1cdd4b8 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-call-start.png diff --git a/addons/web/static/src/img/icons/terp-camera_test.png b/addons/web/static/src/img/icons/terp-camera_test.png Binary files differnew file mode 100644 index 00000000000..e97e0416de3 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-camera_test.png diff --git a/addons/web/static/src/img/icons/terp-check.png b/addons/web/static/src/img/icons/terp-check.png Binary files differnew file mode 100644 index 00000000000..003b09fdeb9 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-check.png diff --git a/addons/web/static/src/img/icons/terp-crm.png b/addons/web/static/src/img/icons/terp-crm.png Binary files differnew file mode 100644 index 00000000000..fdac61ca92d --- /dev/null +++ b/addons/web/static/src/img/icons/terp-crm.png diff --git a/addons/web/static/src/img/icons/terp-dialog-close.png b/addons/web/static/src/img/icons/terp-dialog-close.png Binary files differnew file mode 100644 index 00000000000..8f51ec05fd6 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-dialog-close.png diff --git a/addons/web/static/src/img/icons/terp-document-new.png b/addons/web/static/src/img/icons/terp-document-new.png Binary files differnew file mode 100644 index 00000000000..2815cbb403f --- /dev/null +++ b/addons/web/static/src/img/icons/terp-document-new.png diff --git a/addons/web/static/src/img/icons/terp-dolar.png b/addons/web/static/src/img/icons/terp-dolar.png Binary files differnew file mode 100644 index 00000000000..bca7021c82f --- /dev/null +++ b/addons/web/static/src/img/icons/terp-dolar.png diff --git a/addons/web/static/src/img/icons/terp-dolar_ok!.png b/addons/web/static/src/img/icons/terp-dolar_ok!.png Binary files differnew file mode 100644 index 00000000000..b1005aae7b9 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-dolar_ok!.png diff --git a/addons/web/static/src/img/icons/terp-emblem-important.png b/addons/web/static/src/img/icons/terp-emblem-important.png Binary files differnew file mode 100644 index 00000000000..a8321066abb --- /dev/null +++ b/addons/web/static/src/img/icons/terp-emblem-important.png diff --git a/addons/web/static/src/img/icons/terp-face-plain.png b/addons/web/static/src/img/icons/terp-face-plain.png Binary files differnew file mode 100644 index 00000000000..ea90aaa7ef4 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-face-plain.png diff --git a/addons/web/static/src/img/icons/terp-folder-blue.png b/addons/web/static/src/img/icons/terp-folder-blue.png Binary files differnew file mode 100644 index 00000000000..addc480d49a --- /dev/null +++ b/addons/web/static/src/img/icons/terp-folder-blue.png diff --git a/addons/web/static/src/img/icons/terp-folder-green.png b/addons/web/static/src/img/icons/terp-folder-green.png Binary files differnew file mode 100644 index 00000000000..206cd39603e --- /dev/null +++ b/addons/web/static/src/img/icons/terp-folder-green.png diff --git a/addons/web/static/src/img/icons/terp-folder-orange.png b/addons/web/static/src/img/icons/terp-folder-orange.png Binary files differnew file mode 100644 index 00000000000..726dbd7f00e --- /dev/null +++ b/addons/web/static/src/img/icons/terp-folder-orange.png diff --git a/addons/web/static/src/img/icons/terp-folder-violet.png b/addons/web/static/src/img/icons/terp-folder-violet.png Binary files differnew file mode 100644 index 00000000000..c6f5cfb87ff --- /dev/null +++ b/addons/web/static/src/img/icons/terp-folder-violet.png diff --git a/addons/web/static/src/img/icons/terp-folder-yellow.png b/addons/web/static/src/img/icons/terp-folder-yellow.png Binary files differnew file mode 100644 index 00000000000..cd7295dcdd7 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-folder-yellow.png diff --git a/addons/web/static/src/img/icons/terp-gdu-smart-failing.png b/addons/web/static/src/img/icons/terp-gdu-smart-failing.png Binary files differnew file mode 100644 index 00000000000..a4448fd553e --- /dev/null +++ b/addons/web/static/src/img/icons/terp-gdu-smart-failing.png diff --git a/addons/web/static/src/img/icons/terp-gnome-cpu-frequency-applet+.png b/addons/web/static/src/img/icons/terp-gnome-cpu-frequency-applet+.png Binary files differnew file mode 100644 index 00000000000..23e246f3be9 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-gnome-cpu-frequency-applet+.png diff --git a/addons/web/static/src/img/icons/terp-go-home.png b/addons/web/static/src/img/icons/terp-go-home.png Binary files differnew file mode 100644 index 00000000000..912256bf7a7 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-go-home.png diff --git a/addons/web/static/src/img/icons/terp-go-month.png b/addons/web/static/src/img/icons/terp-go-month.png Binary files differnew file mode 100644 index 00000000000..cf5e305053e --- /dev/null +++ b/addons/web/static/src/img/icons/terp-go-month.png diff --git a/addons/web/static/src/img/icons/terp-go-today.png b/addons/web/static/src/img/icons/terp-go-today.png Binary files differnew file mode 100644 index 00000000000..2a4dbeed49a --- /dev/null +++ b/addons/web/static/src/img/icons/terp-go-today.png diff --git a/addons/web/static/src/img/icons/terp-go-week.png b/addons/web/static/src/img/icons/terp-go-week.png Binary files differnew file mode 100644 index 00000000000..b3ce6f562b2 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-go-week.png diff --git a/addons/web/static/src/img/icons/terp-go-year.png b/addons/web/static/src/img/icons/terp-go-year.png Binary files differnew file mode 100644 index 00000000000..f45efc9dd0c --- /dev/null +++ b/addons/web/static/src/img/icons/terp-go-year.png diff --git a/addons/web/static/src/img/icons/terp-graph.png b/addons/web/static/src/img/icons/terp-graph.png Binary files differnew file mode 100644 index 00000000000..30dea82a105 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-graph.png diff --git a/addons/web/static/src/img/icons/terp-gtk-go-back-ltr.png b/addons/web/static/src/img/icons/terp-gtk-go-back-ltr.png Binary files differnew file mode 100644 index 00000000000..a76138d2e0d --- /dev/null +++ b/addons/web/static/src/img/icons/terp-gtk-go-back-ltr.png diff --git a/addons/web/static/src/img/icons/terp-gtk-go-back-rtl.png b/addons/web/static/src/img/icons/terp-gtk-go-back-rtl.png Binary files differnew file mode 100644 index 00000000000..10d10b582fe --- /dev/null +++ b/addons/web/static/src/img/icons/terp-gtk-go-back-rtl.png diff --git a/addons/web/static/src/img/icons/terp-gtk-jump-to-ltr.png b/addons/web/static/src/img/icons/terp-gtk-jump-to-ltr.png Binary files differnew file mode 100644 index 00000000000..fa517749d10 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-gtk-jump-to-ltr.png diff --git a/addons/web/static/src/img/icons/terp-gtk-jump-to-rtl.png b/addons/web/static/src/img/icons/terp-gtk-jump-to-rtl.png Binary files differnew file mode 100644 index 00000000000..7b5785c4963 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-gtk-jump-to-rtl.png diff --git a/addons/web/static/src/img/icons/terp-gtk-media-pause.png b/addons/web/static/src/img/icons/terp-gtk-media-pause.png Binary files differnew file mode 100644 index 00000000000..2896ad8e27f --- /dev/null +++ b/addons/web/static/src/img/icons/terp-gtk-media-pause.png diff --git a/addons/web/static/src/img/icons/terp-gtk-select-all.png b/addons/web/static/src/img/icons/terp-gtk-select-all.png Binary files differnew file mode 100644 index 00000000000..b71a6493791 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-gtk-select-all.png diff --git a/addons/web/static/src/img/icons/terp-gtk-stop.png b/addons/web/static/src/img/icons/terp-gtk-stop.png Binary files differnew file mode 100644 index 00000000000..5fe021e19df --- /dev/null +++ b/addons/web/static/src/img/icons/terp-gtk-stop.png diff --git a/addons/web/static/src/img/icons/terp-hr.png b/addons/web/static/src/img/icons/terp-hr.png Binary files differnew file mode 100644 index 00000000000..ba0241e6362 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-hr.png diff --git a/addons/web/static/src/img/icons/terp-idea.png b/addons/web/static/src/img/icons/terp-idea.png Binary files differnew file mode 100644 index 00000000000..fd5e2a25f14 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-idea.png diff --git a/addons/web/static/src/img/icons/terp-locked.png b/addons/web/static/src/img/icons/terp-locked.png Binary files differnew file mode 100644 index 00000000000..cd2b4c2c9c2 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-locked.png diff --git a/addons/web/static/src/img/icons/terp-mail-.png b/addons/web/static/src/img/icons/terp-mail-.png Binary files differnew file mode 100644 index 00000000000..cbc85e4c000 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-mail-.png diff --git a/addons/web/static/src/img/icons/terp-mail-forward.png b/addons/web/static/src/img/icons/terp-mail-forward.png Binary files differnew file mode 100644 index 00000000000..3d4e694b6dc --- /dev/null +++ b/addons/web/static/src/img/icons/terp-mail-forward.png diff --git a/addons/web/static/src/img/icons/terp-mail-message-new.png b/addons/web/static/src/img/icons/terp-mail-message-new.png Binary files differnew file mode 100644 index 00000000000..25b4d5b5d50 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-mail-message-new.png diff --git a/addons/web/static/src/img/icons/terp-mail-replied.png b/addons/web/static/src/img/icons/terp-mail-replied.png Binary files differnew file mode 100644 index 00000000000..6615f4352cc --- /dev/null +++ b/addons/web/static/src/img/icons/terp-mail-replied.png diff --git a/addons/web/static/src/img/icons/terp-mail_delete.png b/addons/web/static/src/img/icons/terp-mail_delete.png Binary files differnew file mode 100644 index 00000000000..90fb5239f02 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-mail_delete.png diff --git a/addons/web/static/src/img/icons/terp-marketing.png b/addons/web/static/src/img/icons/terp-marketing.png Binary files differnew file mode 100644 index 00000000000..1b04b599734 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-marketing.png diff --git a/addons/web/static/src/img/icons/terp-mrp.png b/addons/web/static/src/img/icons/terp-mrp.png Binary files differnew file mode 100644 index 00000000000..c2b4ea31ec3 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-mrp.png diff --git a/addons/web/static/src/img/icons/terp-partner.png b/addons/web/static/src/img/icons/terp-partner.png Binary files differnew file mode 100644 index 00000000000..aab206a1d42 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-partner.png diff --git a/addons/web/static/src/img/icons/terp-personal+.png b/addons/web/static/src/img/icons/terp-personal+.png Binary files differnew file mode 100644 index 00000000000..8afa4d7041d --- /dev/null +++ b/addons/web/static/src/img/icons/terp-personal+.png diff --git a/addons/web/static/src/img/icons/terp-personal-.png b/addons/web/static/src/img/icons/terp-personal-.png Binary files differnew file mode 100644 index 00000000000..a2554df3615 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-personal-.png diff --git a/addons/web/static/src/img/icons/terp-personal.png b/addons/web/static/src/img/icons/terp-personal.png Binary files differnew file mode 100644 index 00000000000..0890c4d0e51 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-personal.png diff --git a/addons/web/static/src/img/icons/terp-product.png b/addons/web/static/src/img/icons/terp-product.png Binary files differnew file mode 100644 index 00000000000..99f58d95a8b --- /dev/null +++ b/addons/web/static/src/img/icons/terp-product.png diff --git a/addons/web/static/src/img/icons/terp-project.png b/addons/web/static/src/img/icons/terp-project.png Binary files differnew file mode 100644 index 00000000000..cd95ae84e43 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-project.png diff --git a/addons/web/static/src/img/icons/terp-purchase.png b/addons/web/static/src/img/icons/terp-purchase.png Binary files differnew file mode 100644 index 00000000000..1e801f57485 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-purchase.png diff --git a/addons/web/static/src/img/icons/terp-rating-rated.png b/addons/web/static/src/img/icons/terp-rating-rated.png Binary files differnew file mode 100644 index 00000000000..9bff03fe584 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-rating-rated.png diff --git a/addons/web/static/src/img/icons/terp-report.png b/addons/web/static/src/img/icons/terp-report.png Binary files differnew file mode 100644 index 00000000000..ada36a2de24 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-report.png diff --git a/addons/web/static/src/img/icons/terp-sale.png b/addons/web/static/src/img/icons/terp-sale.png Binary files differnew file mode 100644 index 00000000000..ba0241e6362 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-sale.png diff --git a/addons/web/static/src/img/icons/terp-stage.png b/addons/web/static/src/img/icons/terp-stage.png Binary files differnew file mode 100644 index 00000000000..591c1594f78 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-stage.png diff --git a/addons/web/static/src/img/icons/terp-stock.png b/addons/web/static/src/img/icons/terp-stock.png Binary files differnew file mode 100644 index 00000000000..0365dfdb8e1 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-stock.png diff --git a/addons/web/static/src/img/icons/terp-stock_align_left_24.png b/addons/web/static/src/img/icons/terp-stock_align_left_24.png Binary files differnew file mode 100644 index 00000000000..844367bb1a7 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-stock_align_left_24.png diff --git a/addons/web/static/src/img/icons/terp-stock_effects-object-colorize.png b/addons/web/static/src/img/icons/terp-stock_effects-object-colorize.png Binary files differnew file mode 100644 index 00000000000..0ffd95a740a --- /dev/null +++ b/addons/web/static/src/img/icons/terp-stock_effects-object-colorize.png diff --git a/addons/web/static/src/img/icons/terp-stock_format-default.png b/addons/web/static/src/img/icons/terp-stock_format-default.png Binary files differnew file mode 100644 index 00000000000..e44b2b26be5 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-stock_format-default.png diff --git a/addons/web/static/src/img/icons/terp-stock_format-scientific.png b/addons/web/static/src/img/icons/terp-stock_format-scientific.png Binary files differnew file mode 100644 index 00000000000..01f83950804 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-stock_format-scientific.png diff --git a/addons/web/static/src/img/icons/terp-stock_symbol-selection.png b/addons/web/static/src/img/icons/terp-stock_symbol-selection.png Binary files differnew file mode 100644 index 00000000000..5773be57c9b --- /dev/null +++ b/addons/web/static/src/img/icons/terp-stock_symbol-selection.png diff --git a/addons/web/static/src/img/icons/terp-stock_zoom.png b/addons/web/static/src/img/icons/terp-stock_zoom.png Binary files differnew file mode 100644 index 00000000000..e96f05e5905 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-stock_zoom.png diff --git a/addons/web/static/src/img/icons/terp-tools.png b/addons/web/static/src/img/icons/terp-tools.png Binary files differnew file mode 100644 index 00000000000..379f7617427 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-tools.png diff --git a/addons/web/static/src/img/icons/terp-translate.png b/addons/web/static/src/img/icons/terp-translate.png Binary files differnew file mode 100644 index 00000000000..5ab41d49343 --- /dev/null +++ b/addons/web/static/src/img/icons/terp-translate.png diff --git a/addons/web/static/src/img/iconset-d-drop.gif b/addons/web/static/src/img/iconset-d-drop.gif Binary files differnew file mode 100644 index 00000000000..4907a927619 --- /dev/null +++ b/addons/web/static/src/img/iconset-d-drop.gif diff --git a/addons/web/static/src/img/logo.png b/addons/web/static/src/img/logo.png Binary files differnew file mode 100644 index 00000000000..8f1c5c6085f --- /dev/null +++ b/addons/web/static/src/img/logo.png diff --git a/addons/web/static/src/img/partner.png b/addons/web/static/src/img/partner.png Binary files differnew file mode 100644 index 00000000000..aab206a1d42 --- /dev/null +++ b/addons/web/static/src/img/partner.png diff --git a/addons/web/static/src/img/placeholder.png b/addons/web/static/src/img/placeholder.png Binary files differnew file mode 100644 index 00000000000..25af75ee815 --- /dev/null +++ b/addons/web/static/src/img/placeholder.png diff --git a/addons/web/static/src/img/product.png b/addons/web/static/src/img/product.png Binary files differnew file mode 100644 index 00000000000..99f58d95a8b --- /dev/null +++ b/addons/web/static/src/img/product.png diff --git a/addons/web/static/src/img/remove-shortcut.png b/addons/web/static/src/img/remove-shortcut.png Binary files differnew file mode 100644 index 00000000000..9bff03fe584 --- /dev/null +++ b/addons/web/static/src/img/remove-shortcut.png diff --git a/addons/web/static/src/img/stock_person.png b/addons/web/static/src/img/stock_person.png Binary files differnew file mode 100644 index 00000000000..202cf0ee6c5 --- /dev/null +++ b/addons/web/static/src/img/stock_person.png diff --git a/addons/web/static/src/img/throbber.gif b/addons/web/static/src/img/throbber.gif Binary files differnew file mode 100644 index 00000000000..ded8d36d486 --- /dev/null +++ b/addons/web/static/src/img/throbber.gif diff --git a/addons/web/static/src/img/throbber2.gif b/addons/web/static/src/img/throbber2.gif Binary files differnew file mode 100644 index 00000000000..5d2057ae0e7 --- /dev/null +++ b/addons/web/static/src/img/throbber2.gif diff --git a/addons/web/static/src/img/toggle-a-bg.png b/addons/web/static/src/img/toggle-a-bg.png Binary files differnew file mode 100644 index 00000000000..b9837ab233e --- /dev/null +++ b/addons/web/static/src/img/toggle-a-bg.png diff --git a/addons/web/static/src/img/ui/field_calendar.png b/addons/web/static/src/img/ui/field_calendar.png Binary files differnew file mode 100644 index 00000000000..99a1f3f9ef1 --- /dev/null +++ b/addons/web/static/src/img/ui/field_calendar.png diff --git a/addons/web/static/src/img/ui/group-expanded.png b/addons/web/static/src/img/ui/group-expanded.png Binary files differnew file mode 100644 index 00000000000..3f16536e123 --- /dev/null +++ b/addons/web/static/src/img/ui/group-expanded.png diff --git a/addons/web/static/src/img/ui/group-folded.png b/addons/web/static/src/img/ui/group-folded.png Binary files differnew file mode 100644 index 00000000000..2909b0c268f --- /dev/null +++ b/addons/web/static/src/img/ui/group-folded.png diff --git a/addons/web/static/src/img/warning.png b/addons/web/static/src/img/warning.png Binary files differnew file mode 100644 index 00000000000..cbfb25a89cd --- /dev/null +++ b/addons/web/static/src/img/warning.png diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js new file mode 100644 index 00000000000..38f8eaa1e7c --- /dev/null +++ b/addons/web/static/src/js/boot.js @@ -0,0 +1,69 @@ +/*--------------------------------------------------------- + * OpenERP Web Boostrap Code + *---------------------------------------------------------*/ + +/** + * @name openerp + * @namespace openerp + */ +(function() { + if (this.openerp) + return; + var session_counter = 0; + + var openerp = this.openerp = { + // Per session namespace + // openerp.<module> will map to + // openerp.sessions.sessionname.<module> using a closure + sessions: {}, + /** + * OpenERP instance constructor + * + * @param {Boolean} skip_init if true, skips the built-in initialization + */ + init: function(skip_init) { + var new_instance = { + // links to the global openerp + _openerp: openerp, + // Only web will be loaded, the rest will be by loaded by + // openerp.web.Connection on the first connection + _modules_loaded: false, + // this unique id will be replaced by hostname_databasename by + // openerp.web.Connection on the first connection + _session_id: "session" + session_counter++, + web: {}, + web_mobile: {} + }; + openerp.sessions[new_instance._session_id] = new_instance; + if (!skip_init){ + openerp.web(new_instance); + } + return new_instance; + } + }; +})(); + +/*--------------------------------------------------------- + * OpenERP Web web module split + *---------------------------------------------------------*/ + +/** + * @namespace + */ +openerp.web = function(instance) { + openerp.web.core(instance); + if (openerp.web.dates) { + openerp.web.dates(instance); + } + openerp.web.formats(instance); + openerp.web.chrome(instance); + openerp.web.data(instance); + var files = ["views","search","list","form","list_editable","web_mobile","view_tree","data_export","view_editor"]; + for(var i=0; i<files.length; i++) { + if(openerp.web[files[i]]) { + openerp.web[files[i]](instance); + } + } +}; + +// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js new file mode 100644 index 00000000000..a04456cfaa5 --- /dev/null +++ b/addons/web/static/src/js/chrome.js @@ -0,0 +1,978 @@ +/*--------------------------------------------------------- + * OpenERP Web chrome + *---------------------------------------------------------*/ + +openerp.web.chrome = function(openerp) { +var QWeb = openerp.web.qweb; + +openerp.web.Notification = openerp.web.Widget.extend(/** @lends openerp.web.Notification# */{ + /** + * @constructs openerp.web.Notification + * @extends openerp.web.Widget + * + * @param parent + * @param element_id + */ + init: function(parent, element_id) { + this._super(parent, element_id); + this.$element.notify({ + speed: 500, + expires: 1500 + }); + }, + notify: function(title, text) { + this.$element.notify('create', { + title: title, + text: text + }); + }, + warn: function(title, text) { + this.$element.notify('create', 'oe_notification_alert', { + title: title, + text: text + }); + } +}); + +openerp.web.Dialog = openerp.web.OldWidget.extend(/** @lends openerp.web.Dialog# */{ + dialog_title: "", + identifier_prefix: 'dialog', + /** + * @constructs openerp.web.Dialog + * @extends openerp.web.OldWidget + * + * @param parent + * @param dialog_options + */ + init: function (parent, dialog_options) { + var self = this; + this._super(parent); + this.dialog_options = { + modal: true, + width: 'auto', + min_width: 0, + max_width: '100%', + height: 'auto', + min_height: 0, + max_height: '100%', + autoOpen: false, + buttons: {}, + beforeClose: function () { self.on_close(); } + }; + for (var f in this) { + if (f.substr(0, 10) == 'on_button_') { + this.dialog_options.buttons[f.substr(10)] = this[f]; + } + } + if (dialog_options) { + this.set_options(dialog_options); + } + }, + set_options: function(options) { + options = options || {}; + options.width = this.get_width(options.width || this.dialog_options.width); + options.min_width = this.get_width(options.min_width || this.dialog_options.min_width); + options.max_width = this.get_width(options.max_width || this.dialog_options.max_width); + options.height = this.get_height(options.height || this.dialog_options.height); + options.min_height = this.get_height(options.min_height || this.dialog_options.min_height); + options.max_height = this.get_height(options.max_height || this.dialog_options.max_width); + + if (options.width !== 'auto') { + if (options.width > options.max_width) options.width = options.max_width; + if (options.width < options.min_width) options.width = options.min_width; + } + if (options.height !== 'auto') { + if (options.height > options.max_height) options.height = options.max_height; + if (options.height < options.min_height) options.height = options.min_height; + } + if (!options.title && this.dialog_title) { + options.title = this.dialog_title; + } + _.extend(this.dialog_options, options); + }, + get_width: function(val) { + return this.get_size(val.toString(), $(window.top).width()); + }, + get_height: function(val) { + return this.get_size(val.toString(), $(window.top).height()); + }, + get_size: function(val, available_size) { + if (val === 'auto') { + return val; + } else if (val.slice(-1) == "%") { + return Math.round(available_size / 100 * parseInt(val.slice(0, -1), 10)); + } else { + return parseInt(val, 10); + } + }, + start: function () { + this.$dialog = $('<div id="' + this.element_id + '"></div>').dialog(this.dialog_options); + this._super(); + return this; + }, + open: function(dialog_options) { + // TODO fme: bind window on resize + if (this.template) { + this.$element.html(this.render()); + } + this.set_options(dialog_options); + this.$dialog.dialog(this.dialog_options).dialog('open'); + return this; + }, + close: function() { + // Closes the dialog but leave it in a state where it could be opened again. + this.$dialog.dialog('close'); + }, + on_close: function() { + }, + stop: function () { + // Destroy widget + this.close(); + this.$dialog.dialog('destroy'); + } +}); + +openerp.web.CrashManager = openerp.web.Dialog.extend(/** @lends openerp.web.CrashManager# */{ + identifier_prefix: 'dialog_crash', + /** + * @constructs opener.web.CrashManager + * @extends openerp.web.Dialog + * + * @param parent + */ + init: function(parent) { + this._super(parent); + this.session.on_rpc_error.add(this.on_rpc_error); + }, + on_button_Ok: function() { + this.close(); + }, + on_rpc_error: function(error) { + this.error = error; + if (error.data.fault_code) { + var split = ("" + error.data.fault_code).split('\n')[0].split(' -- '); + if (split.length > 1) { + error.type = split.shift(); + error.data.fault_code = error.data.fault_code.substr(error.type.length + 4); + } + } + if (error.code === 200 && error.type) { + this.dialog_title = "OpenERP " + _.capitalize(error.type); + this.template = 'DialogWarning'; + this.open({ + width: 'auto', + height: 'auto' + }); + } else { + this.dialog_title = "OpenERP Error"; + this.template = 'DialogTraceback'; + this.open({ + width: 'auto', + height: 'auto' + }); + } + } +}); + +openerp.web.Loading = openerp.web.Widget.extend(/** @lends openerp.web.Loading# */{ + /** + * @constructs openerp.web.Loading + * @extends openerp.web.Widget + * + * @param parent + * @param element_id + */ + init: function(parent, element_id) { + this._super(parent, element_id); + this.count = 0; + this.session.on_rpc_request.add_first(this.on_rpc_event, 1); + this.session.on_rpc_response.add_last(this.on_rpc_event, -1); + }, + on_rpc_event : function(increment) { + this.count += increment; + if (this.count) { + //this.$element.html(QWeb.render("Loading", {})); + this.$element.html("Loading ("+this.count+")"); + this.$element.show(); + } else { + this.$element.fadeOut(); + } + } +}); + +openerp.web.Database = openerp.web.Widget.extend(/** @lends openerp.web.Database# */{ + /** + * @constructs openerp.web.Database + * @extends openerp.web.Widget + * + * @param parent + * @param element_id + * @param option_id + */ + init: function(parent, element_id, option_id) { + this._super(parent, element_id); + this.$option_id = $('#' + option_id); + }, + start: function() { + this.$element.html(QWeb.render("Database", this)); + this.$element.closest(".openerp") + .removeClass("login-mode") + .addClass("database_block"); + + var self = this; + + var fetch_db = this.rpc("/web/database/get_list", {}, function(result) { + self.db_list = result.db_list; + }); + var fetch_langs = this.rpc("/web/session/get_lang_list", {}, function(result) { + if (result.error) { + self.display_error(result); + return; + } + self.lang_list = result.lang_list; + }); + $.when(fetch_db, fetch_langs).then(function () {self.do_create();}); + + this.$element.find('#db-create').click(this.do_create); + this.$element.find('#db-drop').click(this.do_drop); + this.$element.find('#db-backup').click(this.do_backup); + this.$element.find('#db-restore').click(this.do_restore); + this.$element.find('#db-change-password').click(this.do_change_password); + this.$element.find('#back-to-login').click(function() { + self.stop(); + }); + }, + stop: function () { + this.$option_id.empty(); + + this.$element + .find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login') + .unbind('click') + .end() + .closest(".openerp") + .addClass("login-mode") + .removeClass("database_block") + .end() + .empty(); + + }, + /** + * Converts a .serializeArray() result into a dict. Does not bother folding + * multiple identical keys into an array, last key wins. + * + * @param {Array} array + */ + to_object: function (array) { + var result = {}; + _(array).each(function (record) { + result[record.name] = record.value; + }); + return result; + }, + /** + * Waits until the new database is done creating, then unblocks the UI and + * logs the user in as admin + * + * @param {Number} db_creation_id identifier for the db-creation operation, used to fetch the current installation progress + * @param {Object} info info fields for this database creation + * @param {String} info.db name of the database being created + * @param {String} info.password super-admin password for the database + */ + wait_for_newdb: function (db_creation_id, info) { + var self = this; + self.rpc('/web/database/progress', { + id: db_creation_id, + password: info.password + }, function (result) { + var progress = result[0]; + // I'd display a progress bar, but turns out the progress status + // the server report kind-of blows goats: it's at 0 for ~75% of + // the installation, then jumps to 75%, then jumps down to either + // 0 or ~40%, then back up to 75%, then terminates. Let's keep that + // mess hidden behind a not-very-useful but not overly weird + // message instead. + if (progress < 1) { + setTimeout(function () { + self.wait_for_newdb(db_creation_id, info); + }, 500); + return; + } + + var admin = result[1][0]; + setTimeout(function () { + self.stop(); + self.widget_parent.do_login( + info.db, admin.login, admin.password); + $.unblockUI(); + }); + }); + }, + /** + * Displays an error dialog resulting from the various RPC communications + * failing over themselves + * + * @param {Object} error error description + * @param {String} error.title title of the error dialog + * @param {String} error.error message of the error dialog + */ + display_error: function (error) { + return $('<div>').dialog({ + modal: true, + title: error.title, + buttons: { + Ok: function() { + $(this).dialog("close"); + } + } + }).html(error.error); + }, + do_create: function() { + var self = this; + self.$option_id.html(QWeb.render("Database.CreateDB", self)); + self.$option_id.find("form[name=create_db_form]").validate({ + submitHandler: function (form) { + var fields = $(form).serializeArray(); + $.blockUI({message:'<img src="/web/static/src/img/throbber2.gif">'}); + self.rpc("/web/database/create", {'fields': fields}, function(result) { + if (result.error) { + $.unblockUI(); + self.display_error(result); + return; + } + self.db_list.push(self.to_object(fields)['db_name']); + self.db_list.sort(); + var form_obj = self.to_object(fields); + self.wait_for_newdb(result, { + password: form_obj['super_admin_pwd'], + db: form_obj['db_name'] + }); + }); + } + }); + }, + do_drop: function() { + var self = this; + self.$option_id.html(QWeb.render("DropDB", self)); + self.$option_id.find("form[name=drop_db_form]").validate({ + submitHandler: function (form) { + var $form = $(form), + fields = $form.serializeArray(), + $db_list = $form.find('select[name=drop_db]'), + db = $db_list.val(); + + if (!confirm("Do you really want to delete the database: " + db + " ?")) { + return; + } + self.rpc("/web/database/drop", {'fields': fields}, function(result) { + if (result.error) { + self.display_error(result); + return; + } + $db_list.find(':selected').remove(); + self.db_list.splice(_.indexOf(self.db_list, db, true), 1); + self.notification.notify("Dropping database", "The database '" + db + "' has been dropped"); + }); + } + }); + }, + do_backup: function() { + var self = this; + self.$option_id + .html(QWeb.render("BackupDB", self)) + .find("form[name=backup_db_form]").validate({ + submitHandler: function (form) { + $.blockUI({message:'<img src="/web/static/src/img/throbber2.gif">'}); + self.session.get_file({ + form: form, + error: function (body) { + var error = body.firstChild.data.split('|'); + self.display_error({ + title: error[0], + error: error[1] + }); + }, + complete: $.unblockUI + }); + } + }); + }, + do_restore: function() { + var self = this; + self.$option_id.html(QWeb.render("RestoreDB", self)); + + self.$option_id.find("form[name=restore_db_form]").validate({ + submitHandler: function (form) { + $.blockUI({message:'<img src="/web/static/src/img/throbber2.gif">'}); + $(form).ajaxSubmit({ + url: '/web/database/restore', + type: 'POST', + resetForm: true, + success: function (body) { + // TODO: ui manipulations + // note: response objects don't work, but we have the + // HTTP body of the response~~ + + // If empty body, everything went fine + if (!body) { return; } + + if (body.indexOf('403 Forbidden') !== -1) { + self.display_error({ + title: 'Access Denied', + error: 'Incorrect super-administrator password' + }) + } else { + self.display_error({ + title: 'Restore Database', + error: 'Could not restore the database' + }) + } + }, + complete: function () { + $.unblockUI(); + } + }); + } + }); + }, + do_change_password: function() { + var self = this; + self.$option_id.html(QWeb.render("Change_DB_Pwd", self)); + + self.$option_id.find("form[name=change_pwd_form]").validate({ + messages: { + old_pwd: "Please enter your previous password", + new_pwd: "Please enter your new password", + confirm_pwd: { + required: "Please confirm your new password", + equalTo: "The confirmation does not match the password" + } + }, + submitHandler: function (form) { + self.rpc("/web/database/change_password", { + 'fields': $(form).serializeArray() + }, function(result) { + if (result.error) { + self.display_error(result); + return; + } + self.notification.notify("Changed Password", "Password has been changed successfully"); + }); + } + }); + } +}); + +openerp.web.Login = openerp.web.Widget.extend(/** @lends openerp.web.Login# */{ + remember_creditentials: true, + /** + * @constructs openerp.web.Login + * @extends openerp.web.Widget + * + * @param parent + * @param element_id + */ + init: function(parent, element_id) { + this._super(parent, element_id); + this.has_local_storage = typeof(localStorage) != 'undefined'; + this.selected_db = null; + this.selected_login = null; + + if (this.has_local_storage && this.remember_creditentials) { + this.selected_db = localStorage.getItem('last_db_login_success'); + this.selected_login = localStorage.getItem('last_login_login_success'); + if (jQuery.deparam(jQuery.param.querystring()).debug != undefined) { + this.selected_password = localStorage.getItem('last_password_login_success'); + } + } + }, + start: function() { + var self = this; + this.rpc("/web/database/get_list", {}, function(result) { + self.db_list = result.db_list; + self.display(); + }, function() { + self.display(); + }); + }, + display: function() { + var self = this; + + this.$element.html(QWeb.render("Login", this)); + this.database = new openerp.web.Database( + this, "oe_database", "oe_db_options"); + + this.$element.find('#oe-db-config').click(function() { + self.database.start(); + }); + + this.$element.find("form").submit(this.on_submit); + }, + on_login_invalid: function() { + this.$element.closest(".openerp").addClass("login-mode"); + }, + on_login_valid: function() { + this.$element.closest(".openerp").removeClass("login-mode"); + }, + on_submit: function(ev) { + ev.preventDefault(); + var $e = this.$element; + var db = $e.find("form [name=db]").val(); + var login = $e.find("form input[name=login]").val(); + var password = $e.find("form input[name=password]").val(); + + this.do_login(db, login, password); + }, + /** + * Performs actual login operation, and UI-related stuff + * + * @param {String} db database to log in + * @param {String} login user login + * @param {String} password user password + */ + do_login: function (db, login, password) { + var self = this; + this.session.session_login(db, login, password, function() { + if(self.session.session_is_valid()) { + if (self.has_local_storage) { + if(self.remember_creditentials) { + localStorage.setItem('last_db_login_success', db); + localStorage.setItem('last_login_login_success', login); + if (jQuery.deparam(jQuery.param.querystring()).debug != undefined) { + localStorage.setItem('last_password_login_success', password); + } + } else { + localStorage.setItem('last_db_login_success', ''); + localStorage.setItem('last_login_login_success', ''); + localStorage.setItem('last_password_login_success', ''); + } + } + self.on_login_valid(); + } else { + self.$element.addClass("login_invalid"); + self.on_login_invalid(); + } + }); + }, + do_ask_login: function(continuation) { + this.on_login_invalid(); + this.$element + .removeClass("login_invalid"); + this.on_login_valid.add({ + position: "last", + unique: true, + callback: continuation || function() {} + }); + }, + on_logout: function() { + this.session.logout(); + } +}); + +openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# */{ + template: "Header", + identifier_prefix: 'oe-app-header-', + /** + * @constructs openerp.web.Header + * @extends openerp.web.Widget + * + * @param parent + */ + init: function(parent) { + this._super(parent); + this.qs = "?" + jQuery.param.querystring(); + this.$content = $(); + console.debug("initializing header with id", this.element_id); + this.update_promise = $.Deferred().resolve(); + }, + start: function() { + this._super(); + }, + do_update: function () { + var self = this; + var fct = function() { + self.$content.remove(); + if (!self.session.uid) + return; + var func = new openerp.web.Model(self.session, "res.users").get_func("read"); + return func(self.session.uid, ["name", "company_id"]).pipe(function(res) { + self.$content = $(QWeb.render("Header-content", {widget: self, user: res})); + self.$content.appendTo(self.$element); + self.$element.find(".logout").click(self.on_logout); + self.$element.find("a.preferences").click(self.on_preferences); + self.$element.find(".about").click(self.on_about); + return self.shortcut_load(); + }); + }; + this.update_promise = this.update_promise.pipe(fct, fct); + }, + on_about: function() { + var self = this; + self.rpc("/web/webclient/version_info", {}).then(function(res) { + var $help = $(QWeb.render("About-Page", {version_info: res})); + $help.dialog({autoOpen: true, + modal: true, width: 960, title: "About"}); + }); + }, + shortcut_load :function(){ + var self = this, + sc = self.session.shortcuts, + shortcuts_ds = new openerp.web.DataSet(this, 'ir.ui.view_sc'); + // TODO: better way to communicate between sections. + // sc.bindings, because jquery does not bind/trigger on arrays... + if (!sc.binding) { + sc.binding = {}; + $(sc.binding).bind({ + 'add': function (e, attrs) { + shortcuts_ds.create(attrs, function (out) { + $('<li>', { + 'data-shortcut-id':out.result, + 'data-id': attrs.res_id + }).text(attrs.name) + .appendTo(self.$element.find('.oe-shortcuts ul')); + attrs.id = out.result; + sc.push(attrs); + }); + }, + 'remove-current': function () { + var menu_id = self.session.active_id; + var $shortcut = self.$element + .find('.oe-shortcuts li[data-id=' + menu_id + ']'); + var shortcut_id = $shortcut.data('shortcut-id'); + $shortcut.remove(); + shortcuts_ds.unlink([shortcut_id]); + var sc_new = _.reject(sc, function(shortcut){ return shortcut_id === shortcut.id}); + sc.splice(0, sc.length); + sc.push.apply(sc, sc_new); + } + }); + } + return this.rpc('/web/session/sc_list', {}, function(shortcuts) { + sc.splice(0, sc.length); + sc.push.apply(sc, shortcuts); + + self.$element.find('.oe-shortcuts') + .html(QWeb.render('Shortcuts', {'shortcuts': shortcuts})) + .undelegate('li', 'click') + + .delegate('li', 'click', function(e) { + e.stopPropagation(); + var id = $(this).data('id'); + self.session.active_id = id; + self.rpc('/web/menu/action', {'menu_id':id}, function(ir_menu_data) { + if (ir_menu_data.action.length){ + self.on_action(ir_menu_data.action[0][2]); + } + }); + }); + }); + }, + + on_action: function(action) { + }, + on_preferences: function(){ + var self = this; + var action_manager = new openerp.web.ActionManager(this); + var dataset = new openerp.web.DataSet (this,'res.users',this.context); + dataset.call ('action_get','',function (result){ + self.rpc('/web/action/load', {action_id:result}, function(result){ + action_manager.do_action(_.extend(result['result'], { + res_id: self.session.uid, + res_model: 'res.users', + flags: { + action_buttons: false, + search_view: false, + sidebar: false, + views_switcher: false, + pager: false + } + })); + }); + }); + this.dialog = new openerp.web.Dialog(this,{ + modal: true, + title: 'Preferences', + width: 600, + height: 500, + buttons: { + "Change password": function(){ + self.change_password(); + }, + Cancel: function(){ + $(this).dialog('destroy'); + }, + Save: function(){ + var inner_viewmanager = action_manager.inner_viewmanager; + inner_viewmanager.views[inner_viewmanager.active_view].controller.do_save(function(){ + inner_viewmanager.start(); + }); + $(this).dialog('destroy') + } + } + }); + this.dialog.start().open(); + action_manager.appendTo(this.dialog); + action_manager.render(this.dialog); + }, + + change_password :function() { + var self = this; + this.dialog = new openerp.web.Dialog(this,{ + modal : true, + title : 'Change Password', + width : 'auto', + height : 'auto' + }); + this.dialog.start().open(); + this.dialog.$element.html(QWeb.render("Change_Pwd", self)); + this.dialog.$element.find("form[name=change_password_form]").validate({ + submitHandler: function (form) { + self.rpc("/web/session/change_password",{ + 'fields': $(form).serializeArray() + }, function(result) { + if (result.error) { + self.display_error(result); + return; + } + else { + if (result.new_password) { + self.session.password = result.new_password; + var session = new openerp.web.Session(self.session.server, self.session.port); + session.start(); + session.session_login(self.session.db, self.session.login, self.session.password) + } + } + self.notification.notify("Changed Password", "Password has been changed successfully"); + self.dialog.close(); + }); + } + }); + }, + display_error: function (error) { + return $('<div>').dialog({ + modal: true, + title: error.title, + buttons: { + Ok: function() { + $(this).dialog("close"); + } + } + }).html(error.error); + }, + on_logout: function() { + } +}); + +openerp.web.Menu = openerp.web.Widget.extend(/** @lends openerp.web.Menu# */{ + /** + * @constructs openerp.web.Menu + * @extends openerp.web.Widget + * + * @param parent + * @param element_id + * @param secondary_menu_id + */ + init: function(parent, element_id, secondary_menu_id) { + this._super(parent, element_id); + this.secondary_menu_id = secondary_menu_id; + this.$secondary_menu = $("#" + secondary_menu_id).hide(); + this.menu = false; + }, + start: function() { + this.rpc("/web/menu/load", {}, this.on_loaded); + }, + on_loaded: function(data) { + this.data = data; + this.$element.html(QWeb.render("Menu", this.data)); + for (var i = 0; i < this.data.data.children.length; i++) { + var v = { menu : this.data.data.children[i] }; + this.$secondary_menu.append(QWeb.render("Menu.secondary", v)); + } + this.$secondary_menu.find("div.menu_accordion").accordion({ + animated : false, + autoHeight : false, + icons : false + }); + this.$secondary_menu.find("div.submenu_accordion").accordion({ + animated : false, + autoHeight : false, + active: false, + collapsible: true, + header: 'h4' + }); + + this.$element.add(this.$secondary_menu).find("a").click(this.on_menu_click); + }, + on_menu_click: function(ev, id) { + id = id || 0; + var $menu, $parent, $secondary; + + if (id) { + // We can manually activate a menu with it's id (for hash url mapping) + $menu = this.$element.find('a[data-menu=' + id + ']'); + if (!$menu.length) { + $menu = this.$secondary_menu.find('a[data-menu=' + id + ']'); + } + } else { + $menu = $(ev.currentTarget); + id = $menu.data('menu'); + } + if (this.$secondary_menu.has($menu).length) { + $secondary = $menu.parents('.menu_accordion'); + $parent = this.$element.find('a[data-menu=' + $secondary.data('menu-parent') + ']'); + } else { + $parent = $menu; + $secondary = this.$secondary_menu.find('.menu_accordion[data-menu-parent=' + $menu.attr('data-menu') + ']'); + } + + this.$secondary_menu.find('.menu_accordion').hide(); + // TODO: ui-accordion : collapse submenus and expand the good one + $secondary.show(); + + if (id) { + this.session.active_id = id; + this.rpc('/web/menu/action', {'menu_id': id}, + this.on_menu_action_loaded); + } + + $('.active', this.$element.add(this.$secondary_menu.show())).removeClass('active'); + $parent.addClass('active'); + $menu.addClass('active'); + $menu.parent('h4').addClass('active'); + + return !$menu.is(".leaf"); + }, + on_menu_action_loaded: function(data) { + var self = this; + if (data.action.length) { + var action = data.action[0][2]; + self.on_action(action); + } + }, + on_action: function(action) { + } +}); + +openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClient */{ + /** + * @constructs openerp.web.WebClient + * @extends openerp.web.Widget + * + * @param element_id + */ + init: function(element_id) { + this._super(null, element_id); + openerp.webclient = this; + + QWeb.add_template("/web/static/src/xml/base.xml"); + var params = {}; + if(jQuery.param != undefined && jQuery.deparam(jQuery.param.querystring()).kitten != undefined) { + this.$element.addClass("kitten-mode-activated"); + } + this.$element.html(QWeb.render("Interface", params)); + + this.session = new openerp.web.Session(); + this.loading = new openerp.web.Loading(this,"oe_loading"); + this.crashmanager = new openerp.web.CrashManager(this); + this.crashmanager.start(); + + // Do you autorize this ? will be replaced by notify() in controller + openerp.web.Widget.prototype.notification = new openerp.web.Notification(this, "oe_notification"); + + + this.header = new openerp.web.Header(this); + this.login = new openerp.web.Login(this, "oe_login"); + this.header.on_logout.add(this.login.on_logout); + this.header.on_action.add(this.on_menu_action); + + this.session.on_session_invalid.add(this.login.do_ask_login); + this.session.on_session_valid.add_last(this.header.do_update); + this.session.on_session_invalid.add_last(this.header.do_update); + this.session.on_session_valid.add_last(this.on_logged); + + this.menu = new openerp.web.Menu(this, "oe_menu", "oe_secondary_menu"); + this.menu.on_action.add(this.on_menu_action); + }, + start: function() { + this.header.appendTo($("#oe_header")); + this.session.start(); + this.login.start(); + this.menu.start(); + console.debug("The openerp client has been initialized."); + }, + on_logged: function() { + if(this.action_manager) + this.action_manager.stop(); + this.action_manager = new openerp.web.ActionManager(this); + this.action_manager.appendTo($("#oe_app")); + + // if using saved actions, load the action and give it to action manager + var parameters = jQuery.deparam(jQuery.param.querystring()); + if (parameters["s_action"] != undefined) { + var key = parseInt(parameters["s_action"], 10); + var self = this; + this.rpc("/web/session/get_session_action", {key:key}, function(action) { + self.action_manager.do_action(action); + }); + } else if (openerp._modules_loaded) { // TODO: find better option than this + this.load_url_state() + } else { + this.session.on_modules_loaded.add({ + callback: $.proxy(this, 'load_url_state'), + unique: true, + position: 'last' + }) + } + }, + /** + * Loads state from URL if any, or checks if there is a home action and + * loads that, assuming we're at the index + */ + load_url_state: function () { + var self = this; + // TODO: add actual loading if there is url state to unpack, test on window.location.hash + + // not logged in + if (!this.session.uid) { return; } + var ds = new openerp.web.DataSetSearch(this, 'res.users'); + ds.read_ids([this.session.uid], ['action_id'], function (users) { + var home_action = users[0].action_id; + if (!home_action) { + self.default_home(); + return; + } + self.execute_home_action(home_action[0], ds); + }) + }, + default_home: function () { + }, + /** + * Bundles the execution of the home action + * + * @param {Number} action action id + * @param {openerp.web.DataSet} dataset action executor + */ + execute_home_action: function (action, dataset) { + var self = this; + this.rpc('/web/action/load', { + action_id: action, + context: dataset.get_context() + }, function (meh) { + var action = meh.result; + action.context = _.extend(action.context || {}, { + active_id: false, + active_ids: [false], + active_model: dataset.model + }); + self.action_manager.do_action(action); + }); + }, + on_menu_action: function(action) { + this.action_manager.do_action(action); + }, + do_about: function() { + } +}); + +}; + +// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/js/core.js b/addons/web/static/src/js/core.js new file mode 100644 index 00000000000..19a915c0f78 --- /dev/null +++ b/addons/web/static/src/js/core.js @@ -0,0 +1,992 @@ +/*--------------------------------------------------------- + * OpenERP Web core + *--------------------------------------------------------*/ + +if (!console.debug) { + console.debug = console.log; +} + +openerp.web.core = function(openerp) { +openerp.web.qweb = new QWeb2.Engine(); +openerp.web.qweb.debug = (window.location.search.indexOf('?debug') !== -1); +/** + * John Resig Class with factory improvement + */ +(function() { + var initializing = false, + fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; + // The web Class implementation (does nothing) + /** + * Extended version of John Resig's Class pattern + * + * @class + */ + openerp.web.Class = function(){}; + + /** + * Subclass an existing class + * + * @param {Object} prop class-level properties (class attributes and instance methods) to set on the new class + */ + openerp.web.Class.extend = function(prop) { + var _super = this.prototype; + + // Instantiate a web class (but only create the instance, + // don't run the init constructor) + initializing = true; + var prototype = new this(); + initializing = false; + + // Copy the properties over onto the new prototype + for (var name in prop) { + // Check if we're overwriting an existing function + prototype[name] = typeof prop[name] == "function" && + typeof _super[name] == "function" && + fnTest.test(prop[name]) ? + (function(name, fn) { + return function() { + var tmp = this._super; + + // Add a new ._super() method that is the same + // method but on the super-class + this._super = _super[name]; + + // The method only need to be bound temporarily, so + // we remove it when we're done executing + var ret = fn.apply(this, arguments); + this._super = tmp; + + return ret; + }; + })(name, prop[name]) : + prop[name]; + } + + // The dummy class constructor + function Class() { + // All construction is actually done in the init method + if (!initializing && this.init) { + var ret = this.init.apply(this, arguments); + if (ret) { return ret; } + } + return this; + } + Class.include = function (properties) { + for (var name in properties) { + if (typeof properties[name] !== 'function' + || !fnTest.test(properties[name])) { + prototype[name] = properties[name]; + } else if (typeof prototype[name] === 'function' + && prototype.hasOwnProperty(name)) { + prototype[name] = (function (name, fn, previous) { + return function () { + var tmp = this._super; + this._super = previous; + var ret = fn.apply(this, arguments); + this._super = tmp; + return ret; + } + })(name, properties[name], prototype[name]); + } else if (typeof _super[name] === 'function') { + prototype[name] = (function (name, fn) { + return function () { + var tmp = this._super; + this._super = _super[name]; + var ret = fn.apply(this, arguments); + this._super = tmp; + return ret; + } + })(name, properties[name]); + } + } + }; + + // Populate our constructed prototype object + Class.prototype = prototype; + + // Enforce the constructor to be what we expect + Class.constructor = Class; + + // And make this class extendable + Class.extend = arguments.callee; + + return Class; + }; +})(); + +openerp.web.callback = function(obj, method) { + var callback = function() { + var args = Array.prototype.slice.call(arguments); + var r; + for(var i = 0; i < callback.callback_chain.length; i++) { + var c = callback.callback_chain[i]; + if(c.unique) { + callback.callback_chain.splice(i, 1); + i -= 1; + } + r = c.callback.apply(c.self, c.args.concat(args)); + // TODO special value to stop the chain + // openerp.web.callback_stop + } + return r; + }; + callback.callback_chain = []; + callback.add = function(f) { + if(typeof(f) == 'function') { + f = { callback: f, args: Array.prototype.slice.call(arguments, 1) }; + } + f.self = f.self || null; + f.args = f.args || []; + f.unique = !!f.unique; + if(f.position == 'last') { + callback.callback_chain.push(f); + } else { + callback.callback_chain.unshift(f); + } + return callback; + }; + callback.add_first = function(f) { + return callback.add.apply(null,arguments); + }; + callback.add_last = function(f) { + return callback.add({ + callback: f, + args: Array.prototype.slice.call(arguments, 1), + position: "last" + }); + }; + + return callback.add({ + callback: method, + self:obj, + args:Array.prototype.slice.call(arguments, 2) + }); +}; + +/** + * Generates an inherited class that replaces all the methods by null methods (methods + * that does nothing and always return undefined). + * + * @param {Class} claz + * @param {Object} add Additional functions to override. + * @return {Class} + */ +openerp.web.generate_null_object_class = function(claz, add) { + var newer = {}; + var copy_proto = function(prototype) { + for (var name in prototype) { + if(typeof prototype[name] == "function") { + newer[name] = function() {}; + } + } + if (prototype.prototype) + copy_proto(prototype.prototype); + }; + copy_proto(claz.prototype); + newer.init = openerp.web.Widget.prototype.init; + var tmpclass = claz.extend(newer); + return tmpclass.extend(add || {}); +}; + +/** + * web error for lookup failure + * + * @class + */ +openerp.web.NotFound = openerp.web.Class.extend( /** @lends openerp.web.NotFound# */ { +}); +openerp.web.KeyNotFound = openerp.web.NotFound.extend( /** @lends openerp.web.KeyNotFound# */ { + /** + * Thrown when a key could not be found in a mapping + * + * @constructs openerp.web.KeyNotFound + * @extends openerp.web.NotFound + * @param {String} key the key which could not be found + */ + init: function (key) { + this.key = key; + }, + toString: function () { + return "The key " + this.key + " was not found"; + } +}); +openerp.web.ObjectNotFound = openerp.web.NotFound.extend( /** @lends openerp.web.ObjectNotFound# */ { + /** + * Thrown when an object path does not designate a valid class or object + * in the openerp hierarchy. + * + * @constructs openerp.web.ObjectNotFound + * @extends openerp.web.NotFound + * @param {String} path the invalid object path + */ + init: function (path) { + this.path = path; + }, + toString: function () { + return "Could not find any object of path " + this.path; + } +}); +openerp.web.Registry = openerp.web.Class.extend( /** @lends openerp.web.Registry# */ { + /** + * Stores a mapping of arbitrary key (strings) to object paths (as strings + * as well). + * + * Resolves those paths at query time in order to always fetch the correct + * object, even if those objects have been overloaded/replaced after the + * registry was created. + * + * An object path is simply a dotted name from the openerp root to the + * object pointed to (e.g. ``"openerp.web.Session"`` for an OpenERP + * session object). + * + * @constructs openerp.web.Registry + * @param {Object} mapping a mapping of keys to object-paths + */ + init: function (mapping) { + this.map = mapping || {}; + }, + /** + * Retrieves the object matching the provided key string. + * + * @param {String} key the key to fetch the object for + * @returns {Class} the stored class, to initialize + * + * @throws {openerp.web.KeyNotFound} if the object was not in the mapping + * @throws {openerp.web.ObjectNotFound} if the object path was invalid + */ + get_object: function (key) { + var path_string = this.map[key]; + if (path_string === undefined) { + throw new openerp.web.KeyNotFound(key); + } + + var object_match = openerp; + var path = path_string.split('.'); + // ignore first section + for(var i=1; i<path.length; ++i) { + object_match = object_match[path[i]]; + + if (object_match === undefined) { + throw new openerp.web.ObjectNotFound(path_string); + } + } + return object_match; + }, + /** + * Tries a number of keys, and returns the first object matching one of + * the keys. + * + * @param {Array} keys a sequence of keys to fetch the object for + * @returns {Class} the first class found matching an object + * + * @throws {openerp.web.KeyNotFound} if none of the keys was in the mapping + * @trows {openerp.web.ObjectNotFound} if a found object path was invalid + */ + get_any: function (keys) { + for (var i=0; i<keys.length; ++i) { + try { + return this.get_object(keys[i]); + } catch (e) { + if (e instanceof openerp.web.KeyNotFound) { + continue; + } + throw e; + } + } + throw new openerp.web.KeyNotFound(keys.join(',')); + }, + /** + * Adds a new key and value to the registry. + * + * This method can be chained. + * + * @param {String} key + * @param {String} object_path fully qualified dotted object path + * @returns {openerp.web.Registry} itself + */ + add: function (key, object_path) { + this.map[key] = object_path; + return this; + }, + /** + * Creates and returns a copy of the current mapping, with the provided + * mapping argument added in (replacing existing keys if needed) + * + * @param {Object} [mapping={}] a mapping of keys to object-paths + */ + clone: function (mapping) { + return new openerp.web.Registry( + _.extend({}, this.map, mapping || {})); + } +}); + +openerp.web.CallbackEnabled = openerp.web.Class.extend(/** @lends openerp.web.CallbackEnabled# */{ + /** + * @constructs openerp.web.CallbackEnabled + * @extends openerp.web.Class + */ + init: function() { + // Transform on_* method into openerp.web.callbacks + for (var name in this) { + if(typeof(this[name]) == "function") { + this[name].debug_name = name; + // bind ALL function to this not only on_and _do ? + if((/^on_|^do_/).test(name)) { + this[name] = openerp.web.callback(this, this[name]); + } + } + } + } +}); + +openerp.web.Session = openerp.web.CallbackEnabled.extend( /** @lends openerp.web.Session# */{ + /** + * @constructs openerp.web.Session + * @extends openerp.web.CallbackEnabled + * + * @param {String} [server] JSON-RPC endpoint hostname + * @param {String} [port] JSON-RPC endpoint port + */ + init: function(server, port) { + this._super(); + this.server = (server == undefined) ? location.hostname : server; + this.port = (port == undefined) ? location.port : port; + this.rpc_mode = (server == location.hostname) ? "ajax" : "jsonp"; + this.debug = (window.location.search.indexOf('?debug') !== -1); + this.session_id = false; + this.uid = false; + this.user_context= {}; + this.db = false; + this.module_list = []; + this.module_loaded = {"web": true}; + this.context = {}; + this.shortcuts = []; + this.active_id = null; + }, + start: function() { + this.session_restore(); + }, + /** + * Executes an RPC call, registering the provided callbacks. + * + * Registers a default error callback if none is provided, and handles + * setting the correct session id and session context in the parameter + * objects + * + * @param {String} url RPC endpoint + * @param {Object} params call parameters + * @param {Function} success_callback function to execute on RPC call success + * @param {Function} error_callback function to execute on RPC call failure + * @returns {jQuery.Deferred} jquery-provided ajax deferred + */ + rpc: function(url, params, success_callback, error_callback) { + var self = this; + // Construct a JSON-RPC2 request, method is currently unused + params.session_id = this.session_id; + + // Call using the rpc_mode + var deferred = $.Deferred(); + this.rpc_ajax(url, { + jsonrpc: "2.0", + method: "call", + params: params, + id: _.uniqueId('browser-client-') + }).then(function () {deferred.resolve.apply(deferred, arguments);}, + function(error) {deferred.reject(error, $.Event());}); + return deferred.fail(function() { + deferred.fail(function(error, event) { + if (!event.isDefaultPrevented()) { + self.on_rpc_error(error, event); + } + }); + }).then(success_callback, error_callback).promise(); + }, + /** + * Raw JSON-RPC call + * + * @returns {jQuery.Deferred} ajax-webd deferred object + */ + rpc_ajax: function(url, payload) { + var self = this; + this.on_rpc_request(); + // url can be an $.ajax option object + if (_.isString(url)) { + url = { + url: url + } + } + var ajax = _.extend({ + type: "POST", + url: url, + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify(payload), + processData: false + }, url); + var deferred = $.Deferred(); + $.ajax(ajax).done(function(response, textStatus, jqXHR) { + self.on_rpc_response(); + if (!response.error) { + deferred.resolve(response["result"], textStatus, jqXHR); + return; + } + if (response.error.data.type !== "session_invalid") { + deferred.reject(response.error); + return; + } + self.uid = false; + self.on_session_invalid(function() { + self.rpc(url, payload.params, + function() { + deferred.resolve.apply(deferred, arguments); + }, + function(error, event) { + event.preventDefault(); + deferred.reject.apply(deferred, arguments); + }); + }); + }).fail(function(jqXHR, textStatus, errorThrown) { + self.on_rpc_response(); + var error = { + code: -32098, + message: "XmlHttpRequestError " + errorThrown, + data: {type: "xhr"+textStatus, debug: jqXHR.responseText, objects: [jqXHR, errorThrown] } + }; + deferred.reject(error); + }); + return deferred.promise(); + }, + on_rpc_request: function() { + }, + on_rpc_response: function() { + }, + on_rpc_error: function(error) { + }, + /** + * The session is validated either by login or by restoration of a previous session + */ + on_session_valid: function() { + if(!openerp._modules_loaded) + this.load_modules(); + }, + on_session_invalid: function(contination) { + }, + session_is_valid: function() { + return this.uid; + }, + session_login: function(db, login, password, success_callback) { + var self = this; + var params = { db: db, login: login, password: password }; + this.rpc("/web/session/login", params, function(result) { + self.session_id = result.session_id; + self.uid = result.uid; + self.user_context = result.context; + self.db = result.db; + self.session_save(); + self.on_session_valid(); + return true; + }).then(success_callback); + }, + /** + * Reloads uid and session_id from local storage, if they exist + */ + session_restore: function () { + var self = this; + this.session_id = this.get_cookie('session_id'); + return this.rpc("/web/session/get_session_info", {}).then(function(result) { + self.uid = result.uid; + self.user_context = result.context; + self.db = result.db; + if (self.uid) + self.on_session_valid(); + else + self.on_session_invalid(); + }); + }, + /** + * Saves the session id and uid locally + */ + session_save: function () { + this.set_cookie('session_id', this.session_id); + }, + logout: function() { + this.set_cookie('session_id', ''); + this.reload_client(); + }, + reload_client: function() { + window.location.reload(); + }, + /** + * Fetches a cookie stored by an openerp session + * + * @private + * @param name the cookie's name + */ + get_cookie: function (name) { + var nameEQ = this.element_id + '|' + name + '='; + var cookies = document.cookie.split(';'); + for(var i=0; i<cookies.length; ++i) { + var cookie = cookies[i].replace(/^\s*/, ''); + if(cookie.indexOf(nameEQ) === 0) { + return JSON.parse(decodeURIComponent(cookie.substring(nameEQ.length))); + } + } + return null; + }, + /** + * Create a new cookie with the provided name and value + * + * @private + * @param name the cookie's name + * @param value the cookie's value + * @param ttl the cookie's time to live, 1 year by default, set to -1 to delete + */ + set_cookie: function (name, value, ttl) { + ttl = ttl || 24*60*60*365; + document.cookie = [ + this.element_id + '|' + name + '=' + encodeURIComponent(JSON.stringify(value)), + 'max-age=' + ttl, + 'expires=' + new Date(new Date().getTime() + ttl*1000).toGMTString() + ].join(';'); + }, + /** + * Load additional web addons of that instance and init them + */ + load_modules: function() { + var self = this; + this.rpc('/web/session/modules', {}, function(result) { + self.module_list = result; + var lang = self.user_context.lang; + var params = { mods: ["web"].concat(result), lang: lang}; + self.rpc('/web/webclient/translations',params).then(function(transs) { + openerp.web._t.database.set_bundle(transs); + var modules = self.module_list.join(','); + var file_list = ["/web/static/lib/datejs/globalization/" + + self.user_context.lang.replace("_", "-") + ".js" + ]; + if(self.debug) { + self.rpc('/web/webclient/csslist', {"mods": modules}, self.do_load_css); + self.rpc('/web/webclient/jslist', {"mods": modules}, function(files) { + self.do_load_js(file_list.concat(files)); + }); + } else { + self.do_load_css(["/web/webclient/css?mods="+modules]); + self.do_load_js(file_list.concat(["/web/webclient/js?mods="+modules])); + } + openerp._modules_loaded = true; + }); + }); + }, + do_load_css: function (files) { + var self = this; + _.each(files, function (file) { + $('head').append($('<link>', { + 'href': file + (self.debug ? '?debug=' + (new Date().getTime()) : ''), + 'rel': 'stylesheet', + 'type': 'text/css' + })); + }); + }, + do_load_js: function(files) { + var self = this; + if(files.length != 0) { + var file = files.shift(); + var tag = document.createElement('script'); + tag.type = 'text/javascript'; + tag.src = file + (this.debug ? '?debug=' + (new Date().getTime()) : ''); + tag.onload = tag.onreadystatechange = function() { + if ( (tag.readyState && tag.readyState != "loaded" && tag.readyState != "complete") || tag.onload_done ) + return; + tag.onload_done = true; + self.do_load_js(files); + }; + $('head').append(tag); + self.do_load_js(files); + } else { + this.on_modules_loaded(); + } + }, + on_modules_loaded: function() { + for(var j=0; j<this.module_list.length; j++) { + var mod = this.module_list[j]; + if(this.module_loaded[mod]) + continue; + openerp[mod] = {}; + // init module mod + if(openerp._openerp[mod] != undefined) { + openerp._openerp[mod](openerp); + this.module_loaded[mod] = true; + } + } + }, + /** + * Cooperative file download implementation, for ajaxy APIs. + * + * Requires that the server side implements an httprequest correctly + * setting the `fileToken` cookie to the value provided as the `token` + * parameter. The cookie *must* be set on the `/` path and *must not* be + * `httpOnly`. + * + * It would probably also be a good idea for the response to use a + * `Content-Disposition: attachment` header, especially if the MIME is a + * "known" type (e.g. text/plain, or for some browsers application/json + * + * @param {Object} options + * @param {String} [options.url] used to dynamically create a form + * @param {Object} [options.data] data to add to the form submission. If can be used without a form, in which case a form is created from scratch. Otherwise, added to form data + * @param {HTMLFormElement} [options.form] the form to submit in order to fetch the file + * @param {Function} [options.success] callback in case of download success + * @param {Function} [options.error] callback in case of request error, provided with the error body + * @param {Function} [options.complete] called after both ``success`` and ``error` callbacks have executed + */ + get_file: function (options) { + // need to detect when the file is done downloading (not used + // yet, but we'll need it to fix the UI e.g. with a throbber + // while dump is being generated), iframe load event only fires + // when the iframe content loads, so we need to go smarter: + // http://geekswithblogs.net/GruffCode/archive/2010/10/28/detecting-the-file-download-dialog-in-the-browser.aspx + var timer, token = new Date().getTime(), + cookie_name = 'fileToken', cookie_length = cookie_name.length, + CHECK_INTERVAL = 1000, id = _.uniqueId('get_file_frame'), + remove_form = false; + + var $form, $form_data = $('<div>'); + + var complete = function () { + if (options.complete) { options.complete(); } + clearTimeout(timer); + $form_data.remove(); + $target.remove(); + if (remove_form && $form) { $form.remove(); } + }; + var $target = $('<iframe style="display: none;">') + .attr({id: id, name: id}) + .appendTo(document.body) + .load(function () { + if (options.error) { options.error(this.contentDocument.body); } + complete(); + }); + + if (options.form) { + $form = $(options.form); + } else { + remove_form = true; + $form = $('<form>', { + action: options.url, + method: 'POST' + }).appendTo(document.body); + } + + _(_.extend({}, options.data || {}, + {session_id: this.session_id, token: token})) + .each(function (value, key) { + $('<input type="hidden" name="' + key + '">') + .val(value) + .appendTo($form_data); + }); + + $form + .append($form_data) + .attr('target', id) + .get(0).submit(); + + var waitLoop = function () { + var cookies = document.cookie.split(';'); + // setup next check + timer = setTimeout(waitLoop, CHECK_INTERVAL); + for (var i=0; i<cookies.length; ++i) { + var cookie = cookies[i].replace(/^\s*/, ''); + if (!cookie.indexOf(cookie_name === 0)) { continue; } + var cookie_val = cookie.substring(cookie_length + 1); + if (parseInt(cookie_val, 10) !== token) { continue; } + + // clear cookie + document.cookie = _.sprintf("%s=;expires=%s;path=/", + cookie_name, new Date().toGMTString()); + if (options.success) { options.success(); } + complete(); + return; + } + }; + timer = setTimeout(waitLoop, CHECK_INTERVAL); + } +}); + +openerp.web.SessionAware = openerp.web.CallbackEnabled.extend(/** @lends openerp.web.SessionAware# */{ + /** + * Utility class that any class is allowed to extend to easy common manipulations. + * + * It provides rpc calls, callback on all methods preceded by "on_" or "do_" and a + * logging facility. + * + * @constructs openerp.web.SessionAware + * @extends openerp.web.CallbackEnabled + * + * @param {openerp.web.Session} session + */ + init: function(session) { + this._super(); + this.session = session; + }, + /** + * Performs a JSON-RPC call + * + * @param {String} url endpoint url + * @param {Object} data RPC parameters + * @param {Function} success RPC call success callback + * @param {Function} error RPC call error callback + * @returns {jQuery.Deferred} deferred object for the RPC call + */ + rpc: function(url, data, success, error) { + return this.session.rpc(url, data, success, error); + } +}); + +openerp.web.Widget = openerp.web.SessionAware.extend(/** @lends openerp.web.Widget# */{ + /** + * The name of the QWeb template that will be used for rendering. Must be + * redefined in subclasses or the default render() method can not be used. + * + * @type string + */ + template: null, + /** + * The prefix used to generate an id automatically. Should be redefined in + * subclasses. If it is not defined, a generic identifier will be used. + * + * @type string + */ + identifier_prefix: 'generic-identifier-', + /** + * Construct the widget and set its parent if a parent is given. + * + * @constructs openerp.web.Widget + * @extends openerp.web.SessionAware + * + * @param {openerp.web.Widget} parent Binds the current instance to the given Widget instance. + * When that widget is destroyed by calling stop(), the current instance will be + * destroyed too. Can be null. + * @param {String} element_id Deprecated. Sets the element_id. Only useful when you want + * to bind the current Widget to an already existing part of the DOM, which is not compatible + * with the DOM insertion methods provided by the current implementation of Widget. So + * for new components this argument should not be provided any more. + */ + init: function(parent, /** @deprecated */ element_id) { + this._super((parent || {}).session); + // if given an element_id, try to get the associated DOM element and save + // a reference in this.$element. Else just generate a unique identifier. + this.element_id = element_id; + this.element_id = this.element_id || _.uniqueId(this.identifier_prefix); + var tmp = document.getElementById(this.element_id); + this.$element = tmp ? $(tmp) : undefined; + + this.widget_parent = parent; + this.widget_children = []; + if(parent && parent.widget_children) { + parent.widget_children.push(this); + } + // useful to know if the widget was destroyed and should not be used anymore + this.widget_is_stopped = false; + }, + /** + * Render the current widget and appends it to the given jQuery object or Widget. + * + * @param target A jQuery object or a Widget instance. + */ + appendTo: function(target) { + var self = this; + return this._render_and_insert(function(t) { + self.$element.appendTo(t); + }, target); + }, + /** + * Render the current widget and prepends it to the given jQuery object or Widget. + * + * @param target A jQuery object or a Widget instance. + */ + prependTo: function(target) { + var self = this; + return this._render_and_insert(function(t) { + self.$element.prependTo(t); + }, target); + }, + /** + * Render the current widget and inserts it after to the given jQuery object or Widget. + * + * @param target A jQuery object or a Widget instance. + */ + insertAfter: function(target) { + var self = this; + return this._render_and_insert(function(t) { + self.$element.insertAfter(t); + }, target); + }, + /** + * Render the current widget and inserts it before to the given jQuery object or Widget. + * + * @param target A jQuery object or a Widget instance. + */ + insertBefore: function(target) { + var self = this; + return this._render_and_insert(function(t) { + self.$element.insertBefore(t); + }, target); + }, + _render_and_insert: function(insertion, target) { + var rendered = this.render(); + this.$element = $(rendered); + if (target instanceof openerp.web.Widget) + target = target.$element; + insertion(target); + this.on_inserted(this.$element, this); + return this.start(); + }, + on_inserted: function(element, widget) {}, + /** + * Renders the widget using QWeb, `this.template` must be defined. + * The context given to QWeb contains the "widget" key that references `this`. + * + * @param {Object} additional Additional context arguments to pass to the template. + */ + render: function (additional) { + return openerp.web.qweb.render(this.template, _.extend({widget: this}, additional || {})); + }, + /** + * Method called after rendering. Mostly used to bind actions, perform asynchronous + * calls, etc... + * + * By convention, the method should return a promise to inform the caller when + * this widget has been initialized. + * + * @returns {jQuery.Deferred} + */ + start: function() { + if (!this.$element) { + var tmp = document.getElementById(this.element_id); + this.$element = tmp ? $(tmp) : undefined; + } + return $.Deferred().done().promise(); + }, + /** + * Destroys the current widget, also destory all its children before destroying itself. + */ + stop: function() { + _.each(_.clone(this.widget_children), function(el) { + el.stop(); + }); + if(this.$element != null) { + this.$element.remove(); + } + if (this.widget_parent && this.widget_parent.widget_children) { + this.widget_parent.widget_children = _.without(this.widget_parent.widget_children, this); + } + this.widget_parent = null; + this.widget_is_stopped = true; + }, + /** + * Inform the action manager to do an action. Of course, this suppose that + * the action manager can be found amongst the ancestors of the current widget. + * If that's not the case this method will simply return `false`. + */ + do_action: function(action, on_finished) { + if (this.widget_parent) { + return this.widget_parent.do_action(action, on_finished); + } + return false; + }, + do_notify: function() { + if (this.widget_parent) { + return this.widget_parent.do_notify.apply(this,arguments); + } + return false; + }, + do_warn: function() { + if (this.widget_parent) { + return this.widget_parent.do_warn.apply(this,arguments); + } + return false; + }, + rpc: function(url, data, success, error) { + var def = $.Deferred().then(success, error); + var self = this; + this._super(url, data). then(function() { + if (!self.widget_is_stopped) + def.resolve.apply(def, arguments); + }, function() { + if (!self.widget_is_stopped) + def.reject.apply(def, arguments); + }); + return def.promise(); + } +}); + +/** + * @class + * @extends openerp.web.Widget + * @deprecated + * For retro compatibility only, the only difference with is that render() uses + * directly ``this`` instead of context with a ``widget`` key. + */ +openerp.web.OldWidget = openerp.web.Widget.extend(/** @lends openerp.web.OldWidget# */{ + render: function (additional) { + return openerp.web.qweb.render(this.template, _.extend(_.extend({}, this), additional || {})); + } +}); + +openerp.web.TranslationDataBase = openerp.web.Class.extend(/** @lends openerp.web.TranslationDataBase# */{ + /** + * @constructs openerp.web.TranslationDataBase + * @extends openerp.web.Class + */ + init: function() { + this.db = {}; + this.parameters = {"direction": 'ltr', + "date_format": '%m/%d/%Y', + "time_format": '%H:%M:%S', + "grouping": "[]", + "decimal_point": ".", + "thousands_sep": ","}; + }, + set_bundle: function(translation_bundle) { + var self = this; + this.db = {}; + var modules = _.keys(translation_bundle.modules).sort(); + if (_.include(modules, "web")) { + modules = ["web"].concat(_.without(modules, "web")); + } + _.each(modules, function(name) { + self.add_module_translation(translation_bundle.modules[name]); + }); + if (translation_bundle.lang_parameters) { + this.parameters = translation_bundle.lang_parameters; + } + }, + add_module_translation: function(mod) { + var self = this; + _.each(mod.messages, function(message) { + if (self.db[message.id] === undefined) { + self.db[message.id] = message.string; + } + }); + }, + build_translation_function: function() { + var self = this; + var fcnt = function(str) { + var tmp = self.get(str); + return tmp === undefined ? str : tmp; + }; + fcnt.database = this; + return fcnt; + }, + get: function(key) { + if (this.db[key]) + return this.db[key]; + return undefined; + } +}); + +openerp.web._t = new openerp.web.TranslationDataBase().build_translation_function(); + +}; + +// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js new file mode 100644 index 00000000000..1eb57241fa6 --- /dev/null +++ b/addons/web/static/src/js/data.js @@ -0,0 +1,833 @@ + +openerp.web.data = function(openerp) { + +/** + * Serializes the sort criterion array of a dataset into a form which can be + * consumed by OpenERP's RPC APIs. + * + * @param {Array} criterion array of fields, from first to last criteria, prefixed with '-' for reverse sorting + * @returns {String} SQL-like sorting string (``ORDER BY``) clause + */ +openerp.web.serialize_sort = function (criterion) { + return _.map(criterion, + function (criteria) { + if (criteria[0] === '-') { + return criteria.slice(1) + ' DESC'; + } + return criteria + ' ASC'; + }).join(', '); +}; + +openerp.web.DataGroup = openerp.web.Widget.extend( /** @lends openerp.web.DataGroup# */{ + /** + * Management interface between views and grouped collections of OpenERP + * records. + * + * The root DataGroup is instantiated with the relevant information + * (a session, a model, a domain, a context and a group_by sequence), the + * domain and context may be empty. It is then interacted with via + * :js:func:`~openerp.web.DataGroup.list`, which is used to read the + * content of the current grouping level. + * + * @constructs openerp.web.DataGroup + * @extends openerp.web.Widget + * + * @param {openerp.web.Session} session Current OpenERP session + * @param {String} model name of the model managed by this DataGroup + * @param {Array} domain search domain for this DataGroup + * @param {Object} context context of the DataGroup's searches + * @param {Array} group_by sequence of fields by which to group + * @param {Number} [level=0] nesting level of the group + */ + init: function(parent, model, domain, context, group_by, level) { + this._super(parent, null); + if (group_by) { + if (group_by.length || context['group_by_no_leaf']) { + return new openerp.web.ContainerDataGroup( this, model, domain, context, group_by, level); + } else { + return new openerp.web.GrouplessDataGroup( this, model, domain, context, level); + } + } + + this.model = model; + this.context = context; + this.domain = domain; + + this.level = level || 0; + }, + cls: 'DataGroup' +}); +openerp.web.ContainerDataGroup = openerp.web.DataGroup.extend( /** @lends openerp.web.ContainerDataGroup# */ { + /** + * + * @constructs openerp.web.ContainerDataGroup + * @extends openerp.web.DataGroup + * + * @param session + * @param model + * @param domain + * @param context + * @param group_by + * @param level + */ + init: function (parent, model, domain, context, group_by, level) { + this._super(parent, model, domain, context, null, level); + + this.group_by = group_by; + }, + /** + * The format returned by ``read_group`` is absolutely dreadful: + * + * * A ``__context`` key provides future grouping levels + * * A ``__domain`` key provides the domain for the next search + * * The current grouping value is provided through the name of the + * current grouping name e.g. if currently grouping on ``user_id``, then + * the ``user_id`` value for this group will be provided through the + * ``user_id`` key. + * * Similarly, the number of items in the group (not necessarily direct) + * is provided via ``${current_field}_count`` + * * Other aggregate fields are just dumped there + * + * This function slightly improves the grouping records by: + * + * * Adding a ``grouped_on`` property providing the current grouping field + * * Adding a ``value`` and a ``length`` properties which replace the + * ``$current_field`` and ``${current_field}_count`` ones + * * Moving aggregate values into an ``aggregates`` property object + * + * Context and domain keys remain as-is, they should not be used externally + * but in case they're needed... + * + * @param {Object} group ``read_group`` record + */ + transform_group: function (group) { + var field_name = this.group_by[0]; + // In cases where group_by_no_leaf and no group_by, the result of + // read_group has aggregate fields but no __context or __domain. + // Create default (empty) values for those so that things don't break + var fixed_group = _.extend( + {__context: {group_by: []}, __domain: []}, + group); + + var aggregates = {}; + _(fixed_group).each(function (value, key) { + if (key.indexOf('__') === 0 + || key === field_name + || key === field_name + '_count') { + return; + } + aggregates[key] = value || 0; + }); + + return { + __context: fixed_group.__context, + __domain: fixed_group.__domain, + + grouped_on: field_name, + // if terminal group (or no group) and group_by_no_leaf => use group.__count + length: fixed_group[field_name + '_count'] || fixed_group.__count, + value: fixed_group[field_name], + + openable: !(this.context['group_by_no_leaf'] + && fixed_group.__context.group_by.length === 0), + + aggregates: aggregates + }; + }, + fetch: function (fields) { + // internal method + var d = new $.Deferred(); + var self = this; + + this.rpc('/web/group/read', { + model: this.model, + context: this.context, + domain: this.domain, + fields: _.uniq(this.group_by.concat(fields)), + group_by_fields: this.group_by, + sort: openerp.web.serialize_sort(this.sort) + }, function () { }).then(function (response) { + var data_groups = _(response).map( + _.bind(self.transform_group, self)); + self.groups = data_groups; + d.resolveWith(self, [data_groups]); + }, function () { + d.rejectWith.apply(d, [self, arguments]); + }); + return d.promise(); + }, + /** + * The items of a list have the following properties: + * + * ``length`` + * the number of records contained in the group (and all of its + * sub-groups). This does *not* provide the size of the "next level" + * of the group, unless the group is terminal (no more groups within + * it). + * ``grouped_on`` + * the name of the field this level was grouped on, this is mostly + * used for display purposes, in order to know the name of the current + * level of grouping. The ``grouped_on`` should be the same for all + * objects of the list. + * ``value`` + * the value which led to this group (this is the value all contained + * records have for the current ``grouped_on`` field name). + * ``aggregates`` + * a mapping of other aggregation fields provided by ``read_group`` + * + * @param {Array} fields the list of fields to aggregate in each group, can be empty + * @param {Function} ifGroups function executed if any group is found (DataGroup.group_by is non-null and non-empty), called with a (potentially empty) list of groups as parameters. + * @param {Function} ifRecords function executed if there is no grouping left to perform, called with a DataSet instance as parameter + */ + list: function (fields, ifGroups, ifRecords) { + var self = this; + this.fetch(fields).then(function (group_records) { + ifGroups(_(group_records).map(function (group) { + var child_context = _.extend({}, self.context, group.__context); + return _.extend( + new openerp.web.DataGroup( + self, self.model, group.__domain, + child_context, child_context.group_by, + self.level + 1), + group, {sort: self.sort}); + })); + }); + } +}); +openerp.web.GrouplessDataGroup = openerp.web.DataGroup.extend( /** @lends openerp.web.GrouplessDataGroup# */ { + /** + * + * @constructs openerp.web.GrouplessDataGroup + * @extends openerp.web.DataGroup + * + * @param session + * @param model + * @param domain + * @param context + * @param level + */ + init: function (parent, model, domain, context, level) { + this._super(parent, model, domain, context, null, level); + }, + list: function (fields, ifGroups, ifRecords) { + ifRecords(_.extend( + new openerp.web.DataSetSearch(this, this.model), + {domain: this.domain, context: this.context, _sort: this.sort})); + } +}); +openerp.web.StaticDataGroup = openerp.web.GrouplessDataGroup.extend( /** @lends openerp.web.StaticDataGroup# */ { + /** + * A specialization of groupless data groups, relying on a single static + * dataset as its records provider. + * + * @constructs openerp.web.StaticDataGroup + * @extends openerp.web.GrouplessDataGroup + * @param {openep.web.DataSetStatic} dataset a static dataset backing the groups + */ + init: function (dataset) { + this.dataset = dataset; + }, + list: function (fields, ifGroups, ifRecords) { + ifRecords(this.dataset); + } +}); + +openerp.web.DataSet = openerp.web.Widget.extend( /** @lends openerp.web.DataSet# */{ + /** + * DateaManagement interface between views and the collection of selected + * OpenERP records (represents the view's state?) + * + * @constructs openerp.web.DataSet + * @extends openerp.web.Widget + * + * @param {String} model the OpenERP model this dataset will manage + */ + init: function(source_controller, model, context) { + // we don't want the dataset to be a child of anything! + this._super(null); + this.session = source_controller ? source_controller.session : undefined; + this.model = model; + this.context = context || {}; + this.index = null; + }, + start: function() { + }, + previous: function () { + this.index -= 1; + if (this.index < 0) { + this.index = this.ids.length - 1; + } + return this; + }, + next: function () { + this.index += 1; + if (this.index >= this.ids.length) { + this.index = 0; + } + return this; + }, + /** + * Read records. + * + * @param {Array} ids identifiers of the records to read + * @param {Array} fields fields to read and return, by default all fields are returned + * @param {Function} callback function called with read result + * @returns {$.Deferred} + */ + read_ids: function (ids, fields, callback) { + return this.rpc('/web/dataset/get', { + model: this.model, + ids: ids, + fields: fields, + context: this.get_context() + }, callback); + }, + /** + * Read a slice of the records represented by this DataSet, based on its + * domain and context. + * + * @param {Array} [fields] fields to read and return, by default all fields are returned + * @params {Object} options + * @param {Number} [options.offset=0] The index from which selected records should be returned + * @param {Number} [options.limit=null] The maximum number of records to return + * @param {Function} callback function called with read_slice result + * @returns {$.Deferred} + */ + read_slice: function (fields, options, callback) { + return null; + }, + /** + * Reads the current dataset record (from its index) + * + * @params {Array} [fields] fields to read and return, by default all fields are returned + * @params {Function} callback function called with read_index result + * @returns {$.Deferred} + */ + read_index: function (fields, callback) { + var def = $.Deferred().then(callback); + if (_.isEmpty(this.ids)) { + def.reject(); + } else { + fields = fields || false; + return this.read_ids([this.ids[this.index]], fields).then(function(records) { + def.resolve(records[0]); + }, function() { + def.reject.apply(def, arguments); + }); + } + return def.promise(); + }, + /** + * Reads default values for the current model + * + * @param {Array} [fields] fields to get default values for, by default all defaults are read + * @param {Function} callback function called with default_get result + * @returns {$.Deferred} + */ + default_get: function(fields, callback) { + return this.rpc('/web/dataset/default_get', { + model: this.model, + fields: fields, + context: this.get_context() + }, callback); + }, + /** + * Creates a new record in db + * + * @param {Object} data field values to set on the new record + * @param {Function} callback function called with operation result + * @param {Function} error_callback function called in case of creation error + * @returns {$.Deferred} + */ + create: function(data, callback, error_callback) { + return this.rpc('/web/dataset/create', { + model: this.model, + data: data, + context: this.get_context() + }, callback, error_callback); + }, + /** + * Saves the provided data in an existing db record + * + * @param {Number|String} id identifier for the record to alter + * @param {Object} data field values to write into the record + * @param {Function} callback function called with operation result + * @param {Function} error_callback function called in case of write error + * @returns {$.Deferred} + */ + write: function (id, data, options, callback, error_callback) { + var options = options || {}; + return this.rpc('/web/dataset/save', { + model: this.model, + id: id, + data: data, + context: this.get_context(options.context) + }, callback, error_callback); + }, + /** + * Deletes an existing record from the database + * + * @param {Number|String} ids identifier of the record to delete + * @param {Function} callback function called with operation result + * @param {Function} error_callback function called in case of deletion error + */ + unlink: function(ids, callback, error_callback) { + var self = this; + return this.call_and_eval("unlink", [ids, this.get_context()], null, 1, + callback, error_callback); + }, + /** + * Calls an arbitrary RPC method + * + * @param {String} method name of the method (on the current model) to call + * @param {Array} [args] arguments to pass to the method + * @param {Function} callback + * @param {Function} error_callback + * @returns {$.Deferred} + */ + call: function (method, args, callback, error_callback) { + return this.rpc('/web/dataset/call', { + model: this.model, + method: method, + args: args || [] + }, callback, error_callback); + }, + /** + * Calls an arbitrary method, with more crazy + * + * @param {String} method + * @param {Array} [args] + * @param {Number} [domain_index] index of a domain to evaluate in the args array + * @param {Number} [context_index] index of a context to evaluate in the args array + * @param {Function} callback + * @param {Function }error_callback + * @returns {$.Deferred} + */ + call_and_eval: function (method, args, domain_index, context_index, callback, error_callback) { + return this.rpc('/web/dataset/call', { + model: this.model, + method: method, + domain_id: domain_index || null, + context_id: context_index || null, + args: args || [] + }, callback, error_callback); + }, + /** + * Calls a button method, usually returning some sort of action + * + * @param {String} method + * @param {Array} [args] + * @param {Function} callback + * @param {Function} error_callback + * @returns {$.Deferred} + */ + call_button: function (method, args, callback, error_callback) { + return this.rpc('/web/dataset/call_button', { + model: this.model, + method: method, + domain_id: null, + context_id: 1, + args: args || [] + }, callback, error_callback); + }, + /** + * Fetches the "readable name" for records, based on intrinsic rules + * + * @param {Array} ids + * @param {Function} callback + * @returns {$.Deferred} + */ + name_get: function(ids, callback) { + return this.call_and_eval('name_get', [ids, this.get_context()], null, 1, callback); + }, + /** + * + * @param {String} name name to perform a search for/on + * @param {Array} [domain=[]] filters for the objects returned, OpenERP domain + * @param {String} [operator='ilike'] matching operator to use with the provided name value + * @param {Number} [limit=100] maximum number of matches to return + * @param {Function} callback function to call with name_search result + * @returns {$.Deferred} + */ + name_search: function (name, domain, operator, limit, callback) { + return this.call_and_eval('name_search', + [name || '', domain || false, operator || 'ilike', this.get_context(), limit || 100], + 1, 3, callback); + }, + /** + * @param name + * @param callback + */ + name_create: function(name, callback) { + return this.call_and_eval('name_create', [name, this.get_context()], null, 1, callback); + }, + exec_workflow: function (id, signal, callback) { + return this.rpc('/web/dataset/exec_workflow', { + model: this.model, + id: id, + signal: signal + }, callback); + }, + get_context: function(request_context) { + if (request_context) { + return new openerp.web.CompoundContext(this.context, request_context); + } + return this.context; + } +}); +openerp.web.DataSetStatic = openerp.web.DataSet.extend({ + init: function(parent, model, context, ids) { + this._super(parent, model, context); + // all local records + this.ids = ids || []; + }, + read_slice: function (fields, options, callback) { + // TODO remove fields from options + var self = this, + offset = options.offset || 0, + limit = options.limit || false, + fields = fields || false; + var end_pos = limit && limit !== -1 ? offset + limit : undefined; + return this.read_ids(this.ids.slice(offset, end_pos), fields, callback); + }, + set_ids: function (ids) { + this.ids = ids; + if (this.index !== null) { + this.index = this.index <= this.ids.length - 1 ? + this.index : (this.ids.length > 0 ? this.length - 1 : 0); + } + }, + unlink: function(ids) { + this.on_unlink(ids); + return $.Deferred().resolve({result: true}); + }, + on_unlink: function(ids) { + this.set_ids(_.without.apply(null, [this.ids].concat(ids))); + } +}); +openerp.web.DataSetSearch = openerp.web.DataSet.extend(/** @lends openerp.web.DataSetSearch */{ + /** + * @constructs openerp.web.DataSetSearch + * @extends openerp.web.DataSet + * + * @param {Object} parent + * @param {String} model + * @param {Object} context + * @param {Array} domain + */ + init: function(parent, model, context, domain) { + this._super(parent, model, context); + this.domain = domain || []; + this._sort = []; + this.offset = 0; + // subset records[offset:offset+limit] + // is it necessary ? + this.ids = []; + }, + /** + * Read a slice of the records represented by this DataSet, based on its + * domain and context. + * + * @params {Object} options + * @param {Array} [options.fields] fields to read and return, by default all fields are returned + * @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context + * @param {Array} [options.domain] domain data to add to the request payload, ANDed with the dataset's domain + * @param {Number} [options.offset=0] The index from which selected records should be returned + * @param {Number} [options.limit=null] The maximum number of records to return + * @param {Function} callback function called with read_slice result + * @returns {$.Deferred} + */ + read_slice: function (fields, options, callback) { + var self = this; + var options = options || {}; + var offset = options.offset || 0; + return this.rpc('/web/dataset/search_read', { + model: this.model, + fields: fields || false, + domain: this.get_domain(options.domain), + context: this.get_context(options.context), + sort: this.sort(), + offset: offset, + limit: options.limit || false + }, function (result) { + self.ids = result.ids; + self.offset = offset; + if (callback) { + callback(result.records); + } + }); + }, + get_domain: function (other_domain) { + if (other_domain) { + return new openerp.web.CompoundDomain(this.domain, other_domain); + } + return this.domain; + }, + /** + * Reads or changes sort criteria on the dataset. + * + * If not provided with any argument, serializes the sort criteria to + * an SQL-like form usable by OpenERP's ORM. + * + * If given a field, will set that field as first sorting criteria or, + * if the field is already the first sorting criteria, will reverse it. + * + * @param {String} [field] field to sort on, reverses it (toggle from ASC to DESC) if already the main sort criteria + * @param {Boolean} [force_reverse=false] forces inserting the field as DESC + * @returns {String|undefined} + */ + sort: function (field, force_reverse) { + if (!field) { + return openerp.web.serialize_sort(this._sort); + } + var reverse = force_reverse || (this._sort[0] === field); + this._sort.splice.apply( + this._sort, [0, this._sort.length].concat( + _.without(this._sort, field, '-' + field))); + + this._sort.unshift((reverse ? '-' : '') + field); + return undefined; + }, + unlink: function(ids, callback, error_callback) { + var self = this; + return this._super(ids, function(result) { + self.ids = _.without.apply(_, [self.ids].concat(ids)); + if (this.index !== null) { + self.index = self.index <= self.ids.length - 1 ? + self.index : (self.ids.length > 0 ? self.ids.length -1 : 0); + } + if (callback) + callback(result); + }, error_callback); + } +}); +openerp.web.BufferedDataSet = openerp.web.DataSetStatic.extend({ + virtual_id_prefix: "one2many_v_id_", + virtual_id_regex: /one2many_v_id_.*/, + debug_mode: true, + init: function() { + this._super.apply(this, arguments); + this.reset_ids([]); + }, + create: function(data, callback, error_callback) { + var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data}; + this.to_create.push(cached); + this.cache.push(cached); + this.on_change(); + var to_return = $.Deferred().then(callback); + to_return.resolve({result: cached.id}); + return to_return.promise(); + }, + write: function (id, data, options, callback) { + var self = this; + var record = _.detect(this.to_create, function(x) {return x.id === id;}); + record = record || _.detect(this.to_write, function(x) {return x.id === id;}); + var dirty = false; + if (record) { + for (var k in data) { + if (record.values[k] === undefined || record.values[k] !== data[k]) { + dirty = true; + break; + } + } + $.extend(record.values, data); + } else { + dirty = true; + record = {id: id, values: data}; + self.to_write.push(record); + } + var cached = _.detect(this.cache, function(x) {return x.id === id;}); + $.extend(cached.values, record.values); + if (dirty) + this.on_change(); + var to_return = $.Deferred().then(callback); + to_return.resolve({result: true}); + return to_return.promise(); + }, + unlink: function(ids, callback, error_callback) { + var self = this; + _.each(ids, function(id) { + if (! _.detect(self.to_create, function(x) { return x.id === id; })) { + self.to_delete.push({id: id}) + } + }); + this.to_create = _.reject(this.to_create, function(x) { return _.include(ids, x.id);}); + this.to_write = _.reject(this.to_write, function(x) { return _.include(ids, x.id);}); + this.cache = _.reject(this.cache, function(x) { return _.include(ids, x.id);}); + this.set_ids(_.without.apply(_, [this.ids].concat(ids))); + this.on_change(); + var to_return = $.Deferred().then(callback); + setTimeout(function () {to_return.resolve({result: true});}, 0); + return to_return.promise(); + }, + reset_ids: function(ids) { + this.set_ids(ids); + this.to_delete = []; + this.to_create = []; + this.to_write = []; + this.cache = []; + this.delete_all = false; + }, + on_change: function() {}, + read_ids: function (ids, fields, callback) { + var self = this; + var to_get = []; + _.each(ids, function(id) { + var cached = _.detect(self.cache, function(x) {return x.id === id;}); + var created = _.detect(self.to_create, function(x) {return x.id === id;}); + if (created) { + _.each(fields, function(x) {if (cached.values[x] === undefined) cached.values[x] = false;}); + } else { + if (!cached || !_.all(fields, function(x) {return cached.values[x] !== undefined})) + to_get.push(id); + } + }); + var completion = $.Deferred().then(callback); + var return_records = function() { + var records = _.map(ids, function(id) { + return _.extend({}, _.detect(self.cache, function(c) {return c.id === id;}).values, {"id": id}); + }); + if (self.debug_mode) { + if (_.include(records, undefined)) { + throw "Record not correctly loaded"; + } + } + setTimeout(function () {completion.resolve(records);}, 0); + }; + if(to_get.length > 0) { + var rpc_promise = this._super(to_get, fields, function(records) { + _.each(records, function(record, index) { + var id = to_get[index]; + var cached = _.detect(self.cache, function(x) {return x.id === id;}); + if (!cached) { + self.cache.push({id: id, values: record}); + } else { + // I assume cache value is prioritary + _.defaults(cached.values, record); + } + }); + return_records(); + }); + $.when(rpc_promise).fail(function() {completion.reject();}); + } else { + return_records(); + } + return completion.promise(); + } +}); +openerp.web.ReadOnlyDataSetSearch = openerp.web.DataSetSearch.extend({ + create: function(data, callback, error_callback) { + this.on_create(data); + var to_return = $.Deferred().then(callback); + setTimeout(function () {to_return.resolve({"result": undefined});}, 0); + return to_return.promise(); + }, + on_create: function(data) {}, + write: function (id, data, options, callback) { + this.on_write(id, data); + var to_return = $.Deferred().then(callback); + setTimeout(function () {to_return.resolve({"result": true});}, 0); + return to_return.promise(); + }, + on_write: function(id, data) {}, + unlink: function(ids, callback, error_callback) { + this.on_unlink(ids); + var to_return = $.Deferred().then(callback); + setTimeout(function () {to_return.resolve({"result": true});}, 0); + return to_return.promise(); + }, + on_unlink: function(ids) {} +}); + +openerp.web.Model = openerp.web.SessionAware.extend({ + init: function(session, model_name) { + this._super(session); + this.model_name = model_name; + }, + get_func: function(method_name) { + var self = this; + return function() { + if (method_name == "search_read") + return self._search_read.apply(self, arguments); + return self._call(method_name, _.toArray(arguments)); + }; + }, + _call: function (method, args) { + return this.rpc('/web/dataset/call', { + model: this.model_name, + method: method, + args: args + }).pipe(function(result) { + if (method == "read" && result instanceof Array && result.length > 0 && result[0]["id"]) { + var index = {}; + _.each(_.range(result.length), function(i) { + index[result[i]["id"]] = result[i]; + }) + result = _.map(args[0], function(x) {return index[x];}); + } + return result; + }); + }, + _search_read: function(domain, fields, offset, limit, order, context) { + return this.rpc('/web/dataset/search_read', { + model: this.model_name, + fields: fields, + offset: offset, + limit: limit, + domain: domain, + sort: order, + context: context + }).pipe(function(result) { + return result.records; + });; + } +}); + +openerp.web.CompoundContext = openerp.web.Class.extend({ + init: function () { + this.__ref = "compound_context"; + this.__contexts = []; + this.__eval_context = null; + var self = this; + _.each(arguments, function(x) { + self.add(x); + }); + }, + add: function (context) { + this.__contexts.push(context); + return this; + }, + set_eval_context: function (eval_context) { + this.__eval_context = eval_context; + return this; + }, + get_eval_context: function () { + return this.__eval_context; + } +}); + +openerp.web.CompoundDomain = openerp.web.Class.extend({ + init: function () { + this.__ref = "compound_domain"; + this.__domains = []; + this.__eval_context = null; + var self = this; + _.each(arguments, function(x) { + self.add(x); + }); + }, + add: function(domain) { + this.__domains.push(domain); + return this; + }, + set_eval_context: function(eval_context) { + this.__eval_context = eval_context; + return this; + }, + get_eval_context: function() { + return this.__eval_context; + } +}); +}; + +// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/js/data_export.js b/addons/web/static/src/js/data_export.js new file mode 100644 index 00000000000..51ac2cbd568 --- /dev/null +++ b/addons/web/static/src/js/data_export.js @@ -0,0 +1,391 @@ +openerp.web.data_export = function(openerp) { +var QWeb = openerp.web.qweb; +openerp.web.DataExport = openerp.web.Dialog.extend({ + template: 'ExportTreeView', + dialog_title: 'Export Data', + init: function(parent, dataset) { + this._super(parent); + this.records = {}; + this.dataset = dataset; + this.exports = new openerp.web.DataSetSearch( + this, 'ir.exports', this.dataset.get_context()); + }, + start: function() { + var self = this; + this._super.apply(this, arguments); + this.open({ + modal: true, + width: '55%', + height: 'auto', + position: 'top', + buttons : { + "Close" : function() { + self.close(); + }, + "Export To File" : function() { + self.on_click_export_data(); + } + }, + close: function(event, ui){ self.close();} + }); + self.$element.removeClass('ui-dialog-content ui-widget-content'); + self.$element.find('#add_field').click(function() { + if ($('#field-tree-structure tr.ui-selected')) { + var fld = self.$element.find('#field-tree-structure tr.ui-selected').find('a'); + for (var i = 0; i < fld.length; i++) { + var id = $(fld[i]).attr('id').split('-')[1]; + var string = $(fld[i]).attr('string'); + self.add_field(id, string); + } + self.$element.find('#field-tree-structure tr').removeClass('ui-selected'); + } + }); + self.$element.find('#remove_field').click(function() { + self.$element.find('#fields_list option:selected').remove(); + }); + self.$element.find('#remove_all_field').click(function() { + self.$element.find('#fields_list').empty(); + }); + this.$element.find('#export_new_list').click(this.on_show_save_list); + + var got_fields = new $.Deferred(); + this.$element.find('#import_compat').change(function() { + self.$element.find('#fields_list').empty(); + self.$element.find('#field-tree-structure').remove(); + var import_comp = self.$element.find("#import_compat").val(); + self.rpc("/web/export/get_fields", { + model: self.dataset.model, + import_compat: Boolean(import_comp) + }, function (records) { + got_fields.resolve(); + self.on_show_data(records); + }); + }).change(); + + return $.when( + got_fields, + this.rpc('/web/export/formats', {}, this.do_setup_export_formats), + this.show_exports_list()); + }, + do_setup_export_formats: function (formats) { + var $fmts = this.$element.find('#export_format'); + _(formats).each(function (format) { + $fmts.append(new Option(format[1], format[0])); + }); + }, + show_exports_list: function() { + var self = this; + if (self.$element.find('#saved_export_list').is(':hidden')) { + self.$element.find('#ExistsExportList').show(); + return; + } + return this.exports.read_slice(['name'], { + domain: [['resource', '=', this.dataset.model]] + }, function (export_list) { + if (!export_list.length) { + return; + } + self.$element.find('#ExistsExportList').append(QWeb.render('Exists.ExportList', {'existing_exports': export_list})); + self.$element.find('#saved_export_list').change(function() { + self.$element.find('#fields_list option').remove(); + var export_id = self.$element.find('#saved_export_list option:selected').val(); + if (export_id) { + self.rpc('/web/export/namelist', {'model': self.dataset.model, export_id: parseInt(export_id)}, self.do_load_export_field); + } + }); + self.$element.find('#delete_export_list').click(function() { + var select_exp = self.$element.find('#saved_export_list option:selected'); + if (select_exp.val()) { + self.exports.unlink([parseInt(select_exp.val(), 10)]); + select_exp.remove(); + if (self.$element.find('#saved_export_list option').length <= 1) { + self.$element.find('#ExistsExportList').hide(); + } + } + }); + }); + }, + do_load_export_field: function(field_list) { + var export_node = this.$element.find("#fields_list"); + _(field_list).each(function (field) { + export_node.append(new Option(field.label, field.name)); + }); + }, + on_show_save_list: function() { + var self = this; + var current_node = self.$element.find("#savenewlist"); + if (!(current_node.find("label")).length) { + current_node.append(QWeb.render('ExportNewList')); + current_node.find("#add_export_list").click(function() { + var value = current_node.find("#savelist_name").val(); + if (value) { + self.do_save_export_list(value); + } else { + alert("Pleae Enter Save Field List Name"); + } + }); + } else { + if (current_node.is(':hidden')) { + current_node.show(); + current_node.find("#savelist_name").val(""); + } else { + current_node.hide(); + } + } + }, + do_save_export_list: function(value) { + var self = this; + var fields = self.get_fields(); + if (!fields.length) { + return; + } + this.exports.create({ + name: value, + resource: this.dataset.model, + export_fields: _(fields).map(function (field) { + return [0, 0, {name: field}]; + }) + }, function (export_list_id) { + if (!export_list_id.result) { + return; + } + self.$element.find("#saved_export_list").append( + new Option(value, export_list_id.result)); + if (self.$element.find("#saved_export_list").is(":hidden")) { + self.show_exports_list(); + } + }); + this.on_show_save_list(); + this.$element.find("#fields_list option").remove(); + }, + on_click: function(id, record) { + var self = this; + if (!record['children']) { + return; + } + var model = record['params']['model'], + prefix = record['params']['prefix'], + name = record['params']['name']; + + if (!record.loaded) { + var import_comp = self.$element.find("#import_compat").val(); + self.rpc("/web/export/get_fields", { + model: model, + prefix: prefix, + parent_name: name, + import_compat: Boolean(import_comp), + parent_field_type : record['field_type'] + }, function(results) { + record.loaded = true; + self.on_show_data(results, record.id); + }); + } else { + self.showcontent(record.id); + } + }, + on_show_data: function(result, after) { + var self = this; + var imp_cmpt = Boolean(self.$element.find("#import_compat").val()); + + if (after) { + var current_tr = self.$element.find("tr[id='treerow-" + after + "']"); + current_tr.addClass('open'); + current_tr.find('img').attr('src','/web/static/src/img/collapse.gif'); + current_tr.after(QWeb.render('ExportTreeView-Secondary.children', {'fields': result})); + } else { + self.$element.find('#left_field_panel').append(QWeb.render('ExportTreeView-Secondary', {'fields': result})); + } + _.each(result, function(record) { + self.records[record.id] = record.value; + if (record.required) { + var required_fld = self.$element.find("tr[id='treerow-" + record.id + "']").find('#tree-column'); + required_fld.addClass("oe_export_requiredfield"); + } + self.$element.find("img[id='parentimg-" + record.id +"']").click(function() { + self.on_click(this.id, record); + }); + + self.$element.find("tr[id='treerow-" + record.id + "']").click(function(e) { + if (e.shiftKey) { + var frst_click, scnd_click = ''; + if (self.row_index == 0) { + self.row_index = this.rowIndex; + frst_click = self.$element.find("tr[id^='treerow-']")[self.row_index-1]; + $(frst_click).addClass("ui-selected"); + } else { + if (this.rowIndex >=self.row_index) { + for (var i = (self.row_index-1); i < this.rowIndex; i++) { + scnd_click = self.$element.find("tr[id^='treerow-']")[i]; + if (!$(scnd_click).find('#tree-column').hasClass("oe_export_readonlyfield")) { + $(scnd_click).addClass("ui-selected"); + } + } + } else { + for (var i = (self.row_index-1); i >= (this.rowIndex-1); i--) { + scnd_click = self.$element.find("tr[id^='treerow-']")[i]; + if (!$(scnd_click).find('#tree-column').hasClass("oe_export_readonlyfield")) { + $(scnd_click).addClass("ui-selected"); + } + } + } + } + } + self.row_index = this.rowIndex; + + self.$element.find("tr[id='treerow-" + record.id + "']").keyup(function() { + self.row_index = 0; + }); + var o2m_selection = self.$element.find("tr[id='treerow-" + record.id + "']").find('#tree-column'); + if ($(o2m_selection).hasClass("oe_export_readonlyfield")) { + return false; + } + if (e.ctrlKey) { + if ($(this).hasClass('ui-selected')) { + $(this).removeClass('ui-selected').find('a').blur(); + } else { + $(this).addClass('ui-selected').find('a').focus(); + } + } else if (!e.shiftKey) { + self.$element.find("tr.ui-selected") + .removeClass("ui-selected").find('a').blur(); + $(this).addClass("ui-selected").find('a').focus(); + } + return false; + }); + + self.$element.find("tr[id='treerow-" + record.id + "']").keydown(function(e) { + var keyCode = e.keyCode || e.which; + var arrow = {left: 37, up: 38, right: 39, down: 40 }; + switch (keyCode) { + case arrow.left: + if ($(this).hasClass('open')) { + self.on_click(this.id, record); + } + break; + case arrow.right: + if (!$(this).hasClass('open')) { + self.on_click(this.id, record); + } + break; + case arrow.up: + var elem = this; + $(elem).removeClass("ui-selected"); + while (!$(elem).prev().is(":visible")) { + elem = $(elem).prev(); + } + if (!$(elem).prev().find('#tree-column').hasClass("oe_export_readonlyfield")) { + $(elem).prev().addClass("ui-selected"); + } + $(elem).prev().find('a').focus(); + break; + case arrow.down: + var elem = this; + $(elem).removeClass("ui-selected"); + while(!$(elem).next().is(":visible")) { + elem = $(elem).next(); + } + if (!$(elem).next().find('#tree-column').hasClass("oe_export_readonlyfield")) { + $(elem).next().addClass("ui-selected"); + } + $(elem).next().find('a').focus(); + break; + } + }); + self.$element.find("tr[id='treerow-" + record.id + "']").dblclick(function() { + var $o2m_selection = self.$element.find("tr[id^='treerow-" + record.id + "']").find('#tree-column'); + if (!$o2m_selection.hasClass("oe_export_readonlyfield")) { + self.add_field(record.id, $(this).find("a").attr("string")); + } + }); + }); + self.$element.find('#fields_list').mouseover(function(event) { + if (event.relatedTarget) { + if (event.relatedTarget.attributes['id'] && event.relatedTarget.attributes['string']) { + var field_id = event.relatedTarget.attributes["id"]["value"]; + if (field_id && field_id.split("-")[0] === 'export') { + if (!self.$element.find("tr[id='treerow-" + field_id.split("-")[1] + "']").find('#tree-column').hasClass("oe_export_readonlyfield")) { + self.add_field(field_id.split("-")[1], event.relatedTarget.attributes["string"]["value"]); + } + } + } + } + }); + }, + showcontent: function(id) { + // show & hide the contents + var $this = this.$element.find("tr[id='treerow-" + id + "']"); + var is_open = $this.hasClass('open'); + $this.toggleClass('open'); + + var first_child = $this.find('img'); + if (is_open) { + first_child.attr('src', '/web/static/src/img/expand.gif'); + } else { + first_child.attr('src', '/web/static/src/img/collapse.gif'); + } + var child_field = this.$element.find("tr[id^='treerow-" + id +"/']"); + var child_len = (id.split("/")).length + 1; + for (var i = 0; i < child_field.length; i++) { + var $child = $(child_field[i]); + if (is_open) { + $child.hide(); + } else if (child_len == (child_field[i].id.split("/")).length) { + if ($child.hasClass('open')) { + $child.removeClass('open'); + $child.find('img').attr('src', '/web/static/src/img/expand.gif'); + } + $child.show(); + } + } + }, + add_field: function(field_id, string) { + var field_list = this.$element.find('#fields_list'); + if (this.$element.find("#fields_list option[value='" + field_id + "']") + && !this.$element.find("#fields_list option[value='" + field_id + "']").length) { + field_list.append(new Option(string, field_id)); + } + }, + get_fields: function() { + var export_field = []; + this.$element.find("#fields_list option").each(function() { + export_field.push($(this).val()); + }); + if (!export_field.length) { + alert('Please select fields to save export list...'); + } + return export_field; + }, + on_click_export_data: function() { + $.blockUI(this.$element); + var exported_fields = [], self = this; + this.$element.find("#fields_list option").each(function() { + var fieldname = self.records[$(this).val()]; + exported_fields.push({name: fieldname, label: $(this).text()}); + }); + if (_.isEmpty(exported_fields)) { + alert('Please select fields to export...'); + return; + } + + exported_fields.unshift({name: 'id', label: 'External ID'}); + var export_format = this.$element.find("#export_format").val(); + this.session.get_file({ + url: '/web/export/' + export_format, + data: {data: JSON.stringify({ + model: this.dataset.model, + fields: exported_fields, + ids: this.dataset.ids, + domain: this.dataset.domain, + import_compat: Boolean( + this.$element.find("#import_compat").val()) + })}, + complete: $.unblockUI + }); + }, + close: function() { + $(this.$dialog).remove(); + this._super(); + } +}); + +}; diff --git a/addons/web/static/src/js/dates.js b/addons/web/static/src/js/dates.js new file mode 100644 index 00000000000..036cf8cd90b --- /dev/null +++ b/addons/web/static/src/js/dates.js @@ -0,0 +1,138 @@ + +openerp.web.dates = function(openerp) { + +/** + * Converts a string to a Date javascript object using OpenERP's + * datetime string format (exemple: '2011-12-01 15:12:35'). + * + * The timezone is assumed to be UTC (standard for OpenERP 6.1) + * and will be converted to the browser's timezone. + * + * @param {String} str A string representing a datetime. + * @returns {Date} + */ +openerp.web.str_to_datetime = function(str) { + if(!str) { + return str; + } + var regex = /^(\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d)(?:\.\d+)?$/; + var res = regex.exec(str); + if ( !res ) { + throw "'" + str + "' is not a valid datetime"; + } + var obj = Date.parse(res[1] + " GMT"); + if (! obj) { + throw "'" + str + "' is not a valid datetime"; + } + return obj; +}; + +/** + * Converts a string to a Date javascript object using OpenERP's + * date string format (exemple: '2011-12-01'). + * + * @param {String} str A string representing a date. + * @returns {Date} + */ +openerp.web.str_to_date = function(str) { + if(!str) { + return str; + } + var regex = /^\d\d\d\d-\d\d-\d\d$/; + var res = regex.exec(str); + if ( !res ) { + throw "'" + str + "' is not a valid date"; + } + var obj = Date.parse(str); + if (! obj) { + throw "'" + str + "' is not a valid date"; + } + return obj; +}; + +/** + * Converts a string to a Date javascript object using OpenERP's + * time string format (exemple: '15:12:35'). + * + * @param {String} str A string representing a time. + * @returns {Date} + */ +openerp.web.str_to_time = function(str) { + if(!str) { + return str; + } + var regex = /^(\d\d:\d\d:\d\d)(?:\.\d+)?$/; + var res = regex.exec(str); + if ( !res ) { + throw "'" + str + "' is not a valid time"; + } + var obj = Date.parse(res[1]); + if (! obj) { + throw "'" + str + "' is not a valid time"; + } + return obj; +}; + +/* + * Left-pad provided arg 1 with zeroes until reaching size provided by second + * argument. + * + * @param {Number|String} str value to pad + * @param {Number} size size to reach on the final padded value + * @returns {String} padded string + */ +var zpad = function(str, size) { + str = "" + str; + return new Array(size - str.length + 1).join('0') + str; +}; + +/** + * Converts a Date javascript object to a string using OpenERP's + * datetime string format (exemple: '2011-12-01 15:12:35'). + * + * The timezone of the Date object is assumed to be the one of the + * browser and it will be converted to UTC (standard for OpenERP 6.1). + * + * @param {Date} obj + * @returns {String} A string representing a datetime. + */ +openerp.web.datetime_to_str = function(obj) { + if (!obj) { + return false; + } + return zpad(obj.getUTCFullYear(),4) + "-" + zpad(obj.getUTCMonth() + 1,2) + "-" + + zpad(obj.getUTCDate(),2) + " " + zpad(obj.getUTCHours(),2) + ":" + + zpad(obj.getUTCMinutes(),2) + ":" + zpad(obj.getUTCSeconds(),2); +}; + +/** + * Converts a Date javascript object to a string using OpenERP's + * date string format (exemple: '2011-12-01'). + * + * @param {Date} obj + * @returns {String} A string representing a date. + */ +openerp.web.date_to_str = function(obj) { + if (!obj) { + return false; + } + return zpad(obj.getFullYear(),4) + "-" + zpad(obj.getMonth() + 1,2) + "-" + + zpad(obj.getDate(),2); +}; + +/** + * Converts a Date javascript object to a string using OpenERP's + * time string format (exemple: '15:12:35'). + * + * @param {Date} obj + * @returns {String} A string representing a time. + */ +openerp.web.time_to_str = function(obj) { + if (!obj) { + return false; + } + return zpad(obj.getHours(),2) + ":" + zpad(obj.getMinutes(),2) + ":" + + zpad(obj.getSeconds(),2); +}; + +}; diff --git a/addons/web/static/src/js/form.js b/addons/web/static/src/js/form.js new file mode 100644 index 00000000000..c4540d591a2 --- /dev/null +++ b/addons/web/static/src/js/form.js @@ -0,0 +1,2588 @@ +openerp.web.form = function (openerp) { + +var _t = openerp.web._t; +var QWeb = openerp.web.qweb; + +openerp.web.views.add('form', 'openerp.web.FormView'); +openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# */{ + /** + * Indicates that this view is not searchable, and thus that no search + * view should be displayed (if there is one active). + */ + searchable: false, + template: "FormView", + /** + * @constructs openerp.web.FormView + * @extends openerp.web.View + * + * @param {openerp.web.Session} session the current openerp session + * @param {String} element_id this view's root element id + * @param {openerp.web.DataSet} dataset the dataset this view will work with + * @param {String} view_id the identifier of the OpenERP view object + * + * @property {openerp.web.Registry} registry=openerp.web.form.widgets widgets registry for this form view instance + */ + init: function(parent, element_id, dataset, view_id, options) { + this._super(parent, element_id); + this.set_default_options(options); + this.dataset = dataset; + this.model = dataset.model; + this.view_id = view_id; + this.fields_view = {}; + this.widgets = {}; + this.widgets_counter = 0; + this.fields = {}; + this.datarecord = {}; + this.ready = false; + this.show_invalid = true; + this.dirty = false; + this.default_focus_field = null; + this.default_focus_button = null; + this.registry = openerp.web.form.widgets; + this.has_been_loaded = $.Deferred(); + this.$form_header = null; + this.translatable_fields = []; + _.defaults(this.options, {"always_show_new_button": true}); + }, + start: function() { + if (this.embedded_view) { + var def = $.Deferred().then(this.on_loaded); + var self = this; + setTimeout(function() {def.resolve(self.embedded_view);}, 0); + return def.promise(); + } else { + var context = new openerp.web.CompoundContext(this.dataset.get_context()); + return this.rpc("/web/view/load", { + "model": this.model, + "view_id": this.view_id, + "view_type": "form", + toolbar: this.options.sidebar, + context: context + }, this.on_loaded); + } + }, + stop: function() { + if (this.sidebar) { + this.sidebar.attachments.stop(); + this.sidebar.stop(); + } + _.each(this.widgets, function(w) { + w.stop(); + }); + }, + on_loaded: function(data) { + var self = this; + this.fields_view = data; + var frame = new (this.registry.get_object('frame'))(this, this.fields_view.arch); + + this.$element.html(QWeb.render(this.template, { 'frame': frame, 'view': this })); + _.each(this.widgets, function(w) { + w.start(); + }); + this.$form_header = this.$element.find('#' + this.element_id + '_header'); + this.$form_header.find('div.oe_form_pager button[data-pager-action]').click(function() { + var action = $(this).data('pager-action'); + self.on_pager_action(action); + }); + + this.$form_header.find('button.oe_form_button_save').click(this.do_save); + this.$form_header.find('button.oe_form_button_save_edit').click(this.do_save_edit); + this.$form_header.find('button.oe_form_button_cancel').click(this.do_cancel); + this.$form_header.find('button.oe_form_button_new').click(this.on_button_new); + + if (this.session.debug) { + this.$form_header.find('button.oe_get_xml_view').click(function() { + $('<xmp>' + openerp.web.json_node_to_xml(self.fields_view.arch, true) + '</xmp>').dialog({ width: '95%', height: 600}); + }); + } + + if (this.options.sidebar && this.options.sidebar_id) { + this.sidebar = new openerp.web.Sidebar(this, this.options.sidebar_id); + this.sidebar.start(); + this.sidebar.do_unfold(); + this.sidebar.attachments = new openerp.web.form.SidebarAttachments(this.sidebar, this.sidebar.add_section('attachments', "Attachments"), this); + this.sidebar.add_toolbar(this.fields_view.toolbar); + this.set_common_sidebar_sections(this.sidebar); + } + this.has_been_loaded.resolve(); + }, + do_show: function () { + var promise; + if (this.dataset.index === null) { + // null index means we should start a new record + promise = this.on_button_new(); + } else { + promise = this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded); + } + this.$element.show(); + if (this.sidebar) { + this.sidebar.$element.show(); + } + return promise; + }, + do_hide: function () { + this.$element.hide(); + if (this.sidebar) { + this.sidebar.$element.hide(); + } + }, + on_record_loaded: function(record) { + if (!record) { + throw("Form: No record received"); + } + if (!record.id) { + this.$form_header.find('.oe_form_on_create').show(); + this.$form_header.find('.oe_form_on_update').hide(); + if (!this.options["always_show_new_button"]) { + this.$form_header.find('button.oe_form_button_new').hide(); + } + } else { + this.$form_header.find('.oe_form_on_create').hide(); + this.$form_header.find('.oe_form_on_update').show(); + this.$form_header.find('button.oe_form_button_new').show(); + } + this.dirty = false; + this.datarecord = record; + for (var f in this.fields) { + var field = this.fields[f]; + field.dirty = false; + field.set_value(this.datarecord[f] || false); + field.validate(); + } + if (!record.id) { + // New record: Second pass in order to trigger the onchanges + this.dirty = true; + this.show_invalid = false; + for (var f in record) { + var field = this.fields[f]; + if (field) { + field.dirty = true; + this.do_onchange(field); + } + } + } + this.on_form_changed(); + this.show_invalid = this.ready = true; + this.do_update_pager(record.id == null); + if (this.sidebar) { + this.sidebar.attachments.do_update(); + this.sidebar.$element.find('.oe_sidebar_translate').toggleClass('oe_hide', !record.id); + } + if (this.default_focus_field && !this.embedded_view) { + this.default_focus_field.focus(); + } + }, + on_form_changed: function() { + for (var w in this.widgets) { + w = this.widgets[w]; + w.process_modifiers(); + w.update_dom(); + } + }, + on_pager_action: function(action) { + switch (action) { + case 'first': + this.dataset.index = 0; + break; + case 'previous': + this.dataset.previous(); + break; + case 'next': + this.dataset.next(); + break; + case 'last': + this.dataset.index = this.dataset.ids.length - 1; + break; + } + this.reload(); + }, + do_update_pager: function(hide_index) { + var $pager = this.$element.find('#' + this.element_id + '_header div.oe_form_pager'); + var index = hide_index ? '-' : this.dataset.index + 1; + $pager.find('span.oe_pager_index').html(index); + $pager.find('span.oe_pager_count').html(this.dataset.ids.length); + }, + do_onchange: function(widget, processed) { + processed = processed || []; + if (widget.node.attrs.on_change) { + var self = this; + this.ready = false; + var onchange = _.trim(widget.node.attrs.on_change); + var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/); + if (call) { + var method = call[1], args = []; + var context_index = null; + var argument_replacement = { + 'False' : function() {return false;}, + 'True' : function() {return true;}, + 'None' : function() {return null;}, + 'context': function(i) { + context_index = i; + var ctx = widget.build_context ? widget.build_context() : {}; + return ctx; + } + }; + var parent_fields = null; + _.each(call[2].split(','), function(a, i) { + var field = _.trim(a); + if (field in argument_replacement) { + args.push(argument_replacement[field](i)); + return; + } else if (self.fields[field]) { + var value = self.fields[field].get_on_change_value(); + args.push(value == null ? false : value); + return; + } else { + var splitted = field.split('.'); + if (splitted.length > 1 && _.trim(splitted[0]) === "parent" && self.dataset.parent_view) { + if (parent_fields === null) { + parent_fields = self.dataset.parent_view.get_fields_values(); + } + var p_val = parent_fields[_.trim(splitted[1])]; + if (p_val !== undefined) { + args.push(p_val == null ? false : p_val); + return; + } + } + } + throw "Could not get field with name '" + field + + "' for onchange '" + onchange + "'"; + }); + var ajax = { + url: '/web/dataset/call', + async: false + }; + return this.rpc(ajax, { + model: this.dataset.model, + method: method, + args: [(this.datarecord.id == null ? [] : [this.datarecord.id])].concat(args), + context_id: context_index === null ? null : context_index + 1 + }, function(response) { + self.on_processed_onchange(response, processed); + }); + } else { + console.log("Wrong on_change format", on_change); + } + } + }, + on_processed_onchange: function(response, processed) { + var result = response; + if (result.value) { + for (var f in result.value) { + var field = this.fields[f]; + // If field is not defined in the view, just ignore it + if (field) { + var value = result.value[f]; + processed.push(field.name); + if (field.get_value() != value) { + field.set_value(value); + field.dirty = true; + if (_.indexOf(processed, field.name) < 0) { + this.do_onchange(field, processed); + } + } + } + } + this.on_form_changed(); + } + if (!_.isEmpty(result.warning)) { + $(QWeb.render("DialogWarning", result.warning)).dialog({ + modal: true, + buttons: { + Ok: function() { + $(this).dialog("close"); + } + } + }); + } + if (result.domain) { + // TODO: + } + this.ready = true; + }, + on_button_new: function() { + var self = this; + var def = $.Deferred(); + $.when(this.has_been_loaded).then(function() { + self.dataset.default_get( + _.keys(self.fields_view.fields)).then(self.on_record_loaded).then(function() { + def.resolve(); + }); + }); + return def.promise(); + }, + /** + * Triggers saving the form's record. Chooses between creating a new + * record or saving an existing one depending on whether the record + * already has an id property. + * + * @param {Function} success callback on save success + * @param {Boolean} [prepend_on_create=false] if ``do_save`` creates a new record, should that record be inserted at the start of the dataset (by default, records are added at the end) + */ + do_save: function(success, prepend_on_create) { + var self = this; + if (!this.ready) { + return false; + } + var form_dirty = false, + form_invalid = false, + values = {}, + first_invalid_field = null; + for (var f in this.fields) { + f = this.fields[f]; + if (!f.is_valid()) { + form_invalid = true; + f.update_dom(); + if (!first_invalid_field) { + first_invalid_field = f; + } + } else if (f.is_dirty()) { + form_dirty = true; + values[f.name] = f.get_value(); + } + } + if (form_invalid) { + first_invalid_field.focus(); + this.on_invalid(); + return false; + } else if (form_dirty) { + console.log("About to save", values); + if (!this.datarecord.id) { + return this.dataset.create(values, function(r) { + self.on_created(r, success, prepend_on_create); + }); + } else { + return this.dataset.write(this.datarecord.id, values, {}, function(r) { + self.on_saved(r, success); + }); + } + } else { + setTimeout(function() { + self.on_saved({ result: true }, success); + }); + return true; + } + }, + do_save_edit: function() { + this.do_save(); + //this.switch_readonly(); Use promises + }, + switch_readonly: function() { + }, + switch_editable: function() { + }, + on_invalid: function() { + var msg = "<ul>"; + _.each(this.fields, function(f) { + if (!f.is_valid()) { + msg += "<li>" + f.string + "</li>"; + } + }); + msg += "</ul>"; + this.notification.warn("The following fields are invalid :", msg); + }, + on_saved: function(r, success) { + if (!r.result) { + // should not happen in the server, but may happen for internal purpose + } else { + console.debug(_.sprintf("The record #%s has been saved.", this.datarecord.id)); + if (success) { + success(r); + } + this.reload(); + } + }, + /** + * Updates the form' dataset to contain the new record: + * + * * Adds the newly created record to the current dataset (at the end by + * default) + * * Selects that record (sets the dataset's index to point to the new + * record's id). + * * Updates the pager and sidebar displays + * + * @param {Object} r + * @param {Function} success callback to execute after having updated the dataset + * @param {Boolean} [prepend_on_create=false] adds the newly created record at the beginning of the dataset instead of the end + */ + on_created: function(r, success, prepend_on_create) { + if (!r.result) { + // should not happen in the server, but may happen for internal purpose + } else { + this.datarecord.id = r.result; + if (!prepend_on_create) { + this.dataset.ids.push(this.datarecord.id); + this.dataset.index = this.dataset.ids.length - 1; + } else { + this.dataset.ids.unshift(this.datarecord.id); + this.dataset.index = 0; + } + this.do_update_pager(); + if (this.sidebar) { + this.sidebar.attachments.do_update(); + } + console.debug("The record has been created with id #" + this.datarecord.id); + if (success) { + success(_.extend(r, {created: true})); + } + this.reload(); + } + }, + do_search: function (domains, contexts, groupbys) { + console.debug("Searching form"); + }, + on_action: function (action) { + console.debug('Executing action', action); + }, + do_cancel: function () { + console.debug("Cancelling form"); + }, + reload: function() { + if (this.dataset.index == null || this.dataset.index < 0) { + this.on_button_new(); + } else { + this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded); + } + }, + get_fields_values: function() { + var values = {}; + _.each(this.fields, function(value, key) { + var val = value.get_value(); + values[key] = val; + }); + return values; + }, + get_selected_ids: function() { + var id = this.dataset.ids[this.dataset.index]; + return id ? [id] : []; + } +}); +openerp.web.FormDialog = openerp.web.Dialog.extend({ + init: function(parent, options, view_id, dataset) { + this._super(parent, options); + this.dataset = dataset; + this.view_id = view_id; + return this; + }, + start: function() { + this._super(); + this.form = new openerp.web.FormView(this, this.element_id, this.dataset, this.view_id, { + sidebar: false, + pager: false + }); + this.form.start(); + this.form.on_created.add_last(this.on_form_dialog_saved); + this.form.on_saved.add_last(this.on_form_dialog_saved); + return this; + }, + load_id: function(id) { + var self = this; + return this.dataset.read_ids([id], _.keys(this.form.fields_view.fields), function(records) { + self.form.on_record_loaded(records[0]); + }); + }, + on_form_dialog_saved: function(r) { + this.close(); + } +}); + +/** @namespace */ +openerp.web.form = {}; + +openerp.web.form.SidebarAttachments = openerp.web.Widget.extend({ + init: function(parent, element_id, form_view) { + this._super(parent, element_id); + this.view = form_view; + }, + do_update: function() { + if (!this.view.datarecord.id) { + this.on_attachments_loaded([]); + } else { + (new openerp.web.DataSetSearch( + this, 'ir.attachment', this.view.dataset.get_context(), + [ + ['res_model', '=', this.view.dataset.model], + ['res_id', '=', this.view.datarecord.id], + ['type', 'in', ['binary', 'url']] + ])).read_slice(['name', 'url', 'type'], {}, this.on_attachments_loaded); + } + }, + on_attachments_loaded: function(attachments) { + this.attachments = attachments; + this.$element.html(QWeb.render('FormView.sidebar.attachments', this)); + this.$element.find('.oe-binary-file').change(this.on_attachment_changed); + this.$element.find('.oe-sidebar-attachment-delete').click(this.on_attachment_delete); + }, + on_attachment_changed: function(e) { + window[this.element_id + '_iframe'] = this.do_update; + var $e = $(e.target); + if ($e.val() != '') { + this.$element.find('form.oe-binary-form').submit(); + $e.parent().find('input[type=file]').attr('disabled', 'true'); + $e.parent().find('button').attr('disabled', 'true').find('img, span').toggle(); + } + }, + on_attachment_delete: function(e) { + var self = this, $e = $(e.currentTarget); + var name = _.trim($e.parent().find('a.oe-sidebar-attachments-link').text()); + if (confirm("Do you really want to delete the attachment " + name + " ?")) { + this.rpc('/web/dataset/unlink', { + model: 'ir.attachment', + ids: [parseInt($e.attr('data-id'))] + }, function(r) { + $e.parent().remove(); + self.notification.notify("Delete an attachment", "The attachment '" + name + "' has been deleted"); + }); + } + } +}); + +openerp.web.form.compute_domain = function(expr, fields) { + var stack = []; + for (var i = expr.length - 1; i >= 0; i--) { + var ex = expr[i]; + if (ex.length == 1) { + var top = stack.pop(); + switch (ex) { + case '|': + stack.push(stack.pop() || top); + continue; + case '&': + stack.push(stack.pop() && top); + continue; + case '!': + stack.push(!top); + continue; + default: + throw new Error('Unknown domain operator ' + ex); + } + } + + var field = fields[ex[0]]; + if (!field) { + throw new Error("Domain references unknown field : " + ex[0]); + } + var field_value = field.get_value ? fields[ex[0]].get_value() : fields[ex[0]].value; + var op = ex[1]; + var val = ex[2]; + + switch (op.toLowerCase()) { + case '=': + case '==': + stack.push(field_value == val); + break; + case '!=': + case '<>': + stack.push(field_value != val); + break; + case '<': + stack.push(field_value < val); + break; + case '>': + stack.push(field_value > val); + break; + case '<=': + stack.push(field_value <= val); + break; + case '>=': + stack.push(field_value >= val); + break; + case 'in': + stack.push(_(val).contains(field_value)); + break; + case 'not in': + stack.push(!_(val).contains(field_value)); + break; + default: + console.log("Unsupported operator in modifiers :", op); + } + } + return _.all(stack, _.identity); +}; + +openerp.web.form.Widget = openerp.web.Widget.extend(/** @lends openerp.web.form.Widget# */{ + template: 'Widget', + /** + * @constructs openerp.web.form.Widget + * @extends openerp.web.Widget + * + * @param view + * @param node + */ + init: function(view, node) { + this.view = view; + this.node = node; + this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}'); + this.type = this.type || node.tag; + this.element_name = this.element_name || this.type; + this.element_id = [this.view.element_id, this.element_name, this.view.widgets_counter++].join("_"); + + this._super(view, this.element_id); + + this.view.widgets[this.element_id] = this; + this.children = node.children; + this.colspan = parseInt(node.attrs.colspan || 1, 10); + this.decrease_max_width = 0; + + this.string = this.string || node.attrs.string; + this.help = this.help || node.attrs.help; + this.invisible = this.modifiers['invisible'] === true; + this.classname = 'oe_form_' + this.type; + + this.width = this.node.attrs.width; + }, + start: function() { + this.$element = $('#' + this.element_id); + }, + stop: function() { + if (this.$element) { + this.$element.remove(); + } + }, + process_modifiers: function() { + var compute_domain = openerp.web.form.compute_domain; + for (var a in this.modifiers) { + this[a] = compute_domain(this.modifiers[a], this.view.fields); + } + }, + update_dom: function() { + this.$element.toggle(!this.invisible); + }, + render: function() { + var template = this.template; + return QWeb.render(template, { "widget": this }); + } +}); + +openerp.web.form.WidgetFrame = openerp.web.form.Widget.extend({ + template: 'WidgetFrame', + init: function(view, node) { + this._super(view, node); + this.columns = parseInt(node.attrs.col || 4, 10); + this.x = 0; + this.y = 0; + this.table = []; + this.add_row(); + for (var i = 0; i < node.children.length; i++) { + var n = node.children[i]; + if (n.tag == "newline") { + this.add_row(); + } else { + this.handle_node(n); + } + } + this.set_row_cells_with(this.table[this.table.length - 1]); + }, + add_row: function(){ + if (this.table.length) { + this.set_row_cells_with(this.table[this.table.length - 1]); + } + var row = []; + this.table.push(row); + this.x = 0; + this.y += 1; + return row; + }, + set_row_cells_with: function(row) { + var bypass = 0, + max_width = 100; + for (var i = 0; i < row.length; i++) { + bypass += row[i].width === undefined ? 0 : 1; + max_width -= row[i].decrease_max_width; + } + var size_unit = Math.round(max_width / (this.columns - bypass)), + colspan_sum = 0; + for (var i = 0; i < row.length; i++) { + var w = row[i]; + colspan_sum += w.colspan; + if (w.width === undefined) { + var width = (i === row.length - 1 && colspan_sum === this.columns) ? max_width : Math.round(size_unit * w.colspan); + max_width -= width; + w.width = width + '%'; + } + } + }, + handle_node: function(node) { + var type = {}; + if (node.tag == 'field') { + type = this.view.fields_view.fields[node.attrs.name] || {}; + } + var widget = new (this.view.registry.get_any( + [node.attrs.widget, type.type, node.tag])) (this.view, node); + if (node.tag == 'field') { + if (!this.view.default_focus_field || node.attrs.default_focus == '1') { + this.view.default_focus_field = widget; + } + if (node.attrs.nolabel != '1') { + var label = new (this.view.registry.get_object('label')) (this.view, node); + label["for"] = widget; + this.add_widget(label, widget.colspan + 1); + } + } + this.add_widget(widget); + }, + add_widget: function(widget, colspan) { + colspan = colspan || widget.colspan; + var current_row = this.table[this.table.length - 1]; + if (current_row.length && (this.x + colspan) > this.columns) { + current_row = this.add_row(); + } + current_row.push(widget); + this.x += widget.colspan; + return widget; + } +}); + +openerp.web.form.WidgetNotebook = openerp.web.form.Widget.extend({ + template: 'WidgetNotebook', + init: function(view, node) { + this._super(view, node); + this.pages = []; + for (var i = 0; i < node.children.length; i++) { + var n = node.children[i]; + if (n.tag == "page") { + var page = new openerp.web.form.WidgetNotebookPage(this.view, n, this, this.pages.length); + this.pages.push(page); + } + } + }, + start: function() { + this._super.apply(this, arguments); + this.$element.tabs(); + this.view.on_button_new.add_last(this.do_select_first_visible_tab); + }, + do_select_first_visible_tab: function() { + for (var i = 0; i < this.pages.length; i++) { + var page = this.pages[i]; + if (page.invisible === false) { + this.$element.tabs('select', page.index); + break; + } + } + } +}); + +openerp.web.form.WidgetNotebookPage = openerp.web.form.WidgetFrame.extend({ + template: 'WidgetNotebookPage', + init: function(view, node, notebook, index) { + this.notebook = notebook; + this.index = index; + this.element_name = 'page_' + index; + this._super(view, node); + this.element_tab_id = this.element_id + '_tab'; + }, + start: function() { + this._super.apply(this, arguments); + this.$element_tab = $('#' + this.element_tab_id); + }, + update_dom: function() { + if (this.invisible && this.index === this.notebook.$element.tabs('option', 'selected')) { + this.notebook.do_select_first_visible_tab(); + } + this.$element_tab.toggle(!this.invisible); + this.$element.toggle(!this.invisible); + } +}); + +openerp.web.form.WidgetSeparator = openerp.web.form.Widget.extend({ + init: function(view, node) { + this._super(view, node); + this.template = "WidgetSeparator"; + this.orientation = node.attrs.orientation || 'horizontal'; + if (this.orientation === 'vertical') { + this.width = '1'; + } + this.classname += '_' + this.orientation; + } +}); + +openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({ + init: function(view, node) { + this._super(view, node); + this.template = "WidgetButton"; + if (this.string) { + // We don't have button key bindings in the webclient + this.string = this.string.replace(/_/g, ''); + } + if (node.attrs.default_focus == '1') { + // TODO fme: provide enter key binding to widgets + this.view.default_focus_button = this; + } + }, + start: function() { + this._super.apply(this, arguments); + this.$element.click(this.on_click); + }, + on_click: function(saved) { + var self = this; + if (!this.node.attrs.special && this.view.dirty && saved !== true) { + this.view.do_save(function() { + self.on_click(true); + }); + } else { + if (this.node.attrs.confirm) { + var dialog = $('<div>' + this.node.attrs.confirm + '</div>').dialog({ + title: 'Confirm', + modal: true, + buttons: { + Ok: function() { + self.on_confirmed(); + $(this).dialog("close"); + }, + Cancel: function() { + $(this).dialog("close"); + } + } + }); + } else { + this.on_confirmed(); + } + } + }, + on_confirmed: function() { + var self = this; + + this.view.execute_action( + this.node.attrs, this.view.dataset, this.view.datarecord.id, function () { + self.view.reload(); + }); + } +}); + +openerp.web.form.WidgetLabel = openerp.web.form.Widget.extend({ + init: function(view, node) { + this.element_name = 'label_' + node.attrs.name; + + this._super(view, node); + + // TODO fme: support for attrs.align + if (this.node.tag == 'label' && (this.node.attrs.colspan || (this.string && this.string.length > 32))) { + this.template = "WidgetParagraph"; + this.colspan = parseInt(this.node.attrs.colspan || 1, 10); + } else { + this.template = "WidgetLabel"; + this.colspan = 1; + this.width = '1%'; + this.decrease_max_width = 1; + this.nowrap = true; + } + }, + render: function () { + if (this['for'] && this.type !== 'label') { + return QWeb.render(this.template, {widget: this['for']}); + } + // Actual label widgets should not have a false and have type label + return QWeb.render(this.template, {widget: this}); + }, + start: function() { + this._super(); + var self = this; + this.$element.find("label").dblclick(function() { + var widget = self['for'] || self; + console.log(widget.element_id , widget); + window.w = widget; + }); + } +}); + +openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.form.Field# */{ + /** + * @constructs openerp.web.form.Field + * @extends openerp.web.form.Widget + * + * @param view + * @param node + */ + init: function(view, node) { + this.name = node.attrs.name; + this.value = undefined; + view.fields[this.name] = this; + this.type = node.attrs.widget || view.fields_view.fields[node.attrs.name].type; + this.element_name = "field_" + this.name + "_" + this.type; + + this._super(view, node); + + if (node.attrs.nolabel != '1' && this.colspan > 1) { + this.colspan--; + } + this.field = view.fields_view.fields[node.attrs.name] || {}; + this.string = node.attrs.string || this.field.string; + this.help = node.attrs.help || this.field.help; + this.nolabel = (this.field.nolabel || node.attrs.nolabel) === '1'; + this.readonly = this.modifiers['readonly'] === true; + this.required = this.modifiers['required'] === true; + this.invalid = false; + this.dirty = false; + + this.classname = 'oe_form_field_' + this.type; + }, + start: function() { + this._super.apply(this, arguments); + if (this.field.translate) { + this.view.translatable_fields.push(this); + this.$element.find('.oe_field_translate').click(this.on_translate); + } + }, + set_value: function(value) { + this.value = value; + this.invalid = false; + this.update_dom(); + this.on_value_changed(); + }, + set_value_from_ui: function() { + this.on_value_changed(); + }, + on_value_changed: function() { + }, + on_translate: function() { + this.view.open_translate_dialog(this); + }, + get_value: function() { + return this.value; + }, + is_valid: function() { + return !this.invalid; + }, + is_dirty: function() { + return this.dirty; + }, + get_on_change_value: function() { + return this.get_value(); + }, + update_dom: function() { + this._super.apply(this, arguments); + if (this.field.translate) { + this.$element.find('.oe_field_translate').toggle(!!this.view.datarecord.id); + } + if (!this.disable_utility_classes) { + this.$element.toggleClass('disabled', this.readonly); + this.$element.toggleClass('required', this.required); + if (this.view.show_invalid) { + this.$element.toggleClass('invalid', !this.is_valid()); + } + } + }, + on_ui_change: function() { + this.dirty = this.view.dirty = true; + this.validate(); + if (this.is_valid()) { + this.set_value_from_ui(); + this.view.do_onchange(this); + this.view.on_form_changed(); + } else { + this.update_dom(); + } + }, + validate: function() { + this.invalid = false; + }, + focus: function() { + }, + _build_view_fields_values: function() { + var a_dataset = this.view.dataset || {}; + var fields_values = this.view.get_fields_values(); + var parent_values = a_dataset.parent_view ? a_dataset.parent_view.get_fields_values() : {}; + fields_values.parent = parent_values; + return fields_values; + }, + /** + * Builds a new context usable for operations related to fields by merging + * the fields'context with the action's context. + */ + build_context: function() { + // I previously belevied contexts should be herrited, but now I doubt it + //var a_context = this.view.dataset.get_context() || {}; + var f_context = this.field.context || null; + // maybe the default_get should only be used when we do a default_get? + var v_context1 = this.node.attrs.default_get || {}; + var v_context2 = this.node.attrs.context || {}; + var v_context = new openerp.web.CompoundContext(v_context1, v_context2); + if (v_context1.__ref || v_context2.__ref || true) { //TODO niv: remove || true + var fields_values = this._build_view_fields_values(); + v_context.set_eval_context(fields_values); + } + // if there is a context on the node, overrides the model's context + var ctx = f_context || v_context; + return ctx; + }, + build_domain: function() { + var f_domain = this.field.domain || null; + var v_domain = this.node.attrs.domain || []; + if (!(v_domain instanceof Array) || true) { //TODO niv: remove || true + var fields_values = this._build_view_fields_values(); + v_domain = new openerp.web.CompoundDomain(v_domain).set_eval_context(fields_values); + } + // if there is a domain on the node, overrides the model's domain + return f_domain || v_domain; + } +}); + +openerp.web.form.FieldChar = openerp.web.form.Field.extend({ + init: function(view, node) { + this._super(view, node); + this.template = "FieldChar"; + }, + start: function() { + this._super.apply(this, arguments); + this.$element.find('input').change(this.on_ui_change); + }, + set_value: function(value) { + this._super.apply(this, arguments); + var show_value = openerp.web.format_value(value, this, ''); + this.$element.find('input').val(show_value); + }, + update_dom: function() { + this._super.apply(this, arguments); + this.$element.find('input').attr('disabled', this.readonly); + }, + set_value_from_ui: function() { + this.value = openerp.web.parse_value(this.$element.find('input').val(), this); + this._super(); + }, + validate: function() { + this.invalid = false; + try { + var value = openerp.web.parse_value(this.$element.find('input').val(), this, ''); + this.invalid = this.required && value === ''; + } catch(e) { + this.invalid = true; + } + }, + focus: function() { + this.$element.find('input').focus(); + } +}); + +openerp.web.form.FieldEmail = openerp.web.form.FieldChar.extend({ + init: function(view, node) { + this._super(view, node); + this.template = "FieldEmail"; + }, + start: function() { + this._super.apply(this, arguments); + this.$element.find('button').click(this.on_button_clicked); + }, + on_button_clicked: function() { + if (!this.value || !this.is_valid()) { + this.notification.warn("E-mail error", "Can't send email to invalid e-mail address"); + } else { + location.href = 'mailto:' + this.value; + } + }, + set_value: function(value) { + this._super.apply(this, arguments); + this.$element.find('a').attr('href', 'mailto:' + this.$element.find('input').val()); + } +}); + +openerp.web.form.FieldUrl = openerp.web.form.FieldChar.extend({ + init: function(view, node) { + this._super(view, node); + this.template = "FieldUrl"; + }, + start: function() { + this._super.apply(this, arguments); + this.$element.find('button').click(this.on_button_clicked); + }, + on_button_clicked: function() { + if (!this.value) { + this.notification.warn("Resource error", "This resource is empty"); + } else { + window.open(this.value); + } + } +}); + +openerp.web.form.FieldFloat = openerp.web.form.FieldChar.extend({ + set_value: function(value) { + if (value === false || value === undefined) { + // As in GTK client, floats default to 0 + value = 0; + this.dirty = true; + } + this._super.apply(this, [value]); + } +}); + +openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({ + init: function(view, node) { + this._super(view, node); + this.template = "FieldDate"; + this.jqueryui_object = 'datetimepicker'; + }, + start: function() { + this._super.apply(this, arguments); + this.$element.find('input').change(this.on_ui_change)[this.jqueryui_object]({ + dateFormat: 'yy-mm-dd', + timeFormat: 'hh:mm:ss', + showOn: 'button', + buttonImage: '/web/static/src/img/ui/field_calendar.png', + buttonImageOnly: true, + constrainInput: false + }); + }, + set_value: function(value) { + this._super.apply(this, arguments); + if (!value) { + this.$element.find('input').val(''); + } else { + this.$element.find('input').unbind('change'); + // jQuery UI date picker wrongly call on_change event herebelow + this.$element.find('input')[this.jqueryui_object]('setDate', this.parse(value)); + this.$element.find('input').change(this.on_ui_change); + } + }, + set_value_from_ui: function() { + this.value = this.$element.find('input')[this.jqueryui_object]('getDate') || false; + if (this.value) { + this.value = this.format(this.value); + } + this._super(); + }, + update_dom: function() { + this._super.apply(this, arguments); + this.$element.find('input').datepicker(this.readonly ? 'disable' : 'enable'); + }, + validate: function() { + this.invalid = false; + var value = this.$element.find('input').val(); + if (value === "") { + this.invalid = this.required; + } else { + this.invalid = !this.$element.find('input')[this.jqueryui_object]('getDate'); + } + }, + focus: function() { + this.$element.find('input').focus(); + }, + parse: openerp.web.auto_str_to_date, + format: function(val) { + return openerp.web.auto_date_to_str(val, this.field.type); + } +}); + +openerp.web.form.FieldDate = openerp.web.form.FieldDatetime.extend({ + init: function(view, node) { + this._super(view, node); + this.jqueryui_object = 'datepicker'; + } +}); + +openerp.web.form.FieldText = openerp.web.form.Field.extend({ + init: function(view, node) { + this._super(view, node); + this.template = "FieldText"; + }, + start: function() { + this._super.apply(this, arguments); + this.$element.find('textarea').change(this.on_ui_change); + }, + set_value: function(value) { + this._super.apply(this, arguments); + var show_value = openerp.web.format_value(value, this, ''); + this.$element.find('textarea').val(show_value); + }, + update_dom: function() { + this._super.apply(this, arguments); + this.$element.find('textarea').attr('disabled', this.readonly); + }, + set_value_from_ui: function() { + this.value = openerp.web.parse_value(this.$element.find('textarea').val(), this); + this._super(); + }, + validate: function() { + this.invalid = false; + try { + var value = openerp.web.parse_value(this.$element.find('textarea').val(), this, ''); + this.invalid = this.required && value === ''; + } catch(e) { + this.invalid = true; + } + }, + focus: function() { + this.$element.find('textarea').focus(); + } +}); + +openerp.web.form.FieldBoolean = openerp.web.form.Field.extend({ + init: function(view, node) { + this._super(view, node); + this.template = "FieldBoolean"; + }, + start: function() { + var self = this; + this._super.apply(this, arguments); + this.$element.find('input').click(function() { + if ($(this).is(':checked') != self.value) { + self.on_ui_change(); + } + }); + }, + set_value: function(value) { + this._super.apply(this, arguments); + this.$element.find('input')[0].checked = value; + }, + set_value_from_ui: function() { + this.value = this.$element.find('input').is(':checked'); + this._super(); + }, + update_dom: function() { + this._super.apply(this, arguments); + this.$element.find('input').attr('disabled', this.readonly); + }, + validate: function() { + this.invalid = this.required && !this.$element.find('input').is(':checked'); + }, + focus: function() { + this.$element.find('input').focus(); + } +}); + +openerp.web.form.FieldProgressBar = openerp.web.form.Field.extend({ + init: function(view, node) { + this._super(view, node); + this.template = "FieldProgressBar"; + }, + start: function() { + this._super.apply(this, arguments); + this.$element.find('div').progressbar({ + value: this.value, + disabled: this.readonly + }); + }, + set_value: function(value) { + this._super.apply(this, arguments); + var show_value = Number(value); + if (isNaN(show_value)) { + show_value = 0; + } + this.$element.find('div').progressbar('option', 'value', show_value).find('span').html(show_value + '%'); + } +}); + +openerp.web.form.FieldTextXml = openerp.web.form.Field.extend({ +// to replace view editor +}); + +openerp.web.form.FieldSelection = openerp.web.form.Field.extend({ + init: function(view, node) { + var self = this; + this._super(view, node); + this.template = "FieldSelection"; + this.values = this.field.selection; + _.each(this.values, function(v, i) { + if (v[0] === false && v[1] === '') { + self.values.splice(i, 1); + } + }); + this.values.unshift([false, '']); + }, + start: function() { + // Flag indicating whether we're in an event chain containing a change + // event on the select, in order to know what to do on keyup[RETURN]: + // * If the user presses [RETURN] as part of changing the value of a + // selection, we should just let the value change and not let the + // event broadcast further (e.g. to validating the current state of + // the form in editable list view, which would lead to saving the + // current row or switching to the next one) + // * If the user presses [RETURN] with a select closed (side-effect: + // also if the user opened the select and pressed [RETURN] without + // changing the selected value), takes the action as validating the + // row + var ischanging = false; + this._super.apply(this, arguments); + this.$element.find('select') + .change(this.on_ui_change) + .change(function () { ischanging = true; }) + .click(function () { ischanging = false; }) + .keyup(function (e) { + if (e.which !== 13 || !ischanging) { return; } + e.stopPropagation(); + ischanging = false; + }); + }, + set_value: function(value) { + value = value === null ? false : value; + value = value instanceof Array ? value[0] : value; + this._super(value); + var index = 0; + for (var i = 0, ii = this.values.length; i < ii; i++) { + if (this.values[i][0] === value) index = i; + } + this.$element.find('select')[0].selectedIndex = index; + }, + set_value_from_ui: function() { + this.value = this.values[this.$element.find('select')[0].selectedIndex][0]; + this._super(); + }, + update_dom: function() { + this._super.apply(this, arguments); + this.$element.find('select').attr('disabled', this.readonly); + }, + validate: function() { + var value = this.values[this.$element.find('select')[0].selectedIndex]; + this.invalid = !(value && !(this.required && value[0] === false)); + }, + focus: function() { + this.$element.find('select').focus(); + } +}); + +// jquery autocomplete tweak to allow html +(function() { + var proto = $.ui.autocomplete.prototype, + initSource = proto._initSource; + + function filter( array, term ) { + var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" ); + return $.grep( array, function(value) { + return matcher.test( $( "<div>" ).html( value.label || value.value || value ).text() ); + }); + } + + $.extend( proto, { + _initSource: function() { + if ( this.options.html && $.isArray(this.options.source) ) { + this.source = function( request, response ) { + response( filter( this.options.source, request.term ) ); + }; + } else { + initSource.call( this ); + } + }, + + _renderItem: function( ul, item) { + return $( "<li></li>" ) + .data( "item.autocomplete", item ) + .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) ) + .appendTo( ul ); + } + }); +})(); + +openerp.web.form.dialog = function(content, options) { + options = _.extend({ + autoOpen: true, + width: '90%', + height: '90%', + min_width: '800px', + min_height: '600px' + }, options || {}); + options.autoOpen = true; + var dialog = new openerp.web.Dialog(null, options); + dialog.$dialog = $(content).dialog(dialog.dialog_options); + return dialog.$dialog; +}; + +openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ + init: function(view, node) { + this._super(view, node); + this.template = "FieldMany2One"; + this.limit = 7; + this.value = null; + this.cm_id = _.uniqueId('m2o_cm_'); + this.last_search = []; + this.tmp_value = undefined; + }, + start: function() { + this._super(); + var self = this; + this.$input = this.$element.find("input"); + this.$drop_down = this.$element.find(".oe-m2o-drop-down-button"); + this.$menu_btn = this.$element.find(".oe-m2o-cm-button"); + + // context menu + var init_context_menu_def = $.Deferred().then(function(e) { + var rdataset = new openerp.web.DataSetStatic(self, "ir.values", self.build_context()); + rdataset.call("get", ['action', 'client_action_relate', + [[self.field.relation, false]], false, rdataset.get_context()], false, 0) + .then(function(result) { + self.related_entries = result; + + var $cmenu = $("#" + self.cm_id); + $cmenu.append(QWeb.render("FieldMany2One.context_menu", {widget: self})); + var bindings = {}; + bindings[self.cm_id + "_search"] = function() { + self._search_create_popup("search"); + }; + bindings[self.cm_id + "_create"] = function() { + self._search_create_popup("form"); + }; + bindings[self.cm_id + "_open"] = function() { + if (!self.value) { + return; + } + var pop = new openerp.web.form.FormOpenPopup(self.view); + pop.show_element(self.field.relation, self.value[0],self.build_context(), {}); + pop.on_write_completed.add_last(function() { + self.set_value(self.value[0]); + }); + }; + _.each(_.range(self.related_entries.length), function(i) { + bindings[self.cm_id + "_related_" + i] = function() { + self.open_related(self.related_entries[i]); + }; + }); + var cmenu = self.$menu_btn.contextMenu(self.cm_id, {'leftClickToo': true, + bindings: bindings, itemStyle: {"color": ""}, + onContextMenu: function() { + if(self.value) { + $("#" + self.cm_id + " .oe_m2o_menu_item_mandatory").removeClass("oe-m2o-disabled-cm"); + } else { + $("#" + self.cm_id + " .oe_m2o_menu_item_mandatory").addClass("oe-m2o-disabled-cm"); + } + return true; + }, menuStyle: {width: "200px"} + }); + setTimeout(function() {self.$menu_btn.trigger(e);}, 0); + }); + }); + var ctx_callback = function(e) {init_context_menu_def.resolve(e); e.preventDefault()}; + this.$menu_btn.bind('contextmenu', ctx_callback); + this.$menu_btn.click(ctx_callback); + + // some behavior for input + this.$input.keyup(function() { + if (self.$input.val() === "") { + self._change_int_value(null); + } else if (self.value === null || (self.value && self.$input.val() !== self.value[1])) { + self._change_int_value(undefined); + } + }); + this.$drop_down.click(function() { + if (self.$input.autocomplete("widget").is(":visible")) { + self.$input.autocomplete("close"); + } else { + if (self.value) { + self.$input.autocomplete("search", ""); + } else { + self.$input.autocomplete("search"); + } + self.$input.focus(); + } + }); + var anyoneLoosesFocus = function() { + if (!self.$input.is(":focus") && + !self.$input.autocomplete("widget").is(":visible") && + !self.value) { + if (self.value === undefined && self.last_search.length > 0) { + self._change_int_ext_value(self.last_search[0]); + } else { + self._change_int_ext_value(null); + } + } + }; + this.$input.focusout(anyoneLoosesFocus); + + var isSelecting = false; + // autocomplete + this.$input.autocomplete({ + source: function(req, resp) { self.get_search_result(req, resp); }, + select: function(event, ui) { + isSelecting = true; + var item = ui.item; + if (item.id) { + self._change_int_value([item.id, item.name]); + } else if (item.action) { + self._change_int_value(undefined); + item.action(); + return false; + } + }, + focus: function(e, ui) { + e.preventDefault(); + }, + html: true, + close: anyoneLoosesFocus, + minLength: 0, + delay: 0 + }); + // used to correct a bug when selecting an element by pushing 'enter' in an editable list + this.$input.keyup(function(e) { + if (e.which === 13) { + if (isSelecting) + e.stopPropagation(); + } + isSelecting = false; + }); + }, + // autocomplete component content handling + get_search_result: function(request, response) { + var search_val = request.term; + var self = this; + + var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context()); + + dataset.name_search(search_val, self.build_domain(), 'ilike', + this.limit + 1, function(data) { + self.last_search = data; + // possible selections for the m2o + var values = _.map(data, function(x) { + return {label: $('<span />').text(x[1]).html(), name:x[1], id:x[0]}; + }); + + // search more... if more results that max + if (values.length > self.limit) { + values = values.slice(0, self.limit); + values.push({label: _t("<em> Search More...</em>"), action: function() { + dataset.name_search(search_val, self.build_domain(), 'ilike' + , false, function(data) { + self._change_int_value(null); + self._search_create_popup("search", data); + }); + }}); + } + // quick create + var raw_result = _(data.result).map(function(x) {return x[1];}); + if (search_val.length > 0 && + !_.include(raw_result, search_val) && + (!self.value || search_val !== self.value[1])) { + values.push({label: _.sprintf(_t('<em> Create "<strong>%s</strong>"</em>'), + $('<span />').text(search_val).html()), action: function() { + self._quick_create(search_val); + }}); + } + // create... + values.push({label: _t("<em> Create and Edit...</em>"), action: function() { + self._change_int_value(null); + self._search_create_popup("form", undefined, {"default_name": search_val}); + }}); + + response(values); + }); + }, + _quick_create: function(name) { + var self = this; + var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context()); + dataset.name_create(name, function(data) { + self._change_int_ext_value(data); + }).fail(function(error, event) { + event.preventDefault(); + self._change_int_value(null); + self._search_create_popup("form", undefined, {"default_name": name}); + }); + }, + // all search/create popup handling + _search_create_popup: function(view, ids, context) { + var self = this; + var pop = new openerp.web.form.SelectCreatePopup(this); + pop.select_element(self.field.relation,{ + initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined, + initial_view: view, + disable_multiple_selection: true + }, self.build_domain(), + new openerp.web.CompoundContext(self.build_context(), context || {})); + pop.on_select_elements.add(function(element_ids) { + var dataset = new openerp.web.DataSetStatic(self, self.field.relation, self.build_context()); + dataset.name_get([element_ids[0]], function(data) { + self._change_int_ext_value(data[0]); + }); + }); + }, + _change_int_ext_value: function(value) { + this._change_int_value(value); + this.$input.val(this.value ? this.value[1] : ""); + }, + _change_int_value: function(value) { + this.value = value; + var back_orig_value = this.original_value; + if (this.value === null || this.value) { + this.original_value = this.value; + } + if (back_orig_value === undefined) { // first use after a set_value() + return; + } + if (this.value !== undefined && ((back_orig_value ? back_orig_value[0] : null) + !== (this.value ? this.value[0] : null))) { + this.on_ui_change(); + } + }, + set_value: function(value) { + value = value || null; + this.invalid = false; + var self = this; + this.tmp_value = value; + self.update_dom(); + self.on_value_changed(); + var real_set_value = function(rval) { + self.tmp_value = undefined; + self.value = rval; + self.original_value = undefined; + self._change_int_ext_value(rval); + }; + if(typeof(value) === "number") { + var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context()); + dataset.name_get([value], function(data) { + real_set_value(data[0]); + }).fail(function() {self.tmp_value = undefined;}); + } else { + setTimeout(function() {real_set_value(value);}, 0); + } + }, + get_value: function() { + if (this.tmp_value !== undefined) { + if (this.tmp_value instanceof Array) { + return this.tmp_value[0]; + } + return this.tmp_value ? this.tmp_value : false; + } + if (this.value === undefined) + return this.original_value ? this.original_value[0] : false; + return this.value ? this.value[0] : false; + }, + validate: function() { + this.invalid = false; + var val = this.tmp_value !== undefined ? this.tmp_value : this.value; + if (val === null) { + this.invalid = this.required; + } + }, + open_related: function(related) { + var self = this; + if (!self.value) + return; + var additional_context = { + active_id: self.value[0], + active_ids: [self.value[0]], + active_model: self.field.relation + }; + self.rpc("/web/action/load", { + action_id: related[2].id, + context: additional_context + }, function(result) { + result.result.context = _.extend(result.result.context || {}, additional_context); + self.do_action(result.result); + }); + } +}); + +/* +# Values: (0, 0, { fields }) create +# (1, ID, { fields }) update +# (2, ID) remove (delete) +# (3, ID) unlink one (target id or target of relation) +# (4, ID) link +# (5) unlink all (only valid for one2many) +*/ +var commands = { + // (0, _, {values}) + CREATE: 0, + 'create': function (values) { + return [commands.CREATE, false, values]; + }, + // (1, id, {values}) + UPDATE: 1, + 'update': function (id, values) { + return [commands.UPDATE, id, values]; + }, + // (2, id[, _]) + DELETE: 2, + 'delete': function (id) { + return [commands.DELETE, id, false]; + }, + // (3, id[, _]) removes relation, but not linked record itself + FORGET: 3, + 'forget': function (id) { + return [commands.FORGET, id, false]; + }, + // (4, id[, _]) + LINK_TO: 4, + 'link_to': function (id) { + return [commands.LINK_TO, id, false]; + }, + // (5[, _[, _]]) + DELETE_ALL: 5, + 'delete_all': function () { + return [5, false, false]; + }, + // (6, _, ids) replaces all linked records with provided ids + REPLACE_WITH: 6, + 'replace_with': function (ids) { + return [6, false, ids]; + } +}; +openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ + multi_selection: false, + init: function(view, node) { + this._super(view, node); + this.template = "FieldOne2Many"; + this.is_started = $.Deferred(); + this.form_last_update = $.Deferred(); + this.disable_utility_classes = true; + }, + start: function() { + this._super.apply(this, arguments); + + var self = this; + + this.dataset = new openerp.web.form.One2ManyDataSet(this, this.field.relation); + this.dataset.o2m = this; + this.dataset.parent_view = this.view; + this.dataset.on_change.add_last(function() { + self.on_ui_change(); + }); + + var modes = this.node.attrs.mode; + modes = !!modes ? modes.split(",") : ["tree", "form"]; + var views = []; + _.each(modes, function(mode) { + var view = { + view_id: false, + view_type: mode == "tree" ? "list" : mode, + options: { sidebar : false } + }; + if (self.field.views && self.field.views[mode]) { + view.embedded_view = self.field.views[mode]; + } + if(view.view_type === "list") { + view.options.selectable = self.multi_selection; + } + views.push(view); + }); + this.views = views; + + this.viewmanager = new openerp.web.ViewManager(this, this.dataset, views); + this.viewmanager.registry = openerp.web.views.clone({ + list: 'openerp.web.form.One2ManyListView', + form: 'openerp.web.form.One2ManyFormView' + }); + var once = $.Deferred().then(function() { + self.form_last_update.resolve(); + }); + this.viewmanager.on_controller_inited.add_last(function(view_type, controller) { + if (view_type == "list") { + controller.o2m = self; + } else if (view_type == "form") { + controller.on_record_loaded.add_last(function() { + once.resolve(); + }); + controller.on_pager_action.add_first(function() { + self.save_form_view(); + }); + controller.$element.find(".oe_form_button_save_edit").hide(); + } + self.is_started.resolve(); + }); + this.viewmanager.on_mode_switch.add_first(function() { + self.save_form_view(); + }); + setTimeout(function () { + self.viewmanager.appendTo(self.$element); + }, 0); + }, + reload_current_view: function() { + var self = this; + var view = self.viewmanager.views[self.viewmanager.active_view].controller; + if(self.viewmanager.active_view === "list") { + view.reload_content(); + } else if (self.viewmanager.active_view === "form") { + if (this.dataset.index === null && this.dataset.ids.length >= 1) { + this.dataset.index = 0; + } + this.form_last_update.then(function() { + this.form_last_update = view.do_show(); + }); + } + }, + set_value: function(value) { + value = value || []; + var self = this; + this.dataset.reset_ids([]); + if(value.length >= 1 && value[0] instanceof Array) { + var ids = []; + _.each(value, function(command) { + var obj = {values: command[2]}; + switch (command[0]) { + case commands.CREATE: + obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix); + self.dataset.to_create.push(obj); + self.dataset.cache.push(_.clone(obj)); + ids.push(obj.id); + return; + case commands.UPDATE: + obj['id'] = command[1]; + self.dataset.to_write.push(obj); + self.dataset.cache.push(_.clone(obj)); + ids.push(obj.id); + return; + case commands.DELETE: + self.dataset.to_delete.push({id: command[1]}); + return; + case commands.LINK_TO: + ids.push(command[1]); + return; + case commands.DELETE_ALL: + self.dataset.delete_all = true; + return; + } + }); + this._super(ids); + this.dataset.set_ids(ids); + } else if (value.length >= 1 && typeof(value[0]) === "object") { + var ids = []; + this.dataset.delete_all = true; + _.each(value, function(command) { + var obj = {values: command}; + obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix); + self.dataset.to_create.push(obj); + self.dataset.cache.push(_.clone(obj)); + ids.push(obj.id); + }); + this._super(ids); + this.dataset.set_ids(ids); + } else { + this._super(value); + this.dataset.reset_ids(value); + } + if (this.dataset.index === null && this.dataset.ids.length > 0) { + this.dataset.index = 0; + } + $.when(this.is_started).then(function() { + self.reload_current_view(); + }); + }, + get_value: function() { + var self = this; + if (!this.dataset) + return []; + var val = this.dataset.delete_all ? [commands.delete_all()] : []; + val = val.concat(_.map(this.dataset.ids, function(id) { + var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;}); + if (alter_order) { + return commands.create(alter_order.values); + } + alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;}); + if (alter_order) { + return commands.update(alter_order.id, alter_order.values); + } + return commands.link_to(id); + })); + return val.concat(_.map( + this.dataset.to_delete, function(x) { + return commands['delete'](x.id);})); + }, + save_form_view: function() { + if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view && + this.viewmanager.views[this.viewmanager.active_view] && + this.viewmanager.views[this.viewmanager.active_view].controller) { + var view = this.viewmanager.views[this.viewmanager.active_view].controller; + if (this.viewmanager.active_view === "form") { + var res = $.when(view.do_save()); + if (res === false) { + // ignore + } else if (res.isRejected()) { + throw "Save or create on one2many dataset is not supposed to fail."; + } else if (!res.isResolved()) { + throw "Asynchronous get_value() is not supported in form view."; + } + return res; + } + } + return false; + }, + is_valid: function() { + this.validate(); + return this._super(); + }, + validate: function() { + this.invalid = false; + var self = this; + var view = self.viewmanager.views[self.viewmanager.active_view].controller; + if (self.viewmanager.active_view === "form") { + for (var f in view.fields) { + f = view.fields[f]; + if (!f.is_valid()) { + this.invalid = true; + return; + } + } + } + }, + is_dirty: function() { + this.save_form_view(); + return this._super(); + }, + update_dom: function() { + this._super.apply(this, arguments); + this.$element.toggleClass('disabled', this.readonly); + } +}); + +openerp.web.form.One2ManyDataSet = openerp.web.BufferedDataSet.extend({ + get_context: function() { + this.context = this.o2m.build_context(); + return this.context; + } +}); + +openerp.web.form.One2ManyFormView = openerp.web.FormView.extend({ +}); + +openerp.web.form.One2ManyListView = openerp.web.ListView.extend({ + do_add_record: function () { + if (this.options.editable) { + this._super.apply(this, arguments); + } else { + var self = this; + var pop = new openerp.web.form.SelectCreatePopup(this); + pop.select_element(self.o2m.field.relation,{ + initial_view: "form", + alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined, + create_function: function(data) { + return self.o2m.dataset.create(data, function(r) { + self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r.result])); + self.o2m.dataset.on_change(); + }); + }, + parent_view: self.o2m.view + }, self.o2m.build_domain(), self.o2m.build_context()); + pop.on_select_elements.add_last(function() { + self.o2m.reload_current_view(); + }); + } + }, + do_activate_record: function(index, id) { + var self = this; + var pop = new openerp.web.form.FormOpenPopup(self.o2m.view); + pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(),{ + auto_write: false, + alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined, + parent_view: self.o2m.view, + read_function: function() { + return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments); + } + }); + pop.on_write.add(function(id, data) { + self.o2m.dataset.write(id, data, {}, function(r) { + self.o2m.reload_current_view(); + }); + }); + } +}); + +openerp.web.form.FieldMany2Many = openerp.web.form.Field.extend({ + multi_selection: false, + init: function(view, node) { + this._super(view, node); + this.template = "FieldMany2Many"; + this.list_id = _.uniqueId("many2many"); + this.is_started = $.Deferred(); + }, + start: function() { + this._super.apply(this, arguments); + + var self = this; + + this.dataset = new openerp.web.form.Many2ManyDataSet(this, this.field.relation); + this.dataset.m2m = this; + this.dataset.on_unlink.add_last(function(ids) { + self.on_ui_change(); + }); + + this.list_view = new openerp.web.form.Many2ManyListView(this, this.list_id, this.dataset, false, { + 'addable': 'Add', + 'selectable': self.multi_selection + }); + this.list_view.m2m_field = this; + this.list_view.on_loaded.add_last(function() { + self.is_started.resolve(); + }); + setTimeout(function () { + self.list_view.start(); + }, 0); + }, + set_value: function(value) { + value = value || []; + if (value.length >= 1 && value[0] instanceof Array) { + value = value[0][2]; + } + this._super(value); + this.dataset.set_ids(value); + var self = this; + $.when(this.is_started).then(function() { + self.list_view.reload_content(); + }); + }, + get_value: function() { + return [commands.replace_with(this.dataset.ids)]; + }, + validate: function() { + this.invalid = false; + // TODO niv + } +}); + +openerp.web.form.Many2ManyDataSet = openerp.web.DataSetStatic.extend({ + get_context: function() { + this.context = this.m2m.build_context(); + return this.context; + } +}); + +/** + * @class + * @extends openerp.web.ListView + */ +openerp.web.form.Many2ManyListView = openerp.web.ListView.extend(/** @lends openerp.web.form.Many2ManyListView# */{ + do_add_record: function () { + var pop = new openerp.web.form.SelectCreatePopup(this); + pop.select_element(this.model, {}, + new openerp.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]), + this.m2m_field.build_context()); + var self = this; + pop.on_select_elements.add(function(element_ids) { + _.each(element_ids, function(element_id) { + if(! _.detect(self.dataset.ids, function(x) {return x == element_id;})) { + self.dataset.set_ids([].concat(self.dataset.ids, [element_id])); + self.m2m_field.on_ui_change(); + self.reload_content(); + } + }); + }); + }, + do_activate_record: function(index, id) { + var self = this; + var pop = new openerp.web.form.FormOpenPopup(this); + pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {}); + pop.on_write_completed.add_last(function() { + self.reload_content(); + }); + } +}); + +/** + * @class + * @extends openerp.web.OldWidget + */ +openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends openerp.web.form.SelectCreatePopup# */{ + identifier_prefix: "selectcreatepopup", + template: "SelectCreatePopup", + /** + * options: + * - initial_ids + * - initial_view: form or search (default search) + * - disable_multiple_selection + * - alternative_form_view + * - create_function (defaults to a naive saving behavior) + * - parent_view + */ + select_element: function(model, options, domain, context) { + var self = this; + this.model = model; + this.domain = domain || []; + this.context = context || {}; + this.options = _.defaults(options || {}, {"initial_view": "search", "create_function": function() { + return self.create_row.apply(self, arguments); + }}); + this.initial_ids = this.options.initial_ids; + this.created_elements = []; + openerp.web.form.dialog(this.render(), {close:function() { + self.check_exit(); + }}); + this.start(); + }, + start: function() { + this._super(); + this.dataset = new openerp.web.ReadOnlyDataSetSearch(this, this.model, + this.context); + this.dataset.parent_view = this.options.parent_view; + if (this.options.initial_view == "search") { + this.setup_search_view(); + } else { // "form" + this.new_object(); + } + }, + setup_search_view: function() { + var self = this; + if (this.searchview) { + this.searchview.stop(); + } + this.searchview = new openerp.web.SearchView(this, + this.element_id + "_search", this.dataset, false, { + "selectable": !this.options.disable_multiple_selection, + "deletable": false + }); + this.searchview.on_search.add(function(domains, contexts, groupbys) { + if (self.initial_ids) { + self.view_list.do_search.call(self, domains.concat([[["id", "in", self.initial_ids]], self.domain]), + contexts, groupbys); + self.initial_ids = undefined; + } else { + self.view_list.do_search.call(self, domains.concat([self.domain]), contexts, groupbys); + } + }); + this.searchview.on_loaded.add_last(function () { + var $buttons = self.searchview.$element.find(".oe_search-view-buttons"); + $buttons.append(QWeb.render("SelectCreatePopup.search.buttons")); + var $cbutton = $buttons.find(".oe_selectcreatepopup-search-close"); + $cbutton.click(function() { + self.stop(); + }); + var $sbutton = $buttons.find(".oe_selectcreatepopup-search-select"); + if(self.options.disable_multiple_selection) { + $sbutton.hide(); + } + $sbutton.click(function() { + self.on_select_elements(self.selected_ids); + self.stop(); + }); + self.view_list = new openerp.web.form.SelectCreateListView(self, + self.element_id + "_view_list", self.dataset, false, + {'deletable': false}); + self.view_list.popup = self; + self.view_list.do_show(); + self.view_list.start().then(function() { + self.searchview.do_search(); + }); + }); + this.searchview.start(); + }, + create_row: function(data) { + var self = this; + var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain); + wdataset.parent_view = this.options.parent_view; + return wdataset.create(data); + }, + on_select_elements: function(element_ids) { + }, + on_click_element: function(ids) { + this.selected_ids = ids || []; + if(this.selected_ids.length > 0) { + this.$element.find(".oe_selectcreatepopup-search-select").removeAttr('disabled'); + } else { + this.$element.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled"); + } + }, + new_object: function() { + var self = this; + if (this.searchview) { + this.searchview.hide(); + } + if (this.view_list) { + this.view_list.$element.hide(); + } + this.dataset.index = null; + this.view_form = new openerp.web.FormView(this, this.element_id + "_view_form", this.dataset, false); + if (this.options.alternative_form_view) { + this.view_form.set_embedded_view(this.options.alternative_form_view); + } + this.view_form.start(); + this.view_form.on_loaded.add_last(function() { + var $buttons = self.view_form.$element.find(".oe_form_buttons"); + $buttons.html(QWeb.render("SelectCreatePopup.form.buttons", {widget:self})); + var $nbutton = $buttons.find(".oe_selectcreatepopup-form-save-new"); + $nbutton.click(function() { + self._created = $.Deferred().then(function() { + self._created = undefined; + self.view_form.on_button_new(); + }); + self.view_form.do_save(); + }); + var $nbutton = $buttons.find(".oe_selectcreatepopup-form-save"); + $nbutton.click(function() { + self._created = $.Deferred().then(function() { + self._created = undefined; + self.check_exit(); + }); + self.view_form.do_save(); + }); + var $cbutton = $buttons.find(".oe_selectcreatepopup-form-close"); + $cbutton.click(function() { + self.check_exit(); + }); + }); + this.dataset.on_create.add(function(data) { + self.options.create_function(data).then(function(r) { + self.created_elements.push(r.result); + if (self._created) { + self._created.resolve(); + } + }); + }); + this.view_form.do_show(); + }, + check_exit: function() { + if (this.created_elements.length > 0) { + this.on_select_elements(this.created_elements); + } + this.stop(); + } +}); + +openerp.web.form.SelectCreateListView = openerp.web.ListView.extend({ + do_add_record: function () { + this.popup.new_object(); + }, + select_record: function(index) { + this.popup.on_select_elements([this.dataset.ids[index]]); + this.popup.stop(); + }, + do_select: function(ids, records) { + this._super(ids, records); + this.popup.on_click_element(ids); + } +}); + +/** + * @class + * @extends openerp.web.OldWidget + */ +openerp.web.form.FormOpenPopup = openerp.web.OldWidget.extend(/** @lends openerp.web.form.FormOpenPopup# */{ + identifier_prefix: "formopenpopup", + template: "FormOpenPopup", + /** + * options: + * - alternative_form_view + * - auto_write (default true) + * - read_function + * - parent_view + */ + show_element: function(model, row_id, context, options) { + this.model = model; + this.row_id = row_id; + this.context = context || {}; + this.options = _.defaults(options || {}, {"auto_write": true}); + jQuery(this.render()).dialog({title: '', + modal: true, + width: 960, + height: 600}); + this.start(); + }, + start: function() { + this._super(); + this.dataset = new openerp.web.form.FormOpenDataset(this, this.model, this.context); + this.dataset.fop = this; + this.dataset.ids = [this.row_id]; + this.dataset.index = 0; + this.dataset.parent_view = this.options.parent_view; + this.setup_form_view(); + }, + on_write: function(id, data) { + this.stop(); + if (!this.options.auto_write) + return; + var self = this; + var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain); + wdataset.parent_view = this.options.parent_view; + wdataset.write(id, data, {}, function(r) { + self.on_write_completed(); + }); + }, + on_write_completed: function() {}, + setup_form_view: function() { + var self = this; + this.view_form = new openerp.web.FormView(this, this.element_id + "_view_form", this.dataset, false); + if (this.options.alternative_form_view) { + this.view_form.set_embedded_view(this.options.alternative_form_view); + } + this.view_form.start(); + this.view_form.on_loaded.add_last(function() { + var $buttons = self.view_form.$element.find(".oe_form_buttons"); + $buttons.html(QWeb.render("FormOpenPopup.form.buttons")); + var $nbutton = $buttons.find(".oe_formopenpopup-form-save"); + $nbutton.click(function() { + self.view_form.do_save(); + }); + var $cbutton = $buttons.find(".oe_formopenpopup-form-close"); + $cbutton.click(function() { + self.stop(); + }); + self.view_form.do_show(); + }); + this.dataset.on_write.add(this.on_write); + } +}); + +openerp.web.form.FormOpenDataset = openerp.web.ReadOnlyDataSetSearch.extend({ + read_ids: function() { + if (this.fop.options.read_function) { + return this.fop.options.read_function.apply(null, arguments); + } else { + return this._super.apply(this, arguments); + } + } +}); + +openerp.web.form.FieldReference = openerp.web.form.Field.extend({ + init: function(view, node) { + this._super(view, node); + this.template = "FieldReference"; + this.fields_view = { + fields: { + selection: { + selection: view.fields_view.fields[this.name].selection + }, + m2o: { + relation: null + } + } + }; + this.get_fields_values = view.get_fields_values; + this.do_onchange = this.on_form_changed = this.on_nop; + this.widgets = {}; + this.fields = {}; + this.selection = new openerp.web.form.FieldSelection(this, { attrs: { + name: 'selection', + widget: 'selection' + }}); + this.selection.on_value_changed.add_last(this.on_selection_changed); + this.m2o = new openerp.web.form.FieldMany2One(this, { attrs: { + name: 'm2o', + widget: 'many2one' + }}); + }, + on_nop: function() { + }, + on_selection_changed: function() { + this.m2o.field.relation = this.selection.get_value(); + this.m2o.set_value(null); + }, + start: function() { + this._super(); + this.selection.start(); + this.m2o.start(); + }, + is_valid: function() { + return this.required === false || typeof(this.get_value()) === 'string'; + }, + is_dirty: function() { + return this.selection.is_dirty() || this.m2o.is_dirty(); + }, + set_value: function(value) { + this._super(value); + if (typeof(value) === 'string') { + var vals = value.split(','); + this.selection.set_value(vals[0]); + this.m2o.set_value(parseInt(vals[1], 10)); + } + }, + get_value: function() { + var model = this.selection.get_value(), + id = this.m2o.get_value(); + if (typeof(model) === 'string' && typeof(id) === 'number') { + return model + ',' + id; + } else { + return false; + } + } +}); + +openerp.web.form.FieldBinary = openerp.web.form.Field.extend({ + init: function(view, node) { + this._super(view, node); + this.iframe = this.element_id + '_iframe'; + this.binary_value = false; + }, + start: function() { + this._super.apply(this, arguments); + this.$element.find('input.oe-binary-file').change(this.on_file_change); + this.$element.find('button.oe-binary-file-save').click(this.on_save_as); + this.$element.find('.oe-binary-file-clear').click(this.on_clear); + }, + update_dom: function() { + this._super.apply(this, arguments); + this.$element.find('.oe-binary').toggle(!this.readonly); + }, + human_filesize : function(size) { + var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + var i = 0; + while (size >= 1024) { + size /= 1024; + ++i; + } + return size.toFixed(2) + ' ' + units[i]; + }, + on_file_change: function(e) { + // TODO: on modern browsers, we could directly read the file locally on client ready to be used on image cropper + // http://www.html5rocks.com/tutorials/file/dndfiles/ + // http://deepliquid.com/projects/Jcrop/demos.php?demo=handler + window[this.iframe] = this.on_file_uploaded; + if ($(e.target).val() != '') { + this.$element.find('form.oe-binary-form input[name=session_id]').val(this.session.session_id); + this.$element.find('form.oe-binary-form').submit(); + this.toggle_progress(); + } + }, + toggle_progress: function() { + this.$element.find('.oe-binary-progress, .oe-binary').toggle(); + }, + on_file_uploaded: function(size, name, content_type, file_base64) { + delete(window[this.iframe]); + if (size === false) { + this.notification.warn("File Upload", "There was a problem while uploading your file"); + // TODO: use openerp web crashmanager + console.warn("Error while uploading file : ", name); + } else { + this.on_file_uploaded_and_valid.apply(this, arguments); + this.on_ui_change(); + } + this.toggle_progress(); + }, + on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { + }, + on_save_as: function() { + if (!this.view.datarecord.id) { + this.notification.warn("Can't save file", "The record has not yet been saved"); + } else { + var url = '/web/binary/saveas?session_id=' + this.session.session_id + '&model=' + + this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name + + '&fieldname=' + (this.node.attrs.filename || '') + '&t=' + (new Date().getTime()); + window.open(url); + } + }, + on_clear: function() { + if (this.value !== false) { + this.value = false; + this.binary_value = false; + this.on_ui_change(); + } + return false; + } +}); + +openerp.web.form.FieldBinaryFile = openerp.web.form.FieldBinary.extend({ + init: function(view, node) { + this._super(view, node); + this.template = "FieldBinaryFile"; + }, + set_value: function(value) { + this._super.apply(this, arguments); + var show_value = (value != null && value !== false) ? value : ''; + this.$element.find('input').eq(0).val(show_value); + }, + on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { + this.value = file_base64; + this.binary_value = true; + var show_value = this.human_filesize(size); + this.$element.find('input').eq(0).val(show_value); + this.set_filename(name); + }, + set_filename: function(value) { + var filename = this.node.attrs.filename; + if (this.view.fields[filename]) { + this.view.fields[filename].set_value(value); + this.view.fields[filename].on_ui_change(); + } + }, + on_clear: function() { + this._super.apply(this, arguments); + this.$element.find('input').eq(0).val(''); + this.set_filename(''); + } +}); + +openerp.web.form.FieldBinaryImage = openerp.web.form.FieldBinary.extend({ + init: function(view, node) { + this._super(view, node); + this.template = "FieldBinaryImage"; + }, + start: function() { + this._super.apply(this, arguments); + this.$image = this.$element.find('img.oe-binary-image'); + }, + set_value: function(value) { + this._super.apply(this, arguments); + this.set_image_maxwidth(); + var url = '/web/binary/image?session_id=' + this.session.session_id + '&model=' + + this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name + '&t=' + (new Date().getTime()); + this.$image.attr('src', url); + }, + set_image_maxwidth: function() { + this.$image.css('max-width', this.$element.width()); + }, + on_file_change: function() { + this.set_image_maxwidth(); + this._super.apply(this, arguments); + }, + on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { + this.value = file_base64; + this.binary_value = true; + this.$image.attr('src', 'data:' + (content_type || 'image/png') + ';base64,' + file_base64); + }, + on_clear: function() { + this._super.apply(this, arguments); + this.$image.attr('src', '/web/static/src/img/placeholder.png'); + } +}); + +openerp.web.form.FieldStatus = openerp.web.form.Field.extend({ + template: "FieldStatus", + start: function() { + this._super(); + this.selected_value = null; + + this.render_list(); + }, + set_value: function(value) { + this._super(value); + this.selected_value = value; + + this.render_list(); + }, + render_list: function() { + var self = this; + var shown = _.map(((this.node.attrs || {}).statusbar_visible || "").split(","), + function(x) { return x.trim(); }); + shown = _.select(shown, function(x) { return x.length > 0; }); + + if (shown.length == 0) { + this.to_show = this.field.selection; + } else { + this.to_show = _.select(this.field.selection, function(x) { + return _.indexOf(shown, x[0]) !== -1 || x[0] === self.selected_value; + }); + } + + var content = openerp.web.qweb.render("FieldStatus.content", {widget: this, _:_}); + this.$element.html(content); + + var colors = JSON.parse((this.node.attrs || {}).statusbar_colors || "{}"); + var color = colors[this.selected_value]; + if (color) { + var elem = this.$element.find("li.oe-arrow-list-selected span"); + elem.css("border-color", color); + elem = this.$element.find("li.oe-arrow-list-selected .oe-arrow-list-before"); + elem.css("border-left-color", "rgba(0,0,0,0)"); + elem = this.$element.find("li.oe-arrow-list-selected .oe-arrow-list-after"); + elem.css("border-color", "rgba(0,0,0,0)"); + elem.css("border-left-color", color); + } + } +}); + +/** + * Registry of form widgets, called by :js:`openerp.web.FormView` + */ +openerp.web.form.widgets = new openerp.web.Registry({ + 'frame' : 'openerp.web.form.WidgetFrame', + 'group' : 'openerp.web.form.WidgetFrame', + 'notebook' : 'openerp.web.form.WidgetNotebook', + 'separator' : 'openerp.web.form.WidgetSeparator', + 'label' : 'openerp.web.form.WidgetLabel', + 'button' : 'openerp.web.form.WidgetButton', + 'char' : 'openerp.web.form.FieldChar', + 'email' : 'openerp.web.form.FieldEmail', + 'url' : 'openerp.web.form.FieldUrl', + 'text' : 'openerp.web.form.FieldText', + 'text_wiki' : 'openerp.web.form.FieldText', + 'date' : 'openerp.web.form.FieldDate', + 'datetime' : 'openerp.web.form.FieldDatetime', + 'selection' : 'openerp.web.form.FieldSelection', + 'many2one' : 'openerp.web.form.FieldMany2One', + 'many2many' : 'openerp.web.form.FieldMany2Many', + 'one2many' : 'openerp.web.form.FieldOne2Many', + 'one2many_list' : 'openerp.web.form.FieldOne2Many', + 'reference' : 'openerp.web.form.FieldReference', + 'boolean' : 'openerp.web.form.FieldBoolean', + 'float' : 'openerp.web.form.FieldFloat', + 'integer': 'openerp.web.form.FieldFloat', + 'float_time': 'openerp.web.form.FieldFloat', + 'progressbar': 'openerp.web.form.FieldProgressBar', + 'image': 'openerp.web.form.FieldBinaryImage', + 'binary': 'openerp.web.form.FieldBinaryFile', + 'statusbar': 'openerp.web.form.FieldStatus' +}); + +}; + +// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/js/formats.js b/addons/web/static/src/js/formats.js new file mode 100644 index 00000000000..b0dc4fcc289 --- /dev/null +++ b/addons/web/static/src/js/formats.js @@ -0,0 +1,197 @@ + +openerp.web.formats = function(openerp) { + +/** + * Formats a single atomic value based on a field descriptor + * + * @param {Object} value read from OpenERP + * @param {Object} descriptor union of orm field and view field + * @param {Object} [descriptor.widget] widget to use to display the value + * @param {Object} descriptor.type fallback if no widget is provided, or if the provided widget is unknown + * @param {Object} [descriptor.digits] used for the formatting of floats + * @param {String} [value_if_empty=''] returned if the ``value`` argument is considered empty + */ +openerp.web.format_value = function (value, descriptor, value_if_empty) { + // If NaN value, display as with a `false` (empty cell) + if (typeof value === 'number' && isNaN(value)) { + value = false; + } + switch (value) { + case false: + case Infinity: + case -Infinity: + return value_if_empty === undefined ? '' : value_if_empty; + } + switch (descriptor.widget || descriptor.type) { + case 'integer': + return _.sprintf('%d', value); + case 'float': + var precision = descriptor.digits ? descriptor.digits[1] : 2; + var int_part = Math.floor(value); + var dec_part = Math.abs(Math.floor((value % 1) * Math.pow(10, precision))); + return _.sprintf('%d%s%d', + int_part, + openerp.web._t.database.parameters.decimal_point, dec_part); + case 'float_time': + return _.sprintf("%02d:%02d", + Math.floor(value), + Math.round((value % 1) * 60)); + case 'progressbar': + return _.sprintf( + '<progress value="%.2f" max="100.0">%.2f%%</progress>', + value, value); + case 'many2one': + // name_get value format + return value[1]; + case 'datetime': + if (typeof(value) == "string") + value = openerp.web.auto_str_to_date(value); + try { + return value.toString(_.sprintf("%s %s", Date.CultureInfo.formatPatterns.shortDate, + Date.CultureInfo.formatPatterns.longTime)); + } catch (e) { + return value.format("%m/%d/%Y %H:%M:%S"); + } + return value; + case 'date': + if (typeof(value) == "string") + value = openerp.web.auto_str_to_date(value); + try { + return value.toString(Date.CultureInfo.formatPatterns.shortDate); + } catch (e) { + return value.format("%m/%d/%Y"); + } + case 'time': + if (typeof(value) == "string") + value = openerp.web.auto_str_to_date(value); + try { + return value.toString(Date.CultureInfo.formatPatterns.longTime); + } catch (e) { + return value.format("%H:%M:%S"); + } + default: + return value; + } +}; + +openerp.web.parse_value = function (value, descriptor, value_if_empty) { + switch (value) { + case false: + case "": + return value_if_empty === undefined ? false : value_if_empty; + } + switch (descriptor.widget || descriptor.type) { + case 'integer': + var tmp = Number(value); + if (isNaN(tmp)) + throw value + " is not a correct integer"; + return tmp; + case 'float': + var tmp = Number(value); + if (!isNaN(tmp)) + return tmp; + tmp = value.replace(openerp.web._t.database.parameters.decimal_point, "."); + var tmp2 = tmp; + do { + tmp = tmp2; + tmp2 = tmp.replace(openerp.web._t.database.parameters.thousands_sep, ""); + } while(tmp !== tmp2); + tmp = Number(tmp); + if (isNaN(tmp)) + throw value + " is not a correct float"; + return tmp; + case 'float_time': + var tmp = value.split(":"); + if (tmp.length != 2) + throw value + " is not a correct float_time"; + var tmp1 = openerp.web.parse_value(tmp[0], {type: "integer"}); + var tmp2 = openerp.web.parse_value(tmp[1], {type: "integer"}); + return tmp1 + (tmp2 / 60); + case 'progressbar': + return openerp.web.parse_value(value, {type: "float"}); + case 'datetime': + var tmp = Date.parseExact(value, _.sprintf("%s %s", Date.CultureInfo.formatPatterns.shortDate, + Date.CultureInfo.formatPatterns.longTime)); + if (tmp !== null) + return tmp; + tmp = Date.parse(value); + if (tmp !== null) + return tmp; + throw value + " is not a valid datetime"; + case 'date': + var tmp = Date.parseExact(value, Date.CultureInfo.formatPatterns.shortDate); + if (tmp !== null) + return tmp; + tmp = Date.parse(value); + if (tmp !== null) + return tmp; + throw value + " is not a valid date"; + case 'time': + var tmp = Date.parseExact(value, Date.CultureInfo.formatPatterns.longTime); + if (tmp !== null) + return tmp; + tmp = Date.parse(value); + if (tmp !== null) + return tmp; + throw value + " is not a valid time"; + } + return value; +}; + +openerp.web.auto_str_to_date = function(value, type) { + try { + return openerp.web.str_to_datetime(value); + } catch(e) {} + try { + return openerp.web.str_to_date(value); + } catch(e) {} + try { + return openerp.web.str_to_time(value); + } catch(e) {} + throw "'" + value + "' is not a valid date, datetime nor time" +}; + +openerp.web.auto_date_to_str = function(value, type) { + switch(type) { + case 'datetime': + return openerp.web.datetime_to_str(value); + case 'date': + return openerp.web.date_to_str(value); + case 'time': + return openerp.web.time_to_str(value); + default: + throw type + " is not convertible to date, datetime nor time" + } +}; + +/** + * Formats a provided cell based on its field type + * + * @param {Object} row_data record whose values should be displayed in the cell + * @param {Object} column column descriptor + * @param {"button"|"field"} column.tag base control type + * @param {String} column.type widget type for a field control + * @param {String} [column.string] button label + * @param {String} [column.icon] button icon + * @param {String} [value_if_empty=''] what to display if the field's value is ``false`` + */ +openerp.web.format_cell = function (row_data, column, value_if_empty) { + var attrs = column.modifiers_for(row_data); + if (attrs.invisible) { return ''; } + if (column.tag === 'button') { + return [ + '<button type="button" title="', column.string || '', '">', + '<img src="/web/static/src/img/icons/', column.icon, '.png"', + ' alt="', column.string || '', '"/>', + '</button>' + ].join('') + } + + if (!row_data[column.id]) { + return value_if_empty === undefined ? '' : value_if_empty; + } + return openerp.web.format_value( + row_data[column.id].value, column, value_if_empty); +} + +}; diff --git a/addons/web/static/src/js/list-editable.js b/addons/web/static/src/js/list-editable.js new file mode 100644 index 00000000000..c38c43a4c9c --- /dev/null +++ b/addons/web/static/src/js/list-editable.js @@ -0,0 +1,336 @@ +/** + * handles editability case for lists, because it depends on form and forms already depends on lists it had to be split out + * @namespace + */ +openerp.web.list_editable = function (openerp) { + var KEY_RETURN = 13, + KEY_ESCAPE = 27; + + // editability status of list rows + openerp.web.ListView.prototype.defaults.editable = null; + + // TODO: not sure second @lends on existing item is correct, to check + openerp.web.ListView.include(/** @lends openerp.web.ListView# */{ + init: function () { + var self = this; + this._super.apply(this, arguments); + $(this.groups).bind({ + 'edit': function (e, id, dataset) { + self.do_edit(dataset.index, id, dataset); + }, + 'saved': function () { + if (self.groups.get_selection().length) { + return; + } + self.compute_aggregates(); + } + }) + }, + /** + * Handles the activation of a record in editable mode (making a record + * editable), called *after* the record has become editable. + * + * The default behavior is to setup the listview's dataset to match + * whatever dataset was provided by the editing List + * + * @param {Number} index index of the record in the dataset + * @param {Object} id identifier of the record being edited + * @param {openerp.web.DataSet} dataset dataset in which the record is available + */ + do_edit: function (index, id, dataset) { + _.extend(this.dataset, dataset); + }, + /** + * Sets editability status for the list, based on defaults, view + * architecture and the provided flag, if any. + * + * @param {Boolean} [force] forces the list to editability. Sets new row edition status to "bottom". + */ + set_editable: function (force) { + // If ``force``, set editability to bottom + // otherwise rely on view default + // view' @editable is handled separately as we have not yet + // fetched and processed the view at this point. + this.options.editable = ( + (force && "bottom") + || this.defaults.editable); + }, + /** + * Replace do_actual_search to handle editability process + */ + do_actual_search: function (results) { + this.set_editable(results.context['set_editable']); + this._super(results); + }, + /** + * Replace do_add_record to handle editability (and adding new record + * as an editable row at the top or bottom of the list) + */ + do_add_record: function () { + if (this.options.editable) { + this.groups.new_record(); + } else { + this._super(); + } + }, + on_loaded: function (data, grouped) { + // tree/@editable takes priority on everything else if present. + this.options.editable = data.arch.attrs.editable || this.options.editable; + return this._super(data, grouped); + } + }); + + openerp.web.ListView.Groups.include(/** @lends openerp.web.ListView.Groups# */{ + passtrough_events: openerp.web.ListView.Groups.prototype.passtrough_events + " edit saved", + new_record: function () { + // TODO: handle multiple children + this.children[null].new_record(); + } + }); + + openerp.web.ListView.List.include(/** @lends openerp.web.ListView.List# */{ + row_clicked: function (event) { + if (!this.options.editable) { + return this._super(event); + } + this.edit_record($(event.currentTarget).data('id')); + }, + /** + * Checks if a record is being edited, and if so cancels it + */ + cancel_pending_edition: function () { + var self = this, cancelled = $.Deferred(); + if (!this.edition) { + cancelled.resolve(); + return cancelled.promise(); + } + + if (this.edition_id != null) { + this.reload_record(self.records.get(this.edition_id)).then(function () { + cancelled.resolve(); + }); + } else { + cancelled.resolve(); + } + cancelled.then(function () { + self.view.unpad_columns(); + self.edition_form.stop(); + self.edition_form.$element.remove(); + delete self.edition_form; + delete self.edition_id; + delete self.edition; + }); + return cancelled.promise(); + }, + /** + * Adapts this list's view description to be suitable to the inner form + * view of a row being edited. + * + * @returns {Object} fields_view_get's view section suitable for putting into form view of editable rows. + */ + get_form_fields_view: function () { + // deep copy of view + var view = $.extend(true, {}, this.group.view.fields_view); + _(view.arch.children).each(function (widget) { + widget.attrs.nolabel = true; + if (widget.tag === 'button') { + delete widget.attrs.string; + } + }); + view.arch.attrs.col = 2 * view.arch.children.length; + return view; + }, + render_row_as_form: function (row) { + var self = this; + this.cancel_pending_edition().then(function () { + var record_id = $(row).data('id'); + var $new_row = $('<tr>', { + id: _.uniqueId('oe-editable-row-'), + 'data-id': record_id, + 'class': $(row).attr('class') + ' oe_forms', + click: function (e) {e.stopPropagation();} + }) + .delegate('button.oe-edit-row-save', 'click', function () { + self.save_row(); + }) + .delegate('button.oe-edit-row-cancel', 'click', function () { + self.cancel_edition(); + }) + .delegate('button', 'keyup', function (e) { + e.stopImmediatePropagation(); + }) + .keyup(function (e) { + switch (e.which) { + case KEY_RETURN: + self.save_row(true); + break; + case KEY_ESCAPE: + self.cancel_edition(); + break; + default: + return; + } + }); + if (row) { + $new_row.replaceAll(row); + } else if (self.options.editable === 'top') { + self.$current.prepend($new_row); + } else if (self.options.editable) { + self.$current.append($new_row); + } + self.edition = true; + self.edition_id = record_id; + self.edition_form = _.extend(new openerp.web.FormView( + self, $new_row.attr('id'), self.dataset, false), { + template: 'ListView.row.form', + registry: openerp.web.list.form.widgets + }); + $.when(self.edition_form.on_loaded(self.get_form_fields_view())).then(function () { + // put in $.when just in case FormView.on_loaded becomes asynchronous + $new_row.find('td') + .addClass('oe-field-cell') + .removeAttr('width') + .end() + .find('td:first').removeClass('oe-field-cell').end() + .find('td:last').removeClass('oe-field-cell').end(); + // pad in case of groupby + _(self.columns).each(function (column) { + if (column.meta) { + $new_row.prepend('<td>'); + } + }); + // Add columns for the cancel and save buttons, if + // there are none in the list + if (!self.options.selectable) { + self.view.pad_columns( + 1, {except: $new_row, position: 'before'}); + } + if (!self.options.deletable) { + self.view.pad_columns( + 1, {except: $new_row}); + } + + self.edition_form.do_show(); + }); + }); + }, + handle_onwrite: function (source_record_id) { + var self = this; + var on_write_callback = self.view.fields_view.arch.attrs.on_write; + if (!on_write_callback) { return; } + this.dataset.call(on_write_callback, [source_record_id], function (ids) { + _(ids).each(function (id) { + var record = self.records.get(id); + if (!record) { + // insert after the source record + var index = self.records.indexOf( + self.records.get(source_record_id)) + 1; + record = new openerp.web.list.Record({id: id}); + self.records.add(record, {at: index}); + self.dataset.ids.splice(index, 0, id); + } + self.reload_record(record); + }); + }); + }, + /** + * Saves the current row, and triggers the edition of its following + * sibling if asked. + * + * @param {Boolean} [edit_next=false] should the next row become editable + */ + save_row: function (edit_next) { + var self = this; + this.edition_form.do_save(function (result) { + if (result.created && !self.edition_id) { + self.records.add({id: result.result}, + {at: self.options.editable === 'top' ? 0 : null}); + self.edition_id = result.result; + } + var edited_record = self.records.get(self.edition_id), + next_record = self.records.at( + self.records.indexOf(edited_record) + 1); + + self.handle_onwrite(self.edition_id); + self.cancel_pending_edition().then(function () { + $(self).trigger('saved', [self.dataset]); + if (!edit_next) { + return; + } + if (result.created) { + self.new_record(); + return; + } + var next_record_id; + if (next_record) { + next_record_id = next_record.get('id'); + self.dataset.index = _(self.dataset.ids) + .indexOf(next_record_id); + } else { + self.dataset.index = 0; + next_record_id = self.records.at(0).get('id'); + } + self.edit_record(next_record_id); + }); + }, this.options.editable === 'top'); + }, + /** + * Cancels the edition of the row for the current dataset index + */ + cancel_edition: function () { + this.cancel_pending_edition(); + }, + /** + * Edits record currently selected via dataset + */ + edit_record: function (record_id) { + this.render_row_as_form( + this.$current.find('[data-id=' + record_id + ']')); + $(this).trigger( + 'edit', + [record_id, this.dataset]); + }, + new_record: function () { + this.dataset.index = null; + this.render_row_as_form(); + } + }); + if (!openerp.web.list) { + openerp.web.list = {}; + } + if (!openerp.web.list.form) { + openerp.web.list.form = {}; + } + openerp.web.list.form.WidgetFrame = openerp.web.form.WidgetFrame.extend({ + template: 'ListView.row.frame' + }); + var form_widgets = openerp.web.form.widgets; + openerp.web.list.form.widgets = form_widgets.clone({ + 'frame': 'openerp.web.list.form.WidgetFrame' + }); + // All form widgets inherit a problematic behavior from + // openerp.web.form.WidgetFrame: the cell itself is removed when invisible + // whether it's @invisible or @attrs[invisible]. In list view, only the + // former should completely remove the cell. We need to override update_dom + // on all widgets since we can't just hit on widget itself (I think) + var list_form_widgets = openerp.web.list.form.widgets; + _(list_form_widgets.map).each(function (widget_path, key) { + if (key === 'frame') { return; } + var new_path = 'openerp.web.list.form.' + key; + + openerp.web.list.form[key] = (form_widgets.get_object(key)).extend({ + update_dom: function () { + this.$element.children().css('visibility', ''); + if (this.modifiers.tree_invisible) { + var old_invisible = this.invisible; + this.invisible = !!this.modifiers.tree_invisible; + this._super(); + this.invisible = old_invisible; + } else if (this.invisible) { + this.$element.children().css('visibility', 'hidden'); + } + } + }); + list_form_widgets.add(key, new_path); + }); +}; diff --git a/addons/web/static/src/js/list.js b/addons/web/static/src/js/list.js new file mode 100644 index 00000000000..95a8207144b --- /dev/null +++ b/addons/web/static/src/js/list.js @@ -0,0 +1,1523 @@ +openerp.web.list = function (openerp) { +var QWeb = openerp.web.qweb; +openerp.web.views.add('list', 'openerp.web.ListView'); +openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# */ { + defaults: { + // records can be selected one by one + 'selectable': true, + // list rows can be deleted + 'deletable': true, + // whether the column headers should be displayed + 'header': true, + // display addition button, with that label + 'addable': "New", + // whether the list view can be sorted, note that once a view has been + // sorted it can not be reordered anymore + 'sortable': true, + // whether the view rows can be reordered (via vertical drag & drop) + 'reorderable': true + }, + /** + * Core class for list-type displays. + * + * As a view, needs a number of view-related parameters to be correctly + * instantiated, provides options and overridable methods for behavioral + * customization. + * + * See constructor parameters and method documentations for information on + * the default behaviors and possible options for the list view. + * + * @constructs openerp.web.ListView + * @extends openerp.web.View + * + * @param parent parent object + * @param element_id the id of the DOM elements this view should link itself to + * @param {openerp.web.DataSet} dataset the dataset the view should work with + * @param {String} view_id the listview's identifier, if any + * @param {Object} options A set of options used to configure the view + * @param {Boolean} [options.selectable=true] determines whether view rows are selectable (e.g. via a checkbox) + * @param {Boolean} [options.header=true] should the list's header be displayed + * @param {Boolean} [options.deletable=true] are the list rows deletable + * @param {void|String} [options.addable="New"] should the new-record button be displayed, and what should its label be. Use ``null`` to hide the button. + * @param {Boolean} [options.sortable=true] is it possible to sort the table by clicking on column headers + * @param {Boolean} [options.reorderable=true] is it possible to reorder list rows + * + * @borrows openerp.web.ActionExecutor#execute_action as #execute_action + */ + init: function(parent, element_id, dataset, view_id, options) { + var self = this; + this._super(parent, element_id); + this.set_default_options(_.extend({}, this.defaults, options || {})); + this.dataset = dataset; + this.model = dataset.model; + this.view_id = view_id; + this.previous_colspan = null; + + this.columns = []; + + this.records = new Collection(); + + this.set_groups(new openerp.web.ListView.Groups(this)); + + if (this.dataset instanceof openerp.web.DataSetStatic) { + this.groups.datagroup = new openerp.web.StaticDataGroup(this.dataset); + } else { + this.groups.datagroup = new openerp.web.DataGroup( + this, this.model, + dataset.get_domain(), + dataset.get_context(), + {}); + this.groups.datagroup.sort = this.dataset._sort; + } + + this.page = 0; + this.records.bind('change', function (event, record, key) { + if (!_(self.aggregate_columns).chain() + .pluck('name').contains(key).value()) { + return; + } + self.compute_aggregates(); + }); + }, + /** + * Retrieves the view's number of records per page (|| section) + * + * options > defaults > parent.action.limit > indefinite + * + * @returns {Number|null} + */ + limit: function () { + if (this._limit === undefined) { + this._limit = (this.options.limit + || this.defaults.limit + || (this.widget_parent.action || {}).limit + || null); + } + return this._limit; + }, + /** + * Set a custom Group construct as the root of the List View. + * + * @param {openerp.web.ListView.Groups} groups + */ + set_groups: function (groups) { + var self = this; + if (this.groups) { + $(this.groups).unbind("selected deleted action row_link"); + delete this.groups; + } + + this.groups = groups; + $(this.groups).bind({ + 'selected': function (e, ids, records) { + self.do_select(ids, records); + }, + 'deleted': function (e, ids) { + self.do_delete(ids); + }, + 'action': function (e, action_name, id, callback) { + self.do_button_action(action_name, id, callback); + }, + 'row_link': function (e, id, dataset) { + self.do_activate_record(dataset.index, id, dataset); + } + }); + }, + /** + * View startup method, the default behavior is to set the ``oe-listview`` + * class on its root element and to perform an RPC load call. + * + * @returns {$.Deferred} loading promise + */ + start: function() { + this.$element.addClass('oe-listview'); + return this.reload_view(null, null, true); + }, + /** + * Called after loading the list view's description, sets up such things + * as the view table's columns, renders the table itself and hooks up the + * various table-level and row-level DOM events (action buttons, deletion + * buttons, selection of records, [New] button, selection of a given + * record, ...) + * + * Sets up the following: + * + * * Processes arch and fields to generate a complete field descriptor for each field + * * Create the table itself and allocate visible columns + * * Hook in the top-level (header) [New|Add] and [Delete] button + * * Sets up showing/hiding the top-level [Delete] button based on records being selected or not + * * Sets up event handlers for action buttons and per-row deletion button + * * Hooks global callback for clicking on a row + * * Sets up its sidebar, if any + * + * @param {Object} data wrapped fields_view_get result + * @param {Object} data.fields_view fields_view_get result (processed) + * @param {Object} data.fields_view.fields mapping of fields for the current model + * @param {Object} data.fields_view.arch current list view descriptor + * @param {Boolean} grouped Is the list view grouped + */ + on_loaded: function(data, grouped) { + var self = this; + this.fields_view = data; + this.name = "" + this.fields_view.arch.attrs.string; + + this.setup_columns(this.fields_view.fields, grouped); + + this.$element.html(QWeb.render("ListView", this)); + + // Head hook + this.$element.find('.oe-list-add') + .click(this.do_add_record) + .attr('disabled', grouped && this.options.editable); + this.$element.find('.oe-list-delete') + .attr('disabled', true) + .click(this.do_delete_selected); + this.$element.find('thead').delegate('th.oe-sortable[data-id]', 'click', function (e) { + e.stopPropagation(); + + var $this = $(this); + self.dataset.sort($this.data('id')); + if ($this.find('span').length) { + $this.find('span').toggleClass( + 'ui-icon-triangle-1-s ui-icon-triangle-1-n'); + } else { + $this.append('<span class="ui-icon ui-icon-triangle-1-s">') + .siblings('.oe-sortable').find('span').remove(); + } + + self.reload_content(); + }); + + this.$element.find('.oe-list-pager') + .delegate('button', 'click', function () { + var $this = $(this); + switch ($this.data('pager-action')) { + case 'first': + self.page = 0; break; + case 'last': + self.page = Math.floor( + self.dataset.ids.length / self.limit()); + break; + case 'next': + self.page += 1; break; + case 'previous': + self.page -= 1; break; + } + self.reload_content(); + }).find('.oe-pager-state') + .click(function (e) { + e.stopPropagation(); + var $this = $(this); + + var $select = $('<select>') + .appendTo($this.empty()) + .click(function (e) {e.stopPropagation();}) + .append('<option value="80">80</option>' + + '<option value="100">100</option>' + + '<option value="200">200</option>' + + '<option value="500">500</option>' + + '<option value="NaN">Unlimited</option>') + .change(function () { + var val = parseInt($select.val(), 10); + self._limit = (isNaN(val) ? null : val); + self.page = 0; + self.reload_content(); + }) + .val(self._limit || 'NaN'); + }); + if (!this.sidebar && this.options.sidebar && this.options.sidebar_id) { + this.sidebar = new openerp.web.Sidebar(this, this.options.sidebar_id); + this.sidebar.start(); + this.sidebar.add_toolbar(this.fields_view.toolbar); + this.set_common_sidebar_sections(this.sidebar); + } + }, + /** + * Configures the ListView pager based on the provided dataset's information + * + * Horrifying side-effect: sets the dataset's data on this.dataset? + * + * @param {openerp.web.DataSet} dataset + */ + configure_pager: function (dataset) { + this.dataset.ids = dataset.ids; + + var limit = this.limit(), + total = dataset.ids.length, + first = (this.page * limit), + last; + if (!limit || (total - first) < limit) { + last = total; + } else { + last = first + limit; + } + this.$element.find('span.oe-pager-state').empty().text(_.sprintf( + "[%d to %d] of %d", first + 1, last, total)); + + this.$element + .find('button[data-pager-action=first], button[data-pager-action=previous]') + .attr('disabled', this.page === 0) + .end() + .find('button[data-pager-action=last], button[data-pager-action=next]') + .attr('disabled', last === total); + }, + /** + * Sets up the listview's columns: merges view and fields data, move + * grouped-by columns to the front of the columns list and make them all + * visible. + * + * @param {Object} fields fields_view_get's fields section + * @param {Boolean} [grouped] Should the grouping columns (group and count) be displayed + */ + setup_columns: function (fields, grouped) { + var domain_computer = openerp.web.form.compute_domain; + + var noop = function () { return {}; }; + var field_to_column = function (field) { + var name = field.attrs.name; + var column = _.extend({id: name, tag: field.tag}, + field.attrs, fields[name]); + // modifiers computer + if (column.modifiers) { + var modifiers = JSON.parse(column.modifiers); + column.modifiers_for = function (fields) { + if (!modifiers.invisible) { + return {}; + } + return { + 'invisible': domain_computer(modifiers.invisible, fields) + }; + }; + if (modifiers['tree_invisible']) { + column.invisible = '1'; + } + } else { + column.modifiers_for = noop; + } + return column; + }; + + this.columns.splice(0, this.columns.length); + this.columns.push.apply( + this.columns, + _(this.fields_view.arch.children).map(field_to_column)); + if (grouped) { + this.columns.unshift({ + id: '_group', tag: '', string: "Group", meta: true, + modifiers_for: function () { return {}; } + }, { + id: '_count', tag: '', string: '#', meta: true, + modifiers_for: function () { return {}; } + }); + } + + this.visible_columns = _.filter(this.columns, function (column) { + return column.invisible !== '1'; + }); + + this.aggregate_columns = _(this.visible_columns) + .map(function (column) { + if (column.type !== 'integer' && column.type !== 'float') { + return {}; + } + var aggregation_func = column['group_operator'] || 'sum'; + + return _.extend({}, column, { + 'function': aggregation_func, + label: column[aggregation_func] + }); + }); + }, + /** + * Used to handle a click on a table row, if no other handler caught the + * event. + * + * The default implementation asks the list view's view manager to switch + * to a different view (by calling + * :js:func:`~openerp.web.ViewManager.on_mode_switch`), using the + * provided record index (within the current list view's dataset). + * + * If the index is null, ``switch_to_record`` asks for the creation of a + * new record. + * + * @param {Number|void} index the record index (in the current dataset) to switch to + * @param {String} [view="form"] the view type to switch to + */ + select_record:function (index, view) { + view = view || 'form'; + this.dataset.index = index; + _.delay(_.bind(function () { + this.do_switch_view(view); + }, this)); + }, + do_show: function () { + this.$element.show(); + if (this.sidebar) { + this.sidebar.$element.show(); + } + if (!_(this.dataset.ids).isEmpty()) { + this.reload_content(); + } + }, + do_hide: function () { + this.$element.hide(); + if (this.sidebar) { + this.sidebar.$element.hide(); + } + }, + /** + * Reloads the list view based on the current settings (dataset & al) + * + * @param {Boolean} [grouped] Should the list be displayed grouped + * @param {Object} [context] context to send the server while loading the view + */ + reload_view: function (grouped, context, initial) { + var self = this; + var callback = function (field_view_get) { + self.on_loaded(field_view_get, grouped); + }; + if (this.embedded_view) { + return $.Deferred().then(callback).resolve(this.embedded_view); + } else { + return this.rpc('/web/listview/load', { + model: this.model, + view_id: this.view_id, + view_type: "tree", + context: this.dataset.get_context(context), + toolbar: this.options.sidebar + }, callback); + } + }, + /** + * re-renders the content of the list view + */ + reload_content: function () { + var self = this; + this.records.reset(); + this.$element.find('.oe-listview-content').append( + this.groups.render(function () { + if (self.dataset.index == null) { + var has_one = false; + self.records.each(function () { has_one = true; }); + if (has_one) { + self.dataset.index = 0; + } + } + self.compute_aggregates(); + })); + }, + /** + * Event handler for a search, asks for the computation/folding of domains + * and contexts (and group-by), then reloads the view's content. + * + * @param {Array} domains a sequence of literal and non-literal domains + * @param {Array} contexts a sequence of literal and non-literal contexts + * @param {Array} groupbys a sequence of literal and non-literal group-by contexts + * @returns {$.Deferred} fold request evaluation promise + */ + do_search: function (domains, contexts, groupbys) { + return this.rpc('/web/session/eval_domain_and_context', { + domains: [this.dataset.get_domain()].concat(domains), + contexts: [this.dataset.get_context()].concat(contexts), + group_by_seq: groupbys + }, $.proxy(this, 'do_actual_search')); + }, + /** + * Handler for the result of eval_domain_and_context, actually perform the + * searching + * + * @param {Object} results results of evaluating domain and process for a search + */ + do_actual_search: function (results) { + this.groups.datagroup = new openerp.web.DataGroup( + this, this.model, + results.domain, + results.context, + results.group_by); + this.groups.datagroup.sort = this.dataset._sort; + + if (_.isEmpty(results.group_by) && !results.context['group_by_no_leaf']) { + results.group_by = null; + } + + this.reload_view(!!results.group_by, results.context).then( + $.proxy(this, 'reload_content')); + }, + /** + * Handles the signal to delete lines from the records list + * + * @param {Array} ids the ids of the records to delete + */ + do_delete: function (ids) { + if (!ids.length) { + return; + } + var self = this; + return $.when(this.dataset.unlink(ids)).then(function () { + _(ids).each(function (id) { + self.records.remove(self.records.get(id)); + }); + self.compute_aggregates(); + }); + }, + /** + * Handles the signal indicating that a new record has been selected + * + * @param {Array} ids selected record ids + * @param {Array} records selected record values + */ + do_select: function (ids, records) { + this.$element.find('.oe-list-delete') + .attr('disabled', !ids.length); + + if (!records.length) { + this.compute_aggregates(); + return; + } + this.compute_aggregates(_(records).map(function (record) { + return {count: 1, values: record}; + })); + }, + /** + * Handles action button signals on a record + * + * @param {String} name action name + * @param {Object} id id of the record the action should be called on + * @param {Function} callback should be called after the action is executed, if non-null + */ + do_button_action: function (name, id, callback) { + var action = _.detect(this.columns, function (field) { + return field.name === name; + }); + if (!action) { return; } + this.execute_action(action, this.dataset, id, callback); + }, + /** + * Handles the activation of a record (clicking on it) + * + * @param {Number} index index of the record in the dataset + * @param {Object} id identifier of the activated record + * @param {openerp.web.DataSet} dataset dataset in which the record is available (may not be the listview's dataset in case of nested groups) + */ + do_activate_record: function (index, id, dataset) { + var self = this; + // TODO is it needed ? + this.dataset.read_slice([],{ + context: dataset.get_context(), + domain: dataset.get_domain() + }, function () { + self.select_record(index); + }); + }, + /** + * Handles signal for the addition of a new record (can be a creation, + * can be the addition from a remote source, ...) + * + * The default implementation is to switch to a new record on the form view + */ + do_add_record: function () { + this.select_record(null); + }, + /** + * Handles deletion of all selected lines + */ + do_delete_selected: function () { + this.do_delete(this.groups.get_selection().ids); + }, + /** + * Computes the aggregates for the current list view, either on the + * records provided or on the records of the internal + * :js:class:`~openerp.web.ListView.Group`, by calling + * :js:func:`~openerp.web.ListView.group.get_records`. + * + * Then displays the aggregates in the table through + * :js:method:`~openerp.web.ListView.display_aggregates`. + * + * @param {Array} [records] + */ + compute_aggregates: function (records) { + var columns = _(this.aggregate_columns).filter(function (column) { + return column['function']; }); + if (_.isEmpty(columns)) { return; } + + if (_.isEmpty(records)) { + records = this.groups.get_records(); + } + + var count = 0, sums = {}; + _(columns).each(function (column) { + switch (column['function']) { + case 'max': + sums[column.id] = -Infinity; + break; + case 'min': + sums[column.id] = Infinity; + break; + default: + sums[column.id] = 0; + } + }); + _(records).each(function (record) { + count += record.count || 1; + _(columns).each(function (column) { + var field = column.id, + value = record.values[field]; + switch (column['function']) { + case 'sum': + sums[field] += value; + break; + case 'avg': + sums[field] += record.count * value; + break; + case 'min': + if (sums[field] > value) { + sums[field] = value; + } + break; + case 'max': + if (sums[field] < value) { + sums[field] = value; + } + break; + } + }); + }); + + var aggregates = {}; + _(columns).each(function (column) { + var field = column.id; + switch (column['function']) { + case 'avg': + aggregates[field] = {value: sums[field] / count}; + break; + default: + aggregates[field] = {value: sums[field]}; + } + }); + + this.display_aggregates(aggregates); + }, + display_aggregates: function (aggregation) { + var $footer_cells = this.$element.find('.oe-list-footer'); + _(this.aggregate_columns).each(function (column) { + if (!column['function']) { + return; + } + + $footer_cells.filter(_.sprintf('[data-field=%s]', column.id)) + .html(openerp.web.format_cell(aggregation, column)); + }); + }, + get_selected_ids: function() { + var ids = this.groups.get_selection().ids; + return ids; + }, + /** + * Adds padding columns at the start or end of all table rows (including + * field names row) + * + * @param {Number} count number of columns to add + * @param {Object} options + * @param {"before"|"after"} [position="after"] insertion position for the new columns + * @param {Object} [except] content row to not pad + */ + pad_columns: function (count, options) { + options = options || {}; + // padding for action/pager header + var $first_header = this.$element.find('thead tr:first th'); + var colspan = $first_header.attr('colspan'); + if (colspan) { + if (!this.previous_colspan) { + this.previous_colspan = colspan; + } + $first_header.attr('colspan', parseInt(colspan, 10) + count); + } + // Padding for column titles, footer and data rows + var $rows = this.$element + .find('.oe-listview-header-columns, tr:not(thead tr)') + .not(options['except']); + var newcols = new Array(count+1).join('<td class="oe-listview-padding"></td>'); + if (options.position === 'before') { + $rows.prepend(newcols); + } else { + $rows.append(newcols); + } + }, + /** + * Removes all padding columns of the table + */ + unpad_columns: function () { + this.$element.find('.oe-listview-padding').remove(); + if (this.previous_colspan) { + this.$element + .find('thead tr:first th') + .attr('colspan', this.previous_colspan); + this.previous_colspan = null; + } + } +}); +openerp.web.ListView.List = openerp.web.Class.extend( /** @lends openerp.web.ListView.List# */{ + /** + * List display for the ListView, handles basic DOM events and transforms + * them in the relevant higher-level events, to which the list view (or + * other consumers) can subscribe. + * + * Events on this object are registered via jQuery. + * + * Available events: + * + * `selected` + * Triggered when a row is selected (using check boxes), provides an + * array of ids of all the selected records. + * `deleted` + * Triggered when deletion buttons are hit, provide an array of ids of + * all the records being marked for suppression. + * `action` + * Triggered when an action button is clicked, provides two parameters: + * + * * The name of the action to execute (as a string) + * * The id of the record to execute the action on + * `row_link` + * Triggered when a row of the table is clicked, provides the index (in + * the rows array) and id of the selected record to the handle function. + * + * @constructs openerp.web.ListView.List + * @extends openerp.web.Class + * + * @param {Object} opts display options, identical to those of :js:class:`openerp.web.ListView` + */ + init: function (group, opts) { + var self = this; + this.group = group; + this.view = group.view; + this.session = this.view.session; + + this.options = opts.options; + this.columns = opts.columns; + this.dataset = opts.dataset; + this.records = opts.records; + + this.record_callbacks = { + 'remove': function (event, record) { + var $row = self.$current.find( + '[data-id=' + record.get('id') + ']'); + var index = $row.data('index'); + $row.remove(); + self.refresh_zebra(index); + }, + 'reset': $.proxy(this, 'on_records_reset'), + 'change': function (event, record) { + var $row = self.$current.find('[data-id=' + record.get('id') + ']'); + $row.replaceWith(self.render_record(record)); + }, + 'add': function (ev, records, record, index) { + var $new_row = $('<tr>').attr({ + 'data-id': record.get('id') + }); + + if (index === 0) { + $new_row.prependTo(self.$current); + } else { + var previous_record = records.at(index-1), + $previous_sibling = self.$current.find( + '[data-id=' + previous_record.get('id') + ']'); + $new_row.insertAfter($previous_sibling); + } + + self.refresh_zebra(index, 1); + } + }; + _(this.record_callbacks).each(function (callback, event) { + this.records.bind(event, callback); + }, this); + + this.$_element = $('<tbody class="ui-widget-content">') + .appendTo(document.body) + .delegate('th.oe-record-selector', 'click', function (e) { + e.stopPropagation(); + var selection = self.get_selection(); + $(self).trigger( + 'selected', [selection.ids, selection.records]); + }) + .delegate('td.oe-record-delete button', 'click', function (e) { + e.stopPropagation(); + var $row = $(e.target).closest('tr'); + $(self).trigger('deleted', [[self.row_id($row)]]); + }) + .delegate('td.oe-field-cell button', 'click', function (e) { + e.stopPropagation(); + var $target = $(e.currentTarget), + field = $target.closest('td').data('field'), + $row = $target.closest('tr'), + record_id = self.row_id($row); + + $(self).trigger('action', [field, record_id, function () { + return self.reload_record(self.records.get(record_id)); + }]); + }) + .delegate('tr', 'click', function (e) { + e.stopPropagation(); + self.dataset.index = self.records.indexOf( + self.records.get( + self.row_id(e.currentTarget))); + self.row_clicked(e); + }); + }, + row_clicked: function () { + $(this).trigger( + 'row_link', + [this.records.at(this.dataset.index).get('id'), + this.dataset]); + }, + render: function () { + if (this.$current) { + this.$current.remove(); + } + this.$current = this.$_element.clone(true); + this.$current.empty().append( + QWeb.render('ListView.rows', _.extend({ + render_cell: openerp.web.format_cell}, this))); + }, + /** + * Gets the ids of all currently selected records, if any + * @returns {Object} object with the keys ``ids`` and ``records``, holding respectively the ids of all selected records and the records themselves. + */ + get_selection: function () { + if (!this.options.selectable) { + return []; + } + var records = this.records; + var result = {ids: [], records: []}; + this.$current.find('th.oe-record-selector input:checked') + .closest('tr').each(function () { + var record = records.get($(this).data('id')); + result.ids.push(record.get('id')); + result.records.push(record.attributes); + }); + return result; + }, + /** + * Returns the identifier of the object displayed in the provided table + * row + * + * @param {Object} row the selected table row + * @returns {Number|String} the identifier of the row's object + */ + row_id: function (row) { + return $(row).data('id'); + }, + /** + * Death signal, cleans up list display + */ + on_records_reset: function () { + _(this.record_callbacks).each(function (callback, event) { + this.records.unbind(event, callback); + }, this); + if (!this.$current) { return; } + this.$current.remove(); + this.$current = null; + this.$_element.remove(); + }, + get_records: function () { + return this.records.map(function (record) { + return {count: 1, values: record.attributes}; + }); + }, + /** + * Reloads the provided record by re-reading its content from the server. + * + * @param {Record} record + * @returns {$.Deferred} promise to the finalization of the reloading + */ + reload_record: function (record) { + return this.dataset.read_ids( + [record.get('id')], + _.pluck(_(this.columns).filter(function (r) { + return r.tag === 'field'; + }), 'name'), + function (records) { + _(records[0]).each(function (value, key) { + record.set(key, value, {silent: true}); + }); + record.trigger('change', record); + } + ); + }, + /** + * Renders a list record to HTML + * + * @param {Record} record index of the record to render in ``this.rows`` + * @returns {String} QWeb rendering of the selected record + */ + render_record: function (record) { + var index = this.records.indexOf(record); + return QWeb.render('ListView.row', { + columns: this.columns, + options: this.options, + record: record, + row_parity: (index % 2 === 0) ? 'even' : 'odd', + render_cell: openerp.web.format_cell + }); + }, + /** + * Fixes fixes the even/odd classes + * + * @param {Number} [from_index] index from which to resequence + * @param {Number} [offset = 0] selection offset for DOM, in case there are rows to ignore in the table + */ + refresh_zebra: function (from_index, offset) { + offset = offset || 0; + from_index = from_index || 0; + var dom_offset = offset + from_index; + var sel = dom_offset ? ':gt(' + (dom_offset - 1) + ')' : null; + this.$current.children(sel).each(function (i, e) { + var index = from_index + i; + // reset record-index accelerators on rows and even/odd + var even = index%2 === 0; + $(e).toggleClass('even', even) + .toggleClass('odd', !even); + }); + } +}); +openerp.web.ListView.Groups = openerp.web.Class.extend( /** @lends openerp.web.ListView.Groups# */{ + passtrough_events: 'action deleted row_link', + /** + * Grouped display for the ListView. Handles basic DOM events and interacts + * with the :js:class:`~openerp.web.DataGroup` bound to it. + * + * Provides events similar to those of + * :js:class:`~openerp.web.ListView.List` + * + * @constructs openerp.web.ListView.Groups + * @extends openerp.web.Class + * + * @param {openerp.web.ListView} view + * @param {Object} [options] + * @param {Collection} [options.records] + * @param {Object} [options.options] + * @param {Array} [options.columns] + */ + init: function (view, options) { + options = options || {}; + this.view = view; + this.records = options.records || view.records; + this.options = options.options || view.options; + this.columns = options.columns || view.columns; + this.datagroup = null; + + this.$row = null; + this.children = {}; + + this.page = 0; + + this.records.bind('reset', $.proxy(this, 'on_records_reset')); + }, + make_fragment: function () { + return document.createDocumentFragment(); + }, + /** + * Returns a DOM node after which a new tbody can be inserted, so that it + * follows the provided row. + * + * Necessary to insert the result of a new group or list view within an + * existing groups render, without losing track of the groups's own + * elements + * + * @param {HTMLTableRowElement} row the row after which the caller wants to insert a body + * @returns {HTMLTableSectionElement} element after which a tbody can be inserted + */ + point_insertion: function (row) { + var $row = $(row); + var red_letter_tboday = $row.closest('tbody')[0]; + + var $next_siblings = $row.nextAll(); + if ($next_siblings.length) { + var $root_kanal = $('<tbody>').insertAfter(red_letter_tboday); + + $root_kanal.append($next_siblings); + this.elements.splice( + _.indexOf(this.elements, red_letter_tboday), + 0, + $root_kanal[0]); + } + return red_letter_tboday; + }, + make_paginator: function () { + var self = this; + var $prev = $('<button type="button" data-pager-action="previous"><</button>') + .click(function (e) { + e.stopPropagation(); + self.page -= 1; + + self.$row.closest('tbody').next() + .replaceWith(self.render()); + }); + var $next = $('<button type="button" data-pager-action="next">></button>') + .click(function (e) { + e.stopPropagation(); + self.page += 1; + + self.$row.closest('tbody').next() + .replaceWith(self.render()); + }); + this.$row.children().last() + .append($prev) + .append('<span class="oe-pager-state"></span>') + .append($next); + }, + open: function (point_insertion) { + this.render().insertAfter(point_insertion); + this.make_paginator(); + }, + close: function () { + this.$row.children().last().empty(); + this.records.reset(); + }, + /** + * Prefixes ``$node`` with floated spaces in order to indent it relative + * to its own left margin/baseline + * + * @param {jQuery} $node jQuery object to indent + * @param {Number} level current nesting level, >= 1 + * @returns {jQuery} the indentation node created + */ + indent: function ($node, level) { + return $('<span>') + .css({'float': 'left', 'white-space': 'pre'}) + .text(new Array(level).join(' ')) + .prependTo($node); + }, + render_groups: function (datagroups) { + var self = this; + var placeholder = this.make_fragment(); + _(datagroups).each(function (group) { + if (self.children[group.value]) { + self.records.proxy(group.value).reset(); + delete self.children[group.value]; + } + var child = self.children[group.value] = new openerp.web.ListView.Groups(self.view, { + records: self.records.proxy(group.value), + options: self.options, + columns: self.columns + }); + self.bind_child_events(child); + child.datagroup = group; + + var $row = child.$row = $('<tr>'); + if (group.openable) { + $row.click(function (e) { + if (!$row.data('open')) { + $row.data('open', true) + .find('span.ui-icon') + .removeClass('ui-icon-triangle-1-e') + .addClass('ui-icon-triangle-1-s'); + child.open(self.point_insertion(e.currentTarget)); + } else { + $row.removeData('open') + .find('span.ui-icon') + .removeClass('ui-icon-triangle-1-s') + .addClass('ui-icon-triangle-1-e'); + child.close(); + } + }); + } + placeholder.appendChild($row[0]); + + var $group_column = $('<th class="oe-group-name">').appendTo($row); + // Don't fill this if group_by_no_leaf but no group_by + if (group.grouped_on) { + var row_data = {}; + row_data[group.grouped_on] = group; + var group_column = _(self.columns).detect(function (column) { + return column.id === group.grouped_on; }); + $group_column.html(openerp.web.format_cell( + row_data, group_column, "Undefined" + )); + if (group.openable) { + // Make openable if not terminal group & group_by_no_leaf + $group_column + .prepend('<span class="ui-icon ui-icon-triangle-1-e" style="float: left;">'); + } + } + self.indent($group_column, group.level); + // count column + $('<td>').text(group.length).appendTo($row); + + if (self.options.selectable) { + $row.append('<td>'); + } + _(self.columns).chain() + .filter(function (column) {return !column.invisible;}) + .each(function (column) { + if (column.meta) { + // do not do anything + } else if (column.id in group.aggregates) { + var value = group.aggregates[column.id]; + var format; + if (column.type === 'integer') { + format = "%.0f"; + } else if (column.type === 'float') { + format = "%.2f"; + } + $('<td>') + .text(_.sprintf(format, value)) + .appendTo($row); + } else { + $row.append('<td>'); + } + }); + if (self.options.deletable) { + $row.append('<td class="oe-group-pagination">'); + } + }); + return placeholder; + }, + bind_child_events: function (child) { + var $this = $(this), + self = this; + $(child).bind('selected', function (e) { + // can have selections spanning multiple links + var selection = self.get_selection(); + $this.trigger(e, [selection.ids, selection.records]); + }).bind(this.passtrough_events, function (e) { + // additional positional parameters are provided to trigger as an + // Array, following the event type or event object, but are + // provided to the .bind event handler as *args. + // Convert our *args back into an Array in order to trigger them + // on the group itself, so it can ultimately be forwarded wherever + // it's supposed to go. + var args = Array.prototype.slice.call(arguments, 1); + $this.trigger.call($this, e, args); + }); + }, + render_dataset: function (dataset) { + var self = this, + list = new openerp.web.ListView.List(this, { + options: this.options, + columns: this.columns, + dataset: dataset, + records: this.records + }); + this.bind_child_events(list); + + var view = this.view, + limit = view.limit(), + d = new $.Deferred(), + page = this.datagroup.openable ? this.page : view.page; + + var fields = _.pluck(_.select(this.columns, function(x) {return x.tag == "field"}), 'name'); + var options = { offset: page * limit, limit: limit }; + dataset.read_slice(fields, options , function (records) { + if (!self.datagroup.openable) { + view.configure_pager(dataset); + } else { + var pages = Math.ceil(dataset.ids.length / limit); + self.$row + .find('.oe-pager-state') + .text(_.sprintf('%d/%d', page + 1, pages)) + .end() + .find('button[data-pager-action=previous]') + .attr('disabled', page === 0) + .end() + .find('button[data-pager-action=next]') + .attr('disabled', page === pages - 1); + } + + self.records.add(records, {silent: true}); + list.render(); + d.resolve(list); + }); + return d.promise(); + }, + setup_resequence_rows: function (list, dataset) { + // drag and drop enabled if list is not sorted and there is a + // "sequence" column in the view. + if ((dataset.sort && dataset.sort()) + || !_(this.columns).any(function (column) { + return column.name === 'sequence'; })) { + return; + } + // ondrop, move relevant record & fix sequences + list.$current.sortable({ + stop: function (event, ui) { + var to_move = list.records.get(ui.item.data('id')), + target_id = ui.item.prev().data('id'); + + list.records.remove(to_move); + var to = target_id ? list.records.indexOf(list.records.get(target_id)) : 0; + list.records.add(to_move, { at: to }); + + // resequencing time! + var record, index = to, + // if drag to 1st row (to = 0), start sequencing from 0 + // (exclusive lower bound) + seq = to ? list.records.at(to - 1).get('sequence') : 0; + while (++seq, record = list.records.at(index++)) { + // write are independent from one another, so we can just + // launch them all at the same time and we don't really + // give a fig about when they're done + dataset.write(record.get('id'), {sequence: seq}); + record.set('sequence', seq); + } + + list.refresh_zebra(); + } + }); + }, + render: function (post_render) { + var self = this; + var $element = $('<tbody>'); + this.elements = [$element[0]]; + + this.datagroup.list( + _(this.view.visible_columns).chain() + .filter(function (column) { return column.tag === 'field' }) + .pluck('name').value(), + function (groups) { + $element[0].appendChild( + self.render_groups(groups)); + if (post_render) { post_render(); } + }, function (dataset) { + self.render_dataset(dataset).then(function (list) { + self.children[null] = list; + self.elements = + [list.$current.replaceAll($element)[0]]; + self.setup_resequence_rows(list, dataset); + if (post_render) { post_render(); } + }); + }); + return $element; + }, + /** + * Returns the ids of all selected records for this group, and the records + * themselves + */ + get_selection: function () { + var ids = [], records = []; + + _(this.children) + .each(function (child) { + var selection = child.get_selection(); + ids.push.apply(ids, selection.ids); + records.push.apply(records, selection.records); + }); + + return {ids: ids, records: records}; + }, + on_records_reset: function () { + this.children = {}; + $(this.elements).remove(); + }, + get_records: function () { + if (_(this.children).isEmpty()) { + return { + count: this.datagroup.length, + values: this.datagroup.aggregates + } + } + return _(this.children).chain() + .map(function (child) { + return child.get_records(); + }).flatten().value(); + } +}); + +/** + * @mixin Events + */ +var Events = /** @lends Events# */{ + /** + * @param {String} event event to listen to on the current object, null for all events + * @param {Function} handler event handler to bind to the relevant event + * @returns this + */ + bind: function (event, handler) { + var calls = this['_callbacks'] || (this._callbacks = {}); + + if (event in calls) { + calls[event].push(handler); + } else { + calls[event] = [handler]; + } + return this; + }, + /** + * @param {String} event event to unbind on the current object + * @param {function} [handler] specific event handler to remove (otherwise unbind all handlers for the event) + * @returns this + */ + unbind: function (event, handler) { + var calls = this._callbacks || {}; + if (!(event in calls)) { return this; } + if (!handler) { + delete calls[event]; + } else { + var handlers = calls[event]; + handlers.splice( + _(handlers).indexOf(handler), + 1); + } + return this; + }, + /** + * @param {String} event + * @returns this + */ + trigger: function (event) { + var calls; + if (!(calls = this._callbacks)) { return this; } + var callbacks = (calls[event] || []).concat(calls[null] || []); + for(var i=0, length=callbacks.length; i<length; ++i) { + callbacks[i].apply(this, arguments); + } + return this; + } +}; +var Record = openerp.web.Class.extend(/** @lends Record# */{ + /** + * @constructs Record + * @extends openerp.web.Class + * + * @mixes Events + * @param {Object} [data] + */ + init: function (data) { + this.attributes = data || {}; + }, + /** + * @param {String} key + * @returns {Object} + */ + get: function (key) { + return this.attributes[key]; + }, + /** + * @param key + * @param value + * @param {Object} [options] + * @param {Boolean} [options.silent=false] + * @returns {Record} + */ + set: function (key, value, options) { + options = options || {}; + var old_value = this.attributes[key]; + if (old_value === value) { + return this; + } + this.attributes[key] = value; + if (!options.silent) { + this.trigger('change:' + key, this, value, old_value); + this.trigger('change', this, key, value, old_value); + } + return this; + }, + /** + * Converts the current record to the format expected by form views: + * + * .. code-block:: javascript + * + * data: { + * $fieldname: { + * value: $value + * } + * } + * + * + * @returns {Object} record displayable in a form view + */ + toForm: function () { + var form_data = {}; + _(this.attributes).each(function (value, key) { + form_data[key] = {value: value}; + }); + + return {data: form_data}; + } +}); +Record.include(Events); +var Collection = openerp.web.Class.extend(/** @lends Collection# */{ + /** + * Smarter collections, with events, very strongly inspired by Backbone's. + * + * Using a "dumb" array of records makes synchronization between the + * various serious + * + * @constructs Collection + * @extends openerp.web.Class + * + * @mixes Events + * @param {Array} [records] records to initialize the collection with + * @param {Object} [options] + */ + init: function (records, options) { + options = options || {}; + _.bindAll(this, '_onRecordEvent'); + this.length = 0; + this.records = []; + this._byId = {}; + this._proxies = {}; + this._key = options.key; + this._parent = options.parent; + + if (records) { + this.add(records); + } + }, + /** + * @param {Object|Array} record + * @param {Object} [options] + * @param {Number} [options.at] + * @param {Boolean} [options.silent=false] + * @returns this + */ + add: function (record, options) { + options = options || {}; + var records = record instanceof Array ? record : [record]; + + for(var i=0, length=records.length; i<length; ++i) { + var instance = (records[i] instanceof Record) ? records[i] : new Record(records[i]); + instance.bind(null, this._onRecordEvent); + this._byId[instance.get('id')] = instance; + if (options.at == undefined) { + this.records.push(instance); + if (!options.silent) { + this.trigger('add', this, instance, this.records.length-1); + } + } else { + var insertion_index = options.at + i; + this.records.splice(insertion_index, 0, instance); + if (!options.silent) { + this.trigger('add', this, instance, insertion_index); + } + } + this.length++; + } + return this; + }, + + /** + * Get a record by its index in the collection, can also take a group if + * the collection is not degenerate + * + * @param {Number} index + * @param {String} [group] + * @returns {Record|undefined} + */ + at: function (index, group) { + if (group) { + var groups = group.split('.'); + return this._proxies[groups[0]].at(index, groups.join('.')); + } + return this.records[index]; + }, + /** + * Get a record by its database id + * + * @param {Number} id + * @returns {Record|undefined} + */ + get: function (id) { + if (!_(this._proxies).isEmpty()) { + var record = null; + _(this._proxies).detect(function (proxy) { + return record = proxy.get(id); + }); + return record; + } + return this._byId[id]; + }, + /** + * Builds a proxy (insert/retrieve) to a subtree of the collection, by + * the subtree's group + * + * @param {String} section group path section + * @returns {Collection} + */ + proxy: function (section) { + return this._proxies[section] = new Collection(null, { + parent: this, + key: section + }).bind(null, this._onRecordEvent); + }, + /** + * @param {Array} [records] + * @returns this + */ + reset: function (records) { + _(this._proxies).each(function (proxy) { + proxy.reset(); + }); + this._proxies = {}; + this.length = 0; + this.records = []; + this._byId = {}; + if (records) { + this.add(records); + } + this.trigger('reset', this); + return this; + }, + /** + * Removes the provided record from the collection + * + * @param {Record} record + * @returns this + */ + remove: function (record) { + var self = this; + var index = _(this.records).indexOf(record); + if (index === -1) { + _(this._proxies).each(function (proxy) { + proxy.remove(record); + }); + return this; + } + + this.records.splice(index, 1); + delete this._byId[record.get('id')]; + this.length--; + this.trigger('remove', record, this); + return this; + }, + + _onRecordEvent: function (event, record, options) { + // don't propagate reset events + if (event === 'reset') { return; } + this.trigger.apply(this, arguments); + }, + + // underscore-type methods + each: function (callback) { + for(var section in this._proxies) { + if (this._proxies.hasOwnProperty(section)) { + this._proxies[section].each(callback); + } + } + for(var i=0; i<this.length; ++i) { + callback(this.records[i]); + } + }, + map: function (callback) { + var results = []; + this.each(function (record) { + results.push(callback(record)); + }); + return results; + }, + pluck: function (fieldname) { + return this.map(function (record) { + return record.get(fieldname); + }); + }, + indexOf: function (record) { + return _(this.records).indexOf(record); + } +}); +Collection.include(Events); +openerp.web.list = { + Events: Events, + Record: Record, + Collection: Collection +} +}; +// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js new file mode 100644 index 00000000000..f6c53b4ad17 --- /dev/null +++ b/addons/web/static/src/js/search.js @@ -0,0 +1,1220 @@ +openerp.web.search = function(openerp) { +var QWeb = openerp.web.qweb; + +openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.SearchView# */{ + /** + * @constructs openerp.web.SearchView + * @extends openerp.web.Widget + * + * @param parent + * @param element_id + * @param dataset + * @param view_id + * @param defaults + */ + init: function(parent, element_id, dataset, view_id, defaults) { + this._super(parent, element_id); + this.dataset = dataset; + this.model = dataset.model; + this.view_id = view_id; + + this.defaults = defaults || {}; + + this.inputs = []; + this.enabled_filters = []; + + this.has_focus = false; + + this.ready = $.Deferred(); + }, + start: function() { + this.rpc("/web/searchview/load", {"model": this.model, "view_id":this.view_id}, this.on_loaded); + return this.ready.promise(); + }, + show: function () { + this.$element.show(); + }, + hide: function () { + this.$element.hide(); + }, + /** + * Builds a list of widget rows (each row is an array of widgets) + * + * @param {Array} items a list of nodes to convert to widgets + * @param {Object} fields a mapping of field names to (ORM) field attributes + * @returns Array + */ + make_widgets: function (items, fields) { + var rows = [], + row = []; + rows.push(row); + var filters = []; + _.each(items, function (item) { + if (filters.length && item.tag !== 'filter') { + row.push( + new openerp.web.search.FilterGroup( + filters, this)); + filters = []; + } + + if (item.tag === 'newline') { + row = []; + rows.push(row); + } else if (item.tag === 'filter') { + if (!this.has_focus) { + item.attrs.default_focus = '1'; + this.has_focus = true; + } + filters.push( + new openerp.web.search.Filter( + item, this)); + } else if (item.tag === 'separator') { + // a separator is a no-op + } else { + if (item.tag === 'group') { + // TODO: group and field should be fetched from registries, maybe even filters + row.push( + new openerp.web.search.Group( + item, this, fields)); + } else if (item.tag === 'field') { + if (!this.has_focus) { + item.attrs.default_focus = '1'; + this.has_focus = true; + } + row.push( + this.make_field( + item, fields[item['attrs'].name])); + } + } + }, this); + if (filters.length) { + row.push(new openerp.web.search.FilterGroup(filters, this)); + } + + return rows; + }, + /** + * Creates a field for the provided field descriptor item (which comes + * from fields_view_get) + * + * @param {Object} item fields_view_get node for the field + * @param {Object} field fields_get result for the field + * @returns openerp.web.search.Field + */ + make_field: function (item, field) { + try { + return new (openerp.web.search.fields.get_object(field.type)) + (item, field, this); + } catch (e) { + if (! e instanceof openerp.web.KeyNotFound) { + throw e; + } + // KeyNotFound means unknown field type + console.group('Unknown field type ' + field.type); + console.error('View node', item); + console.info('View field', field); + console.info('In view', this); + console.groupEnd(); + return null; + } + }, + on_loaded: function(data) { + var self = this, + lines = this.make_widgets( + data.fields_view['arch'].children, + data.fields_view.fields); + + // for extended search view + var ext = new openerp.web.search.ExtendedSearch(this, this.model); + lines.push([ext]); + this.inputs.push(ext); + + var render = QWeb.render("SearchView", { + 'view': data.fields_view['arch'], + 'lines': lines, + 'defaults': this.defaults + }); + + // We don't understand why the following commented line does not work in Chrome but + // the non-commented line does. As far as we investigated, only God knows. + //this.$element.html(render); + jQuery(render).appendTo(this.$element); + this.$element.find(".oe_search-view-custom-filter-btn").click(ext.on_activate); + + var f = this.$element.find('form'); + this.$element.find('form') + .submit(this.do_search) + .bind('reset', this.do_clear); + // start() all the widgets + var widget_starts = _(lines).chain().flatten().map(function (widget) { + return widget.start(); + }).value(); + + $.when.apply(null, widget_starts).then(function () { + self.ready.resolve(); + }); + + this.reload_managed_filters(); + }, + reload_managed_filters: function() { + var self = this; + return this.rpc('/web/searchview/get_filters', { + model: this.dataset.model + }).then(function(result) { + self.managed_filters = result; + var filters = self.$element.find(".oe_search-view-filters-management"); + filters.html(QWeb.render("SearchView.managed-filters", {filters: result})); + filters.change(self.on_filters_management); + }); + }, + /** + * Handle event when the user make a selection in the filters management select box. + */ + on_filters_management: function(e) { + var self = this; + var select = this.$element.find(".oe_search-view-filters-management"); + var val = select.val(); + + if (val.slice(0,1) == "_") { // useless action + select.val("_filters"); + return; + } + if (val.slice(0, "get:".length) == "get:") { + val = val.slice("get:".length); + val = parseInt(val); + var filter = this.managed_filters[val]; + this.on_search([filter.domain], [filter.context], []); + } else if (val == "save_filter") { + select.val("_filters"); + var data = this.build_search_data(); + var context = new openerp.web.CompoundContext(); + _.each(data.contexts, function(x) { + context.add(x); + }); + var domain = new openerp.web.CompoundDomain(); + _.each(data.domains, function(x) { + domain.add(x); + }); + var dial_html = QWeb.render("SearchView.managed-filters.add"); + var $dial = $(dial_html); + $dial.dialog({ + modal: true, + title: "Filter Entry", + buttons: { + Cancel: function() { + $(this).dialog("close"); + }, + OK: function() { + $(this).dialog("close"); + var name = $(this).find("input").val(); + self.rpc('/web/searchview/save_filter', { + model: self.dataset.model, + context_to_save: context, + domain: domain, + name: name + }).then(function() { + self.reload_managed_filters(); + }); + } + } + }); + } else { // manage_filters + select.val("_filters"); + this.do_action({ + res_model: 'ir.filters', + views: [[false, 'list'], [false, 'form']], + type: 'ir.actions.act_window', + context: {"search_default_user_id": this.session.uid, + "search_default_model_id": this.dataset.model}, + target: "current", + limit : 80, + auto_search : true + }); + } + }, + /** + * Performs the search view collection of widget data. + * + * If the collection went well (all fields are valid), then triggers + * :js:func:`openerp.web.SearchView.on_search`. + * + * If at least one field failed its validation, triggers + * :js:func:`openerp.web.SearchView.on_invalid` instead. + * + * @param e jQuery event object coming from the "Search" button + */ + do_search: function (e) { + // reset filters management + var select = this.$element.find(".oe_search-view-filters-management"); + select.val("_filters"); + + if (e && e.preventDefault) { e.preventDefault(); } + + var data = this.build_search_data(); + + if (data.errors.length) { + this.on_invalid(data.errors); + return; + } + + this.on_search(data.domains, data.contexts, data.groupbys); + }, + build_search_data: function() { + var domains = [], + contexts = [], + errors = []; + + _.each(this.inputs, function (input) { + try { + var domain = input.get_domain(); + if (domain) { + domains.push(domain); + } + + var context = input.get_context(); + if (context) { + contexts.push(context); + } + } catch (e) { + if (e instanceof openerp.web.search.Invalid) { + errors.push(e); + } else { + throw e; + } + } + }); + + // TODO: do we need to handle *fields* with group_by in their context? + var groupbys = _(this.enabled_filters) + .chain() + .map(function (filter) { return filter.get_context();}) + .compact() + .value(); + return {domains: domains, contexts: contexts, errors: errors, groupbys: groupbys}; + }, + /** + * Triggered after the SearchView has collected all relevant domains and + * contexts. + * + * It is provided with an Array of domains and an Array of contexts, which + * may or may not be evaluated (each item can be either a valid domain or + * context, or a string to evaluate in order in the sequence) + * + * It is also passed an array of contexts used for group_by (they are in + * the correct order for group_by evaluation, which contexts may not be) + * + * @event + * @param {Array} domains an array of literal domains or domain references + * @param {Array} contexts an array of literal contexts or context refs + * @param {Array} groupbys ordered contexts which may or may not have group_by keys + */ + on_search: function (domains, contexts, groupbys) { + }, + /** + * Triggered after a validation error in the SearchView fields. + * + * Error objects have three keys: + * * ``field`` is the name of the invalid field + * * ``value`` is the invalid value + * * ``message`` is the (in)validation message provided by the field + * + * @event + * @param {Array} errors a never-empty array of error objects + */ + on_invalid: function (errors) { + this.notification.notify("Invalid Search", "triggered from search view"); + }, + do_clear: function () { + $('.filter_label').removeClass('enabled'); + var string = $('a.searchview_group_string'); + _.each(string, function(str){ + $(str).closest('div.searchview_group').removeClass("expanded").addClass('folded'); + }); + this.$element.find('table:last').hide(); + + $('.searchview_extended_groups_list').empty(); + setTimeout(this.on_clear); + }, + /** + * Triggered when the search view gets cleared + * + * @event + */ + on_clear: function () { + this.do_search(); + }, + /** + * Called by a filter propagating its state changes + * + * @param {openerp.web.search.Filter} filter a filter which got toggled + * @param {Boolean} default_enabled filter got enabled through the default values, at render time. + */ + do_toggle_filter: function (filter, default_enabled) { + if (default_enabled || filter.is_enabled()) { + this.enabled_filters.push(filter); + } else { + this.enabled_filters = _.without( + this.enabled_filters, filter); + } + + if (!default_enabled) { + // selecting a filter after initial loading automatically + // triggers refresh + this.$element.find('form').submit(); + } + } +}); + +/** @namespace */ +openerp.web.search = {}; +/** + * Registry of search fields, called by :js:class:`openerp.web.SearchView` to + * find and instantiate its field widgets. + */ +openerp.web.search.fields = new openerp.web.Registry({ + 'char': 'openerp.web.search.CharField', + 'text': 'openerp.web.search.CharField', + 'boolean': 'openerp.web.search.BooleanField', + 'integer': 'openerp.web.search.IntegerField', + 'float': 'openerp.web.search.FloatField', + 'selection': 'openerp.web.search.SelectionField', + 'datetime': 'openerp.web.search.DateTimeField', + 'date': 'openerp.web.search.DateField', + 'many2one': 'openerp.web.search.ManyToOneField', + 'many2many': 'openerp.web.search.CharField', + 'one2many': 'openerp.web.search.CharField' +}); +openerp.web.search.Invalid = openerp.web.Class.extend( /** @lends openerp.web.search.Invalid# */{ + /** + * Exception thrown by search widgets when they hold invalid values, + * which they can not return when asked. + * + * @constructs openerp.web.search.Invalid + * @extends openerp.web.Class + * + * @param field the name of the field holding an invalid value + * @param value the invalid value + * @param message validation failure message + */ + init: function (field, value, message) { + this.field = field; + this.value = value; + this.message = message; + }, + toString: function () { + return ('Incorrect value for field ' + this.field + + ': [' + this.value + '] is ' + this.message); + } +}); +openerp.web.search.Widget = openerp.web.Widget.extend( /** @lends openerp.web.search.Widget# */{ + template: null, + /** + * Root class of all search widgets + * + * @constructs openerp.web.search.Widget + * @extends openerp.web.Widget + * + * @param view the ancestor view of this widget + */ + init: function (view) { + this.view = view; + }, + /** + * Sets and returns a globally unique identifier for the widget. + * + * If a prefix is specified, the identifier will be appended to it. + * + * @params prefix prefix sections, empty/falsy sections will be removed + */ + make_id: function () { + this.element_id = _.uniqueId( + ['search'].concat( + _.compact(_.toArray(arguments)), + ['']).join('_')); + return this.element_id; + }, + /** + * "Starts" the widgets. Called at the end of the rendering, this allows + * widgets to hook themselves to their view sections. + * + * On widgets, if they kept a reference to a view and have an element_id, + * will fetch and set their root element on $element. + */ + start: function () { + this._super(); + if (this.view && this.element_id) { + // id is unique, and no getElementById on elements + this.$element = $(document.getElementById( + this.element_id)); + } + }, + /** + * "Stops" the widgets. Called when the view destroys itself, this + * lets the widgets clean up after themselves. + */ + stop: function () { + delete this.view; + this._super(); + }, + render: function (defaults) { + return QWeb.render( + this.template, _.extend(this, { + defaults: defaults + })); + } +}); +openerp.web.search.FilterGroup = openerp.web.search.Widget.extend(/** @lends openerp.web.search.FilterGroup# */{ + template: 'SearchView.filters', + /** + * Inclusive group of filters, creates a continuous "button" with clickable + * sections (the normal display for filters is to be a self-contained button) + * + * @constructs openerp.web.search.FilterGroup + * @extends openerp.web.search.Widget + * + * @param {Array<openerp.web.search.Filter>} filters elements of the group + * @param {openerp.web.SearchView} view view in which the filters are contained + */ + init: function (filters, view) { + this._super(view); + this.filters = filters; + this.length = filters.length; + }, + start: function () { + this._super(); + _.each(this.filters, function (filter) { + filter.start(); + }); + } +}); +openerp.web.search.add_expand_listener = function($root) { + $root.find('a.searchview_group_string').click(function (e) { + $root.toggleClass('folded expanded'); + e.stopPropagation(); + e.preventDefault(); + }); +}; +openerp.web.search.Group = openerp.web.search.Widget.extend({ + template: 'SearchView.group', + init: function (view_section, view, fields) { + this._super(view); + this.attrs = view_section.attrs; + this.lines = view.make_widgets( + view_section.children, fields); + this.make_id('group'); + }, + start: function () { + this._super(); + openerp.web.search.add_expand_listener(this.$element); + var widget_starts = _(this.lines).chain().flatten() + .map(function (widget) { return widget.start(); }) + .value(); + return $.when.apply(null, widget_starts); + } +}); + +openerp.web.search.Input = openerp.web.search.Widget.extend( /** @lends openerp.web.search.Input# */{ + /** + * @constructs openerp.web.search.Input + * @extends openerp.web.search.Widget + * + * @param view + */ + init: function (view) { + this._super(view); + this.view.inputs.push(this); + }, + get_context: function () { + throw new Error( + "get_context not implemented for widget " + this.attrs.type); + }, + get_domain: function () { + throw new Error( + "get_domain not implemented for widget " + this.attrs.type); + } +}); +openerp.web.search.Filter = openerp.web.search.Input.extend(/** @lends openerp.web.search.Filter# */{ + template: 'SearchView.filter', + /** + * Implementation of the OpenERP filters (button with a context and/or + * a domain sent as-is to the search view) + * + * @constructs openerp.web.search.Filter + * @extends openerp.web.search.Input + * + * @param node + * @param view + */ + init: function (node, view) { + this._super(view); + this.attrs = node.attrs; + this.classes = [this.attrs.string ? 'filter_label' : 'filter_icon']; + this.make_id('filter', this.attrs.name); + }, + start: function () { + this._super(); + var self = this; + this.$element.click(function (e) { + $(this).toggleClass('enabled'); + self.view.do_toggle_filter(self); + }); + }, + /** + * Returns whether the filter is currently enabled (in use) or not. + * + * @returns a boolean + */ + is_enabled:function () { + return this.$element.hasClass('enabled'); + }, + /** + * If the filter is present in the defaults (and has a truthy value), + * enable the filter. + * + * @param {Object} defaults the search view's default values + */ + render: function (defaults) { + if (this.attrs.name && defaults[this.attrs.name]) { + this.classes.push('enabled'); + this.view.do_toggle_filter(this, true); + } + return this._super(defaults); + }, + get_context: function () { + if (!this.is_enabled()) { + return; + } + return this.attrs.context; + }, + get_domain: function () { + if (!this.is_enabled()) { + return; + } + return this.attrs.domain; + } +}); +openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.web.search.Field# */ { + template: 'SearchView.field', + default_operator: '=', + /** + * @constructs openerp.web.search.Field + * @extends openerp.web.search.Input + * + * @param view_section + * @param field + * @param view + */ + init: function (view_section, field, view) { + this._super(view); + this.attrs = _.extend({}, field, view_section.attrs); + this.filters = new openerp.web.search.FilterGroup(_.map( + view_section.children, function (filter_node) { + return new openerp.web.search.Filter( + filter_node, view); + }), view); + this.make_id('input', field.type, this.attrs.name); + }, + start: function () { + this._super(); + this.filters.start(); + }, + get_context: function () { + var val = this.get_value(); + // A field needs a value to be "active", and a context to send when + // active + var has_value = (val !== null && val !== ''); + var context = this.attrs.context; + if (!(has_value && context)) { + return; + } + return _.extend( + {}, context, + {own_values: {self: val}}); + }, + /** + * Function creating the returned domain for the field, override this + * methods in children if you only need to customize the field's domain + * without more complex alterations or tests (and without the need to + * change override the handling of filter_domain) + * + * @param {String} name the field's name + * @param {String} operator the field's operator (either attribute-specified or default operator for the field + * @param {Number|String} value parsed value for the field + * @returns {Array<Array>} domain to include in the resulting search + */ + make_domain: function (name, operator, value) { + return [[name, operator, value]]; + }, + get_domain: function () { + var val = this.get_value(); + if (val === null || val === '') { + return; + } + + var domain = this.attrs['filter_domain']; + if (!domain) { + return this.make_domain( + this.attrs.name, + this.attrs.operator || this.default_operator, + val); + } + return _.extend({}, domain, {own_values: {self: val}}); + } +}); +/** + * Implementation of the ``char`` OpenERP field type: + * + * * Default operator is ``ilike`` rather than ``=`` + * + * * The Javascript and the HTML values are identical (strings) + * + * @class + * @extends openerp.web.search.Field + */ +openerp.web.search.CharField = openerp.web.search.Field.extend( /** @lends openerp.web.search.CharField# */ { + default_operator: 'ilike', + get_value: function () { + return this.$element.val(); + } +}); +openerp.web.search.NumberField = openerp.web.search.Field.extend(/** @lends openerp.web.search.NumberField# */{ + get_value: function () { + if (!this.$element.val()) { + return null; + } + var val = this.parse(this.$element.val()), + check = Number(this.$element.val()); + if (isNaN(val) || val !== check) { + this.$element.addClass('error'); + throw new openerp.web.search.Invalid( + this.attrs.name, this.$element.val(), this.error_message); + } + this.$element.removeClass('error'); + return val; + } +}); +/** + * @class + * @extends openerp.web.search.NumberField + */ +openerp.web.search.IntegerField = openerp.web.search.NumberField.extend(/** @lends openerp.web.search.IntegerField# */{ + error_message: "not a valid integer", + parse: function (value) { + return parseInt(value, 10); + } +}); +/** + * @class + * @extends openerp.web.search.NumberField + */ +openerp.web.search.FloatField = openerp.web.search.NumberField.extend(/** @lends openerp.web.search.FloatField# */{ + error_message: "not a valid number", + parse: function (value) { + return parseFloat(value); + } +}); +/** + * @class + * @extends openerp.web.search.Field + */ +openerp.web.search.SelectionField = openerp.web.search.Field.extend(/** @lends openerp.web.search.SelectionField# */{ + template: 'SearchView.field.selection', + get_value: function () { + return this.$element.val(); + } +}); +openerp.web.search.BooleanField = openerp.web.search.SelectionField.extend(/** @lends openerp.web.search.BooleanField# */{ + /** + * @constructs openerp.web.search.BooleanField + * @extends openerp.web.search.BooleanField + */ + init: function () { + this._super.apply(this, arguments); + this.attrs.selection = [ + ['true', 'Yes'], + ['false', 'No'] + ]; + }, + /** + * Search defaults likely to be boolean values (for a boolean field). + * + * In the HTML, we only want/get strings, and our strings here are ``true`` + * and ``false``, so ensure we use precisely those by truth-testing the + * default value (iif there is one in the view's defaults). + * + * @param {Object} defaults default values for this search view + * @returns {String} rendered boolean field + */ + render: function (defaults) { + var name = this.attrs.name; + if (name in defaults) { + defaults[name] = defaults[name] ? "true" : "false"; + } + return this._super(defaults); + }, + get_value: function () { + switch (this.$element.val()) { + case 'false': return false; + case 'true': return true; + default: return null; + } + } +}); +/** + * @class + * @extends openerp.web.search.DateField + */ +openerp.web.search.DateField = openerp.web.search.Field.extend(/** @lends openerp.web.search.DateField# */{ + /** + * enables date picker on the HTML widgets + */ + start: function () { + this._super(); + this.$element.addClass('field_date').datepicker({ + dateFormat: 'yy-mm-dd' + }); + }, + stop: function () { + this.$element.datepicker('destroy'); + }, + get_value: function () { + return this.$element.val(); + } +}); +/** + * Implementation of the ``datetime`` openerp field type: + * + * * Uses the same widget as the ``date`` field type (a simple date) + * + * * Builds a slighly more complex, it's a datetime range (includes time) + * spanning the whole day selected by the date widget + * + * @class + * @extends openerp.web.DateField + */ +openerp.web.search.DateTimeField = openerp.web.search.DateField.extend(/** @lends openerp.web.search.DateTimeField# */{ + make_domain: function (name, operator, value) { + return ['&', [name, '>=', value + ' 00:00:00'], + [name, '<=', value + ' 23:59:59']]; + } +}); +openerp.web.search.ManyToOneField = openerp.web.search.CharField.extend({ + init: function (view_section, field, view) { + this._super(view_section, field, view); + var self = this; + this.got_name = $.Deferred().then(function () { + self.$element.val(self.name); + }); + this.dataset = new openerp.web.DataSet( + this.view, this.attrs['relation']); + }, + start: function () { + this._super(); + this.setup_autocomplete(); + var started = $.Deferred(); + this.got_name.then(function () { started.resolve();}, + function () { started.resolve(); }); + return started.promise(); + }, + setup_autocomplete: function () { + var self = this; + this.$element.autocomplete({ + source: function (req, resp) { + self.dataset.name_search( + req.term, self.attrs.domain, 'ilike', 8, function (data) { + resp(_.map(data, function (result) { + return {id: result[0], label: result[1]} + })); + }); + }, + select: function (event, ui) { + self.id = ui.item.id; + self.name = ui.item.label; + }, + delay: 0 + }) + }, + on_name_get: function (name_get) { + if (!name_get.length) { + delete this.id; + this.got_name.reject(); + return; + } + this.name = name_get[0][1]; + this.got_name.resolve(); + }, + render: function (defaults) { + if (defaults[this.attrs.name]) { + this.id = defaults[this.attrs.name]; + if (this.id instanceof Array) + this.id = this.id[0]; + // TODO: maybe this should not be completely removed + delete defaults[this.attrs.name]; + this.dataset.name_get([this.id], $.proxy(this, 'on_name_get')); + } else { + this.got_name.reject(); + } + return this._super(defaults); + }, + make_domain: function (name, operator, value) { + if (this.id && this.name) { + if (value === this.name) { + return [[name, '=', this.id]]; + } else { + delete this.id; + delete this.name; + } + } + return this._super(name, operator, value); + } +}); + +openerp.web.search.ExtendedSearch = openerp.web.OldWidget.extend({ + template: 'SearchView.extended_search', + identifier_prefix: 'extended-search', + init: function (parent, model) { + this._super(parent); + this.model = model; + }, + add_group: function() { + var group = new openerp.web.search.ExtendedSearchGroup(this, this.fields); + group.appendTo(this.$element.find('.searchview_extended_groups_list')); + this.check_last_element(); + }, + start: function () { + this._super(); + if (!this.$element) { + return; // not a logical state but sometimes it happens + } + this.$element.closest("table.oe-searchview-render-line").css("display", "none"); + var self = this; + this.rpc("/web/searchview/fields_get", + {"model": this.model}, function(data) { + self.fields = data.fields; + openerp.web.search.add_expand_listener(self.$element); + self.$element.find('.searchview_extended_add_group').click(function (e) { + self.add_group(); + }); + }); + }, + get_context: function() { + return null; + }, + get_domain: function() { + if (!this.$element) { + return null; // not a logical state but sometimes it happens + } + if(this.$element.closest("table.oe-searchview-render-line").css("display") == "none") { + return null; + } + return _.reduce(this.widget_children, + function(mem, x) { return mem.concat(x.get_domain());}, []); + }, + on_activate: function() { + this.add_group(); + var table = this.$element.closest("table.oe-searchview-render-line"); + table.css("display", ""); + if(this.$element.hasClass("folded")) { + this.$element.toggleClass("folded expanded"); + } + }, + hide: function() { + var table = this.$element.closest("table.oe-searchview-render-line"); + table.css("display", "none"); + if(this.$element.hasClass("expanded")) { + this.$element.toggleClass("folded expanded"); + } + }, + check_last_element: function() { + _.each(this.widget_children, function(x) {x.set_last_group(false);}); + if (this.widget_children.length >= 1) { + this.widget_children[this.widget_children.length - 1].set_last_group(true); + } + } +}); + +openerp.web.search.ExtendedSearchGroup = openerp.web.OldWidget.extend({ + template: 'SearchView.extended_search.group', + identifier_prefix: 'extended-search-group', + init: function (parent, fields) { + this._super(parent); + this.fields = fields; + }, + add_prop: function() { + var prop = new openerp.web.search.ExtendedSearchProposition(this, this.fields); + var render = prop.render({'index': this.widget_children.length - 1}); + this.$element.find('.searchview_extended_propositions_list').append(render); + prop.start(); + }, + start: function () { + this._super(); + var _this = this; + this.add_prop(); + this.$element.find('.searchview_extended_add_proposition').click(function () { + _this.add_prop(); + }); + this.$element.find('.searchview_extended_delete_group').click(function () { + _this.stop(); + }); + }, + get_domain: function() { + var props = _(this.widget_children).chain().map(function(x) { + return x.get_proposition(); + }).compact().value(); + var choice = this.$element.find(".searchview_extended_group_choice").val(); + var op = choice == "all" ? "&" : "|"; + return choice == "none" ? ['!'] : [].concat( + _.map(_.range(_.max([0,props.length - 1])), function() { return op; }), + props); + }, + stop: function() { + var parent = this.widget_parent; + if (this.widget_parent.widget_children.length == 1) + this.widget_parent.hide(); + this._super(); + parent.check_last_element(); + }, + set_last_group: function(is_last) { + this.$element.toggleClass('last_group', is_last); + } +}); + +openerp.web.search.ExtendedSearchProposition = openerp.web.OldWidget.extend(/** @lends openerp.web.search.ExtendedSearchProposition# */{ + template: 'SearchView.extended_search.proposition', + identifier_prefix: 'extended-search-proposition', + /** + * @constructs openerp.web.search.ExtendedSearchProposition + * @extends openerp.web.OldWidget + * + * @param parent + * @param fields + */ + init: function (parent, fields) { + this._super(parent); + this.fields = _(fields).chain() + .map(function(val, key) { return _.extend({}, val, {'name': key}); }) + .sortBy(function(field) {return field.string;}) + .value(); + this.attrs = {_: _, fields: this.fields, selected: null}; + this.value = null; + }, + start: function () { + this._super(); + this.select_field(this.fields.length > 0 ? this.fields[0] : null); + var _this = this; + this.$element.find(".searchview_extended_prop_field").change(function() { + _this.changed(); + }); + this.$element.find('.searchview_extended_delete_prop').click(function () { + _this.stop(); + }); + }, + stop: function() { + var parent; + if (this.widget_parent.widget_children.length == 1) + parent = this.widget_parent; + this._super(); + if (parent) + parent.stop(); + }, + changed: function() { + var nval = this.$element.find(".searchview_extended_prop_field").val(); + if(this.attrs.selected == null || nval != this.attrs.selected.name) { + this.select_field(_.detect(this.fields, function(x) {return x.name == nval;})); + } + }, + /** + * Selects the provided field object + * + * @param field a field descriptor object (as returned by fields_get, augmented by the field name) + */ + select_field: function(field) { + var _this = this; + if(this.attrs.selected != null) { + this.value.stop(); + this.value = null; + this.$element.find('.searchview_extended_prop_op').html(''); + } + this.attrs.selected = field; + if(field == null) { + return; + } + + var type = field.type; + try { + openerp.web.search.custom_filters.get_object(type); + } catch (e) { + if (! e instanceof openerp.web.KeyNotFound) { + throw e; + } + type = "char"; + console.log('Unknow field type ' + e.key); + } + this.value = new (openerp.web.search.custom_filters.get_object(type)) + (this); + if(this.value.set_field) { + this.value.set_field(field); + } + _.each(this.value.operators, function(operator) { + var option = jQuery('<option>', {value: operator.value}) + .text(operator.text) + .appendTo(_this.$element.find('.searchview_extended_prop_op')); + }); + this.$element.find('.searchview_extended_prop_value').html( + this.value.render({})); + this.value.start(); + + }, + get_proposition: function() { + if ( this.attrs.selected == null) + return null; + var field = this.attrs.selected.name; + var op = this.$element.find('.searchview_extended_prop_op').val(); + var value = this.value.get_value(); + return [field, op, value]; + } +}); + +openerp.web.search.ExtendedSearchProposition.Char = openerp.web.OldWidget.extend({ + template: 'SearchView.extended_search.proposition.char', + identifier_prefix: 'extended-search-proposition-char', + operators: [ + {value: "ilike", text: "contains"}, + {value: "not ilike", text: "doesn't contain"}, + {value: "=", text: "is equal to"}, + {value: "!=", text: "is not equal to"}, + {value: ">", text: "greater than"}, + {value: "<", text: "less than"}, + {value: ">=", text: "greater or equal than"}, + {value: "<=", text: "less or equal than"} + ], + get_value: function() { + return this.$element.val(); + } +}); +openerp.web.search.ExtendedSearchProposition.DateTime = openerp.web.OldWidget.extend({ + template: 'SearchView.extended_search.proposition.datetime', + identifier_prefix: 'extended-search-proposition-datetime', + operators: [ + {value: "=", text: "is equal to"}, + {value: "!=", text: "is not equal to"}, + {value: ">", text: "greater than"}, + {value: "<", text: "less than"}, + {value: ">=", text: "greater or equal than"}, + {value: "<=", text: "less or equal than"} + ], + get_value: function() { + return this.$element.val(); + }, + start: function() { + this._super(); + this.$element.datetimepicker({ + dateFormat: 'yy-mm-dd', + timeFormat: 'hh:mm:ss' + }); + } +}); +openerp.web.search.ExtendedSearchProposition.Date = openerp.web.OldWidget.extend({ + template: 'SearchView.extended_search.proposition.date', + identifier_prefix: 'extended-search-proposition-date', + operators: [ + {value: "=", text: "is equal to"}, + {value: "!=", text: "is not equal to"}, + {value: ">", text: "greater than"}, + {value: "<", text: "less than"}, + {value: ">=", text: "greater or equal than"}, + {value: "<=", text: "less or equal than"} + ], + get_value: function() { + return this.$element.val(); + }, + start: function() { + this._super(); + this.$element.datepicker({ + dateFormat: 'yy-mm-dd', + timeFormat: 'hh:mm:ss' + }); + } +}); +openerp.web.search.ExtendedSearchProposition.Integer = openerp.web.OldWidget.extend({ + template: 'SearchView.extended_search.proposition.integer', + identifier_prefix: 'extended-search-proposition-integer', + operators: [ + {value: "=", text: "is equal to"}, + {value: "!=", text: "is not equal to"}, + {value: ">", text: "greater than"}, + {value: "<", text: "less than"}, + {value: ">=", text: "greater or equal than"}, + {value: "<=", text: "less or equal than"} + ], + get_value: function() { + var value = parseFloat(this.$element.val()); + if(value != 0 && !value) { + return ""; + } + return Math.round(value); + } +}); +openerp.web.search.ExtendedSearchProposition.Float = openerp.web.OldWidget.extend({ + template: 'SearchView.extended_search.proposition.float', + identifier_prefix: 'extended-search-proposition-float', + operators: [ + {value: "=", text: "is equal to"}, + {value: "!=", text: "is not equal to"}, + {value: ">", text: "greater than"}, + {value: "<", text: "less than"}, + {value: ">=", text: "greater or equal than"}, + {value: "<=", text: "less or equal than"} + ], + get_value: function() { + var value = parseFloat(this.$element.val()); + if(value != 0 && !value) { + return ""; + } + return value; + } +}); +openerp.web.search.ExtendedSearchProposition.Selection = openerp.web.OldWidget.extend({ + template: 'SearchView.extended_search.proposition.selection', + identifier_prefix: 'extended-search-proposition-selection', + operators: [ + {value: "=", text: "is"}, + {value: "!=", text: "is not"} + ], + set_field: function(field) { + this.field = field; + }, + get_value: function() { + return this.$element.val(); + } +}); +openerp.web.search.ExtendedSearchProposition.Boolean = openerp.web.OldWidget.extend({ + template: 'SearchView.extended_search.proposition.boolean', + identifier_prefix: 'extended-search-proposition-boolean', + operators: [ + {value: "=", text: "is true"}, + {value: "!=", text: "is false"} + ], + get_value: function() { + return true; + } +}); + +openerp.web.search.custom_filters = new openerp.web.Registry({ + 'char': 'openerp.web.search.ExtendedSearchProposition.Char', + 'text': 'openerp.web.search.ExtendedSearchProposition.Char', + 'one2many': 'openerp.web.search.ExtendedSearchProposition.Char', + 'many2one': 'openerp.web.search.ExtendedSearchProposition.Char', + 'many2many': 'openerp.web.search.ExtendedSearchProposition.Char', + + 'datetime': 'openerp.web.search.ExtendedSearchProposition.DateTime', + 'date': 'openerp.web.search.ExtendedSearchProposition.Date', + 'integer': 'openerp.web.search.ExtendedSearchProposition.Integer', + 'float': 'openerp.web.search.ExtendedSearchProposition.Float', + 'boolean': 'openerp.web.search.ExtendedSearchProposition.Boolean', + 'selection': 'openerp.web.search.ExtendedSearchProposition.Selection' +}); + +}; + +// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/js/view_editor.js b/addons/web/static/src/js/view_editor.js new file mode 100644 index 00000000000..22800facf9b --- /dev/null +++ b/addons/web/static/src/js/view_editor.js @@ -0,0 +1,147 @@ +openerp.web.view_editor = function(openerp) { +openerp.web.ViewEditor = openerp.web.Widget.extend({ + init: function(parent, element_id, dataset, view, options) { + this._super(parent); + this.element_id = element_id + this.parent = parent + this.dataset = dataset; + this.model = dataset.model; + this.xml_id = 0; + }, + start: function() { + this.View_editor(); + }, + View_editor : function(){ + var self = this; + var action = { + name:'ViewEditor', + context:this.session.user_context, + domain: [["model", "=", this.dataset.model]], + res_model: 'ir.ui.view', + views : [[false, 'list']], + type: 'ir.actions.act_window', + target: "current", + limit : 80, + auto_search : true, + flags: { + sidebar: false, + views_switcher: false, + action_buttons:false, + search_view:false, + pager:false, + radio:true + }, + }; + var action_manager = new openerp.web.ActionManager(this); + this.dialog = new openerp.web.Dialog(this,{ + modal: true, + title: 'ViewEditor', + width: 750, + height: 500, + buttons: { + "Create": function(){ + + }, + "Edit": function(){ + self.xml_id=0; + self.edit_view(); + }, + "Close": function(){ + $(this).dialog('destroy'); + } + }, + + }); + this.dialog.start(); + this.dialog.open(); + action_manager.appendTo(this.dialog); + action_manager.do_action(action); + + + }, + check_attr:function(xml,tag){ + var obj = new Object(); + obj.child_id = []; + obj.id = this.xml_id++; + var att_list = []; + var name1 = "<" + tag; + $(xml).each(function() { + att_list = this.attributes; + att_list = _.select(att_list, function(attrs){ + if(attrs.nodeName == "string" || attrs.nodeName == "name" || attrs.nodeName == "index"){ + name1 += ' ' +attrs.nodeName+'='+'"'+attrs.nodeValue+'"';} + }); + name1+= ">"; + }); + obj.name = name1; + return obj; + }, + recursiveFunction : function(main_object,parent_id,child_id){ + var self = this; + var check = false; + var main_object = _.detect(main_object , function(node){ + if(node.id == parent_id){ + node.child_id = child_id; + check = true; + } + return main_object; + }); + if(check){ + return main_object; + }else{ + //todo recursion for saving object into objects + } + }, + children_function : function(xml,root,main_object,parent_id){ + var self = this; + var child_obj_list = []; + var main_object = main_object; + var children_list = $(xml).filter(root).children(); + _.each(children_list, function(child_node){ + var string = self.check_attr(child_node,child_node.tagName.toLowerCase()); + child_obj_list.push(string); + }); + if(children_list.length != 0){ + main_object = self.recursiveFunction(main_object,parent_id,child_obj_list); + } + for(var i=0;i<children_list.length;i++){ + self.children_function(children_list[i],children_list[i].tagName.toLowerCase(),main_object,child_obj_list[i].id); + } + return child_obj_list; + }, + edit_view : function(){ + var self = this; + var view_id =(($("input[name='radiogroup']:checked").parent()).parent()).attr('data-id'); + var ve_dataset = new openerp.web.DataSet(this,'ir.ui.view'); + ve_dataset.read_ids([parseInt(view_id)],['arch'],function (arch) { + var arch = arch[0].arch; + var root = $(arch).filter(":first")[0]; + var tag = root.tagName.toLowerCase(); + var root_object = self.check_attr(root,tag); + var all_list = self.children_function(arch,tag,[root_object],root_object.id); + + //todo render the final object of tree view so xml with it child node display... + }); + this.dialog = new openerp.web.Dialog(this,{ + modal: true, + title: 'Edit Xml', + width: 750, + height: 500, + buttons: { + "Inherited View": function(){ + + }, + "Preview": function(){ + + }, + "Close": function(){ + $(this).dialog('destroy'); + } + }, + + }); + this.dialog.start().open(); + } + +}); +}; diff --git a/addons/web/static/src/js/view_help.js b/addons/web/static/src/js/view_help.js new file mode 100644 index 00000000000..f7e5f7c259e --- /dev/null +++ b/addons/web/static/src/js/view_help.js @@ -0,0 +1,15 @@ +/*--------------------------------------------------------- + * OpenERP web library + *---------------------------------------------------------*/ + +openerp.web.view_help = function(openerp) { + +openerp.web.ProcessView = openerp.web.Widget.extend({ +}); + +openerp.web.HelpView = openerp.web.Widget.extend({ +}); + +}; + +// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/js/view_tree.js b/addons/web/static/src/js/view_tree.js new file mode 100644 index 00000000000..2a314f57fa8 --- /dev/null +++ b/addons/web/static/src/js/view_tree.js @@ -0,0 +1,216 @@ +/*--------------------------------------------------------- + * OpenERP web library + *---------------------------------------------------------*/ + +openerp.web.view_tree = function(openerp) { +var QWeb = openerp.web.qweb; + +openerp.web.views.add('tree', 'openerp.web.TreeView'); +openerp.web.TreeView = openerp.web.View.extend(/** @lends openerp.web.TreeView# */{ + /** + * Indicates that this view is not searchable, and thus that no search + * view should be displayed (if there is one active). + */ + searchable : false, + /** + * Genuine tree view (the one displayed as a tree, not the list) + * + * @constructs openerp.web.TreeView + * @extends openerp.web.View + * + * @param parent + * @param element_id + * @param dataset + * @param view_id + * @param options + */ + init: function(parent, element_id, dataset, view_id, options) { + this._super(parent, element_id); + this.dataset = dataset; + this.model = dataset.model; + this.view_id = view_id; + + this.records = {}; + + this.options = _.extend({}, this.defaults, options || {}); + }, + + start: function () { + this._super(); + return this.rpc("/web/treeview/load", { + model: this.model, + view_id: this.view_id, + view_type: "tree", + toolbar: this.view_manager ? !!this.view_manager.sidebar : false + }, this.on_loaded); + }, + /** + * Returns the list of fields needed to correctly read objects. + * + * Gathers the names of all fields in fields_view_get, and adds the + * field_parent (children_field in the tree view) if it's not already one + * of the fields to fetch + * + * @returns {Array} an array of fields which can be provided to DataSet.read_slice and others + */ + fields_list: function () { + var fields = _.keys(this.fields); + if (!_(fields).contains(this.children_field)) { + fields.push(this.children_field); + } + return fields; + }, + on_loaded: function (fields_view) { + var self = this; + var has_toolbar = !!fields_view.arch.attrs.toolbar; + // field name in OpenERP is kinda stupid: this is the name of the field + // holding the ids to the children of the current node, why call it + // field_parent? + this.children_field = fields_view['field_parent']; + this.fields_view = fields_view; + _(this.fields_view.arch.children).each(function (field) { + if (field.attrs.modifiers) { + field.attrs.modifiers = JSON.parse(field.attrs.modifiers); + } + }); + this.fields = fields_view.fields; + this.hook_row_click(); + this.$element.html(QWeb.render('TreeView', { + 'title': this.fields_view.arch.attrs.string, + 'fields_view': this.fields_view.arch.children, + 'fields': this.fields, + 'toolbar': has_toolbar + })); + + this.dataset.read_slice(this.fields_list(), {}, function (records) { + if (!has_toolbar) { + // WARNING: will do a second read on the same ids, but only on + // first load so not very important + self.getdata(null, _(records).pluck('id')); + return; + } + + var $select = self.$element.find('select') + .change(function () { + var $option = $(this).find(':selected'); + self.getdata($option.val(), $option.data('children')); + }); + _(records).each(function (record) { + self.records[record.id] = record; + $('<option>') + .val(record.id) + .text(record.name) + .data('children', record[self.children_field]) + .appendTo($select); + }); + + if (!_.isEmpty(records)) { + $select.change(); + } + }); + }, + /** + * Sets up opening a row + */ + hook_row_click: function () { + var self = this; + this.$element.delegate('.treeview-td span, .treeview-tr span', 'click', function (e) { + e.stopImmediatePropagation(); + self.activate($(this).closest('tr').data('id')); + }); + + this.$element.delegate('.treeview-tr', 'click', function () { + var is_loaded = 0, + $this = $(this), + record_id = $this.data('id'), + record = self.records[record_id], + children_ids = record[self.children_field]; + + _(children_ids).each(function(childid) { + if (self.$element.find('#treerow_' + childid).length) { + if (self.$element.find('#treerow_' + childid).is(':hidden')) { + is_loaded = -1; + } else { + is_loaded++; + } + } + }); + if (is_loaded === 0) { + if (!$this.parent().hasClass('oe-open')) { + self.getdata(record_id, children_ids); + } + } else { + self.showcontent(record_id, is_loaded < 0); + } + }); + }, + // get child data of selected value + getdata: function (id, children_ids) { + var self = this; + + self.dataset.read_ids(children_ids, this.fields_list(), function (records) { + _(records).each(function (record) { + self.records[record.id] = record; + }); + + var $curr_node = self.$element.find('#treerow_' + id); + var children_rows = QWeb.render('TreeView.rows', { + 'records': records, + 'children_field': self.children_field, + 'fields_view': self.fields_view.arch.children, + 'fields': self.fields, + 'level': $curr_node.data('level') || 0, + 'render': openerp.web.format_value + }); + + if ($curr_node.length) { + $curr_node.addClass('oe-open'); + $curr_node.after(children_rows); + } else { + self.$element.find('tbody').html(children_rows); + } + }); + }, + + // Get details in listview + activate: function(id) { + var self = this; + this.rpc('/web/treeview/action', { + id: id, + model: this.dataset.model, + context: new openerp.web.CompoundContext( + this.dataset.get_context(), { + active_model: this.dataset.model, + active_id: id, + active_ids: [id]}) + }, function (actions) { + if (!actions.length) { return; } + var action = actions[0][2]; + self.do_action(action); + }); + }, + + // show & hide the contents + showcontent: function (record_id, show) { + this.$element.find('#treerow_' + record_id) + .toggleClass('oe-open', show); + + _(this.records[record_id][this.children_field]).each(function (child_id) { + var $child_row = this.$element.find('#treerow_' + child_id); + if ($child_row.hasClass('oe-open')) { + this.showcontent(child_id, false); + } + $child_row.toggle(show); + }, this); + }, + + do_show: function () { + this.$element.show(); + }, + + do_hide: function () { + this.$element.hide(); + this.hidden = true; + } +}); +}; diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js new file mode 100644 index 00000000000..39d02d221ff --- /dev/null +++ b/addons/web/static/src/js/views.js @@ -0,0 +1,829 @@ +/*--------------------------------------------------------- + * OpenERP web library + *---------------------------------------------------------*/ + +openerp.web.views = function(openerp) { + +var _t = openerp.web._t; +var QWeb = openerp.web.qweb; + +/** + * Registry for all the client actions key: tag value: widget + */ +openerp.web.client_actions = new openerp.web.Registry(); + +openerp.web.ActionManager = openerp.web.Widget.extend({ + identifier_prefix: "actionmanager", + init: function(parent) { + this._super(parent); + this.inner_viewmanager = null; + this.dialog = null; + this.dialog_viewmanager = null; + this.client_widget = null; + this.url = {} + }, + render: function() { + return "<div id='"+this.element_id+"'></div>"; + }, + dialog_stop: function () { + if (this.dialog) { + this.dialog_viewmanager.stop(); + this.dialog_viewmanager = null; + this.dialog.stop(); + this.dialog = null; + } + }, + content_stop: function () { + if (this.inner_viewmanager) { + this.inner_viewmanager.stop(); + this.inner_viewmanager = null; + } + if (this.client_widget) { + this.client_widget.stop(); + this.client_widget = null; + } + }, + url_update: function(action) { + // this.url = { + // "model": action.model, + // "domain": action.domain, + // }; + // action.res_model + // action.domain + // action.context + // after + // action.views + // action.res_id + // mode + // menu + }, + url_stringify: function(action) { + }, + url_parse: function(action) { + }, + on_url_update: function(url) { + }, + do_url_action: function(url) { + }, + do_action: function(action, on_close) { + var type = action.type.replace(/\./g,'_'); + var popup = action.target === 'new'; + action.flags = _.extend({ + views_switcher : !popup, + search_view : !popup, + action_buttons : !popup, + sidebar : !popup, + pager : !popup + }, action.flags || {}); + if (!(type in this)) { + console.log("Action manager can't handle action of type " + action.type, action); + return; + } + this[type](action, on_close); + }, + ir_actions_act_window: function (action, on_close) { + if (action.target === 'new') { + if (this.dialog == null) { + this.dialog = new openerp.web.Dialog(this, { title: action.name, width: '80%' }); + if(on_close) + this.dialog.on_close.add(on_close); + this.dialog.start(); + } else { + this.dialog_viewmanager.stop(); + } + this.dialog_viewmanager = new openerp.web.ViewManagerAction(this, action); + this.dialog_viewmanager.appendTo(this.dialog.$element); + this.dialog.open(); + } else { + this.dialog_stop(); + this.content_stop(); + this.inner_viewmanager = new openerp.web.ViewManagerAction(this, action); + this.inner_viewmanager.appendTo(this.$element); + this.url_update(action); + } + /* new window code + this.rpc("/web/session/save_session_action", { the_action : action}, function(key) { + var url = window.location.protocol + "//" + window.location.host + window.location.pathname + "?" + jQuery.param({ s_action : "" + key }); + window.open(url,'_blank'); + }); + */ + }, + ir_actions_act_window_close: function (action, on_closed) { + this.dialog_stop(); + }, + ir_actions_server: function (action, on_closed) { + var self = this; + this.rpc('/web/action/run', { + action_id: action.id, + context: action.context || {} + }).then(function (action) { + self.do_action(action, on_closed) + }); + }, + ir_actions_client: function (action) { + this.content_stop(); + var ClientWidget = openerp.web.client_actions.get_object(action.tag); + (this.client_widget = new ClientWidget(this, action.params)).appendTo(this); + }, + ir_actions_report_xml: function(action) { + $.blockUI(); + this.session.get_file({ + url: '/web/report', + data: {action: JSON.stringify(action)}, + complete: $.unblockUI + }); + } +}); + +openerp.web.ViewManager = openerp.web.Widget.extend(/** @lends openerp.web.ViewManager# */{ + identifier_prefix: "viewmanager", + template: "ViewManager", + /** + * @constructs openerp.web.ViewManager + * @extends openerp.web.Widget + * + * @param parent + * @param dataset + * @param views + */ + init: function(parent, dataset, views) { + this._super(parent); + this.model = dataset.model; + this.dataset = dataset; + this.searchview = null; + this.active_view = null; + this.views_src = _.map(views, function(x) {return x instanceof Array? {view_id: x[0], view_type: x[1]} : x;}); + this.views = {}; + this.flags = this.flags || {}; + this.registry = openerp.web.views; + }, + render: function() { + return QWeb.render(this.template, {"prefix": this.element_id, views: this.views_src}) + }, + /** + * @returns {jQuery.Deferred} initial view loading promise + */ + start: function() { + this._super(); + var self = this; + this.dataset.start(); + this.$element.find('.oe_vm_switch button').click(function() { + self.on_mode_switch($(this).data('view-type')); + }); + var views_ids = {}; + _.each(this.views_src, function(view) { + self.views[view.view_type] = $.extend({}, view, { + controller : null, + options : _.extend({ + sidebar_id : self.element_id + '_sidebar_' + view.view_type, + action : self.action, + action_views_ids : views_ids + }, self.flags, view.options || {}) + }); + views_ids[view.view_type] = view.view_id; + }); + if (this.flags.views_switcher === false) { + this.$element.find('.oe_vm_switch').hide(); + } + // switch to the first one in sequence + return this.on_mode_switch(this.views_src[0].view_type); + }, + /** + * Asks the view manager to switch visualization mode. + * + * @param {String} view_type type of view to display + * @returns {jQuery.Deferred} new view loading promise + */ + on_mode_switch: function(view_type) { + var self = this, + view_promise; + this.active_view = view_type; + var view = this.views[view_type]; + if (!view.controller) { + // Lazy loading of views + var controllerclass = this.registry.get_object(view_type); + var controller = new controllerclass(this, this.element_id + '_view_' + view_type, + this.dataset, view.view_id, view.options); + if (view.embedded_view) { + controller.set_embedded_view(view.embedded_view); + } + controller.do_switch_view.add_last(this.on_mode_switch); + if (view_type === 'list' && this.flags.search_view === false && this.action && this.action['auto_search']) { + // In case the search view is not instantiated: manually call ListView#search + var domains = !_(self.action.domain).isEmpty() + ? [self.action.domain] : [], + contexts = !_(self.action.context).isEmpty() + ? [self.action.context] : []; + controller.on_loaded.add({ + callback: function () { + controller.do_search(domains, contexts, []); + }, + position: 'last', + unique: true + }); + } + view_promise = controller.start(); + $.when(view_promise).then(function() { + self.on_controller_inited(view_type, controller); + }); + this.views[view_type].controller = controller; + } + + + if (this.searchview) { + if (view.controller.searchable === false) { + this.searchview.hide(); + } else { + this.searchview.show(); + } + } + + this.$element + .find('.views-switchers button').removeAttr('disabled') + .filter('[data-view-type="' + view_type + '"]') + .attr('disabled', true); + + for (var view_name in this.views) { + if (!this.views.hasOwnProperty(view_name)) { continue; } + if (this.views[view_name].controller) { + if (view_name === view_type) { + $.when(view_promise).then(this.views[view_name].controller.do_show); + } else { + this.views[view_name].controller.do_hide(); + } + } + } + return view_promise; + }, + /** + * Event launched when a controller has been inited. + * + * @param {String} view_type type of view + * @param {String} view the inited controller + */ + on_controller_inited: function(view_type, view) { + }, + /** + * Sets up the current viewmanager's search view. + * + * @param {Number|false} view_id the view to use or false for a default one + * @returns {jQuery.Deferred} search view startup deferred + */ + setup_search_view: function(view_id, search_defaults) { + var self = this; + if (this.searchview) { + this.searchview.stop(); + } + this.searchview = new openerp.web.SearchView( + this, this.element_id + "_search", this.dataset, + view_id, search_defaults); + + this.searchview.on_search.add(function(domains, contexts, groupbys) { + var controller = self.views[self.active_view].controller; + controller.do_search.call(controller, domains, contexts, groupbys); + }); + return this.searchview.start(); + }, + /** + * Called when one of the view want to execute an action + */ + on_action: function(action) { + }, + on_create: function() { + }, + on_remove: function() { + }, + on_edit: function() { + } +}); + +openerp.web.ViewManagerAction = openerp.web.ViewManager.extend(/** @lends oepnerp.web.ViewManagerAction# */{ + template:"ViewManagerAction", + /** + * @constructs openerp.web.ViewManagerAction + * @extends openerp.web.ViewManager + * + * @param {openerp.web.ActionManager} parent parent object/widget + * @param {Object} action descriptor for the action this viewmanager needs to manage its views. + */ + init: function(parent, action) { + // dataset initialization will take the session from ``this``, so if we + // do not have it yet (and we don't, because we've not called our own + // ``_super()``) rpc requests will blow up. + this.session = parent.session; + this.action = action; + var dataset = new openerp.web.DataSetSearch(this, action.res_model, action.context, action.domain); + if (action.res_id) { + dataset.ids.push(action.res_id); + dataset.index = 0; + } + this._super(parent, dataset, action.views); + this.flags = this.action.flags || {}; + if (action.res_model == 'board.board' && action.views.length == 1 && action.views) { + // Not elegant but allows to avoid form chrome (pager, save/new + // buttons, sidebar, ...) displaying + this.flags.search_view = this.flags.pager = this.flags.sidebar = this.flags.action_buttons = false; + } + }, + /** + * Initializes the ViewManagerAction: sets up the searchview (if the + * searchview is enabled in the manager's action flags), calls into the + * parent to initialize the primary view and (if the VMA has a searchview) + * launches an initial search after both views are done rendering. + */ + start: function() { + var searchview_loaded; + if (this.flags.search_view !== false) { + var search_defaults = {}; + _.each(this.action.context, function (value, key) { + var match = /^search_default_(.*)$/.exec(key); + if (match) { + search_defaults[match[1]] = value; + } + }); + // init search view + var searchview_id = this.action['search_view_id'] && this.action['search_view_id'][0]; + + searchview_loaded = this.setup_search_view( + searchview_id || false, search_defaults); + } + + var main_view_loaded = this._super(); + + var manager_ready = $.when(searchview_loaded, main_view_loaded); + if (searchview_loaded && this.action['auto_search']) { + // schedule auto_search + manager_ready.then(this.searchview.do_search); + } + return manager_ready; + }, + on_mode_switch: function (view_type) { + return $.when( + this._super(view_type), + this.shortcut_check(this.views[view_type])); + }, + shortcut_check : function(view) { + var self = this; + var grandparent = this.widget_parent && this.widget_parent.widget_parent; + // display shortcuts if on the first view for the action + var $shortcut_toggle = this.$element.find('.oe-shortcut-toggle'); + if (!(grandparent instanceof openerp.web.WebClient) || + !(view.view_type === this.views_src[0].view_type + && view.view_id === this.views_src[0].view_id)) { + $shortcut_toggle.hide(); + return; + } + $shortcut_toggle.removeClass('oe-shortcut-remove').show(); + if (_(this.session.shortcuts).detect(function (shortcut) { + return shortcut.res_id === self.session.active_id; })) { + $shortcut_toggle.addClass("oe-shortcut-remove"); + } + this.shortcut_add_remove(); + }, + shortcut_add_remove: function() { + var self = this; + var $shortcut_toggle = this.$element.find('.oe-shortcut-toggle'); + $shortcut_toggle + .unbind("click") + .click(function() { + if ($shortcut_toggle.hasClass("oe-shortcut-remove")) { + $(self.session.shortcuts.binding).trigger('remove-current'); + $shortcut_toggle.removeClass("oe-shortcut-remove"); + } else { + $(self.session.shortcuts.binding).trigger('add', { + 'user_id': self.session.uid, + 'res_id': self.session.active_id, + 'resource': 'ir.ui.menu', + 'name': self.action.name + }); + $shortcut_toggle.addClass("oe-shortcut-remove"); + } + }); + } +}); + +openerp.web.Sidebar = openerp.web.Widget.extend({ + init: function(parent, element_id) { + this._super(parent, element_id); + this.items = {}; + this.sections = {}; + }, + start: function() { + this._super(this); + var self = this; + this.$element.html(QWeb.render('Sidebar')); + this.$element.find(".toggle-sidebar").click(function(e) { + self.do_toggle(); + }); + }, + add_toolbar: function(toolbar) { + var self = this; + _.each([['print', "Reports"], ['action', "Actions"], ['relate', "Links"]], function(type) { + var items = toolbar[type[0]]; + if (items.length) { + for (var i = 0; i < items.length; i++) { + items[i] = { + label: items[i]['name'], + action: items[i], + classname: 'oe_sidebar_' + type[0] + } + } + self.add_section(type[0], type[1], items); + } + }); + }, + add_section: function(code, name, items) { + // For each section, we pass a name/label and optionally an array of items. + // If no items are passed, then the section will be created as a custom section + // returning back an element_id to be used by a custom controller. + // Else, the section is a standard section with items displayed as links. + // An item is a dictonary : { + // label: label to be displayed for the link, + // action: action to be launch when the link is clicked, + // callback: a function to be executed when the link is clicked, + // classname: optional dom class name for the line, + // title: optional title for the link + // } + // Note: The item should have one action or/and a callback + var self = this, + section_id = _.uniqueId(this.element_id + '_section_' + code + '_'); + if (items) { + for (var i = 0; i < items.length; i++) { + items[i].element_id = _.uniqueId(section_id + '_item_'); + this.items[items[i].element_id] = items[i]; + } + } + var $section = $(QWeb.render("Sidebar.section", { + section_id: section_id, + name: name, + classname: 'oe_sidebar_' + code, + items: items + })); + if (items) { + $section.find('a.oe_sidebar_action_a').click(function() { + var item = self.items[$(this).attr('id')]; + if (item.callback) { + item.callback(); + } + if (item.action) { + var ids = self.widget_parent.get_selected_ids(); + if (ids.length == 0) { + //TODO: make prettier warning? + $("<div />").text(_t("You must choose at least one record.")).dialog({ + title: _t("Warning"), + modal: true + }); + return false; + } + var additional_context = { + active_id: ids[0], + active_ids: ids, + active_model: self.widget_parent.dataset.model + }; + self.rpc("/web/action/load", { + action_id: item.action.id, + context: additional_context + }, function(result) { + result.result.context = _.extend(result.result.context || {}, + additional_context); + result.result.flags = result.result.flags || {}; + result.result.flags.new_window = true; + self.do_action(result.result); + }); + } + return false; + }); + } + $section.appendTo(this.$element.find('div.sidebar-actions')); + this.sections[code] = $section; + return section_id; + }, + do_fold: function() { + this.$element.addClass('closed-sidebar').removeClass('open-sidebar'); + }, + do_unfold: function() { + this.$element.addClass('open-sidebar').removeClass('closed-sidebar'); + }, + do_toggle: function() { + this.$element.toggleClass('open-sidebar closed-sidebar'); + } +}); + +openerp.web.TranslateDialog = openerp.web.Dialog.extend({ + dialog_title: _t("Translations"), + init: function(view) { + // TODO fme: should add the language to fields_view_get because between the fields view get + // and the moment the user opens the translation dialog, the user language could have been changed + this.view_language = view.session.user_context.lang; + this['on_button' + _t("Save")] = this.on_button_Save; + this['on_button' + _t("Close")] = this.on_button_Close; + this._super(view, { + width: '80%', + height: '80%' + }); + this.view = view; + this.view_type = view.fields_view.type || ''; + this.$fields_form = null; + this.$view_form = null; + this.$sidebar_form = null; + this.translatable_fields_keys = _.map(this.view.translatable_fields || [], function(i) { return i.name }); + this.languages = null; + this.languages_loaded = $.Deferred(); + (new openerp.web.DataSetSearch(this, 'res.lang', this.view.dataset.get_context(), + [['translatable', '=', '1']])).read_slice(['code', 'name'], { sort: 'id' }, this.on_languages_loaded); + }, + start: function() { + var self = this; + this._super(); + $.when(this.languages_loaded).then(function() { + self.$element.html(QWeb.render('TranslateDialog', { widget: self })); + self.$element.tabs(); + if (!(self.view.translatable_fields && self.view.translatable_fields.length)) { + self.hide_tabs('fields'); + self.select_tab('view'); + } + self.$fields_form = self.$element.find('.oe_translation_form'); + self.$fields_form.find('.oe_trad_field').change(function() { + $(this).toggleClass('touched', ($(this).val() != $(this).attr('data-value'))); + }); + }); + return this; + }, + on_languages_loaded: function(langs) { + this.languages = langs; + this.languages_loaded.resolve(); + }, + do_load_fields_values: function(callback) { + var self = this, + deffered = []; + this.$fields_form.find('.oe_trad_field').val('').removeClass('touched'); + _.each(self.languages, function(lg) { + var deff = $.Deferred(); + deffered.push(deff); + var callback = function(values) { + _.each(self.translatable_fields_keys, function(f) { + self.$fields_form.find('.oe_trad_field[name="' + lg.code + '-' + f + '"]').val(values[0][f] || '').attr('data-value', values[0][f] || ''); + }); + deff.resolve(); + }; + if (lg.code === self.view_language) { + var values = {}; + _.each(self.translatable_fields_keys, function(field) { + values[field] = self.view.fields[field].get_value(); + }); + callback([values]); + } else { + self.rpc('/web/dataset/get', { + model: self.view.dataset.model, + ids: [self.view.datarecord.id], + fields: self.translatable_fields_keys, + context: self.view.dataset.get_context({ + 'lang': lg.code + })}, callback); + } + }); + $.when.apply(null, deffered).then(callback); + }, + show_tabs: function() { + for (var i = 0; i < arguments.length; i++) { + this.$element.find('ul.oe_translate_tabs li a[href$="' + arguments[i] + '"]').parent().show(); + } + }, + hide_tabs: function() { + for (var i = 0; i < arguments.length; i++) { + this.$element.find('ul.oe_translate_tabs li a[href$="' + arguments[i] + '"]').parent().hide(); + } + }, + select_tab: function(name) { + this.show_tabs(name); + var index = this.$element.find('ul.oe_translate_tabs li a[href$="' + arguments[i] + '"]').parent().index() - 1; + this.$element.tabs('select', index); + }, + open: function(field) { + var self = this, + sup = this._super; + $.when(this.languages_loaded).then(function() { + if (self.view.translatable_fields && self.view.translatable_fields.length) { + self.do_load_fields_values(function() { + sup.call(self); + if (field) { + // TODO: focus and scroll to field + } + }); + } else { + sup.call(self); + } + }); + }, + on_button_Save: function() { + var trads = {}, + self = this; + self.$fields_form.find('.oe_trad_field.touched').each(function() { + var field = $(this).attr('name').split('-'); + if (!trads[field[0]]) { + trads[field[0]] = {}; + } + trads[field[0]][field[1]] = $(this).val(); + }); + _.each(trads, function(data, code) { + if (code === self.view_language) { + _.each(data, function(value, field) { + self.view.fields[field].set_value(value); + }); + } else { + self.view.dataset.write(self.view.datarecord.id, data, { 'lang': code }); + } + }); + this.close(); + }, + on_button_Close: function() { + this.close(); + } +}); + +/** + * @class + * @extends openerp.web.Widget + */ +openerp.web.View = openerp.web.Widget.extend(/** @lends openerp.web.View# */{ + set_default_options: function(options) { + this.options = options || {}; + _.defaults(this.options, { + // All possible views options should be defaulted here + sidebar_id: null, + sidebar: true, + action: null, + action_views_ids: {} + }); + }, + open_translate_dialog: function(field) { + if (!this.translate_dialog) { + this.translate_dialog = new openerp.web.TranslateDialog(this).start(); + } + this.translate_dialog.open(field); + }, + /** + * Fetches and executes the action identified by ``action_data``. + * + * @param {Object} action_data the action descriptor data + * @param {String} action_data.name the action name, used to uniquely identify the action to find and execute it + * @param {String} [action_data.special=null] special action handlers (currently: only ``'cancel'``) + * @param {String} [action_data.type='workflow'] the action type, if present, one of ``'object'``, ``'action'`` or ``'workflow'`` + * @param {Object} [action_data.context=null] additional action context, to add to the current context + * @param {openerp.web.DataSet} dataset a dataset object used to communicate with the server + * @param {Object} [record_id] the identifier of the object on which the action is to be applied + * @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action) + */ + execute_action: function (action_data, dataset, record_id, on_closed) { + var self = this; + var handler = function (r) { + var action = r.result; + if (action && action.constructor == Object) { + action.context = action.context || {}; + _.extend(action.context, { + active_id: record_id || false, + active_ids: [record_id || false], + active_model: dataset.model + }); + action.context = new openerp.web.CompoundContext(dataset.get_context(), action.context); + self.do_action(action, on_closed); + } else if (on_closed) { + on_closed(action); + } + }; + + var context = new openerp.web.CompoundContext(dataset.get_context(), action_data.context || {}); + + if (action_data.special) { + handler({result: {"type":"ir.actions.act_window_close"}}); + } else if (action_data.type=="object") { + return dataset.call_button(action_data.name, [[record_id], context], handler); + } else if (action_data.type=="action") { + return this.rpc('/web/action/load', { action_id: parseInt(action_data.name, 10), context: context }, handler); + } else { + return dataset.exec_workflow(record_id, action_data.name, handler); + } + }, + /** + * Directly set a view to use instead of calling fields_view_get. This method must + * be called before start(). When an embedded view is set, underlying implementations + * of openerp.web.View must use the provided view instead of any other one. + * + * @param embedded_view A view. + */ + set_embedded_view: function(embedded_view) { + this.embedded_view = embedded_view; + this.options.sidebar = false; + }, + do_switch_view: function(view) { + }, + set_common_sidebar_sections: function(sidebar) { + sidebar.add_section('customize', "Customize", [ + { + label: "Manage Views", + callback: this.on_sidebar_manage_view, + title: "Manage views of the current object" + }, { + label: "Edit Workflow", + callback: this.on_sidebar_edit_workflow, + title: "Manage views of the current object", + classname: 'oe_hide oe_sidebar_edit_workflow' + }, { + label: "Customize Object", + callback: this.on_sidebar_customize_object, + title: "Manage views of the current object" + } + ]); + sidebar.add_section('other', "Other Options", [ + { + label: "Import", + callback: this.on_sidebar_import + }, { + label: "Export", + callback: this.on_sidebar_export + }, { + label: "Translate", + callback: this.on_sidebar_translate, + classname: 'oe_sidebar_translate oe_hide' + }, { + label: "View Log", + callback: this.on_sidebar_view_log, + classname: 'oe_hide oe_sidebar_view_log' + } + ]); + }, + on_sidebar_manage_view: function() { + if (this.fields_view && this.fields_view.arch) { +// $('<xmp>' + openerp.base.json_node_to_xml(this.fields_view.arch, true) + '</xmp>').dialog({ width: '95%', height: 600}); + var view_editor = new openerp.base.ViewEditor(this, this.$element, this.dataset, this.fields_view.arch) + view_editor.start(); + } else { + this.notification.warn("Manage Views", "Could not find current view declaration"); + } + }, + on_sidebar_edit_workflow: function() { + console.log('Todo'); + }, + on_sidebar_customize_object: function() { + console.log('Todo'); + }, + on_sidebar_import: function() { + }, + on_sidebar_export: function() { + var export_view = new openerp.web.DataExport(this, this.dataset); + export_view.start(); + }, + on_sidebar_translate: function() { + this.open_translate_dialog(); + }, + on_sidebar_view_log: function() { + } +}); + +/** + * Registry for all the main views + */ +openerp.web.views = new openerp.web.Registry(); + +openerp.web.json_node_to_xml = function(node, single_quote, indent) { + // For debugging purpose, this function will convert a json node back to xml + // Maybe usefull for xml view editor + + if (typeof(node) === 'string') { + return node; + } + else if (typeof(node.tag) !== 'string' || !node.children instanceof Array || !node.attrs instanceof Object) { + throw("Node a json node"); + } + indent = indent || 0; + var sindent = new Array(indent + 1).join('\t'), + r = sindent + '<' + node.tag; + for (var attr in node.attrs) { + var vattr = node.attrs[attr]; + if (typeof(vattr) !== 'string') { + // domains, ... + vattr = JSON.stringify(vattr); + } + vattr = vattr.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); + if (single_quote) { + vattr = vattr.replace(/"/g, "'"); + } + r += ' ' + attr + '="' + vattr + '"'; + } + if (node.children && node.children.length) { + r += '>\n'; + var childs = []; + for (var i = 0, ii = node.children.length; i < ii; i++) { + childs.push(openerp.web.json_node_to_xml(node.children[i], single_quote, indent + 1)); + } + r += childs.join('\n'); + r += '\n' + sindent + '</' + node.tag + '>'; + return r; + } else { + return r + '/>'; + } +} + +}; + +// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml new file mode 100644 index 00000000000..e55d56f5912 --- /dev/null +++ b/addons/web/static/src/xml/base.xml @@ -0,0 +1,1418 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- vim:fdl=1: +--> +<templates id="template" xml:space="preserve"> +<t t-name="Interface"> + <div id="oe_loading" class="loading"></div> + <div id="oe_notification" class="oe_notification"> + <div id="oe_notification_default"> + <a class="ui-notify-cross ui-notify-close" href="#">x</a> + <h1>#{title}</h1> + <p>#{text}</p> + </div> + <div id="oe_notification_alert" class="ui-state-error"> + <a class="ui-notify-cross ui-notify-close" href="#">x</a> + <span style="float:left; margin:2px 5px 0 0;" class="ui-icon ui-icon-alert"></span> + <h1>#{title}</h1> + <p>#{text}</p> + </div> + </div> + <table border="0" cellpadding="0" cellspacing="0" width="100%" height="100%" class="main_table"> + <tr> + <td colspan="2" valign="top"> + <div id="oe_header" class="header"></div> + <div id="oe_menu" class="menu"></div> + </td> + </tr> + <tr> + <td valign="top" class="login-container" colspan="2"> + <div id="oe_login" class="login"></div> + </td> + </tr> + <tr class="db_options_row"> + <td valign="top" class="db_container"> + <div id="oe_database" class="database"></div> + </td> + <td valign="top"> + <div id="oe_db_options"></div> + </td> + </tr> + <tr> + <td valign="top" id="oe_secondary_menu" class="secondary_menu"> + </td> + <td valign="top" class="oe-application-container"> + <div id="oe_app" class="oe-application"> + </div> + </td> + </tr> + <tr> + <td colspan="2"> + <div id="oe_footer" class="oe_footer"> + <p class="oe_footer_powered">Powered by <a href="http://www.openerp.com">openerp.com</a>.</p> + </div> + </td> + </tr> + </table> +</t> +<t t-name="Loading"> + Loading... +</t> +<t t-name="Database"> + <ul class="db_options" style="padding: 0px; display: inline;"> + <li id="db-create">Create</li> + <li id="db-drop">Drop</li> + <li id="db-backup">Backup</li> + <li id="db-restore">Restore</li> + <li id="db-change-password">Password</li> + <li id="back-to-login">Back to Login</li> + </ul> +</t> +<t t-name="Database.CreateDB"> + <form name="create_db_form" class="oe_forms" method="POST"> + <table width="100%"> + <tr> + <td class="option_string"> + CREATE DATABASE + </td> + </tr> + </table> + <table align="center" class="db_option_table"> + <tr> + <td><label for="super_admin_pwd">Master password:</label></td> + <td><input type="password" name="super_admin_pwd" class="required" value="admin"/></td> + </tr> + <tr> + <td><label for="db_name">New database name:</label></td> + <td><input type="text" name="db_name" class="required"/></td> + </tr> + <tr> + <td><label for="demo_data">Load Demonstration data:</label></td> + <td><input type="checkbox" name="demo_data"/></td> + </tr> + <tr> + <td><label for="db_lang">Default language:</label></td> + <td> + <select name="db_lang" t-if="lang_list"> + <t t-foreach="lang_list" t-as="lang"> + <option t-att-value="lang[0]" t-att-selected="lang[0] === 'en_US' ? 'selected' : undefined"><t t-esc="lang[1]"/></option> + </t> + </select> + </td> + </tr> + <tr> + <td><label for="create_admin_pwd">Admin password:</label></td> + <td><input type="password" name="create_admin_pwd" class="required"/></td> + </tr> + <tr> + <td><label for="create_confirm_pwd">Confirm password:</label></td> + <td><input type="password" name="create_confirm_pwd" class="required" + equalTo="input[name=create_admin_pwd]"/></td> + </tr> + <tr> + <td colspan="2" align="right"><input type="submit" value="Create"/></td> + </tr> + </table> + </form> +</t> +<t t-name="DropDB"> + <form name="drop_db_form" class="oe_forms" method="POST"> + <table width="100%"> + <tr> + <td class="option_string"> + DROP DATABASE + </td> + </tr> + </table> + <table align="center" class="db_option_table"> + <tr> + <td><label for="drop_db">Database:</label></td> + <td> + <select t-if="db_list" name="drop_db" autofocus="autofocus"> + <t t-foreach="db_list" t-as="db"> + <option t-att-value="db"><t t-esc="db"/></option> + </t> + </select> + </td> + </tr> + <tr> + <td><label for="drop_password">Master Password:</label></td> + <td><input type="password" name="drop_pwd" class="required"/></td> + </tr> + <tr> + <td colspan="2" align="right"><input type="submit" value="Drop"/></td> + </tr> + </table> + </form> +</t> +<t t-name="BackupDB"> + <form name="backup_db_form" class="oe_forms" method="POST" target="backup-target" + action="/web/database/backup"> + <input type="hidden" name="token"/> + <table width="100%"> + <tr> + <td class="option_string"> + BACKUP DATABASE + </td> + </tr> + </table> + <table align="center" class="db_option_table"> + <tr> + <td><label for="backup_db">Database:</label></td> + <td> + <select t-if="db_list" name="backup_db" autofocus="autofocus"> + <t t-foreach="db_list" t-as="db"> + <option t-att-value="db"><t t-esc="db"/></option> + </t> + </select> + </td> + </tr> + <tr> + <td><label for="backup_pwd">Master Password:</label></td> + <td><input type="password" name="backup_pwd" class="required"/></td> + </tr> + <tr> + <td colspan="2" align="right"><input type="submit" value="Backup"/></td> + </tr> + </table> + </form> +</t> +<t t-name="RestoreDB"> + <form name="restore_db_form" class="oe_forms" method="POST"> + <table width="100%"> + <tr> + <td class="option_string"> + RESTORE DATABASE + </td> + </tr> + </table> + <table align="center" class="db_option_table"> + <tr> + <td><label for="restore_db">File:</label></td> + <td><input type="file" name="db_file" class="required" + autofocus="autofocus"/></td> + </tr> + <tr> + <td><label for="restore_pwd">Master Password:</label></td> + <td><input type="password" name="restore_pwd" class="required"/></td> + </tr> + <tr> + <td><label for="new_db">New database name:</label></td> + <td><input type="text" name="new_db" class="required"/></td> + </tr> + <tr> + <td colspan="2" align="right"><input type="submit" value="Restore"/></td> + </tr> + </table> + </form> +</t> +<t t-name="Change_DB_Pwd"> + <form name="change_pwd_form" class="oe_forms" method="POST"> + <table width="100%"> + <tr> + <td class="option_string"> + CHANGE MASTER PASSWORD + </td> + </tr> + </table> + <table align="center" class="db_option_table"> + <tr> + <td><label for="old_pwd">Master password:</label></td> + <td><input type="password" name="old_pwd" class="required" + minlength="1" autofocus="autofocus"/></td> + </tr> + <tr> + <td><label for="new_pwd">New master password:</label></td> + <td><input type="password" name="new_pwd" class="required" + minlength="1"/></td> + </tr> + <tr> + <td><label for="confirm_pwd">Confirm new master password:</label></td> + <td><input type="password" name="confirm_pwd" class="required" + equalTo="input[name=new_pwd]" minlength="1"/></td> + </tr> + <tr> + <td colspan="2" align="right"><input type="submit" value="Change Password"/></td> + </tr> + </table> + </form> +</t> +<t t-name="Login"> + <form class="oe_forms"> + <fieldset> + <legend style=""> + <img src="/web/static/src/img/stock_person.png" alt="" /> + </legend> + <div class="oe_box2"> + <table align="center" cellspacing="2px" cellpadding="0"> + <tr> + <td><label for="db">Database:</label></td> + <td> + <t t-if="!db_list"> + <input type="text" name="db" t-att-value="selected_db || ''" autofocus="true"/> + </t> + <t t-if="db_list"> + <select name="db"> + <t t-foreach="db_list" t-as="db"> + <t t-if="selected_db === db"> + <option t-att-value="db" selected="true"> + <t t-esc="db"/></option> + </t> + <t t-if="selected_db !== db"> + <option t-att-value="db"><t t-esc="db"/></option> + </t> + </t> + </select> + </t> + </td> + </tr> + <tr> + <td><label for="login">User:</label></td> + <td><input type="text" name="login" + t-att-value="selected_login || ''" autofocus="true"/></td> + </tr> + <tr> + <td><label for="password">Password:</label></td> + <td><input type="password" name="password" + t-att-value="selected_password || ''"/></td> + </tr> + <tr> + <td></td> + <td> + <button type="button" id="oe-db-config">Database</button> + <button type="submit" name="submit">Login</button> + </td> + </tr> + </table> + </div> + </fieldset> + <div class="login_error_message">Bad username or password</div> + </form> + <div class="oe_login_right_pane"> + <p>We think that daily job activities can be more intuitive, efficient, automated, .. and even fun.</p> + <h3>OpenERP's vision to be:</h3> + + <table cellpadding="0" cellspacing="0" width="100%" style="border:none;"> + <tbody> + <tr> + <td> + <img src="/web/static/src/img/product.png"/> + </td> + <td> + <strong>Full featured</strong><br /> + Today's enterprise challenges are multiple. We provide one module for each need. + </td> + </tr> + <tr> + <td> + <img src="/web/static/src/img/accessories-archiver.png"/> + </td> + <td> + <strong>Open Source</strong><br /> + To Build a great product, we rely on the knowledge of thousands of contributors. + </td> + </tr> + <tr> + <td> + <img src="/web/static/src/img/partner.png" /> + </td> + <td> + <strong>User Friendly</strong><br /> + In order to be productive, people need clean and easy to use interface. + </td> + </tr> + </tbody> + </table> + + </div> +</t> +<t t-name="Header"> + <div> + <a t-att-href="'/' + widget.qs" class="company_logo_link"> + <div class="company_logo" /> + </a> + </div> +</t> +<t t-name="Header-content"> + <h1 class="header_title"> + <t t-esc="user.company_id[1]"/> (<t t-esc="widget.session.db"/>)<br/> + <small class="username"><t t-esc="user.name"/></small> + </h1> + <div class="header_corner"> + <ul class="block"> + <li> + <a t-att-href="'/' + widget.qs" title="Home" class="home"><img src="/web/static/src/img/header-home.png" width="16" height="16" border="0"/></a> + </li> + <!-- + <li> + <a href="#requests" title="Requests" class="requests"><img src="/web/static/src/img/header-requests.png" width="16" height="16" border="0"/><small>1</small></a> + </li> + --> + <li class="preferences"> + <a href="#preferences" title="Preferences" class="preferences"><img src="/web/static/src/img/header-preferences.png" width="16" height="16" border="0"/></a> + </li> + <li> + <a href="#about" title="About" class="about"><img src="/web/static/src/img/header-about.png" width="16" height="16" border="0"/></a> + </li> + <!-- + <li> + <a href="http://doc.openerp.com/v6.0/book?version=$version" title="Help" target="_blank" class="help"><img src="/web/static/src/img/header-help.png" width="16" height="16" border="0"/></a> + </li> + --> + </ul> + <div class="block"> + <a href="#logout" class="logout">LOGOUT</a> + </div> + + </div> + <div class="oe-shortcuts"> </div> +</t> +<ul t-name="Shortcuts"> + <li t-foreach="shortcuts" t-as="shortcut" + t-att-data-id="shortcut.res_id" + t-att-data-shortcut-id="shortcut.id" + ><t t-esc="shortcut.name"/></li> +</ul> +<t t-name="Menu"> + <table align="center"> + <tr> + <t t-foreach="data.children" t-as="menu"> + <td> + <a href="#" t-att-data-menu="menu.id"> + <t t-esc="menu.name"/> + </a> + </td> + </t> + </tr> + </table> +</t> +<t t-name="Menu.secondary"> + <div style="display: none" class="menu_accordion" t-att-data-menu-parent="menu.id"> + <t t-foreach="menu.children" t-as="menu"> + <t t-set="header">h3</t> + <t t-call="Menu.secondary.children"/> + </t> + </div> +</t> +<t t-name="Menu.secondary.children"> + <<t t-esc="header"/>> + <a href="#" t-attf-id="menu_#{menu.id}"> + <span><t t-esc="menu.name"/></span> + </a> + </<t t-esc="header"/>> + <div class="menu_content"> + <t t-foreach="menu.children" t-as="menu"> + <t t-if="!menu.children.length"> + <a href="#" class="leaf" t-att-data-menu="menu.id"> + <span><t t-esc="menu.name"/></span> + </a> + </t> + <t t-if="menu.children.length"> + <div class="submenu_accordion"> + <t t-call="Menu.secondary.children"> + <t t-set="header">h4</t> + </t> + </div> + </t> + </t> + </div> +</t> +<t t-name="ViewManager"> + <table class="view-manager-main-table"> + <tr> + <td class="view-manager-main-content"> + <div class="oe_vm_switch"> + <t t-if="views.length != 1" t-foreach="views" t-as="view"> + <button type="button" t-att-data-view-type="view.view_type"> + <t t-esc="view.view_type"/> + </button> + </t> + </div> + <div t-attf-id="#{prefix}_search" t-opentag="true"/> + <t t-foreach="views" t-as="view"> + <div t-attf-id="#{prefix}_view_#{view.view_type}"/> + </t> + </td> + <td class="view-manager-main-sidebar" height="100%"> + <t t-foreach="views" t-as="view"> + <div t-attf-id="#{prefix}_sidebar_#{view.view_type}" class="sidebar-main-div closed-sidebar" style="display: none"/> + </t> + </td> + </tr> + </table> +</t> + +<t t-extend="ViewManager" t-name="ViewManagerAction"> + <t t-jquery=".view-manager-main-content" t-operation="prepend"> + <a class="oe-shortcut-toggle" title="Add / Remove Shortcut..." + href="javascript: void(0)"> </a> + </t> +</t> + +<t t-name="Sidebar"> + <a class="toggle-sidebar"></a> + <div class="sidebar-content"> + <div class="sidebar-actions"> + </div> + </div> +</t> +<t t-name="Sidebar.section"> + <h2><t t-esc="name"/></h2> + <div t-att-id="section_id" t-att-class="classname"> + <ul t-if="items"> + <li t-foreach="items" t-as="item" t-att-class="item.classname"> + <a class="oe_sidebar_action_a" t-att-id="item.element_id" t-att-title="item.title" href="#"> + <t t-esc="item.label"/> + </a> + </li> + </ul> + </div> +</t> +<t t-name="TranslateDialog"> + <ul class="oe_translate_tabs"> + <li><a t-attf-href="##{widget.element_id}_fields">Fields</a></li> + <li><a t-attf-href="##{widget.element_id}_view">View labels</a></li> + <li><a t-attf-href="##{widget.element_id}_sidebar">Sidebar Relates</a></li> + </ul> + <div t-attf-id="#{widget.element_id}_fields"> + <table t-if="widget.view.translatable_fields" class="oe_frame oe_forms oe_translation_form" border="0" cellpadding="0" cellspacing="0" width="100%"> + <tr> + <td class="oe_form_separator" width="1%" nowrap="nowrap"> + <div class="separator horizontal">Field</div> + </td> + <th t-foreach="widget.languages" align="left"> + <div class="separator horizontal"><t t-esc="name"/></div> + </th> + </tr> + <tr t-foreach="widget.view.translatable_fields" t-as="field"> + <td class="oe_form_frame_cell" width="1%" nowrap="nowrap"> + <label class="oe_label"><t t-esc="field.string"/>:</label> + </td> + <td t-foreach="widget.languages" t-as="lg" class="oe_form_frame_cell"> + <input t-if="field.type == 'char'" type="text" t-attf-name="#{lg.code}-#{field.name}" value="" data-value="" class="oe_trad_field" style="width: 100%"/> + <textarea t-if="field.type == 'text'" t-attf-name="#{lg.code}-#{field.name}" data-value="" class="oe_trad_field" style="width: 100%"></textarea> + </td> + </tr> + </table> + </div> + <div t-attf-id="#{widget.element_id}_view"> + Translate view + </div> + <div t-attf-id="#{widget.element_id}_sidebar"> + Translate sidebar + </div> +</t> +<t t-name="TreeView"> + <h2 class="oe_view_title"><t t-esc="title"/></h2> + <select t-if="toolbar" style="width: 30%"> + </select> + <table class="oe-treeview-table"> + <thead> + <tr> + <th t-foreach="fields_view" t-as="field" + t-if="!field.attrs.modifiers.tree_invisible" + class="treeview-header"> + <t t-esc="fields[field.attrs.name].string" /> + </th> + </tr> + </thead> + <tbody> + </tbody> + </table> +</t> +<tr t-name="TreeView.rows" + t-foreach="records" t-as="record" + t-att-id="'treerow_' + record.id" + t-att-data-id="record.id" t-att-data-level="level + 1"> + <t t-set="children" t-value="record[children_field]"/> + <t t-set="has_children" t-value="children and children.length"/> + + <td t-foreach="fields_view" t-as="field" + t-if="!field.attrs.modifiers.tree_invisible" + t-att-data-id="record.id" + t-att-style="!field_index ? 'background-position: ' + 19*level + 'px; padding-left: ' + 19*level + 'px' : undefined" + t-att-class="!field_index and has_children ? 'treeview-tr' : 'treeview-td'"> + <span t-if="!field.attrs.modifiers.invisible"> + <t t-esc="render(record[field.attrs.name], fields[field.attrs.name])" /> + </span> + </td> +</tr> +<table t-name="ListView" class="oe-listview-content"> + <t t-set="columns_count" t-value="visible_columns.length + (options.selectable ? 1 : 0) + (options.deletable ? 1 : 0)"/> + <thead class="ui-widget-header"> + <tr t-if="options.action_buttons !== false or options.pager !== false"> + <th t-att-colspan="columns_count"> + <table> + <tr> + <td t-if="options.action_buttons !== false" class="oe-actions"> + <button type="button" class="oe-list-add" + t-if="options.addable"> + <t t-esc="options.addable"/> + </button> + <button type="button" class="oe-list-delete" + t-if="options.selectable and options.deletable"> + Delete + </button> + </td> + <th t-if="options.pager !== false" class="oe-list-pager"> + <button type="button" disabled="disabled" + data-pager-action="first">First</button> + <button type="button" disabled="disabled" + data-pager-action="previous" + ><</button> + + <span class="oe-pager-state"> + </span> + + <button type="button" disabled="disabled" + data-pager-action="next">></button> + <button type="button" disabled="disabled" + data-pager-action="last">Last</button> + </th> + </tr> + </table> + </th> + </tr> + <tr t-if="options.header" class="oe-listview-header-columns"> + <t t-foreach="columns" t-as="column"> + <th t-if="column.meta"> + <t t-esc="column.string"/> + </th> + </t> + <th t-if="options.selectable" width="1"/> + <t t-foreach="columns" t-as="column"> + <th t-if="!column.meta and column.invisible !== '1'" t-att-data-id="column.id" + t-att-class="((options.sortable and column.tag !== 'button') ? 'oe-sortable' : null)"> + <t t-if="column.tag !== 'button'" + ><t t-esc="column.string"/></t> + </th> + </t> + <th t-if="options.deletable" width="1"/> + </tr> + </thead> + <tfoot class="ui-widget-header"> + <tr> + <td t-if="options.selectable"/> + <td t-foreach="aggregate_columns" t-as="column" class="oe-list-footer oe-number" + t-att-data-field="column.id" t-att-title="column.label"> + </td> + <td t-if="options.deletable"/> + </tr> + </tfoot> +</table> +<t t-name="ListView.rows" t-foreach="records.length" t-as="index"> + <t t-call="ListView.row"> + <t t-set="record" t-value="records.at(index)"/> + <t t-set="row_parity" t-value="index_parity"/> + </t> +</t> +<tr t-name="ListView.row" t-att-class="row_parity" + t-att-data-id="record.get('id')"> + <t t-foreach="columns" t-as="column"> + <td t-if="column.meta"> + + </td> + </t> + <t t-if="options.radio"> + <th t-if="options.selectable" class="oe-record-selector" width="1"> + <input type="radio" name ="radiogroup"/> + </th> + </t> + <t t-if="!options.radio"> + <th t-if="options.selectable" class="oe-record-selector" width="1"> + <input type="checkbox"/> + </th> + </t> + + + <t t-foreach="columns" t-as="column"> + <t t-set="align" t-value="column.type === 'integer' or column.type == 'float'"/> + <td t-if="!column.meta and column.invisible !== '1'" t-att-title="column.help" + t-att-class="'oe-field-cell' + (align ? ' oe-number' : '')" + t-att-data-field="column.id"> + <t t-raw="render_cell(record.toForm().data, column)"/> + </td> + </t> + <td t-if="options.deletable" class='oe-record-delete' width="1"> + <button type="button" name="delete">♻</button> + </td> +</tr> +<t t-name="ListView.row.form"> + <t t-raw="frame.render()"/> +</t> +<t t-name="FormView"> + <div class="oe_form_header" t-att-id="view.element_id + '_header'"> + <h2 class="oe_view_title"><t t-esc="view.fields_view.arch.attrs.string"/> <button t-if="view.session.debug" class="oe_get_xml_view">View#<t t-esc="view.fields_view.view_id"/></button></h2> + <div class="oe_form_buttons" t-if="view.options.action_buttons !== false"> + <!--<button type="button" class="oe_form_button_save"> + <span class="oe_form_on_update">Save</span> + <span class="oe_form_on_create">Create</span> + </button>--> + <button type="button" class="oe_form_button_save_edit"> + <span class="oe_form_on_update">Save & Edit</span> + <span class="oe_form_on_create">Create & Edit</span> + </button> + <!--<button type="button" class="oe_form_button_cancel">Cancel</button>--> + <button type="button" class="oe_form_button_new">New</button> + </div> + <div class="oe_form_pager" t-if="view.options.pager !== false"> + <button type="button" data-pager-action="first">First</button> + <button type="button" data-pager-action="previous"><<</button> + + <span class="oe_pager_index">0</span> / <span class="oe_pager_count">0</span> + + <button type="button" data-pager-action="next">>></button> + <button type="button" data-pager-action="last">Last</button> + </div> + </div> + <t t-raw="frame.render()"/> +</t> +<t t-name="FormView.sidebar.attachments"> + <div class="oe-sidebar-attachments-toolbar"> + <div class="oe-binary-file-set" style="float: right"> + <form class="oe-binary-form" t-attf-target="#{element_id}_iframe" + method="post" enctype="multipart/form-data" action="/web/binary/upload_attachment"> + <input type="hidden" name="session_id" t-att-value="session.session_id"/> + <input type="hidden" name="callback" t-attf-value="#{element_id}_iframe"/> + <input type="hidden" name="model" t-att-value="view.dataset.model"/> + <input type="hidden" name="id" t-att-value="view.datarecord.id"/> + <button class="button" type="button"> + <img src="/web/static/src/img/throbber.gif" width="16" height="16" style="display: none"/> + <span>Add</span> + </button> + <input type="file" class="oe-binary-file" name="ufile" title="Add attachment" + t-att-onclick="view.datarecord.id ? null : 'alert(\'No record selected ! You can only attach to existing record.\'); return false;'"/> + </form> + <iframe t-attf-id="#{element_id}_iframe" t-attf-name="#{element_id}_iframe" style="display: none"> </iframe> + </div> + </div> + <br style="clear: both"/> + <ul class="oe-sidebar-attachments-items"> + <li t-foreach="attachments" t-as="attachment"> + <t t-if="attachment.type == 'binary'" t-set="attachment.url" t-value="'/web/binary/saveas?session_id=' + + session.session_id + '&model=ir.attachment&id=' + attachment.id + + '&field=datas&fieldname=name&t=' + (new Date().getTime())"/> + <a class="oe-sidebar-attachments-link" t-att-href="attachment.url" target="_blank"> + <t t-esc="attachment.name"/> + </a> + <a href="#" class="oe-sidebar-attachment-delete" t-att-data-id="attachment.id" t-attf-title="Delete the attachment #{attachment.name}"> + <img src="/web/static/src/img/attachments-close.png" width="15" height="15" border="0"/> + </a> + </li> + </ul> +</t> +<t t-name="Widget"> + Unhandled widget + <t t-raw="console.log('Unhandled widget', widget)"/> +</t> +<t t-name="WidgetFrame"> + <table border="0" width="100%" cellpadding="0" cellspacing="0" class="oe_frame oe_forms"> + <tr t-foreach="widget.table" t-as="row"> + <t t-foreach="row" t-as="td"> + <td t-att-colspan="td.colspan gt 1 ? td.colspan : undefined" + t-att-width="td.width" + t-att-nowrap="td.nowrap or td.is_field_m2o? 'true' : undefined" + t-att-valign="td.table ? 'top' : undefined" + t-att-id="td.element_id" + t-attf-class="oe_form_frame_cell #{td.classname}" + > + <t t-raw="td.render()"/> + </td> + </t> + </tr> + </table> +</t> +<t t-name="WidgetNotebook"> + <ul> + <li t-foreach="widget.pages" t-as="page" t-att-id="page.element_tab_id"> + <a t-att-href="'#' + page.element_id"> + <t t-esc="page.string"/> + </a> + </li> + </ul> + <t t-foreach="widget.pages" t-as="page"> + <t t-raw="page.render()"/> + </t> +</t> +<t t-name="WidgetNotebookPage"> + <div t-att-id="widget.element_id"> + <t t-call="WidgetFrame"/> + </div> +</t> +<t t-name="WidgetSeparator"> + <div t-if="widget.orientation !== 'vertical'" t-att-class="'separator ' + widget.orientation"> + <t t-esc="widget.string"/> + </div> +</t> +<t t-name="WidgetLabel"> + <label t-att-for="widget.element_id + '_field'" + t-att-class="'oe_label' + (widget.help ? '_help' : '')" + t-att-title="widget.help"> + <t t-esc="widget.string"/> + <span t-if="widget.help">?</span> + <t t-if="widget.string and widget.node.tag != 'label'">:</t> + </label> +</t> +<t t-name="WidgetParagraph"> + <p class="oe_form_paragraph"><t t-esc="widget.string"/></p> +</t> +<t t-name="FieldChar"> + <input type="text" size="1" + t-att-name="widget.name" + t-att-id="widget.element_id + '_field'" + t-att-class="'field_' + widget.type" + t-attf-style="width: #{widget.field.translate ? '99' : '100'}%" + /> + <img class="oe_field_translate" t-if="widget.field.translate" src="/web/static/src/img/icons/terp-translate.png" width="16" height="16" border="0"/> +</t> +<t t-name="FieldEmail"> + <table cellpadding="0" cellspacing="0" border="0" width="100%"> + <tr> + <td width="100%"> + <t t-call="FieldChar"/> + </td> + <td width="16"> + <button type="button" class="button" title="Send an e-mail with your default e-mail client"> + <img src="/web/static/src/img/icons/terp-mail-message-new.png"/> + </button> + </td> + </tr> + </table> +</t> +<t t-name="FieldUrl"> + <table cellpadding="0" cellspacing="0" border="0" width="100%"> + <tr> + <td width="100%"> + <t t-call="FieldChar"/> + </td> + <td width="16"> + <button type="button" class="button" title="Open this resource"> + <img src="/web/static/src/img/icons/gtk-ok.png"/> + </button> + </td> + </tr> + </table> +</t> +<t t-name="FieldText"> + <textarea rows="6" + t-att-name="widget.name" + t-att-id="widget.element_id + '_field'" + t-att-class="'field_' + widget.type" + t-attf-style="width: #{widget.field.translate ? '99' : '100'}%" + ></textarea> + <img class="oe_field_translate" t-if="widget.field.translate" src="/web/static/src/img/icons/terp-translate.png" width="16" height="16" border="0"/> +</t> +<t t-name="FieldDate"> + <input type="text" size="1" style="width: 100%" + t-att-name="widget.name" + t-att-id="widget.element_id + '_field'" + t-att-class="'field_' + widget.type" + /> +</t> +<t t-name="FieldSelection"> + <select + t-att-name="widget.name" + t-att-id="widget.element_id + '_field'" + t-att-class="'field_' + widget.type" + style="width: 100%"> + <t t-foreach="widget.values" t-as="option"> + <option><t t-esc="option[1]"/></option> + </t> + </select> +</t> +<t t-name="FieldMany2One"> + <div t-att-id="widget.element_id" class="oe-m2o"> + <input t-att-id="widget.element_id + '_input'" type="text" size="1" style="width: 100%;"/> + <span class="oe-m2o-drop-down-button" t-att-id="widget.element_id + '_drop_down'"> + <img src="/web/static/src/img/down-arrow.png" /></span> + <span class="oe-m2o-cm-button" t-att-id="widget.name + '_open'"> + <img src="/web/static/src/img/icons/gtk-index.png"/></span> + <div t-att-id="widget.cm_id" class="contextMenu" style="display:none"> + </div> + </div> +</t> +<t t-name="FieldMany2One.context_menu"> + <ul> + <li t-att-id="widget.cm_id + '_open'" style="color:grey">Open...</li> + <li t-att-id="widget.cm_id + '_create'">Create...</li> + <li t-att-id="widget.cm_id + '_search'" class="oe_m2o_menu_item_mandatory">Search...</li> + <t t-set="i" t-value="0"/> + <t t-foreach="widget.related_entries" t-as="entry"> + <li t-att-id="widget.cm_id + '_related_' + i" style="color:grey" class="oe_m2o_menu_item_mandatory"> + ... <t t-esc="(entry[2] || {})['name'] || ''"/> + </li> + <t t-set="i" t-value="i+1"/> + </t> + </ul> +</t> +<t t-name="FieldOne2Many"> + <div t-att-id="widget.element_id"> + </div> +</t> +<t t-name="FieldMany2Many"> + <div t-att-id="widget.list_id"></div> +</t> +<t t-name="FieldReference"> + <table border="0" width="100%" cellpadding="0" cellspacing="0" class="oe_frame oe_forms"> + <tr> + <td t-att-id="widget.selection.element_id" class="oe_form_frame_cell oe_form_selection"> + <t t-raw="widget.selection.render()"/> + </td> + <td t-att-id="widget.m2o.element_id" class="oe_form_frame_cell oe_form_many2one" nowrap="true"> + <t t-raw="widget.m2o.render()"/> + </td> + </tr> + </table> +</t> +<t t-name="FieldBoolean"> + <input type="checkbox" + t-att-name="widget.name" + t-att-id="widget.element_id + '_field'" + t-att-class="'field_' + widget.type"/> +</t> +<t t-name="FieldProgressBar"> + <div t-opentag="true" class="oe-progressbar"> + <span></span> + </div> +</t> +<t t-name="FieldBinaryImage"> + <table cellpadding="0" cellspacing="0" border="0"> + <tr> + <td align="center"> + <img src="/web/static/src/img/placeholder.png" class="oe-binary-image" + t-att-border="widget.readonly ? 0 : 1" + t-att-id="widget.element_id + '_field'" + t-att-name="widget.name" + t-att-class="'field_' + widget.type" + t-att-width="widget.node.attrs.img_width || widget.node.attrs.width" + t-att-height="widget.node.attrs.img_height || widget.node.attrs.height" + /> + </td> + </tr> + <tr> + <td align="center" valign="bottom" height="25"> + <div class="oe-binary"> + <table cellspacing="0" cellpadding="0" border="0"> + <tr> + <td> + <div class="oe-binary-file-set" style="width: 40px; height:22px;"> + <form class="oe-binary-form" t-att-target="widget.iframe" + method="post" enctype="multipart/form-data" action="/web/binary/upload"> + <input type="hidden" name="session_id" value=""/> + <input type="hidden" name="callback" t-att-value="widget.iframe"/> + <button class="button" type="button" title="Set Image"> + <img src="/web/static/src/img/icons/STOCK_DIRECTORY.png"/> + </button> + <input type="file" class="oe-binary-file" name="ufile"/> + </form> + </div> + </td> + <td> + <button class="button oe-binary-file-clear" type="button" title="Clear"> + <img src="/web/static/src/img/icons/STOCK_MISSING_IMAGE.png"/> + </button> + </td> + </tr> + </table> + </div> + <div class="oe-binary-progress" style="display: none"> + <img src="/web/static/src/img/throbber.gif" width="16" height="16"/> + <b>Uploading ...</b> + </div> + <iframe t-att-id="widget.iframe" t-att-name="widget.iframe" style="display: none"> </iframe> + </td> + </tr> + </table> +</t> +<t t-name="FieldBinaryFile"> + <table cellpadding="0" cellspacing="0" border="0" width="100%"> + <tr> + <td width="100%"> + <input type="text" size="1" + t-att-name="widget.name" + t-att-id="widget.element_id + '_field'" + t-att-class="'field_' + widget.type" style="width: 100%" + /> + </td> + <td class="oe-binary" nowrap="true"> + <table cellspacing="0" cellpadding="0" border="0"> + <tr> + <td> + <div class="oe-binary-file-set" style="width: 80px; height:22px;"> + <form class="oe-binary-form" t-att-target="widget.iframe" + method="post" enctype="multipart/form-data" action="/web/binary/upload"> + <input type="hidden" name="session_id" value=""/> + <input type="hidden" name="callback" t-att-value="widget.iframe"/> + <button class="button" type="button" title="Set Image"> + <img src="/web/static/src/img/icons/STOCK_DIRECTORY.png"/> + <span>Select</span> + </button> + <input type="file" class="oe-binary-file" name="ufile"/> + </form> + </div> + </td> + <td> + <button class="button oe-binary-file-save" type="button" title="Save As"> + <img src="/web/static/src/img/icons/gtk-save.png"/> + <span>Save As</span> + </button> + </td> + <td> + <button class="button oe-binary-file-clear" type="button" title="Clear"> + <img src="/web/static/src/img/icons/STOCK_MISSING_IMAGE.png"/> + <span>Clear</span> + </button> + </td> + </tr> + </table> + </td> + <td class="oe-binary-progress" style="display: none" nowrap="true"> + <img src="/web/static/src/img/throbber.gif" width="16" height="16"/> + <b>Uploading ...</b> + <iframe t-att-id="widget.iframe" t-att-name="widget.iframe" style="display: none"> </iframe> + </td> + </tr> + </table> +</t> +<t t-name="WidgetButton"> + <button type="button" + t-att-id="widget.element_id + '_button'" + t-att-title="widget.help" + style="width: 100%" class="button"> + <img t-if="widget.node.attrs.icon" t-att-src="'/web/static/src/img/icons/' + widget.node.attrs.icon + '.png'" width="16" height="16"/> + <span t-if="widget.string"><t t-esc="widget.string"/></span> + </button> +</t> +<t t-name="SearchView"> + <h2 class="oe_view_title"><t t-esc="view.attrs['string']"/></h2> + <form class="oe_forms"> + <t t-call="SearchView.render_lines"/> + <div class="oe_search-view-buttons" style="text-align: right;"> + <input type="submit" value="Search"/> + <input type="reset" value="Clear"/> + <button class="oe_search-view-custom-filter-btn"><span>Advanced Filter</span></button> + <select class="oe_search-view-filters-management"> + </select> + </div> + </form> +</t> +<t t-name="SearchView.managed-filters"> + <option value="_filters">-- Filters --</option> + <t t-set="i" t-value="0"/> + <t t-foreach="filters" t-as="filter"> + <option t-att-value="'get:' + i"><t t-esc="filter.name"/></option> + <t t-set="i" t-value="i+1"/> + </t> + <option value="_actions">-- Actions --</option> + <option value="save_filter">Save Filter</option> + <option value="manage_filters">Manage Filters</option> +</t> +<t t-name="SearchView.managed-filters.add"> + <div> + <p>Filter Name:</p> + <input type="text"/> + <p>(Any existing filter with the same name will be replaced)</p> + </div> +</t> +<t t-name="SearchView.render_lines"> + <table class="oe-searchview-render-line" border="0" cellspacing="0" cellpadding="0" + t-foreach="lines" t-as="line"> + <tr> + <td t-foreach="line" t-as="widget"> + <t t-raw="widget.render(defaults)"/> + </td> + </tr> + </table> +</t> +<button t-name="SearchView.filter" type="button" + t-att-id="element_id" + t-att-title="attrs.help" + t-att-class="classes.join(' ')" + t-att-autofocus="attrs.default_focus === '1' ? 'autofocus' : undefined"> + <img t-if="attrs.icon" t-att-src="'/web/static/src/img/icons/' + attrs.icon + '.png'" width="16" height="16"/> + <br t-if="attrs.icon and attrs.string"/> + <t t-esc="attrs.string"/> +</button> +<span t-name="SearchView.filters" class="filter_label_group" + ><t t-foreach="filters" t-as="filter" + ><t t-raw="filter.render(defaults)"/></t +></span> +<t t-name="SearchView.field"> + <label t-att-class="'oe_label' + (attrs.help ? '_help' : '')" + t-att-title="attrs.help" + t-att-for="element_id"> + <t t-esc="attrs.string || attrs.name"/> + <span t-if="attrs.help">?</span> + </label> + <div style="white-space: nowrap;"> + <input type="text" size="15" t-att-name="attrs.name" + t-att-autofocus="attrs.default_focus === '1' ? 'autofocus' : undefined" + t-att-id="element_id" + t-att-value="defaults[attrs.name] || ''"/> + <t t-if="filters.length" t-raw="filters.render(defaults)"/> + </div> +</t> +<t t-name="SearchView.field.selection"> + <label t-att-title="attrs.help" + t-att-class="'oe_label' + (attrs.help ? '_help' : '')" + t-att-for="element_id"> + <t t-esc="attrs.string || attrs.name"/> + <span t-if="attrs.help">?</span> + </label> + <div style="white-space: nowrap;"> + <select t-att-name="attrs.name" t-att-id="element_id" + t-att-autofocus="attrs.default_focus === '1' || undefined"> + <option/> + <t t-foreach="attrs.selection" t-as="option"> + <t t-set="selected" t-value="defaults[attrs.name] === option[0]"/> + <option t-if="selected" + t-att-value="option[0]" selected="selected"> + <t t-esc="option[1]"/> + </option> + <option t-if="!selected" t-att-value="option[0]"> + <t t-esc="option[1]"/> + </option> + </t> + </select> + <t t-if="filters.length" t-raw="filters.render(defaults)"/> + </div> +</t> +<t t-name="SearchView.util.expand"> + <div t-att-class="'searchview_group ' + (expand == '0' ? 'folded' : 'expanded')" + t-att-id="element_id"> + <a t-if="label" class="searchview_group_string" href="#"> + <t t-esc="label"/> + </a> + <div class="searchview_group_content"> + <t t-raw="content"/> + </div> + </div> +</t> +<t t-name="SearchView.group"> + <t t-call="SearchView.util.expand"> + <t t-set="expand" t-value="attrs.expand"/> + <t t-set="label" t-value="attrs.string"/> + <t t-set="content"> + <t t-call="SearchView.render_lines"/> + </t> + </t> +</t> +<t t-name="SearchView.extended_search"> + <t t-call="SearchView.util.expand"> + <t t-set="expand" t-value="false"/> + <t t-set="label" t-value="'Advanced Filters'"/> + <t t-set="content"> + <div class="searchview_extended_groups_list"> + </div> + </t> + </t> +</t> +<t t-name="SearchView.extended_search.group"> + <div t-att-id="element_id" class="searchview_extended_group"> + <select class="searchview_extended_group_choice"> + <option value="any">Any of the following conditions must match</option> + <option value="all">All the following conditions must match</option> + <option value="none">None of the following conditions must match</option> + </select> + <a class="searchview_extended_delete_group" + href="javascript:void(0)"><span></span></a> + <div class="searchview_extended_propositions_list"> + </div> + <a class="searchview_extended_add_proposition" href="javascript:void(0)"> + <span>Add condition</span></a> + <div class="oe_adv_filters_and"><span>and</span></div> + </div> +</t> +<t t-name="SearchView.extended_search.proposition"> + <div t-att-id="element_id"> + <select class="searchview_extended_prop_field"> + <t t-foreach="attrs.fields" t-as="field"> + <option t-if="typeof field.store === 'undefined' || field.store || field.fnct_search" + t-att="{'selected': field === attrs.selected ? 'selected' : null}" + t-att-value="field.name"> + <t t-esc="field.string"/> + </option> + </t> + </select> + <select class="searchview_extended_prop_op"/> + <span class="searchview_extended_prop_value"/> + <a class="searchview_extended_delete_prop" + href="javascript:void(0)"><span> </span></a> + </div> +</t> +<t t-name="SearchView.extended_search.proposition.char"> + <input t-att-id="element_id" class="field_char"/> +</t> +<t t-name="SearchView.extended_search.proposition.datetime"> + <input t-att-id="element_id" class="field_datetime"/> +</t> +<t t-name="SearchView.extended_search.proposition.date"> + <input t-att-id="element_id" class="field_date"/> +</t> +<t t-name="SearchView.extended_search.proposition.integer"> + <input type="number" t-att-id="element_id" class="field_integer" step="1"/> +</t> +<t t-name="SearchView.extended_search.proposition.float"> + <input type="number" t-att-id="element_id" class="field_float" step="0.01"/> +</t> +<t t-name="SearchView.extended_search.proposition.boolean"> +</t> +<t t-name="SearchView.extended_search.proposition.selection"> + <select t-att-id="element_id"> + <t t-foreach="field.selection" t-as="element"> + <option t-att-value="element[0]"><t t-esc="element[1]"/></option> + </t> + </select> +</t> +<t t-name="DialogWarning"> + <table cellspacing="0" cellpadding="0" border="0" class="oe-dialog-warning"> + <tr> + <td><img src="/web/static/src/img/warning.png" class="oe-dialog-icon"/></td> + <td> + <p> + <t t-js="d"> + var message = d.message ? d.message : d.error.data.fault_code; + d.html_error = context.engine.tools.html_escape(message) + .replace(/\n/g, '<br/>'); + </t> + <t t-raw="html_error"/> + </p> + </td> + </tr> + </table> +</t> +<t t-name="DialogTraceback"> + <pre><t t-esc="error.message"/></pre> + <hr/> + <pre><t t-esc="error.data.debug"/></pre> +</t> +<t t-name="SelectCreatePopup"> + <div t-att-id="element_id"> + <table style="width:100%"> + <tr style="width:100%"> + <td style="width:100%"> + <div t-att-id="element_id + '_search'" style="width:100%"></div> + </td> + </tr> + <tr style="width:100%"> + <td style="width:100%"> + <div t-att-id="element_id + '_view_list'" style="width:100%"></div> + </td> + </tr> + </table> + <div t-att-id="element_id + '_view_form'" style="width:100%"></div> + </div> +</t> +<t t-name="SelectCreatePopup.search.buttons"> + <button type="button" class="oe_selectcreatepopup-search-select" disabled="disabled">Select</button> + <button type="button" class="oe_selectcreatepopup-search-close">Cancel</button> +</t> +<t t-name="SelectCreatePopup.form.buttons"> + <t t-if="widget.options.disable_multiple_selection"> + <button type="button" class="oe_selectcreatepopup-form-save">Save</button> + </t> + <t t-if="! widget.options.disable_multiple_selection"> + <button type="button" class="oe_selectcreatepopup-form-save-new">Save & New</button> + <button type="button" class="oe_selectcreatepopup-form-save">Save & Close</button> + </t> + <button type="button" class="oe_selectcreatepopup-form-close">Cancel</button> +</t> +<t t-name="FormOpenPopup"> + <div t-att-id="element_id"> + <div t-att-id="element_id + '_view_form'" style="width:100%"></div> + </div> +</t> +<t t-name="FormOpenPopup.form.buttons"> + <button type="button" class="oe_formopenpopup-form-save">Save</button> + <button type="button" class="oe_formopenpopup-form-close">Cancel</button> +</t> +<t t-name="ListView.row.frame" t-extend="WidgetFrame"> + <t t-jquery="tr"> + $(document.createElement('t')) + .append(this.contents()) + .attr({ + 't-foreach': this.attr('t-foreach'), + 't-as': this.attr('t-as') + }) + .replaceAll(this) + .after($(document.createElement('td')).append( + $(document.createElement('button')).attr({ + 'class': 'oe-edit-row-save', 'type': 'button'}).text('Save'))) + .before($(document.createElement('td')).append( + $(document.createElement('button')).attr({ + 'class': 'oe-edit-row-cancel', 'type': 'button'}).text('Cancel'))) + .unwrap(); + </t> +</t> + +<t t-name="ExportView"> + <a id="exportview" href="javascript: void(0)" style="text-decoration: none;color: #3D3D3D;">Export</a> +</t> +<table t-name="ExportTreeView" class="oe-export" + style="background-color: #F3F3F3;"> + <tr> + <td colspan="3"> + This wizard will export all data that matches the current search criteria to a CSV file. + You can export all data or only the fields that can be reimported after modification. + </td> + </tr> + <tr> + <td colspan="3"> + <label for="import_compat">Export Type:</label> + <select id="import_compat" name="import_compat"> + <option value="yes">Import Compatible Export</option> + <option value="">Export all Data</option> + </select> + + <label for="export_format">Export Formats</label> + <select id="export_format" name="export_format"></select> + </td> + </tr> + + <tr> + <th>Available fields</th> + <th/> + <th> + Fields to export + <a style="color: blue; text-decoration: none;" href="#" id="export_new_list">Save fields list</a> + <div id="savenewlist"></div> + <div id="ExistsExportList"></div> + </th> + </tr> + <tr style="height: 400px;"> + <td class="oe_export_fields_selector_left"> + <div id="left_field_panel"> + </div> + </td> + <td class="oe_export_fields_selector_center"> + <button id="add_field">Add</button> + <button id="remove_field">Remove</button> + <button id="remove_all_field">Remove All</button> + </td> + <td class="oe_export_fields_selector_right"> + <select name="fields_list" id="fields_list" + multiple="multiple"></select> + </td> + </tr> +</table> + +<table t-name="ExportTreeView-Secondary" + id="field-tree-structure" class="oe_export_fields_selector_export" + cellspacing="0" cellpadding="0"> + <tr><th class="oe_export_tree_header"> Name </th></tr> + <t t-call="ExportTreeView-Secondary.children"/> +</table> +<tr t-name="ExportTreeView-Secondary.children" + t-foreach="fields" t-as="field" + t-att-id="'treerow-' + field.id" class="oe_export_row"> + <td> + <table class="tree_grid" border="0"> + <tr class="oe_export_row"> + <t t-foreach="(field.id).split('/')" t-as="level" > + <t t-if="(field.id).split('/')[0] != level"> + <td width="18">&nbsp;</td> + </t> + </t> + <td valign="top" align="left" style="cursor: pointer;" width="18"> + <t t-if="field.children"> + <t t-if="(field.id).split('/').length != 3"> + <img t-att-id="'parentimg-' + field.id" src="/web/static/src/img/expand.gif" width="16" height="16" border="0"/> + </t> + </t> + </td> + <td id="tree-column" valign="middle" align="left" style="cursor: pointer;"> + <a t-att-id="'export-' + field.id" t-att-string="field.string" href="javascript: void(0);" style="text-decoration: none;"> + <t t-esc="field.string"/> + </a> + </td> + </tr> + </table> + </td> +</tr> + +<t t-name="ExportNewList"> + <tr> + <th><label>Save as:</label></th> + <td><input size="10" type="text" id="savelist_name"/></td> + <td><button class="oe_export_button_export" id="add_export_list">Ok</button></td> + </tr> +</t> + +<t t-name="Exists.ExportList"> + <label for="saved_export_list">Saved exports:</label> + + <select id="saved_export_list"> + <option></option> + <t t-foreach="existing_exports" t-as="export"> + <option t-att-value="export.id"><t t-esc="export.name"/></option> + </t> + </select> + <button class="oe_export_button_export" id="delete_export_list" type="button">Delete</button> +</t> +<t t-name="Change_Pwd"> + <form name="change_password_form" class="oe_forms" method="POST"> + <table align="center"> + <tr> + <td><label for="old_pwd">Old Password:</label></td> + <td><input type="password" name="old_pwd" + minlength="1" autofocus="autofocus"/></td> + </tr> + <tr> + <td><label for="new_password">New Password:</label></td> + <td><input type="password" name="new_password" + minlength="1" autofocus="autofocus"/></td> + </tr> + <tr> + <td><label for="confirm_pwd">Confirm Password:</label></td> + <td><input type="password" name="confirm_pwd" + minlength="1"/></td> + </tr> + <tr> + <td colspan="2" align="right"><input type="submit" value="Change Password"/></td> + </tr> + </table> + </form> +</t> +<t t-name="About-Page"> + <div> + <h1>OpenERP Web</h1> + <h3 style="padding:0 5px 5px">Version <t t-esc="version_info.version"/></h3> + <p> + Copyright © 2011-TODAY OpenERP SA. All Rights Reserved.<br /> + OpenERP is a trademark of the <a target="_blank" href="http://openerp.com/" style="text-decoration: underline;">OpenERP SA Company</a>. + </p> + <p> + Licenced under the terms of <a target="_blank" href="http://www.gnu.org/licenses/agpl.html" style="text-decoration: underline;">GNU Affero General Public License</a> + </p> + <br /> + <h1>About OpenERP</h1> + <p> + <a target="_blank" href="http://openerp.com/" style="text-decoration: underline;">OpenERP</a> is a free enterprise-scale software system that is designed to boost + productivity and profit through data integration. It connects, improves and + manages business processes in areas such as sales, finance, supply chain, + project management, production, services, CRM, etc... + </p> + <p> + The system is platform-independent, and can be installed on Windows, Mac OS X, + and various Linux and other Unix-based distributions. Its architecture enables + new functionality to be rapidly created, modifications to be made to a + production system and migration to a new version to be straightforward. + </p> + <p> + Depending on your needs, OpenERP is available through a web or application client. + </p> + </div> +</t> +<t t-name="FieldStatus"> + <div t-att-id="widget.element_id"></div> +</t> +<t t-name="FieldStatus.content"> + <ul class="oe-arrow-list"> + <t t-set="size" t-value="widget.to_show.length"/> + <t t-foreach="_.range(size)" t-as="i"> + <li t-att-class="widget.to_show[i][0] === widget.selected_value ? 'oe-arrow-list-selected' : ''"> + <span class="oe-arrow-list-before" t-if="i > 0"></span><span><t t-esc="widget.to_show[i][1]"/></span><span class="oe-arrow-list-after" t-if="i < size - 1"></span> + </li> + </t> + </ul> +</t> +</templates> |
