├── .circleci └── config.yml ├── .docker_files ├── ava │ ├── setupMoment.js │ └── setupVue.js ├── main │ ├── __init__.py │ ├── __manifest__.py │ └── tests │ │ ├── __init__.py │ │ └── test_installed_modules.py ├── odoo.conf ├── package-lock.json ├── package.json └── test-requirements.txt ├── .dockerignore ├── .gitignore ├── Dockerfile ├── Dockerfile-client ├── LICENSE ├── README.md ├── docker-compose.yml ├── gitoo.yml ├── vue ├── README.md ├── __init__.py ├── __manifest__.py ├── static │ ├── description │ │ └── icon.png │ ├── lib │ │ └── vue.js │ └── src │ │ └── js │ │ ├── QueryBuilder.js │ │ └── getXmlId.js └── views │ └── assets.xml ├── vue_backend ├── README.rst ├── __init__.py ├── __manifest__.py ├── static │ └── description │ │ └── icon.png └── views │ └── assets.xml ├── vue_element_ui ├── README.md ├── __init__.py ├── __manifest__.py ├── i18n │ └── fr.po ├── models │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-35.pyc │ │ └── ir_translation.cpython-35.pyc │ └── ir_translation.py ├── static │ ├── description │ │ ├── icon.png │ │ └── many2many.png │ ├── dist │ │ └── vueElementUI.js │ ├── lib │ │ ├── element-ui.css │ │ ├── element-ui.js │ │ └── fonts │ │ │ ├── element-icons.ttf │ │ │ └── element-icons.woff │ ├── package-lock.json │ ├── package.json │ ├── src │ │ └── js │ │ │ ├── Many2many.vue │ │ │ ├── en.js │ │ │ ├── i18n.js │ │ │ └── main.js │ └── webpack.config.js └── views │ └── assets.xml ├── vue_frontend ├── README.rst ├── __init__.py ├── __manifest__.py ├── static │ └── description │ │ └── icon.png └── views │ └── assets.xml ├── vue_i18n ├── README.rst ├── __init__.py ├── __manifest__.py ├── static │ ├── description │ │ └── icon.png │ ├── lib │ │ ├── deepmerge.js │ │ └── vue-i18n.js │ └── src │ │ └── js │ │ └── i18n_factory.js └── views │ └── assets.xml ├── vue_router ├── README.rst ├── __init__.py ├── __manifest__.py ├── static │ ├── description │ │ └── icon.png │ ├── lib │ │ └── vue-router.js │ └── src │ │ └── js │ │ └── router_factory.js └── views │ └── assets.xml ├── vue_stock_forecast ├── README.rst ├── __init__.py ├── __manifest__.py ├── i18n │ └── fr.po ├── report │ ├── __init__.py │ └── vue_stock_forecast.py ├── static │ ├── description │ │ ├── filters.png │ │ ├── group_by_category.png │ │ ├── icon.png │ │ ├── product_form.png │ │ ├── product_suppliers.png │ │ ├── purchase_order_form.png │ │ ├── purchase_order_line_list.png │ │ ├── reordering_rules_list.png │ │ ├── report.png │ │ ├── report_filtered_by_supplier.png │ │ ├── report_from_purchase_order.png │ │ ├── report_lines_filtered.png │ │ ├── report_links.png │ │ ├── report_min_max.png │ │ ├── report_quotations.png │ │ ├── report_supplier_filter.png │ │ ├── report_with_more_columns.png │ │ ├── report_with_product_variants.png │ │ ├── search_bar.png │ │ ├── stock_moves.png │ │ └── stock_quants.png │ ├── dist │ │ └── vueStockForecast.js │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── css │ │ │ └── stock_forecast.css │ │ └── js │ │ │ ├── StockForecastReport.js │ │ │ ├── StockForecastReport.vue │ │ │ ├── StockForecastTable.vue │ │ │ ├── main.js │ │ │ └── tests │ │ │ ├── snapshots │ │ │ ├── testStockForecastTable.js.md │ │ │ └── testStockForecastTable.js.snap │ │ │ └── testStockForecastTable.js │ └── webpack.config.js ├── tests │ ├── __init__.py │ └── test_stock_forecast_report.py └── views │ ├── assets.xml │ ├── menu.xml │ ├── product.xml │ └── purchase_order.xml ├── vue_stock_forecast_preferred_supplier ├── README.rst ├── __init__.py ├── __manifest__.py ├── models │ ├── __init__.py │ ├── common.py │ ├── product_product.py │ └── product_template.py ├── report │ ├── __init__.py │ └── vue_stock_forecast.py ├── static │ └── description │ │ ├── product_form.png │ │ └── stock_forecast_report.png └── tests │ ├── __init__.py │ └── test_stock_forecast_report.py └── vuex ├── README.rst ├── __init__.py ├── __manifest__.py ├── static ├── description │ └── icon.png ├── lib │ └── vuex.js └── src │ └── js │ └── store.js └── views └── assets.xml /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.0 2 | 3 | quay_io_login: &quay_io_login 4 | name: Login to Quay.io register 5 | command: docker login quay.io -u "${QUAY_USER}" -p "${QUAY_TOKEN}" 6 | 7 | jobs: 8 | tests: 9 | machine: true 10 | steps: 11 | - checkout 12 | 13 | - run: 14 | <<: *quay_io_login 15 | 16 | - run: 17 | name: Build -- Init Database 18 | command: docker-compose run --rm odoo odoo --stop-after-init -i main 19 | 20 | - run: 21 | name: Setup Log Folder For Reports 22 | command: sudo mkdir -p .log && sudo chmod 777 .log 23 | 24 | - run: 25 | name: Run Test 26 | command: docker-compose run --rm odoo run_pytest.sh 27 | 28 | - run: 29 | name: Run Javascript Tests 30 | command: docker-compose run --rm client npm test 31 | 32 | - run: 33 | name: Codacy Coverage 34 | command: bash <(curl -Ls https://coverage.codacy.com/get.sh) report -l python -r .log/coverage.xml 35 | 36 | - store_test_results: 37 | path: .log 38 | 39 | # job that find the next tag for the current branch/repo and push the tag to github. 40 | # it will trigger the publish of a new docker image. 41 | auto-tag: 42 | machine: true 43 | steps: 44 | - checkout 45 | - run: 46 | <<: *quay_io_login 47 | - run: 48 | name: Get nws 49 | command: | 50 | curl -L $NWS_BIN_LOCATION > ./nws 51 | chmod +x ./nws 52 | - run: 53 | name: Set tag 54 | command: | 55 | ./nws circleci create-tag -t odoo-base 56 | 57 | workflows: 58 | version: 2 59 | odoo: 60 | jobs: 61 | - tests: 62 | context: quay.io 63 | 64 | - auto-tag: 65 | context: nws 66 | requires: 67 | - tests 68 | filters: 69 | branches: 70 | only: /^1\d\.0/ 71 | -------------------------------------------------------------------------------- /.docker_files/ava/setupMoment.js: -------------------------------------------------------------------------------- 1 | global.moment = require('moment'); 2 | -------------------------------------------------------------------------------- /.docker_files/ava/setupVue.js: -------------------------------------------------------------------------------- 1 | // Setup Vue component testing 2 | // See https://github.com/avajs/ava/blob/master/docs/recipes/vue.md 3 | 4 | // Setup browser environment 5 | require('browser-env')(); 6 | const hooks = require('require-extension-hooks'); 7 | const Vue = require('vue'); 8 | 9 | // Setup Vue.js to remove production tip 10 | Vue.config.productionTip = false; 11 | 12 | // Setup vue files to be processed by `require-extension-hooks-vue` 13 | hooks('vue').plugin('vue').push(); 14 | // Setup vue and js files to be processed by `require-extension-hooks-babel` 15 | hooks(['vue', 'js']).plugin('babel').push(); 16 | -------------------------------------------------------------------------------- /.docker_files/main/__init__.py: -------------------------------------------------------------------------------- 1 | # © 2018 Numigi 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). 3 | -------------------------------------------------------------------------------- /.docker_files/main/__manifest__.py: -------------------------------------------------------------------------------- 1 | # © 2018 Numigi 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). 3 | 4 | { 5 | "name": "Main Module", 6 | "version": "1.0.0", 7 | "author": "Numigi", 8 | "maintainer": "Numigi", 9 | "website": "https://www.numigi.com", 10 | "license": "LGPL-3", 11 | "category": "Other", 12 | "summary": "Install all addons required for testing.", 13 | "depends": [ 14 | "vue", 15 | "vue_backend", 16 | "vue_element_ui", 17 | "vue_frontend", 18 | "vue_i18n", 19 | "vue_router", 20 | "vue_stock_forecast", 21 | "vue_stock_forecast_preferred_supplier", 22 | "vuex", 23 | ], 24 | "installable": True, 25 | } 26 | -------------------------------------------------------------------------------- /.docker_files/main/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/.docker_files/main/tests/__init__.py -------------------------------------------------------------------------------- /.docker_files/main/tests/test_installed_modules.py: -------------------------------------------------------------------------------- 1 | 2 | from odoo.tests import TransactionCase 3 | 4 | 5 | class TestModules(TransactionCase): 6 | 7 | def setUp(self): 8 | super(TestModules, self).setUp() 9 | self.modules = self.env['ir.module.module'] 10 | 11 | def test_vue_stock_forcast(self): 12 | module = self.modules.search([('name', '=', 'vue_stock_forecast')]) 13 | self.assertTrue(module.state == "installed") 14 | -------------------------------------------------------------------------------- /.docker_files/odoo.conf: -------------------------------------------------------------------------------- 1 | [options] 2 | addons_path = /mnt/extra-addons 3 | csv_internal_sep = , 4 | data_dir = /var/lib/odoo 5 | db_host = db 6 | db_maxconn = 64 7 | db_name = odoo 8 | db_password = odoo 9 | db_port = 5432 10 | db_template = template1 11 | db_user = odoo 12 | dbfilter = .* 13 | demo = {} 14 | email_from = False 15 | geoip_database = /usr/share/GeoIP/GeoLiteCity.dat 16 | import_partial = 17 | limit_memory_hard = 2684354560 18 | limit_memory_soft = 2147483648 19 | limit_request = 8192 20 | limit_time_cpu = 1500 21 | limit_time_real = 1500 22 | limit_time_real_cron = -1 23 | list_db = True 24 | log_db = False 25 | log_db_level = warning 26 | log_handler = :INFO 27 | log_level = info 28 | logfile = None 29 | logrotate = False 30 | longpolling_port = 8071 31 | max_cron_threads = 1 32 | osv_memory_age_limit = 1.0 33 | osv_memory_count_limit = False 34 | pg_path = None 35 | pidfile = None 36 | proxy_mode = False 37 | reportgz = False 38 | server_wide_modules = web 39 | smtp_password = False 40 | smtp_port = 25 41 | smtp_server = localhost 42 | smtp_ssl = False 43 | smtp_user = False 44 | syslog = False 45 | test_commit = False 46 | test_enable = False 47 | test_file = False 48 | test_report_directory = False 49 | translate_modules = ['all'] 50 | unaccent = False 51 | without_demo = False 52 | workers = False 53 | xmlrpc = True 54 | xmlrpc_interface = 55 | xmlrpc_port = 8069 56 | xmlrpcs = False 57 | -------------------------------------------------------------------------------- /.docker_files/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "odoo-web-addons", 3 | "version": "1.0.0", 4 | "description": "Web Interface Addons For Odoo", 5 | "author": "Numigi (tm)", 6 | "scripts": { 7 | "test": "ava **/test*.js" 8 | }, 9 | "ava": { 10 | "require": [ 11 | "./ava/setupVue.js", 12 | "./ava/setupMoment.js" 13 | ], 14 | "babel": { 15 | "testOptions": { 16 | "presets": [ 17 | "@ava/babel-preset-stage-4" 18 | ] 19 | } 20 | } 21 | }, 22 | "devDependencies": { 23 | "@ava/babel-preset-stage-4": "^1.1.0", 24 | "ava": "^3.12.1", 25 | "browser-env": "^3.2.5", 26 | "element-ui": "^2.4.1", 27 | "moment": "^2.22.2", 28 | "pretty": "^2.0.0", 29 | "require-extension-hooks": "^0.3.2", 30 | "require-extension-hooks-babel": "^0.1.1", 31 | "require-extension-hooks-vue": "^1.0.0", 32 | "vue": "^2.5.2", 33 | "vue-loader": "^13.3.0", 34 | "vue-style-loader": "^3.0.1", 35 | "vue-template-compiler": "^2.5.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.docker_files/test-requirements.txt: -------------------------------------------------------------------------------- 1 | freezegun==1.1.0 2 | ddt==1.2.1 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .log 2 | /.idea 3 | *node_modules 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .log 2 | /.idea 3 | *node_modules 4 | __pycache__/ 5 | *.pyc 6 | .pytest_cache/ 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/numigi/odoo-public:12.latest 2 | MAINTAINER numigi 3 | 4 | USER root 5 | 6 | COPY .docker_files/test-requirements.txt ./test-requirements.txt 7 | RUN pip3 install -r ./test-requirements.txt && rm ./test-requirements.txt 8 | 9 | USER odoo 10 | 11 | COPY vue /mnt/extra-addons/vue 12 | COPY vue_backend /mnt/extra-addons/vue_backend 13 | COPY vue_element_ui /mnt/extra-addons/vue_element_ui 14 | COPY vue_frontend /mnt/extra-addons/vue_frontend 15 | COPY vue_i18n /mnt/extra-addons/vue_i18n 16 | COPY vue_router /mnt/extra-addons/vue_router 17 | COPY vue_stock_forecast /mnt/extra-addons/vue_stock_forecast 18 | COPY vue_stock_forecast_preferred_supplier /mnt/extra-addons/vue_stock_forecast_preferred_supplier 19 | COPY vuex /mnt/extra-addons/vuex 20 | 21 | COPY .docker_files/main /mnt/extra-addons/main 22 | COPY .docker_files/odoo.conf /etc/odoo 23 | -------------------------------------------------------------------------------- /Dockerfile-client: -------------------------------------------------------------------------------- 1 | FROM node:9.11.2-stretch 2 | MAINTAINER numigi 3 | 4 | RUN useradd -ms /bin/bash client 5 | WORKDIR /home/client 6 | 7 | USER client 8 | 9 | COPY .docker_files/package.json .docker_files/package-lock.json ./ 10 | RUN npm install 11 | 12 | COPY .docker_files/ava ava 13 | COPY vue_stock_forecast vue_stock_forecast 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vuejs / Odoo Integration 2 | 3 | A series of modules for rendering Vuejs components inside Odoo. 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | odoo: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | volumes: 8 | - odoo-web-data:/var/lib/odoo 9 | - ./.log:/var/log/odoo 10 | ports: 11 | - "8069:8069" 12 | - "8071:8071" 13 | depends_on: 14 | - db 15 | command: odoo 16 | environment: 17 | - LOG_ODOO=/var/log/odoo 18 | client: 19 | build: 20 | context: . 21 | dockerfile: Dockerfile-client 22 | db: 23 | image: postgres:9.6 24 | environment: 25 | - POSTGRES_PASSWORD=odoo 26 | - POSTGRES_USER=odoo 27 | - PGDATA=/var/lib/postgresql/data/pgdata 28 | volumes: 29 | - odoo-db-data:/var/lib/postgresql/data/pgdata 30 | expose: 31 | - 5432 32 | 33 | volumes: 34 | odoo-web-data: 35 | odoo-db-data: 36 | -------------------------------------------------------------------------------- /gitoo.yml: -------------------------------------------------------------------------------- 1 | - url: https://{{GIT_TOKEN}}@github.com/Numigi/odoo-enterprise 2 | branch: "11.0" 3 | -------------------------------------------------------------------------------- /vue/README.md: -------------------------------------------------------------------------------- 1 | # Vuejs / Odoo Integration 2 | 3 | This module allows to render Vuejs components in the Odoo web interface. 4 | 5 | ## Querying Odoo Data 6 | 7 | The module adds the following assets for easily querying records from Odoo. 8 | 9 | ### Query Builder 10 | 11 | The query builder is an object used for easily searching and reading records from the server. 12 | 13 | Example of usage: 14 | 15 | ```javascript 16 | odoo.define("my_module.myFeature", (require) => { 17 | 18 | var QueryBuilder = require("vue.QueryBuilder") 19 | 20 | var query = new QueryBuilder('res.partner', ['display_name', 'zip', 'city', 'country_id']) 21 | query.filter([['customer', '=', true]]) 22 | query.searchRead().then((customers) => { 23 | // do something with customers 24 | }) 25 | 26 | }) 27 | ``` 28 | 29 | ### Xml References 30 | 31 | The getXmlId function allows to easily retreive an xml id from Odoo. 32 | 33 | Example of usage: 34 | 35 | ```javascript 36 | odoo.define("my_module.myFeature", (require) => { 37 | 38 | var getXmlId = require("vue.getXmlId") 39 | 40 | getXmlId("stock.route_warehouse0_mto").then((routeId) => { 41 | // do something with the route id 42 | }) 43 | 44 | }) 45 | ``` 46 | 47 | Calling getXmlId() with the same reference multiple times will not trigger multiple http queries. 48 | 49 | ## Contributors 50 | 51 | * Numigi (tm) and all its contributors (https://bit.ly/numigiens) 52 | 53 | ## More information 54 | 55 | * Meet us at https://bit.ly/numigi-com 56 | -------------------------------------------------------------------------------- /vue/__init__.py: -------------------------------------------------------------------------------- 1 | # © 2018 Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). 3 | -------------------------------------------------------------------------------- /vue/__manifest__.py: -------------------------------------------------------------------------------- 1 | # © 2018 Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). 3 | 4 | { 5 | 'name': 'Vuejs', 6 | 'version': '1.1.0', 7 | 'author': 'Numigi', 8 | 'maintainer': 'Numigi', 9 | 'website': 'https://bit.ly/numigi-com', 10 | 'license': 'LGPL-3', 11 | 'category': 'Web', 12 | 'summary': 'Render Vuejs templates in Odoo.', 13 | 'depends': ['web'], 14 | 'data': ['views/assets.xml'], 15 | 'installable': True, 16 | } 17 | -------------------------------------------------------------------------------- /vue/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue/static/description/icon.png -------------------------------------------------------------------------------- /vue/static/src/js/QueryBuilder.js: -------------------------------------------------------------------------------- 1 | odoo.define("vue.QueryBuilder", function(require){ 2 | 3 | var rpc = require("web.rpc"); 4 | var Class = require("web.Class"); 5 | 6 | /** 7 | * Class used for searching object using the Odoo rpc api. 8 | * 9 | * Example: 10 | * 11 | * query = QueryBuilder('res.partner', ['zip', 'city', ...]); 12 | * query.filter([['customer', '=', true]]); 13 | * partners = await query.searchRead(); 14 | */ 15 | var QueryBuilder = Class.extend({ 16 | init(model, fields){ 17 | this._model = model; 18 | this._fields = fields; 19 | this._domain = []; 20 | }, 21 | /** 22 | * Filter the query given a domain filter. 23 | * 24 | * @param {Array} domain - the domain filter to add. 25 | * @returns {QueryBuilder} this for chaining. 26 | */ 27 | filter(domain){ 28 | this._domain = this._domain.concat(domain); 29 | return this; 30 | }, 31 | /** 32 | * Search and read the records. 33 | * 34 | * @returns {Deferred} the query's deferred. 35 | */ 36 | searchRead(){ 37 | return rpc.query({ 38 | model: this._model, 39 | method: "search_read", 40 | params: { 41 | context: odoo.session_info.user_context, 42 | }, 43 | args: [this._domain, this._fields], 44 | }); 45 | }, 46 | }); 47 | 48 | return QueryBuilder; 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /vue/static/src/js/getXmlId.js: -------------------------------------------------------------------------------- 1 | odoo.define("vue.getXmlId", function(require){ 2 | 3 | var Class = require("web.Class"); 4 | var QueryBuilder = require("vue.QueryBuilder"); 5 | 6 | /** 7 | * A class responsible for caching an xml reference. 8 | */ 9 | var XmlReference = Class.extend({ 10 | init(ref){ 11 | var parts = ref.split("."); 12 | if(parts.length === 2){ 13 | this._module = parts[0]; 14 | this._name = parts[1]; 15 | } 16 | else{ 17 | this._module = false; 18 | this._name = parts[0]; 19 | } 20 | this._query = null; 21 | }, 22 | /** 23 | * Get the id of the referenced object. 24 | * 25 | * @returns {Integer | null} the record id if it exists. 26 | */ 27 | async getId(){ 28 | if(!this._query){ 29 | this._query = ( 30 | new QueryBuilder("ir.model.data", ["res_id", "model"]) 31 | .filter([["module", "=", this._module], ["name", "=", this._name]]) 32 | .searchRead() 33 | ); 34 | } 35 | var result = await this._query; 36 | return result.length ? result[0].res_id : null; 37 | }, 38 | }); 39 | 40 | var references = new Map(); 41 | 42 | function getXmlId(ref){ 43 | if(!references.has(ref)){ 44 | references.set(ref, new XmlReference(ref)); 45 | } 46 | return references.get(ref).getId(); 47 | } 48 | 49 | return getXmlId; 50 | 51 | }); 52 | -------------------------------------------------------------------------------- /vue/views/assets.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /vue_backend/README.rst: -------------------------------------------------------------------------------- 1 | Vue Backend 2 | =========== 3 | 4 | This module adds Vuejs to the backend assets of Odoo. 5 | 6 | Contributors 7 | ------------ 8 | * Numigi (tm) and all its contributors (https://bit.ly/numigiens) 9 | 10 | More information 11 | ---------------- 12 | * Meet us at https://bit.ly/numigi-com 13 | -------------------------------------------------------------------------------- /vue_backend/__init__.py: -------------------------------------------------------------------------------- 1 | # © 2020 Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). 3 | -------------------------------------------------------------------------------- /vue_backend/__manifest__.py: -------------------------------------------------------------------------------- 1 | # © 2020 Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). 3 | 4 | { 5 | "name": "Vuejs Frontend", 6 | "version": "1.1.0", 7 | "author": "Numigi", 8 | "maintainer": "Numigi", 9 | "website": "https://bit.ly/numigi-com", 10 | "license": "LGPL-3", 11 | "category": "Web", 12 | "summary": "Add vuejs assets to the frontend of Odoo.", 13 | "depends": ["vue"], 14 | "data": ["views/assets.xml"], 15 | "installable": True, 16 | } 17 | -------------------------------------------------------------------------------- /vue_backend/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_backend/static/description/icon.png -------------------------------------------------------------------------------- /vue_backend/views/assets.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /vue_element_ui/README.md: -------------------------------------------------------------------------------- 1 | # Element-Ui / Odoo Integration 2 | 3 | This module allows to render Element-UI (https://element.eleme.io) components in the Odoo web interface. 4 | 5 | ## Translations 6 | 7 | The Element UI components are translated using standard Odoo po files. 8 | This allows editing these transltions without adding javascript code. 9 | 10 | Instead of the term being translated, these translations contain the placement of the term. 11 | (i.e. `el.colorpicker.confirm`). 12 | 13 | ## Many2many Tags Component 14 | 15 | The module also adds a many2many component based on the el-select widget. 16 | 17 | The component is globally registered as `many2many`. 18 | 19 | Example of usage: 20 | 21 | ```xml 22 | 27 | 41 | ``` 42 | 43 | The rendered component should look like the following: 44 | 45 | ![Many2many](static/description/many2many.png?raw=true) 46 | 47 | This widget does not depend on Odoo's api. 48 | Instead, it enables injecting a search method to handle the queries to the server. 49 | 50 | When the selection changes, the change signal is emited. 51 | 52 | ## Contributors 53 | 54 | * Numigi (tm) and all its contributors (https://bit.ly/numigiens) 55 | 56 | ## More information 57 | 58 | * Meet us at https://bit.ly/numigi-com 59 | -------------------------------------------------------------------------------- /vue_element_ui/__init__.py: -------------------------------------------------------------------------------- 1 | # © 2018 Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). 3 | 4 | from . import models 5 | -------------------------------------------------------------------------------- /vue_element_ui/__manifest__.py: -------------------------------------------------------------------------------- 1 | # © 2018 Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). 3 | 4 | { 5 | 'name': 'Element-UI', 6 | 'version': '1.0.0', 7 | 'author': 'Numigi', 8 | 'maintainer': 'Numigi', 9 | 'website': 'https://bit.ly/numigi-com', 10 | 'license': 'LGPL-3', 11 | 'category': 'Web', 12 | 'summary': 'Render Element UI components in Odoo.', 13 | 'depends': ['vue'], 14 | 'data': ['views/assets.xml'], 15 | 'installable': True, 16 | } 17 | -------------------------------------------------------------------------------- /vue_element_ui/i18n/fr.po: -------------------------------------------------------------------------------- 1 | # Translation of Odoo Server. 2 | # This file contains the translation of the following modules: 3 | # * disable_quick_create 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: Odoo Server 10.0+e\n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2017-12-10 00:06+0000\n" 10 | "PO-Revision-Date: 2017-12-10 00:06+0000\n" 11 | "Last-Translator: <>\n" 12 | "Language-Team: \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: \n" 16 | "Plural-Forms: \n" 17 | 18 | #. module: vue_element_ui 19 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 20 | msgid "el.colorpicker.confirm" 21 | msgstr "OK" 22 | 23 | #. module: vue_element_ui 24 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 25 | msgid "el.colorpicker.clear" 26 | msgstr "Effacer" 27 | 28 | #. module: vue_element_ui 29 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 30 | msgid "el.datepicker.now" 31 | msgstr "Maintenant" 32 | 33 | #. module: vue_element_ui 34 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 35 | msgid "el.datepicker.today" 36 | msgstr "Auj." 37 | 38 | #. module: vue_element_ui 39 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 40 | msgid "el.datepicker.cancel" 41 | msgstr "Annuler" 42 | 43 | #. module: vue_element_ui 44 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 45 | msgid "el.datepicker.clear" 46 | msgstr "Effacer" 47 | 48 | #. module: vue_element_ui 49 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 50 | msgid "el.datepicker.confirm" 51 | msgstr "OK" 52 | 53 | #. module: vue_element_ui 54 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 55 | msgid "el.datepicker.selectDate" 56 | msgstr "Sélectionner la date" 57 | 58 | #. module: vue_element_ui 59 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 60 | msgid "el.datepicker.selectTime" 61 | msgstr "Sélectionner l'heure" 62 | 63 | #. module: vue_element_ui 64 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 65 | msgid "el.datepicker.startDate" 66 | msgstr "Date début" 67 | 68 | #. module: vue_element_ui 69 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 70 | msgid "el.datepicker.startTime" 71 | msgstr "Horaire début" 72 | 73 | #. module: vue_element_ui 74 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 75 | msgid "el.datepicker.endDate" 76 | msgstr "Date fin" 77 | 78 | #. module: vue_element_ui 79 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 80 | msgid "el.datepicker.endTime" 81 | msgstr "Horaire fin" 82 | 83 | #. module: vue_element_ui 84 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 85 | msgid "el.datepicker.prevYear" 86 | msgstr "Année précédente" 87 | 88 | #. module: vue_element_ui 89 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 90 | msgid "el.datepicker.nextYear" 91 | msgstr "Année suivante" 92 | 93 | #. module: vue_element_ui 94 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 95 | msgid "el.datepicker.prevMonth" 96 | msgstr "Mois précédent" 97 | 98 | #. module: vue_element_ui 99 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 100 | msgid "el.datepicker.nextMonth" 101 | msgstr "Mois suivant" 102 | 103 | #. module: vue_element_ui 104 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 105 | msgid "el.datepicker.year" 106 | msgstr "" 107 | 108 | #. module: vue_element_ui 109 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 110 | msgid "el.datepicker.month1" 111 | msgstr "Janvier" 112 | 113 | #. module: vue_element_ui 114 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 115 | msgid "el.datepicker.month2" 116 | msgstr "Février" 117 | 118 | #. module: vue_element_ui 119 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 120 | msgid "el.datepicker.month3" 121 | msgstr "Mars" 122 | 123 | #. module: vue_element_ui 124 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 125 | msgid "el.datepicker.month4" 126 | msgstr "Avril" 127 | 128 | #. module: vue_element_ui 129 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 130 | msgid "el.datepicker.month5" 131 | msgstr "Mai" 132 | 133 | #. module: vue_element_ui 134 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 135 | msgid "el.datepicker.month6" 136 | msgstr "Juin" 137 | 138 | #. module: vue_element_ui 139 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 140 | msgid "el.datepicker.month7" 141 | msgstr "Juillet" 142 | 143 | #. module: vue_element_ui 144 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 145 | msgid "el.datepicker.month8" 146 | msgstr "Août" 147 | 148 | #. module: vue_element_ui 149 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 150 | msgid "el.datepicker.month9" 151 | msgstr "Septembre" 152 | 153 | #. module: vue_element_ui 154 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 155 | msgid "el.datepicker.month10" 156 | msgstr "Octobre" 157 | 158 | #. module: vue_element_ui 159 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 160 | msgid "el.datepicker.month11" 161 | msgstr "Novembre" 162 | 163 | #. module: vue_element_ui 164 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 165 | msgid "el.datepicker.month12" 166 | msgstr "Décembre" 167 | 168 | #. module: vue_element_ui 169 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 170 | msgid "el.datepicker.weeks.sun" 171 | msgstr "Dim" 172 | 173 | #. module: vue_element_ui 174 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 175 | msgid "el.datepicker.weeks.mon" 176 | msgstr "Lun" 177 | 178 | #. module: vue_element_ui 179 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 180 | msgid "el.datepicker.weeks.tue" 181 | msgstr "Mar" 182 | 183 | #. module: vue_element_ui 184 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 185 | msgid "el.datepicker.weeks.wed" 186 | msgstr "Mer" 187 | 188 | #. module: vue_element_ui 189 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 190 | msgid "el.datepicker.weeks.thu" 191 | msgstr "Jeu" 192 | 193 | #. module: vue_element_ui 194 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 195 | msgid "el.datepicker.weeks.fri" 196 | msgstr "Ven" 197 | 198 | #. module: vue_element_ui 199 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 200 | msgid "el.datepicker.weeks.sat" 201 | msgstr "Sam" 202 | 203 | #. module: vue_element_ui 204 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 205 | msgid "el.datepicker.months.jan" 206 | msgstr "Jan" 207 | 208 | #. module: vue_element_ui 209 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 210 | msgid "el.datepicker.months.feb" 211 | msgstr "Fév" 212 | 213 | #. module: vue_element_ui 214 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 215 | msgid "el.datepicker.months.mar" 216 | msgstr "Mar" 217 | 218 | #. module: vue_element_ui 219 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 220 | msgid "el.datepicker.months.apr" 221 | msgstr "Avr" 222 | 223 | #. module: vue_element_ui 224 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 225 | msgid "el.datepicker.months.may" 226 | msgstr "Mai" 227 | 228 | #. module: vue_element_ui 229 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 230 | msgid "el.datepicker.months.jun" 231 | msgstr "Jun" 232 | 233 | #. module: vue_element_ui 234 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 235 | msgid "el.datepicker.months.jul" 236 | msgstr "Jul" 237 | 238 | #. module: vue_element_ui 239 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 240 | msgid "el.datepicker.months.aug" 241 | msgstr "Aoû" 242 | 243 | #. module: vue_element_ui 244 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 245 | msgid "el.datepicker.months.sep" 246 | msgstr "Sep" 247 | 248 | #. module: vue_element_ui 249 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 250 | msgid "el.datepicker.months.oct" 251 | msgstr "Oct" 252 | 253 | #. module: vue_element_ui 254 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 255 | msgid "el.datepicker.months.nov" 256 | msgstr "Nov" 257 | 258 | #. module: vue_element_ui 259 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 260 | msgid "el.datepicker.months.dec" 261 | msgstr "Déc" 262 | 263 | #. module: vue_element_ui 264 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 265 | msgid "el.select.loading" 266 | msgstr "Chargement" 267 | 268 | #. module: vue_element_ui 269 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 270 | msgid "el.select.noMatch" 271 | msgstr "Aucune correspondance" 272 | 273 | #. module: vue_element_ui 274 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 275 | msgid "el.select.noData" 276 | msgstr "Aucune donnée" 277 | 278 | #. module: vue_element_ui 279 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 280 | msgid "el.select.placeholder" 281 | msgstr "Sélectionner" 282 | 283 | #. module: vue_element_ui 284 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 285 | msgid "el.cascader.noMatch" 286 | msgstr "Aucune correspondance" 287 | 288 | #. module: vue_element_ui 289 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 290 | msgid "el.cascader.loading" 291 | msgstr "Chargement" 292 | 293 | #. module: vue_element_ui 294 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 295 | msgid "el.cascader.placeholder" 296 | msgstr "Sélectionner" 297 | 298 | #. module: vue_element_ui 299 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 300 | msgid "el.pagination.goto" 301 | msgstr "Aller à" 302 | 303 | #. module: vue_element_ui 304 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 305 | msgid "el.pagination.pagesize" 306 | msgstr "/page" 307 | 308 | #. module: vue_element_ui 309 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 310 | msgid "el.pagination.total" 311 | msgstr "Total {total}" 312 | 313 | #. module: vue_element_ui 314 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 315 | msgid "el.pagination.pageClassifier" 316 | msgstr "" 317 | 318 | #. module: vue_element_ui 319 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 320 | msgid "el.messagebox.confirm" 321 | msgstr "Confirmer" 322 | 323 | #. module: vue_element_ui 324 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 325 | msgid "el.messagebox.cancel" 326 | msgstr "Annuler" 327 | 328 | #. module: vue_element_ui 329 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 330 | msgid "el.messagebox.error" 331 | msgstr "Erreu" 332 | 333 | #. module: vue_element_ui 334 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 335 | msgid "el.upload.deleteTip" 336 | msgstr "press delete to remove" 337 | 338 | #. module: vue_element_ui 339 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 340 | msgid "el.upload.delete" 341 | msgstr "Supprimer" 342 | 343 | #. module: vue_element_ui 344 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 345 | msgid "el.upload.preview" 346 | msgstr "Aperçu" 347 | 348 | #. module: vue_element_ui 349 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 350 | msgid "el.upload.continue" 351 | msgstr "Continuer" 352 | 353 | #. module: vue_element_ui 354 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 355 | msgid "el.table.emptyText" 356 | msgstr "Aucune donnée" 357 | 358 | #. module: vue_element_ui 359 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 360 | msgid "el.table.confirmFilter" 361 | msgstr "Confirmer" 362 | 363 | #. module: vue_element_ui 364 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 365 | msgid "el.table.resetFilter" 366 | msgstr "Réinitialiser" 367 | 368 | #. module: vue_element_ui 369 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 370 | msgid "el.table.clearFilter" 371 | msgstr "Tous" 372 | 373 | #. module: vue_element_ui 374 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 375 | msgid "el.table.sumText" 376 | msgstr "Sum" 377 | 378 | #. module: vue_element_ui 379 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 380 | msgid "el.tree.emptyText" 381 | msgstr "Aucune donnée" 382 | 383 | #. module: vue_element_ui 384 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 385 | msgid "el.transfer.noMatch" 386 | msgstr "Aucune correspondance" 387 | 388 | #. module: vue_element_ui 389 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 390 | msgid "el.transfer.noData" 391 | msgstr "Aucune donnée" 392 | 393 | #. module: vue_element_ui 394 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 395 | msgid "el.transfer.titles" 396 | msgstr "[\"Liste 1", "Liste 2\"]" 397 | 398 | #. module: vue_element_ui 399 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 400 | msgid "el.transfer.filterPlaceholder" 401 | msgstr "Entrez un mot clé" 402 | 403 | #. module: vue_element_ui 404 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 405 | msgid "el.transfer.noCheckedFormat" 406 | msgstr "{total} éléments" 407 | 408 | #. module: vue_element_ui 409 | #: code:addons/vue_element_ui/static/src/js/i18n.js:0 410 | msgid "el.transfer.hasCheckedFormat" 411 | msgstr "{checked}/{total} coché" 412 | 413 | -------------------------------------------------------------------------------- /vue_element_ui/models/__init__.py: -------------------------------------------------------------------------------- 1 | # © 2018 Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). 3 | 4 | from . import ir_translation 5 | -------------------------------------------------------------------------------- /vue_element_ui/models/__pycache__/__init__.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_element_ui/models/__pycache__/__init__.cpython-35.pyc -------------------------------------------------------------------------------- /vue_element_ui/models/__pycache__/ir_translation.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_element_ui/models/__pycache__/ir_translation.cpython-35.pyc -------------------------------------------------------------------------------- /vue_element_ui/models/ir_translation.py: -------------------------------------------------------------------------------- 1 | # © 2018 Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). 3 | 4 | from odoo import api, models 5 | 6 | 7 | class IrTranslation(models.Model): 8 | 9 | _inherit = 'ir.translation' 10 | 11 | @api.model 12 | def get_element_ui_translations(self, lang): 13 | """Get all translations for element ui components. 14 | 15 | The sudo is required so that website and portal users may 16 | access these translations. 17 | """ 18 | translations = self.sudo().search([ 19 | ('name', '=', 'addons/vue_element_ui/static/src/js/i18n.js'), 20 | ('lang', '=', lang), 21 | ]) 22 | return [(t.source, t.value) for t in translations] 23 | -------------------------------------------------------------------------------- /vue_element_ui/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_element_ui/static/description/icon.png -------------------------------------------------------------------------------- /vue_element_ui/static/description/many2many.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_element_ui/static/description/many2many.png -------------------------------------------------------------------------------- /vue_element_ui/static/dist/vueElementUI.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(r,i,function(t){return e[t]}.bind(null,i));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=4)}([function(e,t,n){"use strict";n.r(t);var r=n(1),i=n.n(r);for(var o in r)["default"].indexOf(o)<0&&function(e){n.d(t,e,(function(){return r[e]}))}(o);t.default=i.a},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default={props:{search:{type:Function,required:!0},placeholder:{type:String}},data:function(){return{items:[],selection:[],invisibleItems:[]}},watch:{items:function(){this.updateInvisibleItems()}},methods:{setItems:function(e){this.items=e.map((function(e){return{id:e[0],name:e[1]}}))},onFocus:function(){this.searchItems(this.$refs.select.query)},searchItems:function(e){var t=this;this.search(e).then((function(e){t.selection=e.map((function(e){return{id:e[0],name:e[1]}})),t.updateInvisibleItems()}))},onChange:function(e){this.$emit("change",e)},updateInvisibleItems:function(){var e=this.selection.map((function(e){return e.id}));this.invisibleItems=this.items.filter((function(t){return-1===e.indexOf(t.id)}))}}}},function(e,t,n){"use strict";n.d(t,"a",(function(){return r})),n.d(t,"b",(function(){return i}));var r=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("el-select",{ref:"select",attrs:{multiple:"",filterable:"",remote:"","value-key":"id","remote-method":e.searchItems,placeholder:e.placeholder},on:{focus:e.onFocus,change:e.onChange},model:{value:e.items,callback:function(t){e.items=t},expression:"items"}},[e._l(e.selection,(function(e){return n("el-option",{key:e.id,attrs:{label:e.name,value:e}})})),e._v(" "),e._l(e.invisibleItems,(function(e){return n("el-option",{directives:[{name:"show",rawName:"v-show",value:!1,expression:"false"}],key:e.id,attrs:{label:e.name,value:e}})}))],2)},i=[];r._withStripped=!0},function(e,t,n){"use strict";function r(e,t,n,r,i,o,s,u){var a,c="function"==typeof e?e.options:e;if(t&&(c.render=t,c.staticRenderFns=n,c._compiled=!0),r&&(c.functional=!0),o&&(c._scopeId="data-v-"+o),s?(a=function(e){(e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),i&&i.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(s)},c._ssrRegister=a):i&&(a=u?function(){i.call(this,this.$root.$options.shadowRoot)}:i),a)if(c.functional){c._injectStyles=a;var l=c.render;c.render=function(e,t){return a.call(t),l(e,t)}}else{var f=c.beforeCreate;c.beforeCreate=f?[].concat(f,a):[a]}return{exports:e,options:c}}n.d(t,"a",(function(){return r}))},function(e,t,n){e.exports=n(5)},function(e,t,n){"use strict";var r,i=n(6),o=(r=i)&&r.__esModule?r:{default:r};Vue.component("many2many",o.default),Vue.component("el-select",{extends:Vue.options.components.ElSelect,methods:{handleResize:function(){this.resetInputWidth()}}})},function(e,t,n){"use strict";n.r(t);var r=n(2),i=n(0);for(var o in i)["default"].indexOf(o)<0&&function(e){n.d(t,e,(function(){return i[e]}))}(o);var s=n(3),u=Object(s.a)(i.default,r.a,r.b,!1,null,null,null);u.options.__file="src/js/Many2many.vue",t.default=u.exports}]); -------------------------------------------------------------------------------- /vue_element_ui/static/lib/fonts/element-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_element_ui/static/lib/fonts/element-icons.ttf -------------------------------------------------------------------------------- /vue_element_ui/static/lib/fonts/element-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_element_ui/static/lib/fonts/element-icons.woff -------------------------------------------------------------------------------- /vue_element_ui/static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue_element_ui", 3 | "version": "1.0.0", 4 | "description": "Vue Components for Odoo using the Element-UI assets.", 5 | "devDependencies": { 6 | "ava": "^3.12.1", 7 | "babel-core": "^6.26.3", 8 | "babel-loader": "^7.1.5", 9 | "babel-polyfill": "^6.26.0", 10 | "babel-preset-env": "^1.7.0", 11 | "vue": "^2.5.17", 12 | "vue-loader": "^15.3.0", 13 | "vue-template-compiler": "^2.5.17", 14 | "webpack": "^4.44.2" 15 | }, 16 | "author": "Numigi (tm) and all its contributors (https://bit.ly/numigiens)", 17 | "license": "LGPL-3.0-or-later", 18 | "dependencies": { 19 | "webpack-cli": "^3.3.12" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /vue_element_ui/static/src/js/Many2many.vue: -------------------------------------------------------------------------------- 1 | 23 | 81 | -------------------------------------------------------------------------------- /vue_element_ui/static/src/js/en.js: -------------------------------------------------------------------------------- 1 | odoo.define("vue_element_ui.en", function(require) { 2 | "use strict"; 3 | 4 | return { 5 | el: { 6 | colorpicker: { 7 | confirm: "OK", 8 | clear: "Clear", 9 | }, 10 | datepicker: { 11 | now: "Now", 12 | today: "Today", 13 | cancel: "Cancel", 14 | clear: "Clear", 15 | confirm: "OK", 16 | selectDate: "Select date", 17 | selectTime: "Select time", 18 | startDate: "Start Date", 19 | startTime: "Start Time", 20 | endDate: "End Date", 21 | endTime: "End Time", 22 | prevYear: "Previous Year", 23 | nextYear: "Next Year", 24 | prevMonth: "Previous Month", 25 | nextMonth: "Next Month", 26 | year: "", 27 | month1: "Jan", 28 | month2: "Feb", 29 | month3: "Mar", 30 | month4: "Apr", 31 | month5: "May", 32 | month6: "Jun", 33 | month7: "Jul", 34 | month8: "Aug", 35 | month9: "Sep", 36 | month10: "Oct", 37 | month11: "Nov", 38 | month12: "Dec", 39 | weeks: { 40 | sun: "Sun", 41 | mon: "Mon", 42 | tue: "Tue", 43 | wed: "Wed", 44 | thu: "Thu", 45 | fri: "Fri", 46 | sat: "Sat", 47 | }, 48 | months: { 49 | jan: "Jan", 50 | feb: "Feb", 51 | mar: "Mar", 52 | apr: "Apr", 53 | may: "May", 54 | jun: "Jun", 55 | jul: "Jul", 56 | aug: "Aug", 57 | sep: "Sep", 58 | oct: "Oct", 59 | nov: "Nov", 60 | dec: "Dec", 61 | }, 62 | }, 63 | select: { 64 | loading: "Loading", 65 | noMatch: "No matching data", 66 | noData: "No data", 67 | placeholder: "Select", 68 | }, 69 | cascader: { 70 | noMatch: "No matching data", 71 | loading: "Loading", 72 | placeholder: "Select", 73 | }, 74 | pagination: { 75 | goto: "Go to", 76 | pagesize: "/page", 77 | total: "Total {total}", 78 | pageClassifier: "", 79 | }, 80 | messagebox: { 81 | title: "Message", 82 | confirm: "OK", 83 | cancel: "Cancel", 84 | error: "Illegal input", 85 | }, 86 | upload: { 87 | deleteTip: "press delete to remove", 88 | delete: "Delete", 89 | preview: "Preview", 90 | continue: "Continue", 91 | }, 92 | table: { 93 | emptyText: "No Data", 94 | confirmFilter: "Confirm", 95 | resetFilter: "Reset", 96 | clearFilter: "All", 97 | sumText: "Sum", 98 | }, 99 | tree: { 100 | emptyText: "No Data", 101 | }, 102 | transfer: { 103 | noMatch: "No matching data", 104 | noData: "No data", 105 | titles: ["List 1", "List 2"], 106 | filterPlaceholder: "Enter keyword", 107 | noCheckedFormat: "{total} items", 108 | hasCheckedFormat: "{checked}/{total} checked", 109 | }, 110 | }, 111 | }; 112 | 113 | }); 114 | -------------------------------------------------------------------------------- /vue_element_ui/static/src/js/i18n.js: -------------------------------------------------------------------------------- 1 | /* 2 | © 2018 Numigi (tm) and all its contributors (https://bit.ly/numigiens) 3 | License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL.html). 4 | */ 5 | odoo.define("vue_element_ui.i18n", function(require) { 6 | "use strict"; 7 | 8 | var rpc = require("web.rpc"); 9 | var Class = require("web.Class"); 10 | var enLocale = require("vue_element_ui.en"); 11 | var utils = require("web.utils"); 12 | var _t = require("web.core")._t; 13 | 14 | /** 15 | * Get all Element UI translations from the backend. 16 | * 17 | * @param {String} lang - the language code 18 | * @returns {Array} the translations 19 | */ 20 | function getElementUITranslations(lang){ 21 | return rpc.query({ 22 | model: "ir.translation", 23 | method: "get_element_ui_translations", 24 | args: [lang], 25 | }); 26 | } 27 | 28 | /** 29 | * Build the locale for Element UI components for a given array of translations. 30 | * 31 | * The result is an object with the same structure as found in the file ./en.js. 32 | * 33 | * Given the following array of translations: 34 | * 35 | * [['el.colorpicker.confirm', 'OK'], 36 | * ['el.colorpicker.clear', 'Effacer'], 37 | * ['el.datepicker.now', 'Maintenant'], 38 | * ...] 39 | * 40 | * The result would be: 41 | * 42 | * { 43 | * el: { 44 | * colorpicker: { 45 | * confirm: 'OK', 46 | * clear: 'Effacer', 47 | * }, 48 | * datepicker: { 49 | * now: 'Maintenant', 50 | * ... 51 | * }, 52 | * ... 53 | * } 54 | * } 55 | * 56 | * @param {Array} translations - the component translations 57 | * @returns {Object} the Element UI locale 58 | */ 59 | function buildElementUILocale(translations){ 60 | var userLocale = {}; 61 | 62 | translations.forEach(function(translation){ 63 | var keys = translation[0].split("."); 64 | var currentDict = userLocale; 65 | 66 | for(var i = 0; i < keys.length - 1; i++){ 67 | var key = keys[i]; 68 | if(!(key in currentDict)){ 69 | currentDict[key] = {}; 70 | } 71 | currentDict = currentDict[key]; 72 | } 73 | 74 | var value = translation[1]; 75 | var lastKey = keys[keys.length - 1]; 76 | currentDict[lastKey] = value; 77 | }); 78 | 79 | // Deep merge of the user locale into the en_US locale. 80 | // Any term that does not exist in the user lang will use the english translation. 81 | // Otherwise, we get chinese terms by default. 82 | return $.extend(true, {}, enLocale, userLocale); 83 | } 84 | 85 | var isFrontend = odoo.session_info.is_frontend; 86 | var lang = isFrontend ? utils.get_cookie("frontend_lang") : odoo.session_info.user_context.lang; 87 | 88 | if(!lang || lang === "en_US"){ 89 | ELEMENT.locale(enLocale); 90 | } 91 | else{ 92 | getElementUITranslations(lang).then(function(translations){ 93 | var userLocale = buildElementUILocale(translations); 94 | ELEMENT.locale(userLocale); 95 | }); 96 | } 97 | 98 | }); -------------------------------------------------------------------------------- /vue_element_ui/static/src/js/main.js: -------------------------------------------------------------------------------- 1 | 2 | import Many2many from "./Many2many.vue"; 3 | 4 | Vue.component("many2many", Many2many); 5 | 6 | /** 7 | * Prevent infinite loop with the automatic resize of the el-select component. 8 | * 9 | * The height of the input does not need to be resized. 10 | * Only the width is relevant to resize. 11 | */ 12 | Vue.component("el-select", { 13 | extends: Vue.options.components.ElSelect, 14 | methods: { 15 | handleResize() { 16 | this.resetInputWidth(); 17 | // The following line caused the infinite loop. 18 | // if (this.multiple) this.resetInputHeight(); 19 | }, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /vue_element_ui/static/webpack.config.js: -------------------------------------------------------------------------------- 1 | const VueLoaderPlugin = require("vue-loader/lib/plugin"); 2 | 3 | module.exports = { 4 | entry: ["./src/js/main.js"], 5 | mode: "production", 6 | devtool: false, 7 | output: {filename: "vueElementUI.js"}, 8 | module: { 9 | rules: [ 10 | {test: /\.js$/, loader: "babel-loader", query: {presets: ["env"]}}, 11 | {test: /\.vue$/, loader: "vue-loader"}, 12 | ], 13 | }, 14 | plugins: [ 15 | new VueLoaderPlugin(), 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /vue_element_ui/views/assets.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /vue_frontend/README.rst: -------------------------------------------------------------------------------- 1 | Vue Frontend 2 | ============ 3 | 4 | This module adds Vuejs to the frontend assets of Odoo. 5 | 6 | Contributors 7 | ------------ 8 | * Numigi (tm) and all its contributors (https://bit.ly/numigiens) 9 | 10 | More information 11 | ---------------- 12 | * Meet us at https://bit.ly/numigi-com 13 | -------------------------------------------------------------------------------- /vue_frontend/__init__.py: -------------------------------------------------------------------------------- 1 | # © 2020 Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). 3 | -------------------------------------------------------------------------------- /vue_frontend/__manifest__.py: -------------------------------------------------------------------------------- 1 | # © 2020 Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). 3 | 4 | { 5 | "name": "Vuejs Frontend", 6 | "version": "1.1.0", 7 | "author": "Numigi", 8 | "maintainer": "Numigi", 9 | "website": "https://bit.ly/numigi-com", 10 | "license": "LGPL-3", 11 | "category": "Web", 12 | "summary": "Add vuejs assets to the frontend of Odoo.", 13 | "depends": ["vue"], 14 | "data": ["views/assets.xml"], 15 | "installable": True, 16 | } 17 | -------------------------------------------------------------------------------- /vue_frontend/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_frontend/static/description/icon.png -------------------------------------------------------------------------------- /vue_frontend/views/assets.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /vue_i18n/README.rst: -------------------------------------------------------------------------------- 1 | Vue-I18n 2 | ======== 3 | 4 | This module integrates `Vue I18n `_ with the frontend assets of Odoo. 5 | 6 | Usage Example 7 | ------------- 8 | This example requires that you master of the following topics: 9 | 10 | * Creating a Vuejs application with VueI18n 11 | * Creating a Odoo javascript extension 12 | 13 | Creating a VueI18n instance 14 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 15 | 16 | In a file app.js, define your Vue application. 17 | 18 | .. code-block:: javascript 19 | 20 | require("web.dom_ready"); 21 | 22 | if (!$("#my_app_node").length) { 23 | return $.Deferred().reject("DOM doesn't contain '#my_app_node'"); 24 | } 25 | 26 | const i18nFactory = require("vue_i18n.i18nFactory"); 27 | i18nFactory.setLang("fr_FR") 28 | 29 | const i18n = i18nFactory.makeI18n(); 30 | const app = new Vue({i18n}).$mount('#my_app_node'); 31 | 32 | Inside other javascript files, you may register your messages: 33 | 34 | .. code-block:: javascript 35 | 36 | const i18nFactory = require("vue_i18n.i18nFactory"); 37 | i18nFactory.addMessages( 38 | { 39 | en: { 40 | foo: "bar", 41 | }, 42 | fr_FR: { 43 | foo: "baz", 44 | }, 45 | } 46 | ) 47 | 48 | Known Issues 49 | ------------ 50 | The module does not define how to get the language of the user. 51 | 52 | In the example above, it is hardcoded to "fr_FR". 53 | 54 | Getting the current user's language from the frontend point of view in Odoo is not trivial. 55 | You may not merely call ``navigator.language``. 56 | 57 | Contributors 58 | ------------ 59 | * Numigi (tm) and all its contributors (https://bit.ly/numigiens) 60 | 61 | More information 62 | ---------------- 63 | * Meet us at https://bit.ly/numigi-com 64 | -------------------------------------------------------------------------------- /vue_i18n/__init__.py: -------------------------------------------------------------------------------- 1 | # © 2020 Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). 3 | -------------------------------------------------------------------------------- /vue_i18n/__manifest__.py: -------------------------------------------------------------------------------- 1 | # © 2020 Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). 3 | 4 | { 5 | 'name': 'Vue-I18n', 6 | 'version': '1.0.0', 7 | 'author': 'Numigi', 8 | 'maintainer': 'Numigi', 9 | 'website': 'https://bit.ly/numigi-com', 10 | 'license': 'LGPL-3', 11 | 'category': 'Web', 12 | 'summary': 'Integrate vue-i18n with Odoo.', 13 | 'depends': ['vue'], 14 | 'data': ['views/assets.xml'], 15 | 'installable': True, 16 | } 17 | -------------------------------------------------------------------------------- /vue_i18n/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_i18n/static/description/icon.png -------------------------------------------------------------------------------- /vue_i18n/static/lib/deepmerge.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var index$2 = function isMergeableObject(value) { 4 | return isNonNullObject(value) && isNotSpecial(value) 5 | }; 6 | 7 | function isNonNullObject(value) { 8 | return !!value && typeof value === 'object' 9 | } 10 | 11 | function isNotSpecial(value) { 12 | var stringValue = Object.prototype.toString.call(value); 13 | 14 | return stringValue !== '[object RegExp]' 15 | && stringValue !== '[object Date]' 16 | } 17 | 18 | function emptyTarget(val) { 19 | return Array.isArray(val) ? [] : {} 20 | } 21 | 22 | function cloneIfNecessary(value, optionsArgument) { 23 | var clone = optionsArgument && optionsArgument.clone === true; 24 | return (clone && index$2(value)) ? deepmerge(emptyTarget(value), value, optionsArgument) : value 25 | } 26 | 27 | function defaultArrayMerge(target, source, optionsArgument) { 28 | var destination = target.slice(); 29 | source.forEach(function(e, i) { 30 | if (typeof destination[i] === 'undefined') { 31 | destination[i] = cloneIfNecessary(e, optionsArgument); 32 | } else if (index$2(e)) { 33 | destination[i] = deepmerge(target[i], e, optionsArgument); 34 | } else if (target.indexOf(e) === -1) { 35 | destination.push(cloneIfNecessary(e, optionsArgument)); 36 | } 37 | }); 38 | return destination 39 | } 40 | 41 | function mergeObject(target, source, optionsArgument) { 42 | var destination = {}; 43 | if (index$2(target)) { 44 | Object.keys(target).forEach(function(key) { 45 | destination[key] = cloneIfNecessary(target[key], optionsArgument); 46 | }); 47 | } 48 | Object.keys(source).forEach(function(key) { 49 | if (!index$2(source[key]) || !target[key]) { 50 | destination[key] = cloneIfNecessary(source[key], optionsArgument); 51 | } else { 52 | destination[key] = deepmerge(target[key], source[key], optionsArgument); 53 | } 54 | }); 55 | return destination 56 | } 57 | 58 | function deepmerge(target, source, optionsArgument) { 59 | var sourceIsArray = Array.isArray(source); 60 | var targetIsArray = Array.isArray(target); 61 | var options = optionsArgument || { arrayMerge: defaultArrayMerge }; 62 | var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray; 63 | 64 | if (!sourceAndTargetTypesMatch) { 65 | return cloneIfNecessary(source, optionsArgument) 66 | } else if (sourceIsArray) { 67 | var arrayMerge = options.arrayMerge || defaultArrayMerge; 68 | return arrayMerge(target, source, optionsArgument) 69 | } else { 70 | return mergeObject(target, source, optionsArgument) 71 | } 72 | } 73 | 74 | deepmerge.all = function deepmergeAll(array, optionsArgument) { 75 | if (!Array.isArray(array) || array.length < 2) { 76 | throw new Error('first argument should be an array with at least two elements') 77 | } 78 | 79 | // we are sure there are at least 2 values, so it is safe to have no initial value 80 | return array.reduce(function(prev, next) { 81 | return deepmerge(prev, next, optionsArgument) 82 | }) 83 | }; 84 | 85 | window.deepmerge = deepmerge 86 | -------------------------------------------------------------------------------- /vue_i18n/static/lib/vue-i18n.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * vue-i18n v8.20.0 3 | * (c) 2020 kazuya kawaguchi 4 | * Released under the MIT License. 5 | */ 6 | var t,e;t=this,e=function(){"use strict";var t=["style","currency","currencyDisplay","useGrouping","minimumIntegerDigits","minimumFractionDigits","maximumFractionDigits","minimumSignificantDigits","maximumSignificantDigits","localeMatcher","formatMatcher","unit"];function e(t,e){"undefined"!=typeof console&&(console.warn("[vue-i18n] "+t),e&&console.warn(e.stack))}var n=Array.isArray;function r(t){return null!==t&&"object"==typeof t}function a(t){return"string"==typeof t}var i=Object.prototype.toString,o="[object Object]";function s(t){return i.call(t)===o}function l(t){return null==t}function c(){for(var t=[],e=arguments.length;e--;)t[e]=arguments[e];var n=null,a=null;return 1===t.length?r(t[0])||Array.isArray(t[0])?a=t[0]:"string"==typeof t[0]&&(n=t[0]):2===t.length&&("string"==typeof t[0]&&(n=t[0]),(r(t[1])||Array.isArray(t[1]))&&(a=t[1])),{locale:n,params:a}}function u(t){return JSON.parse(JSON.stringify(t))}function h(t,e){return!!~t.indexOf(e)}var f=Object.prototype.hasOwnProperty;function p(t,e){return f.call(t,e)}function m(t){for(var e=arguments,n=Object(t),a=1;a0;)e[n]=arguments[n+1];var r=this.$i18n;return r._t.apply(r,[t,r.locale,r._getMessages(),this].concat(e))},t.prototype.$tc=function(t,e){for(var n=[],r=arguments.length-2;r-- >0;)n[r]=arguments[r+2];var a=this.$i18n;return a._tc.apply(a,[t,a.locale,a._getMessages(),this,e].concat(n))},t.prototype.$te=function(t,e){var n=this.$i18n;return n._te(t,n.locale,n._getMessages(),e)},t.prototype.$d=function(t){for(var e,n=[],r=arguments.length-1;r-- >0;)n[r]=arguments[r+1];return(e=this.$i18n).d.apply(e,[t].concat(n))},t.prototype.$n=function(t){for(var e,n=[],r=arguments.length-1;r-- >0;)n[r]=arguments[r+1];return(e=this.$i18n).n.apply(e,[t].concat(n))}}(F),F.mixin(g),F.directive("t",{bind:w,update:$,unbind:M}),F.component(v.name,v),F.component(k.name,k),F.config.optionMergeStrategies.i18n=function(t,e){return void 0===e?t:e}}var D=function(){this._caches=Object.create(null)};D.prototype.interpolate=function(t,e){if(!e)return[t];var n=this._caches[t];return n||(n=function(t){var e=[],n=0,r="";for(;n0)h--,u=R,f[W]();else{if(h=0,void 0===n)return!1;if(!1===(n=J(n)))return!1;f[j]()}};null!==u;)if("\\"!==(e=t[++c])||!p()){if(a=U(e),(i=(s=z[u])[a]||s.else||E)===E)return;if(u=i[0],(o=f[i[1]])&&(r=void 0===(r=i[2])?e:r,!1===o()))return;if(u===V)return l}}(t))&&(this._cache[t]=e),e||[]},q.prototype.getPathValue=function(t,e){if(!r(t))return null;var n=this.parsePath(e);if(0===n.length)return null;for(var a=n.length,i=t,o=0;o/,Z=/(?:@(?:\.[a-z]+)?:(?:[\w\-_|.]+|\([\w\-_|.]+\)))/g,K=/^@(?:\.([a-z]+))?:/,Q=/[()]/g,Y={upper:function(t){return t.toLocaleUpperCase()},lower:function(t){return t.toLocaleLowerCase()},capitalize:function(t){return""+t.charAt(0).toLocaleUpperCase()+t.substr(1)}},tt=new D,et=function(t){var e=this;void 0===t&&(t={}),!F&&"undefined"!=typeof window&&window.Vue&&I(window.Vue);var n=t.locale||"en-US",r=!1!==t.fallbackLocale&&(t.fallbackLocale||"en-US"),a=t.messages||{},i=t.dateTimeFormats||{},o=t.numberFormats||{};this._vm=null,this._formatter=t.formatter||tt,this._modifiers=t.modifiers||{},this._missing=t.missing||null,this._root=t.root||null,this._sync=void 0===t.sync||!!t.sync,this._fallbackRoot=void 0===t.fallbackRoot||!!t.fallbackRoot,this._formatFallbackMessages=void 0!==t.formatFallbackMessages&&!!t.formatFallbackMessages,this._silentTranslationWarn=void 0!==t.silentTranslationWarn&&t.silentTranslationWarn,this._silentFallbackWarn=void 0!==t.silentFallbackWarn&&!!t.silentFallbackWarn,this._dateTimeFormatters={},this._numberFormatters={},this._path=new q,this._dataListeners=[],this._componentInstanceCreatedListener=t.componentInstanceCreatedListener||null,this._preserveDirectiveContent=void 0!==t.preserveDirectiveContent&&!!t.preserveDirectiveContent,this.pluralizationRules=t.pluralizationRules||{},this._warnHtmlInMessage=t.warnHtmlInMessage||"off",this._postTranslation=t.postTranslation||null,this.getChoiceIndex=function(t,n){var r=Object.getPrototypeOf(e);if(r&&r.getChoiceIndex)return r.getChoiceIndex.call(e,t,n);var a,i;return e.locale in e.pluralizationRules?e.pluralizationRules[e.locale].apply(e,[t,n]):(a=t,i=n,a=Math.abs(a),2===i?a?a>1?1:0:1:a?Math.min(a,2):0)},this._exist=function(t,n){return!(!t||!n)&&(!l(e._path.getPathValue(t,n))||!!t[n])},"warn"!==this._warnHtmlInMessage&&"error"!==this._warnHtmlInMessage||Object.keys(a).forEach(function(t){e._checkLocaleMessage(t,e._warnHtmlInMessage,a[t])}),this._initVM({locale:n,fallbackLocale:r,messages:a,dateTimeFormats:i,numberFormats:o})},nt={vm:{configurable:!0},messages:{configurable:!0},dateTimeFormats:{configurable:!0},numberFormats:{configurable:!0},availableLocales:{configurable:!0},locale:{configurable:!0},fallbackLocale:{configurable:!0},formatFallbackMessages:{configurable:!0},missing:{configurable:!0},formatter:{configurable:!0},silentTranslationWarn:{configurable:!0},silentFallbackWarn:{configurable:!0},preserveDirectiveContent:{configurable:!0},warnHtmlInMessage:{configurable:!0},postTranslation:{configurable:!0}};return et.prototype._checkLocaleMessage=function(t,n,r){var i=function(t,n,r,o){if(s(r))Object.keys(r).forEach(function(e){var a=r[e];s(a)?(o.push(e),o.push("."),i(t,n,a,o),o.pop(),o.pop()):(o.push(e),i(t,n,a,o),o.pop())});else if(Array.isArray(r))r.forEach(function(e,r){s(e)?(o.push("["+r+"]"),o.push("."),i(t,n,e,o),o.pop(),o.pop()):(o.push("["+r+"]"),i(t,n,e,o),o.pop())});else if(a(r)){if(X.test(r)){var l="Detected HTML in message '"+r+"' of keypath '"+o.join("")+"' at '"+n+"'. Consider component interpolation with '' to avoid XSS. See https://bit.ly/2ZqJzkp";"warn"===t?e(l):"error"===t&&function(t,e){"undefined"!=typeof console&&(console.error("[vue-i18n] "+t),e&&console.error(e.stack))}(l)}}};i(n,t,r,[])},et.prototype._initVM=function(t){var e=F.config.silent;F.config.silent=!0,this._vm=new F({data:t}),F.config.silent=e},et.prototype.destroyVM=function(){this._vm.$destroy()},et.prototype.subscribeDataChanging=function(t){this._dataListeners.push(t)},et.prototype.unsubscribeDataChanging=function(t){!function(t,e){if(t.length){var n=t.indexOf(e);if(n>-1)t.splice(n,1)}}(this._dataListeners,t)},et.prototype.watchI18nData=function(){var t=this;return this._vm.$watch("$data",function(){for(var e=t._dataListeners.length;e--;)F.nextTick(function(){t._dataListeners[e]&&t._dataListeners[e].$forceUpdate()})},{deep:!0})},et.prototype.watchLocale=function(){if(!this._sync||!this._root)return null;var t=this._vm;return this._root.$i18n.vm.$watch("locale",function(e){t.$set(t,"locale",e),t.$forceUpdate()},{immediate:!0})},et.prototype.onComponentInstanceCreated=function(t){this._componentInstanceCreatedListener&&this._componentInstanceCreatedListener(t,this)},nt.vm.get=function(){return this._vm},nt.messages.get=function(){return u(this._getMessages())},nt.dateTimeFormats.get=function(){return u(this._getDateTimeFormats())},nt.numberFormats.get=function(){return u(this._getNumberFormats())},nt.availableLocales.get=function(){return Object.keys(this.messages).sort()},nt.locale.get=function(){return this._vm.locale},nt.locale.set=function(t){this._vm.$set(this._vm,"locale",t)},nt.fallbackLocale.get=function(){return this._vm.fallbackLocale},nt.fallbackLocale.set=function(t){this._localeChainCache={},this._vm.$set(this._vm,"fallbackLocale",t)},nt.formatFallbackMessages.get=function(){return this._formatFallbackMessages},nt.formatFallbackMessages.set=function(t){this._formatFallbackMessages=t},nt.missing.get=function(){return this._missing},nt.missing.set=function(t){this._missing=t},nt.formatter.get=function(){return this._formatter},nt.formatter.set=function(t){this._formatter=t},nt.silentTranslationWarn.get=function(){return this._silentTranslationWarn},nt.silentTranslationWarn.set=function(t){this._silentTranslationWarn=t},nt.silentFallbackWarn.get=function(){return this._silentFallbackWarn},nt.silentFallbackWarn.set=function(t){this._silentFallbackWarn=t},nt.preserveDirectiveContent.get=function(){return this._preserveDirectiveContent},nt.preserveDirectiveContent.set=function(t){this._preserveDirectiveContent=t},nt.warnHtmlInMessage.get=function(){return this._warnHtmlInMessage},nt.warnHtmlInMessage.set=function(t){var e=this,n=this._warnHtmlInMessage;if(this._warnHtmlInMessage=t,n!==t&&("warn"===t||"error"===t)){var r=this._getMessages();Object.keys(r).forEach(function(t){e._checkLocaleMessage(t,e._warnHtmlInMessage,r[t])})}},nt.postTranslation.get=function(){return this._postTranslation},nt.postTranslation.set=function(t){this._postTranslation=t},et.prototype._getMessages=function(){return this._vm.messages},et.prototype._getDateTimeFormats=function(){return this._vm.dateTimeFormats},et.prototype._getNumberFormats=function(){return this._vm.numberFormats},et.prototype._warnDefault=function(t,e,n,r,i,o){if(!l(n))return n;if(this._missing){var s=this._missing.apply(null,[t,e,r,i]);if(a(s))return s}if(this._formatFallbackMessages){var u=c.apply(void 0,i);return this._render(e,o,u.params,e)}return e},et.prototype._isFallbackRoot=function(t){return!t&&!l(this._root)&&this._fallbackRoot},et.prototype._isSilentFallbackWarn=function(t){return this._silentFallbackWarn instanceof RegExp?this._silentFallbackWarn.test(t):this._silentFallbackWarn},et.prototype._isSilentFallback=function(t,e){return this._isSilentFallbackWarn(e)&&(this._isFallbackRoot()||t!==this.fallbackLocale)},et.prototype._isSilentTranslationWarn=function(t){return this._silentTranslationWarn instanceof RegExp?this._silentTranslationWarn.test(t):this._silentTranslationWarn},et.prototype._interpolate=function(t,e,n,r,i,o,c){if(!e)return null;var u,h=this._path.getPathValue(e,n);if(Array.isArray(h)||s(h))return h;if(l(h)){if(!s(e))return null;if(!a(u=e[n]))return null}else{if(!a(h))return null;u=h}return(u.indexOf("@:")>=0||u.indexOf("@.")>=0)&&(u=this._link(t,e,u,r,"raw",o,c)),this._render(u,i,o,n)},et.prototype._link=function(t,e,n,r,a,i,o){var s=n,l=s.match(Z);for(var c in l)if(l.hasOwnProperty(c)){var u=l[c],f=u.match(K),p=f[0],m=f[1],_=u.replace(p,"").replace(Q,"");if(h(o,_))return s;o.push(_);var g=this._interpolate(t,e,_,r,"raw"===a?"string":a,"raw"===a?void 0:i,o);if(this._isFallbackRoot(g)){if(!this._root)throw Error("unexpected error");var v=this._root.$i18n;g=v._translate(v._getMessages(),v.locale,v.fallbackLocale,_,r,a,i)}g=this._warnDefault(t,_,g,r,Array.isArray(i)?i:[i],a),this._modifiers.hasOwnProperty(m)?g=this._modifiers[m](g):Y.hasOwnProperty(m)&&(g=Y[m](g)),o.pop(),s=g?s.replace(u,g):s}return s},et.prototype._render=function(t,e,n,r){var i=this._formatter.interpolate(t,n,r);return i||(i=tt.interpolate(t,n,r)),"string"!==e||a(i)?i:i.join("")},et.prototype._appendItemToChain=function(t,e,n){var r=!1;return h(t,e)||(r=!0,e&&(r="!"!==e[e.length-1],e=e.replace(/!/g,""),t.push(e),n&&n[e]&&(r=n[e]))),r},et.prototype._appendLocaleToChain=function(t,e,n){var r,a=e.split("-");do{var i=a.join("-");r=this._appendItemToChain(t,i,n),a.splice(-1,1)}while(a.length&&!0===r);return r},et.prototype._appendBlockToChain=function(t,e,n){for(var r=!0,i=0;i0;)i[o]=arguments[o+4];if(!t)return"";var s=c.apply(void 0,i),l=s.locale||e,u=this._translate(n,l,this.fallbackLocale,t,r,"string",s.params);if(this._isFallbackRoot(u)){if(!this._root)throw Error("unexpected error");return(a=this._root).$t.apply(a,[t].concat(i))}return u=this._warnDefault(l,t,u,r,i,"string"),this._postTranslation&&null!=u&&(u=this._postTranslation(u,t)),u},et.prototype.t=function(t){for(var e,n=[],r=arguments.length-1;r-- >0;)n[r]=arguments[r+1];return(e=this)._t.apply(e,[t,this.locale,this._getMessages(),null].concat(n))},et.prototype._i=function(t,e,n,r,a){var i=this._translate(n,e,this.fallbackLocale,t,r,"raw",a);if(this._isFallbackRoot(i)){if(!this._root)throw Error("unexpected error");return this._root.$i18n.i(t,e,a)}return this._warnDefault(e,t,i,r,[a],"raw")},et.prototype.i=function(t,e,n){return t?(a(e)||(e=this.locale),this._i(t,e,this._getMessages(),null,n)):""},et.prototype._tc=function(t,e,n,r,a){for(var i,o=[],s=arguments.length-5;s-- >0;)o[s]=arguments[s+5];if(!t)return"";void 0===a&&(a=1);var l={count:a,n:a},u=c.apply(void 0,o);return u.params=Object.assign(l,u.params),o=null===u.locale?[u.params]:[u.locale,u.params],this.fetchChoice((i=this)._t.apply(i,[t,e,n,r].concat(o)),a)},et.prototype.fetchChoice=function(t,e){if(!t&&!a(t))return null;var n=t.split("|");return n[e=this.getChoiceIndex(e,n.length)]?n[e].trim():t},et.prototype.tc=function(t,e){for(var n,r=[],a=arguments.length-2;a-- >0;)r[a]=arguments[a+2];return(n=this)._tc.apply(n,[t,this.locale,this._getMessages(),null,e].concat(r))},et.prototype._te=function(t,e,n){for(var r=[],a=arguments.length-3;a-- >0;)r[a]=arguments[a+3];var i=c.apply(void 0,r).locale||e;return this._exist(n[i],t)},et.prototype.te=function(t,e){return this._te(t,this.locale,this._getMessages(),e)},et.prototype.getLocaleMessage=function(t){return u(this._vm.messages[t]||{})},et.prototype.setLocaleMessage=function(t,e){"warn"!==this._warnHtmlInMessage&&"error"!==this._warnHtmlInMessage||this._checkLocaleMessage(t,this._warnHtmlInMessage,e),this._vm.$set(this._vm.messages,t,e)},et.prototype.mergeLocaleMessage=function(t,e){"warn"!==this._warnHtmlInMessage&&"error"!==this._warnHtmlInMessage||this._checkLocaleMessage(t,this._warnHtmlInMessage,e),this._vm.$set(this._vm.messages,t,m({},this._vm.messages[t]||{},e))},et.prototype.getDateTimeFormat=function(t){return u(this._vm.dateTimeFormats[t]||{})},et.prototype.setDateTimeFormat=function(t,e){this._vm.$set(this._vm.dateTimeFormats,t,e),this._clearDateTimeFormat(t,e)},et.prototype.mergeDateTimeFormat=function(t,e){this._vm.$set(this._vm.dateTimeFormats,t,m(this._vm.dateTimeFormats[t]||{},e)),this._clearDateTimeFormat(t,e)},et.prototype._clearDateTimeFormat=function(t,e){for(var n in e){var r=t+"__"+n;this._dateTimeFormatters.hasOwnProperty(r)&&delete this._dateTimeFormatters[r]}},et.prototype._localizeDateTime=function(t,e,n,r,a){for(var i=e,o=r[i],s=this._getLocaleChain(e,n),c=0;c0;)e[n]=arguments[n+1];var i=this.locale,o=null;return 1===e.length?a(e[0])?o=e[0]:r(e[0])&&(e[0].locale&&(i=e[0].locale),e[0].key&&(o=e[0].key)):2===e.length&&(a(e[0])&&(o=e[0]),a(e[1])&&(i=e[1])),this._d(t,i,o)},et.prototype.getNumberFormat=function(t){return u(this._vm.numberFormats[t]||{})},et.prototype.setNumberFormat=function(t,e){this._vm.$set(this._vm.numberFormats,t,e),this._clearNumberFormat(t,e)},et.prototype.mergeNumberFormat=function(t,e){this._vm.$set(this._vm.numberFormats,t,m(this._vm.numberFormats[t]||{},e)),this._clearNumberFormat(t,e)},et.prototype._clearNumberFormat=function(t,e){for(var n in e){var r=t+"__"+n;this._numberFormatters.hasOwnProperty(r)&&delete this._numberFormatters[r]}},et.prototype._getNumberFormatter=function(t,e,n,r,a,i){for(var o=e,s=r[o],c=this._getLocaleChain(e,n),u=0;u0;)n[i]=arguments[i+1];var o=this.locale,s=null,l=null;return 1===n.length?a(n[0])?s=n[0]:r(n[0])&&(n[0].locale&&(o=n[0].locale),n[0].key&&(s=n[0].key),l=Object.keys(n[0]).reduce(function(e,r){var a;return h(t,r)?Object.assign({},e,((a={})[r]=n[0][r],a)):e},null)):2===n.length&&(a(n[0])&&(s=n[0]),a(n[1])&&(o=n[1])),this._n(e,o,s,l)},et.prototype._ntp=function(t,e,n,r){if(!et.availabilities.numberFormat)return[];if(!n)return(r?new Intl.NumberFormat(e,r):new Intl.NumberFormat(e)).formatToParts(t);var a=this._getNumberFormatter(t,e,this.fallbackLocale,this._getNumberFormats(),n,r),i=a&&a.formatToParts(t);if(this._isFallbackRoot(i)){if(!this._root)throw Error("unexpected error");return this._root.$i18n._ntp(t,e,n,r)}return i||[]},Object.defineProperties(et.prototype,nt),Object.defineProperty(et,"availabilities",{get:function(){if(!G){var t="undefined"!=typeof Intl;G={dateTimeFormat:t&&void 0!==Intl.DateTimeFormat,numberFormat:t&&void 0!==Intl.NumberFormat}}return G}}),et.install=I,et.version="8.20.0",et},"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.VueI18n=e(); 7 | -------------------------------------------------------------------------------- /vue_i18n/static/src/js/i18n_factory.js: -------------------------------------------------------------------------------- 1 | odoo.define("vue_i18n.i18nFactory", function (require) { 2 | "use strict"; 3 | 4 | Vue.use(VueI18n) 5 | 6 | const Class = require("web.Class"); 7 | 8 | const I18nFactory = Class.extend({ 9 | init() { 10 | this.lang = 'en-US' 11 | this.messages = {}; 12 | }, 13 | addMessages(messages) { 14 | this.messages = deepmerge(this.messages, messages) 15 | }, 16 | setLang(lang) { 17 | this.lang = lang 18 | }, 19 | makeI18n(){ 20 | return new VueI18n({ 21 | locale: this.lang, 22 | messages: this.messages, 23 | }) 24 | }, 25 | }) 26 | 27 | return new I18nFactory(); 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /vue_i18n/views/assets.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /vue_router/README.rst: -------------------------------------------------------------------------------- 1 | Vue-Router 2 | ========== 3 | 4 | This module integrates `Vue Router `_ with the frontend assets of Odoo. 5 | 6 | Usage Example 7 | ------------- 8 | This example requires that you master of the following topics: 9 | 10 | * Creating a Vuejs application with VueRouter 11 | * Creating a Odoo controller 12 | * Creating a Odoo javascript extension 13 | 14 | Creating a VueRouter instance 15 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 16 | 17 | First, in a qweb template, you must define where your application will be nested inside Odoo. 18 | 19 | The above example inserts the application inside the portal. 20 | 21 | .. code-block:: xml 22 | 23 | 30 | 31 | Finally, create a controller, so that when you navigate to the URL /my/hello/world, 32 | the qweb template above is rendered. 33 | 34 | .. code-block:: python 35 | 36 | class PortalWithVueJs(CustomerPortal): 37 | 38 | @route(["/my/hello/world"], type="http", auth="user", website=True) 39 | def my_vuejs_application(self, **kw): 40 | return request.render("my_module.my_vuejs_application", {}) 41 | 42 | Then, add a file app.js which defines your Vue application. 43 | 44 | .. code-block:: javascript 45 | 46 | require("web.dom_ready"); 47 | 48 | if (!$("#my_app_node").length) { 49 | return $.Deferred().reject("DOM doesn't contain '#my_app_node'"); 50 | } 51 | 52 | const routerFactory = require("vue_router.routerFactory"); 53 | const router = routerFactory.makeRouter(); 54 | const app = new Vue({router}).$mount('#my_app_node'); 55 | 56 | Inside other javascript files, you may register your routes: 57 | 58 | .. code-block:: javascript 59 | 60 | const HelloWorld = { 61 | template: `
Hello World
`, 62 | } 63 | const routerFactory = require("vue_router.routerFactory"); 64 | routerFactory.addRoutes( 65 | [ 66 | { 67 | path: "/my/hello/world", 68 | component: HelloWorld, 69 | } 70 | ] 71 | ) 72 | 73 | The reason to seperate the routes from the application is to make your application extendable. 74 | 75 | Different Odoo modules may define their own routes. 76 | 77 | The RouterFactory Instance 78 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 79 | The module defines a singleton ``RouterFactory`` in javascript that allows to register routes to the Odoo frontend. 80 | 81 | This object has 2 methods: 82 | 83 | 1. ``addRoutes``: takes in parameter an array of routes. 84 | 2. ``makeRouter``: takes no parameter and returns a VueRouter instance. 85 | 86 | The method ``addRoutes`` can be called multiple times with different arrays of routes. 87 | 88 | However, it must be called before ``makeRouter``. 89 | Otherwise, the added routes will not be passed to the VueRouter instance. 90 | 91 | Contributors 92 | ------------ 93 | * Numigi (tm) and all its contributors (https://bit.ly/numigiens) 94 | 95 | More information 96 | ---------------- 97 | * Meet us at https://bit.ly/numigi-com 98 | -------------------------------------------------------------------------------- /vue_router/__init__.py: -------------------------------------------------------------------------------- 1 | # © 2020 Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). 3 | -------------------------------------------------------------------------------- /vue_router/__manifest__.py: -------------------------------------------------------------------------------- 1 | # © 2018 Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). 3 | 4 | { 5 | 'name': 'Vue-Router', 6 | 'version': '1.0.0', 7 | 'author': 'Numigi', 8 | 'maintainer': 'Numigi', 9 | 'website': 'https://bit.ly/numigi-com', 10 | 'license': 'LGPL-3', 11 | 'category': 'Web', 12 | 'summary': 'Integrate vue-router with Odoo frontend.', 13 | 'depends': ['vue_frontend'], 14 | 'data': ['views/assets.xml'], 15 | 'installable': True, 16 | } 17 | -------------------------------------------------------------------------------- /vue_router/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_router/static/description/icon.png -------------------------------------------------------------------------------- /vue_router/static/src/js/router_factory.js: -------------------------------------------------------------------------------- 1 | odoo.define("vue_router.routerFactory", function (require) { 2 | "use strict"; 3 | 4 | const Class = require("web.Class"); 5 | 6 | const RouterFactory = Class.extend({ 7 | init() { 8 | this.routes = []; 9 | }, 10 | addRoutes(routes) { 11 | this.routes.push(...routes); 12 | }, 13 | makeRouter(){ 14 | return new VueRouter({ 15 | mode: "history", 16 | routes: this.routes, 17 | }) 18 | }, 19 | }) 20 | 21 | return new RouterFactory(); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /vue_router/views/assets.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /vue_stock_forecast/README.rst: -------------------------------------------------------------------------------- 1 | Stock Forecast 2 | ============== 3 | 4 | A dynamic stock forecasting report. 5 | 6 | .. contents:: Table of Contents 7 | 8 | The report displays: 9 | 10 | * The quantities currently reserved and available in stock in the selected stock locations. 11 | * The planned stock moves for the following days / weeks / months. 12 | 13 | .. image:: static/description/report.png 14 | 15 | Behavior 16 | -------- 17 | When clicking on a blue amount, the detail that composes this amount is displayed. 18 | 19 | .. image:: static/description/report_links.png 20 | 21 | Here above, when clicking on the amount in stock `8`, the list of Quants is displayed: 22 | 23 | .. image:: static/description/stock_quants.png 24 | 25 | When clicking on the amount for a given month `128 (+120)`, the list of stock moves is displayed: 26 | 27 | .. image:: static/description/stock_moves.png 28 | 29 | Filters 30 | ------- 31 | Available filters include `Locations`, `Products`, `Product Categories`, `Start Date` and `End Date`. 32 | When changing the selected filters, the table is automatically updated. 33 | 34 | .. image:: static/description/filters.png 35 | 36 | Supplier Filter 37 | ~~~~~~~~~~~~~~~ 38 | It is also possible to filter by the list of suppliers of products. 39 | 40 | .. image:: static/description/product_suppliers.png 41 | 42 | .. image:: static/description/report_supplier_filter.png 43 | 44 | .. image:: static/description/report_filtered_by_supplier.png 45 | 46 | By default, the module show all products that have at least one price entry for the selected supplier. 47 | 48 | The module ``vue_stock_forecast_preferred_supplier`` defines an alternative behavior. 49 | See the module's README for more info. 50 | 51 | Grouping Rows 52 | ------------- 53 | The rows can be grouped by `Product` or `Product Category`. 54 | 55 | When grouping by categories, all children category are also displayed in the table. 56 | 57 | .. image:: static/description/group_by_category.png 58 | 59 | Grouping Columns 60 | ---------------- 61 | The columns can be grouped by `Day`, `Week` or `Month`. 62 | 63 | By default 6 columns of stock moves are displayed. 64 | By selecting specific `Start Date` and `End Date`, you can see more columns. 65 | 66 | .. image:: static/description/report_with_more_columns.png 67 | 68 | Min / Max 69 | --------- 70 | Since version 1.2.0 of the module, the report shows the aggregated sum of reordering rules (Min / Max). 71 | 72 | .. image:: static/description/report_min_max.png 73 | 74 | By clicking on the number, the list of reordering rules for this product is displayed. 75 | 76 | .. image:: static/description/reordering_rules_list.png 77 | 78 | Purchase Quotations 79 | ------------------- 80 | Since version 1.2.0 of the module, the report shows the quantities in draft (or sent) purchase quotations. 81 | 82 | .. image:: static/description/report_quotations.png 83 | 84 | By clicking on the number, the list of purchase order lines for this product is displayed. 85 | 86 | .. image:: static/description/purchase_order_line_list.png 87 | 88 | Search Bar 89 | ---------- 90 | Since version 1.2.0 of the module, a new search bar is available to filter the report lines. 91 | 92 | .. image:: static/description/search_bar.png 93 | 94 | When typing in the search bar, the lines are filtered in real-time. 95 | 96 | .. image:: static/description/report_lines_filtered.png 97 | 98 | Product Smart Button 99 | -------------------- 100 | A smart button is added on the form view of products in order to show the report. 101 | 102 | .. image:: static/description/product_form.png 103 | 104 | When accessing the report from a product template with multiple variants, the report is displayed for all variants. 105 | 106 | .. image:: static/description/report_with_product_variants.png 107 | 108 | Purchase Order Smart Button 109 | --------------------------- 110 | Since version 1.3.0 of the module, a smart button is also available from a purchase order. 111 | 112 | .. image:: static/description/purchase_order_form.png 113 | 114 | When I click on the button, the report is opened with the products contained in the purchase order. 115 | 116 | .. image:: static/description/report_from_purchase_order.png 117 | 118 | Contributors 119 | ------------ 120 | * Numigi (tm) and all its contributors (https://bit.ly/numigiens) 121 | 122 | More information 123 | ---------------- 124 | 125 | * Meet us at https://bit.ly/numigi-com 126 | -------------------------------------------------------------------------------- /vue_stock_forecast/__init__.py: -------------------------------------------------------------------------------- 1 | # © 2020 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 3 | 4 | from . import report 5 | -------------------------------------------------------------------------------- /vue_stock_forecast/__manifest__.py: -------------------------------------------------------------------------------- 1 | # © 2020 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 3 | 4 | { 5 | "name": "Vue Stock Forecast", 6 | "version": "1.3.1", 7 | "author": "Numigi", 8 | "maintainer": "Numigi", 9 | "website": "https://bit.ly/numigi-com", 10 | "license": "LGPL-3", 11 | "category": "Stock", 12 | "summary": "A dynamic stock forecast report using Vuejs.", 13 | "depends": ["vue_backend", "vue_element_ui", "purchase_stock"], 14 | "data": [ 15 | "views/assets.xml", 16 | "views/menu.xml", 17 | "views/product.xml", 18 | "views/purchase_order.xml", 19 | ], 20 | } 21 | -------------------------------------------------------------------------------- /vue_stock_forecast/i18n/fr.po: -------------------------------------------------------------------------------- 1 | # Translation of Odoo Server. 2 | # This file contains the translation of the following modules: 3 | # * disable_quick_create 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: Odoo Server 10.0+e\n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2021-06-02 15:48+0000\n" 10 | "PO-Revision-Date: 2021-06-02 11:50-0400\n" 11 | "Last-Translator: <>\n" 12 | "Language-Team: \n" 13 | "Language: fr\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Plural-Forms: \n" 18 | "X-Generator: Poedit 2.3\n" 19 | 20 | #. module: vue_stock_forecast 21 | #. openerp-web 22 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:370 23 | #, python-format 24 | msgid "Available " 25 | msgstr "Disponible" 26 | 27 | #. module: vue_stock_forecast 28 | #. openerp-web 29 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:371 30 | #, python-format 31 | msgid "Columns " 32 | msgstr "Colonnes" 33 | 34 | #. module: vue_stock_forecast 35 | #. openerp-web 36 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:178 37 | #, python-format 38 | msgid "Current Stocks" 39 | msgstr "Stocks en cours" 40 | 41 | #. module: vue_stock_forecast 42 | #. openerp-web 43 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:372 44 | #, python-format 45 | msgid "Day " 46 | msgstr "Jour" 47 | 48 | #. module: vue_stock_forecast 49 | #: model:ir.model.fields,field_description:vue_stock_forecast.field_vue_stock_forecast__display_name 50 | msgid "Display Name" 51 | msgstr "" 52 | 53 | #. module: vue_stock_forecast 54 | #. openerp-web 55 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:373 56 | #, python-format 57 | msgid "End Date " 58 | msgstr "Date de fin" 59 | 60 | #. module: vue_stock_forecast 61 | #: model:ir.model.fields,field_description:vue_stock_forecast.field_vue_stock_forecast__id 62 | msgid "ID" 63 | msgstr "" 64 | 65 | #. module: vue_stock_forecast 66 | #: model:ir.model.fields,field_description:vue_stock_forecast.field_vue_stock_forecast____last_update 67 | msgid "Last Modified on" 68 | msgstr "" 69 | 70 | #. module: vue_stock_forecast 71 | #. openerp-web 72 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:374 73 | #, python-format 74 | msgid "Location " 75 | msgstr "Emplacement" 76 | 77 | #. module: vue_stock_forecast 78 | #. openerp-web 79 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:375 80 | #, python-format 81 | msgid "Min / Max " 82 | msgstr "Min / Max" 83 | 84 | #. module: vue_stock_forecast 85 | #. openerp-web 86 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:376 87 | #, python-format 88 | msgid "Month " 89 | msgstr "Mois" 90 | 91 | #. module: vue_stock_forecast 92 | #. openerp-web 93 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:377 94 | #, python-format 95 | msgid "Product " 96 | msgstr "Article" 97 | 98 | #. module: vue_stock_forecast 99 | #. openerp-web 100 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:378 101 | #, python-format 102 | msgid "Product Categories " 103 | msgstr "Catégories d'articles" 104 | 105 | #. module: vue_stock_forecast 106 | #. openerp-web 107 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:379 108 | #, python-format 109 | msgid "Product Category " 110 | msgstr "Catégorie d'article" 111 | 112 | #. module: vue_stock_forecast 113 | #. openerp-web 114 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:380 115 | #, python-format 116 | msgid "Products " 117 | msgstr "Articles" 118 | 119 | #. module: vue_stock_forecast 120 | #: model:ir.model,name:vue_stock_forecast.model_purchase_order 121 | msgid "Purchase Order" 122 | msgstr "" 123 | 124 | #. module: vue_stock_forecast 125 | #. openerp-web 126 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:381 127 | #, python-format 128 | msgid "Quotation " 129 | msgstr "Demande de prix" 130 | 131 | #. module: vue_stock_forecast 132 | #. openerp-web 133 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:382 134 | #, python-format 135 | msgid "Reserved " 136 | msgstr "Réservé" 137 | 138 | #. module: vue_stock_forecast 139 | #. openerp-web 140 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:383 141 | #, python-format 142 | msgid "Rows " 143 | msgstr "Lignes" 144 | 145 | #. module: vue_stock_forecast 146 | #. openerp-web 147 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:384 148 | #, python-format 149 | msgid "Search " 150 | msgstr "Rechercher" 151 | 152 | #. module: vue_stock_forecast 153 | #. openerp-web 154 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:385 155 | #, python-format 156 | msgid "Start Date " 157 | msgstr "Date de début" 158 | 159 | #. module: vue_stock_forecast 160 | #. openerp-web 161 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:386 162 | #, python-format 163 | msgid "Stock " 164 | msgstr "Stock" 165 | 166 | #. module: vue_stock_forecast 167 | #: model:ir.actions.client,name:vue_stock_forecast.stock_forecast_action 168 | #: model:ir.ui.menu,name:vue_stock_forecast.stock_forecast_menu 169 | #: model_terms:ir.ui.view,arch_db:vue_stock_forecast.product_form_with_stock_forecast_smart_button 170 | #: model_terms:ir.ui.view,arch_db:vue_stock_forecast.product_template_form_with_stock_forecast_smart_button 171 | #: model_terms:ir.ui.view,arch_db:vue_stock_forecast.purchase_order_form 172 | msgid "Stock Forecast" 173 | msgstr "Prévision d'inventaire" 174 | 175 | #. module: vue_stock_forecast 176 | #. openerp-web 177 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:287 178 | #, python-format 179 | msgid "Stock Moves ({date_from} to {date_to})" 180 | msgstr "Mouvements d'inventaire (du {date_from} au {date_to})" 181 | 182 | #. module: vue_stock_forecast 183 | #. openerp-web 184 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:387 185 | #, python-format 186 | msgid "Supplier " 187 | msgstr "Fournisseur" 188 | 189 | #. module: vue_stock_forecast 190 | #: model:ir.model,name:vue_stock_forecast.model_vue_stock_forecast 191 | #, fuzzy 192 | #| msgid "Vue Stock Forecast " 193 | msgid "Vue Stock Forecast" 194 | msgstr "Prévision d'inventaire" 195 | 196 | #. module: vue_stock_forecast 197 | #. openerp-web 198 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:388 199 | #, python-format 200 | msgid "Week " 201 | msgstr "Semaine" 202 | 203 | #. module: vue_stock_forecast 204 | #. openerp-web 205 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:187 206 | #, python-format 207 | msgid "{}: Min / Max" 208 | msgstr "{} : Min / Max" 209 | 210 | #. module: vue_stock_forecast 211 | #. openerp-web 212 | #: code:addons/vue_stock_forecast/static/src/js/StockForecastReport.js:223 213 | #, python-format 214 | msgid "{}: Quotation" 215 | msgstr "{} : Demande de prix" 216 | -------------------------------------------------------------------------------- /vue_stock_forecast/report/__init__.py: -------------------------------------------------------------------------------- 1 | # © 2020 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 3 | 4 | from . import vue_stock_forecast 5 | -------------------------------------------------------------------------------- /vue_stock_forecast/report/vue_stock_forecast.py: -------------------------------------------------------------------------------- 1 | # © 2020 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 3 | 4 | import pytz 5 | from datetime import datetime 6 | from odoo import api, models 7 | from odoo.tools import DEFAULT_SERVER_DATE_FORMAT 8 | from odoo.osv.expression import OR 9 | 10 | 11 | class VueStockForecast(models.AbstractModel): 12 | 13 | _name = "vue.stock.forecast" 14 | _description = "Vue Stock Forecast" 15 | 16 | @api.model 17 | def fetch(self, options): 18 | stock_data = self._get_stock_data(options) 19 | 20 | if options.get("groupBy") == "category": 21 | rows = self._make_product_category_rows(stock_data, options) 22 | else: 23 | rows = stock_data 24 | 25 | return sorted( 26 | list(rows.values()), 27 | key=lambda r: r["label"], 28 | ) 29 | 30 | def _get_stock_data(self, options): 31 | products = self._get_products(options) 32 | data = self._get_data(products, options) 33 | return {p: self._make_row_data(p, data) for p in products} 34 | 35 | def _get_data(self, products, options): 36 | return { 37 | "incoming_moves": self._get_incoming_stock_moves(products, options), 38 | "outgoing_moves": self._get_outgoing_stock_moves(products, options), 39 | "quants": self._get_stock_quants(products, options), 40 | "orderpoints": self._get_orderpoints(products, options), 41 | "purchase_lines": self._get_purchase_lines(products, options), 42 | } 43 | 44 | def _make_row_data(self, product, data): 45 | in_moves = data["incoming_moves"].filtered(lambda m: m.product_id == product) 46 | out_moves = data["outgoing_moves"].filtered(lambda m: m.product_id == product) 47 | quants = data["quants"].filtered(lambda q: q.product_id == product) 48 | orderpoints = data["orderpoints"].filtered(lambda o: o.product_id == product) 49 | return { 50 | "label": product.display_name, 51 | "productId": product.id, 52 | "uom": product.uom_id, 53 | "currentStock": sum(q.quantity for q in quants), 54 | "reserved": sum(q.reserved_quantity for q in quants), 55 | "incoming": [self._format_stock_move(m) for m in in_moves], 56 | "outgoing": [self._format_stock_move(m) for m in out_moves], 57 | "min": sum(orderpoints.mapped("product_min_qty")), 58 | "max": sum(orderpoints.mapped("product_max_qty")), 59 | "purchased": self._get_product_purchased_quantity(product, data), 60 | } 61 | 62 | def _get_product_purchased_quantity(self, product, data): 63 | purchase_lines = data["purchase_lines"].filtered(lambda o: o.product_id == product) 64 | return sum( 65 | l.product_uom._compute_quantity(l.product_qty, product.uom_id) for l in purchase_lines 66 | ) 67 | 68 | def _format_stock_move(self, move): 69 | return {"qty": move.product_qty, "date": self._format_date(move.date_expected)} 70 | 71 | def _format_date(self, naive_datetime): 72 | utc_datetime = pytz.utc.localize(naive_datetime) 73 | tz = self._context.get("tz") or "UCT" 74 | tz_datetime = utc_datetime.astimezone(pytz.timezone(tz)) 75 | return datetime.strftime(tz_datetime, DEFAULT_SERVER_DATE_FORMAT) 76 | 77 | def _get_products(self, options): 78 | product_ids = options.get("products") or [] 79 | category_ids = options.get("categories") or [] 80 | supplier_ids = options.get("suppliers") or [] 81 | 82 | if not product_ids and not category_ids and not supplier_ids: 83 | return self.env["product.product"] 84 | 85 | domain = [] 86 | 87 | if product_ids: 88 | domain = OR([domain, [("id", "in", product_ids)]]) 89 | 90 | if category_ids: 91 | domain = OR([domain, [("categ_id", "child_of", category_ids)]]) 92 | 93 | if supplier_ids: 94 | supplier_domain = self._get_supplier_domain(supplier_ids) 95 | domain = OR([domain, supplier_domain]) 96 | 97 | products = self.env["product.product"].search(domain) 98 | return products.filtered(lambda p: p.type in ("product", "consu")) 99 | 100 | def _get_supplier_domain(self, supplier_ids): 101 | products = self._get_supplier_products(supplier_ids) 102 | return [("id", "in", products.ids)] 103 | 104 | def _get_supplier_products(self, supplier_ids): 105 | supplier_info = self.env["product.supplierinfo"].search( 106 | [ 107 | ("name.commercial_partner_id", "in", supplier_ids), 108 | ] 109 | ) 110 | products = self.env["product.product"] 111 | 112 | for info in supplier_info: 113 | if info.product_id: 114 | products |= info.product_id 115 | else: 116 | products |= info.product_tmpl_id.product_variant_ids 117 | 118 | return products 119 | 120 | def _get_incoming_stock_moves(self, products, options): 121 | domain = [ 122 | ("state", "not in", ["done", "cancel"]), 123 | ("location_id.usage", "!=", "internal"), 124 | ("location_dest_id.usage", "=", "internal"), 125 | ("product_id", "in", products.ids), 126 | ] 127 | 128 | if options.get("locations"): 129 | domain.append( 130 | ("location_dest_id", "child_of", options["locations"]), 131 | ) 132 | 133 | return self.env["stock.move"].search(domain) 134 | 135 | def _get_outgoing_stock_moves(self, products, options): 136 | domain = [ 137 | ("state", "not in", ["done", "cancel"]), 138 | ("location_id.usage", "=", "internal"), 139 | ("location_dest_id.usage", "!=", "internal"), 140 | ("product_id", "in", products.ids), 141 | ] 142 | 143 | if options.get("locations"): 144 | domain.append( 145 | ("location_id", "child_of", options["locations"]), 146 | ) 147 | 148 | return self.env["stock.move"].search(domain) 149 | 150 | def _get_stock_quants(self, products, options): 151 | domain = [ 152 | ("location_id.usage", "=", "internal"), 153 | ("product_id", "in", products.ids), 154 | ] 155 | 156 | if options.get("locations"): 157 | domain.append( 158 | ("location_id", "child_of", options["locations"]), 159 | ) 160 | 161 | return self.env["stock.quant"].search(domain) 162 | 163 | def _get_orderpoints(self, products, options): 164 | domain = [ 165 | ("product_id", "in", products.ids), 166 | ] 167 | 168 | if options.get("locations"): 169 | domain.append( 170 | ("location_id", "child_of", options["locations"]), 171 | ) 172 | 173 | return self.env["stock.warehouse.orderpoint"].search(domain) 174 | 175 | def _get_purchase_lines(self, products, options): 176 | domain = [ 177 | ("product_id", "in", products.ids), 178 | ("state", "in", ("draft", "sent", "to approve")), 179 | ] 180 | return self.env["purchase.order.line"].search(domain) 181 | 182 | def _make_product_category_rows(self, stock_data, options): 183 | rows = {} 184 | all_categories = self._get_all_categories(options) 185 | 186 | def get_matching_row(category, uom): 187 | index = (category, uom) 188 | if index not in rows: 189 | rows[index] = self._make_empty_category_row(category, uom) 190 | return rows[index] 191 | 192 | for category in all_categories: 193 | for product in self._get_products_from_category(category, stock_data): 194 | row = get_matching_row(category, product.uom_id) 195 | product_data = stock_data[product] 196 | row["currentStock"] += product_data["currentStock"] 197 | row["reserved"] += product_data["reserved"] 198 | row["incoming"].extend(product_data["incoming"]) 199 | row["outgoing"].extend(product_data["outgoing"]) 200 | row["min"] += product_data["min"] 201 | row["max"] += product_data["max"] 202 | row["purchased"] += product_data["purchased"] 203 | 204 | return rows 205 | 206 | def _get_products_from_category(self, category, stock_data): 207 | all_product_ids = [p.id for p in stock_data.keys()] 208 | return self.env["product.product"].search( 209 | [ 210 | ("id", "in", all_product_ids), 211 | ("categ_id", "child_of", category.id), 212 | ] 213 | ) 214 | 215 | def _make_empty_category_row(self, category, uom): 216 | return { 217 | "label": self._get_category_row_label(category, uom), 218 | "categoryId": category.id, 219 | "uomId": uom.id, 220 | "currentStock": 0, 221 | "reserved": 0, 222 | "incoming": [], 223 | "outgoing": [], 224 | "min": 0, 225 | "max": 0, 226 | "purchased": 0, 227 | } 228 | 229 | def _get_category_row_label(self, category, uom): 230 | units = self.env.ref("uom.product_uom_unit") 231 | if uom == units: 232 | return category.display_name 233 | else: 234 | return "{category} ({uom})".format( 235 | category=category.display_name, 236 | uom=uom.display_name, 237 | ) 238 | 239 | def _get_all_categories(self, options): 240 | category_ids = options.get("categories") or [] 241 | return self.env["product.category"].search( 242 | [ 243 | ("id", "child_of", category_ids), 244 | ] 245 | ) 246 | -------------------------------------------------------------------------------- /vue_stock_forecast/static/description/filters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/description/filters.png -------------------------------------------------------------------------------- /vue_stock_forecast/static/description/group_by_category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/description/group_by_category.png -------------------------------------------------------------------------------- /vue_stock_forecast/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/description/icon.png -------------------------------------------------------------------------------- /vue_stock_forecast/static/description/product_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/description/product_form.png -------------------------------------------------------------------------------- /vue_stock_forecast/static/description/product_suppliers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/description/product_suppliers.png -------------------------------------------------------------------------------- /vue_stock_forecast/static/description/purchase_order_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/description/purchase_order_form.png -------------------------------------------------------------------------------- /vue_stock_forecast/static/description/purchase_order_line_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/description/purchase_order_line_list.png -------------------------------------------------------------------------------- /vue_stock_forecast/static/description/reordering_rules_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/description/reordering_rules_list.png -------------------------------------------------------------------------------- /vue_stock_forecast/static/description/report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/description/report.png -------------------------------------------------------------------------------- /vue_stock_forecast/static/description/report_filtered_by_supplier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/description/report_filtered_by_supplier.png -------------------------------------------------------------------------------- /vue_stock_forecast/static/description/report_from_purchase_order.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/description/report_from_purchase_order.png -------------------------------------------------------------------------------- /vue_stock_forecast/static/description/report_lines_filtered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/description/report_lines_filtered.png -------------------------------------------------------------------------------- /vue_stock_forecast/static/description/report_links.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/description/report_links.png -------------------------------------------------------------------------------- /vue_stock_forecast/static/description/report_min_max.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/description/report_min_max.png -------------------------------------------------------------------------------- /vue_stock_forecast/static/description/report_quotations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/description/report_quotations.png -------------------------------------------------------------------------------- /vue_stock_forecast/static/description/report_supplier_filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/description/report_supplier_filter.png -------------------------------------------------------------------------------- /vue_stock_forecast/static/description/report_with_more_columns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/description/report_with_more_columns.png -------------------------------------------------------------------------------- /vue_stock_forecast/static/description/report_with_product_variants.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/description/report_with_product_variants.png -------------------------------------------------------------------------------- /vue_stock_forecast/static/description/search_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/description/search_bar.png -------------------------------------------------------------------------------- /vue_stock_forecast/static/description/stock_moves.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/description/stock_moves.png -------------------------------------------------------------------------------- /vue_stock_forecast/static/description/stock_quants.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/description/stock_quants.png -------------------------------------------------------------------------------- /vue_stock_forecast/static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue_stock_forecast", 3 | "version": "1.0.0", 4 | "description": "Stock Forecasting Widget for Odoo", 5 | "devDependencies": { 6 | "ava": "^3.12.1", 7 | "babel-core": "^6.26.3", 8 | "babel-loader": "^7.1.5", 9 | "babel-polyfill": "^6.26.0", 10 | "babel-preset-env": "^1.7.0", 11 | "vue": "^2.5.17", 12 | "vue-loader": "^15.3.0", 13 | "vue-template-compiler": "^2.5.17", 14 | "webpack": "^4.46.0" 15 | }, 16 | "author": "Numigi (tm) and all its contributors (https://bit.ly/numigiens)", 17 | "license": "LGPL-3.0-or-later", 18 | "dependencies": { 19 | "webpack-cli": "^3.3.12" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /vue_stock_forecast/static/src/css/stock_forecast.css: -------------------------------------------------------------------------------- 1 | .stock-forecast-report { 2 | margin: 8px; 3 | max-width: 100vw; 4 | } 5 | 6 | .stock-forecast-report .el-select{ 7 | width: 220px; 8 | } 9 | 10 | .stock-forecast-report .el-select input{ 11 | min-width: 150px; 12 | } 13 | 14 | .stock-forecast-table table { 15 | width: 100%; 16 | } 17 | 18 | .stock-forecast-table__link { 19 | cursor: pointer; 20 | color: #409eff; 21 | } 22 | 23 | .stock-forecast-report .el-form-item:not(:first-child){ 24 | margin-left: 50px; 25 | } 26 | 27 | .stock-forecast-table__amount { 28 | white-space: nowrap; 29 | } 30 | -------------------------------------------------------------------------------- /vue_stock_forecast/static/src/js/StockForecastReport.js: -------------------------------------------------------------------------------- 1 | odoo.define("vue_stock_forecast.StockForecastReport", function (require) { 2 | "use strict"; 3 | 4 | var QueryBuilder = require("vue.QueryBuilder"); 5 | var ControlPanelMixin = require("web.ControlPanelMixin"); 6 | var core = require("web.core"); 7 | var AbstractAction = require("web.AbstractAction"); 8 | var data = require("web.data"); 9 | 10 | var ReportComponent = Vue.extend(vueStockForecast.StockForecastReport); 11 | 12 | var nextRowKey = 1 13 | 14 | var _t = core._t; 15 | 16 | var StockForecastReport = AbstractAction.extend(ControlPanelMixin, { 17 | init: function (parent, action) { 18 | this._super.apply(this, arguments); 19 | this.context = action.context || {}; 20 | }, 21 | 22 | async start(){ 23 | await this._super(); 24 | 25 | this.$vm = new ReportComponent({ 26 | propsData: { 27 | // Filters 28 | searchProducts: (query) => this.searchProducts(query), 29 | searchProductCategories: (query) => this.searchProductCategories(query), 30 | searchStockLocations: (query) => this.searchStockLocations(query), 31 | searchSuppliers: (query) => this.searchSuppliers(query), 32 | 33 | // Function called when a filter changed 34 | onFilterChange: () => this.onFilterChange(), 35 | 36 | translate: _t, 37 | } 38 | }).$mount(this.$el[0]); 39 | 40 | this.$vm.$on("current-stock-clicked", (row) => this.onCurrentStockClicked(row)); 41 | this.$vm.$on("min-max-clicked", (row) => this.onMinMaxClicked(row)); 42 | this.$vm.$on("purchased-clicked", (row) => this.onPurchasedClicked(row)); 43 | this.$vm.$on( 44 | "move-amount-clicked", 45 | (row, dateFrom, dateTo) => this.onMoveAmountClicked(row, dateFrom, dateTo) 46 | ); 47 | 48 | this.setDefaultProduct(); 49 | this.setDefaultProductTemplate(); 50 | this.updateBreadrumb(); 51 | }, 52 | /** 53 | * Handle passing a default product id through the context. 54 | */ 55 | async setDefaultProduct(){ 56 | const productIds = await this._getProductIdsFromContext() 57 | if(productIds.length){ 58 | var query = new QueryBuilder("product.product", ["display_name"]); 59 | query.filter([["id", "in", productIds]]); 60 | var products = (await query.searchRead()).map((p) => [p.id, p.display_name]); 61 | this.$vm.setProducts(products); 62 | this.onFilterChange(); 63 | } 64 | }, 65 | async _getProductIdsFromContext() { 66 | const context = this.context 67 | if (context.product_id) { 68 | return [context.product_id] 69 | } 70 | else if (context.purchase_order_id) { 71 | return await this._getProductIdsFromPurchaseOrder(context.purchase_order_id) 72 | } 73 | return [] 74 | }, 75 | async _getProductIdsFromPurchaseOrder(order_id) { 76 | const query = new QueryBuilder("purchase.order.line", ["product_id"]); 77 | query.filter([ 78 | ["order_id", "=", order_id], 79 | ["product_id.type", "in", ["consu", "product"]], 80 | ]) 81 | const result = await query.searchRead() 82 | return result.map((p) => p.product_id[0]); 83 | }, 84 | /** 85 | * Handle passing a default product template id through the context. 86 | */ 87 | async setDefaultProductTemplate(){ 88 | if(this.context.product_template_id){ 89 | var query = new QueryBuilder("product.product", ["display_name"]); 90 | query.filter([["product_tmpl_id", "=", this.context.product_template_id]]); 91 | var products = (await query.searchRead()).map((p) => [p.id, p.display_name]); 92 | this.$vm.setProducts(products); 93 | this.onFilterChange(); 94 | } 95 | }, 96 | /** 97 | * Search products by name. 98 | * 99 | * @param {String} query - the expression to search. 100 | * @returns {Array[Object]} - the product records found. 101 | */ 102 | searchProducts(query){ 103 | return this._nameSearchQuery("product.product", query, []); 104 | }, 105 | /** 106 | * Search product categories by name. 107 | * 108 | * @param {String} query - the expression to search. 109 | * @returns {Array[Object]} - the product category records found. 110 | */ 111 | searchProductCategories(query){ 112 | return this._nameSearchQuery("product.category", query, []); 113 | }, 114 | /** 115 | * Search stock locations by name. 116 | * 117 | * @param {String} query - the expression to search. 118 | * @returns {Array[Object]} - the stock location records found. 119 | */ 120 | searchStockLocations(query){ 121 | return this._nameSearchQuery("stock.location", query, [["usage", "=", "internal"]]); 122 | }, 123 | /** 124 | * Search stock suppliers by name. 125 | * 126 | * @param {String} query - the expression to search. 127 | * @returns {Array[Object]} - the res.partner records found. 128 | */ 129 | searchSuppliers(query){ 130 | return this._nameSearchQuery("res.partner", query, [["supplier", "=", true]]); 131 | }, 132 | _nameSearchQuery(model, query, domain, limit){ 133 | return this._rpc({ 134 | model, 135 | method: "name_search", 136 | params: { context: odoo.session_info.user_context }, 137 | kwargs: { 138 | name: query, 139 | args: domain, 140 | limit: limit || 80, 141 | }, 142 | }); 143 | }, 144 | async onFilterChange(products){ 145 | var rows = await this._fetchRowsData(); 146 | rows = rows.map(row => { 147 | return { 148 | key: nextRowKey++, 149 | ...row 150 | } 151 | }); 152 | this.$vm.rows = rows.sort((r1, r2) => r1.label > r2.label); 153 | }, 154 | _fetchRowsData() { 155 | const options = { 156 | products: this.$vm.products.map(r => r.id), 157 | categories: this.$vm.productCategories.map(r => r.id), 158 | locations: this.$vm.locations.map(r => r.id), 159 | suppliers: this.$vm.suppliers.map(r => r.id), 160 | groupBy: this.$vm.rowGroupBy, 161 | } 162 | return this._rpc({ 163 | model: "vue.stock.forecast", 164 | method: "fetch", 165 | params: { context: odoo.session_info.user_context }, 166 | kwargs: { options }, 167 | }) 168 | }, 169 | onCurrentStockClicked(row){ 170 | var domain = [["location_id.usage", "=", "internal"]]; 171 | 172 | if(row.productId){ 173 | domain.push(["product_id", "=", row.productId]); 174 | } 175 | if(row.categoryId) { 176 | domain.push(["product_id.categ_id", "child_of", row.categoryId]); 177 | } 178 | if(row.uomId){ 179 | domain.push(["product_id.uom_id", "=", row.uomId]); 180 | } 181 | 182 | domain = domain.concat(this.getStockQuantLocationDomain()); 183 | 184 | this.do_action({ 185 | res_model: "stock.quant", 186 | name: _t("Current Stocks"), 187 | views: [[false, "list"], [false, "form"]], 188 | type: "ir.actions.act_window", 189 | domain, 190 | }); 191 | }, 192 | onMinMaxClicked(row) { 193 | this.do_action({ 194 | res_model: "stock.warehouse.orderpoint", 195 | name: _t("{}: Min / Max").replace("{}", row.label), 196 | views: [[false, "list"], [false, "form"]], 197 | type: "ir.actions.act_window", 198 | domain: this.getOrderpointDomain(row), 199 | context: this.getOrderpointContext(row), 200 | }); 201 | }, 202 | getOrderpointDomain(row) { 203 | var domain = []; 204 | 205 | if(row.productId){ 206 | domain.push(["product_id", "=", row.productId]); 207 | } 208 | 209 | if(row.categoryId) { 210 | domain.push(["product_id.categ_id", "child_of", row.categoryId]); 211 | } 212 | 213 | if(row.uomId){ 214 | domain.push(["product_id.uom_id", "=", row.uomId]); 215 | } 216 | 217 | return domain 218 | }, 219 | getOrderpointContext(row) { 220 | const context = this.getUserContext() 221 | 222 | if(row.productId){ 223 | context.default_product_id = row.productId 224 | } 225 | 226 | return context 227 | }, 228 | onPurchasedClicked(row) { 229 | this.do_action({ 230 | res_model: "purchase.order.line", 231 | name: _t("{}: Quotation").replace("{}", row.label), 232 | views: [[false, "list"], [false, "form"]], 233 | type: "ir.actions.act_window", 234 | domain: this.getPurchaseOrderLineDomain(row), 235 | context: this.getPurchaseOrderLineContext(row), 236 | }); 237 | }, 238 | getPurchaseOrderLineDomain(row) { 239 | var domain = [ 240 | ["order_id.state", "in", ["draft", "sent", "to approve"]], 241 | ]; 242 | 243 | if(row.productId){ 244 | domain.push(["product_id", "=", row.productId]); 245 | } 246 | 247 | if(row.categoryId) { 248 | domain.push(["product_id.categ_id", "child_of", row.categoryId]); 249 | } 250 | 251 | if(row.uomId){ 252 | domain.push(["product_id.uom_id", "=", row.uomId]); 253 | } 254 | 255 | return domain 256 | }, 257 | getPurchaseOrderLineContext(row) { 258 | const context = this.getUserContext() 259 | 260 | if(row.productId){ 261 | context.default_product_id = row.productId 262 | } 263 | 264 | return context 265 | }, 266 | getStockQuantLocationDomain(){ 267 | var domain = []; 268 | var locationIds = this.$vm.locations.map((l) => l.id); 269 | if(locationIds.length){ 270 | domain.push(["location_id", "child_of", locationIds]); 271 | } 272 | return domain; 273 | }, 274 | onMoveAmountClicked(row, dateFrom, dateTo){ 275 | var dayAfterDateTo = moment(dateTo).add(1, "day").format("YYYY-MM-DD"); 276 | var domain = [ 277 | ["state", "not in", ["done", "cancel"]], 278 | ["date_expected", ">=", this.toUTC(dateFrom)], 279 | ["date_expected", "<", this.toUTC(dayAfterDateTo)], 280 | ]; 281 | 282 | if(row.productId){ 283 | domain.push(["product_id", "=", row.productId]); 284 | } 285 | if(row.categoryId) { 286 | domain.push(["product_id.categ_id", "child_of", row.categoryId]); 287 | } 288 | if(row.uomId){ 289 | domain.push(["product_id.uom_id", "=", row.uomId]); 290 | } 291 | 292 | domain.concat(this.getStockMoveLocationDomain()); 293 | 294 | var actionName = ( 295 | _t("Stock Moves ({date_from} to {date_to})") 296 | .replace("{date_from}", dateFrom) 297 | .replace("{date_to}", dateTo) 298 | ); 299 | 300 | this.do_action({ 301 | res_model: "stock.move", 302 | name: actionName, 303 | views: [[false, "list"], [false, "form"]], 304 | type: "ir.actions.act_window", 305 | domain, 306 | }); 307 | }, 308 | toUTC(date) { 309 | return moment(date).utc().format("YYYY-MM-DD HH:mm:ss") 310 | }, 311 | /** 312 | * Get the domain related to locations used for filtering stock moves. 313 | * 314 | * @returns {Array} the domain filter. 315 | */ 316 | getStockMoveLocationDomain(){ 317 | var domain = []; 318 | var locationIds = this.$vm.locations.map((l) => l.id); 319 | if(locationIds.length){ 320 | domain.push("|"); 321 | domain.push("&"); 322 | domain.push(["location_id", "child_of", locationIds]); 323 | domain.push(["location_id.usage", "=", "internal"]); 324 | domain.push("&"); 325 | domain.push(["location_dest_id", "child_of", locationIds]); 326 | domain.push(["location_dest_id.usage", "=", "internal"]); 327 | } 328 | else{ 329 | domain.push("|"); 330 | domain.push("&"); 331 | domain.push(["location_id.usage", "!=", "internal"]); 332 | domain.push(["location_dest_id.usage", "=", "internal"]); 333 | domain.push("&"); 334 | domain.push(["location_id.usage", "=", "internal"]); 335 | domain.push(["location_dest_id.usage", "!=", "internal"]); 336 | } 337 | return domain; 338 | }, 339 | getUserContext() { 340 | return {...odoo.session_info.user_context} 341 | }, 342 | do_action() { 343 | const res = this._super.apply(this, arguments); 344 | res.then(() => this.do_hide()) 345 | return res 346 | }, 347 | do_show(){ 348 | this.$el.removeClass("o_hidden"); 349 | this.$vm.visible = true 350 | }, 351 | do_hide(){ 352 | this.$el.addClass("o_hidden"); 353 | this.$vm.visible = false 354 | }, 355 | destroy(){ 356 | var parentNode = this.$vm.$el.parentNode; 357 | if(parentNode){ 358 | parentNode.removeChild(this.$vm.$el); 359 | } 360 | this.$vm.$destroy(); 361 | this._super.apply(this, arguments); 362 | }, 363 | on_attach_callback(){ 364 | this.updateBreadrumb(); 365 | }, 366 | updateBreadrumb(){ 367 | var parent = this.getParent(); 368 | var parentIsAction = Boolean(parent.get_breadcrumbs); 369 | if(parentIsAction){ 370 | var controlPanelStatus = {breadcrumbs: parent.get_breadcrumbs()}; 371 | this.update_control_panel(controlPanelStatus); 372 | } 373 | }, 374 | }); 375 | 376 | core.action_registry.add("stock_forecast_report", StockForecastReport); 377 | 378 | /** 379 | / Force add terms to the generated .pot file 380 | / 381 | / Note that terms end with a trailing space 382 | / This ensures that the terms don't collide with existing non-javascript (python) transactions. 383 | / Odoo skips new javascript translations if the same python translation exists. 384 | */ 385 | _t("Available "); 386 | _t("Columns "); 387 | _t("Day "); 388 | _t("End Date "); 389 | _t("Location "); 390 | _t("Min / Max "); 391 | _t("Month "); 392 | _t("Product "); 393 | _t("Product Categories "); 394 | _t("Product Category "); 395 | _t("Products "); 396 | _t("Quotation "); 397 | _t("Reserved "); 398 | _t("Rows "); 399 | _t("Search "); 400 | _t("Start Date "); 401 | _t("Stock "); 402 | _t("Supplier "); 403 | _t("Week "); 404 | 405 | return StockForecastReport; 406 | 407 | }); 408 | -------------------------------------------------------------------------------- /vue_stock_forecast/static/src/js/StockForecastReport.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 182 | -------------------------------------------------------------------------------- /vue_stock_forecast/static/src/js/StockForecastTable.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 235 | -------------------------------------------------------------------------------- /vue_stock_forecast/static/src/js/main.js: -------------------------------------------------------------------------------- 1 | 2 | import StockForecastReport from "./StockForecastReport.vue"; 3 | import StockForecastTable from "./StockForecastTable.vue"; 4 | 5 | Vue.component("stock-forecast-report", StockForecastReport); 6 | Vue.component("stock-forecast-table", StockForecastTable); 7 | 8 | window.vueStockForecast = {StockForecastReport, StockForecastTable}; 9 | -------------------------------------------------------------------------------- /vue_stock_forecast/static/src/js/tests/snapshots/testStockForecastTable.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `vue_stock_forecast/static/src/js/tests/testStockForecastTable.js` 2 | 3 | The actual snapshot is saved in `testStockForecastTable.js.snap`. 4 | 5 | Generated by [AVA](https://ava.li). 6 | 7 | ## Render Forecast Table 8 | 9 | > Snapshot 1 10 | 11 | `
␊ 12 |
␊ 13 |
␊ 14 |
␊ 15 |
␊ 16 |
␊ 17 |
␊ 18 |
␊ 19 |
␊ 20 |
␊ 21 |
␊ 22 |
␊ 23 | ␊ 24 | ␊ 25 | ␊ 26 | ␊ 27 | ␊ 28 | ␊ 29 | ␊ 30 | ␊ 31 | ␊ 32 | ␊ 33 | ␊ 34 | ␊ 35 | ␊ 38 | ␊ 41 | ␊ 44 | ␊ 47 | ␊ 50 | ␊ 53 | ␊ 56 | ␊ 57 | ␊ 58 |
␊ 36 |
Product
␊ 37 |
␊ 39 |
Stock
␊ 40 |
␊ 42 |
Reserved
␊ 43 |
␊ 45 |
Available
␊ 46 |
␊ 48 |
2018-01-31
␊ 49 |
␊ 51 |
2018-02-28
␊ 52 |
␊ 54 |
2018-03-31
␊ 55 |
␊ 59 |
␊ 60 |
␊ 61 | ␊ 62 | ␊ 63 | ␊ 64 | ␊ 65 | ␊ 66 | ␊ 67 | ␊ 68 | ␊ 69 | ␊ 70 | ␊ 71 | ␊ 72 | ␊ 73 | ␊ 80 | ␊ 87 | ␊ 94 | ␊ 101 | ␊ 108 | ␊ 115 | ␊ 122 | ␊ 123 | ␊ 124 | ␊ 131 | ␊ 138 | ␊ 145 | ␊ 152 | ␊ 159 | ␊ 164 | ␊ 171 | ␊ 172 | ␊ 173 | ␊ 174 |
␊ 74 |
␊ 75 |
␊ 76 | Product A␊ 77 |
␊ 78 |
␊ 79 |
␊ 81 |
␊ 82 | ␊ 85 |
␊ 86 |
␊ 88 |
␊ 89 |
␊ 90 | 3␊ 91 |
␊ 92 |
␊ 93 |
␊ 95 |
␊ 96 |
␊ 97 | 1␊ 98 |
␊ 99 |
␊ 100 |
␊ 102 |
␊ 103 | ␊ 106 |
␊ 107 |
␊ 109 |
␊ 110 | ␊ 113 |
␊ 114 |
␊ 116 |
␊ 117 | ␊ 120 |
␊ 121 |
␊ 125 |
␊ 126 |
␊ 127 | Product B␊ 128 |
␊ 129 |
␊ 130 |
␊ 132 |
␊ 133 | ␊ 136 |
␊ 137 |
␊ 139 |
␊ 140 |
␊ 141 | 1␊ 142 |
␊ 143 |
␊ 144 |
␊ 146 |
␊ 147 |
␊ 148 | 1␊ 149 |
␊ 150 |
␊ 151 |
␊ 153 |
␊ 154 | ␊ 157 |
␊ 158 |
␊ 160 |
␊ 161 |
12
␊ 162 |
␊ 163 |
␊ 165 |
␊ 166 | ␊ 169 |
␊ 170 |
␊ 175 | ␊ 176 | ␊ 177 |
␊ 178 | ␊ 179 | ␊ 180 | ␊ 181 | ␊ 182 | ␊ 183 |
␊ 184 |
` 185 | -------------------------------------------------------------------------------- /vue_stock_forecast/static/src/js/tests/snapshots/testStockForecastTable.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/static/src/js/tests/snapshots/testStockForecastTable.js.snap -------------------------------------------------------------------------------- /vue_stock_forecast/static/src/js/tests/testStockForecastTable.js: -------------------------------------------------------------------------------- 1 | 2 | import test from "ava"; 3 | import ElementUI from "element-ui"; 4 | import Vue from "vue"; 5 | 6 | import StockForecastTable from "../StockForecastTable.vue"; 7 | import pretty from "pretty"; 8 | 9 | Vue.use(ElementUI); 10 | 11 | var propsData = { 12 | dateFrom: moment("2018-01-01").toDate(), 13 | dateTo: moment("2018-03-31").toDate(), 14 | dateGroupBy: "month", 15 | rowGroupBy: "product", 16 | rows: [ 17 | { 18 | label: "Product A", 19 | currentStock: 4, 20 | reserved: 3, 21 | incoming: [{date: "2018-01-01", qty: 2}, {date: "2018-02-01", qty: 3}, {date: "2018-03-01", qty: 4}], 22 | outgoing: [{date: "2018-01-31", qty: 5}, {date: "2018-02-28", qty: 6}, {date: "2018-03-31", qty: 7}], 23 | }, 24 | { 25 | label: "Product B", 26 | currentStock: 2, 27 | reserved: 1, 28 | incoming: [{date: "2018-01-10", qty: 10}], 29 | outgoing: [{date: "2018-03-10", qty: 20}], 30 | }, 31 | ], 32 | translate: (term) => term, 33 | firstColumnFixed: false, 34 | }; 35 | 36 | test("Render Forecast Table", async (t) => { 37 | var Constructor = Vue.extend(StockForecastTable); 38 | var vm = new Constructor({propsData}).$mount(); 39 | await vm.$nextTick(); 40 | t.snapshot(pretty(vm.$el.outerHTML)); 41 | }); 42 | -------------------------------------------------------------------------------- /vue_stock_forecast/static/webpack.config.js: -------------------------------------------------------------------------------- 1 | const VueLoaderPlugin = require("vue-loader/lib/plugin"); 2 | 3 | module.exports = { 4 | entry: ["./src/js/main.js"], 5 | mode: "development", 6 | devtool: false, 7 | output: {filename: "vueStockForecast.js"}, 8 | module: { 9 | rules: [ 10 | {test: /\.js$/, loader: "babel-loader", query: {presets: ["env"]}}, 11 | {test: /\.vue$/, loader: "vue-loader"}, 12 | ], 13 | }, 14 | plugins: [ 15 | new VueLoaderPlugin(), 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /vue_stock_forecast/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast/tests/__init__.py -------------------------------------------------------------------------------- /vue_stock_forecast/tests/test_stock_forecast_report.py: -------------------------------------------------------------------------------- 1 | # © 2020 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 3 | 4 | from ddt import ddt, data 5 | from datetime import datetime 6 | from odoo.tests.common import SavepointCase 7 | 8 | 9 | class ForecastReportCase(SavepointCase): 10 | @classmethod 11 | def setUpClass(cls): 12 | super().setUpClass() 13 | cls.category = cls.env["product.category"].create( 14 | { 15 | "name": "All Products", 16 | } 17 | ) 18 | 19 | cls.product_a = cls.env["product.product"].create( 20 | { 21 | "name": "Product A", 22 | "type": "product", 23 | "categ_id": cls.category.id, 24 | } 25 | ) 26 | cls.product_b = cls.product_a.copy( 27 | {"name": "Product B", "type": "product", "categ_id": cls.category.id} 28 | ) 29 | 30 | cls.supplier = cls.env["res.partner"].create( 31 | { 32 | "name": "My Vendor", 33 | "supplier": True, 34 | "is_company": True, 35 | } 36 | ) 37 | 38 | cls.warehouse = cls.env.ref("stock.warehouse0") 39 | 40 | cls.parent_location = cls.warehouse.lot_stock_id 41 | 42 | cls.supplier_location = cls.env.ref("stock.stock_location_suppliers") 43 | cls.customer_location = cls.env.ref("stock.stock_location_customers") 44 | 45 | cls.location_1 = cls.env["stock.location"].create( 46 | { 47 | "name": "Location 1", 48 | "usage": "internal", 49 | "location_id": cls.parent_location.id, 50 | } 51 | ) 52 | cls.location_2 = cls.env["stock.location"].create( 53 | { 54 | "name": "Location 2", 55 | "usage": "internal", 56 | "location_id": cls.parent_location.id, 57 | } 58 | ) 59 | 60 | cls.report = cls.env["vue.stock.forecast"] 61 | 62 | 63 | class TestStockForecastReport(ForecastReportCase): 64 | def test_search_by_product_ids(self): 65 | result = self.report.fetch({"products": [self.product_a.id]}) 66 | assert len(result) == 1 67 | assert result[0]["productId"] == self.product_a.id 68 | 69 | def test_filter_by_location_id(self): 70 | location = self.env["stock.location"].search([], limit=1) 71 | self.report.fetch({"locations": [location.id]}) 72 | 73 | def test_search_by_category_ids(self): 74 | category = self.env["product.category"].create({"name": "My Category"}) 75 | self.product_b.categ_id = category.id 76 | result = self.report.fetch({"categories": [category.id]}) 77 | assert len(result) == 1 78 | assert result[0]["productId"] == self.product_b.id 79 | 80 | def test_group_by_category_ids(self): 81 | result = self.report.fetch( 82 | {"categories": [self.category.id], "groupBy": "category"} 83 | ) 84 | result = sorted(result, key=lambda r: r["categoryId"]) 85 | assert result[0]["categoryId"] == self.category.id 86 | 87 | def test_supplier_with_no_product(self): 88 | result = self.report.fetch({"suppliers": [self.supplier.id]}) 89 | assert len(result) == 0 90 | 91 | def test_supplier_with_one_product(self): 92 | self._add_supplier_to_product_template(self.supplier, self.product_b) 93 | result = self.report.fetch({"suppliers": [self.supplier.id]}) 94 | assert len(result) == 1 95 | assert result[0]["productId"] == self.product_b.id 96 | 97 | def test_supplier_with_one_product_variant(self): 98 | product_c = self.product_b.copy( 99 | {"product_tmpl_id": self.product_b.product_tmpl_id.id} 100 | ) 101 | self._add_supplier_to_product_variant(self.supplier, product_c) 102 | result = self.report.fetch({"suppliers": [self.supplier.id]}) 103 | assert len(result) == 1 104 | assert result[0]["productId"] == product_c.id 105 | 106 | def test_child_supplier_with_one_product(self): 107 | child = self.env["res.partner"].create( 108 | { 109 | "name": "Child A", 110 | "parent_id": self.supplier.id, 111 | } 112 | ) 113 | self._add_supplier_to_product_template(child, self.product_b) 114 | result = self.report.fetch({"suppliers": [self.supplier.id]}) 115 | assert len(result) == 1 116 | assert result[0]["productId"] == self.product_b.id 117 | 118 | def _add_supplier_to_product_template(self, supplier, product): 119 | self.env["product.supplierinfo"].create( 120 | { 121 | "name": supplier.id, 122 | "product_tmpl_id": product.product_tmpl_id.id, 123 | } 124 | ) 125 | 126 | def _add_supplier_to_product_variant(self, supplier, product): 127 | self.env["product.supplierinfo"].create( 128 | { 129 | "name": supplier.id, 130 | "product_tmpl_id": product.product_tmpl_id.id, 131 | "product_id": product.id, 132 | } 133 | ) 134 | 135 | def test_min_max(self): 136 | min_ = 10 137 | max_ = 15 138 | self._setup_min_max(self.product_a, min_, max_) 139 | result = self.report.fetch({"products": [self.product_a.id]}) 140 | assert result[0]["min"] == min_ 141 | assert result[0]["max"] == max_ 142 | 143 | def test_min_max__with_category(self): 144 | min_ = 10 145 | max_ = 15 146 | self._setup_min_max(self.product_a, min_, max_) 147 | self._setup_min_max(self.product_b, min_, max_) 148 | result = self.report.fetch( 149 | {"categories": [self.category.id], "groupBy": "category"} 150 | ) 151 | assert result[0]["min"] == min_ * 2 152 | assert result[0]["max"] == max_ * 2 153 | 154 | def test_min_max__with_category__in_multiple_uom(self): 155 | self.product_b.uom_id = self.env.ref("uom.product_uom_dozen") 156 | min_1 = 11 157 | max_1 = 12 158 | min_2 = 21 159 | max_2 = 22 160 | self._setup_min_max(self.product_a, min_1, max_1) 161 | self._setup_min_max(self.product_b, min_2, max_2) 162 | result = self.report.fetch( 163 | {"categories": [self.category.id], "groupBy": "category"} 164 | ) 165 | assert result[0]["min"] == min_1 166 | assert result[0]["max"] == max_1 167 | assert result[1]["min"] == min_2 168 | assert result[1]["max"] == max_2 169 | 170 | def test_min_max__location_filter(self): 171 | min_ = 10 172 | max_ = 15 173 | self._setup_min_max(self.product_a, min_, max_, self.location_1) 174 | self._setup_min_max(self.product_a, 999, 999, self.location_2) 175 | result = self.report.fetch( 176 | {"products": [self.product_a.id], "locations": [self.location_1.id]} 177 | ) 178 | assert result[0]["min"] == min_ 179 | assert result[0]["max"] == max_ 180 | 181 | def _setup_min_max(self, product, min_, max_, location=None): 182 | product.write( 183 | { 184 | "orderpoint_ids": [ 185 | ( 186 | 0, 187 | 0, 188 | { 189 | "name": "/", 190 | "warehouse_id": self.warehouse.id, 191 | "location_id": (location or self.parent_location).id, 192 | "product_min_qty": min_, 193 | "product_max_qty": max_, 194 | "qty_multiple": 1, 195 | "company_id": self.warehouse.company_id.id, 196 | }, 197 | ) 198 | ] 199 | } 200 | ) 201 | 202 | 203 | @ddt 204 | class TestStockMove(ForecastReportCase): 205 | @classmethod 206 | def setUpClass(cls): 207 | super().setUpClass() 208 | cls.date_expected = datetime(2020, 1, 15) 209 | cls.move = cls.env["stock.move"].create( 210 | { 211 | "location_id": cls.supplier_location.id, 212 | "location_dest_id": cls.location_1.id, 213 | "name": cls.product_a.display_name, 214 | "product_id": cls.product_a.id, 215 | "product_uom": cls.product_a.uom_id.id, 216 | "product_uom_qty": 1, 217 | "date_expected": cls.date_expected, 218 | } 219 | ) 220 | cls.move._action_confirm() 221 | 222 | def test_incoming(self): 223 | result = self.report.fetch({"products": [self.product_a.id]}) 224 | incoming = result[0]["incoming"] 225 | assert len(incoming) == 1 226 | assert incoming[0]["qty"] == 1 227 | assert incoming[0]["date"] == "2020-01-15" 228 | assert not result[0]["outgoing"] 229 | 230 | def test_incoming__location_not_matching(self): 231 | result = self.report.fetch({"products": [self.product_a.id], "locations": [self.location_2.id]}) 232 | assert not result[0]["incoming"] 233 | 234 | def test_incoming__location_matching(self): 235 | result = self.report.fetch({"products": [self.product_a.id], "locations": [self.location_1.id]}) 236 | incoming = result[0]["incoming"] 237 | assert len(incoming) == 1 238 | 239 | def test_incoming__with_category(self): 240 | result = self.report.fetch({"categories": [self.category.id], "groupBy": "category"}) 241 | incoming = result[0]["incoming"] 242 | assert len(incoming) == 1 243 | 244 | @data("done", "cancel") 245 | def test_incoming__state_excluded(self, state): 246 | self.move.state = state 247 | result = self.report.fetch({"products": [self.product_a.id], "locations": [self.location_1.id]}) 248 | assert not result[0]["incoming"] 249 | 250 | def test_outgoing(self): 251 | self.move.location_id = self.location_1 252 | self.move.location_dest_id = self.customer_location 253 | result = self.report.fetch({"products": [self.product_a.id]}) 254 | outgoing = result[0]["outgoing"] 255 | assert len(outgoing) == 1 256 | assert outgoing[0]["qty"] == 1 257 | assert not result[0]["incoming"] 258 | 259 | def test_outgoing__location_not_matching(self): 260 | self.move.location_id = self.location_1 261 | self.move.location_dest_id = self.customer_location 262 | result = self.report.fetch({"products": [self.product_a.id], "locations": [self.location_2.id]}) 263 | assert not result[0]["outgoing"] 264 | 265 | def test_outgoing__location_matching(self): 266 | self.move.location_id = self.location_1 267 | self.move.location_dest_id = self.customer_location 268 | result = self.report.fetch({"products": [self.product_a.id], "locations": [self.location_1.id]}) 269 | outgoing = result[0]["outgoing"] 270 | assert len(outgoing) == 1 271 | 272 | def test_outgoing__with_category(self): 273 | self.move.location_id = self.location_1 274 | self.move.location_dest_id = self.customer_location 275 | result = self.report.fetch({"categories": [self.category.id], "groupBy": "category"}) 276 | outgoing = result[0]["outgoing"] 277 | assert len(outgoing) == 1 278 | 279 | @data("done", "cancel") 280 | def test_outgoing__state_excluded(self, state): 281 | self.move.location_id = self.location_1 282 | self.move.location_dest_id = self.customer_location 283 | self.move.state = state 284 | result = self.report.fetch({"products": [self.product_a.id], "locations": [self.location_1.id]}) 285 | assert not result[0]["outgoing"] 286 | 287 | def test_date_in_specific_timezone(self): 288 | result = self.report.with_context(tz="EST").fetch({"products": [self.product_a.id]}) 289 | incoming = result[0]["incoming"] 290 | assert incoming[0]["date"] == "2020-01-14" 291 | 292 | 293 | class TestStockQuant(ForecastReportCase): 294 | @classmethod 295 | def setUpClass(cls): 296 | super().setUpClass() 297 | cls.quant = cls.env["stock.quant"].create( 298 | { 299 | "location_id": cls.location_1.id, 300 | "product_id": cls.product_a.id, 301 | "quantity": 1, 302 | } 303 | ) 304 | 305 | def test_basic_case(self): 306 | result = self.report.fetch({"products": [self.product_a.id]}) 307 | assert result[0]["currentStock"] == 1 308 | 309 | def test_location_not_matching(self): 310 | result = self.report.fetch({"products": [self.product_a.id], "locations": [self.location_2.id]}) 311 | assert result[0]["currentStock"] == 0 312 | 313 | def test_location_matching(self): 314 | result = self.report.fetch({"products": [self.product_a.id], "locations": [self.location_1.id]}) 315 | assert result[0]["currentStock"] == 1 316 | 317 | def test_group_by_category(self): 318 | result = self.report.fetch({"categories": [self.category.id], "groupBy": "category"}) 319 | assert result[0]["currentStock"] == 1 320 | 321 | def test_no_reserved_quantity(self): 322 | result = self.report.fetch({"products": [self.product_a.id]}) 323 | assert result[0]["reserved"] == 0 324 | 325 | def test_reserved_quantity(self): 326 | self.quant.reserved_quantity = 1 327 | result = self.report.fetch({"products": [self.product_a.id]}) 328 | assert result[0]["reserved"] == 1 329 | 330 | 331 | class TestPurchasedQuantity(ForecastReportCase): 332 | @classmethod 333 | def setUpClass(cls): 334 | super().setUpClass() 335 | cls.purchased_qty_a = 12 336 | cls.purchased_qty_b = 24 337 | cls.order = cls.env["purchase.order"].create( 338 | { 339 | "partner_id": cls.supplier.id, 340 | "order_line": [ 341 | ( 342 | 0, 343 | 0, 344 | { 345 | "product_id": cls.product_a.id, 346 | "product_uom": cls.product_a.uom_po_id.id, 347 | "name": cls.product_a.name, 348 | "product_qty": cls.purchased_qty_a, 349 | "price_unit": 100, 350 | "date_planned": datetime.now(), 351 | }, 352 | ), 353 | ( 354 | 0, 355 | 0, 356 | { 357 | "product_id": cls.product_b.id, 358 | "product_uom": cls.product_b.uom_po_id.id, 359 | "name": cls.product_b.name, 360 | "product_qty": cls.purchased_qty_b, 361 | "price_unit": 100, 362 | "date_planned": datetime.now(), 363 | }, 364 | ), 365 | ], 366 | } 367 | ) 368 | 369 | def test_purchased_quantity(self): 370 | result = self.report.fetch({"products": [self.product_a.id]}) 371 | assert result[0]["purchased"] == self.purchased_qty_a 372 | 373 | def test_purchase_in_different_uom(self): 374 | self.product_a.uom_id = self.env.ref("uom.product_uom_dozen") 375 | result = self.report.fetch({"products": [self.product_a.id]}) 376 | assert result[0]["purchased"] == self.purchased_qty_a / 12 377 | 378 | def test_purchased_quantity__with_categories(self): 379 | result = self.report.fetch({"categories": [self.category.id], "groupBy": "category"}) 380 | assert ( 381 | result[0]["purchased"] 382 | == self.purchased_qty_a + self.purchased_qty_b 383 | ) 384 | 385 | def test_confirmed_order_excluded(self): 386 | self.order.button_confirm() 387 | result = self.report.fetch({"products": [self.product_a.id]}) 388 | assert result[0]["purchased"] == 0 389 | 390 | def test_confirmed_order_sent(self): 391 | self.order.state = "sent" 392 | result = self.report.fetch({"products": [self.product_a.id]}) 393 | assert result[0]["purchased"] == self.purchased_qty_a 394 | 395 | def test_confirmed_order_to_approve(self): 396 | self.order.state = "to approve" 397 | result = self.report.fetch({"products": [self.product_a.id]}) 398 | assert result[0]["purchased"] == self.purchased_qty_a 399 | -------------------------------------------------------------------------------- /vue_stock_forecast/views/assets.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /vue_stock_forecast/views/menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Stock Forecast 6 | stock_forecast_report 7 | current 8 | 9 | 10 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /vue_stock_forecast/views/product.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Product Form With Stock Forecast Smart Button 6 | product.product 7 | 8 | 9 | 19 | 20 | 21 | 22 | 23 | Product Template Form With Stock Forecast Smart Button 24 | product.template 25 | 26 | 27 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /vue_stock_forecast/views/purchase_order.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Purchase Order Form: add Stock Forecast Smart Button 6 | purchase.order 7 | 8 | 9 |
10 | 11 |
20 |
21 |
22 | 23 |
24 | -------------------------------------------------------------------------------- /vue_stock_forecast_preferred_supplier/README.rst: -------------------------------------------------------------------------------- 1 | Stock Forecast - Preferred Supplier 2 | =================================== 3 | 4 | This module extends ``vue_stock_forecast``. 5 | 6 | Context 7 | ------- 8 | A preferred supplier is the first supplier in the list of prices of a product. 9 | 10 | .. image:: static/description/product_form.png 11 | 12 | Overview 13 | -------- 14 | When filtering the report by supplier, it only shows products for which 15 | the supplier is the preferred supplier. 16 | 17 | .. image:: static/description/stock_forecast_report.png 18 | 19 | Without this module, all products containing at least one price with the selected 20 | supplier are shown. 21 | 22 | Contributors 23 | ------------ 24 | 25 | * Numigi (tm) and all its contributors (https://bit.ly/numigiens) 26 | 27 | More information 28 | ---------------- 29 | 30 | * Meet us at https://bit.ly/numigi-com 31 | -------------------------------------------------------------------------------- /vue_stock_forecast_preferred_supplier/__init__.py: -------------------------------------------------------------------------------- 1 | # © 2020 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 3 | 4 | from . import report, models 5 | -------------------------------------------------------------------------------- /vue_stock_forecast_preferred_supplier/__manifest__.py: -------------------------------------------------------------------------------- 1 | # © 2020 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 3 | 4 | { 5 | "name": "Vue Stock Forecast - Preferred Supplier", 6 | "version": "1.0.0", 7 | "author": "Numigi", 8 | "maintainer": "Numigi", 9 | "website": "https://bit.ly/numigi-com", 10 | "license": "LGPL-3", 11 | "category": "Stock", 12 | "summary": "Filter the stock forecast report by preferred supplier", 13 | "depends": ["vue_stock_forecast"], 14 | "installable": True, 15 | } 16 | -------------------------------------------------------------------------------- /vue_stock_forecast_preferred_supplier/models/__init__.py: -------------------------------------------------------------------------------- 1 | # © 2020 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 3 | 4 | from . import product_product, product_template 5 | -------------------------------------------------------------------------------- /vue_stock_forecast_preferred_supplier/models/common.py: -------------------------------------------------------------------------------- 1 | # © 2020 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 3 | 4 | 5 | def matches_supplier(supplier_info, supplier): 6 | return ( 7 | supplier_info.name == supplier or 8 | supplier_info.name.commercial_partner_id == supplier 9 | ) 10 | -------------------------------------------------------------------------------- /vue_stock_forecast_preferred_supplier/models/product_product.py: -------------------------------------------------------------------------------- 1 | # © 2020 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 3 | 4 | from odoo import models 5 | from .common import matches_supplier 6 | 7 | 8 | class Product(models.Model): 9 | 10 | _inherit = "product.product" 11 | 12 | def has_preferred_supplier(self, supplier): 13 | if self.product_tmpl_id.has_preferred_supplier(supplier): 14 | return True 15 | 16 | sellers = self.seller_ids.filtered( 17 | lambda s: s.product_id == self 18 | ) 19 | if not sellers: 20 | return False 21 | 22 | return matches_supplier(sellers[0], supplier) 23 | -------------------------------------------------------------------------------- /vue_stock_forecast_preferred_supplier/models/product_template.py: -------------------------------------------------------------------------------- 1 | # © 2020 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 3 | 4 | from odoo import models 5 | from .common import matches_supplier 6 | 7 | 8 | class ProductTemplate(models.Model): 9 | 10 | _inherit = "product.template" 11 | 12 | def has_preferred_supplier(self, supplier): 13 | sellers = self.seller_ids.filtered( 14 | lambda s: not s.product_id 15 | ) 16 | if not sellers: 17 | return False 18 | 19 | return matches_supplier(sellers[0], supplier) 20 | -------------------------------------------------------------------------------- /vue_stock_forecast_preferred_supplier/report/__init__.py: -------------------------------------------------------------------------------- 1 | # © 2020 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 3 | 4 | from . import vue_stock_forecast 5 | -------------------------------------------------------------------------------- /vue_stock_forecast_preferred_supplier/report/vue_stock_forecast.py: -------------------------------------------------------------------------------- 1 | # © 2020 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 3 | 4 | from odoo import models 5 | 6 | 7 | class VueStockForecast(models.AbstractModel): 8 | 9 | _inherit = "vue.stock.forecast" 10 | 11 | def _get_supplier_products(self, supplier_ids): 12 | products = super()._get_supplier_products(supplier_ids) 13 | suppliers = self.env["res.partner"].browse(supplier_ids) 14 | return products.filtered( 15 | lambda p: any(p.has_preferred_supplier(s) for s in suppliers) 16 | ) 17 | -------------------------------------------------------------------------------- /vue_stock_forecast_preferred_supplier/static/description/product_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast_preferred_supplier/static/description/product_form.png -------------------------------------------------------------------------------- /vue_stock_forecast_preferred_supplier/static/description/stock_forecast_report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast_preferred_supplier/static/description/stock_forecast_report.png -------------------------------------------------------------------------------- /vue_stock_forecast_preferred_supplier/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vue_stock_forecast_preferred_supplier/tests/__init__.py -------------------------------------------------------------------------------- /vue_stock_forecast_preferred_supplier/tests/test_stock_forecast_report.py: -------------------------------------------------------------------------------- 1 | # © 2020 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 3 | 4 | from odoo.tests.common import SavepointCase 5 | 6 | 7 | class TestProductProduct(SavepointCase): 8 | @classmethod 9 | def setUpClass(cls): 10 | super().setUpClass() 11 | cls.product_a1 = cls.env["product.product"].create( 12 | {"name": "Product A1", "type": "product",} 13 | ) 14 | cls.product_a2 = cls.env["product.product"].create( 15 | { 16 | "name": "Product A2", 17 | "type": "product", 18 | "product_tmpl_id": cls.product_a1.product_tmpl_id.id, 19 | } 20 | ) 21 | cls.product_b = cls.env["product.product"].create( 22 | {"name": "Product A", "type": "product",} 23 | ) 24 | 25 | cls.supplier_a = cls.env["res.partner"].create( 26 | {"name": "My Vendor A", "supplier": True, "is_company": True,} 27 | ) 28 | 29 | cls.supplier_b = cls.env["res.partner"].create( 30 | {"name": "My Vendor B", "supplier": True, "is_company": True,} 31 | ) 32 | 33 | cls.supplier_a_contact = cls.env["res.partner"].create( 34 | { 35 | "name": "My Vendor A - Contact", 36 | "supplier": True, 37 | "is_company": False, 38 | "parent_id": cls.supplier_a.id, 39 | } 40 | ) 41 | 42 | cls.report = cls.env["vue.stock.forecast"] 43 | 44 | def test_no_supplier_price(self): 45 | assert not self.product_a1.has_preferred_supplier(self.supplier_a) 46 | 47 | def test_first_supplier_price(self): 48 | self._add_supplier(self.product_a1, self.supplier_a, 1) 49 | assert self.product_a1.has_preferred_supplier(self.supplier_a) 50 | assert not self.product_a2.has_preferred_supplier(self.supplier_a) 51 | 52 | def test_second_supplier_price(self): 53 | self._add_supplier(self.product_a1, self.supplier_a, 1) 54 | self._add_supplier(self.product_a1, self.supplier_b, 2) 55 | assert not self.product_a1.has_preferred_supplier(self.supplier_b) 56 | 57 | def test_child_supplier_as_preffered_supplier(self): 58 | self._add_supplier(self.product_a1, self.supplier_a_contact, 1) 59 | assert self.product_a1.has_preferred_supplier(self.supplier_a) 60 | 61 | def test_first_supplier_price_of_product_template(self): 62 | self._add_supplier_to_template(self.product_a1, self.supplier_a, 1) 63 | assert self.product_a1.has_preferred_supplier(self.supplier_a) 64 | assert self.product_a2.has_preferred_supplier(self.supplier_a) 65 | 66 | def test_report(self): 67 | self._add_supplier(self.product_a1, self.supplier_a, 1) 68 | self._add_supplier(self.product_b, self.supplier_b, 1) 69 | self._add_supplier(self.product_b, self.supplier_a, 2) 70 | products = self.env["vue.stock.forecast"]._get_supplier_products( 71 | [self.supplier_a.id], 72 | ) 73 | assert products == self.product_a1 74 | 75 | def _add_supplier(self, product, partner, sequence): 76 | product.write( 77 | { 78 | "seller_ids": [ 79 | ( 80 | 0, 81 | 0, 82 | { 83 | "name": partner.id, 84 | "sequence": sequence, 85 | "product_id": product.id, 86 | "product_tmpl_id": product.product_tmpl_id.id, 87 | }, 88 | ) 89 | ] 90 | } 91 | ) 92 | 93 | def _add_supplier_to_template(self, product, partner, sequence): 94 | product.product_tmpl_id.write( 95 | { 96 | "seller_ids": [ 97 | ( 98 | 0, 99 | 0, 100 | { 101 | "name": partner.id, 102 | "sequence": sequence, 103 | }, 104 | ) 105 | ] 106 | } 107 | ) 108 | -------------------------------------------------------------------------------- /vuex/README.rst: -------------------------------------------------------------------------------- 1 | Vuex 2 | ==== 3 | 4 | This module integrates `Vuex `_ with the frontend assets of Odoo. 5 | 6 | Usage Example 7 | ------------- 8 | This example requires that you master of the following topics: 9 | 10 | * Creating a Vuejs application with Vuex 11 | * Creating a Odoo javascript extension 12 | 13 | Creating a Store 14 | ~~~~~~~~~~~~~~~~ 15 | Add a file app.js which defines your Vue application. 16 | 17 | .. code-block:: javascript 18 | 19 | require("web.dom_ready"); 20 | 21 | if (!$("#my_app_node").length) { 22 | return $.Deferred().reject("DOM doesn't contain '#my_app_node'"); 23 | } 24 | 25 | const store = require("vuex.store"); 26 | const app = new Vue({store}).$mount('#app'); 27 | 28 | Inside other javascript files, you may register your vuex modules: 29 | 30 | .. code-block:: javascript 31 | 32 | const store = require("vuex.store"); 33 | store.registerModule( 34 | "myModule", 35 | { 36 | namespaced: true, 37 | state: { 38 | ... 39 | }, 40 | actions: { 41 | ... 42 | }, 43 | mutations: { 44 | ... 45 | }, 46 | } 47 | ) 48 | 49 | Contributors 50 | ------------ 51 | * Numigi (tm) and all its contributors (https://bit.ly/numigiens) 52 | 53 | More information 54 | ---------------- 55 | * Meet us at https://bit.ly/numigi-com 56 | -------------------------------------------------------------------------------- /vuex/__init__.py: -------------------------------------------------------------------------------- 1 | # © 2020 Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). 3 | -------------------------------------------------------------------------------- /vuex/__manifest__.py: -------------------------------------------------------------------------------- 1 | # © 2020 Numigi (tm) and all its contributors (https://bit.ly/numigiens) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). 3 | 4 | { 5 | 'name': 'Vuex', 6 | 'version': '1.0.0', 7 | 'author': 'Numigi', 8 | 'maintainer': 'Numigi', 9 | 'website': 'https://bit.ly/numigi-com', 10 | 'license': 'LGPL-3', 11 | 'category': 'Web', 12 | 'summary': 'Integrate vuex with Odoo frontend.', 13 | 'depends': ['vue_frontend'], 14 | 'data': ['views/assets.xml'], 15 | 'installable': True, 16 | } 17 | -------------------------------------------------------------------------------- /vuex/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Numigi/vue-odoo/6d98c548bc80448af0b0479749fdfe1bfb84e0ff/vuex/static/description/icon.png -------------------------------------------------------------------------------- /vuex/static/lib/vuex.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * vuex v3.5.1 3 | * (c) 2020 Evan You 4 | * @license MIT 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Vuex=e()}(this,(function(){"use strict";var t=("undefined"!=typeof window?window:"undefined"!=typeof global?global:{}).__VUE_DEVTOOLS_GLOBAL_HOOK__;function e(t,n){if(void 0===n&&(n=[]),null===t||"object"!=typeof t)return t;var o,r=(o=function(e){return e.original===t},n.filter(o)[0]);if(r)return r.copy;var i=Array.isArray(t)?[]:{};return n.push({original:t,copy:i}),Object.keys(t).forEach((function(o){i[o]=e(t[o],n)})),i}function n(t,e){Object.keys(t).forEach((function(n){return e(t[n],n)}))}function o(t){return null!==t&&"object"==typeof t}var r=function(t,e){this.runtime=e,this._children=Object.create(null),this._rawModule=t;var n=t.state;this.state=("function"==typeof n?n():n)||{}},i={namespaced:{configurable:!0}};i.namespaced.get=function(){return!!this._rawModule.namespaced},r.prototype.addChild=function(t,e){this._children[t]=e},r.prototype.removeChild=function(t){delete this._children[t]},r.prototype.getChild=function(t){return this._children[t]},r.prototype.hasChild=function(t){return t in this._children},r.prototype.update=function(t){this._rawModule.namespaced=t.namespaced,t.actions&&(this._rawModule.actions=t.actions),t.mutations&&(this._rawModule.mutations=t.mutations),t.getters&&(this._rawModule.getters=t.getters)},r.prototype.forEachChild=function(t){n(this._children,t)},r.prototype.forEachGetter=function(t){this._rawModule.getters&&n(this._rawModule.getters,t)},r.prototype.forEachAction=function(t){this._rawModule.actions&&n(this._rawModule.actions,t)},r.prototype.forEachMutation=function(t){this._rawModule.mutations&&n(this._rawModule.mutations,t)},Object.defineProperties(r.prototype,i);var c,a=function(t){this.register([],t,!1)};a.prototype.get=function(t){return t.reduce((function(t,e){return t.getChild(e)}),this.root)},a.prototype.getNamespace=function(t){var e=this.root;return t.reduce((function(t,n){return t+((e=e.getChild(n)).namespaced?n+"/":"")}),"")},a.prototype.update=function(t){!function t(e,n,o){if(n.update(o),o.modules)for(var r in o.modules){if(!n.getChild(r))return;t(e.concat(r),n.getChild(r),o.modules[r])}}([],this.root,t)},a.prototype.register=function(t,e,o){var i=this;void 0===o&&(o=!0);var c=new r(e,o);0===t.length?this.root=c:this.get(t.slice(0,-1)).addChild(t[t.length-1],c);e.modules&&n(e.modules,(function(e,n){i.register(t.concat(n),e,o)}))},a.prototype.unregister=function(t){var e=this.get(t.slice(0,-1)),n=t[t.length-1],o=e.getChild(n);o&&o.runtime&&e.removeChild(n)},a.prototype.isRegistered=function(t){var e=this.get(t.slice(0,-1)),n=t[t.length-1];return e.hasChild(n)};var s=function(e){var n=this;void 0===e&&(e={}),!c&&"undefined"!=typeof window&&window.Vue&&v(window.Vue);var o=e.plugins;void 0===o&&(o=[]);var r=e.strict;void 0===r&&(r=!1),this._committing=!1,this._actions=Object.create(null),this._actionSubscribers=[],this._mutations=Object.create(null),this._wrappedGetters=Object.create(null),this._modules=new a(e),this._modulesNamespaceMap=Object.create(null),this._subscribers=[],this._watcherVM=new c,this._makeLocalGettersCache=Object.create(null);var i=this,s=this.dispatch,u=this.commit;this.dispatch=function(t,e){return s.call(i,t,e)},this.commit=function(t,e,n){return u.call(i,t,e,n)},this.strict=r;var f=this._modules.root.state;h(this,f,[],this._modules.root),p(this,f),o.forEach((function(t){return t(n)})),(void 0!==e.devtools?e.devtools:c.config.devtools)&&function(e){t&&(e._devtoolHook=t,t.emit("vuex:init",e),t.on("vuex:travel-to-state",(function(t){e.replaceState(t)})),e.subscribe((function(e,n){t.emit("vuex:mutation",e,n)}),{prepend:!0}),e.subscribeAction((function(e,n){t.emit("vuex:action",e,n)}),{prepend:!0}))}(this)},u={state:{configurable:!0}};function f(t,e,n){return e.indexOf(t)<0&&(n&&n.prepend?e.unshift(t):e.push(t)),function(){var n=e.indexOf(t);n>-1&&e.splice(n,1)}}function l(t,e){t._actions=Object.create(null),t._mutations=Object.create(null),t._wrappedGetters=Object.create(null),t._modulesNamespaceMap=Object.create(null);var n=t.state;h(t,n,[],t._modules.root,!0),p(t,n,e)}function p(t,e,o){var r=t._vm;t.getters={},t._makeLocalGettersCache=Object.create(null);var i=t._wrappedGetters,a={};n(i,(function(e,n){a[n]=function(t,e){return function(){return t(e)}}(e,t),Object.defineProperty(t.getters,n,{get:function(){return t._vm[n]},enumerable:!0})}));var s=c.config.silent;c.config.silent=!0,t._vm=new c({data:{$$state:e},computed:a}),c.config.silent=s,t.strict&&function(t){t._vm.$watch((function(){return this._data.$$state}),(function(){}),{deep:!0,sync:!0})}(t),r&&(o&&t._withCommit((function(){r._data.$$state=null})),c.nextTick((function(){return r.$destroy()})))}function h(t,e,n,o,r){var i=!n.length,a=t._modules.getNamespace(n);if(o.namespaced&&(t._modulesNamespaceMap[a],t._modulesNamespaceMap[a]=o),!i&&!r){var s=d(e,n.slice(0,-1)),u=n[n.length-1];t._withCommit((function(){c.set(s,u,o.state)}))}var f=o.context=function(t,e,n){var o=""===e,r={dispatch:o?t.dispatch:function(n,o,r){var i=m(n,o,r),c=i.payload,a=i.options,s=i.type;return a&&a.root||(s=e+s),t.dispatch(s,c)},commit:o?t.commit:function(n,o,r){var i=m(n,o,r),c=i.payload,a=i.options,s=i.type;a&&a.root||(s=e+s),t.commit(s,c,a)}};return Object.defineProperties(r,{getters:{get:o?function(){return t.getters}:function(){return function(t,e){if(!t._makeLocalGettersCache[e]){var n={},o=e.length;Object.keys(t.getters).forEach((function(r){if(r.slice(0,o)===e){var i=r.slice(o);Object.defineProperty(n,i,{get:function(){return t.getters[r]},enumerable:!0})}})),t._makeLocalGettersCache[e]=n}return t._makeLocalGettersCache[e]}(t,e)}},state:{get:function(){return d(t.state,n)}}}),r}(t,a,n);o.forEachMutation((function(e,n){!function(t,e,n,o){(t._mutations[e]||(t._mutations[e]=[])).push((function(e){n.call(t,o.state,e)}))}(t,a+n,e,f)})),o.forEachAction((function(e,n){var o=e.root?n:a+n,r=e.handler||e;!function(t,e,n,o){(t._actions[e]||(t._actions[e]=[])).push((function(e){var r,i=n.call(t,{dispatch:o.dispatch,commit:o.commit,getters:o.getters,state:o.state,rootGetters:t.getters,rootState:t.state},e);return(r=i)&&"function"==typeof r.then||(i=Promise.resolve(i)),t._devtoolHook?i.catch((function(e){throw t._devtoolHook.emit("vuex:error",e),e})):i}))}(t,o,r,f)})),o.forEachGetter((function(e,n){!function(t,e,n,o){if(t._wrappedGetters[e])return;t._wrappedGetters[e]=function(t){return n(o.state,o.getters,t.state,t.getters)}}(t,a+n,e,f)})),o.forEachChild((function(o,i){h(t,e,n.concat(i),o,r)}))}function d(t,e){return e.reduce((function(t,e){return t[e]}),t)}function m(t,e,n){return o(t)&&t.type&&(n=e,e=t,t=t.type),{type:t,payload:e,options:n}}function v(t){c&&t===c||function(t){if(Number(t.version.split(".")[0])>=2)t.mixin({beforeCreate:n});else{var e=t.prototype._init;t.prototype._init=function(t){void 0===t&&(t={}),t.init=t.init?[n].concat(t.init):n,e.call(this,t)}}function n(){var t=this.$options;t.store?this.$store="function"==typeof t.store?t.store():t.store:t.parent&&t.parent.$store&&(this.$store=t.parent.$store)}}(c=t)}u.state.get=function(){return this._vm._data.$$state},u.state.set=function(t){},s.prototype.commit=function(t,e,n){var o=this,r=m(t,e,n),i=r.type,c=r.payload,a={type:i,payload:c},s=this._mutations[i];s&&(this._withCommit((function(){s.forEach((function(t){t(c)}))})),this._subscribers.slice().forEach((function(t){return t(a,o.state)})))},s.prototype.dispatch=function(t,e){var n=this,o=m(t,e),r=o.type,i=o.payload,c={type:r,payload:i},a=this._actions[r];if(a){try{this._actionSubscribers.slice().filter((function(t){return t.before})).forEach((function(t){return t.before(c,n.state)}))}catch(t){}var s=a.length>1?Promise.all(a.map((function(t){return t(i)}))):a[0](i);return new Promise((function(t,e){s.then((function(e){try{n._actionSubscribers.filter((function(t){return t.after})).forEach((function(t){return t.after(c,n.state)}))}catch(t){}t(e)}),(function(t){try{n._actionSubscribers.filter((function(t){return t.error})).forEach((function(e){return e.error(c,n.state,t)}))}catch(t){}e(t)}))}))}},s.prototype.subscribe=function(t,e){return f(t,this._subscribers,e)},s.prototype.subscribeAction=function(t,e){return f("function"==typeof t?{before:t}:t,this._actionSubscribers,e)},s.prototype.watch=function(t,e,n){var o=this;return this._watcherVM.$watch((function(){return t(o.state,o.getters)}),e,n)},s.prototype.replaceState=function(t){var e=this;this._withCommit((function(){e._vm._data.$$state=t}))},s.prototype.registerModule=function(t,e,n){void 0===n&&(n={}),"string"==typeof t&&(t=[t]),this._modules.register(t,e),h(this,this.state,t,this._modules.get(t),n.preserveState),p(this,this.state)},s.prototype.unregisterModule=function(t){var e=this;"string"==typeof t&&(t=[t]),this._modules.unregister(t),this._withCommit((function(){var n=d(e.state,t.slice(0,-1));c.delete(n,t[t.length-1])})),l(this)},s.prototype.hasModule=function(t){return"string"==typeof t&&(t=[t]),this._modules.isRegistered(t)},s.prototype.hotUpdate=function(t){this._modules.update(t),l(this,!0)},s.prototype._withCommit=function(t){var e=this._committing;this._committing=!0,t(),this._committing=e},Object.defineProperties(s.prototype,u);var g=M((function(t,e){var n={};return w(e).forEach((function(e){var o=e.key,r=e.val;n[o]=function(){var e=this.$store.state,n=this.$store.getters;if(t){var o=$(this.$store,"mapState",t);if(!o)return;e=o.context.state,n=o.context.getters}return"function"==typeof r?r.call(this,e,n):e[r]},n[o].vuex=!0})),n})),y=M((function(t,e){var n={};return w(e).forEach((function(e){var o=e.key,r=e.val;n[o]=function(){for(var e=[],n=arguments.length;n--;)e[n]=arguments[n];var o=this.$store.commit;if(t){var i=$(this.$store,"mapMutations",t);if(!i)return;o=i.context.commit}return"function"==typeof r?r.apply(this,[o].concat(e)):o.apply(this.$store,[r].concat(e))}})),n})),_=M((function(t,e){var n={};return w(e).forEach((function(e){var o=e.key,r=e.val;r=t+r,n[o]=function(){if(!t||$(this.$store,"mapGetters",t))return this.$store.getters[r]},n[o].vuex=!0})),n})),b=M((function(t,e){var n={};return w(e).forEach((function(e){var o=e.key,r=e.val;n[o]=function(){for(var e=[],n=arguments.length;n--;)e[n]=arguments[n];var o=this.$store.dispatch;if(t){var i=$(this.$store,"mapActions",t);if(!i)return;o=i.context.dispatch}return"function"==typeof r?r.apply(this,[o].concat(e)):o.apply(this.$store,[r].concat(e))}})),n}));function w(t){return function(t){return Array.isArray(t)||o(t)}(t)?Array.isArray(t)?t.map((function(t){return{key:t,val:t}})):Object.keys(t).map((function(e){return{key:e,val:t[e]}})):[]}function M(t){return function(e,n){return"string"!=typeof e?(n=e,e=""):"/"!==e.charAt(e.length-1)&&(e+="/"),t(e,n)}}function $(t,e,n){return t._modulesNamespaceMap[n]}function C(t,e,n){var o=n?t.groupCollapsed:t.group;try{o.call(t,e)}catch(n){t.log(e)}}function E(t){try{t.groupEnd()}catch(e){t.log("—— log end ——")}}function O(){var t=new Date;return" @ "+j(t.getHours(),2)+":"+j(t.getMinutes(),2)+":"+j(t.getSeconds(),2)+"."+j(t.getMilliseconds(),3)}function j(t,e){return n="0",o=e-t.toString().length,new Array(o+1).join(n)+t;var n,o}return{Store:s,install:v,version:"3.5.1",mapState:g,mapMutations:y,mapGetters:_,mapActions:b,createNamespacedHelpers:function(t){return{mapState:g.bind(null,t),mapGetters:_.bind(null,t),mapMutations:y.bind(null,t),mapActions:b.bind(null,t)}},createLogger:function(t){void 0===t&&(t={});var n=t.collapsed;void 0===n&&(n=!0);var o=t.filter;void 0===o&&(o=function(t,e,n){return!0});var r=t.transformer;void 0===r&&(r=function(t){return t});var i=t.mutationTransformer;void 0===i&&(i=function(t){return t});var c=t.actionFilter;void 0===c&&(c=function(t,e){return!0});var a=t.actionTransformer;void 0===a&&(a=function(t){return t});var s=t.logMutations;void 0===s&&(s=!0);var u=t.logActions;void 0===u&&(u=!0);var f=t.logger;return void 0===f&&(f=console),function(t){var l=e(t.state);void 0!==f&&(s&&t.subscribe((function(t,c){var a=e(c);if(o(t,l,a)){var s=O(),u=i(t),p="mutation "+t.type+s;C(f,p,n),f.log("%c prev state","color: #9E9E9E; font-weight: bold",r(l)),f.log("%c mutation","color: #03A9F4; font-weight: bold",u),f.log("%c next state","color: #4CAF50; font-weight: bold",r(a)),E(f)}l=a})),u&&t.subscribeAction((function(t,e){if(c(t,e)){var o=O(),r=a(t),i="action "+t.type+o;C(f,i,n),f.log("%c action","color: #03A9F4; font-weight: bold",r),E(f)}})))}}}})); -------------------------------------------------------------------------------- /vuex/static/src/js/store.js: -------------------------------------------------------------------------------- 1 | odoo.define("vuex.store", function (require) { 2 | "use strict"; 3 | 4 | const Class = require("web.Class"); 5 | 6 | Vue.use(Vuex) 7 | return new Vuex.Store({ modules: [] }); 8 | 9 | }); 10 | -------------------------------------------------------------------------------- /vuex/views/assets.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | --------------------------------------------------------------------------------