]> asedeno.scripts.mit.edu Git - bluechips.git/blob - bluechips/model/types.py
added special Currency validator
[bluechips.git] / bluechips / model / types.py
1 """
2 Define special types used in BlueChips
3 """
4
5 import locale
6 from decimal import Decimal, InvalidOperation
7
8 import sqlalchemy as sa
9 from formencode import validators, Invalid
10 from bluechips.lib.subclass import SmartSubclass
11
12 from weakref import WeakValueDictionary
13
14 def localeconv():
15     "Manually install en_US for systems that don't have it."
16     d = {'currency_symbol': '$',
17      'decimal_point': '.',
18      'frac_digits': 2,
19      'grouping': [3, 3, 0],
20      'int_curr_symbol': 'USD ',
21      'int_frac_digits': 2,
22      'mon_decimal_point': '.',
23      'mon_grouping': [3, 3, 0],
24      'mon_thousands_sep': ',',
25      'n_cs_precedes': 1,
26      'n_sep_by_space': 0,
27      'n_sign_posn': 1,
28      'negative_sign': '-',
29      'p_cs_precedes': 1,
30      'p_sep_by_space': 0,
31      'p_sign_posn': 1,
32      'positive_sign': '',
33      'thousands_sep': ','}
34     return d
35 locale.localeconv = localeconv
36
37
38 class CurrencyValidator(validators.FancyValidator):
39     "A validator to convert to Currency objects."
40     messages = {'amount': "Please enter a valid currency amount",
41                 'precision': "Only two digits after the decimal, please"}
42
43     def _to_python(self, value, state):
44         try:
45             dec = Decimal(value)
46         except InvalidOperation:
47             raise Invalid(self.message('amount', state),
48                           value, state)
49         else:
50             ret = dec.quantize(Decimal('1.00'))
51             if ret != dec:
52                 raise Invalid(self.message('precision', state),
53                               value, state)
54             else:
55                 return Currency(int(ret * 100))
56
57
58 class Currency(object):
59     """
60     Store currency values as an integral number of cents
61     """
62     __metaclass__ = SmartSubclass(int)
63     __old_values__ = WeakValueDictionary()
64     def __new__(cls, value):
65         if value is None:
66             value = 0
67         elif isinstance(value, str):
68             value = int(float(value) * 100)
69         else:
70             value = int(value)
71         
72         if value not in cls.__old_values__:
73             new_object = super(cls, cls).__new__(cls)
74             new_object.value = value
75             cls.__old_values__[value] = new_object
76             return new_object
77         else:
78             return cls.__old_values__[value]
79     
80     def __int__(self):
81         """
82         If I don't define this, SmartSubclass will return
83         Currency(int(self.value))
84         """
85         return self.value
86     def __float__(self):
87         """
88         If I don't define this, SmartSubclass will return
89         Currency(float(self.value))
90         """
91         return float(self.value)
92     def __long__(self):
93         """
94         If I don't define this, SmartSubclass will return
95         Currency(long(self.value))
96         """
97         return long(self.value)
98     
99     def __cmp__(self, other):
100         """
101         This is overridden for when validators compare a Currency to
102         ''
103         """
104         if other == '':
105             return 1
106         else:
107             return self.value.__cmp__(int(other))
108     
109     def __mul__(self, other):
110         """
111         If I don't define this, SmartSubclass will convert the other
112         argument to an int
113         """
114         return Currency(self.value * other)
115     def __rmul__(self, other):
116         """
117         If I don't define this, SmartSubclass will convert the other
118         argument to an int
119         """
120         return self.__mul__(other)
121     
122     def __str_no_dollar__(self):
123         """
124         Get to the formatted string without the dollar sign
125         """
126         return str(self).replace('$', '')
127     
128     def __repr__(self):
129         return '%s("%s")' % (self.__class__.__name__, str(self))
130     def __str__(self):
131         return locale.currency(self.value / 100., grouping=True)
132
133
134 class DBCurrency(sa.types.TypeDecorator):
135     """
136     A type which represents monetary amounts internally as integers.
137     
138     This avoids binary/decimal float conversion issues
139     """
140     
141     impl = sa.types.Integer
142     
143     def process_bind_param(self, value, engine):
144         return int(value)
145     
146     def convert_result_value(self, value, engine):
147         return Currency(value)
148     process_result_value = convert_result_value