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:
Malin
2026-04-01 13:58:27 +02:00
commit 68c1ff4455
13 changed files with 2478 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
<?php
/**
* Template: My Account Book a Meeting
*
* Variables available:
* $upcoming array Upcoming booked meetings from Odoo
* $partner_id int Odoo partner ID (0 if unlinked)
* $user WP_User Current user
*/
defined( 'ABSPATH' ) || exit;
wp_enqueue_style( 'woodoo-frontend' );
wp_enqueue_script( 'woodoo-frontend' );
wp_localize_script( 'woodoo-frontend', 'WooDooCalendar', [
'nonce' => wp_create_nonce( 'woodoo_calendar' ),
'ajax_url' => admin_url( 'admin-ajax.php' ),
'i18n' => [
'loading' => __( 'Loading…', 'woodoo' ),
'no_slots' => __( 'No available slots for this day.', 'woodoo' ),
'select_slot' => __( 'Select a time slot', 'woodoo' ),
'booking' => __( 'Booking…', 'woodoo' ),
'book_btn' => __( 'Book this slot', 'woodoo' ),
'cancel_confirm' => __( 'Cancel this meeting?', 'woodoo' ),
'cancelling' => __( 'Cancelling…', 'woodoo' ),
'cancelled' => __( 'Meeting cancelled.', 'woodoo' ),
'error' => __( 'Something went wrong. Please try again.', 'woodoo' ),
],
] );
?>
<div class="woodoo-calendar-wrap">
<!-- ── Upcoming Meetings ──────────────────────────────────────────── -->
<div class="woodoo-section">
<h3><?php esc_html_e( 'Your Upcoming Meetings', 'woodoo' ); ?></h3>
<div id="woodoo-meetings-list">
<?php if ( empty( $upcoming ) ) : ?>
<p class="woodoo-empty"><?php esc_html_e( 'No upcoming meetings scheduled.', 'woodoo' ); ?></p>
<?php else : ?>
<div class="woodoo-meetings-grid">
<?php foreach ( $upcoming as $event ) :
$start_ts = strtotime( $event['start'] );
$end_ts = strtotime( $event['stop'] );
?>
<div class="woodoo-meeting-card" data-event-id="<?php echo esc_attr( $event['id'] ); ?>">
<div class="woodoo-meeting-card__date">
<span class="woodoo-meeting-card__day"><?php echo esc_html( gmdate( 'd', $start_ts ) ); ?></span>
<span class="woodoo-meeting-card__month"><?php echo esc_html( gmdate( 'M', $start_ts ) ); ?></span>
</div>
<div class="woodoo-meeting-card__info">
<strong><?php echo esc_html( $event['name'] ); ?></strong>
<span class="woodoo-meeting-card__time">
<?php printf(
'%s %s',
esc_html( gmdate( 'H:i', $start_ts ) ),
esc_html( gmdate( 'H:i', $end_ts ) )
); ?>
</span>
<?php if ( ! empty( $event['videocall_location'] ) ) : ?>
<a href="<?php echo esc_url( $event['videocall_location'] ); ?>"
target="_blank" rel="noopener" class="woodoo-meeting-card__video">
<?php esc_html_e( 'Join Video Call', 'woodoo' ); ?>
</a>
<?php elseif ( ! empty( $event['location'] ) ) : ?>
<span class="woodoo-meeting-card__loc">
<?php echo esc_html( $event['location'] ); ?>
</span>
<?php endif; ?>
</div>
<div class="woodoo-meeting-card__actions">
<button class="woodoo-cancel-meeting woodoo-btn woodoo-btn--sm woodoo-btn--outline"
data-event-id="<?php echo esc_attr( $event['id'] ); ?>">
<?php esc_html_e( 'Cancel', 'woodoo' ); ?>
</button>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<!-- ── Book New Meeting ───────────────────────────────────────────── -->
<div class="woodoo-section">
<h3><?php esc_html_e( 'Book a New Meeting', 'woodoo' ); ?></h3>
<p class="woodoo-desc">
<?php printf(
esc_html__( 'Slots are %d minutes. Pick a date to see availability.', 'woodoo' ),
(int) get_option( 'woodoo_meeting_duration', 30 )
); ?>
</p>
<div class="woodoo-booking-form">
<!-- Date picker -->
<div class="woodoo-field">
<label for="woodoo-booking-date">
<?php esc_html_e( 'Select Date', 'woodoo' ); ?>
</label>
<input type="date"
id="woodoo-booking-date"
name="booking_date"
min="<?php echo esc_attr( gmdate( 'Y-m-d', strtotime( '+1 day' ) ) ); ?>"
max="<?php echo esc_attr( gmdate( 'Y-m-d', strtotime( '+60 days' ) ) ); ?>">
</div>
<!-- Slot list (populated via JS) -->
<div id="woodoo-slots-wrap" style="display:none;">
<div class="woodoo-field">
<label><?php esc_html_e( 'Available Slots', 'woodoo' ); ?></label>
<div id="woodoo-slots-grid" class="woodoo-slots-grid">
<!-- Slots injected by JS -->
</div>
</div>
</div>
<!-- Booking confirmation -->
<div id="woodoo-booking-confirm" style="display:none;">
<div class="woodoo-field">
<label for="woodoo-booking-notes">
<?php esc_html_e( 'Notes (optional)', 'woodoo' ); ?>
</label>
<textarea id="woodoo-booking-notes" name="notes" rows="3"
placeholder="<?php esc_attr_e( 'What would you like to discuss?', 'woodoo' ); ?>"></textarea>
</div>
<div class="woodoo-selected-slot-display">
<strong><?php esc_html_e( 'Selected:', 'woodoo' ); ?></strong>
<span id="woodoo-selected-slot-label"></span>
</div>
<button id="woodoo-book-btn" class="woodoo-btn woodoo-btn--primary" disabled>
<?php esc_html_e( 'Confirm Booking', 'woodoo' ); ?>
</button>
</div>
<!-- Success message -->
<div id="woodoo-booking-success" class="woodoo-notice woodoo-success" style="display:none;"></div>
<!-- Error message -->
<div id="woodoo-booking-error" class="woodoo-notice woodoo-error" style="display:none;"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,119 @@
<?php
/**
* Template: My Account Odoo Invoices
*
* Variables available:
* $invoices array List of invoice records from Odoo
* $total int Total invoice count
* $paged int Current page
* $num_pages int Total pages
*/
defined( 'ABSPATH' ) || exit;
// Enqueue frontend assets
wp_enqueue_style( 'woodoo-frontend' );
wp_enqueue_script( 'woodoo-frontend' );
?>
<div class="woodoo-invoices">
<h3><?php esc_html_e( 'Your Invoices', 'woodoo' ); ?></h3>
<?php if ( empty( $invoices ) ) : ?>
<p class="woodoo-empty">
<?php esc_html_e( 'No invoices found.', 'woodoo' ); ?>
</p>
<?php else : ?>
<div class="woodoo-table-wrap">
<table class="woodoo-table">
<thead>
<tr>
<th><?php esc_html_e( 'Invoice #', 'woodoo' ); ?></th>
<th><?php esc_html_e( 'Date', 'woodoo' ); ?></th>
<th><?php esc_html_e( 'Due Date', 'woodoo' ); ?></th>
<th><?php esc_html_e( 'Total', 'woodoo' ); ?></th>
<th><?php esc_html_e( 'Balance Due', 'woodoo' ); ?></th>
<th><?php esc_html_e( 'Status', 'woodoo' ); ?></th>
<th><?php esc_html_e( 'Download', 'woodoo' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $invoices as $inv ) :
$currency = is_array( $inv['currency_id'] ) ? $inv['currency_id'][1] : '';
$pay_state = $inv['payment_state'] ?? 'not_paid';
$badge_class = WooDoo_Invoices::payment_state_class( $pay_state );
$badge_label = WooDoo_Invoices::payment_state_label( $pay_state );
$pdf_url = add_query_arg( [
'action' => 'woodoo_invoice_pdf',
'invoice_id' => $inv['id'],
'nonce' => wp_create_nonce( 'woodoo_invoice_pdf' ),
], admin_url( 'admin-ajax.php' ) );
?>
<tr>
<td class="woodoo-inv-number">
<?php echo esc_html( $inv['name'] ?: "INV-{$inv['id']}" ); ?>
</td>
<td>
<?php echo $inv['invoice_date']
? esc_html( date_i18n( get_option( 'date_format' ), strtotime( $inv['invoice_date'] ) ) )
: '—'; ?>
</td>
<td>
<?php
if ( $inv['invoice_date_due'] ) {
$due_ts = strtotime( $inv['invoice_date_due'] );
$overdue = $pay_state === 'not_paid' && $due_ts < time();
$class = $overdue ? ' class="woodoo-overdue"' : '';
echo '<span' . $class . '>' . // phpcs:ignore WordPress.Security.EscapeOutput
esc_html( date_i18n( get_option( 'date_format' ), $due_ts ) ) .
'</span>';
if ( $overdue ) echo ' <span class="woodoo-badge woodoo-badge--red">' . esc_html__( 'Overdue', 'woodoo' ) . '</span>';
} else {
echo '—';
}
?>
</td>
<td class="woodoo-amount">
<?php echo esc_html( number_format_i18n( $inv['amount_total'], 2 ) . ' ' . $currency ); ?>
</td>
<td class="woodoo-amount">
<?php echo esc_html( number_format_i18n( $inv['amount_residual'], 2 ) . ' ' . $currency ); ?>
</td>
<td>
<span class="woodoo-badge <?php echo esc_attr( $badge_class ); ?>">
<?php echo esc_html( $badge_label ); ?>
</span>
</td>
<td>
<a href="<?php echo esc_url( $pdf_url ); ?>"
class="woodoo-btn woodoo-btn--sm"
target="_blank"
rel="noopener">
<?php esc_html_e( 'PDF', 'woodoo' ); ?>
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if ( $num_pages > 1 ) :
$base_url = wc_get_account_endpoint_url( WooDoo_Invoices::ENDPOINT );
?>
<nav class="woodoo-pagination">
<?php for ( $p = 1; $p <= $num_pages; $p++ ) : ?>
<?php if ( $p === $paged ) : ?>
<span class="woodoo-page-current"><?php echo esc_html( $p ); ?></span>
<?php else : ?>
<a href="<?php echo esc_url( add_query_arg( 'invoice_page', $p, $base_url ) ); ?>">
<?php echo esc_html( $p ); ?>
</a>
<?php endif; ?>
<?php endfor; ?>
</nav>
<?php endif; ?>
<?php endif; ?>
</div>