├── MANIFEST.IN ├── .gitignore ├── LICENSE ├── setup.py ├── djangodoo ├── auth.py ├── __init__.py ├── models.py └── fields.py └── README.rst /MANIFEST.IN: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | *~ 3 | *.pyc 4 | */migrations/* 5 | *.egg-info/* 6 | dist 7 | build/* 8 | !.gitignore 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 François Degrave 4 | University of Namur 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | from setuptools import setup 4 | 5 | with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: 6 | README = readme.read() 7 | 8 | # allow setup.py to be run from any path 9 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 10 | 11 | setup( 12 | name='djangodoo', 13 | version='0.2.5', 14 | packages=['djangodoo'], 15 | include_package_data=True, 16 | license='MIT License', 17 | description='A Django app to copy models, load and save records from a running Odoo instance', 18 | long_description=README, 19 | url='https://github.com/fdegrave/djangodoo', 20 | author='François Degrave', 21 | author_email='francois.degrave@unamur.be', 22 | classifiers=[ 23 | 'Environment :: Web Environment', 24 | 'Framework :: Django', 25 | 'Intended Audience :: Developers', 26 | 'License :: OSI Approved :: MIT License', 27 | 'Operating System :: OS Independent', 28 | 'Programming Language :: Python', 29 | 'Programming Language :: Python :: 2.7', 30 | 'Programming Language :: Python :: 3', 31 | 'Programming Language :: Python :: 3.2', 32 | 'Programming Language :: Python :: 3.3', 33 | 'Programming Language :: Python :: 3.4', 34 | 'Topic :: Internet :: WWW/HTTP', 35 | ], 36 | install_requires=[ 37 | "Django>=1.7.2", 38 | "ERPpeek>=1.6", 39 | "python-memcached" 40 | ], 41 | keywords=['Odoo', 'OpenERP', 'Django', ], 42 | ) 43 | -------------------------------------------------------------------------------- /djangodoo/auth.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth.models import User 3 | import erppeek 4 | from .models import OdooUser 5 | from django.core.cache import caches 6 | from django.db import transaction 7 | 8 | 9 | class OdooAuthBackend(object): 10 | 11 | """ 12 | Authenticate against the user in Odoo 13 | """ 14 | @transaction.atomic 15 | def authenticate(self, username=None, password=None): 16 | config = getattr(settings, "ODOO_HOST", False) 17 | try: 18 | odoo_client = erppeek.Client("%s:%d" % (config['HOST'], config['PORT']), db=config['DB'], 19 | user=username, password=password, verbose=False) 20 | except: 21 | return None 22 | 23 | caches["odoo_auth"].set('%s_credentials' % username, password, None) 24 | 25 | try: 26 | user = User.objects.get(username=username) 27 | odoo_user = user.odoo_user 28 | except User.DoesNotExist: 29 | # Create a new user. Note that we can set password 30 | # to anything, because it won't be checked; the password 31 | # from Odoo will. 32 | user = User(username=username, password='get from Odoo') 33 | user.is_staff = False 34 | user.is_superuser = False 35 | user.save() 36 | odoo_user = OdooUser(user=user) 37 | odoo_user.save() 38 | odoo_user.odoo_client = odoo_client 39 | return user 40 | 41 | def get_user(self, user_id): 42 | try: 43 | return User.objects.get(pk=user_id) 44 | except User.DoesNotExist: 45 | return None 46 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Djangodoo 3 | ========= 4 | 5 | Djangodoo allows you to copy models from Odoo to Django, load records from Odoo as well as modifying them. It also provides an authentication mechanism using the Odoo authentication. This app makes a wide use of the Erppeek library. 6 | 7 | Quick start 8 | ----------- 9 | 10 | 1. Add "Djangodoo" to your INSTALLED_APPS setting like this:: 11 | 12 | INSTALLED_APPS = ( 13 | ... 14 | 'djangodoo', 15 | ) 16 | 17 | 2. Include the Odoo host configuration in your project settings like this:: 18 | 19 | ODOO_HOST = { 20 | 'USER': 'username', 21 | 'PASSWORD': 'password', 22 | 'HOST': 'http://localhost', 23 | 'PORT': 8069, 24 | 'DB': 'dbname' 25 | } 26 | 27 | 3. [optional] Include the Odoo authentication backend in your project settings like this:: 28 | 29 | AUTHENTICATION_BACKENDS = ('djangodoo.auth.OdooAuthBackend') 30 | 31 | 4. Define a model like this:: 32 | 33 | from djangodoo.models import OdooModel 34 | 35 | class Partner(OdooModel): 36 | _odoo_model = "res.partner" 37 | _odoo_fields = ['name'] # optional; if omitted, all fields are copied 38 | _odoo_ignore_fields = None # optional; fields in this list are not copied 39 | 40 | 41 | OdooModel 42 | --------- 43 | 44 | *OdooModel* inherits from **django.db.models.Model**. You can thus do with an *OdooModel* anything you would do with a regular *django.db.models.Model*. However, *OdooModel* provides a number of additional features: 45 | 46 | 1. As stated in the "Quickstart" section, it allows you to provide the name of a model defined in Odoo as the value of the **_odoo_model** attribute. The fields of this latter model will be copied -- and "translated" -- into Django fields at runtime (and during the migration process, of course) (note that a *many2one* field from Odoo will be translated into a *ForeignKey* Django field only if the target model of this field is also copied into Django); 47 | 48 | 49 | 2. The **_odoo_fields** and **_odoo_ignore_fields** allow you to restrict the list of fields that are copied from the original Odoo model; 50 | 51 | 3. Several methods that ease the interactions with the Odoo server regarding the Odoo model under concern are provided: 52 | 53 | * odoo_load(*odoo_ids* [, *client*]): class method that loads records from Odoo, given their identifiers. 54 | * `odoo_ids` is a list of Odoo records identifiers (integers); 55 | * `client` is an instance of *erppeek.Client* that is used to load the data; if none is provided, the client is the one configured in the settings. 56 | 57 | * odoo_search(*domain*, *offset=0*, *limit=None*, *order=None*, *context=None* [, *client*]): class method that searches and loads records from Odoo, given a domain and a series of parameters for the *search* method in Odoo. 58 | 59 | * odoo_write(*objs*, *args* [, *client*]): class method that writes the values provided in `args` into the Odoo records originating the Django instances provided in `objs`. 60 | 61 | * odoo_push(*self*, *fieldnames=None* [, *client*]): method that saves a Django instance into Odoo. If the instance has an *odoo_id* then we call `write`, otherwise we call `create`; we only save the values of the fields indicated in `fieldnames`, or all of them if it is None. 62 | 63 | 64 | .. Authentication 65 | .. -------------- 66 | 67 | 68 | -------------------------------------------------------------------------------- /djangodoo/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.conf import settings 3 | from django.db.models.signals import class_prepared 4 | from django.core.mail import send_mail 5 | import erppeek 6 | from .fields import convert_field 7 | import logging 8 | 9 | from time import sleep 10 | import traceback 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | def set_auth_cache(): 16 | settings.CACHES = settings.CACHES or {} 17 | settings.CACHES["odoo_auth"] = {'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 18 | 'LOCATION': '127.0.0.1:18069'} 19 | 20 | 21 | def set_odoo_client(): 22 | config = getattr(settings, "ODOO_HOST", False) 23 | 24 | logger.info("Setting up the Odoo client...") 25 | max_retry_attempts = getattr(settings, "ODOO_MAX_RETRY_ATTEMPTS", 3) 26 | retry_delay = getattr(settings, "ODOO_RETRY_DELAY", 5) 27 | 28 | def _connect(retry_cnt): 29 | try: 30 | settings.odoo = erppeek.Client("%s:%d" % (config['HOST'], config['PORT']), db=config['DB'], 31 | user=config['USER'], password=config['PASSWORD'], verbose=False) 32 | settings.odoo.context = {"lang": settings.LANGUAGE_CODE} 33 | settings.odoo_models = {} 34 | settings.deferred_m2o = {} 35 | settings.deferred_o2m = {} 36 | except: 37 | logger.warn('Failed to connect to a running Odoo server.') 38 | logger.warn('Waiting {} [s] before the next attempt...'.format(retry_delay)) 39 | logger.warn('{} trials left...'.format(max_retry_attempts-retry_cnt)) 40 | sleep(retry_delay) 41 | if retry_cnt < max_retry_attempts: 42 | _connect(retry_cnt + 1) 43 | else: 44 | logger.error('Unable to connect to a running Odoo server. Aborting.') 45 | mail_config = getattr(settings, "ODOO_EMAIL_NOTIFICATION", False) 46 | mail_content = """Unable to connect to a running Odoo server. Your application may have failed to start up due to a connection problem with an Odoo instance. 47 | 48 | Djangodoo tried to reconnect {} times, waiting {} seconds between each attempt. Still, the server could not be reached. 49 | 50 | The problem occured with the following host configuration: 51 | 52 | USER: {} 53 | HOST: {} 54 | PORT: {} 55 | DB: {} 56 | 57 | And here is the traceback of the exception raised during the last attempt: 58 | 59 | 60 | {} 61 | 62 | """.format(max_retry_attempts, retry_delay, config['USER'], config['HOST'], config['PORT'], config['DB'], traceback.format_exc()) 63 | html_content = """

Unable to connect to a running Odoo server. Your application may have failed to start up due to a connection problem with an Odoo instance.

64 | 65 |

Djangodoo tried to reconnect {} times, waiting {} seconds between each attempt. Still, the server could not be reached.

66 | 67 |

The problem occured with the following host configuration:

68 | 69 |
70 | USER: {}
71 | HOST: {}
72 | PORT: {}
73 | DB: {}
74 |
75 | 76 |

And here is the traceback of the exception raised during the last attempt:

77 | 78 |
 79 | 
 80 |                 {}
 81 | 
 82 |                 
83 | 84 | """.format(max_retry_attempts, retry_delay, config['USER'], config['HOST'], config['PORT'], config['DB'], traceback.format_exc()) 85 | if mail_config: 86 | logger.info('Sending an email notification to the administrator...') 87 | send_mail("APPLICATION FAILURE - DJANGODOO", 88 | mail_content, 89 | getattr(settings, "DEFAULT_FROM_EMAIL", "djangodoo@example.com"), 90 | mail_config["RECIPIENTS"], 91 | html_message=html_content, 92 | fail_silently=False) 93 | raise 94 | 95 | _connect(0) 96 | 97 | 98 | def add_extra_model_fields(sender, **kwargs): 99 | """Dynamically add the fields by reading the fields of the original ODOO model 100 | 101 | The fields are "translated" by using the definitions in fields 102 | """ 103 | def add_field(django_model, field_details): 104 | odoo_field = convert_field(field_details) 105 | if odoo_field: 106 | field = odoo_field.to_django() 107 | field.contribute_to_class(django_model, field_details['name']) 108 | 109 | odoo = settings.odoo 110 | if getattr(sender, "_odoo_model", False): 111 | settings.odoo_models[sender._odoo_model] = sender 112 | _all_fields = odoo.model(sender._odoo_model).fields(sender._get_odoo_fields()) 113 | for fname, fdetails in _all_fields.items(): 114 | fdetails['name'] = fname 115 | fdetails['model'] = sender._odoo_model 116 | add_field(sender, fdetails) 117 | 118 | if sender._odoo_model in settings.deferred_m2o: 119 | for m2o_details in settings.deferred_m2o[sender._odoo_model]: 120 | origin = settings.odoo_models[m2o_details['model']] 121 | add_field(origin, m2o_details) 122 | settings.deferred_m2o[sender._odoo_model] = [] 123 | 124 | set_auth_cache() 125 | set_odoo_client() 126 | class_prepared.connect(add_extra_model_fields, dispatch_uid="FQFEQ#rfq3r") 127 | logger.info("Done initializing Djangodoo.") 128 | -------------------------------------------------------------------------------- /djangodoo/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.conf import settings 3 | from django.db import models 4 | from django.core.cache import caches 5 | import erppeek 6 | 7 | # TODO: traduction des DATA!!! 8 | # TODO: lazy loading des objets many2one 9 | 10 | 11 | class OdooModel(models.Model): 12 | 13 | """Model of a Odoo object copied in Django 14 | 15 | Attributes: 16 | _odoo_model: name of the Odoo model that will be copied in Django 17 | _odoo_fields: list of field names that will be copied from Odoo. If None, all field are copied. 18 | _odoo_ignore_fields: list of field names that will NOT be copied from Odoo 19 | """ 20 | 21 | _odoo_model = None 22 | _odoo_fields = None 23 | _odoo_ignore_fields = None 24 | 25 | odoo_id = models.IntegerField(unique=True) 26 | 27 | def __init__(self, *args, **kwargs): 28 | self.translation_cache = {} 29 | return super(OdooModel, self).__init__(*args, **kwargs) 30 | 31 | class Meta: 32 | abstract = True 33 | 34 | @classmethod 35 | def _get_odoo_fields(cls): 36 | res = cls._odoo_fields or settings.odoo.model(cls._odoo_model).fields() 37 | return [f for f in res if not(f in (cls._odoo_ignore_fields or []))] 38 | 39 | @classmethod 40 | def odoo_get_all_ids(cls, client=None): 41 | odoo_model = cls._odoo_model 42 | client = client or settings.odoo 43 | ans = client.model(odoo_model).keys() 44 | return ans 45 | 46 | @classmethod 47 | def odoo_load(cls, odoo_ids, client=None): 48 | """Loads records from Odoo 49 | 50 | Loads records from Odoo into Django instances given a list of Odoo identifiers *odoo_ids*. 51 | We read the data corresponding to the fields we need and convert it with respect 52 | to the type of field thanks to the methods defined in 'fields.py'. Each django field 53 | generated from a Odoo field contains a "odoo_field" attribute containing a "OdooField" 54 | instance. 55 | """ 56 | def update_or_create(args): 57 | try: 58 | obj = cls.objects.get(odoo_id=args["odoo_id"]) 59 | for (k, v) in args.items(): 60 | setattr(obj, k, v) 61 | except: 62 | obj = cls(**args) 63 | obj.save() 64 | return obj 65 | 66 | odoo_model = cls._odoo_model 67 | odoo_fields = cls._get_odoo_fields() 68 | client = client or settings.odoo 69 | records = client.model(odoo_model).read(odoo_ids, fields=odoo_fields, context=None) 70 | res = [] 71 | for rec in records: 72 | args = {} 73 | args["odoo_id"] = rec["id"] 74 | del rec["id"] 75 | for field in cls._meta.fields: 76 | if hasattr(field, "odoo_field") and field.name in rec: 77 | args[field.name] = field.odoo_field.convert_data(rec[field.name]) 78 | 79 | res.append(update_or_create(args)) 80 | return res 81 | 82 | @classmethod 83 | def odoo_search(cls, domain, offset=0, limit=None, order=None, context=None, client=None): 84 | """Search and load records from Odoo 85 | 86 | We load data from Odoo based on a domain filter 87 | """ 88 | client = client or settings.odoo 89 | odoo_ids = client.search(cls._odoo_model, domain, offset=offset, limit=limit, order=order, context=context) 90 | return cls.odoo_load(odoo_ids, client=client) if odoo_ids else [] 91 | 92 | @classmethod 93 | def odoo_write(cls, objs, args, client=None): 94 | """Writes in multiple records 95 | 96 | Writes the values provided in *args* into the Odoo records originating 97 | the Django instances provided in *objs* 98 | """ 99 | def convert(args): 100 | res = {} 101 | for field in cls._meta.fields: 102 | if field.name in args and hasattr(field, "odoo_field"): 103 | res[field.name] = field.odoo_field.convert_back(args[field.name]) 104 | return res 105 | 106 | client = client or settings.odoo 107 | odoo_model = cls._odoo_model 108 | odoo_ids = [o.odoo_id for o in objs if o.odoo_id] 109 | return client.model(odoo_model).write(odoo_ids, convert(args)) 110 | 111 | @classmethod 112 | def cache_translation(cls, lang): 113 | """ 114 | Récupère les traductions dans la langue `lang` des détails de tous les champs de l'objet 115 | Ces détails traduits sont stockés en cache dans chaque objet OdooField 116 | """ 117 | def convert_lang(lang): 118 | res = lang.replace("-", "_") 119 | if "_" in res: 120 | res = res[:3] + res[3:].upper() 121 | return res 122 | trans_fields = settings.odoo.execute(cls._odoo_model, 'fields_get', [], context={"lang": convert_lang(lang)}) 123 | for field in cls._meta.fields: 124 | if hasattr(field, "odoo_field") and trans_fields.get(field.name): 125 | field.odoo_field.translation_cache[lang] = trans_fields[field.name] 126 | 127 | def _convert_to_push(self, fieldnames=None): 128 | res = {} 129 | fieldnames = fieldnames or type(self)._get_odoo_fields() 130 | for field in type(self)._meta.fields: 131 | if hasattr(field, "odoo_field") and field.name in fieldnames: 132 | res[field.name] = field.odoo_field.convert_back(getattr(self, field.name)) 133 | return res 134 | 135 | def odoo_push(self, fieldnames=None, client=None): 136 | """Saves a Django instance into Odoo 137 | 138 | If the instance has an *odoo_id* then we call `write`, otherwise we call `create`; 139 | we only save the values of the fields indicated in `fieldnames`, or all 140 | of them if it is None. 141 | 142 | :todo: deal with one2many and many2many fields? 143 | """ 144 | odoo_model = type(self)._odoo_model 145 | client = client or settings.odoo 146 | args = self._convert_to_push(fieldnames) 147 | if self.odoo_id: 148 | client.model(odoo_model).write([self.odoo_id], args) 149 | return self.odoo_id 150 | else: 151 | return client.model(odoo_model).create(args) 152 | 153 | # def __getattr__(self, name): 154 | # """Redefine getattr in order to translate translatable fields 155 | # 156 | # Enables the translation of fields values based 157 | # """ 158 | # print("getattr -----------------------", self._odoo_model, self.odoo_id, name) 159 | # for field in type(self)._meta.fields: 160 | # if (hasattr(field, "odoo_field") and field.odoo_field.translatable and 161 | # field.name == name and self.odoo_id): 162 | # return settings.odoo.read(self._odoo_model, self.odoo_id, fields=name) 163 | # return super(OdooModel, self).__getitem__() 164 | 165 | 166 | class OdooUser(models.Model): 167 | user = models.OneToOneField(settings.AUTH_USER_MODEL, blank=False, related_name='odoo_user') 168 | 169 | def __init__(self, *args, **kwargs): 170 | config = getattr(settings, "ODOO_HOST", False) 171 | super(OdooUser, self).__init__(*args, **kwargs) 172 | passwd = kwargs.get('password') or caches["odoo_auth"].get('%s_credentials' % self.user.username) 173 | self.odoo_client = erppeek.Client("%s:%d" % (config['HOST'], config['PORT']), db=config['DB'], 174 | user=self.user.username, password=passwd, verbose=False) 175 | -------------------------------------------------------------------------------- /djangodoo/fields.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.conf import settings 3 | from django.db import models as djangomodels 4 | from django.utils import translation 5 | from django.utils.functional import lazy 6 | from django.utils import six 7 | import base64 8 | 9 | """ 10 | Translation methods 11 | """ 12 | 13 | 14 | def _get_details_in_lang(field, lang): 15 | if field.translation_cache.get(lang): 16 | return field.translation_cache[lang] 17 | else: 18 | settings.odoo_models[field.details['model']].cache_translation(lang) 19 | try: 20 | return field.translation_cache[lang] 21 | except: 22 | return field.details 23 | 24 | 25 | def field_translate(field, key): 26 | lang = translation.get_language() or "en-us" 27 | details = _get_details_in_lang(field, lang) 28 | res = details.get(key, "") 29 | if isinstance(res, six.binary_type): 30 | res = six.text_type(res) 31 | return res 32 | 33 | _ = lazy(field_translate, six.text_type) 34 | 35 | 36 | def selection_translate(field): 37 | def trans(val): 38 | lang = translation.get_language() or "en-us" 39 | details = _get_details_in_lang(field, lang) 40 | return dict(details['selection'])[val] 41 | 42 | trans_lazy = lazy(trans, six.text_type) 43 | 44 | res = [] 45 | for val, _label in field.details.get('selection'): 46 | res.append((val, trans_lazy(val))) 47 | return tuple(res) 48 | 49 | 50 | # TODO: default values 51 | # TODO: domains 52 | 53 | FIELDS_CONV = { 54 | "char": "CharField", 55 | "boolean": "BooleanField", 56 | "integer": "IntegerField", 57 | "text": "TextField", 58 | "float": "DecimalField", 59 | "date": "DateField", 60 | "datetime": "DateTimeField", 61 | "time": "TimeField", 62 | "binary": "BinaryField", 63 | "selection": "CharField", 64 | "many2one": "ForeignKey", 65 | "one2many": None, 66 | # "many2many": "", 67 | # "function": "", 68 | # "related": "", 69 | } 70 | 71 | 72 | class OdooField(object): 73 | 74 | def __init__(self, details): 75 | self.details = details 76 | self.translatable = details.get("translate") 77 | self.django_field = False 78 | self.translation_cache = {} # translations cache 79 | return super(OdooField, self).__init__() 80 | 81 | def to_django(self, **kwargs): 82 | kwargs.update({ 83 | "verbose_name": _(self, 'string'), 84 | "help_text": _(self, 'help'), 85 | "blank": not(self.details.get("required")), 86 | "editable": not(self.details.get("readonly")), 87 | }) 88 | django_field = getattr(djangomodels, FIELDS_CONV[self.details["type"]])(**kwargs) 89 | django_field.odoo_field = self 90 | self.django_field = django_field 91 | return django_field 92 | 93 | def convert_data(self, data): 94 | return data or None 95 | 96 | def convert_back(self, data): 97 | return data or False 98 | 99 | 100 | class TextField(OdooField): 101 | 102 | def to_django(self, **kwargs): 103 | if self.details.get("required"): 104 | kwargs["default"] = "" 105 | kwargs["null"] = not(self.details.get("required")) 106 | return super(TextField, self).to_django(**kwargs) 107 | 108 | 109 | class CharField(TextField): 110 | 111 | def to_django(self, **kwargs): 112 | kwargs['max_length'] = self.details.get('size') or 512 113 | return super(CharField, self).to_django(**kwargs) 114 | 115 | 116 | class BooleanField(OdooField): 117 | 118 | def to_django(self, **kwargs): 119 | kwargs["default"] = False 120 | return super(BooleanField, self).to_django(**kwargs) 121 | 122 | def convert_data(self, data): 123 | return data or False 124 | 125 | 126 | class IntegerField(OdooField): 127 | 128 | def to_django(self, **kwargs): 129 | if self.details.get("required"): 130 | kwargs["default"] = 0 131 | kwargs["null"] = not(self.details.get("required")) 132 | return super(IntegerField, self).to_django(**kwargs) 133 | 134 | 135 | class FloatField(IntegerField): 136 | 137 | def to_django(self, **kwargs): 138 | if self.details.get("digits"): 139 | kwargs["max_digits"] = self.details["digits"][0] 140 | kwargs["decimal_places"] = self.details["digits"][1] 141 | kwargs["null"] = not(self.details.get("required")) 142 | return super(FloatField, self).to_django(**kwargs) 143 | 144 | 145 | class DateField(OdooField): 146 | 147 | def to_django(self, **kwargs): 148 | kwargs["null"] = not(self.details.get("required")) 149 | if self.details.get("required"): 150 | kwargs["auto_now_add"] = True 151 | return super(DateField, self).to_django(**kwargs) 152 | 153 | 154 | class DateTimeField(DateField): 155 | pass 156 | 157 | 158 | class TimeField(DateField): 159 | pass 160 | 161 | 162 | class BinaryField(OdooField): 163 | 164 | def to_django(self, **kwargs): 165 | kwargs["null"] = not(self.details.get("required")) 166 | return super(BinaryField, self).to_django(**kwargs) 167 | 168 | def convert_data(self, data): 169 | """Odoo data is a b64-encoded string""" 170 | return base64.b64decode(data) if data else None 171 | 172 | def convert_back(self, data): 173 | return base64.b64encode(data).decode("utf-8") if data else False 174 | 175 | 176 | class SelectionField(CharField): 177 | 178 | def to_django(self, **kwargs): 179 | kwargs["choices"] = selection_translate(self) 180 | return super(SelectionField, self).to_django(**kwargs) 181 | 182 | 183 | class Many2OneField(OdooField): 184 | 185 | """ 186 | If the model identified by details['relation'] exists in django, then we can create the field directly. 187 | Otherwise, we delay the field creation until the possible creation of this model. 188 | """ 189 | 190 | def __new__(cls, details): 191 | if details['relation'] in settings.odoo_models: 192 | return OdooField.__new__(cls) 193 | else: 194 | settings.deferred_m2o[details['relation']] = settings.deferred_m2o.get(details['relation'], []) 195 | settings.deferred_m2o[details['relation']].append(details) 196 | return None 197 | 198 | def to_django(self, **kwargs): 199 | kwargs["null"] = not(self.details.get("required")) 200 | if self.details['relation'] == self.details['model']: 201 | kwargs["to"] = "self" 202 | else: 203 | to_model = settings.odoo_models[self.details['relation']] 204 | kwargs["to"] = to_model 205 | return super(Many2OneField, self).to_django(**kwargs) 206 | 207 | def convert_data(self, data): 208 | """ 209 | Odoo data is a pair (id, label) 210 | We look for objects in the target model an instance having a odoo_id equal to the first 211 | element of the pair ; if not found, we load it from Odoo 212 | 213 | :param (tuple or False) data: the value to convert 214 | :return (OdooModel or False): the object instance linked to this m2o field 215 | """ 216 | if data and isinstance(data, (list, tuple)) and len(data) == 2: 217 | to_model = settings.odoo_models[self.details['relation']] 218 | targets = to_model.objects.filter(odoo_id=data[0]) 219 | if targets: 220 | return targets[0] 221 | else: 222 | return to_model.odoo_load([data[0]])[0] 223 | return data or None 224 | 225 | def convert_back(self, data): 226 | """ 227 | Django data is either None or a Django instance 228 | We tranform it into False or an integer by getting the odoo_id on the instance. 229 | 230 | :todo: if the target objet has no odoo_id, first push it to odoo 231 | :param (OdooModel or False) data: the value to convert 232 | :return (integer or False): the idi of the object in odoo 233 | """ 234 | from .models import OdooModel 235 | if data and isinstance(data, OdooModel) and hasattr(data, 'odoo_id'): 236 | return data.odoo_id 237 | # elif isinstance(data, (int, long)): 238 | # return data 239 | else: 240 | return False 241 | 242 | 243 | class One2ManyField(OdooField): 244 | 245 | """ 246 | There is no one2many field in Django, so we simply set the "relation_field" 247 | attribute of the foreignKey field encoding the opposite relationship so it bares 248 | the name of this one2many field 249 | """ 250 | def __new__(cls, details): 251 | if details['relation'] in settings.odoo_models: 252 | relation = settings.odoo_models[details['relation']] 253 | for field in relation._meta.Fields: 254 | if field.name == details['relation_field']: 255 | field.related_name = details['name'] 256 | else: 257 | settings.deferred_o2m[details['relation']] = settings.deferred_o2m.get(details['relation'], []) 258 | settings.deferred_o2m[details['relation']].append(details) 259 | 260 | 261 | def convert_field(details): 262 | if not(details['type'] in FIELDS_CONV): 263 | return None 264 | return eval(details["type"].title() + "Field")(details) 265 | --------------------------------------------------------------------------------