Passed
Push — developer ( 6b5868...bed0f9 )
by Radosław
22:42 queued 03:39
created

Invoice   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 312
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 51
eloc 179
c 0
b 0
f 0
dl 0
loc 312
rs 7.92

10 Methods

Rating   Name   Duplication   Size   Complexity  
A getCounter() 0 3 1
A getCurrencyParam() 0 25 6
A convertDate() 0 4 1
B process() 0 51 11
A convertPaymentMethods() 0 12 3
A addProduct() 0 3 1
B loadInventory() 0 24 11
A loadDeliveryAddress() 0 20 4
A importRecord() 0 26 5
B getInventory() 0 46 8

How to fix   Complexity   

Complex Class

Complex classes like Invoice often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Invoice, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * WAPRO ERP invoice synchronizer file.
5
 *
6
 * @package Integration
7
 *
8
 * @copyright YetiForce S.A.
9
 * @license   YetiForce Public License 6.5 (licenses/LicenseEN.txt or yetiforce.com)
10
 * @author    Mariusz Krzaczkowski <[email protected]>
11
 */
12
13
namespace App\Integrations\Wapro\Synchronizer;
14
15
/**
16
 * WAPRO ERP invoice synchronizer class.
17
 */
18
class Invoice extends \App\Integrations\Wapro\Synchronizer
19
{
20
	/** {@inheritdoc} */
21
	const NAME = 'LBL_INVOICE';
22
23
	/** {@inheritdoc} */
24
	const SEQUENCE = 5;
25
26
	/** @var string[] Map for payment methods with WAPRO ERP */
27
	const PAYMENT_METHODS_MAP = [
28
		'gotówka' => 'PLL_CASH',
29
		'przelew' => 'PLL_TRANSFER',
30
		'czek' => 'PLL_CHECK',
31
		'pobranie' => 'PLL_CASH_ON_DELIVERY',
32
	];
33
34
	/** {@inheritdoc} */
35
	protected $fieldMap = [
36
		'ID_FIRMY' => ['fieldName' => 'multiCompanyId', 'fn' => 'findRelationship', 'tableName' => 'FIRMA', 'skipMode' => true],
37
		'ID_KONTRAHENTA' => ['fieldName' => 'accountid', 'fn' => 'findRelationship', 'tableName' => 'KONTRAHENT', 'skipMode' => true],
38
		'FORMA_PLATNOSCI' => ['fieldName' => 'payment_methods', 'fn' => 'convertPaymentMethods'],
39
		'UWAGI' => 'description',
40
		'KONTRAHENT_NAZWA' => ['fieldName' => 'company_name_a', 'fn' => 'decode'],
41
		'issueTime' => ['fieldName' => 'issue_time', 'fn' => 'convertDate'],
42
		'saleDate' => ['fieldName' => 'saledate', 'fn' => 'convertDate'],
43
		'paymentDate' => ['fieldName' => 'paymentdate', 'fn' => 'convertDate'],
44
	];
45
46
	/** {@inheritdoc} */
47
	public function process(): int
48
	{
49
		$query = (new \App\Db\Query())->select([
50
			'ID_DOKUMENTU_HANDLOWEGO', 'ID_FIRMY', 'ID_KONTRAHENTA', 'ID_DOK_ORYGINALNEGO',
51
			'NUMER', 'FORMA_PLATNOSCI', 'UWAGI', 'KONTRAHENT_NAZWA', 'WARTOSC_NETTO', 'WARTOSC_BRUTTO', 'DOK_KOREKTY', 'DATA_KURS_WAL', 'DOK_WAL', 'SYM_WAL',
52
			'issueTime' => 'cast (dbo.DOKUMENT_HANDLOWY.DATA_WYSTAWIENIA - 36163 as datetime)',
53
			'saleDate' => 'cast (dbo.DOKUMENT_HANDLOWY.DATA_SPRZEDAZY - 36163 as datetime)',
54
			'paymentDate' => 'cast (dbo.DOKUMENT_HANDLOWY.TERMIN_PLAT - 36163 as datetime)',
55
			'currencyDate' => 'cast (dbo.DOKUMENT_HANDLOWY.DATA_KURS_WAL - 36163 as datetime)',
56
		])->from('dbo.DOKUMENT_HANDLOWY')
57
			->where(['ID_TYPU' => 1]);
58
		$pauser = \App\Pauser::getInstance('WaproInvoiceLastId');
59
		if ($val = $pauser->getValue()) {
60
			$query->andWhere(['>', 'ID_DOKUMENTU_HANDLOWEGO', $val]);
61
		}
62
		$lastId = $s = $e = $i = $u = 0;
63
		foreach ($query->batch(100, $this->controller->getDb()) as $rows) {
64
			$lastId = 0;
65
			foreach ($rows as $row) {
66
				$this->waproId = $row['ID_DOKUMENTU_HANDLOWEGO'];
67
				$this->row = $row;
68
				$this->skip = false;
69
				try {
70
					switch ($this->importRecord()) {
71
						default:
72
						case 0:
73
							++$s;
74
							break;
75
						case 1:
76
							++$u;
77
							break;
78
						case 2:
79
							++$i;
80
							break;
81
					}
82
					$lastId = $this->waproId;
83
				} catch (\Throwable $th) {
84
					$this->logError($th);
85
					++$e;
86
				}
87
			}
88
			$pauser->setValue($lastId);
89
			if ($this->controller->cron && $this->controller->cron->checkTimeout()) {
90
				break;
91
			}
92
		}
93
		if (0 == $lastId) {
94
			$pauser->destroy();
95
		}
96
		$this->log("Create {$i} | Update {$u} | Skipped {$s} | Error {$e}");
97
		return $i + $u;
98
	}
99
100
	/** {@inheritdoc} */
101
	public function importRecord(): int
102
	{
103
		if ($id = $this->findInMapTable($this->waproId, 'DOKUMENT_HANDLOWY')) {
104
			$this->recordModel = \Vtiger_Record_Model::getInstanceById($id, 'FInvoice');
105
		} else {
106
			$this->recordModel = \Vtiger_Record_Model::getCleanInstance('FInvoice');
107
			$this->recordModel->setDataForSave([\App\Integrations\Wapro::RECORDS_MAP_TABLE_NAME => [
108
				'wtable' => 'DOKUMENT_HANDLOWY',
109
			]]);
110
		}
111
		$this->recordModel->set('wapro_id', $this->waproId);
112
		$this->recordModel->set('finvoice_status', 'PLL_UNASSIGNED');
113
		$this->recordModel->set('finvoice_type', 'PLL_DOMESTIC_INVOICE');
114
		$this->recordModel->set($this->recordModel->getModule()->getSequenceNumberFieldName(), $this->row['NUMER']);
115
		$this->loadFromFieldMap();
116
		$this->loadDeliveryAddress('b');
117
		$this->loadInventory();
118
		if ($this->skip) {
119
			return 0;
120
		}
121
		$this->recordModel->save();
122
		\App\Cache::save('WaproMapTable', "{$this->waproId}|DOKUMENT_HANDLOWY", $this->recordModel->getId());
123
		if ($id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
124
			return $this->recordModel->getPreviousValue() ? 1 : 3;
125
		}
126
		return 2;
127
	}
128
129
	/**
130
	 * Convert payment method to system format.
131
	 *
132
	 * @param string $value
133
	 * @param array  $params
134
	 *
135
	 * @return string
136
	 */
137
	protected function convertPaymentMethods(string $value, array $params): string
138
	{
139
		if (isset(self::PAYMENT_METHODS_MAP[$value])) {
140
			return self::PAYMENT_METHODS_MAP[$value];
141
		}
142
		$fieldModel = $this->recordModel->getField($params['fieldName']);
143
		$key = array_search(mb_strtolower($value), array_map('mb_strtolower', $fieldModel->getPicklistValues()));
144
		if (empty($key)) {
145
			$fieldModel->setPicklistValues([$value]);
146
			$key = $value;
147
		}
148
		return $key ?? '';
149
	}
150
151
	/**
152
	 * Convert date to system format.
153
	 *
154
	 * @param string $value
155
	 * @param array  $params
156
	 *
157
	 * @return string
158
	 */
159
	protected function convertDate(string $value, array $params): string
0 ignored issues
show
Unused Code introduced by
The parameter $params is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

159
	protected function convertDate(string $value, /** @scrutinizer ignore-unused */ array $params): string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
160
	{
161
		$value = explode(' ', $value);
162
		return $value[0];
163
	}
164
165
	/**
166
	 * Load delivery address.
167
	 *
168
	 * @param string $key
169
	 *
170
	 * @return void
171
	 */
172
	protected function loadDeliveryAddress(string $key): void
173
	{
174
		$row = (new \App\Db\Query())->select(['dbo.MIEJSCE_DOSTAWY.*'])->from('dbo.DOSTAWA')
175
			->leftJoin('dbo.MIEJSCE_DOSTAWY', 'dbo.DOSTAWA.ID_MIEJSCA_DOSTAWY = dbo.MIEJSCE_DOSTAWY.ID_MIEJSCA_DOSTAWY')
176
			->where(['dbo.DOSTAWA.ID_DOKUMENTU_HANDLOWEGO' => $this->row['DOK_KOREKTY'] ? $this->row['ID_DOK_ORYGINALNEGO'] : $this->waproId])
177
			->one($this->controller->getDb());
178
		if ($row) {
179
			$this->recordModel->set('addresslevel1' . $key, $this->convertCountry($row['SYM_KRAJU']));
180
			$this->recordModel->set('addresslevel5' . $key, $row['MIEJSCOWOSC']);
181
			$this->recordModel->set('addresslevel7' . $key, $row['KOD_POCZTOWY']);
182
			$this->recordModel->set('addresslevel8' . $key, $row['ULICA_LOKAL']);
183
			$this->recordModel->set('company_name_' . $key, $row['FIRMA']);
184
			if ($row['ODBIORCA']) {
185
				[$firstName, $lastName] = explode(' ', $row['ODBIORCA'], 2);
186
				$this->recordModel->set('first_name_' . $key, $firstName);
187
				$this->recordModel->set('last_name_' . $key, $lastName);
188
			}
189
			$params = ['fieldName' => 'phone_' . $key];
190
			$phone = $this->convertPhone($row['TEL'], $params);
191
			$this->recordModel->set($params['fieldName'], $phone);
192
		}
193
	}
194
195
	/**
196
	 * Load inventory items.
197
	 *
198
	 * @return void
199
	 */
200
	protected function loadInventory(): void
201
	{
202
		$inventory = $this->getInventory();
203
		if (!$this->recordModel->isNew()) {
204
			$oldInventory = $this->recordModel->getInventoryData();
205
			foreach ($oldInventory as $oldSeq => $oldItem) {
206
				foreach ($inventory as $seq => $item) {
207
					$same = true;
208
					foreach ($item as $name => $value) {
209
						if ($same) {
210
							$same = isset($oldItem[$name]) && $value == $oldItem[$name];
211
						}
212
					}
213
					if ($same && $oldItem) {
214
						$inventory[$seq] = $oldItem;
215
						unset($oldInventory[$oldSeq]);
216
						continue 2;
217
					}
218
				}
219
			}
220
		}
221
		$this->recordModel->initInventoryData($inventory);
222
		if (isset($inventory[0]['name'])) {
223
			$this->recordModel->set('subject', \App\Record::getLabel($inventory[0]['name'], true) ?: '-');
224
		}
225
	}
226
227
	/**
228
	 * Get inventory items.
229
	 *
230
	 * @return array
231
	 */
232
	protected function getInventory(): array
233
	{
234
		$currencyId = $this->getBaseCurrency()['currencyId'];
235
		if (!empty($this->row['DOK_WAL'])) {
236
			$currencyId = $this->convertCurrency($this->row['SYM_WAL'], []);
237
		}
238
		$currencyParam = \App\Json::encode($this->getCurrencyParam($currencyId));
239
		$dataReader = (new \App\Db\Query())->select(['ID_ARTYKULU', 'ILOSC', 'KOD_VAT', 'CENA_NETTO',  'JEDNOSTKA', 'OPIS', 'RABAT', 'RABAT2', 'CENA_NETTO_WAL'])
240
			->from('dbo.POZYCJA_DOKUMENTU_MAGAZYNOWEGO')
241
			->where(['ID_DOK_HANDLOWEGO' => $this->waproId])
242
			->createCommand($this->controller->getDb())->query();
243
		$inventory = [];
244
		while ($row = $dataReader->read()) {
245
			$productId = $this->findRelationship($row['ID_ARTYKULU'], ['tableName' => 'ARTYKUL']);
246
			if (!$productId) {
247
				$productId = $this->addProduct($row['ID_ARTYKULU']);
248
			}
249
			$inventory[] = [
250
				'name' => $productId,
251
				'qty' => $row['ILOSC'],
252
				'price' => $this->row['DOK_WAL'] ? $row['CENA_NETTO_WAL'] : $row['CENA_NETTO'],
253
				'comment1' => trim($row['OPIS']),
254
				'unit' => $this->convertUnitName($row['JEDNOSTKA'], ['fieldName' => 'usageunit', 'moduleName' => 'Products']),
255
				'discountmode' => 1,
256
				'discountparam' => \App\Json::encode([
257
					'aggregationType' => ['individual', 'additional'],
258
					'individualDiscount' => empty((float) $row['RABAT']) ? 0 : (-$row['RABAT']),
259
					'individualDiscountType' => 'percentage',
260
					'additionalDiscount' => empty((float) $row['RABAT2']) ? 0 : (-$row['RABAT2']),
261
				]),
262
				'taxmode' => 1,
263
				'taxparam' => \App\Json::encode(
264
					$this->getGlobalTax($row['KOD_VAT']) ? [
265
						'aggregationType' => 'global',
266
						'globalTax' => (float) $row['KOD_VAT'],
267
					] : [
268
						'aggregationType' => 'individual',
269
						'individualTax' => (float) $row['KOD_VAT'],
270
					]
271
				),
272
				'discount_aggreg' => 2,
273
				'currency' => $currencyId,
274
				'currencyparam' => $currencyParam,
275
			];
276
		}
277
		return $inventory;
278
	}
279
280
	/**
281
	 * Get currency param.
282
	 *
283
	 * @param int $currencyId
284
	 *
285
	 * @return array
286
	 */
287
	protected function getCurrencyParam(int $currencyId): array
288
	{
289
		$baseCurrency = $this->getBaseCurrency();
290
		$defaultCurrencyId = $baseCurrency['default']['id'];
291
		if (!empty($this->row['DATA_KURS_WAL'])) {
292
			$date = $this->convertDate($this->row['currencyDate'], []);
293
		} else {
294
			$date = \vtlib\Functions::getLastWorkingDay(date('Y-m-d', strtotime('-1 day', strtotime($this->row['saleDate']))));
295
		}
296
		$params = [
297
			$defaultCurrencyId => ['date' => $date, 'value' => 1.0, 'conversion' => 1.0],
298
		];
299
		$info = ['date' => $date];
300
		if ($currencyId != $defaultCurrencyId) {
301
			if (empty($this->row['PRZELICZNIK_WAL'])) {
302
				$value = \Settings_CurrencyUpdate_Module_Model::getCleanInstance()->getCRMConversionRate($currencyId, $defaultCurrencyId, $date);
303
				$info['value'] = empty($value) ? 1.0 : round($value, 5);
304
				$info['conversion'] = empty($value) ? 1.0 : round(1 / $value, 5);
305
			} else {
306
				$info['value'] = round($this->row['PRZELICZNIK_WAL'], 5);
307
				$info['conversion'] = round(1 / $this->row['PRZELICZNIK_WAL'], 5);
308
			}
309
			$params[$currencyId] = $info;
310
		}
311
		return $params;
312
	}
313
314
	/**
315
	 * Add a product when it does not exist in CRM.
316
	 *
317
	 * @param int $id
318
	 *
319
	 * @return int
320
	 */
321
	protected function addProduct(int $id): int
322
	{
323
		return $this->controller->getSynchronizer('Products')->importRecordById($id);
0 ignored issues
show
Bug introduced by
The method importRecordById() does not exist on App\Integrations\Wapro\Synchronizer. Did you maybe mean importRecord()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

323
		return $this->controller->getSynchronizer('Products')->/** @scrutinizer ignore-call */ importRecordById($id);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
324
	}
325
326
	/** {@inheritdoc} */
327
	public function getCounter(): int
328
	{
329
		return (new \App\Db\Query())->from('dbo.DOKUMENT_HANDLOWY')->where(['ID_TYPU' => 1])->count('*', $this->controller->getDb());
0 ignored issues
show
Bug Best Practice introduced by
The expression return new App\Db\Query(...s->controller->getDb()) could return the type string which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
330
	}
331
}
332