2018-05-04 12:43:21 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
# Copyright 2018 Fabien Bourgeois <fabien@yaltik.com>
|
|
|
|
#
|
|
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
""" Odoo Radicale Storage Plugin """
|
|
|
|
|
2018-05-09 18:24:12 +02:00
|
|
|
# # PLAN
|
2018-05-10 17:31:04 +02:00
|
|
|
# 1. OK : Implement readonly from Odoo only
|
|
|
|
# 2. OK : Implement contacts first (and so vcf)
|
|
|
|
# 3. OK : Implement unique events (.ics) and timezone
|
2018-05-11 16:15:56 +02:00
|
|
|
# 4. OK : Implement notifications for events
|
|
|
|
# 5. OK : Implement recurrent events
|
2018-05-11 16:50:51 +02:00
|
|
|
# 6. OK : Offer two (or more) calendar : own events, events where I'm attended
|
|
|
|
# and all readable events
|
|
|
|
# 7. Offer one calendar per user (via login/email)
|
|
|
|
# 8. Begin write (two way) for contacts
|
|
|
|
# 9. Begin write (two way) for calendar
|
2018-05-09 18:24:12 +02:00
|
|
|
|
|
|
|
|
2018-05-09 09:17:12 +02:00
|
|
|
from contextlib import contextmanager
|
2018-05-09 18:24:12 +02:00
|
|
|
from time import strftime, strptime
|
2018-05-13 09:01:01 +02:00
|
|
|
from datetime import timedelta, datetime
|
2018-05-10 17:31:04 +02:00
|
|
|
import pytz
|
2018-05-09 18:24:12 +02:00
|
|
|
import vobject
|
2018-05-09 09:17:12 +02:00
|
|
|
from odoorpc import ODOO
|
|
|
|
from odoorpc.error import RPCError
|
2018-05-09 18:24:12 +02:00
|
|
|
from radicale import xmlutils
|
|
|
|
from radicale.storage import BaseCollection, Item, get_etag, get_uid_from_object
|
|
|
|
from radicale_odoo_auth import Auth
|
2018-05-04 12:43:21 +02:00
|
|
|
|
|
|
|
|
2018-05-09 11:07:05 +02:00
|
|
|
class Collection(BaseCollection):
|
2018-05-04 12:43:21 +02:00
|
|
|
""" BaseCollection implementation for Odoo Radicale Storage """
|
2018-05-09 09:17:12 +02:00
|
|
|
|
|
|
|
odoo = False
|
|
|
|
|
2018-05-09 11:07:05 +02:00
|
|
|
def __init__(self, path):
|
2018-05-09 09:17:12 +02:00
|
|
|
""" Init function """
|
2018-05-09 18:24:12 +02:00
|
|
|
self.__class__.odoo = Auth.odoo
|
|
|
|
if not self.__class__.odoo:
|
|
|
|
self.logger.error('No auth Odoo found...')
|
|
|
|
raise RuntimeError('No auth Odoo found')
|
2018-05-10 17:31:04 +02:00
|
|
|
odoo_timezone = self.configuration.get('storage', 'odoo_timezone')
|
|
|
|
self.__class__.server_timezone = pytz.timezone(odoo_timezone)
|
2018-05-09 11:07:05 +02:00
|
|
|
|
|
|
|
attributes = path.strip('/').split('/')
|
|
|
|
self.tag = None
|
|
|
|
self.props = {}
|
|
|
|
if 'odoo-contact' in attributes:
|
|
|
|
self.tag = 'VADDRESSBOOK'
|
2018-05-09 18:24:12 +02:00
|
|
|
self.odoo_model = 'res.partner'
|
2018-05-09 11:07:05 +02:00
|
|
|
self.content_suffix = '.vcf'
|
|
|
|
self.props.update({'tag': 'VADDRESSBOOK',
|
|
|
|
'D:displayname': 'Odoo contacts',
|
|
|
|
'CR:addressbook-description': 'Contacts form your Odoo account'})
|
2018-05-11 16:50:51 +02:00
|
|
|
elif ('odoo-calendar-own' in attributes or
|
|
|
|
'odoo-calendar-in' in attributes or 'odoo-calendar-all' in attributes):
|
2018-05-09 11:07:05 +02:00
|
|
|
self.tag = 'VCALENDAR'
|
2018-05-09 18:24:12 +02:00
|
|
|
self.odoo_model = 'calendar.event'
|
2018-05-09 11:07:05 +02:00
|
|
|
self.content_suffix = '.ics'
|
2018-05-11 16:50:51 +02:00
|
|
|
self.props.update({'tag': 'VCALENDAR'})
|
|
|
|
if 'odoo-calendar-own' in attributes:
|
|
|
|
self.props.update({'D:displayname': 'Odoo calendar : own events',
|
|
|
|
'C:calendar-description': 'Own events, from your Odoo calendar'})
|
|
|
|
elif 'odoo-calendar-in' in attributes:
|
|
|
|
self.props.update({'D:displayname': 'Odoo calendar : events I\'m in',
|
|
|
|
'C:calendar-description': 'Events you are ' \
|
|
|
|
'attended from your Odoo calendar'})
|
|
|
|
else:
|
|
|
|
self.props.update({'D:displayname': 'Odoo calendar : all events',
|
|
|
|
'C:calendar-description': 'All events from your Odoo calendar'})
|
2018-05-09 11:07:05 +02:00
|
|
|
|
|
|
|
self.path = path.strip('/')
|
|
|
|
self.owner = attributes[0]
|
|
|
|
self.is_principal = len(attributes) == 0
|
2018-05-09 09:17:12 +02:00
|
|
|
|
2018-05-10 17:31:04 +02:00
|
|
|
@classmethod
|
2018-05-13 09:01:01 +02:00
|
|
|
def odoo_date_to_utc(cls, date_str):
|
2018-05-10 17:31:04 +02:00
|
|
|
""" Transform naive Odoo date object to UTC TZ """
|
2018-05-13 09:01:01 +02:00
|
|
|
date_obj = datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
|
2018-05-10 17:31:04 +02:00
|
|
|
local_date = cls.server_timezone.localize(date_obj, is_dst=None)
|
|
|
|
return local_date.astimezone(pytz.utc)
|
2018-05-09 11:07:05 +02:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
@contextmanager
|
|
|
|
def acquire_lock(cls, mode, user=None):
|
|
|
|
cls.user = user
|
|
|
|
yield
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def discover(cls, path, depth="0"):
|
2018-05-10 17:35:24 +02:00
|
|
|
"""Discover implementation """
|
2018-05-09 11:07:05 +02:00
|
|
|
attributes = path.strip('/').split('/') or []
|
|
|
|
if path and not cls.user:
|
|
|
|
cls.user = attributes[0]
|
|
|
|
|
2018-05-13 09:11:52 +02:00
|
|
|
cls.logger.debug('Discover : %s (path), %s (depth), %s (cls.user), %s (attributes)' %
|
|
|
|
(path, depth, cls.user, attributes))
|
2018-05-09 11:07:05 +02:00
|
|
|
yield cls(path)
|
2018-05-09 18:24:12 +02:00
|
|
|
if len(attributes) == 1: # Got all if root is needed
|
2018-05-10 11:38:28 +02:00
|
|
|
contact_path = '%sodoo-contact' % path
|
2018-05-11 16:50:51 +02:00
|
|
|
calendar_own_path = '%sodoo-calendar-own' % path
|
|
|
|
calendar_in_path = '%sodoo-calendar-in' % path
|
|
|
|
calendar_all_path = '%sodoo-calendar-all' % path
|
2018-05-09 18:24:12 +02:00
|
|
|
yield cls(contact_path)
|
2018-05-11 16:50:51 +02:00
|
|
|
yield cls(calendar_own_path)
|
|
|
|
yield cls(calendar_in_path)
|
|
|
|
yield cls(calendar_all_path)
|
2018-05-10 11:38:28 +02:00
|
|
|
elif len(attributes) == 2: # Then we need children
|
|
|
|
collection = cls(path)
|
|
|
|
for item in collection.list():
|
|
|
|
yield collection.get(item)
|
2018-05-09 11:07:05 +02:00
|
|
|
|
|
|
|
def get_meta(self, key=None):
|
|
|
|
"""Get metadata value for collection """
|
2018-05-09 18:24:12 +02:00
|
|
|
if key:
|
|
|
|
return self.props.get(key)
|
|
|
|
else:
|
|
|
|
return self.props
|
2018-05-09 11:07:05 +02:00
|
|
|
|
|
|
|
@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)
|
2018-05-09 18:24:12 +02:00
|
|
|
|
|
|
|
@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)
|
2018-05-12 07:57:29 +02:00
|
|
|
fields = ['name', 'write_date', 'comment', 'street', 'street2', 'zip',
|
|
|
|
'city', 'phone', 'mobile', 'fax', 'email', 'website',
|
|
|
|
'function', 'comment', 'image',
|
|
|
|
'category_id', 'parent_id', 'state_id', 'country_id']
|
2018-08-24 08:49:31 +02:00
|
|
|
domain = [('type', '=', 'contact')]
|
|
|
|
partners = cls.odoo.execute_kw('res.partner', 'search_read', [domain],
|
2018-05-12 07:57:29 +02:00
|
|
|
{'fields': fields})
|
|
|
|
cls.partners = {p['id']: p for p in partners}
|
|
|
|
categs = cls.odoo.execute_kw('res.partner.category', 'search_read', [[]],
|
|
|
|
{'fields': ['name']})
|
|
|
|
cls.categs = {c['id']: c['name'] for c in categs}
|
|
|
|
return ['res.partner:%s' % pid for pid in cls.partners.keys()]
|
2018-05-09 18:24:12 +02:00
|
|
|
|
2018-05-10 17:31:04 +02:00
|
|
|
@classmethod
|
2018-05-11 16:50:51 +02:00
|
|
|
def get_events_from_odoo(cls, login, path):
|
2018-05-10 17:31:04 +02:00
|
|
|
""" Gets all events available from one Odoo login """
|
|
|
|
cls.logger.info('Get events for Odoo user %s' % login)
|
2018-05-13 09:01:01 +02:00
|
|
|
fields = ['allday', 'start_date', 'stop_date', 'start_datetime',
|
|
|
|
'stop_datetime', 'write_date', 'name', 'location',
|
|
|
|
'description', 'recurrency', 'rrule', 'categ_ids', 'alarm_ids']
|
|
|
|
event_types = cls.odoo.execute_kw('calendar.event.type', 'search_read',
|
|
|
|
[[]], {'fields': ['name']})
|
|
|
|
cls.event_types = {et['id']: et['name'] for et in event_types}
|
|
|
|
event_alarms = cls.odoo.execute_kw('calendar.alarm', 'search_read',
|
|
|
|
[[]], {'fields': ['type',
|
|
|
|
'duration_minutes']})
|
|
|
|
cls.event_alarms = {ea['id']: ea for ea in event_alarms}
|
|
|
|
|
2018-05-11 16:15:56 +02:00
|
|
|
cls.odoo.env.context.update({'virtual_id': False}) # Only real events
|
2018-05-11 16:50:51 +02:00
|
|
|
if path.endswith('odoo-calendar-own'):
|
|
|
|
domain = [('user_id', '=', cls.odoo.env.uid)]
|
|
|
|
elif path.endswith('odoo-calendar-in'):
|
|
|
|
pid = cls.odoo.execute('res.users', 'read', [cls.odoo.env.uid],
|
|
|
|
['partner_id'])[0]['partner_id'][0]
|
|
|
|
domain = [('partner_ids', '=', pid)]
|
|
|
|
else:
|
|
|
|
domain = []
|
2018-05-13 09:11:37 +02:00
|
|
|
# Needed to ensure context
|
|
|
|
event_ids = cls.odoo.env['calendar.event'].search(domain)
|
|
|
|
events = cls.odoo.execute_kw('calendar.event', 'read',
|
|
|
|
[event_ids], {'fields': fields})
|
2018-05-13 09:01:01 +02:00
|
|
|
cls.events = {e['id']: e for e in events}
|
2018-05-11 16:15:56 +02:00
|
|
|
# WARNING: Odoo does not remove from database deleted recurrent events...
|
|
|
|
# Should be fixed on Odoo side and will be fixed via 2way here too
|
2018-05-13 09:01:01 +02:00
|
|
|
return ['calendar.event:%s' % eid for eid in cls.events.keys()]
|
2018-05-10 17:31:04 +02:00
|
|
|
|
2018-05-10 11:38:28 +02:00
|
|
|
def sync(self, old_token=None):
|
|
|
|
""" Debug purpose """
|
|
|
|
token, ilist = super(Collection, self).sync(old_token)
|
|
|
|
self.logger.debug('Sync token : %s' % token)
|
|
|
|
self.logger.debug('Sync list : %s' % ilist)
|
|
|
|
return token, ilist
|
|
|
|
|
2018-05-09 18:24:12 +02:00
|
|
|
def list(self):
|
|
|
|
"""List collection items."""
|
2018-05-10 11:38:28 +02:00
|
|
|
self.logger.debug('List collection %s' % self.path)
|
|
|
|
self.logger.debug('Collection tag %s' % self.tag)
|
2018-05-09 18:24:12 +02:00
|
|
|
if self.tag:
|
|
|
|
if self.tag == 'VADDRESSBOOK':
|
2018-05-10 17:31:04 +02:00
|
|
|
for oid in self.get_contacts_from_odoo(self.owner):
|
|
|
|
yield oid + self.content_suffix
|
|
|
|
elif self.tag == 'VCALENDAR':
|
2018-05-11 16:50:51 +02:00
|
|
|
for oid in self.get_events_from_odoo(self.owner, self.path):
|
2018-05-10 11:38:28 +02:00
|
|
|
yield oid + self.content_suffix
|
2018-05-10 17:31:04 +02:00
|
|
|
else:
|
|
|
|
raise NotImplementedError
|
2018-05-09 18:24:12 +02:00
|
|
|
|
2018-05-10 14:52:17 +02:00
|
|
|
@classmethod
|
2018-05-12 07:57:29 +02:00
|
|
|
def _generate_vcard_from_odoo(cls, database_id):
|
2018-05-10 14:52:17 +02:00
|
|
|
""" Generate and return vCard object from Odoo res.partner record """
|
|
|
|
# last_modified = strftime("%a, %d %b %Y %H:%M:%S GMT",
|
|
|
|
# strptime(data.get('write_date'), '%Y-%m-%d %H:%M:%S'))
|
2018-05-12 07:57:29 +02:00
|
|
|
partner = cls.partners.get(database_id)
|
|
|
|
last_modified = str(partner.get('write_date'))
|
2018-05-10 14:52:17 +02:00
|
|
|
vobject_item = vobject.vCard()
|
|
|
|
vobject_item.add('n')
|
2018-06-08 10:03:01 +02:00
|
|
|
vobject_item.n.value = vobject.vcard.Name(family=partner.get('name'))
|
2018-05-10 14:52:17 +02:00
|
|
|
vobject_item.add('fn')
|
2018-05-12 07:57:29 +02:00
|
|
|
vobject_item.fn.value = partner.get('name')
|
2018-05-10 14:52:17 +02:00
|
|
|
vobject_item.add('adr')
|
2018-05-12 07:57:29 +02:00
|
|
|
state_name = partner.get('state_id')[1] if partner.get('state_id') else ''
|
|
|
|
country_name = partner.get('country_id')[1] if partner.get('country_id') else ''
|
2018-05-10 14:52:17 +02:00
|
|
|
vobject_item.adr.value = vobject.vcard.Address(
|
2018-05-12 07:57:29 +02:00
|
|
|
street=' '.join([partner.get('street') or '', partner.get('street2') or '']),
|
|
|
|
code=partner.get('zip') or '', city=partner.get('city') or '',
|
2018-05-10 14:52:17 +02:00
|
|
|
region=state_name, country=country_name)
|
2018-05-12 07:57:29 +02:00
|
|
|
if partner.get('image'):
|
|
|
|
vobject_item.add('photo;encoding=b;type=jpeg').value = partner.get(
|
|
|
|
'image').replace('\n', '')
|
|
|
|
if partner.get('phone'):
|
2018-05-11 17:10:43 +02:00
|
|
|
tel = vobject_item.add('tel')
|
|
|
|
tel.type_param = 'Tel'
|
2018-05-12 07:57:29 +02:00
|
|
|
tel.value = partner.get('phone')
|
|
|
|
if partner.get('mobile'):
|
2018-05-11 17:10:43 +02:00
|
|
|
tel = vobject_item.add('tel')
|
|
|
|
tel.type_param = 'Mobile'
|
2018-05-12 07:57:29 +02:00
|
|
|
tel.value = partner.get('mobile')
|
|
|
|
if partner.get('fax'):
|
2018-05-11 17:10:43 +02:00
|
|
|
tel = vobject_item.add('tel')
|
|
|
|
tel.type_param = 'Fax'
|
2018-05-12 07:57:29 +02:00
|
|
|
tel.value = partner.get('fax')
|
|
|
|
if partner.get('email'):
|
|
|
|
vobject_item.add('email').value = partner.get('email')
|
|
|
|
if partner.get('website'):
|
|
|
|
vobject_item.add('url').value = partner.get('website')
|
|
|
|
if partner.get('function'):
|
|
|
|
vobject_item.add('role').value = partner.get('function')
|
|
|
|
if partner.get('comment'):
|
|
|
|
vobject_item.add('note').value = partner.get('comment')
|
|
|
|
if partner.get('category_id'):
|
|
|
|
categs = [cls.categs.get(cid) for cid in partner.get('category_id')]
|
2018-05-10 14:52:17 +02:00
|
|
|
vobject_item.add('categories').value = categs
|
2018-05-12 07:57:29 +02:00
|
|
|
if partner.get('parent_id'):
|
|
|
|
vobject_item.add('org').value = [partner.get('parent_id')[1]]
|
2018-05-10 14:52:17 +02:00
|
|
|
vobject_item.add('rev').value = last_modified
|
|
|
|
return vobject_item
|
|
|
|
|
2018-05-10 17:31:04 +02:00
|
|
|
@classmethod
|
2018-05-13 09:01:01 +02:00
|
|
|
def _generate_ics_from_odoo(cls, href, database_id):
|
2018-05-10 17:31:04 +02:00
|
|
|
""" Generate and return UCS object from Odoo calendar.event record """
|
2018-05-10 18:19:45 +02:00
|
|
|
# TODO/IMP : attendees management (not handled directly by vobject)
|
2018-05-13 09:01:01 +02:00
|
|
|
event = cls.events.get(database_id)
|
|
|
|
if event.get('allday'):
|
|
|
|
utc_dtstart = datetime.strptime(event.get('start_date'),
|
|
|
|
'%Y-%m-%d').date()
|
|
|
|
stop_date_obj = datetime.strptime(event.get('stop_date'),
|
|
|
|
'%Y-%m-%d').date()
|
|
|
|
utc_dtstop = stop_date_obj + timedelta(days=1)
|
2018-05-10 19:11:29 +02:00
|
|
|
else:
|
2018-05-13 09:01:01 +02:00
|
|
|
utc_dtstart = cls.odoo_date_to_utc(event.get('start_datetime'))
|
|
|
|
utc_dtstop = cls.odoo_date_to_utc(event.get('stop_datetime'))
|
|
|
|
last_modified = str(cls.odoo_date_to_utc(event.get('write_date')))
|
2018-05-10 17:31:04 +02:00
|
|
|
cal = vobject.iCalendar()
|
|
|
|
cal.add('vevent')
|
2018-05-13 09:01:01 +02:00
|
|
|
cal.vevent.add('summary').value = event.get('name')
|
|
|
|
cal.vevent.add('location').value = event.get('location') or ''
|
|
|
|
cal.vevent.add('description').value = event.get('description') or ''
|
2018-05-10 17:31:04 +02:00
|
|
|
cal.vevent.add('dtstart').value = utc_dtstart
|
|
|
|
cal.vevent.add('dtend').value = utc_dtstop
|
|
|
|
# cal.vevent.add('duration').value = event.duration
|
2018-05-13 09:01:01 +02:00
|
|
|
if event.get('categ_ids'):
|
|
|
|
categs = [cls.event_types.get(etid) for etid in event.get('categ_ids')]
|
2018-05-10 17:31:04 +02:00
|
|
|
cal.vevent.add('categories').value = categs
|
2018-05-13 09:01:01 +02:00
|
|
|
if event.get('alarm_ids'):
|
|
|
|
for alarm_id in event.get('alarm_ids'):
|
|
|
|
alarm = cls.event_alarms.get(alarm_id)
|
2018-05-10 18:19:45 +02:00
|
|
|
valarm = cal.vevent.add('valarm')
|
2018-05-13 09:01:01 +02:00
|
|
|
action = 'DISPLAY' if alarm.get('type') == 'notification' else 'EMAIL'
|
2018-05-10 18:19:45 +02:00
|
|
|
valarm.add('action').value = action
|
2018-05-13 09:01:01 +02:00
|
|
|
valarm.add('trigger').value = timedelta(
|
|
|
|
minutes=-alarm.get('duration_minutes'))
|
|
|
|
if event.get('recurrency'):
|
|
|
|
cal.vevent.add('rrule').value = event.get('rrule')
|
2018-05-10 17:31:04 +02:00
|
|
|
cal.vevent.add('uid').value = href
|
|
|
|
cal.vevent.add('rev').value = last_modified
|
|
|
|
cal.add('rev').value = last_modified
|
|
|
|
return cal
|
|
|
|
|
|
|
|
def _get_item_from_vobject(self, href, vobject_item):
|
|
|
|
""" Return Item from Vobject and HREF """
|
|
|
|
vobject_item.add('uid').value = href
|
|
|
|
tag, start, end = xmlutils.find_tag_and_time_range(vobject_item)
|
|
|
|
text = vobject_item.serialize()
|
|
|
|
etag = get_etag(text)
|
|
|
|
# uid = get_uid_from_object(vobject_item)
|
|
|
|
return Item(
|
|
|
|
self, href=href, last_modified=vobject_item.rev.value, etag=etag,
|
|
|
|
text=text, item=vobject_item, uid=href,
|
|
|
|
name=vobject_item.name, component_name=tag), (tag, start, end)
|
|
|
|
|
2018-05-09 18:24:12 +02:00
|
|
|
def _get_with_metadata(self, href):
|
|
|
|
"""Fetch a single item from Odoo database"""
|
|
|
|
model, database_id = href.split(':')
|
2018-05-10 11:38:28 +02:00
|
|
|
database_id = int(database_id[:-len(self.content_suffix)])
|
2018-05-09 18:24:12 +02:00
|
|
|
if model == 'res.partner':
|
2018-05-12 07:57:29 +02:00
|
|
|
vobject_item = self._generate_vcard_from_odoo(database_id)
|
2018-05-10 17:31:04 +02:00
|
|
|
return self._get_item_from_vobject(href, vobject_item)
|
2018-05-09 18:24:12 +02:00
|
|
|
elif model == 'calendar.event':
|
2018-05-13 09:01:01 +02:00
|
|
|
vobject_item = self._generate_ics_from_odoo(href, database_id)
|
2018-05-10 17:31:04 +02:00
|
|
|
return self._get_item_from_vobject(href, vobject_item)
|
2018-05-09 18:24:12 +02:00
|
|
|
else:
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def get(self, href, verify_href=True):
|
|
|
|
item, metadata = self._get_with_metadata(href)
|
|
|
|
return item
|
|
|
|
|
|
|
|
def delete(self, href=None):
|
|
|
|
""" Can not delete collection but item, yes """
|
|
|
|
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
|
|
|
|
|
|
|
|
@property
|
|
|
|
def last_modified(self):
|
|
|
|
""" Return last modified """
|
|
|
|
last = self.odoo.env[self.odoo_model].search([], limit=1, order='write_date desc')
|
2018-05-13 09:18:51 +02:00
|
|
|
if last:
|
|
|
|
last_fields = self.odoo.execute(self.odoo_model, 'read', last, ['write_date'])[0]
|
|
|
|
return str(last_fields['write_date'])
|
|
|
|
return '1970-01-01 00:00:00'
|