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 / SproutImporter.php

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

245 lines 10.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Import from Sprout 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 * Sprout stores clients as `sa_client` posts (address as an array in
17 * `_address`, users attached through `_associated_users`), invoices as
18 * `sa_invoice`, estimates as `sa_estimate` and payments as `sa_payment`
19 * posts. Line items are `_doc_line_items`: rows of desc / rate / qty / tax
20 * (the per-line adjustment) / total. Dates are Unix timestamps. Status is
21 * the post status, with Sprout's own names: temp is a draft, publish is
22 * pending, complete is paid.
23 *
24 * Read from Sprout Invoices 20.x (`models/Invoice.php`, `Estimate.php`,
25 * `Client.php`, `Payment.php`).
26 */
27 class SproutImporter extends Importer {
28
29 public function source(): string {
30 return 'sprout';
31 }
32
33 public function label(): string {
34 return __( 'Sprout Invoices', 'easy-invoice' );
35 }
36
37 public function available(): bool {
38 return $this->count( 'sa_invoice' ) + $this->count( 'sa_estimate' ) > 0;
39 }
40
41 public function preview(): array {
42 $invoices = $this->ids( 'sa_invoice' );
43 $quotes = $this->ids( 'sa_estimate' );
44 $already = 0;
45 foreach ( $invoices as $id ) {
46 if ( $this->alreadyImported( \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE, (string) $id ) ) {
47 $already++;
48 }
49 }
50 foreach ( $quotes as $id ) {
51 if ( $this->alreadyImported( \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE, (string) $id ) ) {
52 $already++;
53 }
54 }
55 return [
56 'clients' => $this->count( 'sa_client' ),
57 'invoices' => count( $invoices ),
58 'quotes' => count( $quotes ),
59 'payments' => $this->count( 'sa_payment' ),
60 'already' => $already,
61 ];
62 }
63
64 public function run(): array {
65 foreach ( $this->ids( 'sa_estimate' ) as $id ) {
66 $d = $this->document( $id, 'estimate' );
67 $d['status'] = $this->estimateStatus( get_post_status( $id ) );
68 $d['expiry_date'] = $this->toDate( get_post_meta( $id, '_expiration_date', true ) );
69 $this->createQuote( $d );
70 }
71
72 $invoice_map = [];
73 foreach ( $this->ids( 'sa_invoice' ) as $id ) {
74 $d = $this->document( $id, 'invoice' );
75 $d['status'] = $this->invoiceStatus( get_post_status( $id ) );
76 $d['due_date'] = $this->toDate( get_post_meta( $id, '_due_date', true ) );
77 $new = $this->createInvoice( $d );
78 if ( $new > 0 ) {
79 $invoice_map[ $id ] = $new;
80 }
81 }
82
83 foreach ( $this->ids( 'sa_payment', [ 'publish', 'payment-partial', 'refunded', 'void', 'authorized' ] ) as $pid ) {
84 $inv = (int) get_post_meta( $pid, '_payment_invoice', true );
85 if ( ! isset( $invoice_map[ $inv ] ) ) {
86 $mapped = $this->alreadyImported( \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE, (string) $inv );
87 if ( $mapped <= 0 ) {
88 continue;
89 }
90 $invoice_map[ $inv ] = $mapped;
91 }
92 $status = get_post_status( $pid );
93 $this->createPayment( $invoice_map[ $inv ], [
94 'source_id' => (string) $pid,
95 'amount' => $this->toNumber( get_post_meta( $pid, '_amount', true ) ),
96 'date' => get_post_field( 'post_date', $pid ),
97 'method' => (string) get_post_meta( $pid, '_payment_method', true ) ?: 'imported',
98 'transaction_id' => (string) get_post_meta( $pid, '_trans_id', true ),
99 'status' => in_array( $status, [ 'publish', 'payment-partial' ], true ) ? 'completed' : ( 'refunded' === $status ? 'refunded' : ( 'void' === $status ? 'failed' : 'pending' ) ),
100 'notes' => (string) get_post_field( 'post_title', $pid ),
101 ] );
102 }
103
104 return $this->report();
105 }
106
107 /**
108 * Shared fields, normalised.
109 *
110 * @param int $id Sprout post id.
111 * @param string $kind 'invoice' or 'estimate'.
112 * @return array
113 */
114 private function document( int $id, string $kind ): array {
115 $post = get_post( $id );
116 $sa_client = (int) get_post_meta( $id, '_client_id', true );
117 $client_id = $this->client( $sa_client );
118 $client_post = $sa_client ? get_post( $sa_client ) : null;
119
120 $items = [];
121 foreach ( (array) get_post_meta( $id, '_doc_line_items', true ) as $row ) {
122 if ( ! is_array( $row ) ) {
123 continue;
124 }
125 $desc = wp_strip_all_tags( (string) ( $row['desc'] ?? '' ) );
126 $items[] = [
127 'name' => strtok( $desc, "\n" ) ?: __( 'Item', 'easy-invoice' ),
128 'description' => trim( (string) substr( $desc, strlen( strtok( $desc, "\n" ) ?: '' ) ) ),
129 'quantity' => $this->toNumber( $row['qty'] ?? 1 ),
130 'price' => $this->toNumber( $row['rate'] ?? 0 ),
131 'adjust_percentage' => $this->toNumber( $row['tax'] ?? 0 ),
132 'taxable' => true,
133 ];
134 }
135
136 $issue = $this->toDate( get_post_meta( $id, '_' . $kind . '_issue_date', true ) ) ?: substr( $post->post_date, 0, 10 );
137
138 return [
139 'source_id' => (string) $id,
140 'title' => (string) $post->post_title,
141 'number' => (string) get_post_meta( $id, '_' . $kind . '_id', true ) ?: (string) $id,
142 'issue_date' => $issue,
143 'created' => $post->post_date,
144 'client_id' => $client_id,
145 'customer_name' => $client_post ? $client_post->post_title : '',
146 'customer_email' => $client_id ? (string) get_userdata( $client_id )->user_email : '',
147 'customer_address' => $client_post ? $this->address( (array) get_post_meta( $client_post->ID, '_address', true ) ) : '',
148 'items' => $items,
149 'tax_rate' => $this->toNumber( get_post_meta( $id, '_doc_tax', true ) ),
150 'discount_type' => 'percentage',
151 'discount_value' => $this->toNumber( get_post_meta( $id, '_doc_discount', true ) ),
152 'currency_code' => strtoupper( (string) get_post_meta( $id, '_doc_currency', true ) ),
153 'notes' => wp_strip_all_tags( (string) get_post_meta( $id, '_' . $kind . '_notes', true ) ),
154 'terms' => wp_strip_all_tags( (string) get_post_meta( $id, '_doc_terms', true ) ),
155 ];
156 }
157
158 /**
159 * Map a Sprout client post to ours.
160 *
161 * @param int $sa_client Sprout client post id.
162 * @return int
163 */
164 private function client( int $sa_client ): int {
165 if ( $sa_client <= 0 ) {
166 return 0;
167 }
168 $post = get_post( $sa_client );
169 if ( ! $post ) {
170 return 0;
171 }
172 $email = '';
173 $first = '';
174 $last = '';
175 $users = get_post_meta( $sa_client, '_associated_users', false );
176 foreach ( (array) $users as $uid ) {
177 $u = get_userdata( (int) $uid );
178 if ( $u ) {
179 $email = $u->user_email;
180 $first = $u->first_name;
181 $last = $u->last_name;
182 break;
183 }
184 }
185 if ( '' === $first && '' === $last ) {
186 list( $first, $last ) = $this->splitName( $post->post_title );
187 }
188
189 return $this->findOrCreateClient( [
190 'source_id' => (string) $sa_client,
191 'email' => $email,
192 'first_name' => $first,
193 'last_name' => $last,
194 'business' => (string) $post->post_title,
195 'address' => $this->address( (array) get_post_meta( $sa_client, '_address', true ) ),
196 'phone' => (string) get_post_meta( $sa_client, '_phone', true ),
197 'website' => (string) get_post_meta( $sa_client, '_website', true ),
198 ] );
199 }
200
201 /**
202 * Sprout's address array as lines.
203 *
204 * @param array $a street, street_2, city, zone, postal_code, country.
205 * @return string
206 */
207 private function address( array $a ): string {
208 $lines = array_filter( [
209 trim( (string) ( $a['street'] ?? '' ) ),
210 trim( (string) ( $a['street_2'] ?? '' ) ),
211 trim( implode( ' ', array_filter( [ $a['city'] ?? '', $a['zone'] ?? '', $a['postal_code'] ?? '' ] ) ) ),
212 trim( (string) ( $a['country'] ?? '' ) ),
213 ] );
214 return implode( "\n", $lines );
215 }
216
217 private function invoiceStatus( string $status ): string {
218 $map = [ 'temp' => 'draft', 'future' => 'draft', 'publish' => 'available', 'partial' => 'available', 'complete' => 'paid', 'write-off' => 'cancelled', 'archived' => 'cancelled' ];
219 return $map[ $status ] ?? 'available';
220 }
221
222 private function estimateStatus( string $status ): string {
223 $map = [ 'temp' => 'draft', 'request' => 'available', 'publish' => 'sent', 'future' => 'draft', 'approved' => 'accepted', 'declined' => 'declined', 'archived' => 'declined' ];
224 return $map[ $status ] ?? 'available';
225 }
226
227 /**
228 * Ids of a Sprout post type.
229 *
230 * @param string $type Post type.
231 * @param string[] $statuses Post statuses to include.
232 * @return int[]
233 */
234 private function ids( string $type, array $statuses = [ 'temp', 'publish', 'partial', 'complete', 'write-off', 'archived', 'request', 'approved', 'declined', 'future' ] ): array {
235 global $wpdb;
236 $in = implode( ',', array_fill( 0, count( $statuses ), '%s' ) );
237 return array_map( 'intval', (array) $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND post_status IN ($in) ORDER BY ID ASC", array_merge( [ $type ], $statuses ) ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- placeholders list.
238 }
239
240 private function count( string $type ): int {
241 global $wpdb;
242 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 ) );
243 }
244 }
245