diff --git a/currency_rate_update/model/currency_rate_update.py b/currency_rate_update/model/currency_rate_update.py index 91419dfdc..e49677161 100644 --- a/currency_rate_update/model/currency_rate_update.py +++ b/currency_rate_update/model/currency_rate_update.py @@ -1,22 +1,8 @@ # -*- coding: utf-8 -*- ############################################################################## # -# Copyright (c) 2009 Camptocamp SA -# @source JBA and AWST inpiration -# @contributor Grzegorz Grzelak (grzegorz.grzelak@birdglobe.com), -# Joel Grand-Guillaume -# Copyright (c) 2010 Alexis de Lattre (alexis@via.ecp.fr) -# - ported XML-based webservices (Admin.ch, ECB, PL NBP) to new XML lib -# - rates given by ECB webservice is now correct even when main_cur <> EUR -# - rates given by PL_NBP webs. is now correct even when main_cur <> PLN -# - if company_currency <> CHF, you can now update CHF via Admin.ch -# (same for EUR with ECB webservice and PLN with NBP webservice) -# For more details, see Launchpad bug #645263 -# - mecanism to check if rates given by the webservice are "fresh" -# enough to be written in OpenERP -# ('max_delta_days' parameter for each currency update service) -# Ported to OpenERP 7.0 by Lorenzo Battistini -# +# Copyright (c) 2009 CamptoCamp. All rights reserved. +# @author Nicolas Bessi # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -33,767 +19,285 @@ # ############################################################################## -# TODO "nice to have" : restrain the list of currencies that can be added for -# a webservice to the list of currencies supported by the Webservice -# TODO : implement max_delta_days for Yahoo webservice - import logging -import time -from datetime import datetime, timedelta -from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT -from openerp.tools import DEFAULT_SERVER_DATE_FORMAT -from openerp.osv import fields, osv, orm -from openerp.tools.translate import _ + +from datetime import datetime +from dateutil.relativedelta import relativedelta + +from openerp import models, fields, api, _ +from openerp.exceptions import except_orm, Warning + +from ..services.currency_getter import Currency_getter_factory _logger = logging.getLogger(__name__) +_intervalTypes = { + 'days': lambda interval: relativedelta(days=interval), + 'weeks': lambda interval: relativedelta(days=7*interval), + 'months': lambda interval: relativedelta(months=interval), +} -class Currency_rate_update_service(osv.Model): - """Class that tells for wich services wich currencies have to be updated +supported_currency_array = [ + "AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", + "BAM", "BBD", "BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BRL", + "BSD", "BTN", "BWP", "BYR", "BZD", "CAD", "CDF", "CHF", "CLP", "CNY", + "COP", "CRC", "CUP", "CVE", "CYP", "CZK", "DJF", "DKK", "DOP", "DZD", + "EEK", "EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP", "GEL", "GGP", + "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD", "HKD", "HNL", "HRK", "HTG", + "HUF", "IDR", "ILS", "IMP", "INR", "IQD", "IRR", "ISK", "JEP", "JMD", + "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KYD", + "KZT", "LAK", "LBP", "LKR", "LRD", "LSL", "LTL", "LVL", "LYD", "MAD", + "MDL", "MGA", "MKD", "MMK", "MNT", "MOP", "MRO", "MTL", "MUR", "MVR", + "MWK", "MXN", "MYR", "MZN", "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", + "OMR", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG", "QAR", "RON", + "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", + "SLL", "SOS", "SPL", "SRD", "STD", "SVC", "SYP", "SZL", "THB", "TJS", + "TMM", "TND", "TOP", "TRY", "TTD", "TVD", "TWD", "TZS", "UAH", "UGX", + "USD", "UYU", "UZS", "VEB", "VEF", "VND", "VUV", "WST", "XAF", "XAG", + "XAU", "XCD", "XDR", "XOF", "XPD", "XPF", "XPT", "YER", "ZAR", "ZMK", + "ZWD" +] - """ +RO_BNR_supported_currency_array = ["AED", "AUD", "BGN", "BRL", "CAD", "CHF", + "CNY", "CZK", "DKK", "EGP", "EUR", "GBP", "HUF", "INR", "JPY", "KRW", + "MDL", "MXN", "NOK", "NZD", "PLN", "RON", "RSD", "RUB", "SEK", "TRY", + "UAH", "USD", "XAU", "XDR", "ZAR"] + +CA_BOC_supported_currency_array = ["AED", "ANG", "ARS", "AUD", "BOC", "BRL", + "BSD", "CHF", "CLP", "CNY", "COP", "CZK", "DKK", "EUR", "FJD", "GBP", + "GHS", "GTQ", "HKD", "HNL", "HRK", "HUF", "IDR", "ILS", "INR", "ISK", + "JMD", "JPY", "KRW", "LKR", "MAD", "MMK", "MXN", "MYR", "NOK", "NZD", + "PAB", "PEN", "PHP", "PKR", "PLN", "RON", "RSD", "RUB", "SEK", "SGD", + "THB", "TND", "TRY", "TTD", "TWD", "USD", "VEF", "VND", "XAF", "XCD", + "XPF", "ZAR"] + +CH_ADMIN_supported_currency_array = ['AED', 'ALL', 'ARS', 'AUD', 'AZN', 'BAM', + 'BDT', 'BGN', 'BHD', 'BRL', 'CAD', 'CHF', 'CLP', 'CNY', 'COP', 'CRC', + 'CZK', 'DKK', 'DOP', 'EGP', 'ETB', 'EUR', 'GBP', 'GTQ', 'HKD', 'HNL', + 'HRK', 'HUF', 'IDR', 'ILS', 'INR', 'ISK', 'JPY', 'KES', 'KHR', 'KRW', + 'KWD', 'KYD', 'KZT', 'LBP', 'LKR', 'LTL', 'LVL', 'LYD', 'MAD', 'MUR', + 'MXN', 'MYR', 'NGN', 'NOK', 'NZD', 'OMR', 'PAB', 'PEN', 'PHP', 'PKR', + 'PLN', 'QAR', 'RON', 'RSD', 'RUB', 'SAR', 'SEK', 'SGD', 'THB', 'TND', + 'TRY', 'TWD', 'TZS', 'UAH', 'USD', 'UYU', 'VEF', 'VND', 'ZAR'] + +ECB_supported_currency_array = ['AUD', 'BGN', 'BRL', 'CAD', 'CHF', 'CNY', + 'CZK', 'DKK', 'GBP', 'HKD', 'HRK', 'HUF', 'IDR', 'ILS', 'INR', 'JPY', + 'KRW', 'LTL', 'MXN', 'MYR', 'NOK', 'NZD', 'PHP', 'PLN', 'RON', 'RUB', + 'SEK', 'SGD', 'THB', 'TRY', 'USD', 'ZAR'] + +MX_BdM_supported_currency_array = ["ARS", "AUD", "BBD", "BMD", "BOB", "BRL", + "BSD", "BZD", "CAD", "CHF", "CLP", "CNH", "CNY", "COP", "CRC", "CUP", + "CZK", "DKK", "DOP", "DZD", "EGP", "ESD", "EUR", "FJD", "GBP", "GTQ", + "GYD", "HKD", "HNL", "HUF", "IDR", "ILS", "INR", "IQD", "JMD", "JPY", + "KES", "KRW", "KWD", "MAD", "MYR", "NGN", "NIC", "NOK", "NZD", "PAB", + "PEN", "PHP", "PLN", "PYG", "RON", "RUB", "SAR", "SEK", "SGD", "SVC", + "THB", "TRY", "TTD", "TWD", "UAH", "USD", "USD", "UYP", "VEF", "VND", + "ZAR"] + +PL_NBP_supported_currency_array = ['AUD', 'BGN', 'BRL', 'CAD', 'CHF', 'CLP', + 'CNY', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', 'HRK', 'HUF', 'IDR', 'ILS', + 'INR', 'ISK', 'JPY', 'KRW', 'LTL', 'MXN', 'MYR', 'NOK', 'NZD', 'PHP', + 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'THB', 'TRY', 'UAH', 'USD', 'XDR', + 'ZAR'] + + +class Currency_rate_update_service(models.Model): + """Class keep services and currencies that + have to be updated""" _name = "currency.rate.update.service" _description = "Currency Rate Update" - _columns = { - # List of webservicies the value sould be a class name - 'service': fields.selection( - [ - ('Admin_ch_getter', 'Admin.ch'), + + @api.one + @api.constrains('max_delta_days') + def _check_max_delta_days(self): + if self.max_delta_days < 0: + raise Warning(_('Max delta days must be >= 0')) + + @api.one + @api.constrains('interval_number') + def _check_interval_number(self): + if self.interval_number < 0: + raise Warning(_('Interval number must be >= 0')) + + @api.onchange('interval_number') + def _onchange_interval_number(self): + if self.interval_number == 0: + self.note = "%s Service deactivated. Currencies will no longer be updated. \n%s" % ( + fields.Datetime.now(), self.note and self.note or '') + + @api.onchange('service') + def _onchange_service(self): + currency_list = '' + if self.service: + currencies = [] + currency_list = supported_currency_array + company_id = False + if self.company_id.multi_company_currency_enable: + company_id = self.company_id.id + if self.service == 'ECB_getter': + currency_list = ECB_supported_currency_array + if self.service == 'RO_BNR_getter': + currency_list = RO_BNR_supported_currency_array + if self.service == 'CA_BOC_getter': + currency_list = CA_BOC_supported_currency_array + if self.service == 'CH_ADMIN_getter': + currency_list = CH_ADMIN_supported_currency_array + if self.service == 'MX_BdM_getter': + currency_list = MX_BdM_supported_currency_array + if self.service == 'PL_NBP_getter': + currency_list = PL_NBP_supported_currency_array + if company_id: + currencies = self.env['res.currency'].search( + [('name', 'in', currency_list), + '|', ('company_id', '=', company_id), + ('company_id', '=', False)]) + else: + currencies = self.env['res.currency'].search( + [('name', 'in', currency_list), + ('company_id', '=', False)]) + self.currency_list = [(6, 0, [curr.id for curr in currencies])] + + # List of webservicies the value sould be a class name + service = fields.Selection([ + ('CH_ADMIN_getter', 'Admin.ch'), ('ECB_getter', 'European Central Bank'), - ('Yahoo_getter', 'Yahoo Finance '), + ('YAHOO_getter', 'Yahoo Finance'), # Added for polish rates ('PL_NBP_getter', 'Narodowy Bank Polski'), # Added for mexican rates - ('Banxico_getter', 'Banco de México'), + ('MX_BdM_getter', 'Banco de México'), # Bank of Canada is using RSS-CB # http://www.cbwiki.net/wiki/index.php/Specification_1.1 # This RSS format is used by other national banks # (Thailand, Malaysia, Mexico...) ('CA_BOC_getter', 'Bank of Canada - noon rates'), - ], - "Webservice to use", - required=True - ), - # List of currency to update - 'currency_to_update': fields.many2many( - 'res.currency', - 'res_curreny_auto_udate_rel', - 'service_id', - 'currency_id', - 'currency to update with this service', - ), - # Back ref - 'company_id': fields.many2one( - 'res.company', - 'linked company', - ), - # Note fileds that will be used as a logger - 'note': fields.text('update notice'), - 'max_delta_days': fields.integer( - 'Max delta days', - required=True, - help="If the time delta between the " - "rate date given by the webservice and " - "the current date exeeds this value, " - "then the currency rate is not updated in OpenERP." - ), - } - _defaults = {'max_delta_days': lambda *a: 4} - _sql_constraints = [ - ( - 'curr_service_unique', - 'unique (service, company_id)', - _('You can use a service one time per company !') - ) - ] + # Added for romanian rates + ('RO_BNR_getter', 'National Bank of Romania') + ], + "Webservice to use", + required=True) + # List of currencies available on webservice + currency_list = fields.Many2many('res.currency', + 'res_currency_update_avail_rel', + 'service_id', + 'currency_id', + 'Currencies available') + # List of currency to update + currency_to_update = fields.Many2many( + 'res.currency', + 'res_currency_auto_update_rel', + 'service_id', + 'currency_id', + 'Currencies to update with this service') + # Link with company + company_id = fields.Many2one('res.company', 'Linked Company') + # Note fileds that will be used as a logger + note = fields.Text('Update notice') + max_delta_days = fields.Integer('Max delta days', + default=4, required=True, + help="If the time delta between the rate date given by the webservice " + "and the current date exceeds this value, then the currency rate is not" + " updated in OpenERP.") + interval_type = fields.Selection([ + ('days', 'Day(s)'), + ('weeks', 'Week(s)'), + ('months', 'Month(s)')], + string='Currency update frequency', + default='days') + interval_number = fields.Integer('Frequency', default=1) + next_run = fields.Date('Next run on', default=fields.Date.today()) - def _check_max_delta_days(self, cr, uid, ids): - for company in self.read(cr, uid, ids, ['max_delta_days']): - if company['max_delta_days'] >= 0: - continue - else: - return False - return True + _sql_constraints = [('curr_service_unique', 'unique (service, company_id)', + _('You can use a service only one time per company !'))] - _constraints = [ - (_check_max_delta_days, - "'Max delta days' must be >= 0", - ['max_delta_days']), - ] - - -class Currency_rate_update(osv.Model): - """Class that handle an ir cron call who will - update currencies based on a web url""" - _name = "currency.rate.update" - _description = "Currency Rate Update" - # Dict that represent a cron object - nextcall_time = datetime.today() + timedelta(days=1) - nextcall = nextcall_time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) - cron = { - 'active': False, - 'priority': 1, - 'interval_number': 1, - 'interval_type': 'weeks', - 'nextcall': nextcall, - 'numbercall': -1, - 'doall': True, - 'model': 'currency.rate.update', - 'function': 'run_currency_update', - 'args': '()', - } - - LOG_NAME = 'cron-rates' - MOD_NAME = 'currency_rate_update: ' - - def get_cron_id(self, cr, uid, context): - """Returns the updater cron's id. - Create one if the cron does not exists - """ - - cron_id = 0 - cron_obj = self.pool.get('ir.cron') - try: - # Finds the cron that send messages - cron_id = cron_obj.search( - cr, - uid, - [ - ('function', 'ilike', self.cron['function']), - ('model', 'ilike', self.cron['model']) - ], - context={ - 'active_test': False - } - ) - cron_id = int(cron_id[0]) - except Exception: - _logger.info('warning cron not found one will be created') - # Ignore if the cron is missing cause we are - # going to create it in db - pass - if not cron_id: - self.cron['name'] = _('Currency Rate Update') - cron_id = cron_obj.create(cr, uid, self.cron, context) - return cron_id - - def save_cron(self, cr, uid, datas, context=None): - """save the cron config data should be a dict""" - cron_id = self.get_cron_id(cr, uid, context) - return self.pool.get('ir.cron').write(cr, uid, [cron_id], datas) - - def run_currency_update(self, cr, uid): - "update currency at the given frequence" + @api.one + def refresh_currency(self): + """Refresh the currencies rates !!for all companies now""" + self.ensure_one() factory = Currency_getter_factory() - curr_obj = self.pool.get('res.currency') - rate_obj = self.pool.get('res.currency.rate') - companies = self.pool.get('res.company').search(cr, uid, []) - for comp in self.pool.get('res.company').browse(cr, uid, companies): - # The multi company currency can beset or no so we handle - # The two case - if not comp.auto_currency_up: - continue - # We fetch the main currency looking for currency with base = true. - # The main rate should be set at 1.00 - main_curr_ids = curr_obj.search( - cr, uid, - [('base', '=', True), ('company_id', '=', comp.id)] - ) - if not main_curr_ids: + curr_obj = self.env['res.currency'] + rate_obj = self.env['res.currency.rate'] + company = self.company_id + # The multi company currency can be set or no so we handle + # The two case + if company.auto_currency_up: + main_currencies = curr_obj.search( + [('base', '=', True), ('company_id', '=', company.id)]) + if not main_currencies: # If we can not find a base currency for this company # we look for one with no company set - main_curr_ids = curr_obj.search( - cr, uid, - [('base', '=', True), ('company_id', '=', False)] - ) - if main_curr_ids: - main_curr_rec = curr_obj.browse(cr, uid, main_curr_ids[0]) + main_currencies = curr_obj.search( + [('base', '=', True), ('company_id', '=', False)]) + if main_currencies: + main_curr = main_currencies[0] else: - raise orm.except_orm( - _('Error!'), - ('There is no base currency set!') - ) - if main_curr_rec.rate != 1: - raise orm.except_orm( - _('Error!'), - ('Base currency rate should be 1.00!') - ) - main_curr = main_curr_rec.name - for service in comp.services_to_use: - note = service.note or '' - try: - # We initalize the class that will handle the request - # and return a dict of rate - getter = factory.register(service.service) - curr_to_fetch = map(lambda x: x.name, - service.currency_to_update) - res, log_info = getter.get_updated_currency( - curr_to_fetch, - main_curr, - service.max_delta_days + raise Warning(_('There is no base currency set!')) + if main_curr.rate != 1: + raise Warning(_('Base currency rate should be 1.00!')) + note = self.note or '' + try: + # We initalize the class that will handle the request + # and return a dict of rate + getter = factory.register(self.service) + curr_to_fetch = map(lambda x: x.name, + self.currency_to_update) + res, log_info = getter.get_updated_currency( + curr_to_fetch, + main_curr.name, + self.max_delta_days ) - rate_name = time.strftime(DEFAULT_SERVER_DATE_FORMAT) - for curr in service.currency_to_update: - if curr.name == main_curr: - continue - do_create = True - for rate in curr.rate_ids: - if rate.name == rate_name: - rate.write({'rate': res[curr.name]}) - do_create = False - break - if do_create: - vals = { - 'currency_id': curr.id, - 'rate': res[curr.name], - 'name': rate_name - } - rate_obj.create( - cr, - uid, - vals, - ) - - # Show the most recent note at the top - msg = "%s \n%s currency updated. %s" % ( - log_info or '', - datetime.today().strftime( - DEFAULT_SERVER_DATETIME_FORMAT - ), - note - ) - service.write({'note': msg}) - except Exception as exc: - error_msg = "\n%s ERROR : %s %s" % ( - datetime.today().strftime( - DEFAULT_SERVER_DATETIME_FORMAT - ), - repr(exc), - note - ) - _logger.info(repr(exc)) - service.write({'note': error_msg}) - - -class AbstractClassError(Exception): - def __str__(self): - return 'Abstract Class' - - def __repr__(self): - return 'Abstract Class' - - -class AbstractMethodError(Exception): - def __str__(self): - return 'Abstract Method' - - def __repr__(self): - return 'Abstract Method' - - -class UnknowClassError(Exception): - def __str__(self): - return 'Unknown Class' - - def __repr__(self): - return 'Unknown Class' - - -class UnsuportedCurrencyError(Exception): - def __init__(self, value): - self.curr = value - - def __str__(self): - return 'Unsupported currency %s' % self.curr - - def __repr__(self): - return 'Unsupported currency %s' % self.curr - - -class Currency_getter_factory(): - """Factory pattern class that will return - a currency getter class base on the name passed - to the register method - - """ - def register(self, class_name): - allowed = [ - 'Admin_ch_getter', - 'PL_NBP_getter', - 'ECB_getter', - 'NYFB_getter', - 'Google_getter', - 'Yahoo_getter', - 'Banxico_getter', - 'CA_BOC_getter', - ] - if class_name in allowed: - class_def = eval(class_name) - return class_def() - else: - raise UnknowClassError - - -class Curreny_getter_interface(object): - "Abstract class of currency getter" - - log_info = " " - - supported_currency_array = [ - 'AED', 'AFN', 'ALL', 'AMD', 'ANG', 'AOA', 'ARS', 'AUD', 'AWG', 'AZN', - 'BAM', 'BBD', 'BDT', 'BGN', 'BHD', 'BIF', 'BMD', 'BND', 'BOB', 'BRL', - 'BSD', 'BTN', 'BWP', 'BYR', 'BZD', 'CAD', 'CDF', 'CHF', 'CLP', 'CNY', - 'COP', 'CRC', 'CUP', 'CVE', 'CYP', 'CZK', 'DJF', 'DKK', 'DOP', 'DZD', - 'EEK', 'EGP', 'ERN', 'ETB', 'EUR', 'FJD', 'FKP', 'GBP', 'GEL', 'GGP', - 'GHS', 'GIP', 'GMD', 'GNF', 'GTQ', 'GYD', 'HKD', 'HNL', 'HRK', 'HTG', - 'HUF', 'IDR', 'ILS', 'IMP', 'INR', 'IQD', 'IRR', 'ISK', 'JEP', 'JMD', - 'JOD', 'JPY', 'KES', 'KGS', 'KHR', 'KMF', 'KPW', 'KRW', 'KWD', 'KYD', - 'KZT', 'LAK', 'LBP', 'LKR', 'LRD', 'LSL', 'LTL', 'LVL', 'LYD', 'MAD', - 'MDL', 'MGA', 'MKD', 'MMK', 'MNT', 'MOP', 'MRO', 'MTL', 'MUR', 'MVR', - 'MWK', 'MXN', 'MYR', 'MZN', 'NAD', 'NGN', 'NIO', 'NOK', 'NPR', 'NZD', - 'OMR', 'PAB', 'PEN', 'PGK', 'PHP', 'PKR', 'PLN', 'PYG', 'QAR', 'RON', - 'RSD', 'RUB', 'RWF', 'SAR', 'SBD', 'SCR', 'SDG', 'SEK', 'SGD', 'SHP', - 'SLL', 'SOS', 'SPL', 'SRD', 'STD', 'SVC', 'SYP', 'SZL', 'THB', 'TJS', - 'TMM', 'TND', 'TOP', 'TRY', 'TTD', 'TVD', 'TWD', 'TZS', 'UAH', 'UGX', - 'USD', 'UYU', 'UZS', 'VEB', 'VEF', 'VND', 'VUV', 'WST', 'XAF', 'XAG', - 'XAU', 'XCD', 'XDR', 'XOF', 'XPD', 'XPF', 'XPT', 'YER', 'ZAR', 'ZMK', - 'ZWD' - ] - - # Updated currency this arry will contain the final result - updated_currency = {} - - def get_updated_currency(self, currency_array, main_currency, - max_delta_days): - """Interface method that will retrieve the currency - This function has to be reinplemented in child - """ - raise AbstractMethodError - - def validate_cur(self, currency): - """Validate if the currency to update is supported""" - if currency not in self.supported_currency_array: - raise UnsuportedCurrencyError(currency) - - def get_url(self, url): - """Return a string of a get url query""" - try: - import urllib - objfile = urllib.urlopen(url) - rawfile = objfile.read() - objfile.close() - return rawfile - except ImportError: - raise osv.except_osv( - 'Error !', - self.MOD_NAME + 'Unable to import urllib !' - ) - except IOError: - raise osv.except_osv( - 'Error !', - self.MOD_NAME + 'Web Service does not exist !' - ) - - def check_rate_date(self, rate_date, max_delta_days): - """Check date constrains. rate_date must be of datetime type""" - days_delta = (datetime.today() - rate_date).days - if days_delta > max_delta_days: - raise Exception( - 'The rate timestamp (%s) is %d days away from today, ' - 'which is over the limit (%d days). ' - 'Rate not updated in OpenERP.' % (rate_date, - days_delta, - max_delta_days) - ) - - # We always have a warning when rate_date != today - rate_date_str = datetime.strftime(rate_date, - DEFAULT_SERVER_DATE_FORMAT) - if rate_date.date() != datetime.today().date(): - msg = "The rate timestamp (%s) is not today's date" - self.log_info = ("WARNING : %s %s") % (msg, rate_date_str) - _logger.warning(msg, rate_date_str) - - -# Yahoo ####################################################################### -class Yahoo_getter(Curreny_getter_interface): - """Implementation of Currency_getter_factory interface - for Yahoo finance service - """ - - def get_updated_currency(self, currency_array, main_currency, - max_delta_days): - """implementation of abstract method of curreny_getter_interface""" - self.validate_cur(main_currency) - url = ('http://download.finance.yahoo.com/d/' - 'quotes.txt?s="%s"=X&f=sl1c1abg') - if main_currency in currency_array: - currency_array.remove(main_currency) - for curr in currency_array: - self.validate_cur(curr) - res = self.get_url(url % (main_currency + curr)) - val = res.split(',')[1] - if val: - self.updated_currency[curr] = val - else: - raise Exception('Could not update the %s' % (curr)) - - return self.updated_currency, self.log_info - - -# Admin CH #################################################################### -class Admin_ch_getter(Curreny_getter_interface): - """Implementation of Currency_getter_factory interface - for Admin.ch service - - """ - - def rate_retrieve(self, dom, ns, curr): - """Parse a dom node to retrieve currencies data""" - res = {} - xpath_rate_currency = ("/def:wechselkurse/def:devise[@code='%s']/" - "def:kurs/text()") % (curr.lower()) - xpath_rate_ref = ("/def:wechselkurse/def:devise[@code='%s']/" - "def:waehrung/text()") % (curr.lower()) - res['rate_currency'] = float( - dom.xpath(xpath_rate_currency, namespaces=ns)[0] - ) - res['rate_ref'] = float( - (dom.xpath(xpath_rate_ref, namespaces=ns)[0]).split(' ')[0] - ) - return res - - def get_updated_currency(self, currency_array, main_currency, - max_delta_days): - """Implementation of abstract method of Curreny_getter_interface""" - url = ('http://www.afd.admin.ch/publicdb/newdb/' - 'mwst_kurse/wechselkurse.php') - # We do not want to update the main currency - if main_currency in currency_array: - currency_array.remove(main_currency) - # Move to new XML lib cf Launchpad bug #645263 - from lxml import etree - _logger.debug("Admin.ch currency rate service : connecting...") - rawfile = self.get_url(url) - dom = etree.fromstring(rawfile) - _logger.debug("Admin.ch sent a valid XML file") - adminch_ns = { - 'def': 'http://www.afd.admin.ch/publicdb/newdb/mwst_kurse' - } - rate_date = dom.xpath( - '/def:wechselkurse/def:datum/text()', - namespaces=adminch_ns - ) - rate_date = rate_date[0] - rate_date_datetime = datetime.strptime(rate_date, - DEFAULT_SERVER_DATE_FORMAT) - self.check_rate_date(rate_date_datetime, max_delta_days) - # we dynamically update supported currencies - self.supported_currency_array = dom.xpath( - "/def:wechselkurse/def:devise/@code", - namespaces=adminch_ns - ) - self.supported_currency_array = [x.upper() for x - in self.supported_currency_array] - self.supported_currency_array.append('CHF') - - _logger.debug( - "Supported currencies = " + str(self.supported_currency_array) - ) - self.validate_cur(main_currency) - if main_currency != 'CHF': - main_curr_data = self.rate_retrieve(dom, adminch_ns, main_currency) - # 1 MAIN_CURRENCY = main_rate CHF - rate_curr = main_curr_data['rate_currency'] - rate_ref = main_curr_data['rate_ref'] - main_rate = rate_curr / rate_ref - for curr in currency_array: - self.validate_cur(curr) - if curr == 'CHF': - rate = main_rate - else: - curr_data = self.rate_retrieve(dom, adminch_ns, curr) - # 1 MAIN_CURRENCY = rate CURR - if main_currency == 'CHF': - rate = curr_data['rate_ref'] / curr_data['rate_currency'] - else: - rate = (main_rate * curr_data['rate_ref'] / - curr_data['rate_currency']) - self.updated_currency[curr] = rate - _logger.debug( - "Rate retrieved : 1 %s = %s %s" % (main_currency, rate, curr) - ) - return self.updated_currency, self.log_info - - -# ECB getter ################################################################# -class ECB_getter(Curreny_getter_interface): - """Implementation of Currency_getter_factory interface - for ECB service - """ - - def rate_retrieve(self, dom, ns, curr): - """Parse a dom node to retrieve- - currencies data - - """ - res = {} - xpath_curr_rate = ("/gesmes:Envelope/def:Cube/def:Cube/" - "def:Cube[@currency='%s']/@rate") % (curr.upper()) - res['rate_currency'] = float( - dom.xpath(xpath_curr_rate, namespaces=ns)[0] - ) - return res - - def get_updated_currency(self, currency_array, main_currency, - max_delta_days): - """implementation of abstract method of Curreny_getter_interface""" - url = 'http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml' - # Important : as explained on the ECB web site, the currencies are - # at the beginning of the afternoon ; so, until 3 p.m. Paris time - # the currency rates are the ones of trading day N-1 - # http://www.ecb.europa.eu/stats/exchange/eurofxref/html/index.en.html - - # We do not want to update the main currency - if main_currency in currency_array: - currency_array.remove(main_currency) - # Move to new XML lib cf Launchpad bug #645263 - from lxml import etree - _logger.debug("ECB currency rate service : connecting...") - rawfile = self.get_url(url) - dom = etree.fromstring(rawfile) - _logger.debug("ECB sent a valid XML file") - ecb_ns = { - 'gesmes': 'http://www.gesmes.org/xml/2002-08-01', - 'def': 'http://www.ecb.int/vocabulary/2002-08-01/eurofxref' - } - rate_date = dom.xpath('/gesmes:Envelope/def:Cube/def:Cube/@time', - namespaces=ecb_ns)[0] - rate_date_datetime = datetime.strptime(rate_date, - DEFAULT_SERVER_DATE_FORMAT) - self.check_rate_date(rate_date_datetime, max_delta_days) - # We dynamically update supported currencies - self.supported_currency_array = dom.xpath( - "/gesmes:Envelope/def:Cube/def:Cube/def:Cube/@currency", - namespaces=ecb_ns - ) - self.supported_currency_array.append('EUR') - _logger.debug("Supported currencies = %s " % - self.supported_currency_array) - self.validate_cur(main_currency) - if main_currency != 'EUR': - main_curr_data = self.rate_retrieve(dom, ecb_ns, main_currency) - for curr in currency_array: - self.validate_cur(curr) - if curr == 'EUR': - rate = 1 / main_curr_data['rate_currency'] - else: - curr_data = self.rate_retrieve(dom, ecb_ns, curr) - if main_currency == 'EUR': - rate = curr_data['rate_currency'] - else: - rate = (curr_data['rate_currency'] / - main_curr_data['rate_currency']) - self.updated_currency[curr] = rate - _logger.debug( - "Rate retrieved : 1 %s = %s %s" % (main_currency, rate, curr) - ) - return self.updated_currency, self.log_info - - -# PL NBP ###################################################################### -class PL_NBP_getter(Curreny_getter_interface): - """Implementation of Currency_getter_factory interface - for PL NBP service - - """ - - def rate_retrieve(self, dom, ns, curr): - """ Parse a dom node to retrieve - currencies data""" - res = {} - xpath_rate_currency = ("/tabela_kursow/pozycja[kod_waluty='%s']/" - "kurs_sredni/text()") % (curr.upper()) - xpath_rate_ref = ("/tabela_kursow/pozycja[kod_waluty='%s']/" - "przelicznik/text()") % (curr.upper()) - res['rate_currency'] = float( - dom.xpath(xpath_rate_currency, namespaces=ns)[0].replace(',', '.') - ) - res['rate_ref'] = float(dom.xpath(xpath_rate_ref, namespaces=ns)[0]) - return res - - def get_updated_currency(self, currency_array, main_currency, - max_delta_days): - """implementation of abstract method of Curreny_getter_interface""" - # LastA.xml is always the most recent one - url = 'http://www.nbp.pl/kursy/xml/LastA.xml' - # We do not want to update the main currency - if main_currency in currency_array: - currency_array.remove(main_currency) - # Move to new XML lib cf Launchpad bug #645263 - from lxml import etree - _logger.debug("NBP.pl currency rate service : connecting...") - rawfile = self.get_url(url) - dom = etree.fromstring(rawfile) - ns = {} # Cool, there are no namespaces ! - _logger.debug("NBP.pl sent a valid XML file") - rate_date = dom.xpath('/tabela_kursow/data_publikacji/text()', - namespaces=ns)[0] - rate_date_datetime = datetime.strptime(rate_date, - DEFAULT_SERVER_DATE_FORMAT) - self.check_rate_date(rate_date_datetime, max_delta_days) - # We dynamically update supported currencies - self.supported_currency_array = dom.xpath( - '/tabela_kursow/pozycja/kod_waluty/text()', - namespaces=ns - ) - self.supported_currency_array.append('PLN') - _logger.debug("Supported currencies = %s" % - self.supported_currency_array) - self.validate_cur(main_currency) - if main_currency != 'PLN': - main_curr_data = self.rate_retrieve(dom, ns, main_currency) - # 1 MAIN_CURRENCY = main_rate PLN - main_rate = (main_curr_data['rate_currency'] / - main_curr_data['rate_ref']) - for curr in currency_array: - self.validate_cur(curr) - if curr == 'PLN': - rate = main_rate - else: - curr_data = self.rate_retrieve(dom, ns, curr) - # 1 MAIN_CURRENCY = rate CURR - if main_currency == 'PLN': - rate = curr_data['rate_ref'] / curr_data['rate_currency'] - else: - rate = (main_rate * curr_data['rate_ref'] / - curr_data['rate_currency']) - self.updated_currency[curr] = rate - _logger.debug("Rate retrieved : %s = %s %s" % - (main_currency, rate, curr)) - return self.updated_currency, self.log_info - - -# Banco de México ############################################################# -class Banxico_getter(Curreny_getter_interface): - """Implementation of Currency_getter_factory interface - for Banco de México service - - """ - - def rate_retrieve(self): - """ Get currency exchange from Banxico.xml and proccess it - TODO: Get correct data from xml instead of process string - """ - url = ('http://www.banxico.org.mx/rsscb/rss?' - 'BMXC_canal=pagos&BMXC_idioma=es') - - from xml.dom.minidom import parse - from StringIO import StringIO - - logger = logging.getLogger(__name__) - logger.debug("Banxico currency rate service : connecting...") - rawfile = self.get_url(url) - - dom = parse(StringIO(rawfile)) - logger.debug("Banxico sent a valid XML file") - - value = dom.getElementsByTagName('cb:value')[0] - rate = value.firstChild.nodeValue - - return float(rate) - - def get_updated_currency(self, currency_array, main_currency, - max_delta_days=1): - """implementation of abstract method of Curreny_getter_interface""" - logger = logging.getLogger(__name__) - # we do not want to update the main currency - if main_currency in currency_array: - currency_array.remove(main_currency) - - # Suported currencies - suported = ['MXN', 'USD'] - for curr in currency_array: - if curr in suported: - # Get currency data - main_rate = self.rate_retrieve() - if main_currency == 'MXN': - rate = 1 / main_rate - else: - rate = main_rate - else: - # No other currency supported - continue - - self.updated_currency[curr] = rate - logger.debug("Rate retrieved : %s = %s %s" % - (main_currency, rate, curr)) - - -# CA BOC ##### Bank of Canada ############################################# -class CA_BOC_getter(Curreny_getter_interface): - """Implementation of Curreny_getter_factory interface - for Bank of Canada RSS service - - """ - - def get_updated_currency(self, currency_array, main_currency, - max_delta_days): - """implementation of abstract method of Curreny_getter_interface""" - - # as of Jan 2014 BOC is publishing noon rates for about 60 currencies - url = ('http://www.bankofcanada.ca/stats/assets/' - 'rates_rss/noon/en_%s.xml') - # closing rates are available as well (please note there are only 12 - # currencies reported): - # http://www.bankofcanada.ca/stats/assets/rates_rss/closing/en_%s.xml - - # We do not want to update the main currency - if main_currency in currency_array: - currency_array.remove(main_currency) - - import feedparser - import pytz - from dateutil import parser - - for curr in currency_array: - - _logger.debug("BOC currency rate service : connecting...") - dom = feedparser.parse(url % curr) - - self.validate_cur(curr) - - # check if BOC service is running - if dom.bozo and dom.status != 404: - _logger.error("Bank of Canada - service is down - try again\ - later...") - - # check if BOC sent a valid response for this currency - if dom.status != 200: - _logger.error("Exchange data for %s is not reported by Bank\ - of Canada." % curr) - raise osv.except_osv('Error !', 'Exchange data for %s is not\ - reported by Bank of Canada.' % str(curr)) - - _logger.debug("BOC sent a valid RSS file for: " + curr) - - # check for valid exchange data - if (dom.entries[0].cb_basecurrency == main_currency) and \ - (dom.entries[0].cb_targetcurrency == curr): - rate = dom.entries[0].cb_exchangerate.split('\n', 1)[0] - rate_date_datetime = parser.parse(dom.entries[0].updated)\ - .astimezone(pytz.utc).replace(tzinfo=None) - self.check_rate_date(rate_date_datetime, max_delta_days) - self.updated_currency[curr] = rate - _logger.debug("BOC Rate retrieved : %s = %s %s" % - (main_currency, rate, curr)) - else: - _logger.error( - "Exchange data format error for Bank of Canada -" - "%s. Please check provider data format " - "and/or source code." % curr) - raise osv.except_osv('Error !', - 'Exchange data format error for\ - Bank of Canada - %s !' % str(curr)) - - return self.updated_currency, self.log_info + rate_name = fields.Datetime.to_string(datetime.utcnow().replace( + hour=0, minute=0, second=0, microsecond=0)) + for curr in self.currency_to_update: + if curr.id == main_curr.id: + continue + do_create = True + for rate in curr.rate_ids: + if rate.name == rate_name: + rate.rate= res[curr.name] + do_create = False + break + if do_create: + vals = { + 'currency_id': curr.id, + 'rate': res[curr.name], + 'name': rate_name + } + rate_obj.create(vals) + + # Show the most recent note at the top + msg = "%s \n%s currency updated. %s" % ( + log_info or '', + fields.Datetime.to_string(datetime.today()), + note + ) + self.write({'note': msg}) + except Exception as exc: + error_msg = "\n%s ERROR : %s %s" % ( + fields.Datetime.to_string(datetime.today()), + repr(exc), + note + ) + _logger.info(repr(exc)) + self.write({'note': error_msg}) + if self._context.get('cron', False): + next_run = (datetime.combine( + fields.Date.from_string(self.next_run), + datetime.time.min) + + _intervalTypes[str(self.interval_type)] + (self.interval_number)).date() + self.next_run = next_run + + @api.multi + def run_currency_update(self): + "Update currency at the given frequence" + ctx = dict(self._context) + current_date = fields.Date.today() + services = self.search([('next_run', '=', current_date)]) + ctx['cron'] = True + for service in services: + service.with_context(ctx).refresh_currency() + + @api.model + def _run_currency_update(self): + self.run_currency_update() diff --git a/currency_rate_update/services/currency_rate_update.py b/currency_rate_update/services/currency_rate_update.py new file mode 100644 index 000000000..91419dfdc --- /dev/null +++ b/currency_rate_update/services/currency_rate_update.py @@ -0,0 +1,799 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (c) 2009 Camptocamp SA +# @source JBA and AWST inpiration +# @contributor Grzegorz Grzelak (grzegorz.grzelak@birdglobe.com), +# Joel Grand-Guillaume +# Copyright (c) 2010 Alexis de Lattre (alexis@via.ecp.fr) +# - ported XML-based webservices (Admin.ch, ECB, PL NBP) to new XML lib +# - rates given by ECB webservice is now correct even when main_cur <> EUR +# - rates given by PL_NBP webs. is now correct even when main_cur <> PLN +# - if company_currency <> CHF, you can now update CHF via Admin.ch +# (same for EUR with ECB webservice and PLN with NBP webservice) +# For more details, see Launchpad bug #645263 +# - mecanism to check if rates given by the webservice are "fresh" +# enough to be written in OpenERP +# ('max_delta_days' parameter for each currency update service) +# Ported to OpenERP 7.0 by Lorenzo Battistini +# +# +# 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 . +# +############################################################################## + +# TODO "nice to have" : restrain the list of currencies that can be added for +# a webservice to the list of currencies supported by the Webservice +# TODO : implement max_delta_days for Yahoo webservice + +import logging +import time +from datetime import datetime, timedelta +from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT +from openerp.osv import fields, osv, orm +from openerp.tools.translate import _ + +_logger = logging.getLogger(__name__) + + +class Currency_rate_update_service(osv.Model): + """Class that tells for wich services wich currencies have to be updated + + """ + _name = "currency.rate.update.service" + _description = "Currency Rate Update" + _columns = { + # List of webservicies the value sould be a class name + 'service': fields.selection( + [ + ('Admin_ch_getter', 'Admin.ch'), + ('ECB_getter', 'European Central Bank'), + ('Yahoo_getter', 'Yahoo Finance '), + # Added for polish rates + ('PL_NBP_getter', 'Narodowy Bank Polski'), + # Added for mexican rates + ('Banxico_getter', 'Banco de México'), + # Bank of Canada is using RSS-CB + # http://www.cbwiki.net/wiki/index.php/Specification_1.1 + # This RSS format is used by other national banks + # (Thailand, Malaysia, Mexico...) + ('CA_BOC_getter', 'Bank of Canada - noon rates'), + ], + "Webservice to use", + required=True + ), + # List of currency to update + 'currency_to_update': fields.many2many( + 'res.currency', + 'res_curreny_auto_udate_rel', + 'service_id', + 'currency_id', + 'currency to update with this service', + ), + # Back ref + 'company_id': fields.many2one( + 'res.company', + 'linked company', + ), + # Note fileds that will be used as a logger + 'note': fields.text('update notice'), + 'max_delta_days': fields.integer( + 'Max delta days', + required=True, + help="If the time delta between the " + "rate date given by the webservice and " + "the current date exeeds this value, " + "then the currency rate is not updated in OpenERP." + ), + } + _defaults = {'max_delta_days': lambda *a: 4} + _sql_constraints = [ + ( + 'curr_service_unique', + 'unique (service, company_id)', + _('You can use a service one time per company !') + ) + ] + + def _check_max_delta_days(self, cr, uid, ids): + for company in self.read(cr, uid, ids, ['max_delta_days']): + if company['max_delta_days'] >= 0: + continue + else: + return False + return True + + _constraints = [ + (_check_max_delta_days, + "'Max delta days' must be >= 0", + ['max_delta_days']), + ] + + +class Currency_rate_update(osv.Model): + """Class that handle an ir cron call who will + update currencies based on a web url""" + _name = "currency.rate.update" + _description = "Currency Rate Update" + # Dict that represent a cron object + nextcall_time = datetime.today() + timedelta(days=1) + nextcall = nextcall_time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + cron = { + 'active': False, + 'priority': 1, + 'interval_number': 1, + 'interval_type': 'weeks', + 'nextcall': nextcall, + 'numbercall': -1, + 'doall': True, + 'model': 'currency.rate.update', + 'function': 'run_currency_update', + 'args': '()', + } + + LOG_NAME = 'cron-rates' + MOD_NAME = 'currency_rate_update: ' + + def get_cron_id(self, cr, uid, context): + """Returns the updater cron's id. + Create one if the cron does not exists + """ + + cron_id = 0 + cron_obj = self.pool.get('ir.cron') + try: + # Finds the cron that send messages + cron_id = cron_obj.search( + cr, + uid, + [ + ('function', 'ilike', self.cron['function']), + ('model', 'ilike', self.cron['model']) + ], + context={ + 'active_test': False + } + ) + cron_id = int(cron_id[0]) + except Exception: + _logger.info('warning cron not found one will be created') + # Ignore if the cron is missing cause we are + # going to create it in db + pass + if not cron_id: + self.cron['name'] = _('Currency Rate Update') + cron_id = cron_obj.create(cr, uid, self.cron, context) + return cron_id + + def save_cron(self, cr, uid, datas, context=None): + """save the cron config data should be a dict""" + cron_id = self.get_cron_id(cr, uid, context) + return self.pool.get('ir.cron').write(cr, uid, [cron_id], datas) + + def run_currency_update(self, cr, uid): + "update currency at the given frequence" + factory = Currency_getter_factory() + curr_obj = self.pool.get('res.currency') + rate_obj = self.pool.get('res.currency.rate') + companies = self.pool.get('res.company').search(cr, uid, []) + for comp in self.pool.get('res.company').browse(cr, uid, companies): + # The multi company currency can beset or no so we handle + # The two case + if not comp.auto_currency_up: + continue + # We fetch the main currency looking for currency with base = true. + # The main rate should be set at 1.00 + main_curr_ids = curr_obj.search( + cr, uid, + [('base', '=', True), ('company_id', '=', comp.id)] + ) + if not main_curr_ids: + # If we can not find a base currency for this company + # we look for one with no company set + main_curr_ids = curr_obj.search( + cr, uid, + [('base', '=', True), ('company_id', '=', False)] + ) + if main_curr_ids: + main_curr_rec = curr_obj.browse(cr, uid, main_curr_ids[0]) + else: + raise orm.except_orm( + _('Error!'), + ('There is no base currency set!') + ) + if main_curr_rec.rate != 1: + raise orm.except_orm( + _('Error!'), + ('Base currency rate should be 1.00!') + ) + main_curr = main_curr_rec.name + for service in comp.services_to_use: + note = service.note or '' + try: + # We initalize the class that will handle the request + # and return a dict of rate + getter = factory.register(service.service) + curr_to_fetch = map(lambda x: x.name, + service.currency_to_update) + res, log_info = getter.get_updated_currency( + curr_to_fetch, + main_curr, + service.max_delta_days + ) + rate_name = time.strftime(DEFAULT_SERVER_DATE_FORMAT) + for curr in service.currency_to_update: + if curr.name == main_curr: + continue + do_create = True + for rate in curr.rate_ids: + if rate.name == rate_name: + rate.write({'rate': res[curr.name]}) + do_create = False + break + if do_create: + vals = { + 'currency_id': curr.id, + 'rate': res[curr.name], + 'name': rate_name + } + rate_obj.create( + cr, + uid, + vals, + ) + + # Show the most recent note at the top + msg = "%s \n%s currency updated. %s" % ( + log_info or '', + datetime.today().strftime( + DEFAULT_SERVER_DATETIME_FORMAT + ), + note + ) + service.write({'note': msg}) + except Exception as exc: + error_msg = "\n%s ERROR : %s %s" % ( + datetime.today().strftime( + DEFAULT_SERVER_DATETIME_FORMAT + ), + repr(exc), + note + ) + _logger.info(repr(exc)) + service.write({'note': error_msg}) + + +class AbstractClassError(Exception): + def __str__(self): + return 'Abstract Class' + + def __repr__(self): + return 'Abstract Class' + + +class AbstractMethodError(Exception): + def __str__(self): + return 'Abstract Method' + + def __repr__(self): + return 'Abstract Method' + + +class UnknowClassError(Exception): + def __str__(self): + return 'Unknown Class' + + def __repr__(self): + return 'Unknown Class' + + +class UnsuportedCurrencyError(Exception): + def __init__(self, value): + self.curr = value + + def __str__(self): + return 'Unsupported currency %s' % self.curr + + def __repr__(self): + return 'Unsupported currency %s' % self.curr + + +class Currency_getter_factory(): + """Factory pattern class that will return + a currency getter class base on the name passed + to the register method + + """ + def register(self, class_name): + allowed = [ + 'Admin_ch_getter', + 'PL_NBP_getter', + 'ECB_getter', + 'NYFB_getter', + 'Google_getter', + 'Yahoo_getter', + 'Banxico_getter', + 'CA_BOC_getter', + ] + if class_name in allowed: + class_def = eval(class_name) + return class_def() + else: + raise UnknowClassError + + +class Curreny_getter_interface(object): + "Abstract class of currency getter" + + log_info = " " + + supported_currency_array = [ + 'AED', 'AFN', 'ALL', 'AMD', 'ANG', 'AOA', 'ARS', 'AUD', 'AWG', 'AZN', + 'BAM', 'BBD', 'BDT', 'BGN', 'BHD', 'BIF', 'BMD', 'BND', 'BOB', 'BRL', + 'BSD', 'BTN', 'BWP', 'BYR', 'BZD', 'CAD', 'CDF', 'CHF', 'CLP', 'CNY', + 'COP', 'CRC', 'CUP', 'CVE', 'CYP', 'CZK', 'DJF', 'DKK', 'DOP', 'DZD', + 'EEK', 'EGP', 'ERN', 'ETB', 'EUR', 'FJD', 'FKP', 'GBP', 'GEL', 'GGP', + 'GHS', 'GIP', 'GMD', 'GNF', 'GTQ', 'GYD', 'HKD', 'HNL', 'HRK', 'HTG', + 'HUF', 'IDR', 'ILS', 'IMP', 'INR', 'IQD', 'IRR', 'ISK', 'JEP', 'JMD', + 'JOD', 'JPY', 'KES', 'KGS', 'KHR', 'KMF', 'KPW', 'KRW', 'KWD', 'KYD', + 'KZT', 'LAK', 'LBP', 'LKR', 'LRD', 'LSL', 'LTL', 'LVL', 'LYD', 'MAD', + 'MDL', 'MGA', 'MKD', 'MMK', 'MNT', 'MOP', 'MRO', 'MTL', 'MUR', 'MVR', + 'MWK', 'MXN', 'MYR', 'MZN', 'NAD', 'NGN', 'NIO', 'NOK', 'NPR', 'NZD', + 'OMR', 'PAB', 'PEN', 'PGK', 'PHP', 'PKR', 'PLN', 'PYG', 'QAR', 'RON', + 'RSD', 'RUB', 'RWF', 'SAR', 'SBD', 'SCR', 'SDG', 'SEK', 'SGD', 'SHP', + 'SLL', 'SOS', 'SPL', 'SRD', 'STD', 'SVC', 'SYP', 'SZL', 'THB', 'TJS', + 'TMM', 'TND', 'TOP', 'TRY', 'TTD', 'TVD', 'TWD', 'TZS', 'UAH', 'UGX', + 'USD', 'UYU', 'UZS', 'VEB', 'VEF', 'VND', 'VUV', 'WST', 'XAF', 'XAG', + 'XAU', 'XCD', 'XDR', 'XOF', 'XPD', 'XPF', 'XPT', 'YER', 'ZAR', 'ZMK', + 'ZWD' + ] + + # Updated currency this arry will contain the final result + updated_currency = {} + + def get_updated_currency(self, currency_array, main_currency, + max_delta_days): + """Interface method that will retrieve the currency + This function has to be reinplemented in child + """ + raise AbstractMethodError + + def validate_cur(self, currency): + """Validate if the currency to update is supported""" + if currency not in self.supported_currency_array: + raise UnsuportedCurrencyError(currency) + + def get_url(self, url): + """Return a string of a get url query""" + try: + import urllib + objfile = urllib.urlopen(url) + rawfile = objfile.read() + objfile.close() + return rawfile + except ImportError: + raise osv.except_osv( + 'Error !', + self.MOD_NAME + 'Unable to import urllib !' + ) + except IOError: + raise osv.except_osv( + 'Error !', + self.MOD_NAME + 'Web Service does not exist !' + ) + + def check_rate_date(self, rate_date, max_delta_days): + """Check date constrains. rate_date must be of datetime type""" + days_delta = (datetime.today() - rate_date).days + if days_delta > max_delta_days: + raise Exception( + 'The rate timestamp (%s) is %d days away from today, ' + 'which is over the limit (%d days). ' + 'Rate not updated in OpenERP.' % (rate_date, + days_delta, + max_delta_days) + ) + + # We always have a warning when rate_date != today + rate_date_str = datetime.strftime(rate_date, + DEFAULT_SERVER_DATE_FORMAT) + if rate_date.date() != datetime.today().date(): + msg = "The rate timestamp (%s) is not today's date" + self.log_info = ("WARNING : %s %s") % (msg, rate_date_str) + _logger.warning(msg, rate_date_str) + + +# Yahoo ####################################################################### +class Yahoo_getter(Curreny_getter_interface): + """Implementation of Currency_getter_factory interface + for Yahoo finance service + """ + + def get_updated_currency(self, currency_array, main_currency, + max_delta_days): + """implementation of abstract method of curreny_getter_interface""" + self.validate_cur(main_currency) + url = ('http://download.finance.yahoo.com/d/' + 'quotes.txt?s="%s"=X&f=sl1c1abg') + if main_currency in currency_array: + currency_array.remove(main_currency) + for curr in currency_array: + self.validate_cur(curr) + res = self.get_url(url % (main_currency + curr)) + val = res.split(',')[1] + if val: + self.updated_currency[curr] = val + else: + raise Exception('Could not update the %s' % (curr)) + + return self.updated_currency, self.log_info + + +# Admin CH #################################################################### +class Admin_ch_getter(Curreny_getter_interface): + """Implementation of Currency_getter_factory interface + for Admin.ch service + + """ + + def rate_retrieve(self, dom, ns, curr): + """Parse a dom node to retrieve currencies data""" + res = {} + xpath_rate_currency = ("/def:wechselkurse/def:devise[@code='%s']/" + "def:kurs/text()") % (curr.lower()) + xpath_rate_ref = ("/def:wechselkurse/def:devise[@code='%s']/" + "def:waehrung/text()") % (curr.lower()) + res['rate_currency'] = float( + dom.xpath(xpath_rate_currency, namespaces=ns)[0] + ) + res['rate_ref'] = float( + (dom.xpath(xpath_rate_ref, namespaces=ns)[0]).split(' ')[0] + ) + return res + + def get_updated_currency(self, currency_array, main_currency, + max_delta_days): + """Implementation of abstract method of Curreny_getter_interface""" + url = ('http://www.afd.admin.ch/publicdb/newdb/' + 'mwst_kurse/wechselkurse.php') + # We do not want to update the main currency + if main_currency in currency_array: + currency_array.remove(main_currency) + # Move to new XML lib cf Launchpad bug #645263 + from lxml import etree + _logger.debug("Admin.ch currency rate service : connecting...") + rawfile = self.get_url(url) + dom = etree.fromstring(rawfile) + _logger.debug("Admin.ch sent a valid XML file") + adminch_ns = { + 'def': 'http://www.afd.admin.ch/publicdb/newdb/mwst_kurse' + } + rate_date = dom.xpath( + '/def:wechselkurse/def:datum/text()', + namespaces=adminch_ns + ) + rate_date = rate_date[0] + rate_date_datetime = datetime.strptime(rate_date, + DEFAULT_SERVER_DATE_FORMAT) + self.check_rate_date(rate_date_datetime, max_delta_days) + # we dynamically update supported currencies + self.supported_currency_array = dom.xpath( + "/def:wechselkurse/def:devise/@code", + namespaces=adminch_ns + ) + self.supported_currency_array = [x.upper() for x + in self.supported_currency_array] + self.supported_currency_array.append('CHF') + + _logger.debug( + "Supported currencies = " + str(self.supported_currency_array) + ) + self.validate_cur(main_currency) + if main_currency != 'CHF': + main_curr_data = self.rate_retrieve(dom, adminch_ns, main_currency) + # 1 MAIN_CURRENCY = main_rate CHF + rate_curr = main_curr_data['rate_currency'] + rate_ref = main_curr_data['rate_ref'] + main_rate = rate_curr / rate_ref + for curr in currency_array: + self.validate_cur(curr) + if curr == 'CHF': + rate = main_rate + else: + curr_data = self.rate_retrieve(dom, adminch_ns, curr) + # 1 MAIN_CURRENCY = rate CURR + if main_currency == 'CHF': + rate = curr_data['rate_ref'] / curr_data['rate_currency'] + else: + rate = (main_rate * curr_data['rate_ref'] / + curr_data['rate_currency']) + self.updated_currency[curr] = rate + _logger.debug( + "Rate retrieved : 1 %s = %s %s" % (main_currency, rate, curr) + ) + return self.updated_currency, self.log_info + + +# ECB getter ################################################################# +class ECB_getter(Curreny_getter_interface): + """Implementation of Currency_getter_factory interface + for ECB service + """ + + def rate_retrieve(self, dom, ns, curr): + """Parse a dom node to retrieve- + currencies data + + """ + res = {} + xpath_curr_rate = ("/gesmes:Envelope/def:Cube/def:Cube/" + "def:Cube[@currency='%s']/@rate") % (curr.upper()) + res['rate_currency'] = float( + dom.xpath(xpath_curr_rate, namespaces=ns)[0] + ) + return res + + def get_updated_currency(self, currency_array, main_currency, + max_delta_days): + """implementation of abstract method of Curreny_getter_interface""" + url = 'http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml' + # Important : as explained on the ECB web site, the currencies are + # at the beginning of the afternoon ; so, until 3 p.m. Paris time + # the currency rates are the ones of trading day N-1 + # http://www.ecb.europa.eu/stats/exchange/eurofxref/html/index.en.html + + # We do not want to update the main currency + if main_currency in currency_array: + currency_array.remove(main_currency) + # Move to new XML lib cf Launchpad bug #645263 + from lxml import etree + _logger.debug("ECB currency rate service : connecting...") + rawfile = self.get_url(url) + dom = etree.fromstring(rawfile) + _logger.debug("ECB sent a valid XML file") + ecb_ns = { + 'gesmes': 'http://www.gesmes.org/xml/2002-08-01', + 'def': 'http://www.ecb.int/vocabulary/2002-08-01/eurofxref' + } + rate_date = dom.xpath('/gesmes:Envelope/def:Cube/def:Cube/@time', + namespaces=ecb_ns)[0] + rate_date_datetime = datetime.strptime(rate_date, + DEFAULT_SERVER_DATE_FORMAT) + self.check_rate_date(rate_date_datetime, max_delta_days) + # We dynamically update supported currencies + self.supported_currency_array = dom.xpath( + "/gesmes:Envelope/def:Cube/def:Cube/def:Cube/@currency", + namespaces=ecb_ns + ) + self.supported_currency_array.append('EUR') + _logger.debug("Supported currencies = %s " % + self.supported_currency_array) + self.validate_cur(main_currency) + if main_currency != 'EUR': + main_curr_data = self.rate_retrieve(dom, ecb_ns, main_currency) + for curr in currency_array: + self.validate_cur(curr) + if curr == 'EUR': + rate = 1 / main_curr_data['rate_currency'] + else: + curr_data = self.rate_retrieve(dom, ecb_ns, curr) + if main_currency == 'EUR': + rate = curr_data['rate_currency'] + else: + rate = (curr_data['rate_currency'] / + main_curr_data['rate_currency']) + self.updated_currency[curr] = rate + _logger.debug( + "Rate retrieved : 1 %s = %s %s" % (main_currency, rate, curr) + ) + return self.updated_currency, self.log_info + + +# PL NBP ###################################################################### +class PL_NBP_getter(Curreny_getter_interface): + """Implementation of Currency_getter_factory interface + for PL NBP service + + """ + + def rate_retrieve(self, dom, ns, curr): + """ Parse a dom node to retrieve + currencies data""" + res = {} + xpath_rate_currency = ("/tabela_kursow/pozycja[kod_waluty='%s']/" + "kurs_sredni/text()") % (curr.upper()) + xpath_rate_ref = ("/tabela_kursow/pozycja[kod_waluty='%s']/" + "przelicznik/text()") % (curr.upper()) + res['rate_currency'] = float( + dom.xpath(xpath_rate_currency, namespaces=ns)[0].replace(',', '.') + ) + res['rate_ref'] = float(dom.xpath(xpath_rate_ref, namespaces=ns)[0]) + return res + + def get_updated_currency(self, currency_array, main_currency, + max_delta_days): + """implementation of abstract method of Curreny_getter_interface""" + # LastA.xml is always the most recent one + url = 'http://www.nbp.pl/kursy/xml/LastA.xml' + # We do not want to update the main currency + if main_currency in currency_array: + currency_array.remove(main_currency) + # Move to new XML lib cf Launchpad bug #645263 + from lxml import etree + _logger.debug("NBP.pl currency rate service : connecting...") + rawfile = self.get_url(url) + dom = etree.fromstring(rawfile) + ns = {} # Cool, there are no namespaces ! + _logger.debug("NBP.pl sent a valid XML file") + rate_date = dom.xpath('/tabela_kursow/data_publikacji/text()', + namespaces=ns)[0] + rate_date_datetime = datetime.strptime(rate_date, + DEFAULT_SERVER_DATE_FORMAT) + self.check_rate_date(rate_date_datetime, max_delta_days) + # We dynamically update supported currencies + self.supported_currency_array = dom.xpath( + '/tabela_kursow/pozycja/kod_waluty/text()', + namespaces=ns + ) + self.supported_currency_array.append('PLN') + _logger.debug("Supported currencies = %s" % + self.supported_currency_array) + self.validate_cur(main_currency) + if main_currency != 'PLN': + main_curr_data = self.rate_retrieve(dom, ns, main_currency) + # 1 MAIN_CURRENCY = main_rate PLN + main_rate = (main_curr_data['rate_currency'] / + main_curr_data['rate_ref']) + for curr in currency_array: + self.validate_cur(curr) + if curr == 'PLN': + rate = main_rate + else: + curr_data = self.rate_retrieve(dom, ns, curr) + # 1 MAIN_CURRENCY = rate CURR + if main_currency == 'PLN': + rate = curr_data['rate_ref'] / curr_data['rate_currency'] + else: + rate = (main_rate * curr_data['rate_ref'] / + curr_data['rate_currency']) + self.updated_currency[curr] = rate + _logger.debug("Rate retrieved : %s = %s %s" % + (main_currency, rate, curr)) + return self.updated_currency, self.log_info + + +# Banco de México ############################################################# +class Banxico_getter(Curreny_getter_interface): + """Implementation of Currency_getter_factory interface + for Banco de México service + + """ + + def rate_retrieve(self): + """ Get currency exchange from Banxico.xml and proccess it + TODO: Get correct data from xml instead of process string + """ + url = ('http://www.banxico.org.mx/rsscb/rss?' + 'BMXC_canal=pagos&BMXC_idioma=es') + + from xml.dom.minidom import parse + from StringIO import StringIO + + logger = logging.getLogger(__name__) + logger.debug("Banxico currency rate service : connecting...") + rawfile = self.get_url(url) + + dom = parse(StringIO(rawfile)) + logger.debug("Banxico sent a valid XML file") + + value = dom.getElementsByTagName('cb:value')[0] + rate = value.firstChild.nodeValue + + return float(rate) + + def get_updated_currency(self, currency_array, main_currency, + max_delta_days=1): + """implementation of abstract method of Curreny_getter_interface""" + logger = logging.getLogger(__name__) + # we do not want to update the main currency + if main_currency in currency_array: + currency_array.remove(main_currency) + + # Suported currencies + suported = ['MXN', 'USD'] + for curr in currency_array: + if curr in suported: + # Get currency data + main_rate = self.rate_retrieve() + if main_currency == 'MXN': + rate = 1 / main_rate + else: + rate = main_rate + else: + # No other currency supported + continue + + self.updated_currency[curr] = rate + logger.debug("Rate retrieved : %s = %s %s" % + (main_currency, rate, curr)) + + +# CA BOC ##### Bank of Canada ############################################# +class CA_BOC_getter(Curreny_getter_interface): + """Implementation of Curreny_getter_factory interface + for Bank of Canada RSS service + + """ + + def get_updated_currency(self, currency_array, main_currency, + max_delta_days): + """implementation of abstract method of Curreny_getter_interface""" + + # as of Jan 2014 BOC is publishing noon rates for about 60 currencies + url = ('http://www.bankofcanada.ca/stats/assets/' + 'rates_rss/noon/en_%s.xml') + # closing rates are available as well (please note there are only 12 + # currencies reported): + # http://www.bankofcanada.ca/stats/assets/rates_rss/closing/en_%s.xml + + # We do not want to update the main currency + if main_currency in currency_array: + currency_array.remove(main_currency) + + import feedparser + import pytz + from dateutil import parser + + for curr in currency_array: + + _logger.debug("BOC currency rate service : connecting...") + dom = feedparser.parse(url % curr) + + self.validate_cur(curr) + + # check if BOC service is running + if dom.bozo and dom.status != 404: + _logger.error("Bank of Canada - service is down - try again\ + later...") + + # check if BOC sent a valid response for this currency + if dom.status != 200: + _logger.error("Exchange data for %s is not reported by Bank\ + of Canada." % curr) + raise osv.except_osv('Error !', 'Exchange data for %s is not\ + reported by Bank of Canada.' % str(curr)) + + _logger.debug("BOC sent a valid RSS file for: " + curr) + + # check for valid exchange data + if (dom.entries[0].cb_basecurrency == main_currency) and \ + (dom.entries[0].cb_targetcurrency == curr): + rate = dom.entries[0].cb_exchangerate.split('\n', 1)[0] + rate_date_datetime = parser.parse(dom.entries[0].updated)\ + .astimezone(pytz.utc).replace(tzinfo=None) + self.check_rate_date(rate_date_datetime, max_delta_days) + self.updated_currency[curr] = rate + _logger.debug("BOC Rate retrieved : %s = %s %s" % + (main_currency, rate, curr)) + else: + _logger.error( + "Exchange data format error for Bank of Canada -" + "%s. Please check provider data format " + "and/or source code." % curr) + raise osv.except_osv('Error !', + 'Exchange data format error for\ + Bank of Canada - %s !' % str(curr)) + + return self.updated_currency, self.log_info