PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.0
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.0
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / includes / Import / SlicedImporter.php

SlicedImporter.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.0, at includes/Import/SlicedImporter.php

282 lines 10.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Import from Sliced Invoices.
4 *
5 * @package Easy_Invoice
6 * @subpackage Import
7 */
8
9 namespace EasyInvoice\Import;
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 /**
16 * Sliced keeps quotes and invoices as `sliced_quote` / `sliced_invoice` posts,
17 * status as a taxonomy term, line items and payments as arrays of arrays in
18 * post meta, and clients as WordPress users with `_sliced_client_*` meta. All
19 * of it is readable from here whether or not Sliced is still active; only
20 * the posts need to exist.
21 *
22 * Read from Sliced 3.9's model (`Sliced_Shared::get_totals()`): items live in
23 * `_sliced_items[0]`, each with qty / title / description / amount / tax
24 * (which is the per-line "adjust %", not tax) / taxable ('on'); payments in
25 * `_sliced_payment[0]` with amount / date / gateway / status ('success' means
26 * completed). The document-level rate is `_sliced_tax`; the discount
27 * `_sliced_discount` with `_sliced_discount_type` 'amount' or 'percentage'.
28 */
29 class SlicedImporter extends Importer {
30
31 public function source(): string {
32 return 'sliced';
33 }
34
35 public function label(): string {
36 return __( 'Sliced Invoices', 'easy-invoice' );
37 }
38
39 public function available(): bool {
40 return $this->count( 'sliced_invoice' ) + $this->count( 'sliced_quote' ) > 0;
41 }
42
43 public function preview(): array {
44 $invoices = $this->ids( 'sliced_invoice' );
45 $quotes = $this->ids( 'sliced_quote' );
46 $already = 0;
47 foreach ( $invoices as $id ) {
48 if ( $this->alreadyImported( \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE, (string) $id ) ) {
49 $already++;
50 }
51 }
52 foreach ( $quotes as $id ) {
53 if ( $this->alreadyImported( \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE, (string) $id ) ) {
54 $already++;
55 }
56 }
57 $clients = [];
58 $payments = 0;
59 foreach ( array_merge( $invoices, $quotes ) as $id ) {
60 $c = (int) get_post_meta( $id, '_sliced_client', true );
61 if ( $c > 0 ) {
62 $clients[ $c ] = true;
63 }
64 }
65 foreach ( $invoices as $id ) {
66 $payments += count( $this->payments( $id ) );
67 }
68
69 return [
70 'clients' => count( $clients ),
71 'invoices' => count( $invoices ),
72 'quotes' => count( $quotes ),
73 'payments' => $payments,
74 'already' => $already,
75 ];
76 }
77
78 public function run(): array {
79 foreach ( $this->ids( 'sliced_quote' ) as $id ) {
80 $this->importQuote( $id );
81 }
82 foreach ( $this->ids( 'sliced_invoice' ) as $id ) {
83 $this->importInvoice( $id );
84 }
85 return $this->report();
86 }
87
88 /**
89 * One Sliced invoice.
90 *
91 * @param int $id Sliced post id.
92 * @return void
93 */
94 private function importInvoice( int $id ): void {
95 $d = $this->document( $id, 'invoice' );
96 $d['status'] = $this->invoiceStatus( $id );
97 $d['due_date'] = $this->toDate( get_post_meta( $id, '_sliced_invoice_due', true ) );
98
99 $invoice_id = $this->createInvoice( $d );
100 if ( $invoice_id <= 0 ) {
101 return;
102 }
103 foreach ( $this->payments( $id ) as $i => $p ) {
104 $status = (string) ( $p['status'] ?? '' );
105 $this->createPayment( $invoice_id, [
106 'source_id' => $id . ':' . $i,
107 'amount' => $this->toNumber( $p['amount'] ?? 0 ),
108 'date' => $this->toDateTime( $p['date'] ?? '' ) ?: get_post_field( 'post_date', $id ),
109 'method' => (string) ( $p['gateway'] ?? 'imported' ),
110 'transaction_id' => (string) ( $p['payment_id'] ?? '' ),
111 'status' => 'success' === $status ? 'completed' : ( 'refunded' === $status ? 'refunded' : 'pending' ),
112 'notes' => (string) ( $p['memo'] ?? '' ),
113 ] );
114 }
115 }
116
117 /**
118 * One Sliced quote.
119 *
120 * @param int $id Sliced post id.
121 * @return void
122 */
123 private function importQuote( int $id ): void {
124 $d = $this->document( $id, 'quote' );
125 $d['status'] = $this->quoteStatus( $id );
126 $d['expiry_date'] = $this->toDate( get_post_meta( $id, '_sliced_quote_valid_until', true ) );
127 $this->createQuote( $d );
128 }
129
130 /**
131 * The fields both types share, normalised.
132 *
133 * @param int $id Sliced post id.
134 * @param string $kind 'invoice' or 'quote'.
135 * @return array
136 */
137 private function document( int $id, string $kind ): array {
138 $post = get_post( $id );
139 $src_user = get_userdata( (int) get_post_meta( $id, '_sliced_client', true ) ) ?: null;
140 $client_id = $this->client( (int) get_post_meta( $id, '_sliced_client', true ) );
141 $created = $this->toDateTime( get_post_meta( $id, '_sliced_' . $kind . '_created', true ) ) ?: $post->post_date;
142
143 $items = [];
144 $raw = get_post_meta( $id, '_sliced_items', true );
145 // Normally [ [ item, item, … ] ]; older sites stored the list directly.
146 $rows = [];
147 if ( is_array( $raw ) ) {
148 $rows = ( isset( $raw[0] ) && is_array( $raw[0] ) && ! isset( $raw[0]['title'] ) && ! isset( $raw[0]['amount'] ) ) ? $raw[0] : $raw;
149 }
150 foreach ( $rows as $row ) {
151 if ( ! is_array( $row ) ) {
152 continue;
153 }
154 $items[] = [
155 'name' => (string) ( $row['title'] ?? '' ),
156 'description' => wp_strip_all_tags( (string) ( $row['description'] ?? '' ) ),
157 'quantity' => $this->toNumber( $row['qty'] ?? 1 ),
158 'price' => $this->toNumber( $row['amount'] ?? 0 ),
159 'adjust_percentage' => $this->toNumber( $row['tax'] ?? 0 ),
160 'taxable' => ( $row['taxable'] ?? '' ) === 'on',
161 ];
162 }
163
164 $tax = get_post_meta( $id, '_sliced_tax', true );
165 $tax = is_array( $tax ) ? ( $tax[0] ?? 0 ) : $tax;
166
167
168 return [
169 'source_id' => (string) $id,
170 'title' => (string) $post->post_title,
171 'number' => (string) get_post_meta( $id, '_sliced_number', true ),
172 'issue_date' => $this->toDate( get_post_meta( $id, '_sliced_' . $kind . '_created', true ) ) ?: substr( $post->post_date, 0, 10 ),
173 'created' => $created,
174 'client_id' => $client_id,
175 'customer_name' => $src_user ? ( (string) get_user_meta( $src_user->ID, '_sliced_client_business', true ) ?: $src_user->display_name ) : '',
176 'customer_email' => $src_user ? $src_user->user_email : '',
177 'customer_address' => $src_user ? (string) get_user_meta( $src_user->ID, '_sliced_client_address', true ) : '',
178 'items' => $items,
179 'tax_rate' => $this->toNumber( $tax ),
180 'discount_type' => 'percentage' === get_post_meta( $id, '_sliced_discount_type', true ) ? 'percentage' : 'fixed',
181 'discount_value' => $this->toNumber( get_post_meta( $id, '_sliced_discount', true ) ),
182 'currency_code' => strtoupper( (string) get_post_meta( $id, '_sliced_currency', true ) ),
183 'notes' => wp_strip_all_tags( (string) get_post_meta( $id, '_sliced_description', true ) ),
184 'terms' => wp_strip_all_tags( (string) get_post_meta( $id, '_sliced_' . $kind . '_terms', true ) ),
185 ];
186 }
187
188 /**
189 * Map a Sliced client user to ours.
190 *
191 * @param int $user_id Sliced client user id.
192 * @return int
193 */
194 private function client( int $user_id ): int {
195 if ( $user_id <= 0 ) {
196 return 0;
197 }
198 $user = get_userdata( $user_id );
199 if ( ! $user ) {
200 return 0;
201 }
202 list( $first, $last ) = $this->splitName( $user->display_name );
203 return $this->findOrCreateClient( [
204 'source_id' => (string) $user_id,
205 'email' => $user->user_email,
206 'first_name' => $user->first_name ?: $first,
207 'last_name' => $user->last_name ?: $last,
208 'business' => (string) get_user_meta( $user_id, '_sliced_client_business', true ),
209 'address' => (string) get_user_meta( $user_id, '_sliced_client_address', true ),
210 'extra_info' => (string) get_user_meta( $user_id, '_sliced_client_extra_info', true ),
211 ] );
212 }
213
214 /**
215 * Sliced status term → ours.
216 *
217 * @param int $id Sliced invoice.
218 * @return string
219 */
220 private function invoiceStatus( int $id ): string {
221 $terms = wp_get_object_terms( $id, 'invoice_status', [ 'fields' => 'slugs' ] );
222 $slug = is_array( $terms ) && ! empty( $terms ) ? $terms[0] : 'draft';
223 $map = [ 'draft' => 'draft', 'unpaid' => 'available', 'paid' => 'paid', 'overdue' => 'available', 'cancelled' => 'cancelled' ];
224 return $map[ $slug ] ?? 'available';
225 }
226
227 /**
228 * Sliced quote status term → ours.
229 *
230 * @param int $id Sliced quote.
231 * @return string
232 */
233 private function quoteStatus( int $id ): string {
234 $terms = wp_get_object_terms( $id, 'quote_status', [ 'fields' => 'slugs' ] );
235 $slug = is_array( $terms ) && ! empty( $terms ) ? $terms[0] : 'draft';
236 $map = [ 'draft' => 'draft', 'sent' => 'sent', 'accepted' => 'accepted', 'declined' => 'declined', 'cancelled' => 'declined' ];
237 return $map[ $slug ] ?? 'available';
238 }
239
240 /**
241 * Payment rows of a Sliced invoice.
242 *
243 * @param int $id Sliced invoice.
244 * @return array
245 */
246 private function payments( int $id ): array {
247 $raw = get_post_meta( $id, '_sliced_payment', true );
248 if ( is_array( $raw ) && isset( $raw[0] ) && is_array( $raw[0] ) && ! isset( $raw[0]['amount'] ) ) {
249 $raw = $raw[0];
250 }
251 return is_array( $raw ) ? array_values( array_filter( $raw, 'is_array' ) ) : [];
252 }
253
254 /**
255 * Ids of a Sliced post type.
256 *
257 * @param string $type Post type.
258 * @return int[]
259 */
260 private function ids( string $type ): array {
261 return array_map( 'intval', get_posts( [
262 'post_type' => $type,
263 'post_status' => [ 'publish', 'draft', 'pending', 'private', 'future' ],
264 'fields' => 'ids',
265 'posts_per_page' => -1,
266 'orderby' => 'ID',
267 'order' => 'ASC',
268 ] ) );
269 }
270
271 /**
272 * How many of a Sliced post type exist.
273 *
274 * @param string $type Post type.
275 * @return int
276 */
277 private function count( string $type ): int {
278 global $wpdb;
279 return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status NOT IN ('trash','auto-draft')", $type ) );
280 }
281 }
282