# -*- coding: utf-8 -*- # Copyright 2018 Fabien Bourgeois # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . """ Odoo Radicale Storage Plugin """ # # PLAN # 1. Implement readonly from Odoo only # 2. Implement contacts first (and so vcf) # 3. Implement unique events (.ics) # 4. Implement recurrent events # 5. Begin write (two way) for contacts # 5. Begin write (two way) for calendar from contextlib import contextmanager from time import strftime, strptime import vobject from odoorpc import ODOO from odoorpc.error import RPCError from radicale import xmlutils from radicale.storage import BaseCollection, Item, get_etag, get_uid_from_object from radicale_odoo_auth import Auth class Collection(BaseCollection): """ BaseCollection implementation for Odoo Radicale Storage """ odoo = False def __init__(self, path): """ Init function """ self.__class__.odoo = Auth.odoo if not self.__class__.odoo: self.logger.error('No auth Odoo found...') raise RuntimeError('No auth Odoo found') self.__class__.odoo_connect() # self.odoo_init() attributes = path.strip('/').split('/') self.tag = None self.props = {} if 'odoo-contact' in attributes: self.tag = 'VADDRESSBOOK' self.odoo_model = 'res.partner' self.content_suffix = '.vcf' self.props.update({'tag': 'VADDRESSBOOK', 'D:displayname': 'Odoo contacts', 'CR:addressbook-description': 'Contacts form your Odoo account'}) elif 'odoo-calendar' in attributes: self.tag = 'VCALENDAR' self.odoo_model = 'calendar.event' self.content_suffix = '.ics' self.props.update({'tag': 'VCALENDAR', 'D:displayname': 'Odoo calendar', 'C:calendar-description': 'Events form your Odoo calendar'}) self.path = path.strip('/') self.owner = attributes[0] self.is_principal = len(attributes) == 0 @classmethod def odoo_connect(cls): """ Global Odoo connection : server and admin account """ host = cls.configuration.get('storage', 'odoo_host', fallback='127.0.0.1') port = cls.configuration.get('storage', 'odoo_port', fallback=8069) admin = cls.configuration.get('storage', 'odoo_admin_username') password = cls.configuration.get('storage', 'odoo_admin_password') database = cls.configuration.get('storage', 'odoo_database') try: cls.odoo = ODOO(host, port=port) except RPCError as rpcerr: cls.logger.error(rpcerr) raise RuntimeError(rpcerr) try: cls.odoo.login(database, admin, password) cls.logger.info('Login successfull for {} on database {}'.format(admin, database)) except RPCError as rpcerr: cls.logger.error('Login problem for {} on database {}'.format(cls, database)) cls.logger.error(rpcerr) raise RuntimeError(rpcerr) return True def odoo_init(self): """ Init Odoo collections if not found """ # TODO: disallow collection deletion ? user_ids = self.odoo.env['res.users'].search([]) users = self.odoo.execute('res.users', 'read', user_ids, ['login', 'email']) for user in users: principal_path = user.get('login') self.logger.debug('Check collections from Odoo for %s' % principal_path) contact_path = '%s/odoo-contact' % principal_path calendar_path = '%s/odoo-calendar' % principal_path collections = self.discover(principal_path, depth='1') paths = [coll.path for coll in collections] if contact_path not in paths: props = {'tag': 'VADDRESSBOOK', 'D:displayname': 'Odoo contacts', 'C:calendar-description': 'Contacts form your Odoo account'} self.create_collection(contact_path, props=props) self.logger.info('Collection creation for Odoo Sync : %s' % contact_path) if calendar_path not in paths: props = {'tag': 'VCALENDAR', 'D:displayname': 'Odoo calendar', 'C:calendar-description': 'Events form your Odoo calendar'} self.create_collection(calendar_path, props=props) self.logger.info('Collection creation for Odoo Sync : %s' % calendar_path) @classmethod @contextmanager def acquire_lock(cls, mode, user=None): cls.user = user yield @classmethod def discover(cls, path, depth="0"): """Discover a list of collections under the given ``path``. ``path`` is sanitized. If ``depth`` is "0", only the actual object under ``path`` is returned. If ``depth`` is anything but "0", it is considered as "1" and direct children are included in the result. The root collection "/" must always exist. """ attributes = path.strip('/').split('/') or [] if path and not cls.user: cls.user = attributes[0] cls.logger.warning('Discover : %s (path), %s (depth), %s (cls.user), %s (attributes)' % (path, depth, cls.user, attributes)) yield cls(path) if len(attributes) == 1: # Got all if root is needed contact_path = '%s/odoo-contact' % path calendar_path = '%s/odoo-calendar' % path yield cls(contact_path) yield cls(calendar_path) def get_meta(self, key=None): """Get metadata value for collection """ if key: return self.props.get(key) else: return self.props @classmethod def create_collection(cls, href, collection=None, props=None): """ Create collection implementation : only warns ATM """ cls.logger.error('Attemmpt to create a new collection for %s' % href) @classmethod def get_contacts_from_odoo(cls, login): """ Gets all contacts available from one Odoo login """ cls.logger.info('Get contacts for Odoo user %s' % login) partner_ids = cls.odoo.env['res.partner'].search([]) cls.logger.debug(partner_ids) return ['res.partner:%s' % pid for pid in partner_ids] def list(self): """List collection items.""" # TODO : get all ICS from Odoo... self.logger.warning('List collection %s' % self.path) self.logger.warning('Collection tag %s' % self.tag) if self.tag: if self.tag == 'VADDRESSBOOK': for oid in self.__class__.get_contacts_from_odoo(self.owner): yield oid # for item in self.collection.list(): # yield item.uid + self.content_suffix def _get_with_metadata(self, href): """Fetch a single item from Odoo database""" model, database_id = href.split(':') fields = ['name', 'write_date', 'comment'] data = self.odoo.execute(model, 'read', [int(database_id)], fields)[0] self.logger.warning(data) if model == 'res.partner': # last_modified = strftime("%a, %d %b %Y %H:%M:%S GMT", # strptime(data.get('write_date'), '%Y-%m-%d %H:%M:%S')) last_modified = strftime("%Y-%m-%dT%H:%M:%SZ", strptime(data.get('write_date'), '%Y-%m-%d %H:%M:%S')) self.logger.warning(last_modified) vobject_item = vobject.vCard() vobject_item.add('n') vobject_item.n.value = vobject.vcard.Name(family=data.get('name')) vobject_item.add('fn') vobject_item.fn.value = data.get('name') vobject_item.add('uid').value = database_id vobject_item.add('rev').value = last_modified self.logger.warning(vobject_item.name) self.logger.warning([c for c in vobject_item.components()]) tag, start, end = xmlutils.find_tag_and_time_range(vobject_item) self.logger.warning('Tag, start, end : %s %s %s' % (tag, start, end)) text = vobject_item.serialize() etag = get_etag(text) uid = get_uid_from_object(vobject_item) self.logger.warning('Text, ETAG, UID : %s %s %s' % (text, etag, uid)) return Item( self, href=href, last_modified=last_modified, etag=etag, text=text, item=vobject_item, uid=href, name=vobject_item.name, component_name=tag), (tag, start, end) elif model == 'calendar.event': raise NotImplementedError else: raise NotImplementedError def get(self, href, verify_href=True): item, metadata = self._get_with_metadata(href) self.logger.warning(item) self.logger.warning(item.serialize()) self.logger.warning(item.name) self.logger.warning(item.last_modified) self.logger.warning([c for c in item.components()]) self.logger.warning(metadata) return item def delete(self, href=None): """ Can not delete collection but item, yes """ self.logger.warning(href) if href is None: # Delete the collection self.logger.error('Attempt to delete collection %s' % self.path) raise ValueError('Can not delete collection') else: # Delete an item raise NotImplementedError # def serialize(self): # """ Get the whole collection unicode """ # # TODO: CALENDAR # self.logger.warning('From serialize : %s' % self.tag) # if self.tag == "VADDRESSBOOK": # self.logger.warning('HERE') # self.logger.warning([item.serialize() for item in self.get_all()]) # return ''.join((item.serialize() for item in self.get_all())) # return '' @property def last_modified(self): """ Return last modified """ last = self.odoo.env[self.odoo_model].search([], limit=1, order='write_date desc') last_fields = self.odoo.execute(self.odoo_model, 'read', last, ['write_date'])[0] self.logger.info(last_fields) return strftime("%a, %d %b %Y %H:%M:%S GMT", strptime(last_fields.get('write_date'), '%Y-%m-%d %H:%M:%S'))