25.5, 'SE' => 25, 'DK' => 25, 'DE' => 19, 'NL' => 21, 'BE' => 21, 'FR' => 20, 'AT' => 20, 'IT' => 22, 'ES' => 21, 'PT' => 23, 'PL' => 23, 'CZ' => 21, 'EE' => 24, 'LV' => 21, 'LT' => 21, 'IE' => 23); $country = function_exists('WC') && WC()->countries ? WC()->countries->get_base_country() : 'FI'; return $rates[$country] ?? 25.5; } add_filter('usedkit_default_settings', function ($d) { return $d + array('margin' => 'no', 'margin_rate' => '', 'margin_method' => 'item', 'margin_default' => 'yes'); }); add_filter('usedkit_save_settings', function ($out, $post) { $rate = str_replace(',', '.', (string) ($post['margin_rate'] ?? '')); return $out + array( 'margin' => !empty($post['margin']) ? 'yes' : 'no', 'margin_rate' => is_numeric($rate) ? (string) (float) $rate : '', 'margin_method' => ($post['margin_method'] ?? '') === 'global' ? 'global' : 'item', 'margin_default' => !empty($post['margin_default']) ? 'yes' : 'no', ); }, 10, 2); add_action('usedkit_settings_rows', function ($s) { ?>


get_meta('_usedkit_margin'); if ($own === '' && $product->get_parent_id()) { $parent = wc_get_product($product->get_parent_id()); $own = $parent ? $parent->get_meta('_usedkit_margin') : ''; } return $own === '' ? usedkit_settings()['margin_default'] === 'yes' : $own === 'yes'; } function usedkit_cost($product) { $c = $product->get_meta('_usedkit_cost'); if ($c === '' && $product->get_parent_id()) { $parent = wc_get_product($product->get_parent_id()); $c = $parent ? $parent->get_meta('_usedkit_cost') : ''; } return $c === '' ? null : (float) $c; } /** The report's month from ?month=YYYY-MM, else this month. */ function usedkit_margin_month() { $m = (string) usedkit_view_arg('margin', 'month', 'usedkit_margin_csv'); return preg_match('/^\d{4}-\d{2}$/', $m) ? $m : gmdate('Y-m'); } /* ---- the product's fields ---------------------------------------------------------- */ add_action('woocommerce_product_options_pricing', function () { if (!usedkit_margin_on()) { return; } global $product_object; woocommerce_wp_text_input(array( 'id' => '_usedkit_cost', 'data_type' => 'price', 'label' => __('Purchase price', 'usedkit') . ' (' . get_woocommerce_currency_symbol() . ')', 'description' => __('What you paid for this item: the margin-scheme VAT is on the difference.', 'usedkit'), 'desc_tip' => true, 'value' => $product_object ? $product_object->get_meta('_usedkit_cost') : '')); woocommerce_wp_select(array( 'id' => '_usedkit_margin', 'label' => __('Margin scheme', 'usedkit'), 'options' => array('' => __('Shop default', 'usedkit'), 'yes' => __('Yes', 'usedkit'), 'no' => __('No (normal VAT)', 'usedkit')), 'value' => $product_object ? $product_object->get_meta('_usedkit_margin') : '')); woocommerce_wp_text_input(array( 'id' => '_usedkit_bought', 'type' => 'date', 'label' => __('Purchase date', 'usedkit'), 'description' => __('When you bought it; the date it was listed if empty. The global margin counts purchases in the period they were made.', 'usedkit'), 'desc_tip' => true, 'value' => $product_object ? $product_object->get_meta('_usedkit_bought') : '')); }); add_action('woocommerce_admin_process_product_object', function ($product) { if (!usedkit_margin_on() || !usedkit_product_form_ok($product)) { return; } // phpcs:disable WordPress.Security.NonceVerification -- verified in usedkit_product_form_ok() if (isset($_POST['_usedkit_cost'])) { $cost = wc_format_decimal(sanitize_text_field(wp_unslash($_POST['_usedkit_cost']))); $cost === '' ? $product->delete_meta_data('_usedkit_cost') : $product->update_meta_data('_usedkit_cost', $cost); } if (isset($_POST['_usedkit_bought'])) { $d = sanitize_text_field(wp_unslash($_POST['_usedkit_bought'])); preg_match('/^\d{4}-\d{2}-\d{2}$/', $d) ? $product->update_meta_data('_usedkit_bought', $d) : $product->delete_meta_data('_usedkit_bought'); } if (isset($_POST['_usedkit_margin'])) { $m = sanitize_key(wp_unslash($_POST['_usedkit_margin'])); in_array($m, array('yes', 'no'), true) ? $product->update_meta_data('_usedkit_margin', $m) : $product->delete_meta_data('_usedkit_margin'); } // phpcs:enable }); /* No separately charged VAT on a margin-scheme item. */ foreach (array('woocommerce_product_get_tax_status', 'woocommerce_product_variation_get_tax_status') as $usedkit_hook) { add_filter($usedkit_hook, function ($status, $product) { return (usedkit_margin_on() && usedkit_is_margin_item($product)) ? 'none' : $status; }, 10, 2); } /* ---- orders ---------------------------------------------------------------------------- */ /** The margin on each margin-scheme line, recorded as the order is placed: the figures * the report uses, fixed at the time of sale. */ function usedkit_record_margin($item) { if (!usedkit_margin_on()) { return; } $product = $item->get_product(); if (!usedkit_is_margin_item($product)) { return; } $qty = max(1, (int) $item->get_quantity()); $sale = (float) $item->get_total() + (float) $item->get_total_tax(); $cost = usedkit_cost($product); $rate = usedkit_margin_rate(); $item->update_meta_data('_usedkit_margin_item', 'yes'); $item->update_meta_data('_usedkit_rate', $rate); if ($cost !== null) { $item->update_meta_data('_usedkit_cost', $cost * $qty); $item->update_meta_data('_usedkit_margin_vat', usedkit_margin_vat($sale, $cost * $qty, $rate)); } } add_action('woocommerce_checkout_create_order_line_item', function ($item) { usedkit_record_margin($item); }, 10, 1); /* Orders made another way -- in the admin, through the API -- get theirs when paid. */ add_action('woocommerce_order_status_changed', function ($order_id, $from, $to, $order) { if (!usedkit_margin_on() || !in_array($to, array('processing', 'completed'), true)) { return; } foreach ($order->get_items() as $item) { if ($item->get_meta('_usedkit_margin_item') === '') { usedkit_record_margin($item); $item->save(); } } }, 10, 4); function usedkit_order_has_margin_items($order) { foreach ($order->get_items() as $item) { if ($item->get_meta('_usedkit_margin_item') === 'yes') { return true; } } return false; } /** The wording an invoice for margin-scheme goods must carry (Art. 226(14)). */ function usedkit_margin_notice() { return __('Margin scheme – second-hand goods. VAT is not shown separately.', 'usedkit'); } add_action('woocommerce_email_after_order_table', function ($order, $plain = false) { if (usedkit_order_has_margin_items($order)) { echo $plain ? "\n" . esc_html(usedkit_margin_notice()) . "\n" : '

' . esc_html(usedkit_margin_notice()) . '

'; } }, 10, 2); add_action('woocommerce_order_details_after_order_table', function ($order) { if (usedkit_order_has_margin_items($order)) { echo '

' . esc_html(usedkit_margin_notice()) . '

'; } }); /* ---- the report ------------------------------------------------------------------------ */ add_action('admin_menu', function () { if (usedkit_margin_on()) { add_submenu_page('woocommerce', __('Margin VAT', 'usedkit'), __('Margin VAT', 'usedkit'), 'manage_woocommerce', 'usedkit-margin', 'usedkit_margin_report_page'); } }, 20); /** The margin-scheme lines of paid orders in [from, to): one row each, refunds off. */ function usedkit_margin_lines($from, $to) { $orders = wc_get_orders(array('limit' => -1, 'status' => array('processing', 'completed'), 'date_paid' => strtotime($from . ' 00:00:00') . '...' . (strtotime($to . ' 00:00:00') - 1))); $lines = array(); foreach ($orders as $order) { foreach ($order->get_items() as $item) { if ($item->get_meta('_usedkit_margin_item') !== 'yes') { continue; } $sale = (float) $item->get_total() + (float) $item->get_total_tax(); $refunded = (float) $order->get_total_refunded_for_item($item->get_id()); $cost = $item->get_meta('_usedkit_cost'); $paid = $order->get_date_paid(); $lines[] = array( 'order' => $order->get_order_number(), 'date' => $paid ? $paid->date('Y-m-d') : '', 'item' => $item->get_name(), 'sale' => round($sale - $refunded, 2), 'cost' => $cost === '' ? null : (float) $cost, 'rate' => (float) ($item->get_meta('_usedkit_rate') ?: usedkit_margin_rate())); } } return $lines; } /** * Margin-scheme purchases made in [from, to): what the global method sets against the * period's sales (Art. 318: total supplies less total purchases of the period), whether * or not the items have sold. The purchase date is the product's own, else the day it * was listed. Items without a purchase price are returned with cost null. */ function usedkit_margin_purchases($from, $to) { $out = array(); $products = wc_get_products(array('limit' => -1, 'status' => array('publish', 'private', 'draft'), 'type' => array('simple', 'variable'))); foreach ($products as $p) { if (!usedkit_is_margin_item($p)) { continue; } $day = $p->get_meta('_usedkit_bought'); if ($day === '' && $p->get_date_created()) { $day = $p->get_date_created()->date('Y-m-d'); } if ($day === '' || $day < $from || $day >= $to) { continue; } $out[] = array('id' => $p->get_id(), 'date' => $day, 'item' => $p->get_name(), 'cost' => usedkit_cost($p)); } return $out; } function usedkit_margin_report_page() { if (!current_user_can('manage_woocommerce')) { return; } $month = usedkit_margin_month(); $span = absint(usedkit_view_arg('margin', 'span', 'usedkit_margin_csv')) === 3 ? 3 : 1; $from = $month . '-01'; $to = gmdate('Y-m-d', strtotime($from . " +$span month")); $carry = (float) str_replace(',', '.', (string) usedkit_view_arg('margin', 'carry')); $s = usedkit_settings(); $lines = usedkit_margin_lines($from, $to); $rate = usedkit_margin_rate(); if ($s['margin_method'] === 'global') { // The period's sales against the period's purchases, sold or not. $bought = usedkit_margin_purchases($from, $to); $missing = array_filter($bought, function ($b) { return $b['cost'] === null; }); $rows = array_merge(array_map(function ($l) { return array($l['sale'], 0.0); }, $lines), array_map(function ($b) { return array(0.0, (float) $b['cost']); }, array_filter($bought, function ($b) { return $b['cost'] !== null; }))); } else { $missing = array_filter($lines, function ($l) { return $l['cost'] === null; }); $rows = array_map(function ($l) { return array($l['sale'], $l['cost']); }, array_filter($lines, function ($l) { return $l['cost'] !== null; })); } $calc = usedkit_margin_period($rows, $rate, $s['margin_method'], $carry); ?>

CSV