Skip to content

Early-bird lifetime licence: $199 once, for the first 200 buyers only. See the offer

Receipt or invoice at checkout (Απόδειξη / Τιμολόγιο)

Customers choose a receipt or an invoice. For an invoice they fill in company name, VAT number (ΑΦΜ, checked), tax office (ΔΟΥ) and business activity. The details are saved with the order and shown in the admin, the order emails and the order page.

  • Type PHP
  • Runs Everywhere (runs as WordPress loads)
  • Needs WooCommerce
  • Tested with WordPress 7.1, WooCommerce 11.1
PHP
<?php

// Labels are in Greek; change them if you like.
$fields = array(
	'sf_invoice_company'    => 'Επωνυμία εταιρείας',
	'sf_invoice_vat'        => 'ΑΦΜ',
	'sf_invoice_tax_office' => 'ΔΟΥ',
	'sf_invoice_activity'   => 'Δραστηριότητα',
);

// A Greek VAT number (ΑΦΜ): 9 digits with a check digit.
$valid_vat = static function ( string $vat ): bool {
	if ( ! preg_match( '/^\d{9}$/', $vat ) || '000000000' === $vat ) {
		return false;
	}
	$sum = 0;
	for ( $i = 0; $i < 8; $i++ ) {
		$sum += (int) $vat[ $i ] * ( 2 ** ( 8 - $i ) );
	}
	return ( $sum % 11 ) % 10 === (int) $vat[8];
};

add_filter(
	'woocommerce_checkout_fields',
	static function ( $checkout ) use ( $fields ) {
		$checkout['billing']['sf_document'] = array(
			'type'     => 'radio',
			'label'    => 'Παραστατικό',
			'options'  => array(
				'receipt' => 'Απόδειξη',
				'invoice' => 'Τιμολόγιο',
			),
			'default'  => 'receipt',
			'required' => true,
			'class'    => array( 'form-row-wide', 'sf-document' ),
			'priority' => 5,
		);
		$priority = 6;
		foreach ( $fields as $key => $label ) {
			$checkout['billing'][ $key ] = array(
				'type'     => 'text',
				'label'    => $label . ' <abbr class="required" title="υποχρεωτικό">*</abbr>',
				'required' => false, // Required for invoices only: checked below.
				'class'    => array( 'form-row-wide', 'sf-invoice-field' ),
				'priority' => $priority++,
			);
		}
		return $checkout;
	}
);

add_action(
	'woocommerce_after_checkout_validation',
	static function ( $data, $errors ) use ( $fields, $valid_vat ) {
		if ( 'invoice' !== ( $data['sf_document'] ?? '' ) ) {
			return;
		}
		foreach ( $fields as $key => $label ) {
			if ( '' === trim( (string) ( $data[ $key ] ?? '' ) ) ) {
				$errors->add( 'validation', sprintf( 'Συμπληρώστε το πεδίο «%s» για το τιμολόγιο.', $label ) );
			}
		}
		$vat = preg_replace( '/\D/', '', (string) ( $data['sf_invoice_vat'] ?? '' ) );
		if ( '' !== $vat && ! $valid_vat( $vat ) ) {
			$errors->add( 'validation', 'Ο ΑΦΜ δεν είναι έγκυρος.' );
		}
	},
	10,
	2
);

add_action(
	'woocommerce_checkout_create_order',
	static function ( $order, $data ) use ( $fields ) {
		$document = 'invoice' === ( $data['sf_document'] ?? '' ) ? 'invoice' : 'receipt';
		$order->update_meta_data( '_sf_document', $document );
		if ( 'invoice' === $document ) {
			foreach ( array_keys( $fields ) as $key ) {
				$value = (string) ( $data[ $key ] ?? '' );
				$order->update_meta_data( '_' . $key, 'sf_invoice_vat' === $key ? preg_replace( '/\D/', '', $value ) : sanitize_text_field( $value ) );
			}
		}
	},
	10,
	2
);

// The details, as HTML or plain text.
$details = static function ( $order, bool $plain = false ) use ( $fields ): string {
	if ( ! $order instanceof WC_Order || ! $order->get_meta( '_sf_document' ) ) {
		return '';
	}
	$lines = array( 'Παραστατικό' => 'invoice' === $order->get_meta( '_sf_document' ) ? 'Τιμολόγιο' : 'Απόδειξη' );
	if ( 'invoice' === $order->get_meta( '_sf_document' ) ) {
		foreach ( $fields as $key => $label ) {
			$lines[ $label ] = (string) $order->get_meta( '_' . $key );
		}
	}
	$out = '';
	foreach ( $lines as $label => $value ) {
		$out .= $plain ? $label . ': ' . $value . "\n" : '<strong>' . esc_html( $label ) . ':</strong> ' . esc_html( $value ) . '<br>';
	}
	return $plain ? "\n" . $out : '<p class="sf-invoice-details">' . $out . '</p>';
};

add_action(
	'woocommerce_admin_order_data_after_billing_address',
	static function ( $order ) use ( $details ) {
		echo $details( $order ); // phpcs:ignore WordPress.Security.EscapeOutput -- Escaped above.
	}
);

// The order page for customers: classic templates show it with the
// customer's details, block themes in the thank-you page's extra information.
$show_once = static function ( $order ) use ( $details ) {
	static $shown = array();
	$order = $order instanceof WC_Order ? $order : wc_get_order( $order );
	if ( $order && empty( $shown[ $order->get_id() ] ) ) {
		$shown[ $order->get_id() ] = true;
		echo $details( $order ); // phpcs:ignore WordPress.Security.EscapeOutput -- Escaped above.
	}
};
add_action( 'woocommerce_order_details_after_customer_details', $show_once );
add_action( 'woocommerce_thankyou', $show_once, 20 );
add_action(
	'woocommerce_email_after_order_table',
	static function ( $order, $sent_to_admin = false, $plain_text = false ) use ( $details ) {
		echo $plain_text ? esc_html( $details( $order, true ) ) : $details( $order ); // phpcs:ignore WordPress.Security.EscapeOutput -- Escaped above.
	},
	10,
	3
);

// Show the invoice fields only when "Τιμολόγιο" is picked.
add_action(
	'wp_footer',
	static function () {
		if ( ! function_exists( 'is_checkout' ) || ! is_checkout() || is_order_received_page() ) {
			return;
		}
		?>
		<style>.sf-invoice-field .optional{display:none}.sf-document .woocommerce-input-wrapper label{display:inline;margin:0 1em 0 .3em}</style>
		<script>
		jQuery( function ( $ ) {
			var toggle = function () {
				$( '.sf-invoice-field' ).toggle( $( 'input[name="sf_document"]:checked' ).val() === 'invoice' );
			};
			$( document.body ).on( 'change', 'input[name="sf_document"]', toggle );
			toggle();
		} );
		</script>
		<?php
	}
);

Good to know

For the classic checkout (the [woocommerce_checkout] shortcode or the Classic Checkout block). The details are saved as order fields (_sf_document, _sf_invoice_vat…), which invoicing plugins and exports can read.

In these packs

  • Greek e-shop: For shops in Greece: receipt or invoice with a checked VAT number (ΑΦΜ), Greek phone and postcode checks, a cash on delivery fee, and greeklish addresses.

Move your snippets over this afternoon.

Importing changes nothing until you switch over, and your old shortcodes keep working.