feat: initial WooDoo plugin – WooCommerce & Odoo 19 integration
- 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>
This commit is contained in:
254
includes/class-woodoo-orders.php
Normal file
254
includes/class-woodoo-orders.php
Normal file
@@ -0,0 +1,254 @@
|
||||
<?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 );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user