blob: b8893aa773294a2337e1d35561e3b2aee0dc0460 [file] [log] [blame]
vishdhad3ec1c12020-03-24 11:31:41 +05301import traceback
Chillar Anand915b3432021-09-02 16:44:59 +05302
vishdhad3ec1c12020-03-24 11:31:41 +05303import frappe
Subin Tom70049442021-08-31 18:33:16 +05304import taxjar
vishdhad3ec1c12020-03-24 11:31:41 +05305from frappe import _
6from frappe.contacts.doctype.address.address import get_company_address
Subin Tom5c186542021-09-17 00:10:41 +05307from frappe.utils import cint, flt
vishdhad3ec1c12020-03-24 11:31:41 +05308
Subin Tomd2915c62021-11-08 17:59:03 +05309from erpnext import get_default_company, get_region
Chillar Anand915b3432021-09-02 16:44:59 +053010
Ankush Menat494bd9e2022-03-28 18:52:46 +053011SUPPORTED_COUNTRY_CODES = [
12 "AT",
13 "AU",
14 "BE",
15 "BG",
16 "CA",
17 "CY",
18 "CZ",
19 "DE",
20 "DK",
21 "EE",
22 "ES",
23 "FI",
24 "FR",
25 "GB",
26 "GR",
27 "HR",
28 "HU",
29 "IE",
30 "IT",
31 "LT",
32 "LU",
33 "LV",
34 "MT",
35 "NL",
36 "PL",
37 "PT",
38 "RO",
39 "SE",
40 "SI",
41 "SK",
42 "US",
43]
44SUPPORTED_STATE_CODES = [
45 "AL",
46 "AK",
47 "AZ",
48 "AR",
49 "CA",
50 "CO",
51 "CT",
52 "DE",
53 "DC",
54 "FL",
55 "GA",
56 "HI",
57 "ID",
58 "IL",
59 "IN",
60 "IA",
61 "KS",
62 "KY",
63 "LA",
64 "ME",
65 "MD",
66 "MA",
67 "MI",
68 "MN",
69 "MS",
70 "MO",
71 "MT",
72 "NE",
73 "NV",
74 "NH",
75 "NJ",
76 "NM",
77 "NY",
78 "NC",
79 "ND",
80 "OH",
81 "OK",
82 "OR",
83 "PA",
84 "RI",
85 "SC",
86 "SD",
87 "TN",
88 "TX",
89 "UT",
90 "VT",
91 "VA",
92 "WA",
93 "WV",
94 "WI",
95 "WY",
96]
vishdhad3ec1c12020-03-24 11:31:41 +053097
98
99def get_client():
100 taxjar_settings = frappe.get_single("TaxJar Settings")
101
102 if not taxjar_settings.is_sandbox:
103 api_key = taxjar_settings.api_key and taxjar_settings.get_password("api_key")
104 api_url = taxjar.DEFAULT_API_URL
105 else:
106 api_key = taxjar_settings.sandbox_api_key and taxjar_settings.get_password("sandbox_api_key")
107 api_url = taxjar.SANDBOX_API_URL
108
109 if api_key and api_url:
Subin Tom70049442021-08-31 18:33:16 +0530110 client = taxjar.Client(api_key=api_key, api_url=api_url)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530111 client.set_api_config("headers", {"x-api-version": "2022-01-24"})
Subin Tom70049442021-08-31 18:33:16 +0530112 return client
vishdhad3ec1c12020-03-24 11:31:41 +0530113
114
115def create_transaction(doc, method):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530116 TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value(
117 "TaxJar Settings", "taxjar_create_transactions"
118 )
Subin Tom1682a262022-02-22 17:20:48 +0530119
vishdhad3ec1c12020-03-24 11:31:41 +0530120 """Create an order transaction in TaxJar"""
121
122 if not TAXJAR_CREATE_TRANSACTIONS:
123 return
124
125 client = get_client()
126
127 if not client:
128 return
129
Subin Tom1682a262022-02-22 17:20:48 +0530130 TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
vishdhad3ec1c12020-03-24 11:31:41 +0530131 sales_tax = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == TAX_ACCOUNT_HEAD])
132
133 if not sales_tax:
134 return
135
136 tax_dict = get_tax_data(doc)
137
138 if not tax_dict:
139 return
140
Ankush Menat494bd9e2022-03-28 18:52:46 +0530141 tax_dict["transaction_id"] = doc.name
142 tax_dict["transaction_date"] = frappe.utils.today()
143 tax_dict["sales_tax"] = sales_tax
144 tax_dict["amount"] = doc.total + tax_dict["shipping"]
vishdhad3ec1c12020-03-24 11:31:41 +0530145
146 try:
Subin Tom70049442021-08-31 18:33:16 +0530147 if doc.is_return:
148 client.create_refund(tax_dict)
Ankush Menatb147b852021-09-01 16:45:57 +0530149 else:
Subin Tom70049442021-08-31 18:33:16 +0530150 client.create_order(tax_dict)
vishdhad3ec1c12020-03-24 11:31:41 +0530151 except taxjar.exceptions.TaxJarResponseError as err:
152 frappe.throw(_(sanitize_error_response(err)))
153 except Exception as ex:
154 print(traceback.format_exc(ex))
155
156
157def delete_transaction(doc, method):
158 """Delete an existing TaxJar order transaction"""
Ankush Menat494bd9e2022-03-28 18:52:46 +0530159 TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value(
160 "TaxJar Settings", "taxjar_create_transactions"
161 )
vishdhad3ec1c12020-03-24 11:31:41 +0530162
163 if not TAXJAR_CREATE_TRANSACTIONS:
164 return
165
166 client = get_client()
167
168 if not client:
169 return
170
171 client.delete_order(doc.name)
172
173
174def get_tax_data(doc):
Subin Tom1682a262022-02-22 17:20:48 +0530175 SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head")
176
vishdhad3ec1c12020-03-24 11:31:41 +0530177 from_address = get_company_address_details(doc)
178 from_shipping_state = from_address.get("state")
179 from_country_code = frappe.db.get_value("Country", from_address.country, "code")
180 from_country_code = from_country_code.upper()
181
182 to_address = get_shipping_address_details(doc)
183 to_shipping_state = to_address.get("state")
184 to_country_code = frappe.db.get_value("Country", to_address.country, "code")
185 to_country_code = to_country_code.upper()
186
vishdhad3ec1c12020-03-24 11:31:41 +0530187 shipping = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == SHIP_ACCOUNT_HEAD])
188
Subin Tom0e527312021-09-16 14:41:38 +0530189 line_items = [get_line_item_dict(item, doc.docstatus) for item in doc.items]
vishdhad3ec1c12020-03-24 11:31:41 +0530190
Subin Tom70049442021-08-31 18:33:16 +0530191 if from_shipping_state not in SUPPORTED_STATE_CODES:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530192 from_shipping_state = get_state_code(from_address, "Company")
Subin Tom70049442021-08-31 18:33:16 +0530193
194 if to_shipping_state not in SUPPORTED_STATE_CODES:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530195 to_shipping_state = get_state_code(to_address, "Shipping")
Ankush Menatb147b852021-09-01 16:45:57 +0530196
vishdhad3ec1c12020-03-24 11:31:41 +0530197 tax_dict = {
Subin Tom1682a262022-02-22 17:20:48 +0530198 "from_country": from_country_code,
199 "from_zip": from_address.pincode,
200 "from_state": from_shipping_state,
201 "from_city": from_address.city,
202 "from_street": from_address.address_line1,
203 "to_country": to_country_code,
204 "to_zip": to_address.pincode,
205 "to_city": to_address.city,
206 "to_street": to_address.address_line1,
207 "to_state": to_shipping_state,
208 "shipping": shipping,
209 "amount": doc.net_total,
210 "plugin": "erpnext",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530211 "line_items": line_items,
vishdhad3ec1c12020-03-24 11:31:41 +0530212 }
Ankush Menatb147b852021-09-01 16:45:57 +0530213 return tax_dict
vishdhad3ec1c12020-03-24 11:31:41 +0530214
Ankush Menat494bd9e2022-03-28 18:52:46 +0530215
Subin Tom70049442021-08-31 18:33:16 +0530216def get_state_code(address, location):
217 if address is not None:
218 state_code = get_iso_3166_2_state_code(address)
219 if state_code not in SUPPORTED_STATE_CODES:
220 frappe.throw(_("Please enter a valid State in the {0} Address").format(location))
221 else:
222 frappe.throw(_("Please enter a valid State in the {0} Address").format(location))
Ankush Menatb147b852021-09-01 16:45:57 +0530223
Subin Tom70049442021-08-31 18:33:16 +0530224 return state_code
vishdhad3ec1c12020-03-24 11:31:41 +0530225
Ankush Menat494bd9e2022-03-28 18:52:46 +0530226
Subin Tom3bb60a42021-09-14 22:04:57 +0530227def get_line_item_dict(item, docstatus):
228 tax_dict = dict(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530229 id=item.get("idx"),
230 quantity=item.get("qty"),
231 unit_price=item.get("rate"),
232 product_tax_code=item.get("product_tax_category"),
Ankush Menatb147b852021-09-01 16:45:57 +0530233 )
vishdhad3ec1c12020-03-24 11:31:41 +0530234
Subin Tom3bb60a42021-09-14 22:04:57 +0530235 if docstatus == 1:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530236 tax_dict.update({"sales_tax": item.get("tax_collectable")})
Subin Tom3bb60a42021-09-14 22:04:57 +0530237
238 return tax_dict
239
Ankush Menat494bd9e2022-03-28 18:52:46 +0530240
vishdhad3ec1c12020-03-24 11:31:41 +0530241def set_sales_tax(doc, method):
Subin Tom1682a262022-02-22 17:20:48 +0530242 TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
243 TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
244
vishdhad3ec1c12020-03-24 11:31:41 +0530245 if not TAXJAR_CALCULATE_TAX:
246 return
247
Ankush Menat494bd9e2022-03-28 18:52:46 +0530248 if get_region(doc.company) != "United States":
Subin Tome51c4ba2021-11-08 15:16:20 +0530249 return
250
vishdhad3ec1c12020-03-24 11:31:41 +0530251 if not doc.items:
252 return
253
Subin Tom70049442021-08-31 18:33:16 +0530254 if check_sales_tax_exemption(doc):
vishdhad3ec1c12020-03-24 11:31:41 +0530255 return
256
257 tax_dict = get_tax_data(doc)
258
259 if not tax_dict:
260 # Remove existing tax rows if address is changed from a taxable state/country
261 setattr(doc, "taxes", [tax for tax in doc.taxes if tax.account_head != TAX_ACCOUNT_HEAD])
262 return
263
Subin Tomb01fe1c2021-09-14 20:42:47 +0530264 # check if delivering within a nexus
Subin Tom5c186542021-09-17 00:10:41 +0530265 check_for_nexus(doc, tax_dict)
Subin Tomb01fe1c2021-09-14 20:42:47 +0530266
vishdhad3ec1c12020-03-24 11:31:41 +0530267 tax_data = validate_tax_request(tax_dict)
vishdhad3ec1c12020-03-24 11:31:41 +0530268 if tax_data is not None:
269 if not tax_data.amount_to_collect:
270 setattr(doc, "taxes", [tax for tax in doc.taxes if tax.account_head != TAX_ACCOUNT_HEAD])
271 elif tax_data.amount_to_collect > 0:
272 # Loop through tax rows for existing Sales Tax entry
273 # If none are found, add a row with the tax amount
274 for tax in doc.taxes:
275 if tax.account_head == TAX_ACCOUNT_HEAD:
276 tax.tax_amount = tax_data.amount_to_collect
277
278 doc.run_method("calculate_taxes_and_totals")
279 break
280 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530281 doc.append(
282 "taxes",
283 {
284 "charge_type": "Actual",
285 "description": "Sales Tax",
286 "account_head": TAX_ACCOUNT_HEAD,
287 "tax_amount": tax_data.amount_to_collect,
288 },
289 )
Subin Tom70049442021-08-31 18:33:16 +0530290 # Assigning values to tax_collectable and taxable_amount fields in sales item table
291 for item in tax_data.breakdown.line_items:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530292 doc.get("items")[cint(item.id) - 1].tax_collectable = item.tax_collectable
293 doc.get("items")[cint(item.id) - 1].taxable_amount = item.taxable_amount
vishdhad3ec1c12020-03-24 11:31:41 +0530294
295 doc.run_method("calculate_taxes_and_totals")
296
Ankush Menat494bd9e2022-03-28 18:52:46 +0530297
Subin Tom5c186542021-09-17 00:10:41 +0530298def check_for_nexus(doc, tax_dict):
Subin Tom1682a262022-02-22 17:20:48 +0530299 TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530300 if not frappe.db.get_value("TaxJar Nexus", {"region_code": tax_dict["to_state"]}):
Subin Tom5c186542021-09-17 00:10:41 +0530301 for item in doc.get("items"):
302 item.tax_collectable = flt(0)
303 item.taxable_amount = flt(0)
304
305 for tax in doc.taxes:
306 if tax.account_head == TAX_ACCOUNT_HEAD:
307 doc.taxes.remove(tax)
308 return
309
Ankush Menat494bd9e2022-03-28 18:52:46 +0530310
Subin Tom70049442021-08-31 18:33:16 +0530311def check_sales_tax_exemption(doc):
312 # if the party is exempt from sales tax, then set all tax account heads to zero
Subin Tom1682a262022-02-22 17:20:48 +0530313 TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
314
Ankush Menat494bd9e2022-03-28 18:52:46 +0530315 sales_tax_exempted = (
316 hasattr(doc, "exempt_from_sales_tax")
317 and doc.exempt_from_sales_tax
318 or frappe.db.has_column("Customer", "exempt_from_sales_tax")
Subin Tom70049442021-08-31 18:33:16 +0530319 and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530320 )
Subin Tom70049442021-08-31 18:33:16 +0530321
322 if sales_tax_exempted:
323 for tax in doc.taxes:
324 if tax.account_head == TAX_ACCOUNT_HEAD:
325 tax.tax_amount = 0
326 break
327 doc.run_method("calculate_taxes_and_totals")
328 return True
Ankush Menatb147b852021-09-01 16:45:57 +0530329 else:
Subin Tom70049442021-08-31 18:33:16 +0530330 return False
vishdhad3ec1c12020-03-24 11:31:41 +0530331
Ankush Menat494bd9e2022-03-28 18:52:46 +0530332
vishdhad3ec1c12020-03-24 11:31:41 +0530333def validate_tax_request(tax_dict):
334 """Return the sales tax that should be collected for a given order."""
335
336 client = get_client()
337
338 if not client:
339 return
340
341 try:
342 tax_data = client.tax_for_order(tax_dict)
343 except taxjar.exceptions.TaxJarResponseError as err:
344 frappe.throw(_(sanitize_error_response(err)))
345 else:
346 return tax_data
347
348
349def get_company_address_details(doc):
350 """Return default company address details"""
351
352 company_address = get_company_address(get_default_company()).company_address
353
354 if not company_address:
355 frappe.throw(_("Please set a default company address"))
356
357 company_address = frappe.get_doc("Address", company_address)
358 return company_address
359
360
361def get_shipping_address_details(doc):
362 """Return customer shipping address details"""
363
364 if doc.shipping_address_name:
365 shipping_address = frappe.get_doc("Address", doc.shipping_address_name)
Subin Tom70049442021-08-31 18:33:16 +0530366 elif doc.customer_address:
Subin Tom75e91002021-11-08 09:49:11 +0530367 shipping_address = frappe.get_doc("Address", doc.customer_address)
vishdhad3ec1c12020-03-24 11:31:41 +0530368 else:
369 shipping_address = get_company_address_details(doc)
370
371 return shipping_address
372
373
374def get_iso_3166_2_state_code(address):
Deepesh Gargdcb462f2020-08-01 13:47:09 +0530375 import pycountry
Ankush Menat494bd9e2022-03-28 18:52:46 +0530376
vishdhad3ec1c12020-03-24 11:31:41 +0530377 country_code = frappe.db.get_value("Country", address.get("country"), "code")
378
Ankush Menat494bd9e2022-03-28 18:52:46 +0530379 error_message = _(
380 """{0} is not a valid state! Check for typos or enter the ISO code for your state."""
381 ).format(address.get("state"))
vishdhad3ec1c12020-03-24 11:31:41 +0530382 state = address.get("state").upper().strip()
383
384 # The max length for ISO state codes is 3, excluding the country code
385 if len(state) <= 3:
386 # PyCountry returns state code as {country_code}-{state-code} (e.g. US-FL)
387 address_state = (country_code + "-" + state).upper()
388
389 states = pycountry.subdivisions.get(country_code=country_code.upper())
390 states = [pystate.code for pystate in states]
391
392 if address_state in states:
393 return state
394
395 frappe.throw(_(error_message))
396 else:
397 try:
398 lookup_state = pycountry.subdivisions.lookup(state)
399 except LookupError:
400 frappe.throw(_(error_message))
401 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530402 return lookup_state.code.split("-")[1]
vishdhad3ec1c12020-03-24 11:31:41 +0530403
404
405def sanitize_error_response(response):
406 response = response.full_response.get("detail")
407 response = response.replace("_", " ")
408
409 sanitized_responses = {
410 "to zip": "Zipcode",
411 "to city": "City",
412 "to state": "State",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530413 "to country": "Country",
vishdhad3ec1c12020-03-24 11:31:41 +0530414 }
415
416 for k, v in sanitized_responses.items():
417 response = response.replace(k, v)
418
419 return response