From 8741282322d012260d332ea796deed8026cb1b8e Mon Sep 17 00:00:00 2001 From: Marc Date: Tue, 31 Mar 2026 23:09:12 +0200 Subject: [PATCH] Eliminate Python dependency: embed frontend assets in odoo-go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Copy all OWL frontend assets (JS/CSS/XML/fonts/images) into frontend/ directory (2925 files, 43MB) — no more runtime reads from Python Odoo - Replace OdooAddonsPath config with FrontendDir pointing to local frontend/ - Rewire bundle.go, static.go, templates.go, webclient.go to read from frontend/ instead of external Python Odoo addons directory - Auto-detect frontend/ and build/ dirs relative to binary in main.go - Delete obsolete Python helper scripts (tools/*.py) The Go server is now fully self-contained: single binary + frontend/ folder. Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/odoo-server/main.go | 27 + .../static/demo/bank_opening_statement.pdf | Bin 0 -> 21405 bytes .../demo/bank_statement_one_month_old.pdf | Bin 0 -> 21856 bytes .../demo/in_invoice_yourcompany_demo_1.pdf | Bin 0 -> 43487 bytes .../demo/in_invoice_yourcompany_demo_2.pdf | Bin 0 -> 64476 bytes frontend/account/static/description/icon.png | Bin 0 -> 1949 bytes frontend/account/static/description/icon.svg | 1 + .../account/static/description/icon_hi.png | Bin 0 -> 10461 bytes frontend/account/static/description/l10n.png | Bin 0 -> 4625 bytes frontend/account/static/description/l10n.svg | 1 + .../account_batch_sending_summary.js | 21 + .../account_batch_sending_summary.xml | 15 + .../account_file_uploader.js | 48 + .../account_file_uploader.scss | 7 + .../account_file_uploader.xml | 26 + .../account_merge_wizard_line_one2many.js | 61 + .../account_move_form/account_move_form.js | 94 + .../account_move_form_notebook.xml | 10 + .../account_payment_field/account_payment.xml | 120 + .../account_payment_field.js | 84 + .../account_payment_register_html.js | 23 + .../account_payment_register_html.xml | 6 + .../payment_term_line_ids.js | 25 + .../account_pick_currency_rate.js | 44 + .../account_pick_currency_rate.xml | 15 + .../account_resequence/account_resequence.xml | 28 + .../account_resequence_field.js | 22 + .../account_move_statusbar_secured.js | 25 + .../account_move_statusbar_secured.xml | 35 + ...unt_tax_repartition_line_factor_percent.js | 54 + .../account_type_selection.js | 62 + .../account_type_selection.xml | 13 + .../actionable_errors/actionable_errors.js | 57 + .../actionable_errors/actionable_errors.xml | 26 + .../auto_save_res_partner_bank.js | 17 + .../autosave_many2many_tax_tags.js | 53 + .../src/components/bill_guide/bill_guide.js | 69 + .../src/components/bill_guide/bill_guide.scss | 35 + .../src/components/bill_guide/bill_guide.xml | 82 + .../char_with_placeholder_field.js | 22 + .../char_with_placeholder_field.xml | 10 + .../char_with_placeholder_field.xml | 14 + ...ith_placeholder_field_to_check_to_check.js | 16 + .../currency_form/form_controller.js | 43 + .../open_decimal_precision_btn.js | 24 + .../open_decimal_precision_btn_template.xml | 11 + .../document_file_uploader.js | 78 + .../document_file_uploader.xml | 29 + .../document_state/document_state_field.js | 69 + .../document_state/document_state_field.scss | 10 + .../document_state/document_state_field.xml | 20 + .../dynamic_selection/dynamic_selection.js | 63 + .../fetch_einvoices/fetch_einvoices.xml | 8 + .../fetch_einvoices/fetch_einvoices_cog.js | 59 + .../grouped_view_widget.js | 30 + .../grouped_view_widget.xml | 41 + .../mail_attachments/mail_attachments.js | 74 + .../mail_attachments/mail_attachments.xml | 20 + .../mail_attachments_selector.js | 51 + .../many2many_tags_banks.js | 80 + .../many2many_tags_banks.xml | 25 + .../many2many_tags_journals.js | 56 + .../many2many_tags_journals.xml | 17 + .../many2x_tax_tags/many2x_tax_tags.js | 65 + .../src/components/onboarding/onboarding.js | 35 + .../src/components/onboarding/onboarding.xml | 23 + .../open_move_line_move_widget.js | 35 + .../open_move_line_move_widget.xml | 8 + .../open_move_widget/open_move_widget.js | 27 + .../open_move_widget/open_move_widget.xml | 8 + .../product_catalog/account_move_line.js | 8 + .../product_catalog/kanban_controller.js | 19 + .../product_catalog/kanban_model.js | 26 + .../product_catalog/kanban_record.js | 52 + .../components/product_catalog/kanban_view.js | 9 + .../product_catalog/search/search_model.js | 13 + .../product_catalog/search/search_panel.js | 172 + .../product_catalog/search/search_panel.scss | 22 + .../product_catalog/search/search_panel.xml | 105 + .../product_label_section_and_note_field.js | 86 + .../product_label_section_and_note_field.scss | 14 + .../product_label_section_and_note_field.xml | 66 + ...roduct_label_section_and_note_field_o2m.js | 75 + .../receipt_selector/receipt_selector.js | 93 + .../receipt_selector/receipt_selector.xml | 15 + .../section_and_note_backend.scss | 82 + .../section_and_note_fields_backend.js | 606 + .../section_and_note_fields_backend.xml | 86 + .../tax_autocomplete/tax_autocomplete.xml | 16 + .../src/components/tax_totals/tax_totals.css | 24 + .../src/components/tax_totals/tax_totals.js | 173 + .../src/components/tax_totals/tax_totals.xml | 98 + .../test_shared_js_python.xml | 6 + .../tests_shared_js_python.js | 169 + .../upload_drop_zone/upload_drop_zone.js | 45 + .../upload_drop_zone/upload_drop_zone.scss | 21 + .../upload_drop_zone/upload_drop_zone.xml | 60 + .../x2many_buttons/x2many_buttons.js | 58 + .../x2many_buttons/x2many_buttons.xml | 17 + frontend/account/static/src/css/account.css | 78 + .../static/src/css/account_bank_and_cash.css | 31 + .../static/src/css/account_payment.scss | 13 + .../account/static/src/css/report_invoice.css | 23 + .../account/static/src/helpers/account_tax.js | 2739 ++ .../account/static/src/img/Odoo_logo_O.svg | 4 + .../img/account_dashboard_onboarding_bg.jpg | Bin 0 -> 35204 bytes .../src/img/account_invoice_onboarding_bg.jpg | Bin 0 -> 23728 bytes frontend/account/static/src/img/bank.svg | 35 + frontend/account/static/src/img/bill.svg | 13 + .../static/src/img/btn_paynowcc_lg.gif | Bin 0 -> 2916 bytes frontend/account/static/src/img/graph.png | Bin 0 -> 1722 bytes .../account/static/src/img/invoice-stamps.png | Bin 0 -> 2204 bytes .../account/static/src/img/multi_ledger.svg | 50 + .../static/src/interactions/account_portal.js | 11 + .../src/interactions/account_sidebar.js | 61 + .../src/js/search/search_bar/search_bar.js | 16 + .../account/static/src/js/tours/account.js | 157 + .../account/static/src/js/tours/tour_utils.js | 82 + frontend/account/static/src/scss/account.scss | 4 + .../src/scss/account_journal_dashboard.scss | 82 + .../src/scss/account_move_send_wizard.scss | 10 + .../static/src/scss/account_multi_ledger.scss | 10 + .../static/src/scss/account_payment_term.scss | 28 + .../src/scss/account_reconcile_model.scss | 3 + .../static/src/scss/account_searchpanel.scss | 36 + .../account/static/src/scss/variables.scss | 12 + .../src/services/account_move_service.js | 43 + .../services/account_notification_service.js | 29 + .../account_dashboard_kanban.scss | 14 + .../account_dashboard_kanban_record.js | 60 + .../account_dashboard_kanban_record.xml | 24 + .../account_dashboard_kanban_renderer.js | 45 + .../account_dashboard_kanban_renderer.xml | 12 + .../account_dashboard_kanban_view.js | 10 + .../account_move_kanban_controller.js | 14 + .../account_move_kanban_controller.xml | 9 + .../account_move_kanban_view.js | 11 + .../account_move_list_controller.js | 53 + .../account_move_list_controller.xml | 9 + .../account_move_list_renderer.js | 19 + .../account_move_list_renderer.xml | 18 + .../account_move_list_view.js | 13 + .../views/account_x2many_list_controller.js | 22 + .../file_upload_kanban_controller.js | 9 + .../file_upload_kanban_controller.xml | 9 + .../file_upload_kanban_renderer.js | 34 + .../file_upload_kanban_renderer.xml | 17 + .../file_upload_kanban_view.js | 13 + .../file_upload_list_controller.js | 9 + .../file_upload_list_controller.xml | 9 + .../file_upload_list_renderer.js | 34 + .../file_upload_list_renderer.xml | 17 + .../file_upload_list/file_upload_list_view.js | 13 + .../src/views/upload_file_from_data_hook.js | 32 + .../static/tests/account_move_form.test.js | 126 + .../static/tests/account_test_helpers.js | 13 + .../static/tests/account_widgets.test.js | 188 + .../tests/char_with_placeholder_field.test.js | 78 + .../mock_server/mock_models/account_move.js | 10 + .../mock_models/account_move_line.js | 5 + .../static/tests/section_and_note.test.js | 1515 ++ .../tours/account_product_catalog_tests.js | 35 + .../tests/tours/deductible_amount_column.js | 28 + .../static/tests/tours/tax_group_tests.js | 147 + .../tours/tour_tests_shared_js_python.js | 16 + .../static/tests/x2many_buttons.test.js | 138 + .../static/xls/aml_import_template.xlsx | Bin 0 -> 86991 bytes .../static/xls/coa_import_template.xlsx | Bin 0 -> 88869 bytes ...invoices_credit_notes_import_template.xlsx | Bin 0 -> 6897 bytes .../xls/misc_operations_import_template.xlsx | Bin 0 -> 6666 bytes .../vendor_bills_refunds_import_template.xlsx | Bin 0 -> 6902 bytes frontend/base/static/description/board.png | Bin 0 -> 1299 bytes frontend/base/static/description/board.svg | 1 + .../base/static/description/exception.png | Bin 0 -> 4667 bytes .../base/static/description/exception.svg | 1 + frontend/base/static/description/icon.png | Bin 0 -> 2087 bytes frontend/base/static/description/icon.svg | 1 + frontend/base/static/description/modules.png | Bin 0 -> 2282 bytes frontend/base/static/description/modules.svg | 1 + frontend/base/static/description/settings.png | Bin 0 -> 2063 bytes frontend/base/static/description/settings.svg | 1 + frontend/base/static/img/avatar.png | Bin 0 -> 3019 bytes frontend/base/static/img/avatar_grey.png | Bin 0 -> 2871 bytes frontend/base/static/img/bill.png | Bin 0 -> 3016 bytes frontend/base/static/img/company_image.png | Bin 0 -> 4220 bytes .../base/static/img/country_flags/419.png | Bin 0 -> 15347 bytes frontend/base/static/img/country_flags/ad.png | Bin 0 -> 10684 bytes frontend/base/static/img/country_flags/ae.png | Bin 0 -> 565 bytes frontend/base/static/img/country_flags/af.png | Bin 0 -> 12999 bytes frontend/base/static/img/country_flags/ag.png | Bin 0 -> 6076 bytes frontend/base/static/img/country_flags/ai.png | Bin 0 -> 5016 bytes frontend/base/static/img/country_flags/al.png | Bin 0 -> 10091 bytes frontend/base/static/img/country_flags/am.png | Bin 0 -> 516 bytes frontend/base/static/img/country_flags/an.png | Bin 0 -> 3827 bytes frontend/base/static/img/country_flags/ao.png | Bin 0 -> 5651 bytes frontend/base/static/img/country_flags/ar.png | Bin 0 -> 5200 bytes frontend/base/static/img/country_flags/as.png | Bin 0 -> 9621 bytes frontend/base/static/img/country_flags/at.png | Bin 0 -> 621 bytes frontend/base/static/img/country_flags/au.png | Bin 0 -> 4913 bytes frontend/base/static/img/country_flags/aw.png | Bin 0 -> 2623 bytes frontend/base/static/img/country_flags/ax.png | Bin 0 -> 805 bytes frontend/base/static/img/country_flags/az.png | Bin 0 -> 2410 bytes frontend/base/static/img/country_flags/ba.png | Bin 0 -> 5032 bytes frontend/base/static/img/country_flags/bb.png | Bin 0 -> 3045 bytes frontend/base/static/img/country_flags/bd.png | Bin 0 -> 2675 bytes frontend/base/static/img/country_flags/be.png | Bin 0 -> 620 bytes frontend/base/static/img/country_flags/bf.png | Bin 0 -> 1816 bytes frontend/base/static/img/country_flags/bg.png | Bin 0 -> 577 bytes frontend/base/static/img/country_flags/bh.png | Bin 0 -> 1265 bytes frontend/base/static/img/country_flags/bi.png | Bin 0 -> 4827 bytes frontend/base/static/img/country_flags/bj.png | Bin 0 -> 643 bytes frontend/base/static/img/country_flags/bl.png | Bin 0 -> 624 bytes frontend/base/static/img/country_flags/bm.png | Bin 0 -> 8987 bytes frontend/base/static/img/country_flags/bn.png | Bin 0 -> 8668 bytes frontend/base/static/img/country_flags/bo.png | Bin 0 -> 658 bytes frontend/base/static/img/country_flags/br.png | Bin 0 -> 10118 bytes frontend/base/static/img/country_flags/bs.png | Bin 0 -> 1091 bytes frontend/base/static/img/country_flags/bt.png | Bin 0 -> 22551 bytes frontend/base/static/img/country_flags/bw.png | Bin 0 -> 670 bytes frontend/base/static/img/country_flags/by.png | Bin 0 -> 7583 bytes frontend/base/static/img/country_flags/bz.png | Bin 0 -> 27280 bytes frontend/base/static/img/country_flags/ca.png | Bin 0 -> 2819 bytes frontend/base/static/img/country_flags/cc.png | Bin 0 -> 5689 bytes frontend/base/static/img/country_flags/cd.png | Bin 0 -> 9807 bytes frontend/base/static/img/country_flags/cf.png | Bin 0 -> 1871 bytes frontend/base/static/img/country_flags/cg.png | Bin 0 -> 2972 bytes frontend/base/static/img/country_flags/ch.png | Bin 0 -> 1041 bytes frontend/base/static/img/country_flags/ci.png | Bin 0 -> 640 bytes frontend/base/static/img/country_flags/ck.png | Bin 0 -> 8201 bytes frontend/base/static/img/country_flags/cl.png | Bin 0 -> 1801 bytes frontend/base/static/img/country_flags/cm.png | Bin 0 -> 1666 bytes frontend/base/static/img/country_flags/cn.png | Bin 0 -> 3130 bytes frontend/base/static/img/country_flags/co.png | Bin 0 -> 635 bytes frontend/base/static/img/country_flags/cr.png | Bin 0 -> 585 bytes frontend/base/static/img/country_flags/cu.png | Bin 0 -> 3339 bytes frontend/base/static/img/country_flags/cv.png | Bin 0 -> 3844 bytes frontend/base/static/img/country_flags/cw.png | Bin 0 -> 2300 bytes frontend/base/static/img/country_flags/cx.png | Bin 0 -> 6690 bytes frontend/base/static/img/country_flags/cy.png | Bin 0 -> 5491 bytes frontend/base/static/img/country_flags/cz.png | Bin 0 -> 3034 bytes frontend/base/static/img/country_flags/de.png | Bin 0 -> 552 bytes frontend/base/static/img/country_flags/dj.png | Bin 0 -> 3929 bytes frontend/base/static/img/country_flags/dk.png | Bin 0 -> 717 bytes frontend/base/static/img/country_flags/dm.png | Bin 0 -> 5442 bytes frontend/base/static/img/country_flags/do.png | Bin 0 -> 4924 bytes frontend/base/static/img/country_flags/dz.png | Bin 0 -> 4153 bytes frontend/base/static/img/country_flags/ec.png | Bin 0 -> 11466 bytes frontend/base/static/img/country_flags/ee.png | Bin 0 -> 599 bytes frontend/base/static/img/country_flags/eg.png | Bin 0 -> 4982 bytes frontend/base/static/img/country_flags/eh.png | Bin 0 -> 5121 bytes frontend/base/static/img/country_flags/er.png | Bin 0 -> 6905 bytes frontend/base/static/img/country_flags/es.png | Bin 0 -> 7593 bytes frontend/base/static/img/country_flags/et.png | Bin 0 -> 6440 bytes frontend/base/static/img/country_flags/fi.png | Bin 0 -> 714 bytes frontend/base/static/img/country_flags/fj.png | Bin 0 -> 7890 bytes frontend/base/static/img/country_flags/fk.png | Bin 0 -> 10970 bytes frontend/base/static/img/country_flags/fm.png | Bin 0 -> 2875 bytes frontend/base/static/img/country_flags/fo.png | Bin 0 -> 929 bytes frontend/base/static/img/country_flags/fr.png | Bin 0 -> 624 bytes frontend/base/static/img/country_flags/ga.png | Bin 0 -> 709 bytes frontend/base/static/img/country_flags/gb.png | Bin 0 -> 1830 bytes frontend/base/static/img/country_flags/gd.png | Bin 0 -> 6094 bytes frontend/base/static/img/country_flags/ge.png | Bin 0 -> 2977 bytes frontend/base/static/img/country_flags/gg.png | Bin 0 -> 1475 bytes frontend/base/static/img/country_flags/gh.png | Bin 0 -> 1974 bytes frontend/base/static/img/country_flags/gi.png | Bin 0 -> 4575 bytes frontend/base/static/img/country_flags/gl.png | Bin 0 -> 3200 bytes frontend/base/static/img/country_flags/gm.png | Bin 0 -> 659 bytes frontend/base/static/img/country_flags/gn.png | Bin 0 -> 646 bytes frontend/base/static/img/country_flags/gq.png | Bin 0 -> 5765 bytes frontend/base/static/img/country_flags/gr.png | Bin 0 -> 924 bytes frontend/base/static/img/country_flags/gs.png | Bin 0 -> 13470 bytes frontend/base/static/img/country_flags/gt.png | Bin 0 -> 6494 bytes frontend/base/static/img/country_flags/gu.png | Bin 0 -> 6044 bytes frontend/base/static/img/country_flags/gw.png | Bin 0 -> 1564 bytes frontend/base/static/img/country_flags/gy.png | Bin 0 -> 3201 bytes frontend/base/static/img/country_flags/hk.png | Bin 0 -> 7104 bytes frontend/base/static/img/country_flags/hn.png | Bin 0 -> 2119 bytes frontend/base/static/img/country_flags/hr.png | Bin 0 -> 4483 bytes frontend/base/static/img/country_flags/ht.png | Bin 0 -> 4889 bytes frontend/base/static/img/country_flags/hu.png | Bin 0 -> 530 bytes frontend/base/static/img/country_flags/id.png | Bin 0 -> 627 bytes frontend/base/static/img/country_flags/ie.png | Bin 0 -> 524 bytes frontend/base/static/img/country_flags/il.png | Bin 0 -> 3622 bytes frontend/base/static/img/country_flags/im.png | Bin 0 -> 5583 bytes frontend/base/static/img/country_flags/in.png | Bin 0 -> 4993 bytes frontend/base/static/img/country_flags/io.png | Bin 0 -> 22981 bytes frontend/base/static/img/country_flags/iq.png | Bin 0 -> 3262 bytes frontend/base/static/img/country_flags/ir.png | Bin 0 -> 9740 bytes .../base/static/img/country_flags/iran.png | Bin 0 -> 6608 bytes frontend/base/static/img/country_flags/is.png | Bin 0 -> 710 bytes frontend/base/static/img/country_flags/it.png | Bin 0 -> 628 bytes frontend/base/static/img/country_flags/je.png | Bin 0 -> 9668 bytes frontend/base/static/img/country_flags/jm.png | Bin 0 -> 1361 bytes frontend/base/static/img/country_flags/jo.png | Bin 0 -> 1544 bytes frontend/base/static/img/country_flags/jp.png | Bin 0 -> 3017 bytes frontend/base/static/img/country_flags/ke.png | Bin 0 -> 5372 bytes frontend/base/static/img/country_flags/kg.png | Bin 0 -> 11240 bytes frontend/base/static/img/country_flags/kh.png | Bin 0 -> 8625 bytes frontend/base/static/img/country_flags/ki.png | Bin 0 -> 7003 bytes frontend/base/static/img/country_flags/km.png | Bin 0 -> 4185 bytes frontend/base/static/img/country_flags/kn.png | Bin 0 -> 8605 bytes frontend/base/static/img/country_flags/kp.png | Bin 0 -> 2936 bytes frontend/base/static/img/country_flags/kr.png | Bin 0 -> 10230 bytes frontend/base/static/img/country_flags/kw.png | Bin 0 -> 917 bytes frontend/base/static/img/country_flags/ky.png | Bin 0 -> 10270 bytes frontend/base/static/img/country_flags/kz.png | Bin 0 -> 12241 bytes frontend/base/static/img/country_flags/la.png | Bin 0 -> 2312 bytes frontend/base/static/img/country_flags/lb.png | Bin 0 -> 5871 bytes frontend/base/static/img/country_flags/lc.png | Bin 0 -> 3366 bytes frontend/base/static/img/country_flags/li.png | Bin 0 -> 4894 bytes frontend/base/static/img/country_flags/lk.png | Bin 0 -> 10643 bytes frontend/base/static/img/country_flags/lr.png | Bin 0 -> 1605 bytes frontend/base/static/img/country_flags/ls.png | Bin 0 -> 3716 bytes frontend/base/static/img/country_flags/lt.png | Bin 0 -> 580 bytes frontend/base/static/img/country_flags/lu.png | Bin 0 -> 577 bytes frontend/base/static/img/country_flags/lv.png | Bin 0 -> 501 bytes frontend/base/static/img/country_flags/ly.png | Bin 0 -> 1818 bytes frontend/base/static/img/country_flags/ma.png | Bin 0 -> 2410 bytes frontend/base/static/img/country_flags/mc.png | Bin 0 -> 344 bytes frontend/base/static/img/country_flags/md.png | Bin 0 -> 7071 bytes frontend/base/static/img/country_flags/me.png | Bin 0 -> 10461 bytes frontend/base/static/img/country_flags/mg.png | Bin 0 -> 653 bytes frontend/base/static/img/country_flags/mh.png | Bin 0 -> 9616 bytes frontend/base/static/img/country_flags/mk.png | Bin 0 -> 5122 bytes frontend/base/static/img/country_flags/ml.png | Bin 0 -> 646 bytes frontend/base/static/img/country_flags/mm.png | Bin 0 -> 3437 bytes frontend/base/static/img/country_flags/mn.png | Bin 0 -> 3307 bytes frontend/base/static/img/country_flags/mo.png | Bin 0 -> 6937 bytes frontend/base/static/img/country_flags/mp.png | Bin 0 -> 18512 bytes frontend/base/static/img/country_flags/mq.png | Bin 0 -> 15014 bytes frontend/base/static/img/country_flags/mr.png | Bin 0 -> 3990 bytes frontend/base/static/img/country_flags/ms.png | Bin 0 -> 5368 bytes frontend/base/static/img/country_flags/mt.png | Bin 0 -> 2428 bytes frontend/base/static/img/country_flags/mu.png | Bin 0 -> 651 bytes frontend/base/static/img/country_flags/mv.png | Bin 0 -> 2211 bytes frontend/base/static/img/country_flags/mw.png | Bin 0 -> 6473 bytes frontend/base/static/img/country_flags/mx.png | Bin 0 -> 8638 bytes frontend/base/static/img/country_flags/my.png | Bin 0 -> 4669 bytes frontend/base/static/img/country_flags/mz.png | Bin 0 -> 7438 bytes frontend/base/static/img/country_flags/na.png | Bin 0 -> 10245 bytes frontend/base/static/img/country_flags/nc.png | Bin 0 -> 5162 bytes frontend/base/static/img/country_flags/ne.png | Bin 0 -> 2301 bytes frontend/base/static/img/country_flags/nf.png | Bin 0 -> 8480 bytes frontend/base/static/img/country_flags/ng.png | Bin 0 -> 521 bytes frontend/base/static/img/country_flags/ni.png | Bin 0 -> 3935 bytes frontend/base/static/img/country_flags/nl.png | Bin 0 -> 644 bytes frontend/base/static/img/country_flags/no.png | Bin 0 -> 923 bytes frontend/base/static/img/country_flags/np.png | Bin 0 -> 9477 bytes frontend/base/static/img/country_flags/nr.png | Bin 0 -> 2301 bytes frontend/base/static/img/country_flags/nu.png | Bin 0 -> 3661 bytes frontend/base/static/img/country_flags/nz.png | Bin 0 -> 4179 bytes frontend/base/static/img/country_flags/om.png | Bin 0 -> 3018 bytes frontend/base/static/img/country_flags/pa.png | Bin 0 -> 2695 bytes frontend/base/static/img/country_flags/pe.png | Bin 0 -> 626 bytes frontend/base/static/img/country_flags/pf.png | Bin 0 -> 8044 bytes frontend/base/static/img/country_flags/pg.png | Bin 0 -> 9189 bytes frontend/base/static/img/country_flags/ph.png | Bin 0 -> 5714 bytes frontend/base/static/img/country_flags/pk.png | Bin 0 -> 4347 bytes frontend/base/static/img/country_flags/pl.png | Bin 0 -> 566 bytes frontend/base/static/img/country_flags/pm.png | Bin 0 -> 47044 bytes frontend/base/static/img/country_flags/pn.png | Bin 0 -> 12876 bytes frontend/base/static/img/country_flags/pr.png | Bin 0 -> 4263 bytes frontend/base/static/img/country_flags/ps.png | Bin 0 -> 2932 bytes frontend/base/static/img/country_flags/pt.png | Bin 0 -> 10133 bytes frontend/base/static/img/country_flags/pw.png | Bin 0 -> 2797 bytes frontend/base/static/img/country_flags/py.png | Bin 0 -> 4690 bytes frontend/base/static/img/country_flags/qa.png | Bin 0 -> 1864 bytes frontend/base/static/img/country_flags/ro.png | Bin 0 -> 646 bytes frontend/base/static/img/country_flags/rs.png | Bin 0 -> 15497 bytes frontend/base/static/img/country_flags/ru.png | Bin 0 -> 609 bytes frontend/base/static/img/country_flags/rw.png | Bin 0 -> 5152 bytes frontend/base/static/img/country_flags/sa.png | Bin 0 -> 11354 bytes frontend/base/static/img/country_flags/sb.png | Bin 0 -> 3935 bytes frontend/base/static/img/country_flags/sc.png | Bin 0 -> 1738 bytes frontend/base/static/img/country_flags/sd.png | Bin 0 -> 1268 bytes frontend/base/static/img/country_flags/se.png | Bin 0 -> 701 bytes frontend/base/static/img/country_flags/sg.png | Bin 0 -> 3652 bytes frontend/base/static/img/country_flags/sh.png | Bin 0 -> 9050 bytes frontend/base/static/img/country_flags/si.png | Bin 0 -> 2735 bytes frontend/base/static/img/country_flags/sk.png | Bin 0 -> 4718 bytes frontend/base/static/img/country_flags/sl.png | Bin 0 -> 650 bytes frontend/base/static/img/country_flags/sm.png | Bin 0 -> 20023 bytes frontend/base/static/img/country_flags/sn.png | Bin 0 -> 1816 bytes frontend/base/static/img/country_flags/so.png | Bin 0 -> 2424 bytes frontend/base/static/img/country_flags/sr.png | Bin 0 -> 2262 bytes frontend/base/static/img/country_flags/ss.png | Bin 0 -> 3272 bytes frontend/base/static/img/country_flags/st.png | Bin 0 -> 2226 bytes frontend/base/static/img/country_flags/sv.png | Bin 0 -> 4898 bytes frontend/base/static/img/country_flags/sx.png | Bin 0 -> 9464 bytes frontend/base/static/img/country_flags/sy.png | Bin 0 -> 1693 bytes frontend/base/static/img/country_flags/sz.png | Bin 0 -> 8428 bytes frontend/base/static/img/country_flags/tc.png | Bin 0 -> 5539 bytes frontend/base/static/img/country_flags/td.png | Bin 0 -> 643 bytes frontend/base/static/img/country_flags/tf.png | Bin 0 -> 4766 bytes frontend/base/static/img/country_flags/tg.png | Bin 0 -> 2216 bytes frontend/base/static/img/country_flags/th.png | Bin 0 -> 645 bytes frontend/base/static/img/country_flags/tj.png | Bin 0 -> 2872 bytes frontend/base/static/img/country_flags/tk.png | Bin 0 -> 4943 bytes frontend/base/static/img/country_flags/tl.png | Bin 0 -> 2602 bytes frontend/base/static/img/country_flags/tm.png | Bin 0 -> 19625 bytes frontend/base/static/img/country_flags/tn.png | Bin 0 -> 5062 bytes frontend/base/static/img/country_flags/to.png | Bin 0 -> 757 bytes frontend/base/static/img/country_flags/tr.png | Bin 0 -> 4157 bytes frontend/base/static/img/country_flags/tt.png | Bin 0 -> 9007 bytes frontend/base/static/img/country_flags/tv.png | Bin 0 -> 5977 bytes frontend/base/static/img/country_flags/tw.png | Bin 0 -> 6529 bytes frontend/base/static/img/country_flags/tz.png | Bin 0 -> 6370 bytes frontend/base/static/img/country_flags/ua.png | Bin 0 -> 628 bytes frontend/base/static/img/country_flags/ug.png | Bin 0 -> 4199 bytes frontend/base/static/img/country_flags/uk.png | Bin 0 -> 4027 bytes frontend/base/static/img/country_flags/us.png | Bin 0 -> 8477 bytes frontend/base/static/img/country_flags/uy.png | Bin 0 -> 8347 bytes frontend/base/static/img/country_flags/uz.png | Bin 0 -> 1552 bytes frontend/base/static/img/country_flags/va.png | Bin 0 -> 14564 bytes frontend/base/static/img/country_flags/vc.png | Bin 0 -> 2681 bytes frontend/base/static/img/country_flags/ve.png | Bin 0 -> 3851 bytes frontend/base/static/img/country_flags/vg.png | Bin 0 -> 10430 bytes frontend/base/static/img/country_flags/vi.png | Bin 0 -> 22969 bytes frontend/base/static/img/country_flags/vn.png | Bin 0 -> 2734 bytes frontend/base/static/img/country_flags/vu.png | Bin 0 -> 7570 bytes frontend/base/static/img/country_flags/wf.png | Bin 0 -> 3836 bytes frontend/base/static/img/country_flags/ws.png | Bin 0 -> 2261 bytes frontend/base/static/img/country_flags/xk.png | Bin 0 -> 2581 bytes frontend/base/static/img/country_flags/ye.png | Bin 0 -> 617 bytes frontend/base/static/img/country_flags/za.png | Bin 0 -> 5823 bytes frontend/base/static/img/country_flags/zm.png | Bin 0 -> 3924 bytes frontend/base/static/img/country_flags/zw.png | Bin 0 -> 4570 bytes frontend/base/static/img/demo_logo_report.png | Bin 0 -> 10252 bytes .../static/img/icons/account_accountant.png | Bin 0 -> 2228 bytes .../base/static/img/icons/appointment.png | Bin 0 -> 3576 bytes frontend/base/static/img/icons/helpdesk.png | Bin 0 -> 1095 bytes .../base/static/img/icons/hr_appraisal.png | Bin 0 -> 2319 bytes .../base/static/img/icons/industry_fsm.png | Bin 0 -> 2398 bytes frontend/base/static/img/icons/knowledge.png | Bin 0 -> 1346 bytes .../static/img/icons/marketing_automation.png | Bin 0 -> 1417 bytes frontend/base/static/img/icons/mrp_plm.png | Bin 0 -> 1973 bytes .../base/static/img/icons/mrp_workorder.png | Bin 0 -> 1268 bytes .../img/icons/payment_sepa_direct_debit.png | Bin 0 -> 1064 bytes frontend/base/static/img/icons/planning.png | Bin 0 -> 1683 bytes .../base/static/img/icons/quality_control.png | Bin 0 -> 2106 bytes .../base/static/img/icons/sale_amazon.png | Bin 0 -> 1169 bytes frontend/base/static/img/icons/sale_ebay.png | Bin 0 -> 1363 bytes .../static/img/icons/sale_subscription.png | Bin 0 -> 3080 bytes frontend/base/static/img/icons/sign.png | Bin 0 -> 2798 bytes frontend/base/static/img/icons/social.png | Bin 0 -> 1804 bytes .../base/static/img/icons/stock_barcode.png | Bin 0 -> 535 bytes .../base/static/img/icons/timesheet_grid.png | Bin 0 -> 3494 bytes frontend/base/static/img/icons/voip.png | Bin 0 -> 2780 bytes frontend/base/static/img/icons/web_mobile.png | Bin 0 -> 1392 bytes frontend/base/static/img/icons/web_studio.png | Bin 0 -> 1982 bytes .../static/img/icons/website_form_editor.png | Bin 0 -> 5565 bytes .../base/static/img/icons/website_version.png | Bin 0 -> 10836 bytes .../base/static/img/lang_flags/lang_ar.png | Bin 0 -> 9152 bytes .../base/static/img/lang_flags/lang_ca.png | Bin 0 -> 192 bytes frontend/base/static/img/logo_sample.png | Bin 0 -> 4303 bytes frontend/base/static/img/logo_white.png | Bin 0 -> 7321 bytes .../base/static/img/main_partner-image.png | Bin 0 -> 7924 bytes frontend/base/static/img/money.png | Bin 0 -> 11003 bytes .../img/onboarding_accounting-periods.png | Bin 0 -> 1754 bytes .../base/static/img/onboarding_calendar.png | Bin 0 -> 4315 bytes .../img/onboarding_chart-of-accounts.png | Bin 0 -> 2328 bytes frontend/base/static/img/onboarding_cog.png | Bin 0 -> 7274 bytes .../static/img/onboarding_company-data.png | Bin 0 -> 1733 bytes .../base/static/img/onboarding_confetti.svg | 1 + .../base/static/img/onboarding_default.png | Bin 0 -> 2060 bytes .../static/img/onboarding_looking_glass.png | Bin 0 -> 5922 bytes .../base/static/img/onboarding_puzzle.png | Bin 0 -> 1619 bytes .../img/onboarding_quotation-layout.png | Bin 0 -> 1951 bytes .../img/onboarding_sample-quotation.png | Bin 0 -> 998 bytes frontend/base/static/img/onboarding_taxes.png | Bin 0 -> 2505 bytes .../base/static/img/partner_demo_portal.png | Bin 0 -> 52528 bytes frontend/base/static/img/partner_lightsup.png | Bin 0 -> 1963 bytes .../base/static/img/partner_open_wood.png | Bin 0 -> 4724 bytes .../base/static/img/partner_root-image.png | Bin 0 -> 68639 bytes .../base/static/img/public_user-image.png | 0 frontend/base/static/img/puzzle.png | Bin 0 -> 2985 bytes frontend/base/static/img/res_company_logo.png | Bin 0 -> 11445 bytes .../base/static/img/res_partner_1-image.png | Bin 0 -> 8135 bytes .../base/static/img/res_partner_10-image.jpg | Bin 0 -> 4755 bytes .../base/static/img/res_partner_12-image.png | Bin 0 -> 1464 bytes .../base/static/img/res_partner_18-image.png | Bin 0 -> 1277 bytes .../base/static/img/res_partner_2-image.png | Bin 0 -> 7131 bytes .../base/static/img/res_partner_3-image.png | Bin 0 -> 6301 bytes .../base/static/img/res_partner_4-image.png | Bin 0 -> 13440 bytes .../base/static/img/res_partner_address_1.jpg | Bin 0 -> 3598 bytes .../static/img/res_partner_address_10.jpg | Bin 0 -> 5616 bytes .../static/img/res_partner_address_11.jpg | Bin 0 -> 5084 bytes .../static/img/res_partner_address_13.jpg | Bin 0 -> 23707 bytes .../static/img/res_partner_address_14.jpg | Bin 0 -> 6322 bytes .../static/img/res_partner_address_15.jpg | Bin 0 -> 4563 bytes .../static/img/res_partner_address_16.jpg | Bin 0 -> 4105 bytes .../static/img/res_partner_address_17.jpg | Bin 0 -> 4513 bytes .../static/img/res_partner_address_18.jpg | Bin 0 -> 4051 bytes .../base/static/img/res_partner_address_2.jpg | Bin 0 -> 4752 bytes .../static/img/res_partner_address_24.jpg | Bin 0 -> 4726 bytes .../static/img/res_partner_address_25.jpg | Bin 0 -> 6516 bytes .../static/img/res_partner_address_27.jpg | Bin 0 -> 4410 bytes .../static/img/res_partner_address_28.jpg | Bin 0 -> 5157 bytes .../base/static/img/res_partner_address_3.jpg | Bin 0 -> 4896 bytes .../static/img/res_partner_address_30.jpg | Bin 0 -> 6492 bytes .../static/img/res_partner_address_31.jpg | Bin 0 -> 4291 bytes .../static/img/res_partner_address_32.jpg | Bin 0 -> 3767 bytes .../static/img/res_partner_address_33.jpg | Bin 0 -> 4857 bytes .../static/img/res_partner_address_34.jpg | Bin 0 -> 4612 bytes .../base/static/img/res_partner_address_4.jpg | Bin 0 -> 5433 bytes .../base/static/img/res_partner_address_5.jpg | Bin 0 -> 5422 bytes .../base/static/img/res_partner_address_7.jpg | Bin 0 -> 5615 bytes .../base/static/img/res_partner_main1.jpg | Bin 0 -> 3200 bytes .../base/static/img/res_partner_main2.jpg | Bin 0 -> 18074 bytes frontend/base/static/img/truck.png | Bin 0 -> 4101 bytes frontend/base/static/img/user-slash.png | Bin 0 -> 4703 bytes frontend/base/static/img/user_demo-image.png | Bin 0 -> 63193 bytes frontend/base/static/src/css/description.css | 725 + frontend/base/static/src/css/description.sass | 575 + frontend/base/static/src/css/modules.css | 30 + .../base/static/src/scss/res_partner.scss | 8 + frontend/base/static/src/scss/res_users.scss | 5 + .../tests/test_ir_model_fields_translation.js | 42 + .../static/xls/contacts_import_template.xlsx | Bin 0 -> 38715 bytes frontend/crm/static/description/icon.png | Bin 0 -> 1819 bytes frontend/crm/static/description/icon.svg | 1 + frontend/crm/static/description/icon_hi.png | Bin 0 -> 9234 bytes .../crm/static/src/activity_menu_patch.js | 55 + .../static/src/core/common/crm_lead_model.js | 19 + .../core/common/res_partner_model_patch.js | 11 + frontend/crm/static/src/img/milk-autofill.gif | Bin 0 -> 24903 bytes .../static/src/img/milk-generate-leads.gif | Bin 0 -> 32059 bytes .../static/src/img/milk-mapview-toggle.gif | Bin 0 -> 57785 bytes .../static/src/img/milk-pipeline-progress.gif | Bin 0 -> 30098 bytes .../static/src/img/milk-probability-rate.gif | Bin 0 -> 41939 bytes .../static/src/img/pls-tooltip-ai-icon.png | Bin 0 -> 2226 bytes .../js/fields/many2one_avatar_leader_user.js | 31 + frontend/crm/static/src/js/tours/crm.js | 101 + frontend/crm/static/src/scss/crm.scss | 16 + frontend/crm/static/src/scss/crm_team.scss | 11 + .../src/scss/crm_team_member_views.scss | 6 + .../src/views/check_rainbowman_message.js | 9 + .../crm/static/src/views/crm_form/crm_form.js | 70 + .../static/src/views/crm_form/crm_form.scss | 5 + .../views/crm_form/crm_pls_tooltip_button.js | 76 + .../crm_form/crm_pls_tooltip_button.scss | 33 + .../views/crm_form/crm_pls_tooltip_button.xml | 147 + .../views/crm_kanban/crm_column_progress.js | 25 + .../views/crm_kanban/crm_column_progress.xml | 41 + .../crm_kanban/crm_kanban_arch_parser.js | 14 + .../src/views/crm_kanban/crm_kanban_model.js | 35 + .../views/crm_kanban/crm_kanban_renderer.js | 19 + .../src/views/crm_kanban/crm_kanban_view.js | 25 + .../static/src/views/fill_temporal_service.js | 304 + .../forecast_graph/forecast_graph_view.js | 10 + .../forecast_kanban_column_quick_create.js | 22 + .../forecast_kanban_controller.js | 7 + .../forecast_kanban/forecast_kanban_model.js | 78 + .../forecast_kanban_renderer.js | 50 + .../forecast_kanban_renderer.xml | 16 + .../forecast_kanban/forecast_kanban_view.js | 18 + .../views/forecast_list/forecast_list_view.js | 10 + .../forecast_pivot/forecast_pivot_view.js | 10 + .../static/src/views/forecast_search_model.js | 86 + ..._kanban_progress_bar_mrr_sum_field.test.js | 282 + frontend/crm/static/tests/crm_mock_server.js | 70 + .../crm/static/tests/crm_rainbowman.test.js | 444 + frontend/crm/static/tests/crm_test_helpers.js | 12 + .../crm/static/tests/forecast_kanban.test.js | 371 + .../crm/static/tests/forecast_view.test.js | 119 + .../tests/mock_server/mock_models/crm_lead.js | 13 + .../tests/tours/create_crm_team_tour.js | 38 + .../tours/crm_email_and_phone_propagation.js | 25 + .../static/tests/tours/crm_forecast_tour.js | 93 + .../crm/static/tests/tours/crm_rainbowman.js | 119 + frontend/crm/static/xls/crm_lead.xls | Bin 0 -> 33792 bytes frontend/fleet/static/description/icon.png | Bin 0 -> 4226 bytes frontend/fleet/static/description/icon.svg | 1 + frontend/fleet/static/description/icon_hi.png | Bin 0 -> 25330 bytes .../fleet/static/img/brand_abarth-image.png | Bin 0 -> 16548 bytes .../fleet/static/img/brand_acura-image.png | Bin 0 -> 12635 bytes .../fleet/static/img/brand_alfa-image.png | Bin 0 -> 17115 bytes .../fleet/static/img/brand_audi-image.png | Bin 0 -> 12142 bytes .../fleet/static/img/brand_austin-image.png | Bin 0 -> 9262 bytes .../fleet/static/img/brand_bentley-image.png | Bin 0 -> 12632 bytes frontend/fleet/static/img/brand_bmw-image.png | Bin 0 -> 11751 bytes .../fleet/static/img/brand_bugatti-image.png | Bin 0 -> 13638 bytes .../fleet/static/img/brand_buick-image.png | Bin 0 -> 13088 bytes frontend/fleet/static/img/brand_byd-image.png | Bin 0 -> 12047 bytes .../fleet/static/img/brand_cadillac-image.png | Bin 0 -> 15670 bytes .../static/img/brand_chevrolet-image.png | Bin 0 -> 11995 bytes .../fleet/static/img/brand_chrysler-image.png | Bin 0 -> 6563 bytes .../fleet/static/img/brand_citroen-image.png | Bin 0 -> 11599 bytes .../img/brand_corre-la-licorne-image.png | Bin 0 -> 16878 bytes .../fleet/static/img/brand_daewoo-image.png | Bin 0 -> 10848 bytes .../fleet/static/img/brand_dodge-image.png | Bin 0 -> 11493 bytes .../fleet/static/img/brand_ferrari-image.png | Bin 0 -> 4377 bytes .../fleet/static/img/brand_fiat-image.png | Bin 0 -> 14732 bytes .../fleet/static/img/brand_ford-image.png | Bin 0 -> 11242 bytes frontend/fleet/static/img/brand_gmc-image.png | Bin 0 -> 9455 bytes .../fleet/static/img/brand_holden-image.png | Bin 0 -> 14675 bytes .../fleet/static/img/brand_honda-image.png | Bin 0 -> 10405 bytes .../fleet/static/img/brand_hyundai-image.png | Bin 0 -> 12776 bytes .../fleet/static/img/brand_infiniti-image.png | Bin 0 -> 8192 bytes .../fleet/static/img/brand_isuzu-image.png | Bin 0 -> 7364 bytes .../fleet/static/img/brand_jaguar-image.png | Bin 0 -> 7853 bytes .../fleet/static/img/brand_jeep-image.png | Bin 0 -> 7917 bytes frontend/fleet/static/img/brand_kia-image.png | Bin 0 -> 11420 bytes .../static/img/brand_koenigsegg-image.png | Bin 0 -> 11436 bytes .../fleet/static/img/brand_lagonda-image.png | Bin 0 -> 6667 bytes .../static/img/brand_lamborghini-image.png | Bin 0 -> 13958 bytes .../fleet/static/img/brand_lancia-image.png | Bin 0 -> 16723 bytes .../static/img/brand_land-rover-image.png | Bin 0 -> 13232 bytes .../fleet/static/img/brand_lexus-image.png | Bin 0 -> 8365 bytes .../fleet/static/img/brand_lincoln-image.png | Bin 0 -> 6365 bytes .../fleet/static/img/brand_lotus-image.png | Bin 0 -> 19564 bytes .../fleet/static/img/brand_maserati-image.png | Bin 0 -> 11647 bytes .../fleet/static/img/brand_maybach-image.png | Bin 0 -> 14409 bytes .../fleet/static/img/brand_mazda-image.png | Bin 0 -> 14753 bytes .../fleet/static/img/brand_mercedes-image.png | Bin 0 -> 11633 bytes frontend/fleet/static/img/brand_mg-image.png | Bin 0 -> 15882 bytes .../fleet/static/img/brand_mini-image.png | Bin 0 -> 7260 bytes .../static/img/brand_mitsubishi-image.png | Bin 0 -> 8347 bytes .../fleet/static/img/brand_morgan-image.png | Bin 0 -> 7113 bytes .../fleet/static/img/brand_nissan-image.png | Bin 0 -> 14143 bytes .../static/img/brand_oldsmobile-image.png | Bin 0 -> 9707 bytes .../fleet/static/img/brand_opel-image.png | Bin 0 -> 9202 bytes .../fleet/static/img/brand_peugeot-image.png | Bin 0 -> 11485 bytes .../fleet/static/img/brand_pontiac-image.png | Bin 0 -> 6546 bytes .../fleet/static/img/brand_porsche-image.png | Bin 0 -> 5717 bytes .../fleet/static/img/brand_rambler-image.png | Bin 0 -> 12369 bytes .../fleet/static/img/brand_renault-image.png | Bin 0 -> 8495 bytes .../static/img/brand_rolls-royce-image.png | Bin 0 -> 9000 bytes .../fleet/static/img/brand_saab-image.png | Bin 0 -> 15340 bytes .../fleet/static/img/brand_scion-image.png | Bin 0 -> 12282 bytes .../fleet/static/img/brand_skoda-image.png | Bin 0 -> 14828 bytes .../fleet/static/img/brand_smart-image.png | Bin 0 -> 6724 bytes .../fleet/static/img/brand_steyr-image.png | Bin 0 -> 17721 bytes .../fleet/static/img/brand_subaru-image.png | Bin 0 -> 12338 bytes .../fleet/static/img/brand_suzuki-image.png | Bin 0 -> 9976 bytes .../static/img/brand_tesla-motors-image.png | Bin 0 -> 11825 bytes .../fleet/static/img/brand_toyota-image.png | Bin 0 -> 15101 bytes .../fleet/static/img/brand_trabant-image.png | Bin 0 -> 12561 bytes .../static/img/brand_volkswagen-image.png | Bin 0 -> 13034 bytes .../fleet/static/img/brand_volvo-image.png | Bin 0 -> 14778 bytes .../fleet/static/img/brand_willys-image.png | Bin 0 -> 11133 bytes frontend/fleet/static/src/js/fleet_form.js | 32 + .../fleet/static/src/scss/fleet_form.scss | 21 + frontend/hr/static/description/icon.png | Bin 0 -> 2099 bytes frontend/hr/static/description/icon.svg | 1 + frontend/hr/static/description/icon_hi.png | Bin 0 -> 11963 bytes frontend/hr/static/img/employee-image.png | Bin 0 -> 18128 bytes frontend/hr/static/img/employee_al-image.jpg | Bin 0 -> 5728 bytes frontend/hr/static/img/employee_awa-image.jpg | Bin 0 -> 7675 bytes frontend/hr/static/img/employee_chs-image.jpg | Bin 0 -> 5874 bytes frontend/hr/static/img/employee_fme-image.jpg | Bin 0 -> 2843 bytes frontend/hr/static/img/employee_fpi-image.jpg | Bin 0 -> 4035 bytes frontend/hr/static/img/employee_han-image.jpg | Bin 0 -> 16867 bytes frontend/hr/static/img/employee_hne-image.jpg | Bin 0 -> 6769 bytes frontend/hr/static/img/employee_jep-image.jpg | Bin 0 -> 4492 bytes frontend/hr/static/img/employee_jgo-image.jpg | Bin 0 -> 5024 bytes frontend/hr/static/img/employee_jod-image.jpg | Bin 0 -> 3428 bytes frontend/hr/static/img/employee_jog-image.jpg | Bin 0 -> 2967 bytes frontend/hr/static/img/employee_jth-image.jpg | Bin 0 -> 3473 bytes frontend/hr/static/img/employee_jve-image.jpg | Bin 0 -> 3805 bytes frontend/hr/static/img/employee_lur-image.jpg | Bin 0 -> 4537 bytes frontend/hr/static/img/employee_mit-image.jpg | Bin 0 -> 5270 bytes frontend/hr/static/img/employee_ngh-image.jpg | Bin 0 -> 3733 bytes frontend/hr/static/img/employee_niv-image.jpg | Bin 0 -> 4071 bytes frontend/hr/static/img/employee_qdp-image.png | Bin 0 -> 20366 bytes frontend/hr/static/img/employee_stw-image.jpg | Bin 0 -> 4922 bytes frontend/hr/static/img/employee_vad-image.jpg | Bin 0 -> 4859 bytes frontend/hr/static/img/partner_root-image.jpg | Bin 0 -> 4643 bytes frontend/hr/static/src/@types/models.d.ts | 5 + .../avatar_card/avatar_card_popover_patch.js | 28 + .../avatar_card/avatar_card_popover_patch.xml | 18 + .../avatar_card_employee_popover.js | 17 + .../avatar_card_resource_popover.xml | 23 + .../avatar_card_resource_popover_patch.js | 49 + .../avatar_employee/avatar_employee.js | 13 + .../background_image/background_image.js | 14 + .../background_image/background_image.scss | 21 + .../background_image/background_image.xml | 13 + .../button_new_contract.js | 68 + .../button_new_contract.xml | 9 + .../department_chart/department_chart.js | 48 + .../department_chart/department_chart.scss | 16 + .../department_chart/department_chart.xml | 48 + .../components/employee_chat/employee_chat.js | 22 + .../employee_chat/employee_chat.xml | 14 + .../float_without_trailing_zeros.js | 14 + .../hr_presence_status/hr_presence_status.js | 76 + .../hr_presence_status/hr_presence_status.xml | 10 + .../hr_presence_status_pill.js | 35 + .../hr_presence_status_pill.xml | 11 + .../hr_presence_status_private.js | 11 + .../hr_presence_status_private_pill.js | 14 + .../many2many_tags_salary_bank.js | 79 + .../radio_image_field/radio_image_field.js | 11 + .../radio_image_field/radio_image_field.xml | 36 + .../versions_timeline/versions_timeline.js | 129 + .../versions_timeline/versions_timeline.scss | 204 + .../versions_timeline/versions_timeline.xml | 75 + .../work_permit_upload/work_permit_upload.js | 13 + .../work_permit_upload/work_permit_upload.xml | 10 + .../static/src/core/common/@types/models.d.ts | 30 + .../src/core/common/hr_department_model.js | 13 + .../src/core/common/hr_employee_model.js | 23 + .../src/core/common/hr_work_location_model.js | 15 + .../core/common/res_partner_model_patch.js | 25 + .../src/core/common/res_users_model_patch.js | 22 + .../hr/static/src/core/web/thread_actions.js | 34 + frontend/hr/static/src/default_image.png | 0 .../hr/static/src/fields/boolean_radio.js | 72 + .../hr/static/src/fields/boolean_radio.xml | 13 + .../src/fields/radio_followed_by_element.js | 75 + frontend/hr/static/src/img/default_image.png | Bin 0 -> 2723 bytes frontend/hr/static/src/img/icons/hatched.svg | 1 + .../hr/static/src/img/icons/hatched_dark.svg | 1 + frontend/hr/static/src/img/icons/line.svg | 1 + .../hr/static/src/img/icons/line_dark.svg | 1 + frontend/hr/static/src/img/icons/plain.svg | 1 + .../hr/static/src/img/icons/plain_dark.svg | 1 + frontend/hr/static/src/scss/hr.scss | 109 + .../static/src/scss/res_config_settings.scss | 3 + frontend/hr/static/src/scss/res_users.scss | 6 + frontend/hr/static/src/store_service_patch.js | 55 + .../static/src/views/archive_employee_hook.js | 29 + .../fields/employee_field_relation_mixin.js | 44 + .../many2many_avatar_employee_field.js | 89 + .../kanban_many2one_avatar_employee_field.js | 73 + .../kanban_many2one_avatar_employee_field.xml | 18 + .../many2one_avatar_employee_field.js | 55 + .../many2one_avatar_employee_field.xml | 21 + frontend/hr/static/src/views/form_view.js | 24 + frontend/hr/static/src/views/kanban_view.js | 29 + frontend/hr/static/src/views/list_view.js | 33 + .../hr/static/src/views/open_chat_hook.js | 16 + .../static/src/views/preferences_form_view.js | 28 + frontend/hr/static/tests/hr_test_helpers.js | 32 + .../hr/static/tests/legacy/disable_patch.js | 3 + .../static/tests/m2x_avatar_employee.test.js | 501 + .../mock_server/mock_models/fake_user.js | 8 + .../mock_server/mock_models/hr_department.js | 31 + .../mock_server/mock_models/hr_employee.js | 29 + .../mock_models/hr_employee_public.js | 10 + .../tests/mock_server/mock_models/hr_job.js | 10 + .../mock_server/mock_models/hr_version.js | 10 + .../mock_models/hr_work_location.js | 19 + .../mock_models/m2x_avatar_employee.js | 8 + .../mock_server/mock_models/res_partner.js | 23 + .../mock_server/mock_models/res_users.js | 22 + .../static/tests/mock_server/mock_server.js | 12 + .../hr/static/tests/profile_form_view.test.js | 41 + .../check_public_employee_link_redirect.js | 27 + .../hr/static/tests/tours/hr_employee_flow.js | 37 + .../hr_employee_multiple_bank_accounts.js | 78 + .../tours/version_timeline_auto_save_tour.js | 58 + .../static/tests/web/m2x_avatar_user.test.js | 131 + frontend/hr/static/xls/hr_employee.xls | Bin 0 -> 37376 bytes frontend/odoo/base/static/src/css/modules.css | 30 + .../base/static/src/scss/res_partner.scss | 8 + .../odoo/base/static/src/scss/res_users.scss | 5 + .../demo/acoustic_bloc_screen_document.pdf | Bin 0 -> 109506 bytes .../demo/customizable_desk_document.pdf | Bin 0 -> 117289 bytes frontend/product/static/description/icon.png | Bin 0 -> 2474 bytes frontend/product/static/description/icon.svg | 1 + .../product/static/description/icon_hi.png | Bin 0 -> 14778 bytes .../product/static/img/desk_organizer.jpg | Bin 0 -> 37225 bytes frontend/product/static/img/desk_pad.jpg | Bin 0 -> 16715 bytes frontend/product/static/img/dining_table.png | Bin 0 -> 75772 bytes frontend/product/static/img/glass.png | Bin 0 -> 39843 bytes frontend/product/static/img/leather.png | Bin 0 -> 35932 bytes frontend/product/static/img/linen.png | Bin 0 -> 38204 bytes frontend/product/static/img/maroon.png | Bin 0 -> 41199 bytes .../product/static/img/membership_0-image.jpg | Bin 0 -> 3796 bytes .../product/static/img/membership_1-image.jpg | Bin 0 -> 3158 bytes .../product/static/img/membership_2-image.jpg | Bin 0 -> 3691 bytes frontend/product/static/img/metal.png | Bin 0 -> 20919 bytes frontend/product/static/img/monitor_stand.jpg | Bin 0 -> 23905 bytes frontend/product/static/img/office_combo.jpg | Bin 0 -> 72837 bytes frontend/product/static/img/placeholder.png | Bin 0 -> 28789 bytes .../static/img/placeholder_thumbnail.png | Bin 0 -> 7322 bytes frontend/product/static/img/product_chair.jpg | Bin 0 -> 16910 bytes frontend/product/static/img/product_lamp.png | Bin 0 -> 12801 bytes .../static/img/product_product_10-image.jpg | Bin 0 -> 25665 bytes .../static/img/product_product_11-image.jpg | Bin 0 -> 66341 bytes .../static/img/product_product_11b-image.jpg | Bin 0 -> 74667 bytes .../static/img/product_product_12-image.jpg | Bin 0 -> 20829 bytes .../static/img/product_product_13-image.jpg | Bin 0 -> 56076 bytes .../static/img/product_product_16-image.jpg | Bin 0 -> 15565 bytes .../static/img/product_product_20-image.png | Bin 0 -> 42385 bytes .../static/img/product_product_22-image.png | Bin 0 -> 17315 bytes .../static/img/product_product_24-image.jpg | Bin 0 -> 91692 bytes .../static/img/product_product_25-image.jpg | Bin 0 -> 44295 bytes .../img/product_product_25_black-image.jpg | Bin 0 -> 53113 bytes .../static/img/product_product_27-image.jpg | Bin 0 -> 21731 bytes .../static/img/product_product_3-image.jpg | Bin 0 -> 10699 bytes .../static/img/product_product_43-image.jpg | Bin 0 -> 25902 bytes .../static/img/product_product_46-image.jpg | Bin 0 -> 80244 bytes .../static/img/product_product_5-image.jpg | Bin 0 -> 53518 bytes .../static/img/product_product_6-image.jpg | Bin 0 -> 19939 bytes .../static/img/product_product_7-image.png | Bin 0 -> 17769 bytes .../static/img/product_product_8-image.jpg | Bin 0 -> 93675 bytes .../img/product_product_8_glass-image.jpg | Bin 0 -> 34553 bytes .../img/product_product_8_metal-image.jpg | Bin 0 -> 16060 bytes .../static/img/product_product_9-image.jpg | Bin 0 -> 12124 bytes .../static/img/product_product_d01-image.jpg | Bin 0 -> 17594 bytes .../static/img/product_product_d01b-image.jpg | Bin 0 -> 17618 bytes .../static/img/product_product_d01c-image.jpg | Bin 0 -> 17266 bytes .../static/img/product_product_d03-image.png | Bin 0 -> 12836 bytes frontend/product/static/img/purple.png | Bin 0 -> 34170 bytes frontend/product/static/img/table02.jpg | Bin 0 -> 58624 bytes frontend/product/static/img/table03.jpg | Bin 0 -> 49967 bytes frontend/product/static/img/table04.jpg | Bin 0 -> 67900 bytes frontend/product/static/img/velvet.png | Bin 0 -> 37638 bytes frontend/product/static/img/wood.png | Bin 0 -> 36533 bytes .../product_pricelist_report.js | 281 + .../product_pricelist_report.xml | 100 + .../src/js/product_attribute_value_list.js | 63 + .../product_document_kanban_controller.js | 15 + .../product_document_kanban_controller.xml | 17 + .../product_document_kanban_record.js | 30 + .../product_document_kanban_renderer.js | 20 + .../product_document_kanban_renderer.xml | 9 + .../product_document_kanban_view.js | 14 + .../product_document_kanban_view.scss | 3 + .../upload_button/upload_button.js | 76 + .../upload_button/upload_button.xml | 21 + .../src/product_catalog/kanban_controller.js | 67 + .../src/product_catalog/kanban_controller.xml | 8 + .../src/product_catalog/kanban_model.js | 81 + .../src/product_catalog/kanban_record.js | 135 + .../src/product_catalog/kanban_record.xml | 18 + .../src/product_catalog/kanban_renderer.js | 37 + .../src/product_catalog/kanban_renderer.xml | 22 + .../static/src/product_catalog/kanban_view.js | 16 + .../product_catalog/order_line/order_line.js | 57 + .../order_line/order_line.scss | 17 + .../product_catalog/order_line/order_line.xml | 76 + .../product_and_label_autoresize.js | 37 + .../product_name_and_description.js | 158 + .../product/static/src/scss/product_form.scss | 3 + .../static/src/scss/report_label_sheet.scss | 83 + .../mock_server/mock_models/product_combo.js | 30 + .../mock_models/product_product.js | 12 + .../mock_models/product_template.js | 12 + .../tests/product_combo_test_helpers.js | 16 + .../tests/product_pricelist_report.test.js | 88 + .../static/tests/product_test_helpers.js | 13 + .../product/static/xls/product_pricelist.xls | Bin 0 -> 32768 bytes .../static/xls/product_supplierinfo.xls | Bin 0 -> 14665 bytes .../product/static/xls/product_template.xls | Bin 0 -> 36352 bytes frontend/project/static/description/icon.png | Bin 0 -> 2434 bytes frontend/project/static/description/icon.svg | 1 + .../project/static/description/icon_hi.png | Bin 0 -> 13313 bytes .../static/src/actions/client_actions.js | 148 + .../done_checkmark/task_done_checkmark.js | 35 + .../done_checkmark/task_done_checkmark.scss | 26 + .../done_checkmark/task_done_checkmark.xml | 12 + .../notebook_task_list_renderer.js | 55 + .../notebook_task_list_renderer.xml | 18 + .../notebook_task_one2many_field.js | 26 + .../project_is_favorite_field.js | 14 + .../project_many2one_field.js | 26 + .../project_many2one_field.scss | 4 + .../project_many2one_field.xml | 13 + .../components/project_milestone.js | 66 + .../components/project_milestone.xml | 22 + .../components/project_profitability.js | 31 + .../components/project_profitability.xml | 109 + .../project_right_side_panel_section.js | 26 + .../project_right_side_panel_section.xml | 18 + .../project_right_side_panel.dark.scss | 9 + .../project_right_side_panel.js | 177 + .../project_right_side_panel.scss | 69 + .../project_right_side_panel.xml | 109 + .../project_state_selection.js | 29 + .../project_state_selection.scss | 3 + ...oject_status_with_color_selection_field.js | 41 + ...ject_status_with_color_selection_field.xml | 19 + ...task_name_with_subtask_count_char_field.js | 16 + ...ask_name_with_subtask_count_char_field.xml | 14 + .../project_task_priority_switch_field.js | 29 + ...project_task_stage_with_state_selection.js | 60 + ...roject_task_stage_with_state_selection.xml | 10 + .../project_task_state_selection.js | 190 + .../project_task_state_selection.scss | 67 + .../project_task_state_selection.xml | 98 + .../subtask_kanban_create.js | 67 + .../subtask_kanban_create.xml | 18 + .../subtask_kanban_list.js | 116 + .../subtask_kanban_list.scss | 34 + .../subtask_kanban_list.xml | 43 + .../subtask_list_renderer.js | 15 + .../subtask_one2many_field.js | 38 + .../src/components/task_list_renderer.js | 22 + .../static/src/core/web/@types/models.d.ts | 5 + .../src/core/web/follower_list_patch.js | 34 + .../static/src/core/web/thread_model_patch.js | 13 + frontend/project/static/src/css/project.css | 12 + frontend/project/static/src/img/app_store.png | Bin 0 -> 4682 bytes frontend/project/static/src/img/bird.jpg | Bin 0 -> 10802 bytes .../project/static/src/img/chrome_store.png | Bin 0 -> 5919 bytes .../project/static/src/img/planner_icon.png | Bin 0 -> 2029 bytes .../project/static/src/img/play_store.png | Bin 0 -> 8171 bytes .../project/static/src/img/task-state-img.png | Bin 0 -> 66542 bytes frontend/project/static/src/img/tasks.svg | 22 + .../project/static/src/img/tasks_icon.png | Bin 0 -> 2545 bytes .../project/static/src/img/top_left_arrow.png | Bin 0 -> 4187 bytes .../static/src/img/web_planner_email.png | Bin 0 -> 4073 bytes .../static/src/img/web_planner_project.png | Bin 0 -> 42631 bytes .../static/src/img/web_planner_subtype.png | Bin 0 -> 7477 bytes .../src/interactions/project_rating_image.js | 26 + .../project/static/src/js/tours/project.js | 262 + .../project_sharing/chatter/chatter_patch.js | 39 + .../chatter/composer_actions_patch.js | 12 + .../project_sharing/chatter/composer_patch.js | 41 + .../project_sharing/chatter/message_patch.js | 9 + .../chatter/portal_chatter_patch.xml | 19 + .../chatter/suggestion_service_patch.js | 21 + .../depend_on_ids_list_renderer.js | 10 + .../depend_on_ids_list_renderer.xml | 13 + .../depend_on_ids_one2many_field.js | 18 + .../editor/project_sharing_media_plugin.js | 45 + .../static/src/project_sharing/main.js | 4 + .../src/project_sharing/project_sharing.js | 69 + .../src/project_sharing/project_sharing.scss | 4 + .../src/project_sharing/project_sharing.xml | 9 + .../static/src/project_sharing/router.js | 30 + .../search/control_panel/control_panel.xml | 20 + .../form/project_sharing_form_compiler.js | 69 + .../form/project_sharing_form_controller.js | 55 + .../form/project_sharing_form_renderer.js | 10 + .../views/form/project_sharing_form_view.js | 6 + .../views/kanban/kanban_view.js | 16 + .../project_sharing/views/list/list_view.js | 15 + .../static/src/project_sharing/views/view.js | 18 + .../static/src/scss/portal_rating.scss | 46 + .../static/src/scss/project_dashboard.scss | 72 + .../project/static/src/scss/project_form.scss | 57 + .../static/src/scss/project_widgets.scss | 64 + .../project/static/src/utils/project_utils.js | 12 + .../analytic_account_form_controller.js | 31 + .../analytic_account_form_view.js | 11 + .../analytic_account_list_controller.js | 31 + .../analytic_account_list_view.js | 11 + .../burndown_chart/burndown_chart_model.js | 84 + .../burndown_chart_search_model.js | 148 + .../burndown_chart/burndown_chart_view.js | 16 + .../burndown_chart/burndown_chart_view.xml | 11 + .../project_task_template_dropdown.js | 79 + .../project_task_template_dropdown.xml | 25 + .../components/project_template_buttons.js | 41 + .../components/project_template_buttons.scss | 25 + .../components/project_template_buttons.xml | 9 + .../components/project_template_dropdown.js | 91 + .../components/project_template_dropdown.xml | 25 + .../project_project_form_controller.js | 81 + .../project_project_form_controller.xml | 12 + .../project_form/project_project_form_view.js | 10 + .../static/src/views/project_model_mixin.js | 13 + .../project_project_activity_model.js | 10 + .../project_project_activity_view.js | 11 + .../common/project_common_calendar_popover.js | 23 + .../project_common_calendar_popover.xml | 8 + .../project_common_calendar_renderer.js | 9 + .../project_project_calendar_controller.js | 8 + .../project_project_calendar_model.js | 11 + .../project_project_calendar_renderer.js | 11 + .../project_project_calendar_view.js | 16 + .../project_project_group_config_menu.js | 23 + .../project_project_kanban_controller.js | 28 + .../project_project_kanban_controller.xml | 13 + .../project_project_kanban_header.js | 9 + .../project_project_kanban_renderer.js | 10 + .../project_task_kanban_view.js | 14 + .../project_project_list_controller.js | 28 + .../project_project_list_controller.xml | 12 + .../project_project_list_renderer.js | 22 + .../project_project_list_view.js | 14 + .../src/views/project_relational_model.js | 10 + .../project_task_activity_model.js | 10 + .../project_task_activity_view.js | 13 + .../project_task_analysis_graph_model.js | 10 + .../project_task_analysis_graph_renderer.js | 4 + .../project_task_analysis_graph_view.js | 12 + .../project_task_analysis_pivot_model.js | 10 + .../project_task_analysis_pivot_renderer.js | 4 + .../project_task_analysis_pivot_view.js | 12 + .../project_task_analysis_renderer_mixin.js | 30 + ...ct_task_calendar_task_to_plan_draggable.js | 118 + .../project_task_calendar_common_renderer.js | 25 + .../project_task_calendar_common_renderer.xml | 10 + .../project_task_calendar_controller.js | 90 + .../project_task_calendar_filter_section.js | 7 + .../project_task_calendar_filter_section.xml | 12 + .../project_task_calendar_list_to_plan.js | 21 + .../project_task_calendar_list_to_plan.xml | 13 + .../project_task_calendar_model.js | 119 + .../project_task_calendar_renderer.js | 13 + .../project_task_calendar_renderer.scss | 6 + .../project_task_calendar_view.js | 15 + .../project_task_calendar_year_renderer.js | 5 + .../project_task_calendar_side_panel.js | 17 + .../project_task_calendar_side_panel.xml | 12 + .../project_task_control_panel.js | 31 + .../project_task_control_panel.xml | 59 + .../project_task_form_controller.js | 124 + .../project_task_form_controller.xml | 13 + .../project_task_form_view.js | 10 + .../project_task_form_view.scss | 23 + .../project_task_graph_model.js | 10 + .../project_task_graph_view.js | 12 + .../project_task_group_config_menu.js | 42 + .../project_task_kanban_compiler.js | 45 + .../project_task_kanban_controller.js | 16 + .../project_task_kanban_controller.xml | 16 + .../project_task_kanban_examples.js | 113 + .../project_task_kanban_header.js | 9 + .../project_task_kanban_model.js | 56 + .../project_task_kanban_record.js | 26 + .../project_task_kanban_renderer.js | 36 + .../project_task_kanban_renderer.xml | 8 + .../project_task_kanban_view.js | 17 + .../project_task_kanban_view.scss | 20 + .../project_task_list_controller.js | 29 + .../project_task_list_controller.xml | 13 + .../project_task_list_renderer.js | 40 + .../project_task_list_view.js | 16 + .../src/views/project_task_model_mixin.js | 22 + .../project_task_pivot_model.js | 10 + .../project_task_pivot_view.js | 12 + .../views/project_task_relational_model.js | 10 + .../project_update_kanban.scss | 23 + .../project_update_kanban_controller.js | 14 + .../project_update_kanban_controller.xml | 13 + .../project_update_kanban_view.js | 12 + .../project_update_list_controller.js | 14 + .../project_update_list_controller.xml | 13 + .../project_update_list_view.js | 12 + .../src/views/widget/subtask_counter.js | 47 + .../src/views/widget/subtask_counter.xml | 14 + .../src/xml/project_task_kanban_examples.xml | 71 + .../mock_server/mock_models/project_task.js | 9 + .../tests/project_is_favorite_field.test.js | 71 + .../project/static/tests/project_models.js | 192 + .../tests/project_notebook_task_list.test.js | 154 + .../project_notebook_task_list_mobile.test.js | 178 + .../static/tests/project_project.test.js | 68 + .../tests/project_project_calendar.test.js | 44 + .../tests/project_project_form_view.test.js | 228 + .../project_project_state_selection.test.js | 45 + .../tests/project_right_side_panel.test.js | 124 + .../tests/project_task_analysis.test.js | 134 + .../tests/project_task_burndown_chart.test.js | 182 + .../tests/project_task_calendar.test.js | 313 + .../static/tests/project_task_groupby.test.js | 59 + .../tests/project_task_kanban_view.test.js | 93 + .../tests/project_task_list_view.test.js | 83 + .../project_task_priority_switch.test.js | 32 + ...roject_task_project_many2one_field.test.js | 40 + .../project_task_state_selection.test.js | 85 + .../static/tests/project_task_subtask.test.js | 345 + .../project_task_template_dropdown.test.js | 148 + .../tests/project_update_with_color.test.js | 48 + .../static/tests/tours/personal_stage_tour.js | 78 + .../tours/project_burndown_chart_tour.js | 85 + .../tests/tours/project_sharing_tour.js | 256 + .../tours/project_tags_filter_tour_tests.js | 65 + .../tests/tours/project_task_history.js | 278 + .../tours/project_task_templates_tour.js | 47 + .../tests/tours/project_templates_tour.js | 72 + .../static/tests/tours/project_tour.js | 158 + .../tests/tours/project_update_tour_tests.js | 222 + .../static/xls/tasks_import_template.xlsx | Bin 0 -> 7247 bytes frontend/purchase/static/description/icon.png | Bin 0 -> 587 bytes frontend/purchase/static/description/icon.svg | 1 + .../purchase/static/description/icon_hi.png | Bin 0 -> 3464 bytes .../monetary_field_no_zero.js | 23 + .../open_match_line_widget.js | 27 + .../open_match_line_widget.xml | 8 + .../purchase_file_uploader.js | 90 + .../purchase_file_uploader.xml | 18 + .../src/components/tax_totals/tax_totals.xml | 12 + .../purchase/static/src/img/calculator.svg | 19 + .../static/src/img/milk-OTDPurchase.gif | Bin 0 -> 12728 bytes .../interactions/purchase_datetimepicker.js | 34 + .../src/interactions/purchase_sidebar.js | 21 + .../purchase/static/src/js/tours/purchase.js | 133 + .../static/src/js/tours/purchase_steps.js | 14 + .../src/product_catalog/kanban_record.js | 18 + .../src/product_catalog/kanban_renderer.js | 40 + .../src/product_catalog/kanban_renderer.xml | 22 + .../static/src/product_catalog/kanban_view.js | 11 + .../purchase_order_line.js | 13 + .../static/src/scss/purchase_portal.scss | 8 + .../toaster_button/toaster_button_widget.js | 36 + .../toaster_button/toaster_button_widget.xml | 10 + .../static/src/views/purchase_dashboard.js | 38 + .../static/src/views/purchase_dashboard.scss | 42 + .../static/src/views/purchase_dashboard.xml | 186 + .../static/src/views/purchase_kanbanview.js | 16 + .../static/src/views/purchase_kanbanview.xml | 7 + .../static/src/views/purchase_listview.js | 27 + .../static/src/views/purchase_listview.xml | 13 + .../static/tests/tours/purchase_catalog.js | 79 + .../static/tests/tours/purchase_flow_tour.js | 76 + .../static/tests/tours/tour_helper.js | 160 + .../purchase/static/xls/product_purchase.xls | Bin 0 -> 6656 bytes ...equests_for_quotation_import_template.xlsx | Bin 0 -> 6890 bytes frontend/sale/static/description/icon.png | Bin 0 -> 861 bytes frontend/sale/static/description/icon.svg | 1 + frontend/sale/static/description/icon_hi.png | Bin 0 -> 4539 bytes .../static/img/advance_product_0-image.jpg | Bin 0 -> 2494 bytes frontend/sale/static/img/btn_paynowcc_lg.gif | Bin 0 -> 2916 bytes .../static/img/floor_protection-image.jpg | Bin 0 -> 45696 bytes frontend/sale/static/src/img/bag.svg | 9 + .../onboarding_quotation_order_tooltip.jpg | Bin 0 -> 12084 bytes .../src/img/sales_quotation_thumbnail.webp | Bin 0 -> 6924 bytes .../src/interactions/portal_prepayment.js | 63 + .../static/src/interactions/sale_portal.js | 11 + .../static/src/interactions/sale_sidebar.js | 26 + .../js/badge_extra_price/badge_extra_price.js | 19 + .../badge_extra_price/badge_extra_price.xml | 9 + .../combo_configurator_dialog.js | 249 + .../combo_configurator_dialog.scss | 11 + .../combo_configurator_dialog.xml | 110 + .../static/src/js/models/product_combo.js | 41 + .../src/js/models/product_combo_item.js | 42 + .../static/src/js/models/product_product.js | 77 + .../models/product_template_attribute_line.js | 73 + .../product_template_attribute_value.js | 14 + .../sale/static/src/js/product/product.js | 95 + .../sale/static/src/js/product/product.scss | 48 + .../sale/static/src/js/product/product.xml | 138 + .../src/js/product_card/product_card.js | 28 + .../src/js/product_card/product_card.scss | 17 + .../src/js/product_card/product_card.xml | 40 + .../product_configurator_dialog.js | 516 + .../product_configurator_dialog.xml | 35 + .../src/js/product_list/product_list.js | 20 + .../src/js/product_list/product_list.scss | 7 + .../src/js/product_list/product_list.xml | 28 + .../product_template_attribute_line.js | 155 + .../product_template_attribute_line.scss | 114 + .../product_template_attribute_line.xml | 192 + .../js/quantity_buttons/quantity_buttons.js | 42 + .../js/quantity_buttons/quantity_buttons.scss | 22 + .../js/quantity_buttons/quantity_buttons.xml | 32 + .../sale_action_helper/sale_action_helper.js | 20 + .../sale_action_helper.scss | 16 + .../sale_action_helper/sale_action_helper.xml | 24 + .../sale_action_helper_dialog.js | 11 + .../sale_action_helper_dialog.xml | 29 + .../sale_order_line_field.js | 259 + .../sale_order_line_field.xml | 55 + .../sale/static/src/js/sale_product_field.js | 470 + .../static/src/js/sale_product_field.scss | 4 + .../static/src/js/sale_progressbar_field.js | 46 + frontend/sale/static/src/js/sale_utils.js | 65 + .../js/tours/combo_configurator_tour_utils.js | 209 + .../tours/product_configurator_tour_utils.js | 265 + frontend/sale/static/src/js/tours/sale.js | 111 + .../sale/static/src/js/tours/tour_utils.js | 97 + .../upload_rfq_cog_menu.js | 29 + .../upload_rfq_cog_menu.xml | 14 + .../sale/static/src/scss/sale_onboarding.scss | 11 + .../sale/static/src/scss/sale_portal.scss | 46 + .../sale/static/src/scss/sale_report.scss | 3 + .../sale_file_upload_kanban_controller.js | 8 + .../sale_file_upload_kanban_renderer.js | 14 + .../sale_file_upload_kanban_view.js | 12 + .../sale_file_upload_list_controller.js | 8 + .../sale_file_upload_list_renderer.js | 14 + .../sale_file_upload_list_view.js | 12 + .../sale_onboarding_kanban_renderer.js | 10 + .../sale_onboarding_kanban_renderer.xml | 11 + .../sale_onboarding_kanban_view.js | 10 + .../sale_onboarding_list_renderer.js | 10 + .../sale_onboarding_list_renderer.xml | 11 + .../sale_onboarding_list_view.js | 10 + .../static/src/xml/sale_product_field.xml | 21 + .../xml/sales_team_progress_bar_template.xml | 17 + .../mock_server/mock_models/sale_order.js | 14 + .../mock_models/sale_order_line.js | 9 + .../tests/sale_order_line_field.test.js | 251 + .../static/tests/sale_product_field.test.js | 201 + .../sale/static/tests/sale_test_helpers.js | 17 + .../static/tests/sales_team_dashboard.test.js | 72 + .../mail_attachment_removal_test_tour.js | 32 + .../tours/product_attribute_value_tour.js | 56 + .../sale/static/tests/tours/sale_catalog.js | 98 + .../tests/tours/sale_combo_configurator.js | 136 + ...rator_preconfigure_unconfigurable_ptals.js | 37 + ...r_preselect_single_unconfigurable_items.js | 36 + .../tours/sale_order_product_uom_integrity.js | 14 + .../sale/static/tests/tours/sale_signature.js | 99 + .../xls/quotations_import_template.xlsx | Bin 0 -> 6975 bytes frontend/stock/static/description/icon.png | Bin 0 -> 1393 bytes frontend/stock/static/description/icon.svg | 1 + frontend/stock/static/description/icon_hi.png | Bin 0 -> 5747 bytes frontend/stock/static/img/barcode_scanner.png | Bin 0 -> 38231 bytes .../stock/static/img/cable_management.jpg | Bin 0 -> 82986 bytes frontend/stock/static/img/empty_list.png | Bin 0 -> 58808 bytes frontend/stock/static/img/replenishment.svg | 256 + .../static/img/res_partner_address_41.jpg | Bin 0 -> 4344 bytes .../static/img/zpl_label_placeholder.png | Bin 0 -> 4205 bytes .../static/src/client_actions/multi_print.js | 32 + .../stock_traceability_report_backend.js | 157 + .../stock_traceability_report_backend.xml | 85 + .../stock_reception_report_line.js | 72 + .../stock_reception_report_line.xml | 35 + .../stock_reception_report_main.js | 173 + .../stock_reception_report_main.xml | 45 + .../stock_reception_report_table.js | 92 + .../stock_reception_report_table.xml | 43 + .../stock_overview/stock_overview.js | 46 + .../static/src/fields/stock_action_field.js | 95 + .../static/src/fields/stock_action_field.xml | 20 + .../fields/stock_move_line_x2_many_field.js | 190 + frontend/stock/static/src/img/barcode.gif | Bin 0 -> 44599 bytes .../picking_type_dashboard_graph_field.js | 110 + .../picking_type_dashboard_graph_field.scss | 5 + .../static/src/scss/forecast_widget.scss | 3 + .../static/src/scss/forecasted_details.scss | 27 + .../stock/static/src/scss/product_form.scss | 8 + .../src/scss/report_stock_reception.scss | 58 + .../static/src/scss/report_stock_rule.scss | 134 + .../scss/report_stockpicking_operations.scss | 32 + .../static/src/scss/stock_empty_screen.scss | 29 + .../static/src/scss/stock_forecasted.scss | 32 + .../static/src/scss/stock_move_list.scss | 3 + .../stock/static/src/scss/stock_overview.scss | 19 + .../src/scss/stock_replenishment_info.scss | 8 + .../src/scss/stock_traceability_report.scss | 89 + .../stock_forecasted/forecasted_buttons.js | 60 + .../stock_forecasted/forecasted_buttons.xml | 15 + .../stock_forecasted/forecasted_details.js | 242 + .../stock_forecasted/forecasted_details.xml | 147 + .../src/stock_forecasted/forecasted_graph.js | 35 + .../src/stock_forecasted/forecasted_graph.xml | 8 + .../src/stock_forecasted/forecasted_header.js | 87 + .../stock_forecasted/forecasted_header.xml | 85 + .../forecasted_warehouse_filter.js | 37 + .../forecasted_warehouse_filter.xml | 14 + .../src/stock_forecasted/stock_forecasted.js | 137 + .../src/stock_forecasted/stock_forecasted.xml | 30 + .../static/src/stock_warehouse_service.js | 19 + .../views/list/inventory_report_list_model.js | 69 + .../views/list/inventory_report_list_view.js | 10 + .../views/list/stock_add_package_list_view.js | 64 + .../src/views/list/stock_report_list_view.js | 13 + .../views/picking_form/stock_move_one2many.js | 93 + .../picking_form/stock_move_product_label.js | 33 + .../picking_form/stock_move_product_label.xml | 33 + .../search/stock_orderpoint_search_model.js | 35 + .../search/stock_orderpoint_search_panel.js | 25 + .../search/stock_orderpoint_search_panel.xml | 25 + .../views/search/stock_report_search_model.js | 46 + .../views/search/stock_report_search_panel.js | 27 + .../search/stock_report_search_panel.xml | 33 + .../static/src/views/stock_empty_list_help.js | 29 + .../src/views/stock_empty_list_help.xml | 20 + .../views/stock_orderpoint_list_controller.js | 37 + .../src/views/stock_orderpoint_list_view.js | 14 + .../src/views/stock_orderpoint_list_view.xml | 27 + .../src/widgets/counted_quantity_widget.js | 72 + .../static/src/widgets/forced_placeholder.js | 28 + .../static/src/widgets/forced_placeholder.xml | 18 + .../static/src/widgets/forecast_widget.js | 66 + .../static/src/widgets/forecast_widget.xml | 19 + .../static/src/widgets/generate_serial.js | 142 + .../stock/static/src/widgets/json_widget.js | 166 + .../stock/static/src/widgets/json_widget.xml | 43 + .../stock/static/src/widgets/lots_dialog.xml | 106 + .../src/widgets/many2many_barcode_tags.js | 30 + .../static/src/widgets/popover_widget.js | 51 + .../static/src/widgets/popover_widget.xml | 13 + .../static/src/widgets/stock_package_m2m.js | 43 + .../static/src/widgets/stock_package_m2o.js | 98 + .../static/src/widgets/stock_package_m2o.xml | 9 + .../static/src/widgets/stock_pick_from.js | 53 + .../static/src/widgets/stock_pick_from.xml | 8 + .../src/widgets/stock_rescheduling_popover.js | 46 + .../widgets/stock_rescheduling_popover.xml | 12 + .../stock/static/src/xml/inventory_lines.xml | 8 + .../static/src/xml/report_stock_reception.xml | 16 + .../tests/counted_quantity_widget.test.js | 68 + .../tests/inventory_report_list.test.js | 187 + .../stock/static/tests/popover_widget.test.js | 35 + .../stock_traceability_report_backend.test.js | 24 + .../static/tests/tours/stock_flow_tour.js | 120 + .../static/tests/tours/stock_picking_tour.js | 526 + .../static/tests/tours/stock_report_tests.js | 137 + .../stock/static/tests/tours/tour_helper.js | 9 + frontend/stock/static/xlsx/stock_quant.xlsx | Bin 0 -> 6358 bytes frontend/web/static/fonts/fonts.scss | 102 + .../fonts/google/Fira_Mono/Fira_Mono-Bold.ttf | Bin 0 -> 201708 bytes .../google/Fira_Mono/Fira_Mono-Medium.ttf | Bin 0 -> 169056 bytes .../google/Fira_Mono/Fira_Mono-Regular.ttf | Bin 0 -> 170204 bytes .../web/static/fonts/google/Fira_Mono/OFL.txt | 93 + .../google/Montserrat/Montserrat-Black.ttf | Bin 0 -> 257064 bytes .../Montserrat/Montserrat-BlackItalic.ttf | Bin 0 -> 261232 bytes .../google/Montserrat/Montserrat-Bold.ttf | Bin 0 -> 244036 bytes .../Montserrat/Montserrat-BoldItalic.ttf | Bin 0 -> 249124 bytes .../Montserrat/Montserrat-ExtraBold.ttf | Bin 0 -> 244372 bytes .../Montserrat/Montserrat-ExtraBoldItalic.ttf | Bin 0 -> 249268 bytes .../Montserrat/Montserrat-ExtraLight.ttf | Bin 0 -> 241632 bytes .../Montserrat-ExtraLightItalic.ttf | Bin 0 -> 245664 bytes .../google/Montserrat/Montserrat-Italic.ttf | Bin 0 -> 248656 bytes .../google/Montserrat/Montserrat-Light.ttf | Bin 0 -> 241580 bytes .../Montserrat/Montserrat-LightItalic.ttf | Bin 0 -> 245776 bytes .../google/Montserrat/Montserrat-Medium.ttf | Bin 0 -> 242692 bytes .../Montserrat/Montserrat-MediumItalic.ttf | Bin 0 -> 247540 bytes .../google/Montserrat/Montserrat-Regular.ttf | Bin 0 -> 245276 bytes .../google/Montserrat/Montserrat-SemiBold.ttf | Bin 0 -> 243324 bytes .../Montserrat/Montserrat-SemiBoldItalic.ttf | Bin 0 -> 248684 bytes .../google/Montserrat/Montserrat-Thin.ttf | Bin 0 -> 240952 bytes .../Montserrat/Montserrat-ThinItalic.ttf | Bin 0 -> 244872 bytes .../static/fonts/google/Montserrat/OFL.txt | 93 + .../static/fonts/google/Open_Sans/LICENSE.txt | 202 + .../fonts/google/Open_Sans/Open_Sans-Bold.ttf | Bin 0 -> 103616 bytes .../google/Open_Sans/Open_Sans-BoldItalic.ttf | Bin 0 -> 92124 bytes .../google/Open_Sans/Open_Sans-ExtraBold.ttf | Bin 0 -> 101512 bytes .../Open_Sans/Open_Sans-ExtraBoldItalic.ttf | Bin 0 -> 92196 bytes .../google/Open_Sans/Open_Sans-Italic.ttf | Bin 0 -> 91736 bytes .../google/Open_Sans/Open_Sans-Light.ttf | Bin 0 -> 101140 bytes .../Open_Sans/Open_Sans-LightItalic.ttf | Bin 0 -> 91920 bytes .../google/Open_Sans/Open_Sans-Regular.ttf | Bin 0 -> 96428 bytes .../google/Open_Sans/Open_Sans-SemiBold.ttf | Bin 0 -> 100256 bytes .../Open_Sans/Open_Sans-SemiBoldItalic.ttf | Bin 0 -> 91604 bytes .../web/static/fonts/google/Oswald/OFL.txt | 93 + .../fonts/google/Oswald/Oswald-Bold.ttf | Bin 0 -> 87744 bytes .../fonts/google/Oswald/Oswald-ExtraLight.ttf | Bin 0 -> 84484 bytes .../fonts/google/Oswald/Oswald-Light.ttf | Bin 0 -> 85016 bytes .../fonts/google/Oswald/Oswald-Medium.ttf | Bin 0 -> 87756 bytes .../fonts/google/Oswald/Oswald-Regular.ttf | Bin 0 -> 86480 bytes .../fonts/google/Oswald/Oswald-SemiBold.ttf | Bin 0 -> 88720 bytes .../web/static/fonts/google/Raleway/OFL.txt | 95 + .../fonts/google/Raleway/Raleway-Black.ttf | Bin 0 -> 173548 bytes .../google/Raleway/Raleway-BlackItalic.ttf | Bin 0 -> 141316 bytes .../fonts/google/Raleway/Raleway-Bold.ttf | Bin 0 -> 172040 bytes .../google/Raleway/Raleway-BoldItalic.ttf | Bin 0 -> 141076 bytes .../google/Raleway/Raleway-ExtraBold.ttf | Bin 0 -> 170936 bytes .../Raleway/Raleway-ExtraBoldItalic.ttf | Bin 0 -> 140904 bytes .../google/Raleway/Raleway-ExtraLight.ttf | Bin 0 -> 169152 bytes .../Raleway/Raleway-ExtraLightItalic.ttf | Bin 0 -> 134916 bytes .../fonts/google/Raleway/Raleway-Italic.ttf | Bin 0 -> 138804 bytes .../fonts/google/Raleway/Raleway-Light.ttf | Bin 0 -> 175144 bytes .../google/Raleway/Raleway-LightItalic.ttf | Bin 0 -> 141172 bytes .../fonts/google/Raleway/Raleway-Medium.ttf | Bin 0 -> 172264 bytes .../google/Raleway/Raleway-MediumItalic.ttf | Bin 0 -> 140472 bytes .../fonts/google/Raleway/Raleway-Regular.ttf | Bin 0 -> 171280 bytes .../fonts/google/Raleway/Raleway-SemiBold.ttf | Bin 0 -> 173272 bytes .../google/Raleway/Raleway-SemiBoldItalic.ttf | Bin 0 -> 139384 bytes .../fonts/google/Raleway/Raleway-Thin.ttf | Bin 0 -> 170340 bytes .../google/Raleway/Raleway-ThinItalic.ttf | Bin 0 -> 134656 bytes .../static/fonts/google/Roboto/LICENSE.txt | 202 + .../fonts/google/Roboto/Roboto-Black.ttf | Bin 0 -> 170740 bytes .../google/Roboto/Roboto-BlackItalic.ttf | Bin 0 -> 176772 bytes .../fonts/google/Roboto/Roboto-Bold.ttf | Bin 0 -> 170064 bytes .../fonts/google/Roboto/Roboto-BoldItalic.ttf | Bin 0 -> 174236 bytes .../fonts/google/Roboto/Roboto-Italic.ttf | Bin 0 -> 173232 bytes .../fonts/google/Roboto/Roboto-Light.ttf | Bin 0 -> 169680 bytes .../google/Roboto/Roboto-LightItalic.ttf | Bin 0 -> 175836 bytes .../fonts/google/Roboto/Roboto-Medium.ttf | Bin 0 -> 171320 bytes .../google/Roboto/Roboto-MediumItalic.ttf | Bin 0 -> 176080 bytes .../fonts/google/Roboto/Roboto-Regular.ttf | Bin 0 -> 170984 bytes .../fonts/google/Roboto/Roboto-Thin.ttf | Bin 0 -> 171168 bytes .../fonts/google/Roboto/Roboto-ThinItalic.ttf | Bin 0 -> 175528 bytes .../web/static/fonts/google/Roboto/roboto.b64 | 1 + .../web/static/fonts/google/Tajawal/OFL.txt | 93 + .../fonts/google/Tajawal/Tajawal-Black.ttf | Bin 0 -> 55712 bytes .../fonts/google/Tajawal/Tajawal-Bold.ttf | Bin 0 -> 56568 bytes .../google/Tajawal/Tajawal-ExtraBold.ttf | Bin 0 -> 56292 bytes .../google/Tajawal/Tajawal-ExtraLight.ttf | Bin 0 -> 52444 bytes .../fonts/google/Tajawal/Tajawal-Light.ttf | Bin 0 -> 57320 bytes .../fonts/google/Tajawal/Tajawal-Medium.ttf | Bin 0 -> 57360 bytes .../fonts/google/Tajawal/Tajawal-Regular.ttf | Bin 0 -> 56088 bytes .../static/fonts/lato/Lato-Bla-webfont.eot | Bin 0 -> 53590 bytes .../static/fonts/lato/Lato-Bla-webfont.svg | 311 + .../static/fonts/lato/Lato-Bla-webfont.ttf | Bin 0 -> 53372 bytes .../static/fonts/lato/Lato-Bla-webfont.woff | Bin 0 -> 32964 bytes .../static/fonts/lato/Lato-BlaIta-webfont.eot | Bin 0 -> 70998 bytes .../static/fonts/lato/Lato-BlaIta-webfont.svg | 295 + .../static/fonts/lato/Lato-BlaIta-webfont.ttf | Bin 0 -> 70752 bytes .../fonts/lato/Lato-BlaIta-webfont.woff | Bin 0 -> 36596 bytes .../static/fonts/lato/Lato-Bol-webfont.eot | Bin 0 -> 55858 bytes .../static/fonts/lato/Lato-Bol-webfont.svg | 311 + .../static/fonts/lato/Lato-Bol-webfont.ttf | Bin 0 -> 55644 bytes .../static/fonts/lato/Lato-Bol-webfont.woff | Bin 0 -> 34404 bytes .../static/fonts/lato/Lato-BolIta-webfont.eot | Bin 0 -> 76762 bytes .../static/fonts/lato/Lato-BolIta-webfont.svg | 295 + .../static/fonts/lato/Lato-BolIta-webfont.ttf | Bin 0 -> 76520 bytes .../fonts/lato/Lato-BolIta-webfont.woff | Bin 0 -> 38120 bytes .../static/fonts/lato/Lato-Hai-webfont.eot | Bin 0 -> 57458 bytes .../static/fonts/lato/Lato-Hai-webfont.svg | 311 + .../static/fonts/lato/Lato-Hai-webfont.ttf | Bin 0 -> 57228 bytes .../static/fonts/lato/Lato-Hai-webfont.woff | Bin 0 -> 33076 bytes .../static/fonts/lato/Lato-HaiIta-webfont.eot | Bin 0 -> 46330 bytes .../static/fonts/lato/Lato-HaiIta-webfont.svg | 295 + .../static/fonts/lato/Lato-HaiIta-webfont.ttf | Bin 0 -> 46072 bytes .../fonts/lato/Lato-HaiIta-webfont.woff | Bin 0 -> 26204 bytes .../static/fonts/lato/Lato-Lig-webfont.eot | Bin 0 -> 53402 bytes .../static/fonts/lato/Lato-Lig-webfont.svg | 311 + .../static/fonts/lato/Lato-Lig-webfont.ttf | Bin 0 -> 53184 bytes .../static/fonts/lato/Lato-Lig-webfont.woff | Bin 0 -> 33600 bytes .../static/fonts/lato/Lato-LigIta-webfont.eot | Bin 0 -> 46490 bytes .../static/fonts/lato/Lato-LigIta-webfont.svg | 295 + .../static/fonts/lato/Lato-LigIta-webfont.ttf | Bin 0 -> 46244 bytes .../fonts/lato/Lato-LigIta-webfont.woff | Bin 0 -> 27192 bytes .../static/fonts/lato/Lato-Reg-webfont.eot | Bin 0 -> 54102 bytes .../static/fonts/lato/Lato-Reg-webfont.svg | 311 + .../static/fonts/lato/Lato-Reg-webfont.ttf | Bin 0 -> 53876 bytes .../static/fonts/lato/Lato-Reg-webfont.woff | Bin 0 -> 33924 bytes .../static/fonts/lato/Lato-RegIta-webfont.eot | Bin 0 -> 74810 bytes .../static/fonts/lato/Lato-RegIta-webfont.svg | 295 + .../static/fonts/lato/Lato-RegIta-webfont.ttf | Bin 0 -> 74588 bytes .../fonts/lato/Lato-RegIta-webfont.woff | Bin 0 -> 37840 bytes .../fonts/lato/SIL-Open-Font-License-1.1.txt | 91 + .../fonts/sign/LaBelleAurore-Regular.ttf | Bin 0 -> 53488 bytes .../static/fonts/sign/LaBelleAurore-ofl.txt | 93 + .../fonts/sign/MarckScript-Regular-ofl.txt | 94 + .../static/fonts/sign/MarckScript-Regular.ttf | Bin 0 -> 81820 bytes .../web/static/fonts/sign/NotoSans-Reg.ttf | Bin 0 -> 455188 bytes .../fonts/sign/OoohBaby-Regular-ofl.txt | 93 + .../static/fonts/sign/OoohBaby-Regular.ttf | Bin 0 -> 130952 bytes .../fonts/sign/ReenieBeanie-Regular.ttf | Bin 0 -> 140760 bytes .../static/fonts/sign/ReenieBeanie-ofl.txt | 93 + .../fonts/sign/ShadowsIntoLight-Regular.ttf | Bin 0 -> 48292 bytes .../fonts/sign/ShadowsIntoLight-ofl.txt | 93 + .../web/static/fonts/sign/Zeyada-Regular.ttf | Bin 0 -> 57316 bytes frontend/web/static/fonts/sign/Zeyada-ofl.txt | 93 + frontend/web/static/fonts/sign/khand.ttf | Bin 0 -> 38344 bytes frontend/web/static/img/barcode.svg | 20 + frontend/web/static/img/bill.svg | 21 + frontend/web/static/img/default_icon_app.png | Bin 0 -> 9824 bytes frontend/web/static/img/empty_folder.svg | 15 + .../web/static/img/enterprise_upgrade.jpg | Bin 0 -> 375229 bytes frontend/web/static/img/favicon.ico | Bin 0 -> 1150 bytes frontend/web/static/img/folder.svg | 13 + frontend/web/static/img/form_sheetbg.png | Bin 0 -> 83 bytes frontend/web/static/img/graph_background.png | Bin 0 -> 3362 bytes frontend/web/static/img/logo.png | Bin 0 -> 2901 bytes frontend/web/static/img/logo2.png | Bin 0 -> 2745 bytes .../static/img/logo_inverse_white_206px.png | Bin 0 -> 8208 bytes .../web/static/img/mimetypes/addresses.svg | 17 + frontend/web/static/img/mimetypes/archive.svg | 14 + frontend/web/static/img/mimetypes/audio.svg | 14 + frontend/web/static/img/mimetypes/binary.svg | 14 + .../web/static/img/mimetypes/calendar.svg | 14 + .../web/static/img/mimetypes/certificate.svg | 17 + frontend/web/static/img/mimetypes/disk.svg | 13 + .../web/static/img/mimetypes/document.svg | 20 + frontend/web/static/img/mimetypes/font.svg | 18 + frontend/web/static/img/mimetypes/image.svg | 14 + .../web/static/img/mimetypes/javascript.svg | 19 + frontend/web/static/img/mimetypes/pdf.svg | 14 + .../web/static/img/mimetypes/presentation.svg | 13 + frontend/web/static/img/mimetypes/print.svg | 14 + frontend/web/static/img/mimetypes/script.svg | 11 + .../web/static/img/mimetypes/spreadsheet.svg | 13 + frontend/web/static/img/mimetypes/text.svg | 13 + frontend/web/static/img/mimetypes/unknown.svg | 14 + frontend/web/static/img/mimetypes/vector.svg | 27 + frontend/web/static/img/mimetypes/video.svg | 17 + .../web/static/img/mimetypes/web_code.svg | 14 + .../web/static/img/mimetypes/web_style.svg | 22 + frontend/web/static/img/neutral_face.svg | 41 + frontend/web/static/img/nologo.png | Bin 0 -> 3261 bytes frontend/web/static/img/odoo-icon-192x192.png | Bin 0 -> 8623 bytes frontend/web/static/img/odoo-icon-512x512.png | Bin 0 -> 24280 bytes frontend/web/static/img/odoo-icon-ios.png | Bin 0 -> 14019 bytes frontend/web/static/img/odoo-icon.svg | 24 + frontend/web/static/img/odoo_logo.svg | 10 + frontend/web/static/img/odoo_logo_dark.svg | 10 + frontend/web/static/img/odoo_logo_tiny.png | Bin 0 -> 627 bytes frontend/web/static/img/openhand.cur | Bin 0 -> 326 bytes frontend/web/static/img/placeholder.png | Bin 0 -> 6078 bytes frontend/web/static/img/quotation.svg | 19 + frontend/web/static/img/rfq.svg | 9 + frontend/web/static/img/sep-a.gif | Bin 0 -> 43 bytes frontend/web/static/img/smile.svg | 1 + frontend/web/static/img/smiling_face.svg | 44 + frontend/web/static/img/spin.png | Bin 0 -> 570 bytes frontend/web/static/img/spin.svg | 34 + frontend/web/static/img/transform.svg | 4 + frontend/web/static/img/transparent.png | Bin 0 -> 175 bytes frontend/web/static/img/user_menu_avatar.png | Bin 0 -> 453 bytes frontend/web/static/img/user_placeholder.jpg | Bin 0 -> 6462 bytes frontend/web/static/lib/bootstrap/LICENSE | 21 + .../lib/bootstrap/dist/css/bootstrap.css | 12057 +++++++++ .../lib/bootstrap/dist/css/bootstrap.css.map | 1 + .../web/static/lib/bootstrap/js/dist/alert.js | 90 + .../lib/bootstrap/js/dist/base-component.js | 84 + .../static/lib/bootstrap/js/dist/button.js | 79 + .../static/lib/bootstrap/js/dist/carousel.js | 388 + .../static/lib/bootstrap/js/dist/collapse.js | 249 + .../static/lib/bootstrap/js/dist/dom/data.js | 63 + .../bootstrap/js/dist/dom/event-handler.js | 237 + .../lib/bootstrap/js/dist/dom/manipulator.js | 72 + .../bootstrap/js/dist/dom/selector-engine.js | 104 + .../static/lib/bootstrap/js/dist/dropdown.js | 403 + .../web/static/lib/bootstrap/js/dist/modal.js | 320 + .../static/lib/bootstrap/js/dist/offcanvas.js | 246 + .../static/lib/bootstrap/js/dist/popover.js | 96 + .../static/lib/bootstrap/js/dist/scrollspy.js | 275 + .../web/static/lib/bootstrap/js/dist/tab.js | 285 + .../web/static/lib/bootstrap/js/dist/toast.js | 199 + .../static/lib/bootstrap/js/dist/tooltip.js | 546 + .../lib/bootstrap/js/dist/util/backdrop.js | 139 + .../js/dist/util/component-functions.js | 42 + .../lib/bootstrap/js/dist/util/config.js | 68 + .../lib/bootstrap/js/dist/util/focustrap.js | 113 + .../lib/bootstrap/js/dist/util/index.js | 281 + .../lib/bootstrap/js/dist/util/sanitizer.js | 114 + .../lib/bootstrap/js/dist/util/scrollbar.js | 113 + .../lib/bootstrap/js/dist/util/swipe.js | 135 + .../js/dist/util/template-factory.js | 151 + .../static/lib/bootstrap/scss/_accordion.scss | 158 + .../web/static/lib/bootstrap/scss/_alert.scss | 68 + .../web/static/lib/bootstrap/scss/_badge.scss | 38 + .../lib/bootstrap/scss/_breadcrumb.scss | 40 + .../lib/bootstrap/scss/_button-group.scss | 142 + .../static/lib/bootstrap/scss/_buttons.scss | 216 + .../web/static/lib/bootstrap/scss/_card.scss | 239 + .../static/lib/bootstrap/scss/_carousel.scss | 236 + .../web/static/lib/bootstrap/scss/_close.scss | 63 + .../lib/bootstrap/scss/_containers.scss | 41 + .../static/lib/bootstrap/scss/_dropdown.scss | 250 + .../web/static/lib/bootstrap/scss/_forms.scss | 9 + .../static/lib/bootstrap/scss/_functions.scss | 302 + .../web/static/lib/bootstrap/scss/_grid.scss | 39 + .../static/lib/bootstrap/scss/_helpers.scss | 12 + .../static/lib/bootstrap/scss/_images.scss | 42 + .../lib/bootstrap/scss/_list-group.scss | 197 + .../web/static/lib/bootstrap/scss/_maps.scss | 174 + .../static/lib/bootstrap/scss/_mixins.scss | 42 + .../web/static/lib/bootstrap/scss/_modal.scss | 236 + .../web/static/lib/bootstrap/scss/_nav.scss | 197 + .../static/lib/bootstrap/scss/_navbar.scss | 289 + .../static/lib/bootstrap/scss/_offcanvas.scss | 143 + .../lib/bootstrap/scss/_pagination.scss | 109 + .../lib/bootstrap/scss/_placeholders.scss | 51 + .../static/lib/bootstrap/scss/_popover.scss | 196 + .../static/lib/bootstrap/scss/_progress.scss | 68 + .../static/lib/bootstrap/scss/_reboot.scss | 611 + .../web/static/lib/bootstrap/scss/_root.scss | 187 + .../static/lib/bootstrap/scss/_spinners.scss | 85 + .../static/lib/bootstrap/scss/_tables.scss | 171 + .../static/lib/bootstrap/scss/_toasts.scss | 73 + .../static/lib/bootstrap/scss/_tooltip.scss | 119 + .../lib/bootstrap/scss/_transitions.scss | 27 + .../web/static/lib/bootstrap/scss/_type.scss | 106 + .../static/lib/bootstrap/scss/_utilities.scss | 806 + .../lib/bootstrap/scss/_variables-dark.scss | 87 + .../static/lib/bootstrap/scss/_variables.scss | 1751 ++ .../lib/bootstrap/scss/bootstrap-grid.scss | 62 + .../lib/bootstrap/scss/bootstrap-reboot.scss | 10 + .../bootstrap/scss/bootstrap-utilities.scss | 19 + .../static/lib/bootstrap/scss/bootstrap.scss | 52 + .../scss/forms/_floating-labels.scss | 95 + .../lib/bootstrap/scss/forms/_form-check.scss | 189 + .../bootstrap/scss/forms/_form-control.scss | 214 + .../lib/bootstrap/scss/forms/_form-range.scss | 91 + .../bootstrap/scss/forms/_form-select.scss | 80 + .../lib/bootstrap/scss/forms/_form-text.scss | 11 + .../bootstrap/scss/forms/_input-group.scss | 132 + .../lib/bootstrap/scss/forms/_labels.scss | 36 + .../lib/bootstrap/scss/forms/_validation.scss | 12 + .../lib/bootstrap/scss/helpers/_clearfix.scss | 3 + .../lib/bootstrap/scss/helpers/_color-bg.scss | 7 + .../scss/helpers/_colored-links.scss | 30 + .../bootstrap/scss/helpers/_focus-ring.scss | 5 + .../bootstrap/scss/helpers/_icon-link.scss | 25 + .../lib/bootstrap/scss/helpers/_position.scss | 36 + .../lib/bootstrap/scss/helpers/_ratio.scss | 26 + .../lib/bootstrap/scss/helpers/_stacks.scss | 15 + .../scss/helpers/_stretched-link.scss | 15 + .../scss/helpers/_text-truncation.scss | 7 + .../scss/helpers/_visually-hidden.scss | 8 + .../lib/bootstrap/scss/helpers/_vr.scss | 8 + .../lib/bootstrap/scss/mixins/_alert.scss | 18 + .../lib/bootstrap/scss/mixins/_backdrop.scss | 14 + .../lib/bootstrap/scss/mixins/_banner.scss | 7 + .../bootstrap/scss/mixins/_border-radius.scss | 78 + .../bootstrap/scss/mixins/_box-shadow.scss | 18 + .../bootstrap/scss/mixins/_breakpoints.scss | 127 + .../lib/bootstrap/scss/mixins/_buttons.scss | 70 + .../lib/bootstrap/scss/mixins/_caret.scss | 69 + .../lib/bootstrap/scss/mixins/_clearfix.scss | 9 + .../bootstrap/scss/mixins/_color-mode.scss | 21 + .../bootstrap/scss/mixins/_color-scheme.scss | 7 + .../lib/bootstrap/scss/mixins/_container.scss | 11 + .../lib/bootstrap/scss/mixins/_deprecate.scss | 10 + .../lib/bootstrap/scss/mixins/_forms.scss | 163 + .../lib/bootstrap/scss/mixins/_gradients.scss | 47 + .../lib/bootstrap/scss/mixins/_grid.scss | 151 + .../lib/bootstrap/scss/mixins/_image.scss | 16 + .../bootstrap/scss/mixins/_list-group.scss | 26 + .../lib/bootstrap/scss/mixins/_lists.scss | 7 + .../bootstrap/scss/mixins/_pagination.scss | 10 + .../bootstrap/scss/mixins/_reset-text.scss | 17 + .../lib/bootstrap/scss/mixins/_resize.scss | 6 + .../scss/mixins/_table-variants.scss | 24 + .../bootstrap/scss/mixins/_text-truncate.scss | 8 + .../bootstrap/scss/mixins/_transition.scss | 26 + .../lib/bootstrap/scss/mixins/_utilities.scss | 97 + .../scss/mixins/_visually-hidden.scss | 33 + .../lib/bootstrap/scss/utilities/_api.scss | 47 + .../web/static/lib/dompurify/DOMpurify.js | 1562 ++ .../web/static/lib/fullcalendar/LICENSE.md | 22 + frontend/web/static/lib/hoot/tests/index.html | 92 + .../web/static/lib/hoot/ui/hoot_style.css | 1689 ++ frontend/web/static/lib/luxon/luxon.js | 8606 ++++++ .../web/static/lib/odoo_ui_icons/LICENSE.md | 201 + .../web/static/lib/odoo_ui_icons/Read Me.txt | 9 + .../odoo_ui_icons/fonts/odoo_ui_icons.woff | Bin 0 -> 11592 bytes .../odoo_ui_icons/fonts/odoo_ui_icons.woff2 | Bin 0 -> 9708 bytes .../web/static/lib/odoo_ui_icons/style.css | 106 + frontend/web/static/lib/owl/odoo_module.js | 5 + frontend/web/static/lib/owl/owl.js | 6340 +++++ frontend/web/static/lib/pdfjs/LICENSE | 177 + .../static/lib/pdfjs/web/cmaps/78-EUC-H.bcmap | Bin 0 -> 2404 bytes .../static/lib/pdfjs/web/cmaps/78-EUC-V.bcmap | Bin 0 -> 173 bytes .../web/static/lib/pdfjs/web/cmaps/78-H.bcmap | Bin 0 -> 2379 bytes .../lib/pdfjs/web/cmaps/78-RKSJ-H.bcmap | Bin 0 -> 2398 bytes .../lib/pdfjs/web/cmaps/78-RKSJ-V.bcmap | Bin 0 -> 173 bytes .../web/static/lib/pdfjs/web/cmaps/78-V.bcmap | Bin 0 -> 169 bytes .../lib/pdfjs/web/cmaps/78ms-RKSJ-H.bcmap | Bin 0 -> 2651 bytes .../lib/pdfjs/web/cmaps/78ms-RKSJ-V.bcmap | Bin 0 -> 290 bytes .../lib/pdfjs/web/cmaps/83pv-RKSJ-H.bcmap | Bin 0 -> 905 bytes .../lib/pdfjs/web/cmaps/90ms-RKSJ-H.bcmap | Bin 0 -> 721 bytes .../lib/pdfjs/web/cmaps/90ms-RKSJ-V.bcmap | Bin 0 -> 290 bytes .../lib/pdfjs/web/cmaps/90msp-RKSJ-H.bcmap | Bin 0 -> 715 bytes .../lib/pdfjs/web/cmaps/90msp-RKSJ-V.bcmap | Bin 0 -> 291 bytes .../lib/pdfjs/web/cmaps/90pv-RKSJ-H.bcmap | Bin 0 -> 982 bytes .../lib/pdfjs/web/cmaps/90pv-RKSJ-V.bcmap | Bin 0 -> 260 bytes .../static/lib/pdfjs/web/cmaps/Add-H.bcmap | Bin 0 -> 2419 bytes .../lib/pdfjs/web/cmaps/Add-RKSJ-H.bcmap | Bin 0 -> 2413 bytes .../lib/pdfjs/web/cmaps/Add-RKSJ-V.bcmap | Bin 0 -> 287 bytes .../static/lib/pdfjs/web/cmaps/Add-V.bcmap | Bin 0 -> 282 bytes .../lib/pdfjs/web/cmaps/Adobe-CNS1-0.bcmap | Bin 0 -> 317 bytes .../lib/pdfjs/web/cmaps/Adobe-CNS1-1.bcmap | Bin 0 -> 371 bytes .../lib/pdfjs/web/cmaps/Adobe-CNS1-2.bcmap | Bin 0 -> 376 bytes .../lib/pdfjs/web/cmaps/Adobe-CNS1-3.bcmap | Bin 0 -> 401 bytes .../lib/pdfjs/web/cmaps/Adobe-CNS1-4.bcmap | Bin 0 -> 405 bytes .../lib/pdfjs/web/cmaps/Adobe-CNS1-5.bcmap | Bin 0 -> 406 bytes .../lib/pdfjs/web/cmaps/Adobe-CNS1-6.bcmap | Bin 0 -> 406 bytes .../lib/pdfjs/web/cmaps/Adobe-CNS1-UCS2.bcmap | Bin 0 -> 41193 bytes .../lib/pdfjs/web/cmaps/Adobe-GB1-0.bcmap | Bin 0 -> 217 bytes .../lib/pdfjs/web/cmaps/Adobe-GB1-1.bcmap | Bin 0 -> 250 bytes .../lib/pdfjs/web/cmaps/Adobe-GB1-2.bcmap | Bin 0 -> 465 bytes .../lib/pdfjs/web/cmaps/Adobe-GB1-3.bcmap | Bin 0 -> 470 bytes .../lib/pdfjs/web/cmaps/Adobe-GB1-4.bcmap | Bin 0 -> 601 bytes .../lib/pdfjs/web/cmaps/Adobe-GB1-5.bcmap | Bin 0 -> 625 bytes .../lib/pdfjs/web/cmaps/Adobe-GB1-UCS2.bcmap | Bin 0 -> 33974 bytes .../lib/pdfjs/web/cmaps/Adobe-Japan1-0.bcmap | Bin 0 -> 225 bytes .../lib/pdfjs/web/cmaps/Adobe-Japan1-1.bcmap | Bin 0 -> 226 bytes .../lib/pdfjs/web/cmaps/Adobe-Japan1-2.bcmap | Bin 0 -> 233 bytes .../lib/pdfjs/web/cmaps/Adobe-Japan1-3.bcmap | Bin 0 -> 242 bytes .../lib/pdfjs/web/cmaps/Adobe-Japan1-4.bcmap | Bin 0 -> 337 bytes .../lib/pdfjs/web/cmaps/Adobe-Japan1-5.bcmap | Bin 0 -> 430 bytes .../lib/pdfjs/web/cmaps/Adobe-Japan1-6.bcmap | Bin 0 -> 485 bytes .../pdfjs/web/cmaps/Adobe-Japan1-UCS2.bcmap | Bin 0 -> 40951 bytes .../lib/pdfjs/web/cmaps/Adobe-Korea1-0.bcmap | Bin 0 -> 241 bytes .../lib/pdfjs/web/cmaps/Adobe-Korea1-1.bcmap | Bin 0 -> 386 bytes .../lib/pdfjs/web/cmaps/Adobe-Korea1-2.bcmap | Bin 0 -> 391 bytes .../pdfjs/web/cmaps/Adobe-Korea1-UCS2.bcmap | Bin 0 -> 23293 bytes .../web/static/lib/pdfjs/web/cmaps/B5-H.bcmap | Bin 0 -> 1086 bytes .../web/static/lib/pdfjs/web/cmaps/B5-V.bcmap | Bin 0 -> 142 bytes .../static/lib/pdfjs/web/cmaps/B5pc-H.bcmap | Bin 0 -> 1099 bytes .../static/lib/pdfjs/web/cmaps/B5pc-V.bcmap | Bin 0 -> 144 bytes .../lib/pdfjs/web/cmaps/CNS-EUC-H.bcmap | Bin 0 -> 1780 bytes .../lib/pdfjs/web/cmaps/CNS-EUC-V.bcmap | Bin 0 -> 1920 bytes .../static/lib/pdfjs/web/cmaps/CNS1-H.bcmap | Bin 0 -> 706 bytes .../static/lib/pdfjs/web/cmaps/CNS1-V.bcmap | Bin 0 -> 143 bytes .../static/lib/pdfjs/web/cmaps/CNS2-H.bcmap | Bin 0 -> 504 bytes .../static/lib/pdfjs/web/cmaps/CNS2-V.bcmap | 3 + .../lib/pdfjs/web/cmaps/ETHK-B5-H.bcmap | Bin 0 -> 4426 bytes .../lib/pdfjs/web/cmaps/ETHK-B5-V.bcmap | Bin 0 -> 158 bytes .../lib/pdfjs/web/cmaps/ETen-B5-H.bcmap | Bin 0 -> 1125 bytes .../lib/pdfjs/web/cmaps/ETen-B5-V.bcmap | Bin 0 -> 158 bytes .../lib/pdfjs/web/cmaps/ETenms-B5-H.bcmap | 3 + .../lib/pdfjs/web/cmaps/ETenms-B5-V.bcmap | Bin 0 -> 172 bytes .../static/lib/pdfjs/web/cmaps/EUC-H.bcmap | Bin 0 -> 578 bytes .../static/lib/pdfjs/web/cmaps/EUC-V.bcmap | Bin 0 -> 170 bytes .../static/lib/pdfjs/web/cmaps/Ext-H.bcmap | Bin 0 -> 2536 bytes .../lib/pdfjs/web/cmaps/Ext-RKSJ-H.bcmap | Bin 0 -> 2542 bytes .../lib/pdfjs/web/cmaps/Ext-RKSJ-V.bcmap | Bin 0 -> 218 bytes .../static/lib/pdfjs/web/cmaps/Ext-V.bcmap | Bin 0 -> 215 bytes .../static/lib/pdfjs/web/cmaps/GB-EUC-H.bcmap | Bin 0 -> 549 bytes .../static/lib/pdfjs/web/cmaps/GB-EUC-V.bcmap | Bin 0 -> 179 bytes .../web/static/lib/pdfjs/web/cmaps/GB-H.bcmap | 4 + .../web/static/lib/pdfjs/web/cmaps/GB-V.bcmap | Bin 0 -> 175 bytes .../lib/pdfjs/web/cmaps/GBK-EUC-H.bcmap | Bin 0 -> 14692 bytes .../lib/pdfjs/web/cmaps/GBK-EUC-V.bcmap | Bin 0 -> 180 bytes .../static/lib/pdfjs/web/cmaps/GBK2K-H.bcmap | Bin 0 -> 19662 bytes .../static/lib/pdfjs/web/cmaps/GBK2K-V.bcmap | Bin 0 -> 219 bytes .../lib/pdfjs/web/cmaps/GBKp-EUC-H.bcmap | Bin 0 -> 14686 bytes .../lib/pdfjs/web/cmaps/GBKp-EUC-V.bcmap | Bin 0 -> 181 bytes .../lib/pdfjs/web/cmaps/GBT-EUC-H.bcmap | Bin 0 -> 7290 bytes .../lib/pdfjs/web/cmaps/GBT-EUC-V.bcmap | Bin 0 -> 180 bytes .../static/lib/pdfjs/web/cmaps/GBT-H.bcmap | Bin 0 -> 7269 bytes .../static/lib/pdfjs/web/cmaps/GBT-V.bcmap | Bin 0 -> 176 bytes .../lib/pdfjs/web/cmaps/GBTpc-EUC-H.bcmap | Bin 0 -> 7298 bytes .../lib/pdfjs/web/cmaps/GBTpc-EUC-V.bcmap | Bin 0 -> 182 bytes .../lib/pdfjs/web/cmaps/GBpc-EUC-H.bcmap | Bin 0 -> 557 bytes .../lib/pdfjs/web/cmaps/GBpc-EUC-V.bcmap | Bin 0 -> 181 bytes .../web/static/lib/pdfjs/web/cmaps/H.bcmap | Bin 0 -> 553 bytes .../lib/pdfjs/web/cmaps/HKdla-B5-H.bcmap | Bin 0 -> 2654 bytes .../lib/pdfjs/web/cmaps/HKdla-B5-V.bcmap | Bin 0 -> 148 bytes .../lib/pdfjs/web/cmaps/HKdlb-B5-H.bcmap | Bin 0 -> 2414 bytes .../lib/pdfjs/web/cmaps/HKdlb-B5-V.bcmap | Bin 0 -> 148 bytes .../lib/pdfjs/web/cmaps/HKgccs-B5-H.bcmap | Bin 0 -> 2292 bytes .../lib/pdfjs/web/cmaps/HKgccs-B5-V.bcmap | Bin 0 -> 149 bytes .../lib/pdfjs/web/cmaps/HKm314-B5-H.bcmap | Bin 0 -> 1772 bytes .../lib/pdfjs/web/cmaps/HKm314-B5-V.bcmap | Bin 0 -> 149 bytes .../lib/pdfjs/web/cmaps/HKm471-B5-H.bcmap | Bin 0 -> 2171 bytes .../lib/pdfjs/web/cmaps/HKm471-B5-V.bcmap | Bin 0 -> 149 bytes .../lib/pdfjs/web/cmaps/HKscs-B5-H.bcmap | Bin 0 -> 4437 bytes .../lib/pdfjs/web/cmaps/HKscs-B5-V.bcmap | Bin 0 -> 159 bytes .../static/lib/pdfjs/web/cmaps/Hankaku.bcmap | Bin 0 -> 132 bytes .../static/lib/pdfjs/web/cmaps/Hiragana.bcmap | Bin 0 -> 124 bytes .../lib/pdfjs/web/cmaps/KSC-EUC-H.bcmap | Bin 0 -> 1848 bytes .../lib/pdfjs/web/cmaps/KSC-EUC-V.bcmap | Bin 0 -> 164 bytes .../static/lib/pdfjs/web/cmaps/KSC-H.bcmap | Bin 0 -> 1831 bytes .../lib/pdfjs/web/cmaps/KSC-Johab-H.bcmap | Bin 0 -> 16791 bytes .../lib/pdfjs/web/cmaps/KSC-Johab-V.bcmap | Bin 0 -> 166 bytes .../static/lib/pdfjs/web/cmaps/KSC-V.bcmap | Bin 0 -> 160 bytes .../lib/pdfjs/web/cmaps/KSCms-UHC-H.bcmap | Bin 0 -> 2787 bytes .../lib/pdfjs/web/cmaps/KSCms-UHC-HW-H.bcmap | Bin 0 -> 2789 bytes .../lib/pdfjs/web/cmaps/KSCms-UHC-HW-V.bcmap | Bin 0 -> 169 bytes .../lib/pdfjs/web/cmaps/KSCms-UHC-V.bcmap | Bin 0 -> 166 bytes .../lib/pdfjs/web/cmaps/KSCpc-EUC-H.bcmap | Bin 0 -> 2024 bytes .../lib/pdfjs/web/cmaps/KSCpc-EUC-V.bcmap | Bin 0 -> 166 bytes .../static/lib/pdfjs/web/cmaps/Katakana.bcmap | Bin 0 -> 100 bytes .../web/static/lib/pdfjs/web/cmaps/LICENSE | 36 + .../static/lib/pdfjs/web/cmaps/NWP-H.bcmap | Bin 0 -> 2765 bytes .../static/lib/pdfjs/web/cmaps/NWP-V.bcmap | Bin 0 -> 252 bytes .../static/lib/pdfjs/web/cmaps/RKSJ-H.bcmap | Bin 0 -> 534 bytes .../static/lib/pdfjs/web/cmaps/RKSJ-V.bcmap | Bin 0 -> 170 bytes .../static/lib/pdfjs/web/cmaps/Roman.bcmap | Bin 0 -> 96 bytes .../lib/pdfjs/web/cmaps/UniCNS-UCS2-H.bcmap | Bin 0 -> 48280 bytes .../lib/pdfjs/web/cmaps/UniCNS-UCS2-V.bcmap | Bin 0 -> 156 bytes .../lib/pdfjs/web/cmaps/UniCNS-UTF16-H.bcmap | Bin 0 -> 50419 bytes .../lib/pdfjs/web/cmaps/UniCNS-UTF16-V.bcmap | Bin 0 -> 156 bytes .../lib/pdfjs/web/cmaps/UniCNS-UTF32-H.bcmap | Bin 0 -> 52679 bytes .../lib/pdfjs/web/cmaps/UniCNS-UTF32-V.bcmap | Bin 0 -> 160 bytes .../lib/pdfjs/web/cmaps/UniCNS-UTF8-H.bcmap | Bin 0 -> 53629 bytes .../lib/pdfjs/web/cmaps/UniCNS-UTF8-V.bcmap | Bin 0 -> 157 bytes .../lib/pdfjs/web/cmaps/UniGB-UCS2-H.bcmap | Bin 0 -> 43366 bytes .../lib/pdfjs/web/cmaps/UniGB-UCS2-V.bcmap | Bin 0 -> 193 bytes .../lib/pdfjs/web/cmaps/UniGB-UTF16-H.bcmap | Bin 0 -> 44086 bytes .../lib/pdfjs/web/cmaps/UniGB-UTF16-V.bcmap | Bin 0 -> 178 bytes .../lib/pdfjs/web/cmaps/UniGB-UTF32-H.bcmap | Bin 0 -> 45738 bytes .../lib/pdfjs/web/cmaps/UniGB-UTF32-V.bcmap | Bin 0 -> 182 bytes .../lib/pdfjs/web/cmaps/UniGB-UTF8-H.bcmap | Bin 0 -> 46837 bytes .../lib/pdfjs/web/cmaps/UniGB-UTF8-V.bcmap | Bin 0 -> 181 bytes .../lib/pdfjs/web/cmaps/UniJIS-UCS2-H.bcmap | Bin 0 -> 25439 bytes .../pdfjs/web/cmaps/UniJIS-UCS2-HW-H.bcmap | Bin 0 -> 119 bytes .../pdfjs/web/cmaps/UniJIS-UCS2-HW-V.bcmap | Bin 0 -> 680 bytes .../lib/pdfjs/web/cmaps/UniJIS-UCS2-V.bcmap | Bin 0 -> 664 bytes .../lib/pdfjs/web/cmaps/UniJIS-UTF16-H.bcmap | Bin 0 -> 39443 bytes .../lib/pdfjs/web/cmaps/UniJIS-UTF16-V.bcmap | Bin 0 -> 643 bytes .../lib/pdfjs/web/cmaps/UniJIS-UTF32-H.bcmap | Bin 0 -> 40539 bytes .../lib/pdfjs/web/cmaps/UniJIS-UTF32-V.bcmap | Bin 0 -> 677 bytes .../lib/pdfjs/web/cmaps/UniJIS-UTF8-H.bcmap | Bin 0 -> 41695 bytes .../lib/pdfjs/web/cmaps/UniJIS-UTF8-V.bcmap | Bin 0 -> 678 bytes .../pdfjs/web/cmaps/UniJIS2004-UTF16-H.bcmap | Bin 0 -> 39534 bytes .../pdfjs/web/cmaps/UniJIS2004-UTF16-V.bcmap | Bin 0 -> 647 bytes .../pdfjs/web/cmaps/UniJIS2004-UTF32-H.bcmap | Bin 0 -> 40630 bytes .../pdfjs/web/cmaps/UniJIS2004-UTF32-V.bcmap | Bin 0 -> 681 bytes .../pdfjs/web/cmaps/UniJIS2004-UTF8-H.bcmap | Bin 0 -> 41779 bytes .../pdfjs/web/cmaps/UniJIS2004-UTF8-V.bcmap | Bin 0 -> 682 bytes .../pdfjs/web/cmaps/UniJISPro-UCS2-HW-V.bcmap | Bin 0 -> 705 bytes .../pdfjs/web/cmaps/UniJISPro-UCS2-V.bcmap | Bin 0 -> 689 bytes .../pdfjs/web/cmaps/UniJISPro-UTF8-V.bcmap | Bin 0 -> 726 bytes .../pdfjs/web/cmaps/UniJISX0213-UTF32-H.bcmap | Bin 0 -> 40517 bytes .../pdfjs/web/cmaps/UniJISX0213-UTF32-V.bcmap | Bin 0 -> 684 bytes .../web/cmaps/UniJISX02132004-UTF32-H.bcmap | Bin 0 -> 40608 bytes .../web/cmaps/UniJISX02132004-UTF32-V.bcmap | Bin 0 -> 688 bytes .../lib/pdfjs/web/cmaps/UniKS-UCS2-H.bcmap | Bin 0 -> 25783 bytes .../lib/pdfjs/web/cmaps/UniKS-UCS2-V.bcmap | Bin 0 -> 178 bytes .../lib/pdfjs/web/cmaps/UniKS-UTF16-H.bcmap | Bin 0 -> 26327 bytes .../lib/pdfjs/web/cmaps/UniKS-UTF16-V.bcmap | Bin 0 -> 164 bytes .../lib/pdfjs/web/cmaps/UniKS-UTF32-H.bcmap | Bin 0 -> 26451 bytes .../lib/pdfjs/web/cmaps/UniKS-UTF32-V.bcmap | Bin 0 -> 168 bytes .../lib/pdfjs/web/cmaps/UniKS-UTF8-H.bcmap | Bin 0 -> 27790 bytes .../lib/pdfjs/web/cmaps/UniKS-UTF8-V.bcmap | Bin 0 -> 169 bytes .../web/static/lib/pdfjs/web/cmaps/V.bcmap | Bin 0 -> 166 bytes .../lib/pdfjs/web/cmaps/WP-Symbol.bcmap | Bin 0 -> 179 bytes .../web/static/lib/pdfjs/web/debugger.css | 111 + .../lib/pdfjs/web/images/altText_add.svg | 3 + .../pdfjs/web/images/altText_disclaimer.svg | 3 + .../lib/pdfjs/web/images/altText_done.svg | 3 + .../lib/pdfjs/web/images/altText_spinner.svg | 30 + .../lib/pdfjs/web/images/altText_warning.svg | 3 + .../lib/pdfjs/web/images/annotation-check.svg | 11 + .../pdfjs/web/images/annotation-comment.svg | 16 + .../lib/pdfjs/web/images/annotation-help.svg | 26 + .../pdfjs/web/images/annotation-insert.svg | 10 + .../lib/pdfjs/web/images/annotation-key.svg | 11 + .../web/images/annotation-newparagraph.svg | 11 + .../pdfjs/web/images/annotation-noicon.svg | 7 + .../lib/pdfjs/web/images/annotation-note.svg | 42 + .../pdfjs/web/images/annotation-paperclip.svg | 6 + .../pdfjs/web/images/annotation-paragraph.svg | 16 + .../pdfjs/web/images/annotation-pushpin.svg | 7 + .../web/images/cursor-editorFreeHighlight.svg | 6 + .../web/images/cursor-editorFreeText.svg | 3 + .../lib/pdfjs/web/images/cursor-editorInk.svg | 4 + .../web/images/cursor-editorTextHighlight.svg | 8 + .../web/images/editor-toolbar-delete.svg | 5 + .../pdfjs/web/images/findbarButton-next.svg | 3 + .../web/images/findbarButton-previous.svg | 3 + .../web/images/gv-toolbarButton-download.svg | 3 + .../lib/pdfjs/web/images/loading-icon.gif | Bin 0 -> 2545 bytes .../static/lib/pdfjs/web/images/loading.svg | 1 + .../web/images/messageBar_closingButton.svg | 3 + .../pdfjs/web/images/messageBar_warning.svg | 3 + ...ondaryToolbarButton-documentProperties.svg | 3 + .../secondaryToolbarButton-firstPage.svg | 3 + .../secondaryToolbarButton-handTool.svg | 3 + .../secondaryToolbarButton-lastPage.svg | 3 + .../secondaryToolbarButton-rotateCcw.svg | 3 + .../secondaryToolbarButton-rotateCw.svg | 3 + ...econdaryToolbarButton-scrollHorizontal.svg | 3 + .../secondaryToolbarButton-scrollPage.svg | 3 + .../secondaryToolbarButton-scrollVertical.svg | 3 + .../secondaryToolbarButton-scrollWrapped.svg | 3 + .../secondaryToolbarButton-selectTool.svg | 3 + .../secondaryToolbarButton-spreadEven.svg | 3 + .../secondaryToolbarButton-spreadNone.svg | 3 + .../secondaryToolbarButton-spreadOdd.svg | 3 + .../web/images/toolbarButton-bookmark.svg | 3 + .../toolbarButton-currentOutlineItem.svg | 3 + .../web/images/toolbarButton-download.svg | 4 + .../images/toolbarButton-editorFreeText.svg | 5 + .../images/toolbarButton-editorHighlight.svg | 6 + .../web/images/toolbarButton-editorInk.svg | 4 + .../web/images/toolbarButton-editorStamp.svg | 8 + .../web/images/toolbarButton-menuArrow.svg | 3 + .../web/images/toolbarButton-openFile.svg | 3 + .../web/images/toolbarButton-pageDown.svg | 3 + .../pdfjs/web/images/toolbarButton-pageUp.svg | 3 + .../images/toolbarButton-presentationMode.svg | 3 + .../pdfjs/web/images/toolbarButton-print.svg | 3 + .../pdfjs/web/images/toolbarButton-search.svg | 3 + .../toolbarButton-secondaryToolbarToggle.svg | 3 + .../images/toolbarButton-sidebarToggle.svg | 3 + .../images/toolbarButton-viewAttachments.svg | 3 + .../web/images/toolbarButton-viewLayers.svg | 3 + .../web/images/toolbarButton-viewOutline.svg | 3 + .../images/toolbarButton-viewThumbnail.svg | 3 + .../pdfjs/web/images/toolbarButton-zoomIn.svg | 3 + .../web/images/toolbarButton-zoomOut.svg | 3 + .../pdfjs/web/images/treeitem-collapsed.svg | 1 + .../pdfjs/web/images/treeitem-expanded.svg | 1 + .../lib/pdfjs/web/locale/ach/viewer.ftl | 225 + .../static/lib/pdfjs/web/locale/af/viewer.ftl | 212 + .../static/lib/pdfjs/web/locale/an/viewer.ftl | 257 + .../static/lib/pdfjs/web/locale/ar/viewer.ftl | 425 + .../lib/pdfjs/web/locale/ast/viewer.ftl | 201 + .../static/lib/pdfjs/web/locale/az/viewer.ftl | 257 + .../static/lib/pdfjs/web/locale/be/viewer.ftl | 483 + .../static/lib/pdfjs/web/locale/bg/viewer.ftl | 417 + .../static/lib/pdfjs/web/locale/bn/viewer.ftl | 247 + .../static/lib/pdfjs/web/locale/bo/viewer.ftl | 247 + .../static/lib/pdfjs/web/locale/br/viewer.ftl | 340 + .../lib/pdfjs/web/locale/brx/viewer.ftl | 218 + .../static/lib/pdfjs/web/locale/bs/viewer.ftl | 223 + .../static/lib/pdfjs/web/locale/ca/viewer.ftl | 313 + .../lib/pdfjs/web/locale/cak/viewer.ftl | 291 + .../lib/pdfjs/web/locale/ckb/viewer.ftl | 242 + .../static/lib/pdfjs/web/locale/cs/viewer.ftl | 485 + .../static/lib/pdfjs/web/locale/cy/viewer.ftl | 489 + .../static/lib/pdfjs/web/locale/da/viewer.ftl | 481 + .../static/lib/pdfjs/web/locale/de/viewer.ftl | 481 + .../lib/pdfjs/web/locale/dsb/viewer.ftl | 485 + .../static/lib/pdfjs/web/locale/el/viewer.ftl | 481 + .../lib/pdfjs/web/locale/en-CA/viewer.ftl | 481 + .../lib/pdfjs/web/locale/en-GB/viewer.ftl | 481 + .../lib/pdfjs/web/locale/en-US/viewer.ftl | 505 + .../static/lib/pdfjs/web/locale/eo/viewer.ftl | 481 + .../lib/pdfjs/web/locale/es-AR/viewer.ftl | 481 + .../lib/pdfjs/web/locale/es-CL/viewer.ftl | 481 + .../lib/pdfjs/web/locale/es-ES/viewer.ftl | 481 + .../lib/pdfjs/web/locale/es-MX/viewer.ftl | 401 + .../static/lib/pdfjs/web/locale/et/viewer.ftl | 268 + .../static/lib/pdfjs/web/locale/eu/viewer.ftl | 481 + .../static/lib/pdfjs/web/locale/fa/viewer.ftl | 246 + .../static/lib/pdfjs/web/locale/ff/viewer.ftl | 247 + .../static/lib/pdfjs/web/locale/fi/viewer.ftl | 481 + .../static/lib/pdfjs/web/locale/fr/viewer.ftl | 477 + .../lib/pdfjs/web/locale/fur/viewer.ftl | 481 + .../lib/pdfjs/web/locale/fy-NL/viewer.ftl | 481 + .../lib/pdfjs/web/locale/ga-IE/viewer.ftl | 213 + .../static/lib/pdfjs/web/locale/gd/viewer.ftl | 313 + .../static/lib/pdfjs/web/locale/gl/viewer.ftl | 385 + .../static/lib/pdfjs/web/locale/gn/viewer.ftl | 475 + .../lib/pdfjs/web/locale/gu-IN/viewer.ftl | 247 + .../static/lib/pdfjs/web/locale/he/viewer.ftl | 481 + .../lib/pdfjs/web/locale/hi-IN/viewer.ftl | 267 + .../static/lib/pdfjs/web/locale/hr/viewer.ftl | 444 + .../lib/pdfjs/web/locale/hsb/viewer.ftl | 485 + .../static/lib/pdfjs/web/locale/hu/viewer.ftl | 481 + .../lib/pdfjs/web/locale/hy-AM/viewer.ftl | 272 + .../lib/pdfjs/web/locale/hye/viewer.ftl | 268 + .../static/lib/pdfjs/web/locale/ia/viewer.ftl | 481 + .../static/lib/pdfjs/web/locale/id/viewer.ftl | 293 + .../static/lib/pdfjs/web/locale/is/viewer.ftl | 481 + .../static/lib/pdfjs/web/locale/it/viewer.ftl | 475 + .../static/lib/pdfjs/web/locale/ja/viewer.ftl | 473 + .../static/lib/pdfjs/web/locale/ka/viewer.ftl | 406 + .../lib/pdfjs/web/locale/kab/viewer.ftl | 438 + .../static/lib/pdfjs/web/locale/kk/viewer.ftl | 481 + .../static/lib/pdfjs/web/locale/km/viewer.ftl | 223 + .../static/lib/pdfjs/web/locale/kn/viewer.ftl | 213 + .../static/lib/pdfjs/web/locale/ko/viewer.ftl | 473 + .../lib/pdfjs/web/locale/lij/viewer.ftl | 247 + .../static/lib/pdfjs/web/locale/lo/viewer.ftl | 313 + .../static/lib/pdfjs/web/locale/locale.json | 1 + .../static/lib/pdfjs/web/locale/lt/viewer.ftl | 268 + .../lib/pdfjs/web/locale/ltg/viewer.ftl | 246 + .../static/lib/pdfjs/web/locale/lv/viewer.ftl | 247 + .../lib/pdfjs/web/locale/meh/viewer.ftl | 87 + .../static/lib/pdfjs/web/locale/mk/viewer.ftl | 215 + .../static/lib/pdfjs/web/locale/mr/viewer.ftl | 239 + .../static/lib/pdfjs/web/locale/ms/viewer.ftl | 247 + .../static/lib/pdfjs/web/locale/my/viewer.ftl | 206 + .../lib/pdfjs/web/locale/nb-NO/viewer.ftl | 481 + .../lib/pdfjs/web/locale/ne-NP/viewer.ftl | 234 + .../static/lib/pdfjs/web/locale/nl/viewer.ftl | 481 + .../lib/pdfjs/web/locale/nn-NO/viewer.ftl | 481 + .../static/lib/pdfjs/web/locale/oc/viewer.ftl | 361 + .../lib/pdfjs/web/locale/pa-IN/viewer.ftl | 481 + .../static/lib/pdfjs/web/locale/pl/viewer.ftl | 483 + .../lib/pdfjs/web/locale/pt-BR/viewer.ftl | 481 + .../lib/pdfjs/web/locale/pt-PT/viewer.ftl | 481 + .../static/lib/pdfjs/web/locale/rm/viewer.ftl | 481 + .../static/lib/pdfjs/web/locale/ro/viewer.ftl | 251 + .../static/lib/pdfjs/web/locale/ru/viewer.ftl | 483 + .../lib/pdfjs/web/locale/sat/viewer.ftl | 325 + .../static/lib/pdfjs/web/locale/sc/viewer.ftl | 367 + .../lib/pdfjs/web/locale/scn/viewer.ftl | 74 + .../lib/pdfjs/web/locale/sco/viewer.ftl | 264 + .../static/lib/pdfjs/web/locale/si/viewer.ftl | 267 + .../static/lib/pdfjs/web/locale/sk/viewer.ftl | 485 + .../lib/pdfjs/web/locale/skr/viewer.ftl | 481 + .../static/lib/pdfjs/web/locale/sl/viewer.ftl | 485 + .../lib/pdfjs/web/locale/son/viewer.ftl | 206 + .../static/lib/pdfjs/web/locale/sq/viewer.ftl | 466 + .../static/lib/pdfjs/web/locale/sr/viewer.ftl | 313 + .../lib/pdfjs/web/locale/sv-SE/viewer.ftl | 481 + .../lib/pdfjs/web/locale/szl/viewer.ftl | 257 + .../static/lib/pdfjs/web/locale/ta/viewer.ftl | 223 + .../static/lib/pdfjs/web/locale/te/viewer.ftl | 239 + .../static/lib/pdfjs/web/locale/tg/viewer.ftl | 464 + .../static/lib/pdfjs/web/locale/th/viewer.ftl | 473 + .../static/lib/pdfjs/web/locale/tl/viewer.ftl | 257 + .../static/lib/pdfjs/web/locale/tr/viewer.ftl | 481 + .../lib/pdfjs/web/locale/trs/viewer.ftl | 197 + .../static/lib/pdfjs/web/locale/uk/viewer.ftl | 483 + .../static/lib/pdfjs/web/locale/ur/viewer.ftl | 248 + .../static/lib/pdfjs/web/locale/uz/viewer.ftl | 187 + .../static/lib/pdfjs/web/locale/vi/viewer.ftl | 473 + .../static/lib/pdfjs/web/locale/wo/viewer.ftl | 127 + .../static/lib/pdfjs/web/locale/xh/viewer.ftl | 212 + .../lib/pdfjs/web/locale/zh-CN/viewer.ftl | 473 + .../lib/pdfjs/web/locale/zh-TW/viewer.ftl | 473 + frontend/web/static/lib/pdfjs/web/viewer.css | 5016 ++++ frontend/web/static/lib/pdfjs/web/viewer.html | 623 + frontend/web/static/lib/popper/popper.js | 1825 ++ .../web/static/lib/prismjs/themes/default.css | 142 + .../web/static/lib/prismjs/themes/okaida.css | 125 + frontend/web/static/lib/qunit/qunit-2.9.1.css | 436 + frontend/web/static/lib/stacktracejs/LICENSE | 19 + frontend/web/static/lib/zxing-library/LICENSE | 245 + frontend/web/static/lib/zxing-library/version | 1 + .../src/core/action_swiper/action_swiper.js | 225 + .../src/core/action_swiper/action_swiper.scss | 20 + .../src/core/action_swiper/action_swiper.xml | 27 + .../src/core/anchor_scroll_prevention.js | 9 + frontend/web/static/src/core/assets.js | 262 + .../src/core/autocomplete/autocomplete.js | 501 + .../src/core/autocomplete/autocomplete.scss | 53 + .../src/core/autocomplete/autocomplete.xml | 88 + .../web/static/src/core/avatar/avatar.scss | 13 + .../src/core/avatar/avatar.variables.scss | 1 + frontend/web/static/src/core/badge/badge.scss | 8 + .../src/core/barcode/ZXingBarcodeDetector.js | 153 + .../static/src/core/barcode/barcode_dialog.js | 60 + .../src/core/barcode/barcode_dialog.scss | 10 + .../src/core/barcode/barcode_dialog.xml | 15 + .../src/core/barcode/barcode_video_scanner.js | 234 + .../core/barcode/barcode_video_scanner.xml | 8 + .../static/src/core/barcode/crop_overlay.js | 158 + .../static/src/core/barcode/crop_overlay.scss | 45 + .../static/src/core/barcode/crop_overlay.xml | 17 + .../src/core/bottom_sheet/bottom_sheet.js | 317 + .../src/core/bottom_sheet/bottom_sheet.scss | 343 + .../bottom_sheet/bottom_sheet.variables.scss | 8 + .../src/core/bottom_sheet/bottom_sheet.xml | 70 + .../core/bottom_sheet/bottom_sheet_service.js | 71 + .../web/static/src/core/browser/browser.js | 112 + .../web/static/src/core/browser/cookie.js | 37 + .../src/core/browser/feature_detection.js | 83 + .../web/static/src/core/browser/router.js | 423 + .../static/src/core/browser/title_service.js | 60 + .../web/static/src/core/checkbox/checkbox.js | 98 + .../static/src/core/checkbox/checkbox.scss | 3 + .../web/static/src/core/checkbox/checkbox.xml | 22 + .../src/core/code_editor/code_editor.js | 180 + .../src/core/code_editor/code_editor.xml | 8 + .../src/core/color_picker/color_picker.js | 362 + .../src/core/color_picker/color_picker.scss | 94 + .../src/core/color_picker/color_picker.xml | 51 + .../custom_color_picker.js | 689 + .../custom_color_picker.scss | 51 + .../custom_color_picker.xml | 36 + .../tabs/color_picker_custom_tab.js | 45 + .../tabs/color_picker_custom_tab.xml | 45 + .../tabs/color_picker_solid_tab.js | 27 + .../tabs/color_picker_solid_tab.xml | 26 + .../static/src/core/colorlist/colorlist.js | 62 + .../static/src/core/colorlist/colorlist.scss | 75 + .../static/src/core/colorlist/colorlist.xml | 15 + frontend/web/static/src/core/colors/colors.js | 217 + .../src/core/commands/command_category.js | 11 + .../static/src/core/commands/command_hook.js | 23 + .../src/core/commands/command_items.xml | 35 + .../src/core/commands/command_palette.js | 388 + .../src/core/commands/command_palette.scss | 53 + .../src/core/commands/command_palette.xml | 61 + .../src/core/commands/command_service.js | 261 + .../src/core/commands/default_providers.js | 109 + .../confirmation_dialog.js | 103 + .../confirmation_dialog.xml | 24 + frontend/web/static/src/core/context.js | 85 + .../src/core/copy_button/copy_button.js | 48 + .../src/core/copy_button/copy_button.xml | 18 + frontend/web/static/src/core/currency.js | 98 + .../src/core/datetime/datetime_input.js | 48 + .../src/core/datetime/datetime_input.xml | 15 + .../src/core/datetime/datetime_picker.js | 646 + .../src/core/datetime/datetime_picker.scss | 92 + .../src/core/datetime/datetime_picker.xml | 153 + .../src/core/datetime/datetime_picker_hook.js | 36 + .../core/datetime/datetime_picker_popover.js | 31 + .../core/datetime/datetime_picker_popover.xml | 26 + .../core/datetime/datetimepicker_service.js | 551 + .../static/src/core/debug/debug_context.js | 83 + .../web/static/src/core/debug/debug_menu.js | 61 + .../web/static/src/core/debug/debug_menu.scss | 6 + .../web/static/src/core/debug/debug_menu.xml | 34 + .../static/src/core/debug/debug_menu_basic.js | 44 + .../static/src/core/debug/debug_menu_items.js | 70 + .../src/core/debug/debug_menu_items.xml | 124 + .../static/src/core/debug/debug_providers.js | 56 + .../web/static/src/core/debug/debug_utils.js | 11 + frontend/web/static/src/core/dialog/dialog.js | 145 + .../web/static/src/core/dialog/dialog.scss | 82 + .../web/static/src/core/dialog/dialog.xml | 47 + .../static/src/core/dialog/dialog_service.js | 103 + frontend/web/static/src/core/domain.js | 426 + .../core/domain_selector/domain_selector.js | 156 + .../core/domain_selector/domain_selector.xml | 46 + .../domain_selector_operator_editor.js | 62 + .../static/src/core/domain_selector/utils.js | 26 + .../domain_selector_dialog.js | 110 + .../domain_selector_dialog.xml | 20 + .../_behaviours/dropdown_group_hook.js | 36 + .../dropdown/_behaviours/dropdown_nesting.js | 147 + .../dropdown/_behaviours/dropdown_popover.js | 56 + .../src/core/dropdown/accordion_item.js | 38 + .../src/core/dropdown/accordion_item.scss | 12 + .../src/core/dropdown/accordion_item.xml | 21 + .../static/src/core/dropdown/checkbox_item.js | 12 + .../web/static/src/core/dropdown/dropdown.js | 388 + .../static/src/core/dropdown/dropdown.scss | 129 + .../src/core/dropdown/dropdown_group.js | 41 + .../src/core/dropdown/dropdown_hooks.js | 53 + .../static/src/core/dropdown/dropdown_item.js | 58 + .../src/core/dropdown/dropdown_item.xml | 28 + .../web/static/src/core/dropzone/dropzone.js | 23 + .../static/src/core/dropzone/dropzone.scss | 8 + .../web/static/src/core/dropzone/dropzone.xml | 19 + .../static/src/core/dropzone/dropzone_hook.js | 91 + .../static/src/core/effects/effect_service.js | 87 + .../static/src/core/effects/rainbow_man.js | 72 + .../static/src/core/effects/rainbow_man.scss | 146 + .../static/src/core/effects/rainbow_man.xml | 73 + .../src/core/emoji_picker/emoji_data.js | 21892 ++++++++++++++++ .../core/emoji_picker/emoji_picker.dark.scss | 8 + .../src/core/emoji_picker/emoji_picker.js | 695 + .../src/core/emoji_picker/emoji_picker.scss | 69 + .../src/core/emoji_picker/emoji_picker.xml | 122 + .../emoji_picker/frequent_emoji_service.js | 32 + frontend/web/static/src/core/ensure_jquery.js | 25 + .../static/src/core/errors/error_dialog.scss | 46 + .../static/src/core/errors/error_dialogs.js | 240 + .../static/src/core/errors/error_dialogs.xml | 79 + .../static/src/core/errors/error_handlers.js | 208 + .../static/src/core/errors/error_service.js | 188 + .../web/static/src/core/errors/error_utils.js | 186 + .../src/core/errors/scss_error_dialog.js | 56 + .../expression_editor/expression_editor.js | 113 + .../expression_editor/expression_editor.xml | 30 + .../expression_editor_operator_editor.js | 21 + .../expression_editor_dialog.js | 81 + .../expression_editor_dialog.xml | 14 + frontend/web/static/src/core/field_service.js | 232 + .../static/src/core/file_input/file_input.js | 120 + .../static/src/core/file_input/file_input.xml | 23 + .../file_upload/file_upload_progress_bar.js | 28 + .../file_upload/file_upload_progress_bar.scss | 19 + .../file_upload/file_upload_progress_bar.xml | 9 + .../file_upload_progress_container.js | 10 + .../file_upload_progress_container.xml | 8 + .../file_upload_progress_record.js | 40 + .../file_upload_progress_record.scss | 20 + .../file_upload_progress_record.xml | 33 + .../core/file_upload/file_upload_service.js | 155 + .../static/src/core/file_viewer/file_model.js | 134 + .../core/file_viewer/file_viewer.dark.scss | 11 + .../src/core/file_viewer/file_viewer.js | 264 + .../src/core/file_viewer/file_viewer.scss | 48 + .../src/core/file_viewer/file_viewer.xml | 77 + .../src/core/file_viewer/file_viewer_hook.js | 38 + .../static/src/core/hotkeys/hotkey_hook.js | 19 + .../static/src/core/hotkeys/hotkey_service.js | 481 + .../install_scoped_app/install_scoped_app.js | 46 + .../install_scoped_app/install_scoped_app.xml | 38 + .../ir_ui_view_code_editor/code_editor.js | 79 + .../ir_ui_view_code_editor/code_editor.scss | 5 + frontend/web/static/src/core/l10n/dates.js | 709 + .../web/static/src/core/l10n/localization.js | 40 + .../src/core/l10n/localization_service.js | 119 + frontend/web/static/src/core/l10n/time.js | 295 + .../web/static/src/core/l10n/translation.js | 191 + frontend/web/static/src/core/l10n/utils.js | 3 + .../static/src/core/l10n/utils/format_list.js | 81 + .../web/static/src/core/l10n/utils/locales.js | 96 + .../static/src/core/l10n/utils/normalize.js | 191 + frontend/web/static/src/core/macro.js | 238 + .../src/core/main_components_container.js | 45 + .../model_field_selector.js | 97 + .../model_field_selector.scss | 27 + .../model_field_selector.xml | 25 + .../model_field_selector_popover.js | 339 + .../model_field_selector_popover.scss | 16 + .../model_field_selector_popover.xml | 73 + .../src/core/model_selector/model_selector.js | 103 + .../core/model_selector/model_selector.scss | 10 + .../core/model_selector/model_selector.xml | 23 + frontend/web/static/src/core/name_service.js | 103 + .../static/src/core/navigation/navigation.js | 455 + .../web/static/src/core/network/download.js | 574 + .../static/src/core/network/http_service.js | 48 + frontend/web/static/src/core/network/rpc.js | 187 + .../web/static/src/core/network/rpc_cache.js | 244 + .../web/static/src/core/notebook/notebook.js | 182 + .../static/src/core/notebook/notebook.scss | 110 + .../web/static/src/core/notebook/notebook.xml | 25 + .../src/core/notifications/notification.js | 83 + .../src/core/notifications/notification.scss | 31 + .../notifications/notification.variables.scss | 4 + .../src/core/notifications/notification.xml | 30 + .../notifications/notification_container.js | 24 + .../notifications/notification_service.js | 71 + frontend/web/static/src/core/orm_service.js | 415 + .../src/core/overlay/overlay_container.js | 79 + .../src/core/overlay/overlay_container.scss | 4 + .../src/core/overlay/overlay_container.xml | 20 + .../src/core/overlay/overlay_service.js | 60 + frontend/web/static/src/core/pager/pager.js | 208 + frontend/web/static/src/core/pager/pager.xml | 33 + .../static/src/core/pager/pager_indicator.js | 37 + .../src/core/pager/pager_indicator.scss | 16 + .../static/src/core/pager/pager_indicator.xml | 16 + .../web/static/src/core/popover/popover.js | 279 + .../web/static/src/core/popover/popover.scss | 29 + .../web/static/src/core/popover/popover.xml | 22 + .../static/src/core/popover/popover_hook.js | 72 + .../src/core/popover/popover_service.js | 77 + .../static/src/core/position/position_hook.js | 140 + .../web/static/src/core/position/utils.js | 326 + .../web/static/src/core/pwa/install_prompt.js | 23 + .../static/src/core/pwa/install_prompt.scss | 29 + .../static/src/core/pwa/install_prompt.xml | 37 + .../web/static/src/core/pwa/pwa_service.js | 177 + frontend/web/static/src/core/py_js/py.js | 62 + .../web/static/src/core/py_js/py_builtin.js | 113 + frontend/web/static/src/core/py_js/py_date.js | 899 + .../static/src/core/py_js/py_interpreter.js | 484 + .../web/static/src/core/py_js/py_parser.js | 392 + .../web/static/src/core/py_js/py_tokenizer.js | 316 + .../web/static/src/core/py_js/py_utils.js | 128 + .../record_selectors/multi_record_selector.js | 91 + .../multi_record_selector.xml | 30 + .../record_selectors/record_autocomplete.js | 145 + .../record_selectors/record_autocomplete.xml | 25 + .../core/record_selectors/record_selector.js | 68 + .../core/record_selectors/record_selector.xml | 33 + .../record_selectors/record_selectors.scss | 22 + .../record_selectors/tag_navigation_hook.js | 78 + frontend/web/static/src/core/registry.js | 209 + frontend/web/static/src/core/registry_hook.js | 25 + .../core/resizable_panel/resizable_panel.js | 164 + .../core/resizable_panel/resizable_panel.scss | 10 + .../core/resizable_panel/resizable_panel.xml | 14 + .../src/core/select_menu/select_menu.js | 464 + .../src/core/select_menu/select_menu.scss | 90 + .../src/core/select_menu/select_menu.xml | 101 + .../src/core/signature/name_and_signature.js | 345 + .../core/signature/name_and_signature.scss | 19 + .../src/core/signature/name_and_signature.xml | 111 + .../src/core/signature/signature_dialog.js | 45 + .../src/core/signature/signature_dialog.xml | 17 + .../static/src/core/tags_list/tags_list.js | 37 + .../static/src/core/tags_list/tags_list.scss | 16 + .../static/src/core/tags_list/tags_list.xml | 68 + .../static/src/core/template_inheritance.js | 379 + frontend/web/static/src/core/templates.js | 235 + .../src/core/time_picker/time_picker.js | 286 + .../src/core/time_picker/time_picker.scss | 25 + .../src/core/time_picker/time_picker.xml | 45 + .../web/static/src/core/tooltip/tooltip.js | 11 + .../web/static/src/core/tooltip/tooltip.scss | 59 + .../web/static/src/core/tooltip/tooltip.xml | 11 + .../static/src/core/tooltip/tooltip_hook.js | 12 + .../src/core/tooltip/tooltip_service.js | 270 + frontend/web/static/src/core/transition.js | 155 + .../static/src/core/tree_editor/ast_utils.js | 30 + .../src/core/tree_editor/condition_tree.js | 348 + .../tree_editor/construct_domain_from_tree.js | 67 + .../construct_expression_from_tree.js | 146 + .../tree_editor/construct_tree_from_domain.js | 77 + .../construct_tree_from_expression.js | 187 + .../domain_contains_expressions.js | 33 + .../src/core/tree_editor/domain_from_tree.js | 7 + .../core/tree_editor/expression_from_tree.js | 7 + .../static/src/core/tree_editor/operators.js | 24 + .../src/core/tree_editor/tree_editor.js | 232 + .../src/core/tree_editor/tree_editor.scss | 22 + .../src/core/tree_editor/tree_editor.xml | 220 + .../tree_editor/tree_editor_autocomplete.js | 80 + .../tree_editor/tree_editor_components.js | 85 + .../tree_editor/tree_editor_components.xml | 68 + .../tree_editor_operator_editor.js | 178 + .../tree_editor/tree_editor_value_editors.js | 377 + .../src/core/tree_editor/tree_from_domain.js | 7 + .../core/tree_editor/tree_from_expression.js | 7 + .../src/core/tree_editor/tree_processor.js | 423 + .../web/static/src/core/tree_editor/utils.js | 45 + .../src/core/tree_editor/virtual_operators.js | 400 + frontend/web/static/src/core/ui/block_ui.js | 88 + frontend/web/static/src/core/ui/block_ui.scss | 11 + frontend/web/static/src/core/ui/block_ui.xml | 23 + frontend/web/static/src/core/ui/ui_service.js | 251 + frontend/web/static/src/core/user.js | 283 + .../src/core/user_switch/user_switch.js | 51 + .../src/core/user_switch/user_switch.xml | 31 + frontend/web/static/src/core/utils/arrays.js | 274 + .../web/static/src/core/utils/autoresize.js | 134 + frontend/web/static/src/core/utils/binary.js | 32 + frontend/web/static/src/core/utils/cache.js | 34 + .../web/static/src/core/utils/classname.js | 71 + frontend/web/static/src/core/utils/colors.js | 483 + .../web/static/src/core/utils/components.js | 11 + .../web/static/src/core/utils/concurrency.js | 192 + .../web/static/src/core/utils/draggable.js | 49 + .../src/core/utils/draggable_hook_builder.js | 1089 + .../core/utils/draggable_hook_builder.scss | 25 + .../core/utils/draggable_hook_builder_owl.js | 24 + frontend/web/static/src/core/utils/dvu.js | 110 + frontend/web/static/src/core/utils/files.js | 105 + .../web/static/src/core/utils/functions.js | 33 + frontend/web/static/src/core/utils/hooks.js | 287 + frontend/web/static/src/core/utils/html.js | 260 + .../web/static/src/core/utils/indexed_db.js | 240 + frontend/web/static/src/core/utils/misc.js | 28 + .../static/src/core/utils/nested_sortable.js | 406 + .../src/core/utils/nested_sortable.scss | 11 + frontend/web/static/src/core/utils/numbers.js | 303 + frontend/web/static/src/core/utils/objects.js | 136 + frontend/web/static/src/core/utils/patch.js | 138 + frontend/web/static/src/core/utils/pdfjs.js | 71 + .../web/static/src/core/utils/reactive.js | 66 + frontend/web/static/src/core/utils/render.js | 70 + .../web/static/src/core/utils/scrolling.js | 199 + frontend/web/static/src/core/utils/search.js | 168 + .../web/static/src/core/utils/sortable.js | 343 + .../web/static/src/core/utils/sortable_owl.js | 24 + .../static/src/core/utils/sortable_service.js | 96 + frontend/web/static/src/core/utils/strings.js | 272 + frontend/web/static/src/core/utils/timing.js | 212 + .../static/src/core/utils/transitions.scss | 19 + frontend/web/static/src/core/utils/ui.js | 203 + frontend/web/static/src/core/utils/urls.js | 169 + frontend/web/static/src/core/utils/xml.js | 160 + .../web/static/src/core/virtual_grid_hook.js | 180 + frontend/web/static/src/env.js | 250 + frontend/web/static/src/libs/bootstrap.js | 112 + .../src/libs/fontawesome/css/font-awesome.css | 2294 ++ frontend/web/static/src/main.js | 10 + frontend/web/static/src/model/model.js | 249 + frontend/web/static/src/model/record.js | 204 + .../src/model/relational_model/datapoint.js | 64 + .../relational_model/dynamic_group_list.js | 385 + .../model/relational_model/dynamic_list.js | 512 + .../relational_model/dynamic_record_list.js | 181 + .../src/model/relational_model/errors.js | 22 + .../src/model/relational_model/group.js | 136 + .../src/model/relational_model/operation.js | 21 + .../src/model/relational_model/record.js | 1405 + .../relational_model/relational_model.js | 870 + .../src/model/relational_model/static_list.js | 1186 + .../src/model/relational_model/utils.js | 905 + .../web/static/src/model/sample_server.js | 861 + frontend/web/static/src/module_loader.js | 249 + frontend/web/static/src/polyfills/array.js | 12 + .../web/static/src/polyfills/clipboard.js | 98 + frontend/web/static/src/polyfills/object.js | 4 + frontend/web/static/src/polyfills/promise.js | 11 + frontend/web/static/src/polyfills/set.js | 15 + frontend/web/static/src/scss/ace.scss | 12 + frontend/web/static/src/scss/animation.scss | 45 + .../static/src/scss/base_document_layout.scss | 44 + .../static/src/scss/bootstrap_overridden.scss | 315 + .../web/static/src/scss/bootstrap_review.scss | 133 + .../src/scss/bootstrap_review_backend.scss | 326 + .../static/src/scss/bs_mixins_overrides.scss | 96 + .../src/scss/bs_mixins_overrides_backend.scss | 33 + .../src/scss/fontawesome_overridden.scss | 112 + frontend/web/static/src/scss/functions.scss | 51 + .../web/static/src/scss/import_bootstrap.scss | 67 + frontend/web/static/src/scss/mimetypes.scss | 73 + .../static/src/scss/mixins_forwardport.scss | 22 + .../web/static/src/scss/pre_variables.scss | 82 + .../static/src/scss/primary_variables.scss | 297 + .../static/src/scss/secondary_variables.scss | 46 + frontend/web/static/src/scss/ui.scss | 203 + .../web/static/src/scss/utilities_custom.scss | 208 + frontend/web/static/src/scss/utils.scss | 411 + frontend/web/static/src/search/action_hook.js | 153 + .../src/search/action_menus/action_menus.js | 186 + .../src/search/action_menus/action_menus.xml | 48 + .../src/search/breadcrumbs/breadcrumbs.js | 20 + .../src/search/breadcrumbs/breadcrumbs.xml | 69 + .../static/src/search/cog_menu/cog_menu.js | 82 + .../static/src/search/cog_menu/cog_menu.scss | 5 + .../static/src/search/cog_menu/cog_menu.xml | 58 + .../src/search/control_panel/control_panel.js | 708 + .../search/control_panel/control_panel.scss | 221 + .../control_panel.variables.scss | 2 + .../control_panel.variables_print.scss | 2 + .../search/control_panel/control_panel.xml | 169 + .../control_panel/control_panel_mobile.css | 27 + .../custom_favorite_item.js | 95 + .../custom_favorite_item.xml | 28 + .../custom_group_by_item.js | 21 + .../custom_group_by_item.scss | 4 + .../custom_group_by_item.xml | 14 + frontend/web/static/src/search/layout.js | 39 + frontend/web/static/src/search/layout.xml | 15 + frontend/web/static/src/search/pager_hook.js | 35 + .../properties_group_by_item.js | 70 + .../properties_group_by_item.xml | 40 + .../static/src/search/search_arch_parser.js | 414 + .../src/search/search_bar/search_bar.js | 725 + .../src/search/search_bar/search_bar.scss | 50 + .../search_bar/search_bar.variables.scss | 1 + .../src/search/search_bar/search_bar.xml | 138 + .../search/search_bar/search_bar_toggler.js | 54 + .../search/search_bar/search_bar_toggler.xml | 10 + .../search/search_bar_menu/search_bar_menu.js | 174 + .../search_bar_menu/search_bar_menu.scss | 35 + .../search_bar_menu/search_bar_menu.xml | 165 + .../web/static/src/search/search_model.js | 2341 ++ .../src/search/search_panel/search_panel.js | 451 + .../src/search/search_panel/search_panel.scss | 80 + .../search_panel/search_panel.variables.scss | 27 + .../src/search/search_panel/search_panel.xml | 255 + .../src/search/search_panel/search_view.scss | 174 + frontend/web/static/src/search/utils/dates.js | 340 + .../web/static/src/search/utils/group_by.js | 60 + frontend/web/static/src/search/utils/misc.js | 25 + .../web/static/src/search/utils/order_by.js | 38 + .../src/search/with_search/with_search.js | 97 + .../src/search/with_search/with_search.xml | 13 + frontend/web/static/src/session.js | 2 + frontend/web/static/src/start.js | 59 + .../web/static/src/views/action_helper.js | 16 + .../web/static/src/views/action_helper.xml | 25 + .../views/calendar/calendar_arch_parser.js | 151 + .../calendar_common_popover.js | 121 + .../calendar_common_popover.scss | 120 + .../calendar_common_popover.xml | 84 + .../calendar_common_renderer.js | 430 + .../calendar_common_renderer.xml | 20 + .../calendar_common_week_column.js | 16 + .../src/views/calendar/calendar_controller.js | 443 + .../views/calendar/calendar_controller.scss | 81 + .../views/calendar/calendar_controller.xml | 73 + .../calendar/calendar_controller_mobile.scss | 49 + .../calendar_filter_section.js | 177 + .../calendar_filter_section.scss | 12 + .../calendar_filter_section.xml | 101 + .../src/views/calendar/calendar_model.js | 1027 + .../calendar/calendar_renderer.dark.scss | 3 + .../src/views/calendar/calendar_renderer.js | 59 + .../src/views/calendar/calendar_renderer.scss | 760 + .../src/views/calendar/calendar_renderer.xml | 16 + .../calendar/calendar_renderer_mobile.scss | 34 + .../calendar_side_panel.js | 55 + .../calendar_side_panel.scss | 3 + .../calendar_side_panel.xml | 13 + .../src/views/calendar/calendar_view.js | 33 + .../calendar_year/calendar_year_popover.js | 103 + .../calendar_year/calendar_year_popover.scss | 42 + .../calendar_year/calendar_year_popover.xml | 69 + .../calendar_year/calendar_year_renderer.js | 224 + .../calendar_year/calendar_year_renderer.xml | 18 + .../calendar/hooks/calendar_popover_hook.js | 48 + .../calendar/hooks/full_calendar_hook.js | 50 + .../calendar/hooks/square_selection_hook.js | 239 + .../calendar_mobile_filter_panel.js | 40 + .../calendar_mobile_filter_panel.xml | 22 + .../quick_create/calendar_quick_create.js | 85 + .../quick_create/calendar_quick_create.xml | 33 + .../web/static/src/views/calendar/utils.js | 51 + frontend/web/static/src/views/debug_items.js | 418 + .../static/src/views/fields/ace/ace_field.js | 83 + .../src/views/fields/ace/ace_field.scss | 3 + .../static/src/views/fields/ace/ace_field.xml | 20 + .../attachment_image_field.js | 18 + .../attachment_image_field.xml | 13 + .../src/views/fields/badge/badge_field.js | 66 + .../src/views/fields/badge/badge_field.xml | 8 + .../badge_selection/badge_selection.scss | 6 + .../badge_selection/badge_selection_field.js | 119 + .../badge_selection/badge_selection_field.xml | 23 + .../list_badge_selection_field.js | 49 + .../list_badge_selection_field.xml | 18 + .../badge_selection_field_with_filter.js | 33 + .../src/views/fields/binary/binary_field.js | 106 + .../src/views/fields/binary/binary_field.xml | 78 + .../src/views/fields/boolean/boolean_field.js | 38 + .../views/fields/boolean/boolean_field.xml | 8 + .../boolean_favorite_field.js | 62 + .../boolean_favorite_field.scss | 13 + .../boolean_favorite_field.xml | 13 + .../fields/boolean_icon/boolean_icon_field.js | 39 + .../boolean_icon/boolean_icon_field.xml | 8 + .../boolean_toggle/boolean_toggle_field.js | 42 + .../boolean_toggle/boolean_toggle_field.xml | 13 + .../list_boolean_toggle_field.js | 20 + .../list_boolean_toggle_field.xml | 10 + .../src/views/fields/char/char_field.js | 126 + .../src/views/fields/char/char_field.scss | 43 + .../src/views/fields/char/char_field.xml | 39 + .../src/views/fields/color/color_field.js | 42 + .../src/views/fields/color/color_field.xml | 10 + .../fields/color_picker/color_picker_field.js | 36 + .../color_picker/color_picker_field.scss | 14 + .../color_picker/color_picker_field.xml | 8 + .../contact_image/contact_image_field.js | 47 + .../contact_image/contact_image_field.scss | 7 + .../contact_image/contact_image_field.xml | 20 + .../contact_statistics/contact_statistics.js | 23 + .../contact_statistics/contact_statistics.xml | 14 + .../copy_clipboard/copy_clipboard_field.js | 114 + .../copy_clipboard/copy_clipboard_field.scss | 34 + .../copy_clipboard/copy_clipboard_field.xml | 15 + .../views/fields/datetime/datetime_field.js | 592 + .../views/fields/datetime/datetime_field.xml | 78 + .../fields/datetime/list_datetime_field.js | 22 + .../src/views/fields/domain/domain_field.js | 341 + .../src/views/fields/domain/domain_field.xml | 94 + .../views/fields/dynamic_placeholder_hook.js | 100 + .../fields/dynamic_placeholder_popover.js | 102 + .../fields/dynamic_placeholder_popover.xml | 66 + .../src/views/fields/email/email_field.js | 48 + .../src/views/fields/email/email_field.scss | 7 + .../src/views/fields/email/email_field.xml | 38 + frontend/web/static/src/views/fields/field.js | 482 + .../web/static/src/views/fields/field.xml | 10 + .../field_selector/field_selector_field.js | 90 + .../field_selector/field_selector_field.xml | 11 + .../static/src/views/fields/field_tooltip.js | 32 + .../static/src/views/fields/field_tooltip.xml | 89 + .../web/static/src/views/fields/fields.scss | 366 + .../static/src/views/fields/file_handler.js | 108 + .../static/src/views/fields/file_handler.xml | 22 + .../src/views/fields/float/float_field.js | 168 + .../src/views/fields/float/float_field.xml | 19 + .../fields/float_factor/float_factor_field.js | 42 + .../fields/float_time/float_time_field.js | 62 + .../fields/float_time/float_time_field.xml | 9 + .../fields/float_toggle/float_toggle_field.js | 102 + .../float_toggle/float_toggle_field.xml | 9 + .../web/static/src/views/fields/formatters.js | 480 + .../src/views/fields/gauge/gauge_field.js | 129 + .../src/views/fields/gauge/gauge_field.xml | 11 + .../google_slide_viewer.js | 54 + .../google_slide_viewer.scss | 17 + .../google_slide_viewer.xml | 19 + .../src/views/fields/handle/handle_field.js | 27 + .../src/views/fields/handle/handle_field.xml | 8 + .../src/views/fields/html/html_field.js | 13 + .../src/views/fields/html/html_field.scss | 3 + .../src/views/fields/html/html_field.xml | 11 + .../iframe_wrapper/iframe_wrapper_field.js | 44 + .../iframe_wrapper/iframe_wrapper_field.scss | 45 + .../iframe_wrapper/iframe_wrapper_field.xml | 10 + .../src/views/fields/image/image_field.js | 313 + .../src/views/fields/image/image_field.scss | 39 + .../src/views/fields/image/image_field.xml | 53 + .../views/fields/image_url/image_url_field.js | 68 + .../fields/image_url/image_url_field.xml | 19 + .../src/views/fields/input_field_hook.js | 192 + .../src/views/fields/integer/integer_field.js | 126 + .../views/fields/integer/integer_field.xml | 21 + .../views/fields/ir_ui_view_ace/ace_field.js | 17 + .../views/fields/ir_ui_view_ace/ace_field.xml | 18 + .../journal_dashboard_graph_field.js | 180 + .../journal_dashboard_graph_field.scss | 3 + .../journal_dashboard_graph_field.xml | 10 + .../src/views/fields/json/json_field.js | 24 + .../src/views/fields/json/json_field.xml | 8 + .../json_checkboxes/json_checkboxes_field.js | 58 + .../json_checkboxes/json_checkboxes_field.xml | 26 + .../kanban_color_picker_field.js | 31 + .../kanban_color_picker_field.scss | 15 + .../kanban_color_picker_field.xml | 18 + .../label_selection/label_selection_field.js | 44 + .../label_selection/label_selection_field.xml | 8 + .../many2many_binary_field.js | 99 + .../many2many_binary_field.scss | 93 + .../many2many_binary_field.xml | 58 + .../many2many_checkboxes_field.js | 90 + .../many2many_checkboxes_field.xml | 20 + .../kanban_many2many_tags_field.js | 23 + .../kanban_many2many_tags_field.xml | 9 + .../many2many_tags/many2many_tags_field.js | 390 + .../many2many_tags/many2many_tags_field.scss | 87 + .../many2many_tags/many2many_tags_field.xml | 44 + .../many2many_tags_avatar_field.js | 164 + .../many2many_tags_avatar_field.scss | 71 + .../many2many_tags_avatar_field.xml | 72 + .../src/views/fields/many2one/many2one.js | 356 + .../src/views/fields/many2one/many2one.xml | 55 + .../views/fields/many2one/many2one_field.js | 124 + .../views/fields/many2one/many2one_field.scss | 13 + .../views/fields/many2one/many2one_field.xml | 8 + .../kanban_many2one_avatar_field.js | 29 + .../kanban_many2one_avatar_field.xml | 21 + .../many2one_avatar/many2one_avatar_field.js | 33 + .../many2one_avatar_field.scss | 38 + .../many2one_avatar/many2one_avatar_field.xml | 27 + .../many2one_barcode_field.js | 32 + .../many2one_barcode_field.xml | 8 + .../many2one_reference_field.js | 49 + .../many2one_reference_field.xml | 8 + .../many2one_reference_integer_field.js | 18 + .../views/fields/monetary/monetary_field.js | 133 + .../views/fields/monetary/monetary_field.scss | 8 + .../views/fields/monetary/monetary_field.xml | 18 + .../src/views/fields/numpad_decimal_hook.js | 57 + .../web/static/src/views/fields/parsers.js | 225 + .../fields/pdf_viewer/pdf_viewer_field.js | 112 + .../fields/pdf_viewer/pdf_viewer_field.scss | 3 + .../fields/pdf_viewer/pdf_viewer_field.xml | 58 + .../fields/percent_pie/percent_pie_field.js | 33 + .../fields/percent_pie/percent_pie_field.scss | 18 + .../fields/percent_pie/percent_pie_field.xml | 18 + .../fields/percentage/percentage_field.js | 60 + .../fields/percentage/percentage_field.xml | 22 + .../src/views/fields/phone/phone_field.js | 51 + .../src/views/fields/phone/phone_field.scss | 18 + .../src/views/fields/phone/phone_field.xml | 35 + .../views/fields/priority/priority_field.js | 111 + .../views/fields/priority/priority_field.scss | 28 + .../views/fields/priority/priority_field.xml | 39 + .../progress_bar/kanban_progress_bar_field.js | 15 + .../fields/progress_bar/progress_bar_field.js | 166 + .../progress_bar/progress_bar_field.scss | 53 + .../progress_bar/progress_bar_field.xml | 55 + .../properties/calendar_properties_field.js | 16 + .../properties/calendar_properties_field.xml | 31 + .../properties/card_properties_field.js | 18 + .../properties/card_properties_field.scss | 9 + .../properties/card_properties_field.xml | 36 + .../fields/properties/properties_field.js | 1007 + .../fields/properties/properties_field.scss | 151 + .../fields/properties/properties_field.xml | 95 + .../fields/properties/property_definition.js | 483 + .../properties/property_definition.scss | 66 + .../fields/properties/property_definition.xml | 224 + .../property_definition_selection.js | 291 + .../property_definition_selection.scss | 13 + .../property_definition_selection.xml | 49 + .../views/fields/properties/property_tags.js | 326 + .../fields/properties/property_tags.scss | 32 + .../views/fields/properties/property_tags.xml | 27 + .../views/fields/properties/property_text.js | 16 + .../fields/properties/property_text.scss | 5 + .../views/fields/properties/property_text.xml | 8 + .../views/fields/properties/property_value.js | 401 + .../fields/properties/property_value.scss | 78 + .../fields/properties/property_value.xml | 174 + .../src/views/fields/radio/radio_field.js | 97 + .../src/views/fields/radio/radio_field.scss | 31 + .../src/views/fields/radio/radio_field.xml | 29 + .../views/fields/reference/reference_field.js | 247 + .../fields/reference/reference_field.xml | 24 + .../src/views/fields/relational_utils.js | 1026 + .../src/views/fields/relational_utils.xml | 70 + .../remaining_days/remaining_days_field.js | 96 + .../remaining_days/remaining_days_field.xml | 16 + .../selection/filterable_selection_field.js | 77 + .../views/fields/selection/selection_field.js | 133 + .../fields/selection/selection_field.scss | 7 + .../fields/selection/selection_field.xml | 13 + .../views/fields/signature/signature_field.js | 181 + .../fields/signature/signature_field.scss | 28 + .../fields/signature/signature_field.xml | 23 + .../src/views/fields/standard_field_props.js | 14 + .../views/fields/stat_info/stat_info_field.js | 66 + .../fields/stat_info/stat_info_field.xml | 9 + .../state_selection/state_selection_field.js | 106 + .../state_selection_field.scss | 8 + .../state_selection/state_selection_field.xml | 33 + .../views/fields/statusbar/statusbar_field.js | 379 + .../fields/statusbar/statusbar_field.scss | 216 + .../statusbar/statusbar_field.variables.scss | 1 + .../fields/statusbar/statusbar_field.xml | 87 + .../src/views/fields/text/text_field.js | 157 + .../src/views/fields/text/text_field.scss | 5 + .../src/views/fields/text/text_field.xml | 38 + .../timezone_mismatch_field.js | 101 + .../timezone_mismatch_field.xml | 12 + .../src/views/fields/translation_button.js | 63 + .../src/views/fields/translation_button.scss | 31 + .../fields/translation_button.variables.scss | 1 + .../src/views/fields/translation_button.xml | 12 + .../src/views/fields/translation_dialog.js | 112 + .../src/views/fields/translation_dialog.scss | 10 + .../src/views/fields/translation_dialog.xml | 51 + .../static/src/views/fields/url/url_field.js | 71 + .../src/views/fields/url/url_field.scss | 7 + .../static/src/views/fields/url/url_field.xml | 36 + .../views/fields/x2many/list_x2many_field.js | 22 + .../views/fields/x2many/list_x2many_field.xml | 8 + .../src/views/fields/x2many/x2many_field.js | 352 + .../src/views/fields/x2many/x2many_field.xml | 40 + .../src/views/form/button_box/button_box.js | 41 + .../src/views/form/button_box/button_box.scss | 261 + .../src/views/form/button_box/button_box.xml | 23 + .../static/src/views/form/form.variables.scss | 5 + .../static/src/views/form/form_arch_parser.js | 49 + .../views/form/form_cog_menu/form_cog_menu.js | 5 + .../form/form_cog_menu/form_cog_menu.xml | 13 + .../static/src/views/form/form_compiler.js | 683 + .../static/src/views/form/form_controller.js | 708 + .../src/views/form/form_controller.scss | 1159 + .../static/src/views/form/form_controller.xml | 70 + .../form_error_dialog/form_error_dialog.js | 54 + .../form_error_dialog/form_error_dialog.xml | 18 + .../src/views/form/form_group/form_group.js | 98 + .../src/views/form/form_group/form_group.xml | 69 + .../web/static/src/views/form/form_label.js | 67 + .../web/static/src/views/form/form_label.xml | 10 + .../static/src/views/form/form_renderer.js | 155 + .../form_status_indicator.js | 54 + .../form_status_indicator.xml | 35 + .../web/static/src/views/form/form_view.js | 37 + .../static/src/views/form/setting/setting.js | 64 + .../src/views/form/setting/setting.scss | 42 + .../static/src/views/form/setting/setting.xml | 36 + .../status_bar_buttons/status_bar_buttons.js | 24 + .../status_bar_buttons/status_bar_buttons.xml | 31 + .../src/views/graph/graph_arch_parser.js | 95 + .../src/views/graph/graph_controller.js | 68 + .../src/views/graph/graph_controller.xml | 80 + .../web/static/src/views/graph/graph_model.js | 531 + .../static/src/views/graph/graph_renderer.js | 928 + .../static/src/views/graph/graph_renderer.xml | 51 + .../src/views/graph/graph_search_model.js | 19 + .../web/static/src/views/graph/graph_view.js | 56 + .../static/src/views/graph/graph_view.scss | 19 + .../views/kanban/kanban.print_variables.scss | 7 + .../src/views/kanban/kanban.variables.scss | 23 + .../src/views/kanban/kanban_arch_parser.js | 191 + .../src/views/kanban/kanban_cog_menu.js | 12 + .../src/views/kanban/kanban_cog_menu.xml | 10 + .../kanban/kanban_column_examples_dialog.js | 62 + .../kanban/kanban_column_examples_dialog.xml | 31 + .../kanban/kanban_column_progressbar.scss | 47 + .../kanban/kanban_column_quick_create.js | 81 + .../kanban/kanban_column_quick_create.xml | 38 + .../src/views/kanban/kanban_compiler.js | 188 + .../src/views/kanban/kanban_controller.js | 532 + .../src/views/kanban/kanban_controller.scss | 394 + .../src/views/kanban/kanban_controller.xml | 102 + .../views/kanban/kanban_cover_image_dialog.js | 74 + .../kanban/kanban_cover_image_dialog.scss | 4 + .../kanban/kanban_cover_image_dialog.xml | 45 + .../kanban/kanban_dropdown_menu_wrapper.js | 22 + .../views/kanban/kanban_examples_dialog.scss | 46 + .../static/src/views/kanban/kanban_header.js | 145 + .../static/src/views/kanban/kanban_header.xml | 47 + .../static/src/views/kanban/kanban_record.js | 407 + .../src/views/kanban/kanban_record.scss | 217 + .../static/src/views/kanban/kanban_record.xml | 36 + .../kanban/kanban_record_quick_create.js | 271 + .../kanban/kanban_record_quick_create.scss | 11 + .../kanban/kanban_record_quick_create.xml | 30 + .../src/views/kanban/kanban_renderer.js | 713 + .../src/views/kanban/kanban_renderer.xml | 129 + .../static/src/views/kanban/kanban_view.js | 35 + .../src/views/kanban/progress_bar_hook.js | 383 + .../src/views/list/column_width_hook.js | 571 + .../src/views/list/export_all/export_all.js | 41 + .../src/views/list/export_all/export_all.xml | 10 + .../static/src/views/list/list_arch_parser.js | 251 + .../static/src/views/list/list_cog_menu.js | 12 + .../static/src/views/list/list_cog_menu.xml | 10 + .../views/list/list_confirmation_dialog.js | 96 + .../views/list/list_confirmation_dialog.scss | 12 + .../views/list/list_confirmation_dialog.xml | 73 + .../static/src/views/list/list_controller.js | 578 + .../static/src/views/list/list_controller.xml | 116 + .../static/src/views/list/list_renderer.js | 2303 ++ .../static/src/views/list/list_renderer.scss | 580 + .../static/src/views/list/list_renderer.xml | 377 + .../web/static/src/views/list/list_view.js | 34 + .../static/src/views/no_content_helpers.xml | 20 + .../src/views/pivot/pivot_arch_parser.js | 84 + .../src/views/pivot/pivot_controller.js | 77 + .../src/views/pivot/pivot_controller.xml | 42 + .../web/static/src/views/pivot/pivot_model.js | 1598 ++ .../static/src/views/pivot/pivot_renderer.js | 352 + .../static/src/views/pivot/pivot_renderer.xml | 151 + .../src/views/pivot/pivot_search_model.js | 23 + .../web/static/src/views/pivot/pivot_view.js | 60 + .../static/src/views/pivot/pivot_view.scss | 52 + .../static/src/views/standard_view_props.js | 26 + frontend/web/static/src/views/utils.js | 312 + frontend/web/static/src/views/view.js | 487 + frontend/web/static/src/views/view.scss | 88 + frontend/web/static/src/views/view.xml | 17 + .../view_button/multi_record_view_button.js | 26 + .../src/views/view_button/view_button.js | 164 + .../src/views/view_button/view_button.xml | 87 + .../src/views/view_button/view_button_hook.js | 148 + .../web/static/src/views/view_compiler.js | 476 + .../views/view_components/animated_number.js | 86 + .../view_components/animated_number.scss | 23 + .../views/view_components/animated_number.xml | 17 + .../views/view_components/column_progress.js | 22 + .../views/view_components/column_progress.xml | 36 + .../view_components/group_config_menu.js | 106 + .../view_components/group_config_menu.scss | 14 + .../view_components/group_config_menu.xml | 20 + .../view_components/multi_create_popover.js | 85 + .../view_components/multi_create_popover.xml | 35 + .../view_components/multi_currency_popover.js | 47 + .../multi_currency_popover.xml | 16 + .../multi_selection_buttons.js | 204 + .../multi_selection_buttons.xml | 23 + .../view_components/report_view_measures.js | 16 + .../view_components/report_view_measures.xml | 22 + .../views/view_components/selection_box.js | 45 + .../views/view_components/selection_box.scss | 13 + .../views/view_components/selection_box.xml | 31 + .../view_components/view_scale_selector.js | 22 + .../view_components/view_scale_selector.xml | 33 + .../views/view_dialogs/export_data_dialog.js | 421 + .../view_dialogs/export_data_dialog.scss | 35 + .../views/view_dialogs/export_data_dialog.xml | 126 + .../views/view_dialogs/form_view_dialog.js | 135 + .../views/view_dialogs/form_view_dialog.xml | 31 + .../view_dialogs/select_create_dialog.js | 131 + .../view_dialogs/select_create_dialog.scss | 8 + .../view_dialogs/select_create_dialog.xml | 37 + frontend/web/static/src/views/view_hook.js | 218 + frontend/web/static/src/views/view_service.js | 125 + .../attach_document/attach_document.js | 88 + .../attach_document/attach_document.xml | 10 + .../documentation_link/documentation_link.js | 56 + .../documentation_link/documentation_link.xml | 15 + .../notification_alert/notification_alert.js | 20 + .../notification_alert/notification_alert.xml | 8 + .../static/src/views/widgets/ribbon/ribbon.js | 71 + .../src/views/widgets/ribbon/ribbon.scss | 70 + .../src/views/widgets/ribbon/ribbon.xml | 12 + .../src/views/widgets/signature/signature.js | 78 + .../src/views/widgets/signature/signature.xml | 10 + .../views/widgets/standard_widget_props.js | 4 + .../src/views/widgets/week_days/week_days.js | 46 + .../views/widgets/week_days/week_days.scss | 5 + .../src/views/widgets/week_days/week_days.xml | 32 + .../web/static/src/views/widgets/widget.js | 139 + .../src/webclient/actions/action_container.js | 26 + .../src/webclient/actions/action_dialog.js | 26 + .../src/webclient/actions/action_dialog.scss | 15 + .../src/webclient/actions/action_dialog.xml | 25 + .../actions/action_install_kiosk_pwa.js | 37 + .../actions/action_install_kiosk_pwa.xml | 14 + .../src/webclient/actions/action_service.js | 1882 ++ .../src/webclient/actions/blank_component.xml | 10 + .../src/webclient/actions/client_actions.js | 102 + .../src/webclient/actions/debug_items.js | 179 + .../reports/bootstrap_overridden_report.scss | 44 + .../reports/bootstrap_review_report.scss | 162 + .../reports/layout_assets/layout_bubble.scss | 31 + .../reports/layout_assets/layout_folder.scss | 12 + .../reports/layout_assets/layout_wave.scss | 16 + .../src/webclient/actions/reports/report.scss | 299 + .../actions/reports/report_action.js | 57 + .../actions/reports/report_action.xml | 15 + .../webclient/actions/reports/report_hook.js | 65 + .../actions/reports/report_tables.scss | 214 + .../webclient/actions/reports/reset.min.css | 188 + .../reports/utilities_custom_report.scss | 58 + .../src/webclient/actions/reports/utils.js | 86 + .../src/webclient/burger_menu/burger_menu.js | 67 + .../webclient/burger_menu/burger_menu.scss | 78 + .../burger_menu/burger_menu.variables.scss | 7 + .../src/webclient/burger_menu/burger_menu.xml | 35 + .../burger_user_menu/burger_user_menu.js | 15 + .../burger_user_menu/burger_user_menu.xml | 24 + .../mobile_switch_company_menu.js | 20 + .../mobile_switch_company_menu.xml | 16 + .../static/src/webclient/clickbot/clickbot.js | 533 + .../src/webclient/clickbot/clickbot_loader.js | 33 + .../static/src/webclient/currency_service.js | 31 + .../static/src/webclient/debug/debug_items.js | 59 + .../debug/profiling/profiling_item.js | 33 + .../debug/profiling/profiling_item.scss | 7 + .../debug/profiling/profiling_item.xml | 76 + .../debug/profiling/profiling_qweb.js | 336 + .../debug/profiling/profiling_qweb.scss | 136 + .../debug/profiling/profiling_qweb.xml | 60 + .../debug/profiling/profiling_service.js | 102 + .../debug/profiling/profiling_systray_item.js | 10 + .../profiling/profiling_systray_item.xml | 10 + .../offline_fail_to_fetch_error_handler.js | 28 + frontend/web/static/src/webclient/icons.scss | 97 + .../loading_indicator/loading_indicator.js | 66 + .../loading_indicator/loading_indicator.scss | 14 + .../loading_indicator/loading_indicator.xml | 10 + .../src/webclient/menus/menu_command_item.xml | 17 + .../src/webclient/menus/menu_helpers.js | 85 + .../src/webclient/menus/menu_providers.js | 79 + .../src/webclient/menus/menu_service.js | 110 + .../web/static/src/webclient/navbar/navbar.js | 242 + .../static/src/webclient/navbar/navbar.scss | 252 + .../webclient/navbar/navbar.variables.scss | 59 + .../static/src/webclient/navbar/navbar.xml | 266 + .../src/webclient/reload_company_service.js | 21 + .../res_user_group_ids_field.js | 265 + .../res_user_group_ids_field.scss | 12 + .../res_user_group_ids_field.xml | 10 + .../res_user_group_ids_popover.js | 65 + .../res_user_group_ids_popover.xml | 64 + .../res_user_group_ids_privilege_field.js | 106 + .../res_user_group_ids_privilege_field.scss | 25 + .../res_user_group_ids_privilege_field.xml | 12 + .../static/src/webclient/session_service.js | 26 + .../settings_binary_field.js | 25 + .../settings_binary_field.xml | 10 + .../fields/upgrade_boolean_field.js | 41 + .../fields/upgrade_dialog.js | 25 + .../fields/upgrade_dialog.xml | 30 + .../form_label_highlight_text.js | 15 + .../form_label_highlight_text.xml | 11 + .../highlight_text/highlight_text.js | 20 + .../highlight_text/highlight_text.xml | 8 + .../highlight_text/settings_radio_field.js | 15 + .../highlight_text/settings_radio_field.xml | 12 + .../settings/searchable_setting.js | 57 + .../settings/searchable_setting.scss | 14 + .../settings/searchable_setting.xml | 15 + .../settings/setting_header.js | 26 + .../settings/setting_header.xml | 14 + .../settings/settings_app.js | 34 + .../settings/settings_app.xml | 12 + .../settings/settings_block.js | 61 + .../settings/settings_block.xml | 10 + .../settings/settings_page.js | 99 + .../settings/settings_page.xml | 34 + .../settings_confirmation_dialog.js | 20 + .../settings_confirmation_dialog.xml | 19 + .../settings_form_compiler.js | 127 + .../settings_form_controller.js | 148 + .../settings_form_renderer.js | 37 + .../settings_form_view/settings_form_view.js | 72 + .../settings_form_view.scss | 180 + .../settings_form_view/settings_form_view.xml | 42 + .../settings_form_view_mobile.scss | 66 + .../widgets/demo_data_service.js | 18 + .../widgets/res_config_dev_tool.js | 54 + .../widgets/res_config_dev_tool.xml | 16 + .../widgets/res_config_edition.js | 32 + .../widgets/res_config_edition.xml | 20 + .../widgets/res_config_invite_users.js | 147 + .../widgets/res_config_invite_users.xml | 21 + .../widgets/settings_widgets.scss | 11 + .../widgets/user_invite_service.js | 18 + .../share_target/share_target_service.js | 64 + .../switch_company_item.js | 40 + .../switch_company_item.xml | 48 + .../switch_company_menu.js | 364 + .../switch_company_menu.scss | 48 + .../switch_company_menu.xml | 83 + .../src/webclient/user_menu/user_menu.js | 43 + .../src/webclient/user_menu/user_menu.scss | 7 + .../src/webclient/user_menu/user_menu.xml | 46 + .../webclient/user_menu/user_menu_items.js | 145 + .../webclient/user_menu/user_menu_items.xml | 10 + .../web/static/src/webclient/webclient.js | 198 + .../web/static/src/webclient/webclient.scss | 287 + .../web/static/src/webclient/webclient.xml | 12 + .../src/webclient/webclient_layout.scss | 114 + pkg/server/bundle.go | 114 + pkg/server/server.go | 266 +- pkg/server/static.go | 51 +- pkg/server/templates.go | 397 + pkg/server/webclient.go | 60 +- pkg/tools/config.go | 13 +- tools/compile_templates.py | 102 - tools/transpile_assets.py | 120 - 2933 files changed, 280644 insertions(+), 264 deletions(-) create mode 100644 frontend/account/static/demo/bank_opening_statement.pdf create mode 100644 frontend/account/static/demo/bank_statement_one_month_old.pdf create mode 100644 frontend/account/static/demo/in_invoice_yourcompany_demo_1.pdf create mode 100644 frontend/account/static/demo/in_invoice_yourcompany_demo_2.pdf create mode 100644 frontend/account/static/description/icon.png create mode 100644 frontend/account/static/description/icon.svg create mode 100644 frontend/account/static/description/icon_hi.png create mode 100644 frontend/account/static/description/l10n.png create mode 100644 frontend/account/static/description/l10n.svg create mode 100644 frontend/account/static/src/components/account_batch_sending_summary/account_batch_sending_summary.js create mode 100644 frontend/account/static/src/components/account_batch_sending_summary/account_batch_sending_summary.xml create mode 100644 frontend/account/static/src/components/account_file_uploader/account_file_uploader.js create mode 100644 frontend/account/static/src/components/account_file_uploader/account_file_uploader.scss create mode 100644 frontend/account/static/src/components/account_file_uploader/account_file_uploader.xml create mode 100644 frontend/account/static/src/components/account_merge_wizard_line_one2many/account_merge_wizard_line_one2many.js create mode 100644 frontend/account/static/src/components/account_move_form/account_move_form.js create mode 100644 frontend/account/static/src/components/account_move_form/account_move_form_notebook.xml create mode 100644 frontend/account/static/src/components/account_payment_field/account_payment.xml create mode 100644 frontend/account/static/src/components/account_payment_field/account_payment_field.js create mode 100644 frontend/account/static/src/components/account_payment_register_html/account_payment_register_html.js create mode 100644 frontend/account/static/src/components/account_payment_register_html/account_payment_register_html.xml create mode 100644 frontend/account/static/src/components/account_payment_term_form/payment_term_line_ids.js create mode 100644 frontend/account/static/src/components/account_pick_currency_rate/account_pick_currency_rate.js create mode 100644 frontend/account/static/src/components/account_pick_currency_rate/account_pick_currency_rate.xml create mode 100644 frontend/account/static/src/components/account_resequence/account_resequence.xml create mode 100644 frontend/account/static/src/components/account_resequence/account_resequence_field.js create mode 100644 frontend/account/static/src/components/account_statusbar_secured/account_move_statusbar_secured.js create mode 100644 frontend/account/static/src/components/account_statusbar_secured/account_move_statusbar_secured.xml create mode 100644 frontend/account/static/src/components/account_tax_repartition_line_factor_percent/account_tax_repartition_line_factor_percent.js create mode 100644 frontend/account/static/src/components/account_type_selection/account_type_selection.js create mode 100644 frontend/account/static/src/components/account_type_selection/account_type_selection.xml create mode 100644 frontend/account/static/src/components/actionable_errors/actionable_errors.js create mode 100644 frontend/account/static/src/components/actionable_errors/actionable_errors.xml create mode 100644 frontend/account/static/src/components/auto_save_res_partner_bank/auto_save_res_partner_bank.js create mode 100644 frontend/account/static/src/components/autosave_many2many_tax_tags/autosave_many2many_tax_tags.js create mode 100644 frontend/account/static/src/components/bill_guide/bill_guide.js create mode 100644 frontend/account/static/src/components/bill_guide/bill_guide.scss create mode 100644 frontend/account/static/src/components/bill_guide/bill_guide.xml create mode 100644 frontend/account/static/src/components/char_with_placeholder_field/char_with_placeholder_field.js create mode 100644 frontend/account/static/src/components/char_with_placeholder_field/char_with_placeholder_field.xml create mode 100644 frontend/account/static/src/components/char_with_placeholder_field_to_check/char_with_placeholder_field.xml create mode 100644 frontend/account/static/src/components/char_with_placeholder_field_to_check/char_with_placeholder_field_to_check_to_check.js create mode 100644 frontend/account/static/src/components/currency_form/form_controller.js create mode 100644 frontend/account/static/src/components/currency_form/open_decimal_precision_btn.js create mode 100644 frontend/account/static/src/components/currency_form/open_decimal_precision_btn_template.xml create mode 100644 frontend/account/static/src/components/document_file_uploader/document_file_uploader.js create mode 100644 frontend/account/static/src/components/document_file_uploader/document_file_uploader.xml create mode 100644 frontend/account/static/src/components/document_state/document_state_field.js create mode 100644 frontend/account/static/src/components/document_state/document_state_field.scss create mode 100644 frontend/account/static/src/components/document_state/document_state_field.xml create mode 100644 frontend/account/static/src/components/dynamic_selection/dynamic_selection.js create mode 100644 frontend/account/static/src/components/fetch_einvoices/fetch_einvoices.xml create mode 100644 frontend/account/static/src/components/fetch_einvoices/fetch_einvoices_cog.js create mode 100644 frontend/account/static/src/components/grouped_view_widget/grouped_view_widget.js create mode 100644 frontend/account/static/src/components/grouped_view_widget/grouped_view_widget.xml create mode 100644 frontend/account/static/src/components/mail_attachments/mail_attachments.js create mode 100644 frontend/account/static/src/components/mail_attachments/mail_attachments.xml create mode 100644 frontend/account/static/src/components/mail_attachments/mail_attachments_selector.js create mode 100644 frontend/account/static/src/components/many2many_tags_banks/many2many_tags_banks.js create mode 100644 frontend/account/static/src/components/many2many_tags_banks/many2many_tags_banks.xml create mode 100644 frontend/account/static/src/components/many2many_tags_journals/many2many_tags_journals.js create mode 100644 frontend/account/static/src/components/many2many_tags_journals/many2many_tags_journals.xml create mode 100644 frontend/account/static/src/components/many2x_tax_tags/many2x_tax_tags.js create mode 100644 frontend/account/static/src/components/onboarding/onboarding.js create mode 100644 frontend/account/static/src/components/onboarding/onboarding.xml create mode 100644 frontend/account/static/src/components/open_move_line_move_widget/open_move_line_move_widget.js create mode 100644 frontend/account/static/src/components/open_move_line_move_widget/open_move_line_move_widget.xml create mode 100644 frontend/account/static/src/components/open_move_widget/open_move_widget.js create mode 100644 frontend/account/static/src/components/open_move_widget/open_move_widget.xml create mode 100644 frontend/account/static/src/components/product_catalog/account_move_line.js create mode 100644 frontend/account/static/src/components/product_catalog/kanban_controller.js create mode 100644 frontend/account/static/src/components/product_catalog/kanban_model.js create mode 100644 frontend/account/static/src/components/product_catalog/kanban_record.js create mode 100644 frontend/account/static/src/components/product_catalog/kanban_view.js create mode 100644 frontend/account/static/src/components/product_catalog/search/search_model.js create mode 100644 frontend/account/static/src/components/product_catalog/search/search_panel.js create mode 100644 frontend/account/static/src/components/product_catalog/search/search_panel.scss create mode 100644 frontend/account/static/src/components/product_catalog/search/search_panel.xml create mode 100644 frontend/account/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.js create mode 100644 frontend/account/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.scss create mode 100644 frontend/account/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml create mode 100644 frontend/account/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field_o2m.js create mode 100644 frontend/account/static/src/components/receipt_selector/receipt_selector.js create mode 100644 frontend/account/static/src/components/receipt_selector/receipt_selector.xml create mode 100644 frontend/account/static/src/components/section_and_note_fields_backend/section_and_note_backend.scss create mode 100644 frontend/account/static/src/components/section_and_note_fields_backend/section_and_note_fields_backend.js create mode 100644 frontend/account/static/src/components/section_and_note_fields_backend/section_and_note_fields_backend.xml create mode 100644 frontend/account/static/src/components/tax_autocomplete/tax_autocomplete.xml create mode 100644 frontend/account/static/src/components/tax_totals/tax_totals.css create mode 100644 frontend/account/static/src/components/tax_totals/tax_totals.js create mode 100644 frontend/account/static/src/components/tax_totals/tax_totals.xml create mode 100644 frontend/account/static/src/components/tests_shared_js_python/test_shared_js_python.xml create mode 100644 frontend/account/static/src/components/tests_shared_js_python/tests_shared_js_python.js create mode 100644 frontend/account/static/src/components/upload_drop_zone/upload_drop_zone.js create mode 100644 frontend/account/static/src/components/upload_drop_zone/upload_drop_zone.scss create mode 100644 frontend/account/static/src/components/upload_drop_zone/upload_drop_zone.xml create mode 100644 frontend/account/static/src/components/x2many_buttons/x2many_buttons.js create mode 100644 frontend/account/static/src/components/x2many_buttons/x2many_buttons.xml create mode 100644 frontend/account/static/src/css/account.css create mode 100644 frontend/account/static/src/css/account_bank_and_cash.css create mode 100644 frontend/account/static/src/css/account_payment.scss create mode 100644 frontend/account/static/src/css/report_invoice.css create mode 100644 frontend/account/static/src/helpers/account_tax.js create mode 100644 frontend/account/static/src/img/Odoo_logo_O.svg create mode 100644 frontend/account/static/src/img/account_dashboard_onboarding_bg.jpg create mode 100644 frontend/account/static/src/img/account_invoice_onboarding_bg.jpg create mode 100644 frontend/account/static/src/img/bank.svg create mode 100644 frontend/account/static/src/img/bill.svg create mode 100644 frontend/account/static/src/img/btn_paynowcc_lg.gif create mode 100644 frontend/account/static/src/img/graph.png create mode 100644 frontend/account/static/src/img/invoice-stamps.png create mode 100644 frontend/account/static/src/img/multi_ledger.svg create mode 100644 frontend/account/static/src/interactions/account_portal.js create mode 100644 frontend/account/static/src/interactions/account_sidebar.js create mode 100644 frontend/account/static/src/js/search/search_bar/search_bar.js create mode 100644 frontend/account/static/src/js/tours/account.js create mode 100644 frontend/account/static/src/js/tours/tour_utils.js create mode 100644 frontend/account/static/src/scss/account.scss create mode 100644 frontend/account/static/src/scss/account_journal_dashboard.scss create mode 100644 frontend/account/static/src/scss/account_move_send_wizard.scss create mode 100644 frontend/account/static/src/scss/account_multi_ledger.scss create mode 100644 frontend/account/static/src/scss/account_payment_term.scss create mode 100644 frontend/account/static/src/scss/account_reconcile_model.scss create mode 100644 frontend/account/static/src/scss/account_searchpanel.scss create mode 100644 frontend/account/static/src/scss/variables.scss create mode 100644 frontend/account/static/src/services/account_move_service.js create mode 100644 frontend/account/static/src/services/account_notification_service.js create mode 100644 frontend/account/static/src/views/account_dashboard_kanban/account_dashboard_kanban.scss create mode 100644 frontend/account/static/src/views/account_dashboard_kanban/account_dashboard_kanban_record.js create mode 100644 frontend/account/static/src/views/account_dashboard_kanban/account_dashboard_kanban_record.xml create mode 100644 frontend/account/static/src/views/account_dashboard_kanban/account_dashboard_kanban_renderer.js create mode 100644 frontend/account/static/src/views/account_dashboard_kanban/account_dashboard_kanban_renderer.xml create mode 100644 frontend/account/static/src/views/account_dashboard_kanban/account_dashboard_kanban_view.js create mode 100644 frontend/account/static/src/views/account_move_kanban/account_move_kanban_controller.js create mode 100644 frontend/account/static/src/views/account_move_kanban/account_move_kanban_controller.xml create mode 100644 frontend/account/static/src/views/account_move_kanban/account_move_kanban_view.js create mode 100644 frontend/account/static/src/views/account_move_list/account_move_list_controller.js create mode 100644 frontend/account/static/src/views/account_move_list/account_move_list_controller.xml create mode 100644 frontend/account/static/src/views/account_move_list/account_move_list_renderer.js create mode 100644 frontend/account/static/src/views/account_move_list/account_move_list_renderer.xml create mode 100644 frontend/account/static/src/views/account_move_list/account_move_list_view.js create mode 100644 frontend/account/static/src/views/account_x2many_list_controller.js create mode 100644 frontend/account/static/src/views/file_upload_kanban/file_upload_kanban_controller.js create mode 100644 frontend/account/static/src/views/file_upload_kanban/file_upload_kanban_controller.xml create mode 100644 frontend/account/static/src/views/file_upload_kanban/file_upload_kanban_renderer.js create mode 100644 frontend/account/static/src/views/file_upload_kanban/file_upload_kanban_renderer.xml create mode 100644 frontend/account/static/src/views/file_upload_kanban/file_upload_kanban_view.js create mode 100644 frontend/account/static/src/views/file_upload_list/file_upload_list_controller.js create mode 100644 frontend/account/static/src/views/file_upload_list/file_upload_list_controller.xml create mode 100644 frontend/account/static/src/views/file_upload_list/file_upload_list_renderer.js create mode 100644 frontend/account/static/src/views/file_upload_list/file_upload_list_renderer.xml create mode 100644 frontend/account/static/src/views/file_upload_list/file_upload_list_view.js create mode 100644 frontend/account/static/src/views/upload_file_from_data_hook.js create mode 100644 frontend/account/static/tests/account_move_form.test.js create mode 100644 frontend/account/static/tests/account_test_helpers.js create mode 100644 frontend/account/static/tests/account_widgets.test.js create mode 100644 frontend/account/static/tests/char_with_placeholder_field.test.js create mode 100644 frontend/account/static/tests/mock_server/mock_models/account_move.js create mode 100644 frontend/account/static/tests/mock_server/mock_models/account_move_line.js create mode 100644 frontend/account/static/tests/section_and_note.test.js create mode 100644 frontend/account/static/tests/tours/account_product_catalog_tests.js create mode 100644 frontend/account/static/tests/tours/deductible_amount_column.js create mode 100644 frontend/account/static/tests/tours/tax_group_tests.js create mode 100644 frontend/account/static/tests/tours/tour_tests_shared_js_python.js create mode 100644 frontend/account/static/tests/x2many_buttons.test.js create mode 100644 frontend/account/static/xls/aml_import_template.xlsx create mode 100644 frontend/account/static/xls/coa_import_template.xlsx create mode 100644 frontend/account/static/xls/customer_invoices_credit_notes_import_template.xlsx create mode 100644 frontend/account/static/xls/misc_operations_import_template.xlsx create mode 100644 frontend/account/static/xls/vendor_bills_refunds_import_template.xlsx create mode 100644 frontend/base/static/description/board.png create mode 100644 frontend/base/static/description/board.svg create mode 100644 frontend/base/static/description/exception.png create mode 100644 frontend/base/static/description/exception.svg create mode 100644 frontend/base/static/description/icon.png create mode 100644 frontend/base/static/description/icon.svg create mode 100644 frontend/base/static/description/modules.png create mode 100644 frontend/base/static/description/modules.svg create mode 100644 frontend/base/static/description/settings.png create mode 100644 frontend/base/static/description/settings.svg create mode 100644 frontend/base/static/img/avatar.png create mode 100644 frontend/base/static/img/avatar_grey.png create mode 100644 frontend/base/static/img/bill.png create mode 100644 frontend/base/static/img/company_image.png create mode 100644 frontend/base/static/img/country_flags/419.png create mode 100644 frontend/base/static/img/country_flags/ad.png create mode 100644 frontend/base/static/img/country_flags/ae.png create mode 100644 frontend/base/static/img/country_flags/af.png create mode 100644 frontend/base/static/img/country_flags/ag.png create mode 100644 frontend/base/static/img/country_flags/ai.png create mode 100644 frontend/base/static/img/country_flags/al.png create mode 100644 frontend/base/static/img/country_flags/am.png create mode 100644 frontend/base/static/img/country_flags/an.png create mode 100644 frontend/base/static/img/country_flags/ao.png create mode 100644 frontend/base/static/img/country_flags/ar.png create mode 100644 frontend/base/static/img/country_flags/as.png create mode 100644 frontend/base/static/img/country_flags/at.png create mode 100644 frontend/base/static/img/country_flags/au.png create mode 100644 frontend/base/static/img/country_flags/aw.png create mode 100644 frontend/base/static/img/country_flags/ax.png create mode 100644 frontend/base/static/img/country_flags/az.png create mode 100644 frontend/base/static/img/country_flags/ba.png create mode 100644 frontend/base/static/img/country_flags/bb.png create mode 100644 frontend/base/static/img/country_flags/bd.png create mode 100644 frontend/base/static/img/country_flags/be.png create mode 100644 frontend/base/static/img/country_flags/bf.png create mode 100644 frontend/base/static/img/country_flags/bg.png create mode 100644 frontend/base/static/img/country_flags/bh.png create mode 100644 frontend/base/static/img/country_flags/bi.png create mode 100644 frontend/base/static/img/country_flags/bj.png create mode 100644 frontend/base/static/img/country_flags/bl.png create mode 100644 frontend/base/static/img/country_flags/bm.png create mode 100644 frontend/base/static/img/country_flags/bn.png create mode 100644 frontend/base/static/img/country_flags/bo.png create mode 100644 frontend/base/static/img/country_flags/br.png create mode 100644 frontend/base/static/img/country_flags/bs.png create mode 100644 frontend/base/static/img/country_flags/bt.png create mode 100644 frontend/base/static/img/country_flags/bw.png create mode 100644 frontend/base/static/img/country_flags/by.png create mode 100644 frontend/base/static/img/country_flags/bz.png create mode 100644 frontend/base/static/img/country_flags/ca.png create mode 100644 frontend/base/static/img/country_flags/cc.png create mode 100644 frontend/base/static/img/country_flags/cd.png create mode 100644 frontend/base/static/img/country_flags/cf.png create mode 100644 frontend/base/static/img/country_flags/cg.png create mode 100644 frontend/base/static/img/country_flags/ch.png create mode 100644 frontend/base/static/img/country_flags/ci.png create mode 100644 frontend/base/static/img/country_flags/ck.png create mode 100644 frontend/base/static/img/country_flags/cl.png create mode 100644 frontend/base/static/img/country_flags/cm.png create mode 100644 frontend/base/static/img/country_flags/cn.png create mode 100644 frontend/base/static/img/country_flags/co.png create mode 100644 frontend/base/static/img/country_flags/cr.png create mode 100644 frontend/base/static/img/country_flags/cu.png create mode 100644 frontend/base/static/img/country_flags/cv.png create mode 100644 frontend/base/static/img/country_flags/cw.png create mode 100644 frontend/base/static/img/country_flags/cx.png create mode 100644 frontend/base/static/img/country_flags/cy.png create mode 100644 frontend/base/static/img/country_flags/cz.png create mode 100644 frontend/base/static/img/country_flags/de.png create mode 100644 frontend/base/static/img/country_flags/dj.png create mode 100644 frontend/base/static/img/country_flags/dk.png create mode 100644 frontend/base/static/img/country_flags/dm.png create mode 100644 frontend/base/static/img/country_flags/do.png create mode 100644 frontend/base/static/img/country_flags/dz.png create mode 100644 frontend/base/static/img/country_flags/ec.png create mode 100644 frontend/base/static/img/country_flags/ee.png create mode 100644 frontend/base/static/img/country_flags/eg.png create mode 100644 frontend/base/static/img/country_flags/eh.png create mode 100644 frontend/base/static/img/country_flags/er.png create mode 100644 frontend/base/static/img/country_flags/es.png create mode 100644 frontend/base/static/img/country_flags/et.png create mode 100644 frontend/base/static/img/country_flags/fi.png create mode 100644 frontend/base/static/img/country_flags/fj.png create mode 100644 frontend/base/static/img/country_flags/fk.png create mode 100644 frontend/base/static/img/country_flags/fm.png create mode 100644 frontend/base/static/img/country_flags/fo.png create mode 100644 frontend/base/static/img/country_flags/fr.png create mode 100644 frontend/base/static/img/country_flags/ga.png create mode 100644 frontend/base/static/img/country_flags/gb.png create mode 100644 frontend/base/static/img/country_flags/gd.png create mode 100644 frontend/base/static/img/country_flags/ge.png create mode 100644 frontend/base/static/img/country_flags/gg.png create mode 100644 frontend/base/static/img/country_flags/gh.png create mode 100644 frontend/base/static/img/country_flags/gi.png create mode 100644 frontend/base/static/img/country_flags/gl.png create mode 100644 frontend/base/static/img/country_flags/gm.png create mode 100644 frontend/base/static/img/country_flags/gn.png create mode 100644 frontend/base/static/img/country_flags/gq.png create mode 100644 frontend/base/static/img/country_flags/gr.png create mode 100644 frontend/base/static/img/country_flags/gs.png create mode 100644 frontend/base/static/img/country_flags/gt.png create mode 100644 frontend/base/static/img/country_flags/gu.png create mode 100644 frontend/base/static/img/country_flags/gw.png create mode 100644 frontend/base/static/img/country_flags/gy.png create mode 100644 frontend/base/static/img/country_flags/hk.png create mode 100644 frontend/base/static/img/country_flags/hn.png create mode 100644 frontend/base/static/img/country_flags/hr.png create mode 100644 frontend/base/static/img/country_flags/ht.png create mode 100644 frontend/base/static/img/country_flags/hu.png create mode 100644 frontend/base/static/img/country_flags/id.png create mode 100644 frontend/base/static/img/country_flags/ie.png create mode 100644 frontend/base/static/img/country_flags/il.png create mode 100644 frontend/base/static/img/country_flags/im.png create mode 100644 frontend/base/static/img/country_flags/in.png create mode 100644 frontend/base/static/img/country_flags/io.png create mode 100644 frontend/base/static/img/country_flags/iq.png create mode 100644 frontend/base/static/img/country_flags/ir.png create mode 100644 frontend/base/static/img/country_flags/iran.png create mode 100644 frontend/base/static/img/country_flags/is.png create mode 100644 frontend/base/static/img/country_flags/it.png create mode 100644 frontend/base/static/img/country_flags/je.png create mode 100644 frontend/base/static/img/country_flags/jm.png create mode 100644 frontend/base/static/img/country_flags/jo.png create mode 100644 frontend/base/static/img/country_flags/jp.png create mode 100644 frontend/base/static/img/country_flags/ke.png create mode 100644 frontend/base/static/img/country_flags/kg.png create mode 100644 frontend/base/static/img/country_flags/kh.png create mode 100644 frontend/base/static/img/country_flags/ki.png create mode 100644 frontend/base/static/img/country_flags/km.png create mode 100644 frontend/base/static/img/country_flags/kn.png create mode 100644 frontend/base/static/img/country_flags/kp.png create mode 100644 frontend/base/static/img/country_flags/kr.png create mode 100644 frontend/base/static/img/country_flags/kw.png create mode 100644 frontend/base/static/img/country_flags/ky.png create mode 100644 frontend/base/static/img/country_flags/kz.png create mode 100644 frontend/base/static/img/country_flags/la.png create mode 100644 frontend/base/static/img/country_flags/lb.png create mode 100644 frontend/base/static/img/country_flags/lc.png create mode 100644 frontend/base/static/img/country_flags/li.png create mode 100644 frontend/base/static/img/country_flags/lk.png create mode 100644 frontend/base/static/img/country_flags/lr.png create mode 100644 frontend/base/static/img/country_flags/ls.png create mode 100644 frontend/base/static/img/country_flags/lt.png create mode 100644 frontend/base/static/img/country_flags/lu.png create mode 100644 frontend/base/static/img/country_flags/lv.png create mode 100644 frontend/base/static/img/country_flags/ly.png create mode 100644 frontend/base/static/img/country_flags/ma.png create mode 100644 frontend/base/static/img/country_flags/mc.png create mode 100644 frontend/base/static/img/country_flags/md.png create mode 100644 frontend/base/static/img/country_flags/me.png create mode 100644 frontend/base/static/img/country_flags/mg.png create mode 100644 frontend/base/static/img/country_flags/mh.png create mode 100644 frontend/base/static/img/country_flags/mk.png create mode 100644 frontend/base/static/img/country_flags/ml.png create mode 100644 frontend/base/static/img/country_flags/mm.png create mode 100644 frontend/base/static/img/country_flags/mn.png create mode 100644 frontend/base/static/img/country_flags/mo.png create mode 100644 frontend/base/static/img/country_flags/mp.png create mode 100644 frontend/base/static/img/country_flags/mq.png create mode 100644 frontend/base/static/img/country_flags/mr.png create mode 100644 frontend/base/static/img/country_flags/ms.png create mode 100644 frontend/base/static/img/country_flags/mt.png create mode 100644 frontend/base/static/img/country_flags/mu.png create mode 100644 frontend/base/static/img/country_flags/mv.png create mode 100644 frontend/base/static/img/country_flags/mw.png create mode 100644 frontend/base/static/img/country_flags/mx.png create mode 100644 frontend/base/static/img/country_flags/my.png create mode 100644 frontend/base/static/img/country_flags/mz.png create mode 100644 frontend/base/static/img/country_flags/na.png create mode 100644 frontend/base/static/img/country_flags/nc.png create mode 100644 frontend/base/static/img/country_flags/ne.png create mode 100644 frontend/base/static/img/country_flags/nf.png create mode 100644 frontend/base/static/img/country_flags/ng.png create mode 100644 frontend/base/static/img/country_flags/ni.png create mode 100644 frontend/base/static/img/country_flags/nl.png create mode 100644 frontend/base/static/img/country_flags/no.png create mode 100644 frontend/base/static/img/country_flags/np.png create mode 100644 frontend/base/static/img/country_flags/nr.png create mode 100644 frontend/base/static/img/country_flags/nu.png create mode 100644 frontend/base/static/img/country_flags/nz.png create mode 100644 frontend/base/static/img/country_flags/om.png create mode 100644 frontend/base/static/img/country_flags/pa.png create mode 100644 frontend/base/static/img/country_flags/pe.png create mode 100644 frontend/base/static/img/country_flags/pf.png create mode 100644 frontend/base/static/img/country_flags/pg.png create mode 100644 frontend/base/static/img/country_flags/ph.png create mode 100644 frontend/base/static/img/country_flags/pk.png create mode 100644 frontend/base/static/img/country_flags/pl.png create mode 100644 frontend/base/static/img/country_flags/pm.png create mode 100644 frontend/base/static/img/country_flags/pn.png create mode 100644 frontend/base/static/img/country_flags/pr.png create mode 100644 frontend/base/static/img/country_flags/ps.png create mode 100644 frontend/base/static/img/country_flags/pt.png create mode 100644 frontend/base/static/img/country_flags/pw.png create mode 100644 frontend/base/static/img/country_flags/py.png create mode 100644 frontend/base/static/img/country_flags/qa.png create mode 100644 frontend/base/static/img/country_flags/ro.png create mode 100644 frontend/base/static/img/country_flags/rs.png create mode 100644 frontend/base/static/img/country_flags/ru.png create mode 100644 frontend/base/static/img/country_flags/rw.png create mode 100644 frontend/base/static/img/country_flags/sa.png create mode 100644 frontend/base/static/img/country_flags/sb.png create mode 100644 frontend/base/static/img/country_flags/sc.png create mode 100644 frontend/base/static/img/country_flags/sd.png create mode 100644 frontend/base/static/img/country_flags/se.png create mode 100644 frontend/base/static/img/country_flags/sg.png create mode 100644 frontend/base/static/img/country_flags/sh.png create mode 100644 frontend/base/static/img/country_flags/si.png create mode 100644 frontend/base/static/img/country_flags/sk.png create mode 100644 frontend/base/static/img/country_flags/sl.png create mode 100644 frontend/base/static/img/country_flags/sm.png create mode 100644 frontend/base/static/img/country_flags/sn.png create mode 100644 frontend/base/static/img/country_flags/so.png create mode 100644 frontend/base/static/img/country_flags/sr.png create mode 100644 frontend/base/static/img/country_flags/ss.png create mode 100644 frontend/base/static/img/country_flags/st.png create mode 100644 frontend/base/static/img/country_flags/sv.png create mode 100644 frontend/base/static/img/country_flags/sx.png create mode 100644 frontend/base/static/img/country_flags/sy.png create mode 100644 frontend/base/static/img/country_flags/sz.png create mode 100644 frontend/base/static/img/country_flags/tc.png create mode 100644 frontend/base/static/img/country_flags/td.png create mode 100644 frontend/base/static/img/country_flags/tf.png create mode 100644 frontend/base/static/img/country_flags/tg.png create mode 100644 frontend/base/static/img/country_flags/th.png create mode 100644 frontend/base/static/img/country_flags/tj.png create mode 100644 frontend/base/static/img/country_flags/tk.png create mode 100644 frontend/base/static/img/country_flags/tl.png create mode 100644 frontend/base/static/img/country_flags/tm.png create mode 100644 frontend/base/static/img/country_flags/tn.png create mode 100644 frontend/base/static/img/country_flags/to.png create mode 100644 frontend/base/static/img/country_flags/tr.png create mode 100644 frontend/base/static/img/country_flags/tt.png create mode 100644 frontend/base/static/img/country_flags/tv.png create mode 100644 frontend/base/static/img/country_flags/tw.png create mode 100644 frontend/base/static/img/country_flags/tz.png create mode 100644 frontend/base/static/img/country_flags/ua.png create mode 100644 frontend/base/static/img/country_flags/ug.png create mode 100644 frontend/base/static/img/country_flags/uk.png create mode 100644 frontend/base/static/img/country_flags/us.png create mode 100644 frontend/base/static/img/country_flags/uy.png create mode 100644 frontend/base/static/img/country_flags/uz.png create mode 100644 frontend/base/static/img/country_flags/va.png create mode 100644 frontend/base/static/img/country_flags/vc.png create mode 100644 frontend/base/static/img/country_flags/ve.png create mode 100644 frontend/base/static/img/country_flags/vg.png create mode 100644 frontend/base/static/img/country_flags/vi.png create mode 100644 frontend/base/static/img/country_flags/vn.png create mode 100644 frontend/base/static/img/country_flags/vu.png create mode 100644 frontend/base/static/img/country_flags/wf.png create mode 100644 frontend/base/static/img/country_flags/ws.png create mode 100644 frontend/base/static/img/country_flags/xk.png create mode 100644 frontend/base/static/img/country_flags/ye.png create mode 100644 frontend/base/static/img/country_flags/za.png create mode 100644 frontend/base/static/img/country_flags/zm.png create mode 100644 frontend/base/static/img/country_flags/zw.png create mode 100644 frontend/base/static/img/demo_logo_report.png create mode 100644 frontend/base/static/img/icons/account_accountant.png create mode 100644 frontend/base/static/img/icons/appointment.png create mode 100644 frontend/base/static/img/icons/helpdesk.png create mode 100644 frontend/base/static/img/icons/hr_appraisal.png create mode 100644 frontend/base/static/img/icons/industry_fsm.png create mode 100644 frontend/base/static/img/icons/knowledge.png create mode 100644 frontend/base/static/img/icons/marketing_automation.png create mode 100644 frontend/base/static/img/icons/mrp_plm.png create mode 100644 frontend/base/static/img/icons/mrp_workorder.png create mode 100644 frontend/base/static/img/icons/payment_sepa_direct_debit.png create mode 100644 frontend/base/static/img/icons/planning.png create mode 100644 frontend/base/static/img/icons/quality_control.png create mode 100644 frontend/base/static/img/icons/sale_amazon.png create mode 100644 frontend/base/static/img/icons/sale_ebay.png create mode 100644 frontend/base/static/img/icons/sale_subscription.png create mode 100644 frontend/base/static/img/icons/sign.png create mode 100644 frontend/base/static/img/icons/social.png create mode 100644 frontend/base/static/img/icons/stock_barcode.png create mode 100644 frontend/base/static/img/icons/timesheet_grid.png create mode 100644 frontend/base/static/img/icons/voip.png create mode 100644 frontend/base/static/img/icons/web_mobile.png create mode 100644 frontend/base/static/img/icons/web_studio.png create mode 100644 frontend/base/static/img/icons/website_form_editor.png create mode 100644 frontend/base/static/img/icons/website_version.png create mode 100644 frontend/base/static/img/lang_flags/lang_ar.png create mode 100644 frontend/base/static/img/lang_flags/lang_ca.png create mode 100644 frontend/base/static/img/logo_sample.png create mode 100644 frontend/base/static/img/logo_white.png create mode 100644 frontend/base/static/img/main_partner-image.png create mode 100644 frontend/base/static/img/money.png create mode 100644 frontend/base/static/img/onboarding_accounting-periods.png create mode 100644 frontend/base/static/img/onboarding_calendar.png create mode 100644 frontend/base/static/img/onboarding_chart-of-accounts.png create mode 100644 frontend/base/static/img/onboarding_cog.png create mode 100644 frontend/base/static/img/onboarding_company-data.png create mode 100644 frontend/base/static/img/onboarding_confetti.svg create mode 100644 frontend/base/static/img/onboarding_default.png create mode 100644 frontend/base/static/img/onboarding_looking_glass.png create mode 100644 frontend/base/static/img/onboarding_puzzle.png create mode 100644 frontend/base/static/img/onboarding_quotation-layout.png create mode 100644 frontend/base/static/img/onboarding_sample-quotation.png create mode 100644 frontend/base/static/img/onboarding_taxes.png create mode 100644 frontend/base/static/img/partner_demo_portal.png create mode 100644 frontend/base/static/img/partner_lightsup.png create mode 100644 frontend/base/static/img/partner_open_wood.png create mode 100644 frontend/base/static/img/partner_root-image.png create mode 100644 frontend/base/static/img/public_user-image.png create mode 100644 frontend/base/static/img/puzzle.png create mode 100644 frontend/base/static/img/res_company_logo.png create mode 100644 frontend/base/static/img/res_partner_1-image.png create mode 100644 frontend/base/static/img/res_partner_10-image.jpg create mode 100644 frontend/base/static/img/res_partner_12-image.png create mode 100644 frontend/base/static/img/res_partner_18-image.png create mode 100644 frontend/base/static/img/res_partner_2-image.png create mode 100644 frontend/base/static/img/res_partner_3-image.png create mode 100644 frontend/base/static/img/res_partner_4-image.png create mode 100644 frontend/base/static/img/res_partner_address_1.jpg create mode 100644 frontend/base/static/img/res_partner_address_10.jpg create mode 100644 frontend/base/static/img/res_partner_address_11.jpg create mode 100644 frontend/base/static/img/res_partner_address_13.jpg create mode 100644 frontend/base/static/img/res_partner_address_14.jpg create mode 100644 frontend/base/static/img/res_partner_address_15.jpg create mode 100644 frontend/base/static/img/res_partner_address_16.jpg create mode 100644 frontend/base/static/img/res_partner_address_17.jpg create mode 100644 frontend/base/static/img/res_partner_address_18.jpg create mode 100644 frontend/base/static/img/res_partner_address_2.jpg create mode 100644 frontend/base/static/img/res_partner_address_24.jpg create mode 100644 frontend/base/static/img/res_partner_address_25.jpg create mode 100644 frontend/base/static/img/res_partner_address_27.jpg create mode 100644 frontend/base/static/img/res_partner_address_28.jpg create mode 100644 frontend/base/static/img/res_partner_address_3.jpg create mode 100644 frontend/base/static/img/res_partner_address_30.jpg create mode 100644 frontend/base/static/img/res_partner_address_31.jpg create mode 100644 frontend/base/static/img/res_partner_address_32.jpg create mode 100644 frontend/base/static/img/res_partner_address_33.jpg create mode 100644 frontend/base/static/img/res_partner_address_34.jpg create mode 100644 frontend/base/static/img/res_partner_address_4.jpg create mode 100644 frontend/base/static/img/res_partner_address_5.jpg create mode 100644 frontend/base/static/img/res_partner_address_7.jpg create mode 100644 frontend/base/static/img/res_partner_main1.jpg create mode 100644 frontend/base/static/img/res_partner_main2.jpg create mode 100644 frontend/base/static/img/truck.png create mode 100644 frontend/base/static/img/user-slash.png create mode 100644 frontend/base/static/img/user_demo-image.png create mode 100644 frontend/base/static/src/css/description.css create mode 100644 frontend/base/static/src/css/description.sass create mode 100644 frontend/base/static/src/css/modules.css create mode 100644 frontend/base/static/src/scss/res_partner.scss create mode 100644 frontend/base/static/src/scss/res_users.scss create mode 100644 frontend/base/static/tests/test_ir_model_fields_translation.js create mode 100644 frontend/base/static/xls/contacts_import_template.xlsx create mode 100644 frontend/crm/static/description/icon.png create mode 100644 frontend/crm/static/description/icon.svg create mode 100644 frontend/crm/static/description/icon_hi.png create mode 100644 frontend/crm/static/src/activity_menu_patch.js create mode 100644 frontend/crm/static/src/core/common/crm_lead_model.js create mode 100644 frontend/crm/static/src/core/common/res_partner_model_patch.js create mode 100644 frontend/crm/static/src/img/milk-autofill.gif create mode 100644 frontend/crm/static/src/img/milk-generate-leads.gif create mode 100644 frontend/crm/static/src/img/milk-mapview-toggle.gif create mode 100644 frontend/crm/static/src/img/milk-pipeline-progress.gif create mode 100644 frontend/crm/static/src/img/milk-probability-rate.gif create mode 100644 frontend/crm/static/src/img/pls-tooltip-ai-icon.png create mode 100644 frontend/crm/static/src/js/fields/many2one_avatar_leader_user.js create mode 100644 frontend/crm/static/src/js/tours/crm.js create mode 100644 frontend/crm/static/src/scss/crm.scss create mode 100644 frontend/crm/static/src/scss/crm_team.scss create mode 100644 frontend/crm/static/src/scss/crm_team_member_views.scss create mode 100644 frontend/crm/static/src/views/check_rainbowman_message.js create mode 100644 frontend/crm/static/src/views/crm_form/crm_form.js create mode 100644 frontend/crm/static/src/views/crm_form/crm_form.scss create mode 100644 frontend/crm/static/src/views/crm_form/crm_pls_tooltip_button.js create mode 100644 frontend/crm/static/src/views/crm_form/crm_pls_tooltip_button.scss create mode 100644 frontend/crm/static/src/views/crm_form/crm_pls_tooltip_button.xml create mode 100644 frontend/crm/static/src/views/crm_kanban/crm_column_progress.js create mode 100644 frontend/crm/static/src/views/crm_kanban/crm_column_progress.xml create mode 100644 frontend/crm/static/src/views/crm_kanban/crm_kanban_arch_parser.js create mode 100644 frontend/crm/static/src/views/crm_kanban/crm_kanban_model.js create mode 100644 frontend/crm/static/src/views/crm_kanban/crm_kanban_renderer.js create mode 100644 frontend/crm/static/src/views/crm_kanban/crm_kanban_view.js create mode 100644 frontend/crm/static/src/views/fill_temporal_service.js create mode 100644 frontend/crm/static/src/views/forecast_graph/forecast_graph_view.js create mode 100644 frontend/crm/static/src/views/forecast_kanban/forecast_kanban_column_quick_create.js create mode 100644 frontend/crm/static/src/views/forecast_kanban/forecast_kanban_controller.js create mode 100644 frontend/crm/static/src/views/forecast_kanban/forecast_kanban_model.js create mode 100644 frontend/crm/static/src/views/forecast_kanban/forecast_kanban_renderer.js create mode 100644 frontend/crm/static/src/views/forecast_kanban/forecast_kanban_renderer.xml create mode 100644 frontend/crm/static/src/views/forecast_kanban/forecast_kanban_view.js create mode 100644 frontend/crm/static/src/views/forecast_list/forecast_list_view.js create mode 100644 frontend/crm/static/src/views/forecast_pivot/forecast_pivot_view.js create mode 100644 frontend/crm/static/src/views/forecast_search_model.js create mode 100644 frontend/crm/static/tests/crm_kanban_progress_bar_mrr_sum_field.test.js create mode 100644 frontend/crm/static/tests/crm_mock_server.js create mode 100644 frontend/crm/static/tests/crm_rainbowman.test.js create mode 100644 frontend/crm/static/tests/crm_test_helpers.js create mode 100644 frontend/crm/static/tests/forecast_kanban.test.js create mode 100644 frontend/crm/static/tests/forecast_view.test.js create mode 100644 frontend/crm/static/tests/mock_server/mock_models/crm_lead.js create mode 100644 frontend/crm/static/tests/tours/create_crm_team_tour.js create mode 100644 frontend/crm/static/tests/tours/crm_email_and_phone_propagation.js create mode 100644 frontend/crm/static/tests/tours/crm_forecast_tour.js create mode 100644 frontend/crm/static/tests/tours/crm_rainbowman.js create mode 100644 frontend/crm/static/xls/crm_lead.xls create mode 100644 frontend/fleet/static/description/icon.png create mode 100644 frontend/fleet/static/description/icon.svg create mode 100644 frontend/fleet/static/description/icon_hi.png create mode 100644 frontend/fleet/static/img/brand_abarth-image.png create mode 100644 frontend/fleet/static/img/brand_acura-image.png create mode 100644 frontend/fleet/static/img/brand_alfa-image.png create mode 100644 frontend/fleet/static/img/brand_audi-image.png create mode 100644 frontend/fleet/static/img/brand_austin-image.png create mode 100644 frontend/fleet/static/img/brand_bentley-image.png create mode 100644 frontend/fleet/static/img/brand_bmw-image.png create mode 100644 frontend/fleet/static/img/brand_bugatti-image.png create mode 100644 frontend/fleet/static/img/brand_buick-image.png create mode 100644 frontend/fleet/static/img/brand_byd-image.png create mode 100644 frontend/fleet/static/img/brand_cadillac-image.png create mode 100644 frontend/fleet/static/img/brand_chevrolet-image.png create mode 100644 frontend/fleet/static/img/brand_chrysler-image.png create mode 100644 frontend/fleet/static/img/brand_citroen-image.png create mode 100644 frontend/fleet/static/img/brand_corre-la-licorne-image.png create mode 100644 frontend/fleet/static/img/brand_daewoo-image.png create mode 100644 frontend/fleet/static/img/brand_dodge-image.png create mode 100644 frontend/fleet/static/img/brand_ferrari-image.png create mode 100644 frontend/fleet/static/img/brand_fiat-image.png create mode 100644 frontend/fleet/static/img/brand_ford-image.png create mode 100644 frontend/fleet/static/img/brand_gmc-image.png create mode 100644 frontend/fleet/static/img/brand_holden-image.png create mode 100644 frontend/fleet/static/img/brand_honda-image.png create mode 100644 frontend/fleet/static/img/brand_hyundai-image.png create mode 100644 frontend/fleet/static/img/brand_infiniti-image.png create mode 100644 frontend/fleet/static/img/brand_isuzu-image.png create mode 100644 frontend/fleet/static/img/brand_jaguar-image.png create mode 100644 frontend/fleet/static/img/brand_jeep-image.png create mode 100644 frontend/fleet/static/img/brand_kia-image.png create mode 100644 frontend/fleet/static/img/brand_koenigsegg-image.png create mode 100644 frontend/fleet/static/img/brand_lagonda-image.png create mode 100644 frontend/fleet/static/img/brand_lamborghini-image.png create mode 100644 frontend/fleet/static/img/brand_lancia-image.png create mode 100644 frontend/fleet/static/img/brand_land-rover-image.png create mode 100644 frontend/fleet/static/img/brand_lexus-image.png create mode 100644 frontend/fleet/static/img/brand_lincoln-image.png create mode 100644 frontend/fleet/static/img/brand_lotus-image.png create mode 100644 frontend/fleet/static/img/brand_maserati-image.png create mode 100644 frontend/fleet/static/img/brand_maybach-image.png create mode 100644 frontend/fleet/static/img/brand_mazda-image.png create mode 100644 frontend/fleet/static/img/brand_mercedes-image.png create mode 100644 frontend/fleet/static/img/brand_mg-image.png create mode 100644 frontend/fleet/static/img/brand_mini-image.png create mode 100644 frontend/fleet/static/img/brand_mitsubishi-image.png create mode 100644 frontend/fleet/static/img/brand_morgan-image.png create mode 100644 frontend/fleet/static/img/brand_nissan-image.png create mode 100644 frontend/fleet/static/img/brand_oldsmobile-image.png create mode 100644 frontend/fleet/static/img/brand_opel-image.png create mode 100644 frontend/fleet/static/img/brand_peugeot-image.png create mode 100644 frontend/fleet/static/img/brand_pontiac-image.png create mode 100644 frontend/fleet/static/img/brand_porsche-image.png create mode 100644 frontend/fleet/static/img/brand_rambler-image.png create mode 100644 frontend/fleet/static/img/brand_renault-image.png create mode 100644 frontend/fleet/static/img/brand_rolls-royce-image.png create mode 100644 frontend/fleet/static/img/brand_saab-image.png create mode 100644 frontend/fleet/static/img/brand_scion-image.png create mode 100644 frontend/fleet/static/img/brand_skoda-image.png create mode 100644 frontend/fleet/static/img/brand_smart-image.png create mode 100644 frontend/fleet/static/img/brand_steyr-image.png create mode 100644 frontend/fleet/static/img/brand_subaru-image.png create mode 100644 frontend/fleet/static/img/brand_suzuki-image.png create mode 100644 frontend/fleet/static/img/brand_tesla-motors-image.png create mode 100644 frontend/fleet/static/img/brand_toyota-image.png create mode 100644 frontend/fleet/static/img/brand_trabant-image.png create mode 100644 frontend/fleet/static/img/brand_volkswagen-image.png create mode 100644 frontend/fleet/static/img/brand_volvo-image.png create mode 100644 frontend/fleet/static/img/brand_willys-image.png create mode 100644 frontend/fleet/static/src/js/fleet_form.js create mode 100644 frontend/fleet/static/src/scss/fleet_form.scss create mode 100644 frontend/hr/static/description/icon.png create mode 100644 frontend/hr/static/description/icon.svg create mode 100644 frontend/hr/static/description/icon_hi.png create mode 100644 frontend/hr/static/img/employee-image.png create mode 100644 frontend/hr/static/img/employee_al-image.jpg create mode 100644 frontend/hr/static/img/employee_awa-image.jpg create mode 100644 frontend/hr/static/img/employee_chs-image.jpg create mode 100644 frontend/hr/static/img/employee_fme-image.jpg create mode 100644 frontend/hr/static/img/employee_fpi-image.jpg create mode 100644 frontend/hr/static/img/employee_han-image.jpg create mode 100644 frontend/hr/static/img/employee_hne-image.jpg create mode 100644 frontend/hr/static/img/employee_jep-image.jpg create mode 100644 frontend/hr/static/img/employee_jgo-image.jpg create mode 100644 frontend/hr/static/img/employee_jod-image.jpg create mode 100644 frontend/hr/static/img/employee_jog-image.jpg create mode 100644 frontend/hr/static/img/employee_jth-image.jpg create mode 100644 frontend/hr/static/img/employee_jve-image.jpg create mode 100644 frontend/hr/static/img/employee_lur-image.jpg create mode 100644 frontend/hr/static/img/employee_mit-image.jpg create mode 100644 frontend/hr/static/img/employee_ngh-image.jpg create mode 100644 frontend/hr/static/img/employee_niv-image.jpg create mode 100644 frontend/hr/static/img/employee_qdp-image.png create mode 100644 frontend/hr/static/img/employee_stw-image.jpg create mode 100644 frontend/hr/static/img/employee_vad-image.jpg create mode 100644 frontend/hr/static/img/partner_root-image.jpg create mode 100644 frontend/hr/static/src/@types/models.d.ts create mode 100644 frontend/hr/static/src/components/avatar_card/avatar_card_popover_patch.js create mode 100644 frontend/hr/static/src/components/avatar_card/avatar_card_popover_patch.xml create mode 100644 frontend/hr/static/src/components/avatar_card_employee/avatar_card_employee_popover.js create mode 100644 frontend/hr/static/src/components/avatar_card_resource/avatar_card_resource_popover.xml create mode 100644 frontend/hr/static/src/components/avatar_card_resource/avatar_card_resource_popover_patch.js create mode 100644 frontend/hr/static/src/components/avatar_employee/avatar_employee.js create mode 100644 frontend/hr/static/src/components/background_image/background_image.js create mode 100644 frontend/hr/static/src/components/background_image/background_image.scss create mode 100644 frontend/hr/static/src/components/background_image/background_image.xml create mode 100644 frontend/hr/static/src/components/button_new_contract/button_new_contract.js create mode 100644 frontend/hr/static/src/components/button_new_contract/button_new_contract.xml create mode 100644 frontend/hr/static/src/components/department_chart/department_chart.js create mode 100644 frontend/hr/static/src/components/department_chart/department_chart.scss create mode 100644 frontend/hr/static/src/components/department_chart/department_chart.xml create mode 100644 frontend/hr/static/src/components/employee_chat/employee_chat.js create mode 100644 frontend/hr/static/src/components/employee_chat/employee_chat.xml create mode 100644 frontend/hr/static/src/components/float_without_trailing_zeros/float_without_trailing_zeros.js create mode 100644 frontend/hr/static/src/components/hr_presence_status/hr_presence_status.js create mode 100644 frontend/hr/static/src/components/hr_presence_status/hr_presence_status.xml create mode 100644 frontend/hr/static/src/components/hr_presence_status_pill/hr_presence_status_pill.js create mode 100644 frontend/hr/static/src/components/hr_presence_status_pill/hr_presence_status_pill.xml create mode 100644 frontend/hr/static/src/components/hr_presence_status_private/hr_presence_status_private.js create mode 100644 frontend/hr/static/src/components/hr_presence_status_private_pill/hr_presence_status_private_pill.js create mode 100644 frontend/hr/static/src/components/many2many_tags_salary_bank/many2many_tags_salary_bank.js create mode 100644 frontend/hr/static/src/components/radio_image_field/radio_image_field.js create mode 100644 frontend/hr/static/src/components/radio_image_field/radio_image_field.xml create mode 100644 frontend/hr/static/src/components/versions_timeline/versions_timeline.js create mode 100644 frontend/hr/static/src/components/versions_timeline/versions_timeline.scss create mode 100644 frontend/hr/static/src/components/versions_timeline/versions_timeline.xml create mode 100644 frontend/hr/static/src/components/work_permit_upload/work_permit_upload.js create mode 100644 frontend/hr/static/src/components/work_permit_upload/work_permit_upload.xml create mode 100644 frontend/hr/static/src/core/common/@types/models.d.ts create mode 100644 frontend/hr/static/src/core/common/hr_department_model.js create mode 100644 frontend/hr/static/src/core/common/hr_employee_model.js create mode 100644 frontend/hr/static/src/core/common/hr_work_location_model.js create mode 100644 frontend/hr/static/src/core/common/res_partner_model_patch.js create mode 100644 frontend/hr/static/src/core/common/res_users_model_patch.js create mode 100644 frontend/hr/static/src/core/web/thread_actions.js create mode 100644 frontend/hr/static/src/default_image.png create mode 100644 frontend/hr/static/src/fields/boolean_radio.js create mode 100644 frontend/hr/static/src/fields/boolean_radio.xml create mode 100644 frontend/hr/static/src/fields/radio_followed_by_element.js create mode 100644 frontend/hr/static/src/img/default_image.png create mode 100644 frontend/hr/static/src/img/icons/hatched.svg create mode 100644 frontend/hr/static/src/img/icons/hatched_dark.svg create mode 100644 frontend/hr/static/src/img/icons/line.svg create mode 100644 frontend/hr/static/src/img/icons/line_dark.svg create mode 100644 frontend/hr/static/src/img/icons/plain.svg create mode 100644 frontend/hr/static/src/img/icons/plain_dark.svg create mode 100644 frontend/hr/static/src/scss/hr.scss create mode 100644 frontend/hr/static/src/scss/res_config_settings.scss create mode 100644 frontend/hr/static/src/scss/res_users.scss create mode 100644 frontend/hr/static/src/store_service_patch.js create mode 100644 frontend/hr/static/src/views/archive_employee_hook.js create mode 100644 frontend/hr/static/src/views/fields/employee_field_relation_mixin.js create mode 100644 frontend/hr/static/src/views/fields/many2many_avatar_employee_field/many2many_avatar_employee_field.js create mode 100644 frontend/hr/static/src/views/fields/many2one_avatar_employee_field/kanban_many2one_avatar_employee_field.js create mode 100644 frontend/hr/static/src/views/fields/many2one_avatar_employee_field/kanban_many2one_avatar_employee_field.xml create mode 100644 frontend/hr/static/src/views/fields/many2one_avatar_employee_field/many2one_avatar_employee_field.js create mode 100644 frontend/hr/static/src/views/fields/many2one_avatar_employee_field/many2one_avatar_employee_field.xml create mode 100644 frontend/hr/static/src/views/form_view.js create mode 100644 frontend/hr/static/src/views/kanban_view.js create mode 100644 frontend/hr/static/src/views/list_view.js create mode 100644 frontend/hr/static/src/views/open_chat_hook.js create mode 100644 frontend/hr/static/src/views/preferences_form_view.js create mode 100644 frontend/hr/static/tests/hr_test_helpers.js create mode 100644 frontend/hr/static/tests/legacy/disable_patch.js create mode 100644 frontend/hr/static/tests/m2x_avatar_employee.test.js create mode 100644 frontend/hr/static/tests/mock_server/mock_models/fake_user.js create mode 100644 frontend/hr/static/tests/mock_server/mock_models/hr_department.js create mode 100644 frontend/hr/static/tests/mock_server/mock_models/hr_employee.js create mode 100644 frontend/hr/static/tests/mock_server/mock_models/hr_employee_public.js create mode 100644 frontend/hr/static/tests/mock_server/mock_models/hr_job.js create mode 100644 frontend/hr/static/tests/mock_server/mock_models/hr_version.js create mode 100644 frontend/hr/static/tests/mock_server/mock_models/hr_work_location.js create mode 100644 frontend/hr/static/tests/mock_server/mock_models/m2x_avatar_employee.js create mode 100644 frontend/hr/static/tests/mock_server/mock_models/res_partner.js create mode 100644 frontend/hr/static/tests/mock_server/mock_models/res_users.js create mode 100644 frontend/hr/static/tests/mock_server/mock_server.js create mode 100644 frontend/hr/static/tests/profile_form_view.test.js create mode 100644 frontend/hr/static/tests/tours/check_public_employee_link_redirect.js create mode 100644 frontend/hr/static/tests/tours/hr_employee_flow.js create mode 100644 frontend/hr/static/tests/tours/hr_employee_multiple_bank_accounts.js create mode 100644 frontend/hr/static/tests/tours/version_timeline_auto_save_tour.js create mode 100644 frontend/hr/static/tests/web/m2x_avatar_user.test.js create mode 100644 frontend/hr/static/xls/hr_employee.xls create mode 100644 frontend/odoo/base/static/src/css/modules.css create mode 100644 frontend/odoo/base/static/src/scss/res_partner.scss create mode 100644 frontend/odoo/base/static/src/scss/res_users.scss create mode 100644 frontend/product/static/demo/acoustic_bloc_screen_document.pdf create mode 100644 frontend/product/static/demo/customizable_desk_document.pdf create mode 100644 frontend/product/static/description/icon.png create mode 100644 frontend/product/static/description/icon.svg create mode 100644 frontend/product/static/description/icon_hi.png create mode 100644 frontend/product/static/img/desk_organizer.jpg create mode 100644 frontend/product/static/img/desk_pad.jpg create mode 100644 frontend/product/static/img/dining_table.png create mode 100644 frontend/product/static/img/glass.png create mode 100644 frontend/product/static/img/leather.png create mode 100644 frontend/product/static/img/linen.png create mode 100644 frontend/product/static/img/maroon.png create mode 100644 frontend/product/static/img/membership_0-image.jpg create mode 100644 frontend/product/static/img/membership_1-image.jpg create mode 100644 frontend/product/static/img/membership_2-image.jpg create mode 100644 frontend/product/static/img/metal.png create mode 100644 frontend/product/static/img/monitor_stand.jpg create mode 100644 frontend/product/static/img/office_combo.jpg create mode 100644 frontend/product/static/img/placeholder.png create mode 100644 frontend/product/static/img/placeholder_thumbnail.png create mode 100644 frontend/product/static/img/product_chair.jpg create mode 100644 frontend/product/static/img/product_lamp.png create mode 100644 frontend/product/static/img/product_product_10-image.jpg create mode 100644 frontend/product/static/img/product_product_11-image.jpg create mode 100644 frontend/product/static/img/product_product_11b-image.jpg create mode 100644 frontend/product/static/img/product_product_12-image.jpg create mode 100644 frontend/product/static/img/product_product_13-image.jpg create mode 100644 frontend/product/static/img/product_product_16-image.jpg create mode 100644 frontend/product/static/img/product_product_20-image.png create mode 100644 frontend/product/static/img/product_product_22-image.png create mode 100644 frontend/product/static/img/product_product_24-image.jpg create mode 100644 frontend/product/static/img/product_product_25-image.jpg create mode 100644 frontend/product/static/img/product_product_25_black-image.jpg create mode 100644 frontend/product/static/img/product_product_27-image.jpg create mode 100644 frontend/product/static/img/product_product_3-image.jpg create mode 100644 frontend/product/static/img/product_product_43-image.jpg create mode 100644 frontend/product/static/img/product_product_46-image.jpg create mode 100644 frontend/product/static/img/product_product_5-image.jpg create mode 100644 frontend/product/static/img/product_product_6-image.jpg create mode 100644 frontend/product/static/img/product_product_7-image.png create mode 100644 frontend/product/static/img/product_product_8-image.jpg create mode 100644 frontend/product/static/img/product_product_8_glass-image.jpg create mode 100644 frontend/product/static/img/product_product_8_metal-image.jpg create mode 100644 frontend/product/static/img/product_product_9-image.jpg create mode 100644 frontend/product/static/img/product_product_d01-image.jpg create mode 100644 frontend/product/static/img/product_product_d01b-image.jpg create mode 100644 frontend/product/static/img/product_product_d01c-image.jpg create mode 100644 frontend/product/static/img/product_product_d03-image.png create mode 100644 frontend/product/static/img/purple.png create mode 100644 frontend/product/static/img/table02.jpg create mode 100644 frontend/product/static/img/table03.jpg create mode 100644 frontend/product/static/img/table04.jpg create mode 100644 frontend/product/static/img/velvet.png create mode 100644 frontend/product/static/img/wood.png create mode 100644 frontend/product/static/src/js/pricelist_report/product_pricelist_report.js create mode 100644 frontend/product/static/src/js/pricelist_report/product_pricelist_report.xml create mode 100644 frontend/product/static/src/js/product_attribute_value_list.js create mode 100644 frontend/product/static/src/js/product_document_kanban/product_document_kanban_controller.js create mode 100644 frontend/product/static/src/js/product_document_kanban/product_document_kanban_controller.xml create mode 100644 frontend/product/static/src/js/product_document_kanban/product_document_kanban_record.js create mode 100644 frontend/product/static/src/js/product_document_kanban/product_document_kanban_renderer.js create mode 100644 frontend/product/static/src/js/product_document_kanban/product_document_kanban_renderer.xml create mode 100644 frontend/product/static/src/js/product_document_kanban/product_document_kanban_view.js create mode 100644 frontend/product/static/src/js/product_document_kanban/product_document_kanban_view.scss create mode 100644 frontend/product/static/src/js/product_document_kanban/upload_button/upload_button.js create mode 100644 frontend/product/static/src/js/product_document_kanban/upload_button/upload_button.xml create mode 100644 frontend/product/static/src/product_catalog/kanban_controller.js create mode 100644 frontend/product/static/src/product_catalog/kanban_controller.xml create mode 100644 frontend/product/static/src/product_catalog/kanban_model.js create mode 100644 frontend/product/static/src/product_catalog/kanban_record.js create mode 100644 frontend/product/static/src/product_catalog/kanban_record.xml create mode 100644 frontend/product/static/src/product_catalog/kanban_renderer.js create mode 100644 frontend/product/static/src/product_catalog/kanban_renderer.xml create mode 100644 frontend/product/static/src/product_catalog/kanban_view.js create mode 100644 frontend/product/static/src/product_catalog/order_line/order_line.js create mode 100644 frontend/product/static/src/product_catalog/order_line/order_line.scss create mode 100644 frontend/product/static/src/product_catalog/order_line/order_line.xml create mode 100644 frontend/product/static/src/product_name_and_description/product_and_label_autoresize.js create mode 100644 frontend/product/static/src/product_name_and_description/product_name_and_description.js create mode 100644 frontend/product/static/src/scss/product_form.scss create mode 100644 frontend/product/static/src/scss/report_label_sheet.scss create mode 100644 frontend/product/static/tests/mock_server/mock_models/product_combo.js create mode 100644 frontend/product/static/tests/mock_server/mock_models/product_product.js create mode 100644 frontend/product/static/tests/mock_server/mock_models/product_template.js create mode 100644 frontend/product/static/tests/product_combo_test_helpers.js create mode 100644 frontend/product/static/tests/product_pricelist_report.test.js create mode 100644 frontend/product/static/tests/product_test_helpers.js create mode 100644 frontend/product/static/xls/product_pricelist.xls create mode 100644 frontend/product/static/xls/product_supplierinfo.xls create mode 100644 frontend/product/static/xls/product_template.xls create mode 100644 frontend/project/static/description/icon.png create mode 100644 frontend/project/static/description/icon.svg create mode 100644 frontend/project/static/description/icon_hi.png create mode 100644 frontend/project/static/src/actions/client_actions.js create mode 100644 frontend/project/static/src/components/done_checkmark/task_done_checkmark.js create mode 100644 frontend/project/static/src/components/done_checkmark/task_done_checkmark.scss create mode 100644 frontend/project/static/src/components/done_checkmark/task_done_checkmark.xml create mode 100644 frontend/project/static/src/components/notebook_task_one2many_field/notebook_task_list_renderer.js create mode 100644 frontend/project/static/src/components/notebook_task_one2many_field/notebook_task_list_renderer.xml create mode 100644 frontend/project/static/src/components/notebook_task_one2many_field/notebook_task_one2many_field.js create mode 100644 frontend/project/static/src/components/project_is_favorite/project_is_favorite_field.js create mode 100644 frontend/project/static/src/components/project_many2one_field/project_many2one_field.js create mode 100644 frontend/project/static/src/components/project_many2one_field/project_many2one_field.scss create mode 100644 frontend/project/static/src/components/project_many2one_field/project_many2one_field.xml create mode 100644 frontend/project/static/src/components/project_right_side_panel/components/project_milestone.js create mode 100644 frontend/project/static/src/components/project_right_side_panel/components/project_milestone.xml create mode 100644 frontend/project/static/src/components/project_right_side_panel/components/project_profitability.js create mode 100644 frontend/project/static/src/components/project_right_side_panel/components/project_profitability.xml create mode 100644 frontend/project/static/src/components/project_right_side_panel/components/project_right_side_panel_section.js create mode 100644 frontend/project/static/src/components/project_right_side_panel/components/project_right_side_panel_section.xml create mode 100644 frontend/project/static/src/components/project_right_side_panel/project_right_side_panel.dark.scss create mode 100644 frontend/project/static/src/components/project_right_side_panel/project_right_side_panel.js create mode 100644 frontend/project/static/src/components/project_right_side_panel/project_right_side_panel.scss create mode 100644 frontend/project/static/src/components/project_right_side_panel/project_right_side_panel.xml create mode 100644 frontend/project/static/src/components/project_state_selection/project_state_selection.js create mode 100644 frontend/project/static/src/components/project_state_selection/project_state_selection.scss create mode 100644 frontend/project/static/src/components/project_status_with_color_selection/project_status_with_color_selection_field.js create mode 100644 frontend/project/static/src/components/project_status_with_color_selection/project_status_with_color_selection_field.xml create mode 100644 frontend/project/static/src/components/project_task_name_with_subtask_count_char_field/project_task_name_with_subtask_count_char_field.js create mode 100644 frontend/project/static/src/components/project_task_name_with_subtask_count_char_field/project_task_name_with_subtask_count_char_field.xml create mode 100644 frontend/project/static/src/components/project_task_priority_switch_field/project_task_priority_switch_field.js create mode 100644 frontend/project/static/src/components/project_task_state_selection/project_task_stage_state_selection/project_task_stage_with_state_selection.js create mode 100644 frontend/project/static/src/components/project_task_state_selection/project_task_stage_state_selection/project_task_stage_with_state_selection.xml create mode 100644 frontend/project/static/src/components/project_task_state_selection/project_task_state_selection.js create mode 100644 frontend/project/static/src/components/project_task_state_selection/project_task_state_selection.scss create mode 100644 frontend/project/static/src/components/project_task_state_selection/project_task_state_selection.xml create mode 100644 frontend/project/static/src/components/subtask_kanban_list/subtask_kanban_create/subtask_kanban_create.js create mode 100644 frontend/project/static/src/components/subtask_kanban_list/subtask_kanban_create/subtask_kanban_create.xml create mode 100644 frontend/project/static/src/components/subtask_kanban_list/subtask_kanban_list.js create mode 100644 frontend/project/static/src/components/subtask_kanban_list/subtask_kanban_list.scss create mode 100644 frontend/project/static/src/components/subtask_kanban_list/subtask_kanban_list.xml create mode 100644 frontend/project/static/src/components/subtask_one2many_field/subtask_list_renderer.js create mode 100644 frontend/project/static/src/components/subtask_one2many_field/subtask_one2many_field.js create mode 100644 frontend/project/static/src/components/task_list_renderer.js create mode 100644 frontend/project/static/src/core/web/@types/models.d.ts create mode 100644 frontend/project/static/src/core/web/follower_list_patch.js create mode 100644 frontend/project/static/src/core/web/thread_model_patch.js create mode 100644 frontend/project/static/src/css/project.css create mode 100644 frontend/project/static/src/img/app_store.png create mode 100644 frontend/project/static/src/img/bird.jpg create mode 100644 frontend/project/static/src/img/chrome_store.png create mode 100644 frontend/project/static/src/img/planner_icon.png create mode 100644 frontend/project/static/src/img/play_store.png create mode 100644 frontend/project/static/src/img/task-state-img.png create mode 100644 frontend/project/static/src/img/tasks.svg create mode 100644 frontend/project/static/src/img/tasks_icon.png create mode 100644 frontend/project/static/src/img/top_left_arrow.png create mode 100644 frontend/project/static/src/img/web_planner_email.png create mode 100644 frontend/project/static/src/img/web_planner_project.png create mode 100644 frontend/project/static/src/img/web_planner_subtype.png create mode 100644 frontend/project/static/src/interactions/project_rating_image.js create mode 100644 frontend/project/static/src/js/tours/project.js create mode 100644 frontend/project/static/src/project_sharing/chatter/chatter_patch.js create mode 100644 frontend/project/static/src/project_sharing/chatter/composer_actions_patch.js create mode 100644 frontend/project/static/src/project_sharing/chatter/composer_patch.js create mode 100644 frontend/project/static/src/project_sharing/chatter/message_patch.js create mode 100644 frontend/project/static/src/project_sharing/chatter/portal_chatter_patch.xml create mode 100644 frontend/project/static/src/project_sharing/chatter/suggestion_service_patch.js create mode 100644 frontend/project/static/src/project_sharing/components/depend_on_ids_one2many/depend_on_ids_list_renderer.js create mode 100644 frontend/project/static/src/project_sharing/components/depend_on_ids_one2many/depend_on_ids_list_renderer.xml create mode 100644 frontend/project/static/src/project_sharing/components/depend_on_ids_one2many/depend_on_ids_one2many_field.js create mode 100644 frontend/project/static/src/project_sharing/editor/project_sharing_media_plugin.js create mode 100644 frontend/project/static/src/project_sharing/main.js create mode 100644 frontend/project/static/src/project_sharing/project_sharing.js create mode 100644 frontend/project/static/src/project_sharing/project_sharing.scss create mode 100644 frontend/project/static/src/project_sharing/project_sharing.xml create mode 100644 frontend/project/static/src/project_sharing/router.js create mode 100644 frontend/project/static/src/project_sharing/search/control_panel/control_panel.xml create mode 100644 frontend/project/static/src/project_sharing/views/form/project_sharing_form_compiler.js create mode 100644 frontend/project/static/src/project_sharing/views/form/project_sharing_form_controller.js create mode 100644 frontend/project/static/src/project_sharing/views/form/project_sharing_form_renderer.js create mode 100644 frontend/project/static/src/project_sharing/views/form/project_sharing_form_view.js create mode 100644 frontend/project/static/src/project_sharing/views/kanban/kanban_view.js create mode 100644 frontend/project/static/src/project_sharing/views/list/list_view.js create mode 100644 frontend/project/static/src/project_sharing/views/view.js create mode 100644 frontend/project/static/src/scss/portal_rating.scss create mode 100644 frontend/project/static/src/scss/project_dashboard.scss create mode 100644 frontend/project/static/src/scss/project_form.scss create mode 100644 frontend/project/static/src/scss/project_widgets.scss create mode 100644 frontend/project/static/src/utils/project_utils.js create mode 100644 frontend/project/static/src/views/analytic_account_form/analytic_account_form_controller.js create mode 100644 frontend/project/static/src/views/analytic_account_form/analytic_account_form_view.js create mode 100644 frontend/project/static/src/views/analytic_account_list/analytic_account_list_controller.js create mode 100644 frontend/project/static/src/views/analytic_account_list/analytic_account_list_view.js create mode 100644 frontend/project/static/src/views/burndown_chart/burndown_chart_model.js create mode 100644 frontend/project/static/src/views/burndown_chart/burndown_chart_search_model.js create mode 100644 frontend/project/static/src/views/burndown_chart/burndown_chart_view.js create mode 100644 frontend/project/static/src/views/burndown_chart/burndown_chart_view.xml create mode 100644 frontend/project/static/src/views/components/project_task_template_dropdown.js create mode 100644 frontend/project/static/src/views/components/project_task_template_dropdown.xml create mode 100644 frontend/project/static/src/views/components/project_template_buttons.js create mode 100644 frontend/project/static/src/views/components/project_template_buttons.scss create mode 100644 frontend/project/static/src/views/components/project_template_buttons.xml create mode 100644 frontend/project/static/src/views/components/project_template_dropdown.js create mode 100644 frontend/project/static/src/views/components/project_template_dropdown.xml create mode 100644 frontend/project/static/src/views/project_form/project_project_form_controller.js create mode 100644 frontend/project/static/src/views/project_form/project_project_form_controller.xml create mode 100644 frontend/project/static/src/views/project_form/project_project_form_view.js create mode 100644 frontend/project/static/src/views/project_model_mixin.js create mode 100644 frontend/project/static/src/views/project_project_activity/project_project_activity_model.js create mode 100644 frontend/project/static/src/views/project_project_activity/project_project_activity_view.js create mode 100644 frontend/project/static/src/views/project_project_calendar/common/project_common_calendar_popover.js create mode 100644 frontend/project/static/src/views/project_project_calendar/common/project_common_calendar_popover.xml create mode 100644 frontend/project/static/src/views/project_project_calendar/common/project_common_calendar_renderer.js create mode 100644 frontend/project/static/src/views/project_project_calendar/project_project_calendar_controller.js create mode 100644 frontend/project/static/src/views/project_project_calendar/project_project_calendar_model.js create mode 100644 frontend/project/static/src/views/project_project_calendar/project_project_calendar_renderer.js create mode 100644 frontend/project/static/src/views/project_project_calendar/project_project_calendar_view.js create mode 100644 frontend/project/static/src/views/project_project_kanban/project_project_group_config_menu.js create mode 100644 frontend/project/static/src/views/project_project_kanban/project_project_kanban_controller.js create mode 100644 frontend/project/static/src/views/project_project_kanban/project_project_kanban_controller.xml create mode 100644 frontend/project/static/src/views/project_project_kanban/project_project_kanban_header.js create mode 100644 frontend/project/static/src/views/project_project_kanban/project_project_kanban_renderer.js create mode 100644 frontend/project/static/src/views/project_project_kanban/project_task_kanban_view.js create mode 100644 frontend/project/static/src/views/project_project_list/project_project_list_controller.js create mode 100644 frontend/project/static/src/views/project_project_list/project_project_list_controller.xml create mode 100644 frontend/project/static/src/views/project_project_list/project_project_list_renderer.js create mode 100644 frontend/project/static/src/views/project_project_list/project_project_list_view.js create mode 100644 frontend/project/static/src/views/project_relational_model.js create mode 100644 frontend/project/static/src/views/project_task_activity/project_task_activity_model.js create mode 100644 frontend/project/static/src/views/project_task_activity/project_task_activity_view.js create mode 100644 frontend/project/static/src/views/project_task_analysis_graph/project_task_analysis_graph_model.js create mode 100644 frontend/project/static/src/views/project_task_analysis_graph/project_task_analysis_graph_renderer.js create mode 100644 frontend/project/static/src/views/project_task_analysis_graph/project_task_analysis_graph_view.js create mode 100644 frontend/project/static/src/views/project_task_analysis_pivot/project_task_analysis_pivot_model.js create mode 100644 frontend/project/static/src/views/project_task_analysis_pivot/project_task_analysis_pivot_renderer.js create mode 100644 frontend/project/static/src/views/project_task_analysis_pivot/project_task_analysis_pivot_view.js create mode 100644 frontend/project/static/src/views/project_task_analysis_renderer_mixin.js create mode 100644 frontend/project/static/src/views/project_task_calendar/hooks/project_task_calendar_task_to_plan_draggable.js create mode 100644 frontend/project/static/src/views/project_task_calendar/project_task_calendar_common/project_task_calendar_common_renderer.js create mode 100644 frontend/project/static/src/views/project_task_calendar/project_task_calendar_common/project_task_calendar_common_renderer.xml create mode 100644 frontend/project/static/src/views/project_task_calendar/project_task_calendar_controller.js create mode 100644 frontend/project/static/src/views/project_task_calendar/project_task_calendar_filter_section/project_task_calendar_filter_section.js create mode 100644 frontend/project/static/src/views/project_task_calendar/project_task_calendar_filter_section/project_task_calendar_filter_section.xml create mode 100644 frontend/project/static/src/views/project_task_calendar/project_task_calendar_list_to_plan/project_task_calendar_list_to_plan.js create mode 100644 frontend/project/static/src/views/project_task_calendar/project_task_calendar_list_to_plan/project_task_calendar_list_to_plan.xml create mode 100644 frontend/project/static/src/views/project_task_calendar/project_task_calendar_model.js create mode 100644 frontend/project/static/src/views/project_task_calendar/project_task_calendar_renderer.js create mode 100644 frontend/project/static/src/views/project_task_calendar/project_task_calendar_renderer.scss create mode 100644 frontend/project/static/src/views/project_task_calendar/project_task_calendar_view.js create mode 100644 frontend/project/static/src/views/project_task_calendar/project_task_calendar_year/project_task_calendar_year_renderer.js create mode 100644 frontend/project/static/src/views/project_task_calendar/side_panel/project_task_calendar_side_panel.js create mode 100644 frontend/project/static/src/views/project_task_calendar/side_panel/project_task_calendar_side_panel.xml create mode 100644 frontend/project/static/src/views/project_task_control_panel/project_task_control_panel.js create mode 100644 frontend/project/static/src/views/project_task_control_panel/project_task_control_panel.xml create mode 100644 frontend/project/static/src/views/project_task_form/project_task_form_controller.js create mode 100644 frontend/project/static/src/views/project_task_form/project_task_form_controller.xml create mode 100644 frontend/project/static/src/views/project_task_form/project_task_form_view.js create mode 100644 frontend/project/static/src/views/project_task_form/project_task_form_view.scss create mode 100644 frontend/project/static/src/views/project_task_graph/project_task_graph_model.js create mode 100644 frontend/project/static/src/views/project_task_graph/project_task_graph_view.js create mode 100644 frontend/project/static/src/views/project_task_kanban/project_task_group_config_menu.js create mode 100644 frontend/project/static/src/views/project_task_kanban/project_task_kanban_compiler.js create mode 100644 frontend/project/static/src/views/project_task_kanban/project_task_kanban_controller.js create mode 100644 frontend/project/static/src/views/project_task_kanban/project_task_kanban_controller.xml create mode 100644 frontend/project/static/src/views/project_task_kanban/project_task_kanban_examples.js create mode 100644 frontend/project/static/src/views/project_task_kanban/project_task_kanban_header.js create mode 100644 frontend/project/static/src/views/project_task_kanban/project_task_kanban_model.js create mode 100644 frontend/project/static/src/views/project_task_kanban/project_task_kanban_record.js create mode 100644 frontend/project/static/src/views/project_task_kanban/project_task_kanban_renderer.js create mode 100644 frontend/project/static/src/views/project_task_kanban/project_task_kanban_renderer.xml create mode 100644 frontend/project/static/src/views/project_task_kanban/project_task_kanban_view.js create mode 100644 frontend/project/static/src/views/project_task_kanban/project_task_kanban_view.scss create mode 100644 frontend/project/static/src/views/project_task_list/project_task_list_controller.js create mode 100644 frontend/project/static/src/views/project_task_list/project_task_list_controller.xml create mode 100644 frontend/project/static/src/views/project_task_list/project_task_list_renderer.js create mode 100644 frontend/project/static/src/views/project_task_list/project_task_list_view.js create mode 100644 frontend/project/static/src/views/project_task_model_mixin.js create mode 100644 frontend/project/static/src/views/project_task_pivot/project_task_pivot_model.js create mode 100644 frontend/project/static/src/views/project_task_pivot/project_task_pivot_view.js create mode 100644 frontend/project/static/src/views/project_task_relational_model.js create mode 100644 frontend/project/static/src/views/project_update_kanban/project_update_kanban.scss create mode 100644 frontend/project/static/src/views/project_update_kanban/project_update_kanban_controller.js create mode 100644 frontend/project/static/src/views/project_update_kanban/project_update_kanban_controller.xml create mode 100644 frontend/project/static/src/views/project_update_kanban/project_update_kanban_view.js create mode 100644 frontend/project/static/src/views/project_update_list/project_update_list_controller.js create mode 100644 frontend/project/static/src/views/project_update_list/project_update_list_controller.xml create mode 100644 frontend/project/static/src/views/project_update_list/project_update_list_view.js create mode 100644 frontend/project/static/src/views/widget/subtask_counter.js create mode 100644 frontend/project/static/src/views/widget/subtask_counter.xml create mode 100644 frontend/project/static/src/xml/project_task_kanban_examples.xml create mode 100644 frontend/project/static/tests/mock_server/mock_models/project_task.js create mode 100644 frontend/project/static/tests/project_is_favorite_field.test.js create mode 100644 frontend/project/static/tests/project_models.js create mode 100644 frontend/project/static/tests/project_notebook_task_list.test.js create mode 100644 frontend/project/static/tests/project_notebook_task_list_mobile.test.js create mode 100644 frontend/project/static/tests/project_project.test.js create mode 100644 frontend/project/static/tests/project_project_calendar.test.js create mode 100644 frontend/project/static/tests/project_project_form_view.test.js create mode 100644 frontend/project/static/tests/project_project_state_selection.test.js create mode 100644 frontend/project/static/tests/project_right_side_panel.test.js create mode 100644 frontend/project/static/tests/project_task_analysis.test.js create mode 100644 frontend/project/static/tests/project_task_burndown_chart.test.js create mode 100644 frontend/project/static/tests/project_task_calendar.test.js create mode 100644 frontend/project/static/tests/project_task_groupby.test.js create mode 100644 frontend/project/static/tests/project_task_kanban_view.test.js create mode 100644 frontend/project/static/tests/project_task_list_view.test.js create mode 100644 frontend/project/static/tests/project_task_priority_switch.test.js create mode 100644 frontend/project/static/tests/project_task_project_many2one_field.test.js create mode 100644 frontend/project/static/tests/project_task_state_selection.test.js create mode 100644 frontend/project/static/tests/project_task_subtask.test.js create mode 100644 frontend/project/static/tests/project_task_template_dropdown.test.js create mode 100644 frontend/project/static/tests/project_update_with_color.test.js create mode 100644 frontend/project/static/tests/tours/personal_stage_tour.js create mode 100644 frontend/project/static/tests/tours/project_burndown_chart_tour.js create mode 100644 frontend/project/static/tests/tours/project_sharing_tour.js create mode 100644 frontend/project/static/tests/tours/project_tags_filter_tour_tests.js create mode 100644 frontend/project/static/tests/tours/project_task_history.js create mode 100644 frontend/project/static/tests/tours/project_task_templates_tour.js create mode 100644 frontend/project/static/tests/tours/project_templates_tour.js create mode 100644 frontend/project/static/tests/tours/project_tour.js create mode 100644 frontend/project/static/tests/tours/project_update_tour_tests.js create mode 100644 frontend/project/static/xls/tasks_import_template.xlsx create mode 100644 frontend/purchase/static/description/icon.png create mode 100644 frontend/purchase/static/description/icon.svg create mode 100644 frontend/purchase/static/description/icon_hi.png create mode 100644 frontend/purchase/static/src/components/monetary_field_no_zero/monetary_field_no_zero.js create mode 100644 frontend/purchase/static/src/components/open_match_line_widget/open_match_line_widget.js create mode 100644 frontend/purchase/static/src/components/open_match_line_widget/open_match_line_widget.xml create mode 100644 frontend/purchase/static/src/components/purchase_file_uploader/purchase_file_uploader.js create mode 100644 frontend/purchase/static/src/components/purchase_file_uploader/purchase_file_uploader.xml create mode 100644 frontend/purchase/static/src/components/tax_totals/tax_totals.xml create mode 100644 frontend/purchase/static/src/img/calculator.svg create mode 100644 frontend/purchase/static/src/img/milk-OTDPurchase.gif create mode 100644 frontend/purchase/static/src/interactions/purchase_datetimepicker.js create mode 100644 frontend/purchase/static/src/interactions/purchase_sidebar.js create mode 100644 frontend/purchase/static/src/js/tours/purchase.js create mode 100644 frontend/purchase/static/src/js/tours/purchase_steps.js create mode 100644 frontend/purchase/static/src/product_catalog/kanban_record.js create mode 100644 frontend/purchase/static/src/product_catalog/kanban_renderer.js create mode 100644 frontend/purchase/static/src/product_catalog/kanban_renderer.xml create mode 100644 frontend/purchase/static/src/product_catalog/kanban_view.js create mode 100644 frontend/purchase/static/src/product_catalog/purchase_order_line/purchase_order_line.js create mode 100644 frontend/purchase/static/src/scss/purchase_portal.scss create mode 100644 frontend/purchase/static/src/toaster_button/toaster_button_widget.js create mode 100644 frontend/purchase/static/src/toaster_button/toaster_button_widget.xml create mode 100644 frontend/purchase/static/src/views/purchase_dashboard.js create mode 100644 frontend/purchase/static/src/views/purchase_dashboard.scss create mode 100644 frontend/purchase/static/src/views/purchase_dashboard.xml create mode 100644 frontend/purchase/static/src/views/purchase_kanbanview.js create mode 100644 frontend/purchase/static/src/views/purchase_kanbanview.xml create mode 100644 frontend/purchase/static/src/views/purchase_listview.js create mode 100644 frontend/purchase/static/src/views/purchase_listview.xml create mode 100644 frontend/purchase/static/tests/tours/purchase_catalog.js create mode 100644 frontend/purchase/static/tests/tours/purchase_flow_tour.js create mode 100644 frontend/purchase/static/tests/tours/tour_helper.js create mode 100644 frontend/purchase/static/xls/product_purchase.xls create mode 100644 frontend/purchase/static/xls/requests_for_quotation_import_template.xlsx create mode 100644 frontend/sale/static/description/icon.png create mode 100644 frontend/sale/static/description/icon.svg create mode 100644 frontend/sale/static/description/icon_hi.png create mode 100644 frontend/sale/static/img/advance_product_0-image.jpg create mode 100644 frontend/sale/static/img/btn_paynowcc_lg.gif create mode 100644 frontend/sale/static/img/floor_protection-image.jpg create mode 100644 frontend/sale/static/src/img/bag.svg create mode 100644 frontend/sale/static/src/img/onboarding_quotation_order_tooltip.jpg create mode 100644 frontend/sale/static/src/img/sales_quotation_thumbnail.webp create mode 100644 frontend/sale/static/src/interactions/portal_prepayment.js create mode 100644 frontend/sale/static/src/interactions/sale_portal.js create mode 100644 frontend/sale/static/src/interactions/sale_sidebar.js create mode 100644 frontend/sale/static/src/js/badge_extra_price/badge_extra_price.js create mode 100644 frontend/sale/static/src/js/badge_extra_price/badge_extra_price.xml create mode 100644 frontend/sale/static/src/js/combo_configurator_dialog/combo_configurator_dialog.js create mode 100644 frontend/sale/static/src/js/combo_configurator_dialog/combo_configurator_dialog.scss create mode 100644 frontend/sale/static/src/js/combo_configurator_dialog/combo_configurator_dialog.xml create mode 100644 frontend/sale/static/src/js/models/product_combo.js create mode 100644 frontend/sale/static/src/js/models/product_combo_item.js create mode 100644 frontend/sale/static/src/js/models/product_product.js create mode 100644 frontend/sale/static/src/js/models/product_template_attribute_line.js create mode 100644 frontend/sale/static/src/js/models/product_template_attribute_value.js create mode 100644 frontend/sale/static/src/js/product/product.js create mode 100644 frontend/sale/static/src/js/product/product.scss create mode 100644 frontend/sale/static/src/js/product/product.xml create mode 100644 frontend/sale/static/src/js/product_card/product_card.js create mode 100644 frontend/sale/static/src/js/product_card/product_card.scss create mode 100644 frontend/sale/static/src/js/product_card/product_card.xml create mode 100644 frontend/sale/static/src/js/product_configurator_dialog/product_configurator_dialog.js create mode 100644 frontend/sale/static/src/js/product_configurator_dialog/product_configurator_dialog.xml create mode 100644 frontend/sale/static/src/js/product_list/product_list.js create mode 100644 frontend/sale/static/src/js/product_list/product_list.scss create mode 100644 frontend/sale/static/src/js/product_list/product_list.xml create mode 100644 frontend/sale/static/src/js/product_template_attribute_line/product_template_attribute_line.js create mode 100644 frontend/sale/static/src/js/product_template_attribute_line/product_template_attribute_line.scss create mode 100644 frontend/sale/static/src/js/product_template_attribute_line/product_template_attribute_line.xml create mode 100644 frontend/sale/static/src/js/quantity_buttons/quantity_buttons.js create mode 100644 frontend/sale/static/src/js/quantity_buttons/quantity_buttons.scss create mode 100644 frontend/sale/static/src/js/quantity_buttons/quantity_buttons.xml create mode 100644 frontend/sale/static/src/js/sale_action_helper/sale_action_helper.js create mode 100644 frontend/sale/static/src/js/sale_action_helper/sale_action_helper.scss create mode 100644 frontend/sale/static/src/js/sale_action_helper/sale_action_helper.xml create mode 100644 frontend/sale/static/src/js/sale_action_helper/sale_action_helper_dialog.js create mode 100644 frontend/sale/static/src/js/sale_action_helper/sale_action_helper_dialog.xml create mode 100644 frontend/sale/static/src/js/sale_order_line_field/sale_order_line_field.js create mode 100644 frontend/sale/static/src/js/sale_order_line_field/sale_order_line_field.xml create mode 100644 frontend/sale/static/src/js/sale_product_field.js create mode 100644 frontend/sale/static/src/js/sale_product_field.scss create mode 100644 frontend/sale/static/src/js/sale_progressbar_field.js create mode 100644 frontend/sale/static/src/js/sale_utils.js create mode 100644 frontend/sale/static/src/js/tours/combo_configurator_tour_utils.js create mode 100644 frontend/sale/static/src/js/tours/product_configurator_tour_utils.js create mode 100644 frontend/sale/static/src/js/tours/sale.js create mode 100644 frontend/sale/static/src/js/tours/tour_utils.js create mode 100644 frontend/sale/static/src/js/upload_rfq_cog_menu/upload_rfq_cog_menu.js create mode 100644 frontend/sale/static/src/js/upload_rfq_cog_menu/upload_rfq_cog_menu.xml create mode 100644 frontend/sale/static/src/scss/sale_onboarding.scss create mode 100644 frontend/sale/static/src/scss/sale_portal.scss create mode 100644 frontend/sale/static/src/scss/sale_report.scss create mode 100644 frontend/sale/static/src/views/sale_file_upload_kanban/sale_file_upload_kanban_controller.js create mode 100644 frontend/sale/static/src/views/sale_file_upload_kanban/sale_file_upload_kanban_renderer.js create mode 100644 frontend/sale/static/src/views/sale_file_upload_kanban/sale_file_upload_kanban_view.js create mode 100644 frontend/sale/static/src/views/sale_file_upload_list/sale_file_upload_list_controller.js create mode 100644 frontend/sale/static/src/views/sale_file_upload_list/sale_file_upload_list_renderer.js create mode 100644 frontend/sale/static/src/views/sale_file_upload_list/sale_file_upload_list_view.js create mode 100644 frontend/sale/static/src/views/sale_onboarding_kanban/sale_onboarding_kanban_renderer.js create mode 100644 frontend/sale/static/src/views/sale_onboarding_kanban/sale_onboarding_kanban_renderer.xml create mode 100644 frontend/sale/static/src/views/sale_onboarding_kanban/sale_onboarding_kanban_view.js create mode 100644 frontend/sale/static/src/views/sale_onboarding_list/sale_onboarding_list_renderer.js create mode 100644 frontend/sale/static/src/views/sale_onboarding_list/sale_onboarding_list_renderer.xml create mode 100644 frontend/sale/static/src/views/sale_onboarding_list/sale_onboarding_list_view.js create mode 100644 frontend/sale/static/src/xml/sale_product_field.xml create mode 100644 frontend/sale/static/src/xml/sales_team_progress_bar_template.xml create mode 100644 frontend/sale/static/tests/mock_server/mock_models/sale_order.js create mode 100644 frontend/sale/static/tests/mock_server/mock_models/sale_order_line.js create mode 100644 frontend/sale/static/tests/sale_order_line_field.test.js create mode 100644 frontend/sale/static/tests/sale_product_field.test.js create mode 100644 frontend/sale/static/tests/sale_test_helpers.js create mode 100644 frontend/sale/static/tests/sales_team_dashboard.test.js create mode 100644 frontend/sale/static/tests/tours/mail_attachment_removal_test_tour.js create mode 100644 frontend/sale/static/tests/tours/product_attribute_value_tour.js create mode 100644 frontend/sale/static/tests/tours/sale_catalog.js create mode 100644 frontend/sale/static/tests/tours/sale_combo_configurator.js create mode 100644 frontend/sale/static/tests/tours/sale_combo_configurator_preconfigure_unconfigurable_ptals.js create mode 100644 frontend/sale/static/tests/tours/sale_combo_configurator_preselect_single_unconfigurable_items.js create mode 100644 frontend/sale/static/tests/tours/sale_order_product_uom_integrity.js create mode 100644 frontend/sale/static/tests/tours/sale_signature.js create mode 100644 frontend/sale/static/xls/quotations_import_template.xlsx create mode 100644 frontend/stock/static/description/icon.png create mode 100644 frontend/stock/static/description/icon.svg create mode 100644 frontend/stock/static/description/icon_hi.png create mode 100644 frontend/stock/static/img/barcode_scanner.png create mode 100644 frontend/stock/static/img/cable_management.jpg create mode 100644 frontend/stock/static/img/empty_list.png create mode 100644 frontend/stock/static/img/replenishment.svg create mode 100644 frontend/stock/static/img/res_partner_address_41.jpg create mode 100644 frontend/stock/static/img/zpl_label_placeholder.png create mode 100644 frontend/stock/static/src/client_actions/multi_print.js create mode 100644 frontend/stock/static/src/client_actions/stock_traceability_report_backend.js create mode 100644 frontend/stock/static/src/client_actions/stock_traceability_report_backend.xml create mode 100644 frontend/stock/static/src/components/reception_report_line/stock_reception_report_line.js create mode 100644 frontend/stock/static/src/components/reception_report_line/stock_reception_report_line.xml create mode 100644 frontend/stock/static/src/components/reception_report_main/stock_reception_report_main.js create mode 100644 frontend/stock/static/src/components/reception_report_main/stock_reception_report_main.xml create mode 100644 frontend/stock/static/src/components/reception_report_table/stock_reception_report_table.js create mode 100644 frontend/stock/static/src/components/reception_report_table/stock_reception_report_table.xml create mode 100644 frontend/stock/static/src/components/stock_overview/stock_overview.js create mode 100644 frontend/stock/static/src/fields/stock_action_field.js create mode 100644 frontend/stock/static/src/fields/stock_action_field.xml create mode 100644 frontend/stock/static/src/fields/stock_move_line_x2_many_field.js create mode 100644 frontend/stock/static/src/img/barcode.gif create mode 100644 frontend/stock/static/src/picking_type_dashboard_graph/picking_type_dashboard_graph_field.js create mode 100644 frontend/stock/static/src/picking_type_dashboard_graph/picking_type_dashboard_graph_field.scss create mode 100644 frontend/stock/static/src/scss/forecast_widget.scss create mode 100644 frontend/stock/static/src/scss/forecasted_details.scss create mode 100644 frontend/stock/static/src/scss/product_form.scss create mode 100644 frontend/stock/static/src/scss/report_stock_reception.scss create mode 100644 frontend/stock/static/src/scss/report_stock_rule.scss create mode 100644 frontend/stock/static/src/scss/report_stockpicking_operations.scss create mode 100644 frontend/stock/static/src/scss/stock_empty_screen.scss create mode 100644 frontend/stock/static/src/scss/stock_forecasted.scss create mode 100644 frontend/stock/static/src/scss/stock_move_list.scss create mode 100644 frontend/stock/static/src/scss/stock_overview.scss create mode 100644 frontend/stock/static/src/scss/stock_replenishment_info.scss create mode 100644 frontend/stock/static/src/scss/stock_traceability_report.scss create mode 100644 frontend/stock/static/src/stock_forecasted/forecasted_buttons.js create mode 100644 frontend/stock/static/src/stock_forecasted/forecasted_buttons.xml create mode 100644 frontend/stock/static/src/stock_forecasted/forecasted_details.js create mode 100644 frontend/stock/static/src/stock_forecasted/forecasted_details.xml create mode 100644 frontend/stock/static/src/stock_forecasted/forecasted_graph.js create mode 100644 frontend/stock/static/src/stock_forecasted/forecasted_graph.xml create mode 100644 frontend/stock/static/src/stock_forecasted/forecasted_header.js create mode 100644 frontend/stock/static/src/stock_forecasted/forecasted_header.xml create mode 100644 frontend/stock/static/src/stock_forecasted/forecasted_warehouse_filter.js create mode 100644 frontend/stock/static/src/stock_forecasted/forecasted_warehouse_filter.xml create mode 100644 frontend/stock/static/src/stock_forecasted/stock_forecasted.js create mode 100644 frontend/stock/static/src/stock_forecasted/stock_forecasted.xml create mode 100644 frontend/stock/static/src/stock_warehouse_service.js create mode 100644 frontend/stock/static/src/views/list/inventory_report_list_model.js create mode 100644 frontend/stock/static/src/views/list/inventory_report_list_view.js create mode 100644 frontend/stock/static/src/views/list/stock_add_package_list_view.js create mode 100644 frontend/stock/static/src/views/list/stock_report_list_view.js create mode 100644 frontend/stock/static/src/views/picking_form/stock_move_one2many.js create mode 100644 frontend/stock/static/src/views/picking_form/stock_move_product_label.js create mode 100644 frontend/stock/static/src/views/picking_form/stock_move_product_label.xml create mode 100644 frontend/stock/static/src/views/search/stock_orderpoint_search_model.js create mode 100644 frontend/stock/static/src/views/search/stock_orderpoint_search_panel.js create mode 100644 frontend/stock/static/src/views/search/stock_orderpoint_search_panel.xml create mode 100644 frontend/stock/static/src/views/search/stock_report_search_model.js create mode 100644 frontend/stock/static/src/views/search/stock_report_search_panel.js create mode 100644 frontend/stock/static/src/views/search/stock_report_search_panel.xml create mode 100644 frontend/stock/static/src/views/stock_empty_list_help.js create mode 100644 frontend/stock/static/src/views/stock_empty_list_help.xml create mode 100644 frontend/stock/static/src/views/stock_orderpoint_list_controller.js create mode 100644 frontend/stock/static/src/views/stock_orderpoint_list_view.js create mode 100644 frontend/stock/static/src/views/stock_orderpoint_list_view.xml create mode 100644 frontend/stock/static/src/widgets/counted_quantity_widget.js create mode 100644 frontend/stock/static/src/widgets/forced_placeholder.js create mode 100644 frontend/stock/static/src/widgets/forced_placeholder.xml create mode 100644 frontend/stock/static/src/widgets/forecast_widget.js create mode 100644 frontend/stock/static/src/widgets/forecast_widget.xml create mode 100644 frontend/stock/static/src/widgets/generate_serial.js create mode 100644 frontend/stock/static/src/widgets/json_widget.js create mode 100644 frontend/stock/static/src/widgets/json_widget.xml create mode 100644 frontend/stock/static/src/widgets/lots_dialog.xml create mode 100644 frontend/stock/static/src/widgets/many2many_barcode_tags.js create mode 100644 frontend/stock/static/src/widgets/popover_widget.js create mode 100644 frontend/stock/static/src/widgets/popover_widget.xml create mode 100644 frontend/stock/static/src/widgets/stock_package_m2m.js create mode 100644 frontend/stock/static/src/widgets/stock_package_m2o.js create mode 100644 frontend/stock/static/src/widgets/stock_package_m2o.xml create mode 100644 frontend/stock/static/src/widgets/stock_pick_from.js create mode 100644 frontend/stock/static/src/widgets/stock_pick_from.xml create mode 100644 frontend/stock/static/src/widgets/stock_rescheduling_popover.js create mode 100644 frontend/stock/static/src/widgets/stock_rescheduling_popover.xml create mode 100644 frontend/stock/static/src/xml/inventory_lines.xml create mode 100644 frontend/stock/static/src/xml/report_stock_reception.xml create mode 100644 frontend/stock/static/tests/counted_quantity_widget.test.js create mode 100644 frontend/stock/static/tests/inventory_report_list.test.js create mode 100644 frontend/stock/static/tests/popover_widget.test.js create mode 100644 frontend/stock/static/tests/stock_traceability_report_backend.test.js create mode 100644 frontend/stock/static/tests/tours/stock_flow_tour.js create mode 100644 frontend/stock/static/tests/tours/stock_picking_tour.js create mode 100644 frontend/stock/static/tests/tours/stock_report_tests.js create mode 100644 frontend/stock/static/tests/tours/tour_helper.js create mode 100644 frontend/stock/static/xlsx/stock_quant.xlsx create mode 100644 frontend/web/static/fonts/fonts.scss create mode 100644 frontend/web/static/fonts/google/Fira_Mono/Fira_Mono-Bold.ttf create mode 100644 frontend/web/static/fonts/google/Fira_Mono/Fira_Mono-Medium.ttf create mode 100644 frontend/web/static/fonts/google/Fira_Mono/Fira_Mono-Regular.ttf create mode 100644 frontend/web/static/fonts/google/Fira_Mono/OFL.txt create mode 100644 frontend/web/static/fonts/google/Montserrat/Montserrat-Black.ttf create mode 100644 frontend/web/static/fonts/google/Montserrat/Montserrat-BlackItalic.ttf create mode 100644 frontend/web/static/fonts/google/Montserrat/Montserrat-Bold.ttf create mode 100644 frontend/web/static/fonts/google/Montserrat/Montserrat-BoldItalic.ttf create mode 100644 frontend/web/static/fonts/google/Montserrat/Montserrat-ExtraBold.ttf create mode 100644 frontend/web/static/fonts/google/Montserrat/Montserrat-ExtraBoldItalic.ttf create mode 100644 frontend/web/static/fonts/google/Montserrat/Montserrat-ExtraLight.ttf create mode 100644 frontend/web/static/fonts/google/Montserrat/Montserrat-ExtraLightItalic.ttf create mode 100644 frontend/web/static/fonts/google/Montserrat/Montserrat-Italic.ttf create mode 100644 frontend/web/static/fonts/google/Montserrat/Montserrat-Light.ttf create mode 100644 frontend/web/static/fonts/google/Montserrat/Montserrat-LightItalic.ttf create mode 100644 frontend/web/static/fonts/google/Montserrat/Montserrat-Medium.ttf create mode 100644 frontend/web/static/fonts/google/Montserrat/Montserrat-MediumItalic.ttf create mode 100644 frontend/web/static/fonts/google/Montserrat/Montserrat-Regular.ttf create mode 100644 frontend/web/static/fonts/google/Montserrat/Montserrat-SemiBold.ttf create mode 100644 frontend/web/static/fonts/google/Montserrat/Montserrat-SemiBoldItalic.ttf create mode 100644 frontend/web/static/fonts/google/Montserrat/Montserrat-Thin.ttf create mode 100644 frontend/web/static/fonts/google/Montserrat/Montserrat-ThinItalic.ttf create mode 100644 frontend/web/static/fonts/google/Montserrat/OFL.txt create mode 100644 frontend/web/static/fonts/google/Open_Sans/LICENSE.txt create mode 100644 frontend/web/static/fonts/google/Open_Sans/Open_Sans-Bold.ttf create mode 100644 frontend/web/static/fonts/google/Open_Sans/Open_Sans-BoldItalic.ttf create mode 100644 frontend/web/static/fonts/google/Open_Sans/Open_Sans-ExtraBold.ttf create mode 100644 frontend/web/static/fonts/google/Open_Sans/Open_Sans-ExtraBoldItalic.ttf create mode 100644 frontend/web/static/fonts/google/Open_Sans/Open_Sans-Italic.ttf create mode 100644 frontend/web/static/fonts/google/Open_Sans/Open_Sans-Light.ttf create mode 100644 frontend/web/static/fonts/google/Open_Sans/Open_Sans-LightItalic.ttf create mode 100644 frontend/web/static/fonts/google/Open_Sans/Open_Sans-Regular.ttf create mode 100644 frontend/web/static/fonts/google/Open_Sans/Open_Sans-SemiBold.ttf create mode 100644 frontend/web/static/fonts/google/Open_Sans/Open_Sans-SemiBoldItalic.ttf create mode 100644 frontend/web/static/fonts/google/Oswald/OFL.txt create mode 100644 frontend/web/static/fonts/google/Oswald/Oswald-Bold.ttf create mode 100644 frontend/web/static/fonts/google/Oswald/Oswald-ExtraLight.ttf create mode 100644 frontend/web/static/fonts/google/Oswald/Oswald-Light.ttf create mode 100644 frontend/web/static/fonts/google/Oswald/Oswald-Medium.ttf create mode 100644 frontend/web/static/fonts/google/Oswald/Oswald-Regular.ttf create mode 100644 frontend/web/static/fonts/google/Oswald/Oswald-SemiBold.ttf create mode 100644 frontend/web/static/fonts/google/Raleway/OFL.txt create mode 100644 frontend/web/static/fonts/google/Raleway/Raleway-Black.ttf create mode 100644 frontend/web/static/fonts/google/Raleway/Raleway-BlackItalic.ttf create mode 100644 frontend/web/static/fonts/google/Raleway/Raleway-Bold.ttf create mode 100644 frontend/web/static/fonts/google/Raleway/Raleway-BoldItalic.ttf create mode 100644 frontend/web/static/fonts/google/Raleway/Raleway-ExtraBold.ttf create mode 100644 frontend/web/static/fonts/google/Raleway/Raleway-ExtraBoldItalic.ttf create mode 100644 frontend/web/static/fonts/google/Raleway/Raleway-ExtraLight.ttf create mode 100644 frontend/web/static/fonts/google/Raleway/Raleway-ExtraLightItalic.ttf create mode 100644 frontend/web/static/fonts/google/Raleway/Raleway-Italic.ttf create mode 100644 frontend/web/static/fonts/google/Raleway/Raleway-Light.ttf create mode 100644 frontend/web/static/fonts/google/Raleway/Raleway-LightItalic.ttf create mode 100644 frontend/web/static/fonts/google/Raleway/Raleway-Medium.ttf create mode 100644 frontend/web/static/fonts/google/Raleway/Raleway-MediumItalic.ttf create mode 100644 frontend/web/static/fonts/google/Raleway/Raleway-Regular.ttf create mode 100644 frontend/web/static/fonts/google/Raleway/Raleway-SemiBold.ttf create mode 100644 frontend/web/static/fonts/google/Raleway/Raleway-SemiBoldItalic.ttf create mode 100644 frontend/web/static/fonts/google/Raleway/Raleway-Thin.ttf create mode 100644 frontend/web/static/fonts/google/Raleway/Raleway-ThinItalic.ttf create mode 100644 frontend/web/static/fonts/google/Roboto/LICENSE.txt create mode 100644 frontend/web/static/fonts/google/Roboto/Roboto-Black.ttf create mode 100644 frontend/web/static/fonts/google/Roboto/Roboto-BlackItalic.ttf create mode 100644 frontend/web/static/fonts/google/Roboto/Roboto-Bold.ttf create mode 100644 frontend/web/static/fonts/google/Roboto/Roboto-BoldItalic.ttf create mode 100644 frontend/web/static/fonts/google/Roboto/Roboto-Italic.ttf create mode 100644 frontend/web/static/fonts/google/Roboto/Roboto-Light.ttf create mode 100644 frontend/web/static/fonts/google/Roboto/Roboto-LightItalic.ttf create mode 100644 frontend/web/static/fonts/google/Roboto/Roboto-Medium.ttf create mode 100644 frontend/web/static/fonts/google/Roboto/Roboto-MediumItalic.ttf create mode 100644 frontend/web/static/fonts/google/Roboto/Roboto-Regular.ttf create mode 100644 frontend/web/static/fonts/google/Roboto/Roboto-Thin.ttf create mode 100644 frontend/web/static/fonts/google/Roboto/Roboto-ThinItalic.ttf create mode 100644 frontend/web/static/fonts/google/Roboto/roboto.b64 create mode 100644 frontend/web/static/fonts/google/Tajawal/OFL.txt create mode 100644 frontend/web/static/fonts/google/Tajawal/Tajawal-Black.ttf create mode 100644 frontend/web/static/fonts/google/Tajawal/Tajawal-Bold.ttf create mode 100644 frontend/web/static/fonts/google/Tajawal/Tajawal-ExtraBold.ttf create mode 100644 frontend/web/static/fonts/google/Tajawal/Tajawal-ExtraLight.ttf create mode 100644 frontend/web/static/fonts/google/Tajawal/Tajawal-Light.ttf create mode 100644 frontend/web/static/fonts/google/Tajawal/Tajawal-Medium.ttf create mode 100644 frontend/web/static/fonts/google/Tajawal/Tajawal-Regular.ttf create mode 100644 frontend/web/static/fonts/lato/Lato-Bla-webfont.eot create mode 100644 frontend/web/static/fonts/lato/Lato-Bla-webfont.svg create mode 100644 frontend/web/static/fonts/lato/Lato-Bla-webfont.ttf create mode 100644 frontend/web/static/fonts/lato/Lato-Bla-webfont.woff create mode 100644 frontend/web/static/fonts/lato/Lato-BlaIta-webfont.eot create mode 100644 frontend/web/static/fonts/lato/Lato-BlaIta-webfont.svg create mode 100644 frontend/web/static/fonts/lato/Lato-BlaIta-webfont.ttf create mode 100644 frontend/web/static/fonts/lato/Lato-BlaIta-webfont.woff create mode 100644 frontend/web/static/fonts/lato/Lato-Bol-webfont.eot create mode 100644 frontend/web/static/fonts/lato/Lato-Bol-webfont.svg create mode 100644 frontend/web/static/fonts/lato/Lato-Bol-webfont.ttf create mode 100644 frontend/web/static/fonts/lato/Lato-Bol-webfont.woff create mode 100644 frontend/web/static/fonts/lato/Lato-BolIta-webfont.eot create mode 100644 frontend/web/static/fonts/lato/Lato-BolIta-webfont.svg create mode 100644 frontend/web/static/fonts/lato/Lato-BolIta-webfont.ttf create mode 100644 frontend/web/static/fonts/lato/Lato-BolIta-webfont.woff create mode 100644 frontend/web/static/fonts/lato/Lato-Hai-webfont.eot create mode 100644 frontend/web/static/fonts/lato/Lato-Hai-webfont.svg create mode 100644 frontend/web/static/fonts/lato/Lato-Hai-webfont.ttf create mode 100644 frontend/web/static/fonts/lato/Lato-Hai-webfont.woff create mode 100644 frontend/web/static/fonts/lato/Lato-HaiIta-webfont.eot create mode 100644 frontend/web/static/fonts/lato/Lato-HaiIta-webfont.svg create mode 100644 frontend/web/static/fonts/lato/Lato-HaiIta-webfont.ttf create mode 100644 frontend/web/static/fonts/lato/Lato-HaiIta-webfont.woff create mode 100644 frontend/web/static/fonts/lato/Lato-Lig-webfont.eot create mode 100644 frontend/web/static/fonts/lato/Lato-Lig-webfont.svg create mode 100644 frontend/web/static/fonts/lato/Lato-Lig-webfont.ttf create mode 100644 frontend/web/static/fonts/lato/Lato-Lig-webfont.woff create mode 100644 frontend/web/static/fonts/lato/Lato-LigIta-webfont.eot create mode 100644 frontend/web/static/fonts/lato/Lato-LigIta-webfont.svg create mode 100644 frontend/web/static/fonts/lato/Lato-LigIta-webfont.ttf create mode 100644 frontend/web/static/fonts/lato/Lato-LigIta-webfont.woff create mode 100644 frontend/web/static/fonts/lato/Lato-Reg-webfont.eot create mode 100644 frontend/web/static/fonts/lato/Lato-Reg-webfont.svg create mode 100644 frontend/web/static/fonts/lato/Lato-Reg-webfont.ttf create mode 100644 frontend/web/static/fonts/lato/Lato-Reg-webfont.woff create mode 100644 frontend/web/static/fonts/lato/Lato-RegIta-webfont.eot create mode 100644 frontend/web/static/fonts/lato/Lato-RegIta-webfont.svg create mode 100644 frontend/web/static/fonts/lato/Lato-RegIta-webfont.ttf create mode 100644 frontend/web/static/fonts/lato/Lato-RegIta-webfont.woff create mode 100644 frontend/web/static/fonts/lato/SIL-Open-Font-License-1.1.txt create mode 100644 frontend/web/static/fonts/sign/LaBelleAurore-Regular.ttf create mode 100644 frontend/web/static/fonts/sign/LaBelleAurore-ofl.txt create mode 100644 frontend/web/static/fonts/sign/MarckScript-Regular-ofl.txt create mode 100644 frontend/web/static/fonts/sign/MarckScript-Regular.ttf create mode 100644 frontend/web/static/fonts/sign/NotoSans-Reg.ttf create mode 100644 frontend/web/static/fonts/sign/OoohBaby-Regular-ofl.txt create mode 100644 frontend/web/static/fonts/sign/OoohBaby-Regular.ttf create mode 100644 frontend/web/static/fonts/sign/ReenieBeanie-Regular.ttf create mode 100644 frontend/web/static/fonts/sign/ReenieBeanie-ofl.txt create mode 100644 frontend/web/static/fonts/sign/ShadowsIntoLight-Regular.ttf create mode 100644 frontend/web/static/fonts/sign/ShadowsIntoLight-ofl.txt create mode 100644 frontend/web/static/fonts/sign/Zeyada-Regular.ttf create mode 100644 frontend/web/static/fonts/sign/Zeyada-ofl.txt create mode 100644 frontend/web/static/fonts/sign/khand.ttf create mode 100644 frontend/web/static/img/barcode.svg create mode 100644 frontend/web/static/img/bill.svg create mode 100644 frontend/web/static/img/default_icon_app.png create mode 100644 frontend/web/static/img/empty_folder.svg create mode 100644 frontend/web/static/img/enterprise_upgrade.jpg create mode 100644 frontend/web/static/img/favicon.ico create mode 100644 frontend/web/static/img/folder.svg create mode 100644 frontend/web/static/img/form_sheetbg.png create mode 100644 frontend/web/static/img/graph_background.png create mode 100644 frontend/web/static/img/logo.png create mode 100644 frontend/web/static/img/logo2.png create mode 100644 frontend/web/static/img/logo_inverse_white_206px.png create mode 100644 frontend/web/static/img/mimetypes/addresses.svg create mode 100644 frontend/web/static/img/mimetypes/archive.svg create mode 100644 frontend/web/static/img/mimetypes/audio.svg create mode 100644 frontend/web/static/img/mimetypes/binary.svg create mode 100644 frontend/web/static/img/mimetypes/calendar.svg create mode 100644 frontend/web/static/img/mimetypes/certificate.svg create mode 100644 frontend/web/static/img/mimetypes/disk.svg create mode 100644 frontend/web/static/img/mimetypes/document.svg create mode 100644 frontend/web/static/img/mimetypes/font.svg create mode 100644 frontend/web/static/img/mimetypes/image.svg create mode 100644 frontend/web/static/img/mimetypes/javascript.svg create mode 100644 frontend/web/static/img/mimetypes/pdf.svg create mode 100644 frontend/web/static/img/mimetypes/presentation.svg create mode 100644 frontend/web/static/img/mimetypes/print.svg create mode 100644 frontend/web/static/img/mimetypes/script.svg create mode 100644 frontend/web/static/img/mimetypes/spreadsheet.svg create mode 100644 frontend/web/static/img/mimetypes/text.svg create mode 100644 frontend/web/static/img/mimetypes/unknown.svg create mode 100644 frontend/web/static/img/mimetypes/vector.svg create mode 100644 frontend/web/static/img/mimetypes/video.svg create mode 100644 frontend/web/static/img/mimetypes/web_code.svg create mode 100644 frontend/web/static/img/mimetypes/web_style.svg create mode 100644 frontend/web/static/img/neutral_face.svg create mode 100644 frontend/web/static/img/nologo.png create mode 100644 frontend/web/static/img/odoo-icon-192x192.png create mode 100644 frontend/web/static/img/odoo-icon-512x512.png create mode 100644 frontend/web/static/img/odoo-icon-ios.png create mode 100644 frontend/web/static/img/odoo-icon.svg create mode 100644 frontend/web/static/img/odoo_logo.svg create mode 100644 frontend/web/static/img/odoo_logo_dark.svg create mode 100644 frontend/web/static/img/odoo_logo_tiny.png create mode 100644 frontend/web/static/img/openhand.cur create mode 100644 frontend/web/static/img/placeholder.png create mode 100644 frontend/web/static/img/quotation.svg create mode 100644 frontend/web/static/img/rfq.svg create mode 100644 frontend/web/static/img/sep-a.gif create mode 100644 frontend/web/static/img/smile.svg create mode 100644 frontend/web/static/img/smiling_face.svg create mode 100644 frontend/web/static/img/spin.png create mode 100644 frontend/web/static/img/spin.svg create mode 100644 frontend/web/static/img/transform.svg create mode 100644 frontend/web/static/img/transparent.png create mode 100644 frontend/web/static/img/user_menu_avatar.png create mode 100644 frontend/web/static/img/user_placeholder.jpg create mode 100644 frontend/web/static/lib/bootstrap/LICENSE create mode 100644 frontend/web/static/lib/bootstrap/dist/css/bootstrap.css create mode 100644 frontend/web/static/lib/bootstrap/dist/css/bootstrap.css.map create mode 100644 frontend/web/static/lib/bootstrap/js/dist/alert.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/base-component.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/button.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/carousel.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/collapse.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/dom/data.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/dom/event-handler.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/dom/manipulator.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/dom/selector-engine.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/dropdown.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/modal.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/offcanvas.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/popover.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/scrollspy.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/tab.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/toast.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/tooltip.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/util/backdrop.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/util/component-functions.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/util/config.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/util/focustrap.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/util/index.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/util/sanitizer.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/util/scrollbar.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/util/swipe.js create mode 100644 frontend/web/static/lib/bootstrap/js/dist/util/template-factory.js create mode 100644 frontend/web/static/lib/bootstrap/scss/_accordion.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_alert.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_badge.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_breadcrumb.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_button-group.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_buttons.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_card.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_carousel.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_close.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_containers.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_dropdown.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_forms.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_functions.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_grid.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_helpers.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_images.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_list-group.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_maps.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_mixins.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_modal.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_nav.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_navbar.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_offcanvas.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_pagination.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_placeholders.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_popover.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_progress.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_reboot.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_root.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_spinners.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_tables.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_toasts.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_tooltip.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_transitions.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_type.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_utilities.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_variables-dark.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/_variables.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/bootstrap-grid.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/bootstrap-reboot.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/bootstrap-utilities.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/bootstrap.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/forms/_floating-labels.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/forms/_form-check.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/forms/_form-control.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/forms/_form-range.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/forms/_form-select.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/forms/_form-text.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/forms/_input-group.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/forms/_labels.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/forms/_validation.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/helpers/_clearfix.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/helpers/_color-bg.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/helpers/_colored-links.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/helpers/_focus-ring.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/helpers/_icon-link.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/helpers/_position.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/helpers/_ratio.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/helpers/_stacks.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/helpers/_stretched-link.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/helpers/_text-truncation.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/helpers/_visually-hidden.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/helpers/_vr.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_alert.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_backdrop.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_banner.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_border-radius.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_box-shadow.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_breakpoints.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_buttons.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_caret.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_clearfix.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_color-mode.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_color-scheme.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_container.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_deprecate.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_forms.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_gradients.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_grid.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_image.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_list-group.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_lists.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_pagination.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_reset-text.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_resize.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_table-variants.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_text-truncate.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_transition.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_utilities.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/mixins/_visually-hidden.scss create mode 100644 frontend/web/static/lib/bootstrap/scss/utilities/_api.scss create mode 100644 frontend/web/static/lib/dompurify/DOMpurify.js create mode 100644 frontend/web/static/lib/fullcalendar/LICENSE.md create mode 100644 frontend/web/static/lib/hoot/tests/index.html create mode 100644 frontend/web/static/lib/hoot/ui/hoot_style.css create mode 100644 frontend/web/static/lib/luxon/luxon.js create mode 100644 frontend/web/static/lib/odoo_ui_icons/LICENSE.md create mode 100755 frontend/web/static/lib/odoo_ui_icons/Read Me.txt create mode 100644 frontend/web/static/lib/odoo_ui_icons/fonts/odoo_ui_icons.woff create mode 100644 frontend/web/static/lib/odoo_ui_icons/fonts/odoo_ui_icons.woff2 create mode 100644 frontend/web/static/lib/odoo_ui_icons/style.css create mode 100644 frontend/web/static/lib/owl/odoo_module.js create mode 100644 frontend/web/static/lib/owl/owl.js create mode 100644 frontend/web/static/lib/pdfjs/LICENSE create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/78-EUC-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/78-EUC-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/78-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/78-RKSJ-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/78-RKSJ-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/78-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/78ms-RKSJ-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/78ms-RKSJ-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/83pv-RKSJ-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/90ms-RKSJ-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/90ms-RKSJ-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/90msp-RKSJ-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/90msp-RKSJ-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/90pv-RKSJ-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/90pv-RKSJ-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Add-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Add-RKSJ-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Add-RKSJ-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Add-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-CNS1-0.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-CNS1-1.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-CNS1-2.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-CNS1-3.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-CNS1-4.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-CNS1-5.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-CNS1-6.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-CNS1-UCS2.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-GB1-0.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-GB1-1.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-GB1-2.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-GB1-3.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-GB1-4.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-GB1-5.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-GB1-UCS2.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-Japan1-0.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-Japan1-1.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-Japan1-2.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-Japan1-3.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-Japan1-4.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-Japan1-5.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-Japan1-6.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-Japan1-UCS2.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-Korea1-0.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-Korea1-1.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-Korea1-2.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Adobe-Korea1-UCS2.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/B5-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/B5-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/B5pc-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/B5pc-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/CNS-EUC-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/CNS-EUC-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/CNS1-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/CNS1-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/CNS2-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/CNS2-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/ETHK-B5-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/ETHK-B5-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/ETen-B5-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/ETen-B5-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/ETenms-B5-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/ETenms-B5-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/EUC-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/EUC-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Ext-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Ext-RKSJ-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Ext-RKSJ-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Ext-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/GB-EUC-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/GB-EUC-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/GB-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/GB-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/GBK-EUC-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/GBK-EUC-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/GBK2K-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/GBK2K-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/GBKp-EUC-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/GBKp-EUC-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/GBT-EUC-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/GBT-EUC-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/GBT-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/GBT-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/GBTpc-EUC-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/GBTpc-EUC-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/GBpc-EUC-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/GBpc-EUC-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/HKdla-B5-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/HKdla-B5-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/HKdlb-B5-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/HKdlb-B5-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/HKgccs-B5-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/HKgccs-B5-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/HKm314-B5-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/HKm314-B5-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/HKm471-B5-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/HKm471-B5-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/HKscs-B5-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/HKscs-B5-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Hankaku.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Hiragana.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/KSC-EUC-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/KSC-EUC-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/KSC-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/KSC-Johab-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/KSC-Johab-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/KSC-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/KSCms-UHC-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/KSCms-UHC-HW-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/KSCms-UHC-HW-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/KSCms-UHC-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/KSCpc-EUC-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/KSCpc-EUC-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Katakana.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/LICENSE create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/NWP-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/NWP-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/RKSJ-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/RKSJ-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/Roman.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniCNS-UCS2-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniCNS-UCS2-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniCNS-UTF16-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniCNS-UTF16-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniCNS-UTF32-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniCNS-UTF32-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniCNS-UTF8-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniCNS-UTF8-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniGB-UCS2-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniGB-UCS2-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniGB-UTF16-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniGB-UTF16-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniGB-UTF32-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniGB-UTF32-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniGB-UTF8-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniGB-UTF8-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJIS-UCS2-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJIS-UCS2-HW-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJIS-UCS2-HW-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJIS-UCS2-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJIS-UTF16-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJIS-UTF16-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJIS-UTF32-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJIS-UTF32-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJIS-UTF8-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJIS-UTF8-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJIS2004-UTF16-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJIS2004-UTF16-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJIS2004-UTF32-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJIS2004-UTF32-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJIS2004-UTF8-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJIS2004-UTF8-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJISPro-UCS2-HW-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJISPro-UCS2-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJISPro-UTF8-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJISX0213-UTF32-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJISX0213-UTF32-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJISX02132004-UTF32-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniJISX02132004-UTF32-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniKS-UCS2-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniKS-UCS2-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniKS-UTF16-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniKS-UTF16-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniKS-UTF32-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniKS-UTF32-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniKS-UTF8-H.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/UniKS-UTF8-V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/V.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/cmaps/WP-Symbol.bcmap create mode 100644 frontend/web/static/lib/pdfjs/web/debugger.css create mode 100644 frontend/web/static/lib/pdfjs/web/images/altText_add.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/altText_disclaimer.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/altText_done.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/altText_spinner.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/altText_warning.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/annotation-check.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/annotation-comment.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/annotation-help.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/annotation-insert.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/annotation-key.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/annotation-newparagraph.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/annotation-noicon.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/annotation-note.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/annotation-paperclip.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/annotation-paragraph.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/annotation-pushpin.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/cursor-editorFreeHighlight.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/cursor-editorFreeText.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/cursor-editorInk.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/cursor-editorTextHighlight.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/editor-toolbar-delete.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/findbarButton-next.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/findbarButton-previous.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/gv-toolbarButton-download.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/loading-icon.gif create mode 100644 frontend/web/static/lib/pdfjs/web/images/loading.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/messageBar_closingButton.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/messageBar_warning.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/secondaryToolbarButton-documentProperties.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/secondaryToolbarButton-firstPage.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/secondaryToolbarButton-handTool.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/secondaryToolbarButton-lastPage.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/secondaryToolbarButton-rotateCcw.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/secondaryToolbarButton-rotateCw.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/secondaryToolbarButton-scrollHorizontal.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/secondaryToolbarButton-scrollPage.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/secondaryToolbarButton-scrollVertical.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/secondaryToolbarButton-scrollWrapped.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/secondaryToolbarButton-selectTool.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/secondaryToolbarButton-spreadEven.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/secondaryToolbarButton-spreadNone.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/secondaryToolbarButton-spreadOdd.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-bookmark.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-currentOutlineItem.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-download.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-editorFreeText.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-editorHighlight.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-editorInk.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-editorStamp.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-menuArrow.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-openFile.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-pageDown.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-pageUp.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-presentationMode.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-print.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-search.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-secondaryToolbarToggle.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-sidebarToggle.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-viewAttachments.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-viewLayers.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-viewOutline.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-viewThumbnail.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-zoomIn.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/toolbarButton-zoomOut.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/treeitem-collapsed.svg create mode 100644 frontend/web/static/lib/pdfjs/web/images/treeitem-expanded.svg create mode 100644 frontend/web/static/lib/pdfjs/web/locale/ach/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/af/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/an/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/ar/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/ast/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/az/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/be/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/bg/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/bn/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/bo/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/br/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/brx/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/bs/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/ca/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/cak/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/ckb/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/cs/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/cy/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/da/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/de/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/dsb/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/el/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/en-CA/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/en-GB/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/en-US/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/eo/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/es-AR/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/es-CL/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/es-ES/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/es-MX/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/et/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/eu/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/fa/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/ff/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/fi/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/fr/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/fur/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/fy-NL/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/ga-IE/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/gd/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/gl/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/gn/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/gu-IN/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/he/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/hi-IN/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/hr/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/hsb/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/hu/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/hy-AM/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/hye/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/ia/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/id/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/is/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/it/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/ja/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/ka/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/kab/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/kk/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/km/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/kn/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/ko/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/lij/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/lo/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/locale.json create mode 100644 frontend/web/static/lib/pdfjs/web/locale/lt/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/ltg/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/lv/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/meh/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/mk/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/mr/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/ms/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/my/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/nb-NO/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/ne-NP/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/nl/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/nn-NO/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/oc/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/pa-IN/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/pl/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/pt-BR/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/pt-PT/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/rm/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/ro/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/ru/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/sat/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/sc/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/scn/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/sco/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/si/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/sk/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/skr/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/sl/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/son/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/sq/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/sr/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/sv-SE/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/szl/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/ta/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/te/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/tg/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/th/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/tl/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/tr/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/trs/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/uk/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/ur/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/uz/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/vi/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/wo/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/xh/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/zh-CN/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/locale/zh-TW/viewer.ftl create mode 100644 frontend/web/static/lib/pdfjs/web/viewer.css create mode 100644 frontend/web/static/lib/pdfjs/web/viewer.html create mode 100644 frontend/web/static/lib/popper/popper.js create mode 100644 frontend/web/static/lib/prismjs/themes/default.css create mode 100644 frontend/web/static/lib/prismjs/themes/okaida.css create mode 100644 frontend/web/static/lib/qunit/qunit-2.9.1.css create mode 100644 frontend/web/static/lib/stacktracejs/LICENSE create mode 100644 frontend/web/static/lib/zxing-library/LICENSE create mode 100644 frontend/web/static/lib/zxing-library/version create mode 100644 frontend/web/static/src/core/action_swiper/action_swiper.js create mode 100644 frontend/web/static/src/core/action_swiper/action_swiper.scss create mode 100644 frontend/web/static/src/core/action_swiper/action_swiper.xml create mode 100644 frontend/web/static/src/core/anchor_scroll_prevention.js create mode 100644 frontend/web/static/src/core/assets.js create mode 100644 frontend/web/static/src/core/autocomplete/autocomplete.js create mode 100644 frontend/web/static/src/core/autocomplete/autocomplete.scss create mode 100644 frontend/web/static/src/core/autocomplete/autocomplete.xml create mode 100644 frontend/web/static/src/core/avatar/avatar.scss create mode 100644 frontend/web/static/src/core/avatar/avatar.variables.scss create mode 100644 frontend/web/static/src/core/badge/badge.scss create mode 100644 frontend/web/static/src/core/barcode/ZXingBarcodeDetector.js create mode 100644 frontend/web/static/src/core/barcode/barcode_dialog.js create mode 100644 frontend/web/static/src/core/barcode/barcode_dialog.scss create mode 100644 frontend/web/static/src/core/barcode/barcode_dialog.xml create mode 100644 frontend/web/static/src/core/barcode/barcode_video_scanner.js create mode 100644 frontend/web/static/src/core/barcode/barcode_video_scanner.xml create mode 100644 frontend/web/static/src/core/barcode/crop_overlay.js create mode 100644 frontend/web/static/src/core/barcode/crop_overlay.scss create mode 100644 frontend/web/static/src/core/barcode/crop_overlay.xml create mode 100644 frontend/web/static/src/core/bottom_sheet/bottom_sheet.js create mode 100644 frontend/web/static/src/core/bottom_sheet/bottom_sheet.scss create mode 100644 frontend/web/static/src/core/bottom_sheet/bottom_sheet.variables.scss create mode 100644 frontend/web/static/src/core/bottom_sheet/bottom_sheet.xml create mode 100644 frontend/web/static/src/core/bottom_sheet/bottom_sheet_service.js create mode 100644 frontend/web/static/src/core/browser/browser.js create mode 100644 frontend/web/static/src/core/browser/cookie.js create mode 100644 frontend/web/static/src/core/browser/feature_detection.js create mode 100644 frontend/web/static/src/core/browser/router.js create mode 100644 frontend/web/static/src/core/browser/title_service.js create mode 100644 frontend/web/static/src/core/checkbox/checkbox.js create mode 100644 frontend/web/static/src/core/checkbox/checkbox.scss create mode 100644 frontend/web/static/src/core/checkbox/checkbox.xml create mode 100644 frontend/web/static/src/core/code_editor/code_editor.js create mode 100644 frontend/web/static/src/core/code_editor/code_editor.xml create mode 100644 frontend/web/static/src/core/color_picker/color_picker.js create mode 100644 frontend/web/static/src/core/color_picker/color_picker.scss create mode 100644 frontend/web/static/src/core/color_picker/color_picker.xml create mode 100644 frontend/web/static/src/core/color_picker/custom_color_picker/custom_color_picker.js create mode 100644 frontend/web/static/src/core/color_picker/custom_color_picker/custom_color_picker.scss create mode 100644 frontend/web/static/src/core/color_picker/custom_color_picker/custom_color_picker.xml create mode 100644 frontend/web/static/src/core/color_picker/tabs/color_picker_custom_tab.js create mode 100644 frontend/web/static/src/core/color_picker/tabs/color_picker_custom_tab.xml create mode 100644 frontend/web/static/src/core/color_picker/tabs/color_picker_solid_tab.js create mode 100644 frontend/web/static/src/core/color_picker/tabs/color_picker_solid_tab.xml create mode 100644 frontend/web/static/src/core/colorlist/colorlist.js create mode 100644 frontend/web/static/src/core/colorlist/colorlist.scss create mode 100644 frontend/web/static/src/core/colorlist/colorlist.xml create mode 100644 frontend/web/static/src/core/colors/colors.js create mode 100644 frontend/web/static/src/core/commands/command_category.js create mode 100644 frontend/web/static/src/core/commands/command_hook.js create mode 100644 frontend/web/static/src/core/commands/command_items.xml create mode 100644 frontend/web/static/src/core/commands/command_palette.js create mode 100644 frontend/web/static/src/core/commands/command_palette.scss create mode 100644 frontend/web/static/src/core/commands/command_palette.xml create mode 100644 frontend/web/static/src/core/commands/command_service.js create mode 100644 frontend/web/static/src/core/commands/default_providers.js create mode 100644 frontend/web/static/src/core/confirmation_dialog/confirmation_dialog.js create mode 100644 frontend/web/static/src/core/confirmation_dialog/confirmation_dialog.xml create mode 100644 frontend/web/static/src/core/context.js create mode 100644 frontend/web/static/src/core/copy_button/copy_button.js create mode 100644 frontend/web/static/src/core/copy_button/copy_button.xml create mode 100644 frontend/web/static/src/core/currency.js create mode 100644 frontend/web/static/src/core/datetime/datetime_input.js create mode 100644 frontend/web/static/src/core/datetime/datetime_input.xml create mode 100644 frontend/web/static/src/core/datetime/datetime_picker.js create mode 100644 frontend/web/static/src/core/datetime/datetime_picker.scss create mode 100644 frontend/web/static/src/core/datetime/datetime_picker.xml create mode 100644 frontend/web/static/src/core/datetime/datetime_picker_hook.js create mode 100644 frontend/web/static/src/core/datetime/datetime_picker_popover.js create mode 100644 frontend/web/static/src/core/datetime/datetime_picker_popover.xml create mode 100644 frontend/web/static/src/core/datetime/datetimepicker_service.js create mode 100644 frontend/web/static/src/core/debug/debug_context.js create mode 100644 frontend/web/static/src/core/debug/debug_menu.js create mode 100644 frontend/web/static/src/core/debug/debug_menu.scss create mode 100644 frontend/web/static/src/core/debug/debug_menu.xml create mode 100644 frontend/web/static/src/core/debug/debug_menu_basic.js create mode 100644 frontend/web/static/src/core/debug/debug_menu_items.js create mode 100644 frontend/web/static/src/core/debug/debug_menu_items.xml create mode 100644 frontend/web/static/src/core/debug/debug_providers.js create mode 100644 frontend/web/static/src/core/debug/debug_utils.js create mode 100644 frontend/web/static/src/core/dialog/dialog.js create mode 100644 frontend/web/static/src/core/dialog/dialog.scss create mode 100644 frontend/web/static/src/core/dialog/dialog.xml create mode 100644 frontend/web/static/src/core/dialog/dialog_service.js create mode 100644 frontend/web/static/src/core/domain.js create mode 100644 frontend/web/static/src/core/domain_selector/domain_selector.js create mode 100644 frontend/web/static/src/core/domain_selector/domain_selector.xml create mode 100644 frontend/web/static/src/core/domain_selector/domain_selector_operator_editor.js create mode 100644 frontend/web/static/src/core/domain_selector/utils.js create mode 100644 frontend/web/static/src/core/domain_selector_dialog/domain_selector_dialog.js create mode 100644 frontend/web/static/src/core/domain_selector_dialog/domain_selector_dialog.xml create mode 100644 frontend/web/static/src/core/dropdown/_behaviours/dropdown_group_hook.js create mode 100644 frontend/web/static/src/core/dropdown/_behaviours/dropdown_nesting.js create mode 100644 frontend/web/static/src/core/dropdown/_behaviours/dropdown_popover.js create mode 100644 frontend/web/static/src/core/dropdown/accordion_item.js create mode 100644 frontend/web/static/src/core/dropdown/accordion_item.scss create mode 100644 frontend/web/static/src/core/dropdown/accordion_item.xml create mode 100644 frontend/web/static/src/core/dropdown/checkbox_item.js create mode 100644 frontend/web/static/src/core/dropdown/dropdown.js create mode 100644 frontend/web/static/src/core/dropdown/dropdown.scss create mode 100644 frontend/web/static/src/core/dropdown/dropdown_group.js create mode 100644 frontend/web/static/src/core/dropdown/dropdown_hooks.js create mode 100644 frontend/web/static/src/core/dropdown/dropdown_item.js create mode 100644 frontend/web/static/src/core/dropdown/dropdown_item.xml create mode 100644 frontend/web/static/src/core/dropzone/dropzone.js create mode 100644 frontend/web/static/src/core/dropzone/dropzone.scss create mode 100644 frontend/web/static/src/core/dropzone/dropzone.xml create mode 100644 frontend/web/static/src/core/dropzone/dropzone_hook.js create mode 100644 frontend/web/static/src/core/effects/effect_service.js create mode 100644 frontend/web/static/src/core/effects/rainbow_man.js create mode 100644 frontend/web/static/src/core/effects/rainbow_man.scss create mode 100644 frontend/web/static/src/core/effects/rainbow_man.xml create mode 100644 frontend/web/static/src/core/emoji_picker/emoji_data.js create mode 100644 frontend/web/static/src/core/emoji_picker/emoji_picker.dark.scss create mode 100644 frontend/web/static/src/core/emoji_picker/emoji_picker.js create mode 100644 frontend/web/static/src/core/emoji_picker/emoji_picker.scss create mode 100644 frontend/web/static/src/core/emoji_picker/emoji_picker.xml create mode 100644 frontend/web/static/src/core/emoji_picker/frequent_emoji_service.js create mode 100644 frontend/web/static/src/core/ensure_jquery.js create mode 100644 frontend/web/static/src/core/errors/error_dialog.scss create mode 100644 frontend/web/static/src/core/errors/error_dialogs.js create mode 100644 frontend/web/static/src/core/errors/error_dialogs.xml create mode 100644 frontend/web/static/src/core/errors/error_handlers.js create mode 100644 frontend/web/static/src/core/errors/error_service.js create mode 100644 frontend/web/static/src/core/errors/error_utils.js create mode 100644 frontend/web/static/src/core/errors/scss_error_dialog.js create mode 100644 frontend/web/static/src/core/expression_editor/expression_editor.js create mode 100644 frontend/web/static/src/core/expression_editor/expression_editor.xml create mode 100644 frontend/web/static/src/core/expression_editor/expression_editor_operator_editor.js create mode 100644 frontend/web/static/src/core/expression_editor_dialog/expression_editor_dialog.js create mode 100644 frontend/web/static/src/core/expression_editor_dialog/expression_editor_dialog.xml create mode 100644 frontend/web/static/src/core/field_service.js create mode 100644 frontend/web/static/src/core/file_input/file_input.js create mode 100644 frontend/web/static/src/core/file_input/file_input.xml create mode 100644 frontend/web/static/src/core/file_upload/file_upload_progress_bar.js create mode 100644 frontend/web/static/src/core/file_upload/file_upload_progress_bar.scss create mode 100644 frontend/web/static/src/core/file_upload/file_upload_progress_bar.xml create mode 100644 frontend/web/static/src/core/file_upload/file_upload_progress_container.js create mode 100644 frontend/web/static/src/core/file_upload/file_upload_progress_container.xml create mode 100644 frontend/web/static/src/core/file_upload/file_upload_progress_record.js create mode 100644 frontend/web/static/src/core/file_upload/file_upload_progress_record.scss create mode 100644 frontend/web/static/src/core/file_upload/file_upload_progress_record.xml create mode 100644 frontend/web/static/src/core/file_upload/file_upload_service.js create mode 100644 frontend/web/static/src/core/file_viewer/file_model.js create mode 100644 frontend/web/static/src/core/file_viewer/file_viewer.dark.scss create mode 100644 frontend/web/static/src/core/file_viewer/file_viewer.js create mode 100644 frontend/web/static/src/core/file_viewer/file_viewer.scss create mode 100644 frontend/web/static/src/core/file_viewer/file_viewer.xml create mode 100644 frontend/web/static/src/core/file_viewer/file_viewer_hook.js create mode 100644 frontend/web/static/src/core/hotkeys/hotkey_hook.js create mode 100644 frontend/web/static/src/core/hotkeys/hotkey_service.js create mode 100644 frontend/web/static/src/core/install_scoped_app/install_scoped_app.js create mode 100644 frontend/web/static/src/core/install_scoped_app/install_scoped_app.xml create mode 100644 frontend/web/static/src/core/ir_ui_view_code_editor/code_editor.js create mode 100644 frontend/web/static/src/core/ir_ui_view_code_editor/code_editor.scss create mode 100644 frontend/web/static/src/core/l10n/dates.js create mode 100644 frontend/web/static/src/core/l10n/localization.js create mode 100644 frontend/web/static/src/core/l10n/localization_service.js create mode 100644 frontend/web/static/src/core/l10n/time.js create mode 100644 frontend/web/static/src/core/l10n/translation.js create mode 100644 frontend/web/static/src/core/l10n/utils.js create mode 100644 frontend/web/static/src/core/l10n/utils/format_list.js create mode 100644 frontend/web/static/src/core/l10n/utils/locales.js create mode 100644 frontend/web/static/src/core/l10n/utils/normalize.js create mode 100644 frontend/web/static/src/core/macro.js create mode 100644 frontend/web/static/src/core/main_components_container.js create mode 100644 frontend/web/static/src/core/model_field_selector/model_field_selector.js create mode 100644 frontend/web/static/src/core/model_field_selector/model_field_selector.scss create mode 100644 frontend/web/static/src/core/model_field_selector/model_field_selector.xml create mode 100644 frontend/web/static/src/core/model_field_selector/model_field_selector_popover.js create mode 100644 frontend/web/static/src/core/model_field_selector/model_field_selector_popover.scss create mode 100644 frontend/web/static/src/core/model_field_selector/model_field_selector_popover.xml create mode 100644 frontend/web/static/src/core/model_selector/model_selector.js create mode 100644 frontend/web/static/src/core/model_selector/model_selector.scss create mode 100644 frontend/web/static/src/core/model_selector/model_selector.xml create mode 100644 frontend/web/static/src/core/name_service.js create mode 100644 frontend/web/static/src/core/navigation/navigation.js create mode 100644 frontend/web/static/src/core/network/download.js create mode 100644 frontend/web/static/src/core/network/http_service.js create mode 100644 frontend/web/static/src/core/network/rpc.js create mode 100644 frontend/web/static/src/core/network/rpc_cache.js create mode 100644 frontend/web/static/src/core/notebook/notebook.js create mode 100644 frontend/web/static/src/core/notebook/notebook.scss create mode 100644 frontend/web/static/src/core/notebook/notebook.xml create mode 100644 frontend/web/static/src/core/notifications/notification.js create mode 100644 frontend/web/static/src/core/notifications/notification.scss create mode 100644 frontend/web/static/src/core/notifications/notification.variables.scss create mode 100644 frontend/web/static/src/core/notifications/notification.xml create mode 100644 frontend/web/static/src/core/notifications/notification_container.js create mode 100644 frontend/web/static/src/core/notifications/notification_service.js create mode 100644 frontend/web/static/src/core/orm_service.js create mode 100644 frontend/web/static/src/core/overlay/overlay_container.js create mode 100644 frontend/web/static/src/core/overlay/overlay_container.scss create mode 100644 frontend/web/static/src/core/overlay/overlay_container.xml create mode 100644 frontend/web/static/src/core/overlay/overlay_service.js create mode 100644 frontend/web/static/src/core/pager/pager.js create mode 100644 frontend/web/static/src/core/pager/pager.xml create mode 100644 frontend/web/static/src/core/pager/pager_indicator.js create mode 100644 frontend/web/static/src/core/pager/pager_indicator.scss create mode 100644 frontend/web/static/src/core/pager/pager_indicator.xml create mode 100644 frontend/web/static/src/core/popover/popover.js create mode 100644 frontend/web/static/src/core/popover/popover.scss create mode 100644 frontend/web/static/src/core/popover/popover.xml create mode 100644 frontend/web/static/src/core/popover/popover_hook.js create mode 100644 frontend/web/static/src/core/popover/popover_service.js create mode 100644 frontend/web/static/src/core/position/position_hook.js create mode 100644 frontend/web/static/src/core/position/utils.js create mode 100644 frontend/web/static/src/core/pwa/install_prompt.js create mode 100644 frontend/web/static/src/core/pwa/install_prompt.scss create mode 100644 frontend/web/static/src/core/pwa/install_prompt.xml create mode 100644 frontend/web/static/src/core/pwa/pwa_service.js create mode 100644 frontend/web/static/src/core/py_js/py.js create mode 100644 frontend/web/static/src/core/py_js/py_builtin.js create mode 100644 frontend/web/static/src/core/py_js/py_date.js create mode 100644 frontend/web/static/src/core/py_js/py_interpreter.js create mode 100644 frontend/web/static/src/core/py_js/py_parser.js create mode 100644 frontend/web/static/src/core/py_js/py_tokenizer.js create mode 100644 frontend/web/static/src/core/py_js/py_utils.js create mode 100644 frontend/web/static/src/core/record_selectors/multi_record_selector.js create mode 100644 frontend/web/static/src/core/record_selectors/multi_record_selector.xml create mode 100644 frontend/web/static/src/core/record_selectors/record_autocomplete.js create mode 100644 frontend/web/static/src/core/record_selectors/record_autocomplete.xml create mode 100644 frontend/web/static/src/core/record_selectors/record_selector.js create mode 100644 frontend/web/static/src/core/record_selectors/record_selector.xml create mode 100644 frontend/web/static/src/core/record_selectors/record_selectors.scss create mode 100644 frontend/web/static/src/core/record_selectors/tag_navigation_hook.js create mode 100644 frontend/web/static/src/core/registry.js create mode 100644 frontend/web/static/src/core/registry_hook.js create mode 100644 frontend/web/static/src/core/resizable_panel/resizable_panel.js create mode 100644 frontend/web/static/src/core/resizable_panel/resizable_panel.scss create mode 100644 frontend/web/static/src/core/resizable_panel/resizable_panel.xml create mode 100644 frontend/web/static/src/core/select_menu/select_menu.js create mode 100644 frontend/web/static/src/core/select_menu/select_menu.scss create mode 100644 frontend/web/static/src/core/select_menu/select_menu.xml create mode 100644 frontend/web/static/src/core/signature/name_and_signature.js create mode 100644 frontend/web/static/src/core/signature/name_and_signature.scss create mode 100644 frontend/web/static/src/core/signature/name_and_signature.xml create mode 100644 frontend/web/static/src/core/signature/signature_dialog.js create mode 100644 frontend/web/static/src/core/signature/signature_dialog.xml create mode 100644 frontend/web/static/src/core/tags_list/tags_list.js create mode 100644 frontend/web/static/src/core/tags_list/tags_list.scss create mode 100644 frontend/web/static/src/core/tags_list/tags_list.xml create mode 100644 frontend/web/static/src/core/template_inheritance.js create mode 100644 frontend/web/static/src/core/templates.js create mode 100644 frontend/web/static/src/core/time_picker/time_picker.js create mode 100644 frontend/web/static/src/core/time_picker/time_picker.scss create mode 100644 frontend/web/static/src/core/time_picker/time_picker.xml create mode 100644 frontend/web/static/src/core/tooltip/tooltip.js create mode 100644 frontend/web/static/src/core/tooltip/tooltip.scss create mode 100644 frontend/web/static/src/core/tooltip/tooltip.xml create mode 100644 frontend/web/static/src/core/tooltip/tooltip_hook.js create mode 100644 frontend/web/static/src/core/tooltip/tooltip_service.js create mode 100644 frontend/web/static/src/core/transition.js create mode 100644 frontend/web/static/src/core/tree_editor/ast_utils.js create mode 100644 frontend/web/static/src/core/tree_editor/condition_tree.js create mode 100644 frontend/web/static/src/core/tree_editor/construct_domain_from_tree.js create mode 100644 frontend/web/static/src/core/tree_editor/construct_expression_from_tree.js create mode 100644 frontend/web/static/src/core/tree_editor/construct_tree_from_domain.js create mode 100644 frontend/web/static/src/core/tree_editor/construct_tree_from_expression.js create mode 100644 frontend/web/static/src/core/tree_editor/domain_contains_expressions.js create mode 100644 frontend/web/static/src/core/tree_editor/domain_from_tree.js create mode 100644 frontend/web/static/src/core/tree_editor/expression_from_tree.js create mode 100644 frontend/web/static/src/core/tree_editor/operators.js create mode 100644 frontend/web/static/src/core/tree_editor/tree_editor.js create mode 100644 frontend/web/static/src/core/tree_editor/tree_editor.scss create mode 100644 frontend/web/static/src/core/tree_editor/tree_editor.xml create mode 100644 frontend/web/static/src/core/tree_editor/tree_editor_autocomplete.js create mode 100644 frontend/web/static/src/core/tree_editor/tree_editor_components.js create mode 100644 frontend/web/static/src/core/tree_editor/tree_editor_components.xml create mode 100644 frontend/web/static/src/core/tree_editor/tree_editor_operator_editor.js create mode 100644 frontend/web/static/src/core/tree_editor/tree_editor_value_editors.js create mode 100644 frontend/web/static/src/core/tree_editor/tree_from_domain.js create mode 100644 frontend/web/static/src/core/tree_editor/tree_from_expression.js create mode 100644 frontend/web/static/src/core/tree_editor/tree_processor.js create mode 100644 frontend/web/static/src/core/tree_editor/utils.js create mode 100644 frontend/web/static/src/core/tree_editor/virtual_operators.js create mode 100644 frontend/web/static/src/core/ui/block_ui.js create mode 100644 frontend/web/static/src/core/ui/block_ui.scss create mode 100644 frontend/web/static/src/core/ui/block_ui.xml create mode 100644 frontend/web/static/src/core/ui/ui_service.js create mode 100644 frontend/web/static/src/core/user.js create mode 100644 frontend/web/static/src/core/user_switch/user_switch.js create mode 100644 frontend/web/static/src/core/user_switch/user_switch.xml create mode 100644 frontend/web/static/src/core/utils/arrays.js create mode 100644 frontend/web/static/src/core/utils/autoresize.js create mode 100644 frontend/web/static/src/core/utils/binary.js create mode 100644 frontend/web/static/src/core/utils/cache.js create mode 100644 frontend/web/static/src/core/utils/classname.js create mode 100644 frontend/web/static/src/core/utils/colors.js create mode 100644 frontend/web/static/src/core/utils/components.js create mode 100644 frontend/web/static/src/core/utils/concurrency.js create mode 100644 frontend/web/static/src/core/utils/draggable.js create mode 100644 frontend/web/static/src/core/utils/draggable_hook_builder.js create mode 100644 frontend/web/static/src/core/utils/draggable_hook_builder.scss create mode 100644 frontend/web/static/src/core/utils/draggable_hook_builder_owl.js create mode 100644 frontend/web/static/src/core/utils/dvu.js create mode 100644 frontend/web/static/src/core/utils/files.js create mode 100644 frontend/web/static/src/core/utils/functions.js create mode 100644 frontend/web/static/src/core/utils/hooks.js create mode 100644 frontend/web/static/src/core/utils/html.js create mode 100644 frontend/web/static/src/core/utils/indexed_db.js create mode 100644 frontend/web/static/src/core/utils/misc.js create mode 100644 frontend/web/static/src/core/utils/nested_sortable.js create mode 100644 frontend/web/static/src/core/utils/nested_sortable.scss create mode 100644 frontend/web/static/src/core/utils/numbers.js create mode 100644 frontend/web/static/src/core/utils/objects.js create mode 100644 frontend/web/static/src/core/utils/patch.js create mode 100644 frontend/web/static/src/core/utils/pdfjs.js create mode 100644 frontend/web/static/src/core/utils/reactive.js create mode 100644 frontend/web/static/src/core/utils/render.js create mode 100644 frontend/web/static/src/core/utils/scrolling.js create mode 100644 frontend/web/static/src/core/utils/search.js create mode 100644 frontend/web/static/src/core/utils/sortable.js create mode 100644 frontend/web/static/src/core/utils/sortable_owl.js create mode 100644 frontend/web/static/src/core/utils/sortable_service.js create mode 100644 frontend/web/static/src/core/utils/strings.js create mode 100644 frontend/web/static/src/core/utils/timing.js create mode 100644 frontend/web/static/src/core/utils/transitions.scss create mode 100644 frontend/web/static/src/core/utils/ui.js create mode 100644 frontend/web/static/src/core/utils/urls.js create mode 100644 frontend/web/static/src/core/utils/xml.js create mode 100644 frontend/web/static/src/core/virtual_grid_hook.js create mode 100644 frontend/web/static/src/env.js create mode 100644 frontend/web/static/src/libs/bootstrap.js create mode 100644 frontend/web/static/src/libs/fontawesome/css/font-awesome.css create mode 100644 frontend/web/static/src/main.js create mode 100644 frontend/web/static/src/model/model.js create mode 100644 frontend/web/static/src/model/record.js create mode 100644 frontend/web/static/src/model/relational_model/datapoint.js create mode 100644 frontend/web/static/src/model/relational_model/dynamic_group_list.js create mode 100644 frontend/web/static/src/model/relational_model/dynamic_list.js create mode 100644 frontend/web/static/src/model/relational_model/dynamic_record_list.js create mode 100644 frontend/web/static/src/model/relational_model/errors.js create mode 100644 frontend/web/static/src/model/relational_model/group.js create mode 100644 frontend/web/static/src/model/relational_model/operation.js create mode 100644 frontend/web/static/src/model/relational_model/record.js create mode 100644 frontend/web/static/src/model/relational_model/relational_model.js create mode 100644 frontend/web/static/src/model/relational_model/static_list.js create mode 100644 frontend/web/static/src/model/relational_model/utils.js create mode 100644 frontend/web/static/src/model/sample_server.js create mode 100644 frontend/web/static/src/module_loader.js create mode 100644 frontend/web/static/src/polyfills/array.js create mode 100644 frontend/web/static/src/polyfills/clipboard.js create mode 100644 frontend/web/static/src/polyfills/object.js create mode 100644 frontend/web/static/src/polyfills/promise.js create mode 100644 frontend/web/static/src/polyfills/set.js create mode 100644 frontend/web/static/src/scss/ace.scss create mode 100644 frontend/web/static/src/scss/animation.scss create mode 100644 frontend/web/static/src/scss/base_document_layout.scss create mode 100644 frontend/web/static/src/scss/bootstrap_overridden.scss create mode 100644 frontend/web/static/src/scss/bootstrap_review.scss create mode 100644 frontend/web/static/src/scss/bootstrap_review_backend.scss create mode 100644 frontend/web/static/src/scss/bs_mixins_overrides.scss create mode 100644 frontend/web/static/src/scss/bs_mixins_overrides_backend.scss create mode 100644 frontend/web/static/src/scss/fontawesome_overridden.scss create mode 100644 frontend/web/static/src/scss/functions.scss create mode 100644 frontend/web/static/src/scss/import_bootstrap.scss create mode 100644 frontend/web/static/src/scss/mimetypes.scss create mode 100644 frontend/web/static/src/scss/mixins_forwardport.scss create mode 100644 frontend/web/static/src/scss/pre_variables.scss create mode 100644 frontend/web/static/src/scss/primary_variables.scss create mode 100644 frontend/web/static/src/scss/secondary_variables.scss create mode 100644 frontend/web/static/src/scss/ui.scss create mode 100644 frontend/web/static/src/scss/utilities_custom.scss create mode 100644 frontend/web/static/src/scss/utils.scss create mode 100644 frontend/web/static/src/search/action_hook.js create mode 100644 frontend/web/static/src/search/action_menus/action_menus.js create mode 100644 frontend/web/static/src/search/action_menus/action_menus.xml create mode 100644 frontend/web/static/src/search/breadcrumbs/breadcrumbs.js create mode 100644 frontend/web/static/src/search/breadcrumbs/breadcrumbs.xml create mode 100644 frontend/web/static/src/search/cog_menu/cog_menu.js create mode 100644 frontend/web/static/src/search/cog_menu/cog_menu.scss create mode 100644 frontend/web/static/src/search/cog_menu/cog_menu.xml create mode 100644 frontend/web/static/src/search/control_panel/control_panel.js create mode 100644 frontend/web/static/src/search/control_panel/control_panel.scss create mode 100644 frontend/web/static/src/search/control_panel/control_panel.variables.scss create mode 100644 frontend/web/static/src/search/control_panel/control_panel.variables_print.scss create mode 100644 frontend/web/static/src/search/control_panel/control_panel.xml create mode 100644 frontend/web/static/src/search/control_panel/control_panel_mobile.css create mode 100644 frontend/web/static/src/search/custom_favorite_item/custom_favorite_item.js create mode 100644 frontend/web/static/src/search/custom_favorite_item/custom_favorite_item.xml create mode 100644 frontend/web/static/src/search/custom_group_by_item/custom_group_by_item.js create mode 100644 frontend/web/static/src/search/custom_group_by_item/custom_group_by_item.scss create mode 100644 frontend/web/static/src/search/custom_group_by_item/custom_group_by_item.xml create mode 100644 frontend/web/static/src/search/layout.js create mode 100644 frontend/web/static/src/search/layout.xml create mode 100644 frontend/web/static/src/search/pager_hook.js create mode 100644 frontend/web/static/src/search/properties_group_by_item/properties_group_by_item.js create mode 100644 frontend/web/static/src/search/properties_group_by_item/properties_group_by_item.xml create mode 100644 frontend/web/static/src/search/search_arch_parser.js create mode 100644 frontend/web/static/src/search/search_bar/search_bar.js create mode 100644 frontend/web/static/src/search/search_bar/search_bar.scss create mode 100644 frontend/web/static/src/search/search_bar/search_bar.variables.scss create mode 100644 frontend/web/static/src/search/search_bar/search_bar.xml create mode 100644 frontend/web/static/src/search/search_bar/search_bar_toggler.js create mode 100644 frontend/web/static/src/search/search_bar/search_bar_toggler.xml create mode 100644 frontend/web/static/src/search/search_bar_menu/search_bar_menu.js create mode 100644 frontend/web/static/src/search/search_bar_menu/search_bar_menu.scss create mode 100644 frontend/web/static/src/search/search_bar_menu/search_bar_menu.xml create mode 100644 frontend/web/static/src/search/search_model.js create mode 100644 frontend/web/static/src/search/search_panel/search_panel.js create mode 100644 frontend/web/static/src/search/search_panel/search_panel.scss create mode 100644 frontend/web/static/src/search/search_panel/search_panel.variables.scss create mode 100644 frontend/web/static/src/search/search_panel/search_panel.xml create mode 100644 frontend/web/static/src/search/search_panel/search_view.scss create mode 100644 frontend/web/static/src/search/utils/dates.js create mode 100644 frontend/web/static/src/search/utils/group_by.js create mode 100644 frontend/web/static/src/search/utils/misc.js create mode 100644 frontend/web/static/src/search/utils/order_by.js create mode 100644 frontend/web/static/src/search/with_search/with_search.js create mode 100644 frontend/web/static/src/search/with_search/with_search.xml create mode 100644 frontend/web/static/src/session.js create mode 100644 frontend/web/static/src/start.js create mode 100644 frontend/web/static/src/views/action_helper.js create mode 100644 frontend/web/static/src/views/action_helper.xml create mode 100644 frontend/web/static/src/views/calendar/calendar_arch_parser.js create mode 100644 frontend/web/static/src/views/calendar/calendar_common/calendar_common_popover.js create mode 100644 frontend/web/static/src/views/calendar/calendar_common/calendar_common_popover.scss create mode 100644 frontend/web/static/src/views/calendar/calendar_common/calendar_common_popover.xml create mode 100644 frontend/web/static/src/views/calendar/calendar_common/calendar_common_renderer.js create mode 100644 frontend/web/static/src/views/calendar/calendar_common/calendar_common_renderer.xml create mode 100644 frontend/web/static/src/views/calendar/calendar_common/calendar_common_week_column.js create mode 100644 frontend/web/static/src/views/calendar/calendar_controller.js create mode 100644 frontend/web/static/src/views/calendar/calendar_controller.scss create mode 100644 frontend/web/static/src/views/calendar/calendar_controller.xml create mode 100644 frontend/web/static/src/views/calendar/calendar_controller_mobile.scss create mode 100644 frontend/web/static/src/views/calendar/calendar_filter_section/calendar_filter_section.js create mode 100644 frontend/web/static/src/views/calendar/calendar_filter_section/calendar_filter_section.scss create mode 100644 frontend/web/static/src/views/calendar/calendar_filter_section/calendar_filter_section.xml create mode 100644 frontend/web/static/src/views/calendar/calendar_model.js create mode 100644 frontend/web/static/src/views/calendar/calendar_renderer.dark.scss create mode 100644 frontend/web/static/src/views/calendar/calendar_renderer.js create mode 100644 frontend/web/static/src/views/calendar/calendar_renderer.scss create mode 100644 frontend/web/static/src/views/calendar/calendar_renderer.xml create mode 100644 frontend/web/static/src/views/calendar/calendar_renderer_mobile.scss create mode 100644 frontend/web/static/src/views/calendar/calendar_side_panel/calendar_side_panel.js create mode 100644 frontend/web/static/src/views/calendar/calendar_side_panel/calendar_side_panel.scss create mode 100644 frontend/web/static/src/views/calendar/calendar_side_panel/calendar_side_panel.xml create mode 100644 frontend/web/static/src/views/calendar/calendar_view.js create mode 100644 frontend/web/static/src/views/calendar/calendar_year/calendar_year_popover.js create mode 100644 frontend/web/static/src/views/calendar/calendar_year/calendar_year_popover.scss create mode 100644 frontend/web/static/src/views/calendar/calendar_year/calendar_year_popover.xml create mode 100644 frontend/web/static/src/views/calendar/calendar_year/calendar_year_renderer.js create mode 100644 frontend/web/static/src/views/calendar/calendar_year/calendar_year_renderer.xml create mode 100644 frontend/web/static/src/views/calendar/hooks/calendar_popover_hook.js create mode 100644 frontend/web/static/src/views/calendar/hooks/full_calendar_hook.js create mode 100644 frontend/web/static/src/views/calendar/hooks/square_selection_hook.js create mode 100644 frontend/web/static/src/views/calendar/mobile_filter_panel/calendar_mobile_filter_panel.js create mode 100644 frontend/web/static/src/views/calendar/mobile_filter_panel/calendar_mobile_filter_panel.xml create mode 100644 frontend/web/static/src/views/calendar/quick_create/calendar_quick_create.js create mode 100644 frontend/web/static/src/views/calendar/quick_create/calendar_quick_create.xml create mode 100644 frontend/web/static/src/views/calendar/utils.js create mode 100644 frontend/web/static/src/views/debug_items.js create mode 100644 frontend/web/static/src/views/fields/ace/ace_field.js create mode 100644 frontend/web/static/src/views/fields/ace/ace_field.scss create mode 100644 frontend/web/static/src/views/fields/ace/ace_field.xml create mode 100644 frontend/web/static/src/views/fields/attachment_image/attachment_image_field.js create mode 100644 frontend/web/static/src/views/fields/attachment_image/attachment_image_field.xml create mode 100644 frontend/web/static/src/views/fields/badge/badge_field.js create mode 100644 frontend/web/static/src/views/fields/badge/badge_field.xml create mode 100644 frontend/web/static/src/views/fields/badge_selection/badge_selection.scss create mode 100644 frontend/web/static/src/views/fields/badge_selection/badge_selection_field.js create mode 100644 frontend/web/static/src/views/fields/badge_selection/badge_selection_field.xml create mode 100644 frontend/web/static/src/views/fields/badge_selection/list_badge_selection_field.js create mode 100644 frontend/web/static/src/views/fields/badge_selection/list_badge_selection_field.xml create mode 100644 frontend/web/static/src/views/fields/badge_selection_with_filter/badge_selection_field_with_filter.js create mode 100644 frontend/web/static/src/views/fields/binary/binary_field.js create mode 100644 frontend/web/static/src/views/fields/binary/binary_field.xml create mode 100644 frontend/web/static/src/views/fields/boolean/boolean_field.js create mode 100644 frontend/web/static/src/views/fields/boolean/boolean_field.xml create mode 100644 frontend/web/static/src/views/fields/boolean_favorite/boolean_favorite_field.js create mode 100644 frontend/web/static/src/views/fields/boolean_favorite/boolean_favorite_field.scss create mode 100644 frontend/web/static/src/views/fields/boolean_favorite/boolean_favorite_field.xml create mode 100644 frontend/web/static/src/views/fields/boolean_icon/boolean_icon_field.js create mode 100644 frontend/web/static/src/views/fields/boolean_icon/boolean_icon_field.xml create mode 100644 frontend/web/static/src/views/fields/boolean_toggle/boolean_toggle_field.js create mode 100644 frontend/web/static/src/views/fields/boolean_toggle/boolean_toggle_field.xml create mode 100644 frontend/web/static/src/views/fields/boolean_toggle/list_boolean_toggle_field.js create mode 100644 frontend/web/static/src/views/fields/boolean_toggle/list_boolean_toggle_field.xml create mode 100644 frontend/web/static/src/views/fields/char/char_field.js create mode 100644 frontend/web/static/src/views/fields/char/char_field.scss create mode 100644 frontend/web/static/src/views/fields/char/char_field.xml create mode 100644 frontend/web/static/src/views/fields/color/color_field.js create mode 100644 frontend/web/static/src/views/fields/color/color_field.xml create mode 100644 frontend/web/static/src/views/fields/color_picker/color_picker_field.js create mode 100644 frontend/web/static/src/views/fields/color_picker/color_picker_field.scss create mode 100644 frontend/web/static/src/views/fields/color_picker/color_picker_field.xml create mode 100644 frontend/web/static/src/views/fields/contact_image/contact_image_field.js create mode 100644 frontend/web/static/src/views/fields/contact_image/contact_image_field.scss create mode 100644 frontend/web/static/src/views/fields/contact_image/contact_image_field.xml create mode 100644 frontend/web/static/src/views/fields/contact_statistics/contact_statistics.js create mode 100644 frontend/web/static/src/views/fields/contact_statistics/contact_statistics.xml create mode 100644 frontend/web/static/src/views/fields/copy_clipboard/copy_clipboard_field.js create mode 100644 frontend/web/static/src/views/fields/copy_clipboard/copy_clipboard_field.scss create mode 100644 frontend/web/static/src/views/fields/copy_clipboard/copy_clipboard_field.xml create mode 100644 frontend/web/static/src/views/fields/datetime/datetime_field.js create mode 100644 frontend/web/static/src/views/fields/datetime/datetime_field.xml create mode 100644 frontend/web/static/src/views/fields/datetime/list_datetime_field.js create mode 100644 frontend/web/static/src/views/fields/domain/domain_field.js create mode 100644 frontend/web/static/src/views/fields/domain/domain_field.xml create mode 100644 frontend/web/static/src/views/fields/dynamic_placeholder_hook.js create mode 100644 frontend/web/static/src/views/fields/dynamic_placeholder_popover.js create mode 100644 frontend/web/static/src/views/fields/dynamic_placeholder_popover.xml create mode 100644 frontend/web/static/src/views/fields/email/email_field.js create mode 100644 frontend/web/static/src/views/fields/email/email_field.scss create mode 100644 frontend/web/static/src/views/fields/email/email_field.xml create mode 100644 frontend/web/static/src/views/fields/field.js create mode 100644 frontend/web/static/src/views/fields/field.xml create mode 100644 frontend/web/static/src/views/fields/field_selector/field_selector_field.js create mode 100644 frontend/web/static/src/views/fields/field_selector/field_selector_field.xml create mode 100644 frontend/web/static/src/views/fields/field_tooltip.js create mode 100644 frontend/web/static/src/views/fields/field_tooltip.xml create mode 100644 frontend/web/static/src/views/fields/fields.scss create mode 100644 frontend/web/static/src/views/fields/file_handler.js create mode 100644 frontend/web/static/src/views/fields/file_handler.xml create mode 100644 frontend/web/static/src/views/fields/float/float_field.js create mode 100644 frontend/web/static/src/views/fields/float/float_field.xml create mode 100644 frontend/web/static/src/views/fields/float_factor/float_factor_field.js create mode 100644 frontend/web/static/src/views/fields/float_time/float_time_field.js create mode 100644 frontend/web/static/src/views/fields/float_time/float_time_field.xml create mode 100644 frontend/web/static/src/views/fields/float_toggle/float_toggle_field.js create mode 100644 frontend/web/static/src/views/fields/float_toggle/float_toggle_field.xml create mode 100644 frontend/web/static/src/views/fields/formatters.js create mode 100644 frontend/web/static/src/views/fields/gauge/gauge_field.js create mode 100644 frontend/web/static/src/views/fields/gauge/gauge_field.xml create mode 100644 frontend/web/static/src/views/fields/google_slide_viewer/google_slide_viewer.js create mode 100644 frontend/web/static/src/views/fields/google_slide_viewer/google_slide_viewer.scss create mode 100644 frontend/web/static/src/views/fields/google_slide_viewer/google_slide_viewer.xml create mode 100644 frontend/web/static/src/views/fields/handle/handle_field.js create mode 100644 frontend/web/static/src/views/fields/handle/handle_field.xml create mode 100644 frontend/web/static/src/views/fields/html/html_field.js create mode 100644 frontend/web/static/src/views/fields/html/html_field.scss create mode 100644 frontend/web/static/src/views/fields/html/html_field.xml create mode 100644 frontend/web/static/src/views/fields/iframe_wrapper/iframe_wrapper_field.js create mode 100644 frontend/web/static/src/views/fields/iframe_wrapper/iframe_wrapper_field.scss create mode 100644 frontend/web/static/src/views/fields/iframe_wrapper/iframe_wrapper_field.xml create mode 100644 frontend/web/static/src/views/fields/image/image_field.js create mode 100644 frontend/web/static/src/views/fields/image/image_field.scss create mode 100644 frontend/web/static/src/views/fields/image/image_field.xml create mode 100644 frontend/web/static/src/views/fields/image_url/image_url_field.js create mode 100644 frontend/web/static/src/views/fields/image_url/image_url_field.xml create mode 100644 frontend/web/static/src/views/fields/input_field_hook.js create mode 100644 frontend/web/static/src/views/fields/integer/integer_field.js create mode 100644 frontend/web/static/src/views/fields/integer/integer_field.xml create mode 100644 frontend/web/static/src/views/fields/ir_ui_view_ace/ace_field.js create mode 100644 frontend/web/static/src/views/fields/ir_ui_view_ace/ace_field.xml create mode 100644 frontend/web/static/src/views/fields/journal_dashboard_graph/journal_dashboard_graph_field.js create mode 100644 frontend/web/static/src/views/fields/journal_dashboard_graph/journal_dashboard_graph_field.scss create mode 100644 frontend/web/static/src/views/fields/journal_dashboard_graph/journal_dashboard_graph_field.xml create mode 100644 frontend/web/static/src/views/fields/json/json_field.js create mode 100644 frontend/web/static/src/views/fields/json/json_field.xml create mode 100644 frontend/web/static/src/views/fields/json_checkboxes/json_checkboxes_field.js create mode 100644 frontend/web/static/src/views/fields/json_checkboxes/json_checkboxes_field.xml create mode 100644 frontend/web/static/src/views/fields/kanban_color_picker/kanban_color_picker_field.js create mode 100644 frontend/web/static/src/views/fields/kanban_color_picker/kanban_color_picker_field.scss create mode 100644 frontend/web/static/src/views/fields/kanban_color_picker/kanban_color_picker_field.xml create mode 100644 frontend/web/static/src/views/fields/label_selection/label_selection_field.js create mode 100644 frontend/web/static/src/views/fields/label_selection/label_selection_field.xml create mode 100644 frontend/web/static/src/views/fields/many2many_binary/many2many_binary_field.js create mode 100644 frontend/web/static/src/views/fields/many2many_binary/many2many_binary_field.scss create mode 100644 frontend/web/static/src/views/fields/many2many_binary/many2many_binary_field.xml create mode 100644 frontend/web/static/src/views/fields/many2many_checkboxes/many2many_checkboxes_field.js create mode 100644 frontend/web/static/src/views/fields/many2many_checkboxes/many2many_checkboxes_field.xml create mode 100644 frontend/web/static/src/views/fields/many2many_tags/kanban_many2many_tags_field.js create mode 100644 frontend/web/static/src/views/fields/many2many_tags/kanban_many2many_tags_field.xml create mode 100644 frontend/web/static/src/views/fields/many2many_tags/many2many_tags_field.js create mode 100644 frontend/web/static/src/views/fields/many2many_tags/many2many_tags_field.scss create mode 100644 frontend/web/static/src/views/fields/many2many_tags/many2many_tags_field.xml create mode 100644 frontend/web/static/src/views/fields/many2many_tags_avatar/many2many_tags_avatar_field.js create mode 100644 frontend/web/static/src/views/fields/many2many_tags_avatar/many2many_tags_avatar_field.scss create mode 100644 frontend/web/static/src/views/fields/many2many_tags_avatar/many2many_tags_avatar_field.xml create mode 100644 frontend/web/static/src/views/fields/many2one/many2one.js create mode 100644 frontend/web/static/src/views/fields/many2one/many2one.xml create mode 100644 frontend/web/static/src/views/fields/many2one/many2one_field.js create mode 100644 frontend/web/static/src/views/fields/many2one/many2one_field.scss create mode 100644 frontend/web/static/src/views/fields/many2one/many2one_field.xml create mode 100644 frontend/web/static/src/views/fields/many2one_avatar/kanban_many2one_avatar_field.js create mode 100644 frontend/web/static/src/views/fields/many2one_avatar/kanban_many2one_avatar_field.xml create mode 100644 frontend/web/static/src/views/fields/many2one_avatar/many2one_avatar_field.js create mode 100644 frontend/web/static/src/views/fields/many2one_avatar/many2one_avatar_field.scss create mode 100644 frontend/web/static/src/views/fields/many2one_avatar/many2one_avatar_field.xml create mode 100644 frontend/web/static/src/views/fields/many2one_barcode/many2one_barcode_field.js create mode 100644 frontend/web/static/src/views/fields/many2one_barcode/many2one_barcode_field.xml create mode 100644 frontend/web/static/src/views/fields/many2one_reference/many2one_reference_field.js create mode 100644 frontend/web/static/src/views/fields/many2one_reference/many2one_reference_field.xml create mode 100644 frontend/web/static/src/views/fields/many2one_reference_integer/many2one_reference_integer_field.js create mode 100644 frontend/web/static/src/views/fields/monetary/monetary_field.js create mode 100644 frontend/web/static/src/views/fields/monetary/monetary_field.scss create mode 100644 frontend/web/static/src/views/fields/monetary/monetary_field.xml create mode 100644 frontend/web/static/src/views/fields/numpad_decimal_hook.js create mode 100644 frontend/web/static/src/views/fields/parsers.js create mode 100644 frontend/web/static/src/views/fields/pdf_viewer/pdf_viewer_field.js create mode 100644 frontend/web/static/src/views/fields/pdf_viewer/pdf_viewer_field.scss create mode 100644 frontend/web/static/src/views/fields/pdf_viewer/pdf_viewer_field.xml create mode 100644 frontend/web/static/src/views/fields/percent_pie/percent_pie_field.js create mode 100644 frontend/web/static/src/views/fields/percent_pie/percent_pie_field.scss create mode 100644 frontend/web/static/src/views/fields/percent_pie/percent_pie_field.xml create mode 100644 frontend/web/static/src/views/fields/percentage/percentage_field.js create mode 100644 frontend/web/static/src/views/fields/percentage/percentage_field.xml create mode 100644 frontend/web/static/src/views/fields/phone/phone_field.js create mode 100644 frontend/web/static/src/views/fields/phone/phone_field.scss create mode 100644 frontend/web/static/src/views/fields/phone/phone_field.xml create mode 100644 frontend/web/static/src/views/fields/priority/priority_field.js create mode 100644 frontend/web/static/src/views/fields/priority/priority_field.scss create mode 100644 frontend/web/static/src/views/fields/priority/priority_field.xml create mode 100644 frontend/web/static/src/views/fields/progress_bar/kanban_progress_bar_field.js create mode 100644 frontend/web/static/src/views/fields/progress_bar/progress_bar_field.js create mode 100644 frontend/web/static/src/views/fields/progress_bar/progress_bar_field.scss create mode 100644 frontend/web/static/src/views/fields/progress_bar/progress_bar_field.xml create mode 100644 frontend/web/static/src/views/fields/properties/calendar_properties_field.js create mode 100644 frontend/web/static/src/views/fields/properties/calendar_properties_field.xml create mode 100644 frontend/web/static/src/views/fields/properties/card_properties_field.js create mode 100644 frontend/web/static/src/views/fields/properties/card_properties_field.scss create mode 100644 frontend/web/static/src/views/fields/properties/card_properties_field.xml create mode 100644 frontend/web/static/src/views/fields/properties/properties_field.js create mode 100644 frontend/web/static/src/views/fields/properties/properties_field.scss create mode 100644 frontend/web/static/src/views/fields/properties/properties_field.xml create mode 100644 frontend/web/static/src/views/fields/properties/property_definition.js create mode 100644 frontend/web/static/src/views/fields/properties/property_definition.scss create mode 100644 frontend/web/static/src/views/fields/properties/property_definition.xml create mode 100644 frontend/web/static/src/views/fields/properties/property_definition_selection.js create mode 100644 frontend/web/static/src/views/fields/properties/property_definition_selection.scss create mode 100644 frontend/web/static/src/views/fields/properties/property_definition_selection.xml create mode 100644 frontend/web/static/src/views/fields/properties/property_tags.js create mode 100644 frontend/web/static/src/views/fields/properties/property_tags.scss create mode 100644 frontend/web/static/src/views/fields/properties/property_tags.xml create mode 100644 frontend/web/static/src/views/fields/properties/property_text.js create mode 100644 frontend/web/static/src/views/fields/properties/property_text.scss create mode 100644 frontend/web/static/src/views/fields/properties/property_text.xml create mode 100644 frontend/web/static/src/views/fields/properties/property_value.js create mode 100644 frontend/web/static/src/views/fields/properties/property_value.scss create mode 100644 frontend/web/static/src/views/fields/properties/property_value.xml create mode 100644 frontend/web/static/src/views/fields/radio/radio_field.js create mode 100644 frontend/web/static/src/views/fields/radio/radio_field.scss create mode 100644 frontend/web/static/src/views/fields/radio/radio_field.xml create mode 100644 frontend/web/static/src/views/fields/reference/reference_field.js create mode 100644 frontend/web/static/src/views/fields/reference/reference_field.xml create mode 100644 frontend/web/static/src/views/fields/relational_utils.js create mode 100644 frontend/web/static/src/views/fields/relational_utils.xml create mode 100644 frontend/web/static/src/views/fields/remaining_days/remaining_days_field.js create mode 100644 frontend/web/static/src/views/fields/remaining_days/remaining_days_field.xml create mode 100644 frontend/web/static/src/views/fields/selection/filterable_selection_field.js create mode 100644 frontend/web/static/src/views/fields/selection/selection_field.js create mode 100644 frontend/web/static/src/views/fields/selection/selection_field.scss create mode 100644 frontend/web/static/src/views/fields/selection/selection_field.xml create mode 100644 frontend/web/static/src/views/fields/signature/signature_field.js create mode 100644 frontend/web/static/src/views/fields/signature/signature_field.scss create mode 100644 frontend/web/static/src/views/fields/signature/signature_field.xml create mode 100644 frontend/web/static/src/views/fields/standard_field_props.js create mode 100644 frontend/web/static/src/views/fields/stat_info/stat_info_field.js create mode 100644 frontend/web/static/src/views/fields/stat_info/stat_info_field.xml create mode 100644 frontend/web/static/src/views/fields/state_selection/state_selection_field.js create mode 100644 frontend/web/static/src/views/fields/state_selection/state_selection_field.scss create mode 100644 frontend/web/static/src/views/fields/state_selection/state_selection_field.xml create mode 100644 frontend/web/static/src/views/fields/statusbar/statusbar_field.js create mode 100644 frontend/web/static/src/views/fields/statusbar/statusbar_field.scss create mode 100644 frontend/web/static/src/views/fields/statusbar/statusbar_field.variables.scss create mode 100644 frontend/web/static/src/views/fields/statusbar/statusbar_field.xml create mode 100644 frontend/web/static/src/views/fields/text/text_field.js create mode 100644 frontend/web/static/src/views/fields/text/text_field.scss create mode 100644 frontend/web/static/src/views/fields/text/text_field.xml create mode 100644 frontend/web/static/src/views/fields/timezone_mismatch/timezone_mismatch_field.js create mode 100644 frontend/web/static/src/views/fields/timezone_mismatch/timezone_mismatch_field.xml create mode 100644 frontend/web/static/src/views/fields/translation_button.js create mode 100644 frontend/web/static/src/views/fields/translation_button.scss create mode 100644 frontend/web/static/src/views/fields/translation_button.variables.scss create mode 100644 frontend/web/static/src/views/fields/translation_button.xml create mode 100644 frontend/web/static/src/views/fields/translation_dialog.js create mode 100644 frontend/web/static/src/views/fields/translation_dialog.scss create mode 100644 frontend/web/static/src/views/fields/translation_dialog.xml create mode 100644 frontend/web/static/src/views/fields/url/url_field.js create mode 100644 frontend/web/static/src/views/fields/url/url_field.scss create mode 100644 frontend/web/static/src/views/fields/url/url_field.xml create mode 100644 frontend/web/static/src/views/fields/x2many/list_x2many_field.js create mode 100644 frontend/web/static/src/views/fields/x2many/list_x2many_field.xml create mode 100644 frontend/web/static/src/views/fields/x2many/x2many_field.js create mode 100644 frontend/web/static/src/views/fields/x2many/x2many_field.xml create mode 100644 frontend/web/static/src/views/form/button_box/button_box.js create mode 100644 frontend/web/static/src/views/form/button_box/button_box.scss create mode 100644 frontend/web/static/src/views/form/button_box/button_box.xml create mode 100644 frontend/web/static/src/views/form/form.variables.scss create mode 100644 frontend/web/static/src/views/form/form_arch_parser.js create mode 100644 frontend/web/static/src/views/form/form_cog_menu/form_cog_menu.js create mode 100644 frontend/web/static/src/views/form/form_cog_menu/form_cog_menu.xml create mode 100644 frontend/web/static/src/views/form/form_compiler.js create mode 100644 frontend/web/static/src/views/form/form_controller.js create mode 100644 frontend/web/static/src/views/form/form_controller.scss create mode 100644 frontend/web/static/src/views/form/form_controller.xml create mode 100644 frontend/web/static/src/views/form/form_error_dialog/form_error_dialog.js create mode 100644 frontend/web/static/src/views/form/form_error_dialog/form_error_dialog.xml create mode 100644 frontend/web/static/src/views/form/form_group/form_group.js create mode 100644 frontend/web/static/src/views/form/form_group/form_group.xml create mode 100644 frontend/web/static/src/views/form/form_label.js create mode 100644 frontend/web/static/src/views/form/form_label.xml create mode 100644 frontend/web/static/src/views/form/form_renderer.js create mode 100644 frontend/web/static/src/views/form/form_status_indicator/form_status_indicator.js create mode 100644 frontend/web/static/src/views/form/form_status_indicator/form_status_indicator.xml create mode 100644 frontend/web/static/src/views/form/form_view.js create mode 100644 frontend/web/static/src/views/form/setting/setting.js create mode 100644 frontend/web/static/src/views/form/setting/setting.scss create mode 100644 frontend/web/static/src/views/form/setting/setting.xml create mode 100644 frontend/web/static/src/views/form/status_bar_buttons/status_bar_buttons.js create mode 100644 frontend/web/static/src/views/form/status_bar_buttons/status_bar_buttons.xml create mode 100644 frontend/web/static/src/views/graph/graph_arch_parser.js create mode 100644 frontend/web/static/src/views/graph/graph_controller.js create mode 100644 frontend/web/static/src/views/graph/graph_controller.xml create mode 100644 frontend/web/static/src/views/graph/graph_model.js create mode 100644 frontend/web/static/src/views/graph/graph_renderer.js create mode 100644 frontend/web/static/src/views/graph/graph_renderer.xml create mode 100644 frontend/web/static/src/views/graph/graph_search_model.js create mode 100644 frontend/web/static/src/views/graph/graph_view.js create mode 100644 frontend/web/static/src/views/graph/graph_view.scss create mode 100644 frontend/web/static/src/views/kanban/kanban.print_variables.scss create mode 100644 frontend/web/static/src/views/kanban/kanban.variables.scss create mode 100644 frontend/web/static/src/views/kanban/kanban_arch_parser.js create mode 100644 frontend/web/static/src/views/kanban/kanban_cog_menu.js create mode 100644 frontend/web/static/src/views/kanban/kanban_cog_menu.xml create mode 100644 frontend/web/static/src/views/kanban/kanban_column_examples_dialog.js create mode 100644 frontend/web/static/src/views/kanban/kanban_column_examples_dialog.xml create mode 100644 frontend/web/static/src/views/kanban/kanban_column_progressbar.scss create mode 100644 frontend/web/static/src/views/kanban/kanban_column_quick_create.js create mode 100644 frontend/web/static/src/views/kanban/kanban_column_quick_create.xml create mode 100644 frontend/web/static/src/views/kanban/kanban_compiler.js create mode 100644 frontend/web/static/src/views/kanban/kanban_controller.js create mode 100644 frontend/web/static/src/views/kanban/kanban_controller.scss create mode 100644 frontend/web/static/src/views/kanban/kanban_controller.xml create mode 100644 frontend/web/static/src/views/kanban/kanban_cover_image_dialog.js create mode 100644 frontend/web/static/src/views/kanban/kanban_cover_image_dialog.scss create mode 100644 frontend/web/static/src/views/kanban/kanban_cover_image_dialog.xml create mode 100644 frontend/web/static/src/views/kanban/kanban_dropdown_menu_wrapper.js create mode 100644 frontend/web/static/src/views/kanban/kanban_examples_dialog.scss create mode 100644 frontend/web/static/src/views/kanban/kanban_header.js create mode 100644 frontend/web/static/src/views/kanban/kanban_header.xml create mode 100644 frontend/web/static/src/views/kanban/kanban_record.js create mode 100644 frontend/web/static/src/views/kanban/kanban_record.scss create mode 100644 frontend/web/static/src/views/kanban/kanban_record.xml create mode 100644 frontend/web/static/src/views/kanban/kanban_record_quick_create.js create mode 100644 frontend/web/static/src/views/kanban/kanban_record_quick_create.scss create mode 100644 frontend/web/static/src/views/kanban/kanban_record_quick_create.xml create mode 100644 frontend/web/static/src/views/kanban/kanban_renderer.js create mode 100644 frontend/web/static/src/views/kanban/kanban_renderer.xml create mode 100644 frontend/web/static/src/views/kanban/kanban_view.js create mode 100644 frontend/web/static/src/views/kanban/progress_bar_hook.js create mode 100644 frontend/web/static/src/views/list/column_width_hook.js create mode 100644 frontend/web/static/src/views/list/export_all/export_all.js create mode 100644 frontend/web/static/src/views/list/export_all/export_all.xml create mode 100644 frontend/web/static/src/views/list/list_arch_parser.js create mode 100644 frontend/web/static/src/views/list/list_cog_menu.js create mode 100644 frontend/web/static/src/views/list/list_cog_menu.xml create mode 100644 frontend/web/static/src/views/list/list_confirmation_dialog.js create mode 100644 frontend/web/static/src/views/list/list_confirmation_dialog.scss create mode 100644 frontend/web/static/src/views/list/list_confirmation_dialog.xml create mode 100644 frontend/web/static/src/views/list/list_controller.js create mode 100644 frontend/web/static/src/views/list/list_controller.xml create mode 100644 frontend/web/static/src/views/list/list_renderer.js create mode 100644 frontend/web/static/src/views/list/list_renderer.scss create mode 100644 frontend/web/static/src/views/list/list_renderer.xml create mode 100644 frontend/web/static/src/views/list/list_view.js create mode 100644 frontend/web/static/src/views/no_content_helpers.xml create mode 100644 frontend/web/static/src/views/pivot/pivot_arch_parser.js create mode 100644 frontend/web/static/src/views/pivot/pivot_controller.js create mode 100644 frontend/web/static/src/views/pivot/pivot_controller.xml create mode 100644 frontend/web/static/src/views/pivot/pivot_model.js create mode 100644 frontend/web/static/src/views/pivot/pivot_renderer.js create mode 100644 frontend/web/static/src/views/pivot/pivot_renderer.xml create mode 100644 frontend/web/static/src/views/pivot/pivot_search_model.js create mode 100644 frontend/web/static/src/views/pivot/pivot_view.js create mode 100644 frontend/web/static/src/views/pivot/pivot_view.scss create mode 100644 frontend/web/static/src/views/standard_view_props.js create mode 100644 frontend/web/static/src/views/utils.js create mode 100644 frontend/web/static/src/views/view.js create mode 100644 frontend/web/static/src/views/view.scss create mode 100644 frontend/web/static/src/views/view.xml create mode 100644 frontend/web/static/src/views/view_button/multi_record_view_button.js create mode 100644 frontend/web/static/src/views/view_button/view_button.js create mode 100644 frontend/web/static/src/views/view_button/view_button.xml create mode 100644 frontend/web/static/src/views/view_button/view_button_hook.js create mode 100644 frontend/web/static/src/views/view_compiler.js create mode 100644 frontend/web/static/src/views/view_components/animated_number.js create mode 100644 frontend/web/static/src/views/view_components/animated_number.scss create mode 100644 frontend/web/static/src/views/view_components/animated_number.xml create mode 100644 frontend/web/static/src/views/view_components/column_progress.js create mode 100644 frontend/web/static/src/views/view_components/column_progress.xml create mode 100644 frontend/web/static/src/views/view_components/group_config_menu.js create mode 100644 frontend/web/static/src/views/view_components/group_config_menu.scss create mode 100644 frontend/web/static/src/views/view_components/group_config_menu.xml create mode 100644 frontend/web/static/src/views/view_components/multi_create_popover.js create mode 100644 frontend/web/static/src/views/view_components/multi_create_popover.xml create mode 100644 frontend/web/static/src/views/view_components/multi_currency_popover.js create mode 100644 frontend/web/static/src/views/view_components/multi_currency_popover.xml create mode 100644 frontend/web/static/src/views/view_components/multi_selection_buttons.js create mode 100644 frontend/web/static/src/views/view_components/multi_selection_buttons.xml create mode 100644 frontend/web/static/src/views/view_components/report_view_measures.js create mode 100644 frontend/web/static/src/views/view_components/report_view_measures.xml create mode 100644 frontend/web/static/src/views/view_components/selection_box.js create mode 100644 frontend/web/static/src/views/view_components/selection_box.scss create mode 100644 frontend/web/static/src/views/view_components/selection_box.xml create mode 100644 frontend/web/static/src/views/view_components/view_scale_selector.js create mode 100644 frontend/web/static/src/views/view_components/view_scale_selector.xml create mode 100644 frontend/web/static/src/views/view_dialogs/export_data_dialog.js create mode 100644 frontend/web/static/src/views/view_dialogs/export_data_dialog.scss create mode 100644 frontend/web/static/src/views/view_dialogs/export_data_dialog.xml create mode 100644 frontend/web/static/src/views/view_dialogs/form_view_dialog.js create mode 100644 frontend/web/static/src/views/view_dialogs/form_view_dialog.xml create mode 100644 frontend/web/static/src/views/view_dialogs/select_create_dialog.js create mode 100644 frontend/web/static/src/views/view_dialogs/select_create_dialog.scss create mode 100644 frontend/web/static/src/views/view_dialogs/select_create_dialog.xml create mode 100644 frontend/web/static/src/views/view_hook.js create mode 100644 frontend/web/static/src/views/view_service.js create mode 100644 frontend/web/static/src/views/widgets/attach_document/attach_document.js create mode 100644 frontend/web/static/src/views/widgets/attach_document/attach_document.xml create mode 100644 frontend/web/static/src/views/widgets/documentation_link/documentation_link.js create mode 100644 frontend/web/static/src/views/widgets/documentation_link/documentation_link.xml create mode 100644 frontend/web/static/src/views/widgets/notification_alert/notification_alert.js create mode 100644 frontend/web/static/src/views/widgets/notification_alert/notification_alert.xml create mode 100644 frontend/web/static/src/views/widgets/ribbon/ribbon.js create mode 100644 frontend/web/static/src/views/widgets/ribbon/ribbon.scss create mode 100644 frontend/web/static/src/views/widgets/ribbon/ribbon.xml create mode 100644 frontend/web/static/src/views/widgets/signature/signature.js create mode 100644 frontend/web/static/src/views/widgets/signature/signature.xml create mode 100644 frontend/web/static/src/views/widgets/standard_widget_props.js create mode 100644 frontend/web/static/src/views/widgets/week_days/week_days.js create mode 100644 frontend/web/static/src/views/widgets/week_days/week_days.scss create mode 100644 frontend/web/static/src/views/widgets/week_days/week_days.xml create mode 100644 frontend/web/static/src/views/widgets/widget.js create mode 100644 frontend/web/static/src/webclient/actions/action_container.js create mode 100644 frontend/web/static/src/webclient/actions/action_dialog.js create mode 100644 frontend/web/static/src/webclient/actions/action_dialog.scss create mode 100644 frontend/web/static/src/webclient/actions/action_dialog.xml create mode 100644 frontend/web/static/src/webclient/actions/action_install_kiosk_pwa.js create mode 100644 frontend/web/static/src/webclient/actions/action_install_kiosk_pwa.xml create mode 100644 frontend/web/static/src/webclient/actions/action_service.js create mode 100644 frontend/web/static/src/webclient/actions/blank_component.xml create mode 100644 frontend/web/static/src/webclient/actions/client_actions.js create mode 100644 frontend/web/static/src/webclient/actions/debug_items.js create mode 100644 frontend/web/static/src/webclient/actions/reports/bootstrap_overridden_report.scss create mode 100644 frontend/web/static/src/webclient/actions/reports/bootstrap_review_report.scss create mode 100644 frontend/web/static/src/webclient/actions/reports/layout_assets/layout_bubble.scss create mode 100644 frontend/web/static/src/webclient/actions/reports/layout_assets/layout_folder.scss create mode 100644 frontend/web/static/src/webclient/actions/reports/layout_assets/layout_wave.scss create mode 100644 frontend/web/static/src/webclient/actions/reports/report.scss create mode 100644 frontend/web/static/src/webclient/actions/reports/report_action.js create mode 100644 frontend/web/static/src/webclient/actions/reports/report_action.xml create mode 100644 frontend/web/static/src/webclient/actions/reports/report_hook.js create mode 100644 frontend/web/static/src/webclient/actions/reports/report_tables.scss create mode 100644 frontend/web/static/src/webclient/actions/reports/reset.min.css create mode 100644 frontend/web/static/src/webclient/actions/reports/utilities_custom_report.scss create mode 100644 frontend/web/static/src/webclient/actions/reports/utils.js create mode 100644 frontend/web/static/src/webclient/burger_menu/burger_menu.js create mode 100644 frontend/web/static/src/webclient/burger_menu/burger_menu.scss create mode 100644 frontend/web/static/src/webclient/burger_menu/burger_menu.variables.scss create mode 100644 frontend/web/static/src/webclient/burger_menu/burger_menu.xml create mode 100644 frontend/web/static/src/webclient/burger_menu/burger_user_menu/burger_user_menu.js create mode 100644 frontend/web/static/src/webclient/burger_menu/burger_user_menu/burger_user_menu.xml create mode 100644 frontend/web/static/src/webclient/burger_menu/mobile_switch_company_menu/mobile_switch_company_menu.js create mode 100644 frontend/web/static/src/webclient/burger_menu/mobile_switch_company_menu/mobile_switch_company_menu.xml create mode 100644 frontend/web/static/src/webclient/clickbot/clickbot.js create mode 100644 frontend/web/static/src/webclient/clickbot/clickbot_loader.js create mode 100644 frontend/web/static/src/webclient/currency_service.js create mode 100644 frontend/web/static/src/webclient/debug/debug_items.js create mode 100644 frontend/web/static/src/webclient/debug/profiling/profiling_item.js create mode 100644 frontend/web/static/src/webclient/debug/profiling/profiling_item.scss create mode 100644 frontend/web/static/src/webclient/debug/profiling/profiling_item.xml create mode 100644 frontend/web/static/src/webclient/debug/profiling/profiling_qweb.js create mode 100644 frontend/web/static/src/webclient/debug/profiling/profiling_qweb.scss create mode 100644 frontend/web/static/src/webclient/debug/profiling/profiling_qweb.xml create mode 100644 frontend/web/static/src/webclient/debug/profiling/profiling_service.js create mode 100644 frontend/web/static/src/webclient/debug/profiling/profiling_systray_item.js create mode 100644 frontend/web/static/src/webclient/debug/profiling/profiling_systray_item.xml create mode 100644 frontend/web/static/src/webclient/errors/offline_fail_to_fetch_error_handler.js create mode 100644 frontend/web/static/src/webclient/icons.scss create mode 100644 frontend/web/static/src/webclient/loading_indicator/loading_indicator.js create mode 100644 frontend/web/static/src/webclient/loading_indicator/loading_indicator.scss create mode 100644 frontend/web/static/src/webclient/loading_indicator/loading_indicator.xml create mode 100644 frontend/web/static/src/webclient/menus/menu_command_item.xml create mode 100644 frontend/web/static/src/webclient/menus/menu_helpers.js create mode 100644 frontend/web/static/src/webclient/menus/menu_providers.js create mode 100644 frontend/web/static/src/webclient/menus/menu_service.js create mode 100644 frontend/web/static/src/webclient/navbar/navbar.js create mode 100644 frontend/web/static/src/webclient/navbar/navbar.scss create mode 100644 frontend/web/static/src/webclient/navbar/navbar.variables.scss create mode 100644 frontend/web/static/src/webclient/navbar/navbar.xml create mode 100644 frontend/web/static/src/webclient/reload_company_service.js create mode 100644 frontend/web/static/src/webclient/res_user_group_ids_field/res_user_group_ids_field.js create mode 100644 frontend/web/static/src/webclient/res_user_group_ids_field/res_user_group_ids_field.scss create mode 100644 frontend/web/static/src/webclient/res_user_group_ids_field/res_user_group_ids_field.xml create mode 100644 frontend/web/static/src/webclient/res_user_group_ids_field/res_user_group_ids_popover.js create mode 100644 frontend/web/static/src/webclient/res_user_group_ids_field/res_user_group_ids_popover.xml create mode 100644 frontend/web/static/src/webclient/res_user_group_ids_field/res_user_group_ids_privilege_field.js create mode 100644 frontend/web/static/src/webclient/res_user_group_ids_field/res_user_group_ids_privilege_field.scss create mode 100644 frontend/web/static/src/webclient/res_user_group_ids_field/res_user_group_ids_privilege_field.xml create mode 100644 frontend/web/static/src/webclient/session_service.js create mode 100644 frontend/web/static/src/webclient/settings_form_view/fields/settings_binary_field/settings_binary_field.js create mode 100644 frontend/web/static/src/webclient/settings_form_view/fields/settings_binary_field/settings_binary_field.xml create mode 100644 frontend/web/static/src/webclient/settings_form_view/fields/upgrade_boolean_field.js create mode 100644 frontend/web/static/src/webclient/settings_form_view/fields/upgrade_dialog.js create mode 100644 frontend/web/static/src/webclient/settings_form_view/fields/upgrade_dialog.xml create mode 100644 frontend/web/static/src/webclient/settings_form_view/highlight_text/form_label_highlight_text.js create mode 100644 frontend/web/static/src/webclient/settings_form_view/highlight_text/form_label_highlight_text.xml create mode 100644 frontend/web/static/src/webclient/settings_form_view/highlight_text/highlight_text.js create mode 100644 frontend/web/static/src/webclient/settings_form_view/highlight_text/highlight_text.xml create mode 100644 frontend/web/static/src/webclient/settings_form_view/highlight_text/settings_radio_field.js create mode 100644 frontend/web/static/src/webclient/settings_form_view/highlight_text/settings_radio_field.xml create mode 100644 frontend/web/static/src/webclient/settings_form_view/settings/searchable_setting.js create mode 100644 frontend/web/static/src/webclient/settings_form_view/settings/searchable_setting.scss create mode 100644 frontend/web/static/src/webclient/settings_form_view/settings/searchable_setting.xml create mode 100644 frontend/web/static/src/webclient/settings_form_view/settings/setting_header.js create mode 100644 frontend/web/static/src/webclient/settings_form_view/settings/setting_header.xml create mode 100644 frontend/web/static/src/webclient/settings_form_view/settings/settings_app.js create mode 100644 frontend/web/static/src/webclient/settings_form_view/settings/settings_app.xml create mode 100644 frontend/web/static/src/webclient/settings_form_view/settings/settings_block.js create mode 100644 frontend/web/static/src/webclient/settings_form_view/settings/settings_block.xml create mode 100644 frontend/web/static/src/webclient/settings_form_view/settings/settings_page.js create mode 100644 frontend/web/static/src/webclient/settings_form_view/settings/settings_page.xml create mode 100644 frontend/web/static/src/webclient/settings_form_view/settings_confirmation_dialog.js create mode 100644 frontend/web/static/src/webclient/settings_form_view/settings_confirmation_dialog.xml create mode 100644 frontend/web/static/src/webclient/settings_form_view/settings_form_compiler.js create mode 100644 frontend/web/static/src/webclient/settings_form_view/settings_form_controller.js create mode 100644 frontend/web/static/src/webclient/settings_form_view/settings_form_renderer.js create mode 100644 frontend/web/static/src/webclient/settings_form_view/settings_form_view.js create mode 100644 frontend/web/static/src/webclient/settings_form_view/settings_form_view.scss create mode 100644 frontend/web/static/src/webclient/settings_form_view/settings_form_view.xml create mode 100644 frontend/web/static/src/webclient/settings_form_view/settings_form_view_mobile.scss create mode 100644 frontend/web/static/src/webclient/settings_form_view/widgets/demo_data_service.js create mode 100644 frontend/web/static/src/webclient/settings_form_view/widgets/res_config_dev_tool.js create mode 100644 frontend/web/static/src/webclient/settings_form_view/widgets/res_config_dev_tool.xml create mode 100644 frontend/web/static/src/webclient/settings_form_view/widgets/res_config_edition.js create mode 100644 frontend/web/static/src/webclient/settings_form_view/widgets/res_config_edition.xml create mode 100644 frontend/web/static/src/webclient/settings_form_view/widgets/res_config_invite_users.js create mode 100644 frontend/web/static/src/webclient/settings_form_view/widgets/res_config_invite_users.xml create mode 100644 frontend/web/static/src/webclient/settings_form_view/widgets/settings_widgets.scss create mode 100644 frontend/web/static/src/webclient/settings_form_view/widgets/user_invite_service.js create mode 100644 frontend/web/static/src/webclient/share_target/share_target_service.js create mode 100644 frontend/web/static/src/webclient/switch_company_menu/switch_company_item.js create mode 100644 frontend/web/static/src/webclient/switch_company_menu/switch_company_item.xml create mode 100644 frontend/web/static/src/webclient/switch_company_menu/switch_company_menu.js create mode 100644 frontend/web/static/src/webclient/switch_company_menu/switch_company_menu.scss create mode 100644 frontend/web/static/src/webclient/switch_company_menu/switch_company_menu.xml create mode 100644 frontend/web/static/src/webclient/user_menu/user_menu.js create mode 100644 frontend/web/static/src/webclient/user_menu/user_menu.scss create mode 100644 frontend/web/static/src/webclient/user_menu/user_menu.xml create mode 100644 frontend/web/static/src/webclient/user_menu/user_menu_items.js create mode 100644 frontend/web/static/src/webclient/user_menu/user_menu_items.xml create mode 100644 frontend/web/static/src/webclient/webclient.js create mode 100644 frontend/web/static/src/webclient/webclient.scss create mode 100644 frontend/web/static/src/webclient/webclient.xml create mode 100644 frontend/web/static/src/webclient/webclient_layout.scss create mode 100644 pkg/server/bundle.go create mode 100644 pkg/server/templates.go delete mode 100644 tools/compile_templates.py delete mode 100644 tools/transpile_assets.py diff --git a/cmd/odoo-server/main.go b/cmd/odoo-server/main.go index 67abac2..57a8a6f 100644 --- a/cmd/odoo-server/main.go +++ b/cmd/odoo-server/main.go @@ -12,6 +12,7 @@ import ( "log" "os" "os/signal" + "path/filepath" "syscall" "github.com/jackc/pgx/v5/pgxpool" @@ -47,6 +48,26 @@ func main() { cfg := tools.DefaultConfig() cfg.LoadFromEnv() + // Auto-detect frontend/ directory relative to the binary if not set + if cfg.FrontendDir == "" { + exe, _ := os.Executable() + candidate := filepath.Join(filepath.Dir(exe), "frontend") + if _, err := os.Stat(candidate); err != nil { + // Try relative to working directory + candidate = "frontend" + } + cfg.FrontendDir = candidate + } + // Auto-detect build/ directory + if cfg.BuildDir == "" { + exe, _ := os.Executable() + candidate := filepath.Join(filepath.Dir(exe), "build") + if _, err := os.Stat(candidate); err != nil { + candidate = "build" + } + cfg.BuildDir = candidate + } + log.Printf("odoo: Odoo Go Server 19.0") log.Printf("odoo: database: %s@%s:%d/%s", cfg.DBUser, cfg.DBHost, cfg.DBPort, cfg.DBName) @@ -87,6 +108,12 @@ func main() { log.Fatalf("odoo: schema init failed: %v", err) } + // Migrate schema: add any missing columns for newly registered fields + log.Println("odoo: running schema migration...") + if err := service.MigrateSchema(ctx, pool); err != nil { + log.Printf("odoo: schema migration warning: %v", err) + } + // Check if setup is needed (first boot) if service.NeedsSetup(ctx, pool) { log.Println("odoo: database is empty — setup wizard will be shown at /web/setup") diff --git a/frontend/account/static/demo/bank_opening_statement.pdf b/frontend/account/static/demo/bank_opening_statement.pdf new file mode 100644 index 0000000000000000000000000000000000000000..362cec2d06adaf50713954d67f4d1e90842a6957 GIT binary patch literal 21405 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 a$N^VeiA5#gEMjC}YGKNys_N?R#svV7vASCT literal 0 HcmV?d00001 diff --git a/frontend/account/static/demo/bank_statement_one_month_old.pdf b/frontend/account/static/demo/bank_statement_one_month_old.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ff736dd51d634b237c651b3f8ddc2741d86107ed GIT binary patch literal 21856 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 literal 0 HcmV?d00001 diff --git a/frontend/account/static/demo/in_invoice_yourcompany_demo_1.pdf b/frontend/account/static/demo/in_invoice_yourcompany_demo_1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6e44cfb44fe44372f36358c276f95db0be90ad26 GIT binary patch literal 43487 zcmeFZby$^6w=hn3w;-^Q5Tt7ZQqm1lqI9QpcZ0M_2@(p@-Q6W2-5nCr-SOKS@X_~) z_dVyk&hPrpAK%uw=DugvtXVTF?lm)OpnNVYO3%W;ghW|gJv)iS3}6CS>YDQKFe*7) z888az*y+5sG-7=ReK9H-IN32un1c`s-ep1>zfgi3{s|?hZyN&;n%mAnC@t(jZ?*t7 za2JdU2DX;=HhKoOAc94l?8Fr9bnFa3&tgmfb}#@RAEPL!1*k-n2>`rpBFYTlxO-p$ zaNa$z0=VuT*Z|B-cTem9X7E%fFp2^J%y(UjasZfb2gWGM31GhMAB6s;oUW;X9=L~> zOaSKlPC?}Y78aHuGMVo>l`+sa(Gj$CQUjw0f)H|WvHUh{FtWR0i<-Q)Gq7P4eGM9i zuz{YXz5%1OfrXKsF@S}Ig`JPj*3QO2#~jHiVM}$x7N-Mq4(4E2u#JG}D+PrbjUIH6 z=t0ti5?`7h1yQuWJ|d>r977Y$m_XNeq3f$lLqkVP7e_g`4Ld&kX)=cl*Yu3JnUV_r zs9L1Cu{m}Q_^IV~U_t$bWoKpB+aoJ-Hth8F4wYW6753G^(UI>4>c_>oiHDVO=|p5) z6)3h!VP%Rv4+NXPd&YJ|WUEj{4w{f>U*k&CY!k%TkQt=a9(eopk7`TlDWd~rnCPPo zNKi(RJ|#f4iPPmpvz8UYkjoGtx79}H6C+!GfQ_51GF5Lj1_al}M+79t1EDp`z%+4e5d#1_HAy`kN4YkJ z3UNI(DNcin)2Ta6b5x6`?&$_oR6eoh8=)aI{x>bv#K)tWU(6W&7nWKms>&CZvKM49 zJop^E_th#tm*ia{BvRijHyS)1h-MlT=95t(r;rcB*xD5mT>XywK$9}n%x--{T8{QD zOBss>8e%KVBdSIJ=p+f+g&0WkTukc}A>Akz3X1N}=unmx?@%xpV915PK_jc15(lkv z((sgwcaj~vGxs0%fgQjOutkc)*hVx}HT^nmDC47`IYdtmShZAudrsBE2E~$PyI7m@ z6s7Zo6DL*C_mlm>TMcvRrFNttPD|~IZ|dW5l#?mZ&_wVXmOQH?E1vV>w2P9t<rEfiWnN*6-_dy#C z$HoT}P9_B-vrw-b)>N`0!6sUdir2*Ep$q&h&g)>NLZ+DtZ~bX+E5Bt0X|TSrjjjdYBgBOaJNH)g$Mq+oDDRuB5O}@V<4Lf< zt8{jPBZHTfkaHq`HzG1~wQOY~HsL0WKFRj3f3wg*GXG8di=|3_dJ(>d%I)7TzCZp_ zbD%%etSC1Ag?_@F@KpuESn|~e`IupKQxZj78|AdnAA$v#YYay@b6Cnl@}`hAI?~TL z_X8p}Kj8-@&|unMtXA(@r;UZ@huZr8P(O5JH5Mf+N;#+4Mm_s^h?Hk(_YwjZxP_DtRh{aRes==!!MDtW3IqrSeYGdi+QD-sOFy??;%1EHDX$d zq)9rHzK|w&=4+rry$YXSr_28?!PDPH&-CSU|LiS-y$C#&_biM2dJ=>-d-W8Fv2tzi z*(mt-#hsaPe;A!GIXa+2b1i)kX)&%1fTL8Lr16EO#ItkxV$EQ59P?76Wtva(*1H50OE(eTyEm z!kOSkoZtXUSw4b`o>TPWJ%HpZd6JN>swYU6Y>#_7sKBC(q>;CDHv_u~R~@XXr2 zEmFmu;Im9pJA}BqIU)?>!0Uv3ZgiXPCJAv2(c2ZyP-_H|U%s+*&-W_1q8nRX=Hr~8 zJw_l!OPO{($Rx(PqDA0g3=i(Jc2{+S{(O9q-GF2QNmUpb_!4`?!aK|8^afz>9tUU9 z1gsbua4<}yZg$wCUm*?6JVZd6X*weJsq)>)C_GApAw_D6G%P$s=-~;6avvdo6!Q3k zeZO!0XZP5VK#2~0)TsUF5nz33>g$8;#e`!qnkGNd(9hKpaUC&OKCcX_8n3&)y`gXA z9r%i9w}TW&IJHIlejjacnNuTv+-&jt>_=x4slAX7sNpW<6aMa&tx<(x4H=1Qr8!E6 z{xYXfsF#d-Pd~+YgvPnaPtm+eDnH~!zW+8C+3!+ zCd{<@&pxv&qCGCDcEtv&Qgsz$mqyBHc653@Fk2tDBV2b8h`W%#ysBjT{x(tbr3G3~ z>5Pjq>nuEO*J0$>b_=3PUe^ARpg<RuhH4K^ zSxjLSMk>0S-%yVv9b4wcbm6XS&HHb9sQhk=2cFiBaPDBA@!og2_a4En(w)zvC;+gt zu{U5;lmRd>vHWrd!LHM9&LG&4V^p-)wF5h*VDFTPQBcR$;I>%S#9Y_jR#C@7nqI-c z$evNe0^~ZGSQs%%=!3jP6FX;maYkVSTRl+UIu>@|CbnwKw{D;Yqmrevg$dZx0x;kD zgTDv>1N`?15VU-)|6e2E*6aM80G3m(jXxu^ITc- zA85F9wf_SRw_f;vqTx?#np=VSV;+=LWCaw({(**Dcm7W@_m_FN_3-}_4S!lI{VNR; zk}9%_GXFrs-D2fG%)_nV{wEs#w7B|L8dPM3ghdtqfrh)~(Z8pG^;UNO6AiysI=Ab~ zTN-}H`|l8t;Kr3_Pbk*2@5IREyey=e%*}U%tK&E zib{w=Kte)7NP+%A+)P0TK|n!5g8$$DpkblGKR6f|XjphSczAF^Ktx7FKtMu(hetw1 zLPACX{ooPN&{0v)z|UYL;O5{~p#LZc@Caat|K9JW5dsqdLI*+*8uAeY6ec7zCge>k z1StdrBrJ$pFv0gfNGNC+SU3<#NXQ_7G7J#;(9lpIVqsxmK+x`>dI%UySS&JT0XS@V z9r#DqI4s`pJ|mC|7B%524D3>{>e~1qBH`f^JRqc`dQ468gpD1@!O8VhNLWNvOk6@z zQAzoQ%F9=(din;2M#d(lws!Uoj!w=lzJC4zfp3C>-$zD8$Hab!i%&`Yl9ry4nU!5! zQd(ACQCU^p+|t_C-to1wYj9|IWOQtNVsc?|>HG4^>e`R>z5Rp3qvMm)vvcscAR(ad zhV{qD{xmL3(72#LJb-}*j|&pY5tN`YVPMIa;jjec;dQLBAF+5N;0V6^T-1a}&Z@AB zt7|iWgh#=)K)DAV+U>~xYXkH7KN{H|1N$|uNeC2ZND%X&F(LRNt}au8sUbiK3ROJsx>BIe8lPL7w{T$dvARWGtT99qZS8 zY*A(J%`ks0rv8oaOy4yJb4OEdkGAzZ;u3f48VPqX*|%sN|J-x)Y8NO>!WKR#>Rqdq zov>Bp4F726ftro)fwSfM7gw<@jSHzCq{Zl)571lry`OrvTuC)~;EmF4?JNv^78@Fq zy)Z7X#c)rI7S8Mul>p$n`4|%?b$s&Z)Y?^O@bN4^+@Eyg+FOatHN)FG8#2MhHp>!B z9B%@kDLERzKo5^qk98!M2VP=#EEsK#QEsQr=V?jo%5^Xzd?5_~xM+#q^VT$8c>i&n z<#F4aK;!T7?R$x}S(|P{$G{s1pXP77v8~0I_*Z$8a|;=5o@k4+ybhNLY?e5JoKjKA zP;7<-TomLaAGn@VA#xO*6N|W&?HOGoWAAb|y40%T2k?kJZu5Mgd0DPWvP*}WuQABa$o{Jn2c^({>+^6jalg5*+=Ps&d z@zMiS`v#biE04+PCSWB1`Gd5F9$rY)DYuFGG`aQM(WjQnJ!b=dwAwH_{n-hJqa(|k z&*edohK_oT&mk$|PvMswFCXGA&}vYfqx*THUApd3U5mWu1^W7L+y)9>7YQT$B z!y!oxXUmcCvxL<*dVXPGKi&N zl~gyY-N3*GW6w9mm%PK~jOLc5TMyUYnwAemnLXck*3=w0@kRSwjDWlMA;E#G3!`<| zr2PBj58Sf%Yc~+&i_A`!E%C_s$~hGrU5E0jqFv`11((=WAOSSL)ISZ`b)2vDAg|c7 zC_`&lGEq?;&ezn~*YHZ=MEU4FjYshuavplHzO)ns?j^UBW2QzL!Uw;r(t1TTQ4E)t z_Uxc7sYZ$>;xXyWgEkMHORLj`CG?4<1hjVlGiyNACPKGo*Tfk`$}gPf^?ewoLDOrPyYct1?m z55?Xkt?Qj0#>L!#wi(sDO0FdGF+frN$h8Gyu z3*O!F*D7{w4a0BspcrAi)XDgPb=o;-F6sW1%B*e4R)l08iwBR~s&kuJGwnjr4T`^% zvBh$;+iW0Xy&NCeHDj8o>q~b6Tn8uD`qPIbBW(t-@h+CH9S{E;@2s+SH7b$@ZZf{*N98x});y_hvCsj!{M2BULaigq>GW z>|u@_$q(H;^PG};k%`SD1NsCK$;=00LRq=9HVGDr#xXU-wy+nqP)$~lNA_Y87Ygux zAhowmerI+cpFt_~k!t@AA@G1$lvz{lg7|uf$cR@W{lJ3Wb zXG+4Wi5U+qq1VkMX|dHH70kIz(&O`7oYl(PhhH#a#wsTi07qjr_s7&c9l9=ljBk0) zryP7CEy>qF2|v%vW*CMIq?6@ZA~Wl)hreVW_B2Be(1fZOA*&s@B%jZ*eUNBKW1agg z%wN&uOjn6W%AO0LVhPeX(&5j6^M>ywUg;%kR7{03xrxyb^ubuiS6yH^t;+4rZAKeq zt;fobzcR7aK-7=&UiPNN;O*n(j-U0Hj4FOJoKm;#5NmI5&T~$*u&(_1U2`7p&OvES zPL6TJi#Q(UU9lz>&?02_IwV6Bxj3bb(2I#R{AE&NCwtw3=bMd%DI!MPaZL#w;wZDM zCi{S(1!ba^B_Adh>xkxhx;^*~DXmGxCB6{XMKyDWxrMsA1F>Twk-J|L#V~Pt;OU=HSG?)%K7Y`1q9-rqI2!*!c)ekYL%7(gdg!}fjZ`CYm@A` z4liCftYOg_>pjm5hJOYD)sVOooQ%rjz884xFt(LGR8M+}HMLNewHFZS4lLuUD|TgX z3Yh*PMRJCd?XMS_jbQ`pu?$kvryi_MA;i9fRvD6p9RztKR z(r3f%*pcT-PmXk%gB%o%RFsFhbfvahbb5`B&%(qnHP-V3WsYCfi2>7A>(j2tzuy)zs2 zNAqQ+pAI>>6gR@#>tmh}ivuu+;2X}a%MQwYcWFC3OFY|n&E$7+>*E&D5l@5XXZliS zrrl$tBqSa)*CWiohbKk=$@~eE)1*RVj>N^&cO@#(ix(YQ^bskIOIeb#dH9>_?cX$I z+D^i&>4C#%DYFOadsIPRPOUU%`%;Y^>utx^oo^sSDn*QYOvUNFBw)oih6q*%_;B3F zhp#EkHWm==Yhs7!x6$%4TL@_2;@{hKJhtmm`A*kL4O{3kir&FpfPYDQ+Gm?>R`^3E z&nbUohp$5$GC~gWn2bTJ=fa78qwm{b!FBr_48skCF{)=9>LRM?IWOGkOPOW=F9qnw zQGUR0eJ#$W(ALvQKJ^Wl%I@-uI8l0LGiEunlcqNitlP}?vBOEUiK2M2KeS$)o8)XB zHhew|>?rVwNus+-Tp75eeaJlPWUp``0_<>>dV^>W5$*^iFv@xy-+UT-A#xhS2z5#% z+P{gK5m9T8qXVc3U6pwTpU6tKS-V6&n(TOi_R2|eQ`3!akFRdtwKw9S!p9nk8;IGU zKCM&y1r3#E&xuYR^d-bv#rzG&S=v4GR(_+W{)4$DmiQ-+!knHt_i^!ux!Dx>hvH=L zo0yg64%Mukyr?fwT)-oTB{WrjyhS%9(U##*?wVd;R9{1U1F@eG0xsHux3WaAj>&!~!yM=HqLaZmsN*lY|GVY3eu` zif75+Bl;`JF;&aq6mu?nCsilup@OLk^7+2fF72GsRy3{dJ#Gm@Z^tXzj%_(8%Nrf& z3{W@M+sseBz9=X(qwUZ{9=axX;#TM{dx^IyJ7PXN>C|Vh6B;4-)OXxl)5FC3We?r% zPWI5)=T*aTge9xU7O5@9j?bqNHKQ>#4U)$}O(4(K)7ZobD(<(8P2N_)!_#(7)GuVP?7?npgpdE?X0?cOWL*|&R?-*JsN zwunG+IKz=ipHSL* zBJdHI%)!+YnUFjf?59tPw0MLX`ams*>%@>EInOToB3~{x7Y~cTBg<@K_}ZtFW!bw) z2z`X!U2{PMMet}Y_*9jU2Q$8<|o#mvH0 zC2_{31X-cvLXtTzMS5$cdEz$AN0Pwqh3-!~Ib6u^*6w4?Dv*|{L2u?dLT(b)YPFK? ziX}f(Rc9JxcFb@leFE>E-A%vSdG6aMhj@AD(!-No<7;nh;u zZU3xeM1ifU+07NEtmhdg>95O_o9f*;EysR-l;YV$%HwKpmHK=S-9L}4uV;W~h>4`J zJ@u$S_f!qU$IxA)0K;%kY}51DAPQMk7*c}@y`(^xBGO$lEfLuO+c$ohvcbXc%VuNv zk`f=D;7Y!9i6Y0PCvs8S<9m4e_=4XlulwjH) zeK|kZ;Qy5K-%R}VT;n$41GGn}rtvdQ=JufBw_VGB+4K~W5C*@4cSBiz?dJdaJOc!! z=xl3eU@l={XbFln0UdIffR0m~sRclhI0iI~ayI$~HlXtk>YwKwG@w0EE34NA=HSgv zru#4$@Yc1BiIttD4S@ANoJK-e$x=*0SVqU{kE0Ss6*VR`77ic)oH#(k0RlnsDjcA6 z|IE(L0e&6agyT!o_!okGE%E<>%1LbU7 zx36p>w;5>iz?E#GcNyzl#(tZz+`X}YX<%YvzXM^t$97l9%5q!Bde?yUt^q6CZ6WYJ zR9gDfoB9XFqXUi*zP;JZ+F)_%l#Pc`s29ESni3xn*!in55W7$y6XWb zc#9f%*8|5LB*#5!5S_PX=56KhJ0Wqp7+*M*J65_#ZJx#(1`qII`s_4|HLKu6;%j8>Hyyt(=W;g-&|;h&x_4=8Da4qK{s-P zd&ep#y^?lIpj||7hx$mt1>*%<-jP~Ti@*zZSsFvGzSxJts5S`-Bc)Gdb?UIMTe7sW zt9wZDSCwTi*m_4SGS-uqSDe489PMJQ3Xp7&a!Do)%JW`&@bZiXb{D&Sl^DzhW^X7( z6}7r;#O_>4nD+2&=~eOOjb*Ux?RaaAi6|jkj8pW;_ezmcocy33u~5vnI4M)tprpBW zoZ^&4)pOcmO&ZlWv*aMwhJkrj0x2vAjUk9kfQZnKVvO>MjMIzi5JK*a7fHWvyH$Fb zSBw`#i`OVLiVm_Bj4+X?St;;!-oRvGnW%hs3gJM^063jL&_;wTgeAbSr!##%T$Cvg zuNVJEGV2(H%Oh6%2)QfWC^d7H21S6tN!oEx#AMG_22Qsx{l~-Q&v%sy|0x-7RlKaZ z@R-3XbR%E4tx&G~u6|5NiiLMx-lh1yVQ!LzFY& zOHsv|D`w%Q;Vy(3*(NfCOXmQdr7Dn=4wG2KD)(KOe8`#U9iHp%?vA%y`5_b^ajx$9 zP2zwvv1h2SqQ%y|lA)WCccU~u?8nncJ8I_Si)AU#Q9^dc3nk5njjc4L%ZvFAbNL}3 zqI`;82}UVW_LCuroSAg>nQg_YYRQblIkO6b5$iWQ?(Jjcw0#akkZ{jn*odAzVuPS8 z_44+D==N%dBy6UAhQJHJ!1@GVnrWSD0^5(p(})Gth*=IH_pDUU4jEPq_Gb!FMpiK& zP#Paj$tuheQPfdHCjA@%&WRw^ynG`vB7{nfN`Ftj2^k4OqSJuB6*3jrBeJc|H}HdB9bV@PO7 zlNb|1a7u|uxlI-^7KHG+&Hy-b?s9XCErP!!tKD#T%fg779p9CQA4`mTi@2sB{Jdm| zxyw%ZlIA*^zYTr+a}w;ZWR^Q=di6L5rQ!UcDsDbktalr!kA#Mio%H_6mCavaivKK|tW3X_x**wnKV$04 zL=ezd7yPgso-%Bp92!%!qtW+@sRFtG`w${)fH8XQMcU4od7+ng;CE;2S?azKq2&OP z`hd>i(l-`XzpU08boD zO=z`z(wHhWU-R`Iy%@#cX)va!cvD9$rIQuHEEnc!A^P6RAv&zPrFc7KuVTUdh(e)o z@#BZi!A&Be&(^$@ZG7RyPNAvNUne}f*TeJ5S(9ZNEi~G>W@5d_0eYN}HeL!4x?ZUh z@(-RdKZJ;fG>IR=89b!_21V!<5LuGC@O@MN4WE@=475T3nl^+p(i#DV)pxIlfh1@B zwXg~O-FaSSLVK8Pa%auzf^XWE(gse;Hkt-@`sN=qrr2M-w?s8gGWr`?`FEk2e=jS4 zjq>{+mld|#7(CFom+dZq?XR-(Ykl|6vcdxLT7UcYzLha;WkT^k;q#AS4Qa9-bf`uM z(2*nyx$ZB8w|Y$p`t({Sh*)TeNvLHu`k?=K<+3=o^4S`=H@0RUg6Og#-P+kFPj1Kg zvO^fLeZ3UhfH%9*X|%jM%IdZH&MOUQ&+aywHdr+LC2pSmSYswtj>d(hNfKx7z+Hdx zY8Y5?M7WE~Z;?oSroyuEih4E^>B~{Lo~S!g&!~gi%7c#KGCr}1F2m{DzY`jb;Gz&R zx{mJqn7MJ35j?Eknb0kWm4Il0;0I_#&CHveY$X;bgEYz3u$8ltN*S+XVkL`>4n=@5n>#uWSb`|Na_N`|sDse+{hspJ)5sW*&&`cT45J zu>Cfk5#%WX*qK?Gz@q{^Bi(KR{>gp(wL^G&Zw|Z@#VD^V0Q%AXc}o&>Ma>9w4eu6C z5PY?bo|%IUK+nPnx;n=Ma!t9|Sv43XK-ULN^aL!7UK;?I7zMxq*r0e!cGmk~Y`a@{ z&@Dr&pP`zdDn-yX=L-Nk(=BF@lp26`@@{>~UoIr;FBkHUFw(yz{rR6szultvJJMMJ z^sG#v8-7e&9H4+vu74t(gX=VkcpWc$O_sE;Q}&na)4RO#=^h_1U+i}KFC}D>9;}NyGv@Upp9e>4Q6(52HHFU zBjUJC?Cju88lVOXi`s1lyvw-MKtLL-Ks64K1pSsY*qGITpkQ(i5H>bWHBJyFP^|_# zs1(Sd!OjNGK=&#^0H9JJ(`_k`6O8k=^!FT01sD>Xz{GLffq@C*L&~vIgkD2doF6 zbg#g-YH?q5SA4I@96vSrUV}B*{@}6(JChn%li2^yIIO_|()!=^nCsTa{7Hv3IN8Ci&~$0V$8xaMcwxL^KYkEg*ixspk5leiNc>ViC;w{CD;H`CzwoM0 z;bK3n&&iRFqO}HpYNR}^Y+3)Q1cmT5Lits9%;Y|FuPkbt0~A@mlvhHFQBG!Z;i;0O zeZR~8m&}`dJTV8XM}v!RYM2D!(>`IFxWap0X2veV316K&syf*=t)jknA45*VL(3rB+F|J^0j4-cIU}caN&6viJiwD+?pKr7TCAHru>` z5_9&mAP!9%X3Z{$XvwtXVCY<}$DXUg0!@fkb7PlVy0+nz7@u}IZ@RB19o61LyjPPe z$aT$Nx0dbZPTBdq$m6*+Z+2OBU_e7teN>~RF^%kv|3u5@?4thl%y1EulXQa-ZbvYMd5UL?%8(a9s1xJ(5`sw zpUP;kr3V?Krh9hqZ!zCM5SI?V{X_(_j zr_J?4#8OU5yLAe?Nai*xc0Mt9`gx7VKSgEmQ4`tmdA=Xn`%2cmUL)29F3XvBU22Z$ z*Gx2dmu}js%2?{% zbAuG|z-)2@tdo%!dh^H+Mn--mh#+UTNh~Y!BC_5o&wO0MB*{a>dT>*|)u{-h&Y-xxK zz*d&Qa~Wl>odfb2J&p#{Nb#4@nR%y3{%-V7pH>B#GBGrGx|GA`kjW^-$*g+Bozt5w zv;2o}rR?Ck_);Ssu!6j_n!;?XQoMd}6?yKABQlv8+8GIKJSds$BMvXr)a9(A?~VISA&|x+W-{q|;vClX8x{7m?dCVZu)MrL( z-gnVKrpI8s@mdWsrz^NLrxX2imUuRrYautQCW za-Rjj#6A(g`vS3YHtX$8B}Nn3_i-NjW$R9$NaALbz(tU3dXj0V#~PL^k$sO;zeAeR zc<4*I13{CGT2#?EwCk?p>1|-Wy279a2W$3rG|Lonk(#qiE-xj)vyZwVaT)0D*JCYa@UTp<;cJvbs1mO2d!3h&;H!^BCZ z*?GUMdG1sdik3#r3l(cUEG06&D;P8<7K2mA(U6(L@RXswmcw--{}nbBTz+!p$97$6t$gp+v88c`*96EO(L|jT z5qYMT#rhpYhrSEzTVK!QnIteX;Z!(UIq8BH$b)?#XMEKG+2NlNEh@(-lL&+2X3?7y zQHm48`nKq5>2dryP;KFG0XMBgS4dJp)o{$fAJWfA`DH6swnS?GiJk4jVNiXb!4J|k z9$qeP;iUBjyS?e?3Z2Q$gV?Iwriuvy)$K$hZ1{-58Hz=VS}P6`isywQy&7M4XcP6j z3MJ8`5)kt?HE@Zq=CooPX0;&;bDt`_drtQJvxTB-qG+UQXPRJ_Eh&fPX$QObtXtNP z&Th5bnn&g}F{=)n7l{-RVg}3lm7{2w${R zn3+DP8XK#ZWONiIG-WS%0>AQ+HarU3s#o=sbU-bF;whWQg5 z`_EdkY^_wfQ^LbfO4#^|2r-04`(YU%Px#}6Q|)njWf+OeLzFu0E8XKN6g&2k{Ukd+ zl4N0WAx32!;zoBn+al_<2Me{#PhUjT>nOo8@opcLjT}J?Ev3`tzCn*{fYqUAWbm~A ztY)}UnMGLdsr!7-78aN}ryXa6blj^akfUP?G>80PGK%xcHujmQLZ-kH86l;+aj4pU zTC11}WKU<0I8>*2p;;=96#f?@x;-0GUK6gib> zRG`l3#WY_s%b-8b{m8G`u)P${)(Ej|s2~IVlD#Z0p|9?vvKW!BE-Al4=h=MrmS3S( zO6Ye#r$u=bG02z1!0z*75_<&`>1)Y!BC0;MNTng-ZvQ#%zAx$Q%j;e=TI;(yWif+a zw7)WLh-7gn64454U>QwQibSARZ*M1&oF`>^dTqB`u02|!iVu8;m6-a)7WDUmo)3nM z=uXFmTXPT3%xbF78zIX35s`WesdHwYb<_J^#l6z4$PoK9#>|{NRJQ+i)sd;4d4Ry^ zrPc$?cB^#Ew!p-yVW3t85t#VosQ0s8}r=%-%$l0CoI_SH} z2t%nYPZp7qnek&qWr-|j-kyA?F|SvOOfXP?=({kJ?-0!Eu?96oZWX2LD2T6E7KOa# z$r>rT;Fpm-Y)32m^)>lLg&r3%5F6%8e)mjdVl$gc>(te|fx49Xh=XJ0KCk1|6Z}<( z;b@$3&KH-LkY0tk)f4hnL2#C3C%Go0u-=jdKZe4Iy$hYq6ik6x!}}BIt*^Z{rFj~9 zKa*KJ-+e5};*XKotY#IWn6+Ikh~l3iMoAMLrCR645XiMLaqAE!d?N5SnL?MD9)79EnwC$Z2*@Hm4=`cVGswu0(y#(>Y)G z=n6dDuB0x_*{~ojQyQ~Aq*i_DT_6w@7*!y4;U9v$laW!Hh*9)V1uK_{MHTWHDt=*y z&JE@@{%lvrfuqakkQ(bgbpGqaugH<`xkE?M#)vXdNQ43|>t2&P0F+mK78{P2c2j#Z z6B<3@e7nGe#Z+o5&a#g`nk`*E8_qscZ!jR1hT7!p)cRhK?DMqrWNO04b(N-?>U%$K z!Vh0o`DmDva3j{>au*`kT=f2(nMgaPs}O3!PE3FuRK*N7O6zhD|y?XgxCuE4_jr-fUc>Y%ba}N;NFnyq#h6M|8vn&12DgB=%2T+RiL%UWGtUP`U zBV$4HQXZQW@F84QQ9uE~tFC{6L%rau<7ttAoFzQEz0%j|s1F}HiQg^t%QN6!5F_ySRO@5i#-fe5(`Fj-C5WreY6f^%EsQ#6YBvbPA9ZLfQ|;XyplmdYEoi|V{D6`1s}j0Ku~ zOy878zCnB&VKy+SjDC9_^PzZnex8k+b&ZrbHotFL=?!E_mX%^U123Pdykm~Gr;MH5 zx9eKcI8eIBRM6iR zQVb*8m1(i)!DH<4V_3oxT`oYL#K5pz)0L#9yhme9jrg(Lv+WT^$okrNPeT1bm&Oex zvgxg*c&{7MYq%S^>)^Ldz#5iuiOTv(KpIWXd4)8djMqa7YKkM@SE(4462dBS#{vV0 z*S)jumyG$Z%MZUdLwXFn4`0h(t9Dey!d9Ehay>j$?0g&3&f|$~l_{jik|Uoeq>Sn?rkb6&C>@P#QuTw1LD=uGttgR z_{Ws?aNfqgZ3J{{hj1AeAJ*4JdcWG2rWnB*=|&+Ha1lqqT%TM;B<&50eu-P8g%Xtz zt1fXB^KeK5ZZRi1ym+K<)a`?Euoz6T`n0KXDQ~jlg`m5#xs5~=sUII!6fynixH|)} zTZVCHM8pn(ShB1B>9|)R!OUZRzTvUQ3g;K99Q=k;Swv(c$Kr+SpV%(L1fQcdF?1= zU6Y~QQPc}q{}k1>MB<0C^W=>7IV|~Z0dka#)xo(44uA6} zpV7`sCVOJ2S1xkG!?F9Tt!Z0c&wL&Bp4FaxG47by^hTj6R2Mi}F#BfQ*J`0Cg|_#D zh$T}>#t`z4Flo^lxY?fZmfg&no<2G5{NteV^6eINCl#9gQe_ICfb~>*U*vIonvm20 zhGKkw9--ZH&n{*H(}s+viI=Vxkl50eQL)6TdPT#e#mVQ*C65pHhq!e*(%+Yn^u+0_ ztvwvx4K+_@8Q?Iue7qBn~{NUvA(Cr)B>vTPKYRcZD z9F(x(P^G9fGn|yLZenAwF%!-qYbE6_++NK%%tIrj2#n)GHS%g4n%(~ z1^=@AO9^JrGSn+_X<&~@#e^T7XvFBsF%wc_<9r0ok=8njtLw(0ij*Y!uvlN_4~DWG z_Jb>C15fcx=a99)@}4u}_zKU#((T0(TY!5#=o%*tj1B_|)d{ z(AC^>82X2StmOu~wp3FCU-7v1Ka8lK+WKA|)6JVxCk8f}{x(lzVR75V< zI91q(1CK&puIL1Hy!UGJY4a0_pv01TTF-%$9CXSP-bd#~(rU!*_AK6-m|vY0-4Q#Y zPj=I6!2*N+SPS1Va%()Jm611jal09tVGmVtOsyOr{DWD^H(ruXA2FGcKd7$Ij|OVA^YxOf zBB*@jZK-Q>Z@r?B#NWcaqNdW7vwq7dnn0e9w}YpJfAi!OLy=Uu)b%ozt(n6`|CTGq z>=ovT{)zqe)^fk7&kkL#HS6xVh^7!RbpQXf8xUCxGfCQ$2rDn#Og zp_15gji90-P4$c_h|EDjc3qs3hRXlT8zpSdVuTVj-k2(iMEx4o1p;<6iLY9{=whLV z$B(3!snAS(T?tYwiN(MlWQ)})5rlM!)T(3lM%XQ0I|sit@s^P?BZ?vEvR4ummso9|Jm7?H0Ouh+~@KXw65g5L4tVU=)$Wx=Zr{Xpd$YL`$UVdVHC{+t7kU1xQZb`4f@n$H z>sQc?plIQIEjr!yy+Ivja)VMc`g zqZgii445$r-dZs1-6isCntN#VeRbtF)DqJhz11Tq2V#yiwijE-g_XK*Bzf!>_0lCK zpDDc@n70aRrZ#%?eB3a6$XYc|*+M`57>T3ewGKd7>x-h_S%9b?v+IQL8=Q988hqpv z$aLu|uV{$VHJcAeQM&xS>~twH&Q?S40sbF9rTgo?3zz-$UL4rvio8wcwf6R);Y+B` zSXW`X(f}GxQ4*byYL6a>PSD(h_tBdOcX9(m9b=BDCk+_hbzGW9tj_Q9)N>pwE zJ{#yYBZALI$HPJKsJ5Q(YhbuUk{)pvfvp>{w(hNOzSPV>Jh2qn5Dmg&R41h;y0Otz zfE@GJS%|R`A@JU5{3ltjmfB+LwJB-nqy8Jri?{NX zFObFv#ZZN_-sc|&htpthlMvc8$ZH&(*Uj{KIDZ;dBS`f4eir<#Oq2-}So>r%E<@qm zS;B%7Ug3p3WT_?|pXUbs!a)eFjn?Vf4}8slT%%rl$K<)Fu=1?(bs?H~3&G07ly5G% z#9I*Gt}vVRFc*nXQ;CMV6DHbrmjbz*3r$qkr!~TgM7mjd9g0hd0gNqq9^w*iCv}}T zocqM{pcPl@%2R)on0!Lv!^o#Ep5!gA$Q(94pBYg8(Hk9{ypHbhoR$d3X!miR*8S4yQ5nSkHE!l>V;T>ft*q)mTcZk zA%NXpz>9x_@*w8(^`$=!kknlhp-cXe0I5V9HY>L6q+&bi zI_chhu)DAC4?Jh{xz-x%p5yjhN~Df4bCioGP^@O5mO&{cMf^&Ew&>H^dCI z=DsD_uC0f$tIfW5#)_dH==fF`=L1O^lZFn0mWqJKPlFBc^fbwuEbaDE!O|o-3e$hk zcO~(><^wb^-*j|8nS>Vu#sy&YGB?hBnA`bSXQ?q;p;l`)-4zkjBi@fnF)KNQA3+gy zfQx+3DCWeAi&wn1nq!oyJrMm&5SA?VRKzZ8=K3#D0vxo3A4@FOjEBy!-~MzedgV|dHGvDq$W3vrKvY@ zX!LAiARW+ZxNuD-v#@Q!D*)}nUz#AJjPN9C30(8}<6IK#xmh?oD$zQVXJYXu+-QMC zS?6XHv*1ByMm#g7B^08@8>KsOteM*eY-T!I=Jc88r1=x$K4s%UlgXRYG^IY_@y(Gp0?M4!9H$)-M|c(Jh5)+w;&m|#N6cQ^S1VM6I-2RH>DvS4`6{T-j2{~+hYYg z9VG}YpG;aFMmboN6vjFjS^+zUn+8flIU(L`2#Z8EC{MNnL^Urn=K@>pC!gU${Kv}h z;VUO{$i2Cm%12%(BBEw0WobOY8L050w5Y%^eJFM>`kQI#XE;_Gitx89-$B5|0vP@4 zL0MbDj)nM&Z-p_x1sq*~{i`d4;bW1T$iI=jGsf_;{hJ&bP{-)>JgkR(QI3(IQh^2o z1D2{N=I{k!km-*{UW&LV3!AK43f16a^Bg@f>AMfEhnEdZ06$)6!?W zbVF!e147Y;q|FVkg@&#TUa&8;%xtm$Na!)yeb!r44ePMFT<`5V<2u{SN{1E{n(2S5 zqm;Lun7J0*?D%&e2xar`?|Cdr8NM8-DK($Dy!(U!pC=Cqz^t_1~j zgZxG2Xh4rI4#76Y3iu9$FCY6Lv!-> z5Q%A8;X=O{GO6Rf)^cno)vV3KC~FcIspN(s0K_xGDo)>`Ts!O?_ipFrgwB7J4pWu! z-6Vy)wbt~lz>RLiB513&=gSo4H0lsw+cgG~^fP+xZ1z=0d+#u3?a-}GEsPDxKE^O2 zEig-#DpEi@(hv-y#dC{zAzfD_i=3$_sC7J+(};ve(MRor$st9+}>4aYHt!8DT; z5z`8-Y`5%WGK*6XTz0-$Fy8AmubNsivP;%9WltqrL?bUvX^CG_6B(&q_0Ib?sfYR` zQ8IGNh)P;Mx3qkEpfd~D=Dk>-neA^Z;pB9mK3H2Dv36qKXI}In25m||w@_7?F!HMQ zworpo=ZMrDzegPGkcT@>7D(+eWINr(-*_E|jRq>heXi}~d1Yhr=DZOgL6z_&h8}F& z+0uq%p7ni{YoJAswO3w^_U_i)HQh|QG6kZ>C#5&Nux3F^WW}AZ&RTKv70lc5E$XpZ zly!dW#ql*Vk>(dVr5TQFgO3|)=R&^$={Ghh8AwEgGPn^6V&d@m-Ol})#RPFz;QIwM z+UJcDm-g{5fH-!860dJq84v51fT{Solwy%)v*3A;>M2;U94YQ7AN-PHS>U%d*JSXv zBlg!fBX>TOBjwqrFM%q#E2_hHotqM7ViFs^@P(PqpP&9h=OF!&Q6zaR|0@K|j#5IK zXe(ZhOX5D#Art9lII=U6$F7`fu~;x~B>fSD`H(ZJN1TVeuq54<(rYxV%v|&x6_sW|SyLQe;GD~m6NRGXB z6713!K?dg>;9NhE8S=keq?V$mp$m#G<4~$q+hAo zDgDl#KCJ3E8^wDl&GPg&FZbt?^53^$>3VPv@XeASP-55-r;+qCIHjA`@|dKZ$>8uz z1RN7nOQLH?rb`mp5-FL|R1-D20i#JMMwF(>?4Ua$#kT@4vY!w}sQC^`;ru`LK3wBu zveI2#t?63jDe6iOZK*`*N% z>@!-??cbcc#r%2CIyr)%ip>fCBxW|$cPszpvh$zl*#9$>`!5E-^q&|28w>OQy&jXw z1*xMV`Of+6e;nH&{D;#%U&`ri_r{%fD*O5AZNi%<2FXd-H9|D= z4wXWlgfXEOD$%iC8!4ZQw4>oegGfXKU&O=P=f0+$Klqu}Pyl2md`b!d7Lc{G27O^~ zkeiu}h;ANCZI6RL?}hyMdU#|w19c^_Scku%|a$0XtgbaUUs%}C4X;JVxe zln;9*f3=DqWhAUm!0*ii!A^h`Fv>TkGxi8Nx2|N>e~s7 z#PxYp4M|OaGeO6C$q*hl0w^SH(AE3v{U^a1<{kp+t8@L19qu@Hb3psh8P$Ermj`s) zE~DRQj7Kx>`J8Yz{pA7|A=B{q#ScUgX{eW(%an_)b*$2*p_f<}19&H54V~ z$T<)}Afs$ARV>n7sHZ6qBfl=vUrc(b31qN)sI<;}G)nn3OmE^SKQaB{^gwqW84+j; zI2XA2DPVK|2n7sEW{XBNQDazS}OzBIqh*P4LLT0?9l zoD0iEPi?!;MN2|vn7AYJl!_LCGE+f!DAwYEJx}_582ZbHA`)R{xC4Tm6G$MQ*JqJy zO*6yV!r$cUQ*7h*AvngJ&DJHE@Kl0rJZFO8%O{w{#_afZocJ~P+UAwJKqQ~k+iEf^WPjJFWLAc*M&^9f}nQk z+s}l0Kv4nu;6`zw(*i5Te$Hs-sV6B`0#=zMRtOiKgw%m(fV#wm(U`)qX#>*|_>&xt zA;qIclxE%d?g2ieTU$)HhryS#&a6OA11DjeB3wB}k326aDOWl6-cDZ2dVpJQq!x~H z$B`tl!m1FXdQmKu^+c?>yFOkaM1&87o?lc+G_o9)A(AKPRp{0 zRI)-FS^?CPM7WL`e>zW*6N=BE+c})u>vN^%P7;iG(K7}a%y&zHUVQ-X_bm(@(3w-67CRjTm5AdR}*0UONm8 z7x~(pmi@jgwi?YhCtFxmuKdE*N|~igTlf&N2-w5{YO8)g5_2;oqglXb2!BB%gEaac zg724q!=hYF-rXv=x!I1D-13N!j49=RUz`-|O>lf33BdU3#rWVL!Kfv|4e5?V0G(>iI)XqR|!lZhyXHZ@L{9XxCey z-9C>tlB2FR2Y>!?emVo5=Fc5$VdH;TVmq_Si zdPSrEOcB=?zUcVYRVy7j!vEr5u-&LqL{M^{Sb9Ha!8eA48dk}&U}1z{xX0B#0I?Ce z`b71W8b{MYdUA`iK@Js0MgEn75k6sK~$T zeBIt?lszSfMei9Ii;EDG1d;juOX(m^ymPE`dnXTf(^;$HMxfP|8gpKCGCog{b~gfd zk9WgP3~fC&K=5vmw9l!!qbIr-Vn*9^M8+%BO_(9d&kX!!AZs!e?e~rE6Fa*qv!+B~ zta!a+$h5YrPJl$*b3uQ9!Ecb+3Mqew{jr%NXt{4xF|Rnih8@`<0f7&Wn>7JyHT7J* zda{9=aWp=tU165BJ>X|Cebl*_lLhexC zJ^e#^X~85~07&(($A|Qos-zS{)YqhrPk}3)=wLV7-SvoIj>XNYjePC)g*<+5M1u8M z^5;vwr6!b$e(iiDaFxrW%fhh))X@b%1!&x$vF*^Sz$cavxCu`fdPf#_$XHFv5K9o20-Mg9QfOvGy~T&VC?LmoFYm) zH+R|-Jajk@%I+GPc`@(t1rTfASPQX^Q}7NK0HD!!1qSXnMe{1Q!4waf$0An6-F#|P zT68!*y!RD_X1srd%p21}crcV>FQ1u@D_Urr5I0RjDJsu>l)3hqrEZWnlr6u@U%X6c zMrz8`EADG1;PHIzuQaYP0&z#x7|mV>r`|?Ni5ZIn%lQ=qODx?tCx3Z=Ymy$f<23QF zR7gcBN8>1_M>kzxWgp0agz|H^@{3`dTXM(qrl(uyw2_lli1iPG506EeI53oHXc84D zD`RzTjI>yS%SWC@>3Oy0mWmPOZ6V~}jTwD@?Ou1P5;yu;ZC{xv**r}ZHLXh~yDE1# z4j-*`V{^QOOh+id#Y!k+o)AQ9tDi%A$Cd8h1RD+vj;GYJ)=J=+>G4+TUm$5-@H!n8 zHu53R&Gg0XT%;w0+!wz>!434;N$R89++4tD+(U_epnat?k zoUHAGU`5BC0`h*I)wtDPDE3~No9nH&`?`{;0Z|kQ-x<}Hn!PH8l-~M*s|a70!5Y*l z4i_H5^nBpvy-ga7I-NMHMSEK`OSY^>5)aDRr!L_T$j`dJxFB6n6QeFQ|ek{f*MQtu)P>z1>c;^jhPvJg&}*E175nW z?OV-ume>iWjfI}EE$W74q5mM68HEJuP4Z4HTB|!t<;sn5#$()^-#!TawZKq(N(Uo| zlq7fX+5yg{!5s^pjQXB!!c^TMeY23ojpbnZnT|E`cR0NIiP0p~2i8@c)Y^aryGmTH z!8vg&t5mj^eo>kT4BThFmkeqoNO*1p)omhgRF(cchTJBt`*q<eJ$NezuqkW3jdNulzh&LR=*JV;?XV%E%-$=%z zf$sXo_9c$)VsB8(LZC&e+}j5p~JTDq$F~0x*rT>AI4u2(9kQDR6K;sB*t5@ z>O%uu*qt06PqP;s)3|%_2N>`qZ=lH@YfWuW$G@VKFn~r>O^8D5f2%zgj58yQ{(ZHB z_w$#uV=zHl4W^7I2(2;fA{Gg(Zrz1i2YTIEZi%{XA)3i<~81cN*{jEFK zxBTWB9j!FYYIOqlOa80y@X0aj>FM{3r9zG^6GOC%=3kNuaS`gmG&dvD4AXHlQy@qYH(fI@#u=Ioht>@7jcy+aU zttWNu@m5DhL+|0%ry78GHm~3@dJHvf9Gl!9d;NR{~~Y^b?3I3EB(zRPDbVP~;;}VEPb( zLJ@ZYK<@yqjk;P8XdoduPbK(V;zh15E8NxsJUv4DoP-{gc)SzkR*d$PFnI)RG|aFn zTkCFTGT7*hu_~_xMD-r&q;U-mTOB|V**9&FFEG)_Rnz^p*3%=Y4uId3CBV$hLl@BS zwkluUYTezo>R*D{(;=CRRuaI`Mij^A7B9#6yyH_ zG5!Zz=>H%F+kYN$+1UQ+d;YU$oXZ8Rk+QsT%D>AJU;_%rL^jDp*AdtlOB9L(lLZ4p zgEN7EN&**60!_k@k!BQqqGWcgU_zs!rFF1SxG*OWW7IwFOnF?4D5-d4nU`WK<{WLbyX^Ud?#>3O2&}CeU(iM$EgJ4tetSX z{vtgBEHOFvXmzH;*^-2vB5GMInAW8yiKH{a@Q&9w2Yx2^Oh z$~#VuVsN73Z`WzS8Wk<)F}9F}Tth=vv3EO98=A_i9@z}WwNQbi1CoRIfLTBtRqI}F z2oE&o?15L8X^FWsvg?3j2A$G3YHE|HpENbKviA0^F zxOT{zcKq+75fSy@uMQ%;fgVPnOJSG-Jq^GU$T73XCS(Gl<6J&r#-J$Dqlc^zxCt6I zLMY>GJ~KoQx0#{vXfzn|+h{mGXya;lU+8{R0%OD!phnVU|IR7O$_Z7_a3<1Dc;quk%P-nD*?BrE>N83 zEK$a%L?2-uEz(dfb1jQjnO3#cZzn)=d#B6ceEFlo?h_V!OI5K*pmH`uO%3mGp+T%V zt}r|AZ4ulL{DdjMq6zZVtOcZ>UJ92RUY{>;u&px*XC&ZOV* zFGEDP?e~5Qj)(Y+9(1&Dl^tXZNFJ#JrT__MDNXAzwa!t<`wTGw^OUnZnDdBc8mA~;Iw`IU zhrNBK2wTeAwa$h}h@;5fB?g(Bdocx^Daqnz@TMj(m7@)4J(Ps}B}ijNM#DWGN|6uZ zP@G|8drc{53G1)DNh360nyC^fQk}EJ@O9N4u}z(Ri}#MzwFX*~ie3e2`W_Qlw^nGzn8E(uqUH4k*vxVHR3#$mvfYTSRCg zQ;rTwL%)|g$)86Ku4V=+I1x!{$>qNwBXD^0u^r7+%%-`l zM<0}-0H`6~3|kKrGZO+5&LJ>o3S^P(@}mOqv*r7GJTt5gQ^Sr7Zr^=)3^;_ zdl?aP<6QYGKcIPkNt)`-jb;b0YldG17w$)uCHPFqk{c+E^TQHycIZMfYv)*#JlC4) zVW~)onW2!QUBqB-E)f)sPd&&?#{!zL`0%5ZOo(#|=2U2#vm#*(5k7&<80t=``3Qt+ z+xP1N5+KpE8rl($n4bxnhq(NzL+u)62+`3i)h`P8`QC3sDq{;|HN2?EEoa&GJQ>kj z=YMquPT~OO#TR}(;CB{wWg8Df??f|w#%h&^^<1LF80S-!Z@u$c7Pv@wLV6VhUS{VD zGv=OSQ+y59nwb6A&Z}<(&4XDdc$inclv|l&>Sn--fq|cYnQ9)MToVvrkiUQ5PxgJg zf|lfEUv~PN{jCXidGBCNpDCkULuiG@5P?<^CfFS0ApEitepbiF*QakEzXHr_0Xsk^ zTIXs-!7WveWb~3{%sz`UtmO*&>v7$!tFL+~bNk@k5AFLV;BpqdAi^N1oJmSJ*s1WS z=SonDg@$)@MKfQn%;vv*1gvbcUF^S;lQVM%UWPl0M0IQzTWBv1yI2#qT^?|k-NZ0!OLD|FKAnPLeFsf?2PQP%v;QTHdEyn7C)7K-nf#?Ur zj8{BB&MVQPf)MJVi6&`;DZ#*n&H1~8s7(|HzZGZwT4C!waX9Q}V&|xfRR$SeP@VB~ zLD6hh`QHEH)>O8JwTNo@isN)mtcV_ts@@*mDU^5ywKeL&9rfl3d!WttaN`W31j<$$<`30l%*oZe@6)7uR@>hBCW zI@Tbt9jQv_m*o2oa?kQRTM?4!_d%oT3%HV0mVE!i_kOyeJO?bQ1;1K{Q)R`(89*U~ z(Ify8un|c#8*Ar%nF=7mAQncsZCx`jY7?h&VVJ4RJksz)GON*c-uNAx(TH5=_Q&1)_M5ES#hYUcP9|* zQgMpbt6=L(F<6zMPhO-CppVltQux!}?#FddNDqBO%U3bC8 zoi3d#i8`_o+yq8P_ZxPoUN2Rv%jvf6bl2CN&54%kuCB4KB_KRDl8JtJ2}UT7q3Ie( zf(vEHitrB0ut1;-jd-$i(&bC#*57HN>%QJ4qrIEZ)+j*)IT%#G&McA4yxY(@@gpxk zgdGC2{@SY9m3*MlHvSGSkTR`5$d{hf6*8kgxoT|{(bN$Jb!&XuoK}4;8mD_)WvL3`bpX z42`y|x0yYr06U+oO2)qg%38B7HGy6-fz{}YXBn7o!~8YuC1=;;`f@4n>&W%$ z-wYL}8w+8P@9#>zvm2tQ_OWc5N+W zyMMfQQMG)rHEP3*&)d^Q`dU~M12<$+YhK`8AAKP;>u8-Qi`X39B;!;6hoEv13Z<*R zXoPd*)azm7w9biFH>~lOIMvcK*$>ch;})9XD_GyOfOonpAbjt%?Pd#fAbZ`lElhtS zG^#f({RQ7gTns177$f3Q+LDLO}Ybb1@MM9;vm&CoA+JD#@NoNGAY?3zId?rKs`0H%XN;Ti2QCtVppT_$xJK; zMd4K_g}pSks&2n60(! zb{JbDKE%1>uCoXZ!kPza_}6irVza?q!Ixp@++<_5j2d+$tUGh@VCU&Z*UWZS7PD~_ z{RK<^bCZ5N*2O6*GkaCZoP6HxdxzZHOB_}z8}R^=Yu{!yuhvU+QDF%e^)&Lq&ohVi z{T!KRb#QP)y$NwkX;PD{)8!*1CdWTwxJiBcV8QGe|_?$3=X;sctEHDtk1o+_B17r>6M;NPOkpbKzuJ zw0?c}d}eX1#54NG)7-@v!X^JV2LLLe6PW#Q{25j-50GpIN_(;lf15ksDcY}9v zXS{mp%!2u^DXuh)Cm?;X%GnMng2D!|IIiy?BhUYK+I@$b&Y{AG9hZD4gUYPXL<>bq z$_^@a$@%GiBLEc|QO#x+bPK|{i79qN%IMDyIzFL3m+I3!)-MY!MKaBye{%`CWqoaz`7T#I zxng`2e<(7lkplU;8}2HRpIsQYgHNR9tA&FX7jESQS4A zZ=M}#N_N<-!8zt-1htDGPZ~rTy%xuj6cxeoVAm?6g>4B;bG zIr|GX9hD+Ut3Vr2HW8F(u_%jojH?Wp@jbLN`28)5?Sj3(tKNY?KTXd*&CD^-?`*i~ zv){t#^?S<&h1}5h4*x*+Q;Y(qN~d%hrbp-jQ4SMHgG6KJlv%^?X*0Vac4MV%tH@KQ zP(cdm=2<*7pR#ljei&_aJwZ5!)5c2R%dCz(3{fj}Q_-}NFCDZzrd)?VpQ6mKZ`?^X zib;Jp3c)jr)yL?AfTgPmL2AWmHKghR)7FS|DYF;EGk=&Wy{$d2NsS^WX?EJ;M{Wl- zp+6bQtvF9ibH{3S6;LT|N27V&JyxNBfdIa&q>Du6mAS~fOM>6Dc5AKIwzRY~{p7YL zmSZDCPC8AAZ_Ve`S2ovbH1X6tl|0F0>+(E&h|k`9ZQm8qrxSYD^B4YMJcn1j&6g@j zLVvU!wmMZP%tbs*6eNg-8yT!!o)OCNi24dOs!#W>Xz^!2wnn9*_X{Yc?lsw+Cl}* zhv%AOth)tcJwfwpDgSGj0qkK6*dvru z!Yaw`4mcW?O5NBsuF!?GQ{Hi?t#FSD>M~7CR8M>Tj`^#uGl*A6lO8|r8aF5Vl$U&k zp^Bk$6@O=2@pl07u;p&UP&l)dhFt_pNfo}6D9D6G1kUZ>zz<|`;coTQ^h~5uZ=(DC zk)`F&9wgp2is9$)^M6~oj)ld!z0MoKT*+^o$Om|}OK^N;xDZhg)*}U21y=P&d;Ud? z7ky6WAA7rOBeZELGt6a6v-o)er(<=fyPiLGeFYT-2JfQqgv0VuL2IWwYxUd?@55`6 z$S;;TzTNl|H5&ECyJ@j{(0BU{g%FsGqQ8H`-xpr}1PTzxo;Fk^oVH34)WPjzl{}`U zNaj;%X|Fb4XHGpgnHwMaQ1E;JbS2XXO)o3n4pSiUJ#{Sh9PxZrCLJ{r;A(NU_+6;t zkU0GaH|Q;#oudAFE}3mNZgosOu48YQ`578JZ}McVxmzgNiaAy4z3GD>%OaJ7{*>V< z2}h9E1hq7IVa*#B6Eax&hWV0>-a;ArNo(uxWhm|`OeRu(>(jjn@ZGSMfC!5^;^%14 zAZeDhJsU53pxUf#cC4>+^Iirr)6>KbF~x36Al(g>Is+~%-EK=1vL+KIq7uLhjh#g- zb3I$spN|^WjV*O=q~Dt*Jx@GPS3_B~D&~u#J%0A2$h+VQXZprWu&=4}c>Kil?4TwC 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 LgOO1}N%H>zc@7^` literal 0 HcmV?d00001 diff --git a/frontend/account/static/description/icon.png b/frontend/account/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..dd2104e03ae93ca60dd72c8368cb9492e36beca6 GIT binary patch literal 1949 zcmV;O2V(e%P)56Wc)rSVD+2?1czME=jMDc(vBy&(Ze=X4C))FzH_0ACA*CVao6?>N zaU%nKzSMs7hVtYXNO@~PyjOFc2|)sPBLj)sDoh=_snPngdD?vLk&$f|^0cGZI#;~8 zP>%I1@k9vf3^%JOux3VS87Ekh-g^}T|K!pZ34HUp99|f*^HQaM z^y#}AS%EON_nKPtHmF$K5MLn`O>VS9{HVZ})XPkYYIzN2$d2B8iQ6c)lr02}LiDD} zK=aArAUk zj)mY*f?uaY+OHR?aukG>gr0e<^54HS-pwpWj~<~4Cq(=NRr$_d%oNpVtDq&nGA$cL zo}0wKggt4Hn-C7;h@(-)DUl%e!m-qC@6%}8?~{93=h^??${K%J!ICvGKG9EJ+Ise=$8&L>hr6N1)9DX?_i!gl`V)lCjz_o?A`$M&iCLg$z-za5_8hhMS}_E-*^GYPE{ROeRU!ERtRO|i`_4~SOl#7poj z{>DMuWMWuzA_GOHPci}_)Ex398@xY{v9i;MDEVN z#1pzM*^Z^H5J^|hP)Lm47$A;d`9trryU13EoTxoYA(6fIH}Qn3j&|5xV=JU*jf5?s zE()0cE?py?qomHl)zM7Xb`O%eacAmo3Yei^E)YlPs!E?axzLf2%de!mbkdJgKs@Ze z%vM;##0kh9yE9!M^gFrOaqC1;YTjjc+($l>XG`7toxc(<=sNY?>-@-ZgmEkcxg@XU zUT0qpl21U!Ki~a8yZ{-hDy?<%J+7LIy|32y diff --git a/frontend/account/static/description/icon_hi.png b/frontend/account/static/description/icon_hi.png new file mode 100644 index 0000000000000000000000000000000000000000..e967f65ffcdb0d044f870844b85ab70170c2f5c1 GIT binary patch literal 10461 zcmb_>XH=6<(C;SnCMZ=@0wN&2i723nprV3QrI!dwlOob<0*WG_fQ8xhUQs?}{Hi(z)Sudt-vJ{(+p@0_btt-|_`vDXVC9VgQjJxZxT<8IPhb0X>0{wgaH^d@%ebkv&~^tF}U!+dH)kZBe`tMM4*1; z0_=wv01v0PcH2Hnk2R#9L8MPq+HSu{5ZD;)cx!6G5c5irbI!8tblOp6Ry>8^X+Qmi zx`b=)yN#2iDZc;*D7%SErV~$Q(d*H zuFX^~2Ll;GqCIhhp=`tSC9mNtfy*;iE8Ad5Ff{il>i5O=$u4g{q&L zIzAEExTkCFlg!};37Mfu*Vjug1RZ`sZsz51pwgkqPL!QVr5q4!w&JCk&oKfZ-IrXd zj+ZoKN5~Gmel-mFzS+ZR&-%F!ykeA&d(aL+{YX)pXp}7FtWhc-hvtbR=+cogNYBjo zMa^lm~bC{*8VHlS4{y#O@u3W_F#{oU>f_|8oH~_HSv*el^<2Ktk6`&XThL#N_67 z2K-kO?Rj5I{+e#a&?^dUWAZq#ADG46t$G0fYO+AtANkIK4F<5E2l4`x&zWfekm2T( z)g;u%4+#e~pKX@>uQsmZ{)`hg_OD&`7_I-v=PHptW=O=%pWOQ#veduoQ49MCY5BLn zp+u)PiMY<=7JT(`ssj_GQZ9X4{aEUu@A#ulsiIKXqTMiA#t8DQ=S&;{!K znt2>~GVF^J2LLjC4L|J6*7SQB=qOukZOL?qr)L4^Iw8FKLq;;VxP#qnua^rBdhb-S zkRKU)P8jTDLUUzo4yQ`4k@tHuk9OzcOWMKlRnylA| z>_ax&h$~WY&;}BYAidCc3FM=Pf&3i4am|*=y0%3Dqflt;wXS#vH$|8D;5&~4tde}d z^RlTgOPr8WiUH3N9*6i*h|SGuzI9WXmk?37=*Fk?lHChhsfJ=>39{z5L{PpW44A~7 zKT0lJ;hWR2qXp=`tZE!V!s9RoYNKRLeSoi+6#HW0R(Ao67l6sK+o1_i$?MW z!1DLcGZ-ZSpp^NOy;u|mhJQ8uFc^L6y8gM9y_g50!!IJs<7k1&BW9!?0~{2;{b}so zpIf8?kt`4?$U?+6oB(LL^CZ+b6Og;kjErG{ffuO13{QmtUIAj4LcQT(h>lbMA~QB< z?g|0C$f&=Um}PZy_6E&sL4b!(gY+VA&;AZ*lHSvB(A5&#jC#iep1{Ho4m2>({pzt_ z_J&)b83o)G1HJx6do+-7dY%52B9L3i_uy7XT^je1E`{}YSg`DO{onUGhL9u z-L*~A^Z}q|Uc0V+EGqMgVaGee!RwrHb~$Aj_S?xSqOBJa@IvFBuV}y!cP&?gv2={! zLZ~+qKg|dp%D!2zZa|>jX~CyCIRrL`6LjcEZ6%-FHJ@ep=hdba9qi~!P%CBG9{xptwH8_&`>v@I4dSg$WcF{Lv))xJmZsija7&>^UWV6XV1u9S0_g?on=Q z#3Q7iq4KH)?qFkUYrv53edY-Kb5av8*EJfG05~XeS{_RE9P%C`g5e~9FD!3n&#|)*#sa^uc0aP9S+?ZbCW?nFAU3lT$=jW+jNmZ&lnCg7Oo?)N1T99LK=fr zV+5>WaYz(2K-^CW43@+Qq#_|Y4WiF2oFV}ZU=n)i2hxlZ@7BA;(D4D_v%d1+QX3n( zAu0v>*BhNQEoN;$BQlt10nhAN1a|&7NQgfS#@d|#3Gal1U`bkFDr;q(>i#9_+6@)z z{s$)XE)yUnxQx_zU@-FSBi_Zb{l6OCmzt*c|LW*kcwiqG^w6A}@a&75DVjr!Sy^Si(tyc_JC+fnvBpa{fxlMY7O z^E!7(zDqAd7uo{|_yUDywb=fU%`NF`sPiCB3pzDkH4MW+3wKn#)2Sn!ZI4-a?sa-uYAT{!PkN1Pv`5T|o=@0}^|ttOpQ3 zqjPuCoXlqSN)&}je^^6X)|QA=yqDsDqGqA^v%EL)j_VaxqnNWf@80pItAjc|SjOB% z%c0xG)&q`R--9bP5BHsA3-vwoCOgZ1Fk%vH{B0KAWZ}{TSIS!P#&2%&ou6De#z=#= zpvCe$?*AUFKVkb3vtFNt5T2`fh1p&!QS4a=dw}8qGUq_mJx-$|_B(-GpB>gGE3+MD zZ(+pkUWSwzT@!4@ziaoR!*0`J<6eEBsFtqhzE0kjraU`=*{?Ypw9()ysY#4HZb-Yy z^O*kXeycVgV<@Of2EAwN5zDFvrM4{Nnm%4e&XHDzZ8N*Qs*U7!9S#5u))AC!9XP^F zeZmNb)?g;M;1axfMl+I{t?abRKGqhBHsB!9JVf7FaiCcrowXh2BT4asq8MKp*OQWO?P-#31BV2Q zNK6$8x3P9ylJxjoTBPjc<+p3N3Q1ZNO(NVO=Ctm9Wa}28a8B(~PukI8M`_~1eC)G< zVLH?^-yxUq)sY2b(oAa_->xKOmse=&ueoO7f)tGH?+0ZM_bN9o>Rsdc-Fx3`klSRx zSK0BxiLJS1UYN5KU7Q0!i1P$SmVv!Ml{2abUg@U&aVy zr_P3lg~VKj32M~yP!C)Y{+jI_IGD|nD36=JsE{=rW%pEu;{GY!HYiz`$8Gd1puYX@n#N>Gy7W^4{hjt9O5UM5xvClB5v@q)Csg?l3{*-%(Z;TK~#oj zRxgigp{_6ir6>1?6edSw6t^bh58>sM{HCLs0?y{0g9?S!qKP-!te6ymYc|x)#M&N2 zRGIcZZ*SY-k^Uvyos?SkeL}w)&*9(TS+T{?t=;%qDNLzDDcj?6iFuAoRs| zF3k+Nb@$MQ5_SqCSa)Pce{+wBE}Wu0+xy3~IcV9}P)tS84UXq`GT3DIasFO;FV$kE z6(et5aOieZ`11o!_3}YTMCz89?})muLL& z4d&O`jHdlp1v^{UJMWMznrsK-^zwya=no~69Ao69{qpueH<_$Im{N)S^vf~Ek5xiD zgLLoaEPUHuq9eUrwX(~2L$WK)svA~z-COodRz5qL+DF0^Yg7NUs4e6Wg{BBi>iB@t zJp~%CS!It8*EgeugFTvueT3@&zVyd`J3;5PhIb^}096^@tSOIe>BoJKrN zq+({n)aFUNCxoUrCsVua?xJ)IulMpmBEQ^&YWv!d@#fSw+FiwkZV`+M>}skX+Safs zJmKo*2PGB{+`q1W`}s3C!8Y3e*k`XMzr6mozt=9O)cE``fjiv62a51Y7=gycU(1uC^`0O}ssZQ@2fo&jxpsV1 z9q?u4uJ5Kt0zG#3AotYQHF@InrX1jf195j%lJdaWQh++2E2~~a&xI66&(s=wowEeux}e5L(a z+xT8~a2t|+{r51z8usU*Zrb^QZnj9G^0$-lnRiSMCt>K5Hhq(O%iL6p>kL!fORMJX z$&F{^hGBub4seqgajb~b7dm9TQ=me6=uzyc#_x*sfMeEOjEsQ; zKMXBUvFNrSbN8**<{#hfZ{OvWI9TYY)03}v@))lAT59qEehsf=P4lvS%LuYq=T2sA z&bOpQfyG?iiF;nEIv)rHPD$wX3cE>3a3LAUF^mvpx+(POWthz5qVz~#BHoMN0rj~l z)bjb4U>;c>*j2wj;e3L;a6FyO1a;~06)COF1Nr4S+57U#Y-s6xPhQBv^W^yXB26zK zGxThIWlvWqiY6So-HveK>ag;pVpxL6JXv;fYR;#CegOD5e~{!op~~vLcE10Ls@>IS zBG)u3hbFJH3g^#iotA%9Kz8yeeF%kFP8*KDY#hZiPkuS&!9a)2;XC!pl3uZqjXsiv zAyW{34at{si!eKT$S24Feh7h&Eh5MWs@zvsV2S;E3BlY=b^gZlb&>Te&({T=9M9P* z6p(riVzPGEQ>YJ)`X9j1Ijl8;-$ZUc@D@xM7viADhE-h!&dsBjsb6aL6J+pC^>Q35 zNFP++gz*pMMw(XeM84!0=sTM9emzXRQ_h2Su~_-qdPOsUaOnO3_!b#4V3#Lc=^y5p zu#`j5n+c!t#bAO+Dr~B0__IoT9CDi?xg262K$yLkMQ=0qSIlkcvtNaN3UaCo=1^-y z56me>no%kUzXzkYD(&p7eNMkT&gOcFb~MKehL6Jy?JoYc^8ZufSIuP^Vxq=p3A+YH z^ZskvFMw_nu`zw?XM!99G(N7}v>9w>+~A_Md)^x9W9B~uuzmh7AN|F;B2No4DIQWxjQN7?t@>j zUd-9Q>K<%{;>fQ}I0gkr>R`}1yKru3q+!1H75xAl>Syy{N@M+r3CEzNEgaQFd`wnO z7L19myDz81O1o5a9F~FGaBJjEu1PpUYbVRYMZYAX!y2Us$ND>62fykcpX<8{*{uS7 z`kd$FfTCrHl{n+zcdp-Q!7@rsf)vam+>ne+cw+ zP1#dd4;R#D4EwmlYfYktwQ7y&7K2nHJpU)un7i)o?iJ)|EVdd{9j~J!nsM{fF|bEc zOiws_>8T8B)a#wXK8HI#F3TTPYrG4eJfYyWIjcU|7~@}-uI5sMpQylgV_iUaAMOg( zqHvLQQ^4}0x6Ph$qp~K4(sso=Y$<7A|o)+$o#xArb>W< z4hczN>_|aWeOoQqdVS+pq7rr5Xza~8#iLI9Ns~Xq520?cByLlVN!6s9ShYV_LqMh^+201{ENp~R#u)KrA*I>l?e2| zc5ENgrF!4w_d4s>wws9GL$}w*Vlf=yhA`XZht5T`!*4Q`9tuW0vhX2Ih~@UDWjC+z zvRYLN!5(V^Y!quKK1f$LCthr8{BpT%5PUcRfRhnk@ck=F9|Vwb_R4s|tMWZs&~eX` zCV07VfFP;4t!m~opaKCi5NMMakdbsEoF?q4!suN*KPSk53Ha8;>ohp>n$W@kzhExS zXWvWd1G}Za(aitTfo*e9s{!C43?oD6ThTOb|K*`HBkk}DqL=BX24(j#-|j!#c`2m~ z7W^=*$g;JG;%t$n&oOt`17)1@hi_trMYAV>_I>;)CouU`H$t9B-A%pW-|i3##Y~pM z)53X%Jm18;B+w~PW*%-rh(TAt#~L;m0`Q+!kg+`uGBtD;gmhATfUAk!NKQH_c?F6 zRc*$_y|MZF7ubW(wr$k-^{%{wm+jGxR1HgL{xJ&YVROD_U*KZu_zq!7I?ojqn4Ub$ zO{krmSoKl(c5^F2j`-%Ea2;FBqp<+zc{OYWD<)~}a;@)L5p*BMK5O=z-O&yyG*66B zKpJsV*e%CyBYe$cE&sB+A4CgdGp|DSla8Qz%rqB@q`GN1dUN~=C>~)~dS+0~BT|&G zOW7+NqNcfQlIKhh7(Va3-9rw(0(iZ^HS`1wjpm@8&dMgY8zNc18nx5R%$bMc%hj*B z$M*Q#8{qVDE@7s|=`;|Rnj$GEDXwwMpGCydb$~uU-FC%lR#|CQOd?`!d~8}FGtu_B zFozm4tv^J9waMZx=U3v~N^rPV02I9RD=phI2N!G9IbP7&(t3UVfH$HIGfLHyg;1*P zf3K&JQ~qngTyx_?`GNZ}Z@7XNp%Zr~@)Pxtw~CtSx&lW#2$anGdZ=(>hbk8G`;ddN z0N?x72EPc$Lwk5I9@2&Ymm6EKk~?|{A)0&=mVn+WEqq)km?r~*Fki@+ecqhb4QNZC z#`1DuAfy+|y@{_htX91nJ+z(krJ0g>2NeV%cHx(J@%K=wyXI6L%C%L9kpQI)TrCh8 zcWq#c&~s>)T?f8&7ogj|hT?SteMU>Maf>}fL`uyPLO}?WXJzq`CPEySVf->_lMY1W zQY)h41;9rn*E<t)z~sWR0NG^hi)* zgll1+3h5VGCo#6lO6&b&d%b%vNd!>7X;0ly^frT32*85wUGX*qzdAtQZHsMje`=A!=Wr(@ZQAcuC3(j0X$Sj-mjIs> znYBf6yKBm;r?_cx-AAHgjg2zuI(5AucwKX%&U{mH7QuzMUJ|H?IV9sLZ^ zVHdN8?g>Yj-2sB9c4_8c>SOShj4H_^ zygCH)2{phq-^95w(fGI(wc_J+Myu9Hn2{=!Tw`&1O7Z-zZdcVFcMw_#0-zODS-)Or zS(9M>bHQw9a^WgRfj|QvHxdQ?=5&-vD!X; z%qbX%djKJ({Hr{U6*@X1SS1lC=MXrLl7-E7@Ixs(At+f4B~<{H2TBuWgwg(gvmDOB zA`Ro1k_y!VYCPs})Z&)E3e9`f(YppenLs(Cy|qe`4rvmn(|d0~tR1l|!S>~eZQ!iX;6O^kS+zOTPoQ#Umyn#c3cZ zgzM#;q~%FvHTf;esJR=%`g-dDf)1Wou>b4rs{Fv z08h}>|G559`=JMip8ov1LI+WE5TcbqBj@}`f?dQ!xPsin144zAw+&~^p zgwV?2Y^*!AjbMVqFfQ>`t@4QOrk!WBa_Lzek15F6YI?yoX|3ifOh&g?$mzjBYOt>C zCYvwZL<2i0kWG#4zW|EnH8E6nCUkIHnuQEVl5PYWUE`;YCDNm-wNLxXLl!;XpPtA1 zh!=!90wawT@(6`tp0kJ=9Ym%ebxw9F0MB7d0rncG@%<1L-@0W^M#ek><)^S8c!a5l zpq_sUtl1mpeqsfg=OZC&bC^@HVfR+4ZVR4=Jz<~NKo5!35H!>{!(@a%n7o<_IST1P zr%4SgqfDwYc%wip5|W>7I&c$Q2EC>}P^#P#yy#p+nA|eWU17*DWx!InTN(hXeiUh( z#Ly*nlPWBnx>B3HaD^_$6UHZ#je(46O4($mOlq>{bUi`f!Sv;HC8xAs9@J~~NHp4xrA%1O(7|)q+-v*Oz(qjMdPg5gtx52ig zK-cCluKt23xXznl_>tkAD%|8_!hY>KB0-mPk8GVU4--_|u8b#VLJd*pNk$YJmVEj+ zVO$JOHn|9O(C4B`E+|qL`s=`xwKsqTZmK6ms$6AH(5<30j@wyX z9jOBxD#FVGQDV|9r#ltxrARb92H#q7xz5B!Jz*M@6r+@XTbZaKtn2UWWU%=%B4I>( z8E48uwR`*!Fy_J&v{v>pGv9H#z{BzBGA5$}CK$NCAVa(!+h5C>q8K#&(4Qaiw&b5lz&%h>#3?A7UdnnW<>?0riK**JC`PPmd{?6?o_COB;NNXOtn!8+7W9t5hf!nk;JVE0( zA&PV-IRfKwA1tP`_oh<99JbH#m!%{u@?s(>Siko}_zg}Z=|}X+ZRvqRz+t10?n|VY z3T6E5gbp_QXmJWY@JNyp9W*my?q{dH3`g-j$1Z&tIq}nwo0_m)JB>(auU`#bHQB_- zmq5OYyHXzi9GiRDn1tDBreQ)KE0LvS-jenivrcO+8Mz&OA7Pc)kvz>VrM-#F+l2}K zsoCFQdsId=xsUV~+4=|_3=lkh9CROtZ*doqXPhSMW*N}OBB*qh!Q`$y1N}e=zbGfdN~GqIFh-07^Ee-Q_Kfq`=I6-Fx}P&O?cKpm z#<>h{&%k%}fA%zO&h8H^M^h~!n<`COubdgL`^(F|dbsiG>kTHrQZAEmRcen$=31G5 zwQq+FE==4ZUT~OhCs))~WoqWTZH;Kw9HMP95GnUE_yx^D<3XZjDr%{|d5T*_E=@n(>bc5A`q@k_%a353sFgR#c#F;pic3^yCUE2=IBb|ImkK3C}nm3Ap0j~YtyZv zl{Y~PH*WoLT1f1O?WMZXb+YucfpMxS`TfKP+7PW1tCSH0(e9{kpCSaaxQ^Q}UPr1n zxX&P*_SvK1)o@qh(X8boeaag_xtC5~tsr#w(3>HzXxK^%QZqNVxLmcVq zG?n5}LQzgT3QjtB8P63wr9M~h!%UUn07fn7*C>51WRU9!%Av;=)PLCxAvGYY6zq>mMkGwuXF*6Ny(Y#Wu1%om$T%L9-^`MX z@gGkwSek7woWzdxA1<7oT$r`!qxShw#qW}4gtCLC)GN(MsrH5JSFbdDb)a#)Cp^sCYx*?3>%c;;^bw|`Cc&Q zf8+4;()apmqmYc0L(4b%l*IVpm~MfqX55tc{`VRI*pqJJ2lAb=28SOMxF`E2$`CL? znu-!RuiVM|KV!+-yYDSAuAS&9o1VX0X(ON1s1NS1h*|rU_^YOgMmHyQ8--c7yd0L0 zrTB3Xr}gwd;!3Z+8K(HW2_h?}-5yK=F$KSD5+ZE0XrEb?`CD_180*gOlBpH9l<#3& z3ziPz4k>S1+#Bb<{=VR~HP}U5dc3*&9r4+y8Tn{apvnppYc=#)x9=0Cf}XH9p`;66#Q@G+c$Ksm#U*f{||z`P5S@< literal 0 HcmV?d00001 diff --git a/frontend/account/static/description/l10n.png b/frontend/account/static/description/l10n.png new file mode 100644 index 0000000000000000000000000000000000000000..e63103f6b9f7da1837d195f95dfb5b2e9720f797 GIT binary patch literal 4625 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 Hu0mjfHYBTf literal 0 HcmV?d00001 diff --git a/frontend/account/static/description/l10n.svg b/frontend/account/static/description/l10n.svg new file mode 100644 index 0000000..9979219 --- /dev/null +++ b/frontend/account/static/description/l10n.svg @@ -0,0 +1 @@ + diff --git a/frontend/account/static/src/components/account_batch_sending_summary/account_batch_sending_summary.js b/frontend/account/static/src/components/account_batch_sending_summary/account_batch_sending_summary.js new file mode 100644 index 0000000..a8997a2 --- /dev/null +++ b/frontend/account/static/src/components/account_batch_sending_summary/account_batch_sending_summary.js @@ -0,0 +1,21 @@ +import {Component} from "@odoo/owl"; +import {registry} from "@web/core/registry"; +import { standardFieldProps } from "@web/views/fields/standard_field_props"; + +export class AccountBatchSendingSummary extends Component { + static template = "account.BatchSendingSummary"; + static props = { + ...standardFieldProps, + }; + + setup() { + super.setup(); + this.data = this.props.record.data[this.props.name]; + } +} + +export const accountBatchSendingSummary = { + component: AccountBatchSendingSummary, +} + +registry.category("fields").add("account_batch_sending_summary", accountBatchSendingSummary); diff --git a/frontend/account/static/src/components/account_batch_sending_summary/account_batch_sending_summary.xml b/frontend/account/static/src/components/account_batch_sending_summary/account_batch_sending_summary.xml new file mode 100644 index 0000000..cc5f40e --- /dev/null +++ b/frontend/account/static/src/components/account_batch_sending_summary/account_batch_sending_summary.xml @@ -0,0 +1,15 @@ + + diff --git a/frontend/account/static/src/components/account_file_uploader/account_file_uploader.js b/frontend/account/static/src/components/account_file_uploader/account_file_uploader.js new file mode 100644 index 0000000..cc9d048 --- /dev/null +++ b/frontend/account/static/src/components/account_file_uploader/account_file_uploader.js @@ -0,0 +1,48 @@ +import { _t } from "@web/core/l10n/translation"; +import { registry } from "@web/core/registry"; +import { DocumentFileUploader } from "../document_file_uploader/document_file_uploader"; + +export class AccountFileUploader extends DocumentFileUploader { + static template = "account.AccountFileUploader"; + static props = { + ...DocumentFileUploader.props, + btnClass: { type: String, optional: true }, + linkText: { type: String, optional: true }, + togglerTemplate: { type: String, optional: true }, + }; + + getExtraContext() { + const extraContext = super.getExtraContext(); + const record_data = this.props.record ? this.props.record.data : false; + return record_data ? { + ...extraContext, + default_journal_id: record_data.id, + default_move_type: ( + (record_data.type === 'sale' && 'out_invoice') + || (record_data.type === 'purchase' && 'in_invoice') + || 'entry' + ), + } : extraContext; + + } + + getResModel() { + return "account.journal"; + } +} + +//when file uploader is used on account.journal (with a record) +export const accountFileUploader = { + component: AccountFileUploader, + extractProps: ({ attrs }) => ({ + togglerTemplate: attrs.template || "account.JournalUploadLink", + btnClass: attrs.btnClass || "", + linkText: attrs.title || _t("Upload"), + }), + fieldDependencies: [ + { name: "id", type: "integer" }, + { name: "type", type: "selection" }, + ], +}; + +registry.category("view_widgets").add("account_file_uploader", accountFileUploader); diff --git a/frontend/account/static/src/components/account_file_uploader/account_file_uploader.scss b/frontend/account/static/src/components/account_file_uploader/account_file_uploader.scss new file mode 100644 index 0000000..ec551da --- /dev/null +++ b/frontend/account/static/src/components/account_file_uploader/account_file_uploader.scss @@ -0,0 +1,7 @@ +.o_widget_account_file_uploader { + button.oe_kanban_action { + a { + color: var(--btn-color); + } + } +} diff --git a/frontend/account/static/src/components/account_file_uploader/account_file_uploader.xml b/frontend/account/static/src/components/account_file_uploader/account_file_uploader.xml new file mode 100644 index 0000000..41fcd3d --- /dev/null +++ b/frontend/account/static/src/components/account_file_uploader/account_file_uploader.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/account/static/src/components/account_merge_wizard_line_one2many/account_merge_wizard_line_one2many.js b/frontend/account/static/src/components/account_merge_wizard_line_one2many/account_merge_wizard_line_one2many.js new file mode 100644 index 0000000..70e6f9a --- /dev/null +++ b/frontend/account/static/src/components/account_merge_wizard_line_one2many/account_merge_wizard_line_one2many.js @@ -0,0 +1,61 @@ +import { registry } from "@web/core/registry"; +import { + SectionAndNoteListRenderer, + SectionAndNoteFieldOne2Many, + sectionAndNoteFieldOne2Many, +} from "../section_and_note_fields_backend/section_and_note_fields_backend"; + +export class AccountMergeWizardLinesRenderer extends SectionAndNoteListRenderer { + setup() { + super.setup(); + this.titleField = "info"; + } + + getCellClass(column, record) { + const classNames = super.getCellClass(column, record); + // Even though the `is_selected` field is invisible for section lines, we should + // keep its column (which would be hidden by the call to super.getCellClass) + // in order to align the section header name with the account names. + if (this.isSectionOrNote(record) && column.name === "is_selected") { + return classNames.replace(" o_hidden", ""); + } + return classNames; + } + + /** @override **/ + getSectionColumns(columns) { + const sectionCols = columns.filter( + (col) => + col.type === "field" && (col.name === this.titleField || col.name === "is_selected") + ); + return sectionCols.map((col) => { + if (col.name === this.titleField) { + return { ...col, colspan: columns.length - sectionCols.length + 1 }; + } else { + return { ...col }; + } + }); + } + + /** @override */ + isSortable(column) { + // Don't allow sorting columns, as that doesn't make sense in the wizard view. + return false; + } +} + +export class AccountMergeWizardLinesOne2Many extends SectionAndNoteFieldOne2Many { + static components = { + ...SectionAndNoteFieldOne2Many.components, + ListRenderer: AccountMergeWizardLinesRenderer, + }; +} + +export const accountMergeWizardLinesOne2Many = { + ...sectionAndNoteFieldOne2Many, + component: AccountMergeWizardLinesOne2Many, +}; + +registry + .category("fields") + .add("account_merge_wizard_lines_one2many", accountMergeWizardLinesOne2Many); diff --git a/frontend/account/static/src/components/account_move_form/account_move_form.js b/frontend/account/static/src/components/account_move_form/account_move_form.js new file mode 100644 index 0000000..bf7c706 --- /dev/null +++ b/frontend/account/static/src/components/account_move_form/account_move_form.js @@ -0,0 +1,94 @@ +import { registry } from "@web/core/registry"; +import { createElement, append } from "@web/core/utils/xml"; +import { Notebook } from "@web/core/notebook/notebook"; +import { formView } from "@web/views/form/form_view"; +import { FormCompiler } from "@web/views/form/form_compiler"; +import { FormRenderer } from "@web/views/form/form_renderer"; +import { FormController } from '@web/views/form/form_controller'; +import { useService } from "@web/core/utils/hooks"; +import { deleteConfirmationMessage } from "@web/core/confirmation_dialog/confirmation_dialog"; +import {_t} from "@web/core/l10n/translation"; + + +export class AccountMoveFormController extends FormController { + setup() { + super.setup(); + this.account_move_service = useService("account_move"); + } + + get cogMenuProps() { + return { + ...super.cogMenuProps, + printDropdownTitle: _t("Print"), + loadExtraPrintItems: this.loadExtraPrintItems.bind(this), + }; + } + + async loadExtraPrintItems() { + const items = await this.orm.call("account.move", "get_extra_print_items", [this.model.root.resId]); + return items.filter((item) => item.key !== "download_all"); + } + + + async deleteRecord() { + const deleteConfirmationDialogProps = this.deleteConfirmationDialogProps; + deleteConfirmationDialogProps.body = await this.account_move_service.getDeletionDialogBody(deleteConfirmationMessage, this.model.root.resId); + this.deleteRecordsWithConfirmation(deleteConfirmationDialogProps, [this.model.root]); + } +} + +export class AccountMoveFormNotebook extends Notebook { + static template = "account.AccountMoveFormNotebook"; + static props = { + ...Notebook.props, + onBeforeTabSwitch: { type: Function, optional: true }, + }; + + async changeTabTo(page_id) { + if (this.props.onBeforeTabSwitch) { + await this.props.onBeforeTabSwitch(page_id); + } + this.state.currentPage = page_id; + } +} + +export class AccountMoveFormRenderer extends FormRenderer { + static components = { + ...FormRenderer.components, + AccountMoveFormNotebook: AccountMoveFormNotebook, + }; + + async saveBeforeTabChange() { + if (this.props.record.isInEdition && await this.props.record.isDirty()) { + const contentEl = document.querySelector('.o_content'); + const scrollPos = contentEl.scrollTop; + await this.props.record.save(); + if (scrollPos) { + contentEl.scrollTop = scrollPos; + } + } + } +} + +export class AccountMoveFormCompiler extends FormCompiler { + compileNotebook(el, params) { + const originalNoteBook = super.compileNotebook(...arguments); + const noteBook = createElement("AccountMoveFormNotebook"); + for (const attr of originalNoteBook.attributes) { + noteBook.setAttribute(attr.name, attr.value); + } + noteBook.setAttribute("onBeforeTabSwitch", "() => __comp__.saveBeforeTabChange()"); + const slots = originalNoteBook.childNodes; + append(noteBook, [...slots]); + return noteBook; + } +} + +export const AccountMoveFormView = { + ...formView, + Renderer: AccountMoveFormRenderer, + Compiler: AccountMoveFormCompiler, + Controller: AccountMoveFormController, +}; + +registry.category("views").add("account_move_form", AccountMoveFormView); diff --git a/frontend/account/static/src/components/account_move_form/account_move_form_notebook.xml b/frontend/account/static/src/components/account_move_form/account_move_form_notebook.xml new file mode 100644 index 0000000..19ba6a8 --- /dev/null +++ b/frontend/account/static/src/components/account_move_form/account_move_form_notebook.xml @@ -0,0 +1,10 @@ + + + + + + () => this.changeTabTo(navItem[0]) + -1 + + + diff --git a/frontend/account/static/src/components/account_payment_field/account_payment.xml b/frontend/account/static/src/components/account_payment_field/account_payment.xml new file mode 100644 index 0000000..6f6ca53 --- /dev/null +++ b/frontend/account/static/src/components/account_payment_field/account_payment.xml @@ -0,0 +1,120 @@ + + + + + +

+ + + + + + + diff --git a/frontend/account/static/src/components/account_payment_field/account_payment_field.js b/frontend/account/static/src/components/account_payment_field/account_payment_field.js new file mode 100644 index 0000000..84e8b33 --- /dev/null +++ b/frontend/account/static/src/components/account_payment_field/account_payment_field.js @@ -0,0 +1,84 @@ +import { _t } from "@web/core/l10n/translation"; +import { registry } from "@web/core/registry"; +import { usePopover } from "@web/core/popover/popover_hook"; +import { useService } from "@web/core/utils/hooks"; +import { localization } from "@web/core/l10n/localization"; +import { formatDate, deserializeDate } from "@web/core/l10n/dates"; + +import { formatMonetary } from "@web/views/fields/formatters"; +import { standardFieldProps } from "@web/views/fields/standard_field_props"; +import { Component } from "@odoo/owl"; + +class AccountPaymentPopOver extends Component { + static props = { "*": { optional: true } }; + static template = "account.AccountPaymentPopOver"; +} + +export class AccountPaymentField extends Component { + static props = { ...standardFieldProps }; + static template = "account.AccountPaymentField"; + + setup() { + const position = localization.direction === "rtl" ? "bottom" : "left"; + this.popover = usePopover(AccountPaymentPopOver, { position }); + this.orm = useService("orm"); + this.action = useService("action"); + } + + getInfo() { + const info = this.props.record.data[this.props.name] || { + content: [], + outstanding: false, + title: "", + move_id: this.props.record.resId, + }; + for (const [key, value] of Object.entries(info.content)) { + value.index = key; + value.amount_formatted = formatMonetary(value.amount, { + currencyId: value.currency_id, + }); + if (value.date) { + // value.date is a string, parse to date and format to the users date format + value.formattedDate = formatDate(deserializeDate(value.date)) + } + } + return { + lines: info.content, + outstanding: info.outstanding, + title: info.title, + moveId: info.move_id, + }; + } + + onInfoClick(ev, line) { + this.popover.open(ev.currentTarget, { + title: _t("Journal Entry Info"), + ...line, + _onRemoveMoveReconcile: this.removeMoveReconcile.bind(this), + _onOpenMove: this.openMove.bind(this), + }); + } + + async assignOutstandingCredit(moveId, id) { + await this.orm.call(this.props.record.resModel, 'js_assign_outstanding_line', [moveId, id], {}); + await this.props.record.model.root.load(); + } + + async removeMoveReconcile(moveId, partialId) { + this.popover.close(); + await this.orm.call(this.props.record.resModel, 'js_remove_outstanding_partial', [moveId, partialId], {}); + await this.props.record.model.root.load(); + } + + async openMove(moveId) { + const action = await this.orm.call(this.props.record.resModel, 'action_open_business_doc', [moveId], {}); + this.action.doAction(action); + } +} + +export const accountPaymentField = { + component: AccountPaymentField, + supportedTypes: ["binary"], +}; + +registry.category("fields").add("payment", accountPaymentField); diff --git a/frontend/account/static/src/components/account_payment_register_html/account_payment_register_html.js b/frontend/account/static/src/components/account_payment_register_html/account_payment_register_html.js new file mode 100644 index 0000000..ca958d3 --- /dev/null +++ b/frontend/account/static/src/components/account_payment_register_html/account_payment_register_html.js @@ -0,0 +1,23 @@ +import { Component } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { standardFieldProps } from "@web/views/fields/standard_field_props"; + +class AccountPaymentRegisterHtmlField extends Component { + static props = standardFieldProps; + static template = "account.AccountPaymentRegisterHtmlField"; + + get value() { + return this.props.record.data[this.props.name]; + } + + switchInstallmentsAmount(ev) { + if (ev.srcElement.classList.contains("installments_switch_button")) { + const root = this.env.model.root; + root.update({ amount: root.data.installments_switch_amount }); + } + } +} + +const accountPaymentRegisterHtmlField = { component: AccountPaymentRegisterHtmlField }; + +registry.category("fields").add("account_payment_register_html", accountPaymentRegisterHtmlField); diff --git a/frontend/account/static/src/components/account_payment_register_html/account_payment_register_html.xml b/frontend/account/static/src/components/account_payment_register_html/account_payment_register_html.xml new file mode 100644 index 0000000..33e689e --- /dev/null +++ b/frontend/account/static/src/components/account_payment_register_html/account_payment_register_html.xml @@ -0,0 +1,6 @@ + + + +
+ + diff --git a/frontend/account/static/src/components/account_payment_term_form/payment_term_line_ids.js b/frontend/account/static/src/components/account_payment_term_form/payment_term_line_ids.js new file mode 100644 index 0000000..84ac1b3 --- /dev/null +++ b/frontend/account/static/src/components/account_payment_term_form/payment_term_line_ids.js @@ -0,0 +1,25 @@ +import { registry } from "@web/core/registry"; + +import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field"; +import { useAddInlineRecord } from "@web/views/fields/relational_utils"; + +export class PaymentTermLineIdsOne2Many extends X2ManyField { + setup() { + super.setup(); + // Overloads the addInLine method to mark all new records as 'dirty' by calling update with an empty object. + // This prevents the records from being abandoned if the user clicks globally or on an existing record. + this.addInLine = useAddInlineRecord({ + addNew: async (...args) => { + const newRecord = await this.list.addNewRecord(...args); + newRecord.update({}); + } + }); + } +} + +export const PaymentTermLineIds = { + ...x2ManyField, + component: PaymentTermLineIdsOne2Many, +} + +registry.category("fields").add("payment_term_line_ids", PaymentTermLineIds); diff --git a/frontend/account/static/src/components/account_pick_currency_rate/account_pick_currency_rate.js b/frontend/account/static/src/components/account_pick_currency_rate/account_pick_currency_rate.js new file mode 100644 index 0000000..ed6f9ff --- /dev/null +++ b/frontend/account/static/src/components/account_pick_currency_rate/account_pick_currency_rate.js @@ -0,0 +1,44 @@ +import { Component } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { useDateTimePicker } from "@web/core/datetime/datetime_picker_hook"; +import { useService } from "@web/core/utils/hooks"; +import { today } from "@web/core/l10n/dates"; +import { standardWidgetProps } from "@web/views/widgets/standard_widget_props"; + + +export class AccountPickCurrencyDate extends Component { + static template = "account.AccountPickCurrencyDate"; + static props = { + ...standardWidgetProps, + record: { type: Object, optional: true }, + }; + + setup() { + this.orm = useService("orm"); + this.dateTimePicker = useDateTimePicker({ + target: 'datetime-picker-target', + onApply: async (date) => { + const record = this.props.record + const rate = await this.orm.call( + 'account.move', + 'get_currency_rate', + [record.resId, record.data.company_id.id, record.data.currency_id.id, date.toISODate()], + ); + this.props.record.update({ invoice_currency_rate: rate }); + await this.props.record.save(); + }, + get pickerProps() { + return { + type: 'date', + value: today(), + }; + }, + }); + } +} + +export const accountPickCurrencyDate = { + component: AccountPickCurrencyDate, +} + +registry.category("view_widgets").add("account_pick_currency_date", accountPickCurrencyDate); diff --git a/frontend/account/static/src/components/account_pick_currency_rate/account_pick_currency_rate.xml b/frontend/account/static/src/components/account_pick_currency_rate/account_pick_currency_rate.xml new file mode 100644 index 0000000..18befe8 --- /dev/null +++ b/frontend/account/static/src/components/account_pick_currency_rate/account_pick_currency_rate.xml @@ -0,0 +1,15 @@ + + diff --git a/frontend/account/static/src/components/account_resequence/account_resequence.xml b/frontend/account/static/src/components/account_resequence/account_resequence.xml new file mode 100644 index 0000000..2578c65 --- /dev/null +++ b/frontend/account/static/src/components/account_resequence/account_resequence.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + +
DateBeforeAfter
+
+ + + + + + + + + +
diff --git a/frontend/account/static/src/components/account_resequence/account_resequence_field.js b/frontend/account/static/src/components/account_resequence/account_resequence_field.js new file mode 100644 index 0000000..165aafa --- /dev/null +++ b/frontend/account/static/src/components/account_resequence/account_resequence_field.js @@ -0,0 +1,22 @@ +import { registry } from "@web/core/registry"; +import { Component } from "@odoo/owl"; +import { standardFieldProps } from "@web/views/fields/standard_field_props"; + +class ChangeLine extends Component { + static template = "account.ResequenceChangeLine"; + static props = ["changeLine", "ordering"]; +} + +class ShowResequenceRenderer extends Component { + static template = "account.ResequenceRenderer"; + static components = { ChangeLine }; + static props = { ...standardFieldProps }; + getValue() { + const value = this.props.record.data[this.props.name]; + return value ? JSON.parse(value) : { changeLines: [], ordering: "date" }; + } +} + +registry.category("fields").add("account_resequence_widget", { + component: ShowResequenceRenderer, +}); diff --git a/frontend/account/static/src/components/account_statusbar_secured/account_move_statusbar_secured.js b/frontend/account/static/src/components/account_statusbar_secured/account_move_statusbar_secured.js new file mode 100644 index 0000000..c71c95c --- /dev/null +++ b/frontend/account/static/src/components/account_statusbar_secured/account_move_statusbar_secured.js @@ -0,0 +1,25 @@ +import { _t } from "@web/core/l10n/translation"; +import { registry } from "@web/core/registry"; +import { statusBarField, StatusBarField } from "@web/views/fields/statusbar/statusbar_field"; + +export class AccountMoveStatusBarSecuredField extends StatusBarField { + static template = "account.MoveStatusBarSecuredField"; + + get isSecured() { + return this.props.record.data['secured']; + } + + get currentItem() { + return this.getAllItems().find((item) => item.isSelected); + } +} + +export const accountMoveStatusBarSecuredField = { + ...statusBarField, + component: AccountMoveStatusBarSecuredField, + displayName: _t("Status with secured indicator for Journal Entries"), + supportedTypes: ["selection"], + additionalClasses: ["o_field_statusbar"], +}; + +registry.category("fields").add("account_move_statusbar_secured", accountMoveStatusBarSecuredField); diff --git a/frontend/account/static/src/components/account_statusbar_secured/account_move_statusbar_secured.xml b/frontend/account/static/src/components/account_statusbar_secured/account_move_statusbar_secured.xml new file mode 100644 index 0000000..ce22e9c --- /dev/null +++ b/frontend/account/static/src/components/account_statusbar_secured/account_move_statusbar_secured.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + account.MoveStatusBarSecuredField.Dropdown + + + + + + + + + + + + + + + diff --git a/frontend/account/static/src/components/account_tax_repartition_line_factor_percent/account_tax_repartition_line_factor_percent.js b/frontend/account/static/src/components/account_tax_repartition_line_factor_percent/account_tax_repartition_line_factor_percent.js new file mode 100644 index 0000000..87998c5 --- /dev/null +++ b/frontend/account/static/src/components/account_tax_repartition_line_factor_percent/account_tax_repartition_line_factor_percent.js @@ -0,0 +1,54 @@ +import { FloatField, floatField } from "@web/views/fields/float/float_field"; +import { roundPrecision } from "@web/core/utils/numbers"; +import {registry} from "@web/core/registry"; + +export class AccountTaxRepartitionLineFactorPercent extends FloatField { + static defaultProps = { + ...FloatField.defaultProps, + digits: [16, 12], + }; + + /* + * @override + * We don't want to display all amounts with 12 digits behind so we remove the trailing 0 + * as much as possible. + */ + get formattedValue() { + const value = super.formattedValue; + const trailingNumbersMatch = value.match(/(\d+)$/); + if (!trailingNumbersMatch) { + return value; + } + const trailingZeroMatch = trailingNumbersMatch[1].match(/(0+)$/); + if (!trailingZeroMatch) { + return value; + } + const nbTrailingZeroToRemove = Math.min(trailingZeroMatch[1].length, trailingNumbersMatch[1].length - 2); + return value.substring(0, value.length - nbTrailingZeroToRemove); + } + + /* + * @override + * Prevent the users of showing a rounding at 12 digits on the screen but + * getting an unrounded value after typing "= 2/3" on the field when saving. + */ + parse(value) { + const parsedValue = super.parse(value); + try { + Number(parsedValue); + } catch { + return parsedValue; + } + const precisionRounding = Number(`1e-${this.props.digits[1]}`); + return roundPrecision(parsedValue, precisionRounding); + } +} + + +export const accountTaxRepartitionLineFactorPercent = { + ...floatField, + component: AccountTaxRepartitionLineFactorPercent, +}; + + +registry.category("fields").add("account_tax_repartition_line_factor_percent", accountTaxRepartitionLineFactorPercent); diff --git a/frontend/account/static/src/components/account_type_selection/account_type_selection.js b/frontend/account/static/src/components/account_type_selection/account_type_selection.js new file mode 100644 index 0000000..1a43a09 --- /dev/null +++ b/frontend/account/static/src/components/account_type_selection/account_type_selection.js @@ -0,0 +1,62 @@ +import { _t } from "@web/core/l10n/translation"; +import { registry } from "@web/core/registry"; +import { SelectionField, selectionField } from "@web/views/fields/selection/selection_field"; + +export class AccountTypeSelection extends SelectionField { + static template = "account.AccountTypeSelection"; + setup() { + super.setup(); + const getChoicesForGroup = (group) => { + return this.choices.filter(x => x.value.startsWith(group)); + } + this.sections = [ + { + label: _t('Balance Sheet'), + name: "balance_sheet" + }, + { + label: _t('Profit & Loss'), + name: "profit_and_loss" + }, + ] + this.groups = [ + { + label: _t('Assets'), + choices: getChoicesForGroup('asset'), + section: "balance_sheet", + }, + { + label: _t('Liabilities'), + choices: getChoicesForGroup('liability'), + section: "balance_sheet", + }, + { + label: _t('Equity'), + choices: getChoicesForGroup('equity'), + section: "balance_sheet", + }, + { + label: _t('Income'), + choices: getChoicesForGroup('income'), + section: "profit_and_loss", + }, + { + label: _t('Expense'), + choices: getChoicesForGroup('expense'), + section: "profit_and_loss", + }, + { + label: _t('Other'), + choices: getChoicesForGroup('off_balance'), + section: "profit_and_loss", + }, + ]; + } +} + +export const accountTypeSelection = { + ...selectionField, + component: AccountTypeSelection, +}; + +registry.category("fields").add("account_type_selection", accountTypeSelection); diff --git a/frontend/account/static/src/components/account_type_selection/account_type_selection.xml b/frontend/account/static/src/components/account_type_selection/account_type_selection.xml new file mode 100644 index 0000000..11bf302 --- /dev/null +++ b/frontend/account/static/src/components/account_type_selection/account_type_selection.xml @@ -0,0 +1,13 @@ + + + + + + + + groups + sections + + + + diff --git a/frontend/account/static/src/components/actionable_errors/actionable_errors.js b/frontend/account/static/src/components/actionable_errors/actionable_errors.js new file mode 100644 index 0000000..765b315 --- /dev/null +++ b/frontend/account/static/src/components/actionable_errors/actionable_errors.js @@ -0,0 +1,57 @@ +import { registry } from "@web/core/registry"; +import { Component } from "@odoo/owl"; +import { standardFieldProps } from "@web/views/fields/standard_field_props"; +import { useService } from "@web/core/utils/hooks"; + +const WARNING_TYPE_ORDER = ["danger", "warning", "info"]; + +export class ActionableErrors extends Component { + static props = { errorData: {type: Object} }; + static template = "account.ActionableErrors"; + + setup() { + super.setup(); + this.actionService = useService("action"); + this.orm = useService("orm"); + } + + get errorData() { + return this.props.errorData; + } + + async handleOnClick(errorData){ + if (errorData.action?.view_mode) { + // view_mode is not handled JS side + errorData.action['views'] = errorData.action.view_mode.split(',').map(mode => [false, mode]); + delete errorData.action['view_mode']; + } + if (errorData.action_call) { + const [model, method, args] = errorData.action_call; + await this.orm.call(model, method, [args]); + this.env.model.action.doAction("soft_reload"); + } else { + this.env.model.action.doAction(errorData.action); + } + } + + get sortedActionableErrors() { + return this.errorData && Object.fromEntries( + Object.entries(this.errorData).sort( + (a, b) => + WARNING_TYPE_ORDER.indexOf(a[1]["level"] || "warning") - + WARNING_TYPE_ORDER.indexOf(b[1]["level"] || "warning"), + ), + ); + } +} + +export class ActionableErrorsField extends ActionableErrors { + static props = { ...standardFieldProps }; + + get errorData() { + return this.props.record.data[this.props.name]; + } +} + +export const actionableErrorsField = {component: ActionableErrorsField}; +registry.category("fields").add("actionable_errors", actionableErrorsField); diff --git a/frontend/account/static/src/components/actionable_errors/actionable_errors.xml b/frontend/account/static/src/components/actionable_errors/actionable_errors.xml new file mode 100644 index 0000000..e403946 --- /dev/null +++ b/frontend/account/static/src/components/actionable_errors/actionable_errors.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/frontend/account/static/src/components/auto_save_res_partner_bank/auto_save_res_partner_bank.js b/frontend/account/static/src/components/auto_save_res_partner_bank/auto_save_res_partner_bank.js new file mode 100644 index 0000000..c2a94ea --- /dev/null +++ b/frontend/account/static/src/components/auto_save_res_partner_bank/auto_save_res_partner_bank.js @@ -0,0 +1,17 @@ +import { registry } from "@web/core/registry"; +import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field"; + + +export class AutoSaveResPartnerField extends X2ManyField { + async onAdd({ context, editable } = {}) { + await this.props.record.model.root.save(); + await super.onAdd({ context, editable }); + } +} + +export const autoSaveResPartnerField = { + ...x2ManyField, + component: AutoSaveResPartnerField, +}; + +registry.category("fields").add("auto_save_res_partner", autoSaveResPartnerField); diff --git a/frontend/account/static/src/components/autosave_many2many_tax_tags/autosave_many2many_tax_tags.js b/frontend/account/static/src/components/autosave_many2many_tax_tags/autosave_many2many_tax_tags.js new file mode 100644 index 0000000..7a1d394 --- /dev/null +++ b/frontend/account/static/src/components/autosave_many2many_tax_tags/autosave_many2many_tax_tags.js @@ -0,0 +1,53 @@ +import { registry } from "@web/core/registry"; +import { useRecordObserver } from "@web/model/relational_model/utils"; +import { + Many2ManyTaxTagsField, + many2ManyTaxTagsField +} from "@account/components/many2x_tax_tags/many2x_tax_tags"; + +export class AutosaveMany2ManyTaxTagsField extends Many2ManyTaxTagsField { + setup() { + super.setup(); + + this.lastBalance = this.props.record.data.balance; + this.lastAccount = this.props.record.data.account_id; + this.lastPartner = this.props.record.data.partner_id; + + const super_update = this.update; + this.update = (recordlist) => { + super_update(recordlist); + this._saveOnUpdate(); + }; + useRecordObserver(this.onRecordChange.bind(this)); + } + + async deleteTag(id) { + await super.deleteTag(id); + await this._saveOnUpdate(); + } + + onRecordChange(record) { + const line = record.data; + if (line.tax_ids.records.length > 0) { + if (line.balance !== this.lastBalance + || line.account_id.id !== this.lastAccount.id + || line.partner_id.id !== this.lastPartner.id) { + this.lastBalance = line.balance; + this.lastAccount = line.account_id; + this.lastPartner = line.partner_id; + return record.model.root.save(); + } + } + } + + async _saveOnUpdate() { + await this.props.record.model.root.save(); + } +} + +export const autosaveMany2ManyTaxTagsField = { + ...many2ManyTaxTagsField, + component: AutosaveMany2ManyTaxTagsField, +}; + +registry.category("fields").add("autosave_many2many_tax_tags", autosaveMany2ManyTaxTagsField); diff --git a/frontend/account/static/src/components/bill_guide/bill_guide.js b/frontend/account/static/src/components/bill_guide/bill_guide.js new file mode 100644 index 0000000..d99b8db --- /dev/null +++ b/frontend/account/static/src/components/bill_guide/bill_guide.js @@ -0,0 +1,69 @@ +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { DocumentFileUploader } from "../document_file_uploader/document_file_uploader"; + +import { Component, onWillStart } from "@odoo/owl"; + +export class BillGuide extends Component { + static template = "account.BillGuide"; + static components = { + DocumentFileUploader, + }; + static props = ["*"]; // could contain view_widget props + + setup() { + this.orm = useService("orm"); + this.action = useService("action"); + this.context = null; + this.alias = null; + this.showSampleAction = false; + onWillStart(this.onWillStart); + } + + async onWillStart() { + const rec = this.props.record; + const ctx = this.env.searchModel.context; + if (rec) { + // prepare context from journal record + this.context = { + default_journal_id: rec.resId, + default_move_type: (rec.data.type === 'sale' && 'out_invoice') || (rec.data.type === 'purchase' && 'in_invoice') || 'entry', + active_model: rec.resModel, + active_ids: [rec.resId], + } + this.alias = rec.data.alias_domain_id && rec.data.alias_id[1] || false; + } else if (!ctx?.default_journal_id && ctx?.active_id) { + this.context = { + default_journal_id: ctx.active_id, + } + } + this.showSampleAction = await this.orm.call("account.journal", "is_sample_action_available"); + } + + handleButtonClick(action, model="account.journal") { + this.action.doActionButton({ + resModel: model, + name: action, + context: this.context || this.env.searchModel.context, + type: 'object', + }); + } + + openVendorBill() { + return this.action.doAction({ + type: "ir.actions.act_window", + res_model: "account.move", + views: [[false, "form"]], + context: { + default_move_type: "in_invoice", + }, + }); + } +} + + +export const billGuide = { + component: BillGuide, +}; + +registry.category("view_widgets").add("bill_upload_guide", billGuide); diff --git a/frontend/account/static/src/components/bill_guide/bill_guide.scss b/frontend/account/static/src/components/bill_guide/bill_guide.scss new file mode 100644 index 0000000..c09ce84 --- /dev/null +++ b/frontend/account/static/src/components/bill_guide/bill_guide.scss @@ -0,0 +1,35 @@ +.o_view_nocontent { + .o_nocontent_help:has(> .bill_guide_container) { + min-width: 65vw; + } +} + +.bill_guide_container { + @include media-breakpoint-up(sm) { + min-width: 400px; + } + + .bill_guide_left, .bill_guide_right { + width: 45%; + } + + .separator_wrapper { + width: 10%; + } +} + +.bill-guide-img { + width: 100%; + height: 100%; + object-fit: contain; +} + +.mb-9 { + margin-bottom: 9rem !important; +} + +.account_drag_drop_btn { + border-style: dashed !important; + border-color: $o-brand-primary; + background-color: mix($o-brand-primary, $o-view-background-color, 15%); +} diff --git a/frontend/account/static/src/components/bill_guide/bill_guide.xml b/frontend/account/static/src/components/bill_guide/bill_guide.xml new file mode 100644 index 0000000..4670e65 --- /dev/null +++ b/frontend/account/static/src/components/bill_guide/bill_guide.xml @@ -0,0 +1,82 @@ + + + +
+
+
+
+ +
+
+ + +
+
+
+
+
+
+
+ or +
+
+
+
+
+
+
+ Email bills +
+
+
+ Send a bill to + +
+ +
+
+ +
+
+ + + diff --git a/frontend/account/static/src/components/char_with_placeholder_field/char_with_placeholder_field.js b/frontend/account/static/src/components/char_with_placeholder_field/char_with_placeholder_field.js new file mode 100644 index 0000000..ef19f8a --- /dev/null +++ b/frontend/account/static/src/components/char_with_placeholder_field/char_with_placeholder_field.js @@ -0,0 +1,22 @@ +import { registry } from "@web/core/registry"; +import { CharField, charField } from "@web/views/fields/char/char_field"; + +// Ensure that in Hoot tests, this module is loaded after `@mail/js/onchange_on_keydown` +// (needed because that module patches `charField`). +import "@mail/js/onchange_on_keydown"; + +export class CharWithPlaceholderField extends CharField { + static template = "account.CharWithPlaceholderField"; + + /** Override **/ + get formattedValue() { + return super.formattedValue || this.props.placeholder; + } +} + +export const charWithPlaceholderField = { + ...charField, + component: CharWithPlaceholderField, +}; + +registry.category("fields").add("char_with_placeholder_field", charWithPlaceholderField); diff --git a/frontend/account/static/src/components/char_with_placeholder_field/char_with_placeholder_field.xml b/frontend/account/static/src/components/char_with_placeholder_field/char_with_placeholder_field.xml new file mode 100644 index 0000000..8d7b342 --- /dev/null +++ b/frontend/account/static/src/components/char_with_placeholder_field/char_with_placeholder_field.xml @@ -0,0 +1,10 @@ + + + + + + {'text-muted': !this.props.record.data[props.name]} + + + + diff --git a/frontend/account/static/src/components/char_with_placeholder_field_to_check/char_with_placeholder_field.xml b/frontend/account/static/src/components/char_with_placeholder_field_to_check/char_with_placeholder_field.xml new file mode 100644 index 0000000..691dbe1 --- /dev/null +++ b/frontend/account/static/src/components/char_with_placeholder_field_to_check/char_with_placeholder_field.xml @@ -0,0 +1,14 @@ + + + + + + + To review + + + + + diff --git a/frontend/account/static/src/components/char_with_placeholder_field_to_check/char_with_placeholder_field_to_check_to_check.js b/frontend/account/static/src/components/char_with_placeholder_field_to_check/char_with_placeholder_field_to_check_to_check.js new file mode 100644 index 0000000..dd271f1 --- /dev/null +++ b/frontend/account/static/src/components/char_with_placeholder_field_to_check/char_with_placeholder_field_to_check_to_check.js @@ -0,0 +1,16 @@ +import { registry } from "@web/core/registry"; +import { + charWithPlaceholderField, + CharWithPlaceholderField +} from "../char_with_placeholder_field/char_with_placeholder_field"; + +export class CharWithPlaceholderFieldToCheck extends CharWithPlaceholderField { + static template = "account.CharWithPlaceholderField"; +} + +export const charWithPlaceholderFieldToCheck = { + ...charWithPlaceholderField, + component: CharWithPlaceholderFieldToCheck, +}; + +registry.category("fields").add("char_with_placeholder_field_to_check", charWithPlaceholderFieldToCheck); diff --git a/frontend/account/static/src/components/currency_form/form_controller.js b/frontend/account/static/src/components/currency_form/form_controller.js new file mode 100644 index 0000000..d499ad3 --- /dev/null +++ b/frontend/account/static/src/components/currency_form/form_controller.js @@ -0,0 +1,43 @@ +/** @odoo-module **/ + +import { _t } from "@web/core/l10n/translation"; +import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog"; +import { registry } from "@web/core/registry"; +import { FormController } from "@web/views/form/form_controller"; +import { formView } from "@web/views/form/form_view"; + +export class CurrencyFormController extends FormController { + + async onWillSaveRecord(record) { + if (record.data.display_rounding_warning && + record._values.rounding !== undefined && + record.data.rounding < record._values.rounding + ) { + return new Promise((resolve) => { + this.dialogService.add(ConfirmationDialog, { + title: _t("Confirmation Warning"), + body: _t( + "You're about to permanently change the decimals for all prices in your database.\n" + + "This change cannot be undone without technical support." + ), + confirmLabel: _t("Confirm"), + cancelLabel: _t("Cancel"), + confirm: () => resolve(true), + cancel: () => { + record.discard(); + resolve(false); + }, + }); + }); + } + + return true; + } +} + +export const currencyFormView = { + ...formView, + Controller: CurrencyFormController, +}; + +registry.category("views").add("currency_form", currencyFormView); diff --git a/frontend/account/static/src/components/currency_form/open_decimal_precision_btn.js b/frontend/account/static/src/components/currency_form/open_decimal_precision_btn.js new file mode 100644 index 0000000..636774b --- /dev/null +++ b/frontend/account/static/src/components/currency_form/open_decimal_precision_btn.js @@ -0,0 +1,24 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { standardFieldProps } from "@web/views/fields/standard_field_props"; +import { useService } from "@web/core/utils/hooks"; +import { Component } from "@odoo/owl"; + +class OpenDecimalPrecisionButton extends Component { + static template = "account.OpenDecimalPrecisionButton"; + static props = { ...standardFieldProps }; + + setup() { + this.action = useService("action"); + } + + async discardAndOpen() { + await this.props.record.discard(); + this.action.doAction("base.action_decimal_precision_form"); + } +} + +registry.category("fields").add("open_decimal_precision_button", { + component: OpenDecimalPrecisionButton, +}); diff --git a/frontend/account/static/src/components/currency_form/open_decimal_precision_btn_template.xml b/frontend/account/static/src/components/currency_form/open_decimal_precision_btn_template.xml new file mode 100644 index 0000000..6ccdb2e --- /dev/null +++ b/frontend/account/static/src/components/currency_form/open_decimal_precision_btn_template.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/frontend/account/static/src/components/document_file_uploader/document_file_uploader.js b/frontend/account/static/src/components/document_file_uploader/document_file_uploader.js new file mode 100644 index 0000000..ed45730 --- /dev/null +++ b/frontend/account/static/src/components/document_file_uploader/document_file_uploader.js @@ -0,0 +1,78 @@ +import { useService } from "@web/core/utils/hooks"; +import { FileUploader } from "@web/views/fields/file_handler"; +import { standardWidgetProps } from "@web/views/widgets/standard_widget_props"; + +import { Component, markup } from "@odoo/owl"; + +export class DocumentFileUploader extends Component { + static template = "account.DocumentFileUploader"; + static components = { + FileUploader, + }; + static props = { + ...standardWidgetProps, + record: { type: Object, optional: true }, + slots: { type: Object, optional: true }, + resModel: { type: String, optional: true }, + }; + + setup() { + this.orm = useService("orm"); + this.action = useService("action"); + this.notification = useService("notification"); + this.attachmentIdsToProcess = []; + this.extraContext = this.getExtraContext(); + } + + // To pass extra context while creating record + getExtraContext() { + return {}; + } + + async onFileUploaded(file) { + const att_data = { + name: file.name, + mimetype: file.type, + datas: file.data, + }; + // clean the context to ensure the `create` call doesn't fail from unknown `default_*` context + const cleanContext = Object.fromEntries(Object.entries(this.env.searchModel.context).filter(([key]) => !key.startsWith('default_'))); + const [att_id] = await this.orm.create("ir.attachment", [att_data], {context: cleanContext}); + this.attachmentIdsToProcess.push(att_id); + } + + // To define specific resModal from another model + getResModel() { + return this.props.resModel; + } + + async onUploadComplete() { + const resModal = this.getResModel(); + let action; + try { + action = await this.orm.call( + resModal, + "create_document_from_attachment", + ["", this.attachmentIdsToProcess], + { context: { ...this.extraContext, ...this.env.searchModel.context } } + ); + } finally { + // ensures attachments are cleared on success as well as on error + this.attachmentIdsToProcess = []; + } + if (action.context && action.context.notifications) { + for (const [file, msg] of Object.entries(action.context.notifications)) { + this.notification.add(msg, { + title: file, + type: "info", + sticky: true, + }); + } + delete action.context.notifications; + } + if (action.help?.length) { + action.help = markup(action.help); + } + this.action.doAction(action); + } +} diff --git a/frontend/account/static/src/components/document_file_uploader/document_file_uploader.xml b/frontend/account/static/src/components/document_file_uploader/document_file_uploader.xml new file mode 100644 index 0000000..a045a36 --- /dev/null +++ b/frontend/account/static/src/components/document_file_uploader/document_file_uploader.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/account/static/src/components/document_state/document_state_field.js b/frontend/account/static/src/components/document_state/document_state_field.js new file mode 100644 index 0000000..8d59249 --- /dev/null +++ b/frontend/account/static/src/components/document_state/document_state_field.js @@ -0,0 +1,69 @@ +import { _t } from "@web/core/l10n/translation"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; + +import { SelectionField, selectionField } from "@web/views/fields/selection/selection_field"; + +import { Component } from "@odoo/owl"; + +export class DocumentStatePopover extends Component { + static template = "account.DocumentStatePopover"; + static props = { + close: Function, + onClose: Function, + copyText: Function, + message: String, + }; +} + +export class DocumentState extends SelectionField { + static template = "account.DocumentState"; + + setup() { + super.setup(); + this.popover = useService("popover"); + this.notification = useService("notification"); + } + + get message() { + return this.props.record.data.message; + } + + copyText() { + navigator.clipboard.writeText(this.message); + this.notification.add(_t("Text copied"), { type: "success" }); + this.popoverCloseFn(); + this.popoverCloseFn = null; + } + + showMessagePopover(ev) { + const close = () => { + this.popoverCloseFn(); + this.popoverCloseFn = null; + }; + + if (this.popoverCloseFn) { + close(); + return; + } + + this.popoverCloseFn = this.popover.add( + ev.currentTarget, + DocumentStatePopover, + { + message: this.message, + copyText: this.copyText.bind(this), + onClose: close, + }, + { + closeOnClickAway: true, + position: "top", + }, + ); + } +} + +registry.category("fields").add("account_document_state", { + ...selectionField, + component: DocumentState, +}); diff --git a/frontend/account/static/src/components/document_state/document_state_field.scss b/frontend/account/static/src/components/document_state/document_state_field.scss new file mode 100644 index 0000000..3d4322b --- /dev/null +++ b/frontend/account/static/src/components/document_state/document_state_field.scss @@ -0,0 +1,10 @@ +.account_document_state_popover { + width: 500px; +} + +.account_document_state_popover_clone { + &:hover { + color: $o-enterprise-action-color !important; + cursor: pointer; + } +} diff --git a/frontend/account/static/src/components/document_state/document_state_field.xml b/frontend/account/static/src/components/document_state/document_state_field.xml new file mode 100644 index 0000000..add0190 --- /dev/null +++ b/frontend/account/static/src/components/document_state/document_state_field.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/frontend/account/static/src/components/dynamic_selection/dynamic_selection.js b/frontend/account/static/src/components/dynamic_selection/dynamic_selection.js new file mode 100644 index 0000000..1a85d56 --- /dev/null +++ b/frontend/account/static/src/components/dynamic_selection/dynamic_selection.js @@ -0,0 +1,63 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { SelectionField, selectionField } from "@web/views/fields/selection/selection_field"; + +export class DynamicSelectionField extends SelectionField { + + static props = { + ...SelectionField.props, + available_field: { type: String }, + } + + get availableOptions() { + return this.props.record.data[this.props.available_field]?.split(",") || []; + } + + /** + * Filter the options with the accepted available options. + * @override + */ + get options() { + const availableOptions = this.availableOptions; + return super.options.filter(x => availableOptions.includes(x[0])); + } + + /** + * In dynamic selection field, sometimes we can have no options available. + * This override handles that case by adding optional chaining when accessing the found options. + * @override + */ + get string() { + if (this.type === "selection") { + return this.props.record.data[this.props.name] !== false + ? this.options.find((o) => o[0] === this.props.record.data[this.props.name])?.[1] + : ""; + } + return super.string; + } + +} + +/* +EXAMPLE USAGE: + +In python: +the_available_field = fields.Char() # string of comma separated available selection field keys +the_selection_field = fields.Selection([ ... ]) + +In the views: + + + */ + +registry.category("fields").add("dynamic_selection", { + ...selectionField, + component: DynamicSelectionField, + extractProps: (fieldInfo, dynamicInfo) => ({ + ...selectionField.extractProps(fieldInfo, dynamicInfo), + available_field: fieldInfo.options.available_field, + }), +}) diff --git a/frontend/account/static/src/components/fetch_einvoices/fetch_einvoices.xml b/frontend/account/static/src/components/fetch_einvoices/fetch_einvoices.xml new file mode 100644 index 0000000..1bec8ec --- /dev/null +++ b/frontend/account/static/src/components/fetch_einvoices/fetch_einvoices.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/account/static/src/components/fetch_einvoices/fetch_einvoices_cog.js b/frontend/account/static/src/components/fetch_einvoices/fetch_einvoices_cog.js new file mode 100644 index 0000000..bf46d66 --- /dev/null +++ b/frontend/account/static/src/components/fetch_einvoices/fetch_einvoices_cog.js @@ -0,0 +1,59 @@ +import { Component } from "@odoo/owl"; +import { DropdownItem } from "@web/core/dropdown/dropdown_item"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { _t } from "@web/core/l10n/translation"; +import { ACTIONS_GROUP_NUMBER } from "@web/search/action_menus/action_menus"; + +const cogMenuRegistry = registry.category("cogMenu"); + +export class FetchEInvoices extends Component { + static template = "account.FetchEInvoices"; + static props = {}; + static components = { DropdownItem }; + + setup() { + super.setup(); + this.action = useService("action"); + } + + get buttonAction() { + return this.env.searchModel.globalContext.show_fetch_in_einvoices_button + ? "button_fetch_in_einvoices" + : "button_refresh_out_einvoices_status"; + } + + get buttonLabel() { + return this.env.searchModel.globalContext.show_fetch_in_einvoices_button + ? _t("Fetch e-Invoices") + : _t("Refresh e-Invoices Status"); + } + + fetchEInvoices() { + const journalId = this.env.searchModel.globalContext.default_journal_id; + if (!journalId) { + return; + } + + this.action.doActionButton({ + type: "object", + resId: journalId, + name: this.buttonAction, + resModel: "account.journal", + onClose: () => window.location.reload(), + }); + } +} + +export const fetchEInvoicesActionMenu = { + Component: FetchEInvoices, + groupNumber: ACTIONS_GROUP_NUMBER, + isDisplayed: ({ config, searchModel }) => + searchModel.resModel === "account.move" && + (searchModel.globalContext.default_journal_id || false) && + (searchModel.globalContext.show_fetch_in_einvoices_button || + searchModel.globalContext.show_refresh_out_einvoices_status_button || + false), +}; + +cogMenuRegistry.add("account-fetch-e-invoices", fetchEInvoicesActionMenu, { sequence: 11 }); diff --git a/frontend/account/static/src/components/grouped_view_widget/grouped_view_widget.js b/frontend/account/static/src/components/grouped_view_widget/grouped_view_widget.js new file mode 100644 index 0000000..8fa51ba --- /dev/null +++ b/frontend/account/static/src/components/grouped_view_widget/grouped_view_widget.js @@ -0,0 +1,30 @@ +import { registry } from "@web/core/registry"; +import { Component } from "@odoo/owl"; +import { standardFieldProps } from "@web/views/fields/standard_field_props"; + +class ListItem extends Component { + static template = "account.GroupedItemTemplate"; + static props = ["item_vals", "options"]; +} + +class ListGroup extends Component { + static template = "account.GroupedItemsTemplate"; + static components = { ListItem }; + static props = ["group_vals", "options"]; +} + +class ShowGroupedList extends Component { + static template = "account.GroupedListTemplate"; + static components = { ListGroup }; + static props = {...standardFieldProps}; + getValue() { + const value = this.props.record.data[this.props.name]; + return value + ? JSON.parse(value) + : { groups_vals: [], options: { discarded_number: "", columns: [] } }; + } +} + +registry.category("fields").add("grouped_view_widget", { + component: ShowGroupedList, +}); diff --git a/frontend/account/static/src/components/grouped_view_widget/grouped_view_widget.xml b/frontend/account/static/src/components/grouped_view_widget/grouped_view_widget.xml new file mode 100644 index 0000000..b4faa93 --- /dev/null +++ b/frontend/account/static/src/components/grouped_view_widget/grouped_view_widget.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + +
+ +
+ + are not shown in the preview + +
+ + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/frontend/account/static/src/components/mail_attachments/mail_attachments.js b/frontend/account/static/src/components/mail_attachments/mail_attachments.js new file mode 100644 index 0000000..045a97b --- /dev/null +++ b/frontend/account/static/src/components/mail_attachments/mail_attachments.js @@ -0,0 +1,74 @@ +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { FileInput } from "@web/core/file_input/file_input"; +import { Component, onWillUnmount } from "@odoo/owl"; +import { standardFieldProps } from "@web/views/fields/standard_field_props"; + +export class MailAttachments extends Component { + static template = "account.mail_attachments"; + static components = { FileInput }; + static props = {...standardFieldProps}; + + setup() { + this.orm = useService("orm"); + this.notification = useService("notification"); + this.attachmentIdsToUnlink = new Set(); + + onWillUnmount(this.onWillUnmount); + } + + get attachments() { + return this.props.record.data[this.props.name] || []; + } + + get renderedAttachments() { + const attachments = JSON.parse(JSON.stringify(this.attachments)); + const attachmentsNotSupported = this.props.record.data.attachments_not_supported || {}; + for (const attachment of attachments) { + if (attachment.id && attachment.id in attachmentsNotSupported) { + attachment.tooltip = attachmentsNotSupported[attachment.id]; + } + } + return attachments; + } + + onFileRemove(deleteId) { + const newValue = []; + + for (let item of this.attachments) { + if (item.id === deleteId) { + if (item.placeholder || item.protect_from_deletion) { + const copyItem = Object.assign({ skip: true }, item); + newValue.push(copyItem); + } else { + this.attachmentIdsToUnlink.add(item.id); + } + } else { + newValue.push(item); + } + } + + this.props.record.update({ [this.props.name]: newValue }); + } + + async onWillUnmount() { + // Unlink added attachments if the wizard is not saved. + if (!this.props.record.resId) { + this.attachments.forEach((item) => { + if (item.manual) { + this.attachmentIdsToUnlink.add(item.id); + } + }); + } + + if (this.attachmentIdsToUnlink.size) { + await this.orm.unlink("ir.attachment", Array.from(this.attachmentIdsToUnlink)); + } + } +} + +export const mailAttachments = { + component: MailAttachments, +}; + +registry.category("fields").add("mail_attachments", mailAttachments); diff --git a/frontend/account/static/src/components/mail_attachments/mail_attachments.xml b/frontend/account/static/src/components/mail_attachments/mail_attachments.xml new file mode 100644 index 0000000..2e7a7c1 --- /dev/null +++ b/frontend/account/static/src/components/mail_attachments/mail_attachments.xml @@ -0,0 +1,20 @@ + + + +
    + + +
  • + + + + + +
  • +
    +
    +
+
+
diff --git a/frontend/account/static/src/components/mail_attachments/mail_attachments_selector.js b/frontend/account/static/src/components/mail_attachments/mail_attachments_selector.js new file mode 100644 index 0000000..de246ad --- /dev/null +++ b/frontend/account/static/src/components/mail_attachments/mail_attachments_selector.js @@ -0,0 +1,51 @@ +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { FileUploader } from "@web/views/fields/file_handler"; +import { Component } from "@odoo/owl"; +import { standardFieldProps } from "@web/views/fields/standard_field_props"; +import { useX2ManyCrud } from "@web/views/fields/relational_utils"; +import { dataUrlToBlob } from "@mail/core/common/attachment_uploader_hook"; + +export class MailAttachments extends Component { + static template = "mail.MailComposerAttachmentSelector"; + static components = { FileUploader }; + static props = {...standardFieldProps}; + + setup() { + this.mailStore = useService("mail.store"); + this.attachmentUploadService = useService("mail.attachment_upload"); + this.operations = useX2ManyCrud(() => { + return this.props.record.data["attachment_ids"]; + }, true); + } + + get attachments() { + return this.props.record.data[this.props.name] || []; + } + + async onFileUploaded({ name, data, type }) { + const resIds = JSON.parse(this.props.record.data.res_ids); + const thread = await this.mailStore.Thread.insert({ + model: this.props.record.data.model, + id: resIds[0], + }); + + const file = new File([dataUrlToBlob(data, type)], name, { type }); + const attachment = await this.attachmentUploadService.upload(thread, thread.composer, file); + + let fileDict = { + id: attachment.id, + name: attachment.name, + mimetype: attachment.mimetype, + placeholder: false, + manual: true, + }; + this.props.record.update({ [this.props.name]: this.attachments.concat([fileDict]) }); + } +} + +export const mailAttachments = { + component: MailAttachments, +}; + +registry.category("fields").add("mail_attachments_selector", mailAttachments); diff --git a/frontend/account/static/src/components/many2many_tags_banks/many2many_tags_banks.js b/frontend/account/static/src/components/many2many_tags_banks/many2many_tags_banks.js new file mode 100644 index 0000000..c9530d7 --- /dev/null +++ b/frontend/account/static/src/components/many2many_tags_banks/many2many_tags_banks.js @@ -0,0 +1,80 @@ +import { + many2ManyTagsFieldColorEditable, + Many2ManyTagsFieldColorEditable, +} from "@web/views/fields/many2many_tags/many2many_tags_field"; +import { useService } from "@web/core/utils/hooks"; +import { registry } from "@web/core/registry"; +import { TagsList } from "@web/core/tags_list/tags_list"; +import { _t } from "@web/core/l10n/translation"; +import { onMounted } from "@odoo/owl"; + +export class FieldMany2ManyTagsBanksTagsList extends TagsList { + static template = "FieldMany2ManyTagsBanksTagsList"; +} + +export class FieldMany2ManyTagsBanks extends Many2ManyTagsFieldColorEditable { + static template = "account.FieldMany2ManyTagsBanks"; + static components = { + ...FieldMany2ManyTagsBanks.components, + TagsList: FieldMany2ManyTagsBanksTagsList, + }; + + setup() { + super.setup(); + this.actionService = useService("action"); + onMounted(async () => { + // Needed when you create a partner (from a move for example), we want the partner to be saved to be able + // to have it as account holder + const isDirty = await this.props.record.model.root.isDirty(); + if (isDirty) { + this.props.record.model.root.save(); + } + }); + } + + getTagProps(record) { + return { + ...super.getTagProps(record), + allowOutPayment: record.data?.allow_out_payment, + }; + } + + openBanksListView() { + this.actionService.doAction({ + type: "ir.actions.act_window", + name: _t("Banks"), + res_model: this.relation, + views: [ + [false, "list"], + [false, "form"], + ], + domain: this.getDomain(), + target: "current", + }); + } +} + +export const fieldMany2ManyTagsBanks = { + ...many2ManyTagsFieldColorEditable, + component: FieldMany2ManyTagsBanks, + supportedOptions: [ + ...(many2ManyTagsFieldColorEditable.supportedOptions || []), + { + label: _t("Allows out payments"), + name: "allow_out_payment_field", + type: "boolean", + }, + ], + additionalClasses: [ + ...(many2ManyTagsFieldColorEditable.additionalClasses || []), + "o_field_many2many_tags", + ], + relatedFields: ({ options }) => { + return [ + ...many2ManyTagsFieldColorEditable.relatedFields({ options }), + { name: options.allow_out_payment_field, type: "boolean", readonly: false }, + ]; + }, +}; + +registry.category("fields").add("many2many_tags_banks", fieldMany2ManyTagsBanks); diff --git a/frontend/account/static/src/components/many2many_tags_banks/many2many_tags_banks.xml b/frontend/account/static/src/components/many2many_tags_banks/many2many_tags_banks.xml new file mode 100644 index 0000000..6c51d43 --- /dev/null +++ b/frontend/account/static/src/components/many2many_tags_banks/many2many_tags_banks.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + +
+ + + +
+ +
+ + + + + account.ProductCatalogSearchPanelContent + + + diff --git a/frontend/account/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.js b/frontend/account/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.js new file mode 100644 index 0000000..a4c56f6 --- /dev/null +++ b/frontend/account/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.js @@ -0,0 +1,86 @@ +import { _t } from "@web/core/l10n/translation"; +import { buildM2OFieldDescription, extractM2OFieldProps, m2oSupportedOptions } from "@web/views/fields/many2one/many2one_field"; +import { registry } from "@web/core/registry"; +import { ProductNameAndDescriptionField } from "@product/product_name_and_description/product_name_and_description"; + +export class ProductLabelSectionAndNoteField extends ProductNameAndDescriptionField { + static template = "account.ProductLabelSectionAndNoteField"; + static props = { + ...super.props, + show_label_warning: { type: Boolean, optional: true, default: false }, + }; + + static descriptionColumn = "name"; + + get sectionAndNoteClasses() { + return { + "fw-bolder": this.isSection, + "fw-bold": this.isSubSection, + "fst-italic": this.isNote(), + "text-warning": this.shouldShowWarning(), + }; + } + + get sectionAndNoteIsReadonly() { + return ( + this.props.readonly + && this.isProductClickable + && (["cancel", "posted"].includes(this.props.record.evalContext.parent.state) + || this.props.record.evalContext.parent.locked) + ) + } + + get isSection() { + return this.props.record.data.display_type === "line_section"; + } + + get isSubSection() { + return this.props.record.data.display_type === "line_subsection"; + } + + get isSectionOrSubSection() { + return this.isSection || this.isSubSection; + } + + isNote(record = null) { + record = record || this.props.record; + return record.data.display_type === "line_note"; + } + + parseLabel(value) { + return (this.productName && value && this.productName.concat("\n", value)) + || (this.productName && !value && this.productName) + || (value || ""); + } + + shouldShowWarning() { + return ( + !this.productName && + this.props.show_label_warning && + !this.isSectionOrSubSection && + !this.isNote() + ); + } +} + +export const productLabelSectionAndNoteField = { + ...buildM2OFieldDescription(ProductLabelSectionAndNoteField), + listViewWidth: [240, 400], + supportedOptions: [ + ...m2oSupportedOptions, + { + label: _t("Show Label Warning"), + name: "show_label_warning", + type: "boolean", + default: false + }, + ], + extractProps({ options }) { + const props = extractM2OFieldProps(...arguments); + props.show_label_warning = options.show_label_warning; + return props; + }, +}; +registry + .category("fields") + .add("product_label_section_and_note_field", productLabelSectionAndNoteField); diff --git a/frontend/account/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.scss b/frontend/account/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.scss new file mode 100644 index 0000000..f02e625 --- /dev/null +++ b/frontend/account/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.scss @@ -0,0 +1,14 @@ +.o_field_product_label_section_and_note_cell { + + textarea { + resize: none; + } + + div.o_input { + white-space: pre-wrap; + } + + @include media-only(print) { + height: auto !important; + } +} diff --git a/frontend/account/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml b/frontend/account/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml new file mode 100644 index 0000000..9d1101d --- /dev/null +++ b/frontend/account/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.xml @@ -0,0 +1,66 @@ + + + + +
+ + +
+
+
+
+
+ + +
+
+ + This is used for ornamental images, like borders or watermarks. + +
+
+
+
+ + +
+
+ + +
+
+ Edit alt text (image description) +
+
+
+
+
+ Couldn’t create alt text automatically + Please write your own alt text or try again later. +
+ +
+
+
+ + + +
+
+ + + +
+
+ Image alt text settings +
+
+ Automatic alt text +
+
+
+ + +
+
+ Suggests descriptions to help people who can’t see the image or when the image doesn’t load. Learn more +
+
+
+
+ Alt text AI model (180MB) +
+ Runs locally on your device so your data stays private. Required for automatic alt text. +
+
+ + +
+
+
+
+
+ Alt text editor +
+
+ + +
+
+ Helps you make sure all your images have alt text. +
+
+
+
+ +
+
+
+ +
+ Preparing document for printing… +
+
+ + 0% +
+
+ +
+
+
+ + +
+ + diff --git a/frontend/web/static/lib/popper/popper.js b/frontend/web/static/lib/popper/popper.js new file mode 100644 index 0000000..a00f139 --- /dev/null +++ b/frontend/web/static/lib/popper/popper.js @@ -0,0 +1,1825 @@ +/** + * @popperjs/core v2.11.8 - MIT License + */ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Popper = {})); +}(this, (function (exports) { 'use strict'; + + function getWindow(node) { + if (node == null) { + return window; + } + + if (node.toString() !== '[object Window]') { + var ownerDocument = node.ownerDocument; + return ownerDocument ? ownerDocument.defaultView || window : window; + } + + return node; + } + + function isElement(node) { + var OwnElement = getWindow(node).Element; + return node instanceof OwnElement || node instanceof Element; + } + + function isHTMLElement(node) { + var OwnElement = getWindow(node).HTMLElement; + return node instanceof OwnElement || node instanceof HTMLElement; + } + + function isShadowRoot(node) { + // IE 11 has no ShadowRoot + if (typeof ShadowRoot === 'undefined') { + return false; + } + + var OwnElement = getWindow(node).ShadowRoot; + return node instanceof OwnElement || node instanceof ShadowRoot; + } + + var max = Math.max; + var min = Math.min; + var round = Math.round; + + function getUAString() { + var uaData = navigator.userAgentData; + + if (uaData != null && uaData.brands && Array.isArray(uaData.brands)) { + return uaData.brands.map(function (item) { + return item.brand + "/" + item.version; + }).join(' '); + } + + return navigator.userAgent; + } + + function isLayoutViewport() { + return !/^((?!chrome|android).)*safari/i.test(getUAString()); + } + + function getBoundingClientRect(element, includeScale, isFixedStrategy) { + if (includeScale === void 0) { + includeScale = false; + } + + if (isFixedStrategy === void 0) { + isFixedStrategy = false; + } + + var clientRect = element.getBoundingClientRect(); + var scaleX = 1; + var scaleY = 1; + + if (includeScale && isHTMLElement(element)) { + scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1; + scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1; + } + + var _ref = isElement(element) ? getWindow(element) : window, + visualViewport = _ref.visualViewport; + + var addVisualOffsets = !isLayoutViewport() && isFixedStrategy; + var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX; + var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY; + var width = clientRect.width / scaleX; + var height = clientRect.height / scaleY; + return { + width: width, + height: height, + top: y, + right: x + width, + bottom: y + height, + left: x, + x: x, + y: y + }; + } + + function getWindowScroll(node) { + var win = getWindow(node); + var scrollLeft = win.pageXOffset; + var scrollTop = win.pageYOffset; + return { + scrollLeft: scrollLeft, + scrollTop: scrollTop + }; + } + + function getHTMLElementScroll(element) { + return { + scrollLeft: element.scrollLeft, + scrollTop: element.scrollTop + }; + } + + function getNodeScroll(node) { + if (node === getWindow(node) || !isHTMLElement(node)) { + return getWindowScroll(node); + } else { + return getHTMLElementScroll(node); + } + } + + function getNodeName(element) { + return element ? (element.nodeName || '').toLowerCase() : null; + } + + function getDocumentElement(element) { + // $FlowFixMe[incompatible-return]: assume body is always available + return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing] + element.document) || window.document).documentElement; + } + + function getWindowScrollBarX(element) { + // If has a CSS width greater than the viewport, then this will be + // incorrect for RTL. + // Popper 1 is broken in this case and never had a bug report so let's assume + // it's not an issue. I don't think anyone ever specifies width on + // anyway. + // Browsers where the left scrollbar doesn't cause an issue report `0` for + // this (e.g. Edge 2019, IE11, Safari) + return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft; + } + + function getComputedStyle(element) { + return getWindow(element).getComputedStyle(element); + } + + function isScrollParent(element) { + // Firefox wants us to check `-x` and `-y` variations as well + var _getComputedStyle = getComputedStyle(element), + overflow = _getComputedStyle.overflow, + overflowX = _getComputedStyle.overflowX, + overflowY = _getComputedStyle.overflowY; + + return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX); + } + + function isElementScaled(element) { + var rect = element.getBoundingClientRect(); + var scaleX = round(rect.width) / element.offsetWidth || 1; + var scaleY = round(rect.height) / element.offsetHeight || 1; + return scaleX !== 1 || scaleY !== 1; + } // Returns the composite rect of an element relative to its offsetParent. + // Composite means it takes into account transforms as well as layout. + + + function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) { + if (isFixed === void 0) { + isFixed = false; + } + + var isOffsetParentAnElement = isHTMLElement(offsetParent); + var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent); + var documentElement = getDocumentElement(offsetParent); + var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed); + var scroll = { + scrollLeft: 0, + scrollTop: 0 + }; + var offsets = { + x: 0, + y: 0 + }; + + if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) { + if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078 + isScrollParent(documentElement)) { + scroll = getNodeScroll(offsetParent); + } + + if (isHTMLElement(offsetParent)) { + offsets = getBoundingClientRect(offsetParent, true); + offsets.x += offsetParent.clientLeft; + offsets.y += offsetParent.clientTop; + } else if (documentElement) { + offsets.x = getWindowScrollBarX(documentElement); + } + } + + return { + x: rect.left + scroll.scrollLeft - offsets.x, + y: rect.top + scroll.scrollTop - offsets.y, + width: rect.width, + height: rect.height + }; + } + + // means it doesn't take into account transforms. + + function getLayoutRect(element) { + var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed. + // Fixes https://github.com/popperjs/popper-core/issues/1223 + + var width = element.offsetWidth; + var height = element.offsetHeight; + + if (Math.abs(clientRect.width - width) <= 1) { + width = clientRect.width; + } + + if (Math.abs(clientRect.height - height) <= 1) { + height = clientRect.height; + } + + return { + x: element.offsetLeft, + y: element.offsetTop, + width: width, + height: height + }; + } + + function getParentNode(element) { + if (getNodeName(element) === 'html') { + return element; + } + + return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle + // $FlowFixMe[incompatible-return] + // $FlowFixMe[prop-missing] + element.assignedSlot || // step into the shadow DOM of the parent of a slotted node + element.parentNode || ( // DOM Element detected + isShadowRoot(element) ? element.host : null) || // ShadowRoot detected + // $FlowFixMe[incompatible-call]: HTMLElement is a Node + getDocumentElement(element) // fallback + + ); + } + + function getScrollParent(node) { + if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) { + // $FlowFixMe[incompatible-return]: assume body is always available + return node.ownerDocument.body; + } + + if (isHTMLElement(node) && isScrollParent(node)) { + return node; + } + + return getScrollParent(getParentNode(node)); + } + + /* + given a DOM element, return the list of all scroll parents, up the list of ancesors + until we get to the top window object. This list is what we attach scroll listeners + to, because if any of these parent elements scroll, we'll need to re-calculate the + reference element's position. + */ + + function listScrollParents(element, list) { + var _element$ownerDocumen; + + if (list === void 0) { + list = []; + } + + var scrollParent = getScrollParent(element); + var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body); + var win = getWindow(scrollParent); + var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent; + var updatedList = list.concat(target); + return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here + updatedList.concat(listScrollParents(getParentNode(target))); + } + + function isTableElement(element) { + return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0; + } + + function getTrueOffsetParent(element) { + if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837 + getComputedStyle(element).position === 'fixed') { + return null; + } + + return element.offsetParent; + } // `.offsetParent` reports `null` for fixed elements, while absolute elements + // return the containing block + + + function getContainingBlock(element) { + var isFirefox = /firefox/i.test(getUAString()); + var isIE = /Trident/i.test(getUAString()); + + if (isIE && isHTMLElement(element)) { + // In IE 9, 10 and 11 fixed elements containing block is always established by the viewport + var elementCss = getComputedStyle(element); + + if (elementCss.position === 'fixed') { + return null; + } + } + + var currentNode = getParentNode(element); + + if (isShadowRoot(currentNode)) { + currentNode = currentNode.host; + } + + while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) { + var css = getComputedStyle(currentNode); // This is non-exhaustive but covers the most common CSS properties that + // create a containing block. + // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block + + if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') { + return currentNode; + } else { + currentNode = currentNode.parentNode; + } + } + + return null; + } // Gets the closest ancestor positioned element. Handles some edge cases, + // such as table ancestors and cross browser bugs. + + + function getOffsetParent(element) { + var window = getWindow(element); + var offsetParent = getTrueOffsetParent(element); + + while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === 'static') { + offsetParent = getTrueOffsetParent(offsetParent); + } + + if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle(offsetParent).position === 'static')) { + return window; + } + + return offsetParent || getContainingBlock(element) || window; + } + + var top = 'top'; + var bottom = 'bottom'; + var right = 'right'; + var left = 'left'; + var auto = 'auto'; + var basePlacements = [top, bottom, right, left]; + var start = 'start'; + var end = 'end'; + var clippingParents = 'clippingParents'; + var viewport = 'viewport'; + var popper = 'popper'; + var reference = 'reference'; + var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) { + return acc.concat([placement + "-" + start, placement + "-" + end]); + }, []); + var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) { + return acc.concat([placement, placement + "-" + start, placement + "-" + end]); + }, []); // modifiers that need to read the DOM + + var beforeRead = 'beforeRead'; + var read = 'read'; + var afterRead = 'afterRead'; // pure-logic modifiers + + var beforeMain = 'beforeMain'; + var main = 'main'; + var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state) + + var beforeWrite = 'beforeWrite'; + var write = 'write'; + var afterWrite = 'afterWrite'; + var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite]; + + function order(modifiers) { + var map = new Map(); + var visited = new Set(); + var result = []; + modifiers.forEach(function (modifier) { + map.set(modifier.name, modifier); + }); // On visiting object, check for its dependencies and visit them recursively + + function sort(modifier) { + visited.add(modifier.name); + var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []); + requires.forEach(function (dep) { + if (!visited.has(dep)) { + var depModifier = map.get(dep); + + if (depModifier) { + sort(depModifier); + } + } + }); + result.push(modifier); + } + + modifiers.forEach(function (modifier) { + if (!visited.has(modifier.name)) { + // check for visited object + sort(modifier); + } + }); + return result; + } + + function orderModifiers(modifiers) { + // order based on dependencies + var orderedModifiers = order(modifiers); // order based on phase + + return modifierPhases.reduce(function (acc, phase) { + return acc.concat(orderedModifiers.filter(function (modifier) { + return modifier.phase === phase; + })); + }, []); + } + + function debounce(fn) { + var pending; + return function () { + if (!pending) { + pending = new Promise(function (resolve) { + Promise.resolve().then(function () { + pending = undefined; + resolve(fn()); + }); + }); + } + + return pending; + }; + } + + function mergeByName(modifiers) { + var merged = modifiers.reduce(function (merged, current) { + var existing = merged[current.name]; + merged[current.name] = existing ? Object.assign({}, existing, current, { + options: Object.assign({}, existing.options, current.options), + data: Object.assign({}, existing.data, current.data) + }) : current; + return merged; + }, {}); // IE11 does not support Object.values + + return Object.keys(merged).map(function (key) { + return merged[key]; + }); + } + + function getViewportRect(element, strategy) { + var win = getWindow(element); + var html = getDocumentElement(element); + var visualViewport = win.visualViewport; + var width = html.clientWidth; + var height = html.clientHeight; + var x = 0; + var y = 0; + + if (visualViewport) { + width = visualViewport.width; + height = visualViewport.height; + var layoutViewport = isLayoutViewport(); + + if (layoutViewport || !layoutViewport && strategy === 'fixed') { + x = visualViewport.offsetLeft; + y = visualViewport.offsetTop; + } + } + + return { + width: width, + height: height, + x: x + getWindowScrollBarX(element), + y: y + }; + } + + // of the `` and `` rect bounds if horizontally scrollable + + function getDocumentRect(element) { + var _element$ownerDocumen; + + var html = getDocumentElement(element); + var winScroll = getWindowScroll(element); + var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body; + var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0); + var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0); + var x = -winScroll.scrollLeft + getWindowScrollBarX(element); + var y = -winScroll.scrollTop; + + if (getComputedStyle(body || html).direction === 'rtl') { + x += max(html.clientWidth, body ? body.clientWidth : 0) - width; + } + + return { + width: width, + height: height, + x: x, + y: y + }; + } + + function contains(parent, child) { + var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method + + if (parent.contains(child)) { + return true; + } // then fallback to custom implementation with Shadow DOM support + else if (rootNode && isShadowRoot(rootNode)) { + var next = child; + + do { + if (next && parent.isSameNode(next)) { + return true; + } // $FlowFixMe[prop-missing]: need a better way to handle this... + + + next = next.parentNode || next.host; + } while (next); + } // Give up, the result is false + + + return false; + } + + function rectToClientRect(rect) { + return Object.assign({}, rect, { + left: rect.x, + top: rect.y, + right: rect.x + rect.width, + bottom: rect.y + rect.height + }); + } + + function getInnerBoundingClientRect(element, strategy) { + var rect = getBoundingClientRect(element, false, strategy === 'fixed'); + rect.top = rect.top + element.clientTop; + rect.left = rect.left + element.clientLeft; + rect.bottom = rect.top + element.clientHeight; + rect.right = rect.left + element.clientWidth; + rect.width = element.clientWidth; + rect.height = element.clientHeight; + rect.x = rect.left; + rect.y = rect.top; + return rect; + } + + function getClientRectFromMixedType(element, clippingParent, strategy) { + return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element))); + } // A "clipping parent" is an overflowable container with the characteristic of + // clipping (or hiding) overflowing elements with a position different from + // `initial` + + + function getClippingParents(element) { + var clippingParents = listScrollParents(getParentNode(element)); + var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle(element).position) >= 0; + var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element; + + if (!isElement(clipperElement)) { + return []; + } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414 + + + return clippingParents.filter(function (clippingParent) { + return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body'; + }); + } // Gets the maximum area that the element is visible in due to any number of + // clipping parents + + + function getClippingRect(element, boundary, rootBoundary, strategy) { + var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary); + var clippingParents = [].concat(mainClippingParents, [rootBoundary]); + var firstClippingParent = clippingParents[0]; + var clippingRect = clippingParents.reduce(function (accRect, clippingParent) { + var rect = getClientRectFromMixedType(element, clippingParent, strategy); + accRect.top = max(rect.top, accRect.top); + accRect.right = min(rect.right, accRect.right); + accRect.bottom = min(rect.bottom, accRect.bottom); + accRect.left = max(rect.left, accRect.left); + return accRect; + }, getClientRectFromMixedType(element, firstClippingParent, strategy)); + clippingRect.width = clippingRect.right - clippingRect.left; + clippingRect.height = clippingRect.bottom - clippingRect.top; + clippingRect.x = clippingRect.left; + clippingRect.y = clippingRect.top; + return clippingRect; + } + + function getBasePlacement(placement) { + return placement.split('-')[0]; + } + + function getVariation(placement) { + return placement.split('-')[1]; + } + + function getMainAxisFromPlacement(placement) { + return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y'; + } + + function computeOffsets(_ref) { + var reference = _ref.reference, + element = _ref.element, + placement = _ref.placement; + var basePlacement = placement ? getBasePlacement(placement) : null; + var variation = placement ? getVariation(placement) : null; + var commonX = reference.x + reference.width / 2 - element.width / 2; + var commonY = reference.y + reference.height / 2 - element.height / 2; + var offsets; + + switch (basePlacement) { + case top: + offsets = { + x: commonX, + y: reference.y - element.height + }; + break; + + case bottom: + offsets = { + x: commonX, + y: reference.y + reference.height + }; + break; + + case right: + offsets = { + x: reference.x + reference.width, + y: commonY + }; + break; + + case left: + offsets = { + x: reference.x - element.width, + y: commonY + }; + break; + + default: + offsets = { + x: reference.x, + y: reference.y + }; + } + + var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null; + + if (mainAxis != null) { + var len = mainAxis === 'y' ? 'height' : 'width'; + + switch (variation) { + case start: + offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2); + break; + + case end: + offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2); + break; + } + } + + return offsets; + } + + function getFreshSideObject() { + return { + top: 0, + right: 0, + bottom: 0, + left: 0 + }; + } + + function mergePaddingObject(paddingObject) { + return Object.assign({}, getFreshSideObject(), paddingObject); + } + + function expandToHashMap(value, keys) { + return keys.reduce(function (hashMap, key) { + hashMap[key] = value; + return hashMap; + }, {}); + } + + function detectOverflow(state, options) { + if (options === void 0) { + options = {}; + } + + var _options = options, + _options$placement = _options.placement, + placement = _options$placement === void 0 ? state.placement : _options$placement, + _options$strategy = _options.strategy, + strategy = _options$strategy === void 0 ? state.strategy : _options$strategy, + _options$boundary = _options.boundary, + boundary = _options$boundary === void 0 ? clippingParents : _options$boundary, + _options$rootBoundary = _options.rootBoundary, + rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary, + _options$elementConte = _options.elementContext, + elementContext = _options$elementConte === void 0 ? popper : _options$elementConte, + _options$altBoundary = _options.altBoundary, + altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary, + _options$padding = _options.padding, + padding = _options$padding === void 0 ? 0 : _options$padding; + var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements)); + var altContext = elementContext === popper ? reference : popper; + var popperRect = state.rects.popper; + var element = state.elements[altBoundary ? altContext : elementContext]; + var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary, strategy); + var referenceClientRect = getBoundingClientRect(state.elements.reference); + var popperOffsets = computeOffsets({ + reference: referenceClientRect, + element: popperRect, + strategy: 'absolute', + placement: placement + }); + var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets)); + var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect + // 0 or negative = within the clipping rect + + var overflowOffsets = { + top: clippingClientRect.top - elementClientRect.top + paddingObject.top, + bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom, + left: clippingClientRect.left - elementClientRect.left + paddingObject.left, + right: elementClientRect.right - clippingClientRect.right + paddingObject.right + }; + var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element + + if (elementContext === popper && offsetData) { + var offset = offsetData[placement]; + Object.keys(overflowOffsets).forEach(function (key) { + var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1; + var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x'; + overflowOffsets[key] += offset[axis] * multiply; + }); + } + + return overflowOffsets; + } + + var DEFAULT_OPTIONS = { + placement: 'bottom', + modifiers: [], + strategy: 'absolute' + }; + + function areValidElements() { + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + return !args.some(function (element) { + return !(element && typeof element.getBoundingClientRect === 'function'); + }); + } + + function popperGenerator(generatorOptions) { + if (generatorOptions === void 0) { + generatorOptions = {}; + } + + var _generatorOptions = generatorOptions, + _generatorOptions$def = _generatorOptions.defaultModifiers, + defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def, + _generatorOptions$def2 = _generatorOptions.defaultOptions, + defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2; + return function createPopper(reference, popper, options) { + if (options === void 0) { + options = defaultOptions; + } + + var state = { + placement: 'bottom', + orderedModifiers: [], + options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions), + modifiersData: {}, + elements: { + reference: reference, + popper: popper + }, + attributes: {}, + styles: {} + }; + var effectCleanupFns = []; + var isDestroyed = false; + var instance = { + state: state, + setOptions: function setOptions(setOptionsAction) { + var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction; + cleanupModifierEffects(); + state.options = Object.assign({}, defaultOptions, state.options, options); + state.scrollParents = { + reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [], + popper: listScrollParents(popper) + }; // Orders the modifiers based on their dependencies and `phase` + // properties + + var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers + + state.orderedModifiers = orderedModifiers.filter(function (m) { + return m.enabled; + }); + runModifierEffects(); + return instance.update(); + }, + // Sync update – it will always be executed, even if not necessary. This + // is useful for low frequency updates where sync behavior simplifies the + // logic. + // For high frequency updates (e.g. `resize` and `scroll` events), always + // prefer the async Popper#update method + forceUpdate: function forceUpdate() { + if (isDestroyed) { + return; + } + + var _state$elements = state.elements, + reference = _state$elements.reference, + popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements + // anymore + + if (!areValidElements(reference, popper)) { + return; + } // Store the reference and popper rects to be read by modifiers + + + state.rects = { + reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'), + popper: getLayoutRect(popper) + }; // Modifiers have the ability to reset the current update cycle. The + // most common use case for this is the `flip` modifier changing the + // placement, which then needs to re-run all the modifiers, because the + // logic was previously ran for the previous placement and is therefore + // stale/incorrect + + state.reset = false; + state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier + // is filled with the initial data specified by the modifier. This means + // it doesn't persist and is fresh on each update. + // To ensure persistent data, use `${name}#persistent` + + state.orderedModifiers.forEach(function (modifier) { + return state.modifiersData[modifier.name] = Object.assign({}, modifier.data); + }); + + for (var index = 0; index < state.orderedModifiers.length; index++) { + if (state.reset === true) { + state.reset = false; + index = -1; + continue; + } + + var _state$orderedModifie = state.orderedModifiers[index], + fn = _state$orderedModifie.fn, + _state$orderedModifie2 = _state$orderedModifie.options, + _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2, + name = _state$orderedModifie.name; + + if (typeof fn === 'function') { + state = fn({ + state: state, + options: _options, + name: name, + instance: instance + }) || state; + } + } + }, + // Async and optimistically optimized update – it will not be executed if + // not necessary (debounced to run at most once-per-tick) + update: debounce(function () { + return new Promise(function (resolve) { + instance.forceUpdate(); + resolve(state); + }); + }), + destroy: function destroy() { + cleanupModifierEffects(); + isDestroyed = true; + } + }; + + if (!areValidElements(reference, popper)) { + return instance; + } + + instance.setOptions(options).then(function (state) { + if (!isDestroyed && options.onFirstUpdate) { + options.onFirstUpdate(state); + } + }); // Modifiers have the ability to execute arbitrary code before the first + // update cycle runs. They will be executed in the same order as the update + // cycle. This is useful when a modifier adds some persistent data that + // other modifiers need to use, but the modifier is run after the dependent + // one. + + function runModifierEffects() { + state.orderedModifiers.forEach(function (_ref) { + var name = _ref.name, + _ref$options = _ref.options, + options = _ref$options === void 0 ? {} : _ref$options, + effect = _ref.effect; + + if (typeof effect === 'function') { + var cleanupFn = effect({ + state: state, + name: name, + instance: instance, + options: options + }); + + var noopFn = function noopFn() {}; + + effectCleanupFns.push(cleanupFn || noopFn); + } + }); + } + + function cleanupModifierEffects() { + effectCleanupFns.forEach(function (fn) { + return fn(); + }); + effectCleanupFns = []; + } + + return instance; + }; + } + + var passive = { + passive: true + }; + + function effect$2(_ref) { + var state = _ref.state, + instance = _ref.instance, + options = _ref.options; + var _options$scroll = options.scroll, + scroll = _options$scroll === void 0 ? true : _options$scroll, + _options$resize = options.resize, + resize = _options$resize === void 0 ? true : _options$resize; + var window = getWindow(state.elements.popper); + var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper); + + if (scroll) { + scrollParents.forEach(function (scrollParent) { + scrollParent.addEventListener('scroll', instance.update, passive); + }); + } + + if (resize) { + window.addEventListener('resize', instance.update, passive); + } + + return function () { + if (scroll) { + scrollParents.forEach(function (scrollParent) { + scrollParent.removeEventListener('scroll', instance.update, passive); + }); + } + + if (resize) { + window.removeEventListener('resize', instance.update, passive); + } + }; + } // eslint-disable-next-line import/no-unused-modules + + + var eventListeners = { + name: 'eventListeners', + enabled: true, + phase: 'write', + fn: function fn() {}, + effect: effect$2, + data: {} + }; + + function popperOffsets(_ref) { + var state = _ref.state, + name = _ref.name; + // Offsets are the actual position the popper needs to have to be + // properly positioned near its reference element + // This is the most basic placement, and will be adjusted by + // the modifiers in the next step + state.modifiersData[name] = computeOffsets({ + reference: state.rects.reference, + element: state.rects.popper, + strategy: 'absolute', + placement: state.placement + }); + } // eslint-disable-next-line import/no-unused-modules + + + var popperOffsets$1 = { + name: 'popperOffsets', + enabled: true, + phase: 'read', + fn: popperOffsets, + data: {} + }; + + var unsetSides = { + top: 'auto', + right: 'auto', + bottom: 'auto', + left: 'auto' + }; // Round the offsets to the nearest suitable subpixel based on the DPR. + // Zooming can change the DPR, but it seems to report a value that will + // cleanly divide the values into the appropriate subpixels. + + function roundOffsetsByDPR(_ref, win) { + var x = _ref.x, + y = _ref.y; + var dpr = win.devicePixelRatio || 1; + return { + x: round(x * dpr) / dpr || 0, + y: round(y * dpr) / dpr || 0 + }; + } + + function mapToStyles(_ref2) { + var _Object$assign2; + + var popper = _ref2.popper, + popperRect = _ref2.popperRect, + placement = _ref2.placement, + variation = _ref2.variation, + offsets = _ref2.offsets, + position = _ref2.position, + gpuAcceleration = _ref2.gpuAcceleration, + adaptive = _ref2.adaptive, + roundOffsets = _ref2.roundOffsets, + isFixed = _ref2.isFixed; + var _offsets$x = offsets.x, + x = _offsets$x === void 0 ? 0 : _offsets$x, + _offsets$y = offsets.y, + y = _offsets$y === void 0 ? 0 : _offsets$y; + + var _ref3 = typeof roundOffsets === 'function' ? roundOffsets({ + x: x, + y: y + }) : { + x: x, + y: y + }; + + x = _ref3.x; + y = _ref3.y; + var hasX = offsets.hasOwnProperty('x'); + var hasY = offsets.hasOwnProperty('y'); + var sideX = left; + var sideY = top; + var win = window; + + if (adaptive) { + var offsetParent = getOffsetParent(popper); + var heightProp = 'clientHeight'; + var widthProp = 'clientWidth'; + + if (offsetParent === getWindow(popper)) { + offsetParent = getDocumentElement(popper); + + if (getComputedStyle(offsetParent).position !== 'static' && position === 'absolute') { + heightProp = 'scrollHeight'; + widthProp = 'scrollWidth'; + } + } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it + + + offsetParent = offsetParent; + + if (placement === top || (placement === left || placement === right) && variation === end) { + sideY = bottom; + var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing] + offsetParent[heightProp]; + y -= offsetY - popperRect.height; + y *= gpuAcceleration ? 1 : -1; + } + + if (placement === left || (placement === top || placement === bottom) && variation === end) { + sideX = right; + var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing] + offsetParent[widthProp]; + x -= offsetX - popperRect.width; + x *= gpuAcceleration ? 1 : -1; + } + } + + var commonStyles = Object.assign({ + position: position + }, adaptive && unsetSides); + + var _ref4 = roundOffsets === true ? roundOffsetsByDPR({ + x: x, + y: y + }, getWindow(popper)) : { + x: x, + y: y + }; + + x = _ref4.x; + y = _ref4.y; + + if (gpuAcceleration) { + var _Object$assign; + + return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? "translate(" + x + "px, " + y + "px)" : "translate3d(" + x + "px, " + y + "px, 0)", _Object$assign)); + } + + return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + "px" : '', _Object$assign2[sideX] = hasX ? x + "px" : '', _Object$assign2.transform = '', _Object$assign2)); + } + + function computeStyles(_ref5) { + var state = _ref5.state, + options = _ref5.options; + var _options$gpuAccelerat = options.gpuAcceleration, + gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat, + _options$adaptive = options.adaptive, + adaptive = _options$adaptive === void 0 ? true : _options$adaptive, + _options$roundOffsets = options.roundOffsets, + roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets; + var commonStyles = { + placement: getBasePlacement(state.placement), + variation: getVariation(state.placement), + popper: state.elements.popper, + popperRect: state.rects.popper, + gpuAcceleration: gpuAcceleration, + isFixed: state.options.strategy === 'fixed' + }; + + if (state.modifiersData.popperOffsets != null) { + state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, { + offsets: state.modifiersData.popperOffsets, + position: state.options.strategy, + adaptive: adaptive, + roundOffsets: roundOffsets + }))); + } + + if (state.modifiersData.arrow != null) { + state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, { + offsets: state.modifiersData.arrow, + position: 'absolute', + adaptive: false, + roundOffsets: roundOffsets + }))); + } + + state.attributes.popper = Object.assign({}, state.attributes.popper, { + 'data-popper-placement': state.placement + }); + } // eslint-disable-next-line import/no-unused-modules + + + var computeStyles$1 = { + name: 'computeStyles', + enabled: true, + phase: 'beforeWrite', + fn: computeStyles, + data: {} + }; + + // and applies them to the HTMLElements such as popper and arrow + + function applyStyles(_ref) { + var state = _ref.state; + Object.keys(state.elements).forEach(function (name) { + var style = state.styles[name] || {}; + var attributes = state.attributes[name] || {}; + var element = state.elements[name]; // arrow is optional + virtual elements + + if (!isHTMLElement(element) || !getNodeName(element)) { + return; + } // Flow doesn't support to extend this property, but it's the most + // effective way to apply styles to an HTMLElement + // $FlowFixMe[cannot-write] + + + Object.assign(element.style, style); + Object.keys(attributes).forEach(function (name) { + var value = attributes[name]; + + if (value === false) { + element.removeAttribute(name); + } else { + element.setAttribute(name, value === true ? '' : value); + } + }); + }); + } + + function effect$1(_ref2) { + var state = _ref2.state; + var initialStyles = { + popper: { + position: state.options.strategy, + left: '0', + top: '0', + margin: '0' + }, + arrow: { + position: 'absolute' + }, + reference: {} + }; + Object.assign(state.elements.popper.style, initialStyles.popper); + state.styles = initialStyles; + + if (state.elements.arrow) { + Object.assign(state.elements.arrow.style, initialStyles.arrow); + } + + return function () { + Object.keys(state.elements).forEach(function (name) { + var element = state.elements[name]; + var attributes = state.attributes[name] || {}; + var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them + + var style = styleProperties.reduce(function (style, property) { + style[property] = ''; + return style; + }, {}); // arrow is optional + virtual elements + + if (!isHTMLElement(element) || !getNodeName(element)) { + return; + } + + Object.assign(element.style, style); + Object.keys(attributes).forEach(function (attribute) { + element.removeAttribute(attribute); + }); + }); + }; + } // eslint-disable-next-line import/no-unused-modules + + + var applyStyles$1 = { + name: 'applyStyles', + enabled: true, + phase: 'write', + fn: applyStyles, + effect: effect$1, + requires: ['computeStyles'] + }; + + function distanceAndSkiddingToXY(placement, rects, offset) { + var basePlacement = getBasePlacement(placement); + var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1; + + var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, { + placement: placement + })) : offset, + skidding = _ref[0], + distance = _ref[1]; + + skidding = skidding || 0; + distance = (distance || 0) * invertDistance; + return [left, right].indexOf(basePlacement) >= 0 ? { + x: distance, + y: skidding + } : { + x: skidding, + y: distance + }; + } + + function offset(_ref2) { + var state = _ref2.state, + options = _ref2.options, + name = _ref2.name; + var _options$offset = options.offset, + offset = _options$offset === void 0 ? [0, 0] : _options$offset; + var data = placements.reduce(function (acc, placement) { + acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset); + return acc; + }, {}); + var _data$state$placement = data[state.placement], + x = _data$state$placement.x, + y = _data$state$placement.y; + + if (state.modifiersData.popperOffsets != null) { + state.modifiersData.popperOffsets.x += x; + state.modifiersData.popperOffsets.y += y; + } + + state.modifiersData[name] = data; + } // eslint-disable-next-line import/no-unused-modules + + + var offset$1 = { + name: 'offset', + enabled: true, + phase: 'main', + requires: ['popperOffsets'], + fn: offset + }; + + var hash$1 = { + left: 'right', + right: 'left', + bottom: 'top', + top: 'bottom' + }; + function getOppositePlacement(placement) { + return placement.replace(/left|right|bottom|top/g, function (matched) { + return hash$1[matched]; + }); + } + + var hash = { + start: 'end', + end: 'start' + }; + function getOppositeVariationPlacement(placement) { + return placement.replace(/start|end/g, function (matched) { + return hash[matched]; + }); + } + + function computeAutoPlacement(state, options) { + if (options === void 0) { + options = {}; + } + + var _options = options, + placement = _options.placement, + boundary = _options.boundary, + rootBoundary = _options.rootBoundary, + padding = _options.padding, + flipVariations = _options.flipVariations, + _options$allowedAutoP = _options.allowedAutoPlacements, + allowedAutoPlacements = _options$allowedAutoP === void 0 ? placements : _options$allowedAutoP; + var variation = getVariation(placement); + var placements$1 = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) { + return getVariation(placement) === variation; + }) : basePlacements; + var allowedPlacements = placements$1.filter(function (placement) { + return allowedAutoPlacements.indexOf(placement) >= 0; + }); + + if (allowedPlacements.length === 0) { + allowedPlacements = placements$1; + } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions... + + + var overflows = allowedPlacements.reduce(function (acc, placement) { + acc[placement] = detectOverflow(state, { + placement: placement, + boundary: boundary, + rootBoundary: rootBoundary, + padding: padding + })[getBasePlacement(placement)]; + return acc; + }, {}); + return Object.keys(overflows).sort(function (a, b) { + return overflows[a] - overflows[b]; + }); + } + + function getExpandedFallbackPlacements(placement) { + if (getBasePlacement(placement) === auto) { + return []; + } + + var oppositePlacement = getOppositePlacement(placement); + return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)]; + } + + function flip(_ref) { + var state = _ref.state, + options = _ref.options, + name = _ref.name; + + if (state.modifiersData[name]._skip) { + return; + } + + var _options$mainAxis = options.mainAxis, + checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis, + _options$altAxis = options.altAxis, + checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis, + specifiedFallbackPlacements = options.fallbackPlacements, + padding = options.padding, + boundary = options.boundary, + rootBoundary = options.rootBoundary, + altBoundary = options.altBoundary, + _options$flipVariatio = options.flipVariations, + flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio, + allowedAutoPlacements = options.allowedAutoPlacements; + var preferredPlacement = state.options.placement; + var basePlacement = getBasePlacement(preferredPlacement); + var isBasePlacement = basePlacement === preferredPlacement; + var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement)); + var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) { + return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, { + placement: placement, + boundary: boundary, + rootBoundary: rootBoundary, + padding: padding, + flipVariations: flipVariations, + allowedAutoPlacements: allowedAutoPlacements + }) : placement); + }, []); + var referenceRect = state.rects.reference; + var popperRect = state.rects.popper; + var checksMap = new Map(); + var makeFallbackChecks = true; + var firstFittingPlacement = placements[0]; + + for (var i = 0; i < placements.length; i++) { + var placement = placements[i]; + + var _basePlacement = getBasePlacement(placement); + + var isStartVariation = getVariation(placement) === start; + var isVertical = [top, bottom].indexOf(_basePlacement) >= 0; + var len = isVertical ? 'width' : 'height'; + var overflow = detectOverflow(state, { + placement: placement, + boundary: boundary, + rootBoundary: rootBoundary, + altBoundary: altBoundary, + padding: padding + }); + var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top; + + if (referenceRect[len] > popperRect[len]) { + mainVariationSide = getOppositePlacement(mainVariationSide); + } + + var altVariationSide = getOppositePlacement(mainVariationSide); + var checks = []; + + if (checkMainAxis) { + checks.push(overflow[_basePlacement] <= 0); + } + + if (checkAltAxis) { + checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0); + } + + if (checks.every(function (check) { + return check; + })) { + firstFittingPlacement = placement; + makeFallbackChecks = false; + break; + } + + checksMap.set(placement, checks); + } + + if (makeFallbackChecks) { + // `2` may be desired in some cases – research later + var numberOfChecks = flipVariations ? 3 : 1; + + var _loop = function _loop(_i) { + var fittingPlacement = placements.find(function (placement) { + var checks = checksMap.get(placement); + + if (checks) { + return checks.slice(0, _i).every(function (check) { + return check; + }); + } + }); + + if (fittingPlacement) { + firstFittingPlacement = fittingPlacement; + return "break"; + } + }; + + for (var _i = numberOfChecks; _i > 0; _i--) { + var _ret = _loop(_i); + + if (_ret === "break") break; + } + } + + if (state.placement !== firstFittingPlacement) { + state.modifiersData[name]._skip = true; + state.placement = firstFittingPlacement; + state.reset = true; + } + } // eslint-disable-next-line import/no-unused-modules + + + var flip$1 = { + name: 'flip', + enabled: true, + phase: 'main', + fn: flip, + requiresIfExists: ['offset'], + data: { + _skip: false + } + }; + + function getAltAxis(axis) { + return axis === 'x' ? 'y' : 'x'; + } + + function within(min$1, value, max$1) { + return max(min$1, min(value, max$1)); + } + function withinMaxClamp(min, value, max) { + var v = within(min, value, max); + return v > max ? max : v; + } + + function preventOverflow(_ref) { + var state = _ref.state, + options = _ref.options, + name = _ref.name; + var _options$mainAxis = options.mainAxis, + checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis, + _options$altAxis = options.altAxis, + checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis, + boundary = options.boundary, + rootBoundary = options.rootBoundary, + altBoundary = options.altBoundary, + padding = options.padding, + _options$tether = options.tether, + tether = _options$tether === void 0 ? true : _options$tether, + _options$tetherOffset = options.tetherOffset, + tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset; + var overflow = detectOverflow(state, { + boundary: boundary, + rootBoundary: rootBoundary, + padding: padding, + altBoundary: altBoundary + }); + var basePlacement = getBasePlacement(state.placement); + var variation = getVariation(state.placement); + var isBasePlacement = !variation; + var mainAxis = getMainAxisFromPlacement(basePlacement); + var altAxis = getAltAxis(mainAxis); + var popperOffsets = state.modifiersData.popperOffsets; + var referenceRect = state.rects.reference; + var popperRect = state.rects.popper; + var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, { + placement: state.placement + })) : tetherOffset; + var normalizedTetherOffsetValue = typeof tetherOffsetValue === 'number' ? { + mainAxis: tetherOffsetValue, + altAxis: tetherOffsetValue + } : Object.assign({ + mainAxis: 0, + altAxis: 0 + }, tetherOffsetValue); + var offsetModifierState = state.modifiersData.offset ? state.modifiersData.offset[state.placement] : null; + var data = { + x: 0, + y: 0 + }; + + if (!popperOffsets) { + return; + } + + if (checkMainAxis) { + var _offsetModifierState$; + + var mainSide = mainAxis === 'y' ? top : left; + var altSide = mainAxis === 'y' ? bottom : right; + var len = mainAxis === 'y' ? 'height' : 'width'; + var offset = popperOffsets[mainAxis]; + var min$1 = offset + overflow[mainSide]; + var max$1 = offset - overflow[altSide]; + var additive = tether ? -popperRect[len] / 2 : 0; + var minLen = variation === start ? referenceRect[len] : popperRect[len]; + var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go + // outside the reference bounds + + var arrowElement = state.elements.arrow; + var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : { + width: 0, + height: 0 + }; + var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject(); + var arrowPaddingMin = arrowPaddingObject[mainSide]; + var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want + // to include its full size in the calculation. If the reference is small + // and near the edge of a boundary, the popper can overflow even if the + // reference is not overflowing as well (e.g. virtual elements with no + // width or height) + + var arrowLen = within(0, referenceRect[len], arrowRect[len]); + var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis : minLen - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis; + var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis : maxLen + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis; + var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow); + var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0; + var offsetModifierValue = (_offsetModifierState$ = offsetModifierState == null ? void 0 : offsetModifierState[mainAxis]) != null ? _offsetModifierState$ : 0; + var tetherMin = offset + minOffset - offsetModifierValue - clientOffset; + var tetherMax = offset + maxOffset - offsetModifierValue; + var preventedOffset = within(tether ? min(min$1, tetherMin) : min$1, offset, tether ? max(max$1, tetherMax) : max$1); + popperOffsets[mainAxis] = preventedOffset; + data[mainAxis] = preventedOffset - offset; + } + + if (checkAltAxis) { + var _offsetModifierState$2; + + var _mainSide = mainAxis === 'x' ? top : left; + + var _altSide = mainAxis === 'x' ? bottom : right; + + var _offset = popperOffsets[altAxis]; + + var _len = altAxis === 'y' ? 'height' : 'width'; + + var _min = _offset + overflow[_mainSide]; + + var _max = _offset - overflow[_altSide]; + + var isOriginSide = [top, left].indexOf(basePlacement) !== -1; + + var _offsetModifierValue = (_offsetModifierState$2 = offsetModifierState == null ? void 0 : offsetModifierState[altAxis]) != null ? _offsetModifierState$2 : 0; + + var _tetherMin = isOriginSide ? _min : _offset - referenceRect[_len] - popperRect[_len] - _offsetModifierValue + normalizedTetherOffsetValue.altAxis; + + var _tetherMax = isOriginSide ? _offset + referenceRect[_len] + popperRect[_len] - _offsetModifierValue - normalizedTetherOffsetValue.altAxis : _max; + + var _preventedOffset = tether && isOriginSide ? withinMaxClamp(_tetherMin, _offset, _tetherMax) : within(tether ? _tetherMin : _min, _offset, tether ? _tetherMax : _max); + + popperOffsets[altAxis] = _preventedOffset; + data[altAxis] = _preventedOffset - _offset; + } + + state.modifiersData[name] = data; + } // eslint-disable-next-line import/no-unused-modules + + + var preventOverflow$1 = { + name: 'preventOverflow', + enabled: true, + phase: 'main', + fn: preventOverflow, + requiresIfExists: ['offset'] + }; + + var toPaddingObject = function toPaddingObject(padding, state) { + padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, { + placement: state.placement + })) : padding; + return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements)); + }; + + function arrow(_ref) { + var _state$modifiersData$; + + var state = _ref.state, + name = _ref.name, + options = _ref.options; + var arrowElement = state.elements.arrow; + var popperOffsets = state.modifiersData.popperOffsets; + var basePlacement = getBasePlacement(state.placement); + var axis = getMainAxisFromPlacement(basePlacement); + var isVertical = [left, right].indexOf(basePlacement) >= 0; + var len = isVertical ? 'height' : 'width'; + + if (!arrowElement || !popperOffsets) { + return; + } + + var paddingObject = toPaddingObject(options.padding, state); + var arrowRect = getLayoutRect(arrowElement); + var minProp = axis === 'y' ? top : left; + var maxProp = axis === 'y' ? bottom : right; + var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len]; + var startDiff = popperOffsets[axis] - state.rects.reference[axis]; + var arrowOffsetParent = getOffsetParent(arrowElement); + var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0; + var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is + // outside of the popper bounds + + var min = paddingObject[minProp]; + var max = clientSize - arrowRect[len] - paddingObject[maxProp]; + var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference; + var offset = within(min, center, max); // Prevents breaking syntax highlighting... + + var axisProp = axis; + state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$); + } + + function effect(_ref2) { + var state = _ref2.state, + options = _ref2.options; + var _options$element = options.element, + arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element; + + if (arrowElement == null) { + return; + } // CSS selector + + + if (typeof arrowElement === 'string') { + arrowElement = state.elements.popper.querySelector(arrowElement); + + if (!arrowElement) { + return; + } + } + + if (!contains(state.elements.popper, arrowElement)) { + return; + } + + state.elements.arrow = arrowElement; + } // eslint-disable-next-line import/no-unused-modules + + + var arrow$1 = { + name: 'arrow', + enabled: true, + phase: 'main', + fn: arrow, + effect: effect, + requires: ['popperOffsets'], + requiresIfExists: ['preventOverflow'] + }; + + function getSideOffsets(overflow, rect, preventedOffsets) { + if (preventedOffsets === void 0) { + preventedOffsets = { + x: 0, + y: 0 + }; + } + + return { + top: overflow.top - rect.height - preventedOffsets.y, + right: overflow.right - rect.width + preventedOffsets.x, + bottom: overflow.bottom - rect.height + preventedOffsets.y, + left: overflow.left - rect.width - preventedOffsets.x + }; + } + + function isAnySideFullyClipped(overflow) { + return [top, right, bottom, left].some(function (side) { + return overflow[side] >= 0; + }); + } + + function hide(_ref) { + var state = _ref.state, + name = _ref.name; + var referenceRect = state.rects.reference; + var popperRect = state.rects.popper; + var preventedOffsets = state.modifiersData.preventOverflow; + var referenceOverflow = detectOverflow(state, { + elementContext: 'reference' + }); + var popperAltOverflow = detectOverflow(state, { + altBoundary: true + }); + var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect); + var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets); + var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets); + var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets); + state.modifiersData[name] = { + referenceClippingOffsets: referenceClippingOffsets, + popperEscapeOffsets: popperEscapeOffsets, + isReferenceHidden: isReferenceHidden, + hasPopperEscaped: hasPopperEscaped + }; + state.attributes.popper = Object.assign({}, state.attributes.popper, { + 'data-popper-reference-hidden': isReferenceHidden, + 'data-popper-escaped': hasPopperEscaped + }); + } // eslint-disable-next-line import/no-unused-modules + + + var hide$1 = { + name: 'hide', + enabled: true, + phase: 'main', + requiresIfExists: ['preventOverflow'], + fn: hide + }; + + var defaultModifiers$1 = [eventListeners, popperOffsets$1, computeStyles$1, applyStyles$1]; + var createPopper$1 = /*#__PURE__*/popperGenerator({ + defaultModifiers: defaultModifiers$1 + }); // eslint-disable-next-line import/no-unused-modules + + var defaultModifiers = [eventListeners, popperOffsets$1, computeStyles$1, applyStyles$1, offset$1, flip$1, preventOverflow$1, arrow$1, hide$1]; + var createPopper = /*#__PURE__*/popperGenerator({ + defaultModifiers: defaultModifiers + }); // eslint-disable-next-line import/no-unused-modules + + exports.applyStyles = applyStyles$1; + exports.arrow = arrow$1; + exports.computeStyles = computeStyles$1; + exports.createPopper = createPopper; + exports.createPopperLite = createPopper$1; + exports.defaultModifiers = defaultModifiers; + exports.detectOverflow = detectOverflow; + exports.eventListeners = eventListeners; + exports.flip = flip$1; + exports.hide = hide$1; + exports.offset = offset$1; + exports.popperGenerator = popperGenerator; + exports.popperOffsets = popperOffsets$1; + exports.preventOverflow = preventOverflow$1; + + Object.defineProperty(exports, '__esModule', { value: true }); + +}))); +//# sourceMappingURL=popper.js.map diff --git a/frontend/web/static/lib/prismjs/themes/default.css b/frontend/web/static/lib/prismjs/themes/default.css new file mode 100644 index 0000000..b0d1f17 --- /dev/null +++ b/frontend/web/static/lib/prismjs/themes/default.css @@ -0,0 +1,142 @@ +/* PrismJS 1.30.0 +https://prismjs.com/download#themes=prism&languages=markup+css+clike+javascript+diff+java+javadoclike+jsdoc+json+markdown+python+sass+scss+sql+typescript */ +/** + * prism.js default theme for JavaScript, CSS and HTML + * Based on dabblet (http://dabblet.com) + * @author Lea Verou + */ + +code[class*="language-"], +pre[class*="language-"] { + color: black; + background: none; + text-shadow: 0 1px white; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + font-size: 1em; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, +code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { + text-shadow: none; + background: #b3d4fc; +} + +pre[class*="language-"]::selection, pre[class*="language-"] ::selection, +code[class*="language-"]::selection, code[class*="language-"] ::selection { + text-shadow: none; + background: #b3d4fc; +} + +@media print { + code[class*="language-"], + pre[class*="language-"] { + text-shadow: none; + } +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +:not(pre) > code[class*="language-"], +pre[class*="language-"] { + background: #f5f2f0; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: .1em; + border-radius: .3em; + white-space: normal; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: slategray; +} + +.token.punctuation { + color: #999; +} + +.token.namespace { + opacity: .7; +} + +.token.property, +.token.tag, +.token.boolean, +.token.number, +.token.constant, +.token.symbol, +.token.deleted { + color: #905; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + color: #690; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string { + color: #9a6e3a; + /* This background color was intended by the author of this theme. */ + background: hsla(0, 0%, 100%, .5); +} + +.token.atrule, +.token.attr-value, +.token.keyword { + color: #07a; +} + +.token.function, +.token.class-name { + color: #DD4A68; +} + +.token.regex, +.token.important, +.token.variable { + color: #e90; +} + +.token.important, +.token.bold { + font-weight: bold; +} +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} diff --git a/frontend/web/static/lib/prismjs/themes/okaida.css b/frontend/web/static/lib/prismjs/themes/okaida.css new file mode 100644 index 0000000..62c4beb --- /dev/null +++ b/frontend/web/static/lib/prismjs/themes/okaida.css @@ -0,0 +1,125 @@ +/* PrismJS 1.30.0 +https://prismjs.com/download#themes=prism-okaidia&languages=markup+css+clike+javascript+diff+java+javadoclike+jsdoc+json+markdown+python+sass+scss+sql+typescript */ +/** + * okaidia theme for JavaScript, CSS and HTML + * Loosely based on Monokai textmate theme by http://www.monokai.nl/ + * @author ocodia + */ + +code[class*="language-"], +pre[class*="language-"] { + color: #f8f8f2; + background: none; + text-shadow: 0 1px rgba(0, 0, 0, 0.3); + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + font-size: 1em; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; + border-radius: 0.3em; +} + +:not(pre) > code[class*="language-"], +pre[class*="language-"] { + background: #272822; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: .1em; + border-radius: .3em; + white-space: normal; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: #8292a2; +} + +.token.punctuation { + color: #f8f8f2; +} + +.token.namespace { + opacity: .7; +} + +.token.property, +.token.tag, +.token.constant, +.token.symbol, +.token.deleted { + color: #f92672; +} + +.token.boolean, +.token.number { + color: #ae81ff; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + color: #a6e22e; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string, +.token.variable { + color: #f8f8f2; +} + +.token.atrule, +.token.attr-value, +.token.function, +.token.class-name { + color: #e6db74; +} + +.token.keyword { + color: #66d9ef; +} + +.token.regex, +.token.important { + color: #fd971f; +} + +.token.important, +.token.bold { + font-weight: bold; +} +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} diff --git a/frontend/web/static/lib/qunit/qunit-2.9.1.css b/frontend/web/static/lib/qunit/qunit-2.9.1.css new file mode 100644 index 0000000..4e99a39 --- /dev/null +++ b/frontend/web/static/lib/qunit/qunit-2.9.1.css @@ -0,0 +1,436 @@ +/*! + * QUnit 2.9.1 + * https://qunitjs.com/ + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license + * https://jquery.org/license + * + * Date: 2019-01-07T16:37Z + */ + +/** Font Family and Sizes */ + +#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult { + font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; +} + +#qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } +#qunit-tests { font-size: smaller; } + + +/** Resets */ + +#qunit-tests, #qunit-header, #qunit-banner, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { + margin: 0; + padding: 0; +} + + +/** Header (excluding toolbar) */ + +#qunit-header { + padding: 0.5em 0 0.5em 1em; + + color: #8699A4; + background-color: #0D3349; + + font-size: 1.5em; + line-height: 1em; + font-weight: 400; + + border-radius: 5px 5px 0 0; +} + +#qunit-header a { + text-decoration: none; + color: #C2CCD1; +} + +#qunit-header a:hover, +#qunit-header a:focus { + color: #FFF; +} + +#qunit-banner { + height: 5px; +} + +#qunit-filteredTest { + padding: 0.5em 1em 0.5em 1em; + color: #366097; + background-color: #F4FF77; +} + +#qunit-userAgent { + padding: 0.5em 1em 0.5em 1em; + color: #FFF; + background-color: #2B81AF; + text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; +} + + +/** Toolbar */ + +#qunit-testrunner-toolbar { + padding: 0.5em 1em 0.5em 1em; + color: #5E740B; + background-color: #EEE; +} + +#qunit-testrunner-toolbar .clearfix { + height: 0; + clear: both; +} + +#qunit-testrunner-toolbar label { + display: inline-block; +} + +#qunit-testrunner-toolbar input[type=checkbox], +#qunit-testrunner-toolbar input[type=radio] { + margin: 3px; + vertical-align: -2px; +} + +#qunit-testrunner-toolbar input[type=text] { + box-sizing: border-box; + height: 1.6em; +} + +.qunit-url-config, +.qunit-filter, +#qunit-modulefilter { + display: inline-block; + line-height: 2.1em; +} + +.qunit-filter, +#qunit-modulefilter { + float: right; + position: relative; + margin-left: 1em; +} + +.qunit-url-config label { + margin-right: 0.5em; +} + +#qunit-modulefilter-search { + box-sizing: border-box; + width: 400px; +} + +#qunit-modulefilter-search-container:after { + position: absolute; + right: 0.3em; + content: "\25bc"; + color: black; +} + +#qunit-modulefilter-dropdown { + /* align with #qunit-modulefilter-search */ + box-sizing: border-box; + width: 400px; + position: absolute; + right: 0; + top: 50%; + margin-top: 0.8em; + + border: 1px solid #D3D3D3; + border-top: none; + border-radius: 0 0 .25em .25em; + color: #000; + background-color: #F5F5F5; + z-index: 99; +} + +#qunit-modulefilter-dropdown a { + color: inherit; + text-decoration: none; +} + +#qunit-modulefilter-dropdown .clickable.checked { + font-weight: bold; + color: #000; + background-color: #D2E0E6; +} + +#qunit-modulefilter-dropdown .clickable:hover { + color: #FFF; + background-color: #0D3349; +} + +#qunit-modulefilter-actions { + display: block; + overflow: auto; + + /* align with #qunit-modulefilter-dropdown-list */ + font: smaller/1.5em sans-serif; +} + +#qunit-modulefilter-dropdown #qunit-modulefilter-actions > * { + box-sizing: border-box; + max-height: 2.8em; + display: block; + padding: 0.4em; +} + +#qunit-modulefilter-dropdown #qunit-modulefilter-actions > button { + float: right; + font: inherit; +} + +#qunit-modulefilter-dropdown #qunit-modulefilter-actions > :last-child { + /* insert padding to align with checkbox margins */ + padding-left: 3px; +} + +#qunit-modulefilter-dropdown-list { + max-height: 200px; + overflow-y: auto; + margin: 0; + border-top: 2px groove threedhighlight; + padding: 0.4em 0 0; + font: smaller/1.5em sans-serif; +} + +#qunit-modulefilter-dropdown-list li { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +#qunit-modulefilter-dropdown-list .clickable { + display: block; + padding-left: 0.15em; +} + + +/** Tests: Pass/Fail */ + +#qunit-tests { + list-style-position: inside; +} + +#qunit-tests li { + padding: 0.4em 1em 0.4em 1em; + border-bottom: 1px solid #FFF; + list-style-position: inside; +} + +#qunit-tests > li { + display: none; +} + +#qunit-tests li.running, +#qunit-tests li.pass, +#qunit-tests li.fail, +#qunit-tests li.skipped, +#qunit-tests li.aborted { + display: list-item; +} + +#qunit-tests.hidepass { + position: relative; +} + +#qunit-tests.hidepass li.running, +#qunit-tests.hidepass li.pass:not(.todo) { + visibility: hidden; + position: absolute; + width: 0; + height: 0; + padding: 0; + border: 0; + margin: 0; +} + +#qunit-tests li strong { + cursor: pointer; +} + +#qunit-tests li.skipped strong { + cursor: default; +} + +#qunit-tests li a { + padding: 0.5em; + color: #C2CCD1; + text-decoration: none; +} + +#qunit-tests li p a { + padding: 0.25em; + color: #6B6464; +} +#qunit-tests li a:hover, +#qunit-tests li a:focus { + color: #000; +} + +#qunit-tests li .runtime { + float: right; + font-size: smaller; +} + +.qunit-assert-list { + margin-top: 0.5em; + padding: 0.5em; + + background-color: #FFF; + + border-radius: 5px; +} + +.qunit-source { + margin: 0.6em 0 0.3em; +} + +.qunit-collapsed { + display: none; +} + +#qunit-tests table { + border-collapse: collapse; + margin-top: 0.2em; +} + +#qunit-tests th { + text-align: right; + vertical-align: top; + padding: 0 0.5em 0 0; +} + +#qunit-tests td { + vertical-align: top; +} + +#qunit-tests pre { + margin: 0; + white-space: pre-wrap; + word-wrap: break-word; +} + +#qunit-tests del { + color: #374E0C; + background-color: #E0F2BE; + text-decoration: none; +} + +#qunit-tests ins { + color: #500; + background-color: #FFCACA; + text-decoration: none; +} + +/*** Test Counts */ + +#qunit-tests b.counts { color: #000; } +#qunit-tests b.passed { color: #5E740B; } +#qunit-tests b.failed { color: #710909; } + +#qunit-tests li li { + padding: 5px; + background-color: #FFF; + border-bottom: none; + list-style-position: inside; +} + +/*** Passing Styles */ + +#qunit-tests li li.pass { + color: #3C510C; + background-color: #FFF; + border-left: 10px solid #C6E746; +} + +#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } +#qunit-tests .pass .test-name { color: #366097; } + +#qunit-tests .pass .test-actual, +#qunit-tests .pass .test-expected { color: #999; } + +#qunit-banner.qunit-pass { background-color: #C6E746; } + +/*** Failing Styles */ + +#qunit-tests li li.fail { + color: #710909; + background-color: #FFF; + border-left: 10px solid #EE5757; + white-space: pre; +} + +#qunit-tests > li:last-child { + border-radius: 0 0 5px 5px; +} + +#qunit-tests .fail { color: #000; background-color: #EE5757; } +#qunit-tests .fail .test-name, +#qunit-tests .fail .module-name { color: #000; } + +#qunit-tests .fail .test-actual { color: #EE5757; } +#qunit-tests .fail .test-expected { color: #008000; } + +#qunit-banner.qunit-fail { background-color: #EE5757; } + + +/*** Aborted tests */ +#qunit-tests .aborted { color: #000; background-color: orange; } +/*** Skipped tests */ + +#qunit-tests .skipped { + background-color: #EBECE9; +} + +#qunit-tests .qunit-todo-label, +#qunit-tests .qunit-skipped-label { + background-color: #F4FF77; + display: inline-block; + font-style: normal; + color: #366097; + line-height: 1.8em; + padding: 0 0.5em; + margin: -0.4em 0.4em -0.4em 0; +} + +#qunit-tests .qunit-todo-label { + background-color: #EEE; +} + +/** Result */ + +#qunit-testresult { + color: #2B81AF; + background-color: #D2E0E6; + + border-bottom: 1px solid #FFF; +} +#qunit-testresult .clearfix { + height: 0; + clear: both; +} +#qunit-testresult .module-name { + font-weight: 700; +} +#qunit-testresult-display { + padding: 0.5em 1em 0.5em 1em; + width: 85%; + float:left; +} +#qunit-testresult-controls { + padding: 0.5em 1em 0.5em 1em; + width: 10%; + float:left; +} + +/** Fixture */ + +#qunit-fixture { + position: absolute; + top: -10000px; + left: -10000px; + width: 1000px; + height: 1000px; +} diff --git a/frontend/web/static/lib/stacktracejs/LICENSE b/frontend/web/static/lib/stacktracejs/LICENSE new file mode 100644 index 0000000..fb96178 --- /dev/null +++ b/frontend/web/static/lib/stacktracejs/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2017 Eric Wendelin and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/frontend/web/static/lib/zxing-library/LICENSE b/frontend/web/static/lib/zxing-library/LICENSE new file mode 100644 index 0000000..4bcbdae --- /dev/null +++ b/frontend/web/static/lib/zxing-library/LICENSE @@ -0,0 +1,245 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +======================================================================== +jai-imageio +======================================================================== + +Copyright (c) 2005 Sun Microsystems, Inc. +Copyright © 2010-2014 University of Manchester +Copyright © 2010-2015 Stian Soiland-Reyes +Copyright © 2015 Peter Hull +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistribution of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +- Redistribution in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +Neither the name of Sun Microsystems, Inc. or the names of +contributors may be used to endorse or promote products derived +from this software without specific prior written permission. + +This software is provided "AS IS," without a warranty of any +kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND +WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY +EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL +NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF +USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS +DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR +ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, +CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND +REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR +INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +You acknowledge that this software is not designed or intended for +use in the design, construction, operation or maintenance of any +nuclear facility. diff --git a/frontend/web/static/lib/zxing-library/version b/frontend/web/static/lib/zxing-library/version new file mode 100644 index 0000000..16eb94e --- /dev/null +++ b/frontend/web/static/lib/zxing-library/version @@ -0,0 +1 @@ +0.21.3 diff --git a/frontend/web/static/src/core/action_swiper/action_swiper.js b/frontend/web/static/src/core/action_swiper/action_swiper.js new file mode 100644 index 0000000..138726a --- /dev/null +++ b/frontend/web/static/src/core/action_swiper/action_swiper.js @@ -0,0 +1,225 @@ +import { browser } from "@web/core/browser/browser"; +import { localization } from "@web/core/l10n/localization"; +import { clamp } from "@web/core/utils/numbers"; + +import { Component, onMounted, onWillUnmount, useRef, useState } from "@odoo/owl"; +import { Deferred } from "@web/core/utils/concurrency"; + +const isScrollSwipable = (scrollables) => { + return { + left: !scrollables.filter((e) => e.scrollLeft !== 0).length, + right: !scrollables.filter( + (e) => e.scrollLeft + Math.round(e.getBoundingClientRect().width) !== e.scrollWidth + ).length, + }; +}; + +/** + * Action Swiper + * + * This component is intended to perform action once a user has completed a touch swipe. + * You can choose the direction allowed for such behavior (left, right or both). + * The action to perform must be passed as a props. It is possible to define a condition + * to allow the swipe interaction conditionnally. + * @extends Component + */ +export class ActionSwiper extends Component { + static template = "web.ActionSwiper"; + static props = { + onLeftSwipe: { + type: Object, + args: { + action: Function, + icon: String, + bgColor: String, + }, + optional: true, + }, + onRightSwipe: { + type: Object, + args: { + action: Function, + icon: String, + bgColor: String, + }, + optional: true, + }, + slots: Object, + animationOnMove: { type: Boolean, optional: true }, + animationType: { type: String, optional: true }, + swipeDistanceRatio: { type: Number, optional: true }, + swipeInvalid: { type: Function, optional: true }, + }; + + static defaultProps = { + onLeftSwipe: undefined, + onRightSwipe: undefined, + animationOnMove: true, + animationType: "bounce", + swipeDistanceRatio: 2, + }; + + setup() { + this.actionTimeoutId = null; + this.resetTimeoutId = null; + this.defaultState = { + containerStyle: "", + isSwiping: false, + width: undefined, + }; + this.root = useRef("root"); + this.targetContainer = useRef("targetContainer"); + this.state = useState({ ...this.defaultState }); + this.scrollables = undefined; + this.startX = undefined; + this.swipedDistance = 0; + this.isScrollValidated = false; + onMounted(() => { + if (this.targetContainer.el) { + this.state.width = this.targetContainer.el.getBoundingClientRect().width; + } + // Forward classes set on component to slot, as we only want to wrap an + // existing component without altering the DOM structure any more than + // strictly necessary + if (this.props.onLeftSwipe || this.props.onRightSwipe) { + const classes = new Set(this.root.el.classList); + classes.delete("o_actionswiper"); + for (const className of classes) { + this.targetContainer.el.firstChild.classList.add(className); + this.root.el.classList.remove(className); + } + } + }); + onWillUnmount(() => { + browser.clearTimeout(this.actionTimeoutId); + browser.clearTimeout(this.resetTimeoutId); + }); + } + get localizedProps() { + return { + onLeftSwipe: + localization.direction === "rtl" ? this.props.onRightSwipe : this.props.onLeftSwipe, + onRightSwipe: + localization.direction === "rtl" ? this.props.onLeftSwipe : this.props.onRightSwipe, + }; + } + + /** + * @private + * @param {TouchEvent} ev + */ + _onTouchEndSwipe() { + if (this.state.isSwiping) { + this.state.isSwiping = false; + if ( + this.localizedProps.onRightSwipe && + this.swipedDistance > this.state.width / this.props.swipeDistanceRatio + ) { + this.swipedDistance = this.state.width; + this.handleSwipe(this.localizedProps.onRightSwipe.action); + } else if ( + this.localizedProps.onLeftSwipe && + this.swipedDistance < -this.state.width / this.props.swipeDistanceRatio + ) { + this.swipedDistance = -this.state.width; + this.handleSwipe(this.localizedProps.onLeftSwipe.action); + } else { + this.state.containerStyle = ""; + } + } + } + /** + * @private + * @param {TouchEvent} ev + */ + _onTouchMoveSwipe(ev) { + if (this.state.isSwiping) { + if (this.props.swipeInvalid && this.props.swipeInvalid()) { + this.state.isSwiping = false; + return; + } + const { onLeftSwipe, onRightSwipe } = this.localizedProps; + this.swipedDistance = clamp( + ev.touches[0].clientX - this.startX, + onLeftSwipe ? -this.state.width : 0, + onRightSwipe ? this.state.width : 0 + ); + // Prevent the browser to navigate back/forward when using swipe + // gestures while still allowing to scroll vertically. + if (Math.abs(this.swipedDistance) > 40) { + ev.preventDefault(); + } + // If there are scrollable elements under touch pressure, + // they must be at their limits to allow swiping. + if ( + !this.isScrollValidated && + this.scrollables && + !isScrollSwipable(this.scrollables)[this.swipedDistance > 0 ? "left" : "right"] + ) { + return this._reset(); + } + this.isScrollValidated = true; + + if (this.props.animationOnMove) { + this.state.containerStyle = `transform: translateX(${this.swipedDistance}px)`; + } + } + } + /** + * @private + * @param {TouchEvent} ev + */ + _onTouchStartSwipe(ev) { + this.scrollables = ev + .composedPath() + .filter( + (e) => + e.nodeType === 1 && + this.targetContainer.el.contains(e) && + e.scrollWidth > e.getBoundingClientRect().width && + ["auto", "scroll"].includes(window.getComputedStyle(e)["overflow-x"]) + ); + if (!this.state.width) { + this.state.width = + this.targetContainer && this.targetContainer.el.getBoundingClientRect().width; + } + this.state.isSwiping = true; + this.isScrollValidated = false; + this.startX = ev.touches[0].clientX; + } + + /** + * @private + */ + _reset() { + Object.assign(this.state, { ...this.defaultState }); + this.scrollables = undefined; + this.startX = undefined; + this.swipedDistance = 0; + this.isScrollValidated = false; + } + + handleSwipe(action) { + if (this.props.animationType === "bounce") { + this.state.containerStyle = `transform: translateX(${this.swipedDistance}px)`; + this.actionTimeoutId = browser.setTimeout(async () => { + await action(Promise.resolve()); + this._reset(); + }, 500); + } else if (this.props.animationType === "forwards") { + this.state.containerStyle = `transform: translateX(${this.swipedDistance}px)`; + this.actionTimeoutId = browser.setTimeout(async () => { + const prom = new Deferred(); + await action(prom); + this.state.isSwiping = true; + this.state.containerStyle = `transform: translateX(${-this.swipedDistance}px)`; + this.resetTimeoutId = browser.setTimeout(() => { + prom.resolve(); + this._reset(); + }, 100); + }, 100); + } else { + return action(Promise.resolve()); + } + } +} diff --git a/frontend/web/static/src/core/action_swiper/action_swiper.scss b/frontend/web/static/src/core/action_swiper/action_swiper.scss new file mode 100644 index 0000000..b681dc3 --- /dev/null +++ b/frontend/web/static/src/core/action_swiper/action_swiper.scss @@ -0,0 +1,20 @@ +.o_actionswiper { + position: relative; + touch-action: pan-y; +} +.o_actionswiper_target_container { + transition: transform 0.4s; +} +.o_actionswiper_swiping { + transition: none; +} +.o_actionswiper_right_swipe_area { + /*rtl:ignore*/ + transform: translateX(-100%); + inset: 0 auto auto 0; +} +.o_actionswiper_left_swipe_area { + /*rtl:ignore*/ + transform: translateX(100%); + inset: 0 0 auto auto; +} diff --git a/frontend/web/static/src/core/action_swiper/action_swiper.xml b/frontend/web/static/src/core/action_swiper/action_swiper.xml new file mode 100644 index 0000000..b2281aa --- /dev/null +++ b/frontend/web/static/src/core/action_swiper/action_swiper.xml @@ -0,0 +1,27 @@ + + + + +
+
+
+ + +
+ +
+
+ +
+ +
+
+
+
+
+
+ + + + +
diff --git a/frontend/web/static/src/core/anchor_scroll_prevention.js b/frontend/web/static/src/core/anchor_scroll_prevention.js new file mode 100644 index 0000000..81b99bc --- /dev/null +++ b/frontend/web/static/src/core/anchor_scroll_prevention.js @@ -0,0 +1,9 @@ +import { browser } from "./browser/browser"; + +browser.addEventListener("click", (ev) => { + const href = ev.target.closest("a")?.getAttribute("href"); + if (href && href === "#") { + ev.preventDefault(); // single hash in href are just a way to activate A-tags node + return; + } +}); diff --git a/frontend/web/static/src/core/assets.js b/frontend/web/static/src/core/assets.js new file mode 100644 index 0000000..5206828 --- /dev/null +++ b/frontend/web/static/src/core/assets.js @@ -0,0 +1,262 @@ +import { Component, onWillStart, whenReady, xml } from "@odoo/owl"; +import { session } from "@web/session"; +import { registry } from "./registry"; + +/** + * @typedef {{ + * cssLibs: string[]; + * jsLibs: string[]; + * }} BundleFileNames + */ + +export const globalBundleCache = new Map(); +export const assetCacheByDocument = new WeakMap(); + +function getGlobalBundleCache() { + return globalBundleCache; +} + +function getAssetCache(targetDoc) { + if (!assetCacheByDocument.has(targetDoc)) { + assetCacheByDocument.set(targetDoc, new Map()); + } + return assetCacheByDocument.get(targetDoc); +} + +export function computeBundleCacheMap(targetDoc) { + const cacheMap = getGlobalBundleCache(); + for (const script of targetDoc.head.querySelectorAll("script[src]")) { + cacheMap.set(script.getAttribute("src"), Promise.resolve()); + } + for (const link of targetDoc.head.querySelectorAll("link[rel=stylesheet][href]")) { + cacheMap.set(link.getAttribute("href"), Promise.resolve()); + } +} + +whenReady(() => computeBundleCacheMap(document)); + +/** + * @param {HTMLLinkElement | HTMLScriptElement} el + * @param {(event: Event) => any} onLoad + * @param {(error: Error) => any} onError + */ +const onLoadAndError = (el, onLoad, onError) => { + const onLoadListener = (event) => { + removeListeners(); + onLoad(event); + }; + + const onErrorListener = (error) => { + removeListeners(); + onError(error); + }; + + const removeListeners = () => { + el.removeEventListener("load", onLoadListener); + el.removeEventListener("error", onErrorListener); + }; + + el.addEventListener("load", onLoadListener); + el.addEventListener("error", onErrorListener); + + window.addEventListener("pagehide", () => { + removeListeners(); + }); +}; + +/** @type {typeof assets["getBundle"]} */ +export function getBundle() { + return assets.getBundle(...arguments); +} + +/** @type {typeof assets["loadBundle"]} */ +export function loadBundle() { + return assets.loadBundle(...arguments); +} + +/** @type {typeof assets["loadJS"]} */ +export function loadJS() { + return assets.loadJS(...arguments); +} + +/** @type {typeof assets["loadCSS"]} */ +export function loadCSS() { + return assets.loadCSS(...arguments); +} + +export class AssetsLoadingError extends Error {} + +/** + * Utility component that loads an asset bundle before instanciating a component + */ +export class LazyComponent extends Component { + static template = xml``; + static props = { + Component: String, + bundle: String, + props: { type: [Object, Function], optional: true }, + }; + setup() { + onWillStart(async () => { + await loadBundle(this.props.bundle); + this.Component = registry.category("lazy_components").get(this.props.Component); + }); + } + + get componentProps() { + return typeof this.props.props === "function" ? this.props.props() : this.props.props; + } +} + +/** + * This export is done only in order to modify the behavior of the exported + * functions. This is done in order to be able to make a test environment. + * Modules should only use the methods exported below. + */ +export const assets = { + retries: { + count: 3, + delay: 5000, + extraDelay: 2500, + }, + + /** + * Get the files information as descriptor object from a public asset template. + * + * @param {string} bundleName Name of the bundle containing the list of files + * @returns {Promise} + */ + getBundle(bundleName) { + const cacheMap = getGlobalBundleCache(); + if (cacheMap.has(bundleName)) { + return cacheMap.get(bundleName); + } + const url = new URL(`/web/bundle/${bundleName}`, location.origin); + for (const [key, value] of Object.entries(session.bundle_params || {})) { + url.searchParams.set(key, value); + } + const promise = fetch(url) + .then(async (response) => { + const cssLibs = []; + const jsLibs = []; + if (!response.bodyUsed) { + const result = await response.json(); + for (const { src, type } of Object.values(result)) { + if (type === "link" && src) { + cssLibs.push(src); + } else if (type === "script" && src) { + jsLibs.push(src); + } + } + } + return { cssLibs, jsLibs }; + }) + .catch((reason) => { + cacheMap.delete(bundleName); + throw new AssetsLoadingError(`The loading of ${url} failed`, { cause: reason }); + }); + cacheMap.set(bundleName, promise); + return promise; + }, + + /** + * Loads the given js/css libraries and asset bundles. Note that no library or + * asset will be loaded if it was already done before. + * + * @param {string} bundleName + * @param {Object} options + * @param {Document} [options.targetDoc=document] document to which the bundle will be applied (e.g. iframe document) + * @param {Boolean} [options.css=true] apply bundle css on targetDoc + * @param {Boolean} [options.js=true] apply bundle js on targetDoc + * @returns {Promise} + */ + loadBundle(bundleName, { targetDoc = document, css = true, js = true } = {}) { + if (typeof bundleName !== "string") { + throw new Error( + `loadBundle(bundleName:string) accepts only bundleName argument as a string ! Not ${JSON.stringify( + bundleName + )} as ${typeof bundleName}` + ); + } + return getBundle(bundleName).then(({ cssLibs, jsLibs }) => { + const promises = []; + if (css && cssLibs) { + promises.push(...cssLibs.map((url) => assets.loadCSS(url, { targetDoc }))); + } + if (js && jsLibs) { + promises.push(...jsLibs.map((url) => assets.loadJS(url, { targetDoc }))); + } + return Promise.all(promises); + }); + }, + + /** + * Loads the given url as a stylesheet. + * + * @param {string} url the url of the stylesheet + * @param {number} [retryCount] + * @param {Object} options + * @param {number} [retryCount] + * @param {Document} [options.targetDoc=document] document to which the bundle will be applied (e.g. iframe document) + * @returns {Promise} resolved when the stylesheet has been loaded + */ + loadCSS(url, { retryCount = 0, targetDoc = document } = {}) { + const cacheMap = getAssetCache(targetDoc); + if (cacheMap.has(url)) { + return cacheMap.get(url); + } + const linkEl = targetDoc.createElement("link"); + linkEl.setAttribute("href", url); + linkEl.type = "text/css"; + linkEl.rel = "stylesheet"; + const promise = new Promise((resolve, reject) => + onLoadAndError(linkEl, resolve, async (error) => { + cacheMap.delete(url); + if (retryCount < assets.retries.count) { + const delay = assets.retries.delay + assets.retries.extraDelay * retryCount; + await new Promise((res) => setTimeout(res, delay)); + linkEl.remove(); + loadCSS(url, { retryCount: retryCount + 1, targetDoc }) + .then(resolve) + .catch((reason) => { + cacheMap.delete(url); + reject(reason); + }); + } else { + reject( + new AssetsLoadingError(`The loading of ${url} failed`, { cause: error }) + ); + } + }) + ); + cacheMap.set(url, promise); + targetDoc.head.appendChild(linkEl); + return promise; + }, + + /** + * Loads the given url inside a script tag. + * + * @param {string} url the url of the script + * @param {Document} targetDoc document to which the bundle will be applied (e.g. iframe document) + * @returns {Promise} resolved when the script has been loaded + */ + loadJS(url, { targetDoc = document } = {}) { + const cacheMap = getAssetCache(targetDoc); + if (cacheMap.has(url)) { + return cacheMap.get(url); + } + const scriptEl = targetDoc.createElement("script"); + scriptEl.setAttribute("src", url); + scriptEl.type = url.includes("web/static/lib/pdfjs/") ? "module" : "text/javascript"; + const promise = new Promise((resolve, reject) => + onLoadAndError(scriptEl, resolve, (error) => { + cacheMap.delete(url); + reject(new AssetsLoadingError(`The loading of ${url} failed`, { cause: error })); + }) + ); + cacheMap.set(url, promise); + targetDoc.head.appendChild(scriptEl); + return promise; + }, +}; diff --git a/frontend/web/static/src/core/autocomplete/autocomplete.js b/frontend/web/static/src/core/autocomplete/autocomplete.js new file mode 100644 index 0000000..1dc6b0b --- /dev/null +++ b/frontend/web/static/src/core/autocomplete/autocomplete.js @@ -0,0 +1,501 @@ +import { Deferred } from "@web/core/utils/concurrency"; +import { useAutofocus, useForwardRefToParent, useService } from "@web/core/utils/hooks"; +import { isScrollableY, scrollTo } from "@web/core/utils/scrolling"; +import { useDebounced } from "@web/core/utils/timing"; +import { getActiveHotkey } from "@web/core/hotkeys/hotkey_service"; +import { usePosition } from "@web/core/position/position_hook"; +import { Component, onWillUpdateProps, useExternalListener, useRef, useState } from "@odoo/owl"; +import { mergeClasses } from "@web/core/utils/classname"; + +export class AutoComplete extends Component { + static template = "web.AutoComplete"; + static props = { + value: { type: String, optional: true }, + id: { type: String, optional: true }, + sources: { + type: Array, + element: { + type: Object, + shape: { + placeholder: { type: String, optional: true }, + options: [Array, Function], + optionSlot: { type: String, optional: true }, + }, + }, + }, + placeholder: { type: String, optional: true }, + title: { type: String, optional: true }, + autocomplete: { type: String, optional: true }, + autoSelect: { type: Boolean, optional: true }, + resetOnSelect: { type: Boolean, optional: true }, + onInput: { type: Function, optional: true }, + onCancel: { type: Function, optional: true }, + onChange: { type: Function, optional: true }, + onBlur: { type: Function, optional: true }, + onFocus: { type: Function, optional: true }, + searchOnInputClick: { type: Boolean, optional: true }, + input: { type: Function, optional: true }, + inputDebounceDelay: { type: Number, optional: true }, + dropdown: { type: Boolean, optional: true }, + autofocus: { type: Boolean, optional: true }, + class: { type: String, optional: true }, + slots: { type: Object, optional: true }, + menuPositionOptions: { type: Object, optional: true }, + menuCssClass: { type: [String, Array, Object], optional: true }, + selectOnBlur: { type: Boolean, optional: true }, + }; + static defaultProps = { + value: "", + placeholder: "", + title: "", + autocomplete: "new-password", + autoSelect: false, + dropdown: true, + onInput: () => {}, + onCancel: () => {}, + onChange: () => {}, + onBlur: () => {}, + onFocus: () => {}, + searchOnInputClick: true, + inputDebounceDelay: 250, + menuPositionOptions: {}, + menuCssClass: {}, + }; + + get timeout() { + return this.props.inputDebounceDelay; + } + + setup() { + this.nextSourceId = 0; + this.nextOptionId = 0; + this.sources = []; + this.inEdition = false; + this.mouseSelectionActive = false; + this.isOptionSelected = false; + + this.state = useState({ + navigationRev: 0, + optionsRev: 0, + open: false, + activeSourceOption: null, + value: this.props.value, + }); + + this.inputRef = useForwardRefToParent("input"); + this.listRef = useRef("sourcesList"); + if (this.props.autofocus) { + useAutofocus({ refName: "input" }); + } + this.root = useRef("root"); + + this.debouncedProcessInput = useDebounced(async () => { + const currentPromise = this.pendingPromise; + this.pendingPromise = null; + this.props.onInput({ + inputValue: this.inputRef.el.value, + }); + try { + await this.open(true); + currentPromise.resolve(); + } catch { + currentPromise.reject(); + } finally { + if (currentPromise === this.loadingPromise) { + this.loadingPromise = null; + } + } + }, this.timeout); + + useExternalListener(window, "scroll", this.externalClose, true); + useExternalListener(window, "pointerdown", this.externalClose, true); + useExternalListener(window, "mousemove", () => (this.mouseSelectionActive = true), true); + + this.hotkey = useService("hotkey"); + this.hotkeysToRemove = []; + + onWillUpdateProps((nextProps) => { + if (this.props.value !== nextProps.value || this.forceValFromProp) { + this.forceValFromProp = false; + if (!this.inEdition) { + this.state.value = nextProps.value; + this.inputRef.el.value = nextProps.value; + } + this.close(); + } + }); + + // position and size + if (this.props.dropdown) { + usePosition("sourcesList", () => this.targetDropdown, this.dropdownOptions); + } else { + this.open(false); + } + } + + get targetDropdown() { + return this.inputRef.el; + } + + get activeSourceOptionId() { + if (!this.isOpened || !this.state.activeSourceOption) { + return undefined; + } + const [sourceIndex, optionIndex] = this.state.activeSourceOption; + const source = this.sources[sourceIndex]; + return `${this.props.id || "autocomplete"}_${sourceIndex}_${ + source.isLoading ? "loading" : optionIndex + }`; + } + + get dropdownOptions() { + return { + position: "bottom-start", + ...this.props.menuPositionOptions, + }; + } + + get isOpened() { + return this.state.open; + } + + get hasOptions() { + for (const source of this.sources) { + if (source.isLoading || source.options.length) { + return true; + } + } + return false; + } + + get activeOption() { + if (!this.state.activeSourceOption) { + return null; + } + const [sourceIndex, optionIndex] = this.state.activeSourceOption; + return this.sources[sourceIndex].options[optionIndex]; + } + + open(useInput = false) { + this.state.open = true; + return this.loadSources(useInput); + } + + close() { + this.state.open = false; + this.state.activeSourceOption = null; + this.mouseSelectionActive = false; + } + + cancel() { + if (this.inputRef.el.value.length) { + if (this.props.autoSelect) { + this.inputRef.el.value = this.props.value; + this.props.onCancel(); + } + } + this.close(); + } + + async loadSources(useInput) { + this.sources = []; + this.state.activeSourceOption = null; + const proms = []; + for (const pSource of this.props.sources) { + const source = this.makeSource(pSource); + this.sources.push(source); + + const options = this.loadOptions( + pSource.options, + useInput ? this.inputRef.el.value.trim() : "" + ); + if (options instanceof Promise) { + source.isLoading = true; + const prom = options.then((options) => { + source.options = options.map((option) => this.makeOption(option)); + source.isLoading = false; + this.state.optionsRev++; + }); + proms.push(prom); + } else { + source.options = options.map((option) => this.makeOption(option)); + } + } + + await Promise.all(proms); + this.navigate(0); + this.scroll(); + } + get displayOptions() { + return !this.props.dropdown || (this.isOpened && this.hasOptions); + } + loadOptions(options, request) { + if (typeof options === "function") { + return options(request); + } else { + return options; + } + } + makeOption(option) { + return { + cssClass: "", + data: {}, + ...option, + id: ++this.nextOptionId, + unselectable: !option.onSelect, + }; + } + makeSource(source) { + return { + id: ++this.nextSourceId, + options: [], + isLoading: false, + placeholder: source.placeholder, + optionSlot: source.optionSlot, + }; + } + + isActiveSourceOption([sourceIndex, optionIndex]) { + return ( + this.state.activeSourceOption && + this.state.activeSourceOption[0] === sourceIndex && + this.state.activeSourceOption[1] === optionIndex + ); + } + + selectOption(option) { + this.inEdition = false; + if (option.unselectable) { + return; + } + + if (this.props.resetOnSelect) { + this.inputRef.el.value = ""; + } + this.isOptionSelected = true; + this.forceValFromProp = true; + option.onSelect(); + this.close(); + } + + navigate(direction) { + let step = Math.sign(direction); + if (!step) { + this.state.activeSourceOption = null; + step = 1; + } else { + this.state.navigationRev++; + } + + do { + if (this.state.activeSourceOption) { + let [sourceIndex, optionIndex] = this.state.activeSourceOption; + let source = this.sources[sourceIndex]; + + optionIndex += step; + if (0 > optionIndex || optionIndex >= source.options.length) { + sourceIndex += step; + source = this.sources[sourceIndex]; + + while (source && source.isLoading) { + sourceIndex += step; + source = this.sources[sourceIndex]; + } + + if (source) { + optionIndex = step < 0 ? source.options.length - 1 : 0; + } + } + + this.state.activeSourceOption = source ? [sourceIndex, optionIndex] : null; + } else { + let sourceIndex = step < 0 ? this.sources.length - 1 : 0; + let source = this.sources[sourceIndex]; + + while (source && source.isLoading) { + sourceIndex += step; + source = this.sources[sourceIndex]; + } + + if (source) { + const optionIndex = step < 0 ? source.options.length - 1 : 0; + if (optionIndex < source.options.length) { + this.state.activeSourceOption = [sourceIndex, optionIndex]; + } + } + } + } while (this.activeOption?.unselectable); + } + + onInputBlur() { + if (this.ignoreBlur) { + this.ignoreBlur = false; + return; + } + // If selectOnBlur is true, we select the first element + // of the autocomplete suggestions list, if this element exists + if (this.props.selectOnBlur && !this.isOptionSelected && this.sources[0]) { + const firstOption = this.sources[0].options[0]; + if (firstOption) { + this.state.activeSourceOption = firstOption.unselectable ? null : [0, 0]; + this.selectOption(this.activeOption); + } + } + this.props.onBlur({ + inputValue: this.inputRef.el.value, + }); + this.inEdition = false; + this.isOptionSelected = false; + } + onInputClick() { + if (!this.isOpened && this.props.searchOnInputClick) { + this.open(this.inputRef.el.value.trim() !== this.props.value.trim()); + } else { + this.close(); + } + } + onInputChange(ev) { + if (this.ignoreBlur) { + ev.stopImmediatePropagation(); + } + this.props.onChange({ + inputValue: this.inputRef.el.value, + isOptionSelected: this.ignoreBlur, + }); + } + async onInput() { + this.inEdition = true; + this.pendingPromise = this.pendingPromise || new Deferred(); + this.loadingPromise = this.pendingPromise; + this.debouncedProcessInput(); + } + + onInputFocus(ev) { + this.inputRef.el.setSelectionRange(0, this.inputRef.el.value.length); + this.props.onFocus(ev); + } + + get autoCompleteRootClass() { + let classList = ""; + if (this.props.class) { + classList += this.props.class; + } + if (this.props.dropdown) { + classList += " dropdown"; + } + return classList; + } + + get ulDropdownClass() { + return mergeClasses(this.props.menuCssClass, { + "dropdown-menu ui-autocomplete": this.props.dropdown, + "list-group": !this.props.dropdown, + }); + } + + async onInputKeydown(ev) { + const hotkey = getActiveHotkey(ev); + const isSelectKey = hotkey === "enter" || hotkey === "tab"; + + if (this.loadingPromise && isSelectKey) { + if (hotkey === "enter") { + ev.stopPropagation(); + ev.preventDefault(); + } + + await this.loadingPromise; + } + + switch (hotkey) { + case "enter": + if (!this.isOpened || !this.state.activeSourceOption) { + return; + } + this.selectOption(this.activeOption); + break; + case "escape": + if (!this.isOpened) { + return; + } + this.cancel(); + break; + case "tab": + case "shift+tab": + if (!this.isOpened) { + return; + } + if ( + this.props.autoSelect && + this.state.activeSourceOption && + (this.state.navigationRev > 0 || this.inputRef.el.value.length > 0) + ) { + this.selectOption(this.activeOption); + } + this.close(); + return; + case "arrowup": + this.navigate(-1); + if (!this.isOpened) { + this.open(true); + } + this.scroll(); + break; + case "arrowdown": + this.navigate(+1); + if (!this.isOpened) { + this.open(true); + } + this.scroll(); + break; + default: + return; + } + + ev.stopPropagation(); + ev.preventDefault(); + } + + onOptionMouseEnter(indices) { + if (!this.mouseSelectionActive) { + return; + } + + const [sourceIndex, optionIndex] = indices; + if (this.sources[sourceIndex].options[optionIndex]?.unselectable) { + this.state.activeSourceOption = null; + } else { + this.state.activeSourceOption = indices; + } + } + onOptionMouseLeave() { + this.state.activeSourceOption = null; + } + onOptionClick(option) { + this.selectOption(option); + this.inputRef.el.focus(); + } + onOptionPointerDown(option, ev) { + this.ignoreBlur = true; + if (option.unselectable) { + ev.preventDefault(); + } + } + + externalClose(ev) { + if (this.isOpened && !this.root.el.contains(ev.target)) { + this.cancel(); + } + } + + scroll() { + if (!this.activeSourceOptionId) { + return; + } + if (isScrollableY(this.listRef.el)) { + const element = this.listRef.el.querySelector(`#${this.activeSourceOptionId}`); + if (element) { + scrollTo(element); + } + } + } +} diff --git a/frontend/web/static/src/core/autocomplete/autocomplete.scss b/frontend/web/static/src/core/autocomplete/autocomplete.scss new file mode 100644 index 0000000..2dd7bbe --- /dev/null +++ b/frontend/web/static/src/core/autocomplete/autocomplete.scss @@ -0,0 +1,53 @@ + +.o-autocomplete { + .o-autocomplete--dropdown-menu { + // Needed because they are rendered at a lower stacking context compared to modals. + z-index: $zindex-modal + 1; + max-width: 600px; + } + .o-autocomplete--input { + width: 100%; + } + .o-autocomplete--mark { + padding: 0.1875em 0; + } + + .ui-menu-item { + > span { + --dropdown-link-hover-color: var(--dropdown-color); + --dropdown-link-hover-bg: var(--dropdown-bg); + } + + > a.ui-state-active { + margin: 0; + border: none; + font-weight: $font-weight-normal; + color: $dropdown-link-hover-color; + background-color: $dropdown-link-hover-bg; + } + + &.o_m2o_dropdown_option, &.o_m2o_start_typing, &.o_m2o_no_result { + text-indent: $o-dropdown-hpadding * .5; + } + + &.o_m2o_dropdown_option, &.o_calendar_dropdown_option { + > a { + color: $link-color; + &.ui-state-active:not(.o_m2o_start_typing) { + color: $link-hover-color; + } + } + } + + &.o_m2o_start_typing, &.o_m2o_no_result { + font-style: italic; + a.ui-menu-item-wrapper, a.ui-state-active, a.ui-state-active:hover { + background: none; + } + } + + &.o_m2o_start_typing > a.ui-state-active { + color: $dropdown-link-color; + } + } +} diff --git a/frontend/web/static/src/core/autocomplete/autocomplete.xml b/frontend/web/static/src/core/autocomplete/autocomplete.xml new file mode 100644 index 0000000..5d5a82b --- /dev/null +++ b/frontend/web/static/src/core/autocomplete/autocomplete.xml @@ -0,0 +1,88 @@ + + + + +
+ + + + +
+
+ +
diff --git a/frontend/web/static/src/core/avatar/avatar.scss b/frontend/web/static/src/core/avatar/avatar.scss new file mode 100644 index 0000000..df8b8b3 --- /dev/null +++ b/frontend/web/static/src/core/avatar/avatar.scss @@ -0,0 +1,13 @@ +// Avatar +.o_avatar img, +.o_avatar .o_avatar_empty, +img.o_avatar { + height: var(--Avatar-size, #{$o-avatar-size}); + aspect-ratio: 1; + object-fit: cover; +} + +.o_avatar_empty { + background: $o-black; + opacity: .1; +} \ No newline at end of file diff --git a/frontend/web/static/src/core/avatar/avatar.variables.scss b/frontend/web/static/src/core/avatar/avatar.variables.scss new file mode 100644 index 0000000..bed7155 --- /dev/null +++ b/frontend/web/static/src/core/avatar/avatar.variables.scss @@ -0,0 +1 @@ +$o-avatar-size: 1.7145em !default; diff --git a/frontend/web/static/src/core/badge/badge.scss b/frontend/web/static/src/core/badge/badge.scss new file mode 100644 index 0000000..adaf311 --- /dev/null +++ b/frontend/web/static/src/core/badge/badge.scss @@ -0,0 +1,8 @@ +.badge { + @for $size from 1 through length($o-colors) { + &.o_badge_color_#{$size - 1} { + background-color: adjust-color(nth($o-colors, $size), $lightness: 25%, $saturation: 15%) !important; + color: adjust-color(nth($o-colors, $size), $lightness: -40%, $saturation: -15%) !important; + } + } +} diff --git a/frontend/web/static/src/core/barcode/ZXingBarcodeDetector.js b/frontend/web/static/src/core/barcode/ZXingBarcodeDetector.js new file mode 100644 index 0000000..255d83d --- /dev/null +++ b/frontend/web/static/src/core/barcode/ZXingBarcodeDetector.js @@ -0,0 +1,153 @@ +/** + * Builder for BarcodeDetector-like polyfill class using ZXing library. + * + * @param {ZXing} ZXing Zxing library + * @returns {class} ZxingBarcodeDetector class + */ +export function buildZXingBarcodeDetector(ZXing) { + const ZXingFormats = new Map([ + ["aztec", ZXing.BarcodeFormat.AZTEC], + ["code_39", ZXing.BarcodeFormat.CODE_39], + ["code_128", ZXing.BarcodeFormat.CODE_128], + ["data_matrix", ZXing.BarcodeFormat.DATA_MATRIX], + ["ean_8", ZXing.BarcodeFormat.EAN_8], + ["ean_13", ZXing.BarcodeFormat.EAN_13], + ["itf", ZXing.BarcodeFormat.ITF], + ["pdf417", ZXing.BarcodeFormat.PDF_417], + ["qr_code", ZXing.BarcodeFormat.QR_CODE], + ["upc_a", ZXing.BarcodeFormat.UPC_A], + ["upc_e", ZXing.BarcodeFormat.UPC_E], + ]); + + const allSupportedFormats = Array.from(ZXingFormats.keys()); + + /** + * ZXingBarcodeDetector class + * + * BarcodeDetector-like polyfill class using ZXing library. + * API follows the Shape Detection Web API (specifically Barcode Detection). + */ + class ZXingBarcodeDetector { + /** + * @param {object} opts + * @param {Array} opts.formats list of codes' formats to detect + */ + constructor(opts = {}) { + const formats = opts.formats || allSupportedFormats; + const hints = new Map([ + [ + ZXing.DecodeHintType.POSSIBLE_FORMATS, + formats.map((format) => ZXingFormats.get(format)), + ], + // Enable Scanning at 90 degrees rotation + // https://github.com/zxing-js/library/issues/291 + [ZXing.DecodeHintType.TRY_HARDER, true], + ]); + this.reader = new ZXing.MultiFormatReader(); + this.reader.setHints(hints); + } + + /** + * Detect codes in image. + * + * @param {HTMLVideoElement} video source video element + * @returns {Promise} array of detected codes + */ + async detect(video) { + if (!(video instanceof HTMLVideoElement)) { + throw new DOMException( + "imageDataFrom() requires an HTMLVideoElement", + "InvalidArgumentError" + ); + } + if (!isVideoElementReady(video)) { + throw new DOMException("HTMLVideoElement is not ready", "InvalidStateError"); + } + const canvas = document.createElement("canvas"); + + let barcodeArea; + if (this.cropArea && (this.cropArea.x || this.cropArea.y)) { + barcodeArea = this.cropArea; + } else { + barcodeArea = { + x: 0, + y: 0, + width: video.videoWidth, + height: video.videoHeight, + }; + } + canvas.width = barcodeArea.width; + canvas.height = barcodeArea.height; + + const ctx = canvas.getContext("2d"); + + ctx.drawImage( + video, + barcodeArea.x, + barcodeArea.y, + barcodeArea.width, + barcodeArea.height, + 0, + 0, + barcodeArea.width, + barcodeArea.height + ); + + const luminanceSource = new ZXing.HTMLCanvasElementLuminanceSource(canvas); + const binaryBitmap = new ZXing.BinaryBitmap(new ZXing.HybridBinarizer(luminanceSource)); + try { + const result = this.reader.decodeWithState(binaryBitmap); + const { resultPoints } = result; + const boundingBox = DOMRectReadOnly.fromRect({ + x: resultPoints[0].x, + y: resultPoints[0].y, + height: Math.max(1, Math.abs(resultPoints[1].y - resultPoints[0].y)), + width: Math.max(1, Math.abs(resultPoints[1].x - resultPoints[0].x)), + }); + const cornerPoints = resultPoints; + const format = Array.from(ZXingFormats).find( + ([k, val]) => val === result.getBarcodeFormat() + ); + const rawValue = result.getText(); + return [ + { + boundingBox, + cornerPoints, + format, + rawValue, + }, + ]; + } catch (err) { + if (err.name === "NotFoundException") { + return []; + } + throw err; + } + } + + setCropArea(cropArea) { + this.cropArea = cropArea; + } + } + + /** + * Supported codes formats + * + * @static + * @returns {Promise} + */ + ZXingBarcodeDetector.getSupportedFormats = async () => allSupportedFormats; + + return ZXingBarcodeDetector; +} + +/** + * Check for HTMLVideoElement readiness. + * + * See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState + */ +const HAVE_NOTHING = 0; +const HAVE_METADATA = 1; +export function isVideoElementReady(video) { + return ![HAVE_NOTHING, HAVE_METADATA].includes(video.readyState); +} diff --git a/frontend/web/static/src/core/barcode/barcode_dialog.js b/frontend/web/static/src/core/barcode/barcode_dialog.js new file mode 100644 index 0000000..7797e4d --- /dev/null +++ b/frontend/web/static/src/core/barcode/barcode_dialog.js @@ -0,0 +1,60 @@ +import { _t } from "@web/core/l10n/translation"; +import { Dialog } from "@web/core/dialog/dialog"; +import { Component, useState } from "@odoo/owl"; +import { BarcodeVideoScanner, isBarcodeScannerSupported } from "./barcode_video_scanner"; + +export class BarcodeDialog extends Component { + static template = "web.BarcodeDialog"; + static components = { + BarcodeVideoScanner, + Dialog, + }; + static props = ["facingMode", "close", "onResult", "onError"]; + + setup() { + this.state = useState({ + barcodeScannerSupported: isBarcodeScannerSupported(), + errorMessage: _t("Check your browser permissions"), + }); + } + + /** + * Detection success handler + * + * @param {string} result found code + */ + onResult(result) { + this.props.close(); + this.props.onResult(result); + } + + /** + * Detection error handler + * + * @param {Error} error + */ + onError(error) { + this.state.barcodeScannerSupported = false; + this.state.errorMessage = error.message; + } +} + +/** + * Opens the BarcodeScanning dialog and begins code detection using the device's camera. + * + * @returns {Promise} resolves when a {qr,bar}code has been detected + */ +export async function scanBarcode(env, facingMode = "environment") { + let res; + let rej; + const promise = new Promise((resolve, reject) => { + res = resolve; + rej = reject; + }); + env.services.dialog.add(BarcodeDialog, { + facingMode, + onResult: (result) => res(result), + onError: (error) => rej(error), + }); + return promise; +} diff --git a/frontend/web/static/src/core/barcode/barcode_dialog.scss b/frontend/web/static/src/core/barcode/barcode_dialog.scss new file mode 100644 index 0000000..ce759f6 --- /dev/null +++ b/frontend/web/static/src/core/barcode/barcode_dialog.scss @@ -0,0 +1,10 @@ +.modal .o-barcode-modal .modal-body { + overflow: hidden; + @include media-breakpoint-down(md) { + padding: 0; + } + + video { + object-fit: cover; + } +} diff --git a/frontend/web/static/src/core/barcode/barcode_dialog.xml b/frontend/web/static/src/core/barcode/barcode_dialog.xml new file mode 100644 index 0000000..d4046ad --- /dev/null +++ b/frontend/web/static/src/core/barcode/barcode_dialog.xml @@ -0,0 +1,15 @@ + + + + + +
+ + Unable to access camera + +
+
+
+
diff --git a/frontend/web/static/src/core/barcode/barcode_video_scanner.js b/frontend/web/static/src/core/barcode/barcode_video_scanner.js new file mode 100644 index 0000000..06de182 --- /dev/null +++ b/frontend/web/static/src/core/barcode/barcode_video_scanner.js @@ -0,0 +1,234 @@ +/* global BarcodeDetector */ + +import { browser } from "@web/core/browser/browser"; +import { delay } from "@web/core/utils/concurrency"; +import { loadJS } from "@web/core/assets"; +import { isVideoElementReady, buildZXingBarcodeDetector } from "./ZXingBarcodeDetector"; +import { CropOverlay } from "./crop_overlay"; +import { Component, onMounted, onWillStart, onWillUnmount, status, useRef, useState } from "@odoo/owl"; +import { _t } from "@web/core/l10n/translation"; +import { pick } from "@web/core/utils/objects"; + +export class BarcodeVideoScanner extends Component { + static template = "web.BarcodeVideoScanner"; + static components = { + CropOverlay, + }; + static props = { + cssClass: { type: String, optional: true }, + facingMode: { + type: String, + validate: (fm) => ["environment", "left", "right", "user"].includes(fm), + }, + close: { type: Function, optional: true }, + onReady: { type: Function, optional: true }, + onResult: Function, + onError: Function, + placeholder: { type: String, optional: true }, + delayBetweenScan: { type: Number, optional: true }, + }; + static defaultProps = { + cssClass: "w-100 h-100", + }; + /** + * @override + */ + setup() { + this.videoPreviewRef = useRef("videoPreview"); + this.detectorTimeout = null; + this.stream = null; + this.detector = null; + this.overlayInfo = {}; + this.zoomRatio = 1; + this.scanPaused = false; + this.state = useState({ + isReady: false, + }); + + onWillStart(async () => { + let DetectorClass; + // Use Barcode Detection API if available. + // As support is still bleeding edge (mainly Chrome on Android), + // also provides a fallback using ZXing library. + if ("BarcodeDetector" in window) { + DetectorClass = BarcodeDetector; + } else { + await loadJS("/web/static/lib/zxing-library/zxing-library.js"); + DetectorClass = buildZXingBarcodeDetector(window.ZXing); + } + const formats = await DetectorClass.getSupportedFormats(); + this.detector = new DetectorClass({ formats }); + }); + + onMounted(async () => { + const constraints = { + video: { facingMode: this.props.facingMode }, + audio: false, + }; + + try { + this.stream = await browser.navigator.mediaDevices.getUserMedia(constraints); + } catch (err) { + const errors = { + NotFoundError: _t("No device can be found."), + NotAllowedError: _t("Odoo needs your authorization first."), + }; + const errorMessage = _t("Could not start scanning. %(message)s", { + message: errors[err.name] || err.message, + }); + this.props.onError(new Error(errorMessage)); + return; + } + if (!this.videoPreviewRef.el) { + this.cleanStreamAndTimeout(); + const errorMessage = _t("Barcode Video Scanner could not be mounted properly."); + this.props.onError(new Error(errorMessage)); + return; + } + this.videoPreviewRef.el.srcObject = this.stream; + const ready = await this.isVideoReady(); + if (!ready) { + return; + } + const { height, width } = getComputedStyle(this.videoPreviewRef.el); + const divWidth = width.slice(0, -2); + const divHeight = height.slice(0, -2); + const tracks = this.stream.getVideoTracks(); + if (tracks.length) { + const [track] = tracks; + const settings = track.getSettings(); + this.zoomRatio = Math.min(divWidth / settings.width, divHeight / settings.height); + this.addZoomSlider(track, settings); + } + this.detectorTimeout = setTimeout(this.detectCode.bind(this), 100); + }); + + onWillUnmount(() => this.cleanStreamAndTimeout()); + } + + cleanStreamAndTimeout() { + clearTimeout(this.detectorTimeout); + this.detectorTimeout = null; + if (this.stream) { + this.stream.getTracks().forEach((track) => track.stop()); + this.stream = null; + } + } + + isZXingBarcodeDetector() { + return this.detector && this.detector.__proto__.constructor.name === "ZXingBarcodeDetector"; + } + + /** + * Check for camera preview element readiness + * + * @returns {Promise} resolves when the video element is ready + */ + async isVideoReady() { + // FIXME: even if it shouldn't happened, a timeout could be useful here. + while (!isVideoElementReady(this.videoPreviewRef.el)) { + await delay(10); + if (status(this) === "destroyed"){ + return false; + } + } + this.state.isReady = true; + if (this.props.onReady) { + this.props.onReady(); + } + return true; + } + + onResize(overlayInfo) { + this.overlayInfo = overlayInfo; + if (this.isZXingBarcodeDetector()) { + // TODO need refactoring when ZXing will support multiple result in one scan + // https://github.com/zxing-js/library/issues/346 + this.detector.setCropArea(this.adaptValuesWithRatio(this.overlayInfo, true)); + } + } + + /** + * Attempt to detect codes in the current camera preview's frame + */ + async detectCode() { + let barcodeDetected = false; + let codes = []; + try { + codes = await this.detector.detect(this.videoPreviewRef.el); + } catch (err) { + this.props.onError(err); + } + for (const code of codes) { + if ( + !this.isZXingBarcodeDetector() && + this.overlayInfo.x !== undefined && + this.overlayInfo.y !== undefined + ) { + const { x, y, width, height } = this.adaptValuesWithRatio(code.boundingBox); + if ( + x < this.overlayInfo.x || + x + width > this.overlayInfo.x + this.overlayInfo.width || + y < this.overlayInfo.y || + y + height > this.overlayInfo.y + this.overlayInfo.height + ) { + continue; + } + } + barcodeDetected = true; + this.barcodeDetected(code.rawValue); + break; + } + if (this.stream && (!barcodeDetected || !this.props.delayBetweenScan)) { + this.detectorTimeout = setTimeout(this.detectCode.bind(this), 100); + } + } + + barcodeDetected(barcode) { + if (this.props.delayBetweenScan && !this.scanPaused) { + this.scanPaused = true; + this.detectorTimeout = setTimeout(() => { + this.scanPaused = false; + this.detectorTimeout = setTimeout(this.detectCode.bind(this), 100); + }, this.props.delayBetweenScan); + } + this.props.onResult(barcode); + } + + adaptValuesWithRatio(domRect, dividerRatio = false) { + const newObject = pick(domRect, "x", "y", "width", "height"); + for (const key of Object.keys(newObject)) { + if (dividerRatio) { + newObject[key] /= this.zoomRatio; + } else { + newObject[key] *= this.zoomRatio; + } + } + return newObject; + } + + addZoomSlider(track, settings) { + const zoom = track.getCapabilities().zoom; + if (zoom?.min !== undefined && zoom?.max !== undefined) { + const inputElement = document.createElement("input"); + inputElement.type = "range"; + inputElement.min = zoom.min; + inputElement.max = zoom.max; + inputElement.step = zoom.step || 1; + inputElement.value = settings.zoom; + inputElement.classList.add("align-self-end", "m-5", "z-1"); + inputElement.addEventListener("input", async (event) => { + await track?.applyConstraints({ advanced: [{ zoom: inputElement.value }] }); + }); + this.videoPreviewRef.el.parentElement.appendChild(inputElement); + } + } +} + +/** + * Check for BarcodeScanner support + * @returns {boolean} + */ +export function isBarcodeScannerSupported() { + return Boolean(browser.navigator.mediaDevices && browser.navigator.mediaDevices.getUserMedia); +} diff --git a/frontend/web/static/src/core/barcode/barcode_video_scanner.xml b/frontend/web/static/src/core/barcode/barcode_video_scanner.xml new file mode 100644 index 0000000..11d0f8d --- /dev/null +++ b/frontend/web/static/src/core/barcode/barcode_video_scanner.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/frontend/web/static/src/core/barcode/crop_overlay.js b/frontend/web/static/src/core/barcode/crop_overlay.js new file mode 100644 index 0000000..c76f143 --- /dev/null +++ b/frontend/web/static/src/core/barcode/crop_overlay.js @@ -0,0 +1,158 @@ +import { Component, useRef, onPatched } from "@odoo/owl"; +import { browser } from "@web/core/browser/browser"; +import { isIOS } from "@web/core/browser/feature_detection"; +import { clamp } from "@web/core/utils/numbers"; + +export class CropOverlay extends Component { + static template = "web.CropOverlay"; + static props = { + onResize: Function, + isReady: Boolean, + slots: { + type: Object, + shape: { + default: {}, + }, + }, + }; + + setup() { + this.localStorageKey = "o-barcode-scanner-overlay"; + this.cropContainerRef = useRef("crop-container"); + this.isMoving = false; + this.boundaryOverlay = {}; + this.relativePosition = { + x: 0, + y: 0, + }; + onPatched(() => { + this.setupCropRect(); + }); + this.isIOS = isIOS(); + } + + setupCropRect() { + if (!this.props.isReady) { + return; + } + this.computeDefaultPoint(); + this.computeOverlayPosition(); + this.calculateAndSetTransparentRect(); + this.executeOnResizeCallback(); + } + + boundPoint(pointValue, boundaryRect) { + return { + x: clamp(pointValue.x, boundaryRect.left, boundaryRect.left + boundaryRect.width), + y: clamp(pointValue.y, boundaryRect.top, boundaryRect.top + boundaryRect.height), + }; + } + + calculateAndSetTransparentRect() { + const cropTransparentRect = this.getTransparentRec( + this.relativePosition, + this.boundaryOverlay + ); + this.setCropValue(cropTransparentRect, this.relativePosition); + } + + computeOverlayPosition() { + const cropOverlayElement = this.cropContainerRef.el.querySelector(".o_crop_overlay"); + this.boundaryOverlay = cropOverlayElement.getBoundingClientRect(); + } + + executeOnResizeCallback() { + const transparentRec = this.getTransparentRec(this.relativePosition, this.boundaryOverlay); + browser.localStorage.setItem(this.localStorageKey, JSON.stringify(transparentRec)); + this.props.onResize({ + ...transparentRec, + width: this.boundaryOverlay.width - 2 * transparentRec.x, + height: this.boundaryOverlay.height - 2 * transparentRec.y, + }); + } + + computeDefaultPoint() { + const firstChildComputedStyle = getComputedStyle(this.cropContainerRef.el.firstChild); + const elementWidth = firstChildComputedStyle.width.slice(0, -2); + const elementHeight = firstChildComputedStyle.height.slice(0, -2); + + const stringSavedPoint = browser.localStorage.getItem(this.localStorageKey); + if (stringSavedPoint) { + const savedPoint = JSON.parse(stringSavedPoint); + this.relativePosition = { + x: clamp(savedPoint.x, 0, elementWidth), + y: clamp(savedPoint.y, 0, elementHeight), + }; + } else { + const stepWidth = elementWidth / 10; + const width = stepWidth * 8; + const height = width / 4; + const startY = elementHeight / 2 - height / 2; + this.relativePosition = { + x: stepWidth + width, + y: startY + height, + }; + } + } + getTransparentRec(point, rect) { + const middleX = rect.width / 2; + const middleY = rect.height / 2; + const newDeltaX = Math.abs(point.x - middleX); + const newDeltaY = Math.abs(point.y - middleY); + return { + x: middleX - newDeltaX, + y: middleY - newDeltaY, + }; + } + + setCropValue(point, iconPoint) { + if (!iconPoint) { + iconPoint = point; + } + this.cropContainerRef.el.style.setProperty("--o-crop-x", `${point.x}px`); + this.cropContainerRef.el.style.setProperty("--o-crop-y", `${point.y}px`); + this.cropContainerRef.el.style.setProperty("--o-crop-icon-x", `${iconPoint.x}px`); + this.cropContainerRef.el.style.setProperty("--o-crop-icon-y", `${iconPoint.y}px`); + } + + pointerDown(event) { + if (event.target.matches("input")) { + return; + } + event.preventDefault(); + if (event.target.matches(".o_crop_icon")) { + this.computeOverlayPosition(); + this.isMoving = true; + } + } + + pointerMove(event) { + if (!this.isMoving) { + return; + } + let eventPosition; + if (event.touches && event.touches.length) { + eventPosition = event.touches[0]; + } else { + eventPosition = event; + } + const { clientX, clientY } = eventPosition; + const restrictedPosition = this.boundPoint( + { + x: clientX, + y: clientY, + }, + this.boundaryOverlay + ); + this.relativePosition = { + x: restrictedPosition.x - this.boundaryOverlay.left, + y: restrictedPosition.y - this.boundaryOverlay.top, + }; + this.calculateAndSetTransparentRect(this.relativePosition); + } + + pointerUp(event) { + this.isMoving = false; + this.executeOnResizeCallback(); + } +} diff --git a/frontend/web/static/src/core/barcode/crop_overlay.scss b/frontend/web/static/src/core/barcode/crop_overlay.scss new file mode 100644 index 0000000..a6d83ef --- /dev/null +++ b/frontend/web/static/src/core/barcode/crop_overlay.scss @@ -0,0 +1,45 @@ +.o_crop_container { + position: relative; + + > * { + grid-row: 1 / -1; + grid-column: 1 / -1; + } + + .o_crop_overlay::after { + content: ''; + display: block; + } + + .o_crop_overlay:not(.o_crop_overlay_ios) { + background-color: RGB(0 0 0 / 0.75); + mix-blend-mode: darken; + + &::after { + height: 100%; + width: 100%; + clip-path: inset(var(--o-crop-y, 0px) var(--o-crop-x, 0px)); + background-color: white; + } + } + + .o_crop_overlay.o_crop_overlay_ios { + position: relative; + + &::after { + position: absolute; + inset: var(--o-crop-y, 0px) var(--o-crop-x, 0px); + border: 1px solid black; + } + } + + .o_crop_icon { + --o-crop-icon-width: 20px; + --o-crop-icon-height: 20px; + position: absolute; + width: var(--o-crop-icon-width); + height: var(--o-crop-icon-height); + left: calc(var(--o-crop-icon-x, 0px) - (var(--o-crop-icon-width) / 2)); + top: calc(var(--o-crop-icon-y, 0px) - (var(--o-crop-icon-height) / 2)); + } +} diff --git a/frontend/web/static/src/core/barcode/crop_overlay.xml b/frontend/web/static/src/core/barcode/crop_overlay.xml new file mode 100644 index 0000000..a3b3947 --- /dev/null +++ b/frontend/web/static/src/core/barcode/crop_overlay.xml @@ -0,0 +1,17 @@ + + + +
+ + +
+ + +
+
+ diff --git a/frontend/web/static/src/core/bottom_sheet/bottom_sheet.js b/frontend/web/static/src/core/bottom_sheet/bottom_sheet.js new file mode 100644 index 0000000..d9f34fe --- /dev/null +++ b/frontend/web/static/src/core/bottom_sheet/bottom_sheet.js @@ -0,0 +1,317 @@ +/** + * BottomSheet + * + * @class + */ +import { Component, useState, useRef, onMounted, useExternalListener } from "@odoo/owl"; +import { useHotkey } from "@web/core/hotkeys/hotkey_hook"; +import { useForwardRefToParent } from "@web/core/utils/hooks"; +import { useThrottleForAnimation } from "@web/core/utils/timing"; +import { compensateScrollbar } from "@web/core/utils/scrolling"; +import { getViewportDimensions, useViewportChange } from "@web/core/utils/dvu"; +import { clamp } from "@web/core/utils/numbers"; +import { browser } from "@web/core/browser/browser"; + +export class BottomSheet extends Component { + static template = "web.BottomSheet"; + + static defaultProps = { + class: "", + }; + + static props = { + // Main props + component: { type: Function }, + componentProps: { optional: true, type: Object }, + close: { type: Function }, + + class: { optional: true }, + role: { optional: true, type: String }, + + // Technical props + ref: { optional: true, type: Function }, + slots: { optional: true, type: Object }, + }; + + setup() { + this.maxHeightPercent = 90; + + this.state = useState({ + isPositionedReady: false, // Sheet is ready for display + isSnappingEnabled: false, + isDismissing: false, // Sheet is being dismissed + progress: 0, // Visual progress (0-1) + }); + + // Measurements and configuration + this.measurements = { + viewportHeight: 0, + naturalHeight: 0, + maxHeight: 0, + dismissThreshold: 0, + }; + + // Popover Ref Requirement + useForwardRefToParent("ref"); + + // References + this.containerRef = useRef("container"); + this.scrollRailRef = useRef("scrollRail"); + this.sheetRef = useRef("sheet"); + this.sheetBodyRef = useRef("ref"); + + // Create throttled version for onScroll + this.throttledOnScroll = useThrottleForAnimation(this.onScroll.bind(this)); + + // Adapt dimensions when mobile virtual-keyboards or browsers bars toggle + useViewportChange(() => { + if (this.state.isPositionedReady && !this.state.isDismissing) { + this.updateDimensions(); + } + }); + + // Handle "ESC" key press. + useHotkey("escape", () => this.slideOut()); + + // Handle mobile "back" gesture and "back" navigation button. + // Push a history state when the BottomSheet opens, intercept the browser's + // history events, prevents navigation by pushing another state and closes the sheet. + window.history.pushState({ bottomSheet: true }, ""); + this.handlePopState = () => { + if (this.state.isPositionedReady && !this.state.isDismissing) { + window.history.pushState({ bottomSheet: true }, ""); + this.slideOut(); + } + }; + useExternalListener(window, "popstate", this.handlePopState); + + onMounted(() => { + const isReduced = + browser.matchMedia(`(prefers-reduced-motion: reduce)`) === true || + browser.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true; + + this.prefersReducedMotion = + isReduced || getComputedStyle(this.containerRef.el).animationName === "none"; + + this.initializeSheet(); + compensateScrollbar(this.scrollRailRef.el, true, true, "padding-right"); + }); + } + + /** + * Main initialization method for the sheet + * Sets up measurements, snap points, and event handlers + */ + initializeSheet() { + if (!this.containerRef.el || !this.scrollRailRef.el || !this.sheetRef.el) { + return; + } + + // Step 1: Take measurements + this.measureDimensions(); + + // Step 2: Apply Dimensions + this.applyDimensions(); + + // Step 3: Set initial position + this.positionSheet(); + + // Step 4: Setup event handlers after everything has been properly resized and positioned + this.setupEventHandlers(); + + // Step 5: Mark as ready + this.state.isPositionedReady = true; + + if (this.prefersReducedMotion) { + this.state.isSnappingEnabled = true; + } else { + this.sheetRef.el?.addEventListener( + "animationend", + () => (this.state.isSnappingEnabled = true), + { + once: true, + } + ); + this.sheetRef.el?.addEventListener( + "animationcancel", + () => (this.state.isSnappingEnabled = true), + { + once: true, + } + ); + } + } + + /** + * Updates dimensions when viewport changes + * Recalculates measurements and snap points while preserving extended state + */ + updateDimensions() { + // Temporarily disable snapping during update + this.state.isSnappingEnabled = false; + + // Update measurements with new viewport dimensions + this.measureDimensions(); + this.applyDimensions(); + + // // Update scroll position + const scrollTop = this.scrollRailRef.el.scrollTop; + + // Update progress value + this.updateProgressValue(scrollTop); + } + + /** + * Takes measurements of viewport and sheet dimensions + * Calculates natural height and other key measurements + */ + measureDimensions() { + const viewportHeight = getViewportDimensions().height; + + // Calculate heights based on percentages + const maxHeightPx = (this.maxHeightPercent / 100) * viewportHeight; + + // Reset any previously set constraints to measure natural height + const sheet = this.sheetRef.el; + sheet.style.removeProperty("min-height"); + sheet.style.removeProperty("height"); + + const naturalHeight = sheet.offsetHeight; + const initialHeightPx = Math.min(naturalHeight, maxHeightPx); + + // Store all measurements + this.measurements = { + viewportHeight, + naturalHeight, + initialHeight: initialHeightPx, + maxHeight: maxHeightPx, + dismissThreshold: Math.min(initialHeightPx * 0.3, 100), + }; + } + + /** + * Applies calculated dimensions to the DOM elements + * Sets CSS variables and styles based on measurements and snap points + */ + applyDimensions() { + const rail = this.scrollRailRef.el; + + // Convert heights to dvh percentages for CSS variables + const heightPercent = Math.min( + (this.measurements.initialHeight / this.measurements.viewportHeight) * 100, + this.maxHeightPercent + ); + + // Set CSS variables for heights + rail.style.setProperty("--sheet-height", `${heightPercent}dvh`); + rail.style.setProperty("--sheet-max-height", `${this.measurements.viewportHeight}px`); + rail.style.setProperty("--dismiss-height", `${this.measurements.initialHeight || 0}px`); + } + + /** + * Sets the initial position of the sheet + * Configures initial scroll position and overflow behavior + */ + positionSheet() { + const scrollRail = this.scrollRailRef.el; + const bodyContent = this.sheetBodyRef.el; + + const scrollValue = this.measurements.maxHeight; + + // Configure body content overflow + if (bodyContent) { + bodyContent.style.overflowY = "auto"; + } + + // Set scroll position + scrollRail.scrollTop = scrollValue || 0; + scrollRail.style.containerType = "scroll-state size"; + } + + /** + * Sets up event handlers for scroll and touch events + */ + setupEventHandlers() { + const scrollRail = this.scrollRailRef.el; + + // Add scroll event listener + scrollRail.addEventListener("scroll", this.throttledOnScroll); + } + + /** + * Handles scroll events on the rail element + * Updates progress, handles position snapping, and triggers dismissal + */ + onScroll() { + if (!this.scrollRailRef.el) { + return; + } + + const scrollTop = this.scrollRailRef.el.scrollTop; + + // Update progress value for visual effects + this.updateProgressValue(scrollTop); + + // Check for dismissal condition + if (scrollTop < this.measurements.dismissThreshold) { + this.slideOut(); + } + } + + /** + * Calculates and updates the progress value based on scroll position + * + * @param {number} scrollTop - Current scroll position + */ + updateProgressValue(scrollTop) { + const initialPosition = this.measurements.naturalHeight; + const progress = clamp(scrollTop / initialPosition, 0, 1); + + if (Math.abs(this.state.progress - progress) > 0.01) { + this.state.progress = progress; + } + } + + /** + * Initiates the slide out animation and dismissal + */ + slideOut() { + // Prevent duplicate calls + if (this.state.isDismissing) { + return; + } + + if (this.prefersReducedMotion) { + this.props.close?.(); + } else { + this.sheetRef.el?.addEventListener("animationend", () => this.props.close?.(), { + once: true, + }); + this.sheetRef.el?.addEventListener("animationcancel", () => this.props.close?.(), { + once: true, + }); + } + + // Update state to trigger animation + this.state.isDismissing = true; + this.state.isSnappingEnabled = false; + } + + /** + * Closes the sheet (public API) + */ + close() { + this.slideOut(); + } + + /** + * Handles back button press (public API) + */ + back() { + if (this.props.onBack) { + this.props.onBack(); + } else { + this.slideOut(); + } + } +} diff --git a/frontend/web/static/src/core/bottom_sheet/bottom_sheet.scss b/frontend/web/static/src/core/bottom_sheet/bottom_sheet.scss new file mode 100644 index 0000000..36da516 --- /dev/null +++ b/frontend/web/static/src/core/bottom_sheet/bottom_sheet.scss @@ -0,0 +1,343 @@ +.o_bottom_sheet { + // ============================================= + // Layout and inner elements + // ============================================= + --BottomSheet-slideIn-duration: #{$o_BottomSheet_slideIn_duration}; + --BottomSheet-slideIn-easing: #{$o_BottomSheet_slideIn_easing}; + --BottomSheet-slideOut-duration: #{$o_BottomSheet_slideOut_duration}; + --BottomSheet-slideOut-easing: #{$o_BottomSheet_slideOut_easing}; + + --BottomSheet-Sheet-borderColor: #{$o_BottomSheet_Sheet_borderColor}; + + @mixin has-more-content-visual { + content: ""; + position: fixed; + inset: auto 0 0; + height: map-get($spacers, 4); + background: linear-gradient(transparent, #00000050); + z-index: $zindex-offcanvas; + pointer-events: none; + } + + position: fixed; + top: 0; + left: 0; + right: 0; + height: 100dvh; + z-index: $zindex-offcanvas; + opacity: 0; + transform-style: preserve-3d; + contain: layout paint size; + + // Workaround + animation-name: has-animation; + @media (prefers-reduced-motion: reduce) { + animation-name: none; + } + + // Main scroll container for gesture handling + .o_bottom_sheet_rail { + @include o-position-absolute(0, 0, 0, 0); + overflow-y: auto; + scrollbar-width: none; + touch-action: pan-y; + pointer-events: auto; + + &::-webkit-scrollbar { + display: none; + } + + &.o_bottom_sheet_rail_prevent_overscroll, + &.o_bottom_sheet_rail_prevent_overscroll * { + overscroll-behavior: contain; + } + + &::after { + @include has-more-content-visual; + opacity: 0; + transition: opacity var(--BottomSheet-slideIn-duration, 500ms); + } + } + + // Set snapping behaviors + .o_bottom_sheet_dismiss, .o_bottom_sheet_spacer, .o_bottom_sheet_sheet { + scroll-snap-align: start; + scroll-snap-stop: always; + } + + // Backdrop overlay + .o_bottom_sheet_backdrop { + position: fixed; + inset: 0; + background-color: rgba($modal-backdrop-bg, $modal-backdrop-opacity); + opacity: 0; + transition: all 0.2s ease; + pointer-events: auto; + touch-action: none; + z-index: $zindex-offcanvas - 1; + backdrop-filter: blur(0px) grayscale(0%); + @media (prefers-reduced-motion: reduce) { + transition: none; + } + } + + // Dismiss area + .o_bottom_sheet_dismiss { + height: var(--dismiss-height, 50dvh); + } + + // Spacer area + .o_bottom_sheet_spacer { + height: calc(100dvh - var(--sheet-height, 50dvh)); + pointer-events: none; + } + + // The actual sheet + .o_bottom_sheet_sheet { + --offcanvas-box-shadow: #{$box-shadow}; + + margin: 0 auto; + min-height: var(--sheet-height); + max-height: var(--sheet-max-height); + border-radius: $border-radius-xl $border-radius-xl 0 0; + border-bottom-width: 0; + visibility: visible; + transition: none; + contain: content; + backface-visibility: hidden; + perspective: 1000px; + user-select: none; + background-color: $dropdown-bg; + + .o_bottom_sheet_body { + scrollbar-width: none; + flex: 1; + } + } + + // ============================================= + // States + // ============================================= + @keyframes bottom-sheet-in { + from { transform: translateY(100%) translateZ(0); } + to { transform: translateY(0) translateZ(0); } + } + + @keyframes bottom-sheet-out { + from { transform: translateY(0) translateZ(0); } + to { transform: translateY(100%) translateZ(0); } + } + + // BottomSheet is ready to be rendered on screen + &.o_bottom_sheet_ready { + opacity: 1; + + .o_bottom_sheet_sheet { + animation: var(--BottomSheet-slideIn-duration, 500ms) bottom-sheet-in var(--BottomSheet-slideIn-easing, ease-out) forwards; + @media (prefers-reduced-motion: reduce) { + animation: none; + } + } + + .o_bottom_sheet_backdrop { + opacity: MAX(var(--BottomSheet-progress, 0), 0.2); + backdrop-filter: blur(.5px) grayscale(50%); + } + } + + // User interactions are now allowed + &.o_bottom_sheet_snapping .o_bottom_sheet_rail { + // Enable snap behavior + scroll-snap-type: y mandatory; + + .o_bottom_sheet_backdrop { + transition: none; + } + + // Provide a visual safenet in case of elastic + // overscroll (mostly iOS). + &:before { + position: fixed; + inset: auto auto 0 50%; + height: calc(var(--sheet-height) - #{$border-radius-xl * 2}); + width: calc(100% - #{$border-width * 2}); + max-width: map-get($grid-breakpoints, sm) - ($border-width * 2); + background: $offcanvas-bg-color; + z-index: $zindex-offcanvas; + transform: translateY(calc((1 - var(--BottomSheet-progress)) * 150%)) translateX(-50%); + content: ""; + } + + &::after { + @container scroll-state(scrollable: bottom) { + opacity: 1; + } + } + } + + // Dismissing the sheet + &.o_bottom_sheet_dismissing { + .o_bottom_sheet_sheet { + animation: var(--BottomSheet-slideOut-duration, 300ms) bottom-sheet-out var(--BottomSheet-slideOut-easing, ease-in) forwards; + @media (prefers-reduced-motion: reduce) { + animation: none; + } + } + + .o_bottom_sheet_backdrop { + opacity: 0; + backdrop-filter: blur(0) grayscale(0%); + transition: all var(--BottomSheet-slideOut-duration, 300ms) var(--BottomSheet-slideOut-easing, ease-in); + @media (prefers-reduced-motion: reduce) { + transition: none; + } + } + } + + // When bottom sheet is open, apply styles to the body + @at-root .bottom-sheet-open { + overflow: hidden; + + // Scale down the main content + .o_navbar, .o_action_manager { + transition: transform $o_BottomSheet_slideIn_duration ease; + transform: scale(.95) translateZ(0); + transform-origin: center top; + @media (prefers-reduced-motion: reduce) { + transition: none; + } + } + + // Avoid blank on the side + &:not(.o_home_menu_background) .o_main_navbar { + box-shadow: 20px 0 0 $o-navbar-background, -20px 0 0 $o-navbar-background; + } + + &:not(.bottom-sheet-open-multiple):has(.o_bottom_sheet_dismissing) { + .o_navbar, .o_action_manager { + transition: transform $o_BottomSheet_slideOut_duration ease; + transform: scale(1) translateZ(0); + @media (prefers-reduced-motion: reduce) { + transition: none; + } + } + } + } +} + +// ============================================= +// Inner components design +// ============================================= +.o_bottom_sheet .o_bottom_sheet_sheet { + --BottomSheet-Entry-paddingX: #{$list-group-item-padding-x}; + + %BottomSheet-Entry-active { + position: relative; + border: $border-width solid $list-group-active-border-color; + border-radius: $border-radius-lg; + color: var(--BottomSheetStatusBar__entry-color--active, #{color-contrast($component-active-bg)}); + + &:not(.focus) { + background: var(--BottomSheetStatusBar__entry-background--active, #{rgba($component-active-bg, .5)}); + } + + &::before { + content: none !important; + } + + &::after { + @include o-position-absolute(50%, $list-group-item-padding-x); + transform: translateY(-50%); + color: $o-action; + // .fa + text-rendering: auto; + font: normal normal normal 14px/1 FontAwesome; + // .fa-check + content: ""; + } + } + + // TreeEntry + --treeEntry-padding-v: 1.4rem; + + // Dropdown + .dropdown-divider { + --dropdown-divider-bg: #{$border-color}; + margin: map-get($spacers, 2) ($offcanvas-padding-x * .5); + } + + .dropdown-item, .dropdown-header { + --dropdown-item-padding-y: #{map-get($spacers, 3)}; + --dropdown-item-padding-x: var(--BottomSheet-Entry-paddingX); + --dropdown-header-padding-y: var(--dropdown-item-padding-y); + --dropdown-header-padding-x: var(--dropdown-item-padding-x); + + font-size: $h5-font-size; + font-weight: $o-font-weight-medium; + text-align: start !important; + } + + .dropdown-item .o_stat_value { + display: flex; + } + + .o_bottom_sheet_body:not(.o_custom_bottom_sheet) { + // Dropdown + .dropdown-item { + &.active, &.selected { + @extend %BottomSheet-Entry-active; + } + } + } + + .o_accordion_toggle { + &::after { + // Reset original style + border: unset; + transform: unset; + + @include o-position-absolute(var(--dropdown-item-padding-y), $list-group-item-padding-x); + padding-block: map-get($spacers, 2); + // .fa + text-rendering: auto; + font: normal normal normal 14px/1 FontAwesome; + // .fa-caret-down + content: "\f0d7"; + } + &.open::after { + // .fa-caret-up + content: "\f0d8"; + } + } + + .o_kanban_card_manage_settings:has(.o_colorlist) { + &, > div:last-child { + padding: 0; + } + } + .row.o_kanban_card_manage_settings:last-child { + &:has(:not(.o_field_boolean_favorite)) { + flex-direction: column-reverse; + + .o_field_kanban_color_picker { + padding: map-get($spacers, 3); + } + } + + div[class*="col-"] + div[class*="col-"] { + border-left: none; + } + + > div[class*="col-"] { + width: 100%; + padding: 0; + + a { + margin: 0; + padding: #{map-get($spacers, 3)} var(--BottomSheet-Entry-paddingX); + font-size: $h5-font-size; + font-weight: $o-font-weight-medium; + } + } + } +} diff --git a/frontend/web/static/src/core/bottom_sheet/bottom_sheet.variables.scss b/frontend/web/static/src/core/bottom_sheet/bottom_sheet.variables.scss new file mode 100644 index 0000000..4177e24 --- /dev/null +++ b/frontend/web/static/src/core/bottom_sheet/bottom_sheet.variables.scss @@ -0,0 +1,8 @@ + +$o_BottomSheet_Sheet_borderColor: transparent !default; + +$o_BottomSheet_slideIn_duration: 400ms !default; +$o_BottomSheet_slideIn_easing: $o-easing-enter !default; + +$o_BottomSheet_slideOut_duration: 200ms !default; +$o_BottomSheet_slideOut_easing: $o-easing-exit !default; diff --git a/frontend/web/static/src/core/bottom_sheet/bottom_sheet.xml b/frontend/web/static/src/core/bottom_sheet/bottom_sheet.xml new file mode 100644 index 0000000..75cde29 --- /dev/null +++ b/frontend/web/static/src/core/bottom_sheet/bottom_sheet.xml @@ -0,0 +1,70 @@ + + + +
+ +
+ +
+ + +
+ + +
+ + + +
+ + diff --git a/frontend/web/static/src/core/bottom_sheet/bottom_sheet_service.js b/frontend/web/static/src/core/bottom_sheet/bottom_sheet_service.js new file mode 100644 index 0000000..f472a20 --- /dev/null +++ b/frontend/web/static/src/core/bottom_sheet/bottom_sheet_service.js @@ -0,0 +1,71 @@ +import { markRaw } from "@odoo/owl"; +import { BottomSheet } from "@web/core/bottom_sheet/bottom_sheet"; +import { registry } from "@web/core/registry"; + +/** + * @typedef {{ + * env?: object; + * onClose?: () => void; + * class?: string; + * role?: string; + * ref?: Function; + * useBottomSheet?: Boolean; + * }} PopoverServiceAddOptions + * + * @typedef {ReturnType["add"]} PopoverServiceAddFunction + */ + +export const popoverService = { + dependencies: ["overlay"], + start(_, { overlay }) { + let bottomSheetCount = 0; + /** + * Signals the manager to add a popover. + * + * @param {HTMLElement} target + * @param {typeof import("@odoo/owl").Component} component + * @param {object} [props] + * @param {PopoverServiceAddOptions} [options] + * @returns {() => void} + */ + const add = (target, component, props = {}, options = {}) => { + function removeAndUpdateCount() { + _remove(); + bottomSheetCount--; + if (bottomSheetCount === 0) { + document.body.classList.remove("bottom-sheet-open"); + } else if (bottomSheetCount === 1) { + document.body.classList.remove("bottom-sheet-open-multiple"); + } + } + const _remove = overlay.add( + BottomSheet, + { + close: removeAndUpdateCount, + component, + componentProps: markRaw(props), + ref: options.ref, + class: options.class, + role: options.role, + }, + { + env: options.env, + onRemove: options.onClose, + rootId: target.getRootNode()?.host?.id, + } + ); + bottomSheetCount++; + if (bottomSheetCount === 1) { + document.body.classList.add("bottom-sheet-open"); + } else if (bottomSheetCount > 1) { + document.body.classList.add("bottom-sheet-open-multiple"); + } + + return removeAndUpdateCount; + }; + + return { add }; + }, +}; + +registry.category("services").add("bottom_sheet", popoverService); diff --git a/frontend/web/static/src/core/browser/browser.js b/frontend/web/static/src/core/browser/browser.js new file mode 100644 index 0000000..0726129 --- /dev/null +++ b/frontend/web/static/src/core/browser/browser.js @@ -0,0 +1,112 @@ +/** + * Browser + * + * This file exports an object containing common browser API. It may not look + * incredibly useful, but it is very convenient when one needs to test code using + * these methods. With this indirection, it is possible to patch the browser + * object for a test. + */ + +let sessionStorage; +let localStorage; +try { + sessionStorage = window.sessionStorage; + localStorage = window.localStorage; + // Safari crashes in Private Browsing + localStorage.setItem("__localStorage__", "true"); + localStorage.removeItem("__localStorage__"); +} catch { + localStorage = makeRAMLocalStorage(); + sessionStorage = makeRAMLocalStorage(); +} + +export const browser = { + addEventListener: window.addEventListener.bind(window), + dispatchEvent: window.dispatchEvent.bind(window), + AnalyserNode: window.AnalyserNode, + Audio: window.Audio, + AudioBufferSourceNode: window.AudioBufferSourceNode, + AudioContext: window.AudioContext, + AudioWorkletNode: window.AudioWorkletNode, + BeforeInstallPromptEvent: window.BeforeInstallPromptEvent?.bind(window), + GainNode: window.GainNode, + MediaStreamAudioSourceNode: window.MediaStreamAudioSourceNode, + removeEventListener: window.removeEventListener.bind(window), + setTimeout: window.setTimeout.bind(window), + clearTimeout: window.clearTimeout.bind(window), + setInterval: window.setInterval.bind(window), + clearInterval: window.clearInterval.bind(window), + performance: window.performance, + requestAnimationFrame: window.requestAnimationFrame.bind(window), + cancelAnimationFrame: window.cancelAnimationFrame.bind(window), + console: window.console, + history: window.history, + matchMedia: window.matchMedia.bind(window), + navigator, + Notification: window.Notification, + open: window.open.bind(window), + SharedWorker: window.SharedWorker, + Worker: window.Worker, + XMLHttpRequest: window.XMLHttpRequest, + localStorage, + sessionStorage, + fetch: window.fetch.bind(window), + innerHeight: window.innerHeight, + innerWidth: window.innerWidth, + ontouchstart: window.ontouchstart, + BroadcastChannel: window.BroadcastChannel, + visualViewport: window.visualViewport, +}; + +Object.defineProperty(browser, "location", { + set(val) { + window.location = val; + }, + get() { + return window.location; + }, + configurable: true, +}); + +Object.defineProperty(browser, "innerHeight", { + get: () => window.innerHeight, + configurable: true, +}); +Object.defineProperty(browser, "innerWidth", { + get: () => window.innerWidth, + configurable: true, +}); + +// ----------------------------------------------------------------------------- +// memory localStorage +// ----------------------------------------------------------------------------- + +/** + * @returns {typeof window["localStorage"]} + */ +export function makeRAMLocalStorage() { + let store = {}; + return { + setItem(key, value) { + const newValue = String(value); + store[key] = newValue; + window.dispatchEvent(new StorageEvent("storage", { key, newValue })); + }, + getItem(key) { + return store[key] ?? null; + }, + clear() { + store = {}; + }, + removeItem(key) { + delete store[key]; + window.dispatchEvent(new StorageEvent("storage", { key, newValue: null })); + }, + get length() { + return Object.keys(store).length; + }, + key() { + return ""; + }, + }; +} diff --git a/frontend/web/static/src/core/browser/cookie.js b/frontend/web/static/src/core/browser/cookie.js new file mode 100644 index 0000000..c9866a6 --- /dev/null +++ b/frontend/web/static/src/core/browser/cookie.js @@ -0,0 +1,37 @@ +/** + * Utils to make use of document.cookie + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies + * As recommended, storage should not be done by the cookie + * but with localStorage/sessionStorage + */ + +const COOKIE_TTL = 24 * 60 * 60 * 365; + +export const cookie = { + get _cookieMonster() { + return document.cookie; + }, + set _cookieMonster(value) { + document.cookie = value; + }, + get(str) { + const parts = this._cookieMonster.split("; "); + for (const part of parts) { + const [key, value] = part.split(/=(.*)/); + if (key === str) { + return value || ""; + } + } + }, + set(key, value, ttl = COOKIE_TTL) { + let fullCookie = []; + if (value !== undefined) { + fullCookie.push(`${key}=${value}`); + } + fullCookie = fullCookie.concat(["path=/", `max-age=${Math.floor(ttl)}`]); + this._cookieMonster = fullCookie.join("; "); + }, + delete(key) { + this.set(key, "kill", 0); + }, +}; diff --git a/frontend/web/static/src/core/browser/feature_detection.js b/frontend/web/static/src/core/browser/feature_detection.js new file mode 100644 index 0000000..59f7846 --- /dev/null +++ b/frontend/web/static/src/core/browser/feature_detection.js @@ -0,0 +1,83 @@ +import { browser } from "./browser"; + +// ----------------------------------------------------------------------------- +// Feature detection +// ----------------------------------------------------------------------------- + +/** + * True if the browser is based on Chromium (Google Chrome, Opera, Edge). + */ +export function isBrowserChrome() { + return /Chrome/i.test(browser.navigator.userAgent); +} + +export function isBrowserFirefox() { + return /Firefox/i.test(browser.navigator.userAgent); +} + +/** + * True if the browser is Microsoft Edge. + */ +export function isBrowserMicrosoftEdge() { + return /Edg/i.test(browser.navigator.userAgent); +} + +/** + * true if the browser is based on Safari (Safari, Epiphany) + * + * @returns {boolean} + */ +export function isBrowserSafari() { + return !isBrowserChrome() && browser.navigator.userAgent?.includes("Safari"); +} + +export function isAndroid() { + return /Android/i.test(browser.navigator.userAgent); +} + +export function isIOS() { + let isIOSPlatform = false; + if ("platform" in browser.navigator) { + isIOSPlatform = browser.navigator.platform === "MacIntel"; + } + return ( + /(iPad|iPhone|iPod)/i.test(browser.navigator.userAgent) || + (isIOSPlatform && maxTouchPoints() > 1) + ); +} + +export function isOtherMobileOS() { + return /(webOS|BlackBerry|Windows Phone)/i.test(browser.navigator.userAgent); +} + +export function isMacOS() { + return /Mac/i.test(browser.navigator.userAgent); +} + +export function isMobileOS() { + return isAndroid() || isIOS() || isOtherMobileOS(); +} + +export function isIosApp() { + return /OdooMobile \(iOS\)/i.test(browser.navigator.userAgent); +} + +export function isAndroidApp() { + return /OdooMobile.+Android/i.test(browser.navigator.userAgent); +} + +export function isDisplayStandalone() { + return browser.matchMedia("(display-mode: standalone)").matches; +} + +export function hasTouch() { + return browser.ontouchstart !== undefined || browser.matchMedia("(pointer:coarse)").matches; +} + +export function maxTouchPoints() { + return browser.navigator.maxTouchPoints || 1; +} + +export function isVirtualKeyboardSupported() { + return "virtualKeyboard" in browser.navigator; +} diff --git a/frontend/web/static/src/core/browser/router.js b/frontend/web/static/src/core/browser/router.js new file mode 100644 index 0000000..9a19b7c --- /dev/null +++ b/frontend/web/static/src/core/browser/router.js @@ -0,0 +1,423 @@ +import { EventBus } from "@odoo/owl"; +import { omit, pick } from "../utils/objects"; +import { compareUrls, objectToUrlEncodedString } from "../utils/urls"; +import { browser } from "./browser"; +import { isDisplayStandalone } from "@web/core/browser/feature_detection"; +import { slidingWindow } from "@web/core/utils/arrays"; +import { isNumeric } from "@web/core/utils/strings"; + +// Keys that are serialized in the URL as path segments instead of query string +export const PATH_KEYS = ["resId", "action", "active_id", "model"]; + +export const routerBus = new EventBus(); + +function isScopedApp() { + return browser.location.href.includes("/scoped_app") && isDisplayStandalone(); +} + +/** + * Casts the given string to a number if possible. + * + * @param {string} value + * @returns {string|number} + */ +function cast(value) { + return !value || isNaN(value) ? value : Number(value); +} + +/** + * @typedef {{ [key: string]: string }} Query + * @typedef {{ [key: string]: any }} Route + */ + +function parseString(str) { + const parts = str.split("&"); + const result = {}; + for (const part of parts) { + const [key, value] = part.split("="); + const decoded = decodeURIComponent(value || ""); + result[key] = cast(decoded); + } + return result; +} +/** + * @param {object} values An object with the values of the new state + * @param {boolean} replace whether the values should replace the state or be + * layered on top of the current state + * @returns {object} the next state of the router + */ +function computeNextState(values, replace) { + const nextState = replace ? pick(state, ..._lockedKeys) : { ...state }; + Object.assign(nextState, values); + // Update last entry in the actionStack + if (nextState.actionStack?.length) { + Object.assign(nextState.actionStack.at(-1), pick(nextState, ...PATH_KEYS)); + } + return sanitizeSearch(nextState); +} + +function sanitize(obj, valueToRemove) { + return Object.fromEntries( + Object.entries(obj) + .filter(([, v]) => v !== valueToRemove) + .map(([k, v]) => [k, cast(v)]) + ); +} + +function sanitizeSearch(search) { + return sanitize(search); +} + +function sanitizeHash(hash) { + return sanitize(hash, ""); +} + +/** + * @param {string} hash + * @returns {any} + */ +export function parseHash(hash) { + return hash && hash !== "#" ? parseString(hash.slice(1)) : {}; +} + +/** + * @param {string} search + * @returns {any} + */ +export function parseSearchQuery(search) { + return search ? parseString(search.slice(1)) : {}; +} + +function pathFromActionState(state) { + const path = []; + const { action, model, active_id, resId } = state; + if (active_id && typeof active_id === "number") { + path.push(active_id); + } + if (action) { + if (typeof action === "number" || action.includes(".")) { + path.push(`action-${action}`); + } else { + path.push(action); + } + } else if (model) { + if (model.includes(".")) { + path.push(model); + } else { + // A few models don't have a dot at all, we need to distinguish + // them from action paths (eg: website) + path.push(`m-${model}`); + } + } + if (resId && (typeof resId === "number" || resId === "new")) { + path.push(resId); + } + return path.join("/"); +} + +export function startUrl() { + return isScopedApp() ? "scoped_app" : "odoo"; +} + +/** + * @param {{ [key: string]: any }} state + * @returns + */ +function stateToUrl(state) { + let path = ""; + const pathKeysToOmit = [..._hiddenKeysFromUrl]; + const actionStack = (state.actionStack || [state]).map((a) => ({ ...a })); + if (actionStack.at(-1)?.action !== "menu") { + for (const [prevAct, currentAct] of slidingWindow(actionStack, 2).reverse()) { + const { action: prevAction, resId: prevResId, active_id: prevActiveId } = prevAct; + const { action: currentAction, active_id: currentActiveId } = currentAct; + // actions would typically map to a path like `active_id/action/res_id` + if (currentActiveId === prevResId) { + // avoid doubling up when the active_id is the same as the previous action's res_id + delete currentAct.active_id; + } + if (prevAction === currentAction && !prevResId && currentActiveId === prevActiveId) { + //avoid doubling up the action and the active_id when a single-record action is preceded by a multi-record action + delete currentAct.action; + delete currentAct.active_id; + } + } + const pathSegments = actionStack.map(pathFromActionState).filter(Boolean); + if (pathSegments.length) { + path = `/${pathSegments.join("/")}`; + } + } + if (state.active_id && typeof state.active_id !== "number") { + pathKeysToOmit.splice(pathKeysToOmit.indexOf("active_id"), 1); + } + if (state.resId && typeof state.resId !== "number" && state.resId !== "new") { + pathKeysToOmit.splice(pathKeysToOmit.indexOf("resId"), 1); + } + const search = objectToUrlEncodedString(omit(state, ...pathKeysToOmit)); + const start_url = startUrl(); + return `/${start_url}${path}${search ? `?${search}` : ""}`; +} + +function urlToState(urlObj) { + const { pathname, hash, search } = urlObj; + const state = parseSearchQuery(search); + + // ** url-retrocompatibility ** + // If the url contains a hash, it can be for two motives: + // 1. It is an anchor link, in that case, we ignore it, as it will not have a keys/values format + // the sanitizeHash function will remove it from the hash object. + // 2. It has one or more keys/values, in that case, we merge it with the search. + if (pathname === "/web") { + const sanitizedHash = sanitizeHash(parseHash(hash)); + // Old urls used "id", it is now resId for clarity. Remap to the new name. + if (sanitizedHash.id) { + sanitizedHash.resId = sanitizedHash.id; + delete sanitizedHash.id; + delete sanitizedHash.view_type; + } else if (sanitizedHash.view_type === "form") { + sanitizedHash.resId = "new"; + delete sanitizedHash.view_type; + } + Object.assign(state, sanitizedHash); + const url = browser.location.origin + router.stateToUrl(state); + urlObj.href = url; + } + + const [prefix, ...splitPath] = urlObj.pathname.split("/").filter(Boolean); + + if (["odoo", "scoped_app"].includes(prefix)) { + const actionParts = [...splitPath.entries()].filter( + ([_, part]) => !isNumeric(part) && part !== "new" + ); + const actions = []; + for (const [i, part] of actionParts) { + const action = {}; + const [left, right] = [splitPath[i - 1], splitPath[i + 1]]; + if (isNumeric(left)) { + action.active_id = parseInt(left); + } + + if (right === "new") { + action.resId = "new"; + } else if (isNumeric(right)) { + action.resId = parseInt(right); + } + + if (part.startsWith("action-")) { + // numeric id or xml_id + const actionId = part.slice(7); + action.action = isNumeric(actionId) ? parseInt(actionId) : actionId; + } else if (part.startsWith("m-")) { + action.model = part.slice(2); + } else if (part.includes(".")) { + action.model = part; + } else { + // action tag or path + action.action = part; + } + + if (action.resId && action.action) { + actions.push(omit(action, "resId")); + } + // Don't create actions for models without resId unless they're the last one. + // If the last one is a model but doesn't have a view_type, the action service will not mount it anyway. + if (action.action || action.resId || i === splitPath.length - 1) { + actions.push(action); + } + } + const activeAction = actions.at(-1); + if (activeAction) { + Object.assign(state, activeAction); + state.actionStack = actions; + } + if (prefix === "scoped_app" && !isDisplayStandalone()) { + // make sure /scoped_app are redirected to /odoo when using the browser instead of the PWA + const url = browser.location.origin + router.stateToUrl(state); + urlObj.href = url; + } + } + return state; +} + +let state; +let pushTimeout; +let pushArgs; +let _lockedKeys; +let _hiddenKeysFromUrl = new Set(); + +export function startRouter() { + const url = new URL(browser.location); + state = router.urlToState(url); + // ** url-retrocompatibility ** + if (browser.location.pathname === "/web") { + // Change the url of the current history entry to the canonical url. + // This change should be done only at the first load, and not when clicking on old style internal urls. + // Or when clicking back/forward on the browser. + browser.history.replaceState(browser.history.state, null, url.href); + } + pushTimeout = null; + pushArgs = { + replace: false, + reload: false, + state: {}, + }; + _lockedKeys = new Set(["debug", "lang"]); + _hiddenKeysFromUrl = new Set([...PATH_KEYS, "actionStack"]); +} + +/** + * When the user navigates history using the back/forward button, the browser + * dispatches a popstate event with the state that was in the history for the + * corresponding history entry. We just adopt that state so that the webclient + * can use that previous state without forcing a full page reload. + */ +browser.addEventListener("popstate", (ev) => { + browser.clearTimeout(pushTimeout); + if (!ev.state) { + // We are coming from a click on an anchor. + // Add the current state to the history entry so that a future loadstate behaves as expected. + browser.history.replaceState({ nextState: state }, "", browser.location.href); + return; + } + state = ev.state?.nextState || router.urlToState(new URL(browser.location)); + // Some client actions want to handle loading their own state. This is a ugly hack to allow not + // reloading the webclient's state when they manipulate history. + if (!ev.state?.skipRouteChange && !router.skipLoad) { + routerBus.trigger("ROUTE_CHANGE"); + } + router.skipLoad = false; +}); + +/** + * When the user navigates the history using the back/forward button, some browsers (Safari iOS and + * Safari MacOS) can restore the page using the `bfcache` (especially when we come back from an + * external website). Unfortunately, Odoo wasn't designed to be compatible with this cache, which + * leads to inconsistencies. When the `bfcache` is used to restore a page, we reload the current + * page, to be sure that all the elements have been rendered correctly. + */ +browser.addEventListener("pageshow", (ev) => { + if (ev.persisted) { + browser.clearTimeout(pushTimeout); + routerBus.trigger("ROUTE_CHANGE"); + } +}); + +/** + * When clicking internal links, do a loadState instead of a full page reload. + * This also alows the mobile app to not open an in-app browser for them. + */ +browser.addEventListener("click", (ev) => { + if (ev.defaultPrevented || ev.target.closest("[contenteditable]")) { + return; + } + const a = ev.target.closest("a"); + const href = a?.getAttribute("href"); + if (href && !href.startsWith("#")) { + let url; + try { + // ev.target.href is the full url including current path + url = new URL(a.href); + } catch { + return; + } + if ( + browser.location.host === url.host && + browser.location.pathname.startsWith("/odoo") && + (["/web", "/odoo"].includes(url.pathname) || url.pathname.startsWith("/odoo/")) && + a.target !== "_blank" + ) { + ev.preventDefault(); + state = router.urlToState(url); + if (url.pathname.startsWith("/odoo") && url.hash) { + browser.history.pushState({}, "", url.href); + } + new Promise((res) => setTimeout(res, 0)).then(() => routerBus.trigger("ROUTE_CHANGE")); + } + } +}); + +/** + * @param {string} mode + */ +function makeDebouncedPush(mode) { + function doPush() { + // Calculates new route based on aggregated search and options + const nextState = computeNextState(pushArgs.state, pushArgs.replace); + const url = browser.location.origin + router.stateToUrl(nextState); + if (!compareUrls(url + browser.location.hash, browser.location.href)) { + // If the route changed: pushes or replaces browser state + if (mode === "push") { + // Because doPush is delayed, the history entry will have the wrong name. + // We set the document title to what it was at the time of the pushState + // call, then push, which generates the history entry with the right title + // then restore the title to what it's supposed to be + const originalTitle = document.title; + document.title = pushArgs.title; + browser.history.pushState({ nextState }, "", url); + document.title = originalTitle; + } else { + browser.history.replaceState({ nextState }, "", url); + } + } else { + // URL didn't change but state might have, update it in place + browser.history.replaceState({ nextState }, "", browser.location.href); + } + state = nextState; + if (pushArgs.reload) { + browser.location.reload(); + } + } + /** + * @param {object} state + * @param {object} options + */ + return function pushOrReplaceState(state, options = {}) { + pushArgs.replace ||= options.replace; + pushArgs.reload ||= options.reload; + pushArgs.title = document.title; + Object.assign(pushArgs.state, state); + browser.clearTimeout(pushTimeout); + const push = () => { + doPush(); + pushTimeout = null; + pushArgs = { + replace: false, + reload: false, + state: {}, + }; + }; + if (options.sync) { + push(); + } else { + pushTimeout = browser.setTimeout(() => { + push(); + }); + } + }; +} + +export const router = { + get current() { + return state; + }, + // state <-> url conversions can be patched if needed in a custom webclient. + stateToUrl, + urlToState, + // TODO: stop debouncing these and remove the ugly hack to have the correct title for history entries + pushState: makeDebouncedPush("push"), + replaceState: makeDebouncedPush("replace"), + cancelPushes: () => browser.clearTimeout(pushTimeout), + addLockedKey: (key) => _lockedKeys.add(key), + hideKeyFromUrl: (key) => _hiddenKeysFromUrl.add(key), + skipLoad: false, +}; + +startRouter(); + +export function objectToQuery(obj) { + const query = {}; + Object.entries(obj).forEach(([k, v]) => { + query[k] = v ? String(v) : v; + }); + return query; +} diff --git a/frontend/web/static/src/core/browser/title_service.js b/frontend/web/static/src/core/browser/title_service.js new file mode 100644 index 0000000..3dbe69d --- /dev/null +++ b/frontend/web/static/src/core/browser/title_service.js @@ -0,0 +1,60 @@ +import { registry } from "../registry"; + +export const titleService = { + start() { + const titleCounters = {}; + const titleParts = {}; + + function getParts() { + return Object.assign({}, titleParts); + } + + function setCounters(counters) { + for (const key in counters) { + const val = counters[key]; + if (!val) { + delete titleCounters[key]; + } else { + titleCounters[key] = val; + } + } + updateTitle(); + } + + function setParts(parts) { + for (const key in parts) { + const val = parts[key]; + if (!val) { + delete titleParts[key]; + } else { + titleParts[key] = val; + } + } + updateTitle(); + } + + function updateTitle() { + const counter = Object.values(titleCounters).reduce((acc, count) => acc + count, 0); + const name = Object.values(titleParts).join(" - ") || "Odoo"; + if (!counter) { + document.title = name; + } else { + document.title = `(${counter}) ${name}`; + } + } + + return { + /** + * @returns {string} + */ + get current() { + return document.title; + }, + getParts, + setCounters, + setParts, + }; + }, +}; + +registry.category("services").add("title", titleService); diff --git a/frontend/web/static/src/core/checkbox/checkbox.js b/frontend/web/static/src/core/checkbox/checkbox.js new file mode 100644 index 0000000..d9d3284 --- /dev/null +++ b/frontend/web/static/src/core/checkbox/checkbox.js @@ -0,0 +1,98 @@ +import { useHotkey } from "../hotkeys/hotkey_hook"; + +import { Component, useRef } from "@odoo/owl"; + +/** + * Custom checkbox + * + * + * Change the label text + * + * + * @extends Component + */ + +export class CheckBox extends Component { + static template = "web.CheckBox"; + static nextId = 1; + static defaultProps = { + onChange: () => {}, + }; + static props = { + id: { + type: true, + optional: true, + }, + disabled: { + type: Boolean, + optional: true, + }, + value: { + type: Boolean, + optional: true, + }, + slots: { + type: Object, + optional: true, + }, + onChange: { + type: Function, + optional: true, + }, + className: { + type: String, + optional: true, + }, + name: { + type: String, + optional: true, + }, + indeterminate: { + type: Boolean, + optional: true, + }, + }; + + setup() { + this.id = `checkbox-comp-${CheckBox.nextId++}`; + this.rootRef = useRef("root"); + + // Make it toggleable through the Enter hotkey + // when the focus is inside the root element + useHotkey( + "Enter", + ({ area }) => { + const oldValue = area.querySelector("input").checked; + this.props.onChange(!oldValue); + }, + { area: () => this.rootRef.el, bypassEditableProtection: true } + ); + } + + onClick(ev) { + if (ev.composedPath().find((el) => ["INPUT", "LABEL"].includes(el.tagName))) { + // The onChange will handle these cases. + ev.stopPropagation(); + return; + } + + // Reproduce the click event behavior as if it comes from the input element. + const input = this.rootRef.el.querySelector("input"); + input.focus(); + if (!this.props.disabled) { + ev.stopPropagation(); + input.checked = !input.checked; + this.props.onChange(input.checked); + } + } + + onChange(ev) { + if (!this.props.disabled) { + this.props.onChange(ev.target.checked); + } + } +} diff --git a/frontend/web/static/src/core/checkbox/checkbox.scss b/frontend/web/static/src/core/checkbox/checkbox.scss new file mode 100644 index 0000000..5be66ef --- /dev/null +++ b/frontend/web/static/src/core/checkbox/checkbox.scss @@ -0,0 +1,3 @@ +.o-checkbox { + width: fit-content; +} diff --git a/frontend/web/static/src/core/checkbox/checkbox.xml b/frontend/web/static/src/core/checkbox/checkbox.xml new file mode 100644 index 0000000..ba3bd59 --- /dev/null +++ b/frontend/web/static/src/core/checkbox/checkbox.xml @@ -0,0 +1,22 @@ + + + + +
+ + +
+
+ +
diff --git a/frontend/web/static/src/core/code_editor/code_editor.js b/frontend/web/static/src/core/code_editor/code_editor.js new file mode 100644 index 0000000..fff60df --- /dev/null +++ b/frontend/web/static/src/core/code_editor/code_editor.js @@ -0,0 +1,180 @@ +import { Component, onMounted, onWillStart, useEffect, useRef, useState, status } from "@odoo/owl"; +import { loadBundle } from "@web/core/assets"; + +export class CodeEditor extends Component { + static template = "web.CodeEditor"; + static components = {}; + static props = { + mode: { + type: String, + optional: true, + validate: (mode) => CodeEditor.MODES.includes(mode), + }, + value: { validate: (v) => typeof v === "string", optional: true }, + readonly: { type: Boolean, optional: true }, + onChange: { type: Function, optional: true }, + onBlur: { type: Function, optional: true }, + class: { type: String, optional: true }, + theme: { + type: String, + optional: true, + validate: (theme) => CodeEditor.THEMES.includes(theme), + }, + maxLines: { type: Number, optional: true }, + sessionId: { type: [Number, String], optional: true }, + initialCursorPosition: { type: Object, optional: true }, + showLineNumbers: { type: Boolean, optional: true }, + }; + static defaultProps = { + readonly: false, + value: "", + onChange: () => {}, + class: "", + theme: "", + sessionId: 1, + showLineNumbers: true, + }; + + static MODES = ["javascript", "xml", "qweb", "scss", "python"]; + static THEMES = ["", "monokai"]; + + setup() { + this.editorRef = useRef("editorRef"); + this.state = useState({ + activeMode: undefined, + }); + + onWillStart(async () => await loadBundle("web.ace_lib")); + + const sessions = {}; + // The ace library triggers the "change" event even if the change is + // programmatic. Even worse, it triggers 2 "change" events in that case, + // one with the empty string, and one with the new value. We only want + // to notify the parent of changes done by the user, in the UI, so we + // use this flag to filter out noisy "change" events. + let ignoredAceChange = false; + useEffect( + (el) => { + if (!el) { + return; + } + + // keep in closure + const aceEditor = window.ace.edit(el); + this.aceEditor = aceEditor; + + this.aceEditor.setOptions({ + maxLines: this.props.maxLines, + showPrintMargin: false, + useWorker: false, + }); + this.aceEditor.$blockScrolling = true; + + this.aceEditor.on("changeMode", () => { + this.state.activeMode = this.aceEditor.getSession().$modeId.split("/").at(-1); + }); + + const session = aceEditor.getSession(); + if (!sessions[this.props.sessionId]) { + sessions[this.props.sessionId] = session; + } + session.setValue(this.props.value); + session.on("change", () => { + if (this.props.onChange && !ignoredAceChange) { + this.props.onChange( + this.aceEditor.getValue(), + this.aceEditor.getCursorPosition() + ); + } + }); + this.aceEditor.on("blur", () => { + if (this.props.onBlur) { + this.props.onBlur(); + } + }); + + return () => { + aceEditor.destroy(); + }; + }, + () => [this.editorRef.el] + ); + + useEffect( + (theme) => this.aceEditor.setTheme(theme ? `ace/theme/${theme}` : ""), + () => [this.props.theme] + ); + + useEffect( + (readonly, showLineNumbers) => { + this.aceEditor.setOptions({ + readOnly: readonly, + highlightActiveLine: !readonly, + highlightGutterLine: !readonly, + }); + + this.aceEditor.renderer.setOptions({ + displayIndentGuides: !readonly, + showGutter: !readonly && showLineNumbers, + }); + + this.aceEditor.renderer.$cursorLayer.element.style.display = readonly + ? "none" + : "block"; + }, + () => [this.props.readonly, this.props.showLineNumbers] + ); + + useEffect( + (sessionId, mode, value) => { + let session = sessions[sessionId]; + if (session) { + if (session.getValue() !== value) { + ignoredAceChange = true; + session.setValue(value); + ignoredAceChange = false; + } + } else { + session = new window.ace.EditSession(value); + session.setUndoManager(new window.ace.UndoManager()); + session.setOptions({ + useWorker: false, + tabSize: 2, + useSoftTabs: true, + }); + session.on("change", () => { + if (this.props.onChange && !ignoredAceChange) { + this.props.onChange( + this.aceEditor.getValue(), + this.aceEditor.getCursorPosition() + ); + } + }); + sessions[sessionId] = session; + } + session.setMode(mode ? `ace/mode/${mode}` : ""); + this.aceEditor.setSession(session); + }, + () => [this.props.sessionId, this.props.mode, this.props.value] + ); + + const initialCursorPosition = this.props.initialCursorPosition; + if (initialCursorPosition) { + onMounted(() => { + // Wait for ace to be fully operational + window.requestAnimationFrame(() => { + if (status(this) != "destroyed" && this.aceEditor) { + this.aceEditor.focus(); + const { row, column } = initialCursorPosition; + const pos = { + row: row || 0, + column: column || 0, + }; + this.aceEditor.selection.moveToPosition(pos); + this.aceEditor.renderer.scrollCursorIntoView(pos, 0.5); + } + }); + }); + } + } +} diff --git a/frontend/web/static/src/core/code_editor/code_editor.xml b/frontend/web/static/src/core/code_editor/code_editor.xml new file mode 100644 index 0000000..f9e0285 --- /dev/null +++ b/frontend/web/static/src/core/code_editor/code_editor.xml @@ -0,0 +1,8 @@ + + + + +
+ + + diff --git a/frontend/web/static/src/core/color_picker/color_picker.js b/frontend/web/static/src/core/color_picker/color_picker.js new file mode 100644 index 0000000..9fd25ae --- /dev/null +++ b/frontend/web/static/src/core/color_picker/color_picker.js @@ -0,0 +1,362 @@ +import { Component, useEffect, useRef, useState } from "@odoo/owl"; +import { CustomColorPicker } from "@web/core/color_picker/custom_color_picker/custom_color_picker"; +import { usePopover } from "@web/core/popover/popover_hook"; +import { isCSSColor, isColorGradient, normalizeCSSColor } from "@web/core/utils/colors"; +import { cookie } from "@web/core/browser/cookie"; +import { POSITION_BUS } from "../position/position_hook"; +import { registry } from "../registry"; + +// These colors are already normalized as per normalizeCSSColor in @web/legacy/js/widgets/colorpicker +export const DEFAULT_COLORS = [ + ["#000000", "#424242", "#636363", "#9C9C94", "#CEC6CE", "#EFEFEF", "#F7F7F7", "#FFFFFF"], + ["#FF0000", "#FF9C00", "#FFFF00", "#00FF00", "#00FFFF", "#0000FF", "#9C00FF", "#FF00FF"], + ["#F7C6CE", "#FFE7CE", "#FFEFC6", "#D6EFD6", "#CEDEE7", "#CEE7F7", "#D6D6E7", "#E7D6DE"], + ["#E79C9C", "#FFC69C", "#FFE79C", "#B5D6A5", "#A5C6CE", "#9CC6EF", "#B5A5D6", "#D6A5BD"], + ["#E76363", "#F7AD6B", "#FFD663", "#94BD7B", "#73A5AD", "#6BADDE", "#8C7BC6", "#C67BA5"], + ["#CE0000", "#E79439", "#EFC631", "#6BA54A", "#4A7B8C", "#3984C6", "#634AA5", "#A54A7B"], + ["#9C0000", "#B56308", "#BD9400", "#397B21", "#104A5A", "#085294", "#311873", "#731842"], + ["#630000", "#7B3900", "#846300", "#295218", "#083139", "#003163", "#21104A", "#4A1031"], +]; + +export const DEFAULT_GRAYSCALES = { + solid: ["black", "900", "800", "600", "400", "200", "100", "white"], +}; + +// These CSS variables are defined in html_editor. +// Using ColorPicker without html_editor installed is extremely unlikely. +export const DEFAULT_THEME_COLOR_VARS = [ + "o-color-1", + "o-color-2", + "o-color-3", + "o-color-4", + "o-color-5", +]; + +export class ColorPicker extends Component { + static template = "web.ColorPicker"; + static components = { CustomColorPicker }; + static props = { + state: { + type: Object, + shape: { + selectedColor: String, + selectedColorCombination: { type: String, optional: true }, + getTargetedElements: { type: Function, optional: true }, + defaultTab: String, + selectedTab: { type: String, optional: true }, + // todo: remove the `mode` prop in master + mode: { type: String, optional: true }, + }, + }, + getUsedCustomColors: Function, + applyColor: Function, + applyColorPreview: Function, + applyColorResetPreview: Function, + editColorCombination: { type: Function, optional: true }, + setOnCloseCallback: { type: Function, optional: true }, + setOperationCallbacks: { type: Function, optional: true }, + enabledTabs: { type: Array, optional: true }, + colorPrefix: { type: String }, + cssVarColorPrefix: { type: String, optional: true }, + defaultOpacity: { type: Number, optional: true }, + grayscales: { type: Object, optional: true }, + noTransparency: { type: Boolean, optional: true }, + close: { type: Function, optional: true }, + className: { type: String, optional: true }, + useDefaultThemeColors: { type: Boolean, optional: true }, + }; + static defaultProps = { + close: () => {}, + defaultOpacity: 100, + enabledTabs: ["solid", "custom"], + cssVarColorPrefix: "", + setOnCloseCallback: () => {}, + useDefaultThemeColors: true, + }; + + setup() { + this.tabs = registry + .category("color_picker_tabs") + .getAll() + .filter((tab) => this.props.enabledTabs.includes(tab.id)); + this.root = useRef("root"); + + this.DEFAULT_COLORS = DEFAULT_COLORS; + this.grayscales = Object.assign({}, DEFAULT_GRAYSCALES, this.props.grayscales); + this.DEFAULT_THEME_COLOR_VARS = this.props.useDefaultThemeColors + ? DEFAULT_THEME_COLOR_VARS + : []; + this.defaultColorSet = this.getDefaultColorSet(); + this.defaultColor = this.props.state.selectedColor; + this.focusedBtn = null; + this.onApplyCallback = () => {}; + this.onPreviewRevertCallback = () => {}; + this.getPreviewColor = () => {}; + + this.state = useState({ + activeTab: this.props.state.selectedTab || this.getDefaultTab(), + currentCustomColor: this.props.state.selectedColor, + currentColorPreview: undefined, + showGradientPicker: false, + }); + this.usedCustomColors = this.props.getUsedCustomColors(); + useEffect( + () => { + // Recompute the positioning of the popover if any. + this.env[POSITION_BUS]?.trigger("update"); + }, + () => [this.state.activeTab] + ); + } + + getDefaultTab() { + if (this.props.enabledTabs.includes(this.props.state.defaultTab)) { + return this.props.state.defaultTab; + } + return this.props.enabledTabs[0]; + } + + get selectedColor() { + return this.props.state.selectedColor; + } + + get isDarkTheme() { + return cookie.get("color_scheme") === "dark"; + } + + setTab(tab) { + this.state.activeTab = tab; + // Reset the preview revert callback, as it is tab-specific. + this.setOperationCallbacks({ onPreviewRevertCallback: () => {} }); + this.applyColorResetPreview(); + } + + processColorFromEvent(ev) { + const target = this.getTarget(ev); + let color = target.dataset.color || ""; + if (color && isColorCombination(color)) { + return color; + } + if (color && !isCSSColor(color) && !isColorGradient(color)) { + color = this.props.colorPrefix + color; + } + return color; + } + /** + * @param {Object} cbs - callbacks + * @param {Function} cbs.onApplyCallback + * @param {Function} cbs.onPreviewRevertCallback + */ + setOperationCallbacks(cbs) { + // The gradient colorpicker has a nested ColorPicker. We need to use the + // `setOperationCallbacks` from the parent ColorPicker for it to be + // impacted. + if (this.props.setOperationCallbacks) { + this.props.setOperationCallbacks(cbs); + } + if (cbs.onApplyCallback) { + this.onApplyCallback = cbs.onApplyCallback; + } + if (cbs.onPreviewRevertCallback) { + this.onPreviewRevertCallback = cbs.onPreviewRevertCallback; + } + if (cbs.getPreviewColor) { + this.getPreviewColor = cbs.getPreviewColor; + } + } + + applyColor(color) { + this.state.currentCustomColor = color; + this.props.applyColor(color); + this.defaultColorSet = this.getDefaultColorSet(); + this.onApplyCallback(); + } + + onColorApply(ev) { + if (!this.isColorButton(this.getTarget(ev))) { + return; + } + const color = this.processColorFromEvent(ev); + this.applyColor(color); + this.props.close(); + } + + applyColorResetPreview() { + this.props.applyColorResetPreview(); + this.state.currentColorPreview = undefined; + this.onPreviewRevertCallback(); + } + + onColorPreview(ev) { + const color = ev.hex || ev.gradient || this.processColorFromEvent(ev); + this.props.applyColorPreview(color); + this.state.currentColorPreview = this.getPreviewColor(); + } + + onColorHover(ev) { + if (!this.isColorButton(this.getTarget(ev))) { + return; + } + this.onColorPreview(ev); + } + + onColorHoverOut(ev) { + if (!this.isColorButton(this.getTarget(ev))) { + return; + } + this.applyColorResetPreview(); + } + getTarget(ev) { + const target = ev.target.closest(`[data-color]`); + return this.root.el.contains(target) ? target : ev.target; + } + + onColorFocusin(ev) { + // In the editor color picker, the preview and reset reapply the + // selection, which can remove the focus from the current button (if the + // node is recreated). We need to force the focus and break the infinite + // loop that it could trigger. + if (this.focusedBtn === ev.target) { + this.focusedBtn = null; + return; + } + this.focusedBtn = ev.target; + this.onColorHover(ev); + if (document.activeElement !== ev.target) { + // The focus was lost during revert. Reset it where it should be. + ev.target.focus(); + } + } + + onColorFocusout(ev) { + if (!ev.relatedTarget || !this.isColorButton(ev.relatedTarget)) { + // Do not trigger a revert if we are in the focus loop (i.e. focus + // a button > selection is reset > focusout). Otherwise, the + // relatedTarget should always be one of the colorpicker's buttons. + return; + } + const activeEl = document.activeElement; + this.applyColorResetPreview(); + if (document.activeElement !== activeEl) { + // The focus was lost during revert. Reset it where it should be. + ev.relatedTarget.focus(); + } + } + + getDefaultColorSet() { + if (!this.props.state.selectedColor) { + return; + } + let defaultColors = this.props.enabledTabs.includes("solid") + ? this.DEFAULT_THEME_COLOR_VARS + : []; + for (const grayscale of Object.values(this.grayscales)) { + defaultColors = defaultColors.concat(grayscale); + } + + const targetedElement = + this.props.state.getTargetedElements?.()[0] || document.documentElement; + const selectedColor = this.props.state.selectedColor.toUpperCase(); + const htmlStyle = + targetedElement.ownerDocument.defaultView.getComputedStyle(targetedElement); + + for (const color of defaultColors) { + const cssVar = normalizeCSSColor(htmlStyle.getPropertyValue(`--${color}`)); + if (cssVar?.toUpperCase() === selectedColor) { + return color; + } + } + + return false; + } + + colorPickerNavigation(ev) { + const { target, key } = ev; + if (!target.classList.contains("o_color_button")) { + return; + } + if (!["ArrowRight", "ArrowLeft", "ArrowUp", "ArrowDown"].includes(key)) { + return; + } + + let targetBtn; + if (key === "ArrowRight") { + targetBtn = target.nextElementSibling; + } else if (key === "ArrowLeft") { + targetBtn = target.previousElementSibling; + } else if (key === "ArrowUp" || key === "ArrowDown") { + const buttonIndex = [...target.parentElement.children].indexOf(target); + const nbColumns = getComputedStyle(target).getPropertyValue( + "--o-color-picker-grid-columns" + ); + targetBtn = + target.parentElement.children[ + buttonIndex + (key === "ArrowUp" ? -1 : 1) * nbColumns + ]; + if (!targetBtn) { + const row = + key === "ArrowUp" + ? target.parentElement.previousElementSibling + : target.parentElement.nextElementSibling; + if (row?.matches(".o_color_section, .o_colorpicker_section")) { + targetBtn = row.children[buttonIndex]; + } + } + } + if (targetBtn && targetBtn.classList.contains("o_color_button")) { + targetBtn.focus(); + } + } + + isColorButton(targetEl) { + return targetEl.tagName === "BUTTON" && !targetEl.matches(".o_colorpicker_ignore"); + } +} + +export function useColorPicker(refName, props, options = {}) { + // Callback to be overridden by child components (e.g. custom color picker). + let onCloseCallback = () => {}; + const setOnCloseCallback = (cb) => { + onCloseCallback = cb; + }; + props.setOnCloseCallback = setOnCloseCallback; + if (options.onClose) { + const onClose = options.onClose; + options.onClose = () => { + onCloseCallback(); + onClose(); + }; + } + + const colorPicker = usePopover(ColorPicker, options); + const root = useRef(refName); + + function onClick() { + colorPicker.isOpen ? colorPicker.close() : colorPicker.open(root.el, props); + } + + useEffect( + (el) => { + if (!el) { + return; + } + el.addEventListener("click", onClick); + return () => { + el.removeEventListener("click", onClick); + }; + }, + () => [root.el] + ); + + return colorPicker; +} + +/** + * Checks if a given string is a color combination. + * + * @param {string} color + * @returns {boolean} + */ +function isColorCombination(color) { + return color.startsWith("o_cc"); +} diff --git a/frontend/web/static/src/core/color_picker/color_picker.scss b/frontend/web/static/src/core/color_picker/color_picker.scss new file mode 100644 index 0000000..2c3dabb --- /dev/null +++ b/frontend/web/static/src/core/color_picker/color_picker.scss @@ -0,0 +1,94 @@ +$o-we-toolbar-bg: #FFF !default; +$o-we-toolbar-color-text: #2b2b33 !default; // Same as $o-we-bg-light +$o-we-item-spacing: 8px !default; +$o-we-color-success: #00ff9e !default; + +.o_font_color_selector { + @include o-input-number-no-arrows(); + --bg: #{$o-we-toolbar-bg}; + --text-rgb: #{red($o-we-toolbar-color-text)}, #{green($o-we-toolbar-color-text)}, #{blue($o-we-toolbar-color-text)}; + --border-rgb: var(--text-rgb); + width: 208px; + max-height: inherit; + overflow-y: auto; + border-radius: inherit; + background-color: inherit; + box-shadow: $box-shadow; + &::-webkit-scrollbar { + display: none; + } +} + +.o_color_button { + width: 23px; + height: 22px; + box-shadow: inset 0 0 0 1px rgba(var(--border-rgb), .5); + margin: 0.5px; + + &:focus, + &:hover { + transform: scale(1.1); + } +} + +.o_color_picker_button { + @extend %o-preview-alpha-background; + + &:not(.selected):focus, + &:not(.selected):hover { + outline: solid $o-enterprise-action-color; + z-index: 1; + transition: transform 0.1s ease-out; + } +} + +.o_font_color_selector { + .btn-tab { + min-width: 57px; + padding: 3px; + font-size: 12px; + } + + .o_color_picker_button.selected { + border: 3px solid $o-enterprise-action-color !important; + } +} + +.o_font_color_selector .o_colorpicker_section { + margin-bottom: 3px; +} + +.o_font_color_selector { + --o-color-picker-grid-columns: 8; + .o_colorpicker_section, .o_color_section { + display: grid; + grid-template-columns: repeat(var(--o-color-picker-grid-columns), 1fr); + } +} + +.o_font_color_selector .o_colorpicker_widget { + width: 100%; + margin-top: 2px; + .o_hex_input { + border: 1px solid !important; + padding: 0 2px !important; + width: 10ch !important; + opacity: 0.7; + } +} + +:root { + @each $color, $value in $grays { + @include print-variable($color, $value); + } +} + +.color-combination-button.selected h1 { + &::before { + content: "\f00c"; + margin-right: $o-we-item-spacing; + font-size: 0.8em; + font-family: FontAwesome; + color: $o-we-color-success; + } +} diff --git a/frontend/web/static/src/core/color_picker/color_picker.xml b/frontend/web/static/src/core/color_picker/color_picker.xml new file mode 100644 index 0000000..b9d431e --- /dev/null +++ b/frontend/web/static/src/core/color_picker/color_picker.xml @@ -0,0 +1,51 @@ + + +
+
+ + + +
+
+ + +
+ + + diff --git a/frontend/web/static/src/core/color_picker/custom_color_picker/custom_color_picker.js b/frontend/web/static/src/core/color_picker/custom_color_picker/custom_color_picker.js new file mode 100644 index 0000000..2e741ec --- /dev/null +++ b/frontend/web/static/src/core/color_picker/custom_color_picker/custom_color_picker.js @@ -0,0 +1,689 @@ +import { getActiveHotkey } from "@web/core/hotkeys/hotkey_service"; +import { _t } from "@web/core/l10n/translation"; +import { + convertCSSColorToRgba, + convertHslToRgb, + convertRgbaToCSSColor, + convertRgbToHsl, + normalizeCSSColor, +} from "@web/core/utils/colors"; +import { uniqueId } from "@web/core/utils/functions"; +import { clamp } from "@web/core/utils/numbers"; +import { debounce, useThrottleForAnimation } from "@web/core/utils/timing"; + +import { Component, onMounted, onWillUpdateProps, useExternalListener, useRef } from "@odoo/owl"; + +const ARROW_KEYS = ["arrowup", "arrowdown", "arrowleft", "arrowright"]; +const SLIDER_KEYS = [...ARROW_KEYS, "pageup", "pagedown", "home", "end"]; + +const DEFAULT_COLOR = "#FF0000"; + +export class CustomColorPicker extends Component { + static template = "web.CustomColorPicker"; + static props = { + document: { type: true, optional: true }, + defaultColor: { type: String, optional: true }, + selectedColor: { type: String, optional: true }, + noTransparency: { type: Boolean, optional: true }, + stopClickPropagation: { type: Boolean, optional: true }, + onColorSelect: { type: Function, optional: true }, + onColorPreview: { type: Function, optional: true }, + onInputEnter: { type: Function, optional: true }, + defaultOpacity: { type: Number, optional: true }, + setOnCloseCallback: { type: Function, optional: true }, + setOperationCallbacks: { type: Function, optional: true }, + }; + static defaultProps = { + document: window.document, + defaultColor: DEFAULT_COLOR, + defaultOpacity: 100, + noTransparency: false, + stopClickPropagation: false, + onColorSelect: () => {}, + onColorPreview: () => {}, + onInputEnter: () => {}, + }; + + setup() { + this.pickerFlag = false; + this.sliderFlag = false; + this.opacitySliderFlag = false; + if (this.props.defaultOpacity > 0 && this.props.defaultOpacity <= 1) { + this.props.defaultOpacity *= 100; + } + if (this.props.defaultColor.length <= 7) { + const opacityHex = Math.round((this.props.defaultOpacity / 100) * 255) + .toString(16) + .padStart(2, "0"); + this.props.defaultColor += opacityHex; + } + this.colorComponents = {}; + this.uniqueId = uniqueId("colorpicker"); + this.selectedHexValue = ""; + this.shouldSetSelectedColor = false; + this.lastFocusedSliderEl = undefined; + if (!this.props.selectedColor) { + this.props.selectedColor = this.props.defaultColor; + } + this.debouncedOnChangeInputs = debounce(this.onChangeInputs.bind(this), 10, true); + + this.elRef = useRef("el"); + this.colorPickerAreaRef = useRef("colorPickerArea"); + this.colorPickerPointerRef = useRef("colorPickerPointer"); + this.colorSliderRef = useRef("colorSlider"); + this.colorSliderPointerRef = useRef("colorSliderPointer"); + this.opacitySliderRef = useRef("opacitySlider"); + this.opacitySliderPointerRef = useRef("opacitySliderPointer"); + + // Need to be bound on all documents to work in all possible cases (we + // have to be able to start dragging/moving from the colorpicker to + // anywhere on the screen, crossing iframes). + const documents = [ + window.top, + ...Array.from(window.top.frames).filter((frame) => { + try { + const document = frame.document; + return !!document; + } catch { + // We cannot access the document (cross origin). + return false; + } + }), + ].map((w) => w.document); + this.throttleOnPointerMove = useThrottleForAnimation((ev) => { + this.onPointerMovePicker(ev); + this.onPointerMoveSlider(ev); + this.onPointerMoveOpacitySlider(ev); + }); + + for (const doc of documents) { + useExternalListener(doc, "pointermove", this.throttleOnPointerMove); + useExternalListener(doc, "pointerup", this.onPointerUp.bind(this)); + useExternalListener(doc, "keydown", this.onEscapeKeydown.bind(this), { capture: true }); + } + // Apply the previewed custom color when the popover is closed. + this.props.setOnCloseCallback?.(() => { + if (this.shouldSetSelectedColor) { + this._colorSelected(); + } + }); + this.props.setOperationCallbacks?.({ + getPreviewColor: () => { + if (this.shouldSetSelectedColor) { + return this.colorComponents.hex; + } + }, + onApplyCallback: () => { + this.shouldSetSelectedColor = false; + }, + // Reapply the current custom color preview after reverting a preview. + // Typical usecase: 1) modify the custom color, 2) hover one of the + // black-white tints, 3) hover out. + onPreviewRevertCallback: () => { + if (this.previewActive && this.shouldSetSelectedColor) { + this.props.onColorPreview(this.colorComponents); + } + }, + }); + onMounted(async () => { + const rgba = + convertCSSColorToRgba(this.props.selectedColor) || + convertCSSColorToRgba(this.props.defaultColor); + if (rgba) { + this._updateRgba(rgba.red, rgba.green, rgba.blue, rgba.opacity); + } + + this.previewActive = true; + this._updateUI(); + }); + onWillUpdateProps((newProps) => { + const newSelectedColor = newProps.selectedColor + ? newProps.selectedColor + : newProps.defaultColor; + if (normalizeCSSColor(newSelectedColor) !== this.colorComponents.cssColor) { + this.setSelectedColor(newSelectedColor); + } + }); + } + + /** + * Sets the currently selected color + * + * @param {string} color rgb[a] + */ + setSelectedColor(color) { + const rgba = convertCSSColorToRgba(color); + if (rgba) { + const oldPreviewActive = this.previewActive; + this.previewActive = false; + this._updateRgba(rgba.red, rgba.green, rgba.blue, rgba.opacity); + this.previewActive = oldPreviewActive; + this._updateUI(); + } + } + /** + * @param {string[]} allowedKeys + * @returns {string[]} allowed keys + modifiers + */ + getAllowedHotkeys(allowedKeys) { + return allowedKeys.flatMap((key) => [key, `control+${key}`]); + } + /** + * @param {HTMLElement} el + */ + setLastFocusedSliderEl(el) { + this.lastFocusedSliderEl = el; + document.activeElement.blur(); + } + + get el() { + return this.elRef.el; + } + /** + * @param {string} hotkey + * @param {number} value + * @param {Object} [options] + * @param {number} [options.min=0] + * @param {number} [options.max=100] + * @param {number} [options.defaultStep=10] - default step + * @param {number} [options.modifierStep=1] - step when holding ctrl+key + * @param {number} [options.leap=20] - step for pageup / pagedown + * @returns {number} updated and clamped value + */ + handleRangeKeydownValue( + hotkey, + value, + { min = 0, max = 100, defaultStep = 10, modifierStep = 1, leap = 20 } = {} + ) { + let step = defaultStep; + if (hotkey.startsWith("control+")) { + step = modifierStep; + } + const mainKey = hotkey.replace("control+", ""); + if (mainKey === "pageup" || mainKey === "pagedown") { + step = leap; + } + if (["arrowup", "arrowright", "pageup"].includes(mainKey)) { + value += step; + } else if (["arrowdown", "arrowleft", "pagedown"].includes(mainKey)) { + value -= step; + } else if (mainKey === "home") { + value = min; + } else if (mainKey === "end") { + value = max; + } + return clamp(value, min, max); + } + /** + * Selects and applies a currently previewed color if "Enter" was pressed. + * + * @param {String} hotkey + */ + selectColorOnEnter(hotkey) { + if (hotkey === "enter" && this.shouldSetSelectedColor) { + this.pickerFlag = false; + this.sliderFlag = false; + this.opacitySliderFlag = false; + this._colorSelected(); + } + } + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Updates input values, color preview, picker and slider pointer positions. + * + * @private + */ + _updateUI() { + // Update inputs + for (const [color, value] of Object.entries(this.colorComponents)) { + const input = this.el.querySelector(`.o_${color}_input`); + if (input) { + input.value = value; + } + } + + // Update picker area and picker pointer position + const colorPickerArea = this.colorPickerAreaRef.el; + colorPickerArea.style.backgroundColor = `hsl(${this.colorComponents.hue}, 100%, 50%)`; + const top = ((100 - this.colorComponents.lightness) * colorPickerArea.clientHeight) / 100; + const left = (this.colorComponents.saturation * colorPickerArea.clientWidth) / 100; + + const colorpickerPointer = this.colorPickerPointerRef.el; + colorpickerPointer.style.top = top - 5 + "px"; + colorpickerPointer.style.left = left - 5 + "px"; + colorpickerPointer.setAttribute( + "aria-label", + _t("Saturation: %(saturationLvl)s %. Brightness: %(brightnessLvl)s %", { + saturationLvl: this.colorComponents.saturation?.toFixed(2) || "0", + brightnessLvl: this.colorComponents.lightness?.toFixed(2) || "0", + }) + ); + + // Update color slider position + const colorSlider = this.colorSliderRef.el; + const height = colorSlider.clientHeight; + const y = (this.colorComponents.hue * height) / 360; + this.colorSliderPointerRef.el.style.bottom = `${Math.round(y - 4)}px`; + this.colorSliderPointerRef.el.setAttribute( + "aria-valuenow", + this.colorComponents.hue.toFixed(2) + ); + + if (!this.props.noTransparency) { + // Update opacity slider position + const opacitySlider = this.opacitySliderRef.el; + const heightOpacity = opacitySlider.clientHeight; + const z = heightOpacity * (1 - this.colorComponents.opacity / 100.0); + this.opacitySliderPointerRef.el.style.top = `${Math.round(z - 2)}px`; + this.opacitySliderPointerRef.el.setAttribute( + "aria-valuenow", + this.colorComponents.opacity.toFixed(2) + ); + + // Add gradient color on opacity slider + const sliderColor = this.colorComponents.hex.slice(0, 7); + opacitySlider.style.background = `linear-gradient(${sliderColor} 0%, transparent 100%)`; + } + } + /** + * Updates colors according to given hex value. Opacity is left unchanged. + * + * @private + * @param {string} hex - hexadecimal code + */ + _updateHex(hex) { + const rgb = convertCSSColorToRgba(hex); + if (!rgb) { + return; + } + Object.assign( + this.colorComponents, + { hex: hex }, + rgb, + convertRgbToHsl(rgb.red, rgb.green, rgb.blue) + ); + this._updateCssColor(); + } + /** + * Updates colors according to given RGB values. + * + * @private + * @param {integer} r + * @param {integer} g + * @param {integer} b + * @param {integer} [a] + */ + _updateRgba(r, g, b, a) { + // Remove full transparency in case some lightness is added + const opacity = a || this.colorComponents.opacity; + if (opacity < 0.1 && (r > 0.1 || g > 0.1 || b > 0.1)) { + a = this.props.defaultOpacity; + } + + const hex = convertRgbaToCSSColor(r, g, b, a); + if (!hex) { + return; + } + Object.assign( + this.colorComponents, + { red: r, green: g, blue: b }, + a === undefined ? {} : { opacity: a }, + { hex: hex }, + convertRgbToHsl(r, g, b) + ); + this._updateCssColor(); + } + /** + * Updates colors according to given HSL values. + * + * @private + * @param {integer} h + * @param {integer} s + * @param {integer} l + */ + _updateHsl(h, s, l) { + // Remove full darkness/brightness and non-saturation in case hue is changed + if (0.1 < Math.abs(h - this.colorComponents.hue)) { + if (l < 0.1 || 99.9 < l) { + l = 50; + } + if (s < 0.1) { + s = 100; + } + } + // Remove full transparency in case some lightness is added + let a = this.colorComponents.opacity; + if (a < 0.1 && l > 0.1) { + a = this.props.defaultOpacity; + } + + const rgb = convertHslToRgb(h, s, l); + if (!rgb) { + return; + } + // We receive an hexa as we ignore the opacity + const hex = convertRgbaToCSSColor(rgb.red, rgb.green, rgb.blue, a); + Object.assign( + this.colorComponents, + { hue: h, saturation: s, lightness: l }, + rgb, + { hex: hex }, + { opacity: a } + ); + this._updateCssColor(); + } + /** + * Updates color opacity. + * + * @private + * @param {integer} a + */ + _updateOpacity(a) { + if (a < 0 || a > 100) { + return; + } + Object.assign(this.colorComponents, { opacity: a }); + const r = this.colorComponents.red; + const g = this.colorComponents.green; + const b = this.colorComponents.blue; + Object.assign(this.colorComponents, { hex: convertRgbaToCSSColor(r, g, b, a) }); + this._updateCssColor(); + } + /** + * Trigger an event to annonce that the widget value has changed + * + * @private + */ + _colorSelected() { + this.props.onColorSelect(this.colorComponents); + } + /** + * Updates css color representation. + * + * @private + */ + _updateCssColor() { + const r = this.colorComponents.red; + const g = this.colorComponents.green; + const b = this.colorComponents.blue; + const a = this.colorComponents.opacity; + Object.assign(this.colorComponents, { cssColor: convertRgbaToCSSColor(r, g, b, a) }); + if (this.previewActive) { + this.props.onColorPreview(this.colorComponents); + } + } + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + onKeydown(ev) { + if (ev.key === "Enter") { + if (ev.target.tagName === "INPUT") { + this.onChangeInputs(ev); + } + ev.preventDefault(); + this.props.onInputEnter(ev); + } + } + /** + * @param {Event} ev + */ + onClick(ev) { + if (this.props.stopClickPropagation) { + ev.stopPropagation(); + } + //TODO: we should remove it with legacy web_editor + ev.__isColorpickerClick = true; + + if (ev.target.dataset.colorMethod === "hex" && !this.selectedHexValue) { + ev.target.select(); + this.selectedHexValue = ev.target.value; + return; + } + this.selectedHexValue = ""; + } + onPointerUp() { + if (this.pickerFlag || this.sliderFlag || this.opacitySliderFlag) { + this.shouldSetSelectedColor = true; + this._updateCssColor(); + } + this.pickerFlag = false; + this.sliderFlag = false; + this.opacitySliderFlag = false; + + if (this.lastFocusedSliderEl) { + this.lastFocusedSliderEl.focus(); + this.lastFocusedSliderEl = undefined; + } + } + /** + * Removes the close callback on Escape, so that a preview is cancelled with + * escape instead of being applied. + * + * @param {KeydownEvent} ev + */ + onEscapeKeydown(ev) { + const hotkey = getActiveHotkey(ev); + if (hotkey === "escape") { + this.props.setOnCloseCallback?.(() => {}); + } + } + /** + * Updates color when the user starts clicking on the picker. + * + * @private + * @param {Event} ev + */ + onPointerDownPicker(ev) { + this.pickerFlag = true; + ev.preventDefault(); + this.onPointerMovePicker(ev); + this.setLastFocusedSliderEl(this.colorPickerPointerRef.el); + } + /** + * Updates saturation and lightness values on pointer drag over picker. + * + * @private + * @param {Event} ev + */ + onPointerMovePicker(ev) { + if (!this.pickerFlag) { + return; + } + + const colorPickerArea = this.colorPickerAreaRef.el; + const rect = colorPickerArea.getClientRects()[0]; + const top = ev.pageY - rect.top; + const left = ev.pageX - rect.left; + let saturation = Math.round((100 * left) / colorPickerArea.clientWidth); + let lightness = Math.round( + (100 * (colorPickerArea.clientHeight - top)) / colorPickerArea.clientHeight + ); + saturation = clamp(saturation, 0, 100); + lightness = clamp(lightness, 0, 100); + + this._updateHsl(this.colorComponents.hue, saturation, lightness); + this._updateUI(); + } + /** + * Updates saturation and lightness values on arrow keydown over picker. + * + * @private + * @param {Event} ev + */ + onPickerKeydown(ev) { + const hotkey = getActiveHotkey(ev); + this.selectColorOnEnter(hotkey); + if (!this.getAllowedHotkeys(ARROW_KEYS).includes(hotkey)) { + return; + } + let saturation = this.colorComponents.saturation; + let lightness = this.colorComponents.lightness; + let step = 10; + if (hotkey.startsWith("control+")) { + step = 1; + } + const mainKey = hotkey.replace("control+", ""); + if (mainKey === "arrowup") { + lightness += step; + } else if (mainKey === "arrowdown") { + lightness -= step; + } else if (mainKey === "arrowright") { + saturation += step; + } else if (mainKey === "arrowleft") { + saturation -= step; + } + lightness = clamp(lightness, 0, 100); + saturation = clamp(saturation, 0, 100); + + this._updateHsl(this.colorComponents.hue, saturation, lightness); + this._updateUI(); + this.shouldSetSelectedColor = true; + } + /** + * Updates color when user starts clicking on slider. + * + * @private + * @param {Event} ev + */ + onPointerDownSlider(ev) { + this.sliderFlag = true; + ev.preventDefault(); + this.onPointerMoveSlider(ev); + this.setLastFocusedSliderEl(this.colorSliderPointerRef.el); + } + /** + * Updates hue value on pointer drag over slider. + * + * @private + * @param {Event} ev + */ + onPointerMoveSlider(ev) { + if (!this.sliderFlag) { + return; + } + + const colorSlider = this.colorSliderRef.el; + const colorSliderRects = colorSlider.getClientRects(); + const y = colorSliderRects[0].height - (ev.pageY - colorSliderRects[0].top); + let hue = Math.round((360 * y) / colorSlider.clientHeight); + hue = clamp(hue, 0, 360); + + this._updateHsl(hue, this.colorComponents.saturation, this.colorComponents.lightness); + this._updateUI(); + } + /** + * Updates hue value on arrow keydown on slider. + * + * @param {Event} ev + */ + onSliderKeydown(ev) { + const hotkey = getActiveHotkey(ev); + this.selectColorOnEnter(hotkey); + if (!this.getAllowedHotkeys(SLIDER_KEYS).includes(hotkey)) { + return; + } + const hue = this.handleRangeKeydownValue(hotkey, this.colorComponents.hue, { + min: 0, + max: 360, + leap: 30, + }); + this._updateHsl(hue, this.colorComponents.saturation, this.colorComponents.lightness); + this._updateUI(); + this.shouldSetSelectedColor = true; + } + /** + * Updates opacity when user starts clicking on opacity slider. + * + * @private + * @param {Event} ev + */ + onPointerDownOpacitySlider(ev) { + this.opacitySliderFlag = true; + ev.preventDefault(); + this.onPointerMoveOpacitySlider(ev); + this.setLastFocusedSliderEl(this.opacitySliderPointerRef.el); + } + /** + * Updates opacity value on pointer drag over opacity slider. + * + * @private + * @param {Event} ev + */ + onPointerMoveOpacitySlider(ev) { + if (!this.opacitySliderFlag || this.props.noTransparency) { + return; + } + + const opacitySlider = this.opacitySliderRef.el; + const y = ev.pageY - opacitySlider.getClientRects()[0].top; + let opacity = Math.round(100 * (1 - y / opacitySlider.clientHeight)); + opacity = clamp(opacity, 0, 100); + + this._updateOpacity(opacity); + this._updateUI(); + } + /** + * Updates opacity value on arrow keydown on opacity slider. + * + * @param {Event} ev + */ + onOpacitySliderKeydown(ev) { + const hotkey = getActiveHotkey(ev); + this.selectColorOnEnter(hotkey); + if (!this.getAllowedHotkeys(SLIDER_KEYS).includes(hotkey)) { + return; + } + const opacity = this.handleRangeKeydownValue(hotkey, this.colorComponents.opacity); + + this._updateOpacity(opacity); + this._updateUI(); + this.shouldSetSelectedColor = true; + } + /** + * Called when input value is changed -> Updates UI: Set picker and slider + * position and set colors. + * + * @private + * @param {Event} ev + */ + onChangeInputs(ev) { + switch (ev.target.dataset.colorMethod) { + case "hex": + // Handled by the "input" event (see "onHexColorInput"). + return; + case "hsl": + this._updateHsl( + parseInt(this.el.querySelector(".o_hue_input").value), + parseInt(this.el.querySelector(".o_saturation_input").value), + parseInt(this.el.querySelector(".o_lightness_input").value) + ); + break; + } + this._updateUI(); + this._colorSelected(); + } + /** + * Called when the hex color input's input event is triggered. + * + * @private + * @param {Event} ev + */ + onHexColorInput(ev) { + const hexColorValue = ev.target.value.replaceAll("#", ""); + if (hexColorValue.length === 6 || hexColorValue.length === 8) { + this._updateHex(`#${hexColorValue}`); + this._updateUI(); + this._colorSelected(); + } + } +} diff --git a/frontend/web/static/src/core/color_picker/custom_color_picker/custom_color_picker.scss b/frontend/web/static/src/core/color_picker/custom_color_picker/custom_color_picker.scss new file mode 100644 index 0000000..35400dd --- /dev/null +++ b/frontend/web/static/src/core/color_picker/custom_color_picker/custom_color_picker.scss @@ -0,0 +1,51 @@ +// COLOR PICKER +.o_colorpicker_widget { + .o_color_pick_area { + height: 125px; + background-image: linear-gradient(to bottom, hsl(0, 0%, 100%) 0%, hsla(0, 0%, 100%, 0) 50%, hsla(0, 0%, 0%, 0) 50%, hsl(0, 0%, 0%) 100%), + linear-gradient(to right, hsl(0, 0%, 50%) 0%, hsla(0, 0%, 50%, 0) 100%); + cursor: crosshair; + } + .o_color_slider { + background: linear-gradient(#F00 0%, #F0F 16.66%, #00F 33.33%, #0FF 50%, #0F0 66.66%, #FF0 83.33%, #F00 100%); + } + .o_opacity_slider, .o_color_preview { + @extend %o-preview-alpha-background; + } + .o_color_slider, .o_opacity_slider { + width: 4%; + margin-right: 2%; + cursor: pointer; + } + .o_slider_pointer, .o_opacity_pointer { + @include o-position-absolute($left: -50%); + width: 200%; + height: 8px; + margin-top: -2px; + } + .o_slider_pointer, .o_opacity_pointer, .o_picker_pointer { + &:focus-visible { + outline: none; + box-shadow: + inset 0 0 0 1px rgba(white, 0.9), + 0 0 0 1px var(--bg, $white), + 0 0 0 3px var(--o-color-picker-active-color, $o-enterprise-action-color); + } + } + .o_slider_pointer, .o_opacity_pointer, .o_picker_pointer, .o_color_preview { + box-shadow: inset 0 0 0 1px rgba(white, 0.9); + border: 1px solid black; + } + .o_color_picker_inputs { + font-size: 10px; + + input { + font-family: monospace !important; // FIXME: the monospace font used in the editor has not consistent ch units on Firefox + height: 18px; + font-size: 11px; + } + .o_hex_div input { + width: 9ch; + } + } +} diff --git a/frontend/web/static/src/core/color_picker/custom_color_picker/custom_color_picker.xml b/frontend/web/static/src/core/color_picker/custom_color_picker/custom_color_picker.xml new file mode 100644 index 0000000..0637c23 --- /dev/null +++ b/frontend/web/static/src/core/color_picker/custom_color_picker/custom_color_picker.xml @@ -0,0 +1,36 @@ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+ + +
+
+
+ + + diff --git a/frontend/web/static/src/core/color_picker/tabs/color_picker_custom_tab.js b/frontend/web/static/src/core/color_picker/tabs/color_picker_custom_tab.js new file mode 100644 index 0000000..e7feb58 --- /dev/null +++ b/frontend/web/static/src/core/color_picker/tabs/color_picker_custom_tab.js @@ -0,0 +1,45 @@ +import { Component } from "@odoo/owl"; +import { _t } from "@web/core/l10n/translation"; +import { registry } from "@web/core/registry"; +import { isColorGradient } from "@web/core/utils/colors"; +import { CustomColorPicker } from "../custom_color_picker/custom_color_picker"; + +export class ColorPickerCustomTab extends Component { + static template = "web.ColorPickerCustomTab"; + static components = { CustomColorPicker }; + static props = { + applyColor: Function, + colorPickerNavigation: Function, + onColorClick: Function, + onColorPreview: Function, + onColorPointerOver: Function, + onColorPointerOut: Function, + onFocusin: Function, + onFocusout: Function, + getUsedCustomColors: { type: Function, optional: true }, + currentColorPreview: { type: String, optional: true }, + currentCustomColor: { type: String, optional: true }, + defaultColorSet: { type: String | Boolean, optional: true }, + defaultOpacity: { type: Number, optional: true }, + grayscales: { type: Object, optional: true }, + cssVarColorPrefix: { type: String, optional: true }, + noTransparency: { type: Boolean, optional: true }, + setOnCloseCallback: { type: Function, optional: true }, + setOperationCallbacks: { type: Function, optional: true }, + "*": { optional: true }, + }; + + setup() { + this.usedCustomColors = this.props.getUsedCustomColors(); + } + + isValidCustomColor(color) { + return color && color.slice(7, 9) !== "00" && !isColorGradient(color); + } +} + +registry.category("color_picker_tabs").add("web.custom", { + id: "custom", + name: _t("Custom"), + component: ColorPickerCustomTab, +}); diff --git a/frontend/web/static/src/core/color_picker/tabs/color_picker_custom_tab.xml b/frontend/web/static/src/core/color_picker/tabs/color_picker_custom_tab.xml new file mode 100644 index 0000000..40dfa26 --- /dev/null +++ b/frontend/web/static/src/core/color_picker/tabs/color_picker_custom_tab.xml @@ -0,0 +1,45 @@ + + +
+
+ +
+ +
+ +
+
+ +
+
+
diff --git a/frontend/web/static/src/core/color_picker/tabs/color_picker_solid_tab.js b/frontend/web/static/src/core/color_picker/tabs/color_picker_solid_tab.js new file mode 100644 index 0000000..5025a7e --- /dev/null +++ b/frontend/web/static/src/core/color_picker/tabs/color_picker_solid_tab.js @@ -0,0 +1,27 @@ +import { Component } from "@odoo/owl"; +import { _t } from "@web/core/l10n/translation"; +import { registry } from "@web/core/registry"; + +export class ColorPickerSolidTab extends Component { + static template = "web.ColorPickerSolidTab"; + static props = { + colorPickerNavigation: Function, + onColorClick: Function, + onColorPointerOver: Function, + onColorPointerOut: Function, + onFocusin: Function, + onFocusout: Function, + currentCustomColor: { type: String, optional: true }, + defaultColorSet: { type: String | Boolean, optional: true }, + cssVarColorPrefix: { type: String, optional: true }, + defaultColors: Array, + defaultThemeColorVars: Array, + "*": { optional: true }, + }; +} + +registry.category("color_picker_tabs").add("web.solid", { + id: "solid", + name: _t("Solid"), + component: ColorPickerSolidTab, +}); diff --git a/frontend/web/static/src/core/color_picker/tabs/color_picker_solid_tab.xml b/frontend/web/static/src/core/color_picker/tabs/color_picker_solid_tab.xml new file mode 100644 index 0000000..faf4feb --- /dev/null +++ b/frontend/web/static/src/core/color_picker/tabs/color_picker_solid_tab.xml @@ -0,0 +1,26 @@ + + +
+
+ +
+ +
+ + +
+
+
+
diff --git a/frontend/web/static/src/core/colorlist/colorlist.js b/frontend/web/static/src/core/colorlist/colorlist.js new file mode 100644 index 0000000..25633b3 --- /dev/null +++ b/frontend/web/static/src/core/colorlist/colorlist.js @@ -0,0 +1,62 @@ +import { _t } from "@web/core/l10n/translation"; + +import { Component, useRef, useState, useExternalListener } from "@odoo/owl"; + +export class ColorList extends Component { + static COLORS = [ + _t("No color"), + _t("Red"), + _t("Orange"), + _t("Yellow"), + _t("Cyan"), + _t("Purple"), + _t("Almond"), + _t("Teal"), + _t("Blue"), + _t("Raspberry"), + _t("Green"), + _t("Violet"), + ]; + static template = "web.ColorList"; + static defaultProps = { + forceExpanded: false, + isExpanded: false, + }; + static props = { + canToggle: { type: Boolean, optional: true }, + colors: Array, + forceExpanded: { type: Boolean, optional: true }, + isExpanded: { type: Boolean, optional: true }, + onColorSelected: Function, + selectedColor: { type: Number, optional: true }, + }; + + setup() { + this.colorlistRef = useRef("colorlist"); + this.state = useState({ isExpanded: this.props.isExpanded }); + useExternalListener(window, "click", this.onOutsideClick); + } + get colors() { + return this.constructor.COLORS; + } + onColorSelected(id) { + this.props.onColorSelected(id); + if (!this.props.forceExpanded) { + this.state.isExpanded = false; + } + } + onOutsideClick(ev) { + if (this.colorlistRef.el.contains(ev.target) || this.props.forceExpanded) { + return; + } + this.state.isExpanded = false; + } + onToggle(ev) { + if (this.props.canToggle) { + ev.preventDefault(); + ev.stopPropagation(); + this.state.isExpanded = !this.state.isExpanded; + this.colorlistRef.el.firstElementChild.focus(); + } + } +} diff --git a/frontend/web/static/src/core/colorlist/colorlist.scss b/frontend/web/static/src/core/colorlist/colorlist.scss new file mode 100644 index 0000000..536707d --- /dev/null +++ b/frontend/web/static/src/core/colorlist/colorlist.scss @@ -0,0 +1,75 @@ +@mixin o-colorlist($-entry, $-child) { + width: var(--ColorListField-width, none); + padding: var(--ColorListField-padding, 0); + box-sizing: content-box; + margin-bottom: var(--ColorListField-marginBottom, 0); + + .o_bottom_sheet_body & { + --ColorListField-padding: #{map-get($spacers, 3)} var(--BottomSheet-Entry-paddingX); + --fieldWidget-display: block; + --ColorListField-Entry-fontSize: 1.4em; + + grid-template-columns: repeat(var(--ColorListField-columns, 6), 1fr); + } + + #{$-entry} { + #{$-child} { + position: relative; + display: block; + min-width: var(--ColorListField-Entry-minWidth, o-to-rem(20px)); + aspect-ratio: var(--ColorListField-Entry-aspectRatio, 1); + border-radius: var(--ColorListField-Entry-borderRadius, 100%); + font-size: var(--ColorListField-Entry-fontSize, smaller); + overflow: hidden; + } + + &:first-child #{$-child}::after { + box-shadow: inset 0 0 0 $border-width var(--dropdown-color); + border-radius: inherit; + color: currentColor; + } + + &.active { + #{$-child}::after { + @include o-position-absolute(0, 0, 0, 0); + display: flex; + justify-content: center; + align-items: center; + color: var(--color, #{$body-color}); + font: normal normal normal 1em/1 FontAwesome; + content: "\f00c"; + } + } + + &:hover:not(.active) { + opacity: $o-opacity-muted; + } + } +} + +.o_colorlist { + @include o-colorlist("button", "&" ); + grid-template-columns: repeat(auto-fit, $o-bubble-color-size-xl); + + :not(.o_field_widget) > & { + justify-content: center; + } + + > button { + aspect-ratio: 1; + + // No Color + &.o_colorlist_item_color_0 { + background: transparent; + box-shadow: inset 0 0 0 1px $gray-500; + } + + // Set all the colors but the "no-color" one + @for $size from 2 through length($o-colors) { + &.o_colorlist_item_color_#{$size - 1} { + @include o-print-color(nth($o-colors, $size), background-color, bg-opacity); + @include o-print-color(color-contrast(nth($o-colors, $size)), color, text-opacity); + } + } + } +} diff --git a/frontend/web/static/src/core/colorlist/colorlist.xml b/frontend/web/static/src/core/colorlist/colorlist.xml new file mode 100644 index 0000000..aee8405 --- /dev/null +++ b/frontend/web/static/src/core/colorlist/colorlist.xml @@ -0,0 +1,15 @@ + + + + +
+ +
+
+ +
diff --git a/frontend/web/static/src/core/colors/colors.js b/frontend/web/static/src/core/colors/colors.js new file mode 100644 index 0000000..f2e0b2e --- /dev/null +++ b/frontend/web/static/src/core/colors/colors.js @@ -0,0 +1,217 @@ +import { clamp } from "@web/core/utils/numbers"; +/** + * Lists of colors that contrast well with each other to be used in various + * visualizations (eg. graphs/charts), both in bright and dark themes. + */ + +const COLORS_ENT_BRIGHT = ["#875A7B", "#A5D8D7", "#DCD0D9"]; +const COLORS_ENT_DARK = ["#6B3E66", "#147875", "#5A395A"]; +const COLORS_SM = [ + "#4EA7F2", // Blue + "#EA6175", // Red + "#43C5B1", // Teal + "#F4A261", // Orange + "#8481DD", // Purple + "#FFD86D", // Yellow +]; +const COLORS_MD = [ + "#4EA7F2", // Blue #1 + "#3188E6", // Blue #2 + "#43C5B1", // Teal #1 + "#00A78D", // Teal #2 + "#EA6175", // Red #1 + "#CE4257", // Red #2 + "#F4A261", // Orange #1 + "#F48935", // Orange #2 + "#8481DD", // Purple #1 + "#5752D1", // Purple #2 + "#FFD86D", // Yellow #1 + "#FFBC2C", // Yellow #2 +]; +const COLORS_LG = [ + "#4EA7F2", // Blue #1 + "#3188E6", // Blue #2 + "#056BD9", // Blue #3 + "#A76DBC", // Violet #1 + "#7F4295", // Violet #2 + "#6D2387", // Violet #3 + "#EA6175", // Red #1 + "#CE4257", // Red #2 + "#982738", // Red #3 + "#43C5B1", // Teal #1 + "#00A78D", // Teal #2 + "#0E8270", // Teal #3 + "#F4A261", // Orange #1 + "#F48935", // Orange #2 + "#BE5D10", // Orange #3 + "#8481DD", // Purple #1 + "#5752D1", // Purple #2 + "#3A3580", // Purple #3 + "#A4A8B6", // Gray #1 + "#7E8290", // Gray #2 + "#545B70", // Gray #3 + "#FFD86D", // Yellow #1 + "#FFBC2C", // Yellow #2 + "#C08A16", // Yellow #3 +]; +const COLORS_XL = [ + "#4EA7F2", // Blue #1 + "#3188E6", // Blue #2 + "#056BD9", // Blue #3 + "#155193", // Blue #4 + "#A76DBC", // Violet #1 + "#7F4295", // Violet #1 + "#6D2387", // Violet #1 + "#4F1565", // Violet #1 + "#EA6175", // Red #1 + "#CE4257", // Red #2 + "#982738", // Red #3 + "#791B29", // Red #4 + "#43C5B1", // Teal #1 + "#00A78D", // Teal #2 + "#0E8270", // Teal #3 + "#105F53", // Teal #4 + "#F4A261", // Orange #1 + "#F48935", // Orange #2 + "#BE5D10", // Orange #3 + "#7D380D", // Orange #4 + "#8481DD", // Purple #1 + "#5752D1", // Purple #2 + "#3A3580", // Purple #3 + "#26235F", // Purple #4 + "#A4A8B6", // Grey #1 + "#7E8290", // Grey #2 + "#545B70", // Grey #3 + "#3F4250", // Grey #4 + "#FFD86D", // Yellow #1 + "#FFBC2C", // Yellow #2 + "#C08A16", // Yellow #3 + "#936A12", // Yellow #4 +]; + +/** + * @param {string} colorScheme + * @param {string} paletteName + * @returns {array} + */ +export function getColors(colorScheme, paletteName) { + switch (paletteName) { + case "odoo": + return colorScheme === "dark" ? COLORS_ENT_DARK : COLORS_ENT_BRIGHT; + case "sm": + return COLORS_SM; + case "md": + return COLORS_MD; + case "lg": + return COLORS_LG; + default: + return COLORS_XL; + } +} + +/** + * @param {number} index + * @param {string} colorScheme + * @returns {string} + */ +export function getColor(index, colorScheme, paletteSizeOrName) { + let paletteName; + if (paletteSizeOrName === "odoo") { + paletteName = "odoo"; + } else if (paletteSizeOrName <= 6 || paletteSizeOrName === "sm") { + paletteName = "sm"; + } else if (paletteSizeOrName <= 12 || paletteSizeOrName === "md") { + paletteName = "md"; + } else if (paletteSizeOrName <= 24 || paletteSizeOrName === "lg") { + paletteName = "lg"; + } else { + paletteName = "xl"; + } + const colors = getColors(colorScheme, paletteName); + return colors[index % colors.length]; +} + +export const DEFAULT_BG = "#d3d3d3"; + +export function getBorderWhite(colorScheme) { + return colorScheme === "dark" ? "rgba(38, 42, 54, .2)" : "rgba(249,250,251, .2)"; +} + +const RGB_REGEX = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i; + +/** + * @param {string} hex + * @param {number} opacity + * @returns {string} + */ +export function hexToRGBA(hex, opacity) { + const rgb = RGB_REGEX.exec(hex) + .slice(1, 4) + .map((n) => parseInt(n, 16)) + .join(","); + return `rgba(${rgb},${opacity})`; +} + +/** + * Used to return custom colors depending on the color scheme + * @param {string} colorScheme + * @param {string} brightModeColor + * @param {string} darkModeColor + * @returns {string|Number|Boolean} + */ + +export function getCustomColor(colorScheme, brightModeColor, darkModeColor) { + if (darkModeColor === undefined) { + return brightModeColor; + } else { + return colorScheme === "dark" ? darkModeColor : brightModeColor; + } +} + +/** + * Used to lighten a color + * @param {string} color + * @param {number} factor + * @returns {string} + */ +export function lightenColor(color, factor) { + factor = clamp(factor, 0, 1); + + let r = parseInt(color.substring(1, 3), 16); + let g = parseInt(color.substring(3, 5), 16); + let b = parseInt(color.substring(5, 7), 16); + + r = Math.round(r + (255 - r) * factor); + g = Math.round(g + (255 - g) * factor); + b = Math.round(b + (255 - b) * factor); + + r = r.toString(16).padStart(2, "0"); + g = g.toString(16).padStart(2, "0"); + b = b.toString(16).padStart(2, "0"); + + return `#${r}${g}${b}`; +} + +/** + * Used to darken a color + * @param {string} color + * @param {number} factor + * @returns {string} + */ +export function darkenColor(color, factor) { + factor = clamp(factor, 0, 1); + + let r = parseInt(color.substring(1, 3), 16); + let g = parseInt(color.substring(3, 5), 16); + let b = parseInt(color.substring(5, 7), 16); + + r = Math.round(r * (1 - factor)); + g = Math.round(g * (1 - factor)); + b = Math.round(b * (1 - factor)); + + r = r.toString(16).padStart(2, "0"); + g = g.toString(16).padStart(2, "0"); + b = b.toString(16).padStart(2, "0"); + + return `#${r}${g}${b}`; +} diff --git a/frontend/web/static/src/core/commands/command_category.js b/frontend/web/static/src/core/commands/command_category.js new file mode 100644 index 0000000..d40ec49 --- /dev/null +++ b/frontend/web/static/src/core/commands/command_category.js @@ -0,0 +1,11 @@ +import { registry } from "@web/core/registry"; + +const commandCategoryRegistry = registry.category("command_categories"); +commandCategoryRegistry + .add("app", {}, { sequence: 10 }) + .add("smart_action", {}, { sequence: 15 }) + .add("actions", {}, { sequence: 30 }) + .add("default", {}, { sequence: 50 }) + .add("view_switcher", {}, { sequence: 100 }) + .add("debug", {}, { sequence: 110 }) + .add("disabled", {}); diff --git a/frontend/web/static/src/core/commands/command_hook.js b/frontend/web/static/src/core/commands/command_hook.js new file mode 100644 index 0000000..452e88c --- /dev/null +++ b/frontend/web/static/src/core/commands/command_hook.js @@ -0,0 +1,23 @@ +import { useService } from "@web/core/utils/hooks"; + +import { useEffect } from "@odoo/owl"; + +/** + * @typedef {import("./command_service").CommandOptions} CommandOptions + */ + +/** + * This hook will subscribe/unsubscribe the given subscription + * when the caller component will mount/unmount. + * + * @param {string} name + * @param {()=>(void | import("@web/core/commands/command_palette").CommandPaletteConfig)} action + * @param {CommandOptions} [options] + */ +export function useCommand(name, action, options = {}) { + const commandService = useService("command"); + useEffect( + () => commandService.add(name, action, options), + () => [] + ); +} diff --git a/frontend/web/static/src/core/commands/command_items.xml b/frontend/web/static/src/core/commands/command_items.xml new file mode 100644 index 0000000..6a100df --- /dev/null +++ b/frontend/web/static/src/core/commands/command_items.xml @@ -0,0 +1,35 @@ + + + + + TIP — search for + + , + and + + + + + + + + +
+ + +
+
+ + +
+ + + + + + + + +
+
+ +
diff --git a/frontend/web/static/src/core/commands/command_palette.js b/frontend/web/static/src/core/commands/command_palette.js new file mode 100644 index 0000000..c4a1a60 --- /dev/null +++ b/frontend/web/static/src/core/commands/command_palette.js @@ -0,0 +1,388 @@ +import { Dialog } from "@web/core/dialog/dialog"; +import { useHotkey } from "@web/core/hotkeys/hotkey_hook"; +import { _t } from "@web/core/l10n/translation"; +import { KeepLast, Race } from "@web/core/utils/concurrency"; +import { useAutofocus, useService } from "@web/core/utils/hooks"; +import { scrollTo } from "@web/core/utils/scrolling"; +import { fuzzyLookup } from "@web/core/utils/search"; +import { debounce } from "@web/core/utils/timing"; +import { isMacOS, isMobileOS } from "@web/core/browser/feature_detection"; +import { highlightText } from "@web/core/utils/html"; + +import { + Component, + onWillStart, + onWillDestroy, + EventBus, + useRef, + useState, + markRaw, + useExternalListener, +} from "@odoo/owl"; + +const DEFAULT_PLACEHOLDER = _t("Search..."); +const DEFAULT_EMPTY_MESSAGE = _t("No result found"); +const FUZZY_NAMESPACES = ["default"]; + +/** + * @typedef {import("./command_service").Command} Command + */ + +/** + * @typedef {Command & { + * Component?: Component; + * props?: object; + * }} CommandItem + */ + +/** + * @typedef {{ + * namespace?: string; + * provide: ()=>CommandItem[]; + * }} Provider + */ + +/** + * @typedef {{ + * categories: string[]; + * debounceDelay: number; + * emptyMessage: string; + * placeholder: string; + * }} NamespaceConfig + */ + +/** + * @typedef {{ + * configByNamespace?: {[namespace: string]: NamespaceConfig}; + * FooterComponent?: Component; + * providers: Provider[]; + * searchValue?: string; + * }} CommandPaletteConfig + */ + +/** + * Util used to filter commands that are within category. + * Note: for the default category, also get all commands having invalid category. + * + * @param {string} categoryName the category key + * @param {string[]} categories + * @returns an array filter predicate + */ +function commandsWithinCategory(categoryName, categories) { + return (cmd) => { + const inCurrentCategory = categoryName === cmd.category; + const fallbackCategory = categoryName === "default" && !categories.includes(cmd.category); + return inCurrentCategory || fallbackCategory; + }; +} + +export class DefaultCommandItem extends Component { + static template = "web.DefaultCommandItem"; + static props = { + slots: { type: Object, optional: true }, + // Props send by the command palette: + hotkey: { type: String, optional: true }, + hotkeyOptions: { type: String, optional: true }, + name: { type: String, optional: true }, + searchValue: { type: String, optional: true }, + executeCommand: { type: Function, optional: true }, + }; +} + +export class CommandPalette extends Component { + static template = "web.CommandPalette"; + static components = { Dialog }; + static lastSessionId = 0; + static props = { + bus: { type: EventBus, optional: true }, + close: Function, + config: Object, + closeMe: { type: Function, optional: true }, + }; + + setup() { + if (this.props.bus) { + const setConfig = ({ detail }) => this.setCommandPaletteConfig(detail); + this.props.bus.addEventListener(`SET-CONFIG`, setConfig); + onWillDestroy(() => this.props.bus.removeEventListener(`SET-CONFIG`, setConfig)); + } + + this.keyId = 1; + this.race = new Race(); + this.keepLast = new KeepLast(); + this._sessionId = CommandPalette.lastSessionId++; + this.DefaultCommandItem = DefaultCommandItem; + this.activeElement = useService("ui").activeElement; + this.inputRef = useAutofocus(); + + useHotkey("Enter", () => this.executeSelectedCommand(), { bypassEditableProtection: true }); + useHotkey("Control+Enter", () => this.executeSelectedCommand(true), { + bypassEditableProtection: true, + }); + useHotkey("ArrowUp", () => this.selectCommandAndScrollTo("PREV"), { + bypassEditableProtection: true, + allowRepeat: true, + }); + useHotkey("ArrowDown", () => this.selectCommandAndScrollTo("NEXT"), { + bypassEditableProtection: true, + allowRepeat: true, + }); + useExternalListener(window, "mousedown", this.onWindowMouseDown); + + /** + * @type {{ commands: CommandItem[], + * emptyMessage: string, + * FooterComponent: Component, + * namespace: string, + * placeholder: string, + * searchValue: string, + * selectedCommand: CommandItem }} + */ + this.state = useState({}); + + this.root = useRef("root"); + this.listboxRef = useRef("listbox"); + + onWillStart(() => this.setCommandPaletteConfig(this.props.config)); + } + + get commandsByCategory() { + const categories = []; + for (const category of this.categoryKeys) { + const commands = this.state.commands.filter( + commandsWithinCategory(category, this.categoryKeys) + ); + if (commands.length) { + categories.push({ + commands, + name: this.categoryNames[category], + keyId: category, + }); + } + } + return categories; + } + + /** + * Apply the new config to the command pallet + * @param {CommandPaletteConfig} config + */ + async setCommandPaletteConfig(config) { + this.configByNamespace = config.configByNamespace || {}; + this.state.FooterComponent = config.FooterComponent; + + this.providersByNamespace = { default: [] }; + for (const provider of config.providers) { + const namespace = provider.namespace || "default"; + if (namespace in this.providersByNamespace) { + this.providersByNamespace[namespace].push(provider); + } else { + this.providersByNamespace[namespace] = [provider]; + } + } + + const { namespace, searchValue } = this.processSearchValue(config.searchValue || ""); + this.switchNamespace(namespace); + this.state.searchValue = searchValue; + await this.race.add(this.search(searchValue)); + } + + /** + * Modifies the commands to be displayed according to the namespace and the options. + * Selects the first command in the new list. + * @param {string} namespace + * @param {object} options + */ + async setCommands(namespace, options = {}) { + this.categoryKeys = ["default"]; + this.categoryNames = {}; + const proms = this.providersByNamespace[namespace].map((provider) => { + const { provide } = provider; + const result = provide(this.env, options); + return result; + }); + let commands = (await this.keepLast.add(Promise.all(proms))).flat(); + const namespaceConfig = this.configByNamespace[namespace] || {}; + if (options.searchValue && FUZZY_NAMESPACES.includes(namespace)) { + commands = fuzzyLookup(options.searchValue, commands, (c) => c.name); + } else { + // we have to sort the commands by category to avoid navigation issues with the arrows + if (namespaceConfig.categories) { + let commandsSorted = []; + this.categoryKeys = namespaceConfig.categories; + this.categoryNames = namespaceConfig.categoryNames || {}; + if (!this.categoryKeys.includes("default")) { + this.categoryKeys.push("default"); + } + for (const category of this.categoryKeys) { + commandsSorted = commandsSorted.concat( + commands.filter(commandsWithinCategory(category, this.categoryKeys)) + ); + } + commands = commandsSorted; + } + } + + this.state.commands = markRaw( + commands.slice(0, 100).map((command) => ({ + ...command, + keyId: this.keyId++, + text: highlightText(options.searchValue, command.name, "fw-bolder text-primary"), + })) + ); + this.selectCommand(this.state.commands.length ? 0 : -1); + this.mouseSelectionActive = false; + this.state.emptyMessage = ( + namespaceConfig.emptyMessage || DEFAULT_EMPTY_MESSAGE + ).toString(); + } + + selectCommand(index) { + if (index === -1 || index >= this.state.commands.length) { + this.state.selectedCommand = null; + return; + } + this.state.selectedCommand = markRaw(this.state.commands[index]); + } + + selectCommandAndScrollTo(type) { + // In case the mouse is on the palette command, it avoids the selection + // of a command caused by a scroll. + this.mouseSelectionActive = false; + const index = this.state.commands.indexOf(this.state.selectedCommand); + if (index === -1) { + return; + } + let nextIndex; + if (type === "NEXT") { + nextIndex = index < this.state.commands.length - 1 ? index + 1 : 0; + } else if (type === "PREV") { + nextIndex = index > 0 ? index - 1 : this.state.commands.length - 1; + } + this.selectCommand(nextIndex); + + const command = this.listboxRef.el.querySelector(`#o_command_${nextIndex}`); + scrollTo(command, { scrollable: this.listboxRef.el }); + } + + onCommandClicked(event, index) { + event.preventDefault(); // Prevent redirect for commands with href + this.selectCommand(index); + const ctrlKey = isMacOS() ? event.metaKey : event.ctrlKey; + this.executeSelectedCommand(ctrlKey); + } + + /** + * Execute the action related to the order. + * If this action returns a config, then we will use it in the command palette, + * otherwise we close the command palette. + * @param {CommandItem} command + */ + async executeCommand(command) { + const config = await command.action(); + if (config) { + this.setCommandPaletteConfig(config); + } else { + this.props.close(); + } + } + + async executeSelectedCommand(ctrlKey) { + await this.searchValuePromise; + const selectedCommand = this.state.selectedCommand; + if (selectedCommand) { + if (!ctrlKey) { + this.executeCommand(selectedCommand); + } else if (selectedCommand.href) { + window.open(selectedCommand.href, "_blank"); + } + } + } + + onCommandMouseEnter(index) { + if (this.mouseSelectionActive) { + this.selectCommand(index); + } else { + this.mouseSelectionActive = true; + } + } + + async search(searchValue) { + this.state.isLoading = true; + try { + await this.setCommands(this.state.namespace, { + searchValue, + activeElement: this.activeElement, + sessionId: this._sessionId, + }); + } finally { + this.state.isLoading = false; + } + if (this.inputRef.el) { + this.inputRef.el.focus(); + } + } + + debounceSearch(value) { + const { namespace, searchValue } = this.processSearchValue(value); + if (namespace !== "default" && this.state.namespace !== namespace) { + this.switchNamespace(namespace); + } + this.state.searchValue = searchValue; + this.searchValuePromise = this.lastDebounceSearch(searchValue).catch(() => { + this.searchValuePromise = null; + }); + } + + onSearchInput(ev) { + this.debounceSearch(ev.target.value); + } + + onKeyDown(ev) { + if (ev.key.toLowerCase() === "backspace" && !ev.target.value.length && !ev.repeat) { + this.switchNamespace("default"); + this.state.searchValue = ""; + this.searchValuePromise = this.lastDebounceSearch("").catch(() => { + this.searchValuePromise = null; + }); + } + } + + /** + * Close the palette on outside click. + */ + onWindowMouseDown(ev) { + if (!this.root.el.contains(ev.target)) { + this.props.close(); + } + } + + switchNamespace(namespace) { + if (this.lastDebounceSearch) { + this.lastDebounceSearch.cancel(); + } + const namespaceConfig = this.configByNamespace[namespace] || {}; + this.lastDebounceSearch = debounce( + (value) => this.search(value), + namespaceConfig.debounceDelay || 0 + ); + this.state.namespace = namespace; + this.state.placeholder = namespaceConfig.placeholder || DEFAULT_PLACEHOLDER.toString(); + } + + processSearchValue(searchValue) { + let namespace = "default"; + if (searchValue.length && this.providersByNamespace[searchValue[0]]) { + namespace = searchValue[0]; + searchValue = searchValue.slice(1); + } + return { namespace, searchValue }; + } + + get isMacOS() { + return isMacOS(); + } + get isMobileOS() { + return isMobileOS(); + } +} diff --git a/frontend/web/static/src/core/commands/command_palette.scss b/frontend/web/static/src/core/commands/command_palette.scss new file mode 100644 index 0000000..4736e80 --- /dev/null +++ b/frontend/web/static/src/core/commands/command_palette.scss @@ -0,0 +1,53 @@ +.o_command_palette { + $-app-icon-size: 1.8rem; + top: 120px; + position: absolute; + + > .modal-body { + padding: 0; + } + + &_listbox { + max-height: 50vh; + + .o_command { + &.focused { + background: rgba($o-component-active-bg, .65); + } + + &_hotkey { + align-items: center; + justify-content: space-between; + background-color: inherit; + padding: 0.5rem 1.3em; + display: flex; + } + a { + text-decoration: none; + color: inherit; + } + } + + } + + .o_favorite { + color: $o-main-favorite-color; + } + + .o_app_icon { + height: $-app-icon-size; + width: $-app-icon-size; + } + .o_command{ + cursor: pointer; + .text-ellipsis { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + .o_command_focus { + white-space: nowrap; + opacity: 0.9; + } + } +} diff --git a/frontend/web/static/src/core/commands/command_palette.xml b/frontend/web/static/src/core/commands/command_palette.xml new file mode 100644 index 0000000..6d5f1f0 --- /dev/null +++ b/frontend/web/static/src/core/commands/command_palette.xml @@ -0,0 +1,61 @@ + + + + + +
+ + +
+
+ + + +
+
+
+ + +
+
+
+ +
diff --git a/frontend/web/static/src/core/commands/command_service.js b/frontend/web/static/src/core/commands/command_service.js new file mode 100644 index 0000000..aa87071 --- /dev/null +++ b/frontend/web/static/src/core/commands/command_service.js @@ -0,0 +1,261 @@ +import { registry } from "@web/core/registry"; +import { CommandPalette } from "./command_palette"; + +import { Component, EventBus } from "@odoo/owl"; + +/** + * @typedef {import("./command_palette").CommandPaletteConfig} CommandPaletteConfig + * @typedef {import("../hotkeys/hotkey_service").HotkeyOptions} HotkeyOptions + */ + +/** + * @typedef {{ + * name: string; + * action: ()=>(void | CommandPaletteConfig); + * category?: string; + * href?: string; + * className?: string; + * }} Command + */ + +/** + * @typedef {{ + * category?: string; + * isAvailable?: ()=>(boolean); + * global?: boolean; + * hotkey?: string; + * hotkeyOptions?: HotkeyOptions + * }} CommandOptions + */ + +/** + * @typedef {Command & CommandOptions & { + * removeHotkey?: ()=>void; + * }} CommandRegistration + */ + +const commandCategoryRegistry = registry.category("command_categories"); +const commandProviderRegistry = registry.category("command_provider"); +const commandSetupRegistry = registry.category("command_setup"); + +class DefaultFooter extends Component { + static template = "web.DefaultFooter"; + static props = { + switchNamespace: { type: Function }, + }; + setup() { + this.elements = commandSetupRegistry + .getEntries() + .map((el) => ({ namespace: el[0], name: el[1].name })) + .filter((el) => el.name); + } + + onClick(namespace) { + this.props.switchNamespace(namespace); + } +} + +export const commandService = { + dependencies: ["dialog", "hotkey", "ui"], + start(env, { dialog, hotkey: hotkeyService, ui }) { + /** @type {Map} */ + const registeredCommands = new Map(); + let nextToken = 0; + let isPaletteOpened = false; + const bus = new EventBus(); + + hotkeyService.add("control+k", openMainPalette, { + bypassEditableProtection: true, + global: true, + }); + + /** + * @param {CommandPaletteConfig} config command palette config merged with default config + * @param {Function} onClose called when the command palette is closed + * @returns the actual command palette config if the command palette is already open + */ + function openMainPalette(config = {}, onClose) { + const configByNamespace = {}; + for (const provider of commandProviderRegistry.getAll()) { + const namespace = provider.namespace || "default"; + if (!configByNamespace[namespace]) { + configByNamespace[namespace] = { + categories: [], + categoryNames: {}, + }; + } + } + + for (const [category, el] of commandCategoryRegistry.getEntries()) { + const namespace = el.namespace || "default"; + const name = el.name; + if (namespace in configByNamespace) { + configByNamespace[namespace].categories.push(category); + configByNamespace[namespace].categoryNames[category] = name; + } + } + + for (const [ + namespace, + { emptyMessage, debounceDelay, placeholder }, + ] of commandSetupRegistry.getEntries()) { + if (namespace in configByNamespace) { + if (emptyMessage) { + configByNamespace[namespace].emptyMessage = emptyMessage; + } + if (debounceDelay !== undefined) { + configByNamespace[namespace].debounceDelay = debounceDelay; + } + if (placeholder) { + configByNamespace[namespace].placeholder = placeholder; + } + } + } + + config = Object.assign( + { + configByNamespace, + FooterComponent: DefaultFooter, + providers: commandProviderRegistry.getAll(), + }, + config + ); + return openPalette(config, onClose); + } + + /** + * @param {CommandPaletteConfig} config + * @param {Function} onClose called when the command palette is closed + */ + function openPalette(config, onClose) { + if (isPaletteOpened) { + bus.trigger("SET-CONFIG", config); + return; + } + + // Open Command Palette dialog + isPaletteOpened = true; + dialog.add( + CommandPalette, + { + config, + bus, + }, + { + onClose: () => { + isPaletteOpened = false; + if (onClose) { + onClose(); + } + }, + } + ); + } + + /** + * @param {Command} command + * @param {CommandOptions} options + * @returns {number} token + */ + function registerCommand(command, options) { + if (!command.name || !command.action || typeof command.action !== "function") { + throw new Error("A Command must have a name and an action function."); + } + const registration = Object.assign({}, command, options); + if (registration.identifier) { + const commandsArray = Array.from(registeredCommands.values()); + const sameName = commandsArray.find((com) => com.name === registration.name); + if (sameName) { + if (registration.identifier !== sameName.identifier) { + registration.name += ` (${registration.identifier})`; + sameName.name += ` (${sameName.identifier})`; + } + } else { + const sameFullName = commandsArray.find( + (com) => com.name === registration.name + `(${registration.identifier})` + ); + if (sameFullName) { + registration.name += ` (${registration.identifier})`; + } + } + } + if (registration.hotkey) { + const action = async () => { + const commandService = env.services.command; + const config = await command.action(); + if (!isPaletteOpened && config) { + commandService.openPalette(config); + } + }; + registration.removeHotkey = hotkeyService.add(registration.hotkey, action, { + ...options.hotkeyOptions, + global: registration.global, + isAvailable: (...args) => { + let available = true; + if (registration.isAvailable) { + available = registration.isAvailable(...args); + } + if (available && options.hotkeyOptions?.isAvailable) { + available = options.hotkeyOptions?.isAvailable(...args); + } + return available; + }, + }); + } + + const token = nextToken++; + registeredCommands.set(token, registration); + if (!options.activeElement) { + // Due to the way elements are mounted in the DOM by Owl (bottom-to-top), + // we need to wait the next micro task tick to set the context activate + // element of the subscription. + Promise.resolve().then(() => { + registration.activeElement = ui.activeElement; + }); + } + + return token; + } + + /** + * Unsubscribes the token corresponding subscription. + * + * @param {number} token + */ + function unregisterCommand(token) { + const cmd = registeredCommands.get(token); + if (cmd && cmd.removeHotkey) { + cmd.removeHotkey(); + } + registeredCommands.delete(token); + } + + return { + /** + * @param {string} name + * @param {()=>(void | CommandPaletteConfig)} action + * @param {CommandOptions} [options] + * @returns {() => void} + */ + add(name, action, options = {}) { + const token = registerCommand({ name, action }, options); + return () => { + unregisterCommand(token); + }; + }, + /** + * @param {HTMLElement} activeElement + * @returns {Command[]} + */ + getCommands(activeElement) { + return [...registeredCommands.values()].filter( + (command) => command.activeElement === activeElement || command.global + ); + }, + openMainPalette, + openPalette, + }; + }, +}; + +registry.category("services").add("command", commandService); diff --git a/frontend/web/static/src/core/commands/default_providers.js b/frontend/web/static/src/core/commands/default_providers.js new file mode 100644 index 0000000..8deabfc --- /dev/null +++ b/frontend/web/static/src/core/commands/default_providers.js @@ -0,0 +1,109 @@ +import { isMacOS } from "@web/core/browser/feature_detection"; +import { useHotkey } from "@web/core/hotkeys/hotkey_hook"; +import { _t } from "@web/core/l10n/translation"; +import { registry } from "@web/core/registry"; +import { capitalize } from "@web/core/utils/strings"; +import { getVisibleElements } from "@web/core/utils/ui"; +import { DefaultCommandItem } from "./command_palette"; + +import { Component } from "@odoo/owl"; + +const commandSetupRegistry = registry.category("command_setup"); +commandSetupRegistry.add("default", { + emptyMessage: _t("No command found"), + placeholder: _t("Search for a command..."), +}); + +export class HotkeyCommandItem extends Component { + static template = "web.HotkeyCommandItem"; + static props = ["hotkey", "hotkeyOptions?", "name?", "searchValue?", "executeCommand", "slots"]; + setup() { + useHotkey(this.props.hotkey, this.props.executeCommand); + } + + getKeysToPress(command) { + const { hotkey } = command; + let result = hotkey.split("+"); + if (isMacOS()) { + result = result + .map((x) => x.replace("control", "command")) + .map((x) => x.replace("alt", "control")); + } + return result.map((key) => key.toUpperCase()); + } +} + +const commandCategoryRegistry = registry.category("command_categories"); +const commandProviderRegistry = registry.category("command_provider"); +commandProviderRegistry.add("command", { + provide: (env, options = {}) => { + const commands = env.services.command + .getCommands(options.activeElement) + .map((cmd) => { + cmd.category = commandCategoryRegistry.contains(cmd.category) + ? cmd.category + : "default"; + return cmd; + }) + .filter((command) => command.isAvailable === undefined || command.isAvailable()); + // Filter out same category dupplicate commands + const uniqueCommands = commands.filter((obj, index) => { + return ( + index === + commands.findIndex((o) => obj.name === o.name && obj.category === o.category) + ); + }); + return uniqueCommands.map((command) => ({ + Component: command.hotkey ? HotkeyCommandItem : DefaultCommandItem, + action: command.action, + category: command.category, + name: command.name, + props: { + hotkey: command.hotkey, + hotkeyOptions: command.hotkeyOptions, + }, + })); + }, +}); + +commandProviderRegistry.add("data-hotkeys", { + provide: (env, options = {}) => { + const commands = []; + const overlayModifier = registry.category("services").get("hotkey").overlayModifier; + // Also retrieve all hotkeyables elements + for (const el of getVisibleElements( + options.activeElement, + "[data-hotkey]:not(:disabled)" + )) { + const closest = el.closest("[data-command-category]"); + const category = closest ? closest.dataset.commandCategory : "default"; + if (category === "disabled") { + continue; + } + + const description = + el.title || + el.dataset.bsOriginalTitle || // LEGACY: bootstrap moves title to data-bs-original-title + el.dataset.tooltip || + el.placeholder || + (el.innerText && + `${el.innerText.slice(0, 50)}${el.innerText.length > 50 ? "..." : ""}`) || + _t("no description provided"); + + commands.push({ + Component: HotkeyCommandItem, + action: () => { + // AAB: not sure it is enough, we might need to trigger all events that occur when you actually click + el.focus(); + el.click(); + }, + category, + name: capitalize(description.trim().toLowerCase()), + props: { + hotkey: `${overlayModifier}+${el.dataset.hotkey}`, + }, + }); + } + return commands; + }, +}); diff --git a/frontend/web/static/src/core/confirmation_dialog/confirmation_dialog.js b/frontend/web/static/src/core/confirmation_dialog/confirmation_dialog.js new file mode 100644 index 0000000..2c9b133 --- /dev/null +++ b/frontend/web/static/src/core/confirmation_dialog/confirmation_dialog.js @@ -0,0 +1,103 @@ +import { Dialog } from "../dialog/dialog"; +import { _t } from "@web/core/l10n/translation"; +import { useChildRef } from "@web/core/utils/hooks"; + +import { Component } from "@odoo/owl"; + +export const deleteConfirmationMessage = _t( + `Ready to make your record disappear into thin air? Are you sure? +It will be gone forever! + +Think twice before you click that 'Delete' button!` +); + +export class ConfirmationDialog extends Component { + static template = "web.ConfirmationDialog"; + static components = { Dialog }; + static props = { + close: Function, + title: { + validate: (m) => { + return ( + typeof m === "string" || + (typeof m === "object" && typeof m.toString === "function") + ); + }, + optional: true, + }, + body: { type: String, optional: true }, + confirm: { type: Function, optional: true }, + confirmLabel: { type: String, optional: true }, + confirmClass: { type: String, optional: true }, + cancel: { type: Function, optional: true }, + cancelLabel: { type: String, optional: true }, + dismiss: { type: Function, optional: true }, + }; + static defaultProps = { + confirmLabel: _t("Ok"), + cancelLabel: _t("Cancel"), + confirmClass: "btn-primary", + title: _t("Confirmation"), + }; + + setup() { + this.env.dialogData.dismiss = () => this._dismiss(); + this.modalRef = useChildRef(); + this.isProcess = false; + } + + async _cancel() { + return this.execButton(this.props.cancel); + } + + async _confirm() { + return this.execButton(this.props.confirm); + } + + async _dismiss() { + return this.execButton(this.props.dismiss || this.props.cancel); + } + + setButtonsDisabled(disabled) { + this.isProcess = disabled; + if (!this.modalRef.el) { + return; // safety belt for stable versions + } + for (const button of [...this.modalRef.el.querySelectorAll(".modal-footer button")]) { + button.disabled = disabled; + } + } + + async execButton(callback) { + if (this.isProcess) { + return; + } + this.setButtonsDisabled(true); + if (callback) { + let shouldClose; + try { + shouldClose = await callback(); + } catch (e) { + this.props.close(); + throw e; + } + if (shouldClose === false) { + this.setButtonsDisabled(false); + return; + } + } + this.props.close(); + } +} + +export class AlertDialog extends ConfirmationDialog { + static template = "web.AlertDialog"; + static props = { + ...ConfirmationDialog.props, + contentClass: { type: String, optional: true }, + }; + static defaultProps = { + ...ConfirmationDialog.defaultProps, + title: _t("Alert"), + }; +} diff --git a/frontend/web/static/src/core/confirmation_dialog/confirmation_dialog.xml b/frontend/web/static/src/core/confirmation_dialog/confirmation_dialog.xml new file mode 100644 index 0000000..5ac84b5 --- /dev/null +++ b/frontend/web/static/src/core/confirmation_dialog/confirmation_dialog.xml @@ -0,0 +1,24 @@ + + + + + +

+ +

+
+ + + +

+ +

+
+ +
diff --git a/frontend/web/static/src/core/context.js b/frontend/web/static/src/core/context.js new file mode 100644 index 0000000..bc9ba51 --- /dev/null +++ b/frontend/web/static/src/core/context.js @@ -0,0 +1,85 @@ +import { evaluateExpr, parseExpr } from "./py_js/py"; +import { BUILTINS } from "./py_js/py_builtin"; +import { evaluate } from "./py_js/py_interpreter"; + +/** + * @typedef {{ + * lang?: string; + * tz?: string; + * uid?: number | false; + * [key: string]: any; + * }} Context + * @typedef {Context | string | undefined} ContextDescription + */ + +/** + * Create an evaluated context from an arbitrary list of context representations. + * The evaluated context in construction is used along the way to evaluate further parts. + * + * @param {ContextDescription[]} contexts + * @param {Context} [initialEvaluationContext] optional evaluation context to start from. + * @returns {Context} + */ +export function makeContext(contexts, initialEvaluationContext) { + const evaluationContext = Object.assign({}, initialEvaluationContext); + const context = {}; + for (let ctx of contexts) { + if (ctx !== "") { + ctx = typeof ctx === "string" ? evaluateExpr(ctx, evaluationContext) : ctx; + Object.assign(context, ctx); + Object.assign(evaluationContext, context); // is this behavior really wanted ? + } + } + return context; +} + +/** + * Extract a partial list of variable names found in the AST. + * Note that it is not complete. It is used as an heuristic to avoid + * evaluating expressions that we know for sure will fail. + * + * @param {AST} ast + * @returns string[] + */ +function getPartialNames(ast) { + if (ast.type === 5) { + return [ast.value]; + } + if (ast.type === 6) { + return getPartialNames(ast.right); + } + if (ast.type === 14 || ast.type === 7) { + return getPartialNames(ast.left).concat(getPartialNames(ast.right)); + } + if (ast.type === 15) { + return getPartialNames(ast.obj); + } + return []; +} + +/** + * Allow to evaluate a context with an incomplete evaluation context. The evaluated context only + * contains keys whose values are static or can be evaluated with the given evaluation context. + * + * @param {string} context + * @param {Context} [evaluationContext={}] + * @returns {Context} + */ +export function evalPartialContext(_context, evaluationContext = {}) { + const ast = parseExpr(_context); + const context = {}; + for (const key in ast.value) { + const value = ast.value[key]; + if ( + getPartialNames(value).some((name) => !(name in evaluationContext || name in BUILTINS)) + ) { + continue; + } + try { + context[key] = evaluate(value, evaluationContext); + } catch { + // ignore this key as we can't evaluate its value + } + } + return context; +} diff --git a/frontend/web/static/src/core/copy_button/copy_button.js b/frontend/web/static/src/core/copy_button/copy_button.js new file mode 100644 index 0000000..bce509f --- /dev/null +++ b/frontend/web/static/src/core/copy_button/copy_button.js @@ -0,0 +1,48 @@ +import { browser } from "@web/core/browser/browser"; +import { Tooltip } from "@web/core/tooltip/tooltip"; +import { usePopover } from "@web/core/popover/popover_hook"; +import { Component, useRef } from "@odoo/owl"; + +export class CopyButton extends Component { + static template = "web.CopyButton"; + static props = { + className: { type: String, optional: true }, + copyText: { type: String, optional: true }, + disabled: { type: Boolean, optional: true }, + successText: { type: String, optional: true }, + icon: { type: String, optional: true }, + content: { type: [String, Object, Function], optional: true }, + }; + + setup() { + this.button = useRef("button"); + this.popover = usePopover(Tooltip); + } + + showTooltip() { + this.popover.open(this.button.el, { tooltip: this.props.successText }); + browser.setTimeout(this.popover.close, 800); + } + + async onClick() { + let write, content; + if (typeof this.props.content === "function") { + content = this.props.content(); + } else { + content = this.props.content; + } + // any kind of content can be copied into the clipboard using + // the appropriate native methods + if (typeof content === "string" || content instanceof String) { + write = (value) => browser.navigator.clipboard.writeText(value); + } else { + write = (value) => browser.navigator.clipboard.write(value); + } + try { + await write(content); + } catch (error) { + return browser.console.warn(error); + } + this.showTooltip(); + } +} diff --git a/frontend/web/static/src/core/copy_button/copy_button.xml b/frontend/web/static/src/core/copy_button/copy_button.xml new file mode 100644 index 0000000..1848bd5 --- /dev/null +++ b/frontend/web/static/src/core/copy_button/copy_button.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/frontend/web/static/src/core/currency.js b/frontend/web/static/src/core/currency.js new file mode 100644 index 0000000..07cacf9 --- /dev/null +++ b/frontend/web/static/src/core/currency.js @@ -0,0 +1,98 @@ +import { reactive } from "@odoo/owl"; +import { rpc } from "@web/core/network/rpc"; +import { user } from "@web/core/user"; +import { formatFloat, humanNumber } from "@web/core/utils/numbers"; +import { nbsp } from "@web/core/utils/strings"; +import { session } from "@web/session"; + +export const currencies = session.currencies || {}; +// to make sure code is reading currencies from here +delete session.currencies; + +export function getCurrency(id) { + return currencies[id]; +} + +export async function getCurrencyRates() { + const rates = reactive({}); + + function recordsToRates(records) { + return Object.fromEntries(records.map((r) => [r.id, r.inverse_rate])); + } + + const model = "res.currency"; + const method = "read"; + const url = `/web/dataset/call_kw/${model}/${method}`; + const context = { + ...user.context, + to_currency: user.activeCompany.currency_id, + }; + const params = { + model, + method, + args: [Object.keys(currencies).map(Number), ["inverse_rate"]], + kwargs: { context }, + }; + const records = await rpc(url, params, { + cache: { + type: "disk", + update: "once", + callback: (records, hasChanged) => { + if (hasChanged) { + Object.assign(rates, recordsToRates(records)); + } + }, + }, + }); + Object.assign(rates, recordsToRates(records)); + return rates; +} + +/** + * Returns a string representing a monetary value. The result takes into account + * the user settings (to display the correct decimal separator, currency, ...). + * + * @param {number} value the value that should be formatted + * @param {number} [currencyId] the id of the 'res.currency' to use + * @param {Object} [options] + * additional options to override the values in the python description of the + * field. + * @param {Object} [options.data] a mapping of field names to field values, + * required with options.currencyField + * @param {boolean} [options.noSymbol] this currency has not a sympbol + * @param {boolean} [options.humanReadable] if true, large numbers are formatted + * to a human readable format. + * @param {number} [options.minDigits] see @humanNumber + * @param {boolean} [options.trailingZeros] if false, numbers will have zeros + * to the right of the last non-zero digit hidden + * @param {[number, number]} [options.digits] the number of digits that should + * be used, instead of the default digits precision in the field. The first + * number is always ignored (legacy constraint) + * @param {number} [options.minDigits] the minimum number of decimal digits to display. + * Displays maximum 6 decimal places if no precision is provided. + * @returns {string} + */ +export function formatCurrency(amount, currencyId, options = {}) { + const currency = getCurrency(currencyId); + + const digits = (options.digits !== undefined)? options.digits : (currency && currency.digits) + + let formattedAmount; + if (options.humanReadable) { + formattedAmount = humanNumber(amount, { + decimals: digits ? digits[1] : 2, + minDigits: options.minDigits, + }); + } else { + formattedAmount = formatFloat(amount, { digits, minDigits: options.minDigits, trailingZeros: options.trailingZeros }); + } + + if (!currency || options.noSymbol) { + return formattedAmount; + } + const formatted = [currency.symbol, formattedAmount]; + if (currency.position === "after") { + formatted.reverse(); + } + return formatted.join(nbsp); +} diff --git a/frontend/web/static/src/core/datetime/datetime_input.js b/frontend/web/static/src/core/datetime/datetime_input.js new file mode 100644 index 0000000..cca9cc8 --- /dev/null +++ b/frontend/web/static/src/core/datetime/datetime_input.js @@ -0,0 +1,48 @@ +import { Component } from "@odoo/owl"; +import { omit } from "../utils/objects"; +import { DateTimePicker } from "./datetime_picker"; +import { useDateTimePicker } from "./datetime_picker_hook"; + +/** + * @typedef {import("./datetime_picker").DateTimePickerProps & { + * format?: string; + * id?: string; + * onApply?: (value: DateTime) => any; + * onChange?: (value: DateTime) => any; + * placeholder?: string; + * }} DateTimeInputProps + */ + +const dateTimeInputOwnProps = { + format: { type: String, optional: true }, + id: { type: String, optional: true }, + class: { type: String, optional: true }, + onChange: { type: Function, optional: true }, + onApply: { type: Function, optional: true }, + placeholder: { type: String, optional: true }, + disabled: { type: Boolean, optional: true }, +}; + +/** @extends {Component} */ +export class DateTimeInput extends Component { + static props = { + ...DateTimePicker.props, + ...dateTimeInputOwnProps, + }; + + static template = "web.DateTimeInput"; + + setup() { + const getPickerProps = () => omit(this.props, ...Object.keys(dateTimeInputOwnProps)); + + useDateTimePicker({ + format: this.props.format, + showSeconds: this.props.rounding <= 0, + get pickerProps() { + return getPickerProps(); + }, + onApply: (...args) => this.props.onApply?.(...args), + onChange: (...args) => this.props.onChange?.(...args), + }); + } +} diff --git a/frontend/web/static/src/core/datetime/datetime_input.xml b/frontend/web/static/src/core/datetime/datetime_input.xml new file mode 100644 index 0000000..6135316 --- /dev/null +++ b/frontend/web/static/src/core/datetime/datetime_input.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/frontend/web/static/src/core/datetime/datetime_picker.js b/frontend/web/static/src/core/datetime/datetime_picker.js new file mode 100644 index 0000000..d67e4cb --- /dev/null +++ b/frontend/web/static/src/core/datetime/datetime_picker.js @@ -0,0 +1,646 @@ +import { Component, onWillRender, onWillUpdateProps, useState } from "@odoo/owl"; +import { _t } from "@web/core/l10n/translation"; +import { MAX_VALID_DATE, MIN_VALID_DATE, clampDate, isInRange, today } from "../l10n/dates"; +import { localization } from "../l10n/localization"; +import { ensureArray } from "../utils/arrays"; +import { TimePicker } from "@web/core/time_picker/time_picker"; +import { Time } from "@web/core/l10n/time"; + +const { DateTime, Info } = luxon; + +/** + * @typedef DateItem + * @property {string} id + * @property {boolean} includesToday + * @property {boolean} isOutOfRange + * @property {boolean} isValid + * @property {string} label + * @property {DateRange} range + * @property {string} extraClass + * + * @typedef {"today" | NullableDateTime} DateLimit + * + * @typedef {[DateTime, DateTime]} DateRange + * + * @typedef {luxon["DateTime"]["prototype"]} DateTime + * + * @typedef DateTimePickerProps + * @property {number} [focusedDateIndex=0] + * @property {boolean} [showWeekNumbers=true] + * @property {DaysOfWeekFormat} [daysOfWeekFormat="narrow"] + * @property {DateLimit} [maxDate] + * @property {PrecisionLevel} [maxPrecision="decades"] + * @property {DateLimit} [minDate] + * @property {PrecisionLevel} [minPrecision="days"] + * @property {() => any} [onReset] + * @property {(value: DateTime | DateRange, unit: "date" | "time") => any} [onSelect] + * @property {() => any} [onToggleRange] + * @property {boolean} [range] + * @property {number} [rounding=5] the rounding in minutes, pass 0 to show seconds, pass 1 to avoid + * rounding minutes without displaying seconds. + * @property {() => boolean} [showRangeToggler] + * @property {{ buttons?: any }} [slots] + * @property {"date" | "datetime"} [type] + * @property {NullableDateTime | NullableDateRange} [value] + * @property {(date: DateTime) => boolean} [isDateValid] + * @property {(date: DateTime) => string} [dayCellClass] + * + * @typedef {DateItem | MonthItem} Item + * + * @typedef MonthItem + * @property {[string, string][]} daysOfWeek + * @property {string} id + * @property {number} number + * @property {WeekItem[]} weeks + * + * @typedef {import("@web/core/l10n/dates").NullableDateTime} NullableDateTime + * + * @typedef {import("@web/core/l10n/dates").NullableDateRange} NullableDateRange + * + * @typedef PrecisionInfo + * @property {(date: DateTime, params: Partial) => string} getTitle + * @property {(date: DateTime, params: Partial) => Item[]} getItems + * @property {string} mainTitle + * @property {string} nextTitle + * @property {string} prevTitle + * @property {Record} step + * + * @typedef {"days" | "months" | "years" | "decades"} PrecisionLevel + * + * @typedef {"short" | "narrow"} DaysOfWeekFormat + * + * @typedef WeekItem + * @property {DateItem[]} days + * @property {number} number + */ + +/** + * @param {DateTime} date + */ +const getStartOfDecade = (date) => Math.floor(date.year / 10) * 10; + +/** + * @param {DateTime} date + */ +const getStartOfCentury = (date) => Math.floor(date.year / 100) * 100; + +/** + * @param {DateTime} date + */ +const getStartOfWeek = (date) => { + const { weekStart } = localization; + return date.set({ weekday: date.weekday < weekStart ? weekStart - 7 : weekStart }); +}; + +/** + * @param {number} min + * @param {number} max + */ +const numberRange = (min, max) => [...Array(max - min)].map((_, i) => i + min); + +/** + * @param {NullableDateTime | "today"} value + * @param {NullableDateTime | "today"} defaultValue + */ +const parseLimitDate = (value, defaultValue) => + clampDate(value === "today" ? today() : value || defaultValue, MIN_VALID_DATE, MAX_VALID_DATE); + +/** + * @param {Object} params + * @param {boolean} [params.isOutOfRange=false] + * @param {boolean} [params.isValid=true] + * @param {keyof DateTime} params.label + * @param {string} [params.extraClass] + * @param {[DateTime, DateTime]} params.range + * @returns {DateItem} + */ +const toDateItem = ({ isOutOfRange = false, isValid = true, label, range, extraClass }) => ({ + id: range[0].toISODate(), + includesToday: isInRange(today(), range), + isOutOfRange, + isValid, + label: String(range[0][label]), + range, + extraClass, +}); + +/** + * @param {DateItem[]} weekDayItems + * @returns {WeekItem} + */ +const toWeekItem = (weekDayItems) => ({ + number: weekDayItems[3].range[0].weekNumber, + days: weekDayItems, +}); + +/** + * Precision levels + * @type {Map} + */ +const PRECISION_LEVELS = new Map() + .set("days", { + mainTitle: _t("Select month"), + nextTitle: _t("Next month"), + prevTitle: _t("Previous month"), + step: { month: 1 }, + getTitle: (date) => `${date.monthLong} ${date.year}`, + getItems: (date, { maxDate, minDate, showWeekNumbers, isDateValid, dayCellClass }) => { + const startDates = [date]; + + /** @type {WeekItem[]} */ + const lastWeeks = []; + let shouldAddLastWeek = false; + + const dayItems = startDates.map((date, i) => { + const monthRange = [date.startOf("month"), date.endOf("month")]; + /** @type {WeekItem[]} */ + const weeks = []; + + // Generate 6 weeks for current month + let startOfNextWeek = getStartOfWeek(monthRange[0]); + for (let w = 0; w < WEEKS_PER_MONTH; w++) { + const weekDayItems = []; + // Generate all days of the week + for (let d = 0; d < DAYS_PER_WEEK; d++) { + const day = startOfNextWeek.plus({ day: d }); + const range = [day, day.endOf("day")]; + const dayItem = toDateItem({ + isOutOfRange: !isInRange(day, monthRange), + isValid: isInRange(range, [minDate, maxDate]) && isDateValid?.(day), + label: "day", + range, + extraClass: dayCellClass?.(day) || "", + }); + weekDayItems.push(dayItem); + if (d === DAYS_PER_WEEK - 1) { + startOfNextWeek = day.plus({ day: 1 }); + } + if (w === WEEKS_PER_MONTH - 1) { + shouldAddLastWeek = true; + } + } + + const weekItem = toWeekItem(weekDayItems); + if (w === WEEKS_PER_MONTH - 1) { + lastWeeks.push(weekItem); + } else { + weeks.push(weekItem); + } + } + + // Generate days of week labels + const daysOfWeek = weeks[0].days.map((d) => [ + d.range[0].weekdayShort, + d.range[0].weekdayLong, + Info.weekdays("narrow", { locale: d.range[0].locale })[d.range[0].weekday - 1], + ]); + if (showWeekNumbers) { + daysOfWeek.unshift(["", _t("Week numbers"), ""]); + } + + return { + id: `__month__${i}`, + number: monthRange[0].month, + daysOfWeek, + weeks, + }; + }); + + if (shouldAddLastWeek) { + // Add last empty week item if the other month has an extra week + for (let i = 0; i < dayItems.length; i++) { + dayItems[i].weeks.push(lastWeeks[i]); + } + } + + return dayItems; + }, + }) + .set("months", { + mainTitle: _t("Select year"), + nextTitle: _t("Next year"), + prevTitle: _t("Previous year"), + step: { year: 1 }, + getTitle: (date) => String(date.year), + getItems: (date, { maxDate, minDate }) => { + const startOfYear = date.startOf("year"); + return numberRange(0, 12).map((i) => { + const startOfMonth = startOfYear.plus({ month: i }); + const range = [startOfMonth, startOfMonth.endOf("month")]; + return toDateItem({ + isValid: isInRange(range, [minDate, maxDate]), + label: "monthShort", + range, + }); + }); + }, + }) + .set("years", { + mainTitle: _t("Select decade"), + nextTitle: _t("Next decade"), + prevTitle: _t("Previous decade"), + step: { year: 10 }, + getTitle: (date) => `${getStartOfDecade(date) - 1} - ${getStartOfDecade(date) + 10}`, + getItems: (date, { maxDate, minDate }) => { + const startOfDecade = date.startOf("year").set({ year: getStartOfDecade(date) }); + return numberRange(-GRID_MARGIN, GRID_COUNT + GRID_MARGIN).map((i) => { + const startOfYear = startOfDecade.plus({ year: i }); + const range = [startOfYear, startOfYear.endOf("year")]; + return toDateItem({ + isOutOfRange: i < 0 || i >= GRID_COUNT, + isValid: isInRange(range, [minDate, maxDate]), + label: "year", + range, + }); + }); + }, + }) + .set("decades", { + mainTitle: _t("Select century"), + nextTitle: _t("Next century"), + prevTitle: _t("Previous century"), + step: { year: 100 }, + getTitle: (date) => `${getStartOfCentury(date) - 10} - ${getStartOfCentury(date) + 100}`, + getItems: (date, { maxDate, minDate }) => { + const startOfCentury = date.startOf("year").set({ year: getStartOfCentury(date) }); + return numberRange(-GRID_MARGIN, GRID_COUNT + GRID_MARGIN).map((i) => { + const startOfDecade = startOfCentury.plus({ year: i * 10 }); + const range = [startOfDecade, startOfDecade.plus({ year: 10, millisecond: -1 })]; + return toDateItem({ + label: "year", + isOutOfRange: i < 0 || i >= GRID_COUNT, + isValid: isInRange(range, [minDate, maxDate]), + range, + }); + }); + }, + }); + +// Other constants +const GRID_COUNT = 10; +const GRID_MARGIN = 1; +const NULLABLE_DATETIME_PROPERTY = [DateTime, { value: false }, { value: null }]; + +const DAYS_PER_WEEK = 7; +const WEEKS_PER_MONTH = 6; + +/** @extends {Component} */ +export class DateTimePicker extends Component { + static props = { + focusedDateIndex: { type: Number, optional: true }, + showWeekNumbers: { type: Boolean, optional: true }, + daysOfWeekFormat: { type: String, optional: true }, + maxDate: { type: [NULLABLE_DATETIME_PROPERTY, { value: "today" }], optional: true }, + maxPrecision: { + type: [...PRECISION_LEVELS.keys()].map((value) => ({ value })), + optional: true, + }, + minDate: { type: [NULLABLE_DATETIME_PROPERTY, { value: "today" }], optional: true }, + minPrecision: { + type: [...PRECISION_LEVELS.keys()].map((value) => ({ value })), + optional: true, + }, + onReset: { type: Function, optional: true }, + onSelect: { type: Function, optional: true }, + onToggleRange: { type: Function, optional: true }, + range: { type: Boolean, optional: true }, + rounding: { type: Number, optional: true }, + showRangeToggler: { type: Boolean, optional: true }, + slots: { + type: Object, + shape: { buttons: { type: Object, optional: true } }, + optional: true, + }, + type: { type: [{ value: "date" }, { value: "datetime" }], optional: true }, + value: { + type: [ + NULLABLE_DATETIME_PROPERTY, + { type: Array, element: NULLABLE_DATETIME_PROPERTY }, + ], + optional: true, + }, + isDateValid: { type: Function, optional: true }, + dayCellClass: { type: Function, optional: true }, + tz: { type: String, optional: true }, + }; + + static defaultProps = { + focusedDateIndex: 0, + daysOfWeekFormat: "narrow", + maxPrecision: "decades", + minPrecision: "days", + rounding: 5, + showWeekNumbers: true, + type: "datetime", + }; + + static template = "web.DateTimePicker"; + static components = { TimePicker }; + + //------------------------------------------------------------------------- + // Getters + //------------------------------------------------------------------------- + + get activePrecisionLevel() { + return PRECISION_LEVELS.get(this.state.precision); + } + + get isLastPrecisionLevel() { + return ( + this.allowedPrecisionLevels.indexOf(this.state.precision) === + this.allowedPrecisionLevels.length - 1 + ); + } + + get titles() { + return ensureArray(this.title); + } + + //------------------------------------------------------------------------- + // Lifecycle + //------------------------------------------------------------------------- + + setup() { + /** @type {PrecisionLevel[]} */ + this.allowedPrecisionLevels = []; + /** @type {Item[]} */ + this.items = []; + this.title = ""; + this.shouldAdjustFocusDate = false; + + this.state = useState({ + /** @type {DateTime | null} */ + focusDate: null, + /** @type {DateTime | null} */ + hoveredDate: null, + /** @type {Time[]} */ + timeValues: [], + /** @type {PrecisionLevel} */ + precision: this.props.minPrecision, + }); + + this.onPropsUpdated(this.props); + onWillUpdateProps((nextProps) => this.onPropsUpdated(nextProps)); + + onWillRender(() => this.onWillRender()); + } + + /** + * @param {DateTimePickerProps} props + */ + onPropsUpdated(props) { + /** @type {[NullableDateTime] | NullableDateRange} */ + this.values = ensureArray(props.value).map((value) => + value && !value.isValid ? null : value + ); + this.allowedPrecisionLevels = this.filterPrecisionLevels( + props.minPrecision, + props.maxPrecision + ); + + this.maxDate = parseLimitDate(props.maxDate, MAX_VALID_DATE); + this.minDate = parseLimitDate(props.minDate, MIN_VALID_DATE); + if (this.props.type === "date") { + this.maxDate = this.maxDate.endOf("day"); + this.minDate = this.minDate.startOf("day"); + } + + if (this.maxDate < this.minDate) { + throw new Error(`DateTimePicker error: given "maxDate" comes before "minDate".`); + } + + this.state.timeValues = this.getTimeValues(props); + this.shouldAdjustFocusDate = !props.range; + this.adjustFocus(this.values, props.focusedDateIndex); + } + + onWillRender() { + const { dayCellClass, focusedDateIndex, isDateValid, range, showWeekNumbers } = this.props; + const { focusDate, hoveredDate } = this.state; + const precision = this.activePrecisionLevel; + const getterParams = { + maxDate: this.maxDate, + minDate: this.minDate, + showWeekNumbers: showWeekNumbers ?? !range, + isDateValid, + dayCellClass, + }; + + this.title = precision.getTitle(focusDate); + this.items = precision.getItems(focusDate, getterParams); + + this.selectedRange = [...this.values]; + if (range && focusedDateIndex > 0 && (!this.values[1] || hoveredDate > this.values[0])) { + this.selectedRange[1] = hoveredDate; + } + } + + //------------------------------------------------------------------------- + // Methods + //------------------------------------------------------------------------- + + /** + * @param {NullableDateTime[]} values + * @param {number} focusedDateIndex + */ + adjustFocus(values, focusedDateIndex) { + if (!this.shouldAdjustFocusDate && this.state.focusDate) { + return; + } + + const dateToFocus = + values[focusedDateIndex] || values[focusedDateIndex === 1 ? 0 : 1] || today(); + + this.shouldAdjustFocusDate = false; + this.state.focusDate = this.clamp(dateToFocus.startOf("month")); + } + + /** + * @param {DateTime} value + */ + clamp(value) { + return clampDate(value, this.minDate, this.maxDate); + } + + /** + * @param {PrecisionLevel} minPrecision + * @param {PrecisionLevel} maxPrecision + */ + filterPrecisionLevels(minPrecision, maxPrecision) { + const levels = [...PRECISION_LEVELS.keys()]; + return levels.slice(levels.indexOf(minPrecision), levels.indexOf(maxPrecision) + 1); + } + + /** + * Returns various flags indicating what ranges the current date item belongs + * to. Note that these ranges are computed differently according to the current + * value mode (range or single date). This is done to simplify CSS selectors. + * - Selected Range: + * > range: current values with hovered date applied + * > single date: just the hovered date + * - Highlighted Range: + * > range: union of selection range and current values + * > single date: just the current value + * - Current Range (range only): + * > range: current start date or current end date. + * @param {DateItem} item + */ + getActiveRangeInfo({ range }) { + const result = { + isSelected: isInRange(this.selectedRange, range), + isSelectStart: false, + isSelectEnd: false, + isHighlighted: isInRange(this.state.hoveredDate, range), + }; + + if (this.props.range) { + if (result.isSelected) { + const [selectStart, selectEnd] = this.selectedRange.sort(); + result.isSelectStart = !selectStart || isInRange(selectStart, range); + result.isSelectEnd = !selectEnd || isInRange(selectEnd, range); + } + } else { + result.isSelectStart = result.isSelectEnd = result.isSelected; + } + + return result; + } + + /** + * @param {DateTimePickerProps} props + */ + getTimeValues(props) { + const timeValues = this.values.map( + (val, index) => + new Time({ + hour: + index === 1 && !this.values[1] + ? (val || DateTime.local()).hour + 1 + : (val || DateTime.local()).hour, + minute: val?.minute || 0, + second: val?.second || 0, + }) + ); + + if (props.range) { + return timeValues; + } else { + const values = []; + values[props.focusedDateIndex] = timeValues[props.focusedDateIndex]; + return values; + } + } + + /** + * @param {DateItem} item + */ + isSelectedDate({ range }) { + return this.values.some((value) => isInRange(value, range)); + } + + /** + * Goes to the next panel (e.g. next month if precision is "days"). + * If an event is given it will be prevented. + * @param {PointerEvent} ev + */ + next(ev) { + ev.preventDefault(); + const { step } = this.activePrecisionLevel; + this.state.focusDate = this.clamp(this.state.focusDate.plus(step)); + } + + /** + * Goes to the previous panel (e.g. previous month if precision is "days"). + * If an event is given it will be prevented. + * @param {PointerEvent} ev + */ + previous(ev) { + ev.preventDefault(); + const { step } = this.activePrecisionLevel; + this.state.focusDate = this.clamp(this.state.focusDate.minus(step)); + } + + /** + * @param {number} valueIndex + * @param {Time} newTime + */ + onTimeChange(valueIndex, newTime) { + this.state.timeValues[valueIndex] = newTime; + const value = this.values[valueIndex] || today(); + this.validateAndSelect(value, valueIndex, "time"); + } + + /** + * @param {DateTime} value + * @param {number} valueIndex + * @param {"date" | "time"} unit + */ + validateAndSelect(value, valueIndex, unit) { + if (!this.props.onSelect) { + // No onSelect handler + return false; + } + + const result = [...this.values]; + result[valueIndex] = value; + + if (this.props.type === "datetime") { + // Adjusts result according to the current time values + const { hour, minute, second } = this.state.timeValues[valueIndex]; + result[valueIndex] = result[valueIndex].set({ hour, minute, second }); + } + if (!isInRange(result[valueIndex], [this.minDate, this.maxDate])) { + // Date is outside range defined by min and max dates + return false; + } + this.props.onSelect(result.length === 2 ? result : result[0], unit); + return true; + } + + /** + * Returns whether the zoom has occurred + * @param {DateTime} date + */ + zoomIn(date) { + const index = this.allowedPrecisionLevels.indexOf(this.state.precision) - 1; + if (index in this.allowedPrecisionLevels) { + this.state.focusDate = this.clamp(date); + this.state.precision = this.allowedPrecisionLevels[index]; + return true; + } + return false; + } + + /** + * Returns whether the zoom has occurred + */ + zoomOut() { + const index = this.allowedPrecisionLevels.indexOf(this.state.precision) + 1; + if (index in this.allowedPrecisionLevels) { + this.state.precision = this.allowedPrecisionLevels[index]; + return true; + } + return false; + } + + /** + * Happens when a date item is selected: + * - first tries to zoom in on the item + * - if could not zoom in: date is considered as final value and triggers a hard select + * @param {DateItem} dateItem + */ + zoomOrSelect(dateItem) { + if (!dateItem.isValid) { + // Invalid item + return; + } + if (this.zoomIn(dateItem.range[0])) { + // Zoom was successful + return; + } + const [value] = dateItem.range; + const valueIndex = this.props.focusedDateIndex; + const isValid = this.validateAndSelect(value, valueIndex, "date"); + this.shouldAdjustFocusDate = isValid && !this.props.range; + } +} diff --git a/frontend/web/static/src/core/datetime/datetime_picker.scss b/frontend/web/static/src/core/datetime/datetime_picker.scss new file mode 100644 index 0000000..3f7ec1f --- /dev/null +++ b/frontend/web/static/src/core/datetime/datetime_picker.scss @@ -0,0 +1,92 @@ +.o_datetime_picker { + --DateTimePicker__Template-rows: 3; + --DateTimePicker__Template-columns: 4; + --DateTimePicker__Day-template-rows: 6; + + width: $o-datetime-picker-width; + + // Day + .o_selected { + color: $o-component-active-color; + background: $o-component-active-bg; + } + + .o_select_start, + .o_select_end { + --selected-day-color: #{mix(lighten($o-component-active-border, 10%), $o-component-active-bg, 15%)}; + --percent: calc(100% / sqrt(2)); + background: + #{$o-component-active-bg} + radial-gradient( + circle, + var(--selected-day-color) 0% var(--percent), + transparent var(--percent) 100% + ) + ; + } + + .o_select_start{ + border-top-left-radius: 50%; + border-bottom-left-radius: 50%; + } + + .o_select_end { + border-top-right-radius: 50%; + border-bottom-right-radius: 50%; + } + + .o_today > div { + aspect-ratio: 1; + background-color: $o-calendar-today-background-color; + color: $o-calendar-today-color; + } + + // Grids + + .o_date_picker { + grid-template-rows: repeat(var(--DateTimePicker__Day-template-rows), 1fr); + grid-template-columns: repeat(var(--DateTimePicker__Day-template-columns), 1fr); + } + + .o_date_item_picker { + grid-template-rows: repeat(var(--DateTimePicker__Template-rows), 1fr); + grid-template-columns: repeat(var(--DateTimePicker__Template-columns), 1fr); + } + + // Utilities + + .o_date_item_picker .o_datetime_button { + &.o_selected:not(.o_select_start, .o_select_end) { + background: $o-component-active-bg; + color: $o-component-active-color; + } + } + + .o_center { + display: grid; + place-items: center; + } + + .o_date_item_cell { + aspect-ratio: 1; + position: relative; + + &:hover, &:focus { + --DateTimePicker__date-cell-border-color-hover: #{$o-component-active-border}; + } + + &:not([disabled])::before { + @include o-position-absolute(0,0,0,0); + + content: ''; + aspect-ratio: 1; + border: $border-width solid var(--DateTimePicker__date-cell-border-color-hover); + border-radius: $border-radius-pill; + pointer-events: none; + } + } + + .o_week_number_cell { + font-variant: tabular-nums; + } +} diff --git a/frontend/web/static/src/core/datetime/datetime_picker.xml b/frontend/web/static/src/core/datetime/datetime_picker.xml new file mode 100644 index 0000000..fef537f --- /dev/null +++ b/frontend/web/static/src/core/datetime/datetime_picker.xml @@ -0,0 +1,153 @@ + + + +
+ +
+ +
+
+
+ + + +
+ + + +
+
+ +
+
+
+
+
+
+
+
+
+ + + +
+ +
+
+ + +
+ + +
+ +
+
+
+
+ + +
+ + + + + + + +
+
+ diff --git a/frontend/web/static/src/core/datetime/datetime_picker_hook.js b/frontend/web/static/src/core/datetime/datetime_picker_hook.js new file mode 100644 index 0000000..d30a641 --- /dev/null +++ b/frontend/web/static/src/core/datetime/datetime_picker_hook.js @@ -0,0 +1,36 @@ +import { onWillDestroy, useRef } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; + +/** + * @typedef {import("./datetimepicker_service").DateTimePickerServiceParams & { + * endDateRefName?: string; + * startDateRefName?: string; + * }} DateTimePickerHookParams + */ + +/** + * @param {DateTimePickerHookParams} params + */ +export function useDateTimePicker(params) { + function getInputs() { + return inputRefs.map((ref) => ref.el); + } + + const inputRefs = [ + useRef(params.startDateRefName || "start-date"), + useRef(params.endDateRefName || "end-date"), + ]; + + // Need original object since 'pickerProps' (or any other param) can be defined + // as getters + const serviceParams = Object.assign(Object.create(params), { + getInputs, + useOwlHooks: true, + }); + + const picker = useService("datetime_picker").create(serviceParams); + onWillDestroy(() => { + picker.disable(); + }); + return picker; +} diff --git a/frontend/web/static/src/core/datetime/datetime_picker_popover.js b/frontend/web/static/src/core/datetime/datetime_picker_popover.js new file mode 100644 index 0000000..53c1923 --- /dev/null +++ b/frontend/web/static/src/core/datetime/datetime_picker_popover.js @@ -0,0 +1,31 @@ +import { Component } from "@odoo/owl"; +import { useHotkey } from "../hotkeys/hotkey_hook"; +import { DateTimePicker } from "./datetime_picker"; + +/** + * @typedef {import("./datetime_picker").DateTimePickerProps} DateTimePickerProps + * + * @typedef DateTimePickerPopoverProps + * @property {() => void} close + * @property {DateTimePickerProps} pickerProps + */ + +/** @extends {Component} */ +export class DateTimePickerPopover extends Component { + static components = { DateTimePicker }; + + static props = { + close: Function, // Given by the Popover service + pickerProps: { type: Object, shape: DateTimePicker.props }, + }; + + static template = "web.DateTimePickerPopover"; + + //------------------------------------------------------------------------- + // Lifecycle + //------------------------------------------------------------------------- + + setup() { + useHotkey("enter", () => this.props.close()); + } +} diff --git a/frontend/web/static/src/core/datetime/datetime_picker_popover.xml b/frontend/web/static/src/core/datetime/datetime_picker_popover.xml new file mode 100644 index 0000000..e24333d --- /dev/null +++ b/frontend/web/static/src/core/datetime/datetime_picker_popover.xml @@ -0,0 +1,26 @@ + + + + + +
+ + +
+
+
+
+
diff --git a/frontend/web/static/src/core/datetime/datetimepicker_service.js b/frontend/web/static/src/core/datetime/datetimepicker_service.js new file mode 100644 index 0000000..afdf042 --- /dev/null +++ b/frontend/web/static/src/core/datetime/datetimepicker_service.js @@ -0,0 +1,551 @@ +import { markRaw, onPatched, onWillRender, reactive, useEffect, useRef } from "@odoo/owl"; +import { areDatesEqual, formatDate, formatDateTime, parseDate, parseDateTime } from "../l10n/dates"; +import { makePopover } from "../popover/popover_hook"; +import { registry } from "../registry"; +import { ensureArray, zip, zipWith } from "../utils/arrays"; +import { shallowEqual } from "../utils/objects"; +import { DateTimePicker } from "./datetime_picker"; +import { DateTimePickerPopover } from "./datetime_picker_popover"; + +/** + * @typedef {luxon["DateTime"]["prototype"]} DateTime + * + * @typedef {import("./datetime_picker").DateTimePickerProps} DateTimePickerProps + * @typedef {import("../popover/popover_hook").PopoverHookReturnType} PopoverHookReturnType + * @typedef {import("../popover/popover_service").PopoverServiceAddOptions} PopoverServiceAddOptions + * @typedef {import("@odoo/owl").Component} Component + * @typedef {ReturnType} OwlRef + * + * @typedef {{ + * createPopover?: (component: Component, options: PopoverServiceAddOptions) => PopoverHookReturnType; + * ensureVisibility?: () => boolean; + * format?: string; + * getInputs?: () => HTMLElement[]; + * onApply?: (value: DateTimePickerProps["value"]) => any; + * onChange?: (value: DateTimePickerProps["value"]) => any; + * onClose?: () => any; + * pickerProps?: DateTimePickerProps; + * showSeconds?: boolean; + * target: HTMLElement | string; + * useOwlHooks?: boolean; + * }} DateTimePickerServiceParams + */ + +/** + * @template {object} T + * @param {T} obj + */ +function markValuesRaw(obj) { + /** @type {T} */ + const copy = {}; + for (const [key, value] of Object.entries(obj)) { + if (value && typeof value === "object") { + copy[key] = markRaw(value); + } else { + copy[key] = value; + } + } + return copy; +} + +/** + * @param {Record} props + */ +function stringifyProps(props) { + const copy = {}; + for (const [key, value] of Object.entries(props)) { + copy[key] = JSON.stringify(value); + } + return copy; +} + +const FOCUS_CLASSNAME = "text-primary"; + +const formatters = { + date: formatDate, + datetime: formatDateTime, +}; +const listenedElements = new WeakSet(); +const parsers = { + date: parseDate, + datetime: parseDateTime, +}; + +export const datetimePickerService = { + dependencies: ["popover"], + start(env, { popover: popoverService }) { + const dateTimePickerList = new Set(); + return { + /** + * @param {DateTimePickerServiceParams} [params] + */ + create(params = {}) { + /** + * Wrapper method on the "onApply" callback to only call it when the + * value has changed, and set other internal variables accordingly. + */ + async function apply() { + const { value } = pickerProps; + const stringValue = JSON.stringify(value); + if ( + stringValue === lastAppliedStringValue || + stringValue === stringProps.value + ) { + return; + } + + lastAppliedStringValue = stringValue; + inputsChanged = ensureArray(value).map(() => false); + + await params.onApply?.(value); + + stringProps.value = stringValue; + } + + function enable() { + for (const [el, value] of zip( + getInputs(), + ensureArray(pickerProps.value), + true + )) { + updateInput(el, value); + if (el && !el.disabled && !el.readOnly && !listenedElements.has(el)) { + listenedElements.add(el); + el.addEventListener("change", onInputChange); + el.addEventListener("click", onInputClick); + el.addEventListener("focus", onInputFocus); + el.addEventListener("keydown", onInputKeydown); + } + } + const calendarIconGroupEl = getInput(0)?.parentElement.querySelector( + ".o_input_group_date_icon" + ); + if (calendarIconGroupEl) { + calendarIconGroupEl.classList.add("cursor-pointer"); + calendarIconGroupEl.addEventListener("click", () => open(0)); + } + return () => {}; + } + + /** + * Ensures the current focused input (indicated by `pickerProps.focusedDateIndex`) + * is actually focused. + */ + function focusActiveInput() { + const inputEl = getInput(pickerProps.focusedDateIndex); + if (!inputEl) { + shouldFocus = true; + return; + } + + const { activeElement } = inputEl.ownerDocument; + if (activeElement !== inputEl) { + inputEl.focus(); + } + setInputFocus(inputEl); + } + + /** + * @param {number} valueIndex + * @returns {HTMLInputElement | null} + */ + function getInput(valueIndex) { + const el = getInputs()[valueIndex]; + if (el?.isConnected) { + return el; + } + return null; + } + + /** + * Returns the appropriate root element to attach the popover: + * - if the value is a range: the closest common parent of the two inputs + * - if not: the first input + */ + function getPopoverTarget() { + const target = getTarget(); + if (target) { + return target; + } + if (pickerProps.range) { + let parentElement = getInput(0).parentElement; + const inputEls = getInputs(); + while ( + parentElement && + !inputEls.every((inputEl) => parentElement.contains(inputEl)) + ) { + parentElement = parentElement.parentElement; + } + return parentElement || getInput(0); + } else { + return getInput(0); + } + } + + function getTarget() { + return targetRef ? targetRef.el : params.target; + } + + function isOpen() { + return popover.isOpen; + } + + /** + * Inputs "change" event handler. This will trigger an "onApply" callback if + * one of the following is true: + * - there is only one input; + * - the popover is closed; + * - the other input has also changed. + * + * @param {Event} ev + */ + function onInputChange(ev) { + updateValueFromInputs(); + inputsChanged[ev.target === getInput(1) ? 1 : 0] = true; + if (!isOpen() || inputsChanged.every(Boolean)) { + saveAndClose(); + } + } + + /** + * @param {PointerEvent} ev + */ + function onInputClick({ target }) { + open(target === getInput(1) ? 1 : 0); + } + + /** + * @param {FocusEvent} ev + */ + function onInputFocus({ target }) { + pickerProps.focusedDateIndex = target === getInput(1) ? 1 : 0; + setInputFocus(target); + } + + /** + * @param {KeyboardEvent} ev + */ + function onInputKeydown(ev) { + if (ev.key == "Enter" && ev.ctrlKey) { + ev.preventDefault(); + updateValueFromInputs(); + return open(ev.target === getInput(1) ? 1 : 0); + } + switch (ev.key) { + case "Enter": + case "Escape": { + return saveAndClose(); + } + case "Tab": { + if ( + !getInput(0) || + !getInput(1) || + ev.target !== getInput(ev.shiftKey ? 1 : 0) + ) { + return saveAndClose(); + } + } + } + } + + /** + * @param {number} inputIndex Input from which to open the picker + */ + function open(inputIndex) { + pickerProps.focusedDateIndex = inputIndex; + + if (!isOpen()) { + const popoverTarget = getPopoverTarget(); + if (ensureVisibility()) { + const { marginBottom } = popoverTarget.style; + // Adds enough space for the popover to be displayed below the target + // even on small screens. + popoverTarget.style.marginBottom = `100vh`; + popoverTarget.scrollIntoView(true); + restoreTargetMargin = async () => { + popoverTarget.style.marginBottom = marginBottom; + }; + } + for (const picker of dateTimePickerList) { + picker.close(); + } + popover.open(popoverTarget, { pickerProps }); + } + + focusActiveInput(); + } + + /** + * @template {"format" | "parse"} T + * @param {T} operation + * @param {T extends "format" ? DateTime : string} value + * @returns {[T extends "format" ? string : DateTime, null] | [null, Error]} + */ + function safeConvert(operation, value) { + const { type } = pickerProps; + const convertFn = (operation === "format" ? formatters : parsers)[type]; + const options = { tz: pickerProps.tz, format: params.format }; + if (operation === "format") { + options.showSeconds = params.showSeconds ?? true; + } + try { + return [convertFn(value, options), null]; + } catch (error) { + if (error?.name === "ConversionError") { + return [null, error]; + } else { + throw error; + } + } + } + + /** + * Wrapper method to ensure the "onApply" callback is called, either: + * - by closing the popover (if any); + * - or by directly calling "apply", without updating the values. + */ + function saveAndClose() { + if (isOpen()) { + // apply will be done in the "onClose" callback + popover.close(); + } else { + apply(); + } + } + + /** + * Updates class names on given inputs according to the currently selected input. + * + * @param {HTMLInputElement | null} input + */ + function setFocusClass(input) { + for (const el of getInputs()) { + if (el) { + el.classList.toggle(FOCUS_CLASSNAME, isOpen() && el === input); + } + } + } + + /** + * Applies class names to all inputs according to whether they are focused or not. + * + * @param {HTMLInputElement} inputEl + */ + function setInputFocus(inputEl) { + inputEl.selectionStart = 0; + inputEl.selectionEnd = inputEl.value.length; + + setFocusClass(inputEl); + + shouldFocus = false; + } + + /** + * Synchronizes the given input with the given value. + * + * @param {HTMLInputElement} el + * @param {DateTime} value + */ + function updateInput(el, value) { + if (!el) { + return; + } + const [formattedValue] = safeConvert("format", value); + el.value = formattedValue || ""; + } + + /** + * @param {DateTimePickerProps["value"]} value + * @param {"date" | "time"} unit + * @param {"input" | "picker"} source + */ + function updateValue(value, unit, source) { + if (source === "input" && areDatesEqual(pickerProps.value, value)) { + return; + } + + pickerProps.value = value; + + if (pickerProps.range && unit !== "time" && source === "picker") { + if (!value[0]) { + pickerProps.focusedDateIndex = 0; + } else if ( + pickerProps.focusedDateIndex === 0 || + (value[0] && value[1] && value[1] < value[0]) + ) { + // If selecting either: + // - the first value + // - OR a second value before the first: + // Then: + // - Set the DATE (year + month + day) of all values + // to the one that has been selected. + const { year, month, day } = value[pickerProps.focusedDateIndex]; + for (let i = 0; i < value.length; i++) { + value[i] = value[i] && value[i].set({ year, month, day }); + } + pickerProps.focusedDateIndex = 1; + } else { + // If selecting the second value after the first: + // - simply toggle the focus index + pickerProps.focusedDateIndex = + pickerProps.focusedDateIndex === 1 ? 0 : 1; + } + } + + params.onChange?.(value); + } + + function updateValueFromInputs() { + const values = zipWith( + getInputs(), + ensureArray(pickerProps.value), + (el, currentValue) => { + if (!el || el.tagName?.toLowerCase() !== "input") { + return currentValue; + } + const [parsedValue, error] = safeConvert("parse", el.value); + if (error) { + updateInput(el, currentValue); + return currentValue; + } else { + return parsedValue; + } + } + ); + updateValue(values.length === 2 ? values : values[0], "date", "input"); + } + + const createPopover = + params.createPopover || + function defaultCreatePopover(...args) { + return makePopover(popoverService.add, ...args); + }; + const ensureVisibility = + params.ensureVisibility || + function defaultEnsureVisibility() { + return env.isSmall; + }; + const getInputs = + params.getInputs || + function defaultGetInputs() { + return [getTarget(), null]; + }; + + // Hook variables + + /** @type {DateTimePickerProps} */ + const rawPickerProps = { + ...DateTimePicker.defaultProps, + onReset: () => { + updateValue( + ensureArray(pickerProps.value).length === 2 ? [false, false] : false, + "date", + "picker" + ); + saveAndClose(); + }, + onSelect: (value, unit) => { + value &&= markRaw(value); + updateValue(value, unit, "picker"); + if (!pickerProps.range && pickerProps.type === "date") { + saveAndClose(); + } + }, + ...markValuesRaw(params.pickerProps), + }; + const pickerProps = reactive(rawPickerProps, () => { + // Update inputs + for (const [el, value] of zip( + getInputs(), + ensureArray(pickerProps.value), + true + )) { + if (el) { + updateInput(el, value); + // Apply changes immediately if the popover is already closed. + // Otherwise ´apply()´ will be called later on close. + if (!isOpen()) { + apply(); + } + } + } + + shouldFocus = true; + }); + const popover = createPopover(DateTimePickerPopover, { + async onClose() { + updateValueFromInputs(); + setFocusClass(null); + restoreTargetMargin?.(); + restoreTargetMargin = null; + await apply(); + params.onClose?.(); + }, + }); + + /** @type {boolean[]} */ + let inputsChanged = []; + let lastAppliedStringValue = ""; + /** @type {(() => void) | null} */ + let restoreTargetMargin = null; + let shouldFocus = false; + /** @type {Partial} */ + let stringProps = {}; + /** @type {OwlRef | null} */ + let targetRef = null; + + if (params.useOwlHooks) { + if (typeof params.target === "string") { + targetRef = useRef(params.target); + } + + onWillRender(function computeBasePickerProps() { + const nextProps = markValuesRaw(params.pickerProps); + const oldStringProps = stringProps; + + stringProps = stringifyProps(nextProps); + lastAppliedStringValue = stringProps.value; + + if (shallowEqual(oldStringProps, stringProps)) { + return; + } + + inputsChanged = ensureArray(nextProps.value).map(() => false); + + for (const [key, value] of Object.entries(nextProps)) { + if (!areDatesEqual(pickerProps[key], value)) { + pickerProps[key] = value; + } + } + }); + + useEffect(enable, getInputs); + + // Note: this `onPatched` callback must be called after the `useEffect` since + // the effect may change input values that will be selected by the patch callback. + onPatched(function focusIfNeeded() { + if (isOpen() && shouldFocus) { + focusActiveInput(); + } + }); + } else if (typeof params.target === "string") { + throw new Error( + `datetime picker service error: cannot use target as ref name when not using Owl hooks` + ); + } + const picker = { + enable, + disable: () => dateTimePickerList.delete(picker), + isOpen, + open, + close: () => popover.close(), + state: pickerProps, + }; + dateTimePickerList.add(picker); + return picker; + }, + }; + }, +}; + +registry.category("services").add("datetime_picker", datetimePickerService); diff --git a/frontend/web/static/src/core/debug/debug_context.js b/frontend/web/static/src/core/debug/debug_context.js new file mode 100644 index 0000000..1898182 --- /dev/null +++ b/frontend/web/static/src/core/debug/debug_context.js @@ -0,0 +1,83 @@ +import { user } from "@web/core/user"; +import { registry } from "../registry"; + +import { useEffect, useEnv, useSubEnv } from "@odoo/owl"; +const debugRegistry = registry.category("debug"); + +const getAccessRights = async () => { + const rightsToCheck = { + "ir.ui.view": "write", + "ir.rule": "read", + "ir.model.access": "read", + }; + const proms = Object.entries(rightsToCheck).map(([model, operation]) => { + return user.checkAccessRight(model, operation); + }); + const [canEditView, canSeeRecordRules, canSeeModelAccess] = await Promise.all(proms); + const accessRights = { canEditView, canSeeRecordRules, canSeeModelAccess }; + return accessRights; +}; + +class DebugContext { + constructor(defaultCategories) { + this.categories = new Map(defaultCategories.map((cat) => [cat, [{}]])); + } + + activateCategory(category, context) { + const contexts = this.categories.get(category) || new Set(); + contexts.add(context); + this.categories.set(category, contexts); + + return () => { + contexts.delete(context); + if (contexts.size === 0) { + this.categories.delete(category); + } + }; + } + + async getItems(env) { + const accessRights = await getAccessRights(); + return [...this.categories.entries()] + .flatMap(([category, contexts]) => { + return debugRegistry + .category(category) + .getAll() + .map((factory) => factory(Object.assign({ env, accessRights }, ...contexts))); + }) + .filter(Boolean) + .sort((x, y) => { + const xSeq = x.sequence || 1000; + const ySeq = y.sequence || 1000; + return xSeq - ySeq; + }); + } +} + +const debugContextSymbol = Symbol("debugContext"); +export function createDebugContext({ categories = [] } = {}) { + return { [debugContextSymbol]: new DebugContext(categories) }; +} + +export function useOwnDebugContext({ categories = [] } = {}) { + useSubEnv(createDebugContext({ categories })); +} + +export function useEnvDebugContext() { + const debugContext = useEnv()[debugContextSymbol]; + if (!debugContext) { + throw new Error("There is no debug context available in the current environment."); + } + return debugContext; +} + +export function useDebugCategory(category, context = {}) { + const env = useEnv(); + if (env.debug) { + const debugContext = useEnvDebugContext(); + useEffect( + () => debugContext.activateCategory(category, context), + () => [] + ); + } +} diff --git a/frontend/web/static/src/core/debug/debug_menu.js b/frontend/web/static/src/core/debug/debug_menu.js new file mode 100644 index 0000000..c3c859b --- /dev/null +++ b/frontend/web/static/src/core/debug/debug_menu.js @@ -0,0 +1,61 @@ +import { _t } from "@web/core/l10n/translation"; +import { Dropdown } from "@web/core/dropdown/dropdown"; +import { DropdownItem } from "@web/core/dropdown/dropdown_item"; +import { DebugMenuBasic } from "@web/core/debug/debug_menu_basic"; +import { useCommand } from "@web/core/commands/command_hook"; +import { useService } from "@web/core/utils/hooks"; +import { useEnvDebugContext } from "./debug_context"; + +export class DebugMenu extends DebugMenuBasic { + static components = { Dropdown, DropdownItem }; + static props = {}; + setup() { + super.setup(); + const debugContext = useEnvDebugContext(); + this.command = useService("command"); + useCommand( + _t("Debug tools..."), + async () => { + const items = await debugContext.getItems(this.env); + let index = 0; + const defaultCategories = items + .filter((item) => item.type === "separator") + .map(() => (index += 1)); + const provider = { + async provide() { + const categories = [...defaultCategories]; + let category = categories.shift(); + const result = []; + items.forEach((item) => { + if (item.type === "item") { + result.push({ + name: item.description.toString(), + action: item.callback, + category, + }); + } else if (item.type === "separator") { + category = categories.shift(); + } + }); + return result; + }, + }; + const configByNamespace = { + default: { + categories: defaultCategories, + emptyMessage: _t("No debug command found"), + placeholder: _t("Choose a debug command..."), + }, + }; + const commandPaletteConfig = { + configByNamespace, + providers: [provider], + }; + return commandPaletteConfig; + }, + { + category: "debug", + } + ); + } +} diff --git a/frontend/web/static/src/core/debug/debug_menu.scss b/frontend/web/static/src/core/debug/debug_menu.scss new file mode 100644 index 0000000..073858f --- /dev/null +++ b/frontend/web/static/src/core/debug/debug_menu.scss @@ -0,0 +1,6 @@ +.o_dialog { + .o_debug_manager .dropdown-toggle { + padding: 0 4px; + margin: 2px 10px 2px 0; + } +} diff --git a/frontend/web/static/src/core/debug/debug_menu.xml b/frontend/web/static/src/core/debug/debug_menu.xml new file mode 100644 index 0000000..b533c2d --- /dev/null +++ b/frontend/web/static/src/core/debug/debug_menu.xml @@ -0,0 +1,34 @@ + + + + +
+ + + + + + + + + + + + + + +
+
+ +
diff --git a/frontend/web/static/src/core/debug/debug_menu_basic.js b/frontend/web/static/src/core/debug/debug_menu_basic.js new file mode 100644 index 0000000..4d61a72 --- /dev/null +++ b/frontend/web/static/src/core/debug/debug_menu_basic.js @@ -0,0 +1,44 @@ +import { useEnvDebugContext } from "./debug_context"; +import { Dropdown } from "@web/core/dropdown/dropdown"; +import { DropdownItem } from "@web/core/dropdown/dropdown_item"; +import { _t } from "@web/core/l10n/translation"; +import { groupBy, sortBy } from "@web/core/utils/arrays"; + +import { Component } from "@odoo/owl"; +import { registry } from "@web/core/registry"; + +const debugSectionRegistry = registry.category("debug_section"); + +debugSectionRegistry + .add("record", { label: _t("Record"), sequence: 10 }) + .add("records", { label: _t("Records"), sequence: 10 }) + .add("ui", { label: _t("User Interface"), sequence: 20 }) + .add("security", { label: _t("Security"), sequence: 30 }) + .add("testing", { label: _t("Tours & Testing"), sequence: 40 }) + .add("tools", { label: _t("Tools"), sequence: 50 }); + +export class DebugMenuBasic extends Component { + static template = "web.DebugMenu"; + static components = { + Dropdown, + DropdownItem, + }; + static props = {}; + + setup() { + this.debugContext = useEnvDebugContext(); + } + + async loadGroupedItems() { + const items = await this.debugContext.getItems(this.env); + const sections = groupBy(items, (item) => item.section || ""); + this.sectionEntries = sortBy( + Object.entries(sections), + ([section]) => debugSectionRegistry.get(section, { sequence: 50 }).sequence + ); + } + + getSectionLabel(section) { + return debugSectionRegistry.get(section, { label: section }).label; + } +} diff --git a/frontend/web/static/src/core/debug/debug_menu_items.js b/frontend/web/static/src/core/debug/debug_menu_items.js new file mode 100644 index 0000000..a6b6bdb --- /dev/null +++ b/frontend/web/static/src/core/debug/debug_menu_items.js @@ -0,0 +1,70 @@ +import { _t } from "@web/core/l10n/translation"; +import { browser } from "@web/core/browser/browser"; +import { router } from "@web/core/browser/router"; +import { registry } from "@web/core/registry"; +import { user } from "@web/core/user"; + +function activateTestsAssetsDebugging({ env }) { + if (String(router.current.debug).includes("tests")) { + return; + } + + return { + type: "item", + description: _t("Activate Test Mode"), + callback: () => { + router.pushState({ debug: "assets,tests" }, { reload: true }); + }, + sequence: 580, + section: "tools", + }; +} + +export function regenerateAssets({ env }) { + return { + type: "item", + description: _t("Regenerate Assets"), + callback: async () => { + await env.services.orm.call("ir.attachment", "regenerate_assets_bundles"); + browser.location.reload(); + }, + sequence: 550, + section: "tools", + }; +} + +export function becomeSuperuser({ env }) { + const becomeSuperuserURL = browser.location.origin + "/web/become"; + if (!user.isAdmin) { + return false; + } + return { + type: "item", + description: _t("Become Superuser"), + href: becomeSuperuserURL, + callback: () => { + browser.open(becomeSuperuserURL, "_self"); + }, + sequence: 560, + section: "tools", + }; +} + +function leaveDebugMode() { + return { + type: "item", + description: _t("Leave Debug Mode"), + callback: () => { + router.pushState({ debug: 0 }, { reload: true }); + }, + sequence: 650, + }; +} + +registry + .category("debug") + .category("default") + .add("regenerateAssets", regenerateAssets) + .add("becomeSuperuser", becomeSuperuser) + .add("activateTestsAssetsDebugging", activateTestsAssetsDebugging) + .add("leaveDebugMode", leaveDebugMode); diff --git a/frontend/web/static/src/core/debug/debug_menu_items.xml b/frontend/web/static/src/core/debug/debug_menu_items.xml new file mode 100644 index 0000000..07a71a4 --- /dev/null +++ b/frontend/web/static/src/core/debug/debug_menu_items.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + +
+ + + +
+ + + +
+ + +
+ + +
+ + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID:
XML ID: + + +
+ + + + + / (create) + +
No Update: + + + (change) + +
Creation User:
Creation Date:
Latest Modification by:
Latest Modification Date:
+
+
+ + + +
+            
+                
+            
+        
+
+
diff --git a/frontend/web/static/src/core/debug/debug_providers.js b/frontend/web/static/src/core/debug/debug_providers.js new file mode 100644 index 0000000..09589df --- /dev/null +++ b/frontend/web/static/src/core/debug/debug_providers.js @@ -0,0 +1,56 @@ +import { _t } from "@web/core/l10n/translation"; +import { registry } from "../registry"; +import { browser } from "../browser/browser"; +import { router } from "../browser/router"; + +const commandProviderRegistry = registry.category("command_provider"); + +commandProviderRegistry.add("debug", { + provide: (env, options) => { + const result = []; + if (env.debug) { + if (!env.debug.includes("assets")) { + result.push({ + action() { + router.pushState({ debug: "assets" }, { reload: true }); + }, + category: "debug", + name: _t("Activate debug mode (with assets)"), + }); + } + result.push({ + action() { + router.pushState({ debug: 0 }, { reload: true }); + }, + category: "debug", + name: _t("Deactivate debug mode"), + }); + result.push({ + action() { + browser.open("/web/tests?debug=assets"); + }, + category: "debug", + name: _t("Run Unit Tests"), + }); + } else { + const debugKey = "debug"; + if (options.searchValue.toLowerCase() === debugKey) { + result.push({ + action() { + router.pushState({ debug: "1" }, { reload: true }); + }, + category: "debug", + name: `${_t("Activate debug mode")} (${debugKey})`, + }); + result.push({ + action() { + router.pushState({ debug: "assets" }, { reload: true }); + }, + category: "debug", + name: `${_t("Activate debug mode (with assets)")} (${debugKey})`, + }); + } + } + return result; + }, +}); diff --git a/frontend/web/static/src/core/debug/debug_utils.js b/frontend/web/static/src/core/debug/debug_utils.js new file mode 100644 index 0000000..d62e86a --- /dev/null +++ b/frontend/web/static/src/core/debug/debug_utils.js @@ -0,0 +1,11 @@ +export function editModelDebug(env, title, model, id) { + return env.services.action.doAction({ + res_model: model, + res_id: id, + name: title, + type: "ir.actions.act_window", + views: [[false, "form"]], + view_mode: "form", + target: "current", + }); +} diff --git a/frontend/web/static/src/core/dialog/dialog.js b/frontend/web/static/src/core/dialog/dialog.js new file mode 100644 index 0000000..209c6f1 --- /dev/null +++ b/frontend/web/static/src/core/dialog/dialog.js @@ -0,0 +1,145 @@ +import { useHotkey } from "@web/core/hotkeys/hotkey_hook"; +import { useActiveElement } from "../ui/ui_service"; +import { useForwardRefToParent } from "@web/core/utils/hooks"; +import { Component, onWillDestroy, useChildSubEnv, useExternalListener, useState } from "@odoo/owl"; +import { throttleForAnimation } from "@web/core/utils/timing"; +import { makeDraggableHook } from "../utils/draggable_hook_builder_owl"; + +const useDialogDraggable = makeDraggableHook({ + name: "useDialogDraggable", + onWillStartDrag({ ctx, addCleanup, addStyle, getRect }) { + const { height, width } = getRect(ctx.current.element); + ctx.current.container = document.createElement("div"); + addStyle(ctx.current.container, { + position: "fixed", + top: "0", + bottom: `${70 - height}px`, + left: `${70 - width}px`, + right: `${70 - width}px`, + }); + ctx.current.element.after(ctx.current.container); + addCleanup(() => ctx.current.container.remove()); + }, + onDrop({ ctx, getRect }) { + const { top, left } = getRect(ctx.current.element); + return { + left: left - ctx.current.elementRect.left, + top: top - ctx.current.elementRect.top, + }; + }, +}); + +export class Dialog extends Component { + static template = "web.Dialog"; + static props = { + contentClass: { type: String, optional: true }, + bodyClass: { type: String, optional: true }, + fullscreen: { type: Boolean, optional: true }, + footer: { type: Boolean, optional: true }, + header: { type: Boolean, optional: true }, + size: { + type: String, + optional: true, + validate: (s) => ["sm", "md", "lg", "xl", "fs", "fullscreen"].includes(s), + }, + technical: { type: Boolean, optional: true }, + title: { type: String, optional: true }, + modalRef: { type: Function, optional: true }, + slots: { + type: Object, + shape: { + default: Object, // Content is not optional + header: { type: Object, optional: true }, + footer: { type: Object, optional: true }, + }, + }, + withBodyPadding: { type: Boolean, optional: true }, + onExpand: { type: Function, optional: true }, + }; + static defaultProps = { + contentClass: "", + bodyClass: "", + fullscreen: false, + footer: true, + header: true, + size: "lg", + technical: true, + title: "Odoo", + withBodyPadding: true, + }; + + setup() { + this.modalRef = useForwardRefToParent("modalRef"); + useActiveElement("modalRef"); + this.data = useState(this.env.dialogData); + useHotkey("escape", () => this.onEscape()); + useHotkey( + "control+enter", + () => { + const btns = document.querySelectorAll( + ".o_dialog:not(.o_inactive_modal) .modal-footer button" + ); + const firstVisibleBtn = Array.from(btns).find((btn) => { + const styles = getComputedStyle(btn); + return styles.display !== "none"; + }); + if (firstVisibleBtn) { + firstVisibleBtn.click(); + } + }, + { bypassEditableProtection: true } + ); + this.id = `dialog_${this.data.id}`; + useChildSubEnv({ inDialog: true, dialogId: this.id }); + this.isMovable = this.props.header; + if (this.isMovable) { + this.position = useState({ left: 0, top: 0 }); + useDialogDraggable({ + enable: () => !this.env.isSmall, + ref: this.modalRef, + elements: ".modal-content", + handle: ".modal-header", + ignore: "button, input", + edgeScrolling: { enabled: false }, + onDrop: ({ top, left }) => { + this.position.left += left; + this.position.top += top; + }, + }); + const throttledResize = throttleForAnimation(this.onResize.bind(this)); + useExternalListener(window, "resize", throttledResize); + } + onWillDestroy(() => { + if (this.env.isSmall) { + this.data.scrollToOrigin(); + } + }); + } + + get isFullscreen() { + return this.props.fullscreen || this.env.isSmall; + } + + get contentStyle() { + if (this.isMovable) { + return `top: ${this.position.top}px; left: ${this.position.left}px;`; + } + return ""; + } + + onResize() { + this.position.left = 0; + this.position.top = 0; + } + + onEscape() { + return this.dismiss(); + } + + async dismiss() { + if (this.data.dismiss) { + await this.data.dismiss(); + } + return this.data.close({ dismiss: true }); + } +} diff --git a/frontend/web/static/src/core/dialog/dialog.scss b/frontend/web/static/src/core/dialog/dialog.scss new file mode 100644 index 0000000..5262856 --- /dev/null +++ b/frontend/web/static/src/core/dialog/dialog.scss @@ -0,0 +1,82 @@ +.modal.o_technical_modal { + .modal-content { + .modal-header .modal-title { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + + .modal-footer { + text-align: left; + + button { + margin: 0; // Reset boostrap. + } + + button.o-default-button:not(:only-child) { + display: none; + } + + @include media-breakpoint-down(md) { + .btn { + width: 45%; + text-overflow: ellipsis; + white-space: inherit; + } + } + + } + + @include media-breakpoint-down(sm) { + &.o_modal_full { + .modal-dialog { + margin: 0px; + height: 100%; + + .modal-content { + height: 100%; + border: none; + + .modal-body { + height: 100%; + overflow-y: auto; + } + } + } + } + } +} + +.modal.o_inactive_modal { + z-index: $zindex-modal-backdrop - 1; +} + +.o_dialog > .modal { + display: block; +} + +@include media-breakpoint-up(sm) { + .modal-fs { + width: calc(100% - #{2 * $modal-dialog-margin-y-sm-up}); + max-width: none; + } +} + +@include media-breakpoint-down(md) { + .modal { + &.o_modal_full .modal-content { + .modal-header { + align-items: center; + height: $o-navbar-height; + padding: 0 1rem; + } + + .modal-footer { + @include o-webclient-padding($top: 1rem, $bottom: 0.5rem); + box-shadow: 0 1rem 2rem black; + z-index: 0; + } + } + } +} diff --git a/frontend/web/static/src/core/dialog/dialog.xml b/frontend/web/static/src/core/dialog/dialog.xml new file mode 100644 index 0000000..b032e79 --- /dev/null +++ b/frontend/web/static/src/core/dialog/dialog.xml @@ -0,0 +1,47 @@ + + + + +
+ +
+
+ + + + + + +
diff --git a/frontend/web/static/src/core/dialog/dialog_service.js b/frontend/web/static/src/core/dialog/dialog_service.js new file mode 100644 index 0000000..8dc913d --- /dev/null +++ b/frontend/web/static/src/core/dialog/dialog_service.js @@ -0,0 +1,103 @@ +import { Component, markRaw, reactive, useChildSubEnv, xml } from "@odoo/owl"; +import { registry } from "@web/core/registry"; + +class DialogWrapper extends Component { + static template = xml``; + static props = ["*"]; + setup() { + useChildSubEnv({ dialogData: this.props.subEnv }); + } +} + +/** + * @typedef {{ + * onClose?(): void; + * }} DialogServiceInterfaceAddOptions + */ +/** + * @typedef {{ + * add( + * Component: typeof import("@odoo/owl").Component, + * props: {}, + * options?: DialogServiceInterfaceAddOptions + * ): () => void; + * }} DialogServiceInterface + */ + +export const dialogService = { + dependencies: ["overlay"], + /** @returns {DialogServiceInterface} */ + start(env, { overlay }) { + const stack = []; + let nextId = 0; + + const deactivate = () => { + for (const subEnv of stack) { + subEnv.isActive = false; + } + }; + + const add = (dialogClass, props, options = {}) => { + const id = nextId++; + const close = (params) => remove(params); + const subEnv = reactive({ + id, + close, + isActive: true, + }); + + deactivate(); + stack.push(subEnv); + document.body.classList.add("modal-open"); + let isBeingClosed = false; + + const scrollOrigin = { top: window.scrollY, left: window.scrollX }; + subEnv.scrollToOrigin = () => { + if (!stack.length) { + window.scrollTo(scrollOrigin); + } + }; + + const remove = overlay.add( + DialogWrapper, + { + subComponent: dialogClass, + subProps: markRaw({ ...props, close }), + subEnv, + }, + { + onRemove: async (closeParams) => { + if (isBeingClosed) { + return; + } + isBeingClosed = true; + await options.onClose?.(closeParams); + stack.splice( + stack.findIndex((d) => d.id === id), + 1 + ); + deactivate(); + if (stack.length) { + stack.at(-1).isActive = true; + } else { + document.body.classList.remove("modal-open"); + } + }, + rootId: options.context?.root?.el?.getRootNode()?.host?.id, + } + ); + + return remove; + }; + + function closeAll(params) { + for (const dialog of [...stack].reverse()) { + dialog.close(params); + } + } + + return { add, closeAll }; + }, +}; + +registry.category("services").add("dialog", dialogService); diff --git a/frontend/web/static/src/core/domain.js b/frontend/web/static/src/core/domain.js new file mode 100644 index 0000000..36bb493 --- /dev/null +++ b/frontend/web/static/src/core/domain.js @@ -0,0 +1,426 @@ +import { shallowEqual } from "@web/core/utils/arrays"; +import { evaluate, formatAST, parseExpr } from "./py_js/py"; +import { toPyValue } from "./py_js/py_utils"; +import { escapeRegExp } from "@web/core/utils/strings"; + +/** + * @typedef {import("./py_js/py_parser").AST} AST + * @typedef {[string | 0 | 1, string, any]} Condition + * @typedef {("&" | "|" | "!" | Condition)[]} DomainListRepr + * @typedef {DomainListRepr | string | Domain} DomainRepr + */ + +export class InvalidDomainError extends Error {} + +/** + * Javascript representation of an Odoo domain + */ +export class Domain { + /** + * Combine various domains together with a given operator + * @param {DomainRepr[]} domains + * @param {"AND" | "OR"} operator + * @returns {Domain} + */ + static combine(domains, operator) { + if (domains.length === 0) { + return new Domain([]); + } + const domain1 = domains[0] instanceof Domain ? domains[0] : new Domain(domains[0]); + if (domains.length === 1) { + return domain1; + } + const domain2 = Domain.combine(domains.slice(1), operator); + const result = new Domain([]); + const astValues1 = domain1.ast.value; + const astValues2 = domain2.ast.value; + const op = operator === "AND" ? "&" : "|"; + const combinedAST = { type: 4 /* List */, value: astValues1.concat(astValues2) }; + result.ast = normalizeDomainAST(combinedAST, op); + return result; + } + + /** + * Combine various domains together with `AND` operator + * @param {DomainRepr[]} domains + * @returns {Domain} + */ + static and(domains) { + return Domain.combine(domains, "AND"); + } + + /** + * Combine various domains together with `OR` operator + * @param {DomainRepr[]} domains + * @returns {Domain} + */ + static or(domains) { + return Domain.combine(domains, "OR"); + } + + /** + * Return the negation of the domain + * @returns {Domain} + */ + static not(domain) { + const result = new Domain(domain); + result.ast.value.unshift({ type: 1, value: "!" }); + return result; + } + + /** + * Return a new domain with `neutralized` leaves (for the leaves that are applied on the field that are part of + * keysToRemove). + * @param {DomainRepr} domain + * @param {string[]} keysToRemove + * @return {Domain} + */ + static removeDomainLeaves(domain, keysToRemove) { + function processLeaf(elements, idx, operatorCtx, newDomain) { + const leaf = elements[idx]; + if (leaf.type === 10) { + if (keysToRemove.includes(leaf.value[0].value)) { + if (operatorCtx === "&") { + newDomain.ast.value.push(...Domain.TRUE.ast.value); + } else if (operatorCtx === "|") { + newDomain.ast.value.push(...Domain.FALSE.ast.value); + } + } else { + newDomain.ast.value.push(leaf); + } + return 1; + } else if (leaf.type === 1) { + // Special case to avoid OR ('|') that can never resolve to true + if ( + leaf.value === "|" && + elements[idx + 1].type === 10 && + elements[idx + 2].type === 10 && + keysToRemove.includes(elements[idx + 1].value[0].value) && + keysToRemove.includes(elements[idx + 2].value[0].value) + ) { + newDomain.ast.value.push(...Domain.TRUE.ast.value); + return 3; + } + newDomain.ast.value.push(leaf); + if (leaf.value === "!") { + return 1 + processLeaf(elements, idx + 1, "&", newDomain); + } + const firstLeafSkip = processLeaf(elements, idx + 1, leaf.value, newDomain); + const secondLeafSkip = processLeaf( + elements, + idx + 1 + firstLeafSkip, + leaf.value, + newDomain + ); + return 1 + firstLeafSkip + secondLeafSkip; + } + return 0; + } + + domain = new Domain(domain); + if (domain.ast.value.length === 0) { + return domain; + } + const newDomain = new Domain([]); + processLeaf(domain.ast.value, 0, "&", newDomain); + return newDomain; + } + + /** + * @param {DomainRepr} [descr] + */ + constructor(descr = []) { + if (descr instanceof Domain) { + /** @type {AST} */ + return new Domain(descr.toString()); + } else { + let rawAST; + try { + rawAST = typeof descr === "string" ? parseExpr(descr) : toAST(descr); + } catch (error) { + throw new InvalidDomainError(`Invalid domain representation: ${descr.toString()}`, { + cause: error, + }); + } + this.ast = normalizeDomainAST(rawAST); + } + } + + /** + * Check if the set of records represented by a domain contains a record + * Warning: smart dates (see parseSmartDateInput) are not handled here. + * + * @param {Object} record + * @returns {boolean} + */ + contains(record) { + const expr = evaluate(this.ast, record); + return matchDomain(record, expr); + } + + /** + * @returns {string} + */ + toString() { + return formatAST(this.ast); + } + + /** + * @param {Object} context + * @returns {DomainListRepr} + */ + toList(context) { + return evaluate(this.ast, context); + } + + /** + * Converts the domain into a human-readable format for JSON representation. + * If the domain does not contain any contextual value, it is converted to a list. + * Otherwise, it is returned as a string. + * + * The string format is less readable due to escaped double quotes. + * Example: "[\"&\",[\"user_id\",\"=\",uid],[\"team_id\",\"!=\",false]]" + * @returns {DomainListRepr | string} + */ + toJson() { + try { + // Attempt to evaluate the domain without context + const evaluatedAsList = this.toList({}); + const evaluatedDomain = new Domain(evaluatedAsList); + if (evaluatedDomain.toString() === this.toString()) { + return evaluatedAsList; + } + return this.toString(); + } catch { + // The domain couldn't be evaluated due to contextual values + return this.toString(); + } + } +} + +/** @type {Condition} */ +const TRUE_LEAF = [1, "=", 1]; +/** @type {Condition} */ +const FALSE_LEAF = [0, "=", 1]; +const TRUE_DOMAIN = new Domain([TRUE_LEAF]); +const FALSE_DOMAIN = new Domain([FALSE_LEAF]); + +Domain.TRUE = TRUE_DOMAIN; +Domain.FALSE = FALSE_DOMAIN; + +// ----------------------------------------------------------------------------- +// Helpers +// ----------------------------------------------------------------------------- + +/** + * @param {DomainListRepr} domain + * @returns {AST} + */ +function toAST(domain) { + const elems = domain.map((elem) => { + switch (elem) { + case "!": + case "&": + case "|": + return { type: 1 /* String */, value: elem }; + default: + return { + type: 10 /* Tuple */, + value: elem.map(toPyValue), + }; + } + }); + return { type: 4 /* List */, value: elems }; +} + +/** + * Normalizes a domain + * + * @param {AST} domain + * @param {'&' | '|'} [op] + * @returns {AST} + */ + +function normalizeDomainAST(domain, op = "&") { + if (domain.type !== 4 /* List */) { + if (domain.type === 10 /* Tuple */) { + const value = domain.value; + /* Tuple contains at least one Tuple and optionally string */ + if ( + value.findIndex((e) => e.type === 10) === -1 || + !value.every((e) => e.type === 10 || e.type === 1) + ) { + throw new InvalidDomainError("Invalid domain AST"); + } + } else { + throw new InvalidDomainError("Invalid domain AST"); + } + } + if (domain.value.length === 0) { + return domain; + } + let expected = 1; + for (const child of domain.value) { + switch (child.type) { + case 1 /* String */: + if (child.value === "&" || child.value === "|") { + expected++; + } else if (child.value !== "!") { + throw new InvalidDomainError("Invalid domain AST"); + } + break; + case 4: /* list */ + case 10 /* tuple */: + if (child.value.length === 3) { + expected--; + break; + } + throw new InvalidDomainError("Invalid domain AST"); + default: + throw new InvalidDomainError("Invalid domain AST"); + } + } + const values = domain.value.slice(); + while (expected < 0) { + expected++; + values.unshift({ type: 1 /* String */, value: op }); + } + if (expected > 0) { + throw new InvalidDomainError( + `invalid domain ${formatAST(domain)} (missing ${expected} segment(s))` + ); + } + return { type: 4 /* List */, value: values }; +} + +/** + * @param {Object} record + * @param {Condition | boolean} condition + * @returns {boolean} + */ +function matchCondition(record, condition) { + if (typeof condition === "boolean") { + return condition; + } + const [field, operator, value] = condition; + + if (typeof field === "string") { + const names = field.split("."); + if (names.length >= 2) { + return matchCondition(record[names[0]], [names.slice(1).join("."), operator, value]); + } + } + let likeRegexp, ilikeRegexp; + if (["like", "not like", "ilike", "not ilike"].includes(operator)) { + likeRegexp = new RegExp(`(.*)${escapeRegExp(value).replaceAll("%", "(.*)")}(.*)`, "g"); + ilikeRegexp = new RegExp(`(.*)${escapeRegExp(value).replaceAll("%", "(.*)")}(.*)`, "gi"); + } + const fieldValue = typeof field === "number" ? field : record[field]; + const isNot = operator.startsWith("not "); + switch (operator) { + case "=?": + if ([false, null].includes(value)) { + return true; + } + // eslint-disable-next-line no-fallthrough + case "=": + case "==": + if (Array.isArray(fieldValue) && Array.isArray(value)) { + return shallowEqual(fieldValue, value); + } + return fieldValue === value; + case "!=": + case "<>": + return !matchCondition(record, [field, "=", value]); + case "<": + return fieldValue < value; + case "<=": + return fieldValue <= value; + case ">": + return fieldValue > value; + case ">=": + return fieldValue >= value; + case "in": + case "not in": { + const val = Array.isArray(value) ? value : [value]; + const fieldVal = Array.isArray(fieldValue) ? fieldValue : [fieldValue]; + return Boolean(fieldVal.some((fv) => val.includes(fv))) != isNot; + } + case "like": + case "not like": + if (fieldValue === false) { + return isNot; + } + return Boolean(fieldValue.match(likeRegexp)) != isNot; + case "=like": + case "not =like": + if (fieldValue === false) { + return isNot; + } + return ( + Boolean(new RegExp(escapeRegExp(value).replace(/%/g, ".*")).test(fieldValue)) != + isNot + ); + case "ilike": + case "not ilike": + if (fieldValue === false) { + return isNot; + } + return Boolean(fieldValue.match(ilikeRegexp)) != isNot; + case "=ilike": + case "not =ilike": + if (fieldValue === false) { + return isNot; + } + return ( + Boolean( + new RegExp(escapeRegExp(value).replace(/%/g, ".*"), "i").test(fieldValue) + ) != isNot + ); + case "any": + case "not any": + return true; + case "child_of": + case "parent_of": + return true; + } + throw new InvalidDomainError("could not match domain"); +} + +/** + * @param {Object} record + * @returns {Object} + */ +function makeOperators(record) { + const match = matchCondition.bind(null, record); + return { + "!": (x) => !match(x), + "&": (a, b) => match(a) && match(b), + "|": (a, b) => match(a) || match(b), + }; +} + +/** + * + * @param {Object} record + * @param {DomainListRepr} domain + * @returns {boolean} + */ +function matchDomain(record, domain) { + if (domain.length === 0) { + return true; + } + const operators = makeOperators(record); + const reversedDomain = Array.from(domain).reverse(); + const condStack = []; + for (const item of reversedDomain) { + const operator = typeof item === "string" && operators[item]; + if (operator) { + const operands = condStack.splice(-operator.length); + condStack.push(operator(...operands)); + } else { + condStack.push(item); + } + } + return matchCondition(record, condStack.pop()); +} diff --git a/frontend/web/static/src/core/domain_selector/domain_selector.js b/frontend/web/static/src/core/domain_selector/domain_selector.js new file mode 100644 index 0000000..9479749 --- /dev/null +++ b/frontend/web/static/src/core/domain_selector/domain_selector.js @@ -0,0 +1,156 @@ +import { Component, onWillStart, onWillUpdateProps } from "@odoo/owl"; +import { CheckBox } from "@web/core/checkbox/checkbox"; +import { Domain } from "@web/core/domain"; +import { getDomainDisplayedOperators } from "@web/core/domain_selector/domain_selector_operator_editor"; +import { _t } from "@web/core/l10n/translation"; +import { ModelFieldSelector } from "@web/core/model_field_selector/model_field_selector"; +import { + areEqualTrees, + condition, + connector, + formatValue, +} from "@web/core/tree_editor/condition_tree"; +import { domainFromTree } from "@web/core/tree_editor/domain_from_tree"; +import { TreeEditor } from "@web/core/tree_editor/tree_editor"; +import { getOperatorEditorInfo } from "@web/core/tree_editor/tree_editor_operator_editor"; +import { useService } from "@web/core/utils/hooks"; +import { getDefaultCondition } from "./utils"; + +const ARCHIVED_CONDITION = condition("active", "in", [true, false]); +const ARCHIVED_DOMAIN = `[("active", "in", [True, False])]`; + +export class DomainSelector extends Component { + static template = "web.DomainSelector"; + static components = { TreeEditor, CheckBox }; + static props = { + domain: String, + resModel: String, + className: { type: String, optional: true }, + defaultConnector: { type: [{ value: "&" }, { value: "|" }], optional: true }, + isDebugMode: { type: Boolean, optional: true }, + readonly: { type: Boolean, optional: true }, + update: { type: Function, optional: true }, + debugUpdate: { type: Function, optional: true }, + }; + static defaultProps = { + isDebugMode: false, + readonly: true, + update: () => {}, + }; + + setup() { + this.fieldService = useService("field"); + this.treeProcessor = useService("tree_processor"); + + this.tree = null; + this.showArchivedCheckbox = false; + this.includeArchived = false; + + onWillStart(() => this.onPropsUpdated(this.props)); + onWillUpdateProps((np) => this.onPropsUpdated(np)); + } + + async onPropsUpdated(p) { + let domain; + let isSupported = true; + try { + domain = new Domain(p.domain); + } catch { + isSupported = false; + } + if (!isSupported) { + this.tree = null; + this.showArchivedCheckbox = false; + this.includeArchived = false; + return; + } + + const [tree, { fieldDef: activeFieldDef }] = await Promise.all([ + this.treeProcessor.treeFromDomain(p.resModel, domain, !p.isDebugMode), + this.fieldService.loadFieldInfo(p.resModel, "active"), + ]); + + this.tree = tree; + this.showArchivedCheckbox = this.getShowArchivedCheckBox(Boolean(activeFieldDef), p); + + this.includeArchived = false; + if (this.showArchivedCheckbox) { + if (this.tree.type === "connector" && this.tree.value === "&") { + this.tree.children = this.tree.children.filter((child) => { + if (areEqualTrees(child, ARCHIVED_CONDITION)) { + this.includeArchived = true; + return false; + } + return true; + }); + if (this.tree.children.length === 1) { + this.tree = this.tree.children[0]; + } + } else if (areEqualTrees(this.tree, ARCHIVED_CONDITION)) { + this.includeArchived = true; + this.tree = connector("&"); + } + } + } + + getShowArchivedCheckBox(hasActiveField, props) { + return hasActiveField; + } + + getDefaultCondition(fieldDefs) { + return getDefaultCondition(fieldDefs); + } + + getDefaultOperator(fieldDef) { + return getDomainDisplayedOperators(fieldDef)[0]; + } + + getOperatorEditorInfo(fieldDef) { + const operators = getDomainDisplayedOperators(fieldDef); + return getOperatorEditorInfo(operators, fieldDef); + } + + getPathEditorInfo(resModel, defaultCondition) { + const { isDebugMode } = this.props; + return { + component: ModelFieldSelector, + extractProps: ({ update, value: path }) => ({ + path, + update, + resModel, + isDebugMode, + readonly: false, + }), + isSupported: (path) => [0, 1].includes(path) || typeof path === "string", + defaultValue: () => defaultCondition.path, + stringify: (path) => formatValue(path), + message: _t("Invalid field chain"), + }; + } + + toggleIncludeArchived() { + this.includeArchived = !this.includeArchived; + this.update(this.tree); + } + + resetDomain() { + this.props.update("[]"); + } + + onDomainInput(domain) { + if (this.props.debugUpdate) { + this.props.debugUpdate(domain); + } + } + + onDomainChange(domain) { + this.props.update(domain, true); + } + update(tree) { + const archiveDomain = this.includeArchived ? ARCHIVED_DOMAIN : `[]`; + const domain = tree + ? Domain.and([domainFromTree(tree), archiveDomain]).toString() + : archiveDomain; + this.props.update(domain); + } +} diff --git a/frontend/web/static/src/core/domain_selector/domain_selector.xml b/frontend/web/static/src/core/domain_selector/domain_selector.xml new file mode 100644 index 0000000..347443e --- /dev/null +++ b/frontend/web/static/src/core/domain_selector/domain_selector.xml @@ -0,0 +1,46 @@ + + + + +
+ + + + Include archived + + + + +
+ This domain is not supported. + + + +
+
+ +