ultimatepos/Modules/Woocommerce/Utils/WoocommerceUtil.php

1542 lines
63 KiB
PHP

<?php
namespace Modules\Woocommerce\Utils;
use App\Business;
use App\Category;
use App\Contact;
use App\Exceptions\PurchaseSellMismatch;
use App\Product;
use App\TaxRate;
use App\Transaction;
use App\Utils\ContactUtil;
use App\Utils\ProductUtil;
use App\Utils\TransactionUtil;
use App\Utils\Util;
use App\VariationLocationDetails;
use App\VariationTemplate;
use Automattic\WooCommerce\Client;
use DB;
use Modules\Woocommerce\Entities\WoocommerceSyncLog;
use Modules\Woocommerce\Exceptions\WooCommerceError;
class WoocommerceUtil extends Util
{
/**
* All Utils instance.
*/
protected $transactionUtil;
protected $productUtil;
/**
* Constructor
*
* @param ProductUtils $product
* @return void
*/
public function __construct(TransactionUtil $transactionUtil, ProductUtil $productUtil)
{
$this->transactionUtil = $transactionUtil;
$this->productUtil = $productUtil;
}
public function get_api_settings($business_id)
{
$business = Business::find($business_id);
$woocommerce_api_settings = json_decode($business->woocommerce_api_settings);
return $woocommerce_api_settings;
}
private function add_to_skipped_orders($business, $order_id)
{
$business = ! is_object($business) ? Business::find($business) : $business;
$skipped_orders = ! empty($business->woocommerce_skipped_orders) ? json_decode($business->woocommerce_skipped_orders, true) : [];
if (! in_array($order_id, $skipped_orders)) {
$skipped_orders[] = $order_id;
}
$business->woocommerce_skipped_orders = json_encode($skipped_orders);
$business->save();
}
private function remove_from_skipped_orders($business, $order_id)
{
$business = ! is_object($business) ? Business::find($business) : $business;
$skipped_orders = ! empty($business->woocommerce_skipped_orders) ? json_decode($business->woocommerce_skipped_orders, true) : [];
if (in_array($order_id, $skipped_orders)) {
$skipped_orders = array_diff($skipped_orders, [$order_id]);
}
$business->woocommerce_skipped_orders = json_encode($skipped_orders);
$business->save();
}
/**
* Creates Automattic\WooCommerce\Client object
*
* @param int $business_id
* @return obj
*/
public function woo_client($business_id)
{
$woocommerce_api_settings = $this->get_api_settings($business_id);
if (empty($woocommerce_api_settings)) {
throw new WooCommerceError(__('woocommerce::lang.unable_to_connect'));
}
$woocommerce = new Client(
$woocommerce_api_settings->woocommerce_app_url,
$woocommerce_api_settings->woocommerce_consumer_key,
$woocommerce_api_settings->woocommerce_consumer_secret,
[
'wp_api' => true,
'version' => 'wc/v2',
'timeout' => 10000,
'verify_ssl' => false,
]
);
return $woocommerce;
}
public function syncCat($business_id, $data, $type, $new_categories = [])
{
//woocommerce api client object
$woocommerce = $this->woo_client($business_id);
$count = 0;
foreach (array_chunk($data, 99) as $chunked_array) {
$sync_data = [];
$sync_data[$type] = $chunked_array;
//Batch update categories
$response = $woocommerce->post('products/categories/batch', $sync_data);
//update woocommerce_cat_id
if (! empty($response->create)) {
foreach ($response->create as $key => $value) {
$new_category = $new_categories[$count];
if ($value->id != 0) {
$new_category->woocommerce_cat_id = $value->id;
} else {
if (! empty($value->error->data->resource_id)) {
$new_category->woocommerce_cat_id = $value->error->data->resource_id;
}
}
$new_category->save();
$count++;
}
}
}
}
/**
* Synchronizes pos categories with Woocommerce categories
*
* @param int $business_id
* @return void
*/
public function syncCategories($business_id, $user_id)
{
$last_synced = $this->getLastSync($business_id, 'categories', false);
//Update parent categories
$query = Category::where('business_id', $business_id)
->where('category_type', 'product')
->where('parent_id', 0);
//Limit query to last sync
if (! empty($last_synced)) {
$query->where('updated_at', '>', $last_synced);
}
$categories = $query->get();
$category_data = [];
$new_categories = [];
$created_data = [];
$updated_data = [];
foreach ($categories as $category) {
if (empty($category->woocommerce_cat_id)) {
$category_data['create'][] = [
'name' => $category->name,
];
$new_categories[] = $category;
$created_data[] = $category->name;
} else {
$category_data['update'][] = [
'id' => $category->woocommerce_cat_id,
'name' => $category->name,
];
$updated_data[] = $category->name;
}
}
if (! empty($category_data['create'])) {
$this->syncCat($business_id, $category_data['create'], 'create', $new_categories);
}
if (! empty($category_data['update'])) {
$this->syncCat($business_id, $category_data['update'], 'update', $new_categories);
}
//Sync child categories
$query2 = Category::where('business_id', $business_id)
->where('category_type', 'product')
->where('parent_id', '!=', 0);
//Limit query to last sync
if (! empty($last_synced)) {
$query2->where('updated_at', '>', $last_synced);
}
$child_categories = $query2->get();
$cat_id_woocommerce_id = Category::where('business_id', $business_id)
->where('parent_id', 0)
->where('category_type', 'product')
->pluck('woocommerce_cat_id', 'id')
->toArray();
$category_data = [];
$new_categories = [];
foreach ($child_categories as $category) {
if (empty($cat_id_woocommerce_id[$category->parent_id])) {
continue;
}
if (empty($category->woocommerce_cat_id)) {
$category_data['create'][] = [
'name' => $category->name,
'parent' => $cat_id_woocommerce_id[$category->parent_id],
];
$new_categories[] = $category;
$created_data[] = $category->name;
} else {
$category_data['update'][] = [
'id' => $category->woocommerce_cat_id,
'name' => $category->name,
'parent' => $cat_id_woocommerce_id[$category->parent_id],
];
$updated_data[] = $category->name;
}
}
if (! empty($category_data['create'])) {
$this->syncCat($business_id, $category_data['create'], 'create', $new_categories);
}
if (! empty($category_data['update'])) {
$this->syncCat($business_id, $category_data['update'], 'update', $new_categories);
}
//Create log
if (! empty($created_data)) {
$this->createSyncLog($business_id, $user_id, 'categories', 'created', $created_data);
}
if (! empty($updated_data)) {
$this->createSyncLog($business_id, $user_id, 'categories', 'updated', $updated_data);
}
if (empty($created_data) && empty($updated_data)) {
$this->createSyncLog($business_id, $user_id, 'categories');
}
}
/**
* Synchronizes pos products with Woocommerce products
*
* @param int $business_id
* @return void
*/
public function syncProducts($business_id, $user_id, $sync_type, $limit = 100, $page = 0)
{
//$limit is zero for console command
if ($page == 0 || $limit == 0) {
//Sync Categories
$this->syncCategories($business_id, $user_id);
//Sync variation attributes
$this->syncVariationAttributes($business_id);
if ($limit > 0) {
request()->session()->forget('last_product_synced');
}
}
$last_synced = ! empty(session('last_product_synced')) ? session('last_product_synced') : $this->getLastSync($business_id, 'all_products', false);
//store last_synced if page is 0
if ($page == 0) {
session(['last_product_synced' => $last_synced]);
}
$woocommerce_api_settings = $this->get_api_settings($business_id);
$created_data = [];
$updated_data = [];
$business_location_id = $woocommerce_api_settings->location_id;
$offset = $page * $limit;
$query = Product::where('business_id', $business_id)
->whereIn('type', ['single', 'variable'])
->where('woocommerce_disable_sync', 0)
->with(['variations', 'category', 'sub_category',
'variations.variation_location_details',
'variations.product_variation',
'variations.product_variation.variation_template', ]);
if ($limit > 0) {
$query->limit($limit)
->offset($offset);
}
if ($sync_type == 'new') {
$query->whereNull('woocommerce_product_id');
}
//Select products only from selected location
if (! empty($business_location_id)) {
$query->ForLocation($business_location_id);
}
$all_products = $query->get();
$product_data = [];
$new_products = [];
$updated_products = [];
if (count($all_products) == 0) {
request()->session()->forget('last_product_synced');
}
foreach ($all_products as $product) {
//Skip product if last updated is less than last sync
$last_updated = $product->updated_at;
//check last stock updated
$last_stock_updated = $this->getLastStockUpdated($business_location_id, $product->id);
if (! empty($last_stock_updated)) {
$last_updated = strtotime($last_stock_updated) > strtotime($last_updated) ?
$last_stock_updated : $last_updated;
}
if (! empty($product->woocommerce_product_id) && ! empty($last_synced) && strtotime($last_updated) < strtotime($last_synced)) {
continue;
}
//Set common data
$array = [
'type' => $product->type == 'single' ? 'simple' : 'variable',
'sku' => $product->sku,
];
$manage_stock = false;
if ($product->enable_stock == 1 && $product->type == 'single') {
$manage_stock = true;
}
//Get details from first variation for single product only
$first_variation = $product->variations->first();
if (empty($first_variation)) {
continue;
}
$price = $woocommerce_api_settings->product_tax_type == 'exc' ? $first_variation->default_sell_price : $first_variation->sell_price_inc_tax;
if (! empty($woocommerce_api_settings->default_selling_price_group)) {
$group_prices = $this->productUtil->getVariationGroupPrice($first_variation->id, $woocommerce_api_settings->default_selling_price_group, $product->tax_id);
$price = $woocommerce_api_settings->product_tax_type == 'exc' ? $group_prices['price_exc_tax'] : $group_prices['price_inc_tax'];
}
//Set product stock
$qty_available = 0;
if ($manage_stock) {
$variation_location_details = $first_variation->variation_location_details;
foreach ($variation_location_details as $vld) {
if ($vld->location_id == $business_location_id) {
$qty_available = $vld->qty_available;
}
}
}
//Set product category
$product_cat = [];
if (! empty($product->category)) {
$product_cat[] = ['id' => $product->category->woocommerce_cat_id];
}
if (! empty($product->sub_category)) {
$product_cat[] = ['id' => $product->sub_category->woocommerce_cat_id];
}
//set attributes for variable products
if ($product->type == 'variable') {
$variation_attr_data = [];
foreach ($product->variations as $variation) {
if (! empty($variation->product_variation->variation_template->woocommerce_attr_id)) {
$woocommerce_attr_id = $variation->product_variation->variation_template->woocommerce_attr_id;
$variation_attr_data[$woocommerce_attr_id][] = $variation->name;
}
}
foreach ($variation_attr_data as $key => $value) {
$array['attributes'][] = [
'id' => $key,
'variation' => true,
'visible' => true,
'options' => $value,
];
}
}
$sync_description_as = ! empty($woocommerce_api_settings->sync_description_as) ? $woocommerce_api_settings->sync_description_as : 'long';
if (empty($product->woocommerce_product_id)) {
$array['tax_class'] = ! empty($woocommerce_api_settings->default_tax_class) ?
$woocommerce_api_settings->default_tax_class : 'standard';
//assign category
if (in_array('category', $woocommerce_api_settings->product_fields_for_create)) {
if (! empty($product_cat)) {
$array['categories'] = $product_cat;
}
}
if (in_array('weight', $woocommerce_api_settings->product_fields_for_create)) {
$array['weight'] = $this->formatDecimalPoint($product->weight);
}
//sync product description
if (in_array('description', $woocommerce_api_settings->product_fields_for_create)) {
if ($sync_description_as == 'long') {
$array['description'] = $product->product_description;
} elseif ($sync_description_as == 'short') {
$array['short_description'] = $product->product_description;
} else {
$array['description'] = $product->product_description;
$array['short_description'] = $product->product_description;
}
}
//Set product image url
//If media id is set use media id else use image src
if (! empty($product->image) && in_array('image', $woocommerce_api_settings->product_fields_for_create)) {
if ($this->isValidImage($product->image_path)) {
$array['images'] = ! empty($product->woocommerce_media_id) ? [['id' => $product->woocommerce_media_id]] : [['src' => $product->image_url]];
}
}
//assign quantity and price if single product
if ($product->type == 'single') {
$array['manage_stock'] = $manage_stock;
if (in_array('quantity', $woocommerce_api_settings->product_fields_for_create)) {
$array['stock_quantity'] = $this->formatDecimalPoint($qty_available, 'quantity');
} else {
//set manage stock and in_stock if quantity disabled
if (isset($woocommerce_api_settings->manage_stock_for_create)) {
if ($woocommerce_api_settings->manage_stock_for_create == 'true') {
$array['manage_stock'] = true;
} elseif ($woocommerce_api_settings->manage_stock_for_create == 'false') {
$array['manage_stock'] = false;
} else {
unset($array['manage_stock']);
}
}
if (isset($woocommerce_api_settings->in_stock_for_create)) {
if ($woocommerce_api_settings->in_stock_for_create == 'true') {
$array['in_stock'] = true;
} elseif ($woocommerce_api_settings->in_stock_for_create == 'false') {
$array['in_stock'] = false;
}
}
}
$array['regular_price'] = $this->formatDecimalPoint($price);
}
//assign name
$array['name'] = $product->name;
$product_data['create'][] = $array;
$new_products[] = $product;
$created_data[] = $product->sku;
} else {
$array['id'] = $product->woocommerce_product_id;
//assign category
if (in_array('category', $woocommerce_api_settings->product_fields_for_update)) {
if (! empty($product_cat)) {
$array['categories'] = $product_cat;
}
}
if (in_array('weight', $woocommerce_api_settings->product_fields_for_update)) {
$array['weight'] = $this->formatDecimalPoint($product->weight);
}
//sync product description
if (in_array('description', $woocommerce_api_settings->product_fields_for_update)) {
if ($sync_description_as == 'long') {
$array['description'] = $product->product_description;
} elseif ($sync_description_as == 'short') {
$array['short_description'] = $product->product_description;
} else {
$array['description'] = $product->product_description;
$array['short_description'] = $product->product_description;
}
}
//If media id is set use media id else use image src
if (! empty($product->image) && in_array('image', $woocommerce_api_settings->product_fields_for_update)) {
if ($this->isValidImage($product->image_path)) {
$array['images'] = ! empty($product->woocommerce_media_id) ? [['id' => $product->woocommerce_media_id]] : [['src' => $product->image_url]];
}
}
if ($product->type == 'single') {
//assign quantity
$array['manage_stock'] = $manage_stock;
if (in_array('quantity', $woocommerce_api_settings->product_fields_for_update)) {
$array['stock_quantity'] = $this->formatDecimalPoint($qty_available, 'quantity');
} else {
//set manage stock and in_stock if quantity disabled
if (isset($woocommerce_api_settings->manage_stock_for_update)) {
if ($woocommerce_api_settings->manage_stock_for_update == 'true') {
$array['manage_stock'] = true;
} elseif ($woocommerce_api_settings->manage_stock_for_update == 'false') {
$array['manage_stock'] = false;
} else {
unset($array['manage_stock']);
}
}
if (isset($woocommerce_api_settings->in_stock_for_update)) {
if ($woocommerce_api_settings->in_stock_for_update == 'true') {
$array['in_stock'] = true;
} elseif ($woocommerce_api_settings->in_stock_for_update == 'false') {
$array['in_stock'] = false;
}
}
}
//assign price
if (in_array('price', $woocommerce_api_settings->product_fields_for_update)) {
$array['regular_price'] = $this->formatDecimalPoint($price);
}
}
//assign name
if (in_array('name', $woocommerce_api_settings->product_fields_for_update)) {
$array['name'] = $product->name;
}
$product_data['update'][] = $array;
$updated_data[] = $product->sku;
$updated_products[] = $product;
}
}
$create_response = [];
$update_response = [];
if (! empty($product_data['create'])) {
$create_response = $this->syncProd($business_id, $product_data['create'], 'create', $new_products);
}
if (! empty($product_data['update'])) {
$update_response = $this->syncProd($business_id, $product_data['update'], 'update', $updated_products);
}
$new_woocommerce_product_ids = array_merge($create_response, $update_response);
//Create log
if (! empty($created_data)) {
if ($sync_type == 'new') {
$this->createSyncLog($business_id, $user_id, 'new_products', 'created', $created_data);
} else {
$this->createSyncLog($business_id, $user_id, 'all_products', 'created', $created_data);
}
}
if (! empty($updated_data)) {
$this->createSyncLog($business_id, $user_id, 'all_products', 'updated', $updated_data);
}
//Sync variable product variations
$this->syncProductVariations($business_id, $sync_type, $new_woocommerce_product_ids);
if (empty($created_data) && empty($updated_data)) {
if ($sync_type == 'new') {
$this->createSyncLog($business_id, $user_id, 'new_products');
} else {
$this->createSyncLog($business_id, $user_id, 'all_products');
}
}
return $all_products;
}
public function syncProd($business_id, $data, $type, $new_products)
{
//woocommerce api client object
$woocommerce = $this->woo_client($business_id);
$new_woocommerce_product_ids = [];
$count = 0;
foreach (array_chunk($data, 99) as $chunked_array) {
$sync_data = [];
$sync_data[$type] = $chunked_array;
$response = $woocommerce->post('products/batch', $sync_data);
if (! empty($response->create)) {
foreach ($response->create as $key => $value) {
$new_product = $new_products[$count];
if ($value->id != 0) {
$new_product->woocommerce_product_id = $value->id;
//Sync woocommerce media id
$new_product->woocommerce_media_id = ! empty($value->images[0]->id) ? $value->images[0]->id : null;
} else {
if (! empty($value->error->data->resource_id)) {
$new_product->woocommerce_product_id = $value->error->data->resource_id;
}
}
$new_product->save();
$new_woocommerce_product_ids[] = $new_product->woocommerce_product_id;
$count++;
}
}
if (! empty($response->update)) {
foreach ($response->update as $key => $value) {
$updated_product = $new_products[$count];
if ($value->id != 0) {
//Sync woocommerce media id
$updated_product->woocommerce_media_id = ! empty($value->images[0]->id) ? $value->images[0]->id : null;
$updated_product->save();
}
$new_woocommerce_product_ids[] = $updated_product->woocommerce_product_id;
$count++;
}
}
}
return $new_woocommerce_product_ids;
}
/**
* Synchronizes pos variation templates with Woocommerce product attributes
*
* @param int $business_id
* @return void
*/
public function syncVariationAttributes($business_id)
{
$woocommerce = $this->woo_client($business_id);
$query = VariationTemplate::where('business_id', $business_id);
$attributes = $query->get();
$data = [];
$new_attrs = [];
foreach ($attributes as $attr) {
if (empty($attr->woocommerce_attr_id)) {
$data['create'][] = ['name' => $attr->name];
$new_attrs[] = $attr;
} else {
$data['update'][] = [
'name' => $attr->name,
'id' => $attr->woocommerce_attr_id,
];
}
}
if (! empty($data)) {
$response = $woocommerce->post('products/attributes/batch', $data);
//update woocommerce_attr_id
if (! empty($response->create)) {
foreach ($response->create as $key => $value) {
$new_attr = $new_attrs[$key];
if ($value->id != 0) {
$new_attr->woocommerce_attr_id = $value->id;
} else {
$all_attrs = $woocommerce->get('products/attributes');
foreach ($all_attrs as $attr) {
if (strtolower($attr->name) == strtolower($new_attr->name)) {
$new_attr->woocommerce_attr_id = $attr->id;
}
}
}
$new_attr->save();
}
}
}
}
/**
* Synchronizes pos products variations with Woocommerce product variations
*
* @param int $business_id
* @param string $sync_type
* @param array $new_woocommerce_product_ids (woocommerce product id of newly created products to sync)
* @return void
*/
public function syncProductVariations($business_id, $sync_type = 'all', $new_woocommerce_product_ids = [])
{
//woocommerce api client object
$woocommerce = $this->woo_client($business_id);
$woocommerce_api_settings = $this->get_api_settings($business_id);
$query = Product::where('business_id', $business_id)
->where('type', 'variable')
->where('woocommerce_disable_sync', 0)
->with(['variations',
'variations.variation_location_details',
'variations.product_variation',
'variations.product_variation.variation_template', ]);
$query->whereIn('woocommerce_product_id', $new_woocommerce_product_ids);
$variable_products = $query->get();
$business_location_id = $woocommerce_api_settings->location_id;
foreach ($variable_products as $product) {
//Skip product if last updated is less than last sync
$last_updated = $product->updated_at;
$last_stock_updated = $this->getLastStockUpdated($business_location_id, $product->id);
if (! empty($last_stock_updated)) {
$last_updated = strtotime($last_stock_updated) > strtotime($last_updated) ?
$last_stock_updated : $last_updated;
}
if (! empty($last_synced) && strtotime($last_updated) < strtotime($last_synced)) {
continue;
}
$variations = $product->variations;
$variation_data = [];
$new_variations = [];
$updated_variations = [];
foreach ($variations as $variation) {
$variation_arr = [
'sku' => $variation->sub_sku,
];
$manage_stock = false;
if ($product->enable_stock == 1) {
$manage_stock = true;
}
if (! empty($variation->product_variation->variation_template->woocommerce_attr_id)) {
$variation_arr['attributes'][] = [
'id' => $variation->product_variation->variation_template->woocommerce_attr_id,
'option' => $variation->name,
];
}
$price = $woocommerce_api_settings->product_tax_type == 'exc' ? $variation->default_sell_price : $variation->sell_price_inc_tax;
if (! empty($woocommerce_api_settings->default_selling_price_group)) {
$group_prices = $this->productUtil->getVariationGroupPrice($variation->id, $woocommerce_api_settings->default_selling_price_group, $product->tax_id);
$price = $woocommerce_api_settings->product_tax_type == 'exc' ? $group_prices['price_exc_tax'] : $group_prices['price_inc_tax'];
}
//Set product stock
$qty_available = 0;
if ($product->enable_stock == 1) {
$variation_location_details = $variation->variation_location_details;
foreach ($variation_location_details as $vld) {
if ($vld->location_id == $business_location_id) {
$qty_available = $vld->qty_available;
}
}
}
if (empty($variation->woocommerce_variation_id)) {
$variation_arr['manage_stock'] = $manage_stock;
if (in_array('quantity', $woocommerce_api_settings->product_fields_for_create)) {
$variation_arr['stock_quantity'] = $this->formatDecimalPoint($qty_available, 'quantity');
} else {
//set manage stock and in_stock if quantity disabled
if (isset($woocommerce_api_settings->manage_stock_for_create)) {
if ($woocommerce_api_settings->manage_stock_for_create == 'true') {
$variation_arr['manage_stock'] = true;
} elseif ($woocommerce_api_settings->manage_stock_for_create == 'false') {
$variation_arr['manage_stock'] = false;
} else {
unset($variation_arr['manage_stock']);
}
}
if (isset($woocommerce_api_settings->in_stock_for_create)) {
if ($woocommerce_api_settings->in_stock_for_create == 'true') {
$variation_arr['in_stock'] = true;
} elseif ($woocommerce_api_settings->in_stock_for_create == 'false') {
$variation_arr['in_stock'] = false;
}
}
}
//Set variation images
//If media id is set use media id else use image src
if (! empty($variation->media) && count($variation->media) > 0 && in_array('image', $woocommerce_api_settings->product_fields_for_create)) {
$url = $variation->media->first()->display_url;
$path = $variation->media->first()->display_path;
$woocommerce_media_id = $variation->media->first()->woocommerce_media_id;
if ($this->isValidImage($path)) {
$variation_arr['image'] = ! empty($woocommerce_media_id) ? ['id' => $woocommerce_media_id] : ['src' => $url];
}
}
$variation_arr['regular_price'] = $this->formatDecimalPoint($price);
$new_variations[] = $variation;
$variation_data['create'][] = $variation_arr;
} else {
$variation_arr['id'] = $variation->woocommerce_variation_id;
$variation_arr['manage_stock'] = $manage_stock;
if (in_array('quantity', $woocommerce_api_settings->product_fields_for_update)) {
$variation_arr['stock_quantity'] = $this->formatDecimalPoint($qty_available, 'quantity');
} else {
//set manage stock and in_stock if quantity disabled
if (isset($woocommerce_api_settings->manage_stock_for_update)) {
if ($woocommerce_api_settings->manage_stock_for_update == 'true') {
$variation_arr['manage_stock'] = true;
} elseif ($woocommerce_api_settings->manage_stock_for_update == 'false') {
$variation_arr['manage_stock'] = false;
} else {
unset($variation_arr['manage_stock']);
}
}
if (isset($woocommerce_api_settings->in_stock_for_update)) {
if ($woocommerce_api_settings->in_stock_for_update == 'true') {
$variation_arr['in_stock'] = true;
} elseif ($woocommerce_api_settings->in_stock_for_update == 'false') {
$variation_arr['in_stock'] = false;
}
}
}
//Set variation images
//If media id is set use media id else use image src
if (! empty($variation->media) && count($variation->media) > 0 && in_array('image', $woocommerce_api_settings->product_fields_for_update)) {
$url = $variation->media->first()->display_url;
$path = $variation->media->first()->display_path;
$woocommerce_media_id = $variation->media->first()->woocommerce_media_id;
if ($this->isValidImage($path)) {
$variation_arr['image'] = ! empty($woocommerce_media_id) ? ['id' => $woocommerce_media_id] : ['src' => $url];
}
}
//assign price
if (in_array('price', $woocommerce_api_settings->product_fields_for_update)) {
$variation_arr['regular_price'] = $this->formatDecimalPoint($price);
}
$variation_data['update'][] = $variation_arr;
$updated_variations[] = $variation;
}
}
if (! empty($variation_data)) {
$response = $woocommerce->post('products/'.$product->woocommerce_product_id.'/variations/batch', $variation_data);
//update woocommerce_variation_id
if (! empty($response->create)) {
foreach ($response->create as $key => $value) {
$new_variation = $new_variations[$key];
if ($value->id != 0) {
$new_variation->woocommerce_variation_id = $value->id;
$media = $new_variation->media->first();
if (! empty($media)) {
$media->woocommerce_media_id = ! empty($value->image->id) ? $value->image->id : null;
$media->save();
}
} else {
if (! empty($value->error->data->resource_id)) {
$new_variation->woocommerce_variation_id = $value->error->data->resource_id;
}
}
$new_variation->save();
}
}
//Update media id if changed from woocommerce site
if (! empty($response->update)) {
foreach ($response->update as $key => $value) {
$updated_variation = $updated_variations[$key];
if ($value->id != 0) {
$media = $updated_variation->media->first();
if (! empty($media)) {
$media->woocommerce_media_id = ! empty($value->image->id) ? $value->image->id : null;
$media->save();
}
}
}
}
}
}
}
/**
* Synchronizes Woocommers Orders with POS sales
*
* @param int $business_id
* @param int $user_id
* @return void
*/
public function syncOrders($business_id, $user_id)
{
$last_synced = $this->getLastSync($business_id, 'orders', false);
$orders = $this->getAllResponse($business_id, 'orders');
$woocommerce_sells = Transaction::where('business_id', $business_id)
->whereNotNull('woocommerce_order_id')
->with('sell_lines', 'sell_lines.product', 'payment_lines')
->get();
$new_orders = [];
$updated_orders = [];
$woocommerce_api_settings = $this->get_api_settings($business_id);
$business = Business::find($business_id);
$skipped_orders = ! empty($business->woocommerce_skipped_orders) ? json_decode($business->woocommerce_skipped_orders, true) : [];
$business_data = [
'id' => $business_id,
'accounting_method' => $business->accounting_method,
'location_id' => $woocommerce_api_settings->location_id,
'pos_settings' => json_decode($business->pos_settings, true),
'business' => $business,
];
$created_data = [];
$updated_data = [];
$create_error_data = [];
$update_error_data = [];
foreach ($orders as $order) {
//Only consider orders modified after last sync
if ((! empty($last_synced) && strtotime($order->date_modified) <= strtotime($last_synced) && ! in_array($order->id, $skipped_orders)) || in_array($order->status, ['auto-draft'])) {
continue;
}
//Search if order already exists
$sell = $woocommerce_sells->filter(function ($item) use ($order) {
return $item->woocommerce_order_id == $order->id;
})->first();
$order_number = $order->number;
$sell_status = $this->woocommerceOrderStatusToPosSellStatus($order->status, $business_id);
if ($sell_status == 'draft') {
$order_number .= ' ('.__('sale.draft').')';
}
if (empty($sell)) {
$created = $this->createNewSaleFromOrder($business_id, $user_id, $order, $business_data);
$created_data[] = $order_number;
if ($created !== true) {
$create_error_data[] = $created;
}
} else {
$updated = $this->updateSaleFromOrder($business_id, $user_id, $order, $sell, $business_data);
$updated_data[] = $order_number;
if ($updated !== true) {
$update_error_data[] = $updated;
}
}
}
//Create log
if (! empty($created_data)) {
$this->createSyncLog($business_id, $user_id, 'orders', 'created', $created_data, $create_error_data);
}
if (! empty($updated_data)) {
$this->createSyncLog($business_id, $user_id, 'orders', 'updated', $updated_data, $update_error_data);
}
if (empty($created_data) && empty($updated_data)) {
$error_data = $create_error_data + $update_error_data;
$this->createSyncLog($business_id, $user_id, 'orders', null, [], $error_data);
}
}
/**
* Creates new sales in POSfrom woocommerce order list
*
* @param id $business_id
* @param id $user_id
* @param obj $order
* @param array $business_data
*/
public function createNewSaleFromOrder($business_id, $user_id, $order, $business_data)
{
$input = $this->formatOrderToSale($business_id, $user_id, $order);
if (! empty($input['has_error'])) {
return $input['has_error'];
}
$invoice_total = [
'total_before_tax' => $order->total,
'tax' => 0,
];
DB::beginTransaction();
$transaction = $this->transactionUtil->createSellTransaction($business_id, $input, $invoice_total, $user_id, false);
$transaction->woocommerce_order_id = $order->id;
$transaction->save();
//Create sell lines
$this->transactionUtil->createOrUpdateSellLines($transaction, $input['products'], $input['location_id'], false, null, ['woocommerce_line_items_id' => 'line_item_id'], false);
$this->transactionUtil->createOrUpdatePaymentLines($transaction, $input['payment'], $business_id, $user_id, false);
if ($input['status'] == 'final') {
//update product stock
foreach ($input['products'] as $product) {
if ($product['enable_stock']) {
$this->productUtil->decreaseProductQuantity(
$product['product_id'],
$product['variation_id'],
$input['location_id'],
$product['quantity']
);
}
}
//Update payment status
$transaction->payment_status = 'paid';
$transaction->save();
try {
$this->transactionUtil->mapPurchaseSell($business_data, $transaction->sell_lines, 'purchase');
} catch (PurchaseSellMismatch $e) {
DB::rollBack();
$this->add_to_skipped_orders($business_data['business'], $order->id);
return [
'error_type' => 'order_insuficient_product_qty',
'order_number' => $order->number,
'msg' => $e->getMessage(),
];
}
}
$this->remove_from_skipped_orders($business_data['business'], $order->id);
DB::commit();
return true;
}
/**
* Formats Woocommerce order response to pos sale request
*
* @param id $business_id
* @param id $user_id
* @param obj $order
* @param obj $sell = null
*/
public function formatOrderToSale($business_id, $user_id, $order, $sell = null)
{
$woocommerce_api_settings = $this->get_api_settings($business_id);
//Create sell line data
$product_lines = [];
//For updating sell lines
$sell_lines = [];
if (! empty($sell)) {
$sell_lines = $sell->sell_lines;
}
foreach ($order->line_items as $product_line) {
$product = Product::where('business_id', $business_id)
->where('woocommerce_product_id', $product_line->product_id)
->with(['variations'])
->first();
$unit_price = $product_line->total / $product_line->quantity;
$line_tax = ! empty($product_line->total_tax) ? $product_line->total_tax : 0;
$unit_line_tax = $line_tax / $product_line->quantity;
$unit_price_inc_tax = $unit_price + $unit_line_tax;
if (! empty($product)) {
//Set sale line variation;If single product then first variation
//else search for woocommerce_variation_id in all the variations
if ($product->type == 'single') {
$variation = $product->variations->first();
} else {
foreach ($product->variations as $v) {
if ($v->woocommerce_variation_id == $product_line->variation_id) {
$variation = $v;
}
}
}
if (empty($variation)) {
return ['has_error' => [
'error_type' => 'order_product_not_found',
'order_number' => $order->number,
'product' => $product_line->name.' SKU:'.$product_line->sku,
],
];
exit;
}
//Check if line tax exists append to sale line data
$tax_id = null;
if (! empty($product_line->taxes)) {
foreach ($product_line->taxes as $tax) {
$pos_tax = TaxRate::where('business_id', $business_id)
->where('woocommerce_tax_rate_id', $tax->id)
->first();
if (! empty($pos_tax)) {
$tax_id = $pos_tax->id;
break;
}
}
}
$product_data = [
'product_id' => $product->id,
'unit_price' => $unit_price,
'unit_price_inc_tax' => $unit_price_inc_tax,
'variation_id' => $variation->id,
'quantity' => $product_line->quantity,
'enable_stock' => $product->enable_stock,
'item_tax' => $line_tax,
'tax_id' => $tax_id,
'line_item_id' => $product_line->id,
];
//append transaction_sell_lines_id if update
if (! empty($sell_lines)) {
foreach ($sell_lines as $sell_line) {
if ($sell_line->woocommerce_line_items_id ==
$product_line->id) {
$product_data['transaction_sell_lines_id'] = $sell_line->id;
}
}
}
$product_lines[] = $product_data;
} else {
return ['has_error' => [
'error_type' => 'order_product_not_found',
'order_number' => $order->number,
'product' => $product_line->name.' SKU:'.$product_line->sku,
],
];
exit;
}
}
//Get customer details
$order_customer_id = $order->customer_id;
$customer_details = [];
//If Customer empty skip get guest customer details from billing address
if (empty($order_customer_id)) {
$f_name = ! empty($order->billing->first_name) ? $order->billing->first_name : '';
$l_name = ! empty($order->billing->last_name) ? $order->billing->last_name : '';
$customer_details = [
'first_name' => $f_name,
'last_name' => $l_name,
'email' => ! empty($order->billing->email) ? $order->billing->email : null,
'name' => $f_name.' '.$l_name,
'mobile' => $order->billing->phone,
'address_line_1' => ! empty($order->billing->address_1) ? $order->billing->address_1 : null,
'address_line_2' => ! empty($order->billing->address_2) ? $order->billing->address_2 : null,
'city' => ! empty($order->billing->city) ? $order->billing->city : null,
'state' => ! empty($order->billing->state) ? $order->billing->state : null,
'country' => ! empty($order->billing->country) ? $order->billing->country : null,
'zip_code' => ! empty($order->billing->postcode) ? $order->billing->postcode : null,
];
} else {
//woocommerce api client object
$woocommerce = $this->woo_client($business_id);
$order_customer = $woocommerce->get('customers/'.$order_customer_id);
$customer_details = [
'first_name' => $order_customer->first_name,
'last_name' => $order_customer->last_name,
'email' => $order_customer->email,
'name' => $order_customer->first_name.' '.$order_customer->last_name,
'mobile' => $order_customer->billing->phone,
'city' => $order_customer->billing->city,
'state' => $order_customer->billing->state,
'country' => $order_customer->billing->country,
'address_line_1' => $order_customer->billing->address_1,
'address_line_2' => $order_customer->billing->address_2,
'zip_code' => $order_customer->billing->postcode,
];
}
if (! empty($customer_details['email'])) {
$customer = Contact::where('business_id', $business_id)
->where('email', $customer_details['email'])
->OnlyCustomers()
->first();
}
if (empty($order_customer_id) && empty($customer_details['email'])) {
$contactUtil = new ContactUtil;
$customer = $contactUtil->getWalkInCustomer($business_id, false);
}
//If customer not found create new
if (empty($customer)) {
$ref_count = $this->transactionUtil->setAndGetReferenceCount('contacts', $business_id);
$contact_id = $this->transactionUtil->generateReferenceNumber('contacts', $ref_count, $business_id);
$customer_data = [
'business_id' => $business_id,
'type' => 'customer',
'first_name' => $customer_details['first_name'],
'last_name' => $customer_details['last_name'],
'name' => $customer_details['name'],
'email' => $customer_details['email'],
'contact_id' => $contact_id,
'mobile' => $customer_details['mobile'],
'city' => $customer_details['city'],
'state' => $customer_details['state'],
'country' => $customer_details['country'],
'created_by' => $user_id,
'address_line_1' => $customer_details['address_line_1'],
'address_line_2' => $customer_details['address_line_2'],
'zip_code' => $customer_details['zip_code'],
];
//if name is blank make email address as name
if (empty(trim($customer_data['name']))) {
$customer_data['first_name'] = $customer_details['email'];
$customer_data['name'] = $customer_details['email'];
}
$customer = Contact::create($customer_data);
}
$sell_status = $this->woocommerceOrderStatusToPosSellStatus($order->status, $business_id);
$shipping_status = $this->woocommerceOrderStatusToPosShippingStatus($order->status, $business_id);
$shipping_address = [];
if (! empty($order->shipping->first_name)) {
$shipping_address[] = $order->shipping->first_name.' '.$order->shipping->last_name;
}
if (! empty($order->shipping->company)) {
$shipping_address[] = $order->shipping->company;
}
if (! empty($order->shipping->address_1)) {
$shipping_address[] = $order->shipping->address_1;
}
if (! empty($order->shipping->address_2)) {
$shipping_address[] = $order->shipping->address_2;
}
if (! empty($order->shipping->city)) {
$shipping_address[] = $order->shipping->city;
}
if (! empty($order->shipping->state)) {
$shipping_address[] = $order->shipping->state;
}
if (! empty($order->shipping->country)) {
$shipping_address[] = $order->shipping->country;
}
if (! empty($order->shipping->postcode)) {
$shipping_address[] = $order->shipping->postcode;
}
$addresses['shipping_address'] = [
'shipping_name' => $order->shipping->first_name.' '.$order->shipping->last_name,
'company' => $order->shipping->company,
'shipping_address_line_1' => $order->shipping->address_1,
'shipping_address_line_2' => $order->shipping->address_2,
'shipping_city' => $order->shipping->city,
'shipping_state' => $order->shipping->state,
'shipping_country' => $order->shipping->country,
'shipping_zip_code' => $order->shipping->postcode,
];
$addresses['billing_address'] = [
'billing_name' => $order->billing->first_name.' '.$order->billing->last_name,
'company' => $order->billing->company,
'billing_address_line_1' => $order->billing->address_1,
'billing_address_line_2' => $order->billing->address_2,
'billing_city' => $order->billing->city,
'billing_state' => $order->billing->state,
'billing_country' => $order->billing->country,
'billing_zip_code' => $order->billing->postcode,
];
$shipping_lines_array = [];
if (! empty($order->shipping_lines)) {
foreach ($order->shipping_lines as $shipping_lines) {
$shipping_lines_array[] = $shipping_lines->method_title;
}
}
$new_sell_data = [
'business_id' => $business_id,
'location_id' => $woocommerce_api_settings->location_id,
'contact_id' => $customer->id,
'discount_type' => 'fixed',
'discount_amount' => $order->discount_total,
'shipping_charges' => $order->shipping_total,
'final_total' => $order->total,
'created_by' => $user_id,
'status' => $sell_status == 'quotation' ? 'draft' : $sell_status,
'is_quotation' => $sell_status == 'quotation' ? 1 : 0,
'sub_status' => $sell_status == 'quotation' ? 'quotation' : null,
'payment_status' => 'paid',
'additional_notes' => '',
'transaction_date' => $order->date_created,
'customer_group_id' => $customer->customer_group_id,
'tax_rate_id' => null,
'sale_note' => null,
'commission_agent' => null,
'invoice_no' => $order->number,
'order_addresses' => json_encode($addresses),
'shipping_charges' => ! empty($order->shipping_total) ? $order->shipping_total : 0,
'shipping_details' => ! empty($shipping_lines_array) ? implode(', ', $shipping_lines_array) : '',
'shipping_status' => $shipping_status,
'shipping_address' => implode(', ', $shipping_address),
];
$payment = [
'amount' => $order->total,
'method' => 'cash',
'card_transaction_number' => '',
'card_number' => '',
'card_type' => '',
'card_holder_name' => '',
'card_month' => '',
'card_security' => '',
'cheque_number' => '',
'bank_account_number' => '',
'note' => $order->payment_method_title,
'paid_on' => $order->date_paid,
];
if (! empty($sell) && count($sell->payment_lines) > 0) {
$payment['payment_id'] = $sell->payment_lines->first()->id;
}
$new_sell_data['products'] = $product_lines;
$new_sell_data['payment'] = [$payment];
return $new_sell_data;
}
/**
* Updates existing sale
*
* @param id $business_id
* @param id $user_id
* @param obj $order
* @param obj $sell
* @param array $business_data
*/
public function updateSaleFromOrder($business_id, $user_id, $order, $sell, $business_data)
{
$input = $this->formatOrderToSale($business_id, $user_id, $order, $sell);
if (! empty($input['has_error'])) {
return $input['has_error'];
}
$invoice_total = [
'total_before_tax' => $order->total,
'tax' => 0,
];
$status_before = $sell->status;
DB::beginTransaction();
$transaction = $this->transactionUtil->updateSellTransaction($sell, $business_id, $input, $invoice_total, $user_id, false, false);
//Update Sell lines
$deleted_lines = $this->transactionUtil->createOrUpdateSellLines($transaction, $input['products'], $input['location_id'], true, $status_before, [], false);
$this->transactionUtil->createOrUpdatePaymentLines($transaction, $input['payment'], null, null, false);
//Update payment status
$transaction->payment_status = 'paid';
$transaction->save();
//Update product stock
$this->productUtil->adjustProductStockForInvoice($status_before, $transaction, $input, false);
try {
$this->transactionUtil->adjustMappingPurchaseSell($status_before, $transaction, $business_data, $deleted_lines);
} catch (PurchaseSellMismatch $e) {
DB::rollBack();
return [
'error_type' => 'order_insuficient_product_qty',
'order_number' => $order->number,
'msg' => $e->getMessage(),
];
}
DB::commit();
return true;
}
/**
* Creates sync log in the database
*
* @param id $business_id
* @param id $user_id
* @param string $type
* @param array $errors = null
*/
public function createSyncLog($business_id, $user_id, $type, $operation = null, $data = [], $errors = null)
{
WoocommerceSyncLog::create([
'business_id' => $business_id,
'sync_type' => $type,
'created_by' => $user_id,
'operation_type' => $operation,
'data' => ! empty($data) ? json_encode($data) : null,
'details' => ! empty($errors) ? json_encode($errors) : null,
]);
}
/**
* Retrives last synced date from the database
*
* @param id $business_id
* @param string $type
* @param bool $for_humans = true
*/
public function getLastSync($business_id, $type, $for_humans = true)
{
$last_sync = WoocommerceSyncLog::where('business_id', $business_id)
->where('sync_type', $type)
->max('created_at');
//If last reset present make last sync to null
$last_reset = WoocommerceSyncLog::where('business_id', $business_id)
->where('sync_type', $type)
->where('operation_type', 'reset')
->max('created_at');
if (! empty($last_reset) && ! empty($last_sync) && $last_reset >= $last_sync) {
$last_sync = null;
}
if (! empty($last_sync) && $for_humans) {
$last_sync = format_relative_time($last_sync);
}
return $last_sync;
}
public function woocommerceOrderStatusToPosSellStatus($status, $business_id)
{
$default_status_array = [
'pending' => 'draft',
'processing' => 'final',
'on-hold' => 'draft',
'completed' => 'final',
'cancelled' => 'draft',
'refunded' => 'draft',
'failed' => 'draft',
'shipped' => 'final',
];
$api_settings = $this->get_api_settings($business_id);
$status_settings = $api_settings->order_statuses ?? null;
$sale_status = ! empty($status_settings) ? $status_settings->$status : null;
$sale_status = empty($sale_status) && array_key_exists($status, $default_status_array) ? $default_status_array[$status] : $sale_status;
$sale_status = empty($sale_status) ? 'final' : $sale_status;
return $sale_status;
}
public function woocommerceOrderStatusToPosShippingStatus($status, $business_id)
{
$api_settings = $this->get_api_settings($business_id);
$status_settings = $api_settings->shipping_statuses ?? null;
$shipping_status = ! empty($status_settings) ? $status_settings->$status : null;
return $shipping_status;
}
/**
* Splits response to list of 100 and merges all
*
* @param int $business_id
* @param string $endpoint
* @param array $params = []
* @return array
*/
public function getAllResponse($business_id, $endpoint, $params = [])
{
//woocommerce api client object
$woocommerce = $this->woo_client($business_id);
$page = 1;
$list = [];
$all_list = [];
$params['per_page'] = 100;
do {
$params['page'] = $page;
try {
$list = $woocommerce->get($endpoint, $params);
} catch (\Exception $e) {
return [];
}
$all_list = array_merge($all_list, $list);
$page++;
} while (count($list) > 0);
return $all_list;
}
/**
* Retrives all tax rates from woocommerce api
*
* @param id $business_id
* @param obj $tax_rates
*/
public function getTaxRates($business_id)
{
$tax_rates = $this->getAllResponse($business_id, 'taxes');
return $tax_rates;
}
public function getLastStockUpdated($location_id, $product_id)
{
$last_updated = VariationLocationDetails::where('location_id', $location_id)
->where('product_id', $product_id)
->max('updated_at');
return $last_updated;
}
private function formatDecimalPoint($number, $type = 'currency')
{
$precision = 4;
$currency_precision = session('business.currency_precision', 2);
$quantity_precision = session('business.quantity_precision', 2);
if ($type == 'currency' && ! empty($currency_precision)) {
$precision = $currency_precision;
}
if ($type == 'quantity' && ! empty($quantity_precision)) {
$precision = $quantity_precision;
}
return number_format((float) $number, $precision, '.', '');
}
public function isValidImage($path)
{
$valid_extenstions = ['jpg', 'jpeg', 'png', 'gif'];
return ! empty($path) && file_exists($path) && in_array(strtolower(pathinfo($path, PATHINFO_EXTENSION)), $valid_extenstions);
}
}