255 lines
10 KiB
PHP
255 lines
10 KiB
PHP
|
|
<?php
|
|||
|
|
/**
|
|||
|
|
* WooCommerce Order → Odoo Sales Order sync.
|
|||
|
|
*
|
|||
|
|
* When a WC order reaches "processing" status, this class:
|
|||
|
|
* 1. Finds or creates the customer in Odoo (res.partner)
|
|||
|
|
* 2. Creates a sale.order with all line items
|
|||
|
|
* 3. Stores the Odoo SO ID in WC order meta
|
|||
|
|
*
|
|||
|
|
* Products are matched by SKU (default_code). Unknown SKUs become
|
|||
|
|
* generic service lines so nothing is lost.
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
defined( 'ABSPATH' ) || exit;
|
|||
|
|
|
|||
|
|
class WooDoo_Orders {
|
|||
|
|
|
|||
|
|
public static function init(): void {
|
|||
|
|
if ( ! get_option( 'woodoo_sync_orders', 1 ) ) return;
|
|||
|
|
|
|||
|
|
// Trigger sync when order becomes "processing"
|
|||
|
|
add_action( 'woocommerce_order_status_processing', [ __CLASS__, 'sync_order' ], 10, 2 );
|
|||
|
|
|
|||
|
|
// Also show Odoo SO reference in the WC order admin screen
|
|||
|
|
add_action( 'woocommerce_admin_order_data_after_order_details', [ __CLASS__, 'show_so_meta' ] );
|
|||
|
|
|
|||
|
|
// Manual re-sync button (order actions)
|
|||
|
|
add_filter( 'woocommerce_order_actions', [ __CLASS__, 'add_order_action' ] );
|
|||
|
|
add_action( 'woocommerce_order_action_woodoo_resync', [ __CLASS__, 'manual_resync' ] );
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Main Sync Entry ───────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
public static function sync_order( int $order_id, WC_Order $order ): void {
|
|||
|
|
// Skip if already synced
|
|||
|
|
if ( $order->get_meta( '_woodoo_so_id' ) ) return;
|
|||
|
|
|
|||
|
|
$api = woodoo_api();
|
|||
|
|
if ( ! $api ) {
|
|||
|
|
$order->add_order_note( __( '[WooDoo] Odoo integration not configured – order not synced.', 'woodoo' ) );
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$partner_id = self::resolve_partner( $api, $order );
|
|||
|
|
if ( ! $partner_id ) {
|
|||
|
|
$order->add_order_note( __( '[WooDoo] Could not find/create Odoo partner – order not synced.', 'woodoo' ) );
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$so_id = self::create_sales_order( $api, $order, $partner_id );
|
|||
|
|
|
|||
|
|
if ( $so_id ) {
|
|||
|
|
$order->update_meta_data( '_woodoo_so_id', $so_id );
|
|||
|
|
$order->update_meta_data( '_woodoo_partner_id', $partner_id );
|
|||
|
|
$order->save_meta_data();
|
|||
|
|
|
|||
|
|
$odoo_url = get_option( 'woodoo_odoo_url', '' );
|
|||
|
|
$so_link = $odoo_url ? sprintf(
|
|||
|
|
'<a href="%s/odoo/sales/%d" target="_blank">#%d</a>',
|
|||
|
|
esc_url( $odoo_url ),
|
|||
|
|
$so_id,
|
|||
|
|
$so_id
|
|||
|
|
) : "#$so_id";
|
|||
|
|
|
|||
|
|
/* translators: %s: Odoo SO link */
|
|||
|
|
$order->add_order_note( sprintf( __( '[WooDoo] Sales Order created in Odoo: %s', 'woodoo' ), $so_link ) );
|
|||
|
|
|
|||
|
|
// If the WC customer has no partner_id set yet, save it
|
|||
|
|
$wc_user_id = $order->get_customer_id();
|
|||
|
|
if ( $wc_user_id && ! get_user_meta( $wc_user_id, 'woodoo_odoo_partner_id', true ) ) {
|
|||
|
|
update_user_meta( $wc_user_id, 'woodoo_odoo_partner_id', $partner_id );
|
|||
|
|
// Count synced orders
|
|||
|
|
$count = (int) get_user_meta( $wc_user_id, 'woodoo_so_count', true );
|
|||
|
|
update_user_meta( $wc_user_id, 'woodoo_so_count', $count + 1 );
|
|||
|
|
} else {
|
|||
|
|
$count = (int) get_user_meta( $wc_user_id, 'woodoo_so_count', true );
|
|||
|
|
update_user_meta( $wc_user_id, 'woodoo_so_count', $count + 1 );
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
$order->add_order_note( __( '[WooDoo] Failed to create Odoo Sales Order. Check WooDoo logs.', 'woodoo' ) );
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Partner Resolution ────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
private static function resolve_partner( WooDoo_API $api, WC_Order $order ): ?int {
|
|||
|
|
// 1. Try from WC user meta first
|
|||
|
|
$wc_user_id = $order->get_customer_id();
|
|||
|
|
if ( $wc_user_id ) {
|
|||
|
|
$partner_id = (int) get_user_meta( $wc_user_id, 'woodoo_odoo_partner_id', true );
|
|||
|
|
if ( $partner_id > 0 ) return $partner_id;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. Search Odoo by billing email
|
|||
|
|
$email = $order->get_billing_email();
|
|||
|
|
if ( $email ) {
|
|||
|
|
$found = $api->search( 'res.partner', [ [ 'email', '=', $email ] ], 1 );
|
|||
|
|
if ( ! empty( $found ) ) return (int) $found[0];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. Create new partner from order billing info
|
|||
|
|
$name = trim( $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() );
|
|||
|
|
if ( ! $name ) $name = $order->get_billing_company() ?: 'WooCommerce Customer';
|
|||
|
|
|
|||
|
|
$vals = [
|
|||
|
|
'name' => $name,
|
|||
|
|
'email' => $email ?: false,
|
|||
|
|
'phone' => $order->get_billing_phone() ?: false,
|
|||
|
|
'street' => $order->get_billing_address_1() ?: false,
|
|||
|
|
'street2' => $order->get_billing_address_2() ?: false,
|
|||
|
|
'city' => $order->get_billing_city() ?: false,
|
|||
|
|
'zip' => $order->get_billing_postcode() ?: false,
|
|||
|
|
'is_company' => (bool) $order->get_billing_company(),
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// Country
|
|||
|
|
$country_code = $order->get_billing_country();
|
|||
|
|
if ( $country_code ) {
|
|||
|
|
$countries = $api->search_read(
|
|||
|
|
'res.country',
|
|||
|
|
[ [ 'code', '=', $country_code ] ],
|
|||
|
|
[ 'id' ],
|
|||
|
|
1
|
|||
|
|
);
|
|||
|
|
if ( ! empty( $countries ) ) {
|
|||
|
|
$vals['country_id'] = $countries[0]['id'];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Remove false values
|
|||
|
|
$vals = array_filter( $vals, fn( $v ) => $v !== false );
|
|||
|
|
|
|||
|
|
return $api->create( 'res.partner', $vals );
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Sales Order Creation ──────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
private static function create_sales_order( WooDoo_API $api, WC_Order $order, int $partner_id ): ?int {
|
|||
|
|
$lines = self::build_order_lines( $api, $order );
|
|||
|
|
|
|||
|
|
$so_vals = [
|
|||
|
|
'partner_id' => $partner_id,
|
|||
|
|
'client_order_ref' => 'WC-' . $order->get_order_number(),
|
|||
|
|
'note' => $order->get_customer_note() ?: false,
|
|||
|
|
'order_line' => $lines,
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// If there's a WC-created date set it
|
|||
|
|
$date_created = $order->get_date_created();
|
|||
|
|
if ( $date_created ) {
|
|||
|
|
$so_vals['date_order'] = $date_created->date( 'Y-m-d H:i:s' );
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Remove false values
|
|||
|
|
$so_vals = array_filter( $so_vals, fn( $v ) => $v !== false );
|
|||
|
|
|
|||
|
|
return $api->create( 'sale.order', $so_vals );
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Order Line Builder ────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
private static function build_order_lines( WooDoo_API $api, WC_Order $order ): array {
|
|||
|
|
$lines = [];
|
|||
|
|
|
|||
|
|
foreach ( $order->get_items() as $item ) {
|
|||
|
|
/** @var WC_Order_Item_Product $item */
|
|||
|
|
$sku = '';
|
|||
|
|
$product = $item->get_product();
|
|||
|
|
if ( $product ) $sku = $product->get_sku();
|
|||
|
|
|
|||
|
|
$odoo_product_id = $api->find_product_by_sku( $sku );
|
|||
|
|
|
|||
|
|
$line = [
|
|||
|
|
'name' => $item->get_name(),
|
|||
|
|
'product_uom_qty' => (float) $item->get_quantity(),
|
|||
|
|
'price_unit' => (float) $order->get_item_subtotal( $item, false, false ),
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
if ( $odoo_product_id ) {
|
|||
|
|
$line['product_id'] = $odoo_product_id;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Command.create: [0, 0, vals]
|
|||
|
|
$lines[] = [ 0, 0, $line ];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Shipping line
|
|||
|
|
$shipping_total = (float) $order->get_shipping_total();
|
|||
|
|
if ( $shipping_total > 0 ) {
|
|||
|
|
$lines[] = [ 0, 0, [
|
|||
|
|
'name' => sprintf(
|
|||
|
|
__( 'Shipping: %s', 'woodoo' ),
|
|||
|
|
$order->get_shipping_method()
|
|||
|
|
),
|
|||
|
|
'product_uom_qty' => 1,
|
|||
|
|
'price_unit' => $shipping_total,
|
|||
|
|
] ];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Discount line (if any coupon was applied)
|
|||
|
|
$discount = (float) $order->get_discount_total();
|
|||
|
|
if ( $discount > 0 ) {
|
|||
|
|
$lines[] = [ 0, 0, [
|
|||
|
|
'name' => __( 'Discount', 'woodoo' ),
|
|||
|
|
'product_uom_qty' => 1,
|
|||
|
|
'price_unit' => -$discount,
|
|||
|
|
] ];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $lines;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Admin UI ──────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
public static function show_so_meta( WC_Order $order ): void {
|
|||
|
|
$so_id = $order->get_meta( '_woodoo_so_id' );
|
|||
|
|
$partner_id = $order->get_meta( '_woodoo_partner_id' );
|
|||
|
|
|
|||
|
|
if ( ! $so_id && ! $partner_id ) return;
|
|||
|
|
|
|||
|
|
$odoo_url = get_option( 'woodoo_odoo_url', '' );
|
|||
|
|
|
|||
|
|
echo '<div class="woodoo-order-meta" style="margin-top:12px;">';
|
|||
|
|
echo '<h4 style="margin:0 0 4px;">' . esc_html__( 'Odoo', 'woodoo' ) . '</h4>';
|
|||
|
|
|
|||
|
|
if ( $so_id ) {
|
|||
|
|
$link = $odoo_url
|
|||
|
|
? sprintf( '<a href="%s/odoo/sales/%d" target="_blank">SO #%d</a>', esc_url( $odoo_url ), (int) $so_id, (int) $so_id )
|
|||
|
|
: 'SO #' . esc_html( $so_id );
|
|||
|
|
echo '<p><strong>' . esc_html__( 'Sales Order:', 'woodoo' ) . '</strong> ' . wp_kses_post( $link ) . '</p>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ( $partner_id ) {
|
|||
|
|
$link = $odoo_url
|
|||
|
|
? sprintf( '<a href="%s/odoo/contacts/%d" target="_blank">Partner #%d</a>', esc_url( $odoo_url ), (int) $partner_id, (int) $partner_id )
|
|||
|
|
: 'Partner #' . esc_html( $partner_id );
|
|||
|
|
echo '<p><strong>' . esc_html__( 'Partner:', 'woodoo' ) . '</strong> ' . wp_kses_post( $link ) . '</p>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
echo '</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static function add_order_action( array $actions ): array {
|
|||
|
|
$actions['woodoo_resync'] = __( 'Re-sync to Odoo (WooDoo)', 'woodoo' );
|
|||
|
|
return $actions;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static function manual_resync( WC_Order $order ): void {
|
|||
|
|
// Clear previous SO ID so sync runs fresh
|
|||
|
|
$order->delete_meta_data( '_woodoo_so_id' );
|
|||
|
|
$order->save_meta_data();
|
|||
|
|
self::sync_order( $order->get_id(), $order );
|
|||
|
|
}
|
|||
|
|
}
|