Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 03fdf89442 | |||
| 97ca8c4015 | |||
| 6ededb139b | |||
| 124f7b4e94 | |||
| de87021339 | |||
| ee43e18b25 | |||
| d8146457ea | |||
| 3c442eb28b | |||
| 1b2ff44eb4 | |||
| 5ba5c9f6a4 |
31
.github/workflows/release.yml
vendored
31
.github/workflows/release.yml
vendored
@@ -9,6 +9,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- 'siti-stock-plugin.php'
|
||||
- 'includes/**'
|
||||
@@ -27,11 +28,14 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fetch tags
|
||||
run: git fetch --tags origin
|
||||
|
||||
- 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')
|
||||
VERSION=$(grep -E "^[[:space:]]*\\*[[:space:]]*Version:" -m 1 siti-stock-plugin.php | sed -E 's/.*Version:[[:space:]]*//')
|
||||
VERSION=$(echo "$VERSION" | tr -d '\\r' | xargs)
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "::error::Kon pluginversie niet bepalen."
|
||||
exit 1
|
||||
@@ -42,12 +46,21 @@ jobs:
|
||||
id: tagcheck
|
||||
run: |
|
||||
TAG="v${{ steps.meta.outputs.version }}"
|
||||
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
||||
if git show-ref --tags --verify --quiet "refs/tags/$TAG"; then
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Maak of update git-tag
|
||||
if: steps.tagcheck.outputs.exists == 'false'
|
||||
run: |
|
||||
TAG="v${{ steps.meta.outputs.version }}"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag "$TAG" "$GITHUB_SHA"
|
||||
git push origin "$TAG"
|
||||
|
||||
- name: Tag bestaat al – workflow afronden
|
||||
if: steps.tagcheck.outputs.exists == 'true'
|
||||
run: |
|
||||
@@ -93,10 +106,14 @@ jobs:
|
||||
|
||||
- name: Maak GitHub release
|
||||
if: steps.tagcheck.outputs.exists == 'false'
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
tag_name: v${{ steps.meta.outputs.version }}
|
||||
tag: v${{ steps.meta.outputs.version }}
|
||||
commit: ${{ github.sha }}
|
||||
name: Siti Stock Plugin v${{ steps.meta.outputs.version }}
|
||||
body: ${{ steps.releasebody.outputs.text }}
|
||||
generate_release_notes: true
|
||||
files: ${{ steps.package.outputs.asset_path }}
|
||||
artifacts: ${{ steps.package.outputs.asset_path }}
|
||||
generateReleaseNotes: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
allowUpdates: true
|
||||
updateOnlyUnreleased: true
|
||||
|
||||
41
assets/css/order-admin.css
Normal file
41
assets/css/order-admin.css
Normal file
@@ -0,0 +1,41 @@
|
||||
.siti-stock-order-item-sources {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
margin: 4px 0 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.siti-stock-order-item-sources__label {
|
||||
font-weight: 600;
|
||||
color: #1d2327;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.siti-stock-order-item-sources__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: #f1f5f9;
|
||||
color: #1d2327;
|
||||
}
|
||||
|
||||
.siti-stock-order-item-sources__badge .dashicons {
|
||||
font-size: 13px;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
line-height: 13px;
|
||||
}
|
||||
|
||||
.siti-stock-order-item-sources__badge.is-local {
|
||||
background: #e6f8ef;
|
||||
color: #17633d;
|
||||
}
|
||||
|
||||
.siti-stock-order-item-sources__badge.is-external {
|
||||
background: #f1ecff;
|
||||
color: #3e2a85;
|
||||
}
|
||||
@@ -24,6 +24,11 @@ class Siti_Stock_Admin {
|
||||
*/
|
||||
private $notices;
|
||||
|
||||
/**
|
||||
* @var Siti_Stock_Supplier_Exports
|
||||
*/
|
||||
private $supplier_exports;
|
||||
|
||||
/**
|
||||
* Cached settings snapshot for rendering.
|
||||
*
|
||||
@@ -35,11 +40,13 @@ class Siti_Stock_Admin {
|
||||
* @param Siti_Stock_Settings $settings Settings repository.
|
||||
* @param Siti_Stock_Sync_Controller $sync_controller Sync controller.
|
||||
* @param Siti_Stock_Admin_Notices $notices Notice handler.
|
||||
* @param Siti_Stock_Supplier_Exports $supplier_exports Supplier exports handler.
|
||||
*/
|
||||
public function __construct( Siti_Stock_Settings $settings, Siti_Stock_Sync_Controller $sync_controller, Siti_Stock_Admin_Notices $notices ) {
|
||||
public function __construct( Siti_Stock_Settings $settings, Siti_Stock_Sync_Controller $sync_controller, Siti_Stock_Admin_Notices $notices, Siti_Stock_Supplier_Exports $supplier_exports ) {
|
||||
$this->settings = $settings;
|
||||
$this->sync_controller = $sync_controller;
|
||||
$this->notices = $notices;
|
||||
$this->supplier_exports = $supplier_exports;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,6 +167,15 @@ class Siti_Stock_Admin {
|
||||
array( $this, 'render_settings_page' ),
|
||||
'dashicons-products'
|
||||
);
|
||||
|
||||
add_submenu_page(
|
||||
'siti-stock-plugin',
|
||||
__( 'Stock updates', 'siti-stock-plugin' ),
|
||||
__( 'Stock updates', 'siti-stock-plugin' ),
|
||||
'manage_options',
|
||||
'siti-stock-plugin-stock-updates',
|
||||
array( $this, 'render_stock_updates_page' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,7 +184,12 @@ class Siti_Stock_Admin {
|
||||
* @param string $hook Hook suffix.
|
||||
*/
|
||||
public function enqueue_admin_assets( $hook ) {
|
||||
if ( 'toplevel_page_siti-stock-plugin' !== $hook ) {
|
||||
$pages = array(
|
||||
'toplevel_page_siti-stock-plugin',
|
||||
'siti-stock-plugin_page_siti-stock-plugin-stock-updates',
|
||||
);
|
||||
|
||||
if ( ! in_array( $hook, $pages, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -241,6 +262,112 @@ class Siti_Stock_Admin {
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render stock updates admin page.
|
||||
*/
|
||||
public function render_stock_updates_page() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$suppliers = $this->supplier_exports->get_supplier_configs();
|
||||
$admin_email = get_option( 'admin_email' );
|
||||
?>
|
||||
<div class="wrap siti-stock-settings">
|
||||
<h1><?php esc_html_e( 'Stock updates', 'siti-stock-plugin' ); ?></h1>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Plan automatische e-mails met de bestellingen per leverancier in CSV-formaat.', 'siti-stock-plugin' ); ?>
|
||||
<?php
|
||||
if ( $admin_email ) {
|
||||
printf(
|
||||
/* translators: %s admin email */
|
||||
esc_html__( 'E-mails worden verzonden naar %s.', 'siti-stock-plugin' ),
|
||||
esc_html( $admin_email )
|
||||
);
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
|
||||
<?php if ( empty( $suppliers ) ) : ?>
|
||||
<p><?php esc_html_e( 'Er zijn momenteel geen leveranciers gevonden.', 'siti-stock-plugin' ); ?></p>
|
||||
<?php else : ?>
|
||||
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
|
||||
<?php wp_nonce_field( 'siti_stock_save_supplier_schedule' ); ?>
|
||||
<input type="hidden" name="action" value="siti_stock_save_supplier_schedule" />
|
||||
|
||||
<table class="widefat fixed striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Leverancier', 'siti-stock-plugin' ); ?></th>
|
||||
<th><?php esc_html_e( 'Tijd (24u)', 'siti-stock-plugin' ); ?></th>
|
||||
<th><?php esc_html_e( 'Automatisch verzenden', 'siti-stock-plugin' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( $suppliers as $key => $config ) : ?>
|
||||
<tr>
|
||||
<td><?php echo esc_html( isset( $config['supplier'] ) ? $config['supplier'] : $key ); ?></td>
|
||||
<td>
|
||||
<input
|
||||
type="time"
|
||||
name="<?php echo esc_attr( 'supplier_exports[' . $key . '][time]' ); ?>"
|
||||
value="<?php echo esc_attr( isset( $config['time'] ) ? $config['time'] : '09:00' ); ?>"
|
||||
step="60"
|
||||
class="regular-text"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="<?php echo esc_attr( 'supplier_exports[' . $key . '][enabled]' ); ?>"
|
||||
value="1"
|
||||
<?php checked( ! empty( $config['enabled'] ) ); ?>
|
||||
/>
|
||||
<?php esc_html_e( 'Activeer e-mail', 'siti-stock-plugin' ); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php submit_button( __( 'Instellingen opslaan', 'siti-stock-plugin' ) ); ?>
|
||||
</form>
|
||||
|
||||
<h2><?php esc_html_e( 'Handmatig versturen', 'siti-stock-plugin' ); ?></h2>
|
||||
<p class="description"><?php esc_html_e( 'Gebruik deze knoppen om direct een export te versturen, ook als de automatische taak uit staat.', 'siti-stock-plugin' ); ?></p>
|
||||
|
||||
<table class="widefat fixed striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Leverancier', 'siti-stock-plugin' ); ?></th>
|
||||
<th><?php esc_html_e( 'Geplande tijd', 'siti-stock-plugin' ); ?></th>
|
||||
<th><?php esc_html_e( 'Actie', 'siti-stock-plugin' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( $suppliers as $key => $config ) : ?>
|
||||
<tr>
|
||||
<td><?php echo esc_html( isset( $config['supplier'] ) ? $config['supplier'] : $key ); ?></td>
|
||||
<td><?php echo esc_html( isset( $config['time'] ) ? $config['time'] : '00:00' ); ?></td>
|
||||
<td>
|
||||
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
|
||||
<?php wp_nonce_field( 'siti_stock_send_supplier_export' ); ?>
|
||||
<input type="hidden" name="action" value="siti_stock_send_supplier_export" />
|
||||
<input type="hidden" name="supplier_key" value="<?php echo esc_attr( $key ); ?>" />
|
||||
<?php submit_button( __( 'Verstuur nu', 'siti-stock-plugin' ), 'secondary', 'submit', false ); ?>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a text/password input.
|
||||
*
|
||||
|
||||
@@ -14,11 +14,17 @@ class Siti_Stock_Inventory_Manager {
|
||||
*/
|
||||
private $external_stock_key;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $order_item_stock_source_key;
|
||||
|
||||
/**
|
||||
* @param string $meta_key Meta key used to store external stock.
|
||||
*/
|
||||
public function __construct( $meta_key ) {
|
||||
$this->external_stock_key = $meta_key;
|
||||
$this->order_item_stock_source_key = '_siti_stock_source_breakdown';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,11 +33,15 @@ class Siti_Stock_Inventory_Manager {
|
||||
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_action( 'woocommerce_variation_options_inventory', array( $this, 'render_variation_external_stock_field' ), 10, 3 );
|
||||
add_action( 'woocommerce_save_product_variation', array( $this, 'save_variation_external_stock_value' ), 10, 2 );
|
||||
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 );
|
||||
add_action( 'woocommerce_after_order_itemmeta', array( $this, 'render_order_item_stock_source' ), 10, 3 );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_order_admin_assets' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,6 +97,67 @@ class Siti_Stock_Inventory_Manager {
|
||||
$product->update_meta_data( $this->external_stock_key, $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the external stock field for each variation row.
|
||||
*
|
||||
* @param int $loop Loop index.
|
||||
* @param array $variation_data Raw variation data.
|
||||
* @param WP_Post $variation Variation post object.
|
||||
*/
|
||||
public function render_variation_external_stock_field( $loop, $variation_data, $variation ) {
|
||||
unset( $variation_data );
|
||||
|
||||
if ( ! function_exists( 'woocommerce_wp_text_input' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$product = $variation instanceof WC_Product ? $variation : wc_get_product( $variation->ID );
|
||||
|
||||
if ( ! $product instanceof WC_Product ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$external_stock = $this->get_external_stock( $product );
|
||||
$field_key = 'variable_' . $this->external_stock_key;
|
||||
|
||||
woocommerce_wp_text_input(
|
||||
array(
|
||||
'id' => $this->external_stock_key . '_' . $loop,
|
||||
'name' => $field_key . '[' . $loop . ']',
|
||||
'value' => $external_stock,
|
||||
'label' => __( 'Externe voorraad', 'siti-stock-plugin' ),
|
||||
'desc_tip' => true,
|
||||
'description' => __( 'Voorraad beschikbaar op externe locatie(s).', 'siti-stock-plugin' ),
|
||||
'type' => 'number',
|
||||
'wrapper_class' => 'form-row form-row-full',
|
||||
'custom_attributes' => array(
|
||||
'step' => '1',
|
||||
'min' => '0',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist external stock for variations.
|
||||
*
|
||||
* @param int $variation_id Variation post ID.
|
||||
* @param int $index Loop index.
|
||||
*/
|
||||
public function save_variation_external_stock_value( $variation_id, $index ) {
|
||||
$field_key = 'variable_' . $this->external_stock_key;
|
||||
|
||||
if ( ! isset( $_POST[ $field_key ][ $index ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
return;
|
||||
}
|
||||
|
||||
$raw_value = wp_unslash( $_POST[ $field_key ][ $index ] ); // 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 );
|
||||
|
||||
update_post_meta( $variation_id, $this->external_stock_key, $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure WooCommerce exposes combined stock (local + external) on the frontend.
|
||||
*
|
||||
@@ -153,6 +224,71 @@ class Siti_Stock_Inventory_Manager {
|
||||
return max( 0, (int) $base_stock ) + $external_stock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the quantity reduced for the order item.
|
||||
*
|
||||
* @param WC_Order_Item_Product $item Order line item.
|
||||
* @param array<string,mixed> $change Change context from WooCommerce.
|
||||
* @return int
|
||||
*/
|
||||
private function determine_reduced_quantity( $item, $change ) {
|
||||
if ( ! is_array( $change ) ) {
|
||||
$change = array(
|
||||
'quantity' => $change,
|
||||
);
|
||||
}
|
||||
|
||||
$from = isset( $change['from'] ) ? $change['from'] : null;
|
||||
$to = isset( $change['to'] ) ? $change['to'] : null;
|
||||
|
||||
if ( null !== $from && null !== $to ) {
|
||||
$qty = $from - $to;
|
||||
} elseif ( isset( $change['quantity'] ) ) {
|
||||
$qty = $change['quantity'];
|
||||
} elseif ( $item instanceof WC_Order_Item_Product ) {
|
||||
$qty = $item->get_quantity();
|
||||
} else {
|
||||
$qty = 0;
|
||||
}
|
||||
|
||||
$qty = function_exists( 'wc_stock_amount' ) ? wc_stock_amount( $qty ) : (int) $qty;
|
||||
|
||||
return max( 0, (int) $qty );
|
||||
}
|
||||
|
||||
/**
|
||||
* Record the stock source split for an order item.
|
||||
*
|
||||
* @param WC_Order_Item_Product $item Order line item.
|
||||
* @param WC_Product $stock_holder Product that holds stock.
|
||||
* @param int $quantity Total reduced quantity.
|
||||
*/
|
||||
private function record_order_item_stock_source_breakdown( $item, $stock_holder, $quantity ) {
|
||||
$previous_local = (int) $stock_holder->get_stock_quantity( 'edit' ) + $quantity;
|
||||
$local_available = max( 0, $previous_local );
|
||||
$external_available = (int) $this->get_external_stock( $stock_holder );
|
||||
$from_local = min( $quantity, $local_available );
|
||||
$remaining = max( 0, $quantity - $from_local );
|
||||
$from_external = min( $remaining, max( 0, $external_available ) );
|
||||
|
||||
$existing = $item->get_meta( $this->order_item_stock_source_key, true );
|
||||
$existing = is_array( $existing )
|
||||
? array(
|
||||
'local' => isset( $existing['local'] ) ? (int) $existing['local'] : 0,
|
||||
'external' => isset( $existing['external'] ) ? (int) $existing['external'] : 0,
|
||||
)
|
||||
: array(
|
||||
'local' => 0,
|
||||
'external' => 0,
|
||||
);
|
||||
|
||||
$existing['local'] += $from_local;
|
||||
$existing['external'] += $from_external;
|
||||
|
||||
$item->update_meta_data( $this->order_item_stock_source_key, $existing );
|
||||
$item->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure local stock only dips below zero when the external stock is depleted.
|
||||
*
|
||||
@@ -161,7 +297,7 @@ class Siti_Stock_Inventory_Manager {
|
||||
* @param WC_Order $order Order instance (unused).
|
||||
*/
|
||||
public function rebalance_stock_after_order_reduction( $item, $change, $order ) {
|
||||
unset( $change, $order );
|
||||
unset( $order );
|
||||
|
||||
if ( ! $item instanceof WC_Order_Item_Product ) {
|
||||
return;
|
||||
@@ -180,6 +316,12 @@ class Siti_Stock_Inventory_Manager {
|
||||
return;
|
||||
}
|
||||
|
||||
$quantity = $this->determine_reduced_quantity( $item, $change );
|
||||
|
||||
if ( $quantity > 0 ) {
|
||||
$this->record_order_item_stock_source_breakdown( $item, $stock_holder, $quantity );
|
||||
}
|
||||
|
||||
$current_local = (int) $stock_holder->get_stock_quantity( 'edit' );
|
||||
$external_stock = $this->get_external_stock( $stock_holder );
|
||||
|
||||
@@ -206,4 +348,91 @@ class Siti_Stock_Inventory_Manager {
|
||||
$stock_holder->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the recorded stock source on the admin order screen.
|
||||
*
|
||||
* @param int $item_id Order item ID.
|
||||
* @param WC_Order_Item $item Order item instance.
|
||||
* @param WC_Product|false $product Product (unused).
|
||||
*/
|
||||
public function render_order_item_stock_source( $item_id, $item, $product ) {
|
||||
unset( $item_id, $product );
|
||||
|
||||
if ( ! is_admin() || ! $item instanceof WC_Order_Item_Product ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = $item->get_meta( $this->order_item_stock_source_key, true );
|
||||
|
||||
if ( ! is_array( $data ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$local = isset( $data['local'] ) ? max( 0, (int) $data['local'] ) : 0;
|
||||
$external = isset( $data['external'] ) ? max( 0, (int) $data['external'] ) : 0;
|
||||
|
||||
if ( $local <= 0 && $external <= 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo '<div class="siti-stock-order-item-sources">';
|
||||
echo '<span class="siti-stock-order-item-sources__label">' . esc_html__( 'Voorraadbron', 'siti-stock-plugin' ) . ':</span>';
|
||||
|
||||
if ( $local > 0 ) {
|
||||
$local_label = sprintf(
|
||||
/* translators: %d: quantity fulfilled from regular stock. */
|
||||
_n( '%d uit reguliere voorraad', '%d uit reguliere voorraad', $local, 'siti-stock-plugin' ),
|
||||
$local
|
||||
);
|
||||
|
||||
printf(
|
||||
'<span class="siti-stock-order-item-sources__badge is-local"><span class="dashicons dashicons-archive" aria-hidden="true"></span>%s</span>',
|
||||
esc_html( $local_label )
|
||||
);
|
||||
}
|
||||
|
||||
if ( $external > 0 ) {
|
||||
$external_label = sprintf(
|
||||
/* translators: %d: quantity fulfilled from external stock. */
|
||||
_n( '%d uit externe voorraad', '%d uit externe voorraad', $external, 'siti-stock-plugin' ),
|
||||
$external
|
||||
);
|
||||
|
||||
printf(
|
||||
'<span class="siti-stock-order-item-sources__badge is-external"><span class="dashicons dashicons-cloud" aria-hidden="true"></span>%s</span>',
|
||||
esc_html( $external_label )
|
||||
);
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue lightweight styling on the order edit screen.
|
||||
*
|
||||
* @param string $hook Current admin hook.
|
||||
*/
|
||||
public function enqueue_order_admin_assets( $hook ) {
|
||||
if ( 'post.php' !== $hook && 'post-new.php' !== $hook ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'get_current_screen' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$screen = get_current_screen();
|
||||
|
||||
if ( ! $screen || 'shop_order' !== $screen->post_type ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style(
|
||||
'siti-stock-order-admin',
|
||||
plugins_url( 'assets/css/order-admin.css', SITI_STOCK_PLUGIN_FILE ),
|
||||
array(),
|
||||
SITI_STOCK_PLUGIN_VERSION
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ 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';
|
||||
require_once __DIR__ . '/class-siti-stock-supplier-exports.php';
|
||||
|
||||
/**
|
||||
* Core plugin bootstrap that wires together all sub-components.
|
||||
@@ -50,6 +51,11 @@ class Siti_Stock_Plugin {
|
||||
*/
|
||||
private $sync_controller;
|
||||
|
||||
/**
|
||||
* @var Siti_Stock_Supplier_Exports
|
||||
*/
|
||||
private $supplier_exports;
|
||||
|
||||
/**
|
||||
* Get singleton instance.
|
||||
*
|
||||
@@ -80,6 +86,10 @@ class Siti_Stock_Plugin {
|
||||
|
||||
$settings_repo = new Siti_Stock_Settings( self::OPTION_KEY );
|
||||
Siti_Stock_Sync_Controller::maybe_schedule_from_settings( self::CRON_HOOK, $settings_repo->get_all() );
|
||||
|
||||
$notices = new Siti_Stock_Admin_Notices();
|
||||
$supplier_exports = new Siti_Stock_Supplier_Exports( $settings_repo, $notices );
|
||||
$supplier_exports->reschedule_all();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,18 +97,22 @@ class Siti_Stock_Plugin {
|
||||
*/
|
||||
public static function deactivate() {
|
||||
Siti_Stock_Sync_Controller::clear_schedule( self::CRON_HOOK );
|
||||
Siti_Stock_Supplier_Exports::clear_all_schedules();
|
||||
}
|
||||
|
||||
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->supplier_exports = new Siti_Stock_Supplier_Exports( $this->settings, $this->notices );
|
||||
$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 = new Siti_Stock_Admin( $this->settings, $this->sync_controller, $this->notices, $this->supplier_exports );
|
||||
|
||||
$this->admin->register_hooks();
|
||||
$this->inventory_manager->register_hooks();
|
||||
$this->sync_controller->register_hooks();
|
||||
$this->supplier_exports->register_hooks();
|
||||
$this->supplier_exports->maybe_schedule_all();
|
||||
add_filter( 'woocommerce_data_stores', array( $this, 'override_product_data_store' ), 20 );
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ class Siti_Stock_Settings {
|
||||
'default_status' => 'instock',
|
||||
'enable_auto_sync' => false,
|
||||
'sync_interval' => 'hourly',
|
||||
'supplier_exports' => array(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
671
includes/class-siti-stock-supplier-exports.php
Normal file
671
includes/class-siti-stock-supplier-exports.php
Normal file
@@ -0,0 +1,671 @@
|
||||
<?php
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles scheduled supplier exports and related admin actions.
|
||||
*/
|
||||
class Siti_Stock_Supplier_Exports {
|
||||
|
||||
const CRON_HOOK = 'siti_stock_plugin_supplier_export';
|
||||
const SUPPLIER_META_KEY = '_wpbc_supplier';
|
||||
|
||||
/**
|
||||
* @var Siti_Stock_Settings
|
||||
*/
|
||||
private $settings;
|
||||
|
||||
/**
|
||||
* @var Siti_Stock_Admin_Notices
|
||||
*/
|
||||
private $notices;
|
||||
|
||||
/**
|
||||
* Cached supplier configs.
|
||||
*
|
||||
* @var array<string,array<string,mixed>>|null
|
||||
*/
|
||||
private $supplier_cache = null;
|
||||
|
||||
/**
|
||||
* @param Siti_Stock_Settings $settings Settings repository.
|
||||
* @param Siti_Stock_Admin_Notices $notices Notice handler.
|
||||
*/
|
||||
public function __construct( Siti_Stock_Settings $settings, Siti_Stock_Admin_Notices $notices ) {
|
||||
$this->settings = $settings;
|
||||
$this->notices = $notices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register hooks for cron and admin actions.
|
||||
*/
|
||||
public function register_hooks() {
|
||||
add_action( self::CRON_HOOK, array( $this, 'run_scheduled_export' ), 10, 1 );
|
||||
add_action( 'admin_post_siti_stock_send_supplier_export', array( $this, 'handle_manual_export' ) );
|
||||
add_action( 'admin_post_siti_stock_save_supplier_schedule', array( $this, 'handle_schedule_save' ) );
|
||||
add_action( 'update_option_' . $this->settings->get_option_key(), array( $this, 'handle_settings_update' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger export when cron fires.
|
||||
*
|
||||
* @param string $supplier_key Supplier identifier.
|
||||
*/
|
||||
public function run_scheduled_export( $supplier_key ) {
|
||||
$this->send_export( $supplier_key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure all configured suppliers are scheduled.
|
||||
*/
|
||||
public function maybe_schedule_all() {
|
||||
$configs = $this->get_supplier_configs();
|
||||
|
||||
foreach ( $configs as $key => $config ) {
|
||||
if ( empty( $config['enabled'] ) || empty( $config['time'] ) ) {
|
||||
$this->clear_schedule_for_supplier( $key );
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! wp_next_scheduled( self::CRON_HOOK, array( $key ) ) ) {
|
||||
$this->schedule_supplier( $key, $config );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear and reschedule cron hooks based on current settings.
|
||||
*/
|
||||
public function reschedule_all() {
|
||||
wp_clear_scheduled_hook( self::CRON_HOOK );
|
||||
|
||||
$configs = $this->get_supplier_configs();
|
||||
foreach ( $configs as $key => $config ) {
|
||||
$this->schedule_supplier( $key, $config );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove scheduled events and cache.
|
||||
*/
|
||||
public function reset_cache() {
|
||||
$this->supplier_cache = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cron events for this feature.
|
||||
*/
|
||||
public static function clear_all_schedules() {
|
||||
wp_clear_scheduled_hook( self::CRON_HOOK );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle manual export submission.
|
||||
*/
|
||||
public function handle_manual_export() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_die( esc_html__( 'Onvoldoende rechten.', 'siti-stock-plugin' ) );
|
||||
}
|
||||
|
||||
check_admin_referer( 'siti_stock_send_supplier_export' );
|
||||
|
||||
$supplier_key = isset( $_POST['supplier_key'] ) ? sanitize_text_field( wp_unslash( $_POST['supplier_key'] ) ) : '';
|
||||
$result = $this->send_export( $supplier_key );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
$this->notices->add_notice( $result->get_error_message(), 'error' );
|
||||
} else {
|
||||
$this->notices->add_notice(
|
||||
sprintf(
|
||||
/* translators: %s supplier label */
|
||||
__( 'Export voor %s verzonden.', 'siti-stock-plugin' ),
|
||||
isset( $result['label'] ) ? $result['label'] : $supplier_key
|
||||
),
|
||||
'success'
|
||||
);
|
||||
}
|
||||
|
||||
wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url( 'admin.php?page=siti-stock-plugin-stock-updates' ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle saving of schedule settings.
|
||||
*/
|
||||
public function handle_schedule_save() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_die( esc_html__( 'Onvoldoende rechten.', 'siti-stock-plugin' ) );
|
||||
}
|
||||
|
||||
check_admin_referer( 'siti_stock_save_supplier_schedule' );
|
||||
|
||||
$raw_input = isset( $_POST['supplier_exports'] ) ? (array) wp_unslash( $_POST['supplier_exports'] ) : array();
|
||||
$this->persist_supplier_settings( $raw_input );
|
||||
$this->notices->add_notice( __( 'Voorkeuren opgeslagen.', 'siti-stock-plugin' ) );
|
||||
|
||||
wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url( 'admin.php?page=siti-stock-plugin-stock-updates' ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* React to option updates by refreshing cron.
|
||||
*
|
||||
* @param array<string,mixed> $old_value Old settings.
|
||||
* @param array<string,mixed> $value New settings.
|
||||
*/
|
||||
public function handle_settings_update( $old_value, $value ) {
|
||||
unset( $old_value ); // Unused.
|
||||
unset( $value );
|
||||
|
||||
$this->reset_cache();
|
||||
$this->reschedule_all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve supplier configs for display/scheduling.
|
||||
*
|
||||
* @return array<string,array<string,mixed>>
|
||||
*/
|
||||
public function get_supplier_configs() {
|
||||
if ( null !== $this->supplier_cache ) {
|
||||
return $this->supplier_cache;
|
||||
}
|
||||
|
||||
$settings = $this->settings->get_all();
|
||||
$configured = isset( $settings['supplier_exports'] ) && is_array( $settings['supplier_exports'] )
|
||||
? $settings['supplier_exports']
|
||||
: array();
|
||||
|
||||
$configured = $this->ensure_defaults( $configured );
|
||||
$configured = $this->ensure_distinct_meta_suppliers( $configured );
|
||||
|
||||
uasort(
|
||||
$configured,
|
||||
function ( $a, $b ) {
|
||||
return strcmp(
|
||||
strtolower( isset( $a['supplier'] ) ? $a['supplier'] : '' ),
|
||||
strtolower( isset( $b['supplier'] ) ? $b['supplier'] : '' )
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
$this->supplier_cache = $configured;
|
||||
|
||||
return $configured;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve config for specific supplier key.
|
||||
*
|
||||
* @param string $supplier_key Supplier key.
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
public function get_supplier_config( $supplier_key ) {
|
||||
$configs = $this->get_supplier_configs();
|
||||
|
||||
return isset( $configs[ $supplier_key ] ) ? $configs[ $supplier_key ] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the mail export for supplier key.
|
||||
*
|
||||
* @param string $supplier_key Supplier key.
|
||||
* @return array<string,mixed>|WP_Error
|
||||
*/
|
||||
public function send_export( $supplier_key ) {
|
||||
$supplier_key = sanitize_title( $supplier_key );
|
||||
$config = $this->get_supplier_config( $supplier_key );
|
||||
|
||||
if ( empty( $config ) || empty( $config['supplier'] ) ) {
|
||||
return new WP_Error( 'siti_stock_unknown_supplier', __( 'Onbekende leverancier.', 'siti-stock-plugin' ) );
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'wc_get_orders' ) ) {
|
||||
return new WP_Error( 'siti_stock_missing_wc', __( 'WooCommerce is vereist voor de exports.', 'siti-stock-plugin' ) );
|
||||
}
|
||||
|
||||
$data = $this->collect_rows_for_supplier( $config['supplier'] );
|
||||
|
||||
$admin_email = get_option( 'admin_email' );
|
||||
if ( ! $admin_email || ! is_email( $admin_email ) ) {
|
||||
return new WP_Error( 'siti_stock_missing_email', __( 'Admin e-mailadres kon niet worden opgehaald.', 'siti-stock-plugin' ) );
|
||||
}
|
||||
|
||||
$csv = $this->render_csv( $data['rows'] );
|
||||
$subject = sprintf(
|
||||
/* translators: %s supplier name */
|
||||
__( 'Bestellingen %s (24 uur)', 'siti-stock-plugin' ),
|
||||
$config['supplier']
|
||||
);
|
||||
|
||||
$body_lines = array();
|
||||
$body_lines[] = sprintf(
|
||||
/* translators: %s supplier name */
|
||||
__( 'Overzicht voor leverancier %s.', 'siti-stock-plugin' ),
|
||||
$config['supplier']
|
||||
);
|
||||
$body_lines[] = sprintf(
|
||||
/* translators: %d total items */
|
||||
__( 'Totaal aantal regels: %d.', 'siti-stock-plugin' ),
|
||||
count( $data['rows'] )
|
||||
);
|
||||
if ( empty( $data['rows'] ) ) {
|
||||
$body_lines[] = __( 'Er zijn geen bestellingen gevonden in de afgelopen 24 uur.', 'siti-stock-plugin' );
|
||||
}
|
||||
$body_lines[] = '';
|
||||
$body_lines[] = $csv;
|
||||
|
||||
$headers = array( 'Content-Type: text/plain; charset=UTF-8' );
|
||||
$sent = wp_mail( $admin_email, $subject, implode( "\n", $body_lines ), $headers );
|
||||
|
||||
if ( ! $sent ) {
|
||||
return new WP_Error( 'siti_stock_mail_failed', __( 'E-mail verzenden is mislukt.', 'siti-stock-plugin' ) );
|
||||
}
|
||||
|
||||
return array(
|
||||
'label' => $config['supplier'],
|
||||
'rows' => count( $data['rows'] ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist sanitized supplier settings.
|
||||
*
|
||||
* @param array<string,mixed> $input Input from form.
|
||||
*/
|
||||
private function persist_supplier_settings( array $input ) {
|
||||
$current = $this->get_supplier_configs();
|
||||
$sanitized = array();
|
||||
|
||||
foreach ( $current as $key => $config ) {
|
||||
$row = isset( $input[ $key ] ) ? (array) $input[ $key ] : array();
|
||||
$time_raw = isset( $row['time'] ) ? sanitize_text_field( $row['time'] ) : ( isset( $config['time'] ) ? $config['time'] : '' );
|
||||
$time = $this->normalize_time( $time_raw );
|
||||
|
||||
$sanitized[ $key ] = array(
|
||||
'supplier' => isset( $config['supplier'] ) ? $config['supplier'] : '',
|
||||
'time' => $time,
|
||||
'enabled' => ! empty( $row['enabled'] ),
|
||||
);
|
||||
}
|
||||
|
||||
$settings = $this->settings->get_all();
|
||||
$settings['supplier_exports'] = $sanitized;
|
||||
$this->settings->reset_cache();
|
||||
update_option( $this->settings->get_option_key(), $settings );
|
||||
$this->reset_cache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect CSV rows for supplier meta value.
|
||||
*
|
||||
* @param string $supplier Supplier label.
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function collect_rows_for_supplier( $supplier ) {
|
||||
$key = $this->normalize_supplier_key( $supplier );
|
||||
|
||||
$after_timestamp = current_time( 'timestamp' ) - DAY_IN_SECONDS;
|
||||
$date_filter = array(
|
||||
'after' => gmdate( 'Y-m-d H:i:s', $after_timestamp ),
|
||||
);
|
||||
|
||||
if ( class_exists( 'WC_DateTime' ) ) {
|
||||
$after_object = new WC_DateTime( '@' . $after_timestamp );
|
||||
$after_object->setTimezone( new DateTimeZone( 'UTC' ) );
|
||||
$date_filter = array( 'after' => $after_object );
|
||||
}
|
||||
|
||||
$query = new WC_Order_Query(
|
||||
array(
|
||||
'status' => array( 'pending', 'processing', 'on-hold', 'completed' ),
|
||||
'limit' => -1,
|
||||
'type' => 'shop_order',
|
||||
'return' => 'objects',
|
||||
'date_created' => $date_filter,
|
||||
)
|
||||
);
|
||||
|
||||
$orders = $query->get_orders();
|
||||
$results = array();
|
||||
|
||||
foreach ( $orders as $order ) {
|
||||
foreach ( $order->get_items( 'line_item' ) as $item ) {
|
||||
$product = $item->get_product();
|
||||
|
||||
if ( ! $product ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$item_supplier = $this->get_product_supplier( $product );
|
||||
|
||||
if ( '' === $item_supplier || $this->normalize_supplier_key( $item_supplier ) !== $key ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sku = $this->get_product_sku( $product );
|
||||
|
||||
if ( '' === $sku ) {
|
||||
$sku = sprintf( 'product-%d', $product->get_id() );
|
||||
}
|
||||
|
||||
$index = strtolower( $sku );
|
||||
|
||||
if ( ! isset( $results[ $index ] ) ) {
|
||||
$results[ $index ] = array(
|
||||
'sku' => $sku,
|
||||
'quantity' => 0,
|
||||
);
|
||||
}
|
||||
|
||||
$results[ $index ]['quantity'] += (int) $item->get_quantity();
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'rows' => array_values( $results ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve supplier meta for product (falls back to parent).
|
||||
*
|
||||
* @param WC_Product $product Product.
|
||||
* @return string
|
||||
*/
|
||||
private function get_product_supplier( $product ) {
|
||||
$product_id = $product->get_id();
|
||||
$value = get_post_meta( $product_id, self::SUPPLIER_META_KEY, true );
|
||||
|
||||
if ( '' !== $value ) {
|
||||
return trim( (string) $value );
|
||||
}
|
||||
|
||||
$parent_id = $product->get_parent_id();
|
||||
|
||||
if ( $parent_id ) {
|
||||
$parent_value = get_post_meta( $parent_id, self::SUPPLIER_META_KEY, true );
|
||||
|
||||
if ( '' !== $parent_value ) {
|
||||
return trim( (string) $parent_value );
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve SKU, falling back to parent product SKU.
|
||||
*
|
||||
* @param WC_Product $product Product.
|
||||
* @return string
|
||||
*/
|
||||
private function get_product_sku( $product ) {
|
||||
$sku = $product->get_sku();
|
||||
|
||||
if ( '' !== $sku ) {
|
||||
return $sku;
|
||||
}
|
||||
|
||||
$parent_id = $product->get_parent_id();
|
||||
|
||||
if ( $parent_id ) {
|
||||
$parent = wc_get_product( $parent_id );
|
||||
|
||||
if ( $parent && '' !== $parent->get_sku() ) {
|
||||
return $parent->get_sku();
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an array of SKU rows to CSV text separated with semicolons.
|
||||
*
|
||||
* @param array<int,array<string,int|string>> $rows Rows.
|
||||
* @return string
|
||||
*/
|
||||
private function render_csv( array $rows ) {
|
||||
$lines = array( 'sku;aantal' );
|
||||
|
||||
foreach ( $rows as $row ) {
|
||||
$sku = isset( $row['sku'] ) ? $row['sku'] : '';
|
||||
$quantity = isset( $row['quantity'] ) ? (int) $row['quantity'] : 0;
|
||||
$lines[] = sprintf( '%s;%d', $sku, $quantity );
|
||||
}
|
||||
|
||||
return implode( "\n", $lines );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule supplier if enabled.
|
||||
*
|
||||
* @param string $supplier_key Key.
|
||||
* @param array<string,mixed>|null $config Config.
|
||||
*/
|
||||
private function schedule_supplier( $supplier_key, $config ) {
|
||||
if ( empty( $config ) || empty( $config['enabled'] ) || empty( $config['time'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$timestamp = $this->calculate_next_timestamp( $config['time'] );
|
||||
wp_schedule_event( $timestamp, 'daily', self::CRON_HOOK, array( $supplier_key ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove scheduled event for supplier.
|
||||
*
|
||||
* @param string $supplier_key Key.
|
||||
*/
|
||||
private function clear_schedule_for_supplier( $supplier_key ) {
|
||||
$timestamp = wp_next_scheduled( self::CRON_HOOK, array( $supplier_key ) );
|
||||
|
||||
while ( $timestamp ) {
|
||||
wp_unschedule_event( $timestamp, self::CRON_HOOK, array( $supplier_key ) );
|
||||
$timestamp = wp_next_scheduled( self::CRON_HOOK, array( $supplier_key ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure default suppliers exist in config.
|
||||
*
|
||||
* @param array<string,mixed> $configured Configured values.
|
||||
* @return array<string,array<string,mixed>>
|
||||
*/
|
||||
private function ensure_defaults( array $configured ) {
|
||||
foreach ( $this->get_default_suppliers() as $key => $data ) {
|
||||
if ( isset( $configured[ $key ] ) ) {
|
||||
if ( empty( $configured[ $key ]['supplier'] ) ) {
|
||||
$configured[ $key ]['supplier'] = $data['label'];
|
||||
}
|
||||
$configured[ $key ]['time'] = $this->normalize_time(
|
||||
isset( $configured[ $key ]['time'] ) ? $configured[ $key ]['time'] : $data['time']
|
||||
);
|
||||
if ( ! isset( $configured[ $key ]['enabled'] ) ) {
|
||||
$configured[ $key ]['enabled'] = $data['enabled'];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$configured[ $key ] = array(
|
||||
'supplier' => $data['label'],
|
||||
'time' => $data['time'],
|
||||
'enabled' => $data['enabled'],
|
||||
);
|
||||
}
|
||||
|
||||
return $configured;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add entries for suppliers found in product meta.
|
||||
*
|
||||
* @param array<string,mixed> $configured Configured values.
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function ensure_distinct_meta_suppliers( array $configured ) {
|
||||
foreach ( $this->get_distinct_suppliers() as $supplier ) {
|
||||
$key = $this->normalize_supplier_key( $supplier );
|
||||
|
||||
if ( isset( $configured[ $key ] ) ) {
|
||||
$configured[ $key ]['supplier'] = $supplier;
|
||||
continue;
|
||||
}
|
||||
|
||||
$configured[ $key ] = array(
|
||||
'supplier' => $supplier,
|
||||
'time' => $this->get_default_time_for_key( $key ),
|
||||
'enabled' => $this->is_default_enabled( $key ),
|
||||
);
|
||||
}
|
||||
|
||||
return $configured;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve suppliers from product meta.
|
||||
*
|
||||
* @return array<int,string>
|
||||
*/
|
||||
private function get_distinct_suppliers() {
|
||||
global $wpdb;
|
||||
|
||||
$meta_table = $wpdb->postmeta;
|
||||
$post_table = $wpdb->posts;
|
||||
$sql = "
|
||||
SELECT DISTINCT pm.meta_value
|
||||
FROM {$meta_table} pm
|
||||
INNER JOIN {$post_table} p ON p.ID = pm.post_id
|
||||
WHERE pm.meta_key = %s
|
||||
AND pm.meta_value <> ''
|
||||
AND p.post_type IN ('product','product_variation')
|
||||
AND p.post_status NOT IN ('trash','auto-draft')
|
||||
";
|
||||
|
||||
$results = $wpdb->get_col( $wpdb->prepare( $sql, self::SUPPLIER_META_KEY ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
|
||||
if ( empty( $results ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return array_map(
|
||||
function ( $value ) {
|
||||
return trim( (string) $value );
|
||||
},
|
||||
$results
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize supplier key to slug.
|
||||
*
|
||||
* @param string $value Value.
|
||||
* @return string
|
||||
*/
|
||||
private function normalize_supplier_key( $value ) {
|
||||
return sanitize_title( (string) $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize HH:MM strings, fallback to midnight.
|
||||
*
|
||||
* @param string $time Time string.
|
||||
* @return string
|
||||
*/
|
||||
private function normalize_time( $time ) {
|
||||
$time = trim( (string) $time );
|
||||
if ( ! preg_match( '/^\d{1,2}:\d{2}$/', $time ) ) {
|
||||
return '00:00';
|
||||
}
|
||||
|
||||
list( $hour, $minute ) = array_map( 'intval', explode( ':', $time, 2 ) );
|
||||
|
||||
$hour = max( 0, min( 23, $hour ) );
|
||||
$minute = max( 0, min( 59, $minute ) );
|
||||
|
||||
return sprintf( '%02d:%02d', $hour, $minute );
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the next timestamp for a given HH:MM.
|
||||
*
|
||||
* @param string $time Time in HH:MM.
|
||||
* @return int
|
||||
*/
|
||||
private function calculate_next_timestamp( $time ) {
|
||||
$time = $this->normalize_time( $time );
|
||||
list( $hour, $minute ) = array_map( 'intval', explode( ':', $time ) );
|
||||
|
||||
$timezone = wp_timezone();
|
||||
$now = new DateTimeImmutable( 'now', $timezone );
|
||||
$target = $now->setTime( $hour, $minute );
|
||||
|
||||
if ( $target <= $now ) {
|
||||
$target = $target->modify( '+1 day' );
|
||||
}
|
||||
|
||||
return $target->getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Default suppliers (label + default time/settings).
|
||||
*
|
||||
* @return array<string,array<string,mixed>>
|
||||
*/
|
||||
private function get_default_suppliers() {
|
||||
return array(
|
||||
'orion' => array(
|
||||
'label' => 'Orion',
|
||||
'time' => '09:00',
|
||||
'enabled' => true,
|
||||
),
|
||||
'shots' => array(
|
||||
'label' => 'Shots',
|
||||
'time' => '10:00',
|
||||
'enabled' => true,
|
||||
),
|
||||
'stots' => array(
|
||||
'label' => 'Stots',
|
||||
'time' => '10:00',
|
||||
'enabled' => true,
|
||||
),
|
||||
'leg-avenue' => array(
|
||||
'label' => 'Leg Avenue',
|
||||
'time' => '14:00',
|
||||
'enabled' => true,
|
||||
),
|
||||
'oproducts' => array(
|
||||
'label' => 'Oproducts',
|
||||
'time' => '13:00',
|
||||
'enabled' => true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve default time for supplier key.
|
||||
*
|
||||
* @param string $key Supplier key.
|
||||
* @return string
|
||||
*/
|
||||
private function get_default_time_for_key( $key ) {
|
||||
$defaults = $this->get_default_suppliers();
|
||||
|
||||
return isset( $defaults[ $key ] ) ? $defaults[ $key ]['time'] : '09:00';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine default enabled state for supplier key.
|
||||
*
|
||||
* @param string $key Supplier key.
|
||||
* @return bool
|
||||
*/
|
||||
private function is_default_enabled( $key ) {
|
||||
$defaults = $this->get_default_suppliers();
|
||||
|
||||
return isset( $defaults[ $key ] ) ? (bool) $defaults[ $key ]['enabled'] : false;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* Plugin Name: Siti Stock Plugin
|
||||
* Plugin URI: https://github.com/SitiWeb/siti-stock-plugin
|
||||
* Description: Synchroniseert WooCommerce voorraad met het externe Siti voorraadplatform.
|
||||
* Version: 1.0.0
|
||||
* Version: 1.2.3
|
||||
* Author: Siti Web
|
||||
* Author URI: https://www.siti.nl
|
||||
* Requires PHP: 8.1
|
||||
@@ -16,12 +16,15 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
define( 'SITI_STOCK_PLUGIN_VERSION', '0.1.0' );
|
||||
define( 'SITI_STOCK_PLUGIN_VERSION', '1.2.3' );
|
||||
define( 'SITI_STOCK_PLUGIN_FILE', __FILE__ );
|
||||
define( 'SITI_STOCK_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
|
||||
require_once __DIR__ . '/includes/class-siti-stock-plugin.php';
|
||||
|
||||
if ( ! class_exists( 'SitiWebUpdater' ) ) {
|
||||
require_once __DIR__ . '/SitiWebUpdater.php';
|
||||
}
|
||||
|
||||
register_activation_hook( __FILE__, array( 'Siti_Stock_Plugin', 'activate' ) );
|
||||
register_deactivation_hook( __FILE__, array( 'Siti_Stock_Plugin', 'deactivate' ) );
|
||||
|
||||
Reference in New Issue
Block a user