.
\ No newline at end of file
diff --git a/packages/one-stop-shop-woocommerce/one-stop-shop-woocommerce.php b/packages/one-stop-shop-woocommerce/one-stop-shop-woocommerce.php
new file mode 100644
index 000000000..0fcb99c23
--- /dev/null
+++ b/packages/one-stop-shop-woocommerce/one-stop-shop-woocommerce.php
@@ -0,0 +1,77 @@
+
+
+
+ composer install',
+ '' . esc_html( str_replace( ABSPATH, '', __DIR__ ) ) . '
'
+ );
+ ?>
+
+
+ get_name() === $wc_admin_note_name ) {
+ /**
+ * Update notice hide in case note has been actioned (e.g. button click by user)
+ */
+ if ( Note::E_WC_ADMIN_NOTE_ACTIONED === $note->get_status() ) {
+ update_option( 'oss_hide_notice_' . sanitize_key( $oss_note::get_id() ), 'yes' );
+ }
+
+ break;
+ }
+ }
+ }
+ } catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
+ }
+ }
+
+ public static function register_tax_rate_refresh_tool( $tools ) {
+ $tools['refresh_oss_tax_rates'] = array(
+ 'name' => _x( 'Refresh VAT rates (OSS)', 'oss', 'woocommerce-germanized' ),
+ 'button' => _x( 'Refresh VAT rates (OSS)', 'oss', 'woocommerce-germanized' ),
+ 'callback' => array( __CLASS__, 'refresh_vat_rates' ),
+ 'desc' => sprintf(
+ '%1$s %2$s',
+ _x( 'Note:', 'oss', 'woocommerce-germanized' ),
+ sprintf( _x( 'This option will delete all of your current EU VAT rates and re-import them based on your current OSS status .', 'oss', 'woocommerce-germanized' ), esc_url( Settings::get_settings_url() ) )
+ ),
+ );
+
+ return $tools;
+ }
+
+ public static function refresh_vat_rates() {
+ if ( Helper::oss_procedure_is_enabled() ) {
+ Helper::import_oss_tax_rates();
+ } else {
+ Helper::import_default_tax_rates();
+ }
+ }
+
+ public static function html_field( $value ) {
+ ?>
+
+
+
+
+
+
+ id : '';
+ $supports_notes = self::supports_wc_admin();
+
+ if ( ! $supports_notes || in_array( $screen_id, array( 'dashboard', 'plugins' ), true ) ) {
+ foreach ( self::get_notes() as $note ) {
+ if ( $note::is_enabled() ) {
+ $note::render();
+ }
+ }
+ }
+ }
+
+ /**
+ * @return AdminNote[]
+ */
+ public static function get_notes() {
+ $notes = array( 'Vendidero\OneStopShop\DeliveryThresholdWarning' );
+
+ if ( ! Package::enable_auto_observer() ) {
+ $notes = array();
+ }
+
+ return $notes;
+ }
+
+ public static function supports_wc_admin() {
+ $supports_notes = class_exists( 'Automattic\WooCommerce\Admin\Notes\Note' );
+
+ try {
+ $data_store = \WC_Data_Store::load( 'admin-note' );
+ } catch ( \Exception $e ) {
+ $supports_notes = false;
+ }
+
+ return $supports_notes;
+ }
+
+ protected static function get_wc_admin_note_name( $oss_note_id ) {
+ return 'oss_' . $oss_note_id;
+ }
+
+ protected static function get_wc_admin_note( $oss_note_id ) {
+ $note_name = self::get_wc_admin_note_name( $oss_note_id );
+ $data_store = \WC_Data_Store::load( 'admin-note' );
+ $note_ids = $data_store->get_notes_with_name( $note_name );
+
+ if ( ! empty( $note_ids ) && ( $note = Notes::get_note( $note_ids[0] ) ) ) {
+ return $note;
+ }
+
+ return false;
+ }
+
+ public static function queue_wc_admin_notes() {
+ if ( self::supports_wc_admin() ) {
+ foreach ( self::get_notes() as $oss_note ) {
+ $note = self::get_wc_admin_note( $oss_note::get_id() );
+
+ if ( ! $note && $oss_note::is_enabled() ) {
+ $note = new Note();
+ $note->set_title( $oss_note::get_title() );
+ $note->set_content( $oss_note::get_content() );
+ $note->set_content_data( (object) array() );
+ $note->set_type( 'update' );
+ $note->set_name( self::get_wc_admin_note_name( $oss_note::get_id() ) );
+ $note->set_source( 'oss-woocommerce' );
+ $note->set_status( Note::E_WC_ADMIN_NOTE_UNACTIONED );
+
+ foreach ( $oss_note::get_actions() as $action ) {
+ $note->add_action(
+ 'oss_' . sanitize_key( $action['title'] ),
+ $action['title'],
+ $action['url'],
+ Note::E_WC_ADMIN_NOTE_ACTIONED,
+ $action['is_primary'] ? true : false
+ );
+ }
+
+ $note->save();
+ } elseif ( $oss_note::is_enabled() && $note ) {
+ $note->set_status( Note::E_WC_ADMIN_NOTE_UNACTIONED );
+ $note->save();
+ }
+ }
+ }
+ }
+
+ public static function get_threshold_notice_content() {
+ return sprintf( _x( 'Seems like you have reached (or are close to reaching) the delivery threshold for the current year. Please make sure to check the report details and take action in case necessary.', 'oss', 'woocommerce-germanized' ), esc_url( Package::get_observer_report()->get_url() ) );
+ }
+
+ public static function get_threshold_notice_title() {
+ return _x( 'Delivery threshold reached (OSS)', 'oss', 'woocommerce-germanized' );
+ }
+
+ public static function init_observer() {
+ if ( ! current_user_can( 'manage_woocommerce' ) || ! wp_verify_nonce( isset( $_GET['_wpnonce'] ) ? wp_unslash( $_GET['_wpnonce'] ) : '', 'oss_init_observer' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ wp_die();
+ }
+
+ if ( ! Queue::get_running_observer() ) {
+ Package::update_observer_report();
+ }
+
+ wp_safe_redirect( wp_get_referer() );
+ exit();
+ }
+
+ public static function switch_procedure() {
+ if ( ! current_user_can( 'manage_woocommerce' ) || ! wp_verify_nonce( isset( $_GET['_wpnonce'] ) ? wp_unslash( $_GET['_wpnonce'] ) : '', 'oss_switch_procedure' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ wp_die();
+ }
+
+ if ( Helper::oss_procedure_is_enabled() ) {
+ update_option( 'oss_use_oss_procedure', 'no' );
+
+ Helper::import_default_tax_rates();
+
+ do_action( 'woocommerce_oss_disabled_oss_procedure' );
+ } else {
+ update_option( 'woocommerce_tax_based_on', 'shipping' );
+ update_option( 'oss_use_oss_procedure', 'yes' );
+
+ Helper::import_oss_tax_rates();
+
+ do_action( 'woocommerce_oss_enabled_oss_procedure' );
+ }
+
+ do_action( 'woocommerce_oss_switched_oss_procedure_status' );
+
+ wp_safe_redirect( wp_get_referer() );
+ exit();
+ }
+
+ public static function hide_notice() {
+ if ( ! current_user_can( 'manage_woocommerce' ) || ! wp_verify_nonce( isset( $_GET['_wpnonce'] ) ? wp_unslash( $_GET['_wpnonce'] ) : '', 'oss_hide_notice' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ wp_die();
+ }
+
+ $notice_id = isset( $_GET['notice'] ) ? wc_clean( wp_unslash( $_GET['notice'] ) ) : '';
+
+ foreach ( self::get_notes() as $oss_note ) {
+ if ( $oss_note::get_id() === $notice_id ) {
+ update_option( 'oss_hide_notice_' . sanitize_key( $oss_note::get_id() ), 'yes' );
+
+ if ( self::supports_wc_admin() ) {
+ self::delete_wc_admin_note( $oss_note );
+ }
+
+ break;
+ }
+ }
+
+ wp_safe_redirect( wp_get_referer() );
+ exit();
+ }
+
+ /**
+ * @param AdminNote $oss_note
+ */
+ public static function delete_wc_admin_note( $oss_note ) {
+ if ( ! self::supports_wc_admin() ) {
+ return false;
+ }
+
+ try {
+ if ( $note = self::get_wc_admin_note( $oss_note::get_id() ) ) {
+ $note->delete( true );
+ return true;
+ }
+
+ return false;
+ } catch ( \Exception $e ) {
+ return false;
+ }
+ }
+
+ public static function delete_report() {
+ if ( ! current_user_can( 'manage_woocommerce' ) || ! wp_verify_nonce( isset( $_GET['_wpnonce'] ) ? wp_unslash( $_GET['_wpnonce'] ) : '', 'oss_delete_report' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ wp_die();
+ }
+
+ $report_id = isset( $_GET['report_id'] ) ? wc_clean( wp_unslash( $_GET['report_id'] ) ) : '';
+
+ if ( ! empty( $report_id ) && ( $report = Package::get_report( $report_id ) ) ) {
+ $report->delete();
+
+ $referer = self::get_clean_referer();
+
+ /**
+ * Do not redirect deleted, refreshed reports back to report details page
+ */
+ if ( strstr( $referer, '&report=' ) ) {
+ $referer = admin_url( 'admin.php?page=oss-reports' );
+ }
+
+ wp_safe_redirect( esc_url_raw( add_query_arg( array( 'report_deleted' => $report_id ), $referer ) ) );
+ exit();
+ }
+
+ wp_safe_redirect( esc_url_raw( wp_get_referer() ) );
+ exit();
+ }
+
+ protected static function get_clean_referer() {
+ $referer = wp_get_referer();
+
+ return remove_query_arg( array( 'report_created', 'report_deleted', 'report_restarted', 'report_cancelled' ), $referer );
+ }
+
+ public static function export_report() {
+ if ( ! current_user_can( 'manage_woocommerce' ) || ! wp_verify_nonce( isset( $_GET['_wpnonce'] ) ? wp_unslash( $_GET['_wpnonce'] ) : '', 'oss_export_report' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ wp_die();
+ }
+
+ $report_id = isset( $_GET['report_id'] ) ? wc_clean( wp_unslash( $_GET['report_id'] ) ) : '';
+ $decimals = isset( $_GET['decimals'] ) ? absint( wp_unslash( $_GET['decimals'] ) ) : wc_get_price_decimals();
+ $export_type = isset( $_GET['export_type'] ) ? wc_clean( wp_unslash( $_GET['export_type'] ) ) : '';
+
+ if ( ! empty( $report_id ) && ( $report = Package::get_report( $report_id ) ) ) {
+ $exporter_class = '\Vendidero\OneStopShop\CSVExporter';
+ $base_country = Helper::get_base_country();
+
+ if ( 'bop' === $export_type ) {
+ $exporter_class = '\Vendidero\OneStopShop\CSVExporterBOP';
+ }
+
+ $exporter_class = apply_filters( 'oss_csv_exporter_classname', $exporter_class, $base_country );
+
+ if ( ! class_exists( $exporter_class ) ) {
+ $exporter_class = '\Vendidero\OneStopShop\CSVExporter';
+ }
+
+ $csv = new $exporter_class( $report_id, $decimals );
+ $csv->export();
+ } else {
+ wp_safe_redirect( wp_get_referer() );
+ exit();
+ }
+ }
+
+ public static function refresh_report() {
+ if ( ! current_user_can( 'manage_woocommerce' ) || ! wp_verify_nonce( isset( $_GET['_wpnonce'] ) ? wp_unslash( $_GET['_wpnonce'] ) : '', 'oss_refresh_report' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ wp_die();
+ }
+
+ $report_id = isset( $_GET['report_id'] ) ? wc_clean( wp_unslash( $_GET['report_id'] ) ) : '';
+
+ if ( ! empty( $report_id ) && ( $report = Package::get_report( $report_id ) ) ) {
+ Queue::start( $report->get_type(), $report->get_date_start(), $report->get_date_end() );
+
+ wp_safe_redirect( esc_url_raw( add_query_arg( array( 'report_restarted' => $report_id ), self::get_clean_referer() ) ) );
+ exit();
+ }
+
+ wp_safe_redirect( esc_url_raw( wp_get_referer() ) );
+ exit();
+ }
+
+ public static function cancel_report() {
+ if ( ! current_user_can( 'manage_woocommerce' ) || ! wp_verify_nonce( isset( $_GET['_wpnonce'] ) ? wp_unslash( $_GET['_wpnonce'] ) : '', 'oss_cancel_report' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ wp_die();
+ }
+
+ $report_id = isset( $_GET['report_id'] ) ? wc_clean( wp_unslash( $_GET['report_id'] ) ) : '';
+
+ if ( ! empty( $report_id ) && Queue::is_running( $report_id ) ) {
+ Queue::cancel( $report_id );
+
+ $referer = self::get_clean_referer();
+
+ /**
+ * Do not redirect deleted, refreshed reports back to report details page
+ */
+ if ( strstr( $referer, '&report=' ) ) {
+ $referer = admin_url( 'admin.php?page=oss-reports' );
+ }
+
+ wp_safe_redirect( esc_url_raw( add_query_arg( array( 'report_cancelled' => $report_id ), $referer ) ) );
+ exit();
+ }
+
+ wp_safe_redirect( esc_url_raw( wp_get_referer() ) );
+ exit();
+ }
+
+ public static function create_report() {
+ if ( ! current_user_can( 'manage_woocommerce' ) || ! wp_verify_nonce( isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : '', 'oss_create_report' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ wp_die();
+ }
+
+ $report_type = ! empty( $_POST['report_type'] ) ? wc_clean( wp_unslash( $_POST['report_type'] ) ) : 'yearly';
+ $report_type = array_key_exists( $report_type, Package::get_available_report_types() ) ? $report_type : 'yearly';
+ $start_date = null;
+ $end_date = null;
+
+ if ( 'quarterly' === $report_type ) {
+ $start_date = ! empty( $_POST['report_quarter'] ) ? wc_clean( wp_unslash( $_POST['report_quarter'] ) ) : null;
+ } elseif ( 'yearly' === $report_type ) {
+ $start_date = ! empty( $_POST['report_year'] ) ? wc_clean( wp_unslash( $_POST['report_year'] ) ) : null;
+ } elseif ( 'monthly' === $report_type ) {
+ $start_date = ! empty( $_POST['report_month'] ) ? wc_clean( wp_unslash( $_POST['report_month'] ) ) : null;
+ } elseif ( 'custom' === $report_type ) {
+ $start_date = ! empty( $_POST['date_start'] ) ? wc_clean( wp_unslash( $_POST['date_start'] ) ) : null;
+ $end_date = ! empty( $_POST['date_end'] ) ? wc_clean( wp_unslash( $_POST['date_end'] ) ) : null;
+ }
+
+ if ( ! is_null( $start_date ) ) {
+ $start_date = Package::string_to_datetime( $start_date );
+ }
+
+ if ( ! is_null( $end_date ) ) {
+ $end_date = Package::string_to_datetime( $end_date );
+ }
+
+ $generator_id = Queue::start( $report_type, $start_date, $end_date );
+
+ wp_safe_redirect( admin_url( 'admin.php?page=oss-reports&report_created=' . $generator_id ) );
+ exit();
+ }
+
+ public static function add_menu() {
+ add_submenu_page( 'woocommerce', _x( 'OSS', 'oss', 'woocommerce-germanized' ), _x( 'One Stop Shop', 'oss', 'woocommerce-germanized' ), 'manage_woocommerce', 'oss-reports', array( __CLASS__, 'render_report_page' ) );
+ }
+
+ protected static function render_create_report() {
+ $years = array();
+ $years[] = date( 'Y' ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+ $years[] = date( 'Y', strtotime( '-1 year' ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+
+ $quarters_selectable = array();
+ $years_selectable = array();
+ $months_selectable = array();
+
+ foreach ( $years as $year ) {
+ $start_day = date( 'Y-m-d', strtotime( $year . '-01-01' ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+ $years_selectable[ $start_day ] = $year;
+
+ for ( $i = 4; $i >= 1; $i-- ) {
+ $start_month = ( $i - 1 ) * 3 + 1;
+ $start_day = date( 'Y-m-d', strtotime( $year . '-' . $start_month . '-01' ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+
+ if ( date( 'Y-m-d' ) >= $start_day ) { // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+ $quarters_selectable[ $start_day ] = sprintf( _x( 'Q%1$s/%2$s', 'oss', 'woocommerce-germanized' ), $i, $year );
+ }
+ }
+
+ for ( $i = 12; $i >= 1; $i-- ) {
+ $start_day = date( 'Y-m-d', strtotime( $year . '-' . $i . '-01' ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+ $month = date( 'm', strtotime( $year . '-' . $i . '-01' ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+
+ if ( date( 'Y-m-d' ) >= $start_day ) { // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+ $months_selectable[ $start_day ] = sprintf( _x( '%1$s/%2$s', 'oss', 'woocommerce-germanized' ), $month, $year );
+ }
+ }
+ }
+ ?>
+
+
+
+
+
+
+
+
+ output_notices();
+ $_SERVER['REQUEST_URI'] = remove_query_arg( array( 'updated', 'changed', 'deleted', 'trashed', 'untrashed' ), ( isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : admin_url( 'admin.php?page=oss-reports' ) ) );
+ ?>
+
+ views(); ?>
+
+
+
+
+
+
+ $action ) {
+ if ( isset( $action['url'] ) ) {
+ $target = isset( $action['target'] ) ? $action['target'] : '_self';
+
+ printf( '%5$s ', esc_attr( $action_name ), esc_url( $action['url'] ), ( ( isset( $action['title'] ) ) ? esc_attr( $action['title'] ) : esc_attr( $action_name ) ), esc_attr( $target ), ( isset( $action['title'] ) ? esc_html( $action['title'] ) : esc_html( $action_name ) ) );
+ }
+ }
+ }
+
+ /**
+ * @param Report $report
+ *
+ * @return array[]
+ */
+ public static function get_report_actions( $report ) {
+ $actions = array(
+ 'view' => array(
+ 'url' => $report->get_url(),
+ 'title' => _x( 'View', 'oss', 'woocommerce-germanized' ),
+ ),
+ 'export' => array(
+ 'url' => $report->get_export_link(),
+ 'title' => _x( 'Export', 'oss', 'woocommerce-germanized' ),
+ ),
+ 'export_bop' => array(
+ 'url' => $report->get_export_link( 'bop' ),
+ 'title' => _x( 'Export BOP', 'oss', 'woocommerce-germanized' ),
+ ),
+ 'refresh' => array(
+ 'url' => $report->get_refresh_link(),
+ 'title' => _x( 'Refresh', 'oss', 'woocommerce-germanized' ),
+ ),
+ 'delete' => array(
+ 'url' => $report->get_delete_link(),
+ 'title' => _x( 'Delete', 'oss', 'woocommerce-germanized' ),
+ ),
+ );
+
+ if ( 'completed' !== $report->get_status() ) {
+ $actions['cancel'] = $actions['delete'];
+ $actions['cancel']['title'] = _x( 'Cancel', 'oss', 'woocommerce-germanized' );
+
+ unset( $actions['view'] );
+ unset( $actions['refresh'] );
+ unset( $actions['delete'] );
+ unset( $actions['export'] );
+ unset( $actions['export_bop'] );
+ }
+
+ if ( 'DE' !== Helper::get_base_country() && isset( $actions['export_bop'] ) ) {
+ unset( $actions['export_bop'] );
+ }
+
+ if ( 'observer' === $report->get_type() ) {
+ unset( $actions['refresh'] );
+ unset( $actions['cancel'] );
+ }
+
+ return $actions;
+ }
+
+ public static function render_report_details() {
+ global $wp_list_table;
+
+ $report_id = isset( $_GET['report'] ) ? wc_clean( wp_unslash( $_GET['report'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+
+ if ( ! $report_id ) {
+ return;
+ }
+
+ if ( ! $report = Package::get_report( $report_id ) ) {
+ return;
+ }
+
+ $actions = self::get_report_actions( $report );
+ unset( $actions['view'] );
+
+ $columns = array(
+ 'country' => _x( 'Country', 'oss', 'woocommerce-germanized' ),
+ 'tax_rate' => _x( 'Tax Rate', 'oss', 'woocommerce-germanized' ),
+ 'net_total' => _x( 'Net Total', 'oss', 'woocommerce-germanized' ),
+ 'tax_total' => _x( 'Tax Total', 'oss', 'woocommerce-germanized' ),
+ );
+
+ $countries = $report->get_countries();
+ ?>
+
+
get_title() ); ?>
+
+ $action ) : ?>
+
+
+
+ get_status() ) : ?>
+
get_date_start()->date_i18n( wc_date_format() ) ); ?> – get_date_end()->date_i18n( wc_date_format() ) ); ?>: get_net_total() ); ?> (get_tax_total() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>)
+
+
+
+
+
+ $column ) : ?>
+
+
+
+
+
+ get_tax_rates_by_country( $country ) as $tax_rate ) :
+ ?>
+
+
+
+ get_country_net_total( $country, $tax_rate ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
+ get_country_tax_total( $country, $tax_rate ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
+
+
+
+
+
+
+
+
Find pending actions', 'oss', 'woocommerce-germanized' ), esc_html( $details['order_count'] ), ( $details['next_date'] ? esc_html( $details['next_date']->date_i18n( wc_date_format() . ' @ ' . wc_time_format() ) ) : esc_html_x( 'Not yet known', 'oss', 'woocommerce-germanized' ) ), esc_url( $details['link'] ) ); ?>
+
+
+ current_action();
+
+ if ( $doaction ) {
+ /**
+ * This nonce is dynamically constructed by WP_List_Table and uses
+ * the normalized plural argument.
+ */
+ check_admin_referer( 'bulk-' . sanitize_key( _x( 'Reports', 'oss', 'woocommerce-germanized' ) ) );
+
+ $pagenum = $wp_list_table->get_pagenum();
+ $parent_file = $wp_list_table->get_main_page();
+ $sendback = remove_query_arg( array( 'deleted', 'ids', 'changed', 'bulk_action' ), wp_get_referer() );
+
+ if ( ! $sendback ) {
+ $sendback = admin_url( $parent_file );
+ }
+
+ $sendback = add_query_arg( 'paged', $pagenum, $sendback );
+ $report_ids = array();
+
+ if ( isset( $_REQUEST['ids'] ) ) {
+ $report_ids = explode( ',', wc_clean( wp_unslash( $_REQUEST['ids'] ) ) );
+ } elseif ( ! empty( $_REQUEST['report'] ) ) {
+ $report_ids = wc_clean( wp_unslash( $_REQUEST['report'] ) );
+ }
+
+ if ( ! empty( $report_ids ) ) {
+ $sendback = $wp_list_table->handle_bulk_actions( $doaction, $report_ids, $sendback );
+ }
+
+ $sendback = remove_query_arg( array( 'action', 'action2', '_status', 'bulk_edit', 'report', 'report_created' ), $sendback );
+
+ wp_safe_redirect( esc_url_raw( $sendback ) );
+ exit();
+ } elseif ( ! empty( $_REQUEST['_wp_http_referer'] ) ) {
+ wp_safe_redirect( esc_url_raw( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
+ exit;
+ }
+
+ $wp_list_table->set_bulk_notice();
+ $wp_list_table->prepare_items();
+
+ add_screen_option( 'per_page' );
+ }
+
+ public static function register_settings( $settings ) {
+ if ( ! Package::is_integration() ) {
+ $settings[] = new SettingsPage();
+ }
+
+ return $settings;
+ }
+
+ public static function get_screen_ids() {
+ $screen_ids = array( 'woocommerce_page_wc-settings', 'woocommerce_page_oss-reports', 'product' );
+
+ return $screen_ids;
+ }
+
+ public static function admin_styles() {
+ $screen = get_current_screen();
+ $screen_id = $screen ? $screen->id : '';
+ $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
+
+ wp_register_style( 'oss_woo', Package::get_url() . '/assets/css/admin' . $suffix . '.css', array(), Package::get_version() );
+
+ // Admin styles for WC pages only.
+ if ( in_array( $screen_id, self::get_screen_ids(), true ) ) {
+ wp_enqueue_style( 'oss_woo' );
+ }
+ }
+
+ public static function admin_scripts() {
+ global $post;
+
+ $screen = get_current_screen();
+ $screen_id = $screen ? $screen->id : '';
+ $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
+ $deps = array( 'jquery', 'woocommerce_admin' );
+
+ if ( in_array( $screen_id, array( 'woocommerce_page_oss-reports' ), true ) ) {
+ $deps[] = 'jquery-ui-datepicker';
+ }
+
+ wp_register_script( 'oss-admin', Package::get_assets_url() . '/js/admin' . $suffix . '.js', $deps, Package::get_version() ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NotInFooter
+
+ if ( in_array( $screen_id, self::get_screen_ids(), true ) ) {
+ wp_enqueue_script( 'oss-admin' );
+
+ wp_localize_script(
+ 'oss-admin',
+ 'oss_admin_params',
+ array(
+ 'ajax_url' => admin_url( 'admin-ajax.php' ),
+ )
+ );
+ }
+ }
+}
diff --git a/packages/one-stop-shop-woocommerce/src/AdminNote.php b/packages/one-stop-shop-woocommerce/src/AdminNote.php
new file mode 100644
index 000000000..c5c6eef9d
--- /dev/null
+++ b/packages/one-stop-shop-woocommerce/src/AdminNote.php
@@ -0,0 +1,92 @@
+ 'oss_hide_notice',
+ 'notice' => static::get_id(),
+ '_wpnonce' => wp_create_nonce( 'oss_hide_notice' ),
+ ),
+ admin_url( 'admin-post.php' )
+ );
+ }
+
+ public static function has_actions() {
+ $actions = static::get_actions();
+
+ return empty( $actions ) ? false : true;
+ }
+
+ public static function get_actions() {
+ return array(
+ array(
+ 'target' => '',
+ 'title' => _x( 'Dismiss', 'oss', 'woocommerce-germanized' ),
+ 'url' => static::get_dismiss_url(),
+ 'is_primary' => false,
+ ),
+ );
+ }
+
+ public static function is_enabled() {
+ $enabled = true;
+
+ if ( 'yes' === get_option( 'oss_hide_notice_' . sanitize_key( static::get_id() ) ) ) {
+ $enabled = false;
+ }
+
+ return $enabled;
+ }
+
+ public static function render() {
+ ?>
+
+
+
+
+
+
+
+
+ '',
+ 'url' => '',
+ 'is_primary' => true,
+ 'target' => '_blank',
+ )
+ );
+ ?>
+
+
+
+
+
+ type = $type;
+ $default_end = new \WC_DateTime();
+ $default_start = new \WC_DateTime( 'now' );
+ $default_start->modify( '-1 year' );
+
+ $args = wp_parse_args(
+ $args,
+ array(
+ 'start' => $default_start->format( 'Y-m-d' ),
+ 'end' => $default_end->format( 'Y-m-d' ),
+ 'limit' => Queue::get_batch_size(),
+ 'status' => Queue::get_order_statuses(),
+ 'offset' => 0,
+ 'order_types' => array( 'shop_order' ),
+ 'orders_processed' => 0,
+ 'date_field' => Queue::use_date_paid() ? 'date_paid' : 'date_created',
+ )
+ );
+
+ /**
+ * Observers do not treat refunds separately
+ */
+ if ( 'observer' === $type ) {
+ $args['order_types'] = array( 'shop_order' );
+ }
+
+ foreach ( array( 'start', 'end' ) as $date_field ) {
+ if ( is_a( $args[ $date_field ], 'WC_DateTime' ) ) {
+ $args[ $date_field ] = $args[ $date_field ]->format( 'Y-m-d' );
+ } elseif ( is_numeric( $args[ $date_field ] ) ) {
+ $date = new \WC_DateTime( '@' . $args[ $date_field ] );
+ $args[ $date_field ] = $date->format( 'Y-m-d' );
+ }
+ }
+
+ $this->args = $args;
+ }
+
+ public function get_type() {
+ return $this->type;
+ }
+
+ public function get_args() {
+ return $this->args;
+ }
+
+ public function get_id() {
+ return sanitize_key( 'oss_' . $this->type . '_report_' . $this->args['start'] . '_' . $this->args['end'] );
+ }
+
+ public function delete() {
+ $report = new Report( $this->get_id() );
+ $report->delete();
+
+ delete_option( $this->get_id() . '_tmp_result' );
+ }
+
+ public function start() {
+ $report = new Report( $this->get_id() );
+ $report->reset();
+ $report->save();
+
+ return $report;
+ }
+
+ /**
+ * @param \WC_Order $order
+ *
+ * @return mixed
+ */
+ protected function get_order_taxable_country( $order ) {
+ if ( ! is_callable( array( $order, 'get_shipping_country' ) ) ) {
+ return Helper::get_base_country();
+ }
+
+ $taxable_country_type = ! empty( $order->get_shipping_country() ) ? 'shipping' : 'billing';
+ $taxable_country = 'shipping' === $taxable_country_type ? $order->get_shipping_country() : $order->get_billing_country();
+
+ return $taxable_country;
+ }
+
+ /**
+ * @param \WC_Order $order
+ *
+ * @return mixed
+ */
+ protected function get_order_taxable_postcode( $order ) {
+ $taxable_type = ! empty( $order->get_shipping_postcode() ) ? 'shipping' : 'billing';
+ $taxable_postcode = 'shipping' === $taxable_type ? $order->get_shipping_postcode() : $order->get_billing_postcode();
+
+ return $taxable_postcode;
+ }
+
+ /**
+ * @param \WC_Order $order
+ *
+ * @return bool
+ */
+ protected function include_order( $order ) {
+ $taxable_country = $this->get_order_taxable_country( $order );
+ $taxable_postcode = $this->get_order_taxable_postcode( $order );
+ $included = true;
+
+ if ( ! Helper::is_eu_vat_country( $taxable_country, $taxable_postcode ) ) {
+ $included = false;
+ }
+
+ if ( floatval( $order->get_total_tax() ) === 0.0 ) {
+ $included = false;
+ }
+
+ return apply_filters( 'oss_woocommerce_report_include_order', $included, $order );
+ }
+
+ protected function get_taxable_country_iso( $country ) {
+ if ( 'GB' === $country ) {
+ $country = 'XI';
+ }
+
+ return $country;
+ }
+
+ /**
+ * @param \WC_Order|\WC_Order_Refund $order
+ */
+ protected function get_order_number( $order ) {
+ if ( is_callable( $order, 'get_order_number' ) ) {
+ return $order->get_order_number();
+ } else {
+ return $order->get_id();
+ }
+ }
+
+ /**
+ * @return true|\WP_Error
+ */
+ public function next() {
+ $args = $this->args;
+ $results = Queue::query( $args );
+ $orders_processed = 0;
+ $tax_data = $this->get_temporary_result();
+ $supports_refunds = in_array( 'shop_order_refund', $args['order_types'], true );
+
+ Package::extended_log( sprintf( '%d applicable orders found', count( $results ) ) );
+
+ if ( ! empty( $results ) ) {
+ foreach ( $results as $result ) {
+ if ( $order = wc_get_order( $result->ID ) ) {
+ $forced_parent_order = false;
+
+ /**
+ * Query refund's parent order as the refund does not contain enough data (e.g. billing_country)
+ */
+ if ( $order->get_parent_id() > 0 ) {
+ $forced_parent_order = wc_get_order( $order->get_parent_id() );
+
+ if ( ! $forced_parent_order ) {
+ continue;
+ }
+
+ Package::extended_log( sprintf( 'Parent order: %s', $this->get_order_number( $forced_parent_order ) ) );
+ } elseif ( is_callable( array( $order, 'get_shipping_country' ) ) ) {
+ $forced_parent_order = $order;
+ }
+
+ if ( ! $forced_parent_order ) {
+ continue;
+ }
+
+ $taxable_country = $this->get_order_taxable_country( $forced_parent_order );
+
+ if ( ! $this->include_order( $forced_parent_order ) ) {
+ Package::extended_log( sprintf( 'Skipping order #%1$s based on taxable country %2$s, tax total: %3$s', $this->get_order_number( $order ), $taxable_country, $order->get_total_tax() ) );
+ continue;
+ }
+
+ $country_iso = $this->get_taxable_country_iso( $taxable_country );
+
+ Package::extended_log( sprintf( 'Processing order #%1$s (%2$s) based on taxable country %3$s', $this->get_order_number( $order ), $order->get_type(), $country_iso ) );
+
+ if ( ! isset( $tax_data[ $country_iso ] ) ) {
+ $tax_data[ $country_iso ] = array();
+ }
+
+ foreach ( $order->get_taxes() as $key => $tax ) {
+ $tax_percent = (float) Helper::get_tax_rate_percent( $tax->get_rate_id(), $forced_parent_order );
+ $tax_total = (float) $tax->get_tax_total() + (float) $tax->get_shipping_tax_total();
+
+ /**
+ * Do only remove refunded tax total in case this query does not explicitly support refunds (e.g. observers)
+ */
+ if ( ! $supports_refunds ) {
+ $refunded = (float) $forced_parent_order->get_total_tax_refunded_by_rate_id( $tax->get_rate_id() );
+ $tax_total = $tax_total - $refunded;
+
+ Package::extended_log( sprintf( 'Refunded tax %1$s = %2$s', $tax_percent, $refunded ) );
+ }
+
+ if ( $tax_percent <= 0 || 0.0 === $tax_total ) {
+ if ( $tax_percent <= 0 ) {
+ Package::extended_log( sprintf( 'Skipping order due to missing tax percentage' ) );
+ }
+
+ if ( 0.0 === $tax_total ) {
+ Package::extended_log( sprintf( 'Skipping order due to tax total = 0' ) );
+ }
+
+ continue;
+ }
+
+ if ( ! isset( $tax_data[ $country_iso ][ "$tax_percent" ] ) ) {
+ $tax_data[ $country_iso ][ "$tax_percent" ] = array(
+ 'tax_total' => 0,
+ 'net_total' => 0,
+ );
+ }
+
+ $net_total = ( $tax_total / ( (float) $tax_percent / 100 ) );
+
+ Package::extended_log( sprintf( 'Tax total %1$s = %2$s', $tax_percent, $tax_total ) );
+ Package::extended_log( sprintf( 'Net total %1$s = %2$s', $tax_percent, $net_total ) );
+
+ $net_total = wc_add_number_precision( $net_total, false );
+ $tax_total = wc_add_number_precision( $tax_total, false );
+
+ $tax_data[ $country_iso ][ "$tax_percent" ]['tax_total'] = (float) $tax_data[ $country_iso ][ "$tax_percent" ]['tax_total'];
+ $tax_data[ $country_iso ][ "$tax_percent" ]['tax_total'] += $tax_total;
+
+ $tax_data[ $country_iso ][ "$tax_percent" ]['net_total'] = (float) $tax_data[ $country_iso ][ "$tax_percent" ]['net_total'];
+ $tax_data[ $country_iso ][ "$tax_percent" ]['net_total'] += $net_total;
+
+ $orders_processed++;
+ }
+ }
+ }
+
+ $this->args['orders_processed'] = absint( $this->args['orders_processed'] ) + $orders_processed;
+
+ update_option( $this->get_id() . '_tmp_result', $tax_data, false );
+
+ return true;
+ } else {
+ return new \WP_Error( 'empty', _x( 'No orders found.', 'oss', 'woocommerce-germanized' ) );
+ }
+ }
+
+ /**
+ * @return Report
+ */
+ public function complete() {
+ Package::extended_log( sprintf( 'Completed called' ) );
+
+ $tmp_result = $this->get_temporary_result();
+ $report = new Report( $this->get_id() );
+ $tax_total = 0;
+ $net_total = 0;
+
+ foreach ( $tmp_result as $country => $tax_data ) {
+ foreach ( $tax_data as $percent => $totals ) {
+ $tax_total += (float) $totals['tax_total'];
+ $net_total += (float) $totals['net_total'];
+
+ $report->set_country_net_total( $country, $percent, (float) wc_remove_number_precision( $totals['net_total'] ) );
+ $report->set_country_tax_total( $country, $percent, (float) wc_remove_number_precision( $totals['tax_total'] ) );
+ }
+ }
+
+ $net_total = (float) wc_remove_number_precision( $net_total );
+ $tax_total = (float) wc_remove_number_precision( $tax_total );
+
+ Package::extended_log( sprintf( 'Completed net total: %s', $net_total ) );
+ Package::extended_log( sprintf( 'Completed tax total: %s', $tax_total ) );
+
+ $report->set_net_total( $net_total );
+ $report->set_tax_total( $tax_total );
+ $report->set_status( 'completed' );
+ $report->set_version( Package::get_version() );
+ $report->save();
+
+ return $report;
+ }
+
+ protected function get_temporary_result() {
+ return (array) get_option( $this->get_id() . '_tmp_result', array() );
+ }
+}
diff --git a/packages/one-stop-shop-woocommerce/src/CSVExporter.php b/packages/one-stop-shop-woocommerce/src/CSVExporter.php
new file mode 100644
index 000000000..89ffa40f8
--- /dev/null
+++ b/packages/one-stop-shop-woocommerce/src/CSVExporter.php
@@ -0,0 +1,119 @@
+report = new Report( $id );
+ $this->decimals = apply_filters( 'oss_woocommerce_csv_export_decimals', $decimals, $this );
+ $this->column_names = $this->get_default_column_names();
+ $this->filename = sanitize_file_name( $this->report->get_id() . '.csv' );
+ }
+
+ /**
+ * Return an array of columns to export.
+ *
+ * @since 3.1.0
+ * @return array
+ */
+ public function get_default_column_names() {
+ return apply_filters(
+ 'one_stop_shop_woocommerce_export_default_columns',
+ array(
+ 'country' => _x( 'Country code', 'oss', 'woocommerce-germanized' ),
+ 'tax_rate' => _x( 'Tax rate', 'oss', 'woocommerce-germanized' ),
+ 'taxable_base' => _x( 'Taxable base', 'oss', 'woocommerce-germanized' ),
+ 'amount' => _x( 'Amount', 'oss', 'woocommerce-germanized' ),
+ )
+ );
+ }
+
+ public function get_report() {
+ return $this->report;
+ }
+
+ public function get_decimals() {
+ return $this->decimals;
+ }
+
+ protected function format_decimal( $value ) {
+ return wc_format_decimal( $value, $this->get_decimals() );
+ }
+
+ protected function format_country( $country ) {
+ return strtoupper( $country );
+ }
+
+ protected function get_row_data( $country, $tax_rate ) {
+ $row = array();
+
+ foreach ( array_keys( $this->get_column_names() ) as $column_id ) {
+ $column_id = strstr( $column_id, ':' ) ? current( explode( ':', $column_id ) ) : $column_id;
+ $value = '';
+
+ if ( 'country' === $column_id ) {
+ $value = $this->format_country( $country );
+ } elseif ( 'tax_rate' === $column_id ) {
+ $value = $this->format_decimal( $tax_rate );
+ } elseif ( 'taxable_base' === $column_id ) {
+ $value = $this->format_decimal( $this->report->get_country_net_total( $country, $tax_rate, $this->get_decimals() ) );
+ } elseif ( 'amount' === $column_id ) {
+ $value = $this->format_decimal( $this->report->get_country_tax_total( $country, $tax_rate, $this->get_decimals() ) );
+ } elseif ( is_callable( array( $this, "get_column_value_{$column_id}" ) ) ) {
+ $value = $this->{"get_column_value_{$column_id}"}( $country, $tax_rate );
+ } else {
+ $value = apply_filters( "one_stop_shop_woocommerce_export_column_{$column_id}", $value, $country, $tax_rate, $this );
+ }
+
+ $row[ $column_id ] = $value;
+ }
+
+ return $row;
+ }
+
+ /**
+ * Prepare data that will be exported.
+ */
+ public function prepare_data_to_export() {
+ $countries = $this->report->get_countries();
+
+ if ( ! empty( $countries ) ) {
+ foreach ( $countries as $country ) {
+ foreach ( $this->report->get_tax_rates_by_country( $country ) as $tax_rate ) {
+ $this->row_data[] = apply_filters( 'one_stop_shop_woocommerce_export_row_data', $this->get_row_data( $country, $tax_rate ), $country, $tax_rate, $this );
+ }
+ }
+ }
+ }
+}
diff --git a/packages/one-stop-shop-woocommerce/src/CSVExporterBOP.php b/packages/one-stop-shop-woocommerce/src/CSVExporterBOP.php
new file mode 100644
index 000000000..f832f942b
--- /dev/null
+++ b/packages/one-stop-shop-woocommerce/src/CSVExporterBOP.php
@@ -0,0 +1,125 @@
+ 'Satzart',
+ 'country' => 'Land des Verbrauchs',
+ 'tax_type' => 'Umsatzsteuertyp',
+ 'tax_rate' => 'Umsatzsteuersatz',
+ 'taxable_base' => 'Steuerbemessungsgrundlage, Nettobetrag',
+ 'amount' => 'Umsatzsteuerbetrag',
+ )
+ );
+ }
+
+ protected function get_column_value_bop_type( $country, $tax_rate ) {
+ return apply_filters( 'one_stop_shop_woocommerce_bop_export_type', 3 );
+ }
+
+ protected function get_column_value_tax_type( $country, $tax_rate ) {
+ $tax_type = Helper::get_tax_type_by_country_rate( $tax_rate, $country );
+ $tax_return_type = 'STANDARD';
+
+ switch ( $tax_type ) {
+ case 'reduced':
+ case 'greater-reduced':
+ case 'super-reduced':
+ $tax_return_type = 'REDUCED';
+ break;
+ default:
+ $tax_return_type = strtoupper( $tax_type );
+ break;
+ }
+
+ return $tax_return_type;
+ }
+
+ protected function format_country( $country ) {
+ $country = parent::format_country( $country );
+
+ if ( 'GR' === $country ) {
+ $country = 'EL';
+ }
+
+ return $country;
+ }
+
+ /**
+ * Prepare data that will be exported.
+ */
+ public function prepare_data_to_export() {
+ $countries = $this->report->get_countries();
+
+ if ( ! empty( $countries ) ) {
+ foreach ( $countries as $country ) {
+ $tax_rates = $this->report->get_tax_rates_by_country( $country );
+
+ if ( ! empty( $tax_rates ) ) {
+ $this->row_data[] = apply_filters(
+ 'one_stop_shop_woocommerce_export_bop_country_header_data',
+ array(
+ 'country' => $this->format_country( $country ),
+ 'bop_type' => 1,
+ ),
+ $country,
+ $this
+ );
+
+ foreach ( $tax_rates as $tax_rate ) {
+ $this->row_data[] = apply_filters( 'one_stop_shop_woocommerce_bop_export_row_data', $this->get_row_data( $country, $tax_rate ), $country, $tax_rate, $this );
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Do the export. Prevent Woo from prepending a BOM.
+ */
+ public function export() {
+ $this->prepare_data_to_export();
+ $this->send_headers();
+
+ $csv_data = $this->export_column_headers() . $this->get_csv_data();
+
+ // Replace newlines with Windows-style.
+ $csv_data = preg_replace( '~\R~u', "\r", $csv_data );
+
+ $this->send_content( $csv_data );
+ die();
+ }
+
+ protected function export_column_headers() {
+ $buffer = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen
+ ob_start();
+ fwrite( $buffer, '#v1.1' . PHP_EOL ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite
+ $content = ob_get_clean();
+
+ return $content;
+ }
+
+ protected function fputcsv( $buffer, $export_row ) {
+ fputcsv( $buffer, $export_row, $this->get_delimiter(), "'", "\0" ); // @codingStandardsIgnoreLine
+ }
+}
diff --git a/packages/one-stop-shop-woocommerce/src/DeliveryThresholdEmailNotification.php b/packages/one-stop-shop-woocommerce/src/DeliveryThresholdEmailNotification.php
new file mode 100644
index 000000000..39d7ffc80
--- /dev/null
+++ b/packages/one-stop-shop-woocommerce/src/DeliveryThresholdEmailNotification.php
@@ -0,0 +1,113 @@
+template_base = Package::get_path() . '/templates/';
+ $this->id = 'oss_delivery_threshold_email_notification';
+ $this->title = _x( 'OSS Delivery Threshold Notification', 'oss', 'woocommerce-germanized' );
+ $this->description = _x( 'This email notifies shop owners in case the delivery threshold (OSS) is close to being reached.', 'oss', 'woocommerce-germanized' );
+ $this->template_html = 'emails/admin-delivery-threshold.php';
+ $this->template_plain = 'emails/plain/admin-delivery-threshold.php';
+ $this->customer_email = false;
+
+ parent::__construct();
+
+ // Other settings.
+ $this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) );
+ }
+
+ /**
+ * Get email subject.
+ *
+ * @since 3.1.0
+ * @return string
+ */
+ public function get_default_subject() {
+ return _x( '[{site_title}]: OSS delivery threshold reached', 'oss', 'woocommerce-germanized' );
+ }
+
+ /**
+ * Get email heading.
+ *
+ * @since 3.1.0
+ * @return string
+ */
+ public function get_default_heading() {
+ return _x( 'OSS delivery threshold reached', 'oss', 'woocommerce-germanized' );
+ }
+
+ /**
+ * Get content html.
+ *
+ * @return string
+ */
+ public function get_content_html() {
+ return wc_get_template_html(
+ $this->template_html,
+ array(
+ 'report' => $this->object,
+ 'email_heading' => $this->get_heading(),
+ 'additional_content' => $this->get_additional_content(),
+ 'sent_to_admin' => true,
+ 'plain_text' => false,
+ 'email' => $this,
+ ),
+ '',
+ $this->template_base
+ );
+ }
+
+ /**
+ * Get content plain.
+ *
+ * @return string
+ */
+ public function get_content_plain() {
+ return wc_get_template_html(
+ $this->template_plain,
+ array(
+ 'report' => $this->object,
+ 'email_heading' => $this->get_heading(),
+ 'additional_content' => $this->get_additional_content(),
+ 'sent_to_admin' => true,
+ 'plain_text' => true,
+ 'email' => $this,
+ ),
+ '',
+ $this->template_base
+ );
+ }
+
+ /**
+ * Trigger the sending of this email.
+ *
+ * @param Report $report
+ */
+ public function trigger( $report ) {
+ $this->object = $report;
+
+ $success = $this->send(
+ $this->get_recipient(),
+ $this->get_subject(),
+ $this->get_content(),
+ $this->get_headers(),
+ $this->get_attachments()
+ );
+
+ if ( $success ) {
+ update_option( 'oss_woocommerce_notification_sent_' . $report->get_date_start()->format( 'Y' ), 'yes', false );
+ }
+ }
+}
diff --git a/packages/one-stop-shop-woocommerce/src/DeliveryThresholdWarning.php b/packages/one-stop-shop-woocommerce/src/DeliveryThresholdWarning.php
new file mode 100644
index 000000000..59d023aa8
--- /dev/null
+++ b/packages/one-stop-shop-woocommerce/src/DeliveryThresholdWarning.php
@@ -0,0 +1,40 @@
+ '',
+ 'title' => _x( 'See details', 'oss', 'woocommerce-germanized' ),
+ 'url' => Settings::get_settings_url(),
+ 'is_primary' => true,
+ ),
+ ),
+ parent::get_actions()
+ );
+ }
+
+ public static function get_content() {
+ return Admin::get_threshold_notice_content();
+ }
+
+ public static function get_title() {
+ return Admin::get_threshold_notice_title();
+ }
+
+ public static function is_enabled() {
+ $is_enabled = parent::is_enabled();
+
+ return $is_enabled && Package::enable_auto_observer() && Package::observer_report_needs_notification();
+ }
+
+ public static function get_id() {
+ return 'delivery-threshold-warning-' . date( 'Y' ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+ }
+}
diff --git a/packages/one-stop-shop-woocommerce/src/Install.php b/packages/one-stop-shop-woocommerce/src/Install.php
new file mode 100644
index 000000000..1aae1b7c3
--- /dev/null
+++ b/packages/one-stop-shop-woocommerce/src/Install.php
@@ -0,0 +1,41 @@
+get_status() ) {
+ $report->delete();
+ }
+ }
+ }
+ }
+
+ /**
+ * Make sure there is only one observer running at a time.
+ */
+ foreach ( $running as $k => $report_id ) {
+ if ( in_array( $report_id, $running_observers, true ) && $report_id !== $has_running_observer ) {
+ if ( $report = self::get_report( $report_id ) ) {
+ $report->delete();
+ }
+
+ unset( $running[ $k ] );
+ }
+ }
+
+ $running = array_values( $running );
+
+ update_option( 'oss_woocommerce_reports_running', $running, false );
+ Queue::clear_cache();
+
+ $observer_reports = self::get_reports(
+ array(
+ 'type' => 'observer',
+ 'include_observer' => true,
+ )
+ );
+
+ foreach ( $observer_reports as $observer ) {
+ if ( ! self::enable_auto_observer() ) {
+ /**
+ * Delete observers in case observing was disabled.
+ */
+ $observer->delete();
+ } else {
+ /*
+ * Do not delete running observers (which are orphans by design)
+ */
+ if ( $observer->get_id() === $has_running_observer ) {
+ continue;
+ }
+
+ $year = $observer->get_date_start()->format( 'Y' );
+
+ /**
+ * Delete orphan observer reports (reports not linked as a main observer for a certain year).
+ */
+ if ( get_option( 'oss_woocommerce_observer_report_' . $year ) !== $observer->get_id() ) {
+ $observer->delete();
+ }
+ }
+ }
+
+ /**
+ * In case the current observer report does not exist - delete the option
+ */
+ if ( self::enable_auto_observer() ) {
+ $year = date( 'Y' ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+ $report_id = get_option( 'oss_woocommerce_observer_report_' . $year );
+
+ if ( ! empty( $report_id ) ) {
+ if ( ! self::get_report( $report_id ) ) {
+ delete_option( 'oss_woocommerce_observer_report_' . $year );
+ }
+ }
+ }
+ }
+
+ public static function dependency_notice() {
+ ?>
+
+ get_net_total();
+ }
+
+ $total_left = self::get_delivery_threshold() - $net_total;
+
+ if ( $total_left <= 0 ) {
+ $total_left = 0;
+ }
+
+ return $total_left;
+ }
+
+ /**
+ * @param null $year
+ *
+ * @return false|Report
+ */
+ public static function get_completed_observer_report( $year = null ) {
+ $observer_report = self::get_observer_report( $year );
+
+ if ( ! $observer_report || 'completed' !== $observer_report->get_status() ) {
+ return false;
+ }
+
+ return $observer_report;
+ }
+
+ /**
+ * @param null $year
+ *
+ * @return false|Report
+ */
+ public static function get_observer_report( $year = null ) {
+ if ( is_null( $year ) ) {
+ $year = date( 'Y' ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+ }
+
+ $report_id = get_option( 'oss_woocommerce_observer_report_' . $year );
+ $report = false;
+
+ if ( ! empty( $report_id ) ) {
+ $report = self::get_report( $report_id );
+ }
+
+ return $report;
+ }
+
+ public static function observer_report_is_outdated() {
+ $is_outdated = true;
+
+ if ( $observer = self::get_observer_report() ) {
+ $date_end = $observer->get_date_end();
+ $now = new \WC_DateTime();
+
+ $diff = $now->diff( $date_end );
+
+ if ( $diff->days <= 1 ) {
+ $is_outdated = false;
+ }
+ }
+
+ return $is_outdated;
+ }
+
+ public static function string_to_datetime( $time_string ) {
+ if ( is_string( $time_string ) && ! is_numeric( $time_string ) ) {
+ $time_string = strtotime( $time_string );
+ }
+
+ $date_time = $time_string;
+
+ if ( is_numeric( $date_time ) ) {
+ $date_time = new \WC_DateTime( "@{$date_time}", new \DateTimeZone( 'UTC' ) );
+ }
+
+ if ( ! is_a( $date_time, 'WC_DateTime' ) ) {
+ return null;
+ }
+
+ return $date_time;
+ }
+
+ /**
+ * @param $id
+ *
+ * @return false|Report
+ */
+ public static function get_report( $id ) {
+ $report = new Report( $id );
+
+ if ( $report->exists() ) {
+ return $report;
+ }
+
+ return false;
+ }
+
+ public static function get_report_id( $parts ) {
+ $parts = wp_parse_args(
+ $parts,
+ array(
+ 'type' => 'daily',
+ 'date_start' => date( 'Y-m-d' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+ 'date_end' => date( 'Y-m-d' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+ )
+ );
+
+ if ( is_a( $parts['date_start'], 'WC_DateTime' ) ) {
+ $parts['date_start'] = $parts['date_start']->format( 'Y-m-d' );
+ }
+
+ if ( is_a( $parts['date_end'], 'WC_DateTime' ) ) {
+ $parts['date_end'] = $parts['date_end']->format( 'Y-m-d' );
+ }
+
+ return 'oss_' . $parts['type'] . '_report_' . $parts['date_start'] . '_' . $parts['date_end'];
+ }
+
+ public static function get_report_data( $id ) {
+ $id_parts = explode( '_', $id );
+ $data = array(
+ 'id' => $id,
+ 'type' => $id_parts[1],
+ 'date_start' => self::string_to_datetime( $id_parts[3] ),
+ 'date_end' => self::string_to_datetime( $id_parts[4] ),
+ );
+
+ return $data;
+ }
+
+ public static function get_report_title( $id ) {
+ $args = self::get_report_data( $id );
+ $title = _x( 'Report', 'oss', 'woocommerce-germanized' );
+
+ if ( 'quarterly' === $args['type'] ) {
+ $date_start = $args['date_start'];
+ $quarter = 1;
+ $month_num = (int) $date_start->date_i18n( 'n' );
+
+ if ( 4 === $month_num ) {
+ $quarter = 2;
+ } elseif ( 7 === $month_num ) {
+ $quarter = 3;
+ } elseif ( 10 === $month_num ) {
+ $quarter = 4;
+ }
+
+ $title = sprintf( _x( 'Q%1$s/%2$s', 'oss', 'woocommerce-germanized' ), $quarter, $date_start->date_i18n( 'Y' ) );
+ } elseif ( 'monthly' === $args['type'] ) {
+ $date_start = $args['date_start'];
+ $month_num = $date_start->date_i18n( 'm' );
+
+ $title = sprintf( _x( '%1$s/%2$s', 'oss', 'woocommerce-germanized' ), $month_num, $date_start->date_i18n( 'Y' ) );
+ } elseif ( 'yearly' === $args['type'] ) {
+ $date_start = $args['date_start'];
+
+ $title = sprintf( _x( '%1$s', 'oss', 'woocommerce-germanized' ), $date_start->date_i18n( 'Y' ) ); // phpcs:ignore WordPress.WP.I18n.NoEmptyStrings
+ } elseif ( 'custom' === $args['type'] ) {
+ $date_start = $args['date_start'];
+ $date_end = $args['date_end'];
+
+ $title = sprintf( _x( '%1$s - %2$s', 'oss', 'woocommerce-germanized' ), $date_start->date_i18n( 'Y-m-d' ), $date_end->date_i18n( 'Y-m-d' ) );
+ } elseif ( 'observer' === $args['type'] ) {
+ $date_start = $args['date_start'];
+ $date_end = $args['date_end'];
+
+ $title = sprintf( _x( 'Observer %1$s', 'oss', 'woocommerce-germanized' ), $date_start->date_i18n( 'Y' ) );
+ }
+
+ return $title;
+ }
+
+ /**
+ * @param Report $report
+ */
+ public static function remove_report( $report ) {
+ $reports_available = self::get_report_ids();
+
+ if ( in_array( $report->get_id(), $reports_available[ $report->get_type() ], true ) ) {
+ $reports_available[ $report->get_type() ] = array_diff( $reports_available[ $report->get_type() ], array( $report->get_id() ) );
+
+ update_option( 'oss_woocommerce_reports', $reports_available, false );
+
+ /**
+ * Force non-cached option
+ */
+ wp_cache_delete( 'oss_woocommerce_reports', 'options' );
+ }
+ }
+
+ /**
+ * @param array $args
+ *
+ * @return Report[]
+ */
+ public static function get_reports( $args = array() ) {
+ $args = wp_parse_args(
+ $args,
+ array(
+ 'type' => '',
+ 'limit' => -1,
+ 'offset' => 0,
+ 'orderby' => 'date_start',
+ 'include_observer' => false,
+ )
+ );
+
+ $ids = self::get_report_ids( $args['include_observer'] );
+
+ if ( ! empty( $args['type'] ) ) {
+ $report_ids = array_key_exists( $args['type'], $ids ) ? $ids[ $args['type'] ] : array();
+ } else {
+ $report_ids = array_merge( ...array_values( $ids ) );
+ }
+
+ $reports_sorted = array();
+
+ foreach ( $report_ids as $id ) {
+ $reports_sorted[] = self::get_report_data( $id );
+ }
+
+ if ( array_key_exists( $args['orderby'], array( 'date_start', 'date_end' ) ) ) {
+ usort(
+ $reports_sorted,
+ function( $a, $b ) use ( $args ) {
+ if ( $a[ $args['orderby'] ] === $b[ $args['orderby'] ] ) {
+ return 0;
+ }
+
+ return $a[ $args['orderby'] ] < $b[ $args['orderby'] ] ? -1 : 1;
+ }
+ );
+ }
+
+ if ( -1 !== $args['limit'] ) {
+ $reports_sorted = array_slice( $reports_sorted, $args['offset'], $args['limit'] );
+ }
+
+ $reports = array();
+
+ foreach ( $reports_sorted as $data ) {
+ if ( $report = self::get_report( $data['id'] ) ) {
+ $reports[] = $report;
+ }
+ }
+
+ return $reports;
+ }
+
+ public static function clear_caches() {
+ delete_transient( 'oss_reports_counts' );
+ wp_cache_delete( 'oss_woocommerce_reports', 'options' );
+ }
+
+ public static function get_report_counts() {
+ $types = array_keys( self::get_available_report_types( true ) );
+ $cache_key = 'oss_reports_counts';
+ $counts = get_transient( $cache_key );
+
+ if ( false === $counts ) {
+ $counts = array();
+
+ foreach ( $types as $type ) {
+ $counts[ $type ] = 0;
+ }
+
+ foreach ( self::get_reports( array( 'include_observer' => true ) ) as $report ) {
+ if ( ! array_key_exists( $report->get_type(), $counts ) ) {
+ continue;
+ }
+
+ $counts[ $report->get_type() ] += 1;
+ }
+
+ set_transient( $cache_key, $counts );
+ }
+
+ return (array) $counts;
+ }
+
+ public static function load_plugin_textdomain() {
+ if ( function_exists( 'determine_locale' ) ) {
+ $locale = determine_locale();
+ } else {
+ // @todo Remove when start supporting WP 5.0 or later.
+ $locale = is_admin() ? get_user_locale() : get_locale();
+ }
+
+ $locale = apply_filters( 'plugin_locale', $locale, 'woocommerce-germanized' );
+
+ unload_textdomain( 'oss-woocommerce' );
+ load_textdomain( 'oss-woocommerce', trailingslashit( WP_LANG_DIR ) . 'oss-woocommerce/oss-woocommerce-' . $locale . '.mo' );
+ load_plugin_textdomain( 'oss-woocommerce', false, plugin_basename( dirname( __FILE__ ) ) . '/i18n/languages/' );
+ }
+
+ public static function register_emails( $emails ) {
+ $mails = array(
+ '\Vendidero\OneStopShop\DeliveryThresholdEmailNotification',
+ );
+
+ foreach ( $mails as $mail ) {
+ $emails[ self::sanitize_email_class( $mail ) ] = new $mail();
+ }
+
+ return $emails;
+ }
+
+ protected static function sanitize_email_class( $class ) {
+ return 'oss_woocommerce_' . sanitize_key( str_replace( __NAMESPACE__ . '\\', '', $class ) );
+ }
+
+ public static function observer_report_needs_notification() {
+ $needs_notification = false;
+
+ if ( $report = self::get_observer_report() ) {
+ $net_total = $report->get_net_total();
+ $threshold = self::get_delivery_notification_threshold();
+
+ if ( $net_total >= $threshold ) {
+ $needs_notification = true;
+ }
+ }
+
+ return apply_filters( 'oss_woocommerce_observer_report_needs_notification', $needs_notification );
+ }
+
+ /**
+ * @param Report $observer_report
+ */
+ public static function maybe_send_notification( $observer_report ) {
+ if ( self::observer_report_needs_notification() ) {
+ if ( 'yes' !== get_option( 'oss_woocommerce_notification_sent_' . $observer_report->get_date_start()->format( 'Y' ) ) ) {
+ $mails = WC()->mailer()->get_emails();
+ $mail = self::sanitize_email_class( '\Vendidero\OneStopShop\DeliveryThresholdEmailNotification' );
+
+ if ( isset( $mails[ $mail ] ) ) {
+ $mails[ $mail ]->trigger( $observer_report );
+ }
+ }
+ }
+ }
+
+ /**
+ * Let the observer date back 7 days to make sure most of the orders
+ * have already been processed (e.g. received payment etc) to reduce the chance of missing out on orders.
+ *
+ * @return int
+ */
+ public static function get_observer_backdating_days() {
+ return 7;
+ }
+
+ public static function update_observer_report() {
+ if ( self::enable_auto_observer() ) {
+ /**
+ * Delete observer reports with missing versions to make sure the report
+ * is re-created with the new backdating functionality.
+ */
+ if ( $report = self::get_observer_report() ) {
+ if ( '' === $report->get_version() ) {
+ $report->delete();
+ }
+ }
+
+ $days = (int) self::get_observer_backdating_days();
+
+ $date_start = new \WC_DateTime();
+ $date_start->modify( "-{$days} day" . ( $days > 1 ? 's' : '' ) );
+
+ Queue::start( 'observer', $date_start );
+ }
+ }
+
+ public static function setup_recurring_actions() {
+ if ( $queue = Queue::get_queue() ) {
+
+ // Schedule once per day at 2:00
+ if ( null === $queue->get_next( 'oss_woocommerce_daily_cleanup', array(), 'oss_woocommerce' ) ) {
+ $timestamp = strtotime( 'tomorrow midnight' );
+ $date = new \WC_DateTime();
+
+ $date->setTimestamp( $timestamp );
+ $date->modify( '+2 hours' );
+
+ $queue->cancel_all( 'oss_woocommerce_daily_cleanup', array(), 'oss_woocommerce' );
+ $queue->schedule_recurring( $date->getTimestamp(), DAY_IN_SECONDS, 'oss_woocommerce_daily_cleanup', array(), 'oss_woocommerce' );
+ }
+
+ if ( self::enable_auto_observer() ) {
+ // Schedule once per day at 3:00
+ if ( null === $queue->get_next( 'oss_woocommerce_daily_observer', array(), 'oss_woocommerce' ) ) {
+ $timestamp = strtotime( 'tomorrow midnight' );
+ $date = new \WC_DateTime();
+
+ $date->setTimestamp( $timestamp );
+ $date->modify( '+3 hours' );
+
+ $queue->cancel_all( 'oss_woocommerce_daily_observer', array(), 'oss_woocommerce' );
+ $queue->schedule_recurring( $date->getTimestamp(), DAY_IN_SECONDS, 'oss_woocommerce_daily_observer', array(), 'oss_woocommerce' );
+ }
+ } else {
+ $queue->cancel( 'oss_woocommerce_daily_observer', array(), 'oss_woocommerce' );
+ }
+ }
+ }
+
+ public static function get_available_report_types( $include_observer = false ) {
+ $types = array(
+ 'quarterly' => _x( 'Quarterly', 'oss', 'woocommerce-germanized' ),
+ 'yearly' => _x( 'Yearly', 'oss', 'woocommerce-germanized' ),
+ 'monthly' => _x( 'Monthly', 'oss', 'woocommerce-germanized' ),
+ 'custom' => _x( 'Custom', 'oss', 'woocommerce-germanized' ),
+ );
+
+ if ( $include_observer ) {
+ $types['observer'] = _x( 'Observer', 'oss', 'woocommerce-germanized' );
+ }
+
+ return $types;
+ }
+
+ public static function get_type_title( $type ) {
+ $types = self::get_available_report_types( true );
+
+ return array_key_exists( $type, $types ) ? $types[ $type ] : '';
+ }
+
+ public static function get_report_statuses() {
+ return array(
+ 'pending' => _x( 'Pending', 'oss', 'woocommerce-germanized' ),
+ 'completed' => _x( 'Completed', 'oss', 'woocommerce-germanized' ),
+ 'failed' => _x( 'Failed', 'oss', 'woocommerce-germanized' ),
+ );
+ }
+
+ public static function get_report_status_title( $status ) {
+ $statuses = self::get_report_statuses();
+
+ return array_key_exists( $status, $statuses ) ? $statuses[ $status ] : '';
+ }
+
+ public static function has_dependencies() {
+ return ( class_exists( 'WooCommerce' ) );
+ }
+
+ public static function install() {
+ self::init();
+ Install::install();
+ }
+
+ public static function deactivate() {
+ if ( self::has_dependencies() && Admin::supports_wc_admin() ) {
+ foreach ( Admin::get_notes() as $oss_note ) {
+ Admin::delete_wc_admin_note( $oss_note );
+ }
+ }
+ }
+
+ public static function install_integration() {
+ self::install();
+ }
+
+ public static function is_integration() {
+ $gzd_installed = class_exists( 'WooCommerce_Germanized' );
+ $gzd_version = get_option( 'woocommerce_gzd_version', '1.0' );
+
+ return $gzd_installed && version_compare( $gzd_version, '3.5.0', '>=' ) ? true : false;
+ }
+
+ /**
+ * Return the version of the package.
+ *
+ * @return string
+ */
+ public static function get_version() {
+ return self::VERSION;
+ }
+
+ /**
+ * Return the path to the package.
+ *
+ * @return string
+ */
+ public static function get_path() {
+ return dirname( __DIR__ );
+ }
+
+ /**
+ * Return the path to the package.
+ *
+ * @return string
+ */
+ public static function get_url() {
+ return plugins_url( '', __DIR__ );
+ }
+
+ public static function get_assets_url() {
+ return self::get_url() . '/assets';
+ }
+
+ private static function define_constant( $name, $value ) {
+ if ( ! defined( $name ) ) {
+ define( $name, $value );
+ }
+ }
+
+ public static function log( $message, $type = 'info' ) {
+ $logger = wc_get_logger();
+
+ if ( ! $logger || ! apply_filters( 'oss_woocommerce_enable_logging', true ) ) {
+ return;
+ }
+
+ if ( ! is_callable( array( $logger, $type ) ) ) {
+ $type = 'info';
+ }
+
+ $logger->{$type}( $message, array( 'source' => 'one-stop-shop-woocommerce' ) );
+ }
+
+ public static function extended_log( $message, $type = 'info' ) {
+ if ( apply_filters( 'oss_woocommerce_enable_extended_logging', ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) ) {
+ self::log( $message, $type );
+ }
+ }
+}
diff --git a/packages/one-stop-shop-woocommerce/src/Queue.php b/packages/one-stop-shop-woocommerce/src/Queue.php
new file mode 100644
index 000000000..bbec8efbb
--- /dev/null
+++ b/packages/one-stop-shop-woocommerce/src/Queue.php
@@ -0,0 +1,514 @@
+diff( $args['end'] );
+
+ /**
+ * Except observers, all new queries treat refunds separately
+ */
+ if ( 'observer' !== $type ) {
+ $args['order_types'] = array(
+ 'shop_order',
+ 'shop_order_refund',
+ );
+ }
+
+ // Add version
+ $args['version'] = Package::get_version();
+
+ $generator = new AsyncReportGenerator( $type, $args );
+ $queue_args = $generator->get_args();
+ $queue = self::get_queue();
+
+ self::cancel( $generator->get_id() );
+
+ $report = $generator->start();
+
+ if ( is_a( $report, '\Vendidero\OneStopShop\Report' ) && $report->exists() ) {
+ Package::log( sprintf( 'Starting new %1$s', $report->get_title() ) );
+ Package::extended_log( sprintf( 'Default report arguments: %s', wc_print_r( $queue_args, true ) ) );
+
+ $queue->schedule_single(
+ time() + 10,
+ 'oss_woocommerce_' . $generator->get_id(),
+ array( 'args' => $queue_args ),
+ 'oss_woocommerce'
+ );
+
+ $running = self::get_reports_running();
+
+ if ( ! in_array( $generator->get_id(), $running, true ) ) {
+ $running[] = $generator->get_id();
+ }
+
+ update_option( 'oss_woocommerce_reports_running', $running, false );
+ self::clear_cache();
+
+ return $generator->get_id();
+ }
+
+ return false;
+ }
+
+ public static function clear_cache() {
+ wp_cache_delete( 'oss_woocommerce_reports_running', 'options' );
+ }
+
+ public static function get_queue_details( $report_id ) {
+ $details = array(
+ 'next_date' => null,
+ 'link' => admin_url( 'admin.php?page=wc-status&tab=action-scheduler&s=' . esc_attr( $report_id ) . '&status=pending' ),
+ 'order_count' => 0,
+ 'has_action' => false,
+ 'is_finished' => false,
+ 'action' => false,
+ );
+
+ if ( $queue = self::get_queue() ) {
+
+ if ( $next_date = $queue->get_next( 'oss_woocommerce_' . $report_id ) ) {
+ $details['next_date'] = $next_date;
+ }
+
+ $search_args = array(
+ 'hook' => 'oss_woocommerce_' . $report_id,
+ 'status' => \ActionScheduler_Store::STATUS_RUNNING,
+ 'order' => 'DESC',
+ 'per_page' => 1,
+ );
+
+ $results = $queue->search( $search_args );
+
+ /**
+ * Search for pending as fallback
+ */
+ if ( empty( $results ) ) {
+ $search_args['status'] = \ActionScheduler_Store::STATUS_PENDING;
+ $results = $queue->search( $search_args );
+ }
+
+ /**
+ * Last resort: Search for completed (e.g. if no pending and no running are found - must have been completed)
+ */
+ if ( empty( $results ) ) {
+ $search_args['status'] = \ActionScheduler_Store::STATUS_COMPLETE;
+ $results = $queue->search( $search_args );
+ }
+
+ if ( ! empty( $results ) ) {
+ $action = array_values( $results )[0];
+ $args = $action->get_args();
+ $processed = isset( $args['args']['orders_processed'] ) ? (int) $args['args']['orders_processed'] : 0;
+
+ $details['order_count'] = absint( $processed );
+ $details['has_action'] = true;
+ $details['action'] = $action;
+ $details['is_finished'] = $action->is_finished();
+ }
+ }
+
+ return $details;
+ }
+
+ public static function get_batch_size() {
+ return apply_filters( 'oss_woocommerce_report_batch_size', 25 );
+ }
+
+ public static function use_date_paid() {
+ $use_date_paid = 'date_paid' === get_option( 'oss_report_date_type', 'date_paid' );
+
+ return apply_filters( 'oss_woocommerce_report_use_date_paid', $use_date_paid );
+ }
+
+ public static function get_order_statuses() {
+ $statuses = array_keys( wc_get_order_statuses() );
+ $statuses = array_diff( $statuses, array( 'wc-cancelled', 'wc-failed' ) );
+
+ if ( self::use_date_paid() ) {
+ $statuses = array_diff( $statuses, array( 'wc-pending' ) );
+ }
+
+ return apply_filters( 'oss_woocommerce_valid_order_statuses', $statuses );
+ }
+
+ public static function build_query( $args ) {
+ global $wpdb;
+
+ $joins = array(
+ "LEFT JOIN {$wpdb->postmeta} AS mt1 ON {$wpdb->posts}.ID = mt1.post_id AND (mt1.meta_key = '_shipping_country' OR mt1.meta_key = '_billing_country')",
+ );
+
+ $taxable_countries_in = self::generate_in_query_sql( Helper::get_non_base_eu_countries( true ) );
+ $post_status_in = self::generate_in_query_sql( $args['status'] );
+ $post_type_in = self::generate_in_query_sql( isset( $args['order_types'] ) ? (array) $args['order_types'] : array( 'shop_order' ) );
+ $where_country_sql = "mt1.meta_value IN {$taxable_countries_in}";
+
+ if ( in_array( 'shop_order_refund', $args['order_types'], true ) ) {
+ $joins[] = "LEFT JOIN {$wpdb->postmeta} AS mt1_parent ON {$wpdb->posts}.post_parent = mt1_parent.post_id AND (mt1_parent.meta_key = '_shipping_country' OR mt1_parent.meta_key = '_billing_country')";
+ $where_country_sql = "( {$wpdb->posts}.post_parent > 0 AND (mt1_parent.meta_value IN {$taxable_countries_in}) ) OR ( mt1.meta_value IN {$taxable_countries_in} )";
+ }
+
+ $where_date_sql = $wpdb->prepare( "{$wpdb->posts}.post_date >= %s AND {$wpdb->posts}.post_date <= %s", $args['start'], $args['end'] );
+
+ if ( 'date_paid' === $args['date_field'] ) {
+ /**
+ * Add one day to the end date to capture timestamps (including time data) in between
+ */
+ $end_adjusted = strtotime( $args['end'] ) + DAY_IN_SECONDS;
+
+ /**
+ * Use a max end date to limit potential query results in case date_paid meta field is used.
+ * This way we will only register payments made max 2 month after the order created date.
+ */
+ $max_end = new \WC_DateTime( $args['end'] );
+ $max_end->modify( '+2 months' );
+
+ $joins[] = "LEFT JOIN {$wpdb->postmeta} AS mt3 ON ( {$wpdb->posts}.ID = mt3.post_id AND mt3.meta_key = '_date_paid' )";
+
+ $where_date_sql = $wpdb->prepare(
+ "( {$wpdb->posts}.post_date >= %s AND {$wpdb->posts}.post_date <= %s ) AND NOT mt3.post_id IS NULL AND (
+ mt3.meta_key = '_date_paid' AND mt3.meta_value >= %s AND mt3.meta_value <= %s
+ ) OR {$wpdb->posts}.post_parent > 0 AND (
+ {$wpdb->posts}.post_date >= %s AND {$wpdb->posts}.post_date <= %s
+ )",
+ $args['start'],
+ $max_end->format( 'Y-m-d' ),
+ strtotime( $args['start'] ),
+ $end_adjusted,
+ $args['start'],
+ $args['end']
+ );
+ }
+
+ $join_sql = implode( ' ', $joins );
+
+ // @codingStandardsIgnoreStart
+ $sql = $wpdb->prepare(
+ "
+ SELECT {$wpdb->posts}.* FROM {$wpdb->posts}
+ $join_sql
+ WHERE 1=1
+ AND ( {$wpdb->posts}.post_type IN {$post_type_in} ) AND ( {$wpdb->posts}.post_status IN {$post_status_in} ) AND ( {$where_date_sql} )
+ AND ( {$where_country_sql} )
+ GROUP BY {$wpdb->posts}.ID
+ ORDER BY {$wpdb->posts}.post_date ASC
+ LIMIT %d, %d",
+ $args['offset'],
+ $args['limit']
+ );
+ // @codingStandardsIgnoreEnd
+
+ return $sql;
+ }
+
+ private static function generate_in_query_sql( $values ) {
+ global $wpdb;
+
+ $in_query = array();
+
+ foreach ( $values as $value ) {
+ $in_query[] = $wpdb->prepare( "'%s'", $value ); // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.QuotedSimplePlaceholder
+ }
+
+ return '(' . implode( ',', $in_query ) . ')';
+ }
+
+ public static function query( $args ) {
+ global $wpdb;
+
+ $query = self::build_query( $args );
+
+ Package::extended_log( sprintf( 'Building new query: %s', wc_print_r( $args, true ) ) );
+ Package::extended_log( $query );
+
+ return $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ }
+
+ public static function cancel( $id ) {
+ $data = Package::get_report_data( $id );
+ $generator = new AsyncReportGenerator( $data['type'], $data );
+ $queue = self::get_queue();
+ $running = self::get_reports_running();
+
+ if ( self::is_running( $id ) ) {
+ $running = array_diff( $running, array( $id ) );
+ Package::log( sprintf( 'Cancelled %s', Package::get_report_title( $id ) ) );
+
+ update_option( 'oss_woocommerce_reports_running', $running, false );
+ self::clear_cache();
+ $generator->delete();
+ }
+
+ /**
+ * Cancel outstanding events and queue new.
+ */
+ $queue->cancel_all( 'oss_woocommerce_' . $id );
+ }
+
+ public static function get_queue() {
+ return function_exists( 'WC' ) ? WC()->queue() : false;
+ }
+
+ public static function is_running( $id ) {
+ $running = self::get_reports_running();
+
+ if ( in_array( $id, $running, true ) && self::get_queue()->get_next( 'oss_woocommerce_' . $id ) ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public static function next( $type, $args ) {
+ /**
+ * Older versions didn't include refunds as separate orders
+ */
+ if ( ! isset( $args['order_types'] ) ) {
+ $args['order_types'] = array( 'shop_order' );
+ }
+
+ $generator = new AsyncReportGenerator( $type, $args );
+ $result = $generator->next();
+ $is_empty = false;
+ $queue = self::get_queue();
+
+ if ( is_wp_error( $result ) ) {
+ $is_empty = $result->get_error_message( 'empty' );
+ }
+
+ if ( ! $is_empty ) {
+ $new_args = $generator->get_args();
+
+ // Increase offset
+ $new_args['offset'] = (int) $new_args['offset'] + (int) $new_args['limit'];
+
+ $queue->cancel_all( 'oss_woocommerce_' . $generator->get_id() );
+
+ Package::extended_log( sprintf( 'Starting new queue: %s', wc_print_r( $new_args, true ) ) );
+
+ $queue->schedule_single(
+ time() + 10,
+ 'oss_woocommerce_' . $generator->get_id(),
+ array( 'args' => $new_args ),
+ 'oss_woocommerce'
+ );
+ } else {
+ self::complete( $generator );
+ }
+ }
+
+ /**
+ * @param AsyncReportGenerator $generator
+ */
+ public static function complete( $generator ) {
+ $queue = self::get_queue();
+ $type = $generator->get_type();
+
+ /**
+ * Cancel outstanding events.
+ */
+ $queue->cancel_all( 'oss_woocommerce_' . $generator->get_id() );
+
+ $report = $generator->complete();
+ $status = 'failed';
+
+ if ( is_a( $report, '\Vendidero\OneStopShop\Report' ) && $report->exists() ) {
+ $status = 'completed';
+ }
+
+ Package::log( sprintf( 'Completed %1$s. Status: %2$s', $report->get_title(), $status ) );
+
+ self::maybe_stop_report( $report->get_id() );
+
+ if ( 'observer' === $report->get_type() ) {
+ self::update_observer( $report );
+ }
+ }
+
+ /**
+ * @param Report $report
+ */
+ protected static function update_observer( $report ) {
+ $end = $report->get_date_end();
+ $year = $end->date( 'Y' );
+
+ if ( ! $observer_report = Package::get_observer_report( $year ) ) {
+ $observer_report = $report;
+ } else {
+ $observer_report->set_net_total( $observer_report->get_net_total( false ) + $report->get_net_total( false ) );
+ $observer_report->set_tax_total( $observer_report->get_tax_total( false ) + $report->get_tax_total( false ) );
+
+ foreach ( $report->get_countries() as $country ) {
+ foreach ( $report->get_tax_rates_by_country( $country ) as $tax_rate ) {
+ $observer_report->set_country_tax_total( $country, $tax_rate, ( $observer_report->get_country_tax_total( $country, $tax_rate, false ) + $report->get_country_tax_total( $country, $tax_rate, false ) ) );
+ $observer_report->set_country_net_total( $country, $tax_rate, ( $observer_report->get_country_net_total( $country, $tax_rate, false ) + $report->get_country_net_total( $country, $tax_rate, false ) ) );
+ }
+ }
+
+ // Delete the old observer report
+ $observer_report->delete();
+ }
+
+ // Delete the tmp report
+ $report->delete();
+
+ $observer_report->set_date_requested( $report->get_date_requested() );
+
+ // Use the last report date as new end date
+ $observer_report->set_date_end( $report->get_date_end() );
+ $observer_report->save();
+
+ update_option( 'oss_woocommerce_observer_report_' . $year, $observer_report->get_id(), false );
+
+ do_action( 'oss_woocommerce_updated_observer', $observer_report );
+ }
+
+ /**
+ * @return false|Report
+ */
+ public static function get_running_observer() {
+ $report = false;
+
+ foreach ( self::get_reports_running() as $id ) {
+ /**
+ * Make sure to return the last running observer in case more of one observer exists
+ * in running queue.
+ */
+ if ( strstr( $id, 'observer_' ) ) {
+ $report = Package::get_report( $id );
+ }
+ }
+
+ return $report;
+ }
+
+ public static function maybe_stop_report( $report_id ) {
+ $reports_running = self::get_reports_running();
+
+ if ( in_array( $report_id, $reports_running, true ) ) {
+ $reports_running = array_diff( $reports_running, array( $report_id ) );
+ update_option( 'oss_woocommerce_reports_running', $reports_running, false );
+
+ if ( $queue = self::get_queue() ) {
+ $queue->cancel_all( 'oss_woocommerce_' . $report_id );
+ }
+
+ /**
+ * Force non-cached running option
+ */
+ wp_cache_delete( 'oss_woocommerce_reports_running', 'options' );
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public static function get_reports_running() {
+ return (array) get_option( 'oss_woocommerce_reports_running', array() );
+ }
+
+ public static function get_timeframe( $type, $date = null, $date_end = null ) {
+ $date_start = null;
+ $date_end = is_null( $date_end ) ? null : $date_end;
+ $start_indicator = is_null( $date ) ? new \WC_DateTime() : $date;
+
+ if ( ! is_a( $start_indicator, 'WC_DateTime' ) && is_numeric( $start_indicator ) ) {
+ $start_indicator = new \WC_DateTime( '@' . $start_indicator );
+ }
+
+ if ( ! is_null( $date_end ) && ! is_a( $date_end, 'WC_DateTime' ) && is_numeric( $date_end ) ) {
+ $date_end = new \WC_DateTime( '@' . $date_end );
+ }
+
+ if ( 'quarterly' === $type ) {
+ $month = $start_indicator->date( 'n' );
+ $quarter = (int) ceil( $month / 3 );
+ $start_month = 'Jan';
+ $end_month = 'Mar';
+
+ if ( 2 === $quarter ) {
+ $start_month = 'Apr';
+ $end_month = 'Jun';
+ } elseif ( 3 === $quarter ) {
+ $start_month = 'Jul';
+ $end_month = 'Sep';
+ } elseif ( 4 === $quarter ) {
+ $start_month = 'Oct';
+ $end_month = 'Dec';
+ }
+
+ $date_start = new \WC_DateTime( 'first day of ' . $start_month . ' ' . $start_indicator->format( 'Y' ) . ' midnight' );
+ $date_end = new \WC_DateTime( 'last day of ' . $end_month . ' ' . $start_indicator->format( 'Y' ) . ' midnight' );
+ } elseif ( 'monthly' === $type ) {
+ $month = $start_indicator->format( 'M' );
+
+ $date_start = new \WC_DateTime( 'first day of ' . $month . ' ' . $start_indicator->format( 'Y' ) . ' midnight' );
+ $date_end = new \WC_DateTime( 'last day of ' . $month . ' ' . $start_indicator->format( 'Y' ) . ' midnight' );
+ } elseif ( 'yearly' === $type ) {
+ $date_end = clone $start_indicator;
+ $date_start = clone $start_indicator;
+
+ $date_end->modify( 'last day of dec ' . $start_indicator->format( 'Y' ) . ' midnight' );
+ $date_start->modify( 'first day of jan ' . $start_indicator->format( 'Y' ) . ' midnight' );
+ } elseif ( 'observer' === $type ) {
+ $date_start = clone $start_indicator;
+ $report = Package::get_observer_report( $date_start->format( 'Y' ) );
+
+ if ( ! $report ) {
+ // Calculate starting with the first day of the current year until yesterday
+ $date_end = clone $date_start;
+ $date_start = new \WC_DateTime( 'first day of jan ' . $start_indicator->format( 'Y' ) . ' midnight' );
+ } else {
+ // In case a report has already been generated lets do only calculate the timeframe between the end of the last report and now
+ $date_end = clone $date_start;
+ $date_end->setTime( 0, 0 );
+
+ $date_start = clone $report->get_date_end();
+ $date_start->modify( '+1 day' );
+
+ if ( $date_start > $date_end ) {
+ $date_start = clone $date_end;
+ }
+ }
+ } else {
+ if ( is_null( $date_end ) ) {
+ $date_end = clone $start_indicator;
+ $date_end->modify( '-1 year' );
+ }
+
+ $date_start = clone $start_indicator;
+ }
+
+ /**
+ * Always set start and end time to midnight
+ */
+ if ( $date_start ) {
+ $date_start->setTime( 0, 0 );
+ }
+
+ if ( $date_end ) {
+ $date_end->setTime( 0, 0 );
+ }
+
+ return array(
+ 'start' => $date_start,
+ 'end' => $date_end,
+ );
+ }
+}
diff --git a/packages/one-stop-shop-woocommerce/src/Report.php b/packages/one-stop-shop-woocommerce/src/Report.php
new file mode 100644
index 000000000..dcf3519f0
--- /dev/null
+++ b/packages/one-stop-shop-woocommerce/src/Report.php
@@ -0,0 +1,330 @@
+set_id( $id );
+
+ if ( empty( $args ) ) {
+ $args = (array) get_option( $this->id . '_result', array() );
+ }
+
+ $args = wp_parse_args(
+ $args,
+ array(
+ 'countries' => array(),
+ 'totals' => array(),
+ 'meta' => array(),
+ )
+ );
+
+ $args['totals'] = wp_parse_args(
+ $args['totals'],
+ array(
+ 'net_total' => 0,
+ 'tax_total' => 0,
+ )
+ );
+
+ $args['meta'] = wp_parse_args(
+ $args['meta'],
+ array(
+ 'date_requested' => null,
+ 'status' => 'pending',
+ 'version' => '',
+ )
+ );
+
+ $this->set_date_requested( $args['meta']['date_requested'] );
+ $this->set_status( $args['meta']['status'] );
+ $this->set_version( $args['meta']['version'] );
+
+ $this->args = $args;
+ }
+
+ public function exists() {
+ return get_option( $this->id . '_result', false );
+ }
+
+ public function get_title() {
+ $title = Package::get_report_title( $this->get_id() );
+
+ if ( $this->get_date_requested() ) {
+ $title = $title . ' @ ' . $this->get_date_requested()->date_i18n();
+ }
+
+ return $title;
+ }
+
+ public function get_url() {
+ return admin_url( 'admin.php?page=oss-reports&report=' . $this->get_id() );
+ }
+
+ public function get_type() {
+ return $this->type;
+ }
+
+ public function set_type( $type ) {
+ $this->set_id_part( $type, 'type' );
+ }
+
+ public function set_id( $id ) {
+ $this->id = $id;
+ $data = Package::get_report_data( $this->id );
+ $this->type = $data['type'];
+ $this->date_start = $data['date_start'];
+ $this->date_end = $data['date_end'];
+ }
+
+ public function set_id_part( $value, $part = 'type' ) {
+ $data = Package::get_report_data( $this->id );
+ $data[ $part ] = $value;
+
+ $this->set_id( Package::get_report_id( $data ) );
+ }
+
+ public function get_id() {
+ return $this->id;
+ }
+
+ public function get_date_start() {
+ return $this->date_start;
+ }
+
+ public function set_date_start( $date ) {
+ $date = Package::string_to_datetime( $date );
+
+ $this->set_id_part( $date->format( 'Y-m-d' ), 'date_start' );
+ }
+
+ public function get_date_end() {
+ return $this->date_end;
+ }
+
+ public function set_date_end( $date ) {
+ $date = Package::string_to_datetime( $date );
+
+ $this->set_id_part( $date->format( 'Y-m-d' ), 'date_end' );
+ }
+
+ public function get_status() {
+ return $this->args['meta']['status'];
+ }
+
+ public function get_version() {
+ return $this->args['meta']['version'];
+ }
+
+ public function set_status( $status ) {
+ $this->args['meta']['status'] = $status;
+ }
+
+ public function set_version( $version ) {
+ $this->args['meta']['version'] = $version;
+ }
+
+ public function get_date_requested() {
+ return is_null( $this->args['meta']['date_requested'] ) ? null : Package::string_to_datetime( $this->args['meta']['date_requested'] );
+ }
+
+ public function set_date_requested( $date ) {
+ if ( ! empty( $date ) ) {
+ $date = Package::string_to_datetime( $date );
+ }
+
+ $this->args['meta']['date_requested'] = is_a( $date, 'WC_DateTime' ) ? $date->date( 'Y-m-d' ) : null;
+ }
+
+ public function get_tax_total( $round = true ) {
+ return $this->maybe_round( $this->args['totals']['tax_total'], $round );
+ }
+
+ public function get_net_total( $round = true ) {
+ return $this->maybe_round( $this->args['totals']['net_total'], $round );
+ }
+
+ public function set_tax_total( $total ) {
+ $this->args['totals']['tax_total'] = wc_format_decimal( floatval( $total ) );
+ }
+
+ public function set_net_total( $total ) {
+ $this->args['totals']['net_total'] = wc_format_decimal( floatval( $total ) );
+ }
+
+ public function get_countries() {
+ return array_keys( $this->args['countries'] );
+ }
+
+ public function reset() {
+ $this->args['countries'] = array();
+
+ $this->set_net_total( 0 );
+ $this->set_tax_total( 0 );
+ $this->set_date_requested( new \WC_DateTime() );
+ $this->set_status( 'pending' );
+ $this->set_version( Package::get_version() );
+
+ delete_option( $this->id . '_tmp_result' );
+ }
+
+ public function get_tax_rates_by_country( $country ) {
+ $tax_rates = array();
+
+ if ( array_key_exists( $country, $this->args['countries'] ) ) {
+ $tax_rates = array_keys( $this->args['countries'][ $country ] );
+ }
+
+ return $tax_rates;
+ }
+
+ public function get_country_tax_total( $country, $tax_rate, $round = true ) {
+ $tax_total = 0;
+
+ if ( isset( $this->args['countries'][ $country ], $this->args['countries'][ $country ][ "$tax_rate" ] ) ) {
+ $tax_total = $this->args['countries'][ $country ][ "$tax_rate" ]['tax_total'];
+ }
+
+ return $this->maybe_round( $tax_total, $round );
+ }
+
+ protected function maybe_round( $total, $round = true ) {
+ $decimals = is_numeric( $round ) ? (int) $round : '';
+
+ return (float) wc_format_decimal( $total, $round ? $decimals : false );
+ }
+
+ public function get_country_net_total( $country, $tax_rate, $round = true ) {
+ $net_total = 0;
+
+ if ( isset( $this->args['countries'][ $country ], $this->args['countries'][ $country ][ "$tax_rate" ] ) ) {
+ $net_total = $this->args['countries'][ $country ][ "$tax_rate" ]['net_total'];
+ }
+
+ return $this->maybe_round( $net_total, $round );
+ }
+
+ public function set_country_tax_total( $country, $tax_rate, $tax_total = 0 ) {
+ if ( ! isset( $this->args['countries'][ $country ] ) ) {
+ $this->args['countries'][ $country ] = array();
+ }
+
+ if ( ! isset( $this->args['countries'][ $country ][ "$tax_rate" ] ) ) {
+ $this->args['countries'][ $country ][ "$tax_rate" ] = array(
+ 'net_total' => 0,
+ 'tax_total' => 0,
+ );
+ }
+
+ $this->args['countries'][ $country ][ "$tax_rate" ]['tax_total'] = $tax_total;
+ }
+
+ public function set_country_net_total( $country, $tax_rate, $net_total = 0 ) {
+ if ( ! isset( $this->args['countries'][ $country ] ) ) {
+ $this->args['countries'][ $country ] = array();
+ }
+
+ if ( ! isset( $this->args['countries'][ $country ][ "$tax_rate" ] ) ) {
+ $this->args['countries'][ $country ][ "$tax_rate" ] = array(
+ 'net_total' => 0,
+ 'tax_total' => 0,
+ );
+ }
+
+ $this->args['countries'][ $country ][ "$tax_rate" ]['net_total'] = $net_total;
+ }
+
+ public function save() {
+ update_option( $this->id . '_result', $this->args, false );
+
+ $reports_available = Package::get_report_ids();
+
+ if ( ! in_array( $this->get_id(), $reports_available[ $this->get_type() ], true ) ) {
+ // Add new report to start of the list
+ array_unshift( $reports_available[ $this->get_type() ], $this->get_id() );
+ update_option( 'oss_woocommerce_reports', $reports_available, false );
+ }
+
+ delete_option( $this->id . '_tmp_result' );
+
+ Package::clear_caches();
+
+ return $this->id;
+ }
+
+ public function delete() {
+ delete_option( $this->id . '_result' );
+ delete_option( $this->id . '_tmp_result' );
+
+ Queue::maybe_stop_report( $this->get_id() );
+ Package::remove_report( $this );
+
+ if ( 'observer' === $this->get_type() ) {
+ delete_option( 'oss_woocommerce_observer_report_' . $this->get_date_start()->format( 'Y' ) );
+ }
+
+ Package::clear_caches();
+
+ return true;
+ }
+
+ public function get_export_link( $export_type = '' ) {
+ return add_query_arg(
+ array(
+ 'action' => 'oss_export_report',
+ 'export_type' => $export_type,
+ 'report_id' => $this->get_id(),
+ ),
+ wp_nonce_url( admin_url( 'admin-post.php' ), 'oss_export_report' )
+ );
+ }
+
+ public function get_delete_link() {
+ return add_query_arg(
+ array(
+ 'action' => 'oss_delete_report',
+ 'report_id' => $this->get_id(),
+ ),
+ wp_nonce_url( admin_url( 'admin-post.php' ), 'oss_delete_report' )
+ );
+ }
+
+ public function get_refresh_link() {
+ return add_query_arg(
+ array(
+ 'action' => 'oss_refresh_report',
+ 'report_id' => $this->get_id(),
+ ),
+ wp_nonce_url( admin_url( 'admin-post.php' ), 'oss_refresh_report' )
+ );
+ }
+
+ public function get_cancel_link() {
+ return add_query_arg(
+ array(
+ 'action' => 'oss_cancel_report',
+ 'report_id' => $this->get_id(),
+ ),
+ wp_nonce_url( admin_url( 'admin-post.php' ), 'oss_cancel_report' )
+ );
+ }
+}
diff --git a/packages/one-stop-shop-woocommerce/src/ReportTable.php b/packages/one-stop-shop-woocommerce/src/ReportTable.php
new file mode 100644
index 000000000..fa136fb37
--- /dev/null
+++ b/packages/one-stop-shop-woocommerce/src/ReportTable.php
@@ -0,0 +1,520 @@
+ _x( 'Reports', 'oss', 'woocommerce-germanized' ),
+ 'singular' => _x( 'Report', 'oss', 'woocommerce-germanized' ),
+ 'screen' => isset( $args['screen'] ) ? $args['screen'] : null,
+ )
+ );
+ }
+
+ public function set_default_hidden_columns( $columns, $screen ) {
+ if ( $this->screen->id === $screen->id ) {
+ $columns = array_merge( $columns, $this->get_default_hidden_columns() );
+ }
+
+ return $columns;
+ }
+
+ protected function get_default_hidden_columns() {
+ return array();
+ }
+
+ protected function get_hook_prefix() {
+ return 'oss_woocommerce_admin_reports_table_';
+ }
+
+ public function enable_query_removing( $args ) {
+ $args = array_merge(
+ $args,
+ array(
+ 'changed',
+ 'bulk_action',
+ )
+ );
+
+ return $args;
+ }
+
+ /**
+ * Handle bulk actions.
+ *
+ * @param string $redirect_to URL to redirect to.
+ * @param string $action Action name.
+ * @param array $ids List of ids.
+ * @return string
+ */
+ public function handle_bulk_actions( $action, $ids, $redirect_to ) {
+ $ids = array_reverse( wc_clean( $ids ) );
+ $changed = 0;
+
+ if ( 'delete' === $action ) {
+ foreach ( $ids as $id ) {
+ if ( $report = Package::get_report( $id ) ) {
+ if ( $report->delete() ) {
+ $changed++;
+ }
+ }
+ }
+ }
+
+ $changed = apply_filters( "{$this->get_hook_prefix()}bulk_action", $changed, $action, $ids, $redirect_to, $this );
+
+ if ( $changed ) {
+ $redirect_to = add_query_arg(
+ array(
+ 'changed' => $changed,
+ 'ids' => join( ',', $ids ),
+ 'bulk_action' => $action,
+ ),
+ $redirect_to
+ );
+ }
+
+ return esc_url_raw( $redirect_to );
+ }
+
+ public function output_notices() {
+
+ }
+
+ /**
+ * Show confirmation message that order status changed for number of orders.
+ */
+ public function set_bulk_notice() {
+ $number = isset( $_REQUEST['changed'] ) ? absint( $_REQUEST['changed'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $bulk_action = isset( $_REQUEST['bulk_action'] ) ? wc_clean( wp_unslash( $_REQUEST['bulk_action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+
+ if ( 'delete' === $bulk_action ) {
+ $this->add_notice( sprintf( _nx( '%d report deleted.', '%d reports deleted.', $number, 'oss', 'woocommerce-germanized' ), number_format_i18n( $number ) ) );
+ }
+
+ do_action( "{$this->get_hook_prefix()}bulk_notice", $bulk_action, $this );
+ }
+
+ public function add_notice( $message, $type = 'success' ) {
+
+ }
+
+ /**
+ * @return bool
+ */
+ public function ajax_user_can() {
+ return current_user_can( 'manage_woocommerce' );
+ }
+
+ public function get_page_option() {
+ return 'woocommerce_page_oss_reports_per_page';
+ }
+
+ public function get_reports( $args ) {
+ return Package::get_reports( $args );
+ }
+
+ /**
+ * @global array $avail_post_stati
+ * @global WP_Query $wp_query
+ * @global int $per_page
+ * @global string $mode
+ */
+ public function prepare_items() {
+ global $per_page;
+
+ $per_page = $this->get_items_per_page( $this->get_page_option(), 10 ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+ $per_page = apply_filters( "{$this->get_hook_prefix()}edit_per_page", $per_page ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+ $this->counts = Package::get_report_counts();
+ $paged = $this->get_pagenum();
+ $report_type = isset( $_REQUEST['type'] ) ? wc_clean( wp_unslash( $_REQUEST['type'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $report_type = in_array( $report_type, array_keys( Package::get_available_report_types( true ) ), true ) ? $report_type : '';
+
+ $args = array(
+ 'limit' => $per_page,
+ 'paginate' => true,
+ 'offset' => ( $paged - 1 ) * $per_page,
+ 'count_total' => true,
+ 'type' => $report_type,
+ 'include_observer' => 'observer' === $report_type ? true : false,
+ );
+
+ $this->items = $this->get_reports( $args );
+
+ $this->set_pagination_args(
+ array(
+ 'total_items' => empty( $args['type'] ) ? array_sum( $this->counts ) : $this->counts[ $args['type'] ],
+ 'per_page' => $per_page,
+ )
+ );
+ }
+
+ /**
+ */
+ public function no_items() {
+ echo esc_html_x( 'No reports found', 'oss', 'woocommerce-germanized' );
+ }
+
+ /**
+ * Determine if the current view is the "All" view.
+ *
+ * @since 4.2.0
+ *
+ * @return bool Whether the current view is the "All" view.
+ */
+ protected function is_base_request() {
+ $vars = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ unset( $vars['paged'] );
+
+ if ( empty( $vars ) ) {
+ return true;
+ }
+
+ return 1 === count( $vars );
+ }
+
+ /**
+ * @global array $locked_post_status This seems to be deprecated.
+ * @global array $avail_post_stati
+ * @return array
+ */
+ protected function get_views() {
+ $type_links = array();
+ $num_reports = $this->counts;
+ $total_reports = array_sum( (array) $num_reports );
+ $total_reports = $total_reports - ( isset( $num_reports['observer'] ) ? $num_reports['observer'] : 0 );
+ $class = '';
+ $all_args = array();
+ $include_observers = Package::enable_auto_observer();
+
+ if ( empty( $class ) && ( $this->is_base_request() || isset( $_REQUEST['all_reports'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $class = 'current';
+ }
+
+ $all_inner_html = sprintf(
+ _nx(
+ 'All (%s) ',
+ 'All (%s) ',
+ $total_reports,
+ 'oss',
+ 'oss-woocommerce'
+ ),
+ number_format_i18n( $total_reports )
+ );
+
+ $type_links['all'] = $this->get_edit_link( $all_args, $all_inner_html, $class );
+
+ foreach ( Package::get_available_report_types( $include_observers ) as $type => $title ) {
+ $class = '';
+
+ if ( empty( $num_reports[ $type ] ) ) {
+ continue;
+ }
+
+ if ( isset( $_REQUEST['type'] ) && $type === $_REQUEST['type'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $class = 'current';
+ }
+
+ $type_args = array(
+ 'type' => $type,
+ );
+
+ $type_label = sprintf(
+ translate_nooped_plural( _nx_noop( $title . ' (%s) ', $title . ' (%s) ', 'oss', 'woocommerce-germanized' ), $num_reports[ $type ] ), // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralPlural,WordPress.WP.I18n.NonSingularStringLiteralSingle
+ number_format_i18n( $num_reports[ $type ] )
+ );
+
+ $type_links[ $type ] = $this->get_edit_link( $type_args, $type_label, $class );
+ }
+
+ return $type_links;
+ }
+
+ /**
+ * Helper to create links to edit.php with params.
+ *
+ * @since 4.4.0
+ *
+ * @param string[] $args Associative array of URL parameters for the link.
+ * @param string $label Link text.
+ * @param string $class Optional. Class attribute. Default empty string.
+ * @return string The formatted link string.
+ */
+ protected function get_edit_link( $args, $label, $class = '' ) {
+ $url = add_query_arg( $args, $this->get_main_page() );
+
+ $class_html = $aria_current = '';
+ if ( ! empty( $class ) ) {
+ $class_html = sprintf(
+ ' class="%s"',
+ esc_attr( $class )
+ );
+
+ if ( 'current' === $class ) {
+ $aria_current = ' aria-current="page"';
+ }
+ }
+
+ return sprintf(
+ '%s ',
+ esc_url( $url ),
+ $class_html,
+ $aria_current,
+ $label
+ );
+ }
+
+ /**
+ * @return string
+ */
+ public function current_action() {
+ if ( isset( $_REQUEST['delete_all'] ) || isset( $_REQUEST['delete_all2'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ return 'delete_all';
+ }
+
+ return parent::current_action();
+ }
+
+ /**
+ * @param string $which
+ */
+ protected function extra_tablenav( $which ) {
+ ?>
+
+ render_filters();
+ do_action( "{$this->get_hook_prefix()}filters", $which );
+ $output = ob_get_clean();
+
+ if ( ! empty( $output ) ) {
+ echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+
+ submit_button( _x( 'Filter', 'oss', 'woocommerce-germanized' ), '', 'filter_action', false, array( 'id' => 'oss-filter-submit' ) );
+ }
+ }
+ ?>
+
+ ';
+ $columns['title'] = _x( 'Title', 'oss', 'woocommerce-germanized' );
+ $columns['date_start'] = _x( 'Start', 'oss', 'woocommerce-germanized' );
+ $columns['date_end'] = _x( 'End', 'oss', 'woocommerce-germanized' );
+ $columns['net_total'] = _x( 'Net total', 'oss', 'woocommerce-germanized' );
+ $columns['tax_total'] = _x( 'Tax total', 'oss', 'woocommerce-germanized' );
+ $columns['status'] = _x( 'Status', 'oss', 'woocommerce-germanized' );
+ $columns['actions'] = _x( 'Actions', 'oss', 'woocommerce-germanized' );
+
+ $columns = apply_filters( "{$this->get_hook_prefix()}columns", $columns );
+
+ return $columns;
+ }
+
+ /**
+ * @return array
+ */
+ protected function get_sortable_columns() {
+ return array(
+ 'date_start' => array( 'date_start', false ),
+ 'date_end' => array( 'date_end', false ),
+ );
+ }
+
+ /**
+ * Gets the name of the default primary column.
+ *
+ * @since 4.3.0
+ *
+ * @return string Name of the default primary column, in this case, 'title'.
+ */
+ protected function get_default_primary_column_name() {
+ return 'title';
+ }
+
+ /**
+ * Handles the default column output.
+ *
+ * @since 4.3.0
+ *
+ * @param Report $report The current shipment object.
+ * @param string $column_name The current column name.
+ */
+ public function column_default( $report, $column_name ) {
+ do_action( "{$this->get_hook_prefix()}custom_column", $column_name, $report );
+ }
+
+ public function get_main_page() {
+ return 'admin.php?page=oss-reports';
+ }
+
+ /**
+ * Handles actions.
+ *
+ * @since 0.0.1
+ *
+ * @param Report $report The current report object.
+ */
+ protected function column_actions( $report ) {
+ do_action( "{$this->get_hook_prefix()}actions_start", $report );
+
+ $actions = Admin::get_report_actions( $report );
+
+ Admin::render_actions( $actions );
+
+ do_action( "{$this->get_hook_prefix()}actions_end", $report );
+ }
+
+ public function column_cb( $report ) {
+ ?>
+
+ get_id() ) ); ?>
+
+
+ get_title();
+
+ echo '' . esc_html( $title ) . ' ';
+ }
+
+ /**
+ * @param Report $report
+ */
+ public function column_status( $report ) {
+ $status = $report->get_status();
+
+ return '' . esc_html( Package::get_report_status_title( $status ) ) . ' ';
+ }
+
+ /**
+ * @param Report $report
+ */
+ public function column_net_total( $report ) {
+ return wc_price( $report->get_net_total() );
+ }
+
+ /**
+ * @param Report $report
+ */
+ public function column_tax_total( $report ) {
+ return wc_price( $report->get_tax_total() );
+ }
+
+ /**
+ * Handles the post author column output.
+ *
+ * @since 4.3.0
+ *
+ * @param Report $report
+ */
+ public function column_date_start( $report ) {
+ $show_date = $report->get_date_start()->date_i18n( apply_filters( "{$this->get_hook_prefix()}date_format", wc_date_format() ) );
+
+ printf(
+ '%3$s ',
+ esc_attr( $report->get_date_start()->date( 'c' ) ),
+ esc_html( $report->get_date_start()->date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ) ),
+ esc_html( $show_date )
+ );
+ }
+
+ /**
+ * Handles the post author column output.
+ *
+ * @since 4.3.0
+ *
+ * @param Report $report
+ */
+ public function column_date_end( $report ) {
+ $show_date = $report->get_date_end()->date_i18n( apply_filters( "{$this->get_hook_prefix()}date_format", wc_date_format() ) );
+
+ printf(
+ '%3$s ',
+ esc_attr( $report->get_date_end()->date( 'c' ) ),
+ esc_html( $report->get_date_end()->date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ) ),
+ esc_html( $show_date )
+ );
+ }
+
+ /**
+ *
+ * @param Report $report
+ */
+ public function single_row( $report ) {
+ $GLOBALS['report'] = $report;
+ $classes = 'report report-' . $report->get_type();
+ ?>
+
+ single_row_columns( $report ); ?>
+
+ get_hook_prefix()}bulk_actions", $actions );
+ }
+}
diff --git a/packages/one-stop-shop-woocommerce/src/Settings.php b/packages/one-stop-shop-woocommerce/src/Settings.php
new file mode 100644
index 000000000..f983c39e4
--- /dev/null
+++ b/packages/one-stop-shop-woocommerce/src/Settings.php
@@ -0,0 +1,203 @@
+ _x( 'General', 'oss', 'woocommerce-germanized' ),
+ );
+ }
+
+ public static function get_description() {
+ return sprintf( _x( 'Find useful options regarding the One Stop Shop procedure here.', 'oss', 'woocommerce-germanized' ) );
+ }
+
+ public static function get_help_url() {
+ return 'https://vendidero.github.io/one-stop-shop-woocommerce/';
+ }
+
+ public static function get_settings( $current_section = '' ) {
+ $settings = array(
+ array(
+ 'title' => '',
+ 'type' => 'title',
+ 'id' => 'oss_options',
+ 'desc' => Package::is_integration() ? '' : self::get_description(),
+ ),
+
+ array(
+ 'title' => _x( 'OSS status', 'oss', 'woocommerce-germanized' ),
+ 'desc' => _x( 'Yes, I\'m currently participating in the OSS procedure.', 'oss', 'woocommerce-germanized' ),
+ 'id' => 'oss_use_oss_procedure',
+ 'type' => Package::is_integration() ? 'gzd_toggle' : 'checkbox',
+ 'default' => 'no',
+ ),
+
+ array(
+ 'title' => _x( 'Observation', 'oss', 'woocommerce-germanized' ),
+ 'desc' => _x( 'Automatically observe the delivery threshold of the current year.', 'oss', 'woocommerce-germanized' ) . '' . _x( 'This option will automatically calculate the amount applicable for the OSS procedure delivery threshold once per day for the current year. The report will only recalculated for the days which are not yet subject to the observation to save processing time.', 'oss', 'woocommerce-germanized' ) . '
',
+ 'id' => 'oss_enable_auto_observation',
+ 'type' => Package::is_integration() ? 'gzd_toggle' : 'checkbox',
+ 'default' => 'yes',
+ ),
+ );
+
+ if ( Package::enable_auto_observer() ) {
+ $settings = array_merge(
+ $settings,
+ array(
+ array(
+ 'title' => sprintf( _x( 'Delivery threshold', 'oss', 'woocommerce-germanized' ) ),
+ 'id' => 'oss_delivery_threshold',
+ 'type' => 'html',
+ 'html' => self::get_observer_report_html(),
+ ),
+ )
+ );
+ }
+
+ $settings = array_merge(
+ $settings,
+ array(
+ array(
+ 'title' => _x( 'Participation', 'oss', 'woocommerce-germanized' ),
+ 'id' => 'oss_switch',
+ 'type' => 'html',
+ 'html' => self::get_oss_switch_html(),
+ ),
+
+ array(
+ 'title' => _x( 'Report Order Date', 'oss', 'woocommerce-germanized' ),
+ 'desc' => '' . _x( 'Select the relevant order date to be used to determine whether to include an order in a report.', 'oss', 'woocommerce-germanized' ) . '
',
+ 'id' => 'oss_report_date_type',
+ 'type' => 'select',
+ 'default' => 'date_paid',
+ 'options' => array(
+ 'date_paid' => _x( 'Date paid', 'oss', 'woocommerce-germanized' ),
+ 'date_created' => _x( 'Date created', 'oss', 'woocommerce-germanized' ),
+ ),
+ ),
+ )
+ );
+
+ if ( Helper::oss_procedure_is_enabled() && wc_prices_include_tax() ) {
+ $settings = array_merge(
+ $settings,
+ array(
+ array(
+ 'title' => _x( 'Fixed gross prices', 'oss', 'woocommerce-germanized' ),
+ 'desc' => _x( 'Apply the same gross price regardless of the tax rate for EU countries.', 'oss', 'woocommerce-germanized' ) . '' . _x( 'This option will make sure that your customers pay the same price no matter the tax rate (based on the country chosen) to be applied.', 'oss', 'woocommerce-germanized' ) . '
',
+ 'id' => 'oss_fixed_gross_prices',
+ 'type' => Package::is_integration() ? 'gzd_toggle' : 'checkbox',
+ 'default' => 'yes',
+ ),
+ array(
+ 'title' => _x( 'Third countries', 'oss', 'woocommerce-germanized' ),
+ 'desc' => _x( 'Apply the same gross price for third countries too.', 'oss', 'woocommerce-germanized' ),
+ 'id' => 'oss_fixed_gross_prices_for_third_countries',
+ 'type' => Package::is_integration() ? 'gzd_toggle' : 'checkbox',
+ 'default' => 'no',
+ 'custom_attributes' => array(
+ 'data-show_if_oss_fixed_gross_prices' => '',
+ ),
+ ),
+ )
+ );
+ }
+
+ $settings = array_merge(
+ $settings,
+ array(
+ array(
+ 'type' => 'sectionend',
+ 'id' => 'oss_options',
+ ),
+ )
+ );
+
+ return $settings;
+ }
+
+ public static function get_oss_switch_link() {
+ return add_query_arg( array( 'action' => 'oss_switch_procedure' ), wp_nonce_url( admin_url( 'admin-post.php' ), 'oss_switch_procedure' ) );
+ }
+
+ protected static function get_oss_switch_html() {
+ ob_start();
+ ?>
+
+
+
+
+
+
+ get_url() ) . '">' . esc_html_x( 'See status', 'oss', 'woocommerce-germanized' ) . '