summaryrefslogtreecommitdiff
path: root/addons/web/static/src
diff options
context:
space:
mode:
authorAntony Lesuisse <al@openerp.com>2011-09-05 13:03:09 +0200
committerAntony Lesuisse <al@openerp.com>2011-09-05 13:03:09 +0200
commitbedfc48dcfb066e61209337235e339975dd093d0 (patch)
treefd1bab7b9f21380f1aa3ca7675018f13b4d9e652 /addons/web/static/src
parent272424894a91aa136952cf88ef206afab5b5505c (diff)
rename base into web part1
bzr revid: al@openerp.com-20110905110309-egx167t1ni1fzho1
Diffstat (limited to 'addons/web/static/src')
-rw-r--r--addons/web/static/src/css/base-ie7.css4
-rw-r--r--addons/web/static/src/css/base.css1277
-rw-r--r--addons/web/static/src/css/data_export.css85
-rw-r--r--addons/web/static/src/img/accessories-archiver.pngbin0 -> 822 bytes
-rw-r--r--addons/web/static/src/img/add-shortcut.pngbin0 -> 395 bytes
-rw-r--r--addons/web/static/src/img/attachments-close.pngbin0 -> 486 bytes
-rw-r--r--addons/web/static/src/img/collapse.gifbin0 -> 80 bytes
-rw-r--r--addons/web/static/src/img/db.pngbin0 -> 2042 bytes
-rw-r--r--addons/web/static/src/img/down-arrow.pngbin0 -> 263 bytes
-rw-r--r--addons/web/static/src/img/expand.gifbin0 -> 81 bytes
-rw-r--r--addons/web/static/src/img/favicon.icobin0 -> 1150 bytes
-rw-r--r--addons/web/static/src/img/header-about.pngbin0 -> 442 bytes
-rw-r--r--addons/web/static/src/img/header-background.pngbin0 -> 149 bytes
-rw-r--r--addons/web/static/src/img/header-help.pngbin0 -> 454 bytes
-rw-r--r--addons/web/static/src/img/header-home.pngbin0 -> 452 bytes
-rw-r--r--addons/web/static/src/img/header-preferences.pngbin0 -> 420 bytes
-rw-r--r--addons/web/static/src/img/header-requests.pngbin0 -> 353 bytes
-rw-r--r--addons/web/static/src/img/header.gifbin0 -> 182 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_ABOUT.pngbin0 -> 523 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_ADD.pngbin0 -> 212 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_APPLY.pngbin0 -> 709 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_BOLD.pngbin0 -> 604 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_CANCEL.pngbin0 -> 407 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_CDROM.pngbin0 -> 711 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_CLEAR.pngbin0 -> 682 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_CLOSE.pngbin0 -> 274 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_COLOR_PICKER.pngbin0 -> 470 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_CONNECT.pngbin0 -> 558 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_CONVERT.pngbin0 -> 500 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_COPY.pngbin0 -> 339 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_CUT.pngbin0 -> 626 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_DELETE.pngbin0 -> 531 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_DIALOG_AUTHENTICATION.pngbin0 -> 590 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_DIALOG_ERROR.pngbin0 -> 559 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_DIALOG_INFO.pngbin0 -> 687 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_DIALOG_QUESTION.pngbin0 -> 491 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_DIALOG_WARNING.pngbin0 -> 428 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_DIRECTORY.pngbin0 -> 392 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_DISCONNECT.pngbin0 -> 566 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_DND.pngbin0 -> 585 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_DND_MULTIPLE.pngbin0 -> 501 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_EDIT.pngbin0 -> 350 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_EXECUTE.pngbin0 -> 318 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_FILE.pngbin0 -> 289 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_FIND.pngbin0 -> 465 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_FIND_AND_REPLACE.pngbin0 -> 580 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_FLOPPY.pngbin0 -> 565 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_FULLSCREEN.pngbin0 -> 450 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_GOTO_BOTTOM.pngbin0 -> 574 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_GOTO_FIRST.pngbin0 -> 573 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_GOTO_LAST.pngbin0 -> 575 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_GOTO_TOP.pngbin0 -> 580 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_GO_BACK.pngbin0 -> 570 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_GO_DOWN.pngbin0 -> 572 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_GO_FORWARD.pngbin0 -> 575 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_GO_UP.pngbin0 -> 596 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_HARDDISK.pngbin0 -> 354 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_HELP.pngbin0 -> 789 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_HOME.pngbin0 -> 477 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_INDENT.pngbin0 -> 276 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_INDEX.pngbin0 -> 590 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_INFO.pngbin0 -> 738 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_ITALIC.pngbin0 -> 529 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_JUMP_TO.pngbin0 -> 591 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_JUSTIFY_CENTER.pngbin0 -> 251 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_JUSTIFY_FILL.pngbin0 -> 243 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_JUSTIFY_LEFT.pngbin0 -> 240 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_JUSTIFY_RIGHT.pngbin0 -> 254 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_LEAVE_FULLSCREEN.pngbin0 -> 448 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_MEDIA_FORWARD.pngbin0 -> 704 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_MEDIA_NEXT.pngbin0 -> 693 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_MEDIA_PAUSE.pngbin0 -> 466 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_MEDIA_PLAY.pngbin0 -> 556 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_MEDIA_PREVIOUS.pngbin0 -> 686 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_MEDIA_RECORD.pngbin0 -> 594 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_MEDIA_REWIND.pngbin0 -> 704 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_MEDIA_STOP.pngbin0 -> 421 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_MISSING_IMAGE.pngbin0 -> 330 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_NETWORK.pngbin0 -> 324 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_NEW.pngbin0 -> 405 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_NO.pngbin0 -> 751 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_OK.pngbin0 -> 795 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_OPEN.pngbin0 -> 418 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_PASTE.pngbin0 -> 473 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_PREFERENCES.pngbin0 -> 339 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_PRINT.pngbin0 -> 411 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_PRINT_PREVIEW.pngbin0 -> 616 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_PROPERTIES.pngbin0 -> 313 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_QUIT.pngbin0 -> 394 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_REDO.pngbin0 -> 430 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_REFRESH.pngbin0 -> 753 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_REMOVE.pngbin0 -> 176 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_REVERT_TO_SAVED.pngbin0 -> 413 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_SAVE.pngbin0 -> 599 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_SAVE_AS.pngbin0 -> 560 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_SELECT_COLOR.pngbin0 -> 656 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_SELECT_FONT.pngbin0 -> 598 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_SORT_ASCENDING.pngbin0 -> 371 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_SORT_DESCENDING.pngbin0 -> 370 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_SPELL_CHECK.pngbin0 -> 485 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_STOP.pngbin0 -> 678 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_STRIKETHROUGH.pngbin0 -> 459 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_UNDELETE.pngbin0 -> 875 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_UNDERLINE.pngbin0 -> 539 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_UNDO.pngbin0 -> 459 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_UNINDENT.pngbin0 -> 295 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_YES.pngbin0 -> 830 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_ZOOM_100.pngbin0 -> 469 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_ZOOM_FIT.pngbin0 -> 466 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_ZOOM_IN.pngbin0 -> 446 bytes
-rw-r--r--addons/web/static/src/img/icons/STOCK_ZOOM_OUT.pngbin0 -> 463 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-add.pngbin0 -> 212 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-apply.pngbin0 -> 709 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-cancel.pngbin0 -> 407 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-close.pngbin0 -> 274 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-convert.pngbin0 -> 500 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-delete.pngbin0 -> 531 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-dnd.pngbin0 -> 585 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-edit.pngbin0 -> 350 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-execute.pngbin0 -> 318 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-find.pngbin0 -> 465 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-go-back.pngbin0 -> 570 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-go-down.pngbin0 -> 572 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-go-forward.pngbin0 -> 575 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-go-up.pngbin0 -> 596 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-goto-last.pngbin0 -> 575 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-indent.pngbin0 -> 276 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-index.pngbin0 -> 590 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-jump-to.pngbin0 -> 591 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-justify-fill.pngbin0 -> 243 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-media-pause.pngbin0 -> 466 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-media-play.pngbin0 -> 556 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-network.pngbin0 -> 324 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-new.pngbin0 -> 405 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-no.pngbin0 -> 751 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-ok.pngbin0 -> 795 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-open.pngbin0 -> 418 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-print.pngbin0 -> 411 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-redo.pngbin0 -> 430 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-remove.pngbin0 -> 176 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-save.pngbin0 -> 599 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-sort-descending.pngbin0 -> 370 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-stop.pngbin0 -> 678 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-undo.pngbin0 -> 459 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-yes.pngbin0 -> 830 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-zoom-fit.pngbin0 -> 466 bytes
-rw-r--r--addons/web/static/src/img/icons/gtk-zoom-in.pngbin0 -> 446 bytes
-rw-r--r--addons/web/static/src/img/icons/index1.jpegbin0 -> 473 bytes
-rw-r--r--addons/web/static/src/img/icons/index2.jpegbin0 -> 471 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-accessories-archiver+.pngbin0 -> 838 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-accessories-archiver-minus.pngbin0 -> 838 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-accessories-archiver.pngbin0 -> 822 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-account.pngbin0 -> 784 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-administration.pngbin0 -> 1015 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-calendar.pngbin0 -> 908 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-call-start.pngbin0 -> 492 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-camera_test.pngbin0 -> 739 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-check.pngbin0 -> 832 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-crm.pngbin0 -> 877 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-dialog-close.pngbin0 -> 305 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-document-new.pngbin0 -> 738 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-dolar.pngbin0 -> 270 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-dolar_ok!.pngbin0 -> 220 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-emblem-important.pngbin0 -> 703 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-face-plain.pngbin0 -> 788 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-folder-blue.pngbin0 -> 438 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-folder-green.pngbin0 -> 453 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-folder-orange.pngbin0 -> 411 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-folder-violet.pngbin0 -> 538 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-folder-yellow.pngbin0 -> 432 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-gdu-smart-failing.pngbin0 -> 451 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-gnome-cpu-frequency-applet+.pngbin0 -> 488 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-go-home.pngbin0 -> 378 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-go-month.pngbin0 -> 517 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-go-today.pngbin0 -> 501 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-go-week.pngbin0 -> 442 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-go-year.pngbin0 -> 514 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-graph.pngbin0 -> 784 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-gtk-go-back-ltr.pngbin0 -> 759 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-gtk-go-back-rtl.pngbin0 -> 762 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-gtk-jump-to-ltr.pngbin0 -> 859 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-gtk-jump-to-rtl.pngbin0 -> 851 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-gtk-media-pause.pngbin0 -> 544 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-gtk-select-all.pngbin0 -> 554 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-gtk-stop.pngbin0 -> 622 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-hr.pngbin0 -> 699 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-idea.pngbin0 -> 384 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-locked.pngbin0 -> 588 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-mail-.pngbin0 -> 427 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-mail-forward.pngbin0 -> 382 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-mail-message-new.pngbin0 -> 696 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-mail-replied.pngbin0 -> 637 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-mail_delete.pngbin0 -> 988 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-marketing.pngbin0 -> 808 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-mrp.pngbin0 -> 880 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-partner.pngbin0 -> 905 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-personal+.pngbin0 -> 735 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-personal-.pngbin0 -> 791 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-personal.pngbin0 -> 930 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-product.pngbin0 -> 708 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-project.pngbin0 -> 993 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-purchase.pngbin0 -> 885 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-rating-rated.pngbin0 -> 559 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-report.pngbin0 -> 831 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-sale.pngbin0 -> 699 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-stage.pngbin0 -> 742 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-stock.pngbin0 -> 967 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-stock_align_left_24.pngbin0 -> 358 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-stock_effects-object-colorize.pngbin0 -> 173 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-stock_format-default.pngbin0 -> 267 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-stock_format-scientific.pngbin0 -> 201 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-stock_symbol-selection.pngbin0 -> 382 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-stock_zoom.pngbin0 -> 425 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-tools.pngbin0 -> 914 bytes
-rw-r--r--addons/web/static/src/img/icons/terp-translate.pngbin0 -> 519 bytes
-rw-r--r--addons/web/static/src/img/iconset-d-drop.gifbin0 -> 238 bytes
-rw-r--r--addons/web/static/src/img/logo.pngbin0 -> 3551 bytes
-rw-r--r--addons/web/static/src/img/partner.pngbin0 -> 905 bytes
-rw-r--r--addons/web/static/src/img/placeholder.pngbin0 -> 1362 bytes
-rw-r--r--addons/web/static/src/img/product.pngbin0 -> 708 bytes
-rw-r--r--addons/web/static/src/img/remove-shortcut.pngbin0 -> 559 bytes
-rw-r--r--addons/web/static/src/img/stock_person.pngbin0 -> 1074 bytes
-rw-r--r--addons/web/static/src/img/throbber.gifbin0 -> 673 bytes
-rw-r--r--addons/web/static/src/img/throbber2.gifbin0 -> 7195 bytes
-rw-r--r--addons/web/static/src/img/toggle-a-bg.pngbin0 -> 412 bytes
-rw-r--r--addons/web/static/src/img/ui/field_calendar.pngbin0 -> 454 bytes
-rw-r--r--addons/web/static/src/img/ui/group-expanded.pngbin0 -> 229 bytes
-rw-r--r--addons/web/static/src/img/ui/group-folded.pngbin0 -> 259 bytes
-rw-r--r--addons/web/static/src/img/warning.pngbin0 -> 1342 bytes
-rw-r--r--addons/web/static/src/js/boot.js67
-rw-r--r--addons/web/static/src/js/chrome.js940
-rw-r--r--addons/web/static/src/js/core.js914
-rw-r--r--addons/web/static/src/js/data.js787
-rw-r--r--addons/web/static/src/js/data_export.js402
-rw-r--r--addons/web/static/src/js/dates.js138
-rw-r--r--addons/web/static/src/js/form.js2477
-rw-r--r--addons/web/static/src/js/formats.js197
-rw-r--r--addons/web/static/src/js/list-editable.js324
-rw-r--r--addons/web/static/src/js/list.js1467
-rw-r--r--addons/web/static/src/js/search.js1177
-rw-r--r--addons/web/static/src/js/view_help.js15
-rw-r--r--addons/web/static/src/js/view_tree.js206
-rw-r--r--addons/web/static/src/js/views.js789
-rw-r--r--addons/web/static/src/xml/base.xml1392
244 files changed, 12658 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..a4104e8a839
--- /dev/null
+++ b/addons/web/static/src/css/base.css
@@ -0,0 +1,1277 @@
+/* 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: 0px; -webkit-border-radius: 0px; border-radius: 0px;
+ 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;
+}
+/* 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: 0px; -webkit-border-radius: 0px; border-radius: 0px;
+}
+.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 0;
+ 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: 0px;
+}
+.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 .oe_form_field label.oe_label, .openerp .oe_form_field 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 5px;
+ 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;
+}
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..9dab440e964
--- /dev/null
+++ b/addons/web/static/src/css/data_export.css
@@ -0,0 +1,85 @@
+.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_export {
+ width: 100%;
+ height: 400px;
+}
+
+.openerp .oe_export_fields_selector_left {
+ width: 50%;
+}
+
+.openerp div#left_field_panel {
+ overflow: scroll;
+ width: 100%;
+ height: 400px;
+ border: solid #999999 1px;
+}
+
+.openerp .oe_export_fields_selector_center {
+ width: 102px;
+}
+
+.openerp .oe_export_fields_selector_right {
+ width: 45%;
+ height: 400px;
+}
+
+.openerp .oe_export_fields_selector_export 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;
+}
+
+.openerp .oe_export_button_export {
+ border: 1px solid #006;
+ background-color: #F3F3F3;
+}
diff --git a/addons/web/static/src/img/accessories-archiver.png b/addons/web/static/src/img/accessories-archiver.png
new file mode 100644
index 00000000000..8bcbe4566bc
--- /dev/null
+++ b/addons/web/static/src/img/accessories-archiver.png
Binary files differ
diff --git a/addons/web/static/src/img/add-shortcut.png b/addons/web/static/src/img/add-shortcut.png
new file mode 100644
index 00000000000..bcc0a09eb45
--- /dev/null
+++ b/addons/web/static/src/img/add-shortcut.png
Binary files differ
diff --git a/addons/web/static/src/img/attachments-close.png b/addons/web/static/src/img/attachments-close.png
new file mode 100644
index 00000000000..14d645534b8
--- /dev/null
+++ b/addons/web/static/src/img/attachments-close.png
Binary files differ
diff --git a/addons/web/static/src/img/collapse.gif b/addons/web/static/src/img/collapse.gif
new file mode 100644
index 00000000000..759e72ebcfe
--- /dev/null
+++ b/addons/web/static/src/img/collapse.gif
Binary files differ
diff --git a/addons/web/static/src/img/db.png b/addons/web/static/src/img/db.png
new file mode 100644
index 00000000000..83e42df26b3
--- /dev/null
+++ b/addons/web/static/src/img/db.png
Binary files differ
diff --git a/addons/web/static/src/img/down-arrow.png b/addons/web/static/src/img/down-arrow.png
new file mode 100644
index 00000000000..b1288e52682
--- /dev/null
+++ b/addons/web/static/src/img/down-arrow.png
Binary files differ
diff --git a/addons/web/static/src/img/expand.gif b/addons/web/static/src/img/expand.gif
new file mode 100644
index 00000000000..ca98d0b5f2b
--- /dev/null
+++ b/addons/web/static/src/img/expand.gif
Binary files differ
diff --git a/addons/web/static/src/img/favicon.ico b/addons/web/static/src/img/favicon.ico
new file mode 100644
index 00000000000..a323cc21c00
--- /dev/null
+++ b/addons/web/static/src/img/favicon.ico
Binary files differ
diff --git a/addons/web/static/src/img/header-about.png b/addons/web/static/src/img/header-about.png
new file mode 100644
index 00000000000..b53e9a86ebe
--- /dev/null
+++ b/addons/web/static/src/img/header-about.png
Binary files differ
diff --git a/addons/web/static/src/img/header-background.png b/addons/web/static/src/img/header-background.png
new file mode 100644
index 00000000000..774b3954124
--- /dev/null
+++ b/addons/web/static/src/img/header-background.png
Binary files differ
diff --git a/addons/web/static/src/img/header-help.png b/addons/web/static/src/img/header-help.png
new file mode 100644
index 00000000000..19e9906fdef
--- /dev/null
+++ b/addons/web/static/src/img/header-help.png
Binary files differ
diff --git a/addons/web/static/src/img/header-home.png b/addons/web/static/src/img/header-home.png
new file mode 100644
index 00000000000..fd96742d15a
--- /dev/null
+++ b/addons/web/static/src/img/header-home.png
Binary files differ
diff --git a/addons/web/static/src/img/header-preferences.png b/addons/web/static/src/img/header-preferences.png
new file mode 100644
index 00000000000..6c978b49f55
--- /dev/null
+++ b/addons/web/static/src/img/header-preferences.png
Binary files differ
diff --git a/addons/web/static/src/img/header-requests.png b/addons/web/static/src/img/header-requests.png
new file mode 100644
index 00000000000..6577e7e6448
--- /dev/null
+++ b/addons/web/static/src/img/header-requests.png
Binary files differ
diff --git a/addons/web/static/src/img/header.gif b/addons/web/static/src/img/header.gif
new file mode 100644
index 00000000000..f40bd9c0f6a
--- /dev/null
+++ b/addons/web/static/src/img/header.gif
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_ABOUT.png b/addons/web/static/src/img/icons/STOCK_ABOUT.png
new file mode 100644
index 00000000000..1b430a35758
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_ABOUT.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_ADD.png b/addons/web/static/src/img/icons/STOCK_ADD.png
new file mode 100644
index 00000000000..70612c181c6
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_ADD.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_APPLY.png b/addons/web/static/src/img/icons/STOCK_APPLY.png
new file mode 100644
index 00000000000..116f2e433bb
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_APPLY.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_BOLD.png b/addons/web/static/src/img/icons/STOCK_BOLD.png
new file mode 100644
index 00000000000..408f25aa20b
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_BOLD.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_CANCEL.png b/addons/web/static/src/img/icons/STOCK_CANCEL.png
new file mode 100644
index 00000000000..8cf6273281b
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_CANCEL.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_CDROM.png b/addons/web/static/src/img/icons/STOCK_CDROM.png
new file mode 100644
index 00000000000..27e8edd8a0b
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_CDROM.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_CLEAR.png b/addons/web/static/src/img/icons/STOCK_CLEAR.png
new file mode 100644
index 00000000000..b2874fb84fa
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_CLEAR.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_CLOSE.png b/addons/web/static/src/img/icons/STOCK_CLOSE.png
new file mode 100644
index 00000000000..4966974e178
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_CLOSE.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_COLOR_PICKER.png b/addons/web/static/src/img/icons/STOCK_COLOR_PICKER.png
new file mode 100644
index 00000000000..232d54f8f3b
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_COLOR_PICKER.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_CONNECT.png b/addons/web/static/src/img/icons/STOCK_CONNECT.png
new file mode 100644
index 00000000000..c820b54abcc
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_CONNECT.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_CONVERT.png b/addons/web/static/src/img/icons/STOCK_CONVERT.png
new file mode 100644
index 00000000000..6288b964cc0
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_CONVERT.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_COPY.png b/addons/web/static/src/img/icons/STOCK_COPY.png
new file mode 100644
index 00000000000..3d68da606d2
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_COPY.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_CUT.png b/addons/web/static/src/img/icons/STOCK_CUT.png
new file mode 100644
index 00000000000..96681d26968
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_CUT.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_DELETE.png b/addons/web/static/src/img/icons/STOCK_DELETE.png
new file mode 100644
index 00000000000..879d6fd1cbe
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_DELETE.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_DIALOG_AUTHENTICATION.png b/addons/web/static/src/img/icons/STOCK_DIALOG_AUTHENTICATION.png
new file mode 100644
index 00000000000..eb411112f83
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_DIALOG_AUTHENTICATION.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_DIALOG_ERROR.png b/addons/web/static/src/img/icons/STOCK_DIALOG_ERROR.png
new file mode 100644
index 00000000000..e97bc43504b
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_DIALOG_ERROR.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_DIALOG_INFO.png b/addons/web/static/src/img/icons/STOCK_DIALOG_INFO.png
new file mode 100644
index 00000000000..ed46c3ecdb3
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_DIALOG_INFO.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_DIALOG_QUESTION.png b/addons/web/static/src/img/icons/STOCK_DIALOG_QUESTION.png
new file mode 100644
index 00000000000..ef68f43f43f
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_DIALOG_QUESTION.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_DIALOG_WARNING.png b/addons/web/static/src/img/icons/STOCK_DIALOG_WARNING.png
new file mode 100644
index 00000000000..65a4d2c7220
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_DIALOG_WARNING.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_DIRECTORY.png b/addons/web/static/src/img/icons/STOCK_DIRECTORY.png
new file mode 100644
index 00000000000..8e41b7878a9
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_DIRECTORY.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_DISCONNECT.png b/addons/web/static/src/img/icons/STOCK_DISCONNECT.png
new file mode 100644
index 00000000000..fd188701fc2
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_DISCONNECT.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_DND.png b/addons/web/static/src/img/icons/STOCK_DND.png
new file mode 100644
index 00000000000..bf3b8068397
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_DND.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_DND_MULTIPLE.png b/addons/web/static/src/img/icons/STOCK_DND_MULTIPLE.png
new file mode 100644
index 00000000000..aa4c65ad5b1
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_DND_MULTIPLE.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_EDIT.png b/addons/web/static/src/img/icons/STOCK_EDIT.png
new file mode 100644
index 00000000000..53b7d0855ed
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_EDIT.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_EXECUTE.png b/addons/web/static/src/img/icons/STOCK_EXECUTE.png
new file mode 100644
index 00000000000..eddc0f86b25
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_EXECUTE.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_FILE.png b/addons/web/static/src/img/icons/STOCK_FILE.png
new file mode 100644
index 00000000000..fa157156bcc
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_FILE.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_FIND.png b/addons/web/static/src/img/icons/STOCK_FIND.png
new file mode 100644
index 00000000000..7a23b0125b5
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_FIND.png
Binary files differ
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
new file mode 100644
index 00000000000..b8d8eeec1b2
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_FIND_AND_REPLACE.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_FLOPPY.png b/addons/web/static/src/img/icons/STOCK_FLOPPY.png
new file mode 100644
index 00000000000..7d8209cfb6f
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_FLOPPY.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_FULLSCREEN.png b/addons/web/static/src/img/icons/STOCK_FULLSCREEN.png
new file mode 100644
index 00000000000..8ddc31e7481
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_FULLSCREEN.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_GOTO_BOTTOM.png b/addons/web/static/src/img/icons/STOCK_GOTO_BOTTOM.png
new file mode 100644
index 00000000000..be52840dee1
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_GOTO_BOTTOM.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_GOTO_FIRST.png b/addons/web/static/src/img/icons/STOCK_GOTO_FIRST.png
new file mode 100644
index 00000000000..3d00e36d696
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_GOTO_FIRST.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_GOTO_LAST.png b/addons/web/static/src/img/icons/STOCK_GOTO_LAST.png
new file mode 100644
index 00000000000..2028226996d
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_GOTO_LAST.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_GOTO_TOP.png b/addons/web/static/src/img/icons/STOCK_GOTO_TOP.png
new file mode 100644
index 00000000000..227a91e5157
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_GOTO_TOP.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_GO_BACK.png b/addons/web/static/src/img/icons/STOCK_GO_BACK.png
new file mode 100644
index 00000000000..568117dc170
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_GO_BACK.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_GO_DOWN.png b/addons/web/static/src/img/icons/STOCK_GO_DOWN.png
new file mode 100644
index 00000000000..5677cffe6a3
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_GO_DOWN.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_GO_FORWARD.png b/addons/web/static/src/img/icons/STOCK_GO_FORWARD.png
new file mode 100644
index 00000000000..fa89d31b734
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_GO_FORWARD.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_GO_UP.png b/addons/web/static/src/img/icons/STOCK_GO_UP.png
new file mode 100644
index 00000000000..84f96fdfca2
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_GO_UP.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_HARDDISK.png b/addons/web/static/src/img/icons/STOCK_HARDDISK.png
new file mode 100644
index 00000000000..73e5ef0a82e
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_HARDDISK.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_HELP.png b/addons/web/static/src/img/icons/STOCK_HELP.png
new file mode 100644
index 00000000000..f8e17009dd6
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_HELP.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_HOME.png b/addons/web/static/src/img/icons/STOCK_HOME.png
new file mode 100644
index 00000000000..9d3d50d0fcb
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_HOME.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_INDENT.png b/addons/web/static/src/img/icons/STOCK_INDENT.png
new file mode 100644
index 00000000000..19f2669447d
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_INDENT.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_INDEX.png b/addons/web/static/src/img/icons/STOCK_INDEX.png
new file mode 100644
index 00000000000..0b63cc25fa8
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_INDEX.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_INFO.png b/addons/web/static/src/img/icons/STOCK_INFO.png
new file mode 100644
index 00000000000..272019f5c99
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_INFO.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_ITALIC.png b/addons/web/static/src/img/icons/STOCK_ITALIC.png
new file mode 100644
index 00000000000..26c57226a31
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_ITALIC.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_JUMP_TO.png b/addons/web/static/src/img/icons/STOCK_JUMP_TO.png
new file mode 100644
index 00000000000..82d7896bdac
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_JUMP_TO.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_JUSTIFY_CENTER.png b/addons/web/static/src/img/icons/STOCK_JUSTIFY_CENTER.png
new file mode 100644
index 00000000000..d5f514298eb
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_JUSTIFY_CENTER.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_JUSTIFY_FILL.png b/addons/web/static/src/img/icons/STOCK_JUSTIFY_FILL.png
new file mode 100644
index 00000000000..5e2f5c73f30
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_JUSTIFY_FILL.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_JUSTIFY_LEFT.png b/addons/web/static/src/img/icons/STOCK_JUSTIFY_LEFT.png
new file mode 100644
index 00000000000..ecb76b117eb
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_JUSTIFY_LEFT.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_JUSTIFY_RIGHT.png b/addons/web/static/src/img/icons/STOCK_JUSTIFY_RIGHT.png
new file mode 100644
index 00000000000..3cd4d161420
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_JUSTIFY_RIGHT.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_LEAVE_FULLSCREEN.png b/addons/web/static/src/img/icons/STOCK_LEAVE_FULLSCREEN.png
new file mode 100644
index 00000000000..15bc8ab7d7b
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_LEAVE_FULLSCREEN.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_MEDIA_FORWARD.png b/addons/web/static/src/img/icons/STOCK_MEDIA_FORWARD.png
new file mode 100644
index 00000000000..e5e198fbb79
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_MEDIA_FORWARD.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_MEDIA_NEXT.png b/addons/web/static/src/img/icons/STOCK_MEDIA_NEXT.png
new file mode 100644
index 00000000000..e0aa2431cf0
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_MEDIA_NEXT.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_MEDIA_PAUSE.png b/addons/web/static/src/img/icons/STOCK_MEDIA_PAUSE.png
new file mode 100644
index 00000000000..6721994eb8d
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_MEDIA_PAUSE.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_MEDIA_PLAY.png b/addons/web/static/src/img/icons/STOCK_MEDIA_PLAY.png
new file mode 100644
index 00000000000..b0832712c81
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_MEDIA_PLAY.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_MEDIA_PREVIOUS.png b/addons/web/static/src/img/icons/STOCK_MEDIA_PREVIOUS.png
new file mode 100644
index 00000000000..756899e0bc1
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_MEDIA_PREVIOUS.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_MEDIA_RECORD.png b/addons/web/static/src/img/icons/STOCK_MEDIA_RECORD.png
new file mode 100644
index 00000000000..904974c81e5
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_MEDIA_RECORD.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_MEDIA_REWIND.png b/addons/web/static/src/img/icons/STOCK_MEDIA_REWIND.png
new file mode 100644
index 00000000000..9c22d9e2778
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_MEDIA_REWIND.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_MEDIA_STOP.png b/addons/web/static/src/img/icons/STOCK_MEDIA_STOP.png
new file mode 100644
index 00000000000..128cc2313ec
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_MEDIA_STOP.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_MISSING_IMAGE.png b/addons/web/static/src/img/icons/STOCK_MISSING_IMAGE.png
new file mode 100644
index 00000000000..cc69ed33e4b
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_MISSING_IMAGE.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_NETWORK.png b/addons/web/static/src/img/icons/STOCK_NETWORK.png
new file mode 100644
index 00000000000..df94a515fec
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_NETWORK.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_NEW.png b/addons/web/static/src/img/icons/STOCK_NEW.png
new file mode 100644
index 00000000000..04a290fac7d
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_NEW.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_NO.png b/addons/web/static/src/img/icons/STOCK_NO.png
new file mode 100644
index 00000000000..047ddcd9289
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_NO.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_OK.png b/addons/web/static/src/img/icons/STOCK_OK.png
new file mode 100644
index 00000000000..7755b8769f0
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_OK.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_OPEN.png b/addons/web/static/src/img/icons/STOCK_OPEN.png
new file mode 100644
index 00000000000..9cdd8c66770
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_OPEN.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_PASTE.png b/addons/web/static/src/img/icons/STOCK_PASTE.png
new file mode 100644
index 00000000000..840f26c8b38
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_PASTE.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_PREFERENCES.png b/addons/web/static/src/img/icons/STOCK_PREFERENCES.png
new file mode 100644
index 00000000000..9ba17a0ab20
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_PREFERENCES.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_PRINT.png b/addons/web/static/src/img/icons/STOCK_PRINT.png
new file mode 100644
index 00000000000..be0ed4efb44
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_PRINT.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_PRINT_PREVIEW.png b/addons/web/static/src/img/icons/STOCK_PRINT_PREVIEW.png
new file mode 100644
index 00000000000..ab15fcbd307
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_PRINT_PREVIEW.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_PROPERTIES.png b/addons/web/static/src/img/icons/STOCK_PROPERTIES.png
new file mode 100644
index 00000000000..894415ec1ce
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_PROPERTIES.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_QUIT.png b/addons/web/static/src/img/icons/STOCK_QUIT.png
new file mode 100644
index 00000000000..50b5144660b
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_QUIT.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_REDO.png b/addons/web/static/src/img/icons/STOCK_REDO.png
new file mode 100644
index 00000000000..54055b3966d
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_REDO.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_REFRESH.png b/addons/web/static/src/img/icons/STOCK_REFRESH.png
new file mode 100644
index 00000000000..7474728b4c0
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_REFRESH.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_REMOVE.png b/addons/web/static/src/img/icons/STOCK_REMOVE.png
new file mode 100644
index 00000000000..c2b6e915f77
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_REMOVE.png
Binary files differ
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
new file mode 100644
index 00000000000..eaf54584edf
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_REVERT_TO_SAVED.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_SAVE.png b/addons/web/static/src/img/icons/STOCK_SAVE.png
new file mode 100644
index 00000000000..c61836548a7
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_SAVE.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_SAVE_AS.png b/addons/web/static/src/img/icons/STOCK_SAVE_AS.png
new file mode 100644
index 00000000000..1831e4e0a4b
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_SAVE_AS.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_SELECT_COLOR.png b/addons/web/static/src/img/icons/STOCK_SELECT_COLOR.png
new file mode 100644
index 00000000000..12b015df107
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_SELECT_COLOR.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_SELECT_FONT.png b/addons/web/static/src/img/icons/STOCK_SELECT_FONT.png
new file mode 100644
index 00000000000..66e606313a5
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_SELECT_FONT.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_SORT_ASCENDING.png b/addons/web/static/src/img/icons/STOCK_SORT_ASCENDING.png
new file mode 100644
index 00000000000..a648fa0d359
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_SORT_ASCENDING.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_SORT_DESCENDING.png b/addons/web/static/src/img/icons/STOCK_SORT_DESCENDING.png
new file mode 100644
index 00000000000..a90b588d797
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_SORT_DESCENDING.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_SPELL_CHECK.png b/addons/web/static/src/img/icons/STOCK_SPELL_CHECK.png
new file mode 100644
index 00000000000..698f7c24a43
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_SPELL_CHECK.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_STOP.png b/addons/web/static/src/img/icons/STOCK_STOP.png
new file mode 100644
index 00000000000..2ee93ed6415
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_STOP.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_STRIKETHROUGH.png b/addons/web/static/src/img/icons/STOCK_STRIKETHROUGH.png
new file mode 100644
index 00000000000..1c11f214622
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_STRIKETHROUGH.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_UNDELETE.png b/addons/web/static/src/img/icons/STOCK_UNDELETE.png
new file mode 100644
index 00000000000..c44b940c03b
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_UNDELETE.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_UNDERLINE.png b/addons/web/static/src/img/icons/STOCK_UNDERLINE.png
new file mode 100644
index 00000000000..7dbb5d7e60a
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_UNDERLINE.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_UNDO.png b/addons/web/static/src/img/icons/STOCK_UNDO.png
new file mode 100644
index 00000000000..7a50de0c61e
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_UNDO.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_UNINDENT.png b/addons/web/static/src/img/icons/STOCK_UNINDENT.png
new file mode 100644
index 00000000000..8ace914c1bd
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_UNINDENT.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_YES.png b/addons/web/static/src/img/icons/STOCK_YES.png
new file mode 100644
index 00000000000..01fb373c251
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_YES.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_ZOOM_100.png b/addons/web/static/src/img/icons/STOCK_ZOOM_100.png
new file mode 100644
index 00000000000..5193406b787
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_ZOOM_100.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_ZOOM_FIT.png b/addons/web/static/src/img/icons/STOCK_ZOOM_FIT.png
new file mode 100644
index 00000000000..1e70e619466
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_ZOOM_FIT.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_ZOOM_IN.png b/addons/web/static/src/img/icons/STOCK_ZOOM_IN.png
new file mode 100644
index 00000000000..29e50e45d4a
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_ZOOM_IN.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/STOCK_ZOOM_OUT.png b/addons/web/static/src/img/icons/STOCK_ZOOM_OUT.png
new file mode 100644
index 00000000000..0da5ee7ec13
--- /dev/null
+++ b/addons/web/static/src/img/icons/STOCK_ZOOM_OUT.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-add.png b/addons/web/static/src/img/icons/gtk-add.png
new file mode 100644
index 00000000000..70612c181c6
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-add.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-apply.png b/addons/web/static/src/img/icons/gtk-apply.png
new file mode 100644
index 00000000000..116f2e433bb
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-apply.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-cancel.png b/addons/web/static/src/img/icons/gtk-cancel.png
new file mode 100644
index 00000000000..8cf6273281b
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-cancel.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-close.png b/addons/web/static/src/img/icons/gtk-close.png
new file mode 100644
index 00000000000..4966974e178
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-close.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-convert.png b/addons/web/static/src/img/icons/gtk-convert.png
new file mode 100644
index 00000000000..6288b964cc0
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-convert.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-delete.png b/addons/web/static/src/img/icons/gtk-delete.png
new file mode 100644
index 00000000000..879d6fd1cbe
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-delete.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-dnd.png b/addons/web/static/src/img/icons/gtk-dnd.png
new file mode 100644
index 00000000000..bf3b8068397
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-dnd.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-edit.png b/addons/web/static/src/img/icons/gtk-edit.png
new file mode 100644
index 00000000000..53b7d0855ed
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-edit.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-execute.png b/addons/web/static/src/img/icons/gtk-execute.png
new file mode 100644
index 00000000000..eddc0f86b25
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-execute.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-find.png b/addons/web/static/src/img/icons/gtk-find.png
new file mode 100644
index 00000000000..7a23b0125b5
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-find.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-go-back.png b/addons/web/static/src/img/icons/gtk-go-back.png
new file mode 100644
index 00000000000..568117dc170
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-go-back.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-go-down.png b/addons/web/static/src/img/icons/gtk-go-down.png
new file mode 100644
index 00000000000..5677cffe6a3
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-go-down.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-go-forward.png b/addons/web/static/src/img/icons/gtk-go-forward.png
new file mode 100644
index 00000000000..fa89d31b734
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-go-forward.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-go-up.png b/addons/web/static/src/img/icons/gtk-go-up.png
new file mode 100644
index 00000000000..84f96fdfca2
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-go-up.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-goto-last.png b/addons/web/static/src/img/icons/gtk-goto-last.png
new file mode 100644
index 00000000000..2028226996d
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-goto-last.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-indent.png b/addons/web/static/src/img/icons/gtk-indent.png
new file mode 100644
index 00000000000..19f2669447d
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-indent.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-index.png b/addons/web/static/src/img/icons/gtk-index.png
new file mode 100644
index 00000000000..0b63cc25fa8
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-index.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-jump-to.png b/addons/web/static/src/img/icons/gtk-jump-to.png
new file mode 100644
index 00000000000..82d7896bdac
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-jump-to.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-justify-fill.png b/addons/web/static/src/img/icons/gtk-justify-fill.png
new file mode 100644
index 00000000000..5e2f5c73f30
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-justify-fill.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-media-pause.png b/addons/web/static/src/img/icons/gtk-media-pause.png
new file mode 100644
index 00000000000..6721994eb8d
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-media-pause.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-media-play.png b/addons/web/static/src/img/icons/gtk-media-play.png
new file mode 100644
index 00000000000..b0832712c81
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-media-play.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-network.png b/addons/web/static/src/img/icons/gtk-network.png
new file mode 100644
index 00000000000..df94a515fec
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-network.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-new.png b/addons/web/static/src/img/icons/gtk-new.png
new file mode 100644
index 00000000000..04a290fac7d
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-new.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-no.png b/addons/web/static/src/img/icons/gtk-no.png
new file mode 100644
index 00000000000..047ddcd9289
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-no.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-ok.png b/addons/web/static/src/img/icons/gtk-ok.png
new file mode 100644
index 00000000000..7755b8769f0
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-ok.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-open.png b/addons/web/static/src/img/icons/gtk-open.png
new file mode 100644
index 00000000000..9cdd8c66770
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-open.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-print.png b/addons/web/static/src/img/icons/gtk-print.png
new file mode 100644
index 00000000000..be0ed4efb44
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-print.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-redo.png b/addons/web/static/src/img/icons/gtk-redo.png
new file mode 100644
index 00000000000..54055b3966d
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-redo.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-remove.png b/addons/web/static/src/img/icons/gtk-remove.png
new file mode 100644
index 00000000000..c2b6e915f77
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-remove.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-save.png b/addons/web/static/src/img/icons/gtk-save.png
new file mode 100644
index 00000000000..c61836548a7
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-save.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-sort-descending.png b/addons/web/static/src/img/icons/gtk-sort-descending.png
new file mode 100644
index 00000000000..a90b588d797
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-sort-descending.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-stop.png b/addons/web/static/src/img/icons/gtk-stop.png
new file mode 100644
index 00000000000..2ee93ed6415
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-stop.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-undo.png b/addons/web/static/src/img/icons/gtk-undo.png
new file mode 100644
index 00000000000..7a50de0c61e
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-undo.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-yes.png b/addons/web/static/src/img/icons/gtk-yes.png
new file mode 100644
index 00000000000..01fb373c251
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-yes.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-zoom-fit.png b/addons/web/static/src/img/icons/gtk-zoom-fit.png
new file mode 100644
index 00000000000..1e70e619466
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-zoom-fit.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/gtk-zoom-in.png b/addons/web/static/src/img/icons/gtk-zoom-in.png
new file mode 100644
index 00000000000..29e50e45d4a
--- /dev/null
+++ b/addons/web/static/src/img/icons/gtk-zoom-in.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/index1.jpeg b/addons/web/static/src/img/icons/index1.jpeg
new file mode 100644
index 00000000000..f43c68d79ec
--- /dev/null
+++ b/addons/web/static/src/img/icons/index1.jpeg
Binary files differ
diff --git a/addons/web/static/src/img/icons/index2.jpeg b/addons/web/static/src/img/icons/index2.jpeg
new file mode 100644
index 00000000000..ec9d1eb6d3c
--- /dev/null
+++ b/addons/web/static/src/img/icons/index2.jpeg
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-accessories-archiver+.png b/addons/web/static/src/img/icons/terp-accessories-archiver+.png
new file mode 100644
index 00000000000..14d6d02536e
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-accessories-archiver+.png
Binary files differ
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
new file mode 100644
index 00000000000..024c8f692d8
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-accessories-archiver-minus.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-accessories-archiver.png b/addons/web/static/src/img/icons/terp-accessories-archiver.png
new file mode 100644
index 00000000000..8bcbe4566bc
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-accessories-archiver.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-account.png b/addons/web/static/src/img/icons/terp-account.png
new file mode 100644
index 00000000000..30dea82a105
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-account.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-administration.png b/addons/web/static/src/img/icons/terp-administration.png
new file mode 100644
index 00000000000..ab129b2e4b3
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-administration.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-calendar.png b/addons/web/static/src/img/icons/terp-calendar.png
new file mode 100644
index 00000000000..37206565792
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-calendar.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-call-start.png b/addons/web/static/src/img/icons/terp-call-start.png
new file mode 100644
index 00000000000..602a1cdd4b8
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-call-start.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-camera_test.png b/addons/web/static/src/img/icons/terp-camera_test.png
new file mode 100644
index 00000000000..e97e0416de3
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-camera_test.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-check.png b/addons/web/static/src/img/icons/terp-check.png
new file mode 100644
index 00000000000..003b09fdeb9
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-check.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-crm.png b/addons/web/static/src/img/icons/terp-crm.png
new file mode 100644
index 00000000000..fdac61ca92d
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-crm.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-dialog-close.png b/addons/web/static/src/img/icons/terp-dialog-close.png
new file mode 100644
index 00000000000..8f51ec05fd6
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-dialog-close.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-document-new.png b/addons/web/static/src/img/icons/terp-document-new.png
new file mode 100644
index 00000000000..2815cbb403f
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-document-new.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-dolar.png b/addons/web/static/src/img/icons/terp-dolar.png
new file mode 100644
index 00000000000..bca7021c82f
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-dolar.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-dolar_ok!.png b/addons/web/static/src/img/icons/terp-dolar_ok!.png
new file mode 100644
index 00000000000..b1005aae7b9
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-dolar_ok!.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-emblem-important.png b/addons/web/static/src/img/icons/terp-emblem-important.png
new file mode 100644
index 00000000000..a8321066abb
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-emblem-important.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-face-plain.png b/addons/web/static/src/img/icons/terp-face-plain.png
new file mode 100644
index 00000000000..ea90aaa7ef4
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-face-plain.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-folder-blue.png b/addons/web/static/src/img/icons/terp-folder-blue.png
new file mode 100644
index 00000000000..addc480d49a
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-folder-blue.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-folder-green.png b/addons/web/static/src/img/icons/terp-folder-green.png
new file mode 100644
index 00000000000..206cd39603e
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-folder-green.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-folder-orange.png b/addons/web/static/src/img/icons/terp-folder-orange.png
new file mode 100644
index 00000000000..726dbd7f00e
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-folder-orange.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-folder-violet.png b/addons/web/static/src/img/icons/terp-folder-violet.png
new file mode 100644
index 00000000000..c6f5cfb87ff
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-folder-violet.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-folder-yellow.png b/addons/web/static/src/img/icons/terp-folder-yellow.png
new file mode 100644
index 00000000000..cd7295dcdd7
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-folder-yellow.png
Binary files differ
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
new file mode 100644
index 00000000000..a4448fd553e
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-gdu-smart-failing.png
Binary files differ
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
new file mode 100644
index 00000000000..23e246f3be9
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-gnome-cpu-frequency-applet+.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-go-home.png b/addons/web/static/src/img/icons/terp-go-home.png
new file mode 100644
index 00000000000..912256bf7a7
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-go-home.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-go-month.png b/addons/web/static/src/img/icons/terp-go-month.png
new file mode 100644
index 00000000000..cf5e305053e
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-go-month.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-go-today.png b/addons/web/static/src/img/icons/terp-go-today.png
new file mode 100644
index 00000000000..2a4dbeed49a
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-go-today.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-go-week.png b/addons/web/static/src/img/icons/terp-go-week.png
new file mode 100644
index 00000000000..b3ce6f562b2
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-go-week.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-go-year.png b/addons/web/static/src/img/icons/terp-go-year.png
new file mode 100644
index 00000000000..f45efc9dd0c
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-go-year.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-graph.png b/addons/web/static/src/img/icons/terp-graph.png
new file mode 100644
index 00000000000..30dea82a105
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-graph.png
Binary files differ
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
new file mode 100644
index 00000000000..a76138d2e0d
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-gtk-go-back-ltr.png
Binary files differ
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
new file mode 100644
index 00000000000..10d10b582fe
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-gtk-go-back-rtl.png
Binary files differ
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
new file mode 100644
index 00000000000..fa517749d10
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-gtk-jump-to-ltr.png
Binary files differ
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
new file mode 100644
index 00000000000..7b5785c4963
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-gtk-jump-to-rtl.png
Binary files differ
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
new file mode 100644
index 00000000000..2896ad8e27f
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-gtk-media-pause.png
Binary files differ
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
new file mode 100644
index 00000000000..b71a6493791
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-gtk-select-all.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-gtk-stop.png b/addons/web/static/src/img/icons/terp-gtk-stop.png
new file mode 100644
index 00000000000..5fe021e19df
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-gtk-stop.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-hr.png b/addons/web/static/src/img/icons/terp-hr.png
new file mode 100644
index 00000000000..ba0241e6362
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-hr.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-idea.png b/addons/web/static/src/img/icons/terp-idea.png
new file mode 100644
index 00000000000..fd5e2a25f14
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-idea.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-locked.png b/addons/web/static/src/img/icons/terp-locked.png
new file mode 100644
index 00000000000..cd2b4c2c9c2
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-locked.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-mail-.png b/addons/web/static/src/img/icons/terp-mail-.png
new file mode 100644
index 00000000000..cbc85e4c000
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-mail-.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-mail-forward.png b/addons/web/static/src/img/icons/terp-mail-forward.png
new file mode 100644
index 00000000000..3d4e694b6dc
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-mail-forward.png
Binary files differ
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
new file mode 100644
index 00000000000..25b4d5b5d50
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-mail-message-new.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-mail-replied.png b/addons/web/static/src/img/icons/terp-mail-replied.png
new file mode 100644
index 00000000000..6615f4352cc
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-mail-replied.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-mail_delete.png b/addons/web/static/src/img/icons/terp-mail_delete.png
new file mode 100644
index 00000000000..90fb5239f02
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-mail_delete.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-marketing.png b/addons/web/static/src/img/icons/terp-marketing.png
new file mode 100644
index 00000000000..1b04b599734
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-marketing.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-mrp.png b/addons/web/static/src/img/icons/terp-mrp.png
new file mode 100644
index 00000000000..c2b4ea31ec3
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-mrp.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-partner.png b/addons/web/static/src/img/icons/terp-partner.png
new file mode 100644
index 00000000000..aab206a1d42
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-partner.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-personal+.png b/addons/web/static/src/img/icons/terp-personal+.png
new file mode 100644
index 00000000000..8afa4d7041d
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-personal+.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-personal-.png b/addons/web/static/src/img/icons/terp-personal-.png
new file mode 100644
index 00000000000..a2554df3615
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-personal-.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-personal.png b/addons/web/static/src/img/icons/terp-personal.png
new file mode 100644
index 00000000000..0890c4d0e51
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-personal.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-product.png b/addons/web/static/src/img/icons/terp-product.png
new file mode 100644
index 00000000000..99f58d95a8b
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-product.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-project.png b/addons/web/static/src/img/icons/terp-project.png
new file mode 100644
index 00000000000..cd95ae84e43
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-project.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-purchase.png b/addons/web/static/src/img/icons/terp-purchase.png
new file mode 100644
index 00000000000..1e801f57485
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-purchase.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-rating-rated.png b/addons/web/static/src/img/icons/terp-rating-rated.png
new file mode 100644
index 00000000000..9bff03fe584
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-rating-rated.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-report.png b/addons/web/static/src/img/icons/terp-report.png
new file mode 100644
index 00000000000..ada36a2de24
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-report.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-sale.png b/addons/web/static/src/img/icons/terp-sale.png
new file mode 100644
index 00000000000..ba0241e6362
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-sale.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-stage.png b/addons/web/static/src/img/icons/terp-stage.png
new file mode 100644
index 00000000000..591c1594f78
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-stage.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-stock.png b/addons/web/static/src/img/icons/terp-stock.png
new file mode 100644
index 00000000000..0365dfdb8e1
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-stock.png
Binary files differ
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
new file mode 100644
index 00000000000..844367bb1a7
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-stock_align_left_24.png
Binary files differ
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
new file mode 100644
index 00000000000..0ffd95a740a
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-stock_effects-object-colorize.png
Binary files differ
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
new file mode 100644
index 00000000000..e44b2b26be5
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-stock_format-default.png
Binary files differ
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
new file mode 100644
index 00000000000..01f83950804
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-stock_format-scientific.png
Binary files differ
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
new file mode 100644
index 00000000000..5773be57c9b
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-stock_symbol-selection.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-stock_zoom.png b/addons/web/static/src/img/icons/terp-stock_zoom.png
new file mode 100644
index 00000000000..e96f05e5905
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-stock_zoom.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-tools.png b/addons/web/static/src/img/icons/terp-tools.png
new file mode 100644
index 00000000000..379f7617427
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-tools.png
Binary files differ
diff --git a/addons/web/static/src/img/icons/terp-translate.png b/addons/web/static/src/img/icons/terp-translate.png
new file mode 100644
index 00000000000..5ab41d49343
--- /dev/null
+++ b/addons/web/static/src/img/icons/terp-translate.png
Binary files differ
diff --git a/addons/web/static/src/img/iconset-d-drop.gif b/addons/web/static/src/img/iconset-d-drop.gif
new file mode 100644
index 00000000000..4907a927619
--- /dev/null
+++ b/addons/web/static/src/img/iconset-d-drop.gif
Binary files differ
diff --git a/addons/web/static/src/img/logo.png b/addons/web/static/src/img/logo.png
new file mode 100644
index 00000000000..8f1c5c6085f
--- /dev/null
+++ b/addons/web/static/src/img/logo.png
Binary files differ
diff --git a/addons/web/static/src/img/partner.png b/addons/web/static/src/img/partner.png
new file mode 100644
index 00000000000..aab206a1d42
--- /dev/null
+++ b/addons/web/static/src/img/partner.png
Binary files differ
diff --git a/addons/web/static/src/img/placeholder.png b/addons/web/static/src/img/placeholder.png
new file mode 100644
index 00000000000..25af75ee815
--- /dev/null
+++ b/addons/web/static/src/img/placeholder.png
Binary files differ
diff --git a/addons/web/static/src/img/product.png b/addons/web/static/src/img/product.png
new file mode 100644
index 00000000000..99f58d95a8b
--- /dev/null
+++ b/addons/web/static/src/img/product.png
Binary files differ
diff --git a/addons/web/static/src/img/remove-shortcut.png b/addons/web/static/src/img/remove-shortcut.png
new file mode 100644
index 00000000000..9bff03fe584
--- /dev/null
+++ b/addons/web/static/src/img/remove-shortcut.png
Binary files differ
diff --git a/addons/web/static/src/img/stock_person.png b/addons/web/static/src/img/stock_person.png
new file mode 100644
index 00000000000..202cf0ee6c5
--- /dev/null
+++ b/addons/web/static/src/img/stock_person.png
Binary files differ
diff --git a/addons/web/static/src/img/throbber.gif b/addons/web/static/src/img/throbber.gif
new file mode 100644
index 00000000000..ded8d36d486
--- /dev/null
+++ b/addons/web/static/src/img/throbber.gif
Binary files differ
diff --git a/addons/web/static/src/img/throbber2.gif b/addons/web/static/src/img/throbber2.gif
new file mode 100644
index 00000000000..5d2057ae0e7
--- /dev/null
+++ b/addons/web/static/src/img/throbber2.gif
Binary files differ
diff --git a/addons/web/static/src/img/toggle-a-bg.png b/addons/web/static/src/img/toggle-a-bg.png
new file mode 100644
index 00000000000..b9837ab233e
--- /dev/null
+++ b/addons/web/static/src/img/toggle-a-bg.png
Binary files differ
diff --git a/addons/web/static/src/img/ui/field_calendar.png b/addons/web/static/src/img/ui/field_calendar.png
new file mode 100644
index 00000000000..99a1f3f9ef1
--- /dev/null
+++ b/addons/web/static/src/img/ui/field_calendar.png
Binary files differ
diff --git a/addons/web/static/src/img/ui/group-expanded.png b/addons/web/static/src/img/ui/group-expanded.png
new file mode 100644
index 00000000000..3f16536e123
--- /dev/null
+++ b/addons/web/static/src/img/ui/group-expanded.png
Binary files differ
diff --git a/addons/web/static/src/img/ui/group-folded.png b/addons/web/static/src/img/ui/group-folded.png
new file mode 100644
index 00000000000..2909b0c268f
--- /dev/null
+++ b/addons/web/static/src/img/ui/group-folded.png
Binary files differ
diff --git a/addons/web/static/src/img/warning.png b/addons/web/static/src/img/warning.png
new file mode 100644
index 00000000000..cbfb25a89cd
--- /dev/null
+++ b/addons/web/static/src/img/warning.png
Binary files differ
diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js
new file mode 100644
index 00000000000..c5d8bbfc1ca
--- /dev/null
+++ b/addons/web/static/src/js/boot.js
@@ -0,0 +1,67 @@
+/*---------------------------------------------------------
+ * OpenERP Web Boostrap Code
+ *---------------------------------------------------------*/
+
+/**
+ * @name openerp
+ * @namespace openerp
+ * @namespace window.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
+ *---------------------------------------------------------*/
+
+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);
+ files = ["views","search","list","form","list_editable","web_mobile","view_tree","data_export"];
+ for(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..153234e9891
--- /dev/null
+++ b/addons/web/static/src/js/chrome.js
@@ -0,0 +1,940 @@
+/*---------------------------------------------------------
+ * OpenERP Web chrome
+ *---------------------------------------------------------*/
+
+openerp.web.chrome = function(openerp) {
+
+openerp.web.Notification = openerp.web.Widget.extend({
+ 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({
+ dialog_title: "",
+ identifier_prefix: 'dialog',
+ 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({
+ identifier_prefix: 'dialog_crash',
+ 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({
+ 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.Dataweb = openerp.web.Widget.extend({
+ 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");
+ });
+ }
+ });
+ },
+ wait_for_file: function (token, cleanup) {
+ var self = this,
+ cookie_name = 'fileToken',
+ cookie_length = cookie_name.length;
+ this.backup_timer = setInterval(function () {
+ var cookies = document.cookie.split(';');
+ 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 waiter
+ clearInterval(self.backup_timer);
+ // clear cookie
+ document.cookie = _.sprintf("%s=;expires=%s;path=/",
+ cookie_name, new Date().toGMTString());
+
+ if (cleanup) { cleanup(); }
+ }
+ }, 200);
+ },
+ do_backup: function() {
+ var self = this;
+ self.$option_id.html(QWeb.render("BackupDB", self));
+
+ self.$option_id.find("form[name=backup_db_form]").validate({
+ submitHandler: function (form) {
+ $.blockUI({message:'<img src="/web/static/src/img/throbber2.gif">'});
+ // 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 $target = $('#backup-target'),
+ token = new Date().getTime();
+ if (!$target.length) {
+ $target = $('<iframe id="backup-target" style="display: none;">')
+ .appendTo(document.body)
+ .load(function () {
+ $.unblockUI();
+ clearInterval(self.backup_timer);
+ var error = this.contentDocument.body
+ .firstChild.data
+ .split('|');
+ self.display_error({
+ title: error[0],
+ error: error[1]
+ });
+ });
+ }
+ $(form).find('input[name=token]').val(token);
+ form.submit();
+
+ self.wait_for_file(token, function () {
+ $.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({
+ remember_creditentials: true,
+
+ 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 = this.selected_password || "a";
+ }
+ },
+ 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.dataweb.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);
+ } else {
+ localStorage.setItem('last_db_login_success', '');
+ localStorage.setItem('last_login_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
+ });
+ },
+ on_logout: function() {
+ this.session.logout();
+ }
+});
+
+openerp.web.Header = openerp.web.Widget.extend({
+ template: "Header",
+ identifier_prefix: 'oe-app-header-',
+ init: function(parent) {
+ this._super(parent);
+ this.qs = "?" + jQuery.param.querystring();
+ this.$content = $();
+ },
+ start: function() {
+ this._super();
+ },
+ do_update: function () {
+ this.$content = $(QWeb.render("Header-content", {widget: this}));
+ this.$content.appendTo(this.$element);
+ this.$element.find(".logout").click(this.on_logout);
+ this.$element.find("a.preferences").click(this.on_preferences);
+ return this.shortcut_load();
+ },
+ do_reset: function() {
+ this.$content.remove();
+ },
+ 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() {
+ this.$element.find('.oe-shortcuts ul').empty();
+ }
+});
+
+openerp.web.Menu = openerp.web.Widget.extend({
+ 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.Homepage = openerp.web.Widget.extend({
+});
+
+openerp.web.Preferences = openerp.web.Widget.extend({
+});
+
+openerp.web.WebClient = openerp.web.Widget.extend({
+ init: function(element_id) {
+ this._super(null, element_id);
+ openerp.webclient = this;
+
+ QWeb.add_template("/web/static/src/xml/web.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_reset);
+ 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();
+ this.notification.notify("OpenERP Client", "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..e22812b34b3
--- /dev/null
+++ b/addons/web/static/src/js/core.js
@@ -0,0 +1,914 @@
+/*---------------------------------------------------------
+ * OpenERP Web core
+ *--------------------------------------------------------*/
+
+openerp.web.core = function(openerp) {
+/**
+ * John Resig Class with factory improvement
+ */
+(function() {
+ var initializing = false,
+ fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
+ // The web Class implementation (does nothing)
+ /**
+ * @class
+ */
+ openerp.web.Class = function(){};
+
+ // Create a new Class that inherits from this 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 {dict} 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
+ * @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
+ * @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
+ * @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({
+ 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
+ * @param server
+ * @param 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.db = "";
+ this.login = "";
+ this.password = "";
+ this.user_context= {};
+ this.uid = false;
+ this.session_id = false;
+ this.module_list = [];
+ this.module_loaded = {"web": true};
+ this.context = {};
+ this.shortcuts = [];
+ this.active_id = null;
+ this.session = this;
+ },
+ 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:null
+ }).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;
+ this.db = db;
+ this.login = login;
+ this.password = password;
+ var params = { db: this.db, login: this.login, password: this.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.session_save();
+ self.on_session_valid();
+ if (success_callback)
+ success_callback();
+ });
+ },
+ session_logout: function() {
+ this.uid = false;
+ },
+ /**
+ * Reloads uid and session_id from local storage, if they exist
+ */
+ session_restore: function () {
+ this.uid = this.get_cookie('uid');
+ this.session_id = this.get_cookie('session_id');
+ this.db = this.get_cookie('db');
+ this.login = this.get_cookie('login');
+ this.user_context = this.get_cookie("user_context");
+ // we should do an rpc to confirm that this session_id is valid and if it is retrieve the information about db and login
+ // then call on_session_valid
+ this.on_session_valid();
+ },
+ /**
+ * Saves the session id and uid locally
+ */
+ session_save: function () {
+ this.set_cookie('uid', this.uid);
+ this.set_cookie('session_id', this.session_id);
+ this.set_cookie('db', this.db);
+ this.set_cookie('login', this.login);
+ this.set_cookie('user_context', this.user_context);
+ },
+ logout: function() {
+ delete this.uid;
+ delete this.session_id;
+ delete this.db;
+ delete this.login;
+ this.set_cookie('uid', '');
+ this.set_cookie('session_id', '');
+ this.set_cookie('db', '');
+ this.set_cookie('login', '');
+ this.on_session_invalid(function() {});
+ },
+ /**
+ * 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;
+ 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) {
+ _.each(files, function (file) {
+ $('head').append($('<link>', {
+ 'href': file,
+ '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;
+ 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);
+ };
+ document.head.appendChild(tag);
+ } 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;
+ }
+ }
+ }
+});
+
+/**
+ * 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.
+ */
+openerp.web.SessionAware = openerp.web.CallbackEnabled.extend({
+ 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);
+ },
+ log: function() {
+ var args = Array.prototype.slice.call(arguments);
+ var caller = arguments.callee.caller;
+ // TODO add support for line number using
+ // https://github.com/emwendelin/javascript-stacktrace/blob/master/stacktrace.js
+ // args.unshift("" + caller.debug_name);
+ this.on_log.apply(this,args);
+ },
+ on_log: function() {
+ if(this.session.debug) {
+ var notify = false;
+ var body = false;
+ if(window.console) {
+ console.log(arguments);
+ } else {
+ body = true;
+ }
+ var a = Array.prototype.slice.call(arguments, 0);
+ for(var i = 0; i < a.length; i++) {
+ var v = a[i]==null ? "null" : a[i].toString();
+ if(i==0) {
+ notify = v.match(/^not/);
+ body = v.match(/^bod/);
+ }
+ if(body) {
+ $('<pre></pre>').text(v).appendTo($('body'));
+ }
+ if(notify && this.notification) {
+ this.notification.notify("Logging:",v);
+ }
+ }
+ }
+ }
+});
+
+openerp.web.Widget = openerp.web.SessionAware.extend({
+ /**
+ * 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
+ * @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);
+ return this.start();
+ },
+ /**
+ * 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 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();
+ }
+});
+
+/**
+ * @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({
+ render: function (additional) {
+ return QWeb.render(this.template, _.extend(_.extend({}, this), additional || {}));
+ }
+});
+
+openerp.web.TranslationDataBase = openerp.web.Class.extend({
+ 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..d687cc3c310
--- /dev/null
+++ b/addons/web/static/src/js/data.js
@@ -0,0 +1,787 @@
+
+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
+ * @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
+ * @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
+ * @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
+ * @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
+ * @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 dataweb
+ *
+ * @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({
+ /**
+ * @constructs
+ *
+ * @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);
+ var to_return = $.Deferred().then(callback);
+ to_return.resolve({result: cached.id});
+ return to_return.promise();
+ },
+ write: function (id, data, 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 (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, 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.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..3fdf277463d
--- /dev/null
+++ b/addons/web/static/src/js/data_export.js
@@ -0,0 +1,402 @@
+openerp.web.data_export = function(openerp) {
+openerp.web.DataExport = openerp.web.Dialog.extend({
+ init: function(parent, dataset) {
+ this._super(parent);
+ this.dataset = dataset;
+ },
+ start: function() {
+ var self = this;
+ self._super(false);
+ self.template = 'ExportTreeView';
+ self.dialog_title = "Export Data";
+ self.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.on_show_exists_export_list();
+ 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 option').remove();
+ });
+ self.$element.find('#export_new_list').click(function() {
+ self.on_show_save_list();
+ });
+ var import_comp = self.$element.find('#import_compat option:selected').val(),
+ params = {
+ import_compat: parseInt(import_comp)
+ };
+ self.rpc('/web/export/get_fields', { model: self.dataset.model, params: params }, self.on_show_data);
+
+ self.$element.find('#import_compat').change(function() {
+ self.$element.find('#fields_list option').remove();
+ self.$element.find('#field-tree-structure').remove();
+ var import_comp = self.$element.find("#import_compat option:selected").val();
+ if (import_comp) {
+ var params = {
+ import_compat: parseInt(import_comp)
+ }
+ self.rpc("/web/export/get_fields", { model: self.dataset.model, params: params}, self.on_show_data);
+ }
+ });
+ },
+ on_show_exists_export_list: function() {
+ var self = this;
+ if (self.$element.find('#saved_export_list').is(':hidden')) {
+ self.$element.find('#ExistsExportList').show();
+ } else {
+ this.rpc('/web/export/exist_export_lists', { 'model': this.dataset.model}, function(export_list) {
+ if (export_list.length) {
+ 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.rpc('/web/export/delete_export', { export_id: parseInt(select_exp.val())}, {});
+ 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");
+ for (var key in field_list) {
+ export_node.append(new Option(field_list[key], key));
+ }
+ },
+ 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 export_field = self.get_fields();
+ if (export_field.length) {
+ self.rpc("/web/export/save_export_lists", {"model": self.dataset.model, "name":value, "field_list":export_field}, function(exp_id) {
+ if (exp_id) {
+ if (self.$element.find("#saved_export_list").length > 0) {
+ self.$element.find("#saved_export_list").append(new Option(value, exp_id));
+ } else {
+ self.on_show_exists_export_list();
+ }
+ if (self.$element.find("#saved_export_list").is(":hidden")) {
+ self.on_show_exists_export_list();
+ }
+ }
+ });
+ self.on_show_save_list();
+ self.$element.find("#fields_list option").remove();
+ }
+ },
+ on_click: function(id, result) {
+ var self = this;
+ self.field_id = id.split("-")[1];
+ var is_loaded = 0;
+ _.each(result, function(record) {
+ if (record['id'] == self.field_id && (record['children']).length >= 1) {
+ var model = record['params']['model'],
+ prefix = record['params']['prefix'],
+ name = record['params']['name'];
+ $(record['children']).each(function(e, childid) {
+ if (self.$element.find("tr[id='treerow-" + childid + "']").length > 0) {
+ if (self.$element.find("tr[id='treerow-" + childid + "']").is(':hidden')) {
+ is_loaded = -1;
+ } else {
+ is_loaded++;
+ }
+ }
+ });
+ if (is_loaded == 0) {
+ if (self.$element.find("tr[id='treerow-" + self.field_id +"']").find('img').attr('src') === '/web/static/src/img/expand.gif') {
+ if (model) {
+ var import_comp = self.$element.find("#import_compat option:selected").val();
+ var params = {
+ import_compat: parseInt(import_comp),
+ parent_field_type : record['field_type']
+ }
+ self.rpc("/web/export/get_fields", {
+ model: model,
+ prefix: prefix,
+ name: name,
+ field_parent : self.field_id,
+ params: params
+ }, function(results) {
+ self.on_show_data(results);
+ });
+ }
+ }
+ } else if (is_loaded > 0) {
+ self.showcontent(self.field_id, true);
+ } else {
+ self.showcontent(self.field_id, false);
+ }
+ }
+ });
+ },
+ on_show_data: function(result) {
+ var self = this;
+ var imp_cmpt = parseInt(self.$element.find("#import_compat option:selected").val());
+ var current_tr = self.$element.find("tr[id='treerow-" + self.field_id + "']");
+ if (current_tr.length >= 1) {
+ 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) {
+ if ((record.field_type == "one2many") && imp_cmpt) {
+ var o2m_fld = self.$element.find("tr[id='treerow-" + record.id + "']").find('#tree-column');
+ o2m_fld.addClass("oe_export_readonlyfield");
+ }
+ if ((record.required == true) || record.required == "True") {
+ 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, result);
+ });
+
+ self.$element.find("tr[id='treerow-" + record.id + "']").click(function(e) {
+ if (e.shiftKey == true) {
+ 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 (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 (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(e) {
+ 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;
+ }
+ var selected = self.$element.find("tr.ui-selected");
+ if ($(this).hasClass("ui-selected") && (e.ctrlKey == true)) {
+ $(this).find('a').blur();
+ $(this).removeClass("ui-selected");
+ } else if ($(this).hasClass("ui-selected") && (e.ctrlKey == false) && (e.shiftKey == false)) {
+ selected.find('a').blur();
+ selected.removeClass("ui-selected");
+ $(this).find('a').focus();
+ $(this).addClass("ui-selected");
+ } else if (!$(this).hasClass("ui-selected") && (e.ctrlKey == false) && (e.shiftKey == false)) {
+ selected.find('a').blur();
+ selected.removeClass("ui-selected");
+ $(this).find('a').focus();
+ $(this).addClass("ui-selected");
+ } else if (!$(this).hasClass("ui-selected") && (e.ctrlKey == true)) {
+ $(this).find('a').focus();
+ $(this).addClass("ui-selected");
+ }
+ return false;
+ });
+
+ self.$element.find("tr[id='treerow-" + record.id + "']").keydown(function(e) {
+ var keyCode = e.keyCode || e.which;
+ arrow = {left: 37, up: 38, right: 39, down: 40 };
+ switch (keyCode) {
+ case arrow.left:
+ if ($(this).find('img').attr('src') === '/web/static/src/img/collapse.gif') {
+ self.on_click(this.id, result);
+ }
+ break;
+ case arrow.up:
+ var elem = this;
+ $(elem).removeClass("ui-selected");
+ while ($(elem).prev().is(":visible") == false) {
+ 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.right:
+ if ($(this).find('img').attr('src') == '/web/static/src/img/expand.gif') {
+ self.on_click(this.id, result);
+ }
+ break;
+ case arrow.down:
+ var elem = this;
+ $(elem).removeClass("ui-selected");
+ while($(elem).next().is(":visible") == false) {
+ 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(e) {
+ var $o2m_selection = self.$element.find("tr[id^='treerow-" + record.id + "']").find('#tree-column');
+ if (!$o2m_selection.hasClass("oe_export_readonlyfield")) {
+ var field_id = $(this).find("a").attr("id");
+ if (field_id) {
+ self.add_field(field_id.split('-')[1], $(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, flag) {
+ // show & hide the contents
+ var first_child = this.$element.find("tr[id='treerow-" + id + "']").find('img');
+ if (flag) {
+ 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++) {
+ if (flag) {
+ $(child_field[i]).hide();
+ } else {
+ if (child_len == (child_field[i].id.split("/")).length) {
+ if ($(child_field[i]).find('img').attr('src') == '/web/static/src/img/collapse.gif') {
+ $(child_field[i]).find('img').attr('src', '/web/static/src/img/expand.gif');
+ }
+ $(child_field[i]).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() {
+ var self = this;
+ var export_field = {};
+ var flag = true;
+ self.$element.find("#fields_list option").each(function() {
+ export_field[$(this).val()] = $(this).text();
+ flag = false;
+ });
+ if (flag) {
+ alert('Please select fields to export...');
+ return;
+ }
+
+ var import_comp = self.$element.find("#import_compat option:selected").val(),
+ export_format = self.$element.find("#export_format").val();
+
+ self.rpc("/web/export/export_data", {
+ model: self.dataset.model,
+ fields: export_field,
+ ids: self.dataset.ids,
+ domain: self.dataset.domain,
+ import_compat: parseInt(import_comp),
+ export_format: export_format
+ }, function(data) {
+ window.location = "data:text/csv/excel;charset=utf8," + data;
+ self.close();
+ });
+ },
+ 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..aac3b383dd3
--- /dev/null
+++ b/addons/web/static/src/js/form.js
@@ -0,0 +1,2477 @@
+openerp.web.form = function (openerp) {
+
+var _t = openerp.web._t;
+
+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
+ * @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() {
+ //this.log('Starting FormView '+this.model+this.view_id)
+ if (this.embedded_view) {
+ var def = $.Deferred().then(this.on_loaded);
+ var self = this;
+ setTimeout(function() {def.resolve({fields_view: self.embedded_view});}, 0);
+ return def.promise();
+ } else {
+ var context = new openerp.web.CompoundContext(this.dataset.get_context());
+ return this.rpc("/web/formview/load", {"model": this.model, "view_id": this.view_id,
+ 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.fields_view;
+ 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(data.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 {
+ this.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) {
+ this.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) {
+ this.notification.warn("Record not saved", "Problem while saving record.");
+ } else {
+ this.notification.notify("Record saved", "The record #" + this.datarecord.id + " has been saved.");
+ 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) {
+ this.notification.warn("Record not created", "Problem while creating record.");
+ } 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();
+ }
+ this.notification.notify("Record created", "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) {
+ this.notification.notify("Searching form");
+ },
+ on_action: function (action) {
+ this.notification.notify('Executing action ' + action);
+ },
+ do_cancel: function () {
+ this.notification.notify("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] : [];
+ }
+});
+
+/** @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:
+ this.log("Unsupported operator in modifiers :", op);
+ }
+ }
+ return _.all(stack, _.identity);
+};
+
+openerp.web.form.Widget = openerp.web.Widget.extend({
+ template: 'Widget',
+ 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;
+ self.log(widget.element_id , widget);
+ window.w = widget;
+ });
+ }
+});
+
+openerp.web.form.Field = openerp.web.form.Widget.extend({
+ 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) {
+ this._super(view, node);
+ this.template = "FieldSelection";
+ this.field_index = _.map(this.field.selection, function(x, index) {
+ return {"ikey": "" + index, "ekey": x[0], "label": x[1]};
+ });
+ },
+ 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 option = _.detect(this.field_index, function(x) {return x.ekey === value;});
+ this.$element.find('select').val(option === undefined ? '' : option.ikey);
+ },
+ set_value_from_ui: function() {
+ var ikey = this.$element.find('select').val();
+ var option = _.detect(this.field_index, function(x) {return x.ikey === ikey;});
+ this.value = option === undefined ? false : option.ekey;
+ this._super();
+ },
+ update_dom: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('select').attr('disabled', this.readonly);
+ },
+ validate: function() {
+ var ikey = this.$element.find('select').val();
+ var option = _.detect(this.field_index, function(x) {return x.ikey === ikey;});
+ this.invalid = this.required && (option === undefined || option.ekey === 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 = 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;
+ }
+});
+
+openerp.web.form.Many2ManyListView = openerp.web.ListView.extend({
+ 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();
+ });
+ }
+});
+
+openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend({
+ 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);
+ }
+});
+
+openerp.web.form.FormOpenPopup = openerp.web.OldWidget.extend({
+ 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_web64) {
+ 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
+ this.log("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_web64) {
+ },
+ 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_web64) {
+ 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_web64) {
+ 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');
+ }
+});
+
+/**
+ * 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'
+});
+
+};
+
+// 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..d7a235f5a32
--- /dev/null
+++ b/addons/web/static/src/js/list-editable.js
@@ -0,0 +1,324 @@
+/**
+ * @namespace handles editability case for lists, because it depends on form and forms already depends on lists it had to be split out
+ */
+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.fields_view.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.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({fields_view: 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>');
+ }
+ });
+
+ 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..1942bac179a
--- /dev/null
+++ b/addons/web/static/src/js/list.js
@@ -0,0 +1,1467 @@
+openerp.web.list = function (openerp) {
+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
+ * @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.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);
+ }
+
+ 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.fields_view;
+ //this.log(this.fields_view);
+ 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(data.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.hidden) {
+ this.reload_content();
+ }
+ },
+ do_hide: function () {
+ this.$element.hide();
+ if (this.sidebar) {
+ this.sidebar.$element.hide();
+ }
+ this.hidden = true;
+ },
+ /**
+ * 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({fields_view: this.embedded_view});
+ } else {
+ return this.rpc('/web/listview/load', {
+ model: this.model,
+ view_id: this.view_id,
+ 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;
+ }
+});
+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
+ * @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
+ * @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">&lt;</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">&gt;</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();
+ }
+});
+
+/**
+ * @class
+ * @extends openerp.web.Class
+ */
+var 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
+ * @extends openerp.web.Class
+ * @borrows Events#bind as this.bind
+ * @borrows Events#trigger as this.trigger
+ * @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
+ * @extends openerp.web.Class
+ * @borrows Events#bind as this.bind
+ * @borrows Events#trigger as this.trigger
+ * @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..23fb1ec5215
--- /dev/null
+++ b/addons/web/static/src/js/search.js
@@ -0,0 +1,1177 @@
+openerp.web.search = function(openerp) {
+
+openerp.web.SearchView = openerp.web.Widget.extend({
+ 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.log('Starting SearchView '+this.model+this.view_id)
+ 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
+ * @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
+ * @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({
+ template: 'SearchView.filters',
+ 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
+ * @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({
+ template: 'SearchView.filter',
+ 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
+ * @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
+ * @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;
+ }
+ }
+});
+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({
+ template: 'SearchView.extended_search.proposition',
+ identifier_prefix: 'extended-search-proposition',
+ 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";
+ this.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_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..6257dc7e587
--- /dev/null
+++ b/addons/web/static/src/js/view_tree.js
@@ -0,0 +1,206 @@
+/*---------------------------------------------------------
+ * OpenERP web library
+ *---------------------------------------------------------*/
+
+openerp.web.view_tree = function(openerp) {
+
+openerp.web.views.add('tree', 'openerp.web.TreeView');
+/**
+ * Genuine tree view (the one displayed as a tree, not the list)
+ */
+openerp.web.TreeView = openerp.web.View.extend({
+ /**
+ * Indicates that this view is not searchable, and thus that no search
+ * view should be displayed (if there is one active).
+ */
+ searchable : false,
+
+ 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,
+ 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..e00b802c65f
--- /dev/null
+++ b/addons/web/static/src/js/views.js
@@ -0,0 +1,789 @@
+/*---------------------------------------------------------
+ * OpenERP web library
+ *---------------------------------------------------------*/
+
+openerp.web.views = function(openerp) {
+
+var _t = openerp.web._t;
+
+/**
+ * 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)) {
+ this.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: {active_id: 66, active_ids: [66], active_model: 'ir.ui.menu'}
+ }).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) {
+ this.rpc('/web/report/get_report', {
+ action: action,
+ context: {}
+ }).then(function(result) {
+ debugger;
+ });
+ }
+});
+
+openerp.web.ViewManager = openerp.web.Widget.extend({
+ identifier_prefix: "viewmanager",
+ 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("ViewManager", {"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 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);
+ if (this.flags.search_view === false) {
+ this.searchview.hide();
+ }
+ 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({
+ init: function(parent, action) {
+ this.session = parent.session;
+ this.action = action;
+ var dataset;
+ if (!action.res_id) {
+ dataset = new openerp.web.DataSetSearch(this, action.res_model, action.context, action.domain);
+ } else {
+ this.action.flags.search_view = false;
+ dataset = new openerp.web.DataSetStatic(this, action.res_model, action.context, [action.res_id]);
+ dataset.index = 0;
+ }
+ this._super(parent, dataset, action.views);
+ this.action = action;
+ this.flags = this.action.flags || {};
+ if (action.res_model == 'board.board' && action.views.length == 1 && action.views) {
+ // Not elegant but allows to avoid flickering of SearchView#do_hide
+ this.flags.search_view = this.flags.pager = this.flags.sidebar = this.flags.action_buttons = false;
+ }
+ },
+ start: function() {
+ var inital_view_loaded = this._super();
+
+ var search_defaults = {};
+ _.each(this.action.context, function (value, key) {
+ var match = /^search_default_(.*)$/.exec(key);
+ if (match) {
+ search_defaults[match[1]] = value;
+ }
+ });
+
+ if (this.flags.search_view !== false) {
+ // init search view
+ var searchview_id = this.action.search_view_id && this.action.search_view_id[0];
+
+ var searchview_loaded = this.setup_search_view(
+ searchview_id || false, search_defaults);
+
+ // schedule auto_search
+ if (searchview_loaded != null && this.action['auto_search']) {
+ $.when(searchview_loaded, inital_view_loaded)
+ .then(this.searchview.do_search);
+ }
+ }
+ },
+ 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 niv: maybe show a warning?
+ 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();
+ }
+});
+
+openerp.web.View = openerp.web.Widget.extend({
+ 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
+ });
+ 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.web.json_node_to_xml(this.fields_view.arch, true) + '</xmp>').dialog({ width: '95%', height: 600});
+ } else {
+ this.notification.warn("Manage Views", "Could not find current view declaration");
+ }
+ },
+ on_sidebar_edit_workflow: function() {
+ this.log('Todo');
+ },
+ on_sidebar_customize_object: function() {
+ this.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.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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
+ if (single_quote) {
+ vattr = vattr.replace(/&quot;/g, "'");
+ }
+ r += ' ' + attr + '="' + vattr + '"';
+ }
+ if (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..be21524fec6
--- /dev/null
+++ b/addons/web/static/src/xml/base.xml
@@ -0,0 +1,1392 @@
+<?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">
+ <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">
+ <span class="database"><t t-esc="widget.session.db"/></span> - <t t-esc="widget.session.login"/> <br/>
+ <small class="username">rpc_session_id: <t t-esc="widget.session.session_id"/></small>
+ </h1>
+ <div class="header_corner">
+ <ul class="block">
+ <li>
+ <a href="#home" 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">
+ &lt;<t t-esc="header"/>&gt;
+ <a href="#" t-attf-id="menu_#{menu.id}">
+ <span><t t-esc="menu.name"/></span>
+ </a>
+ &lt;/<t t-esc="header"/>&gt;
+ <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">
+ <a class="oe-shortcut-toggle" title="Add / Remove Shortcut..."
+ href="javascript: void(0)"> </a>
+ <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-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"
+ >&lt;</button>
+
+ <span class="oe-pager-state">
+ </span>
+
+ <button type="button" disabled="disabled"
+ data-pager-action="next">&gt;</button>
+ <button type="button" disabled="disabled"
+ data-pager-action="last">Last</button>
+ </th>
+ </tr>
+ </table>
+ </th>
+ </tr>
+ <tr t-if="options.header">
+ <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>
+ <th t-if="options.selectable" class="oe-record-selector" width="1">
+ <input type="checkbox"/>
+ </th>
+ <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 &amp; Edit</span>
+ <span class="oe_form_on_create">Create &amp; 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">&lt;&lt;</button>
+
+ <span class="oe_pager_index">0</span> / <span class="oe_pager_count">0</span>
+
+ <button type="button" data-pager-action="next">&gt;&gt;</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 + '&amp;model=ir.attachment&amp;id=' + attachment.id
+ + '&amp;field=datas&amp;fieldname=name&amp;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.field_index" t-as="options">
+ <option t-att-value="options.ikey">
+ <t t-esc="options.label"/>
+ </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-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 &amp; New</button>
+ <button type="button" class="oe_selectcreatepopup-form-save">Save &amp; 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>
+<t t-name="ExportTreeView">
+ <table class="view" style="background-color: #F3F3F3;">
+ <tr>
+ <td align="left">
+ 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>
+ <table>
+ <tr>
+ <td class="label"><label>Export Type:</label></td>
+ <td>
+ <select id="import_compat" name="import_compat">
+ <option value="1">Import Compatible Export</option>
+ <option value="0">Export all Data</option>
+ </select>
+ </td>
+ <td class="label"><label>Export Format</label></td>
+ <td>
+ <select id="export_format" name="export_format">
+ <option value="csv">CSV</option>
+ <option value="xls">Excel</option>
+ </select>
+ </td>
+
+ </tr>
+ </table>
+ </td>
+ </tr>
+
+ <tr>
+ <td>
+ <table class="oe_export_fields_selector_export">
+ <tr>
+ <th class="oe_view_title" valign="bottom">Available fields</th>
+ <th class="oe_view_title"></th>
+ <th class="oe_view_title">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>
+ <td class="oe_export_fields_selector_left">
+ <div id="left_field_panel">
+ </div>
+ </td>
+ <td>
+ <table class="oe_export_fields_selector_center">
+ <tr>
+ <td align="center">
+ <button id="add_field" class="oe_export_button_export">Add</button>
+ </td>
+ </tr>
+ <tr>
+ <td align="center">
+ <button id="remove_field" class="oe_export_button_export">Remove</button>
+ </td>
+ </tr>
+ <tr>
+ <td align="center">
+ <button id="remove_all_field" class="oe_export_button_export">Remove All</button>
+ </td>
+ </tr>
+ </table>
+ </td>
+ <td class="oe_export_fields_selector_right">
+ <select name="fields_list" id="fields_list" multiple="multiple"></select>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+</t>
+
+<t t-name="ExportTreeView-Secondary">
+ <table 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>
+</t>
+<t t-name="ExportTreeView-Secondary.children">
+ <t t-foreach="fields" t-as="field" >
+ <tr 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">&amp;nbsp;</td>
+ </t>
+ </t>
+ <td valign="top" align="left" style="cursor: pointer;" width="18">
+ <t t-if="(field.children).length >= 1">
+ <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>
+
+<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">
+ <tr><th align="right"><label >Saved exports:</label></th></tr>
+ <tr align="left">
+ <td>
+ <select id="saved_export_list" style="width: 100%;">
+ <option></option>
+ <t t-foreach="existing_exports" t-as="export">
+ <option t-att-value="export.id"><t t-esc="export.name"/></option>
+ </t>
+ </select>
+ </td>
+ <td><button class="oe_export_button_export" id="delete_export_list">Delete</button></td>
+ </tr>
+</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>
+</templates>