25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 
 

420 satır
9.8 KiB

  1. import traceback
  2. import frappe
  3. import taxjar
  4. from frappe import _
  5. from frappe.contacts.doctype.address.address import get_company_address
  6. from frappe.utils import cint, flt
  7. from erpnext import get_default_company, get_region
  8. SUPPORTED_COUNTRY_CODES = [
  9. "AT",
  10. "AU",
  11. "BE",
  12. "BG",
  13. "CA",
  14. "CY",
  15. "CZ",
  16. "DE",
  17. "DK",
  18. "EE",
  19. "ES",
  20. "FI",
  21. "FR",
  22. "GB",
  23. "GR",
  24. "HR",
  25. "HU",
  26. "IE",
  27. "IT",
  28. "LT",
  29. "LU",
  30. "LV",
  31. "MT",
  32. "NL",
  33. "PL",
  34. "PT",
  35. "RO",
  36. "SE",
  37. "SI",
  38. "SK",
  39. "US",
  40. ]
  41. SUPPORTED_STATE_CODES = [
  42. "AL",
  43. "AK",
  44. "AZ",
  45. "AR",
  46. "CA",
  47. "CO",
  48. "CT",
  49. "DE",
  50. "DC",
  51. "FL",
  52. "GA",
  53. "HI",
  54. "ID",
  55. "IL",
  56. "IN",
  57. "IA",
  58. "KS",
  59. "KY",
  60. "LA",
  61. "ME",
  62. "MD",
  63. "MA",
  64. "MI",
  65. "MN",
  66. "MS",
  67. "MO",
  68. "MT",
  69. "NE",
  70. "NV",
  71. "NH",
  72. "NJ",
  73. "NM",
  74. "NY",
  75. "NC",
  76. "ND",
  77. "OH",
  78. "OK",
  79. "OR",
  80. "PA",
  81. "RI",
  82. "SC",
  83. "SD",
  84. "TN",
  85. "TX",
  86. "UT",
  87. "VT",
  88. "VA",
  89. "WA",
  90. "WV",
  91. "WI",
  92. "WY",
  93. ]
  94. def get_client():
  95. taxjar_settings = frappe.get_single("TaxJar Settings")
  96. if not taxjar_settings.is_sandbox:
  97. api_key = taxjar_settings.api_key and taxjar_settings.get_password("api_key")
  98. api_url = taxjar.DEFAULT_API_URL
  99. else:
  100. api_key = taxjar_settings.sandbox_api_key and taxjar_settings.get_password("sandbox_api_key")
  101. api_url = taxjar.SANDBOX_API_URL
  102. if api_key and api_url:
  103. client = taxjar.Client(api_key=api_key, api_url=api_url)
  104. client.set_api_config("headers", {"x-api-version": "2022-01-24"})
  105. return client
  106. def create_transaction(doc, method):
  107. TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value(
  108. "TaxJar Settings", "taxjar_create_transactions"
  109. )
  110. """Create an order transaction in TaxJar"""
  111. if not TAXJAR_CREATE_TRANSACTIONS:
  112. return
  113. client = get_client()
  114. if not client:
  115. return
  116. TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
  117. sales_tax = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == TAX_ACCOUNT_HEAD])
  118. if not sales_tax:
  119. return
  120. tax_dict = get_tax_data(doc)
  121. if not tax_dict:
  122. return
  123. tax_dict["transaction_id"] = doc.name
  124. tax_dict["transaction_date"] = frappe.utils.today()
  125. tax_dict["sales_tax"] = sales_tax
  126. tax_dict["amount"] = doc.total + tax_dict["shipping"]
  127. try:
  128. if doc.is_return:
  129. client.create_refund(tax_dict)
  130. else:
  131. client.create_order(tax_dict)
  132. except taxjar.exceptions.TaxJarResponseError as err:
  133. frappe.throw(_(sanitize_error_response(err)))
  134. except Exception as ex:
  135. print(traceback.format_exc(ex))
  136. def delete_transaction(doc, method):
  137. """Delete an existing TaxJar order transaction"""
  138. TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value(
  139. "TaxJar Settings", "taxjar_create_transactions"
  140. )
  141. if not TAXJAR_CREATE_TRANSACTIONS:
  142. return
  143. client = get_client()
  144. if not client:
  145. return
  146. client.delete_order(doc.name)
  147. def get_tax_data(doc):
  148. SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head")
  149. from_address = get_company_address_details(doc)
  150. from_shipping_state = from_address.get("state")
  151. from_country_code = frappe.db.get_value("Country", from_address.country, "code")
  152. from_country_code = from_country_code.upper()
  153. to_address = get_shipping_address_details(doc)
  154. to_shipping_state = to_address.get("state")
  155. to_country_code = frappe.db.get_value("Country", to_address.country, "code")
  156. to_country_code = to_country_code.upper()
  157. shipping = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == SHIP_ACCOUNT_HEAD])
  158. line_items = [get_line_item_dict(item, doc.docstatus) for item in doc.items]
  159. if from_shipping_state not in SUPPORTED_STATE_CODES:
  160. from_shipping_state = get_state_code(from_address, "Company")
  161. if to_shipping_state not in SUPPORTED_STATE_CODES:
  162. to_shipping_state = get_state_code(to_address, "Shipping")
  163. tax_dict = {
  164. "from_country": from_country_code,
  165. "from_zip": from_address.pincode,
  166. "from_state": from_shipping_state,
  167. "from_city": from_address.city,
  168. "from_street": from_address.address_line1,
  169. "to_country": to_country_code,
  170. "to_zip": to_address.pincode,
  171. "to_city": to_address.city,
  172. "to_street": to_address.address_line1,
  173. "to_state": to_shipping_state,
  174. "shipping": shipping,
  175. "amount": doc.net_total,
  176. "plugin": "erpnext",
  177. "line_items": line_items,
  178. }
  179. return tax_dict
  180. def get_state_code(address, location):
  181. if address is not None:
  182. state_code = get_iso_3166_2_state_code(address)
  183. if state_code not in SUPPORTED_STATE_CODES:
  184. frappe.throw(_("Please enter a valid State in the {0} Address").format(location))
  185. else:
  186. frappe.throw(_("Please enter a valid State in the {0} Address").format(location))
  187. return state_code
  188. def get_line_item_dict(item, docstatus):
  189. tax_dict = dict(
  190. id=item.get("idx"),
  191. quantity=item.get("qty"),
  192. unit_price=item.get("rate"),
  193. product_tax_code=item.get("product_tax_category"),
  194. )
  195. if docstatus == 1:
  196. tax_dict.update({"sales_tax": item.get("tax_collectable")})
  197. return tax_dict
  198. def set_sales_tax(doc, method):
  199. TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
  200. TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
  201. if not TAXJAR_CALCULATE_TAX:
  202. return
  203. if get_region(doc.company) != "United States":
  204. return
  205. if not doc.items:
  206. return
  207. if check_sales_tax_exemption(doc):
  208. return
  209. tax_dict = get_tax_data(doc)
  210. if not tax_dict:
  211. # Remove existing tax rows if address is changed from a taxable state/country
  212. setattr(doc, "taxes", [tax for tax in doc.taxes if tax.account_head != TAX_ACCOUNT_HEAD])
  213. return
  214. # check if delivering within a nexus
  215. check_for_nexus(doc, tax_dict)
  216. tax_data = validate_tax_request(tax_dict)
  217. if tax_data is not None:
  218. if not tax_data.amount_to_collect:
  219. setattr(doc, "taxes", [tax for tax in doc.taxes if tax.account_head != TAX_ACCOUNT_HEAD])
  220. elif tax_data.amount_to_collect > 0:
  221. # Loop through tax rows for existing Sales Tax entry
  222. # If none are found, add a row with the tax amount
  223. for tax in doc.taxes:
  224. if tax.account_head == TAX_ACCOUNT_HEAD:
  225. tax.tax_amount = tax_data.amount_to_collect
  226. doc.run_method("calculate_taxes_and_totals")
  227. break
  228. else:
  229. doc.append(
  230. "taxes",
  231. {
  232. "charge_type": "Actual",
  233. "description": "Sales Tax",
  234. "account_head": TAX_ACCOUNT_HEAD,
  235. "tax_amount": tax_data.amount_to_collect,
  236. },
  237. )
  238. # Assigning values to tax_collectable and taxable_amount fields in sales item table
  239. for item in tax_data.breakdown.line_items:
  240. doc.get("items")[cint(item.id) - 1].tax_collectable = item.tax_collectable
  241. doc.get("items")[cint(item.id) - 1].taxable_amount = item.taxable_amount
  242. doc.run_method("calculate_taxes_and_totals")
  243. def check_for_nexus(doc, tax_dict):
  244. TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
  245. if not frappe.db.get_value("TaxJar Nexus", {"region_code": tax_dict["to_state"]}):
  246. for item in doc.get("items"):
  247. item.tax_collectable = flt(0)
  248. item.taxable_amount = flt(0)
  249. for tax in list(doc.taxes):
  250. if tax.account_head == TAX_ACCOUNT_HEAD:
  251. doc.taxes.remove(tax)
  252. return
  253. def check_sales_tax_exemption(doc):
  254. # if the party is exempt from sales tax, then set all tax account heads to zero
  255. TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
  256. sales_tax_exempted = (
  257. hasattr(doc, "exempt_from_sales_tax")
  258. and doc.exempt_from_sales_tax
  259. or frappe.db.has_column("Customer", "exempt_from_sales_tax")
  260. and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax")
  261. )
  262. if sales_tax_exempted:
  263. for tax in doc.taxes:
  264. if tax.account_head == TAX_ACCOUNT_HEAD:
  265. tax.tax_amount = 0
  266. break
  267. doc.run_method("calculate_taxes_and_totals")
  268. return True
  269. else:
  270. return False
  271. def validate_tax_request(tax_dict):
  272. """Return the sales tax that should be collected for a given order."""
  273. client = get_client()
  274. if not client:
  275. return
  276. try:
  277. tax_data = client.tax_for_order(tax_dict)
  278. except taxjar.exceptions.TaxJarResponseError as err:
  279. frappe.throw(_(sanitize_error_response(err)))
  280. else:
  281. return tax_data
  282. def get_company_address_details(doc):
  283. """Return default company address details"""
  284. company_address = get_company_address(get_default_company()).company_address
  285. if not company_address:
  286. frappe.throw(_("Please set a default company address"))
  287. company_address = frappe.get_doc("Address", company_address)
  288. return company_address
  289. def get_shipping_address_details(doc):
  290. """Return customer shipping address details"""
  291. if doc.shipping_address_name:
  292. shipping_address = frappe.get_doc("Address", doc.shipping_address_name)
  293. elif doc.customer_address:
  294. shipping_address = frappe.get_doc("Address", doc.customer_address)
  295. else:
  296. shipping_address = get_company_address_details(doc)
  297. return shipping_address
  298. def get_iso_3166_2_state_code(address):
  299. import pycountry
  300. country_code = frappe.db.get_value("Country", address.get("country"), "code")
  301. error_message = _(
  302. """{0} is not a valid state! Check for typos or enter the ISO code for your state."""
  303. ).format(address.get("state"))
  304. state = address.get("state").upper().strip()
  305. # The max length for ISO state codes is 3, excluding the country code
  306. if len(state) <= 3:
  307. # PyCountry returns state code as {country_code}-{state-code} (e.g. US-FL)
  308. address_state = (country_code + "-" + state).upper()
  309. states = pycountry.subdivisions.get(country_code=country_code.upper())
  310. states = [pystate.code for pystate in states]
  311. if address_state in states:
  312. return state
  313. frappe.throw(_(error_message))
  314. else:
  315. try:
  316. lookup_state = pycountry.subdivisions.lookup(state)
  317. except LookupError:
  318. frappe.throw(_(error_message))
  319. else:
  320. return lookup_state.code.split("-")[1]
  321. def sanitize_error_response(response):
  322. response = response.full_response.get("detail")
  323. response = response.replace("_", " ")
  324. sanitized_responses = {
  325. "to zip": "Zipcode",
  326. "to city": "City",
  327. "to state": "State",
  328. "to country": "Country",
  329. }
  330. for k, v in sanitized_responses.items():
  331. response = response.replace(k, v)
  332. return response