forked from trytonus/trytond-shipping-ups
/
party.py
executable file
·332 lines (268 loc) · 10.4 KB
/
party.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# -*- encoding: utf-8 -*-
"""
Customizes party address to have address in correct format for UPS API .
"""
import re
# Remove when we are on python 3.x :)
from orderedset import OrderedSet
from lxml import etree
from logbook import Logger
from ups.shipping_package import ShipmentConfirm
from ups.base import PyUPSException
from trytond.pool import Pool, PoolMeta
from trytond.transaction import Transaction
__all__ = ['Address']
__metaclass__ = PoolMeta
digits_only_re = re.compile('\D+')
logger = Logger('trytond_ups')
class Address:
'''
Address
'''
__name__ = "party.address"
@classmethod
def __setup__(cls):
super(Address, cls).__setup__()
cls._error_messages.update({
'ups_field_missing':
'%s is missing in %s.'
})
def _get_ups_address_xml(self):
"""
Return Address XML
"""
if not all([self.street, self.city, self.country]):
self.raise_user_error("Street, City and Country are required.")
if self.country.code in ['US', 'CA'] and not self.subdivision:
self.raise_user_error(
"State is required for %s" % self.country.code
)
if self.country.code in ['US', 'CA', 'PR'] and not self.zip:
# If Shipper country is US or Puerto Rico, 5 or 9 digits is
# required. The character - may be used to separate the first five
# digits and the last four digits. If the Shipper country is CA,
# then the postal code is required and must be 6 alphanumeric
# characters whose format is A#A#A# where A is an uppercase letter
# and # is a digit. For all other countries the postal code is
# optional and must be no more than 9 alphanumeric characters long.
self.raise_user_error("ZIP is required for %s" % self.country.code)
vals = {
'AddressLine1': self.street[:35], # Limit to 35 Char
'City': self.city[:30], # Limit 30 Char
'CountryCode': self.country.code,
}
if self.streetbis:
vals['AddressLine2'] = self.streetbis[:35] # Limit to 35 char
if self.subdivision:
# TODO: Handle Ireland Case
vals['StateProvinceCode'] = self.subdivision.code[3:]
if self.zip:
vals['PostalCode'] = self.zip
return ShipmentConfirm.address_type(**vals)
def to_ups_from_address(self):
'''
Converts party address to UPS `From Address`.
:return: Returns instance of FromAddress
'''
Company = Pool().get('company.company')
vals = {}
if not self.party.phone:
self.raise_user_error(
"ups_field_missing",
error_args=('Phone no.', '"from address"')
)
company_id = Transaction().context.get('company')
if not company_id:
self.raise_user_error(
"ups_field_missing",
error_args=('Company', 'context')
)
company_party = Company(company_id).party
vals = {
'CompanyName': company_party.name,
'AttentionName': self.name or self.party.name,
'TaxIdentificationNumber': company_party.vat_number,
'PhoneNumber': digits_only_re.sub('', self.party.phone),
}
fax = self.party.fax
if fax:
vals['FaxNumber'] = fax
# EMailAddress
email = self.party.email
if email:
vals['EMailAddress'] = email
return ShipmentConfirm.ship_from_type(
self._get_ups_address_xml(), **vals)
def to_ups_to_address(self):
'''
Converts party address to UPS `To Address`.
:return: Returns instance of ToAddress
'''
party = self.party
tax_identification_number = ''
if party.vat_number:
tax_identification_number = party.vat_number
elif hasattr(party, 'tax_exemption_number') and \
party.tax_exemption_number:
tax_identification_number = party.tax_exemption_number
vals = {
'CompanyName': self.name or party.name,
'TaxIdentificationNumber': tax_identification_number,
'AttentionName': self.name or party.name,
}
if party.phone:
vals['PhoneNumber'] = digits_only_re.sub('', party.phone)
fax = party.fax
if fax:
vals['FaxNumber'] = fax
# EMailAddress
email = party.email
if email:
vals['EMailAddress'] = email
# TODO: LocationID is optional
return ShipmentConfirm.ship_to_type(self._get_ups_address_xml(), **vals)
def to_ups_shipper(self, carrier):
'''
Converts party address to UPS `Shipper Address`.
:return: Returns instance of ShipperAddress
'''
Company = Pool().get('company.company')
vals = {}
if not self.party.phone:
self.raise_user_error(
"ups_field_missing",
error_args=('Phone no.', '"Shipper Address"')
)
company_id = Transaction().context.get('company')
if not company_id:
self.raise_user_error(
"ups_field_missing", error_args=('Company', 'context')
)
company_party = Company(company_id).party
vals = {
'CompanyName': company_party.name,
'TaxIdentificationNumber': company_party.vat_number,
'Name': self.name or self.party.name,
'AttentionName': self.name or self.party.name,
'PhoneNumber': digits_only_re.sub('', self.party.phone),
'ShipperNumber': carrier.ups_shipper_no,
}
fax = self.party.fax
if fax:
vals['FaxNumber'] = fax
# EMailAddress
email = self.party.email
if email:
vals['EMailAddress'] = email
return ShipmentConfirm.shipper_type(
self._get_ups_address_xml(),
**vals
)
def _ups_address_validate(self):
"""
Validates the address using the PyUPS API.
.. tip::
This method is not intended to be called directly. It is
automatically called by the address validation API of
trytond-shipping module.
"""
Subdivision = Pool().get('country.subdivision')
Address = Pool().get('party.address')
CarrierConfig = Pool().get('carrier.configuration')
config = CarrierConfig(1)
carrier = config.default_validation_carrier
if not carrier:
# TODO: Make this translatable error message
self.raise_user_error(
"Validation Carrier is not selected in carrier configuration."
)
api_instance = carrier.ups_api_instance(call='address_val')
if not self.country:
# XXX: Either this or assume it is the US of A
self.raise_user_error('Country is required to validate address.')
values = {
'CountryCode': self.country.code,
}
if self.subdivision:
# Fetch ups compatible subdivision
values['StateProvinceCode'] = self.subdivision.code.split('-')[-1]
if self.city:
values['City'] = self.city
if self.zip:
values['PostalCode'] = self.zip
address_request = api_instance.request_type(**values)
# Logging.
logger.debug(
'Making Address Validation Request to UPS for Address Id: {0}'
.format(self.id)
)
logger.debug(
'--------AV API REQUEST--------\n%s'
'\n--------END REQUEST--------'
% etree.tostring(address_request, pretty_print=True)
)
try:
address_response = api_instance.request(address_request)
# Logging.
logger.debug(
'--------AV API RESPONSE--------\n%s'
'\n--------END RESPONSE--------'
% etree.tostring(address_response, pretty_print=True)
)
except PyUPSException, exc:
self.raise_user_error(unicode(exc[0]))
if (len(address_response.AddressValidationResult) == 1) and \
address_response.AddressValidationResult.Quality.pyval == 1:
# This is a perfect match and there is no need to make
# suggestions.
return True
# The UPS response will include the following::
#
# * City
# * StateProvinceCode
# * PostalCodeLowEnd (Not very useful)
# * PostalCodeHighEnd (Not Very Useful)
#
# Example: https://gist.github.com/tarunbhardwaj/4df0673bdd1c7bc6ab89
#
# The approach here is to clear out the duplicates with just city
# and the state and only return combinations of address which
# differentiate based on city and state.
#
# (In most practical uses, it would just be the city that keeps
# changing).
unique_combinations = OrderedSet([
(node.Address.City.text, node.Address.StateProvinceCode.text)
for node in address_response.AddressValidationResult
])
# This part is sadly static... wish we could verify more than the
# state and city... like the street.
base_address = {
'name': self.name,
'street': self.street,
'streetbis': self.streetbis,
'country': self.country,
'zip': self.zip,
}
matches = []
for city, subdivision_code in unique_combinations:
try:
subdivision, = Subdivision.search([
('code', '=', '%s-%s' % (
self.country.code, subdivision_code
))
])
except ValueError:
# If a unique match cannot be found for the subdivision,
# we wont be able to save the address anyway.
continue
if (self.city.upper() == city.upper()) and \
(self.subdivision == subdivision):
# UPS does not know it, but this is a right address too
# because we are suggesting exactly what is already in the
# address.
return True
matches.append(
Address(city=city, subdivision=subdivision, **base_address)
)
return matches