timesheet source code
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

461 line
17KB

  1. <?php
  2. /* xero integration oauth 2 */
  3. /* required by XeroOauth1 in 2021 */
  4. namespace Biukop;
  5. require_once(dirname(__FILE__) . '/vendor/autoload.php');
  6. // require_once(ABSPATH . 'wp-includes/pluggable.php');
  7. require_once(dirname(__FILE__) . '/Storage.php');
  8. use \Carbon_Fields\Container;
  9. use \Carbon_Fields\Field;
  10. use \Carbon_Fields\Carbon_Fields;
  11. use phpDocumentor\Reflection\DocBlock\Tags\Method;
  12. class XeroOAuth2
  13. {
  14. private $office; // parent
  15. private $clientID = '83CC79EEC6A54B4E8C2CA7AD61D1BF69';
  16. private $clientSecret = 'axgKF-Ri60D89conDFhqZsi1wu7uLdQFGvMpino9nI-nfO3f';
  17. private $clientContactGroupID="48646f3d-cf5e-4fea-8c8b-5812bd540e1b";
  18. private $shortcodes;
  19. public $provider;
  20. public $options = [
  21. 'scope' => ['openid email profile offline_access assets projects accounting.settings accounting.transactions accounting.contacts accounting.journals.read accounting.reports.read accounting.attachments payroll.employees payroll.payruns payroll.payslip payroll.timesheets payroll.settings files'],
  22. 'scope1' => [
  23. 'openid',
  24. 'email',
  25. 'profile',
  26. 'offline_access',
  27. 'assets',
  28. 'projects',
  29. 'accounting.settings',
  30. 'accounting.transactions',
  31. 'accounting.contacts',
  32. 'accounting.journals.read',
  33. 'accounting.reports.read',
  34. 'accounting.attachments',
  35. 'payroll.employees',
  36. 'payroll.payruns',
  37. 'payroll.payslip',
  38. 'payroll.timesheets',
  39. 'payroll.settings'
  40. ]
  41. ];
  42. public $storage;
  43. public $config;
  44. public $apiAccountingInstance; //accounting instance
  45. public $apiPayrollInstance; // payroll au instance
  46. public $xeroTenantId;
  47. public function __construct($office)
  48. {
  49. $this->office = $office;
  50. $this->shortcodes = new XeroOauth2ShortCode($this);
  51. $this->storage = new StorageClass();
  52. add_action('init', array($this, 'init'));
  53. add_action( 'plugins_loaded', array( '\\Carbon_Fields\\Carbon_Fields', 'boot' ) );
  54. add_action('carbon_fields_register_fields', array($this, 'build_settings_page'));
  55. add_action('carbon_fields_container_activated', array($this, 'field_activated'));
  56. add_action('parse_request', array($this, 'xero_callback'));
  57. }
  58. public function __call($method, $args) {
  59. if ( method_exists($this->XeroOauth1, $method) ) {
  60. syslog(LOG_INFO,"Calling $method");
  61. $this->XeroOauth1->$method($args);
  62. } else {
  63. error_log("$method is not defined" );
  64. }
  65. }
  66. public function init()
  67. {
  68. $this->provider = $this->create_provider();
  69. $this->init_wp();
  70. }
  71. public function getTenantId() {
  72. return $this->xeroTenantId;
  73. }
  74. private function create_provider() {
  75. $provider = new \League\OAuth2\Client\Provider\GenericProvider([
  76. 'clientId' => $this->clientID,
  77. 'clientSecret' => $this->clientSecret,
  78. 'redirectUri' => $this->getRedirectURL(),
  79. 'urlAuthorize' => 'https://login.xero.com/identity/connect/authorize',
  80. 'urlAccessToken' => 'https://identity.xero.com/connect/token',
  81. 'urlResourceOwnerDetails' => 'https://api.xero.com/api.xro/2.0/Organisation'
  82. ]);
  83. return $provider;
  84. }
  85. public function load_storage()
  86. {
  87. //$this->storage->read_value();
  88. }
  89. public function boot_carbon()
  90. {
  91. \Carbon_Fields\Carbon_Fields::boot();
  92. }
  93. function build_settings_page()
  94. {
  95. Container::make('theme_options', __('Xero Integration'))
  96. ->set_page_parent('options-general.php')
  97. ->add_fields(array(
  98. Field::make('text', 'xero_oauth2state', 'Xero Oauth2 State')
  99. ->set_attribute('maxLength', 2048)
  100. ->set_attribute('readOnly', true)
  101. ->set_default_value($this->storage->getOauth2State()),
  102. Field::make('text', 'xero_token', 'Xero Token')
  103. ->set_attribute('maxLength', 2048)
  104. ->set_attribute('readOnly', true)
  105. ->set_default_value($this->storage->getSession()['token']),
  106. Field::make('text', 'xero_refresh_token', 'RefreshToken')
  107. ->set_attribute('readOnly', true)
  108. ->set_default_value($this->storage->getSession()['refresh_token']),
  109. Field::make('text', 'xero_id_token', 'ID Token')
  110. ->set_attribute('readOnly', true)
  111. ->set_default_value($this->storage->getSession()['id_token']),
  112. Field::make('text', 'xero_tenant_id', 'Tenant ID')
  113. ->set_attribute('readOnly', true)
  114. ->set_default_value($this->storage->getSession()['tenant_id']),
  115. Field::make('text', 'xero_expires', 'Expires')
  116. ->set_attribute('readOnly', true)
  117. ->set_default_value($this->storage->getSession()['expires']),
  118. Field::make('text', 'xero_expires_human', 'Expires')
  119. ->set_attribute('readOnly', true)
  120. ->set_default_value($this->storage->getSession()['expires_human']),
  121. Field::make('html', 'crb_information_text')
  122. ->set_html('<h1>Connect/Reconnect</h2><p>if the above field is empty,
  123. or the expire date looks suspicous, please reconnect to XeroOauth1 </p>
  124. <form action="/">
  125. <input type="hidden" name="xero_reauth" value="1" />
  126. <input type="submit" class="button button-primary button-large" value="Xero Connect" />
  127. </form>')
  128. ));
  129. $this->storage->read_value();
  130. }
  131. function field_activated()
  132. {
  133. $this->storage->read_value();
  134. $xeroTenantId = (string)$this->storage->getSession()['tenant_id'];
  135. if ($xeroTenantId == "") {
  136. $this->startAuthorization();
  137. } else {
  138. $this->refresh_token();
  139. }
  140. }
  141. public function startAuthorization()
  142. {
  143. // This returns the authorizeUrl with necessary parameters applied (e.g. state).
  144. $authorizationUrl = $this->provider->getAuthorizationUrl($this->options);
  145. // Save the state generated for you and store it to the session.
  146. // For security, on callback we compare the saved state with the one returned to ensure they match.
  147. $this->storage->setOauth2State($this->provider->getState());
  148. // Redirect the user to the authorization URL.
  149. header('Location: ' . $authorizationUrl);
  150. exit();
  151. }
  152. function xero_callback()
  153. {
  154. if (isset($_GET['xero_reauth'])) {
  155. $this->startAuthorization();
  156. return;
  157. }
  158. if (!isset($_GET['xero_callback'])) {
  159. return;
  160. }
  161. // If we don't have an authorization code then get one
  162. if (!isset($_GET['code'])) {
  163. echo "Something went wrong, no authorization code found";
  164. exit("Something went wrong, no authorization code found");
  165. // Check given state against previously stored one to mitigate CSRF attack
  166. } elseif (empty($_GET['state']) || ($_GET['state'] !== $this->storage->getOauth2State())) {
  167. echo "Invalid State";
  168. $this->storage->setOauth2State("");
  169. exit('Invalid state');
  170. } else {
  171. try {
  172. // Try to get an access token using the authorization code grant.
  173. $accessToken = $this->provider->getAccessToken('authorization_code', [
  174. 'code' => $_GET['code']
  175. ]);
  176. $config = \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string)$accessToken->getToken());
  177. $identityInstance = new \XeroAPI\XeroPHP\Api\IdentityApi(
  178. new \GuzzleHttp\Client(),
  179. $config
  180. );
  181. $result = $identityInstance->getConnections();
  182. // Save my tokens, expiration tenant_id
  183. $this->storage->setToken(
  184. $accessToken->getToken(),
  185. $accessToken->getExpires(),
  186. $result[0]->getTenantId(),
  187. $accessToken->getRefreshToken(),
  188. $accessToken->getValues()["id_token"]
  189. );
  190. // related to $this->build_settings_page
  191. $url = "/wp-admin/options-general.php?page=crb_carbon_fields_container_xero_integration.php";
  192. header('Location: ' . $url);
  193. exit();
  194. } catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {
  195. echo "Xero Callback failed";
  196. exit();
  197. }
  198. }
  199. }
  200. private function getRedirectURL() {
  201. return get_site_url() . "/?xero_callback";
  202. }
  203. function refresh_token($enforced=false)
  204. {
  205. $this->storage->read_value();
  206. $this->xeroTenantId = (string)$this->storage->getSession()['tenant_id'];
  207. if ($this->storage->getHasExpired() || $enforced) {
  208. $this->provider = $this->create_provider();
  209. try {
  210. $newAccessToken = $this->provider->getAccessToken('refresh_token', [
  211. 'refresh_token' => $this->storage->getRefreshToken()
  212. ]);
  213. } catch (\Exception $e) {
  214. $this->startAuthorization();
  215. return;
  216. }
  217. // Save my token, expiration and refresh token
  218. $this->storage->setToken(
  219. $newAccessToken->getToken(),
  220. $newAccessToken->getExpires(),
  221. $this->xeroTenantId,
  222. $newAccessToken->getRefreshToken(),
  223. $newAccessToken->getValues()["id_token"]);
  224. }
  225. }
  226. public function get_accounting_instance() {
  227. $this->refresh_token();
  228. if ($this->apiAccountingInstance == null) {
  229. $this->config = \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken(
  230. (string)$this->storage->getSession()['token']);
  231. $this->apiAccountingInstance = new \XeroAPI\XeroPHP\Api\AccountingApi(
  232. new \GuzzleHttp\Client(),
  233. $this->config
  234. );
  235. }
  236. return $this->apiAccountingInstance;
  237. }
  238. public function get_payroll_au_instance() {
  239. $this->refresh_token();
  240. if ($this->apiPayrollInstance == null) {
  241. $this->config = \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken(
  242. (string)$this->storage->getSession()['token']);
  243. $this->config->setDebug(true);
  244. $this->config->setDebugFile("/home/acaresydneycom/public_html/wp-content/plugins/ts/debug.log");
  245. $this->apiPayrollInstance = new \XeroAPI\XeroPHP\Api\PayrollAuApi(
  246. new \GuzzleHttp\Client(),
  247. $this->config
  248. );
  249. }
  250. return $this->apiPayrollInstance;
  251. }
  252. //
  253. //TS implementation
  254. //
  255. /* sync xero to wp options */
  256. public function init_wp(){
  257. try{
  258. $this->sync_pay_item();
  259. // $this->add_new_client();
  260. // $this->add_new_employee();
  261. $this->sync_payroll_calendar();
  262. }catch(\Exception $e){
  263. $this->office->log("XeroAuth2\\init_wp() has exception", $e->getMessage());
  264. }
  265. }
  266. private function sync_pay_item() {
  267. if ($this->too_close_to_sync_payitem()){
  268. return;
  269. }
  270. $api = $this->get_payroll_au_instance();
  271. $xeroTenantId = $this->xeroTenantId;
  272. $page = 1;
  273. try {
  274. $result = $api->getPayItems($xeroTenantId, null, null, null, $page);
  275. foreach ($result->getPayItems()->getEarningsRates() as $e){
  276. // "EarningsRateID": "34e17d08-237a-4ae2-8115-375d1ff8a9ed",
  277. // "Name": "Overtime Hours (exempt from super)",
  278. // "EarningsType": "OVERTIMEEARNINGS",
  279. // "RateType": "MULTIPLE",
  280. // "AccountCode": "477",
  281. // "Multiplier": 1.5,
  282. // "IsExemptFromTax": true,
  283. // "IsExemptFromSuper": true,
  284. // "AccrueLeave": false,
  285. // "IsReportableAsW1": true,
  286. // "UpdatedDateUTC": "2019-03-16T13:18:19+00:00",
  287. // "CurrentRecord": false
  288. if ($e->getCurrentRecord() == "true"){
  289. $payitem_options[]= array(
  290. 'EarningsRateID' => $e->getEarningsRateID(),
  291. 'Name'=> $e->getName(),
  292. 'EarningsType'=> $e->getEarningstype(),
  293. 'RatePerUnit' => $e->getRatePerUnit(),
  294. 'RateType' => $e->getRateType(),
  295. 'AccountCode' => $e->getAccountCode(),
  296. "Multiplier"=> $e->getMultiplier(),
  297. "IsExemptFromTax" => $e->getIsExemptFromTax(),
  298. "IsExemptFromSuper"=> $e->getIsExemptFromSuper(),
  299. "AccrueLeave" => $e->getAccrueLeave(),
  300. "TypeOfUnits" => $e->getTypeOfUnits(),
  301. "CurrentRecord"=> $e->getCurrentRecord(),
  302. );
  303. }
  304. }
  305. update_option('bts_payitem_earnings_rate', $payitem_options);
  306. update_option('bts_payitem_last_sync', time());
  307. } catch (Exception $e) {
  308. echo 'Exception when calling PayrollAuApi->getPayItems: ', $e->getMessage(), PHP_EOL;
  309. }
  310. }
  311. public function sync_users($mininterval, $employeeonly, $clientsonly){
  312. echo "not implemented "; //TODO;
  313. }
  314. public function sync_payroll_calendar() {
  315. if ($this->too_close_to_sync_payroll_calendar()){
  316. // return;
  317. }
  318. update_option('bts_pay_roll_calendar_last_sync', time());
  319. $pc = $this->get_payroll_calendar();
  320. $start = $pc->getStartDateAsDate()->format('Y-m-d');
  321. $finish = new \DateTime($start);
  322. $finish = $finish->modify("+13 days")->format('Y-m-d');
  323. $paydate = $pc->getPaymentDateAsDate()->format('Y-m-d');
  324. $calendar["start"] = $start;
  325. $calendar["finish"] = $finish;
  326. $calendar["paydate"] = $paydate;
  327. update_option('bts_pay_roll_calendar', $calendar);
  328. }
  329. private function too_close_to_sync_payitem(){
  330. $lastsync = get_option('bts_payitem_last_sync', 0);
  331. $now = time();
  332. $diff = $now - (int) $lastsync;
  333. return $diff < $this->minimum_sync_interval_in_seconds; //default 10 minutes
  334. }
  335. private function too_close_to_add_employee(){
  336. $lastsync = get_option('bts_add_employee_last_sync', 0);
  337. $now = time();
  338. $diff = $now - (int) $lastsync;
  339. return $diff < 1.5 * $this->minimum_sync_interval_in_seconds; //default 1.1 * 10 minutes
  340. }
  341. private function too_close_to_add_client(){
  342. $lastsync = get_option('bts_add_client_last_sync', 0);
  343. $now = time();
  344. $diff = $now - (int) $lastsync;
  345. return $diff < 2.0 * $this->minimum_sync_interval_in_seconds; //default 1.2 * 10 minutes
  346. }
  347. private function too_close_to_sync_payroll_calendar(){
  348. $lastsync = get_option('bts_pay_roll_calendar_last_sync', 0);
  349. $now = time();
  350. $diff = $now - (int) $lastsync;
  351. return $diff < 3.0 * $this->minimum_sync_interval_in_seconds; //default 1.3 * 10 minutes
  352. }
  353. public function getClients($contact_group_id = null) {
  354. if ( $contact_group_id == null ){
  355. $contact_group_id = $this->clientContactGroupID;
  356. }
  357. $apiAcc = $this->get_accounting_instance();
  358. $result = $apiAcc->getContactGroup($this->xeroTenantId, $contact_group_id);
  359. $cg = $result->getContactGroups();
  360. $allClients = $cg[0]->getContacts();
  361. $ifModifiedSince = new \DateTime();
  362. $recent = new \DateInterval("P30D");
  363. $ifModifiedSince->sub($recent);
  364. $allContacts = $apiAcc->getContacts($this->xeroTenantId, $ifModifiedSince);
  365. $ret = [];
  366. foreach ( $allContacts as $ac ) {
  367. //search from within the group
  368. $found = false;
  369. $id = $ac->getContactID();
  370. foreach ($allClients as $client) {
  371. $clientID = $client->getContactID();
  372. if ( $clientID == $id ) {
  373. $found = true;
  374. break;
  375. }
  376. }
  377. if ( $found ) {
  378. $ret[] = $ac;
  379. }
  380. }
  381. return $ret;
  382. }
  383. /*
  384. * operation get_payroll_calendar
  385. * @throws \XeroAPI\XeroPHP\ApiException on non-2xx response
  386. * @throws \InvalidArgumentException
  387. */
  388. public function get_payroll_calendar()
  389. {
  390. $id = "33dc7df5-3060-4d76-b4da-57c20685d77d"; //fortnightly
  391. $api = $this->get_payroll_au_instance();
  392. $calendars = $api->getPayrollCalendar($this->xeroTenantId, $id);
  393. return $calendars[0];
  394. }
  395. }