summaryrefslogtreecommitdiff
path: root/addons/base_import/static
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/base_import/static
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/base_import/static')
-rw-r--r--addons/base_import/static/csv/External_id_3rd_party_application_product_categories.csv6
-rw-r--r--addons/base_import/static/csv/External_id_3rd_party_application_products.csv6
-rw-r--r--addons/base_import/static/csv/Name_products.csv6
-rw-r--r--addons/base_import/static/csv/database_import_test.sql175
-rw-r--r--addons/base_import/static/csv/m2m_customers_tags.csv6
-rw-r--r--addons/base_import/static/csv/o2m_customers_contacts.csv9
-rw-r--r--addons/base_import/static/csv/o2m_purchase_order_lines.csv6
-rw-r--r--addons/base_import/static/csv/purchase.order_functional_error_line_cant_adpat.csv3
-rw-r--r--addons/base_import/static/lib/javascript-state-machine/LICENSE20
-rw-r--r--addons/base_import/static/lib/javascript-state-machine/README.md327
-rw-r--r--addons/base_import/static/lib/javascript-state-machine/RELEASE_NOTES.md32
-rw-r--r--addons/base_import/static/lib/javascript-state-machine/Rakefile8
-rw-r--r--addons/base_import/static/lib/javascript-state-machine/index.html39
-rw-r--r--addons/base_import/static/lib/javascript-state-machine/state-machine.js155
-rw-r--r--addons/base_import/static/src/js/import_action.js927
-rw-r--r--addons/base_import/static/src/js/import_menu.js63
-rw-r--r--addons/base_import/static/src/scss/base_import.scss164
-rw-r--r--addons/base_import/static/src/xml/base_import.xml227
-rw-r--r--addons/base_import/static/tests/import_buttons_tests.js170
19 files changed, 2349 insertions, 0 deletions
diff --git a/addons/base_import/static/csv/External_id_3rd_party_application_product_categories.csv b/addons/base_import/static/csv/External_id_3rd_party_application_product_categories.csv
new file mode 100644
index 00000000..5b77f269
--- /dev/null
+++ b/addons/base_import/static/csv/External_id_3rd_party_application_product_categories.csv
@@ -0,0 +1,6 @@
+External ID,Name,Parent Category/External ID
+a1,Expenses,product.product_category_all
+a2,Other Products,product.product_category_all
+a3,Sellable Products,product.product_category_all
+a4,Tables,a1
+a5,Seating furniture,a2
diff --git a/addons/base_import/static/csv/External_id_3rd_party_application_products.csv b/addons/base_import/static/csv/External_id_3rd_party_application_products.csv
new file mode 100644
index 00000000..bf2a5238
--- /dev/null
+++ b/addons/base_import/static/csv/External_id_3rd_party_application_products.csv
@@ -0,0 +1,6 @@
+External ID,Name,Internal Reference,Category/External ID,Can be Expensed,Can be Purchased,Can be Sold,Sale Price,Cost,Supply Method,Product Type,Procurement Method
+a6,Aluminum Stool,ALS,a5,False,True,True,49.00,25.00,Buy,Stockable Product,Make to Stock
+a7,Chair,CHR,a5,False,True,True,89.00,40.00,Buy,Stockable Product,Make to Stock
+a8,Table,TBL,a4,False,True,True,169.00,100.00,Buy,Stockable Product,Make to Stock
+a9,Software Book Tutorial,SBT,a2,False,True,False,19.00,8.00,Buy,Consumable,Make to Stock
+a10,Fuel,FL,a1,True,False,False,0.30,0.25,Buy,Service,Make to Stock
diff --git a/addons/base_import/static/csv/Name_products.csv b/addons/base_import/static/csv/Name_products.csv
new file mode 100644
index 00000000..b3fa9b57
--- /dev/null
+++ b/addons/base_import/static/csv/Name_products.csv
@@ -0,0 +1,6 @@
+Name,Internal Reference,Can be Expensed,Can be Purchased,Can be Sold,Sale Price,Cost,Supply Method,Product Type,Procurement Method
+Aluminum Stool,ALS,False,True,True,49.00,25.00,Buy,Stockable Product,Make to Stock
+Chair,CHR,False,True,True,89.00,40.00,Buy,Stockable Product,Make to Stock
+Table,TBL,False,True,True,169.00,100.00,Buy,Stockable Product,Make to Stock
+Software Book Tutorial,SBT,False,True,False,19.00,8.00,Buy,Consumable,Make to Stock
+Fuel,FL,True,False,False,0.30,0.25,Buy,Service,Make to Stock
diff --git a/addons/base_import/static/csv/database_import_test.sql b/addons/base_import/static/csv/database_import_test.sql
new file mode 100644
index 00000000..13a735d1
--- /dev/null
+++ b/addons/base_import/static/csv/database_import_test.sql
@@ -0,0 +1,175 @@
+--
+-- PostgreSQL database dump
+--
+
+-- Dumped from database version 9.5.2
+-- Dumped by pg_dump version 9.5.2
+
+SET statement_timeout = 0;
+SET lock_timeout = 0;
+SET client_encoding = 'UTF8';
+SET standard_conforming_strings = on;
+SET check_function_bodies = false;
+SET client_min_messages = warning;
+SET row_security = off;
+
+--
+-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: -
+--
+
+CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;
+
+
+--
+-- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: -
+--
+
+COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';
+
+
+SET search_path = public, pg_catalog;
+
+SET default_tablespace = '';
+
+SET default_with_oids = false;
+
+--
+-- Name: companies; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE companies (
+ id integer NOT NULL,
+ company_name character varying
+);
+
+
+--
+-- Name: companies_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE companies_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: companies_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE companies_id_seq OWNED BY companies.id;
+
+
+--
+-- Name: persons; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE persons (
+ id integer NOT NULL,
+ company_id integer,
+ person_name character varying
+);
+
+
+--
+-- Name: persons_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE persons_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: persons_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE persons_id_seq OWNED BY persons.id;
+
+
+--
+-- Name: id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY companies ALTER COLUMN id SET DEFAULT nextval('companies_id_seq'::regclass);
+
+
+--
+-- Name: id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY persons ALTER COLUMN id SET DEFAULT nextval('persons_id_seq'::regclass);
+
+
+--
+-- Data for Name: companies; Type: TABLE DATA; Schema: public; Owner: -
+--
+
+COPY companies (id, company_name) FROM stdin;
+1 Bigees
+2 Organi
+3 Boum
+\.
+
+
+--
+-- Name: companies_id_seq; Type: SEQUENCE SET; Schema: public; Owner: -
+--
+
+SELECT pg_catalog.setval('companies_id_seq', 3, true);
+
+
+--
+-- Data for Name: persons; Type: TABLE DATA; Schema: public; Owner: -
+--
+
+COPY persons (id, company_id, person_name) FROM stdin;
+1 1 Fabien
+2 1 Laurence
+3 2 Eric
+4 3 Ramzy
+\.
+
+
+--
+-- Name: persons_id_seq; Type: SEQUENCE SET; Schema: public; Owner: -
+--
+
+SELECT pg_catalog.setval('persons_id_seq', 4, true);
+
+
+--
+-- Name: companies_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY companies
+ ADD CONSTRAINT companies_pkey PRIMARY KEY (id);
+
+
+--
+-- Name: persons_company_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY persons
+ ADD CONSTRAINT persons_company_id_fkey FOREIGN KEY (company_id) REFERENCES companies(id);
+
+
+--
+-- Name: public; Type: ACL; Schema: -; Owner: -
+--
+
+REVOKE ALL ON SCHEMA public FROM PUBLIC;
+REVOKE ALL ON SCHEMA public FROM postgres;
+GRANT ALL ON SCHEMA public TO postgres;
+GRANT ALL ON SCHEMA public TO PUBLIC;
+
+
+--
+-- PostgreSQL database dump complete
+--
+
diff --git a/addons/base_import/static/csv/m2m_customers_tags.csv b/addons/base_import/static/csv/m2m_customers_tags.csv
new file mode 100644
index 00000000..e6d74d4f
--- /dev/null
+++ b/addons/base_import/static/csv/m2m_customers_tags.csv
@@ -0,0 +1,6 @@
+Name,Reference,Tags,Customer,Street,City,Country
+Credit & Leasing,3,Services,True,Central Avenue 814,Johannesburg,South Africa
+Services & Finance,5,"Consultancy Services,IT Services",True,Grove Road 5,London,United Kingdom
+Hydra Supplies,6,"Manufacturer,Retailer",True,Palm Street 9,Los Angeles,United States
+Bolts & Screws,8,"Wholesaler,Components Buyer",True,Rua Américo 1000,Campinas,Brazil
+National Parts & Supplies,18,"Manufacturer,Wholesaler",True,Guangdong Way 20,Shenzen,China
diff --git a/addons/base_import/static/csv/o2m_customers_contacts.csv b/addons/base_import/static/csv/o2m_customers_contacts.csv
new file mode 100644
index 00000000..ac912025
--- /dev/null
+++ b/addons/base_import/static/csv/o2m_customers_contacts.csv
@@ -0,0 +1,9 @@
+Name,Is a company,Related company,Address type,Street,ZIP,City,State,Country
+Aurora Shelves,1,,,25 Pacific Road,95101,San José,California,United States
+Roger Martins,0,Aurora Shelves,Invoice address,27 Pacific Road,95102,San José,California,United States
+House Sales Direct,1,,,104 Saint Mary Avenue,94059,Redwood,California,United States
+Yvan Holiday,0,House Sales Direct,Contact,104 Saint Mary Avenue,94060,Redwood,California,United States
+Jack Unsworth,0,House Sales Direct,Invoice address,227 Jackson Road,94061,Redwood,California,United States
+Michael Mason,0,,,16 5th Avenue,94104,San Francisco,California,United States
+International Wood,1,,,748 White House Boulevard,20004,Washington,D.C.,United States
+Sharon Pecker,0,International Wood,Invoice address,755 White House Boulevard,20005,Washington,D.C.,United States \ No newline at end of file
diff --git a/addons/base_import/static/csv/o2m_purchase_order_lines.csv b/addons/base_import/static/csv/o2m_purchase_order_lines.csv
new file mode 100644
index 00000000..100f93e4
--- /dev/null
+++ b/addons/base_import/static/csv/o2m_purchase_order_lines.csv
@@ -0,0 +1,6 @@
+"Order Date","Order Reference",Vendor,"Order Lines / Product","Order Lines / Description","Order Lines / Quantity","Order Lines / Product Unit of Measure","Order Lines / Unit Price","Order Lines / Scheduled Date"
+2016-12-15,PO00008,ASUSTeK,ADPT,ADPT,20.0,"Units",13.0,2016-12-15
+,,,CARD,CARD,30,"Units",876.0,2016-12-15
+,,,C-Case,C-Case,40,"Units",20.0,2016-12-15
+2016-12-15,PO00009,Camptocamp,CD,CD,5,"Units",18.4,2016-12-15
+,,,CPUa8,CPUa8,15,"Units",1910.0,2016-12-15
diff --git a/addons/base_import/static/csv/purchase.order_functional_error_line_cant_adpat.csv b/addons/base_import/static/csv/purchase.order_functional_error_line_cant_adpat.csv
new file mode 100644
index 00000000..35c07c08
--- /dev/null
+++ b/addons/base_import/static/csv/purchase.order_functional_error_line_cant_adpat.csv
@@ -0,0 +1,3 @@
+"Order Reference","Vendor"
+"PO000020","ASUSTeK"
+"PO000021","Camptocamp"
diff --git a/addons/base_import/static/lib/javascript-state-machine/LICENSE b/addons/base_import/static/lib/javascript-state-machine/LICENSE
new file mode 100644
index 00000000..8ad703ca
--- /dev/null
+++ b/addons/base_import/static/lib/javascript-state-machine/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2012 Jake Gordon and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/addons/base_import/static/lib/javascript-state-machine/README.md b/addons/base_import/static/lib/javascript-state-machine/README.md
new file mode 100644
index 00000000..64c045e6
--- /dev/null
+++ b/addons/base_import/static/lib/javascript-state-machine/README.md
@@ -0,0 +1,327 @@
+Javascript Finite State Machine (v2.1.0)
+========================================
+
+This standalone javascript micro-framework provides a finite state machine for your pleasure.
+
+ * You can find the [code here](https://github.com/jakesgordon/javascript-state-machine)
+ * You can find a [description here](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/)
+ * You can find a [working demo here](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/example/)
+
+Download
+========
+
+You can download [state-machine.js](https://github.com/jakesgordon/javascript-state-machine/raw/master/state-machine.js),
+or the [minified version](https://github.com/jakesgordon/javascript-state-machine/raw/master/state-machine.min.js)
+
+Alternatively:
+
+ git clone git@github.com:jakesgordon/javascript-state-machine
+
+
+ * All code is in state-machine.js
+ * Minified version provided in state-machine.min.js
+ * No 3rd party library is required
+ * Demo can be found in /index.html
+ * QUnit tests can be found in /test/index.html
+
+Usage
+=====
+
+Include `state-machine.min.js` in your application.
+
+In its simplest form, create a standalone state machine using:
+
+ var fsm = StateMachine.create({
+ initial: 'green',
+ events: [
+ { name: 'warn', from: 'green', to: 'yellow' },
+ { name: 'panic', from: 'yellow', to: 'red' },
+ { name: 'calm', from: 'red', to: 'yellow' },
+ { name: 'clear', from: 'yellow', to: 'green' }
+ ]});
+
+... will create an object with a method for each event:
+
+ * fsm.warn() - transition from 'green' to 'yellow'
+ * fsm.panic() - transition from 'yellow' to 'red'
+ * fsm.calm() - transition from 'red' to 'yellow'
+ * fsm.clear() - transition from 'yellow' to 'green'
+
+along with the following members:
+
+ * fsm.current - contains the current state
+ * fsm.is(s) - return true if state `s` is the current state
+ * fsm.can(e) - return true if event `e` can be fired in the current state
+ * fsm.cannot(e) - return true if event `e` cannot be fired in the current state
+
+Multiple 'from' and 'to' states for a single event
+==================================================
+
+If an event is allowed **from** multiple states, and always transitions to the same
+state, then simply provide an array of states in the `from` attribute of an event. However,
+if an event is allowed from multiple states, but should transition **to** a different
+state depending on the current state, then provide multiple event entries with
+the same name:
+
+ var fsm = StateMachine.create({
+ initial: 'hungry',
+ events: [
+ { name: 'eat', from: 'hungry', to: 'satisfied' },
+ { name: 'eat', from: 'satisfied', to: 'full' },
+ { name: 'eat', from: 'full', to: 'sick' },
+ { name: 'rest', from: ['hungry', 'satisfied', 'full', 'sick'], to: 'hungry' },
+ ]});
+
+This example will create an object with 2 event methods:
+
+ * fsm.eat()
+ * fsm.rest()
+
+The `rest` event will always transition to the `hungry` state, while the `eat` event
+will transition to a state that is dependent on the current state.
+
+>> NOTE: The `rest` event could use a wildcard '*' for the 'from' state if it should be
+allowed from any current state.
+
+>> NOTE: The `rest` event in the above example can also be specified as multiple events with
+the same name if you prefer the verbose approach.
+
+Callbacks
+=========
+
+4 callbacks are available if your state machine has methods using the following naming conventions:
+
+ * onbefore**event** - fired before the event
+ * onleave**state** - fired when leaving the old state
+ * onenter**state** - fired when entering the new state
+ * onafter**event** - fired after the event
+
+You can affect the event in 3 ways:
+
+ * return `false` from an `onbeforeevent` handler to cancel the event.
+ * return `false` from an `onleavestate` handler to cancel the event.
+ * return `ASYNC` from an `onleavestate` handler to perform an asynchronous state transition (see next section)
+
+For convenience, the 2 most useful callbacks can be shortened:
+
+ * on**event** - convenience shorthand for onafter**event**
+ * on**state** - convenience shorthand for onenter**state**
+
+In addition, a generic `onchangestate()` callback can be used to call a single function for _all_ state changes:
+
+All callbacks will be passed the same arguments:
+
+ * **event** name
+ * **from** state
+ * **to** state
+ * _(followed by any arguments you passed into the original event method)_
+
+Callbacks can be specified when the state machine is first created:
+
+ var fsm = StateMachine.create({
+ initial: 'green',
+ events: [
+ { name: 'warn', from: 'green', to: 'yellow' },
+ { name: 'panic', from: 'yellow', to: 'red' },
+ { name: 'calm', from: 'red', to: 'yellow' },
+ { name: 'clear', from: 'yellow', to: 'green' }
+ ],
+ callbacks: {
+ onpanic: function(event, from, to, msg) { alert('panic! ' + msg); },
+ onclear: function(event, from, to, msg) { alert('thanks to ' + msg); },
+ ongreen: function(event, from, to) { document.body.className = 'green'; },
+ onyellow: function(event, from, to) { document.body.className = 'yellow'; },
+ onred: function(event, from, to) { document.body.className = 'red'; },
+ }
+ });
+
+ fsm.panic('killer bees');
+ fsm.clear('sedatives in the honey pots');
+ ...
+
+Additionally, they can be added and removed from the state machine at any time:
+
+ fsm.ongreen = null;
+ fsm.onyellow = null;
+ fsm.onred = null;
+ fsm.onchangestate = function(event, from, to) { document.body.className = to; };
+
+Asynchronous State Transitions
+==============================
+
+Sometimes, you need to execute some asynchronous code during a state transition and ensure the
+new state is not entered until your code has completed.
+
+A good example of this is when you transition out of a `menu` state, perhaps you want to gradually
+fade the menu away, or slide it off the screen and don't want to transition to your `game` state
+until after that animation has been performed.
+
+You can now return `StateMachine.ASYNC` from your `onleavestate` handler and the state machine
+will be _'put on hold'_ until you are ready to trigger the transition using the new `transition()`
+method.
+
+For example, using jQuery effects:
+
+ var fsm = StateMachine.create({
+
+ initial: 'menu',
+
+ events: [
+ { name: 'play', from: 'menu', to: 'game' },
+ { name: 'quit', from: 'game', to: 'menu' }
+ ],
+
+ callbacks: {
+
+ onentermenu: function() { $('#menu').show(); },
+ onentergame: function() { $('#game').show(); },
+
+ onleavemenu: function() {
+ $('#menu').fadeOut('fast', function() {
+ fsm.transition();
+ });
+ return StateMachine.ASYNC; // tell StateMachine to defer next state until we call transition (in fadeOut callback above)
+ },
+
+ onleavegame: function() {
+ $('#game').slideDown('slow', function() {
+ fsm.transition();
+ };
+ return StateMachine.ASYNC; // tell StateMachine to defer next state until we call transition (in slideDown callback above)
+ }
+
+ }
+ });
+
+
+State Machine Classes
+=====================
+
+You can also turn all instances of a _class_ into an FSM by applying
+the state machine functionality to the prototype, including your callbacks
+in your prototype, and providing a `startup` event for use when constructing
+instances:
+
+ MyFSM = function() { // my constructor function
+ this.startup();
+ };
+
+ MyFSM.prototype = {
+
+ onpanic: function(event, from, to) { alert('panic'); },
+ onclear: function(event, from, to) { alert('all is clear'); },
+
+ // my other prototype methods
+
+ };
+
+ StateMachine.create({
+ target: MyFSM.prototype,
+ events: [
+ { name: 'startup', from: 'none', to: 'green' },
+ { name: 'warn', from: 'green', to: 'yellow' },
+ { name: 'panic', from: 'yellow', to: 'red' },
+ { name: 'calm', from: 'red', to: 'yellow' },
+ { name: 'clear', from: 'yellow', to: 'green' }
+ ]});
+
+
+This should be easy to adjust to fit your appropriate mechanism for object construction.
+
+Initialization Options
+======================
+
+How the state machine should initialize can depend on your application requirements, so
+the library provides a number of simple options.
+
+By default, if you dont specify any initial state, the state machine will be in the `'none'`
+state and you would need to provide an event to take it out of this state:
+
+ var fsm = StateMachine.create({
+ events: [
+ { name: 'startup', from: 'none', to: 'green' },
+ { name: 'panic', from: 'green', to: 'red' },
+ { name: 'calm', from: 'red', to: 'green' },
+ ]});
+ alert(fsm.current); // "none"
+ fsm.startup();
+ alert(fsm.current); // "green"
+
+If you specify the name of your initial event (as in all the earlier examples), then an
+implicit `startup` event will be created for you and fired when the state machine is constructed.
+
+ var fsm = StateMachine.create({
+ initial: 'green',
+ events: [
+ { name: 'panic', from: 'green', to: 'red' },
+ { name: 'calm', from: 'red', to: 'green' },
+ ]});
+ alert(fsm.current); // "green"
+
+If your object already has a `startup` method you can use a different name for the initial event
+
+ var fsm = StateMachine.create({
+ initial: { state: 'green', event: 'init' },
+ events: [
+ { name: 'panic', from: 'green', to: 'red' },
+ { name: 'calm', from: 'red', to: 'green' },
+ ]});
+ alert(fsm.current); // "green"
+
+Finally, if you want to wait to call the initial state transition event until a later date you
+can `defer` it:
+
+ var fsm = StateMachine.create({
+ initial: { state: 'green', event: 'init', defer: true },
+ events: [
+ { name: 'panic', from: 'green', to: 'red' },
+ { name: 'calm', from: 'red', to: 'green' },
+ ]});
+ alert(fsm.current); // "none"
+ fsm.init();
+ alert(fsm.current); // "green"
+
+Of course, we have now come full circle, this last example is pretty much functionally the
+same as the first example in this section where you simply define your own startup event.
+
+So you have a number of choices available to you when initializing your state machine.
+
+Handling Failures
+======================
+
+By default, if you try to call an event method that is not allowed in the current state, the
+state machine will throw an exception. If you prefer to handle the problem yourself, you can
+define a custom `error` handler:
+
+ var fsm = StateMachine.create({
+ initial: 'green',
+ error: function(eventName, from, to, args, errorCode, errorMessage) {
+ return 'event ' + eventName + ' was naughty :- ' + errorMessage;
+ },
+ events: [
+ { name: 'panic', from: 'green', to: 'red' },
+ { name: 'calm', from: 'red', to: 'green' },
+ ]});
+ alert(fsm.calm()); // "event calm was naughty :- event not allowed in current state green"
+
+Release Notes
+=============
+
+See [RELEASE NOTES](https://github.com/jakesgordon/javascript-state-machine/blob/master/RELEASE_NOTES.md) file.
+
+License
+=======
+
+See [LICENSE](https://github.com/jakesgordon/javascript-state-machine/blob/master/LICENSE) file.
+
+Contact
+=======
+
+If you have any ideas, feedback, requests or bug reports, you can reach me at
+[jake@codeincomplete.com](mailto:jake@codeincomplete.com), or via
+my website: [Code inComplete](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/)
+
+
+
+
+
diff --git a/addons/base_import/static/lib/javascript-state-machine/RELEASE_NOTES.md b/addons/base_import/static/lib/javascript-state-machine/RELEASE_NOTES.md
new file mode 100644
index 00000000..06abf402
--- /dev/null
+++ b/addons/base_import/static/lib/javascript-state-machine/RELEASE_NOTES.md
@@ -0,0 +1,32 @@
+Version 2.1.0 (January 7th 2012)
+--------------------------------
+
+ * Wrapped in self executing function to be more easily used with loaders like `require.js` or `curl.js` (issue #15)
+ * Allow event to be cancelled by returning `false` from `onleavestate` handler (issue #13) - WARNING: this breaks backward compatibility for async transitions (you now need to return `StateMachine.ASYNC` instead of `false`)
+ * Added explicit return values for event methods (issue #12)
+ * Added support for wildcard events that can be fired 'from' any state (issue #11)
+ * Added support for no-op events that transition 'to' the same state (issue #5)
+ * extended custom error callback to handle any exceptions caused by caller provided callbacks
+ * added custom error callback to override exception when an illegal state transition is attempted (thanks to cboone)
+ * fixed typos (thanks to cboone)
+ * fixed issue #4 - ensure before/after event hooks are called even if the event doesn't result in a state change
+
+Version 2.0.0 (August 19th 2011)
+--------------------------------
+
+ * adding support for asynchronous state transitions (see README) - with lots of qunit tests (see test/async.js).
+ * consistent arguments for ALL callbacks, first 3 args are ALWAYS event name, from state and to state, followed by whatever arguments the user passed to the original event method.
+ * added a generic `onchangestate(event,from,to)` callback to detect all state changes with a single function.
+ * allow callbacks to be declared at creation time (instead of having to attach them afterwards)
+ * renamed 'hooks' => 'callbacks'
+ * [read more...](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/)
+
+Version 1.2.0 (June 21st 2011)
+------------------------------
+ * allows the same event to transition to different states, depending on the current state (see 'Multiple...' section in README.md)
+ * [read more...](http://codeincomplete.com/posts/2011/6/21/javascript_state_machine_v1_2_0/)
+
+Version 1.0.0 (June 1st 2011)
+-----------------------------
+ * initial version
+ * [read more...](http://codeincomplete.com/posts/2011/6/1/javascript_state_machine/)
diff --git a/addons/base_import/static/lib/javascript-state-machine/Rakefile b/addons/base_import/static/lib/javascript-state-machine/Rakefile
new file mode 100644
index 00000000..beb8702a
--- /dev/null
+++ b/addons/base_import/static/lib/javascript-state-machine/Rakefile
@@ -0,0 +1,8 @@
+
+desc "create minified version of state-machine.js"
+task :minify do
+ require File.expand_path(File.join(File.dirname(__FILE__), 'minifier/minifier'))
+ Minifier.enabled = true
+ Minifier.minify('state-machine.js')
+end
+
diff --git a/addons/base_import/static/lib/javascript-state-machine/index.html b/addons/base_import/static/lib/javascript-state-machine/index.html
new file mode 100644
index 00000000..2d6cb626
--- /dev/null
+++ b/addons/base_import/static/lib/javascript-state-machine/index.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Javascript Finite State Machine</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+ <link href="demo/demo.css" media="screen, print" rel="stylesheet" type="text/css" />
+</head>
+
+<body>
+
+ <div id="demo" class='green'>
+
+ <h1> Finite State Machine </h1>
+
+ <div id="controls">
+ <button id="clear" onclick="Demo.clear();">clear</button>
+ <button id="calm" onclick="Demo.calm();">calm</button>
+ <button id="warn" onclick="Demo.warn();">warn</button>
+ <button id="panic" onclick="Demo.panic();">panic!</button>
+ </div>
+
+ <div id="diagram">
+ </div>
+
+ <div id="notes">
+ <i>dashed lines are asynchronous state transitions (3 seconds)</i>
+ </div>
+
+ <textarea id="output">
+ </textarea>
+
+ </div>
+
+
+ <script src="state-machine.js"></script>
+ <script src="demo/demo.js"></script>
+
+</body>
+</html>
diff --git a/addons/base_import/static/lib/javascript-state-machine/state-machine.js b/addons/base_import/static/lib/javascript-state-machine/state-machine.js
new file mode 100644
index 00000000..0d503ee7
--- /dev/null
+++ b/addons/base_import/static/lib/javascript-state-machine/state-machine.js
@@ -0,0 +1,155 @@
+(function (window) {
+
+ StateMachine = {
+
+ //---------------------------------------------------------------------------
+
+ VERSION: "2.1.0",
+
+ //---------------------------------------------------------------------------
+
+ Result: {
+ SUCCEEDED: 1, // the event transitioned successfully from one state to another
+ NOTRANSITION: 2, // the event was successfull but no state transition was necessary
+ CANCELLED: 3, // the event was cancelled by the caller in a beforeEvent callback
+ ASYNC: 4, // the event is asynchronous and the caller is in control of when the transition occurs
+ },
+
+ Error: {
+ INVALID_TRANSITION: 100, // caller tried to fire an event that was innapropriate in the current state
+ PENDING_TRANSITION: 200, // caller tried to fire an event while an async transition was still pending
+ INVALID_CALLBACK: 300, // caller provided callback function threw an exception
+ },
+
+ WILDCARD: '*',
+ ASYNC: 'async',
+
+ //---------------------------------------------------------------------------
+
+ create: function(cfg, target) {
+
+ var initial = (typeof cfg.initial == 'string') ? { state: cfg.initial } : cfg.initial; // allow for a simple string, or an object with { state: 'foo', event: 'setup', defer: true|false }
+ var fsm = target || cfg.target || {};
+ var events = cfg.events || [];
+ var callbacks = cfg.callbacks || {};
+ var map = {};
+
+ var add = function(e) {
+ var from = (e.from instanceof Array) ? e.from : (e.from ? [e.from] : [StateMachine.WILDCARD]); // allow 'wildcard' transition if 'from' is not specified
+ map[e.name] = map[e.name] || {};
+ for (var n = 0 ; n < from.length ; n++)
+ map[e.name][from[n]] = e.to || from[n]; // allow no-op transition if 'to' is not specified
+ };
+
+ if (initial) {
+ initial.event = initial.event || 'startup';
+ add({ name: initial.event, from: 'none', to: initial.state });
+ }
+
+ for(var n = 0 ; n < events.length ; n++)
+ add(events[n]);
+
+ for(var name in map) {
+ if (map.hasOwnProperty(name))
+ fsm[name] = StateMachine.buildEvent(name, map[name]);
+ }
+
+ for(var name in callbacks) {
+ if (callbacks.hasOwnProperty(name))
+ fsm[name] = callbacks[name]
+ }
+
+ fsm.current = 'none';
+ fsm.is = function(state) { return this.current == state; };
+ fsm.can = function(event) { return !this.transition && (map[event].hasOwnProperty(this.current) || map[event].hasOwnProperty(StateMachine.WILDCARD)); }
+ fsm.cannot = function(event) { return !this.can(event); };
+ fsm.error = cfg.error || function(name, from, to, args, error, msg) { throw msg; }; // default behavior when something unexpected happens is to throw an exception, but caller can override this behavior if desired (see github issue #3)
+
+ if (initial && !initial.defer)
+ fsm[initial.event]();
+
+ return fsm;
+
+ },
+
+ //===========================================================================
+
+ doCallback: function(fsm, func, name, from, to, args) {
+ if (func) {
+ try {
+ return func.apply(fsm, [name, from, to].concat(args));
+ }
+ catch(e) {
+ return fsm.error(name, from, to, args, StateMachine.Error.INVALID_CALLBACK, "an exception occurred in a caller-provided callback function");
+ }
+ }
+ },
+
+ beforeEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onbefore' + name], name, from, to, args); },
+ afterEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onafter' + name] || fsm['on' + name], name, from, to, args); },
+ leaveState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onleave' + from], name, from, to, args); },
+ enterState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onenter' + to] || fsm['on' + to], name, from, to, args); },
+ changeState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onchangestate'], name, from, to, args); },
+
+
+ buildEvent: function(name, map) {
+ return function() {
+
+ var from = this.current;
+ var to = map[from] || map[StateMachine.WILDCARD] || from;
+ var args = Array.prototype.slice.call(arguments); // turn arguments into pure array
+
+ if (this.transition)
+ return this.error(name, from, to, args, StateMachine.Error.PENDING_TRANSITION, "event " + name + " inappropriate because previous transition did not complete");
+
+ if (this.cannot(name))
+ return this.error(name, from, to, args, StateMachine.Error.INVALID_TRANSITION, "event " + name + " inappropriate in current state " + this.current);
+
+ if (false === StateMachine.beforeEvent(this, name, from, to, args))
+ return StateMachine.CANCELLED;
+
+ if (from === to) {
+ StateMachine.afterEvent(this, name, from, to, args);
+ return StateMachine.NOTRANSITION;
+ }
+
+ // prepare a transition method for use EITHER lower down, or by caller if they want an async transition (indicated by an ASYNC return value from leaveState)
+ var fsm = this;
+ this.transition = function() {
+ fsm.transition = null; // this method should only ever be called once
+ fsm.current = to;
+ StateMachine.enterState( fsm, name, from, to, args);
+ StateMachine.changeState(fsm, name, from, to, args);
+ StateMachine.afterEvent( fsm, name, from, to, args);
+ };
+
+ var leave = StateMachine.leaveState(this, name, from, to, args);
+ if (false === leave) {
+ this.transition = null;
+ return StateMachine.CANCELLED;
+ }
+ else if ("async" === leave) {
+ return StateMachine.ASYNC;
+ }
+ else {
+ if (this.transition)
+ this.transition(); // in case user manually called transition() but forgot to return ASYNC
+ return StateMachine.SUCCEEDED;
+ }
+
+ };
+ }
+
+ }; // StateMachine
+
+ //===========================================================================
+
+ if ("function" === typeof define) {
+ define("statemachine", [], function() { return StateMachine; });
+ }
+ else {
+ window.StateMachine = StateMachine;
+ }
+
+}(this));
+
diff --git a/addons/base_import/static/src/js/import_action.js b/addons/base_import/static/src/js/import_action.js
new file mode 100644
index 00000000..0b608953
--- /dev/null
+++ b/addons/base_import/static/src/js/import_action.js
@@ -0,0 +1,927 @@
+odoo.define('base_import.import', function (require) {
+"use strict";
+
+var AbstractAction = require('web.AbstractAction');
+var config = require('web.config');
+var core = require('web.core');
+var session = require('web.session');
+var time = require('web.time');
+var AbstractWebClient = require('web.AbstractWebClient');
+var Loading = require('web.Loading');
+
+var QWeb = core.qweb;
+var _t = core._t;
+var _lt = core._lt;
+var StateMachine = window.StateMachine;
+
+/**
+ * Safari does not deal well at all with raw JSON data being
+ * returned. As a result, we're going to cheat by using a
+ * pseudo-jsonp: instead of getting JSON data in the iframe, we're
+ * getting a ``script`` tag which consists of a function call and
+ * the returned data (the json dump).
+ *
+ * The function is an auto-generated name bound to ``window``,
+ * which calls back into the callback provided here.
+ *
+ * @param {Object} form the form element (DOM or jQuery) to use in the call
+ * @param {Object} attributes jquery.form attributes object
+ * @param {Function} callback function to call with the returned data
+ */
+function jsonp(form, attributes, callback) {
+ attributes = attributes || {};
+ var options = {jsonp: _.uniqueId('import_callback_')};
+ window[options.jsonp] = function () {
+ delete window[options.jsonp];
+ callback.apply(null, arguments);
+ };
+ if ('data' in attributes) {
+ _.extend(attributes.data, options);
+ } else {
+ _.extend(attributes, {data: options});
+ }
+ _.extend(attributes, {
+ dataType: 'script',
+ });
+ $(form).ajaxSubmit(attributes);
+}
+function _make_option(term) { return {id: term, text: term }; }
+function _from_data(data, term) {
+ return _.findWhere(data, {id: term}) || _make_option(term);
+}
+
+/**
+ * query returns a list of suggestion select2 objects, this function:
+ *
+ * * returns data exactly matching query by either id or text if those exist
+ * * otherwise it returns a select2 option matching the term and any data
+ * option whose id or text matches (by substring)
+ */
+function dataFilteredQuery(q) {
+ var suggestions = _.clone(this.data);
+ if (q.term) {
+ var exact = _.filter(suggestions, function (s) {
+ return s.id === q.term || s.text === q.term;
+ });
+ if (exact.length) {
+ suggestions = exact;
+ } else {
+ suggestions = [_make_option(q.term)].concat(_.filter(suggestions, function (s) {
+ return s.id.indexOf(q.term) !== -1 || s.text.indexOf(q.term) !== -1
+ }));
+ }
+ }
+ q.callback({results: suggestions});
+}
+
+var DataImport = AbstractAction.extend({
+ hasControlPanel: true,
+ contentTemplate: 'ImportView',
+ opts: [
+ {name: 'encoding', label: _lt("Encoding:"), value: ''},
+ {name: 'separator', label: _lt("Separator:"), value: ''},
+ {name: 'quoting', label: _lt("Text Delimiter:"), value: '"'}
+ ],
+ spreadsheet_opts: [
+ {name: 'sheet', label: _lt("Selected Sheet:"), value: ''},
+ ],
+ parse_opts_formats: [
+ {name: 'date_format', label: _lt("Date Format:"), value: ''},
+ {name: 'datetime_format', label: _lt("Datetime Format:"), value: ''},
+ ],
+ parse_opts_separators: [
+ {name: 'float_thousand_separator', label: _lt("Thousands Separator:"), value: ','},
+ {name: 'float_decimal_separator', label: _lt("Decimal Separator:"), value: '.'}
+ ],
+ events: {
+ // 'change .oe_import_grid input': 'import_dryrun',
+ 'change .oe_import_file': 'loaded_file',
+ 'change input.oe_import_has_header, .js_import_options input': 'settings_changed',
+ 'change input.oe_import_advanced_mode': function (e) {
+ this.do_not_change_match = true;
+ this['settings_changed']();
+ },
+ 'click a.oe_import_toggle': function (e) {
+ e.preventDefault();
+ this.$('.oe_import_options').toggle();
+ },
+ 'click .oe_import_report a.oe_import_report_count': function (e) {
+ e.preventDefault();
+ $(e.target).parent().parent().toggleClass('oe_import_report_showmore');
+ },
+ 'click .oe_import_report_see_possible_value': function (e) {
+ e.preventDefault();
+ $(e.target).parent().toggleClass('oe_import_report_showmore');
+ },
+ 'click .oe_import_moreinfo_action a': function (e) {
+ e.preventDefault();
+ // #data will parse the attribute on its own, we don't like
+ // that sort of things
+ var action = JSON.parse($(e.target).attr('data-action'));
+ // FIXME: when JS-side clean_action
+ action.views = _(action.views).map(function (view) {
+ var id = view[0], type = view[1];
+ return [
+ id,
+ type !== 'tree' ? type
+ : action.view_type === 'form' ? 'list'
+ : 'tree'
+ ];
+ });
+ this.do_action(_.extend(action, {
+ target: 'new',
+ flags: {
+ search_view: true,
+ display_title: true,
+ pager: true,
+ list: {selectable: false}
+ }
+ }));
+ },
+ },
+ init: function (parent, action) {
+ this._super.apply(this, arguments);
+ this.action_manager = parent;
+ this.res_model = action.params.model;
+ this.parent_context = action.params.context || {};
+ // import object id
+ this.id = null;
+ this.session = session;
+ this._title = _t('Import a File'); // Displayed in the breadcrumbs
+ this.do_not_change_match = false;
+ this.sheets = [];
+ },
+ /**
+ * @override
+ */
+ willStart: function () {
+ var self = this;
+ var def = this._rpc({
+ model: this.res_model,
+ method: 'get_import_templates',
+ context: this.parent_context,
+ }).then(function (result) {
+ self.importTemplates = result;
+ });
+ return Promise.all([this._super.apply(this, arguments), def]);
+ },
+ start: function () {
+ var self = this;
+ this.$form = this.$('form');
+ this.setup_encoding_picker();
+ this.setup_separator_picker();
+ this.setup_float_format_picker();
+ this.setup_date_format_picker();
+ this.setup_sheets_picker();
+
+ return Promise.all([
+ this._super(),
+ self.create_model().then(function (id) {
+ self.id = id;
+ self.$('input[name=import_id]').val(id);
+
+ self.renderButtons();
+ var status = {
+ cp_content: {$buttons: self.$buttons},
+ };
+ self.updateControlPanel(status);
+ }),
+ ]);
+ },
+ create_model: function() {
+ return this._rpc({
+ model: 'base_import.import',
+ method: 'create',
+ args: [{res_model: this.res_model}],
+ kwargs: {context: session.user_context},
+ });
+ },
+ renderButtons: function() {
+ var self = this;
+ this.$buttons = $(QWeb.render("ImportView.buttons", this));
+ this.$buttons.filter('.o_import_validate').on('click', this.validate.bind(this));
+ this.$buttons.filter('.o_import_import').on('click', this.import.bind(this));
+ this.$buttons.filter('.oe_import_file').on('click', function () {
+ self.$('.o_content .oe_import_file').click();
+ });
+ this.$buttons.filter('.o_import_cancel').on('click', function(e) {
+ e.preventDefault();
+ self.exit();
+ });
+ },
+ setup_encoding_picker: function () {
+ this.$('input.oe_import_encoding').select2({
+ width: '50%',
+ data: _.map(('utf-8 utf-16 windows-1252 latin1 latin2 big5 gb18030 shift_jis windows-1251 koir8_r').split(/\s+/), _make_option),
+ query: dataFilteredQuery,
+ initSelection: function ($e, c) {
+ return c(_make_option($e.val()));
+ }
+ });
+ },
+ setup_separator_picker: function () {
+ var data = [
+ {id: ',', text: _t("Comma")},
+ {id: ';', text: _t("Semicolon")},
+ {id: '\t', text: _t("Tab")},
+ {id: ' ', text: _t("Space")}
+ ];
+ this.$('input.oe_import_separator').select2({
+ width: '50%',
+ data: data,
+ query: dataFilteredQuery,
+ // this is not provided to initSelection so can't use this.data
+ initSelection: function ($e, c) {
+ c(_from_data(data, $e.val()) || _make_option($e.val()))
+ }
+ });
+ },
+ setup_float_format_picker: function () {
+ var data_decimal = [
+ {id: ',', text: _t("Comma")},
+ {id: '.', text: _t("Dot")},
+ ];
+ var data_digits = data_decimal.concat([{id: '', text: _t("No Separator")}]);
+ this.$('input.oe_import_float_thousand_separator').select2({
+ width: '50%',
+ data: data_digits,
+ query: dataFilteredQuery,
+ initSelection: function ($e, c) {
+ c(_from_data(data_digits, $e.val()) || _make_option($e.val()))
+ }
+ });
+ this.$('input.oe_import_float_decimal_separator').select2({
+ width: '50%',
+ data: data_decimal,
+ query: dataFilteredQuery,
+ initSelection: function ($e, c) {
+ c(_from_data(data_decimal, $e.val()) || _make_option($e.val()))
+ }
+ });
+ },
+ setup_date_format_picker: function () {
+ var data = _([
+ 'YYYY-MM-DD',
+ 'DD/MM/YY',
+ 'DD/MM/YYYY',
+ 'DD-MM-YYYY',
+ 'DD-MMM-YY',
+ 'DD-MMM-YYYY',
+ 'MM/DD/YY',
+ 'MM/DD/YYYY',
+ 'MM-DD-YY',
+ 'MM-DD-YYYY',
+ 'DDMMYY',
+ 'DDMMYYYY',
+ 'YYMMDD',
+ 'YYYYMMDD',
+ 'YY/MM/DD',
+ 'YYYY/MM/DD',
+ 'MMDDYY',
+ 'MMDDYYYY',
+ ]).map(_make_option);
+ this.$('input.oe_import_date_format').select2({
+ width: '50%',
+ data: data,
+ query: dataFilteredQuery,
+ initSelection: function ($e, c) {
+ c(_from_data(data, $e.val()) || _make_option($e.val()));
+ }
+ })
+ },
+ setup_sheets_picker: function () {
+ var data = this.sheets.map(_make_option);
+ this.$('input.oe_import_sheet').select2({
+ width: '50%',
+ data: data,
+ query: dataFilteredQuery,
+ initSelection: function ($e, c) {
+ c(_from_data(data, $e.val()) || _make_option($e.val()))
+ },
+ minimumResultsForSearch: 10,
+ });
+ },
+
+ import_options: function () {
+ var self = this;
+ var options = {
+ headers: this.$('input.oe_import_has_header').prop('checked'),
+ advanced: this.$('input.oe_import_advanced_mode').prop('checked'),
+ keep_matches: this.do_not_change_match,
+ name_create_enabled_fields: {},
+ // start at row 1 = skip 0 lines
+ skip: Number(this.$('#oe_import_row_start').val()) - 1 || 0,
+ limit: Number(this.$('#oe_import_batch_limit').val()) || null,
+ };
+ _.each(_.union(this.opts, this.spreadsheet_opts), function (opt) {
+ options[opt.name] =
+ self.$('input.oe_import_' + opt.name).val();
+ });
+ _(this.parse_opts_formats).each(function (opt) {
+ options[opt.name] = time.moment_to_strftime_format(self.$('input.oe_import_' + opt.name).val());
+ });
+ _(this.parse_opts_separators).each(function (opt) {
+ options[opt.name] = self.$('input.oe_import_' + opt.name).val();
+ });
+ options['fields'] = [];
+ if (this.do_not_change_match) {
+ options['fields'] = this.$('.oe_import_fields input.oe_import_match_field').map(function (index, el) {
+ return $(el).select2('val') || false;
+ }).get();
+ }
+ this.do_not_change_match = false;
+ this.$('input.o_import_create_option').each(function () {
+ var field = this.getAttribute('field');
+ if (field) {
+ options.name_create_enabled_fields[field] = this.checked;
+ }
+ });
+ return options;
+ },
+
+ //- File & settings change section
+ onfile_loaded: function (event, from, to, arg) {
+ // arg is null if reload -> don't reset partial import
+ if (arg != null ) {
+ this.toggle_partial(null);
+ }
+
+ this.$buttons.filter('.o_import_import, .o_import_validate').addClass('d-none');
+ if (!this.$('input.oe_import_file').val()) { return this['settings_changed'](); }
+ this.$('.oe_import_date_format').select2('val', '');
+ this.$('.oe_import_datetime_format').val('');
+ this.$('.oe_import_sheet').val('');
+
+ this.$form.removeClass('oe_import_preview oe_import_error');
+ var import_toggle = false;
+ var file = this.$('input.oe_import_file')[0].files[0];
+ // some platforms send text/csv, application/csv, or other things if Excel is prevent
+ if ((file.type && _.last(file.type.split('/')) === "csv") || ( _.last(file.name.split('.')) === "csv")) {
+ import_toggle = true;
+ }
+ this.$form.find('.oe_import_box').toggle(import_toggle);
+ jsonp(this.$form, {
+ url: '/base_import/set_file'
+ }, this.proxy('settings_changed'));
+ },
+ onpreviewing: function () {
+ var self = this;
+ this.$buttons.filter('.o_import_import, .o_import_validate').addClass('d-none');
+ this.$form.addClass('oe_import_with_file');
+ // TODO: test that write // succeeded?
+ this.$form.removeClass('oe_import_preview_error oe_import_error');
+ this.$form.toggleClass(
+ 'oe_import_noheaders text-muted',
+ !this.$('input.oe_import_has_header').prop('checked'));
+
+ // Clear the input value to allow onchange to be triggered
+ // if the file is the same (for all browsers)
+ self.$('input.oe_import_file').val('');
+
+ this._rpc({
+ model: 'base_import.import',
+ method: 'parse_preview',
+ args: [this.id, this.import_options()],
+ kwargs: {context: session.user_context},
+ }).then(function (result) {
+ var signal = result.error ? 'preview_failed' : 'preview_succeeded';
+ self[signal](result);
+ });
+ },
+ onpreview_error: function (event, from, to, result) {
+ this.$('.oe_import_options').show();
+ this.$form.addClass('oe_import_preview_error oe_import_error');
+ this.$form.find('.oe_import_box, .oe_import_with_file').removeClass('d-none');
+ this.$form.find('.o_view_nocontent').addClass('d-none');
+ this.$('.oe_import_error_report').html(
+ QWeb.render('ImportView.preview.error', result));
+ },
+ onpreview_success: function (event, from, to, result) {
+ var self = this;
+ this.$buttons.filter('.oe_import_file')
+ .text(_t('Load New File'))
+ .removeClass('btn-primary').addClass('btn-secondary')
+ .blur();
+ this.$buttons.filter('.o_import_import, .o_import_validate').removeClass('d-none');
+ this.$form.find('.oe_import_box, .oe_import_with_file').removeClass('d-none');
+ this.$form.find('.o_view_nocontent').addClass('d-none');
+ this.$form.addClass('oe_import_preview');
+ this.$('input.oe_import_advanced_mode').prop('checked', result.advanced_mode);
+ this.$('.oe_import_grid').html(QWeb.render('ImportView.preview', result));
+
+ this.$('.o_import_batch_alert').toggleClass('d-none', !result.batch);
+
+ var messages = [];
+ if (result.headers.length === 1) {
+ messages.push({type: 'warning', message: _t("A single column was found in the file, this often means the file separator is incorrect")});
+ }
+
+ if (!_.isEmpty(messages)) {
+ this.$('.oe_import_options').show();
+ this.onresults(null, null, null, {'messages': messages});
+ }
+
+ if (!_.isEqual(this.sheets, result.options.sheets)) {
+ this.sheets = result.options.sheets || [];
+ this.setup_sheets_picker();
+ }
+ this.$('div.oe_import_has_multiple_sheets').toggle(
+ this.sheets.length > 1
+ );
+
+ // merge option values back in case they were updated/guessed
+ _.each(['encoding', 'separator', 'float_thousand_separator', 'float_decimal_separator', 'sheet'], function (id) {
+ self.$('.oe_import_' + id).select2('val', result.options[id])
+ });
+ this.$('.oe_import_date_format').select2('val', time.strftime_to_moment_format(result.options.date_format));
+ this.$('.oe_import_datetime_format').val(time.strftime_to_moment_format(result.options.datetime_format));
+ // hide all "true debug" options when not in debug mode
+ this.$('.oe_import_debug_option').toggleClass('d-none', !result.debug);
+
+ var $fields = this.$('.oe_import_fields input');
+ this.render_fields_matches(result, $fields);
+ var data = this.generate_fields_completion(result);
+ var item_finder = function (id, items) {
+ items = items || data;
+ for (var i=0; i < items.length; ++i) {
+ var item = items[i];
+ if (item.id === id) {
+ return item;
+ }
+ var val;
+ if (item.children && (val = item_finder(id, item.children))) {
+ return val;
+ }
+ }
+ return '';
+ };
+ $fields.each(function (k,v) {
+ var filtered_data = self.generate_fields_completion(result, k);
+
+ var $thing = $();
+ var bind = function (d) {};
+ if (config.isDebug()) {
+ $thing = $(QWeb.render('ImportView.create_record_option')).insertAfter(v).hide();
+ bind = function (data) {
+ switch (data.type) {
+ case 'many2one': case 'many2many':
+ $thing.find('input').attr('field', data.id);
+ $thing.show();
+ break;
+ default:
+ $thing.find('input').attr('field', '').prop('checked', false);
+ $thing.hide();
+ }
+ }
+ }
+
+ $(v).select2({
+ allowClear: true,
+ minimumInputLength: 0,
+ data: filtered_data,
+ initSelection: function (element, callback) {
+ var default_value = element.val();
+ if (!default_value) {
+ callback('');
+ return;
+ }
+
+ var data = item_finder(default_value);
+ bind(data);
+ callback(data);
+ },
+ placeholder: _t('Don\'t import'),
+ width: 'resolve',
+ dropdownCssClass: 'oe_import_selector'
+ }).on('change', function (e) {
+ bind(item_finder(e.currentTarget.value));
+ });
+ });
+ },
+ generate_fields_completion: function (root, index) {
+ var self = this;
+ var basic = [];
+ var regulars = [];
+ var o2m = [];
+ var headers_type = root.headers_type;
+ function traverse(field, ancestors, collection, type) {
+ var subfields = field.fields;
+ var advanced_mode = self.$('input.oe_import_advanced_mode').prop('checked');
+ var field_path = ancestors.concat(field);
+ var label = _(field_path).pluck('string').join(' / ');
+ var id = _(field_path).pluck('name').join('/');
+ if (type === undefined || (type !== undefined && (type.indexOf('all') !== -1 || type.indexOf(field['type']) !== -1))){
+ // If non-relational, m2o or m2m, collection is regulars
+ if (!collection) {
+ if (field.name === 'id') {
+ collection = basic;
+ } else if (_.isEmpty(subfields)
+ || _.isEqual(_.pluck(subfields, 'name'), ['id', '.id'])) {
+ collection = regulars;
+ } else {
+ collection = o2m;
+ }
+ }
+
+ collection.push({
+ id: id,
+ text: label,
+ required: field.required,
+ type: field.type
+ });
+
+ }
+ if (advanced_mode){
+ for(var i=0, end=subfields.length; i<end; ++i) {
+ traverse(subfields[i], field_path, collection, type);
+ }
+ }
+ }
+ _(root.fields).each(function (field) {
+ if (index === undefined) {
+ traverse(field, []);
+ }
+ else {
+ if (self.$('input.oe_import_advanced_mode').prop('checked')){
+ traverse(field, [], undefined, ['all']);
+ }
+ else {
+ traverse(field, [], undefined, headers_type[index]);
+ }
+ }
+ });
+
+ var cmp = function (field1, field2) {
+ return field1.text.localeCompare(field2.text);
+
+ };
+ regulars.sort(cmp);
+ o2m.sort(cmp);
+ if (!_.isEmpty(regulars) && !_.isEmpty(o2m)){
+ basic = basic.concat([
+ { text: _t("Normal Fields"), children: regulars },
+ { text: _t("Relation Fields"), children: o2m },
+ ]);
+ }
+ else if (!_.isEmpty(regulars)) {
+ basic = basic.concat(regulars);
+ }
+ else if (!_.isEmpty(o2m)) {
+ basic = basic.concat(o2m);
+ }
+ return basic;
+ },
+ render_fields_matches: function (result, $fields) {
+ if (_(result.matches).isEmpty()) { return; }
+ $fields.each(function (index, input) {
+ var match = result.matches[index];
+ if (!match) { return; }
+
+ var current_field = result;
+ input.value = _(match).chain()
+ .map(function (name) {
+ // WARNING: does both mapping and folding (over the
+ // ``field`` iterator variable)
+ return current_field = _(current_field.fields).find(function (subfield) {
+ return subfield.name === name;
+ });
+ })
+ .pluck('name')
+ .value()
+ .join('/');
+ });
+ },
+
+ //- import itself
+ call_import: function (kwargs) {
+ var fields = this.$('.oe_import_fields input.oe_import_match_field').map(function (index, el) {
+ return $(el).select2('val') || false;
+ }).get();
+ var columns = this.$('.oe_import_grid-header .oe_import_grid-cell .o_import_header_name').map(function () {
+ return $(this).text().trim().toLowerCase() || false;
+ }).get();
+
+ var tracking_disable = 'tracking_disable' in kwargs ? kwargs.tracking_disable : !this.$('#oe_import_tracking').prop('checked')
+ delete kwargs.tracking_disable;
+ kwargs.context = _.extend(
+ {}, this.parent_context,
+ {tracking_disable: tracking_disable}
+ );
+ var self = this;
+ this.trigger_up('with_client', {callback: function () {
+ this.loading.ignore_events = true;
+ }});
+ $.blockUI({message: QWeb.render('Throbber')});
+ $(document.body).addClass('o_ui_blocked');
+ var opts = this.import_options();
+
+ var $el = $('.oe_throbber_message');
+ var msg = kwargs.dryrun ? _t("%d records tested...")
+ : _t("%d records successfully imported...");
+ opts.callback = function (count) {
+ $el.text(_.str.sprintf(msg, count));
+ };
+
+ return this._batchedImport(opts, [this.id, fields, columns], kwargs, {done: 0, prev: 0})
+ .then(null, function (reason) {
+ var error = reason.message;
+ var event = reason.event;
+ // In case of unexpected exception, convert
+ // "JSON-RPC error" to an import failure, and
+ // prevent default handling (warning dialog)
+ if (event) { event.preventDefault(); }
+
+ var msg;
+ var errordata = error.data || {};
+ if (errordata.type === 'xhrerror') {
+ var xhr = errordata.objects[0];
+ switch (xhr.status) {
+ case 504: // gateway timeout
+ msg = _t("Import timed out. Please retry. If you still encounter this issue, the file may be too big for the system's configuration, try to split it (import less records per file).");
+ break;
+ default:
+ msg = _t("An unknown issue occurred during import (possibly lost connection, data limit exceeded or memory limits exceeded). Please retry in case the issue is transient. If the issue still occurs, try to split the file rather than import it at once.");
+ }
+ } else {
+ msg = errordata.arguments && (errordata.arguments[1] || errordata.arguments[0])
+ || error.message;
+ }
+
+ return Promise.resolve({'messages': [{
+ type: 'error',
+ record: false,
+ message: msg,
+ }]});
+ }).finally(function () {
+ $(document.body).removeClass('o_ui_blocked');
+ $.unblockUI();
+ self.trigger_up('with_client', {callback: function () {
+ delete this.loading.ignore_events;
+ }});
+ });
+ }, /**
+ *
+ * @param opts import options
+ * @param args positional arguments to pass along (augmented with the options)
+ * @param kwargs keyword arguments to pass along (directly)
+ * @param {Object} rec recursion information record
+ * @param {Number} rec.done how many records have been loaded so far
+ * @param {Number} rec.prev nextrow of the previous call so we can know
+ * how many rows the call we're here performing
+ * will have consumed, and thus by how much we
+ * need to offset the messages of the *next* call
+ * @returns {Promise<{name, ids, messages}>}
+ * @private
+ */
+ _batchedImport: function (opts, args, kwargs, rec) {
+ opts.callback && opts.callback(rec.done || 0);
+ var self = this;
+ return this._rpc({
+ model: 'base_import.import',
+ method: 'do',
+ args: args.concat([opts]),
+ kwargs: kwargs
+ }).then(function (results) {
+ _.each(results.messages, offset_by(opts.skip));
+ if (!kwargs.dryrun && !results.ids) {
+ // update skip to failed batch
+ self.$('#oe_import_row_start').val(opts.skip + 1);
+ if (opts.skip) {
+ // there's been an error during a "proper" import, stop & warn
+ // about partial import maybe
+ results.messages.push({
+ type: 'info',
+ priority: true,
+ message: _.str.sprintf(_t("This file has been successfully imported up to line %d."), opts.skip)
+ });
+ }
+ return results;
+ }
+ if (!results.nextrow) {
+ // we're done
+ return results;
+ }
+
+ // do the next batch
+ return self._batchedImport(
+ // avoid modifying opts in-place
+ _.defaults({skip: results.nextrow}, opts),
+ args, kwargs, {
+ done: rec.done + (results.ids || []).length,
+ prev: results.nextrow
+ }
+ ).then(function (r2) {
+ return {
+ name: _.zip(results.name, r2.name).map(function (names) {
+ return names[0] || names[1];
+ }),
+ ids: (results.ids || []).concat(r2.ids || []),
+ messages: results.messages.concat(r2.messages),
+ skip: r2.skip || results.nextrow,
+ nextrow: r2.nextrow
+ }
+ });
+ });
+ },
+ onvalidate: function () {
+ var prom = this.call_import({ dryrun: true, tracking_disable: true });
+ prom.then(this.proxy('validated'));
+ return prom;
+ },
+ onimport: function () {
+ var self = this;
+ var prom = this.call_import({ dryrun: false });
+ prom.then(function (results) {
+ var message = results.messages;
+ if (!_.any(message, function (message) {
+ return message.type === 'error'; })) {
+ self['import_succeeded'](results);
+ return;
+ }
+ self['import_failed'](results);
+ });
+ return prom;
+ },
+ onimported: function (event, from, to, results) {
+ this.do_notify(false, _.str.sprintf(
+ _t("%d records successfully imported"),
+ results.ids.length
+ ));
+ this.exit();
+ },
+ exit: function () {
+ this.trigger_up('history_back');
+ },
+ onresults: function (event, from, to, results) {
+ var fields = this.$('.oe_import_fields input.oe_import_match_field').map(function (index, el) {
+ return $(el).select2('val') || false;
+ }).get();
+
+ var message = results.messages;
+ var no_messages = _.isEmpty(message);
+ if (no_messages) {
+ message.push({
+ type: 'info',
+ message: _t("Everything seems valid.")
+ });
+ } else if (event === 'import_failed' && results.ids) {
+ // both ids in a failed import -> partial import
+ this.toggle_partial(results);
+ }
+
+ // row indexes come back 0-indexed, spreadsheets
+ // display 1-indexed.
+ var offset = 1;
+ // offset more if header
+ if (this.import_options().headers) { offset += 1; }
+
+ var messagesSorted = _.sortBy(_(message).groupBy('message'), function (group) {
+ if (group[0].priority){
+ return -2;
+ }
+
+ // sort by gravity, then, order of field in list
+ var order = 0;
+ switch (group[0].type) {
+ case 'error': order = 0; break;
+ case 'warning': order = fields.length + 1; break;
+ case 'info': order = 2 * (fields.length + 1); break;
+ default: order = 3 * (fields.length + 1); break;
+ }
+ return order + _.indexOf(fields, group[0].field);
+ });
+
+ this.$form.addClass('oe_import_error');
+ this.$('.oe_import_error_report').html(
+ QWeb.render('ImportView.error', {
+ errors: messagesSorted,
+ at: function (rows) {
+ var from = rows.from + offset;
+ var to = rows.to + offset;
+ var rowName = '';
+ if (results.name.length > rows.from && results.name[rows.from] !== '') {
+ rowName = _.str.sprintf(' (%s)', results.name[rows.from]);
+ }
+ if (from === to) {
+ return _.str.sprintf(_t("at row %d%s"), from, rowName);
+ }
+ return _.str.sprintf(_t("between rows %d and %d"),
+ from, to);
+ },
+ at_multi: function (rows) {
+ var from = rows.from + offset;
+ var to = rows.to + offset;
+ var rowName = '';
+ if (results.name.length > rows.from && results.name[rows.from] !== '') {
+ rowName = _.str.sprintf(' (%s)', results.name[rows.from]);
+ }
+ if (from === to) {
+ return _.str.sprintf(_t("Row %d%s"), from, rowName);
+ }
+ return _.str.sprintf(_t("Between rows %d and %d"),
+ from, to);
+ },
+ at_multi_header: function (numberLines) {
+ return _.str.sprintf(_t("at %d different rows:"),
+ numberLines);
+ },
+ more: function (n) {
+ return _.str.sprintf(_t("(%d more)"), n);
+ },
+ info: function (msg) {
+ if (typeof msg === 'string') {
+ return _.str.sprintf(
+ '<div class="oe_import_moreinfo oe_import_moreinfo_message">%s</div>',
+ _.str.escapeHTML(msg));
+ }
+ if (msg instanceof Array) {
+ return _.str.sprintf(
+ '<div class="oe_import_moreinfo oe_import_moreinfo_choices"><a href="#" class="oe_import_report_see_possible_value oe_import_see_all"><i class="fa fa-arrow-right"/> %s </a><ul class="oe_import_report_more">%s</ul></div>',
+ _.str.escapeHTML(_t("See possible values")),
+ _(msg).map(function (msg) {
+ return '<li>'
+ + _.str.escapeHTML(msg)
+ + '</li>';
+ }).join(''));
+ }
+ // Final should be object, action descriptor
+ return [
+ '<div class="oe_import_moreinfo oe_import_moreinfo_action">',
+ _.str.sprintf('<a href="#" data-action="%s" class="oe_import_see_all"><i class="fa fa-arrow-right"/> ',
+ _.str.escapeHTML(JSON.stringify(msg))),
+ _.str.escapeHTML(
+ _t("See possible values")),
+ '</a>',
+ '</div>'
+ ].join('');
+ },
+ }));
+ },
+ toggle_partial: function (result) {
+ var $form = this.$('.oe_import');
+ var $partial_warning = this.$('.o_import_partial_alert');
+ var $partial_count = this.$('.o_import_partial_count');
+ if (result == null) {
+ $partial_warning.addClass('d-none');
+ $form.add(this.$buttons).removeClass('o_import_partial_mode');
+ var $skip = this.$('#oe_import_row_start');
+ $skip.val($skip.attr('value'));
+ $partial_count.text('');
+ return;
+ }
+
+ this.$('.o_import_batch_alert').addClass('d-none');
+ $partial_warning.removeClass('d-none');
+ $form.add(this.$buttons).addClass('o_import_partial_mode');
+ $partial_count.text((result.skip || 0) + 1);
+ }
+});
+core.action_registry.add('import', DataImport);
+
+// FSM-ize DataImport
+StateMachine.create({
+ target: DataImport.prototype,
+ events: [
+ { name: 'loaded_file',
+ from: ['none', 'file_loaded', 'preview_error', 'preview_success', 'results'],
+ to: 'file_loaded' },
+ { name: 'settings_changed',
+ from: ['file_loaded', 'preview_error', 'preview_success', 'results'],
+ to: 'previewing' },
+ { name: 'preview_failed', from: 'previewing', to: 'preview_error' },
+ { name: 'preview_succeeded', from: 'previewing', to: 'preview_success' },
+ { name: 'validate', from: 'preview_success', to: 'validating' },
+ { name: 'validate', from: 'results', to: 'validating' },
+ { name: 'validated', from: 'validating', to: 'results' },
+ { name: 'import', from: ['preview_success', 'results'], to: 'importing' },
+ { name: 'import_succeeded', from: 'importing', to: 'imported'},
+ { name: 'import_failed', from: 'importing', to: 'results' }
+ ],
+});
+
+Loading.include({
+ on_rpc_event: function () {
+ if (this.ignore_events) {
+ return
+ }
+ this._super.apply(this, arguments);
+ }
+});
+AbstractWebClient.prototype.custom_events['with_client'] = function (ev) {
+ ev.data.callback.call(this);
+};
+
+function offset_by(by) {
+ return function offset_message(msg) {
+ if (msg.rows) {
+ msg.rows.from += by;
+ msg.rows.to += by;
+ }
+ }
+}
+
+return {
+ DataImport: DataImport,
+};
+
+});
diff --git a/addons/base_import/static/src/js/import_menu.js b/addons/base_import/static/src/js/import_menu.js
new file mode 100644
index 00000000..e5442fac
--- /dev/null
+++ b/addons/base_import/static/src/js/import_menu.js
@@ -0,0 +1,63 @@
+odoo.define('base_import.ImportMenu', function (require) {
+ "use strict";
+
+ const DropdownMenuItem = require('web.DropdownMenuItem');
+ const FavoriteMenu = require('web.FavoriteMenu');
+ const { useModel } = require('web/static/src/js/model.js');
+
+ /**
+ * Import Records menu
+ *
+ * This component is used to import the records for particular model.
+ *
+ * @extends DropdownMenuItem
+ */
+ class ImportMenu extends DropdownMenuItem {
+ constructor() {
+ super(...arguments);
+ this.model = useModel('searchModel');
+ }
+
+ //---------------------------------------------------------------------
+ // Handlers
+ //---------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _onImportClick() {
+ const action = {
+ type: 'ir.actions.client',
+ tag: 'import',
+ params: {
+ model: this.model.config.modelName,
+ context: this.model.config.context,
+ }
+ };
+ this.trigger('do-action', {action: action});
+ }
+
+ //---------------------------------------------------------------------
+ // Static
+ //---------------------------------------------------------------------
+
+ /**
+ * @param {Object} env
+ * @returns {boolean}
+ */
+ static shouldBeDisplayed(env) {
+ return env.view &&
+ ['kanban', 'list'].includes(env.view.type) &&
+ !env.device.isMobile &&
+ !!JSON.parse(env.view.arch.attrs.import || '1') &&
+ !!JSON.parse(env.view.arch.attrs.create || '1');
+ }
+ }
+
+ ImportMenu.props = {};
+ ImportMenu.template = 'base_import.ImportMenu';
+
+ FavoriteMenu.registry.add('import-menu', ImportMenu, 1);
+
+ return ImportMenu;
+});
diff --git a/addons/base_import/static/src/scss/base_import.scss b/addons/base_import/static/src/scss/base_import.scss
new file mode 100644
index 00000000..7e737a71
--- /dev/null
+++ b/addons/base_import/static/src/scss/base_import.scss
@@ -0,0 +1,164 @@
+.oe_import {
+ @include o-webclient-padding($top: 8px);
+ overflow: auto;
+ position: absolute; // Needed for chrome
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ @include media-breakpoint-down(sm) {
+ position: static;
+ }
+
+ > p {
+ text-align: justify
+ }
+ h2 {
+ margin-top: 0.5em;
+ font-size: large; // override h2 font-size which is too large
+ }
+ .oe_padding {
+ padding: 13px 0;
+ }
+
+ .oe_import_box {
+ padding: 8px;
+ background: #F0EEEE;
+ border-radius: $border-radius;
+ border: solid 1px #dddddd;
+ label {
+ font-weight: normal;
+ }
+ .oe_import_file {
+ display: inline-block;
+ }
+ }
+
+ a.oe_import_toggle {
+ display: block;
+ &:before {
+ content: '+'
+ }
+ }
+ .oe_import_options {
+ margin-top: 8px;
+ p {
+ margin: 0;
+ }
+ label {
+ width: 48%;
+ line-height: 32px;
+ text-align: right;
+ }
+ }
+ /* ----------- INITIAL SETUP ------------ */
+ dd,
+ .oe_import_toggled,
+ .oe_import_grid,
+ .oe_import_error_report,
+ .oe_import_noheaders,
+ .oe_import_report_more {
+ display: none;
+ }
+
+ .oe_import_with_file label {
+ font-weight: normal;
+ }
+ .oe_import_debug_options {
+ max-width: 800px;
+ columns: 1;
+ @include media-breakpoint-up(md) {
+ columns: 2;
+ }
+ // try to keep the batch fields together, doesn't work on firefox &
+ // not sure how to do that (except by adding intermediate dom
+ // elements)
+ .oe_import_batch_limit {
+ break-before: column;
+ }
+ }
+
+ &.oe_import_preview .oe_import_grid {
+ display: table;
+ }
+ &.oe_import_error .oe_import_error_report,
+ &.oe_import_noheaders .oe_import_noheaders{
+ display: block;
+ }
+ .oe_import_report_showmore .oe_import_report_more {
+ display: list-item;
+ }
+
+ /* ------------- ERRORS AND WARNINGS REPORT ------------ */
+ .oe_import_error_report > ul {
+ padding: 0;
+ }
+ .oe_import_report {
+ list-style: none;
+ }
+ .alert {
+ padding: 0.50rem 1.25rem;
+ margin: 0.25rem 0;
+
+ a {
+ @extend .alert-link;
+ &:hover {opacity: 0.8;}
+ }
+
+ // alias -error to -danger
+ &.alert-error {
+ @extend .alert-danger;
+ }
+ &.text-error {
+ @extend .text-danger;
+ }
+ }
+
+ /* ------------- THE CSV TABLE ------------ */
+
+ $cell-max-width: 350px;
+ $cell-padding: 4px;
+ .oe_import_grid {
+ tr {
+ &.oe_import_grid-header:first-child {
+ line-height: 24px;
+ font-weight: normal;
+ }
+ .oe_import_grid-cell {
+ max-width: $cell-max-width;
+ padding: $cell-padding;
+ vertical-align: top;
+ .o_multi_line_text {
+ word-break: break-word;
+ }
+ .o_single_line_text {
+ @include o-text-overflow($display: table-cell, $max-width: $cell-max-width - $cell-padding);
+ }
+ }
+ }
+ }
+
+ /* Default Color for placeholder on import fields*/
+ .select2-default{
+ color: #F00 !important;
+ }
+}
+/* ------------- PARTIAL MODE buttons ------------ */
+// hide import in partial mode, resume otherwise
+.o_import_import_full.o_import_partial_mode,
+.o_import_import_partial:not(.o_import_partial_mode) {
+ display: none;
+}
+
+/* Field dropdown */
+.oe_import_selector {
+ font-size: $font-size-sm;
+ ul, li {
+ margin: 0; padding: 0;
+ }
+ width: 250px !important;
+}
+
+.o-list-buttons.o-editing .o_button_import {
+ display: none; // hidden for list view editable
+}
diff --git a/addons/base_import/static/src/xml/base_import.xml b/addons/base_import/static/src/xml/base_import.xml
new file mode 100644
index 00000000..4bee0203
--- /dev/null
+++ b/addons/base_import/static/src/xml/base_import.xml
@@ -0,0 +1,227 @@
+<templates>
+ <t t-name="base_import.ImportMenu" owl="1">
+ <li class="o_menu_item o_import_menu" role="menuitem">
+ <button type="button" class="dropdown-item" t-on-click="_onImportClick">
+ Import records
+ </button>
+ </li>
+ </t>
+
+ <t t-name="ImportView">
+ <t t-set="_id" t-value="_.uniqueId('export')"/>
+ <form action="" method="post" enctype="multipart/form-data" class="oe_import">
+ <input type="hidden" name="csrf_token" t-att-value="csrf_token"/>
+ <input type="hidden" name="import_id"/>
+ <div class="oe_import_box d-none">
+ <input accept=".csv, .xls, .xlsx, .xlsm, .ods" t-attf-id="file_#{_id}"
+ name="file" class="oe_import_file" type="file" style="display:none;"/>
+ <div class="oe_import_with_file row">
+ <a href="#" class="oe_import_toggle col-sm-12">
+ Formatting Options…</a>
+ <div class="row col-sm-12">
+ <div class="oe_import_toggled oe_import_options js_import_options col-md-6 col-lg-4">
+ <p t-foreach="widget.opts" t-as="option">
+ <!-- no @name, avoid submission when file_update called -->
+ <label t-attf-for="#{option.name}_#{_id}">
+ <t t-esc="option.label"/></label>
+ <input t-attf-id="#{option.name}_#{_id}"
+ t-attf-class="oe_import_#{option.name}"
+ style="width: 50%;"
+ t-att-value="option.value"/>
+ </p>
+ </div>
+ <div t-foreach="[widget.parse_opts_formats, widget.parse_opts_separators]" t-as="options" class="oe_import_toggled oe_import_options col-md-6 col-lg-4">
+ <p t-foreach="options" t-as="option">
+ <!-- no @name, avoid submission when file_update called -->
+ <label t-attf-for="#{option.name}_#{_id}">
+ <t t-esc="option.label"/></label>
+ <input t-attf-id="#{option.name}_#{_id}"
+ t-attf-class="oe_import_#{option.name}"
+ style="width: 50%;"
+ t-att-value="option.value"/>
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="o_import_batch_alert alert alert-warning d-none">
+ Due to its large size, the file will be imported by batches.
+ </div>
+ <div class="o_import_partial_alert alert alert-warning d-none">
+ Click 'Resume' to proceed with the import, resuming at line
+ <span class="o_import_partial_count">0</span>.<br/>
+ You can test or reload your file before resuming the import.
+ </div>
+
+ <div class="oe_import_with_file d-none">
+ <h2>Map your columns to import</h2>
+ <div class="oe_import_debug_options">
+ <div title="If the model uses openchatter, history tracking will set up subscriptions and send notifications during the import, but lead to a slower import." class="oe_import_debug_option">
+ <input type="checkbox" id="oe_import_tracking"/>
+ <label for="oe_import_tracking">
+ Track history during import
+ </label>
+ </div>
+
+ <div class="oe_import_has_multiple_sheets js_import_options">
+ <label for="oe_import_sheet">Selected Sheet:</label>
+ <input class="oe_import_sheet" id="oe_import_sheet"/>
+ </div>
+
+ <div>
+ <input type="checkbox" class="oe_import_has_header"
+ id="oe_import_has_header" checked="checked"/>
+ <label for="oe_import_has_header">The first row
+ contains the label of the column</label>
+ </div>
+ <div class="js_import_options oe_import_debug_option oe_import_batch_limit">
+ <label for="oe_import_batch_limit">Batch limit</label>
+ <input id="oe_import_batch_limit" value="2000"/>
+ </div>
+ <div class="js_import_options oe_import_debug_option" title="Warning: ignores the labels line, empty lines and
+ lines composed only of empty cells">
+ <label for="oe_import_row_start">Start at line</label>
+ <input id="oe_import_row_start" value="1"/>
+ </div>
+ <div class="oe_import_debug_option">
+ <input type="checkbox" class="oe_import_advanced_mode" checked="checked"
+ id="oe_import_advanced_mode"/>
+ <label for="oe_import_advanced_mode">Show fields of relation fields (advanced)</label>
+ </div>
+ </div>
+ <p class="oe_import_noheaders text-muted">If the file contains
+ the column names, Odoo can try auto-detecting the
+ field corresponding to the column. This makes imports
+ simpler especially when the file has many columns.</p>
+
+ <div class="oe_import_error_report"></div>
+ <div class="table-responsive">
+ <table class="table-striped table-bordered oe_import_grid bg-white" />
+ </div>
+ <h6 class="oe_padding">This is a preview of the first 10 rows of your file</h6>
+ </div>
+ <div class="o_view_nocontent">
+ <div class="o_nocontent_help">
+ <p class="o_view_nocontent_smiling_face">
+ Select a CSV or Excel file to import.
+ </p>
+ <p>
+ Excel files are recommended as fields formatting is automatic.
+ </p>
+ <div class="mt16 mb4">Need Help?</div>
+ <div t-foreach="widget.importTemplates" t-as="template">
+ <a t-att-href="template.template" aria-label="Download" title="Download">
+ <i class="fa fa-download"/> <span><t t-esc="template.label"/></span>
+ </a>
+ </div>
+ <a href="https://www.odoo.com/documentation/14.0/applications/general/base_import/import_faq.html" target="new">Import FAQ</a>
+ </div>
+ </div>
+ </form>
+ </t>
+
+ <t t-name="ImportView.buttons">
+ <button type="button" class="btn btn-primary o_import_import o_import_import_full d-none">Import</button>
+ <button type="button" class="btn btn-primary o_import_import o_import_import_partial d-none">Resume</button>
+ <button type="button" class="btn btn-secondary o_import_validate d-none">Test</button>
+ <button type="button" class="btn btn-primary oe_import_file">Load File</button>
+ <button type="button" class="btn btn-secondary o_import_cancel">Cancel</button>
+ </t>
+
+ <t t-name="ImportView.create_record_option">
+ <div class="mt4">
+ <label title="Creates new records if they can't be found (instead of failing to import). Note that the value in the column will be used as the new record's 'name', and assumes this is sufficient to create the record.">
+ <input type="checkbox" class="o_import_create_option"/>
+ Create if doesn't exist
+ </label>
+ </div>
+ </t>
+
+ <t t-name="ImportView.preview">
+ <thead>
+ <tr t-if="headers" class="oe_import_grid-header">
+ <td t-foreach="headers" t-as="header" class="oe_import_grid-cell"
+ ><span class="o_import_header_name o_single_line_text" t-att-title="header"><t t-esc="header"/></span></td>
+ </tr>
+ <tr class="oe_import_fields">
+ <!-- Iterate on first row to ensure we have all columns -->
+ <td t-foreach="preview[0]" t-as="column">
+ <input class="oe_import_match_field"/>
+ </td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr t-foreach="preview" t-as="row" class="oe_import_grid-row">
+ <td t-foreach="row" t-as="cell" class="oe_import_grid-cell">
+ <!-- content can be displayed on several lines if it contains whitespaces -->
+ <!-- in that case, we only display the 120 first characters -->
+ <!-- otherwise, we let the text-overflow: ellipsis do the job -->
+ <t t-set="multiline" t-value="cell.includes(' ')"/>
+ <t t-set="content" t-value="(multiline &amp;&amp; cell.length &gt; 120) ? (cell.substring(0, 120) + '...') : cell"/>
+ <span t-attf-class="#{multiline ? 'o_multi_line_text' : 'o_single_line_text'}">
+ <t t-esc="content"/>
+ </span>
+ </td>
+ </tr>
+ </tbody>
+ </t>
+ <t t-name="ImportView.preview.error">
+ <div class="oe_import_report alert alert-danger">
+ <p>Import preview failed due to: <t t-esc="error"/>.</p>
+ <p>For CSV files, you may need to select the correct separator.</p>
+ <p t-if="preview">Here is the start of the file we could not import:</p>
+ </div>
+ <pre t-if="preview"><t t-esc="preview"/></pre>
+ </t>
+ <ul t-name="ImportView.error">
+ <li t-foreach="errors" t-as="error"
+ t-attf-class="oe_import_report alert alert-#{error_value[0].type}">
+ <t t-if="error_value.length gt 1">
+ <t t-call="ImportView.error.multi.header">
+ <t t-set="error" t-value="error_value[0]"/>
+ <t t-set="error_length" t-value="error_value.length"/>
+ </t>
+ <ul>
+ <t t-foreach="error_value.length" t-as="index">
+ <li t-att-class="index gt 4 ? 'oe_import_report_more':''">
+ <t t-call="ImportView.error.multi.body">
+ <t t-set="error" t-value="error_value[index]"/>
+ <t t-set="index" t-value="index"/>
+ </t>
+ </li>
+ <li t-if="error_value.length gt 5 and index == 4" style="display: block;">
+ <a href="#" class="oe_import_report_count">
+ <t t-esc="more(error_value.length - 5)"/>
+ </a>
+ </li>
+ </t>
+ </ul>
+ <t t-if="error_value[0].moreinfo" t-raw="info(error_value[0].moreinfo)"/>
+
+ </t>
+ <t t-else="">
+ <t t-call="ImportView.error.single">
+ <t t-set="error" t-value="error_value[0]"/>
+ </t>
+ </t>
+ </li>
+ </ul>
+ <t t-name="ImportView.error.multi.header">
+ <span class="oe_import_report_message">
+ <t t-esc="error.message"/>
+ <t t-esc="at_multi_header(error_length)"/>
+ </span>
+ </t>
+ <t t-name="ImportView.error.multi.body">
+ <span class="oe_import_report_message" t-if="error.rows">
+ <t t-esc="at_multi(error.rows)"/>
+ </span>
+ </t>
+ <t t-name="ImportView.error.single">
+ <span class="oe_import_report_message">
+ <t t-esc="error.message"/>
+ </span>
+ <t t-if="error.rows" t-esc="at(error.rows)"/>
+ <t t-if="error.moreinfo" t-raw="info(error.moreinfo)"/>
+ </t>
+</templates>
diff --git a/addons/base_import/static/tests/import_buttons_tests.js b/addons/base_import/static/tests/import_buttons_tests.js
new file mode 100644
index 00000000..e561d021
--- /dev/null
+++ b/addons/base_import/static/tests/import_buttons_tests.js
@@ -0,0 +1,170 @@
+odoo.define('web.base_import_tests', function (require) {
+"use strict";
+
+const KanbanView = require('web.KanbanView');
+const ListView = require('web.ListView');
+const PivotView = require('web.PivotView');
+const testUtils = require('web.test_utils');
+
+const createView = testUtils.createView;
+
+QUnit.module('Base Import Tests', {
+ beforeEach: function () {
+ this.data = {
+ foo: {
+ fields: {
+ foo: {string: "Foo", type: "char"},
+ },
+ records: [
+ {id: 1, foo: "yop"},
+ ]
+ },
+ };
+ }
+});
+
+QUnit.test('import in favorite dropdown in list', async function (assert) {
+ assert.expect(2);
+
+ const list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree><field name="foo"/></tree>',
+ });
+
+ testUtils.mock.intercept(list, 'do_action', function () {
+ assert.ok(true, "should have triggered a do_action");
+ });
+
+ await testUtils.dom.click(list.$('.o_favorite_menu button'));
+ assert.containsOnce(list, '.o_import_menu');
+
+ await testUtils.dom.click(list.$('.o_import_menu button'));
+
+ list.destroy();
+});
+
+QUnit.test('import favorite dropdown item should not in list with create="0"', async function (assert) {
+ assert.expect(1);
+
+ const list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree create="0"><field name="foo"/></tree>',
+ });
+
+ await testUtils.dom.click(list.$('.o_favorite_menu button'));
+ assert.containsNone(list, '.o_import_menu');
+
+ list.destroy();
+});
+
+QUnit.test('import favorite dropdown item should not in list with import="0"', async function (assert) {
+ assert.expect(1);
+
+ const list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree import="0"><field name="foo"/></tree>',
+ });
+
+ await testUtils.dom.click(list.$('.o_favorite_menu button'));
+ assert.containsNone(list, '.o_import_menu');
+
+ list.destroy();
+});
+
+QUnit.test('import in favorite dropdown in kanban', async function (assert) {
+ assert.expect(2);
+
+ const kanban = await createView({
+ View: KanbanView,
+ model: 'foo',
+ data: this.data,
+ arch: `<kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="foo"/></div>
+ </t>
+ </templates>
+ </kanban>`,
+ });
+
+ testUtils.mock.intercept(kanban, 'do_action', function () {
+ assert.ok(true, "should have triggered a do_action");
+ });
+
+ await testUtils.dom.click(kanban.$('.o_favorite_menu button'));
+ assert.containsOnce(kanban, '.o_import_menu');
+
+ await testUtils.dom.click(kanban.$('.o_import_menu button'));
+
+ kanban.destroy();
+});
+
+QUnit.test('import favorite dropdown item should not in list with create="0"', async function (assert) {
+ assert.expect(1);
+
+ const kanban = await createView({
+ View: KanbanView,
+ model: 'foo',
+ data: this.data,
+ arch: `<kanban create="0">
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="foo"/></div>
+ </t>
+ </templates>
+ </kanban>`,
+ });
+
+ await testUtils.dom.click(kanban.$('.o_favorite_menu button'));
+ assert.containsNone(kanban, '.o_import_menu');
+
+ kanban.destroy();
+});
+
+QUnit.test('import dropdown favorite should not in kanban with import="0"', async function (assert) {
+ assert.expect(1);
+
+ const kanban = await createView({
+ View: KanbanView,
+ model: 'foo',
+ data: this.data,
+ arch: `<kanban import="0">
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="foo"/></div>
+ </t>
+ </templates>
+ </kanban>`,
+ });
+
+ await testUtils.dom.click(kanban.$('.o_favorite_menu button'));
+ assert.containsNone(kanban, '.o_import_menu');
+
+ kanban.destroy();
+});
+
+QUnit.test('import should not available in favorite dropdown in pivot (other than kanban or list)', async function (assert) {
+ assert.expect(1);
+
+ this.data.foo.fields.foobar = { string: "Fubar", type: "integer", group_operator: 'sum' };
+
+ const pivot = await createView({
+ View: PivotView,
+ model: 'foo',
+ data: this.data,
+ arch: '<pivot><field name="foobar" type="measure"/></pivot>',
+ });
+
+ await testUtils.dom.click(pivot.$('.o_favorite_menu button'));
+ assert.containsNone(pivot, '.o_import_menu');
+
+ pivot.destroy();
+});
+
+});