- Odoo JSON-RPC client (no Composer, uses wp_remote_post) - Admin settings page under WooCommerce with connection test - Customer linking: search Odoo partners from WP user profile - My Account: Odoo Invoices tab with PDF proxy download - My Account: Book a Meeting tab (slot calculator + calendar.event) - WC order → Odoo sale.order auto-sync on processing status - Products matched by SKU; partner auto-created from billing info - Uninstall cleanup (options, user meta, order meta, DB table) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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 );
|
||
}
|
||
}
|