python-retrofix/retrofix/fields.py
2019-04-29 17:01:31 +02:00

314 lines
9.5 KiB
Python

##############################################################################
#
# Copyright (C) 2013 NaN Projectes de Programari Lliure, S.L.
# http://www.NaN-tic.com
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import sys
try:
import cdecimal
# Use cdecimal globally
if 'decimal' not in sys.modules:
sys.modules["decimal"] = cdecimal
except ImportError:
pass
from decimal import Decimal
import re
from datetime import datetime
from stdnum.es.ccc import is_valid
from .formatting import format_string, format_number
from .exception import RetrofixException, raise_assert_error
__all__ = ['Field', 'Char', 'Const', 'Account', 'Number', 'Numeric', 'Integer',
'Date', 'Selection', 'Boolean', 'SIGN_DEFAULT', 'SIGN_12', 'SIGN_N',
'SIGN_N_BLANK', 'SIGN_POSITIVE', 'BOOLEAN_01', 'BOOLEAN_12', 'BOOLEAN_X',
'BOOLEAN_W1']
class Field(object):
def __init__(self):
self._name = None
self._size = None
def set_from_file(self, value):
raise_assert_error(len(value) == self._size, (
'Invalid length of field "%s". '
'Expected "%d" but got "%d".' % (self._name, self._size,
len(value))))
return value
def get_for_file(self, value):
if value is None:
value = ''
return format_string(value, self._size)
def get(self, value):
return value
def set(self, value):
return value
class Char(Field):
def __init__(self):
self._name = None
self._size = None
def set_from_file(self, value):
value = value.ljust(self._size)
return super(Char, self).set_from_file(value)
def set(self, value):
if value is None:
value = ''
if len(value) > self._size:
value = value[:self._size]
return super(Char, self).set(value)
class Const(Char):
def __init__(self, const):
super(Const, self).__init__()
self._const = const
def set_from_file(self, value):
raise_assert_error(value == self._const, (
'Invalid value "%s" in Const field '
'"%s". Expected "%s".' % (value, self._name, self._const)))
return super(Const, self).set_from_file(value)
def get_for_file(self, value):
return self._const
def get(self, value):
return self._const
def set(self, value):
raise_assert_error(value == self._const, ('Invalid value for field "%s"'
% self._name))
return super(Const, self).set(value)
class Account(Char):
def __init__(self):
super(Account, self).__init__()
def set_from_file(self, value):
account = value.strip()
if account and not is_valid(account):
raise RetrofixException('Invalid bank account "%s" in field "%s"'
% (value, self._name))
return super(Account, self).set_from_file(value)
def set(self, value):
account = value
if account:
account = account.strip()
if account and not is_valid(account):
raise RetrofixException('Invalid bank account "%s" in field "%s"'
% (value, self._name))
return super(Account, self).set(value)
class Number(Char):
def __init__(self, align='left'):
super(Number, self).__init__()
self._align = align
def set_from_file(self, value):
raise_assert_error(re.match('[0-9]*$', value), (
'Non-number value "%s" in field "%s"' % (value, self._name)))
return super(Number, self).set_from_file(value)
def set(self, value):
if value is None:
value = ''
raise_assert_error(re.match('[0-9]*$', value), (
'Non-number value "%s" in field "%s"' % (value, self._name)))
l = self._size - len(value)
if self._align == 'right':
if l:
value = (l * '0') + value
return super(Number, self).set(value)
SIGN_DEFAULT = 'default'
SIGN_12 = 'sign_12'
SIGN_N = 'sgin_n'
SIGN_N_BLANK = 'sign_n_blank'
SIGN_POSITIVE = 'positive'
class Numeric(Field):
def __init__(self, decimals=2, sign=SIGN_DEFAULT):
super(Numeric, self).__init__()
self._decimals = decimals
self._sign = sign
def get_sign(self, value):
if self._sign == SIGN_DEFAULT:
return '' if value >= Decimal('0.0') else '-'
if self._sign == SIGN_12:
return '2' if value >= Decimal('0.0') else '1'
if self._sign == SIGN_N:
return '' if value >= Decimal('0.0') else 'N'
if self._sign == SIGN_N_BLANK:
return ' ' if value >= Decimal('0.0') else 'N'
if self._sign == SIGN_POSITIVE:
raise_assert_error(value >= Decimal('0.0'), (
'Field "%s" must be >= 0.0 but '
'got "%.2f"' % (self._name, value)))
return ''
def set_from_file(self, value):
super(Numeric, self).set_from_file(value)
sign = 1
if self._sign in (SIGN_12, SIGN_N):
sign = -1 if value[0] in ('1', 'N') else 1
value = value[1:]
if not self._decimals:
num = sign * Decimal(value)
else:
num = sign * Decimal('%s.%s' % (value[:-self._decimals],
value[-self._decimals:]))
return num
def get_for_file(self, value):
if value is None:
value = Decimal('0')
sign = self.get_sign(value)
length = self._size - len(sign)
raise_assert_error(length > 0, ('Number formatting error. Field size '
'"%d" but only "%d" characters left for formatting field "%s".'
) % (self._size, length, self._name))
return sign + format_number(abs(value), length, self._decimals)
def set(self, value):
try:
return Decimal(value)
except TypeError:
raise RetrofixException('Invalid value "%s" for Numeric Field "%s"'
% (value, self._name))
except ValueError:
raise RetrofixException('Invalid value "%s" for Numeric Field "%s"'
% (value, self._name))
class Integer(Numeric):
def __init__(self):
super(Integer, self).__init__(decimals=0)
class Date(Field):
def __init__(self, pattern):
super(Date, self).__init__()
self._pattern = pattern
def set_from_file(self, value):
if value == '0' * len(value):
return
try:
return datetime.strptime(value, self._pattern)
except ValueError:
raise RetrofixException('Invalid date value "%s" does not '
'match pattern "%s" in field "%s"' % (value,
self._pattern, self._name))
def get_for_file(self, value):
if value is None:
res = ''
else:
res = datetime.strftime(value, self._pattern)
return super(Date, self).get_for_file(res)
def set(self, value):
if value is not None:
raise_assert_error(value, datetime)
return super(Date, self).set(value)
class Selection(Char):
def __init__(self, selection):
super(Selection, self).__init__()
self._selection = selection
self._keys = dict([(x[0], x[1]) for x in selection])
self._values = dict([(x[1], x[0]) for x in selection])
def get_for_file(self, value):
return super(Selection, self).get_for_file(value)
def set_from_file(self, value):
raise_assert_error(value in self._keys, (
'Value "%s" not found in selection field '
'"%s". Expected one of: %s' % (value, self._name, self._keys)))
return super(Selection, self).set_from_file(value)
def set(self, value):
raise_assert_error(value in self._values, (
'Value "%s" not found in selection field '
'"%s". Expected one of: %s' % (value, self._name, self._values)))
value = self._values[value]
return super(Selection, self).set(value)
BOOLEAN_01 = {
True: '1',
False: '0',
}
BOOLEAN_12 = {
True: '1',
False: '2',
}
BOOLEAN_X = {
True: 'X',
False: ' ',
}
BOOLEAN_W1 = {
True: '1',
False: ' ',
}
class Boolean(Field):
def __init__(self, formatting=None):
super(Boolean, self).__init__()
if formatting is None:
formatting = BOOLEAN_01
self._formatting = formatting
def set_from_file(self, value):
for key, text in self._formatting.items():
if value == text:
return key
raise RetrofixException('Invalid value "%s" for boolean field "%s"' % (
value, self._name))
def get_for_file(self, value):
return self._formatting[bool(value)]
def get(self, value):
return bool(value)
def set(self, value):
return super(Boolean, self).set(bool(value))