olaunch 0.7.15
Odoo tools for Odoo partners

# OLaunch
> Tools for Odoo partners and developers

## Scripts
#### Execute python script file inside Odoo with access to an Odoo environment
1. Install olaunch pip package
    ```bash
    > pip install olaunch
    ```
1. Create python script file `test.py`
   ```python
   users = env['res.users'].search([])
   ```
1. Execute python file in Odoo. Your code will have access to `env`
   ```bash
    > olunch_run_odoo_code [script.py]
   ```

## Dev-Tools
#### Run code snippets in PyCharm with debugger
1. Install olaunch pip package
    ```bash
    > pip install olaunch
    ```
1. Create python file or scratch file in IDE and run with debugger
    ```python
    import olaunch
    
    with olaunch.get_odoo_env(
        odoo_config_file='path-to-odoo-config/odoo.conf',
        db_name='database-nane',
    ) as env:
        users = env['res.users'].search([])
    ```

## Data import tools (ETL)
ETL tools to make importing data into Odoo easier. Extract, Transform, Load (ETL) is a way to help make importing and re-running data migrations simple, reliable and consistant

### Simple Example
````python
import olaunch
from olaunch.etl import task

olaunch.etl.import_data(
    datasource=olaunch.etl.datasources.Xlsx('data/contacts.xlsx'),
    model='res.partner',
    tasks=[
        task.Skip(lambda ctx: ctx.row['Is Supplier']),
        task.Field('active', True),
        task.Field("id", lambda ctx: "__import__.test_res_partner_{}".format(ctx.row['ID'])),
        task.Field('name', lambda ctx: ctx.row['Contact Name']),
        task.Field('customer_rank', lambda ctx: ctx.row['Is Customer'] == "Yes"),
        task.Field('phone', lambda ctx: ctx.row['Phone']),
    ],
)
````

### A more complex example
This example repeats for several files with each file having additional data loaded

```python
import uuid
import olaunch
from olaunch.etl import task


def my_custom_init_code(ctx):
   ctx.data['prep_data'] = {
      'test': 'This is loaded at the start of the import and is available to all rows/tasks',
   }


def my_custom_code(ctx):
   ctx.values['test'] = "Test data that is only for this row"
   ctx.data['test'] = "Test data that is shared between all rows and tasks"
   ctx.row['field_name'] = uuid.uuid4()


olaunch.etl.import_data(
   repeat_for_items=[
      {'filename': 'contact-1.xlsx', 'contact_type': 'customer'},
      {'filename': 'contact-2.xlsx', 'contact_type': 'supplier'},
   ],
   datasource=edi.datasources.Xlsx(
      filename=lambda ctx: ctx.item['filename'],
      skip_rows=3,
   ),
   model='res.partner',
   init=my_custom_init_code,
   tasks=[
      task.Exec(my_custom_code),
      task.Field('active', True),
      task.Value("id_ref", lambda ctx: "__import__.test_res_partner_{}_{}".format(
         ctx.item['contact_type'].lower(),
         ctx.row['ID'],
      )),
      task.Field('id', lambda ctx: ctx.values['id_ref']),
      task.Field('name', lambda ctx: '{} [{}]'.format(
         ctx.row['Contact Name'],
         ctx.item['contact_type'],
      )),
      task.Field('ref', lambda ctx: ctx.values['test']),
      task.Field('customer_rank', 1, condition=lambda ctx: ctx.item['contact_type'] == 'Customer'),
      task.Field('supplier_rank', 1, condition=lambda ctx: ctx.item['contact_type'] == 'Supplier'),
      task.Skip(lambda ctx: ctx.row['Is Supplier']),

      # Only run the following steps on customers
      task.Field('phone', lambda ctx: ctx.row['Phone']),
   ],
)
```

### Datasources
> Extensible way to load data into the data migration ETL system
#### File (Xlsx, Xls, Csv)
````python
import olaunch
olaunch.etl.import_data(
   datasource=olaunch.edi.datasources.Xlsx(
      filename='filename.xlsx',  # filename/path to load
      skip_rows=None,  # Number of rows to skip to find header row
   )
)
````
#### Custom datasource
To create a new datasource to use on etl jobs inherit the `Datasource` class and implement `load(ctx)`
```python
from olaunch.etl.datasources import Datasource

class MyDatasource(Datasource):
    # Load data
    def load(self, ctx):
        self.record_count = 2
        self.rows = [
            {'id': 1, 'name': 'Record 1'},
            {'id': 2, 'name': 'Record 2'},
        ]

    # Optional
    def get_rows(self):
        for row in self.rows:
            yield row

    
```

### Transformations
Perform any of the following transformation in the order they are specified on the etl job.

Note: Any of the parameters can be a value or they can be an expression that is evaluated.

e.g. `value=lambda ctx: ctx.row['name']` or `value="Contact Name"`

#### task.Field(name, value, condition=None)
   Set the field on the imported record to the value
   The value can be either a lambda ctx: expression or the value to write to the field

#### task.Value(name, value, condition=None)
   Save the value to the context to be used in other steps in the transformation
   The value can be either a lambda ctx: expression or the value to write to the field

#### task.Exec(code, condition=None)
   Execute code that has access to the context so far for the current row

#### task.Skip(condition=None)
   Stop executing any further steps in the transformation and move on to the next row from the datasource.
   Note: The current row will be imported

#### task.Abort(condition=None)
   Stop executing any further steps in the transformation and move on to the next row from the datasource.
   Note: The current row will *NOT* be imported