feat: Implement Siti Stock Plugin for WooCommerce integration
- Added core plugin structure with main class Siti_Stock_Plugin. - Implemented settings management through Siti_Stock_Settings. - Developed admin interface for settings configuration via Siti_Stock_Admin. - Created inventory management with external stock handling in Siti_Stock_Inventory_Manager. - Integrated synchronization service to fetch and apply stock updates from external API in Siti_Stock_Sync_Service. - Added custom product data store to manage combined stock values in Siti_Stock_Product_Data_Store. - Registered hooks for admin menus, settings, and synchronization processes. - Implemented REST API endpoint for triggering stock sync. - Added cron scheduling for automatic stock synchronization. - Included localization support for Dutch language.
This commit is contained in:
102
.github/workflows/release.yml
vendored
Normal file
102
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
name: Build & Release Plugin
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_notes:
|
||||
description: 'Optionele release-opmerkingen'
|
||||
required: false
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'siti-stock-plugin.php'
|
||||
- 'includes/**'
|
||||
- 'assets/**'
|
||||
- 'README.md'
|
||||
- '.github/workflows/release.yml'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Determine plugin version
|
||||
id: meta
|
||||
run: |
|
||||
VERSION=$(grep -E "^\\s*\\*\\s*Version:" -m 1 siti-stock-plugin.php | sed -E 's/.*Version:\\s*//')
|
||||
VERSION=$(echo "$VERSION" | tr -d '\\r')
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "::error::Kon pluginversie niet bepalen."
|
||||
exit 1
|
||||
fi
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check if tag exists
|
||||
id: tagcheck
|
||||
run: |
|
||||
TAG="v${{ steps.meta.outputs.version }}"
|
||||
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Tag bestaat al – workflow afronden
|
||||
if: steps.tagcheck.outputs.exists == 'true'
|
||||
run: |
|
||||
echo "Tag v${{ steps.meta.outputs.version }} bestaat al. Release wordt overgeslagen."
|
||||
|
||||
- name: Build distributie-zip
|
||||
if: steps.tagcheck.outputs.exists == 'false'
|
||||
id: package
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
SLUG="siti-stock-plugin"
|
||||
BUILD_ROOT="$RUNNER_TEMP/build"
|
||||
DEST_DIR="$BUILD_ROOT/$SLUG"
|
||||
mkdir -p "$DEST_DIR"
|
||||
|
||||
rsync -a ./ "$DEST_DIR" \
|
||||
--exclude '.git/' \
|
||||
--exclude '.github/' \
|
||||
--exclude 'docker/' \
|
||||
--exclude 'docs/' \
|
||||
--exclude 'dist/' \
|
||||
--exclude 'docker-compose.yml' \
|
||||
--exclude 'PLAN.md'
|
||||
|
||||
mkdir -p dist
|
||||
ZIP_PATH="dist/${SLUG}-${VERSION}.zip"
|
||||
(cd "$BUILD_ROOT" && zip -r "$GITHUB_WORKSPACE/$ZIP_PATH" "$SLUG")
|
||||
|
||||
echo "asset_path=$ZIP_PATH" >> "$GITHUB_OUTPUT"
|
||||
echo "asset_name=${SLUG}-${VERSION}.zip" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Stel release-body samen
|
||||
if: steps.tagcheck.outputs.exists == 'false'
|
||||
id: releasebody
|
||||
run: |
|
||||
if [ -n "$RELEASE_NOTES" ]; then
|
||||
echo "text=$RELEASE_NOTES" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "text=Automatische release op basis van versie ${{ steps.meta.outputs.version }}." >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
env:
|
||||
RELEASE_NOTES: ${{ github.event.inputs.release_notes }}
|
||||
|
||||
- name: Maak GitHub release
|
||||
if: steps.tagcheck.outputs.exists == 'false'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ steps.meta.outputs.version }}
|
||||
name: Siti Stock Plugin v${{ steps.meta.outputs.version }}
|
||||
body: ${{ steps.releasebody.outputs.text }}
|
||||
generate_release_notes: true
|
||||
files: ${{ steps.package.outputs.asset_path }}
|
||||
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# OS cruft
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# IDE / tooling
|
||||
.idea/
|
||||
.vscode/
|
||||
*.code-workspace
|
||||
|
||||
# Dependencies & builds
|
||||
node_modules/
|
||||
vendor/
|
||||
dist/
|
||||
|
||||
# Logs / env
|
||||
*.log
|
||||
.env*
|
||||
|
||||
# Docker artifacts
|
||||
docker/wordpress/wp-content/
|
||||
63
README.md
Normal file
63
README.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Siti Stock Plugin (WordPress plugin)
|
||||
|
||||
Deze repository bevat de WordPress plugin **Siti Stock Plugin**. De plugincode leeft geheel in deze map en volgt dezelfde ontwikkelworkflow als onze andere Siti plugins, zodat je eenvoudig lokaal kunt bouwen, testen en releasen.
|
||||
|
||||
## Functionaliteit
|
||||
|
||||
- Houd WooCommerce voorraad synchroon via een extern API-endpoint.
|
||||
- Stel API-sleutel, standaard voorraadstatus en cron-interval in vanuit het beheerscherm **Siti Stock**.
|
||||
- Start een sync handmatig vanuit de beheerpagina of via de REST-route `siti-stock/v1/sync`.
|
||||
|
||||
## Installatie & gebruik
|
||||
|
||||
1. Download de nieuwste release (`siti-stock-plugin-x.y.z.zip`) vanaf GitHub Releases of gebruik het zip-bestand uit `dist/`.
|
||||
2. Upload het zip-bestand in WordPress via **Plugins → Nieuwe plugin → Plugin uploaden** of plaats de map handmatig in `wp-content/plugins/`.
|
||||
3. Activeer **Siti Stock Plugin** en configureer eventueel de instellingen onder **Instellingen → Siti Stock**.
|
||||
|
||||
## Ontwikkelvereisten
|
||||
|
||||
- Docker Desktop of Docker Engine + Docker Compose v2
|
||||
- Een API-key of andere geheimen plaats je in `.env` (wordt genegeerd door git)
|
||||
|
||||
## Lokale ontwikkeling met Docker
|
||||
|
||||
1. Start de containers:
|
||||
```bash
|
||||
docker compose up --build -d
|
||||
```
|
||||
2. Doorloop de WordPress installatie op http://localhost:8086.
|
||||
- Database host: `db`
|
||||
- Database naam: `wordpress`
|
||||
- Database gebruiker/wachtwoord: `wordpress`
|
||||
3. Activeer binnen WordPress de plugin **Siti Stock Plugin** (deze map wordt in de container gemount naar `wp-content/plugins/siti-stock-plugin`).
|
||||
|
||||
### Handige commando's
|
||||
|
||||
```bash
|
||||
# Bash in de WordPress container (voor wp-cli of composer)
|
||||
docker compose exec wordpress bash
|
||||
|
||||
# Voorbeeld: lijst plugins met WP-CLI
|
||||
docker compose exec wordpress wp plugin list
|
||||
|
||||
# phpMyAdmin
|
||||
open http://localhost:8089 (zelfde DB-gegevens als hierboven)
|
||||
|
||||
# Containers stoppen
|
||||
docker compose down
|
||||
```
|
||||
|
||||
## Werken met git
|
||||
|
||||
De code blijft op de host staan en wordt alleen als bind-mount gebruikt. Daardoor kun je gewoon lokaal commits maken:
|
||||
|
||||
```bash
|
||||
git status
|
||||
git add .
|
||||
git commit -m "Omschrijf je wijziging"
|
||||
git push origin <branch>
|
||||
```
|
||||
|
||||
## Releasen
|
||||
|
||||
De workflow `.github/workflows/release.yml` maakt op basis van de pluginversie automatisch een distributie-zip en GitHub Release aan. Pas vóór een release de `Version` in `siti-stock-plugin.php` aan en zorg dat alle wijzigingen gecommit zijn.
|
||||
202
SitiWebUpdater.php
Normal file
202
SitiWebUpdater.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
class SitiWebUpdater {
|
||||
|
||||
private $file;
|
||||
|
||||
private $plugin;
|
||||
|
||||
private $basename;
|
||||
|
||||
private $active;
|
||||
|
||||
private $username;
|
||||
|
||||
private $repository;
|
||||
|
||||
private $authorize_token;
|
||||
|
||||
private $github_response;
|
||||
|
||||
public function __construct( $file ) {
|
||||
|
||||
$this->file = $file;
|
||||
|
||||
$this->set_plugin_properties();
|
||||
add_action( 'admin_init', array( $this, 'set_plugin_properties' ) );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function set_plugin_properties() {
|
||||
if ( ! function_exists( 'get_plugin_data' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
}
|
||||
|
||||
$this->plugin = get_plugin_data( $this->file );
|
||||
$this->basename = plugin_basename( $this->file );
|
||||
$this->active = is_plugin_active( $this->basename );
|
||||
}
|
||||
|
||||
public function set_username( $username ) {
|
||||
$this->username = $username;
|
||||
}
|
||||
|
||||
public function set_repository( $repository ) {
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
public function authorize( $token ) {
|
||||
$this->authorize_token = $token;
|
||||
}
|
||||
|
||||
private function get_repository_info() {
|
||||
if ( is_null( $this->github_response ) ) { // Do we have a response?
|
||||
$args = array();
|
||||
$request_uri = sprintf( 'https://api.github.com/repos/%s/%s/releases', $this->username, $this->repository ); // Build URI
|
||||
|
||||
$args = array();
|
||||
|
||||
if( $this->authorize_token ) { // Is there an access token?
|
||||
$args['headers']['Authorization'] = "bearer {$this->authorize_token}"; // Set the headers
|
||||
}
|
||||
|
||||
$response = json_decode( wp_remote_retrieve_body( wp_remote_get( $request_uri, $args ) ), true ); // Get JSON and parse it
|
||||
|
||||
if( is_array( $response ) ) { // If it is an array
|
||||
$response = current( $response ); // Get the first item
|
||||
}
|
||||
|
||||
$this->github_response = $response; // Set it to our property
|
||||
}
|
||||
}
|
||||
|
||||
private function get_latest_version_from_response() {
|
||||
if ( empty( $this->github_response['tag_name'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ltrim( $this->github_response['tag_name'], 'vV' );
|
||||
}
|
||||
|
||||
public function initialize() {
|
||||
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'modify_transient' ), 10, 1 );
|
||||
add_filter( 'plugins_api', array( $this, 'plugin_popup' ), 10, 3);
|
||||
add_filter( 'upgrader_post_install', array( $this, 'after_install' ), 10, 3 );
|
||||
|
||||
// Add Authorization Token to download_package
|
||||
add_filter( 'upgrader_pre_download',
|
||||
function() {
|
||||
add_filter( 'http_request_args', [ $this, 'download_package' ], 15, 2 );
|
||||
return false; // upgrader_pre_download filter default return value.
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function modify_transient( $transient ) {
|
||||
|
||||
if( property_exists( $transient, 'checked') ) { // Check if transient has a checked property
|
||||
|
||||
if( $checked = $transient->checked ) { // Did Wordpress check for updates?
|
||||
|
||||
$this->get_repository_info(); // Get the repo info
|
||||
|
||||
$latest_version = $this->get_latest_version_from_response();
|
||||
|
||||
if ( null === $latest_version ) {
|
||||
return $transient;
|
||||
}
|
||||
|
||||
$out_of_date = version_compare( $latest_version, $checked[ $this->basename ], 'gt' ); // Check if we're out of date
|
||||
|
||||
if( $out_of_date ) {
|
||||
|
||||
$new_files = $this->github_response['zipball_url']; // Get the ZIP
|
||||
|
||||
$slug = current( explode('/', $this->basename ) ); // Create valid slug
|
||||
|
||||
$plugin = array( // setup our plugin info
|
||||
'url' => $this->plugin["PluginURI"],
|
||||
'slug' => $slug,
|
||||
'package' => $new_files,
|
||||
'new_version' => $latest_version
|
||||
);
|
||||
|
||||
$transient->response[$this->basename] = (object) $plugin; // Return it in response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $transient; // Return filtered transient
|
||||
}
|
||||
|
||||
public function plugin_popup( $result, $action, $args ) {
|
||||
|
||||
if( ! empty( $args->slug ) ) { // If there is a slug
|
||||
|
||||
if( $args->slug == current( explode( '/' , $this->basename ) ) ) { // And it's our slug
|
||||
|
||||
$this->get_repository_info(); // Get our repo info
|
||||
$latest_version = $this->get_latest_version_from_response();
|
||||
|
||||
if ( null === $latest_version ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Set it to an array
|
||||
$plugin = array(
|
||||
'name' => $this->plugin["Name"],
|
||||
'slug' => $this->basename,
|
||||
'requires' => '5.1',
|
||||
'tested' => '6.4.3',
|
||||
'rating' => '100.0',
|
||||
'num_ratings' => '10823',
|
||||
'downloaded' => '14249',
|
||||
'added' => '2016-01-05',
|
||||
'version' => $latest_version,
|
||||
'author' => $this->plugin["AuthorName"],
|
||||
'author_profile' => $this->plugin["AuthorURI"],
|
||||
'last_updated' => $this->github_response['published_at'],
|
||||
'homepage' => $this->plugin["PluginURI"],
|
||||
'short_description' => $this->plugin["Description"],
|
||||
'sections' => array(
|
||||
'Description' => $this->plugin["Description"],
|
||||
'Updates' => $this->github_response['body'],
|
||||
),
|
||||
'download_link' => $this->github_response['zipball_url']
|
||||
);
|
||||
|
||||
return (object) $plugin; // Return the data
|
||||
}
|
||||
|
||||
}
|
||||
return $result; // Otherwise return default
|
||||
}
|
||||
|
||||
public function download_package( $args, $url ) {
|
||||
|
||||
if ( null !== $args['filename'] ) {
|
||||
if( $this->authorize_token ) {
|
||||
$args = array_merge( $args, array( "headers" => array( "Authorization" => "token {$this->authorize_token}" ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
remove_filter( 'http_request_args', [ $this, 'download_package' ] );
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
public function after_install( $response, $hook_extra, $result ) {
|
||||
global $wp_filesystem; // Get global FS object
|
||||
|
||||
$install_directory = plugin_dir_path( $this->file ); // Our plugin directory
|
||||
$wp_filesystem->move( $result['destination'], $install_directory ); // Move files to the plugin dir
|
||||
$result['destination'] = $install_directory; // Set the destination for the rest of the stack
|
||||
|
||||
if ( $this->active ) { // If it was active
|
||||
activate_plugin( $this->basename ); // Reactivate
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
19
assets/css/admin.css
Normal file
19
assets/css/admin.css
Normal file
@@ -0,0 +1,19 @@
|
||||
.siti-stock-settings {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.siti-stock-settings .description {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.siti-stock-cron-info {
|
||||
margin-top: 20px;
|
||||
border-left: 4px solid #2271b1;
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.siti-stock-cron-info.is-visible {
|
||||
display: block;
|
||||
}
|
||||
15
assets/js/admin.js
Normal file
15
assets/js/admin.js
Normal file
@@ -0,0 +1,15 @@
|
||||
(function ($) {
|
||||
function toggleCronInfo() {
|
||||
var info = $('.siti-stock-cron-info');
|
||||
var checkbox = $('[data-siti-stock-toggle="cron"]');
|
||||
|
||||
if (!info.length || !checkbox.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
info.toggleClass('is-visible', checkbox.is(':checked'));
|
||||
}
|
||||
|
||||
$(document).on('change', '[data-siti-stock-toggle="cron"]', toggleCronInfo);
|
||||
$(toggleCronInfo);
|
||||
})(jQuery);
|
||||
49
docker-compose.yml
Normal file
49
docker-compose.yml
Normal file
@@ -0,0 +1,49 @@
|
||||
services:
|
||||
db:
|
||||
image: mariadb:10.11
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_DATABASE: wordpress
|
||||
MYSQL_USER: wordpress
|
||||
MYSQL_PASSWORD: wordpress
|
||||
MYSQL_ROOT_PASSWORD: supersecret
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
ports:
|
||||
- "3312:3306"
|
||||
|
||||
wordpress:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/wordpress/Dockerfile
|
||||
depends_on:
|
||||
- db
|
||||
ports:
|
||||
- "8082:80"
|
||||
environment:
|
||||
WORDPRESS_DB_HOST: db:3306
|
||||
WORDPRESS_DB_USER: wordpress
|
||||
WORDPRESS_DB_PASSWORD: wordpress
|
||||
WORDPRESS_DB_NAME: wordpress
|
||||
WORDPRESS_DEBUG: 1
|
||||
WP_ENVIRONMENT_TYPE: local
|
||||
volumes:
|
||||
- wordpress_data:/var/www/html
|
||||
- ./:/var/www/html/wp-content/plugins/siti-stock-plugin
|
||||
restart: unless-stopped
|
||||
|
||||
phpmyadmin:
|
||||
image: phpmyadmin/phpmyadmin:5
|
||||
depends_on:
|
||||
- db
|
||||
ports:
|
||||
- "8010:80"
|
||||
environment:
|
||||
PMA_HOST: db
|
||||
PMA_USER: wordpress
|
||||
PMA_PASSWORD: wordpress
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
wordpress_data:
|
||||
13
docker/wordpress/Dockerfile
Normal file
13
docker/wordpress/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM wordpress:6.9-php8.2
|
||||
|
||||
# Install handy tooling (git, mariadb client, wp-cli) for local development.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends git less vim unzip mariadb-client \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& curl -o /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \
|
||||
&& chmod +x /usr/local/bin/wp
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# Match the default Linux UID/GID to avoid permission headaches with bind mounts.
|
||||
RUN usermod -u 1000 www-data && groupmod -g 1000 www-data
|
||||
58
includes/class-siti-stock-admin-notices.php
Normal file
58
includes/class-siti-stock-admin-notices.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles temporary admin notices.
|
||||
*/
|
||||
class Siti_Stock_Admin_Notices {
|
||||
|
||||
const TRANSIENT_KEY = 'siti_stock_notice';
|
||||
|
||||
/**
|
||||
* Register hooks.
|
||||
*/
|
||||
public function register_hooks() {
|
||||
add_action( 'admin_notices', array( $this, 'render_flash_notice' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Store notice in transient for later output.
|
||||
*
|
||||
* @param string $message Message.
|
||||
* @param string $type Notice type.
|
||||
*/
|
||||
public function add_notice( $message, $type = 'success' ) {
|
||||
set_transient(
|
||||
self::TRANSIENT_KEY,
|
||||
array(
|
||||
'message' => $message,
|
||||
'type' => $type,
|
||||
),
|
||||
30
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render notice if stored.
|
||||
*/
|
||||
public function render_flash_notice() {
|
||||
$notice = get_transient( self::TRANSIENT_KEY );
|
||||
|
||||
if ( ! $notice ) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete_transient( self::TRANSIENT_KEY );
|
||||
|
||||
$type = in_array( $notice['type'], array( 'error', 'warning', 'success', 'info' ), true ) ? $notice['type'] : 'info';
|
||||
|
||||
printf(
|
||||
'<div class="notice notice-%1$s is-dismissible"><p>%2$s</p></div>',
|
||||
esc_attr( $type ),
|
||||
esc_html( $notice['message'] )
|
||||
);
|
||||
}
|
||||
}
|
||||
349
includes/class-siti-stock-admin.php
Normal file
349
includes/class-siti-stock-admin.php
Normal file
@@ -0,0 +1,349 @@
|
||||
<?php
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers admin UI, menus and asset loading.
|
||||
*/
|
||||
class Siti_Stock_Admin {
|
||||
|
||||
/**
|
||||
* @var Siti_Stock_Settings
|
||||
*/
|
||||
private $settings;
|
||||
|
||||
/**
|
||||
* @var Siti_Stock_Sync_Controller
|
||||
*/
|
||||
private $sync_controller;
|
||||
|
||||
/**
|
||||
* @var Siti_Stock_Admin_Notices
|
||||
*/
|
||||
private $notices;
|
||||
|
||||
/**
|
||||
* Cached settings snapshot for rendering.
|
||||
*
|
||||
* @var array<string,mixed>|null
|
||||
*/
|
||||
private $settings_cache = null;
|
||||
|
||||
/**
|
||||
* @param Siti_Stock_Settings $settings Settings repository.
|
||||
* @param Siti_Stock_Sync_Controller $sync_controller Sync controller.
|
||||
* @param Siti_Stock_Admin_Notices $notices Notice handler.
|
||||
*/
|
||||
public function __construct( Siti_Stock_Settings $settings, Siti_Stock_Sync_Controller $sync_controller, Siti_Stock_Admin_Notices $notices ) {
|
||||
$this->settings = $settings;
|
||||
$this->sync_controller = $sync_controller;
|
||||
$this->notices = $notices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all admin-related hooks.
|
||||
*/
|
||||
public function register_hooks() {
|
||||
add_action( 'admin_init', array( $this, 'register_settings' ) );
|
||||
add_action( 'admin_menu', array( $this, 'register_menu' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
|
||||
add_action( 'admin_notices', array( $this, 'maybe_show_missing_dependencies' ) );
|
||||
$this->notices->register_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register settings sections/fields.
|
||||
*/
|
||||
public function register_settings() {
|
||||
$this->settings->register();
|
||||
|
||||
add_settings_section(
|
||||
'siti_stock_api',
|
||||
__( 'API-instellingen', 'siti-stock-plugin' ),
|
||||
function () {
|
||||
echo '<p>' . esc_html__( 'Configureer het endpoint dat de actuele voorraad teruggeeft.', 'siti-stock-plugin' ) . '</p>';
|
||||
},
|
||||
'siti-stock-plugin'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'api_endpoint',
|
||||
__( 'API-endpoint', 'siti-stock-plugin' ),
|
||||
array( $this, 'render_text_field' ),
|
||||
'siti-stock-plugin',
|
||||
'siti_stock_api',
|
||||
array(
|
||||
'key' => 'api_endpoint',
|
||||
'placeholder' => 'https://example.com/api/stock',
|
||||
)
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'api_key',
|
||||
__( 'API-sleutel', 'siti-stock-plugin' ),
|
||||
array( $this, 'render_text_field' ),
|
||||
'siti-stock-plugin',
|
||||
'siti_stock_api',
|
||||
array(
|
||||
'key' => 'api_key',
|
||||
'type' => 'password',
|
||||
'description' => __( 'Wordt als Bearer-token meegestuurd.', 'siti-stock-plugin' ),
|
||||
)
|
||||
);
|
||||
|
||||
add_settings_section(
|
||||
'siti_stock_sync',
|
||||
__( 'Synchronisatie', 'siti-stock-plugin' ),
|
||||
function () {
|
||||
echo '<p>' . esc_html__( 'Stel in hoe en wanneer de voorraad automatisch bijgewerkt moet worden.', 'siti-stock-plugin' ) . '</p>';
|
||||
},
|
||||
'siti-stock-plugin'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'default_status',
|
||||
__( 'Standaardstatus', 'siti-stock-plugin' ),
|
||||
array( $this, 'render_select_field' ),
|
||||
'siti-stock-plugin',
|
||||
'siti_stock_sync',
|
||||
array(
|
||||
'key' => 'default_status',
|
||||
'options' => array(
|
||||
'instock' => __( 'Op voorraad', 'siti-stock-plugin' ),
|
||||
'outofstock' => __( 'Niet op voorraad', 'siti-stock-plugin' ),
|
||||
'onbackorder'=> __( 'In nabestelling', 'siti-stock-plugin' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'enable_auto_sync',
|
||||
__( 'Automatisch synchroniseren', 'siti-stock-plugin' ),
|
||||
array( $this, 'render_checkbox_field' ),
|
||||
'siti-stock-plugin',
|
||||
'siti_stock_sync',
|
||||
array(
|
||||
'key' => 'enable_auto_sync',
|
||||
'description' => __( 'Voer de synchronisatie periodiek uit via WP-Cron.', 'siti-stock-plugin' ),
|
||||
)
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'sync_interval',
|
||||
__( 'Interval', 'siti-stock-plugin' ),
|
||||
array( $this, 'render_select_field' ),
|
||||
'siti-stock-plugin',
|
||||
'siti_stock_sync',
|
||||
array(
|
||||
'key' => 'sync_interval',
|
||||
'options' => array(
|
||||
'siti_stock_quarter_hour' => __( 'Elke 15 minuten', 'siti-stock-plugin' ),
|
||||
'hourly' => __( 'Elk uur', 'siti-stock-plugin' ),
|
||||
'twicedaily' => __( 'Twee keer per dag', 'siti-stock-plugin' ),
|
||||
'daily' => __( 'Dagelijks', 'siti-stock-plugin' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register admin menu page.
|
||||
*/
|
||||
public function register_menu() {
|
||||
add_menu_page(
|
||||
__( 'Siti Stock', 'siti-stock-plugin' ),
|
||||
__( 'Siti Stock', 'siti-stock-plugin' ),
|
||||
'manage_options',
|
||||
'siti-stock-plugin',
|
||||
array( $this, 'render_settings_page' ),
|
||||
'dashicons-products'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue admin assets only on our page.
|
||||
*
|
||||
* @param string $hook Hook suffix.
|
||||
*/
|
||||
public function enqueue_admin_assets( $hook ) {
|
||||
if ( 'toplevel_page_siti-stock-plugin' !== $hook ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style(
|
||||
'siti-stock-admin',
|
||||
plugins_url( 'assets/css/admin.css', SITI_STOCK_PLUGIN_FILE ),
|
||||
array(),
|
||||
SITI_STOCK_PLUGIN_VERSION
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'siti-stock-admin',
|
||||
plugins_url( 'assets/js/admin.js', SITI_STOCK_PLUGIN_FILE ),
|
||||
array( 'jquery' ),
|
||||
SITI_STOCK_PLUGIN_VERSION,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display main admin page.
|
||||
*/
|
||||
public function render_settings_page() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$settings = $this->get_settings_snapshot();
|
||||
$next_run = $this->sync_controller->get_next_scheduled_run();
|
||||
?>
|
||||
<div class="wrap siti-stock-settings">
|
||||
<h1><?php esc_html_e( 'Siti Stock Plugin', 'siti-stock-plugin' ); ?></h1>
|
||||
<p class="description"><?php esc_html_e( 'Beheer API-sleutels, synchronisatie-instellingen en voer handmatige voorraadupdates uit.', 'siti-stock-plugin' ); ?></p>
|
||||
|
||||
<form method="post" action="options.php">
|
||||
<?php
|
||||
settings_fields( $this->settings->get_settings_group() );
|
||||
do_settings_sections( 'siti-stock-plugin' );
|
||||
submit_button();
|
||||
?>
|
||||
</form>
|
||||
|
||||
<hr />
|
||||
|
||||
<h2><?php esc_html_e( 'Handmatige synchronisatie', 'siti-stock-plugin' ); ?></h2>
|
||||
<p><?php esc_html_e( 'Voer direct een synchronisatie uit wanneer je net voorraad hebt bijgewerkt in het bronsysteem.', 'siti-stock-plugin' ); ?></p>
|
||||
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
|
||||
<?php wp_nonce_field( 'siti_stock_manual_sync' ); ?>
|
||||
<input type="hidden" name="action" value="siti_stock_manual_sync" />
|
||||
<?php submit_button( __( 'Start handmatige sync', 'siti-stock-plugin' ), 'secondary', 'siti_stock_manual_sync', false ); ?>
|
||||
</form>
|
||||
|
||||
<div class="siti-stock-cron-info<?php echo ! empty( $settings['enable_auto_sync'] ) ? ' is-visible' : ''; ?>" data-visible="<?php echo esc_attr( ! empty( $settings['enable_auto_sync'] ) ? '1' : '0' ); ?>">
|
||||
<h3><?php esc_html_e( 'Cron status', 'siti-stock-plugin' ); ?></h3>
|
||||
<p>
|
||||
<?php
|
||||
if ( $next_run ) {
|
||||
printf(
|
||||
/* translators: %s = formatted datetime */
|
||||
esc_html__( 'Volgende geplande run: %s', 'siti-stock-plugin' ),
|
||||
esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $next_run ) )
|
||||
);
|
||||
} else {
|
||||
esc_html_e( 'Momenteel staat er geen geplande sync klaar.', 'siti-stock-plugin' );
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a text/password input.
|
||||
*
|
||||
* @param array<string,string> $args Field args.
|
||||
*/
|
||||
public function render_text_field( $args ) {
|
||||
$key = $args['key'];
|
||||
$type = isset( $args['type'] ) ? $args['type'] : 'text';
|
||||
$placeholder = isset( $args['placeholder'] ) ? $args['placeholder'] : '';
|
||||
$value = $this->get_settings_value( $key );
|
||||
?>
|
||||
<input
|
||||
type="<?php echo esc_attr( $type ); ?>"
|
||||
name="<?php echo esc_attr( $this->settings->get_option_key() . '[' . $key . ']' ); ?>"
|
||||
id="<?php echo esc_attr( $key ); ?>"
|
||||
class="regular-text"
|
||||
value="<?php echo esc_attr( $value ); ?>"
|
||||
placeholder="<?php echo esc_attr( $placeholder ); ?>"
|
||||
/>
|
||||
<?php if ( ! empty( $args['description'] ) ) : ?>
|
||||
<p class="description"><?php echo esc_html( $args['description'] ); ?></p>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render select field.
|
||||
*
|
||||
* @param array<string,mixed> $args Field args.
|
||||
*/
|
||||
public function render_select_field( $args ) {
|
||||
$key = $args['key'];
|
||||
$value = $this->get_settings_value( $key );
|
||||
$options = isset( $args['options'] ) ? $args['options'] : array();
|
||||
?>
|
||||
<select name="<?php echo esc_attr( $this->settings->get_option_key() . '[' . $key . ']' ); ?>" id="<?php echo esc_attr( $key ); ?>">
|
||||
<?php foreach ( $options as $option_key => $label ) : ?>
|
||||
<option value="<?php echo esc_attr( $option_key ); ?>" <?php selected( $value, $option_key ); ?>>
|
||||
<?php echo esc_html( $label ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render checkbox.
|
||||
*
|
||||
* @param array<string,mixed> $args Arguments.
|
||||
*/
|
||||
public function render_checkbox_field( $args ) {
|
||||
$key = $args['key'];
|
||||
$value = (bool) $this->get_settings_value( $key );
|
||||
?>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="<?php echo esc_attr( $this->settings->get_option_key() . '[' . $key . ']' ); ?>"
|
||||
value="1"
|
||||
<?php checked( $value ); ?>
|
||||
data-siti-stock-toggle="cron"
|
||||
/>
|
||||
<?php esc_html_e( 'Inschakelen', 'siti-stock-plugin' ); ?>
|
||||
</label>
|
||||
<?php if ( ! empty( $args['description'] ) ) : ?>
|
||||
<p class="description"><?php echo esc_html( $args['description'] ); ?></p>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Show dependencies notice.
|
||||
*/
|
||||
public function maybe_show_missing_dependencies() {
|
||||
if ( class_exists( 'WooCommerce' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__( 'WooCommerce is vereist voor de Siti Stock Plugin.', 'siti-stock-plugin' ) . '</p></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached settings snapshot for admin rendering.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function get_settings_snapshot() {
|
||||
if ( null === $this->settings_cache ) {
|
||||
$this->settings_cache = $this->settings->get_all();
|
||||
}
|
||||
|
||||
return $this->settings_cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for retrieving a specific setting.
|
||||
*
|
||||
* @param string $key Array key.
|
||||
* @return mixed
|
||||
*/
|
||||
private function get_settings_value( $key ) {
|
||||
$settings = $this->get_settings_snapshot();
|
||||
|
||||
return isset( $settings[ $key ] ) ? $settings[ $key ] : '';
|
||||
}
|
||||
}
|
||||
209
includes/class-siti-stock-inventory-manager.php
Normal file
209
includes/class-siti-stock-inventory-manager.php
Normal file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the external stock field and ensures WooCommerce exposes combined stock values.
|
||||
*/
|
||||
class Siti_Stock_Inventory_Manager {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $external_stock_key;
|
||||
|
||||
/**
|
||||
* @param string $meta_key Meta key used to store external stock.
|
||||
*/
|
||||
public function __construct( $meta_key ) {
|
||||
$this->external_stock_key = $meta_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register relevant hooks.
|
||||
*/
|
||||
public function register_hooks() {
|
||||
add_action( 'woocommerce_product_options_stock_fields', array( $this, 'render_external_stock_field' ) );
|
||||
add_action( 'woocommerce_admin_process_product_object', array( $this, 'save_external_stock_value' ) );
|
||||
add_filter( 'woocommerce_product_get_stock_quantity', array( $this, 'filter_stock_quantity_with_external' ), 10, 2 );
|
||||
add_filter( 'woocommerce_product_variation_get_stock_quantity', array( $this, 'filter_stock_quantity_with_external' ), 10, 2 );
|
||||
add_filter( 'woocommerce_product_get_stock_status', array( $this, 'filter_stock_status_with_external' ), 10, 2 );
|
||||
add_filter( 'woocommerce_product_variation_get_stock_status', array( $this, 'filter_stock_status_with_external' ), 10, 2 );
|
||||
add_action( 'woocommerce_reduce_order_item_stock', array( $this, 'rebalance_stock_after_order_reduction' ), 20, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render external stock field within the inventory tab.
|
||||
*/
|
||||
public function render_external_stock_field() {
|
||||
if ( ! function_exists( 'woocommerce_wp_text_input' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $product_object;
|
||||
|
||||
if ( ! $product_object instanceof WC_Product ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$external_stock = $this->get_external_stock( $product_object );
|
||||
|
||||
woocommerce_wp_text_input(
|
||||
array(
|
||||
'id' => $this->external_stock_key,
|
||||
'value' => $external_stock,
|
||||
'label' => __( 'Externe voorraad', 'siti-stock-plugin' ),
|
||||
'desc_tip' => true,
|
||||
'description' => __( 'Voorraad beschikbaar op externe locatie(s).', 'siti-stock-plugin' ),
|
||||
'type' => 'number',
|
||||
'custom_attributes' => array(
|
||||
'step' => '1',
|
||||
'min' => '0',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist external stock coming from the product edit screen.
|
||||
*
|
||||
* @param WC_Product $product Product being saved.
|
||||
*/
|
||||
public function save_external_stock_value( $product ) {
|
||||
if ( ! $product instanceof WC_Product ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! isset( $_POST[ $this->external_stock_key ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
return;
|
||||
}
|
||||
|
||||
$raw_value = wp_unslash( $_POST[ $this->external_stock_key ] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
$value = function_exists( 'wc_stock_amount' ) ? wc_stock_amount( $raw_value ) : (int) $raw_value;
|
||||
$value = max( 0, (int) $value );
|
||||
|
||||
$product->update_meta_data( $this->external_stock_key, $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure WooCommerce exposes combined stock (local + external) on the frontend.
|
||||
*
|
||||
* @param int|null $stock Stock reported by WooCommerce.
|
||||
* @param WC_Product $product Product instance.
|
||||
* @return int|null
|
||||
*/
|
||||
public function filter_stock_quantity_with_external( $stock, $product ) {
|
||||
if ( ! $product instanceof WC_Product || ! $product->managing_stock() ) {
|
||||
return $stock;
|
||||
}
|
||||
|
||||
return $this->calculate_combined_stock( $product );
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirror combined stock towards frontend stock status without touching edit context.
|
||||
*
|
||||
* @param string $status Current status.
|
||||
* @param WC_Product $product Product instance.
|
||||
* @return string
|
||||
*/
|
||||
public function filter_stock_status_with_external( $status, $product ) {
|
||||
if ( ! $product instanceof WC_Product || ! $product->managing_stock() ) {
|
||||
return $status;
|
||||
}
|
||||
|
||||
$combined = $this->calculate_combined_stock( $product );
|
||||
|
||||
if ( 'outofstock' === $status && $combined > 0 ) {
|
||||
return 'instock';
|
||||
}
|
||||
|
||||
if ( 'instock' === $status && $combined <= 0 ) {
|
||||
return 'outofstock';
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve stored external stock value.
|
||||
*
|
||||
* @param WC_Product $product Product instance.
|
||||
* @return int
|
||||
*/
|
||||
private function get_external_stock( $product ) {
|
||||
$raw = (int) $product->get_meta( $this->external_stock_key, true );
|
||||
|
||||
return max( 0, $raw );
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate combined stock without triggering recursion.
|
||||
*
|
||||
* @param WC_Product $product Product instance.
|
||||
* @return int
|
||||
*/
|
||||
private function calculate_combined_stock( $product ) {
|
||||
$raw_stock = $product->get_stock_quantity( 'edit' );
|
||||
$base_stock = function_exists( 'wc_stock_amount' ) ? wc_stock_amount( $raw_stock ) : (int) $raw_stock;
|
||||
$external_stock = $this->get_external_stock( $product );
|
||||
|
||||
return max( 0, (int) $base_stock ) + $external_stock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure local stock only dips below zero when the external stock is depleted.
|
||||
*
|
||||
* @param WC_Order_Item_Product $item Order line item that triggered the reduction.
|
||||
* @param array $change Change context provided by WooCommerce.
|
||||
* @param WC_Order $order Order instance (unused).
|
||||
*/
|
||||
public function rebalance_stock_after_order_reduction( $item, $change, $order ) {
|
||||
unset( $change, $order );
|
||||
|
||||
if ( ! $item instanceof WC_Order_Item_Product ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$product = $item->get_product();
|
||||
|
||||
if ( ! $product instanceof WC_Product || ! $product->managing_stock() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$managed_id = $product->get_stock_managed_by_id();
|
||||
$stock_holder = ( $managed_id === $product->get_id() ) ? $product : wc_get_product( $managed_id );
|
||||
|
||||
if ( ! $stock_holder instanceof WC_Product ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$current_local = (int) $stock_holder->get_stock_quantity( 'edit' );
|
||||
$external_stock = $this->get_external_stock( $stock_holder );
|
||||
|
||||
if ( $current_local >= 0 || $external_stock <= 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$shortage = min( abs( $current_local ), $external_stock );
|
||||
$new_local = $current_local + $shortage;
|
||||
$new_external = $external_stock - $shortage;
|
||||
$needs_save = false;
|
||||
|
||||
if ( $new_local !== $current_local ) {
|
||||
$stock_holder->set_stock_quantity( $new_local );
|
||||
$needs_save = true;
|
||||
}
|
||||
|
||||
if ( $new_external !== $external_stock ) {
|
||||
$stock_holder->update_meta_data( $this->external_stock_key, $new_external );
|
||||
$needs_save = true;
|
||||
}
|
||||
|
||||
if ( $needs_save ) {
|
||||
$stock_holder->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
141
includes/class-siti-stock-plugin.php
Normal file
141
includes/class-siti-stock-plugin.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/class-siti-stock-settings.php';
|
||||
require_once __DIR__ . '/class-siti-stock-admin-notices.php';
|
||||
require_once __DIR__ . '/class-siti-stock-admin.php';
|
||||
require_once __DIR__ . '/class-siti-stock-inventory-manager.php';
|
||||
require_once __DIR__ . '/class-siti-stock-sync-service.php';
|
||||
require_once __DIR__ . '/class-siti-stock-sync-controller.php';
|
||||
|
||||
/**
|
||||
* Core plugin bootstrap that wires together all sub-components.
|
||||
*/
|
||||
class Siti_Stock_Plugin {
|
||||
|
||||
const OPTION_KEY = 'siti_stock_settings';
|
||||
const CRON_HOOK = 'siti_stock_plugin_sync_inventory';
|
||||
const EXTERNAL_STOCK_META_KEY = '_siti_external_stock';
|
||||
|
||||
/**
|
||||
* @var Siti_Stock_Plugin|null
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* @var Siti_Stock_Settings
|
||||
*/
|
||||
private $settings;
|
||||
|
||||
/**
|
||||
* @var Siti_Stock_Admin_Notices
|
||||
*/
|
||||
private $notices;
|
||||
|
||||
/**
|
||||
* @var Siti_Stock_Admin
|
||||
*/
|
||||
private $admin;
|
||||
|
||||
/**
|
||||
* @var Siti_Stock_Inventory_Manager
|
||||
*/
|
||||
private $inventory_manager;
|
||||
|
||||
/**
|
||||
* @var Siti_Stock_Sync_Controller
|
||||
*/
|
||||
private $sync_controller;
|
||||
|
||||
/**
|
||||
* Get singleton instance.
|
||||
*
|
||||
* @return Siti_Stock_Plugin
|
||||
*/
|
||||
public static function instance() {
|
||||
if ( ! self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin activation callback.
|
||||
*/
|
||||
public static function activate() {
|
||||
if ( ! class_exists( 'WooCommerce' ) ) {
|
||||
if ( defined( 'SITI_STOCK_PLUGIN_FILE' ) ) {
|
||||
deactivate_plugins( plugin_basename( SITI_STOCK_PLUGIN_FILE ) );
|
||||
}
|
||||
wp_die(
|
||||
__( 'WooCommerce is vereist voor de Siti Stock Plugin.', 'siti-stock-plugin' ),
|
||||
__( 'Siti Stock Plugin', 'siti-stock-plugin' ),
|
||||
array( 'back_link' => true )
|
||||
);
|
||||
}
|
||||
|
||||
$settings_repo = new Siti_Stock_Settings( self::OPTION_KEY );
|
||||
Siti_Stock_Sync_Controller::maybe_schedule_from_settings( self::CRON_HOOK, $settings_repo->get_all() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin deactivation callback.
|
||||
*/
|
||||
public static function deactivate() {
|
||||
Siti_Stock_Sync_Controller::clear_schedule( self::CRON_HOOK );
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
$this->settings = new Siti_Stock_Settings( self::OPTION_KEY );
|
||||
$this->notices = new Siti_Stock_Admin_Notices();
|
||||
$this->sync_controller = new Siti_Stock_Sync_Controller( $this->settings, $this->notices, self::CRON_HOOK );
|
||||
$this->inventory_manager = new Siti_Stock_Inventory_Manager( self::EXTERNAL_STOCK_META_KEY );
|
||||
$this->admin = new Siti_Stock_Admin( $this->settings, $this->sync_controller, $this->notices );
|
||||
|
||||
$this->admin->register_hooks();
|
||||
$this->inventory_manager->register_hooks();
|
||||
$this->sync_controller->register_hooks();
|
||||
add_filter( 'woocommerce_data_stores', array( $this, 'override_product_data_store' ), 20 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure WooCommerce uses the custom data store so reservations see combined stock.
|
||||
*
|
||||
* @param array<string,string> $stores Registered data store map.
|
||||
* @return array<string,string>
|
||||
*/
|
||||
public function override_product_data_store( $stores ) {
|
||||
if ( isset( $stores['product'] ) && $this->ensure_product_data_store_loaded() ) {
|
||||
$is_cpt_store = is_a( $stores['product'], 'WC_Product_Data_Store_CPT', true );
|
||||
|
||||
if ( $is_cpt_store ) {
|
||||
$stores['product'] = 'Siti_Stock_Product_Data_Store';
|
||||
}
|
||||
}
|
||||
|
||||
return $stores;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the custom product data store once WooCommerce base classes exist.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function ensure_product_data_store_loaded() {
|
||||
if ( class_exists( 'Siti_Stock_Product_Data_Store' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( ! class_exists( 'WC_Product_Data_Store_CPT' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/class-siti-stock-product-data-store.php';
|
||||
|
||||
return class_exists( 'Siti_Stock_Product_Data_Store' );
|
||||
}
|
||||
}
|
||||
51
includes/class-siti-stock-product-data-store.php
Normal file
51
includes/class-siti-stock-product-data-store.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom product data store that exposes combined (local + external) stock in SQL queries.
|
||||
*
|
||||
* WooCommerce's stock reservation system queries the raw `_stock` meta directly and therefore
|
||||
* ignores any runtime filters. By decorating the default CPT data store we make sure that the
|
||||
* reservation queries see the same combined stock value that the storefront shows.
|
||||
*/
|
||||
class Siti_Stock_Product_Data_Store extends WC_Product_Data_Store_CPT {
|
||||
|
||||
/**
|
||||
* Build a SQL snippet that adds the external stock meta value to the native `_stock`.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @param int $product_id Product ID that the stock query should represent.
|
||||
* @return string
|
||||
*/
|
||||
public function get_query_for_stock( $product_id ) {
|
||||
global $wpdb;
|
||||
|
||||
return $wpdb->prepare(
|
||||
"
|
||||
SELECT
|
||||
GREATEST(
|
||||
0,
|
||||
CAST( COALESCE( stock_meta.meta_value, 0 ) AS SIGNED )
|
||||
) +
|
||||
GREATEST(
|
||||
0,
|
||||
CAST( COALESCE( external_meta.meta_value, 0 ) AS SIGNED )
|
||||
)
|
||||
FROM {$wpdb->posts} AS posts
|
||||
LEFT JOIN {$wpdb->postmeta} AS stock_meta
|
||||
ON stock_meta.post_id = posts.ID AND stock_meta.meta_key = %s
|
||||
LEFT JOIN {$wpdb->postmeta} AS external_meta
|
||||
ON external_meta.post_id = posts.ID AND external_meta.meta_key = %s
|
||||
WHERE posts.ID = %d
|
||||
LIMIT 1
|
||||
",
|
||||
'_stock',
|
||||
Siti_Stock_Plugin::EXTERNAL_STOCK_META_KEY,
|
||||
$product_id
|
||||
);
|
||||
}
|
||||
}
|
||||
128
includes/class-siti-stock-settings.php
Normal file
128
includes/class-siti-stock-settings.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings repository with caching/sanitization helpers.
|
||||
*/
|
||||
class Siti_Stock_Settings {
|
||||
|
||||
/**
|
||||
* Cached settings.
|
||||
*
|
||||
* @var array<string,mixed>|null
|
||||
*/
|
||||
private $cache = null;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $option_key;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $settings_group;
|
||||
|
||||
/**
|
||||
* @param string $option_key Option key.
|
||||
* @param string|null $settings_group Settings group (defaults to option key).
|
||||
*/
|
||||
public function __construct( $option_key, $settings_group = null ) {
|
||||
$this->option_key = $option_key;
|
||||
$this->settings_group = $settings_group ? $settings_group : $option_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register option storage with WordPress.
|
||||
*/
|
||||
public function register() {
|
||||
register_setting(
|
||||
$this->settings_group,
|
||||
$this->option_key,
|
||||
array( $this, 'sanitize' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize input before saving.
|
||||
*
|
||||
* @param array<string,mixed> $input Raw input.
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function sanitize( $input ) {
|
||||
$output = $this->get_all();
|
||||
|
||||
$output['api_endpoint'] = isset( $input['api_endpoint'] ) ? esc_url_raw( $input['api_endpoint'] ) : '';
|
||||
$output['api_key'] = isset( $input['api_key'] ) ? sanitize_text_field( $input['api_key'] ) : '';
|
||||
$output['default_status'] = isset( $input['default_status'] ) ? sanitize_key( $input['default_status'] ) : 'instock';
|
||||
$output['enable_auto_sync'] = ! empty( $input['enable_auto_sync'] );
|
||||
$output['sync_interval'] = isset( $input['sync_interval'] ) ? sanitize_key( $input['sync_interval'] ) : 'hourly';
|
||||
|
||||
$this->cache = $output;
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get option key.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_option_key() {
|
||||
return $this->option_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings group name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_settings_group() {
|
||||
return $this->settings_group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all settings (cached).
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function get_all() {
|
||||
if ( null === $this->cache ) {
|
||||
$this->cache = wp_parse_args(
|
||||
get_option( $this->option_key, array() ),
|
||||
array(
|
||||
'api_endpoint' => '',
|
||||
'api_key' => '',
|
||||
'default_status' => 'instock',
|
||||
'enable_auto_sync' => false,
|
||||
'sync_interval' => 'hourly',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $this->cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve single setting.
|
||||
*
|
||||
* @param string $key Setting key.
|
||||
* @param mixed $default Default value.
|
||||
* @return mixed
|
||||
*/
|
||||
public function get( $key, $default = '' ) {
|
||||
$settings = $this->get_all();
|
||||
|
||||
return isset( $settings[ $key ] ) ? $settings[ $key ] : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop cache so latest values get read.
|
||||
*/
|
||||
public function reset_cache() {
|
||||
$this->cache = null;
|
||||
}
|
||||
}
|
||||
234
includes/class-siti-stock-sync-controller.php
Normal file
234
includes/class-siti-stock-sync-controller.php
Normal file
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Coordinates sync triggers (cron, REST, manual).
|
||||
*/
|
||||
class Siti_Stock_Sync_Controller {
|
||||
|
||||
/**
|
||||
* @var Siti_Stock_Settings
|
||||
*/
|
||||
private $settings;
|
||||
|
||||
/**
|
||||
* @var Siti_Stock_Admin_Notices
|
||||
*/
|
||||
private $notices;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $cron_hook;
|
||||
|
||||
/**
|
||||
* @param Siti_Stock_Settings $settings Settings repository.
|
||||
* @param Siti_Stock_Admin_Notices $notices Notice handler.
|
||||
* @param string $cron_hook Cron hook identifier.
|
||||
*/
|
||||
public function __construct( Siti_Stock_Settings $settings, Siti_Stock_Admin_Notices $notices, $cron_hook ) {
|
||||
$this->settings = $settings;
|
||||
$this->notices = $notices;
|
||||
$this->cron_hook = $cron_hook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register hooks.
|
||||
*/
|
||||
public function register_hooks() {
|
||||
add_filter( 'cron_schedules', array( $this, 'register_custom_schedules' ) );
|
||||
add_action( $this->cron_hook, array( $this, 'run_scheduled_sync' ) );
|
||||
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );
|
||||
add_action( 'admin_post_siti_stock_manual_sync', array( $this, 'handle_manual_sync' ) );
|
||||
add_action( 'update_option_' . $this->settings->get_option_key(), array( $this, 'handle_settings_update' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register cron intervals.
|
||||
*
|
||||
* @param array<string,array<string,int|string>> $schedules Schedules.
|
||||
* @return array
|
||||
*/
|
||||
public function register_custom_schedules( $schedules ) {
|
||||
$label = 'Elke 15 minuten (Siti Stock)';
|
||||
|
||||
if ( did_action( 'init' ) ) {
|
||||
$label = __( 'Elke 15 minuten (Siti Stock)', 'siti-stock-plugin' );
|
||||
}
|
||||
|
||||
$schedules['siti_stock_quarter_hour'] = array(
|
||||
'interval' => 15 * MINUTE_IN_SECONDS,
|
||||
'display' => $label,
|
||||
);
|
||||
|
||||
return $schedules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle manual sync submissions.
|
||||
*/
|
||||
public function handle_manual_sync() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_die( esc_html__( 'Onvoldoende rechten.', 'siti-stock-plugin' ) );
|
||||
}
|
||||
|
||||
check_admin_referer( 'siti_stock_manual_sync' );
|
||||
|
||||
$result = $this->run_sync();
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
$this->notices->add_notice( $result->get_error_message(), 'error' );
|
||||
} else {
|
||||
$this->notices->add_notice(
|
||||
sprintf(
|
||||
/* translators: 1: updated count, 2: skipped count */
|
||||
__( 'Sync voltooid. %1$d producten bijgewerkt, %2$d overgeslagen.', 'siti-stock-plugin' ),
|
||||
(int) $result['updated'],
|
||||
(int) $result['skipped']
|
||||
),
|
||||
'success'
|
||||
);
|
||||
}
|
||||
|
||||
wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url( 'admin.php?page=siti-stock-plugin' ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle scheduled sync.
|
||||
*/
|
||||
public function run_scheduled_sync() {
|
||||
$this->run_sync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute actual sync.
|
||||
*
|
||||
* @return array<string,mixed>|WP_Error
|
||||
*/
|
||||
private function run_sync() {
|
||||
$settings = $this->settings->get_all();
|
||||
|
||||
if ( empty( $settings['api_endpoint'] ) ) {
|
||||
return new WP_Error( 'siti_stock_missing_endpoint', __( 'Stel eerst een API-endpoint in.', 'siti-stock-plugin' ) );
|
||||
}
|
||||
|
||||
$service = new Siti_Stock_Sync_Service( $settings );
|
||||
|
||||
return $service->sync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register REST endpoint for remote triggers.
|
||||
*/
|
||||
public function register_rest_routes() {
|
||||
register_rest_route(
|
||||
'siti-stock/v1',
|
||||
'/sync',
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'rest_trigger_sync' ),
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'manage_options' );
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* REST callback to trigger sync.
|
||||
*
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function rest_trigger_sync( WP_REST_Request $request ) {
|
||||
$result = $this->run_sync();
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return new WP_REST_Response(
|
||||
array(
|
||||
'error' => $result->get_error_code(),
|
||||
'message' => $result->get_error_message(),
|
||||
),
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
return new WP_REST_Response(
|
||||
array(
|
||||
'updated' => (int) $result['updated'],
|
||||
'skipped' => (int) $result['skipped'],
|
||||
'errors' => $result['errors'],
|
||||
),
|
||||
200
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* React when settings change to schedule/unschedule cron.
|
||||
*
|
||||
* @param array<string,mixed> $old_value Old settings.
|
||||
* @param array<string,mixed> $value New value.
|
||||
*/
|
||||
public function handle_settings_update( $old_value, $value ) {
|
||||
if ( empty( $value['enable_auto_sync'] ) ) {
|
||||
self::clear_schedule( $this->cron_hook );
|
||||
return;
|
||||
}
|
||||
|
||||
$interval = isset( $value['sync_interval'] ) ? $value['sync_interval'] : 'hourly';
|
||||
$this->schedule_sync( $interval );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule cron hook.
|
||||
*
|
||||
* @param string $interval WP interval.
|
||||
*/
|
||||
private function schedule_sync( $interval ) {
|
||||
self::clear_schedule( $this->cron_hook );
|
||||
wp_schedule_event( time() + MINUTE_IN_SECONDS, $interval, $this->cron_hook );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve next scheduled timestamp.
|
||||
*
|
||||
* @return int|false
|
||||
*/
|
||||
public function get_next_scheduled_run() {
|
||||
return wp_next_scheduled( $this->cron_hook );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule cron during plugin activation if needed.
|
||||
*
|
||||
* @param string $cron_hook Cron hook.
|
||||
* @param array<string,mixed> $settings Settings snapshot.
|
||||
*/
|
||||
public static function maybe_schedule_from_settings( $cron_hook, $settings ) {
|
||||
if ( empty( $settings['enable_auto_sync'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::clear_schedule( $cron_hook );
|
||||
$interval = isset( $settings['sync_interval'] ) ? sanitize_key( $settings['sync_interval'] ) : 'hourly';
|
||||
wp_schedule_event( time() + MINUTE_IN_SECONDS, $interval, $cron_hook );
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear scheduled events (used on deactivation/settings change).
|
||||
*
|
||||
* @param string $cron_hook Cron hook identifier.
|
||||
*/
|
||||
public static function clear_schedule( $cron_hook ) {
|
||||
$timestamp = wp_next_scheduled( $cron_hook );
|
||||
|
||||
while ( $timestamp ) {
|
||||
wp_unschedule_event( $timestamp, $cron_hook );
|
||||
$timestamp = wp_next_scheduled( $cron_hook );
|
||||
}
|
||||
}
|
||||
}
|
||||
172
includes/class-siti-stock-sync-service.php
Normal file
172
includes/class-siti-stock-sync-service.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles communication with the remote stock API and applies updates to WooCommerce products.
|
||||
*/
|
||||
class Siti_Stock_Sync_Service {
|
||||
|
||||
/**
|
||||
* @var array<string,mixed>
|
||||
*/
|
||||
private $settings;
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $settings
|
||||
*/
|
||||
public function __construct( array $settings ) {
|
||||
$this->settings = wp_parse_args(
|
||||
$settings,
|
||||
array(
|
||||
'api_endpoint' => '',
|
||||
'api_key' => '',
|
||||
'default_status' => 'instock',
|
||||
'enable_auto_sync' => false,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a sync run.
|
||||
*
|
||||
* @return array<string,mixed>|WP_Error
|
||||
*/
|
||||
public function sync() {
|
||||
$data = $this->fetch_remote_stock();
|
||||
|
||||
if ( is_wp_error( $data ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
return $this->apply_stock_updates( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch stock payload from the configured API.
|
||||
*
|
||||
* @return array<int,array<string,mixed>>|WP_Error
|
||||
*/
|
||||
private function fetch_remote_stock() {
|
||||
$endpoint = trim( (string) $this->settings['api_endpoint'] );
|
||||
|
||||
if ( empty( $endpoint ) ) {
|
||||
return new WP_Error( 'siti_stock_missing_endpoint', __( 'Geen API-endpoint ingesteld.', 'siti-stock-plugin' ) );
|
||||
}
|
||||
|
||||
$args = array(
|
||||
'timeout' => 15,
|
||||
'headers' => array(
|
||||
'Accept' => 'application/json',
|
||||
),
|
||||
);
|
||||
|
||||
if ( ! empty( $this->settings['api_key'] ) ) {
|
||||
$args['headers']['Authorization'] = 'Bearer ' . trim( (string) $this->settings['api_key'] );
|
||||
}
|
||||
|
||||
$response = wp_remote_get( $endpoint, $args );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( $code < 200 || $code >= 300 ) {
|
||||
return new WP_Error(
|
||||
'siti_stock_bad_response',
|
||||
sprintf(
|
||||
/* translators: %d = HTTP status code */
|
||||
__( 'API gaf een onverwachte status terug (%d).', 'siti-stock-plugin' ),
|
||||
$code
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$data = json_decode( $body, true );
|
||||
|
||||
if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $data ) ) {
|
||||
return new WP_Error( 'siti_stock_bad_json', __( 'Kan de API-respons niet parseren.', 'siti-stock-plugin' ) );
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply stock changes to WooCommerce products.
|
||||
*
|
||||
* Expected payload: [{ \"sku\": \"123\", \"stock_quantity\": 12, \"external_stock\": 5, \"status\": \"instock\" }]
|
||||
*
|
||||
* @param array<int,array<string,mixed>> $records Array of stock records.
|
||||
* @return array<string,mixed>|WP_Error
|
||||
*/
|
||||
private function apply_stock_updates( array $records ) {
|
||||
if ( ! function_exists( 'wc_get_product_id_by_sku' ) ) {
|
||||
return new WP_Error( 'siti_stock_missing_wc', __( 'WooCommerce is vereist voor de Siti Stock Plugin.', 'siti-stock-plugin' ) );
|
||||
}
|
||||
|
||||
$summary = array(
|
||||
'updated' => 0,
|
||||
'skipped' => 0,
|
||||
'errors' => array(),
|
||||
);
|
||||
|
||||
$default_status = in_array( $this->settings['default_status'], array( 'instock', 'outofstock' ), true )
|
||||
? $this->settings['default_status']
|
||||
: 'instock';
|
||||
|
||||
foreach ( $records as $record ) {
|
||||
$sku = isset( $record['sku'] ) ? sanitize_text_field( (string) $record['sku'] ) : '';
|
||||
|
||||
if ( empty( $sku ) ) {
|
||||
$summary['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$product_id = wc_get_product_id_by_sku( $sku );
|
||||
|
||||
if ( ! $product_id ) {
|
||||
$summary['skipped']++;
|
||||
$summary['errors'][] = sprintf(
|
||||
/* translators: %s = product SKU */
|
||||
__( 'Geen product gevonden met SKU %s.', 'siti-stock-plugin' ),
|
||||
$sku
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
if ( ! $product ) {
|
||||
$summary['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$qty = isset( $record['stock_quantity'] ) ? (int) $record['stock_quantity'] : null;
|
||||
$status = isset( $record['status'] ) ? sanitize_key( (string) $record['status'] ) : $default_status;
|
||||
$status = in_array( $status, array( 'instock', 'outofstock', 'onbackorder' ), true ) ? $status : $default_status;
|
||||
$external_stock = array_key_exists( 'external_stock', $record ) ? (int) $record['external_stock'] : null;
|
||||
$external_stock = null !== $external_stock ? max( 0, $external_stock ) : null;
|
||||
|
||||
if ( null !== $qty ) {
|
||||
$product->set_manage_stock( true );
|
||||
$product->set_stock_quantity( $qty );
|
||||
}
|
||||
|
||||
if ( null !== $external_stock ) {
|
||||
$product->update_meta_data( Siti_Stock_Plugin::EXTERNAL_STOCK_META_KEY, $external_stock );
|
||||
}
|
||||
|
||||
$product->set_stock_status( $status );
|
||||
$product->save();
|
||||
|
||||
$summary['updated']++;
|
||||
}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
}
|
||||
50
siti-stock-plugin.php
Normal file
50
siti-stock-plugin.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Siti Stock Plugin
|
||||
* Plugin URI: https://github.com/SitiWeb/siti-stock-plugin
|
||||
* Description: Synchroniseert WooCommerce voorraad met het externe Siti voorraadplatform.
|
||||
* Version: 0.1.0
|
||||
* Author: Siti Web
|
||||
* Author URI: https://www.siti.nl
|
||||
* Requires PHP: 8.1
|
||||
* Requires at least: 6.4
|
||||
* Text Domain: siti-stock-plugin
|
||||
* Domain Path: /languages
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
define( 'SITI_STOCK_PLUGIN_VERSION', '0.1.0' );
|
||||
define( 'SITI_STOCK_PLUGIN_FILE', __FILE__ );
|
||||
define( 'SITI_STOCK_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
|
||||
require_once __DIR__ . '/includes/class-siti-stock-plugin.php';
|
||||
require_once __DIR__ . '/SitiWebUpdater.php';
|
||||
|
||||
register_activation_hook( __FILE__, array( 'Siti_Stock_Plugin', 'activate' ) );
|
||||
register_deactivation_hook( __FILE__, array( 'Siti_Stock_Plugin', 'deactivate' ) );
|
||||
|
||||
add_action(
|
||||
'init',
|
||||
function () {
|
||||
load_plugin_textdomain( 'siti-stock-plugin', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
|
||||
}
|
||||
);
|
||||
|
||||
Siti_Stock_Plugin::instance();
|
||||
|
||||
add_action(
|
||||
'admin_init',
|
||||
function () {
|
||||
if ( ! class_exists( 'SitiWebUpdater' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$updater = new SitiWebUpdater( __FILE__ );
|
||||
$updater->set_username( 'SitiWeb' );
|
||||
$updater->set_repository( 'siti-stock-plugin' );
|
||||
$updater->initialize();
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user