1
0
Fork 0
mirror of synced 2025-04-10 04:30:56 +00:00

Compare commits

...

371 commits

Author SHA1 Message Date
ellynoize
4363cccc40
Bump version of module (#361)
Co-authored-by: ellynoize <a.kolesnikov@retailcrm.ru>
2025-02-19 13:09:06 +03:00
ellynoize
3610cec728
Add returned types for methods offsetExists, offsetSet, offsetUnset in WC_Retailcrm_Response (#360) 2025-02-19 12:31:06 +03:00
Kocmonavtik
bce545979c
Optimization of order unloading (#359) 2025-02-06 10:14:39 +03:00
Kocmonavtik
cd57de01e6
Added additional parameters to GET requests (#358) 2025-02-04 11:28:44 +03:00
ellynoize
b2cc25e716
Fix deploy (#357) 2025-01-22 09:49:41 +03:00
ellynoize
63ca5015c9
Fix tests errors (#356) 2025-01-13 16:15:25 +03:00
ellynoize
dee71bf631
Fix export archive from CRM with console script (#355) 2025-01-13 09:54:58 +03:00
Uryvskiy Dima
cdc7ca71bc
The method for determining the stock quantity has been optimized (#354) 2024-11-11 16:50:57 +03:00
Kocmonavtik
6e39fca72c
Supports custom cart and checkout templates (#353) 2024-11-11 15:08:21 +03:00
Kocmonavtik
894c065b99
Fixed multiple execution of order updates (#352) 2024-10-28 10:30:27 +03:00
ellynoize
aa4e337cd0
Added additional parameters to GET requests (#351) 2024-10-15 09:27:57 +03:00
ellynoize
618dbbe963
Fix Js-errors (#350) 2024-10-09 09:33:09 +03:00
Kocmonavtik
b195d169e1
Improvement of customer registration form in Loyalty program (#349) 2024-10-01 09:44:07 +03:00
Chupocabra
b00ff321ab
Add svn --ignore-externals option (#348) 2024-09-30 15:44:02 +03:00
Chupocabra
ec0b66c194
Logs refactoring (#344) 2024-09-30 09:29:21 +03:00
ellynoize
9fa0421c23
Replace the file_get_content method with wp_remote_get (#346) 2024-09-26 17:02:41 +03:00
Kocmonavtik
61fcad66c5
Project testing updated (#345) 2024-09-23 09:07:21 +03:00
Kocmonavtik
a23e96b4d6
Transferring base price of goods for loyalty program (#342) 2024-09-18 11:49:00 +03:00
Kocmonavtik
dac3c9f6f7
Adding loyalty program coupon entry to form by click (#343) 2024-09-12 09:40:05 +03:00
Kocmonavtik
78fd206c0c
Corrects directory path when customizing base file (#341) 2024-09-02 15:58:26 +03:00
Kocmonavtik
8b01b9844b
Fix filtering of api query results (#340) 2024-08-06 12:03:16 +03:00
Kocmonavtik
01fe3c9741 ref #71456 Added loyalty program support (#338) 2024-07-15 17:56:31 +03:00
Chupocabra
69ef4e24c5
Fixed undefined array key number in order history (#337) 2024-06-27 12:45:00 +03:00
Kocmonavtik
4ec5446a95
Added passing link field for abandoned baskets (#336) 2024-06-26 13:04:30 +03:00
Kocmonavtik
8627548d7c
ref #95242 Support for services in ICML (#329) 2024-04-24 12:12:43 +03:00
Uryvskiy Dima
90d7e1f866
ref #95035 Support WP 6.5 2024-04-22 09:48:10 +03:00
Uryvskiy Dima
9ea8818e5b
ref #87349 Added automatic catalog generation when changing 'Activate the binding via sku (xml)' (#327) 2024-04-22 09:01:59 +03:00
Uryvskiy Dima
498646c10a
ref #94710 Fixed an error when transferring abandoned carts (#323) 2024-02-29 16:00:48 +03:00
Kocmonavtik
53ad81aec3
ref #93708 Adding filters after creating and updating order (#322) 2024-02-08 12:09:20 +03:00
Uryvskiy Dima
f32fb6a4c4
ref #94134 Fixed error with send address by history (#321) 2024-01-31 13:01:47 +03:00
Uryvskiy Dima
55a154da83
ref #91065 Fixed the error of displaying the 'Add' button to mapping custom fields (#320) 2024-01-31 09:10:05 +03:00
Uryvskiy Dima
a0d681509d
Added support WooCommerce 8.2 (HPOS) (#319) 2023-12-07 12:54:40 +03:00
Kocmonavtik
f49cc3870d
ref #85780 Fix module activation (#318) 2023-11-20 16:26:20 +03:00
Dmitriy
97cc1c1337
ref #92584 Fix not correct scoring product total price (#317) 2023-10-27 10:20:28 +03:00
Uryvskiy Dima
4ea6015d74
ref #92047 Added currency validation (#316) 2023-10-03 15:18:09 +03:00
Kocmonavtik
0566309c24
ref #89649 Coupon transfer (#312) 2023-09-01 12:24:57 +03:00
Kocmonavtik
4d3883fe45
ref #90909 Abandoned cart transfer fix (#314) 2023-07-20 14:36:00 +03:00
Uryvskiy Dima
2df9ee64c4
ref #90632 Changed the logic of customer subscriptions to promotional newsletters 2023-07-19 15:34:23 +03:00
Uryvskiy Dima
382d25ce55 ref #90632 Changed the logic of customer subscriptions to promotional newsletters 2023-07-19 14:39:06 +03:00
Uryvskiy Dima
ad21aaf178
ref #68451 Added the ability to select CRM warehouses to synchronize the balance of offers 2023-07-10 09:58:23 +03:00
Uryvskiy Dima
9d96f38b81 Add translate 2023-07-07 13:57:22 +03:00
Uryvskiy Dima
0f01a08f99 ref #68451 Added the ability to select CRM warehouses to synchronize the balance of offers 2023-07-06 18:18:09 +03:00
Uryvskiy Dima
df0b6a6c18
Fixed customer phone sending to CRM when order will create by guest 2023-07-04 16:25:13 +03:00
Dmitriy
aa1adc0ced
updated changelog in readme.txt 2023-07-04 15:29:28 +03:00
KMityai
ed81c5f052 version bump 2023-06-27 17:36:06 +03:00
KMityai
578bcb63a2 fixed customer phone sending to crm 2023-06-27 17:28:16 +03:00
Uryvskiy Dima
e4735a255e
ref #90318 Added handling of fatal errors when working with abandoned carts 2023-06-14 11:00:56 +03:00
Uryvskiy Dima
0e51289625 ref #90318 Added handling of fatal errors when working with abandoned carts 2023-06-14 10:20:07 +03:00
Uryvskiy Dima
02de28a7f5
Updated FAQ 2023-06-09 15:54:30 +03:00
gleemand
3dfab91fb6
Additional info 2023-06-09 14:14:23 +02:00
gleemand
2d608a64dc
Updated FAQ 2023-06-09 12:13:58 +02:00
Uryvskiy Dima
d43f45196a
ref #89652 Transferring WC meta fields to standard CRM order and customer fields 2023-06-08 12:24:49 +03:00
Uryvskiy Dima
59054f114e ref #89652 Transferring meta fields to standard order\customer fields 2023-06-08 11:43:12 +03:00
Uryvskiy Dima
7dec9b40fb
Bump version 2023-06-05 15:31:38 +03:00
Ivan Chaplygin
eb1e6fb30e fix version 2023-06-05 14:25:49 +03:00
Uryvskiy Dima
8525db0d30
ref #90237 Optimizing unloading of stock 2023-06-05 10:31:59 +03:00
Ivan Chaplygin
d284c25e62 ref #90237 fix code
update version
2023-06-01 17:56:34 +03:00
Ivan Chaplygin
5c70a32d36 ref #90237 Optimizing unloading of stock, update tests.
Change version
2023-06-01 17:56:34 +03:00
Uryvskiy Dima
79ca29fa03
ref #90086 Types of deliveries and payments are displayed only for available stores 2023-05-30 09:18:05 +03:00
Uryvskiy Dima
a2980469e1 ref #90086 Types of deliveries and payments are displayed only for available stores 2023-05-29 18:46:43 +03:00
Kocmonavtik
a84b7261d2
ref #88866 Доработан метод получения адреса заказа по истории (#303) 2023-05-17 15:33:12 +03:00
Uryvskiy Dima
9a533b97dd
ref #89481 The 'page' parameter has been removed from the history API methods (#302) 2023-04-25 11:59:57 +03:00
Uryvskiy Dima
d3f298645d Added functionality of abandoned carts 2023-03-29 14:14:08 +03:00
Uryvskiy Dima
aed62d49f9 ref #88115 Added an option in the module settings for abandoned carts 2023-03-29 14:14:08 +03:00
Kocmonavtik
be5aedef94 ref #88120 Adding API methods for handling cart (#296)
* Adding and testing new cart API methods
* Fix the causes of errors in old tests

---------

Co-authored-by: Ivan Chaplygin <chaplygin@retailcrm.ru>
2023-03-29 14:14:08 +03:00
Kocmonavtik
6c08749e8b
ref #88434 fix display payment methods (#298) 2023-03-13 11:37:44 +03:00
Uryvskiy Dima
d929a1fd5f
ref #87089 Fix bug with products tax (#295) 2022-12-28 10:36:03 +03:00
Dima Uryvskiy
4edd42604b
Validator for CRM URL 2022-11-25 12:17:22 +03:00
Kocmonavtik
6623f1ef19
Fix a critical memory overflow error when generating ICML 2022-11-10 12:35:15 +03:00
Dima Uryvskiy
855ecc3aa7
Change the logic of the tests 2022-10-10 11:28:58 +03:00
Dima Uryvskiy
769907d812
Add generators in the ICML generation process 2022-09-30 17:50:15 +03:00
Dima Uryvskiy
222f5efde1
Fix path for js scripts 2022-09-30 12:36:41 +03:00
Dima Uryvskiy
8a47750818
Change logic work with address 2022-09-22 15:17:34 +03:00
Dima Uryvskiy
b77635f2c5
Fix bug with product tax 2022-09-05 19:20:27 +03:00
Dima Uryvskiy
fdbac7a2ec
Fix a critical bug when working with taxes 2022-09-02 18:36:06 +03:00
Dima Uryvskiy
91a7c02e25
Add support for payment method on delivery 2022-08-11 21:17:55 +03:00
Dima Uryvskiy
9d8eb2256c
Add filter for changing ICML product information 2022-08-10 14:02:35 +03:00
Dima Uryvskiy
d872730796
Add automatically upload ICML in CRM 2022-08-10 10:14:35 +03:00
Dima Uryvskiy
2b4f670b8c
Fix bug with shipping tax 2022-08-01 16:51:55 +03:00
Dima Uryvskiy
ebdc410fda
Change logic work with delivery cost 2022-07-20 14:50:15 +03:00
Dima Uryvskiy
f287ce8aa1
Add price rounding from WC settings 2022-07-18 11:19:22 +03:00
Dima Uryvskiy
b8c1cd9ec8
Add functionality for changing the time interval for cron tasks 2022-07-05 18:24:04 +03:00
Dima Uryvskiy
2b17ec6a1c
Change composer version 2022-07-05 13:26:02 +03:00
Dima Uryvskiy
20a2992551
Fix error with empty 'paidAt' 2022-06-28 18:27:39 +03:00
Dima Uryvskiy
88ca4e9c11
Change processing history by sinceId 2022-06-28 11:17:33 +03:00
gleemand
b5adba84be
Fixed spanish accents processing in ICML 2022-06-23 17:27:07 +03:00
gleemand
c9b92919c8
Fixed WA icon positioning 2022-06-23 12:10:48 +03:00
Dima Uryvskiy
c9294d62a1
Fix fatal error when using API without api_key 2022-05-26 17:13:35 +03:00
Dima Uryvskiy
e1e7331f89
Add product description to ICML 2022-05-26 11:58:54 +03:00
Dima Uryvskiy
01e3ad71ee
Add priceType processing to CRM order by history 2022-05-24 13:01:29 +03:00
Dima Uryvskiy
6d94082c22
Add method in API V5 and delete use another version 2022-05-06 17:34:51 +03:00
Dima Uryvskiy
ad15739798
Fix error with integration payments 2022-05-05 18:09:54 +03:00
Dima Uryvskiy
5838dde9eb
Improvement of tests for history 2022-04-18 18:01:57 +03:00
Dima Uryvskiy
c1e913d063
Improvement of tests for corporate customers 2022-04-18 09:56:49 +03:00
Dima Uryvskiy
554a3f420f
Fix bug with changing order status by history 2022-04-12 15:55:09 +03:00
Dima Uryvskiy
54992c9904
Fix bug in updating order number by history 2022-03-24 16:37:38 +03:00
Dima Uryvskiy
a12bdf7a74
Add multiple image transfer in ICML 2022-03-23 10:48:34 +03:00
Dima Uryvskiy
96fc850a40
Add filters for custom fields 2022-03-22 15:29:39 +03:00
Dima Uryvskiy
81dc5071dd
Change order number transfer for history 2022-03-21 16:12:45 +03:00
Dima Uryvskiy
7f04fe822a
Add a check for the existence of a method in a class 2022-03-18 15:57:42 +03:00
Dima Uryvskiy
4a68135e33
Delete deprecated API V4. Refactoring API V5 and history getting method 2022-02-25 10:40:06 +03:00
Dima Uryvskiy
9113a89cd2
Add transfer of order numbers from CMS to CRM by history sync 2022-02-24 18:56:48 +03:00
Dima Uryvskiy
fa32395161
Fix the error of using xmlId field to link products in orders 2022-02-24 18:45:17 +03:00
Dima Uryvskiy
e07cfbd1f5
Improvements history class. Add page limit, refactoring code 2022-02-08 12:53:07 +03:00
Dima Uryvskiy
a3a79ee9c5
Add documentation for registering client functionality 2022-01-26 12:59:20 +03:00
Dima Uryvskiy
5cec16c266
Delete legacy code for update customer name and surname 2022-01-24 17:27:05 +03:00
Dima Uryvskiy
f8ac961520
Added functionality to skip some orders statuses 2022-01-20 17:26:24 +03:00
Dima Uryvskiy
6cbb10e0ec
Improved the create/update method when registering customers 2022-01-14 12:52:13 +03:00
Dima Uryvskiy
05a0f64a13
Remove legacy code for updating orders by history associated with payments 2022-01-10 19:15:01 +03:00
Dima Uryvskiy
f4c44d486c
Refactoring of the starting local tests 2022-01-10 17:22:43 +03:00
Dima Uryvskiy
4b074a464b
Fix warning with hooks 2022-01-10 14:54:51 +03:00
Dima Uryvskiy
593bbd4f2f
Refactoring class description 2022-01-10 12:53:00 +03:00
Dima Uryvskiy
5a480f0b31
Add mapping metadata fields in settings 2021-12-30 11:22:34 +03:00
Dima Uryvskiy
c57cb099e5
Refactor code, finalized user interface and translations for the upload functionality 2021-12-28 13:35:36 +03:00
Dima Uryvskiy
6132172da5
Update old tests, add new tests 2021-12-20 11:41:41 +03:00
Dima Uryvskiy
b0577a18fb
Update versions WP/WC in tests 2021-12-10 11:54:05 +03:00
Dima Uryvskiy
6a7c565cb4
Add validate countryIso. Fix bug with duplicate customer address 2021-12-10 10:17:02 +03:00
Dima Uryvskiy
5407d9111f
Fix transfer payment status 2021-12-10 10:13:39 +03:00
max-baranikov
07735ed6d9
Get full delivery info on order create 2021-12-02 18:58:48 +03:00
Dima Uryvskiy
6d95580155
Add check for add new items 2021-11-17 11:51:03 +03:00
Dima Uryvskiy
6a716cc14e
Fix error in tests, use stable version WordPress 5.8 2021-11-17 10:32:15 +03:00
Dima Uryvskiy
2816b81445
Add transfer of zero shipping cost from CMS 2021-11-15 12:33:37 +03:00
Dima Uryvskiy
ec97a280ba
Fix PHP warning and deprecated 2021-10-27 19:14:38 +03:00
Dima Uryvskiy
f2b50e6d72
Fix bug with variation product id 2021-10-27 15:53:04 +03:00
Victorija Saprykina
5d999f6fde
Documentation 2021-09-17 15:43:42 +03:00
Dima Uryvskiy
45193886fc
Fix change in history 2021-09-17 15:43:10 +03:00
Victorija Saprykina
2d2267caf5
doc: Loading data from CRM.md 2021-09-13 16:30:22 +03:00
Victorija Saprykina
0feeb3d479
doc: generating icml catalog 2021-09-13 16:23:06 +03:00
Victorija Saprykina
6a34f41239
doc: Setting WhatsApp.md 2021-09-13 16:22:41 +03:00
Victorija Saprykina
3c58287629
doc: loading orders 2021-09-13 16:22:18 +03:00
Victorija Saprykina
140c1b9b8f
doc: Setting stock.md 2021-09-13 16:21:34 +03:00
Victorija Saprykina
e30feb1211
doc: payment methods 2021-09-13 15:51:56 +03:00
Victorija Saprykina
a4477a94e0
doc: Delivery methods.md 2021-09-13 15:51:15 +03:00
Victorija Saprykina
da806c2d87
Doc: Methods order registration.md 2021-09-13 15:35:24 +03:00
Victorija Saprykina
52d812766a
Doc: catalog settings (product statuses) 2021-09-13 15:34:11 +03:00
Victorija Saprykina
b19502dddf
Doc: corporate clients 2021-09-13 15:32:33 +03:00
Victorija Saprykina
f693eff094
Doc: order statuses 2021-09-10 16:50:09 +03:00
Dima Uryvskiy
a527017bc8
Bump version in MP 2021-08-30 17:03:13 +03:00
Dima Uryvskiy
ea29da0025
Update logic work address 2021-08-27 16:17:18 +03:00
Dima Uryvskiy
a92253c0a4
Add transfer of the client's comment to the WC order 2021-08-25 16:00:07 +03:00
Dima Uryvskiy
22eaceb6f5
Translate module version history 2021-08-24 12:22:45 +03:00
Dima Uryvskiy
e9e2d52c31
Skip inactive statuses 2021-08-18 12:17:06 +03:00
Dima Uryvskiy
07aab1078e
Delete option 'Do not transmit the cost of delivery' 2021-08-16 11:14:53 +03:00
Dima Uryvskiy
6a77e02e89
Fix bug in uploader 2021-08-04 15:41:48 +03:00
Dima Uryvskiy
eb95f3e375
Update logic client roles. Delete setting role 2021-08-03 18:26:55 +03:00
Dima Uryvskiy
57154ae7e3
Fix bug with variation products 2021-07-27 14:21:49 +03:00
Dima Uryvskiy
4ef67f595e
ICML catalog settings updating 2021-07-27 13:09:30 +03:00
Dima Uryvskiy
6ad9fc2661
Update version in Marketplace 2021-07-22 13:42:18 +03:00
Dima Uryvskiy
eb1f2f5e70
Update version 2021-07-22 11:08:18 +03:00
Dima Uryvskiy
79e8b46019
Bump version 4.3.3 2021-07-21 18:01:47 +03:00
Dima Uryvskiy
31c24aad34
Add label for whatsapp icon and update style 2021-07-21 10:34:41 +03:00
Dima Uryvskiy
bb0c17a0a8
Add processing of all types of goods 2021-07-20 18:08:56 +03:00
Dima Uryvskiy
30f16915d9
Add debug mode option in settings and little fix debug info 2021-07-16 12:35:11 +03:00
Dima Uryvskiy
58e0e8b22c
Add enable debug info 2021-07-14 10:49:42 +03:00
Dima Uryvskiy
83e900ba95
Update logic generate icml catalog. Check option 'Manage stocks' and 'Stocks status' 2021-07-09 16:02:35 +03:00
Dima Uryvskiy
5979c7cb0e
Add export functionality 2021-07-09 13:46:28 +03:00
Dima Uryvskiy
badcc0e38f
Update description plugin 2021-07-06 13:08:52 +03:00
Dima Uryvskiy
db7a66c6a1
Update link ymlUrl 2021-07-05 13:31:32 +03:00
Dima Uryvskiy
c72da3e761
Bump version 2021-07-02 17:21:20 +03:00
Dima Uryvskiy
e7e9d5fe02
Bump version 2021-07-02 11:57:09 +03:00
Dima Uryvskiy
e7b0d2b3ab
Rebranding Simla.com 2021-06-30 13:06:18 +03:00
Dima Uryvskiy
fca30cb4f7
Fix option 'Activate the binding via sku (xml)' 2021-06-28 18:36:07 +03:00
Dima Uryvskiy
e1df68f209
Fix 'Warning: array_flip(): Can only flip STRING and INTEGER values' 2021-06-18 12:46:18 +03:00
Dima Uryvskiy
948be17b92
Add link on WhatsApp chat 2021-06-09 13:48:21 +03:00
Dima Uryvskiy
d1d2037bf0
Fix misprint in dimensions 2021-04-23 15:02:24 +03:00
max-baranikov
523f4bf2a9
Added check for deleted items when creating orders from history 2021-03-29 14:08:36 +03:00
max-baranikov
a29835d6fc
Improved module customization 2021-03-29 13:38:03 +03:00
Dima Uryvskiy
84d36f7c33
Add display of the total number of product variables 2021-03-15 18:18:55 +03:00
Dima Uryvskiy
3ba2f1662e
Do not create orders with the auto-draft status 2021-03-15 18:14:58 +03:00
Dima Uryvskiy
adfc893c95
Change for local tests 2021-03-12 11:42:13 +03:00
Dima Uryvskiy
4ab7311435
Update transferring the order number 2021-03-11 17:40:45 +03:00
Dima Uryvskiy
c48f50195a
Bump version 2021-01-20 18:10:47 +03:00
Eugene Polozov
df461b8555 Bump version 2021-01-20 15:09:50 +03:00
Dima Uryvskiy
d9c31e8871
Add validate method for address 2020-12-22 11:58:35 +03:00
Eugene Polozov
ccdc39ce05 Edit github actions 2020-12-17 16:58:09 +03:00
opheugene
b4af719779
Move to github actions, bump version 2020-12-17 13:48:10 +03:00
opheugene
94e175153e
Rebranding RetailCRM 2020-12-15 12:05:43 +03:00
Dima Uryvskiy
9cb449f7b5
Update version php in travis.yml 2020-12-12 14:57:16 +03:00
opheugene
e991e13cbb
Merge pull request #168 from dima-uryvskiy/time 2020-12-08 10:19:15 +03:00
dima-uryvskiy
af203e9260 Bump version on 4.2.0 2020-12-02 22:31:17 +03:00
dima-uryvskiy
60f5c77685 Update WC_Retailcrm_Response 2020-12-02 20:51:46 +03:00
dima-uryvskiy
e67e1e6690 Refactoring code style class-wc-retailcrm-history.php 2020-12-02 18:36:37 +03:00
dima-uryvskiy
277d1924b4 Update filling field createAt customer 2020-12-02 12:53:40 +03:00
Dima Uryvskiy
415c2edf0e
Delete option 'Transferring the payment amount' 2020-12-02 12:36:32 +03:00
Dima Uryvskiy
2fd1a1088b
Fix client info if shipping address empty, use billing address 2020-11-23 13:05:46 +03:00
Dima Uryvskiy
c7c3065834
New translation of WooCommerce RetailCRM description 2020-11-22 17:37:00 +03:00
Dima Uryvskiy
53bea71089
Fix delete items with xmldId/sku 2020-11-21 14:47:03 +03:00
Dima Uryvskiy
222a90d755
Fix problem with composer. Use composer version 1.10.17 2020-11-19 10:51:49 +03:00
Dima Uryvskiy
e7dc2343f4
Fix misprint: change history_orders to history_customers (#162) 2020-10-08 17:17:17 +03:00
Dima Uryvskiy
ca5fbfa3b9
Fix send zero amount (#160) 2020-09-30 18:22:17 +03:00
Dima Uryvskiy
60d093d7a0
Correct email, transfer in lowercase (#161) 2020-09-30 12:55:49 +03:00
Dima Uryvskiy
30cd87cad9
Correct send and calculate discount (#159) 2020-09-28 13:42:35 +03:00
max-baranikov
155a2d5716
Использование ФИО из платежного адреса, в случае, если ФИО в адресе доставки пусто 2020-09-28 13:38:23 +03:00
Dima Uryvskiy
754e1e8245
Fix online consultant show if page equals wp-login (#152) 2020-09-04 11:32:18 +03:00
Dima Uryvskiy
46b7ff03fa
Fix error with travis tests php 5.3 (#156) 2020-09-04 09:09:03 +03:00
Dima Uryvskiy
1e821105e0
Add translate for 'Do not transmit the cost of delivery' (#154) 2020-09-02 15:38:02 +03:00
Dima Uryvskiy
716c418205
Fix error 'Element 'phpunit', attribute 'syntaxCheck': The attribute 'syntaxCheck' is not allowed' (#153) 2020-08-25 15:20:35 +03:00
Dima Uryvskiy
3238ff3a42
Add option transfering net cost (#149) 2020-08-21 12:48:33 +03:00
Dima Uryvskiy
c17f4cf78e
Compile translate for roles client (#151) 2020-08-21 10:02:57 +03:00
Dima Uryvskiy
c1cc4bd5d5
Fix bug with missing settings reference (#150) 2020-08-20 14:40:04 +03:00
max-baranikov
6f9ce78d50
Add selection of client roles (#148) 2020-08-18 16:22:26 +03:00
Сергей Чазов
e1a22abab0
Online Consultant 2020-08-05 18:02:17 +03:00
Сергей Чазов
c0ff7028f2
Online Consultant 2020-08-05 14:12:06 +03:00
Evgeniy-Goroh
a3a70707ae
Fix order payment 2020-08-03 17:56:14 +03:00
Yura
782b4781ea
Order creation date 2020-07-23 14:50:09 +03:00
b25e9e4a2b
New customization system with fallback to old one 2020-07-22 15:01:10 +03:00
804cbfac37
Corporate clients support
* corporate customers support
* skip new payments without type
* extract customer data from order for guests
* extract customer phone and email from order for guests
* set item discount to zero if no discount applies
* create order from back-office
* sync phone via history
* fixed customer squashing
* fixed createdAt crash
* fixed customer id assigning & possible crash after errors in order creation
2020-07-10 13:14:03 +03:00
53f6327da0
create order from adminpanel 2020-07-08 16:00:57 +03:00
DanielWeiser
ea03321667
Skip product if item not found by id 2020-07-08 15:54:34 +03:00
fec8645764
State fix 2020-06-18 11:15:49 +03:00
Dima Uryvskiy
61ae452aec
Closes #118 - Fix Proxy response (#137) 2020-06-15 10:21:37 +03:00
ef796ebd75
sync more fields in the history 2020-06-10 12:13:51 +03:00
Evgeniy-Goroh
4a80d07681
fix notice (#136)
Co-authored-by: gorokh <gorokh@retailcrm.ru>
2020-06-04 12:00:53 +03:00
1efac9612d
Closes #119 - prevent search with empty email (#130) 2020-05-27 16:21:50 +03:00
548d466bd0
Closes #117 - updated from retailcrm/api-client-php (#129) 2020-05-27 16:00:44 +03:00
d0dfc39da0
[fix] Tests compatibility with changes in current woocommerce master 2020-05-27 11:51:56 +03:00
Alex Lushpai
eef81f506b
Client deduplication
[fix] error with client duplication
2020-04-15 12:14:04 +03:00
bf13ca3e53 fix indentation 2020-04-14 15:48:01 +03:00
Alex Lushpai
45bbd5dd81
ICML improvements 2020-04-14 15:45:19 +03:00
bd59c6b7bc fix for tests 2020-04-14 15:44:05 +03:00
1481edae95 fix error with client duplication 2020-04-14 15:44:05 +03:00
Akolzin Dmitry
1912a779e0 create category in tests 2020-04-05 12:13:09 +03:00
Akolzin Dmitry
f09d4c1f72 Generate icml test 2020-04-05 10:36:45 +03:00
Alex Lushpai
dd7b4df5da
Merge pull request #125 from iyzoer/master
Bug fixes of generating ICML
2020-03-31 16:28:59 +03:00
Akolzin Dmitry
3a57c25f9b Bug fixes of generating ICML 2020-03-31 15:26:33 +03:00
Alex Lushpai
889797330a
Merge pull request #124 from iyzoer/master
Setting payment amount transfer
2020-03-25 15:11:08 +03:00
Akolzin Dmitry
3475f369be update readme.txt 2020-03-25 13:58:12 +03:00
Akolzin Dmitry
295d89490c Setting payment amount transfer 2020-03-25 10:35:08 +03:00
Alex Lushpai
f5adbfc367
Merge pull request #123 from iyzoer/master
Fix deployment
2020-03-06 12:16:15 +03:00
Akolzin Dmitry
6a4118fa17 bugfixes 2020-03-06 10:22:59 +03:00
Akolzin Dmitry
74991719e2 fix svn deploy 2020-03-06 09:21:16 +03:00
Alex Lushpai
6886824347
Merge pull request #122 from iyzoer/master
update required versions
2020-03-05 14:51:25 +03:00
Akolzin Dmitry
9648513fb9 update required versions 2020-03-05 13:06:25 +03:00
Alex Lushpai
055f8a6f83
Merge pull request #121 from iyzoer/master
fix travis deploy
2020-03-05 12:46:22 +03:00
Alex Lushpai
889476fa89 Update README.md 2020-03-05 11:08:38 +03:00
Akolzin Dmitry
79401b25bc fix travis deploy 2020-03-05 11:07:59 +03:00
Alex Lushpai
75ff9528f9
Update README.md 2020-03-03 17:53:54 +03:00
Alex Lushpai
0212ddf823
Merge pull request #120 from iyzoer/master
Travis build matrix
2020-03-03 17:51:53 +03:00
Akolzin Dmitry
a877cf500c update travis config 2020-03-03 15:29:48 +03:00
Akolzin Dmitry
5e1f52a57e github releases 2020-03-03 14:28:16 +03:00
Akolzin Dmitry
0720148f9e travis php7.3 2020-03-03 13:55:46 +03:00
Akolzin Dmitry
19d1f1b179 travis php7.3 2020-03-03 13:51:28 +03:00
Akolzin Dmitry
33afc77c09 travis php7.3 2020-03-03 12:33:05 +03:00
Akolzin Dmitry
f9b872e628 travis php7.3 2020-03-03 11:30:13 +03:00
Akolzin Dmitry
92a5673614 fix for php5.3 2020-03-03 10:24:43 +03:00
Akolzin Dmitry
baa1f3d581 add php5.3 2020-03-03 10:20:58 +03:00
Akolzin Dmitry
68343b7e49 update history 2020-03-02 18:02:40 +03:00
Akolzin Dmitry
d3448908e7 fix bash script 2020-03-02 14:20:45 +03:00
Akolzin Dmitry
a0613d83e5 fix bash script 2020-03-02 14:13:27 +03:00
Akolzin Dmitry
31865ae3ea fix bash script 2020-03-02 14:03:56 +03:00
Akolzin Dmitry
2d75f50a08 update matrix 2020-03-02 13:54:34 +03:00
Akolzin Dmitry
56b1eb81d2 update transfer DB_PASS 2020-02-28 16:32:18 +03:00
Akolzin Dmitry
24efcdf23a update jobs 2020-02-27 14:12:42 +03:00
Akolzin Dmitry
2142e2a01f matrix 2020-02-26 17:23:12 +03:00
Akolzin Dmitry
7d0c1ac904 update bootstrap 2020-02-25 14:49:47 +03:00
Akolzin Dmitry
147ebb02ab update bootstrap 2020-02-25 14:45:04 +03:00
Akolzin Dmitry
c8cdedce3f update phpunit config 2020-02-25 14:40:36 +03:00
Akolzin Dmitry
68522a8e7e update jobs 2020-02-25 14:36:12 +03:00
Akolzin Dmitry
3c0f053028 fix Makefile 2020-02-25 14:30:38 +03:00
Akolzin Dmitry
239c5d43a8 fix Makefile 2020-02-25 14:26:27 +03:00
Akolzin Dmitry
da07d142ac fix Makefile 2020-02-25 14:05:51 +03:00
Akolzin Dmitry
a18de2625e update Makefile 2020-02-25 14:00:08 +03:00
Akolzin Dmitry
9018d547af update travis config 2020-02-25 13:56:08 +03:00
Akolzin Dmitry
cc4e8c9c5b update build 2020-02-25 13:45:17 +03:00
Alex Lushpai
6fa5e3d511
Merge pull request #114 from Evgeniy-Goroh/master
Поддержка передачи одинаковых товаров в заказе как разных товарных по…
2019-10-17 11:48:10 +03:00
gorokh
8518b43d03 небольшие правки 2019-10-15 16:14:42 +03:00
gorokh
4eecba2d86 fix 2019-10-09 17:50:54 +03:00
gorokh
606de96a91 Поддержка передачи одинаковых товаров в заказе как разных товарных позиций 2019-10-07 17:36:31 +03:00
Alex Lushpai
af86ba2821
Merge pull request #113 from Frosin/master
Исправлен баг с некорректной передачей дробного количества
2019-09-18 16:50:15 +03:00
Frosin
6763a237e9 fixed retailcrm-order-item 2019-09-18 13:17:58 +03:00
Frosin
2358e0ec0a fixed retailcrm-order-item 2019-09-18 13:13:07 +03:00
Frosin
f698e1c7b2
Merge pull request #3 from retailcrm/master
Updated from original
2019-09-18 13:07:22 +03:00
Alex Lushpai
55a52667bd
Merge pull request #111 from KMityai/master
fixed customer transmission
2019-08-28 12:07:15 +03:00
Дмитрий
1b2f0d053a fixed customer transmission 2019-08-27 15:14:34 +03:00
Alex Lushpai
db56c05725
Merge pull request #110 from KMityai/master
fixed getting sku from product
2019-07-25 14:59:11 +03:00
Alex Lushpai
a73d8ef737
Merge pull request #107 from Frosin/master
Code text correction
2019-07-25 14:58:55 +03:00
Круглов Дмитрий
b8c1305eca fixed getting sku from product 2019-07-25 12:07:20 +03:00
Alex Lushpai
635114633d
Merge pull request #109 from iyzoer/master
Fix tests
2019-07-17 18:04:39 +03:00
Akolzin Dmitry
228488bec2 Add trusty dist 2019-07-17 17:47:11 +03:00
Akolzin Dmitry
3d8eb0cebe Update woocommerce installation 2019-07-17 16:26:20 +03:00
Akolzin Dmitry
e1f98191b5 Update travis.yml 2019-07-17 15:59:35 +03:00
Frosin
9cd0f833aa code text correction 2019-07-10 11:52:58 +03:00
Frosin
c83869f9ec
Merge pull request #2 from retailcrm/master
merged from master
2019-07-10 11:47:58 +03:00
Alex Lushpai
1fb010e421
Update VERSION 2019-07-01 11:51:12 +03:00
Alex Lushpai
13c532da0e
Merge pull request #105 from KMityai/master
fix discount calculation
2019-07-01 11:49:02 +03:00
Alex Lushpai
52817d9e08
Merge pull request #104 from kolyasapphire/master
Фикс на получение аттрибутов товаров в каталоге
2019-07-01 11:48:55 +03:00
Круглов Дмитрий
36d973dbae fix discount calculation 2019-06-07 16:51:18 +03:00
kolyasapphire
f8d1febe47 Фикс на получение аттрибутов товаров в каталоге
wc_attribute_taxonomy_name_by_id принимает int, необходим intval.
2019-06-06 18:33:18 +03:00
Alex Lushpai
3a0ee18ceb
Merge pull request #100 from Frosin/master
fixed generate payment externalId
2019-05-24 12:04:05 +03:00
Alex Lushpai
a1f23559ad
Merge pull request #102 from KMityai/master
fix price in calculate_discount
2019-05-24 11:37:58 +03:00
Круглов Дмитрий
37d8eb660b fix price in calculate_discount 2019-05-16 12:05:56 +03:00
Alex Lushpai
c51fb0fd8a
Merge pull request #99 from Neur0toxine/master
Исправлена ошибка передачи заказов в retailCRM
2019-05-07 17:37:37 +03:00
Alex Lushpai
b7760f21e9
Merge pull request #101 from KMityai/master
Фикс подсчета цены товара
2019-05-07 17:37:22 +03:00
Круглов Дмитрий
901af9b767 fix calculate_discount 2019-04-25 15:24:16 +03:00
Круглов Дмитрий
65fcb2ed46 Merge branch 'master' of https://github.com/KMityai/woocommerce-module 2019-04-25 15:16:46 +03:00
Круглов Дмитрий
c7da9a0ca7 fix calculate_discount 2019-04-25 15:10:57 +03:00
Konstantin
2060b29e20 fixed generate payment externalId number 2019-04-22 15:39:14 +03:00
540564b5f9 Исправлена ошибка передачи заказов в retailCRM 2019-04-22 15:07:14 +03:00
Frosin
c79e3ab064
Merge pull request #1 from retailcrm/master
My
2019-04-19 15:00:18 +03:00
Alex Lushpai
78a299ac42
Merge pull request #98 from iyzoer/master
Bug fix
2019-04-16 14:09:25 +03:00
Akolzin Dmitry
09b8902e9c Bug fix 2019-04-16 13:37:53 +03:00
Alex Lushpai
b0524bd069
Merge pull request #95 from sergeygw1990/master
v3.5.0 add functional and translations
2019-04-15 17:00:14 +03:00
Sergey
d8bbbc2d8a fix send empty order number 2019-04-12 12:46:04 +03:00
Sergey
65ee374c94 v3.5.0 add functional and translations 2019-03-25 18:09:15 +03:00
Alex Lushpai
51a9ab3805
Merge pull request #94 from iyzoer/master 2019-03-25 15:19:47 +03:00
Akolzin Dmitry
9394cd3524 some improvements 2019-03-25 13:33:43 +03:00
Alex Lushpai
a04886c57f
Merge pull request #92 from Frosin/master
Fixed bug of sending incorrect externalId when editing payment
2019-03-20 16:32:27 +03:00
Alex Lushpai
70286efabc
Merge pull request #93 from iyzoer/makefile-fix
update Makefile
2019-03-20 16:17:01 +03:00
Akolzin Dmitry
b546a9c96c update Makefile 2019-03-20 15:56:37 +03:00
Konstantin
5055bb710c Fixed bug of sending incorrect externalId when editing payment 2 2019-03-14 15:58:36 +03:00
Konstantin
da3a828560 Fixed bug of sending incorrect externalId when editing payment 2 2019-03-14 15:54:25 +03:00
Konstantin
c82faf616a Fixed bug of sending incorrect externalId when editing payment 2019-03-13 11:46:58 +03:00
Alex Lushpai
af90b22c3e
Merge pull request #89 from iyzoer/master
Fixes: #87
2019-03-06 13:02:53 +03:00
Akolzin Dmitry
c836c2b293 fix for tests 2019-03-06 12:28:48 +03:00
Akolzin Dmitry
86e87be8d9 Fixes: #87 2019-03-06 10:09:12 +03:00
Alex Lushpai
48be7c2abb
Merge pull request #85 from sergeygw1990/master
v3.4.4
2019-02-26 14:52:56 +03:00
Sergey
bd683e9678 fixes #64 добавление префикса к внешнему id оплаты 2019-02-25 17:08:24 +03:00
Alex Lushpai
90b4e5f608
Merge pull request #84 from sergeygw1990/master
v3.4.3
2019-02-25 12:01:38 +03:00
Sergey
15c62b2cf9 изменение версии в аннотациях и правка changelog 2019-02-20 15:05:58 +03:00
Sergey
63f9a15564 v3.4.3 2019-02-15 15:23:06 +03:00
Alex Lushpai
3c0705315d
Merge pull request #83 from iyzoer/master
v3.4.2
2019-02-07 11:12:47 +03:00
Akolzin Dmitry
2713e089c8 Исправлен баг с изменением типа оплаты в WC, добавлен вывод неактивных типов оплаты в настройках плагина 2019-02-07 10:59:06 +03:00
Alex Lushpai
2db51fde4a
Merge pull request #82 from iyzoer/master
v3.4.1
2019-01-23 13:40:12 +03:00
Akolzin Dmitry
c7f39563a7 v3.4.1 2019-01-22 12:04:00 +03:00
Alex Lushpai
db89f73de5
Merge pull request #81 from iyzoer/master
v3.4.0
2019-01-17 16:10:54 +03:00
Akolzin Dmitry
c110053075 v3.4.0 2019-01-17 15:58:13 +03:00
Akolzin Dmitry
1866514483 Добавлен Daemon Collector 2019-01-16 15:33:41 +03:00
Alex Lushpai
5484c56063
Merge pull request #80 from iyzoer/master
v3.3.8
2018-12-17 11:58:02 +03:00
Akolzin Dmitry
7ddcafbaba v3.3.8 2018-12-14 16:51:30 +03:00
Alex Lushpai
65482423d6
Merge pull request #79 from sergeygw1990/master
v3.3.7
2018-12-11 18:26:02 +03:00
Sergey
8c1dfbd065 v3.3.7 2018-12-11 17:21:03 +03:00
Alex Lushpai
a5f77ec638
Merge pull request #78 from sergeygw1990/master
v3.3.6
2018-12-07 17:19:55 +03:00
Sergey
eb169b9fc8 v3.3.6 2018-12-07 13:02:37 +03:00
Alex Lushpai
cc3867d275
Merge pull request #77 from iyzoer/master
v3.3.5
2018-10-25 11:28:23 +03:00
Akolzin Dmitry
610acefcfb v3.3.5 2018-10-25 11:20:26 +03:00
Akolzin Dmitry
fead96c4ca Add delete options on delete plugin 2018-10-25 11:20:26 +03:00
Akolzin Dmitry
83b668deb3 Send activate/deactivate information to crm 2018-10-25 11:20:26 +03:00
Akolzin Dmitry
7c6e55c60a Added activate/deactivate in marketplace methods 2018-10-25 11:20:26 +03:00
Alex Lushpai
88363fa962
Merge pull request #76 from iyzoer/master
v3.3.4
2018-09-05 12:13:07 +03:00
Akolzin Dmitry
c92345b630 v3.3.4 2018-09-05 11:36:33 +03:00
Alex Lushpai
4cf127d3d0
Merge pull request #75 from sergeygw1990/master
v3.3.3
2018-08-30 16:38:49 +03:00
Sergey
da8cccb180 v3.3.3 2018-08-30 14:52:48 +03:00
Alex Lushpai
5d865a2528
Merge pull request #73 from iyzoer/master
v3.3.2
2018-08-22 16:22:56 +03:00
Akolzin Dmitry
4a90087e61 v3.3.2 2018-08-22 15:54:47 +03:00
Alex Lushpai
a0541627d7
Merge pull request #72 from sergeygw1990/master
adding a translations
2018-08-15 17:36:06 +03:00
Sergey
06843a2786 adding a translation to Spanish 2018-08-15 17:30:43 +03:00
Sergey
dc630be64f adding a translation to Spanish 2018-08-15 17:01:29 +03:00
Alex Lushpai
524d26aa72
Merge pull request #71 from iyzoer/master
Bug fix
2018-08-10 12:19:30 +03:00
Akolzin Dmitry
2202557584 Bug fix 2018-08-09 14:25:03 +03:00
Alex Lushpai
8e888dcc2f
Merge pull request #70 from iyzoer/master
v3.3.0
2018-08-06 16:52:51 +03:00
Akolzin Dmitry
bc498ff0a7 Update version 2018-08-06 16:26:04 +03:00
Akolzin Dmitry
c8c3b10329 v3.3.0 2018-08-06 16:24:22 +03:00
Alex Lushpai
8a28888c3a
Merge pull request #68 from iyzoer/master
Fix delivery and payment methods, add history tests
2018-07-19 13:02:09 +03:00
Akolzin Dmitry
4273c99886 Update readme and changelog, fix classes names 2018-07-19 12:59:31 +03:00
Akolzin Dmitry
59e9c69f5b Fix delivery and payment methods, add history tests 2018-07-19 12:16:51 +03:00
Alex Lushpai
f301687739
Merge pull request #67 from iyzoer/master
UA fix, add new filters
2018-06-19 12:51:52 +03:00
Akolzin Dmitry
9c71bc36a0 UA fix, add new filters 2018-06-19 10:03:49 +03:00
Alex Lushpai
cbc6eaef93
Update README.md 2018-06-18 13:57:59 +03:00
Alex Lushpai
d11af323d8
Merge pull request #65 from iyzoer/master
v3.1.0
2018-05-28 13:10:03 +03:00
Akolzin Dmitry
01d970af8f v3.1.0 2018-05-28 12:56:45 +03:00
Akolzin Dmitry
1b8328b941 Deploy script fix (#62)
* Deploy fix
* Bootstrap update
2018-04-27 16:28:28 +03:00
Alex Lushpai
56bc008af4
Update README.md 2018-04-27 12:39:23 +03:00
Akolzin Dmitry
e649a865d4 Fix bootstrap for tests (#61) 2018-04-27 12:06:09 +03:00
Alex Lushpai
60dbb8dd0e
Update README.md 2018-04-27 11:28:57 +03:00
Akolzin Dmitry
b5b6419ca4 v3.0.0 (#60)
* Tests
* Travis
* Deployment script
2018-04-27 11:19:02 +03:00
Alex Lushpai
7ad9ae7886
Merge pull request #57 from iyzoer/master
* Исправлена ошибка при активированном модуле без настроек
* Добавлен фильтр при формировании массива заказа
* Исправлена генерация icml с неполной картинкой товара
2018-04-10 11:09:27 +03:00
Akolzin Dmitry
b04a9ce701 Fix analytics code, added filter to process order, fix picture in icml 2018-04-10 11:04:38 +03:00
Alex Lushpai
8fd4a8fe35
Merge pull request #56 from iyzoer/master
Fix for php 5.3
2018-03-23 13:28:54 +03:00
Akolzin Dmitry
6a326e471f Fix for php 5.3 2018-03-22 16:53:44 +03:00
Alex Lushpai
580c341c99
Merge pull request #55 from iyzoer/master
Localization, universal analytics
2018-03-21 13:19:45 +03:00
Akolzin Dmitry
52218b0d22 Localization, universal analytics 2018-03-21 11:03:24 +03:00
161 changed files with 27942 additions and 5248 deletions

35
.docker/Dockerfile Normal file
View file

@ -0,0 +1,35 @@
FROM php:7.1-fpm
RUN apt-get update
RUN apt-get install -y zlib1g-dev libpq-dev git libicu-dev libxml2-dev libpng-dev libjpeg-dev libmcrypt-dev libxslt-dev libfreetype6-dev \
&& docker-php-ext-configure intl \
&& docker-php-ext-install intl \
&& docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \
&& docker-php-ext-install mysqli pdo pdo_mysql \
&& docker-php-ext-install zip \
&& docker-php-ext-install xml \
&& docker-php-ext-configure gd --with-png-dir=/usr/local/ --with-jpeg-dir=/usr/local/ --with-freetype-dir=/usr/local/ \
&& docker-php-ext-install gd \
&& docker-php-ext-install mcrypt \
&& docker-php-ext-install bcmath \
&& docker-php-ext-install soap \
&& docker-php-ext-install xsl \
&& docker-php-ext-install mbstring
RUN apt-get install -y gettext
RUN apt-get install -y subversion
RUN apt-get install -y wget
RUN wget -O /usr/bin/phpunit https://phar.phpunit.de/phpunit-7.phar && chmod +x /usr/bin/phpunit
RUN curl --insecure https://getcomposer.org/download/1.9.3/composer.phar -o /usr/bin/composer && chmod +x /usr/bin/composer
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash -
RUN apt-get install -y nodejs
# Set timezone
RUN rm /etc/localtime
RUN ln -s /usr/share/zoneinfo/Europe/Moscow /etc/localtime
RUN "date"
WORKDIR /code

12
.env-dist Normal file
View file

@ -0,0 +1,12 @@
# MySQL host and credentials
DB_NAME=wc_retailcrm_test
DB_USER=wc_retailcrm
DB_PASS=wc_retailcrm
DB_HOST=mysql
# WordPress and WooCommerce versions
WP_VERSION=5.3
WC_VERSION=3.9.0
# Enable this in order to pipe all module log messages (including debug ones) to STDOUT.
MODULE_LOGS_TO_STDOUT=0

159
.github/workflows/woo.yml vendored Normal file
View file

@ -0,0 +1,159 @@
name: woo
on:
push:
branches:
- '**'
tags-ignore:
- '*.*'
pull_request:
env:
DB_HOST: 127.0.0.1
DB_USER: root
DB_PASS: root
DB_NAME: wc_retailcrm_test
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
include:
# WordPress 5.3
#PHP 7.1 and 7.4
- php-version: '7.1'
wp: '5.3'
wc: '5.4.3'
coverage: 1
phpunit: 'phpunit:7.5.20'
- php-version: '7.1'
wp: '5.3'
wc: '6.4.0'
phpunit: 'phpunit:7.5.20'
- php-version: '7.4'
wp: '5.3'
wc: '5.4.3'
phpunit: 'phpunit:7.5.20'
- php-version: '7.4'
wp: '5.3'
wc: '6.4.0'
phpunit: 'phpunit:7.5.20'
# WordPress 6.0
# PHP 7.1 and 7.4
- php-version: '7.1'
wp: '6.0'
wc: '5.4.3'
phpunit: 'phpunit:7.5.20'
- php-version: '7.1'
wp: '6.0'
wc: '6.4.0'
phpunit: 'phpunit:7.5.20'
- php-version: '7.4'
wp: '6.0'
wc: '5.4.3'
phpunit: 'phpunit:7.5.20'
- php-version: '7.4'
wp: '6.0'
wc: '6.4.0'
phpunit: 'phpunit:7.5.20'
services:
mysql:
image: mysql:5.7
env:
MYSQL_ALLOW_EMPTY_PASSWORD: false
MYSQL_ROOT_PASSWORD: ${{ env.DB_PASS }}
MYSQL_DATABASE: ${{ env.DB_NAME }}
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5
steps:
- uses: actions/checkout@v2
- name: Setup PHP ${{ matrix.php-version }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer:2.1.14, ${{ matrix.phpunit }}
extensions: gd, mbstring, mysqli, zip, unzip, mcrypt, mysql, pdo_mysql, dom
coverage: xdebug
- name: Tool versions
run: |
php --version
composer --version
phpunit --version
- name: Install Polyfills dependency for WP 5.9 and 6.0
if: ${{ matrix.wp }} == '6.0' || ${{ matrix.wp }} == '5.9'
run: |
composer require --dev yoast/phpunit-polyfills --ignore-platform-reqs
- name: Install Woocommerce
env:
WP_VERSION: ${{ matrix.wp }}
WC_VERSION: ${{ matrix.wc }}
run: make install
- name: Run tests
env:
WP_VERSION: ${{ matrix.wp }}
WC_VERSION: ${{ matrix.wc }}
run: make test
- name: Coverage
env:
COVERAGE: ${{ matrix.coverage }}
if: env.COVERAGE == 1
run: |
make coverage
bash <(curl -s https://codecov.io/bash)
deploy:
needs: ['test']
if: success() && github.event_name == 'push' && github.repository_owner == 'retailcrm' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup PHP 7.2
uses: shivammathur/setup-php@v2
with:
php-version: '7.2'
tools: composer:v2
- name: Build release
run: |
git fetch origin --unshallow --tags
export LAST_TAG=`git describe --abbrev=0 --tags`
export VERSION=`cat VERSION`
export ARCHIVE_NAME=retailcrm-$VERSION.zip
export ARCHIVE_PATH="/tmp/$ARCHIVE_NAME"
export RELEASE_TAG=v$VERSION
export LAST_COMMIT=`git log --oneline --format=%B -n 1 HEAD | head -n 1`
echo RELEASE_TAG=$RELEASE_TAG >> $GITHUB_ENV
echo LAST_TAG=$LAST_TAG >> $GITHUB_ENV
echo LAST_COMMIT=$LAST_COMMIT >> $GITHUB_ENV
echo ARCHIVE_PATH=$ARCHIVE_PATH >> $GITHUB_ENV
echo ARCHIVE_NAME=$ARCHIVE_NAME >> $GITHUB_ENV
make build_archive
- name: Create Release
id: create_release
uses: actions/create-release@v1
if: env.LAST_TAG != env.RELEASE_TAG
env:
GITHUB_TOKEN: ${{ secrets.TOKEN }}
with:
tag_name: ${{ env.RELEASE_TAG }}
release_name: ${{ env.RELEASE_TAG }}
body: ${{ env.LAST_COMMIT }}
draft: false
prerelease: false
- name: Deploy
env:
SVNREPOURL: ${{ secrets.SVNREPOURL }}
USERNAME: ${{ secrets.USERNAME }}
PASSWORD: ${{ secrets.PASSWORD }}
run: |
make svn_clone
make svn_push
- name: Cleanup
if: env.LAST_TAG != env.RELEASE_TAG
run: make remove_dir

5
.gitignore vendored
View file

@ -1 +1,4 @@
/nbproject/private/
/nbproject/
/vendor/
.idea/
.env

View file

@ -1,15 +1,439 @@
## 2018-03-12 v.2.1.1
* Исправлена ошибка редактирования информации о клиенте
## 2025-02-19 4.8.21
* Fix version of module
## 2018-02-26 v.2.1.0
* Переработана механика генерации icml каталога товаров
* В icml каталог добавлена выгрузка налоговой ставки
* Исправлен пересчет итогов после изменения количества товара в RetailCRM
## 2025-02-18 4.8.20
* Add returned types for methods offsetExists, offsetSet, offsetUnset in WC_Retailcrm_Response
## 2018-02-19 v.2.0.6
* Исправлено возникновение Warning на PHP 7.2 при генерации каталога товаров
* Добавлена настройка выгрузки заказов из RetailCRM с определенными способами оформления
* Выгрузка изменений из RetailCRM осуществляется по sinceId
## 2025-02-04 4.8.19
* Optimization of order unloading
## 2018-02-02 v.2.0.5
* Исправлен неверный подсчет скидки на товары
## 2025-02-03 4.8.18
* Added additional parameters to GET requests
## 2025-01-21 4.8.17
* Fix deploy
## 2025-01-13 4.8.16
* Fix tests svn error
## 2024-12-19 4.8.15
* Fix uploading archive in CRM using console script
## 2024-11-07 4.8.14
* The method for determining the stock quantity has been optimized
## 2024-11-07 4.8.13
* Supports custom cart and checkout templates
## 2024-10-24 4.8.12
* Fixed multiple execution of order updates
## 2024-10-14 4.8.11
* Added additional parameters to GET requests
## 2024-10-08 4.8.10
* Fixed errors in catalog formation when changing synchronization parameters (sku/externalId)
## 2024-09-30 4.8.9
* Improvement of customer registration form in loyalty program
## 2024-09-30 4.8.8
* Fix tests svn externals definitions error
## 2024-09-26 4.8.7
* Logs refactoring
## 2024-09-26 4.8.6
* Optimized url-validator
## 2024-09-20 4.8.5
* Project testing has been updated
## 2024-09-13 4.8.4
* Updated work with promotional items when loyalty program is enabled
## 2024-09-11 4.8.3
* Added loyalty program coupon entry in the form by click
## 2024-08-26 4.8.2
* Fixed base file customization issue
* Added a hook to update the list of meta fields
## 2024-08-06 4.8.1
* Fix filtering of api query results
## 2024-07-15 4.8.0
* Added loyalty program
## 2024-06-27 4.7.9
* Fixed undefined array key number in order history
## 2024-06-26 4.7.8
* Added passing link field for abandoned baskets
## 2024-04-23 4.7.7
* Added transfer of services via ICML catalog
## 2024-04-22 4.7.6
* Support WP 6.5
## 2024-04-19 4.7.5
* Added automatic catalog generation when changing "Activate the binding via sku (xml)"
## 2024-02-29 4.7.4
* Fixed an error when transferring abandoned carts
## 2024-02-07 4.7.3
* Added filters after creating and updating an order
## 2024-01-31 4.7.2
* Fixed error with send address by history
## 2024-01-30 4.7.1
* Fixed the error of displaying the 'Add' button to mapping custom fields
## 2023-12-07 4.7.0
* Added support WooCommerce 8.2 (HPOS)
## 2023-11-20 4.6.14
* Fix module activation/deactivation
## 2023-10-26 4.6.13
* Fix not correct scoring product total price when product price is not number
## 2023-10-02 4.6.12
* Added currency validation when configuring the module
## 2023-08-31 4.6.11
* Added the ability to work with coupons through the CRM system
## 2023-07-19 4.6.10
* Abandoned cart transfer fix
## 2023-07-19 4.6.9
* Changed the logic of customer subscriptions to promotional newsletters
## 2023-06-27 4.6.8
* Added the ability to select CRM warehouses to synchronize the balance of offers
## 2023-06-27 4.6.7
* Fixed customer phone sending to crm when order will create by guest
## 2022-06-14 4.6.6
* Added handling of fatal errors when working with abandoned carts
## 2022-06-08 4.6.5
* Transferring WC meta fields to standard CRM order and customer fields
## 2022-05-30 4.6.4
* Optimizing unloading of stock
## 2022-05-29 4.6.3
* Types of deliveries and payments are displayed only for available stores
## 2022-05-17 4.6.2
* Modified method getting an address by history
## 2022-04-25 4.6.1
* The algorithm for getting the history of orders and customers has been optimized
## 2022-03-17 4.6.0
* Added functionality of abandoned carts
## 2023-03-02 4.5.4
* Fix display payment methods
## 2022-12-26 4.5.3
* Fix bug with products tax
## 2022-11-09 4.5.2
* Add validator for CRM URL
## 2022-11-09 4.5.1
* Correction of RAM overflow during ICMP product catalog generation.
## 2022-09-30 4.5.0
* Fix path for js scripts
* Migrating to PHP 7.0.
* Change logic work with ICML catalog: added streaming generation, added generators in the ICML generation process.
* Change logic work with address
## 2022-09-05 4.4.9
* Fix bug with product tax
## 2022-09-02 4.4.8
* Fix a critical bug when working with taxes
## 2022-08-10 4.4.7
* Add support for payment method on delivery
## 2022-08-06 4.4.6
* Add automatically upload ICML in CRM
* Add filter for changing ICML product information
* Important fix bug with shipping tax
## 2022-07-18 4.4.5
* Change logic work with delivery cost
* Add price rounding from WC settings
* Add functionality for changing the time interval for cron tasks
* Fix error with empty 'paidAt'
* Change processing history by sinceId
* Fix spanish accents processing in ICML
* Fix WA icon positioning
## 2022-05-26 4.4.4
* Add product description to ICML
* Fix fatal error using API without api_key
* Add priceType processing to CRM order by history
* Add method in API V5 and delete use another version
* Fix error with integration payments
* Fix bug with changing order status by history
## 2022-03-24 4.4.3
* Fix bug in updating order number by history
* Add multiple image transfer in ICML
* Add filters for custom fields
* Fix bug with create/update customer
## 2022-02-24 4.4.2
* Delete deprecated API V4. Refactoring API V5 and history getting method
* Fix bug with use xmlId
* Add order number transfer CMS -> CRM by history
* Add documentation for registering client functionality
* Delete legacy code for update customer name and surname
## 2022-01-17 4.4.1
* Added functionality to skip some orders statuses
* Improved the create/update method when registering customers
* Add mapping metadata fields in settings
* Improvement of the user interface, plugin operation, fix bugs
## 2021-12-15 4.4.0
* Migrating to PHP 5.6. We tested the module, improved performance, security and test coverage.
* Add validate countryIso. Fix bug with duplicate customer address
* Fix bugs in history
* Fix PHP warning and deprecated
* Add documentation for module
## 2021-08-30 4.3.8
* Updated logic work address
* Added transfer of the client's comment to the WC order
* Added the ability to skip inactive statuses in settings module
* Deleted option 'Do not transmit the cost of delivery'
* Fix bug in archive upload
## 2021-08-04 4.3.7
* Fixed an error with incorrect unloading of archived clients
* Removed the "Client roles" option from the module settings
## 2021-07-27 4.3.6
* Updated the presentation of the module settings
* Fixed a bug connected with adding variable products to the catalogue as usual products
## 2021-07-22 4.3.5
* Updated version in Marketplace
## 2021-07-22 4.3.4
* Minor bugs were fixed
## 2021-07-21 4.3.3
* Redesigned the WhatsApp icon
* Added the ability to enable extended logging in the module settings
* Added the"Debug info" block
* Improved ICML catalogue generation
* Added batch export of archived orders and customers
## 2021-07-05 4.3.2
* Minor bugs were fixed
## 2021-07-02 4.3.1
* Rebranding of RetailCRM module --> Simla.com
## 2021-06-30 4.3.0
* Rebranding of RetailCRM module --> Simla.com
* Fixed a bug in the "Activate link by sku (xmlId)" option
* Added the ability to use the WhatsApp chat link on the site
* Fixed minor bugs in the history and generation of the ICML catalogue
## 2021-03-15 4.2.4
* Added a display of the total number of variable products
* Added validation for the order creation date
* Added validation for orders with auto-draft status
* Updated WP and WC versions in local tests
* Fixed a bug in the "Transfer of order number" option
## 2021-01-20 4.2.3
* Updated version in the Marketplace
## 2020-12-17 4.2.2
* RetailCRMs redesign
## 2020-12-15 4.2.1
* RetailCRMs redesign
## 2020-12-02 4.2.0
* Fixed a bug connected with receiving the date of creation of an unregistered user
* Changed the logic of payments by deleting the "Transfer of payment amount" option
* Fixed the “shipping” address bug. If it is empty, use a “billing” address
* Added Spanish and English translations of the main page
* Fixed a bug connected with deleting products when using the "Activate link by sku (xmlId)" option
## 2020-09-21 4.1.5
* Fixed a bug connected with transferring emails. Before being sent to RetailCRM, emails are always converted to lower case.
* Fixed a bug connected with transfers of payments of no amount
* Improved the work of discounts in the order
## 2020-08-27 4.1.4
* Added translations to transfer the prime cost of the delivery
* Fixed a bug connected with an incorrect displaying the Live Chat on the login page
## 2020-08-20 4.1.3
* Added translations for the option of customers role
* Added the ability to optionally transfer the prime cost of delivery
## 2020-08-20 4.1.2
* Fixed a bug connected with missing data books settings
## 2020-08-08 4.1.1
* Added a setting for selecting customer roles for upload to RetailCRM
## 2020-08-05 4.1.0
* Added the ability to connect the Live Chat
## 2020-07-28 4.0.1
* Fixed transfer of payment status
## 2020-06-18 4.0.0
* Support for corporate customers
* Support for changing a customer in an order
## 2020-06-18 3.6.4
* Passing the region / state / province name instead of the code
## 2020-06-10 3.6.3
* Improved order data updating by history
## 2020-04-13 3.6.2
* Fixed a bug that led to duplication of some customers
## 2019-03-31 3.6.1
* Fixed a bug connected with generating a product catalogue
## 2020-03-25 3.6.0
* Added the setting for transferring the payment amount
## 2019-10-07 3.5.4
* Added the ability to process identical product items
## 2019-04-22 3.5.2
* Fixed a bug connected with exporting orders to RetailCRM
* Fixed a translation error
## 2019-04-16 3.5.1
* Fixed a bug connected with plugin activation
## 2019-03-06 3.5.0
* Added a setting to deactivate uploading order changes to RetailCRM
* Added a setting for activating SKU exporting to xmlId and linking products by the “xmlId” field
* Added a setting for transferring order numbers to RetailCRM
## 2019-03-06 3.4.5
* Fixed a bug connected with adding a discount when decreasing the quantity of a product
* Moved the initialization of the settings form after the initialization of all plugins
## 2019-02-25 3.4.4
* Added generation of a unique id to the externalId of the payment being sent
## 2019-02-15 3.4.3
* Fixed saving of the payment type when creating an order when processing the history of changes on the WC side
* Fixed saving of the payment type when changing an order when processing the history of changes on the WC side
* Fixed connecting files using the checkCustomFile method
## 2019-02-07 3.4.2
* Fixed change of payment type on the WC side
* Added inactive payment types in the settings
* Removed external code generation of a customer
## 2019-01-22 v3.4.1
* Fixed archive export of customers
## 2019-01-17 v3.4.0
* Added Daemon Collector setting
* Changed the logic of data transfer for orders and customers. Delivery data is transferred to the order, payment data to the customer card.
## 2018-12-14 v3.3.8
* Added export of images for product categories to ICML
## 2018-12-11 v3.3.7
* Fixed a bug connected with activation
## 2018-12-06 v3.3.6
* Fixed module activation in RetailCRMs marketplace when using api v4
* Expanded configuration for sending
## 2018-10-25 v3.3.5
* Added module activation in RetailCRMs marketplace
## 2018-08-30 v3.3.4
* Fixed a bug connected with zeroing the quantity of the product in the WC order
## 2018-08-30 v3.3.3
* Added buttons to go to the plugin settings and to generate a catalogue in the WordPress admin panel
* Added transfer of payment status on v5
## 2018-08-22 v3.3.2
* Removed check for the existence of tasks in wp-cron on every page loading
* Tasks in wp-cron are now activated in the plugin settings
## 2018-08-09 v3.3.1
* Fixed a bug connected with duplication of products from WC
## 2018-08-06 v3.3.0
* Reworked the mechanics of handling change history (added merging of all changes)
* Added filter "retailcrm_history_before_save" to modify the history data
## 2018-07-19 v3.2.0
* Improved the method for selection of data on deliveries and payments in the plugin settings. (All types of payments are selected, not just allowed ones. Deliveries that are created for individual zones are transferred as services)
* Fixed bugs when processing change history
* Added tests for processing history of changes
## 2018-06-19 v3.1.1
* Fixed the code for sending data to UA
* Added new filters, and added transfer of new parameters to existing ones
## 2018-05-28 v3.1.0
* Added the ability to manually export orders to the plugin settings interface
* Fixed initialization of the UA code for sending orders on all pages
## 2018-04-26 v3.0.0
* Added tests
* Refactoring of the code
* Webhooks added
## 2018-03-22 v2.1.4
* Fixed a bug connected with the activated module without settings
* Added a filter when forming an order array
* Fixed generation of icml with incomplete product picture
## 2018-03-22 v2.1.3
* Fixed a bug on php5.3
## 2018-03-21 v2.1.2
* Added plugin localization
* Added integration with UA
## 2018-03-12 v2.1.1
* Fixed a bug connected with editing customer information
## 2018-02-26 v2.1.0
* Reworked mechanics of generating icml product catalog
* Added tax rate export to icml catalog
* Fixed recalculation of totals after changing the quantity of a product in RetailCRM
## 2018-02-19 v2.0.6
* Fixed occurrence of a Warning in PHP 7.2 when generating a product catalog
* Added a setting for exporting orders from RetailCRM with certain order methods
* Changes are exported from RetailCRM by sinceId
## 2018-02-02 v2.0.5
* Fixed an incorrect calculation of discounts for products

51
Makefile Normal file
View file

@ -0,0 +1,51 @@
ROOT_DIR=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
VERSION = `cat $(ROOT_DIR)/VERSION`
ARCHIVE_NAME = '/tmp/retailcrm-'$(VERSION)'.zip'
.PHONY: test
svn_clone:
sudo apt install subversion
mkdir -p /tmp/svn_plugin_dir
svn co $(SVNREPOURL) /tmp/svn_plugin_dir --no-auth-cache
svn_push: /tmp/svn_plugin_dir
if [ ! -d "/tmp/svn_plugin_dir/tags/$(VERSION)" ]; then \
svn delete /tmp/svn_plugin_dir/trunk/*; \
rm -rf /tmp/svn_plugin_dir/trunk/*; \
cp -R $(ROOT_DIR)/src/* /tmp/svn_plugin_dir/trunk; \
svn copy /tmp/svn_plugin_dir/trunk /tmp/svn_plugin_dir/tags/$(VERSION) --no-auth-cache; \
svn add /tmp/svn_plugin_dir/trunk/* --force; \
svn add /tmp/svn_plugin_dir/tags/$(VERSION)/* --force; \
svn ci /tmp/svn_plugin_dir -m $(VERSION) --username $(USERNAME) --password $(PASSWORD) --no-auth-cache; \
fi
remove_dir:
rm -rf /tmp/svn_plugin_dir
compile_pot:
msgfmt resources/pot/retailcrm-ru_RU.pot -o src/languages/retailcrm-ru_RU.mo
msgfmt resources/pot/retailcrm-es_ES.pot -o src/languages/retailcrm-es_ES.mo
install:
sudo apt install subversion
mkdir -p coverage
bash tests/bin/install.sh $(DB_NAME) $(DB_USER) $(DB_HOST) $(DB_PASS) $(WP_VERSION) $(WC_VERSION)
test:
phpunit -c phpunit.xml.dist
local_test:
bash tests/bin/install.sh $(DB_NAME) $(DB_USER) $(DB_HOST) $(DB_PASS) $(WP_VERSION) $(WC_VERSION)
phpunit -c phpunit.xml.dist
run_tests:
docker-compose --no-ansi up -d --build mysql
docker-compose --no-ansi run --rm --no-deps app make local_test
docker-compose down -v
coverage:
wget https://phar.phpunit.de/phpcov-2.0.2.phar && php phpcov-2.0.2.phar merge coverage/ --clover coverage.xml
build_archive:
zip -r $(ARCHIVE_NAME) ./src/*

View file

@ -1,6 +1,17 @@
woocommerce-module
[![Build Status](https://github.com/retailcrm/woocommerce-module/workflows/woo/badge.svg)](https://github.com/retailcrm/woocommerce-module/actions)
[![Coverage](https://img.shields.io/codecov/c/gh/retailcrm/woocommerce-module/master.svg?logo=github)](https://codecov.io/gh/retailcrm/woocommerce-module)
[![GitHub release](https://img.shields.io/github/release/retailcrm/woocommerce-module.svg?logo=codecov)](https://github.com/retailcrm/woocommerce-module/releases)
[![PHP version](https://img.shields.io/badge/PHP->=7.0-blue.svg?logo=php)](https://php.net/)
Woocommerce-module
==================
Модуль интеграции с [retailCRM](http://retailcrm.ru)
Integration plugin for WooCommerce and [Simla.com](https://www.simla.com)
Информация о [кастомизации](https://github.com/retailcrm/woocommerce-module/wiki/%D0%9A%D0%B0%D1%81%D1%82%D0%BE%D0%BC%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F-%D0%B8%D0%BD%D1%82%D0%B5%D0%B3%D1%80%D0%B0%D1%86%D0%B8%D0%BE%D0%BD%D0%BD%D0%BE%D0%B3%D0%BE-%D0%BF%D0%BB%D0%B0%D0%B3%D0%B8%D0%BD%D0%B0)
[Documentation](https://docs.retailcrm.ru/Users/Integration/SiteModules/WooCommerce) page
[Customization](https://github.com/retailcrm/woocommerce-module/wiki/%D0%9A%D0%B0%D1%81%D1%82%D0%BE%D0%BC%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F-%D0%B8%D0%BD%D1%82%D0%B5%D0%B3%D1%80%D0%B0%D1%86%D0%B8%D0%BE%D0%BD%D0%BD%D0%BE%D0%B3%D0%BE-%D0%BF%D0%BB%D0%B0%D0%B3%D0%B8%D0%BD%D0%B0) info
#### Local testing
To local testing run `make run_tests`

1
VERSION Normal file
View file

@ -0,0 +1 @@
4.8.21

22
composer.json Normal file
View file

@ -0,0 +1,22 @@
{
"name": "retailcrm/woocommerce-retailcrm",
"description": "Integration plugin for WooCommerce & RetailCRM",
"type": "wordpress-plugin",
"authors": [
{
"name": "RetailDriver LLC",
"email": "integration@retailcrm.ru"
}
],
"minimum-stability": "dev",
"require": {
"ext-simplexml": "*",
"ext-xmlwriter": "*"
},
"require-dev": {
"ext-json": "*",
"ext-mbstring": "*",
"phpunit/phpunit": "7.*",
"yoast/phpunit-polyfills": "1.x-dev"
}
}

1550
composer.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,45 @@
### Настройки брошенных корзин
В версии 4.6.0 добавлен функционал выгрузки брошенных корзин.
Для активации необходимо включить опцию ***Выгружать брошенные корзины***
### Брошенные корзины
Брошенная корзина - клиент заходит на сайт, добавляет/удаляет товары в корзине, а затем завершает визит без оформления заказа.
> Важно:
> * Корзины выгружаются только для зарегестрированных клиентов;
> * Для корректной работы корзин, один API ключ = один магизн в CRM;
При разработке функционала, ориентировались на хуки корзины в WooCommerce:
* Хуки для метода **set_cart**:
* **woocommerce_add_to_cart** - добавление товара в корзину;
* **woocommerce_after_cart_item_quantity_update** - изменение кол-во товара в корзине;
* **woocommerce_cart_item_removed** - удаление товара с корзины;
* Хуки для метода **clear_cart**:
* **woocommerce_cart_emptied** - полная очистка корзины. Также срабатывает при создании заказа;
Корзина создается в CRM, при первом добавлении товара.
**Фильтры:**
> retailcrm_process_cart - позволяет кастомизировать данные корзины.
**Пример использования:**
```php
<?php
add_filter('retailcrm_process_cart', 'process_crm_cart');
function process_crm_cart($crmCart, $cartItems)
{
$crmCart['updatedAt'] = null;
return $crmCart;
}
```
/
**Возможные API ошибки:**
* WC_Retailcrm_Client_V5::cartGet : Error: [HTTP-code 404] - корзина не найдена в CRM;

View file

@ -0,0 +1,7 @@
### Настройки каталога
В настройке представлены статусы товаров в WooCommerce *(Товары -> карточка товара -> блок Опубликовано)*. Из товаров, чей статус будет соответствовать выбранному, будет сгенерирован ICML-файл каталога. Для выбора необходимо поставить галочку напротив нужного статуса и сохранить настройки.
Статус видимости товара Личное, также относится к статусам товара "Статус: Опубликовано как личное". Анализ статусов товара был произведен в задаче [#76054](https://redmine.retailcrm.tech/issues/76054)
![](https://lh3.googleusercontent.com/A64aLvFUecO7kd73gEH0SbfQsYkhjDfOl0DRmcx6FsMfAWX7Z5DFX_Y5_lHnm7z3D3SpKzNHOFINI26mlihBNbqsuV_8Kd0S3QOqWt32Pv2AvrDWJQc44eG03J5wkyz2VL3BXV06=s0)![](https://lh6.googleusercontent.com/aG6m6-TGpU4kWPIVeMQ_EfN1kBsG0l3ISVRx9CU1KlvZdZ4n8NkhkM-DFLZctmQXqKi65Hv83paSZf9jK1mCj7QWCUn1syfvBme8kjYGzPBHH-3feSJE-G8dKtLTBqwvER4RbLON=s0)

52
doc/1.Setup/Cron tasks.md Normal file
View file

@ -0,0 +1,52 @@
### Настройки cron задач
В версии 4.4.5 добавлен функционал для изменения интервалов времени выполнения cron задач.
Для изменения интервала времени необходимо с помощью фильтра **retailcrm_add_cron_interval** добавить пользовательский интервал. Затем изменить интервал для cron задач с помощью фильтра **retailcrm_cron_schedules**.
Кастомизацию необходимо добавить на сервере в директорию wp-content -> mu-plugins -> mu-simla.php. После добавления кастомизации в настройках модуля необходимо очистить старые cron задачи.
Перейдите в настройки, откройте "Отладочная информация" и нажмите на кнопку "Очистить cron задачи", появится окно с сообщением об успешной очистке, интервалы будут применены.
Если необходимо вернуть стандартные интервалы, то удаляем кастомизацию и в настройках так же очищаем старые cron задачи.
**Интервалы по умолчанию:**
```php
[
'icml' => 'three_hours',
'history' => 'five_minutes',
'inventories' => 'fiveteen_minutes',
'loyalty_upload_price' => 'four_hours'
]
```
> Важно! При использовании фильтра **retailcrm_cron_schedules**, можно использовать ключи: 'icml', 'history', 'inventories'.
**Фильтры:**
> retailcrm_add_cron_interval - позволяет добавить пользовательский интервал времени.
> retailcrm_cron_schedules - позволяет изменить интервал времени для cron задач.
**Пример использования:**
```php
<?php
add_filter('retailcrm_add_cron_interval', 'add_cron_interval');
function add_cron_interval($schedules)
{
return ['two_minutes' => [
'interval' => 120, // seconds
'display' => __('Every 2 minutes')
]
];
}
add_filter('retailcrm_cron_schedules', 'change_cron_tasks');
function change_cron_tasks($cronTasks)
{
$cronTasks['history'] = 'two_minutes';
return $cronTasks;
}
```

View file

@ -0,0 +1,37 @@
### Пользовательские поля
В настройках модуля есть возможность настроить передачу пользовательских полей из CMS в CRM и обратно. Для этого необходимо настроить соответствие между мета полями CMS и пользовательскими полями CRM.
Слева располагаются мета поля CMS, справа пользовательские поля CRM. В выпадающем списке слева нужно выбрать мета поля CMS, в выпадающем списке справа нужно выбрать пользовательское поле CRM и таким образом сопоставить их.
Пользовательские поля разбиты на две группы для заказов "Custom fields for order" и для клиентов "Custom fields for customer".
При нажатии на кнопку "Custom fields for order/customer" будет добавлены новые списки для выбора. Количество списков не может быть больше количества мета полей CMS или пользовательских полей CRM. Поэтому когда добавили максимальное число списков, кнопка добавления пропадает. Для каждой группы предусмотрена своя кнопка добавления.
Обратите внимание, что значения не должны повторяться, т.е. для двух мета полей CMS, не может быть выбрана одно и то же пользовательское поле CRM. Необходимо выбирать 1 к 1. Если вдруг выбрать значение которое уже используется, будет показано предупреждающее уведомление, выбранный список начнет мигать красным и в этом списке будет установленно значение по умолчанию "Select value".
Сопоставление можно удалить, если нажать на крестик справа от списка пользовательский полей CRM.
Для сохранения выбранных сопоставлений необходимо нажать на кнопку "Сохранить изменения" в конце страницы настроек.
Также в списке значений для сопоставления выводятся **только Активные в CRM пользовательские поля**.
На стороне CRM доступные пользовательские поля можно увидеть перейдя в **Настройки - Системные - Пользовательские поля.**
В версии 4.6.5 добавлен функционал для передачи мета полей CMS в некоторые стандартные поля CRM.
Список стандартных полей CRM доступных для передачи данных:
* Имя
* Фамилия
* Телефон
* E-mail
* Адрес
* Город
* Индекс
* Регион
* Теги (доступно только для клиентов)
* Комментарий клиента (доступно только для заказов)
* Комментарий менеджера (доступно только для заказов)
**Важно! Передача мета полей WC в стандартные поля CRM реализована только из CMS в CRM, обратная синхронизация не предусмотрена.**

View file

@ -0,0 +1,16 @@
### Режим отладки
Служит для включения расширенного логирования, предназначен для разработчиков. Для просмотра логов необходимо перейти *WooCommerce -> Статус -> Журналы* и там смотреть на файлы **retailcrm_debug**.
### Отладочная информация
Вывод основной информации о работе модуля. На данный момент выводит время последнего запуска крон-задач:
<br/><br/>**History** - показывает время и дату запуска обновления по истории *(данные будут отображены, если активна опция “Загрузка данных из CRM”)*.
**Icml** - показывает время и дату, когда последний раз срабатывала команда генерации каталога по WP-CRON *(информация будет отображена, если активна опция “Генерация ICML каталога товаров с помощью wp-cron”)*.
**Inventories** - показывает время и дату запуска получения остатков по товарам из CRM в WooCommerce *(данные будут отображены, если активна опция “Настройка управления остатками”)*.
Добавлено в версии 4.3.3.
![](https://lh4.googleusercontent.com/_tdjxonwGp40uafwaEK__QenxJurbNPsvlMu8Pw3eZk_vN6-KzKB8RWeYXwJqYPRfjzTZnvT0TGYVBry-4JWXTzQuDt9zDIBWQO0lnrUSHP84PxoomFiDPIVHsRIZ7gHzKdgmDUc=s0)

View file

@ -0,0 +1,24 @@
### Способы доставки
Необходимо настроить соответствие между доставками WooCommerce и CRM.
Слева располагаются доставки WooCommerce, справа CRM. В выпадающем списке справа нужно выбрать справочник доставки в CRM, который будет соответствовать доставке в WooCommerce. Данное сопоставление требуется **выбрать для всех доставок**. Если ни одно из предложенных значений не подходит, то необходимо создать в CRM новый справочник доставки, либо пропустить сопоставление.
После создания новой доставки *(справочника)* на стороне CRM, в WooCommerce необходимо обновить страницу настроек *(перед этим не забудьте сохранить указанные ранее настройки)*, чтобы в предложенных значениях появился новый созданный справочник.
Если сопоставление пропустить и не выбрать ни одного значения, в этом случае, если в заказе в WooCommerce будет выбрана данная доставка, то в CRM в заказе “Тип доставки” **будет не указан**.
Обратите внимание, что значения не должны повторяться, т.е. для двух доставок WooCommerce, не может быть выбрана одна и та же доставка *(справочник доставки)* в CRM. Необходимо выбирать 1 к 1.
**1 доставка WooCommerce = 1 доставка CRM**. Иначе будут наблюдаться ошибки в работе модуля.
Также в списке значений для сопоставления выводятся **только Активные в CRM справочники**.
Доставки на стороне WooCommerce можно посмотреть перейдя в *WooCommerce - Настройки - вкладка Доставка - Зоны доставки*; Для каждой зоны доставки будет перечень своих транспортных компаний.
![](https://lh3.googleusercontent.com/KVMF00-y5AvkDLQLEhwRWinDxfV9limBxlpPO16_SKjsmYTYwsbQ06EI3_avJwQlO6jDWnJ8Z2QqhHyqw166gHY__dAWb1dgpLwYY-vmxnJyX049mHKdJpBLmfnlYmRh8ghed3Qh=s0)
На стороне CRM доступные доставки можно увидеть перейдя в *Настройки - Справочники - Типы доставок.*
![](https://lh3.googleusercontent.com/G4DvOhXuLXTPmyHmeUkAgdgbRHBn2ZJH9vnJaYpOQbHrKIpaBjddQ83AdjLfukRUnxiLZ4bLbrvCRNqsJPJQ1xk0oHpfnbfWyVMrt1Tem9fT_6GplLYFBBDuuK0pXDdXFw9A6rV1=s0)

View file

@ -0,0 +1,57 @@
### Генерация ICML
Данная опция позволяет сгенерировать каталог вручную.
Для активации необходимо нажать на кнопку и дождаться сообщения о завершении генерации. Сгенерированный каталог будет доступен по ссылке https://yoursite.com/simla.xml
Чтобы проверить, что команда по генерации каталога отработала успешно, можно перейти по ссылке и сравнить дату генерации каталога с текущей:
![](https://lh3.googleusercontent.com/Z6qwxSjGA9AHjaDi3RgbJyeQRhvXAkXRvtzXmEJzVdMpwQ2Rc4N0FM9YFhMWYTL-dwoxKmSYgPsJU68TfCer_og-BmnUruKJMJlmjIM7suz42OMCJFH3cwPoKfbW66AYzUL3UZTd=s0)
Ссылку на каталог требуется указать в настройках магазина в CRM. Для его загрузки в CRM необходимо активировать опцию “Загрузить каталог из ICML сейчас” и сохранить настройки.
![](https://lh4.googleusercontent.com/oPPNIbm11rgYUbeWIKkyp1XdqC47_N-iTh-jp0C3V5QrRemD-0Gco6pholEP0HKKmkZCpag1n7oiMNvPXiUh5yjSF2DSLtX5_wJPqwQ97XsJDdFdbAPsTYU4LpMdSlPyfs-hviw7=s0)
### Генерация ICML каталога товаров с помощью wp-cron
Данный функционал позволяет генерировать каталог в автоматическом режиме с помощью WP-CRON, в этом случае не потребуется каждый раз при необходимости обновить каталог, заходить в настройки и запускать его загрузку вручную.
Каталог генерируется **раз в 3 часа**.
### Синхронизация остатков и связь товаров
Функционал служит для **идентификации товаров по SKU (xmlID)**. Для активации необходимо поставить галочку.
После активации опции при генерации каталога в нем появится параметр xmlID *(артикул)*.
С версии 4.7.5 после активации/деактивации опции и сохранении настроек, каталог будет сгенерирован автоматически.
**xmlID** - внешний идентификатор товара, элемент не является обязательным. В случае, если интернет-магазин использует выгрузку номенклатуры товаров из складской системы *(1С, МойСклад)*, то значение этого элемента соответствует идентификатору товара в данной системе. Активируется, когда клиенты используют Woocommerce + MC/1C.
### Недавние обновления:
**В версии 4.4.4** добавлен функционал передачи описания товара в каталог. В настройках каталога необходимо выбрать, какое описание передавать краткое или полное. По умолчанию передается полное описание товара.
Поле description(описание) выводится в карточке товара, так же его можно использовать в twig-шаблонах. Например, для вывода в печатных формах.
Пример получения описание торгового предложения:
```twig
{% for availableOrderProduct in order.availableOrderProducts %} {{ availableOrderProduct.getOffer().getDescription() }} {% endfor %}
```
**В версии 4.4.6** добавлен фильтр:
> retailcrm_process_offer - позволяет изменить данные товара, перед записью в ICML каталог.
**Пример использования:**
```php
<?php
add_action('retailcrm_process_offer', 'changeProductInfo', 10, 2);
function changeProductInfo($productData, $wcProduct)
{
$productData['name'] .= 'Test';
return $productData;
}
```

View file

@ -0,0 +1,7 @@
### Загрузка данных из Simla.com
Функционал необходим для выгрузки заказов и изменений по ним, изменений по существующим клиентам из CRM в WooCommerce.
**Новые клиенты *(без заказов)*** из CRM в WooCommerce **не выгружаются**.
История грузится **раз в 5 минут**. Для активации необходимо поставить галочку.

View file

@ -0,0 +1,21 @@
### Настройки выгрузки
Данный функционал позволяет выгрузить архивные заказы и клиентов из WooCommerce в CRM.
Для активации нужно нажать “Выгрузить”, появится progress bar, отображающий процесс выгрузки заказов. После окончания выгрузки, должно появиться сообщение об успешном окончании. В момент выгрузки заказов, **страницу браузера закрывать нельзя**.
Ранее, модуль мог выгрузить не более 700-800 заказов, после реализации задачи [#70113](https://redmine.retailcrm.tech/issues/70113), механика была изменена, сейчас **выгрузка происходит пошагово**, шаг равен 50 элементам *(заказам/клиентам)*.
Если при выгрузке возникнут проблемы с каким-либо заказом/клиентом, то он будет пропущен и выгрузка пойдет дальше. Информацию по заказу/клиенту, с которым возникли проблемы при выгрузке, можно получить только включив расширенное логирование.
### Выгрузка заказов по идентификаторам
Данный функционал позволяет выгружать заказы в CRM, если они по каким-либо причинам не выгрузились автоматически. Рекомендуется выгружать таким способом, **не более 50 заказов**.
Для выгрузки заказов в CRM требуется ввести в поле “Идентификаторы заказов” ID заказов и нажать на кнопку “Выгрузить”. После выгрузки должно появится сообщение об успешном завершении выгрузки.
Идентификаторы выгружаемых заказов требуется **разделять запятой**.
**ID заказа = номеру заказа**.
![](https://lh3.googleusercontent.com/RTHSa4Uam7kPfIrrjkEa3-OoNKZzs2lHK_QAGz5qw8OnEEI-MSITY9aEaqCUMNhfP-TdwzvFTM3Md9Rxb3resSDY3EI7PaHTXC6jaHc4Pex2dQWyqOs3VoLlH7qRL_o-wFh7vy9q=s0)

View file

@ -0,0 +1,15 @@
### API URL
Требуется ввести полный адрес системы (https://test.retailcrm.ru). Для ввода в поле можно использовать как адрес RetailCRM, так и Simla.com.
### API ключ
Ключ должен иметь доступ только к одному магазину (**1 магазин = 1 ключ**). Также для ключа должны быть разрешены все методы API *(рекомендуется)* в настройках системы.
![](https://lh3.googleusercontent.com/GQlvq7WurtSreRy05xxkNw-yEPaIyISWgYrDUSDurxr4wFx_EMocT8MjRa4HktFyXnBiwsVL2a_wQFtXSCNEjJvhryUpvcNeR8xKpQBOpKGxtm3zFrJxnud8-ahwhIOcpmAZS_Be=s0)
![](https://lh5.googleusercontent.com/j27aD9LvAlFsfV0S2f_nCkRQL4iQR40dpw35K6yyb7latJScYFwP3GQceaLx5NfFYSBhhKHmrm8XH3v2KA57hujoJ_o99l104y_hEE6P08kV6Hi-ocWx28YAFN1Jrt7zZIk23bTf=s0)
***При первичной настройке модуля, дальнейшие опции отображаются после корректного ввода API URL и API ключа!***

View file

@ -0,0 +1,5 @@
### Способы оформления заказа
Опция предоставляет выбор, заказы с какими способами оформления, перечисленными в CRM, будут выгружаться из CRM в WooCommerce. Для активации необходимо выбрать нужные способы и сохранить настройки. *Чтобы выбрать только некоторые из способов, нужно зажать Ctrl и выбрать левой кнопкой мыши необходимые способы.*
*Обратите внимание, в опции перечислены только те способы оформления заказа, которые Активны в CRM.*

View file

@ -0,0 +1,34 @@
### Статусы
В данной опции необходимо настроить соответствие между статусами WooCommerce и CRM.
Слева располагаются статусы WooCommerce, справа CRM. В выпадающем списке справа нужно выбрать статус в CRM, который будет соответствовать статусу в WooCommerce. Данное сопоставление требуется выбрать **для всех статусов**. Если ни одно из предложенных значений **не подходит**, с версии 4.4.1 вы можете выбрать "Не отправлять в CRM", либо **создать в CRM новый статус**
После создания нового статуса на стороне CRM, в WooCommerce необходимо обновить страницу настроек *(перед этим не забудьте сохранить указанные ранее настройки)*, чтобы в предложенных значениях появился новый созданный статус.
Также в списке значений для сопоставления выводятся только **Активные в CRM статусы**.
Обратите внимание, что **значения не должны повторяться**, т.е. для двух статусов WooCommerce, не может быть выбран один и тот же статус в CRM. Необходимо выбирать 1 к 1.
**1 статус в WooCommerce = 1 статус в CRM**. Иначе будут наблюдаться ошибки в работе модуля.
По умолчанию в WooCommerce присутствуют **7 стандартных статусов**:
**В ожидании оплаты** заказ получен *(не оплачен)*.
**Не удался** платеж не удался или был отклонен *(неоплаченный)*.
**Обработка** оплата получена, а остатки по товарам были уменьшены заказ ожидает выполнения. Все заказы товаров требуют обработки, кроме заказов цифровых и загружаемых товаров.
**Выполнен** заказ выполнен и завершен не требует дальнейших действий.
**На удержании** ожидается платеж остатки по товару уменьшены, но необходимо подтвердить оплату. В данном статусе нет возможности вернуть остаток по товару в WooCommerce.
**Отменен** заказ отменен не требует дальнейших действий. При указании данного статуса заказу, остаток по товару, который был списан, автоматически возвращается.
**Возвращен** возврат по заказу - в данном статусе есть возможность вернуть остаток по товару, но для этого требуется перейти в WooCommerce и явно указать флаг возврата и сумму по товару, которая будет возвращена. При установлении в CRM статуса, соответствующего статусу возврата в WooCommerce, списанные остатки по товару **не вернутся автоматически в запас**. Для того, чтобы их вернуть, требуется перейти в админ панель WooCommerce, зайти в заказ в блоке с перечнем товаров, нажать “Возврат”
Возвращен возврат по заказу - в данном статусе есть возможность вернуть остаток по товару, но для этого требуется перейти в WooCommerce и явно указать флаг возврата и сумму по товару, которая будет возвращена. При установлении в CRM статуса, соответствующего статусу возврата в Woo, списанные остатки по товару не вернутся автоматически в запас. Для того, чтобы их вернуть, требуется перейти в админ панель Woo, зайти в заказ в блоке с перечнем товаров, нажать “Возврат”
![](https://lh4.googleusercontent.com/tcCI7C73J6_-JGZ1379arvSslcJDUGSH_FHag4WAe1RQcXFiS1m-WjgBPjCOZ1YhTNjjjr4Dd58kqoYNAwbkMJuLOFHZyoe9_0pQMlE9_44KyLyk7nxQuURJudSB1X38__fqn0hM=s0)
![](https://lh4.googleusercontent.com/XpcrC0WpTAMBqDW-PcfhVUC7tgXWfxCFfbxZEexE4v6NE5GOOZpiuHqqmvW7yM7cT3jyFFrUq744YuUd1AkI7VeX2KVP2sfxhWN0LGAqkMygApP3zk3tMS2l5k9o9IBKrUYildED=s0)

View file

@ -0,0 +1,25 @@
### Способы оплаты
Необходимо настроить соответствие между оплатами WooCommerce и CRM.
Слева располагаются оплаты WooCommerce, справа CRM. В выпадающем списке справа нужно выбрать справочник оплаты в CRM, который будет соответствовать оплате в WooCommerce. Данное сопоставление требуется выбрать **для всех оплат**. Если ни одно из предложенных значений не подходит, то необходимо создать в CRM новый справочник оплаты, либо пропустить сопоставление.
После создания нового типа оплаты *(справочника)* на стороне CRM, в WooCommerce необходимо обновить страницу настроек *(перед этим не забудьте сохранить указанные ранее настройки)*, чтобы в предложенных значениях появился новый созданный справочник.
Если сопоставление пропустить и не выбрать ни одного значения, в этом случае, если в заказе в WooCommerce будет выбрана данная оплата, то в CRM в заказе “Тип оплаты” **будет не указан**.
Обратите внимание, что значения не должны повторяться, т.е. для двух оплат WooCommerce, не может быть выбрана одна и та же оплата *(справочник оплаты)* в CRM. Необходимо выбирать 1 к 1.
**1 оплата WooCommerce = 1 оплата CRM**. Иначе будут наблюдаться ошибки в работе модуля.
Также в списке значений для сопоставления выводятся *только Активные в CRM справочники*.
Перечень оплат в WooCommerce можно посмотреть перейдя в *WooCommerce - Настройки - вкладка Платежи*
![](https://lh4.googleusercontent.com/gYTT8iHXVZKweniR9Wi5SknH0WUqGnz8bP_TOOUs1WpOyZiKZ8FxhKL73rT0f0GilHmqqhd7YXOJ3kokBTbQrJsseZ0yixiuGusnKD__9LmR2L5L4b3tlZfEClAMOP91I1_88V0M=s0)
Обратите внимание, что в настройках сопоставления, перечень существующих оплат в WooCommerce выводится *вне зависимости от их активности*.
На стороне CRM доступные доставки можно увидеть перейдя в *Настройки - Справочники - Типы оплат*
![](https://lh6.googleusercontent.com/JnOEnozZt6WmRV-uAQzUsYyPX7McyoT-l8OP1TbG_gUGoagJw7Gy4u9zofmSD0IBP3YQ8RH4l-ZcWiT9cZ_aUlPq7f33WLEufv_C1dox-eObQBwaHaOc0ptJdrwkjGaD2ZXn8i3-=s0)

View file

@ -0,0 +1,9 @@
### Настройки WhatsApp
Данная опция позволяет отображать на сайте иконку WhatsApp, при нажатии на иконку будет открываться чат с пользователем, номер телефона которого указан в настройке.
Для активации необходимо поставить галочку “Активировать WhatsApp”, если необходимо отображать иконку справа, поставить галочку “Разместить в правом нижнем углу сайта” по умолчанию располагается в левом нижнем углу.
В поле “Введите номер телефона” требуется ввести корректный номер телефона. *Для РФ номер требуется вводить с 7, если номер будет введен начиная с 8, при открытии WhatsApp будет выведена ошибка.*
Опция добавлена в версии 4.3.0.

View file

@ -0,0 +1,30 @@
### Настройка управления остатками
Данная опция позволяет управлять остатками на стороне CRM. Для активации необходимо поставить галочку. Информация по остаткам из CRM в WooCommerce обновляется **раз в 15 минут**.
Обратите внимание, для того чтобы вести остатки в CRM, требуется в настройках системы указать магазину "Вести каталог в системе", присвоить ему склад и включить редактирование остатков, а на стороне WooCommerce в настройках модуля активировать опцию "Синхронизация остатков".
Анализ работы по списанию остатков был произведен в задаче [#75178](https://redmine.retailcrm.tech/issues/75178).
**В версии 4.6.8** появилась возможность указать, с каких складов CRM выгружать остатки для торговых предложений. В блоке "Настройка управления остатками" добавлен мультисписок с доступными складами CRM. Для выбора нескольких складов зажмите CTRL (для Windows и Linux) или ⌘ Command (для MacOS).
Если в CRM несколько складов, а в настройках не выбрали склады для выгрузки, в таком случае остатки будут передаваться по всем складам.
#### На стороне WooCommerce
Если остатки **ведут на сайте**, то в этом случае, при оформлении заказа на сайте, остаток по товару сразу же списывается в WooCommerce. В CRM обновление остатков по товарам происходит в момент синхронизации ICML-файла каталога *(~ 4 часа)*.
| Кейс | Результат |
|----------|-------------|
| Оформить заказ на сайте, после указать статус Отменен заказу на стороне WooCommerce | При оформлении заказа остатки сразу списываются, как в WooCommerce, так и в CRM; <br/> После указания отмены заказу, в WooCommerce остатки сразу возвращаются, в CRM, если настроена автоматическая отмена товара при отмене заказа, остатки возвращаются, иначе нужно ждать обновления каталога |
| Оформить заказ в CRM, после указать статус из группы Выполнен в CRM | В CRM по товарам остатки не списываются; <br/> В WooCommerce остаток по товару списывается, когда приходит статус Выполнен |
| Оформить заказ в CRM, после указать статус из группы Отмена в CRM | В CRM остатки не списываются *(в т.ч. и при установлении отмены статусу товара)*, в WooCommerce остатки по товару также не возвращаются |
#### На стороне CRM
При ведении стока **на стороне CRM**, остаток по товару сразу списывается в CRM. На стороне WooCommerce остатки по товарам обновляются **раз в 15 минут**.
Если данный статус в CRM не выставляет автоматически статус отмены товару, то в CRM остатки не возвращаются, также, как и на стороне WooCommerce <br/> Если будет выставлен статус отмены товару, остатки возвратятся в CRM и спустя 15 минут в WooCommerce
| Кейс | Результат |
|------------------------------------------------------------------------------|-----------|
| Оформить заказ в CRM, после указать в CRM **статус товару** из группы Отмена | В CRM остаток по товару возвращается, в WooCommerce сток обновляется через 15 минут |
| Оформить заказ на сайте в WooCommerce | Заказ выгружается в CRM, по нему списываются остатки по товару, данное списание передается на сторону WooCommerce |
| Оформить заказ на сайте в WooCommerce, после указать статус Отменен в админке сайта WooCommerce | В CRM приходит статус отмены заказа, если в CRM настроена опция автоматической отмены товара при отмене заказа, то остатки возвращаются в CRM, спустя время и в WooCommerce |

View file

@ -0,0 +1,7 @@
### Передача номера заказа
Функционал позволяет в номер заказа CRM передавать номер заказа WooCommerce.
Если опция не активна, то номер, с которым выгрузится заказ из WooCommerce в CRM, будет сгенерирован с использованием “Шаблона генерации номера заказа из API” *(в CRM Настройки - Системные - Заказы)*.
![](https://lh4.googleusercontent.com/SeL8ZOuBRSOqpMeuORL8WBEOgFc4iGijFK6SgKRnyYShVYtTueUtJEJrFE0_o1tJgZFU7-sTOvtN5XeulqeCGpORNIsBLiq9N50qoxZgC4V0W-7Y0anVuK83nZTnpRZdynI4Ue9c=s0)

3
doc/1.Setup/Settings.md Normal file
View file

@ -0,0 +1,3 @@
# Settings

View file

@ -0,0 +1,3 @@
### Обновление данных в Simla.com
Если активировать данную опцию, **любые изменения заказов из Woocommerce не будут передаваться в CRM**. Изменения заказа из CRM в Woocomerce передаются.

33
doc/1.Setup/User roles.md Normal file
View file

@ -0,0 +1,33 @@
# User roles
С версии `4.3.7` опция `"Роли клиентов"` была удалена из настроек модуля. Сейчас выгружаются пользователи со всеми доступными ролями в CMS.
Добавили фильтр `retailcrm_customer_roles` для корректировки выгружаемых пользователей.
## Пример работы фильтра
В приведенном ниже примере показано, как возможно корректировать роли выгружаемых пользователей:
```php
<?php
add_filter('retailcrm_customer_roles', 'editCustomerRoles', 10, 1);
function editCustomerRoles($roles)
{
if (isset($roles['customer'])) {
unset($roles['customer']);
}
return $roles;
}
```
## Описание работы функционала
У каждого зарегистрированного пользователя в WP есть роль. Роль отображает права пользователя на сайте. Ниже приведены основные кейсы работы с пользователями:
* Пользователь "гость (клиент без регистрации)" создает заказ. Заказ корректно выгрузился в CRM, клиент был создан без externalId. Если этот "гость" создаст еще один заказ, то заказ корректно выгрузится и установится связь с этим клиентом (будет произведен поиск по email).
* Создали пользователя, например с ролью "Подписчик", он корректно выгрузился в CRM, есть externalId и данные по клиенту, только те, что указаны при создании в админке WP. Данный пользователь оформляет заказ, заказ корректно выгрузился в CRM и связался с клиентом, клиенту добавилась данные: телефон, адрес. Обновление данных пользователя происходит корректно (будет произведен поиск по externalId).
* Функционал корректно работает и для корпоративных клиентов.
* Запустили архивную выгрузку, данные выгрузились корректно, все заказы, все клиенты без дублей.
* Запретили через фильтр передавать пользователей с ролью "Клиент", такие пользователи в CRM выгружаться не будут.
* Создали заказ для пользователя с ролью "Клиент", заказ выгрузился в CRM, так же создался клиент без externalId, модуль воспринимает таких клиентов как "гостей". Данное поведение считается корректным тк мы не выгружаем пользователей с определенной ролью, но они создаются с заказов (заказы мы передаем).

View file

@ -0,0 +1,53 @@
### Поддержка корпоративных клиентов
Для включения функционала корпоративных клиентов, необходимо поставить галочку в настройках модуля напротив опции **Поддержка корпоративных клиентов**. Также в CRM должен быть активен функционал корпоративных клиентов.
В WooCommerce клиент считается корпоративным, если заполнено поле **Компания** в Платежном адресе *(Billing address)* в профиле клиента.
![](https://lh3.googleusercontent.com/tIaEcm0EZkUoyphatT9k6B1tBtcf0icvLrjITR71jOj5i9pRDMXkP_fZI1jOYDbIywzat8IiYTKeF2CH712Gnu_998U5gnvaOZMKWHoExHkaGvXNEKcZJKQFl8jh-rVKgA_k2u5F=s0)
При регистрации покупателя с необходимыми данными, в CRM будет выгружен новый клиент, как обычное физ лицо, однако, если этот клиент оформит заказ, то в CRM будет создан корп клиент с наименованием из поля *Компания*, а клиент станет *Контактным лицом* данного корп клиента, тип клиента также изменится на *Контактное лицо*.
В карточке корп клиента указаны:
- Наименование,
- Магазин,
- Дата регистрации. <br/><br/>
*__В блоке Контактные лица__*:
- ФИО клиента,
- Email,
- Email-подписка *(значение зависит от флага подписки на рассылку на сайте)*,
- признак Основное,
- Телефон,
- нет привязки к Компании. <br/><br/>
*__В блоке Компании__*:
- Название,
- Статус,
- признак Основная,
- Тип контрагента (Юр лицо),
- Адрес регистрации *(из Платежного адреса)*,
- есть привязка к адресу. <br/><br/>
*__В блоке Адреса__*
- указан Платежный адрес *(заполнены все строки)*.
В карточку контактного лица *(клиента, на которого оформлен заказ)* в данные адреса выгружается **Платежный адрес**.
#### На стороне WooCommerce
| # | Кейс *(действие на стороне Woo)* | Результат |
|--|--|--|
|1| Зарегистрировать клиента, указать в профиле Billing address | При добавлении Billing address в профиле клиента в Woo, он выгружается в CRM в карточку клиента |
|2| Зарегистрировать клиента, указать в профиле Shipping address | Shipping address не выгружается в CRM в карточку клиента |
|3| Зарегистрировать клиента, указать в профиле Billing и Shipping address | В карточку клиента в CRM выгружается только Billing address |
|4| Изменить в профиле клиента Billing address | В карточке клиента в CRM обновляется адрес |
|5| Изменить в профиле клиента Shipping address | Shipping address не выгружается в карточку клиента в CRM |
|6| Зарегистрировать клиента, указать в профиле Billing address, после удалить адрес | При указании Billing address в профиле клиента в Woo, он выгружается в карточку клиента в CRM <br/> Адрес в Woo можно изменить, но удалить нельзя *(возможность отсутствует)* |
|7| Зарегистрировать клиента, указать в профиле Shipping address, после удалить | При указании Shipping address в профиле клиента в Woo, он не выгружается в карточку клиента в CRM <br/> Адрес в Woo можно изменить, но удалить нельзя (возможность отсутствует) |
|8| Оформить заказ с указанием только Billing address | В карточке клиента в CRM указан Billing address <br/> В карточке заказа в CRM указан Billing address |
|9| Оформить заказ с указанием Billing и Shipping address | В карточке клиента в CRM указан Billing address <br/> В карточке заказа в CRM в блоке Доставка указан Shipping address |
| | | |
|10| Зарегистрировать клиента, указать при оформлении заказа значение в поле Компания в Billing address | При указании компании в Billing address в профиле клиента, **корп клиент не создается** <br/> **Корп клиент создается при оформлении заказа** <br/> **billing = shipping** <br/> В карточке заказа в CRM в блоке Доставка указан Billing address <br/> В карточке корп клиента в CRM в блоке Компании в поле "Адрес регистрации" указан Billing address; в блоке Адреса также указан Billing address <br/> В карточке Контактного лица *(клиента, на которого оформлен заказ)* в CRM указан Billing address |
|11| Указать при оформлении заказа значение в поле Компания в Shipping address (в Billing address не указано значение в поле Компания) | Корп клиент не создается в CRM; В карточке заказа в CRM указан Shipping address <br/> В карточке клиента в CRM указан Billing address |
|12| Указать при оформлении заказа значение в поле Компания в Billing и Shipping address | Корп клиент создан <br/> В карточке заказа в CRM указан Shipping address <br/> В карточке корп клиента в CRM в блоке Компании в поле Адрес регистрации указан Billing address; в блоке Адреса также указан Billing address <br/> В карточке Контактного лица в CRM указан Billing address |
|13| Изменить значение в поле Компания в профиле клиента в Billing адресе, оформить заказ | Создается новый корп клиент в CRM |
|14| Изменить значение в поле Компания в профиле клиента в Shipping адресе | Никаких изменений на стороне CRM нет |
|15| Зарегистрировать нового клиента, в Billing address в поле Компания указать название существующего корп клиента в CRM | Новый корп клиент не создается в CRM, добавляется новое Контактное лицо в карточку корп клиента + добавляется Billing address в блок Адреса, если он отличен от существующего |
|16| Зарегистрировать нового клиента, в Billing address в поле Компания указать название существующего корп клиента в CRM и существующий адрес | Адрес не дублируется |
| | | |

View file

@ -0,0 +1,17 @@
#Регистрация клиентов
Клиент может зарегистрироваться на сайте:
* Администратор может зарегистрировать клиента через административную панель WordPress.
* На странице wp-admin.
* При заполнении email в форме регистрации на сайте.
* Когда оформляет новый заказ, если клиент является "гостем" ему будет предложено зарегистрироваться.
Модуль обрабатывает регистрацию клиентов через хук **user_register**.
Модуль обрабатывает таких клиентов по следующей логике:
1. Производится поиск по email в CRM, если клиент найден, происходит обновление его данных в CRM и актуализируется его externalId.
2. Если клиент не найден в CRM, будет создан новый.
Данная логика позволяет минимизировать количество дублей.
Возможно ситуация, когда клиент был удален в CMS, но в CRM такой клиент продолжает существовать, чтобы не потерять таких клиентов производится актуализация их данных.

View file

@ -0,0 +1,28 @@
# Работа с адресами
С версии `4.3.8` изменена логика работы с адресами.
В заказ CRM c заказа WooCommerce будет передаваться, только shipping адрес. Если при оформлении заказа указан только billing адрес, то WooCommerce записывает в БД эти же данные в shipping, то есть shipping = billing.
При создании обычных/корпоративных клиентов в CRM будет передаваться billing адрес с заказа/пользователя WooCommerce. Если клиент гость и у него нет данных по billing адресу, тогда будет передан billing адрес заказа.
Для кастомизаций адресов в CRM, добавили новые фильтры:
* `retailcrm_process_order_address`
* `retailcrm_process_customer_address`
* `retailcrm_process_customer_corporate_address`
## Пример работы фильтров
В приведенном ниже примере показано, как возможно кастомизировать адрес заказа:
```php
<?php
add_filter('retailcrm_process_order_address', 'editOrderAddress', 10, 2);
function editOrderAddress($address, $order)
{
$address['text'] = 'Test';
return $address;
}
```

View file

@ -0,0 +1,10 @@
## Статусы заказов
Обратная синхронизация (CRM --> WC):
* Если в CRM создают заказ со статусом для которого не выбран маппинг, модуль по умолчанию поставит "pending" для этого заказа в WC.
* Если в CRM изменении заказ со статусом для которого не выбран маппинг, статус заказа в WC изменен не будет.
Прямая синхронизация (WC --> CRM):
* Если в WC создают/изменяют заказ со статусом для которого в настройках маппинга статусов выбрано "Не отправлять в CRM", при создании заказа, в массиве данных заказа поля "status" не будет, CRM поставит статус по умолчанию, при изменении статус заказа в CRM изменен не будет.

View file

@ -0,0 +1,63 @@
### Фильтры
Если вы хотите изменить данные отправляемые между CRM и CMS, вы можете использовать **пользовательские фильтры**.
Чтобы использовать фильтры, необходимо в директории wp-content создать директорию mu-plugins и в ней создать кастомный файл mu-simla.php.
### Список доступных фильтров
> retailcrm_process_customer - позволяет изменить данные клиента при передачи из CMS -> CRM.
> retailcrm_process_customer_address - позволяет изменить адрес клиента при передачи из CMS -> CRM.
> retailcrm_process_customer_corporate - позволяет изменить данные корпоративного клиента при передачи из CMS -> CRM.
> retailcrm_process_customer_corporate_address - позволяет изменить адрес корпоративного клиента при передачи из CMS -> CRM.
> retailcrm_process_customer_corporate_company - позволяет изменить компанию корпоративного клиента при передачи из CMS -> CRM.
> retailcrm_customer_roles - позволяет изменить допустимые роли клиентов.
> retailcrm_daemon_collector - позволяет изменить данные для Daemon Collector.
> retailcrm_initialize_analytics - позволяет изменить данные скрипта для Google Analytics.
> retailcrm_send_analytics - позволяет изменить отправляемые данные Google Analytics.
> retailcrm_process_customer_custom_fields - позволяет изменить данные кастомных полей клиента при передачи из CRM -> CMS .
> retailcrm_history_before_save - позволяет изменить данные заказа и клиента при передачи из CRM -> CMS.
> retailcrm_process_order_custom_fields - позволяет изменить данные кастомных полей заказ при передачи из CRM -> CMS.
> retailcrm_process_offer - позволяет изменить данные товара перед записью в ICML каталог.
> retailcrm_process_order - позволяет изменить данные заказа при передачи из CMS -> CRM.
> retailcrm_process_order_address - позволяет изменить адрес заказа при передачи из CMS -> CRM.
> retailcrm_add_cron_interval - позволяет добавить пользовательский интервал времени.
> retailcrm_cron_schedules - позволяет изменить интервал времени для cron задач.
> retailcrm_shipping_list - позволяет изменить методы доставки с CMS.
> retailcrm_order_create_after - позволяет проверить создание заказа и произвести дополнительные действия
> retailcrm_order_update_after - позволяет проверить изменение заказа и произвести дополнительные действия
> retailcrm_change_default_meta_fields - позволяет изменить список получаемых по умолчанию мета-полей
**Пример использования:**
```php
<?php
add_action('retailcrm_process_offer', 'changeProductInfo', 10, 2);
function changeProductInfo($productData, $wcProduct)
{
$productData['name'] .= 'Test';
return $productData;
}
```

View file

@ -0,0 +1,4 @@
## Платежи
1. Модуль при обратной синхронизации устанавливает только метод оплаты, но никак не реагирует на статус оплат в WooCommerce.

161
doc/FAQ/FAQ.md Normal file
View file

@ -0,0 +1,161 @@
## FAQ
**Ребрендинг модуля с RetailCRM на Simla.com**
Ребрендинг модуля WooCommerce связан с тем, что данные он востребован на зарубежном направлении и название нашей системы в Испании и ЛатАм - Simla.com. Для клиентов, которые работают с данным модулем CMS в РФ, ничего не меняется, модули будут также поддерживаться и развиваться.
Прошу обратить внимание, что в связи с ребрендингом изменилось название ICML-файла названия каталога - simla.xml *(ранее было retailcrm.xml)*.
Ребрендинг произошел в версии 4.3.0.
**Работа с более чем одним магазином в Woo**
В данный момент в модуле не предусмотрена работа с более чем 1 магазином в Woo. <br>
**Работа с одинаковыми позициями в заказе**
Модуль поддерживает работу с одинаковыми товарными позициями в заказе начиная с версии 3.5.4 <br>
**Изменения генерации ICML-файла**
В модуле возможно сделать необходимые кастомизация для генерации ICML-файла в желаемом формате <br>
**Выгрузка архивных данных** <br>
Ранее модуль мог выгружать не более 700-800 архивных заказов *(т.к. выгрузка происходила по web-хиту, работа скрипта была ограничена и не все данные успевали прогрузится в CRM)*.<br>
Сейчас архивные данные можно выгрузить в CRM с использованием консольного скрипта. Этот скрипт позволяет выгружать все архивные заказы и данные о клиентах без ограничения на количество записей. Процесс выгрузки выполняется пакетами по 50 заказов или 50 клиентов за раз. <br>
После завершения обработки каждой пачки выводится ее порядковый номер, что позволяет отслеживать прогресс работы скрипта. Если во время выгрузки возникает ошибка, скрипт можно перезапустить с той страницы, где произошел сбой, что минимизирует потерю данных и время на повторную выгрузку.<br>
Для запуска выгрузки нужно:<br>
1. В корневой директории вашего сайта (по умолчанию - */var/www/html*) разместить файл **upload_to_crm.php** и вставить в него код:
```
<?php
require_once __DIR__ . '/wp-load.php';
$options = getopt('',['entity::','page::']);
do_action('wp_console_upload', $options['entity'] ?? '', (int) $options['page'] ?? 0);
```
2. После чего в командной строке ввести команду для запуска скрипта: <br>
> php upload_to_crm.php --entity=orders/customers/full_upload --page=номер страницы
Параметры для выгрузки:<br>
**--entity**: Указывает, какие данные выгружать. Данный параметр **является обязательным**. Возможные значения:
- **orders**: архивные заказы;
- **customers**: архивные клиенты;
- **full_upload**: полная выгрузка всех заказов и клиентов (весь архив).
**--page**: Указывает номер страницы для выгрузки. Каждая страница содержит 50 заказов или клиентов. Нумерация страниц начинается с 0.<br>
Пример:
> php upload_to_crm.php --entity=orders --page=3
В этом примере будет выгружен архив заказов, начиная с 3-й страницы.
**Работа с зонами доставки** *(WooCommerce - Настройки - Доставка - Зоны доставки)*
С зонами доставки модуль не работает
[https://github.com/retailcrm/woocommerce-module/blob/master/src/include/abstracts/class-wc-retailcrm-abstracts-settings.php#L256](https://github.com/retailcrm/woocommerce-module/blob/master/src/include/abstracts/class-wc-retailcrm-abstracts-settings.php#L256)
Как только появляются новые методы доставки они автоматически добавляются в настройки CRM. <br>
**Выгрузка вариативных товаров**
В модуле добавлен обработка всех типов товаров в WooCommerce. Раньше обрабатывались только variable/simple, теперь, если тип товара не является variation/variable + кастомные типы плагинов содержащие торговые предложения *(variable-subscription и т.п.)*, то обрабатываются как simple. Стандартные типы товаров обрабатываются корректно, но любые кастомные типы с плагинов все равно необходимо исследовать.
## Причины возникновения ошибок
**Не пришел заказ из Woo в CRM** <br>
Причина может быть в Методе оплаты. Если в настройках CRM *(Настройки - Справочники - Типы оплат)* в конкретном типе оплаты не выбран статус оплаты, с которым пришел заказ из CMS, то заказ не будет выгружен в CRM. Данная проблема должна решиться после реализации задачи [#73631](https://redmine.retailcrm.tech/issues/73631).
![](https://lh3.googleusercontent.com/v3W9GDMvBy9ASK6t27utB4PtsH0EkO7cxfG_317D4FWw3y5sWe9GaNeuJj_NmAR0pMLsJnAnBpCZtK3_yA2kAg50iwO2yV2m1zDfKdSYYSw-2PIXRv3j2r3d3BpaogvTS44OV1U0=s0)
**Не выгрузились корп клиенты при архивной выгрузке** <br>
Необходимо проверить включение данной опции в настройках модуля в WooCommerce.
**В CRM не отображаются остатки по товару** / **При создании заказа в CRM, товар не списывается со склада в Woocomerce** <br>
Необходимо выключить редактирование остатков в настройках CRM *(Настройки - Системные - Склад - Разрешить редактирование остатков)*. При включенной опции количество товаров из ICML-файла будет игнорироваться при загрузке. Также для магазина, привязанного к сайту Woo, должны быть разрешены все склады.
По решению проблемы должны немного помочь задачи [#54708](https://redmine.retailcrm.tech/issues/54708) и [#74803](https://redmine.retailcrm.tech/issues/74803).
**В CRM приходит оплаченный заказ, хотя в Woo оплаты не было** <br>
Требуется проверить активность опции “Оплата произведена” *(Настройки - Справочники - Статусы оплат)* в статусе оплаты, с которым приходит заказ в CRM.
**Когда заказ создается через Woocommerce, то ему на почту приходит вся информация по заказу, а если создается через CRM, то на почту приходит чаще всего приходит пустая карточка, но не всегда, от чего это может зависеть?** <br>
Это не проблема, а логика работы нашего модуля. Модуль не отправляет письма, то есть в Woo сохраняется заказ без суммы (она проставляется чуть позже), но как только заказ появляется в Woo, срабатывает хук и отправляется письмо на email, как раз без суммы.
**Работоспособность модуля** <br>
На работоспособность модуля влияют: <br>
**1**) Оплата модуля Онлайн-консультанта. <br>
**2**) Для работы модуля требуются файлы generate_icml.php и retailcrm_history.php *(не удалять их)*.
**Ранее заказы могли не выгружаться из WooCommerce в CRM по причинам** *(сейчас заказы выгружаются, т.к. была убрана валидация)*: <br>
- **Отсутствовала страна в настройках**. <br> Если в настройках CRM в списке доступных стран (Настройки - Системные - Общие - Список доступных стран) не была выбрана страна, указанная в выгружаемом заказе, то заказ не появлялся в CRM. После внедрения задачи [#74148](https://redmine.retailcrm.tech/issues/74148), заказы с неразрешенной страной приходят в CRM. При открытии карточки заказа, появляется pop-up с информацией о том, что указанную в заказе страну нужно добавить в настройках.
- **Неразрешенный способ оплаты в доставке**. <br> Если в настройках CRM в справочниках Типов доставок (Настройки - Справочники - Типы доставок - перейти в конкретный справочник - вкладка Способы оплаты) не был разрешен способ оплаты для доставки, указанной в выгружаемом заказе, то заказ не приходил в CRM. После внедрения задачи [#71543](https://redmine.retailcrm.tech/issues/71543), заказы с неразрешенной оплатой для доставки, выгружаются в CRM. При заходе в карточку данного заказа, появляется pop-up с информацией о том, что для доставки требуется разрешить указанный метод оплаты.
### Не синхронизируется история изменений из Simla.com, не генерируется каталог или не синхронизируются остатки
Необходимо проверить, выполняет ли wp-cron задачи модуля. В разделе Debug настроек модуля должна быть указана текущая дата (+/- 4 часа).
Далее требуется проверить, что все задачи wp-cron выполняются на сайте. Для этого нужно перейти в раздел WooCommerce -> Статус (Estado) -> Статус системы (Estado del sistema) -> WordPress Cron (Cron de WordPress).
Если wp-cron не выполняет задачи, то в первую очередь нужно восстановить его работу.
Для быстрого решения можно перезапустить задачи wp-cron в настройках модуля, нажав на кнопку "Очистить" в блоке "Отладочная информация".
В некоторых случаях задачи модуля можно перенести на cron сервера. Имейте ввиду, что остальные задачи wp-cron останутся в таком же состоянии (не будут выполняться).
Действия для переноса:
- отключить wp-cron (см. инструкции по WooCommerce и WordPress),
- в зависимости от требуемых задач расположить в корневой директории сайта скрипты (в директории с файлом wp-config.php):
**simla_history.php** - для синхронизации истории
```
<?php
/** Load WordPress Bootstrap */
require_once dirname( __FILE__ ) . '/wp-load.php';
do_action("retailcrm_history");
```
**simla_icml.php** - для генерации каталога
```
<?php
/** Load WordPress Bootstrap */
require_once dirname( __FILE__ ) . '/wp-load.php';
do_action("retailcrm_icml");
```
**simla_stocks.php** - для синхронизации остатков
```
<?php
/** Load WordPress Bootstrap */
require_once dirname( __FILE__ ) . '/wp-load.php';
do_action("retailcrm_inventories");
```
- добавить нужные задачи в расписание cron сервера:
```
*/5 * * * * {path to php} {wp dir}/simla_history.php
* */3 * * * {path to php} {wp dir}/simla_icml.php
*/15 * * * * {path to php} {wp dir}/simla_stocks.php
```
Пример:
```
*/5 * * * * /usr/local/bin/php /home/conquero/public_html/simla_history.php
* */3 * * * /usr/local/bin/php /home/my_site/public_html/simla_icml.php
*/15 * * * * /usr/local/bin/php /home/conquero/public_html/simla_stocks.php
```

1
doc/README.md Normal file
View file

@ -0,0 +1 @@
# Developers documentation

33
docker-compose.yml Normal file
View file

@ -0,0 +1,33 @@
version: '3'
services:
app:
build:
context: ./.docker
volumes:
- ./:/code
links:
- "mysql"
user: ${UID:-1000}:${GID:-1000}
depends_on:
- mysql
env_file:
- ./.env
environment:
- DB_NAME=${DB_NAME}
- DB_USER=${DB_USER}
- DB_PASS=${DB_PASS}
- DB_HOST=${DB_HOST}
- WP_VERSION=${WP_VERSION}
- WC_VERSION=${WC_VERSION}
- SKIP_DB_CREATE=true
mysql:
image: mysql:5.7
env_file:
- ./.env
environment:
- MYSQL_DATABASE=${DB_NAME}
- MYSQL_USER=${DB_USER}
- MYSQL_PASSWORD=${DB_PASS}
- MYSQL_ROOT_PASSWORD=root
ports:
- "3306:3306"

32
phpunit.xml.dist Normal file
View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
bootstrap="tests/bootstrap.php"
backupGlobals="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
verbose="true"
processIsolation="false"
stopOnFailure="false"
>
<testsuites>
<testsuite name="RetailCRM WooCommerce Test Suite">
<directory suffix=".php">tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist addUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
<exclude>
<directory suffix=".php">src/include/api</directory>
<directory>src/config</directory>
<directory>src/languages</directory>
<file>src/readme.txt</file>
</exclude>
</whitelist>
</filter>
<logging>
<log type="coverage-clover" target="coverage.xml"/>
</logging>
</phpunit>

View file

@ -0,0 +1,611 @@
# Translation of Plugins - Woocommerce Simla.com - Development (trunk) in Spanish
# This file is distributed under the same license as the Plugins - Woocommerce Simla.com - Development (trunk) package.
msgid ""
msgstr ""
"PO-Revision-Date: 2018-06-06 08:53:26+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: GlotPress/2.4.0-alpha\n"
"Language: es\n"
"Project-Id-Version: Plugins - Woocommerce Simla.com - Development (trunk)\n"
#. Author URI of the plugin/theme
msgid "https://simla.com/"
msgstr "https://simla.com/"
#. Author of the plugin/theme
msgid "RetailDriver LLC"
msgstr "RetailDriver LLC"
#. Description of the plugin/theme
msgid "Integration plugin for WooCommerce & Simla.com"
msgstr "El plugin de integración para WooCommerce & Simla.com"
#. Plugin URI of the plugin/theme
msgid "https://wordpress.org/plugins/woo-retailcrm/"
msgstr "https://wordpress.org/plugins/woo-retailcrm/"
#. Plugin Name of the plugin/theme
msgid "WooCommerce Simla.com"
msgstr "WooCommerce Simla.com"
msgid "Orders"
msgstr "Pedidos"
msgid "Customers"
msgstr "Clientes"
msgid "This functionality allows to upload orders to Simla.com differentially"
msgstr "Esta función permite la subida selectiva de los pedidos al Simla.com"
msgid "Uploading orders by identifiers"
msgstr "Subida de los pedidos por identificadores"
msgid "Enter orders identifiers separated by a comma, but no more than 50"
msgstr "Introduce los identificadores de pedidos separados por coma, pero no más de 50"
msgid "Orders identifiers"
msgstr "Los identificadores de pedidos"
msgid "Orders were uploaded"
msgstr "Los pedidos están subidos"
msgid "The field cannot be empty, enter the order ID"
msgstr "El campo no puede estar vacío, introduce el identificador de pedido"
msgid "Catalog was generated"
msgstr "El catálogo está generado"
msgid "Customers and orders were uploaded"
msgstr "Los clientes y pedidos están subidos"
msgid "Enter the correct API key"
msgstr "Introduce la llave API correcta"
msgid "This functionality allows to generate ICML products catalog for uploading to Simla.com"
msgstr "Esta función permite generar los catálogos de pedidos ICML para subida al Simla.com"
msgid "Generating ICML"
msgstr "Generando ICML"
msgid "Generate now"
msgstr "Generar ahora"
msgid "Generating ICML catalog"
msgstr "Generar catálogo ICML"
msgid "Settings"
msgstr "Ajustes"
msgid "Uploading the existing customers and orders to Simla.com"
msgstr "Subida de los clientes existentes y pedidos al Simla.com"
msgid "Uploading all customers and orders"
msgstr "Subir todos los clientes y pedidos"
msgid "Upload"
msgstr "Subir"
msgid "Settings of uploading"
msgstr "Ajustes de subida"
msgid "User parameter"
msgstr "El parámetro personalizado"
msgid "UA tracking code"
msgstr "El código de seguimiento UA"
msgid "Enable this setting for uploading data to UA"
msgstr "Activa esta opción para subir los datos al UA"
msgid "UA"
msgstr "UA"
msgid "Activate UA"
msgstr "Activar UA"
msgid "UA settings"
msgstr "Ajustes de UA"
msgid "Enable this setting if you would like to get information on leftover stocks from Simla.com to the website"
msgstr "Active esta opción si quiere recibir la información del stock de los productos desde Simla.com a la página web"
msgid "Stock balance"
msgstr "El stock"
msgid "Synchronization of the stock balance"
msgstr "Sincronizar el stock"
msgid "Setting of the stock balance"
msgstr "Ajustes del stock"
msgid "Statuses"
msgstr "Los estados"
msgid "Coupon"
msgstr "Cupón"
msgid "When working with coupons via CRM, it is impossible to transfer manual discounts."
msgstr "El trabajo con cupones a través del CRM no permite transferir descuentos manuales."
msgid "The user field must be in the String or Text format."
msgstr "El campo personalizado debe tener el formato Hilo o Texto."
msgid "When using multiple coupons, separation is supported using spaces, line breaks, characters `;` `,`."
msgstr "Si se usan varios cupones, es posible separarlos por espacios, nueva línea o caracteres `;` `,`."
msgid "For example: code_coupon_1; code_coupon_2, code_coupon_3 code_coupon_4"
msgstr "Por ejemplo, code_coupon_1; code_coupon_2, code_coupon_3 code_coupon_4"
msgid "Payment types"
msgstr "Métodos de pago"
msgid "Delivery types"
msgstr "Métodos de envío"
msgid "Select order methods which will be uploaded from Simla.com to the website"
msgstr "Elige el método de formalización de los pedidos que se van a subir desde Simla.com a la página web"
msgid "Order methods available for uploading from Simla.com"
msgstr "Los métodos de la formalización de los pedidos disponibles para subida desde Simla.com"
msgid "Order methods"
msgstr "Los métodos de la formalización del pedido"
msgid "Catalog settings"
msgstr "Ajustes del catálogo"
msgid "Online assistant"
msgstr "Consultor en línea"
msgid "Insert the Online consultant code here"
msgstr "Inserte el código de consultor en Línea aquí"
msgid "Select API version"
msgstr "Selecciona la versión de API"
msgid "API version"
msgstr "Versión de API"
msgid "API settings"
msgstr "Ajustes de API"
msgid "Enter your API key. You can find it in the administration section of Simla.com"
msgstr "Introduce la llave API. Puede encontrarla en apartado administrativo del Simla.com"
msgid "Enter API URL (https://yourdomain.simla.com)"
msgstr "Introduce enlace de API (https://yourdomain.simla.com)"
msgid "Integration with Simla.com management system"
msgstr "La integración con el sistema de gestión del Simla.com"
msgid "Every 15 minutes"
msgstr "Cada 15 minutos"
msgid "Every 3 hours"
msgstr "Cada 3 horas"
msgid "Every 5 minutes"
msgstr "Cada 5 minutos"
msgid "API Key"
msgstr "La llave API"
msgid "API of URL"
msgstr "El enlace API"
msgid "Main settings"
msgstr "Los ajustes generales"
msgid "Simla.com"
msgstr "Simla.com"
msgid "Daemon Collector settings"
msgstr "Ajustes de Daemon Collector"
msgid "Activate Daemon Collector"
msgstr "Activar Daemon Collector"
msgid "Daemon Collector"
msgstr "Daemon Collector"
msgid "Enable this setting for activate Daemon Collector on site"
msgstr "Active esta configuración para activar Daemon Collector en la página web"
msgid "Site key"
msgstr "Clave de la página web"
msgid "Disable data editing in Simla.com"
msgstr "Desactivar edición de datos en Simla.com"
msgid "Data updating in Simla.com"
msgstr "Actualización de datos en Simla.com"
msgid "Activate the binding via sku (xml)"
msgstr "Activar conexión por sku (xmlId)"
msgid "Stock synchronization and link between products"
msgstr "Sincronización de stock y conexión de productos"
msgid "Enable transferring the number to Simla.com"
msgstr "Activar la transferencia de números en Simla.com"
msgid "Transferring the order number"
msgstr "Transferencia de un número de pedido"
msgid "Corporate customers support"
msgstr "Soporte a clientes corporativos"
msgid "Enabled"
msgstr "Habilitado"
msgid "Settings of WhatsApp"
msgstr "Ajustes de WhatsApp"
msgid "Activar WhatsApp"
msgstr "Активировать WhatsApp"
msgid "Activate this setting to activate WhatsApp on the website"
msgstr "Activa esta configuración para activar WhatsApp en la página web"
msgid "WhatsApp icon location"
msgstr "Ubicación del ícono de WhatsApp"
msgid "Place in the lower right corner of the website"
msgstr "Colocar en la esquina inferior derecha de la pagina web"
msgid "By default, WhatsApp icon is located in the lower left corner of the website"
msgstr "Por defecto, el ícono de WhatsApp se encuentra en la esquina inferior izquierda de la página web"
msgid "Enter your phone number"
msgstr "Introduce tu número de teléfono"
msgid "WhatsApp chat will be opened with this contact"
msgstr "Se abrirá una ventana de chat con este contacto en WhatsApp"
msgid "Introduce the correct phone number"
msgstr "Introduce el número de teléfono correcto"
msgid "You can export all orders and customers from CMS to Simla.com by clicking the «Upload» button. This process can take much time and before it is completed, you need to keep the tab open"
msgstr "Presionando el botón «Exportar» puedes descargar a todos los pedidos y clientes de CMS a Simla.com. Este proceso puede llevar mucho tiempo y es necesario mantener abierta la pestaña hasta que termine el proceso"
msgid "Debug information"
msgstr "Información Debug"
msgid "Custom fields"
msgstr "Campos personalizados"
msgid "Select value"
msgstr "Selecciona un valor"
msgid "Custom fields for order"
msgstr "Campos de pedido personalizados"
msgid "Custom fields for customer"
msgstr "Campos de clientes personalizados"
msgid "Add new select for order"
msgstr "Añadir"
msgid "Add new select for customer"
msgstr "Añadir"
msgid "This option is disabled"
msgstr "Opción desactivada"
msgid "Cron launches"
msgstr "Tareas de cron"
msgid "Generation ICML"
msgstr "Generación del catálogo"
msgid "Syncing history"
msgstr "Sincronización del historial"
msgid "Syncing inventories"
msgstr "Sincronización de inventario"
msgid "Don't send to CRM"
msgstr "No enviar al CRM"
msgid "Integration payment"
msgstr "Integración pago"
msgid "Attention!"
msgstr "¡Atención!"
msgid "If payment type linked to the CRM integration module choosed, payment must be proceed in the CRM"
msgstr "Al seleccionar en el enlace de tipos de pago un método de pago de integración en CRM, el pago se debe realizar por parte del CRM"
msgid "Product description"
msgstr "Descripción del Producto"
msgid "Full description"
msgstr "Descripción completa"
msgid "Short description"
msgstr "Descripción corta"
msgid "In the catalog, you can use a full or short description of the product"
msgstr "En el catálogo, puedes utilizar una descripción del producto corta o completa"
msgid "If you change the time interval, need to clear the old cron tasks"
msgstr "Si cambias el Intervalo de Tiempo tienes que limpiar los cron tareas antiguos"
msgid "Clear cron tasks"
msgstr "Borrar tareas cron"
msgid "Clear"
msgstr "Borrar"
msgid "Cron tasks cleared"
msgstr "Trabajos cron borrados"
msgid "Untitled"
msgstr "Intitulado"
msgid "Incorrect protocol. Only https is allowed."
msgstr "Protocolo incorrecto. Sólo se permite https."
msgid "The domain path must be empty."
msgstr "La ruta del dominio debe estar vacía."
msgid "An invalid domain is specified."
msgstr "Se especifica un dominio no válido."
msgid "The port does not need to be specified."
msgstr "No es necesario especificar el puerto."
msgid "Incorrect Host URL."
msgstr "URL del Host incorrecta."
msgid "Incorrect URL."
msgstr "URL incorrecta."
msgid "The query must be blank."
msgstr "La consulta debe estar en blanco."
msgid "The fragment should be blank."
msgstr "El fragmento debe estar en blanco."
msgid "No need to provide authorization data."
msgstr "No es necesario proporcionar datos de autorización."
msgid "Unable to obtain reference values."
msgstr "No se pueden obtener valores de referencia."
msgid "Abandoned carts"
msgstr "Carritos Abandonadas"
msgid "Upload abandoned carts"
msgstr "Importar los carritos abandonados"
msgid "Enable if you want to in CRM abandoned shopping carts were unloaded"
msgstr "Habilitar Si desea que en CRM se descargaron las cestas abandonadas de los compradores"
msgid "firstName"
msgstr "Nombre"
msgid "lastName"
msgstr "Apellido"
msgid "phone"
msgstr "Número de teléfono"
msgid "tags"
msgstr "Etiquetas"
msgid "customerComment"
msgstr "Comentario del cliente"
msgid "managerComment"
msgstr "Comentario del asesor"
msgid "email"
msgstr "E-mail"
msgid "addressText"
msgstr "Dirección"
msgid "addressCity"
msgstr "Ciudad"
msgid "addressIndex"
msgstr "Código postal"
msgid "addressRegion"
msgstr "Región"
msgid "Standard CRM fields"
msgstr "Los campos del CRM por defecto"
msgid "Warehouses available in CRM"
msgstr "Almacenes disponibles en CRM"
msgid "Select warehouses to receive balances from CRM. To select several warehouses, hold down CTRL (for Windows and Linux) or ⌘ Command (for MacOS)"
msgstr "Selecciona los almacenes para recibir el stock desde CRM. Para seleccionar varios mantén pulsado CTRL (para Windows y Linux) o ⌘ Command (para MacOS)"
msgid "I agree to receive promotional newsletters"
msgstr "Estoy de acuerdo en recibir los boletines informativos"
msgid "API key with one-shop access required"
msgstr "Se requiere clave API con acceso a una tienda"
msgid "The currency of the site differs from the currency of the store in CRM. For the integration to work correctly, the currencies in CRM and CMS must match"
msgstr "La moneda del sitio web es distinto a la tienda del CRM. Para el funcionamiento correcto de la integración, las monedas del CMS y CRM deben coincid"
msgid "Loyalty program"
msgstr "Programa de fidelización"
msgid "Activate program loyalty"
msgstr "Activar programa de fidelización"
msgid "Attention! When activating the loyalty program, the method of ICML catalog generation changes. Details in"
msgstr "¡Atención! Al activar el programa de fidelización, cambia el método de generación del catálogo ICML. Detalles en la"
msgid "<a href='https://docs.simla.com/Users/Integration/SiteModules/WooCommerce/PLWoocommerce'>documentation loyalty program</a>"
msgstr "<a href='https://docs.simla.com/es/Users/Integration/SiteModules/WooCommerce/PLWoocommerce'>documentación del programa de fidelización</a>"
msgid "Terms of loyalty program"
msgstr "Condiciones del programa de fidelización"
msgid "Insert the terms and conditions of the loyalty program"
msgstr "Introduce las condiciones del programa de fidelización"
msgid "Conditions of personal data processing"
msgstr "Condiciones de procesamiento de datos personales"
msgid "Insert the terms and conditions for processing personal data"
msgstr "Introduce las condiciones para el procesamiento de datos personales"
msgid "To activate the loyalty program it is necessary to activate the <a href='?page=wc-settings'>'enable use of coupons option'</a>"
msgstr "Para activar el programa de fidelización es necesario activar la opción <a href='?page=wc-settings'>'habilitar el uso de cupones'</a>"
msgid "Bonus account"
msgstr "Cuenta de bonos"
msgid "Participation ID: "
msgstr "ID de participación: "
msgid "Current level: "
msgstr "Nivel actual: "
msgid "Bonuses on the account: "
msgstr "Bonos en la cuenta: "
msgid "Bonus card number: "
msgstr "Número de tarjeta de bonos: "
msgid "Date of registration: "
msgstr "Fecha de registro: "
msgid "Current level rules"
msgstr "Reglas del nivel actual"
msgid "Required amount of purchases to move to the next level: "
msgstr "Cantidad de compras necesarias para pasar al siguiente nivel: "
msgid "Activate participation in the loyalty program"
msgstr "Activar participación en el programa de fidelización"
msgid "Send"
msgstr "Enviar"
msgid "To register in the loyalty program, fill in the form:"
msgstr "Para registrarse en el programa de fidelización, rellena el formulario:"
msgid " I agree with "
msgstr " Estoy de acuerdo con "
msgid "loyalty program terms"
msgstr "términos del programa de fidelización"
msgid "terms of personal data processing"
msgstr "términos de procesamiento de datos personales"
msgid "Phone"
msgstr "Teléfono"
msgid "Error while registering in the loyalty program. Try again later"
msgstr "Error al registrarse en el programa de fidelización. Inténtalo de nuevo más tarde"
msgid "The card is not linked"
msgstr "La tarjeta no está vinculada"
msgid "Error while retrieving data. Try again later"
msgstr "Error al recuperar los datos. Inténtalo de nuevo más tarde"
msgid "Error when activating the loyalty program. Try again later"
msgstr "Error al activar el programa de fidelización. Inténtalo de nuevo más tarde"
msgid "Enter the correct phone number"
msgstr "Introduce el número de teléfono correcto"
msgid "Close"
msgstr "Cerrar"
msgid "Ordinary products: accrual of 1 bonus for each %s %s"
msgstr "Productos ordinarios: acumulación de 1 bono por cada %s %s"
msgid "Promotional products: accrual of 1 bonus for each %s %s"
msgstr "Productos promocionales: acumulación de 1 bono por cada %s %s"
msgid "Ordinary products: bonus accrual in the amount of %s%% of the purchase amount"
msgstr "Productos ordinarios: acumulación de bonos en la cantidad de %s%% de la suma de la compra"
msgid "Promotional products: bonus accrual in the amount of %s%% of the purchase amount"
msgstr "Productos promocionales: acumulación de bonos en la cantidad de %s%% de la suma de la compra"
msgid "Ordinary products: %s%% discount"
msgstr "Productos ordinarios: %s%% de descuento"
msgid "Promotional products: %s%% discount"
msgstr "Productos promocionales: %s%% de descuento"
msgid "Uploading services"
msgstr "Subida de servicios"
msgid "Goods with the 'virtual' option enabled will be uploaded to Simla as services"
msgstr "Los productos con la opción 'virtual' activada se subirán a Simla como servicios"
msgid "User not found in the system"
msgstr "Usuario no encontrado en el sistema"
msgid "Error when searching for participation in loyalty programs"
msgstr "Error al buscar la participación en programas de fidelización"
msgid "No active participation in the loyalty program was detected"
msgstr "No se detectó la participación activa en el programa de fidelización"
msgid "No bonuses for debiting"
msgstr "No hay bonos para debitar"
msgid "Loyalty program not found"
msgstr "Programa de fidelización no encontrado"
msgid "Loyalty program is not active"
msgstr "Programa de fidelización no está activo"
msgid "Loyalty program blocked"
msgstr "Programa de fidelización bloqueado"
msgid "This user is a corporate person"
msgstr "Este usuario es una persona jurídica"
msgid "It is possible to write off"
msgstr "Es posible debitar"
msgid "bonuses"
msgstr "bonos"
msgid "Use coupon:"
msgstr "Utiliza el cupón:"
msgid "Points will be awarded upon completion of the order:"
msgstr "Los puntos se concederán al finalizar el pedido:"
msgid "Unloading promotional prices of offers"
msgstr "Descarga de precios promocionales de ofertas comerciales"
msgid "Every 4 hours"
msgstr "Cada 4 horas"
msgid "Upload prices now"
msgstr "Descargar precios ahora"
msgid "Uploaded discount price"
msgstr "Descarga de precios promocionales"
msgid "This functionality loads the promotional prices offers into Simla.com"
msgstr "Esta función carga los precios promocionales de las ofertas comerciales en Simla.com"
msgid "Promotional prices unloaded"
msgstr "Se han cargado los precios promocionales"
msgid "Woocommerce promotional price"
msgstr "Precio promocional Woocommerce"
msgid "Promotional price type for Woocommerce store, generated automatically. Necessary for correct synchronization work when loyalty program is enabled (Do not delete. Do not deactivate)"
msgstr "Tipo de precio promocional para la tienda Woocommerce, generado automáticamente. Necesario para el correcto funcionamiento de la sincronización cuando el programa de fidelización está habilitado (No eliminar. No desactivar)"

View file

@ -0,0 +1,620 @@
# Translation of Plugins - Woocommerce Simla.com - Development (trunk) in Russian
# This file is distributed under the same license as the Plugins - Woocommerce Simla.com - Development (trunk) package.
msgid ""
msgstr ""
"PO-Revision-Date: 2018-06-06 08:53:26+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: GlotPress/2.4.0-alpha\n"
"Language: ru\n"
"Project-Id-Version: Plugins - Woocommerce Simla.com - Development (trunk)\n"
#. Author URI of the plugin/theme
msgid "https://simla.com/"
msgstr "https://simla.com/"
#. Author of the plugin/theme
msgid "RetailDriver LLC"
msgstr "RetailDriver LLC"
#. Description of the plugin/theme
msgid "Integration plugin for WooCommerce & Simla.com"
msgstr "Интеграционный плагин для WooCommerce & Simla.com"
#. Plugin URI of the plugin/theme
msgid "https://wordpress.org/plugins/woo-retailcrm/"
msgstr "https://wordpress.org/plugins/woo-retailcrm/"
#. Plugin Name of the plugin/theme
msgid "WooCommerce Simla.com"
msgstr "WooCommerce Simla.com"
msgid "Orders"
msgstr "Заказы"
msgid "Customers"
msgstr "Клиенты"
msgid "Activate history uploads"
msgstr "Активировать загрузку истории изменений"
msgid "Upload data from Simla.com"
msgstr "Загрузка данных из Simla.com"
msgid "Generating ICML catalog by wp-cron"
msgstr "Генерация ICML каталога товаров с помощью wp-cron"
msgid "This functionality allows to upload orders to Simla.com differentially"
msgstr "Эта функция позволяет производить выборочную выгрузку заказов в Simla.com"
msgid "Uploading orders by identifiers"
msgstr "Выгрузка заказов по идентификаторам"
msgid "Enter orders identifiers separated by a comma, but no more than 50"
msgstr "Введите идентификаторы заказов через запятую, но не более 50"
msgid "Orders identifiers"
msgstr "Идентификаторы заказов"
msgid "Orders were uploaded"
msgstr "Заказы были выгружены"
msgid "The field cannot be empty, enter the order ID"
msgstr "Поле не может быть пустым, введите идентификатор заказа"
msgid "Catalog was generated"
msgstr "Каталог был сгенерирован"
msgid "Customers and orders were uploaded"
msgstr "Клиенты и заказы были выгружены"
msgid "Enter the correct API key"
msgstr "Введите корректный API ключ"
msgid "This functionality allows to generate ICML products catalog for uploading to Simla.com"
msgstr "Эта функция позволяет сгенерировать ICML каталог товаров для выгрузки в Simla.com"
msgid "Generating ICML"
msgstr "Генерация ICML"
msgid "Generate now"
msgstr "Сгенерировать сейчас"
msgid "Generating ICML catalog"
msgstr "Генерация ICML каталога"
msgid "Settings"
msgstr "Настройки"
msgid "Uploading the existing customers and orders to Simla.com"
msgstr "Выгрузка существующих клиентов и заказов в Simla.com"
msgid "Uploading all customers and orders"
msgstr "Выгрузка всех клиентов и заказов"
msgid "Upload"
msgstr "Выгрузить"
msgid "Settings of uploading"
msgstr "Настройки выгрузки"
msgid "User parameter"
msgstr "Пользовательский параметр"
msgid "UA tracking code"
msgstr "Код отслеживания UA"
msgid "Enable this setting for uploading data to UA"
msgstr "Активируйте эту настройку для выгрузки данных в UA"
msgid "UA"
msgstr "UA"
msgid "Activate UA"
msgstr "Активировать UA"
msgid "UA settings"
msgstr "Настройки UA"
msgid "Enable this setting if you would like to get information on leftover stocks from Simla.com to the website"
msgstr "Активируйте данную настройку, если хотите получать остатки по товарам из Simla.com на сайт"
msgid "Stock balance"
msgstr "Остатки"
msgid "Synchronization of the stock balance"
msgstr "Синхронизация остатков"
msgid "Setting of the stock balance"
msgstr "Настройка управления остатками"
msgid "Statuses"
msgstr "Статусы"
msgid "Coupon"
msgstr "Купон"
msgid "When working with coupons via CRM, it is impossible to transfer manual discounts."
msgstr "При работе с купонами через CRM невозможно передавать ручные скидки."
msgid "The user field must be in the String or Text format."
msgstr "Пользовательское поле должно быть формата Строка или Текст."
msgid "When using multiple coupons, separation is supported using spaces, line breaks, characters `;` `,`."
msgstr "При использовании нескольких купонов, поддерживается разделение с помощью пробелов, переноса строки, символами `;` `,`."
msgid "For example: code_coupon_1; code_coupon_2, code_coupon_3 code_coupon_4"
msgstr "Например: code_coupon_1; code_coupon_2, code_coupon_3 code_coupon_4"
msgid "Payment types"
msgstr "Способы оплаты"
msgid "Delivery types"
msgstr "Способы доставки"
msgid "Select order methods which will be uploaded from Simla.com to the website"
msgstr "Выберите способы оформления заказов, которые будут выгружаться из Simla.com на сайт"
msgid "Order methods available for uploading from Simla.com"
msgstr "Способы оформления заказа, доступные для выгрузки из Simla.com"
msgid "Order methods"
msgstr "Способы оформления заказа"
msgid "Catalog settings"
msgstr "Настройки каталога"
msgid "Online assistant"
msgstr "Онлайн консультант"
msgid "Insert the Online consultant code here"
msgstr "Вставьте код Онлайн-консультанта здесь"
msgid "Select API version"
msgstr "Выберите версию API"
msgid "API version"
msgstr "Версия API"
msgid "API settings"
msgstr "Настройки API"
msgid "Enter your API key. You can find it in the administration section of Simla.com"
msgstr "Введите API ключ. Вы можете найти его в административном разделе Simla.com"
msgid "Enter API URL (https://yourdomain.simla.com)"
msgstr "Введите API URL (https://yourdomain.simla.com)"
msgid "Integration with Simla.com management system"
msgstr "Интеграция с системой управления Simla.com"
msgid "Every 15 minutes"
msgstr "Каждые 15 минут"
msgid "Every 3 hours"
msgstr "Каждые 3 часа"
msgid "Every 5 minutes"
msgstr "Каждые 5 минут"
msgid "API key"
msgstr "API ключ"
msgid "API of URL"
msgstr "API URL"
msgid "Main settings"
msgstr "Главные настройки"
msgid "Simla.com"
msgstr "Simla.com"
msgid "Daemon Collector settings"
msgstr "Настройка Daemon Collector"
msgid "Activate Daemon Collector"
msgstr "Активировать Daemon Collector"
msgid "Daemon Collector"
msgstr "Daemon Collector"
msgid "Enable this setting for activate Daemon Collector on site"
msgstr "Активируйте эту настройку для активации Daemon Collector на сайте"
msgid "Site key"
msgstr "Ключ сайта"
msgid "Disable data editing in Simla.com"
msgstr "Деактивировать редактирование данных в Simla.com"
msgid "Data updating in Simla.com"
msgstr "Обновление данных в Simla.com"
msgid "Activate the binding via sku (xml)"
msgstr "Активировать связь по sku(xmlId)"
msgid "Stock synchronization and link between products"
msgstr "Синхронизация остатков и связь товаров"
msgid "Enable transferring the number to Simla.com"
msgstr "Активировать передачу номера в Simla.com"
msgid "Transferring the order number"
msgstr "Передача номера заказа"
msgid "Corporate customers support"
msgstr "Поддержка корпоративных клиентов"
msgid "Enabled"
msgstr "Включено"
msgid "Settings of WhatsApp"
msgstr "Настройки WhatsApp"
msgid "Activate WhatsApp"
msgstr "Активировать WhatsApp"
msgid "Activate this setting to activate WhatsApp on the website"
msgstr "Активируйте эту настройку для активации WhatsApp на сайте"
msgid "WhatsApp icon location"
msgstr "Расположение иконки WhatsApp"
msgid "Place in the lower right corner of the website"
msgstr "Разместить в правом нижнем углу сайта"
msgid "By default, WhatsApp icon is located in the lower left corner of the website"
msgstr "По умолчанию иконка WhatsApp расположена в левом нижнем углу сайта"
msgid "Enter your phone number"
msgstr "Введите номер телефона"
msgid "WhatsApp chat will be opened with this contact"
msgstr "Будет открыт чат в WhatsApp с данным контактом"
msgid "Introduce the correct phone number"
msgstr "Введите корректный номер телефона"
msgid "You can export all orders and customers from CMS to Simla.com by clicking the «Upload» button. This process can take much time and before it is completed, you need to keep the tab open"
msgstr "Вы можете экспортировать все заказы и клиентов из CMS в Simla.com, нажав кнопку «Выгрузить». Этот процесс может занять много времени, и до его завершения необходимо держать вкладку открытой"
msgid "Debug information"
msgstr "Отладочная информация"
msgid "Custom fields"
msgstr "Пользовательские поля"
msgid "Select value"
msgstr "Выберите значение"
msgid "Custom fields for order"
msgstr "Пользовательские поля для заказа"
msgid "Custom fields for customer"
msgstr "Пользовательские поля для клиента"
msgid "Add new select for order"
msgstr "Добавить"
msgid "Add new select for customer"
msgstr "Добавить"
msgid "This option is disabled"
msgstr "Опция отключена"
msgid "Cron launches"
msgstr "Работающие крон задачи"
msgid "Generation ICML"
msgstr "Генерация каталога"
msgid "Syncing history"
msgstr "Синхронизация истории"
msgid "Syncing inventories"
msgstr "Синхронизация запасов"
msgid "Don't send to CRM"
msgstr "Не отправлять в CRM"
msgid "Integration payment"
msgstr "Интеграционная оплата"
msgid "Attention!"
msgstr "Внимание!"
msgid "If payment type linked to the CRM integration module choosed, payment must be proceed in the CRM"
msgstr "При указании в соответствии типа оплаты, привязанного к интеграционному модулю в CRM, оплата должна происходить на стороне CRM"
msgid "Product description"
msgstr "Описание товара"
msgid "Full description"
msgstr "Полное описание"
msgid "Short description"
msgstr "Краткое описание"
msgid "In the catalog, you can use a full or short description of the product"
msgstr "В каталоге можно использовать полное или краткое описание товара"
msgid "If you change the time interval, need to clear the old cron tasks"
msgstr "Если вы изменили временной интервал, необходимо очистить старые cron задачи"
msgid "Clear cron tasks"
msgstr "Очистить cron задачи"
msgid "Clear"
msgstr "Очистить"
msgid "Cron tasks cleared"
msgstr "Cron задачи очищены"
msgid "Untitled"
msgstr "Без названия"
msgid "Incorrect protocol. Only https is allowed."
msgstr "Некорректный протокол. Допускается только https."
msgid "The domain path must be empty."
msgstr "Путь к домену должен быть пустым."
msgid "An invalid domain is specified."
msgstr "Указан недопустимый домен."
msgid "The port does not need to be specified."
msgstr "Не нужно указывать порт."
msgid "Incorrect Host URL."
msgstr "Некорректный URL хоста"
msgid "Incorrect URL."
msgstr "Некорректный URL."
msgid "The query must be blank."
msgstr "Запрос должен быть пустым."
msgid "The fragment should be blank."
msgstr "Фрагмент должен быть пустым."
msgid "No need to provide authorization data."
msgstr "Нет необходимости предоставлять авторизационные данные."
msgid "Unable to obtain reference values."
msgstr "Не удалось получить эталонное значение"
msgid "Abandoned carts"
msgstr "Брошенные корзины"
msgid "Upload abandoned carts"
msgstr "Выгружать брошенные корзины"
msgid "Enable if you want to in CRM abandoned shopping carts were unloaded"
msgstr "Включите, если хотите, чтобы в CRM выгружались брошенные корзины покупателей"
msgid "firstName"
msgstr "Имя"
msgid "lastName"
msgstr "Фамилия"
msgid "phone"
msgstr "Телефон"
msgid "tags"
msgstr "Теги"
msgid "customerComment"
msgstr "Комментарий клиента"
msgid "managerComment"
msgstr "Комментарий менеджера"
msgid "email"
msgstr "E-mail"
msgid "addressText"
msgstr "Адрес"
msgid "addressCity"
msgstr "Город"
msgid "addressIndex"
msgstr "Индекс"
msgid "addressRegion"
msgstr "Регион"
msgid "Standard CRM fields"
msgstr "Стандартные поля CRM"
msgid "Warehouses available in CRM"
msgstr "Склады, доступные в CRM"
msgid "Select warehouses to receive balances from CRM. To select several warehouses, hold down CTRL (for Windows and Linux) or ⌘ Command (for MacOS)"
msgstr "Выберите склады для получения остатков из CRM. Для выбора нескольких складов зажмите CTRL (для Windows и Linux) или ⌘ Command (для MacOS)"
msgid "I agree to receive promotional newsletters"
msgstr "Согласен на рекламно-информационные рассылки"
msgid "API key with one-shop access required"
msgstr "Требуется API ключ с доступом к одному магазину"
msgid "The currency of the site differs from the currency of the store in CRM. For the integration to work correctly, the currencies in CRM and CMS must match"
msgstr "Валюта сайта отличается от валюты магазина в CRM. Для корректной работы интеграции, валюты в CRM и CMS должны совпадать"
msgid "Loyalty program"
msgstr "Программа лояльности"
msgid "Activate program loyalty"
msgstr "Активировать программу лояльности"
msgid "Attention! When activating the loyalty program, the method of ICML catalog generation changes. Details in"
msgstr "Внимание! При активации программы лояльности, изменяется способ генерации ICML каталога. Подробности в"
msgid "<a href='https://docs.simla.com/Users/Integration/SiteModules/WooCommerce/PLWoocommerce'>documentation loyalty program</a>"
msgstr "<a href='https://docs.retailcrm.ru/Users/Integration/SiteModules/WooCommerce/PLWoocommerce#h-2'>документации программы лояльности</a>"
msgid "Terms of loyalty program"
msgstr "Условия программы лояльности"
msgid "Insert the terms and conditions of the loyalty program"
msgstr "Вставьте условия участия в программе лояльности"
msgid "Conditions of personal data processing"
msgstr "Условия обработки персональных данных"
msgid "Insert the terms and conditions for processing personal data"
msgstr "Вставьте условия обработки персональных данных"
msgid "To activate the loyalty program it is necessary to activate the <a href='?page=wc-settings'>'enable use of coupons option'</a>"
msgstr "Для активации программы лояльности необходимо активировать опцию <a href='?page=wc-settings'>'включить использование купонов'</a>"
msgid "Bonus account"
msgstr "Бонусный счёт"
msgid "Participation ID: "
msgstr "ID участия: "
msgid "Current level: "
msgstr "Текущий уровень: "
msgid "Bonuses on the account: "
msgstr "Бонусов на счёте: "
msgid "Bonus card number: "
msgstr "Номер бонусной карты: "
msgid "Date of registration: "
msgstr "Дата регистрации: "
msgid "Current level rules"
msgstr "Правила текущего уровня"
msgid "Required amount of purchases to move to the next level: "
msgstr "Необходимая сумма покупок для перехода на следующий уровень: "
msgid "Activate participation in the loyalty program"
msgstr "Активировать участие в программе лояльности"
msgid "Send"
msgstr "Отправить"
msgid "To register in the loyalty program, fill in the form:"
msgstr "Для регистрации в программе лояльности заполните форму:"
msgid " I agree with "
msgstr " Я согласен с "
msgid "loyalty program terms"
msgstr "условиями программы лояльности"
msgid "terms of personal data processing"
msgstr "условиями обработки персональных данных"
msgid "Phone"
msgstr "Телефон"
msgid "Error while registering in the loyalty program. Try again later"
msgstr "Ошибка при регистрации в программе лояльности. Попробуйте позже"
msgid "The card is not linked"
msgstr "Карта не привязана"
msgid "Error while retrieving data. Try again later"
msgstr "Ошибка при получении данных. Попробуйте позже"
msgid "Error when activating the loyalty program. Try again later"
msgstr "Ошибка при активации программы лояльности. Попробуйте позже"
msgid "Enter the correct phone number"
msgstr "Введите корректный номер телефона"
msgid "Close"
msgstr "Закрыть"
msgid "Ordinary products: accrual of 1 bonus for each %s %s"
msgstr "Обычные товары: начисление 1 бонуса за каждые %s %s"
msgid "Promotional products: accrual of 1 bonus for each %s %s"
msgstr "Акционные товары: начисление 1 бонуса за каждые %s %s"
msgid "Ordinary products: bonus accrual in the amount of %s%% of the purchase amount"
msgstr "Обычные товары: начисление бонусов в размере %s%% от суммы покупки"
msgid "Promotional products: bonus accrual in the amount of %s%% of the purchase amount"
msgstr "Акционные товары: начисление бонусов в размере %s%% от суммы покупки"
msgid "Ordinary products: %s%% discount"
msgstr "Обычные товары: %s%% скидка"
msgid "Promotional products: %s%% discount"
msgstr "Акционные товары: %s%% скидка"
msgid "Uploading services"
msgstr "Выгрузка услуг"
msgid "Goods with the 'virtual' option enabled will be uploaded to Simla as services"
msgstr "Товары с включенной опцией 'виртуальные' будут выгружаться в CRM как услуги"
msgid "User not found in the system"
msgstr "Пользователь не найден в системе"
msgid "Error when searching for participation in loyalty programs"
msgstr "Ошибка при поиске участия в программах лояльности"
msgid "No active participation in the loyalty program was detected"
msgstr "Не обнаружено активного участия в программе лояльности"
msgid "No bonuses for debiting"
msgstr "Нет бонусов для списания"
msgid "Loyalty program not found"
msgstr "Программа лояльности не найдена"
msgid "Loyalty program is not active"
msgstr "Программа лояльности не активна"
msgid "Loyalty program blocked"
msgstr "Программа лояльности заблокирована"
msgid "This user is a corporate person"
msgstr "Данный пользователь является корпоратиным лицом"
msgid "It is possible to write off"
msgstr "Возможно списать"
msgid "bonuses"
msgstr "бонусов"
msgid "Use coupon:"
msgstr "Используйте купон:"
msgid "Points will be awarded upon completion of the order:"
msgstr "По завершению заказа будет начислено баллов:"
msgid "Unloading promotional prices of offers"
msgstr "Выгрузка акционных цен торговых предложений"
msgid "Every 4 hours"
msgstr "Каждые 4 часа"
msgid "Upload prices now"
msgstr "Выгрузить цены сейчас"
msgid "Uploaded discount price"
msgstr "Выгрузка акционных цен"
msgid "This functionality loads the promotional prices offers into Simla.com"
msgstr "Эта функция загружает акционные цены торговых предложений в Simla.com"
msgid "Promotional prices unloaded"
msgstr "Акционные цены выгружены"
msgid "Woocommerce promotional price"
msgstr "Акционная цена Woocommerce"
msgid "Promotional price type for Woocommerce store, generated automatically. Necessary for correct synchronization work when loyalty program is enabled (Do not delete. Do not deactivate)"
msgstr "Акционный тип цены для магазина Woocommerce, сгенерированный автоматически. Необходим для корректной работы синхронизации при включенной программы лояльности (Не удалять. Не деактивировать)"

View file

@ -0,0 +1,3 @@
.retail-cron-info-title {
font-weight: bold;
}

1
src/assets/css/debug-info.min.css vendored Normal file
View file

@ -0,0 +1 @@
.retail-cron-info-title{font-weight:700}

View file

@ -0,0 +1,59 @@
.retailcrm-meta-select {
margin-right: 25px !important;
float:left !important;
width: 200px !important;
text-align-last: center;
border: 2px solid #2271b1;
}
.retailcrm-select-pair {
margin-bottom: 30px;
}
.retailcrm-order-label, .retailcrm-customer-label {
display: block;
font-size: 14px !important;
margin-top: 16px;
margin-bottom: 10px;
}
.add-new-select-retailcrm {
display:block;
width: 200px !important;
height: 34px;
color: #4169e1;
border-color: #4169e1;
background: #f6f7f7;
vertical-align: top;
margin-top: 16px;
margin-left: 128px;
margin-bottom: 10px;
}
.delete-select-retailcrm {
display: block;
width: 34px;
height: 34px;
--weight: 0.5px;
--aa: 0.5px; /* anti-aliasing */
--color: #4169e1;
border-color: #4169e1;
background: #f6f7f7;
padding: 0;
background:
linear-gradient(45deg, transparent calc(50% - var(--weight) - var(--aa)), var(--color) calc(50% - var(--weight)), var(--color) calc(50% + var(--weight)), transparent calc(50% + var(--weight) + var(--aa))),
linear-gradient(-45deg, transparent calc(50% - var(--weight) - var(--aa)), var(--color) calc(50% - var(--weight)), var(--color) calc(50% + var(--weight)), transparent calc(50% + var(--weight) + var(--aa)));
}
.red-selected-retailcrm {
border: solid 3px red !important;
animation: 2s blinker linear infinite;
#transition: border-width 0.6s linear;
}
@keyframes blinker {
0% { opacity: 1.0; }
50% { opacity: 0.3; }
100% { opacity: 1.0; }
}

1
src/assets/css/meta-fields.min.css vendored Normal file
View file

@ -0,0 +1 @@
.retailcrm-meta-select{margin-right:25px!important;float:left!important;width:200px!important;text-align-last:center;border:2px solid #2271b1}.retailcrm-select-pair{margin-bottom:30px}.retailcrm-customer-label,.retailcrm-order-label{display:block;font-size:14px!important;margin-top:16px;margin-bottom:10px}.add-new-select-retailcrm{display:block;width:200px!important;height:34px;color:#4169e1;border-color:#4169e1;background:#f6f7f7;vertical-align:top;margin-top:16px;margin-left:128px;margin-bottom:10px}.delete-select-retailcrm{display:block;width:34px;height:34px;--weight:0.5px;--aa:0.5px;--color:#4169e1;border-color:#4169e1;background:#f6f7f7;padding:0;background:linear-gradient(45deg,transparent calc(50% - var(--weight) - var(--aa)),var(--color) calc(50% - var(--weight)),var(--color) calc(50% + var(--weight)),transparent calc(50% + var(--weight) + var(--aa))),linear-gradient(-45deg,transparent calc(50% - var(--weight) - var(--aa)),var(--color) calc(50% - var(--weight)),var(--color) calc(50% + var(--weight)),transparent calc(50% + var(--weight) + var(--aa)))}.red-selected-retailcrm{border:solid 3px red!important;animation:2s blinker linear infinite}@keyframes blinker{0%{opacity:1}50%{opacity:.3}100%{opacity:1}}

View file

@ -0,0 +1,33 @@
.retail-progress {
border-radius: 18px;
border: 1px solid rgba(122, 122, 122, 0.15);
width: 400px;
height: 18px;
overflow: hidden;
transition: height 0.25s ease;
}
.retail-progress__loader {
width: 0;
border-radius: 18px;
background: #4169e1;
color: white;
text-align: center;
padding: 0 30px;
font-size: 18px;
font-weight: 600;
transition: width 0.4s ease-in;
line-height: 18px;
box-sizing: border-box;
}
.retailcrm-hidden {
display: none !important;
}
.retail-count-data-upload {
margin-bottom: 20px;
size: 30px;
color: #4169e1;
font-weight: bold;
}

5
src/assets/css/progress-bar.min.css vendored Normal file
View file

@ -0,0 +1,5 @@
<<<<<<< HEAD
.retail-progress{border-radius:18px;border:1px solid rgba(122,122,122,.15);width:400px;height:18px;overflow:hidden;transition:height .25s ease}.retail-progress__loader{width:0;border-radius:18px;background:#4169e1;color:#fff;text-align:center;padding:0 30px;font-size:18px;font-weight:600;transition:width .4s ease-in;line-height:18px;box-sizing:border-box}.retail-hidden{display:none!important}.retail-count-data-upload{margin-bottom:20px;size:30px;color:#4169e1;font-weight:700}
=======
.retail-progress{border-radius:18px;border:1px solid rgba(122,122,122,.15);width:400px;height:18px;overflow:hidden;transition:height .25s ease}.retail-progress__loader{width:0;border-radius:18px;background:#4169e1;color:#fff;text-align:center;padding:0 30px;font-size:18px;font-weight:600;transition:width .4s ease-in;line-height:18px;box-sizing:border-box}.retailcrm-hidden{display:none!important}.retail-count-data-upload{margin-bottom:20px;size:30px;color:#4169e1;font-weight:700}
>>>>>>> Add mapping metadata fields in settings

View file

@ -0,0 +1,34 @@
.popup-fade-loyalty {
display: none;
}
.popup-fade-loyalty:before {
content: '';
background: #000;
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
opacity: 0.7;
z-index: 9999;
}
.popup-loyalty {
position: fixed;
top: 20%;
left: 50%;
padding: 20px;
width: 1000px;
margin-left: -500px;
height: 50%;
background: #fff;
border: 1px solid orange;
border-radius: 4px;
z-index: 99999;
opacity: 1;
overflow: auto;
}
.popup-close-loyalty {
position: absolute;
top: 10px;
right: 10px;
}

View file

@ -0,0 +1,34 @@
.whatsapp-icon {
position: fixed;
z-index: 999;
left: 0;
bottom: 32px;
width: 60px;
height: 60px;
box-sizing: border-box;
}
.whatsapp-icon_left {
left: 32px;
right: auto;
}
.whatsapp-icon_right {
right: 32px;
left: auto;
}
.whatsapp-icon__icon {
width: 100%;
height: 100%;
}
.chat-btn__text {
color: #8A96A6;
font-weight: 600;
font-size: 8px;
line-height: 10px;
text-align: center;
margin: 0 0 8px;
white-space: nowrap;
}

1
src/assets/css/whatsapp-icon.min.css vendored Normal file
View file

@ -0,0 +1 @@
.whatsapp-icon{position:fixed;z-index: 999;left:0;bottom:32px;width:60px;height:60px;box-sizing:border-box}.whatsapp-icon_left{left:32px;right:auto}.whatsapp-icon_right{right:32px;left:auto}.whatsapp-icon__icon{width:100%;height:100%}.chat-btn__text{color:#8A96A6;font-weight:600;font-size:8px;line-height:10px;text-align:center;margin:0 0 8px;white-space:nowrap}

View file

@ -0,0 +1,178 @@
_backorders
_billing_address_1
_billing_address_2
_billing_address_index
_billing_city
_billing_company
_billing_country
_billing_email
_billing_first_name
_billing_last_name
_billing_phone
_billing_postcode
_billing_state
_button_text
_cart_discount
_cart_discount_tax
_cart_hash
_children
_completed_date
_created_via
_customer_ip_address
_customer_user
_customer_user_agent
_date_completed
_date_paid
_download_expiry
_download_limit
_download_permissions_granted
_downloadable
_downloadable_files
_edit_last
_edit_lock
_manage_stock
_new_order_email_sent
_order_currency
_order_key
_order_shipping
_order_shipping_tax
_order_stock_reduced
_order_tax
_order_total
_order_version
_paid_date
_payment_method
_payment_method_title
_price
_prices_include_tax
_product_attributes
_product_image_gallery
_product_url
_product_version
_recorded_coupon_usage_counts
_recorded_sales
_refund_amount
_refund_reason
_refunded_by
_refunded_payment
_regular_price
_sale_price
_shipping_address_1
_shipping_address_2
_shipping_address_index
_shipping_city
_shipping_company
_shipping_country
_shipping_first_name
_shipping_last_name
_shipping_postcode
_shipping_state
_sku
_sold_individually
_stock
_stock_status
_stripe_charge_captured
_stripe_currency
_stripe_customer_id
_stripe_fee
_stripe_intent_id
_stripe_net
_stripe_source_id
_tax_class
_tax_status
_thumbnail_id
_transaction_id
_variation_description
_virtual
_wc_attachment_source
_wc_average_rating
_wc_review_count
_wp_attached_file
_wp_attachment_metadata
_wp_desired_post_slug
_wp_old_slug
_wp_page_template
_wp_trash_meta_comments_status
_wp_trash_meta_status
_wp_trash_meta_time
_wpcom_is_markdown
_used_by
attribute_logo
attribute_pa_color
attribute_pa_size
total_sales
coupon_amount
date_expires
discount_type
exclude_sale_items
free_shipping
individual_use
is_vat_exempt
limit_usage_to_x_items
usage_count
usage_limit
usage_limit_per_user
_shipping_phone
## Customer
_last_order
_order_count
_woocommerce_persistent_cart_1
_woocommerce_tracks_anon_id
admin_color
billing_address_1
billing_address_2
billing_city
billing_company
billing_country
billing_email
billing_first_name
billing_last_name
billing_phone
billing_postcode
billing_state
comment_shortcuts
community-events-location
default_password_nag
description
dismissed_maxmind_license_key_notice
dismissed_no_secure_connection_notice
dismissed_regenerating_thumbnails_notice
dismissed_update_notice
dismissed_wc_admin_notice
dismissed_wp_pointers
first_name
last_name
last_update
locale
paying_customer
rich_editing
session_tokens
shipping_address_1
shipping_address_2
shipping_city
shipping_company
shipping_country
shipping_first_name
shipping_last_name
shipping_method
shipping_phone
shipping_postcode
shipping_state
show_admin_bar_front
show_welcome_panel
syntax_highlighting
use_ssl
wc_last_active
woocommerce_admin_activity_panel_inbox_last_read
wp_capabilities
wp_dashboard_quick_press_last_post_id
wp_product_import_error_log
wp_user_level
wp_user-settings
wp_user-settings-time
wp_woocommerce_product_import_mapping
closedpostboxes_shop_order
metaboxhidden_shop_order
wp__stripe_customer_id

View file

@ -0,0 +1,82 @@
jQuery(function () {
function RetailcrmCronInfo()
{
this.title = jQuery('.debug_info_options').get(0)
this.submitButton = jQuery('button[id="clear_cron_tasks"]').get(0);
if (typeof this.title === 'undefined') {
return false;
}
if (typeof this.submitButton === 'undefined') {
return false;
}
this.icml = 0;
this.history = 0;
this.inventories = 0;
this.messageSuccessful = '';
this.loyaltyUploadPrice = 0;
this.adminUrl = AdminUrl.url;
let _this = this;
jQuery.ajax({
url: this.adminUrl + '/admin-ajax.php?action=cron_info',
method: "POST",
timeout: 0,
data: {ajax: 1},
dataType: "json"
})
.done(function (response) {
_this.history = response.history;
_this.icml = response.icml;
_this.inventories = response.inventories;
_this.messageSuccessful = response.translate.tr_successful;
_this.loyaltyUploadPrice = response.loyaltyUploadPrice
_this.displayInfoAboutCron(
response.translate.tr_td_cron,
response.translate.tr_td_icml,
response.translate.tr_td_history,
response.translate.tr_td_inventories,
response.translate.tr_td_loyaltyUploadPrice
);
})
this.clearCronTasks = this.clearCronTasks.bind(this);
jQuery(this.submitButton).click(this.clearCronTasks);
}
RetailcrmCronInfo.prototype.displayInfoAboutCron = function (cron, icml, history, inventories, loyaltyUploadPrice) {
this.table = jQuery(this.title).next();
this.table.append('<tbody class="retail-debug-info"></tbody>');
this.infoTable = jQuery('tbody[class="retail-debug-info"]').get(0);
jQuery(this.infoTable).append("<tr><td class='retail-cron-info-title'>" + cron + " : " + "</td></tr>");
jQuery(this.infoTable).append("<tr><td>" + icml + "</td><td> " + this.icml + "</td></tr>");
jQuery(this.infoTable).append("<tr><td>" + history + "</td><td> " + this.history + "</td></tr>");
jQuery(this.infoTable).append("<tr><td>" + inventories + "</td><td> " + this.inventories + "</td></tr>");
jQuery(this.infoTable).append("<tr><td>" + loyaltyUploadPrice + "</td><td>" + this.loyaltyUploadPrice + "</td></tr>");
}
RetailcrmCronInfo.prototype.clearCronTasks = function () {
let _this = this;
jQuery.ajax({
type: "POST",
url: this.adminUrl + '/admin-ajax.php?action=clear_cron_tasks',
success: function (response) {
alert(_this.messageSuccessful);
}
});
};
window.RetailcrmCronInfo = RetailcrmCronInfo;
if (!(typeof RetailcrmCronInfo === 'undefined')) {
new window.RetailcrmCronInfo();
}
});

View file

@ -0,0 +1,186 @@
jQuery(function () {
function RetailcrmExportForm()
{
this.submitButton = jQuery('button[id="export-orders-submit"]').get(0);
this.selectedOrdersButton = jQuery('button[id="export_selected_orders_btn"]').get(0);
jQuery(this.submitButton).after('<div id="export-orders-progress" class="retail-progress retailcrm-hidden"></div');
jQuery(this.submitButton).before('<div id="export-orders-count" class="retail-count-data-upload"></div');
this.progressBar = jQuery('div[id="export-orders-progress"]').get(0);
if (typeof this.submitButton === 'undefined') {
return false;
}
if (typeof this.selectedOrdersButton === 'undefined') {
return false;
}
if (typeof this.progressBar === 'undefined') {
return false;
}
jQuery(this.submitButton).addClass('retailcrm-hidden');
this.ordersCount = 0;
this.customersCount = 0;
this.adminUrl = AdminUrl.url;
let _this = this;
jQuery.ajax({
url: this.adminUrl + '/admin-ajax.php?action=content_upload',
method: "POST",
timeout: 0,
data: {ajax: 1},
dataType: "json"
})
.done(function (response) {
_this.ordersCount = Number(response.count_orders);
_this.customersCount = Number(response.count_users);
jQuery(_this.submitButton).removeClass('retailcrm-hidden');
_this.messageEmtyField = response.translate.tr_empty_field;
_this.messageSuccessful = response.translate.tr_successful;
_this.displayCountUploadData(response.translate.tr_order, response.translate.tr_customer);
})
this.isDone = false;
this.ordersStepSize = 50;
this.customersStepSize = 50;
this.ordersStep = 0;
this.customersStep = 0;
this.displayCountUploadData = this.displayCountUploadData.bind(this);
this.submitAction = this.submitAction.bind(this);
this.actionExportSelectedOrders = this.actionExportSelectedOrders.bind(this);
this.exportAction = this.exportAction.bind(this);
this.exportDone = this.exportDone.bind(this);
this.initializeProgressBar = this.initializeProgressBar.bind(this);
this.updateProgressBar = this.updateProgressBar.bind(this);
jQuery(this.submitButton).click(this.submitAction);
jQuery(this.selectedOrdersButton).click(this.actionExportSelectedOrders);
}
RetailcrmExportForm.prototype.displayCountUploadData = function (order, customer) {
this.counter = jQuery('div[id="export-orders-count"]').get(0);
jQuery(this.counter).text(`${customer}: ${this.customersCount} ${order}: ${this.ordersCount}`);
}
RetailcrmExportForm.prototype.submitAction = function (event) {
event.preventDefault();
jQuery(this.counter).css("margin-left", "120px");
this.initializeProgressBar();
this.exportAction();
};
RetailcrmExportForm.prototype.exportAction = function () {
let data = {};
if (this.customersStep * this.customersStepSize < this.customersCount) {
data.Step = this.customersStep;
data.Entity = 'customer';
this.customersStep++;
} else {
if (this.ordersStep * this.ordersStepSize < this.ordersCount) {
data.Step = this.ordersStep;
data.Entity = 'order';
this.ordersStep++;
} else {
data.RETAILCRM_UPDATE_SINCE_ID = 1;
this.isDone = true;
}
}
let _this = this;
jQuery.ajax({
url: this.adminUrl + '/admin-ajax.php?action=do_upload',
method: "POST",
timeout: 0,
data: data
})
.done(function (response) {
if (_this.isDone) {
return _this.exportDone();
}
_this.updateProgressBar();
_this.exportAction();
})
};
RetailcrmExportForm.prototype.initializeProgressBar = function () {
jQuery(this.submitButton).addClass('retailcrm-hidden');
jQuery(this.progressBar)
.removeClass('retailcrm-hidden')
.append(jQuery('<div/>', {class: 'retail-progress__loader', text: '0%'}))
window.addEventListener('beforeunload', this.confirmLeave);
};
RetailcrmExportForm.prototype.updateProgressBar = function () {
let processedOrders = this.ordersStep * this.ordersStepSize;
if (processedOrders > this.ordersCount) {
processedOrders = this.ordersCount;
}
let processedCustomers = this.customersStep * this.customersStepSize;
if (processedCustomers > this.customersCount) {
processedCustomers = this.customersCount;
}
const processed = processedOrders + processedCustomers;
const total = this.ordersCount + this.customersCount;
const percents = Math.round(100 * processed / total);
jQuery(this.progressBar).find('.retail-progress__loader').text(percents + '%');
jQuery(this.progressBar).find('.retail-progress__loader').css('width', percents + '%');
jQuery(this.progressBar).find('.retail-progress__loader').attr('title', processed + '/' + total);
};
RetailcrmExportForm.prototype.confirmLeave = function (event) {
event.preventDefault();
event.returnValue = 'Export process has been started';
}
RetailcrmExportForm.prototype.exportDone = function () {
window.removeEventListener('beforeunload', this.confirmLeave);
alert(this.messageSuccessful);
}
RetailcrmExportForm.prototype.actionExportSelectedOrders = function () {
let ids = jQuery('#woocommerce_integration-retailcrm_export_selected_orders_ids').val();
if (ids === '') {
alert(this.messageEmtyField);
} else {
let _this = this;
jQuery.ajax({
type: "POST",
url: this.adminUrl + '/admin-ajax.php?action=upload_selected_orders&order_ids_retailcrm=' + ids,
success: function (response) {
alert(_this.messageSuccessful);
}
});
}
};
window.RetailcrmExportForm = RetailcrmExportForm;
if (!(typeof RetailcrmExportForm === 'undefined')) {
new window.RetailcrmExportForm();
}
});

View file

@ -0,0 +1,121 @@
jQuery(function() {
jQuery('#loyaltyRegisterForm').on("submit", function (event) {
var termsCheck = jQuery('#termsLoyalty');
var privacyCheck = jQuery('#privacyLoyalty');
if (termsCheck.length) {
if (!termsCheck.is(':checked')) {
event.preventDefault();
return false;
}
}
if (privacyCheck.length) {
if (!privacyCheck.is(':checked')) {
event.preventDefault();
return false;
}
}
let phone = jQuery('#phoneLoyalty');
if (!phone.val().match(/(?:\+|\d)[\d\-\(\) ]{7,}\d/)) {
if (!jQuery('#warningLoyaltyPhone').length) {
phone.parent().append('<span style="color: red" id="warningLoyaltyPhone">' + messagePhone + '</span>')
}
event.preventDefault();
return false;
} else {
jQuery('#warningLoyaltyPhone').remove();
}
jQuery.ajax({
url: loyaltyUrl.url + '/admin-ajax.php?action=register_customer_loyalty',
method: 'POST',
timeout: 0,
data: {ajax: 1, phone: phone.val(), userId: customerId},
dataType: 'json'
})
.done(function (response) {
if (response.hasOwnProperty('error')) {
jQuery('#loyaltyRegisterForm').append('<p style="color: red">'+ response.error + '</p>')
event.preventDefault();
return false;
} else {
location.reload();
}
})
event.preventDefault();
});
jQuery('#loyaltyActivateForm').on("submit", function (event) {
var activateCheckbox = jQuery('#loyaltyActiveCheckbox');
if (activateCheckbox.length) {
if (!activateCheckbox.is(':checked')) {
event.preventDefault();
return false;
}
}
jQuery.ajax({
url: loyaltyUrl.url + '/admin-ajax.php?action=activate_customer_loyalty',
method: 'POST',
timeout: 0,
data: {ajax: 1, loyaltyId: loyaltyId},
dataType: 'json'
})
.done(function (response) {
if (response.hasOwnProperty('error')) {
jQuery('#loyaltyRegisterForm').append('<p style="color: red">'+ response.error + '</p>')
event.preventDefault();
return false;
} else {
location.reload();
}
})
event.preventDefault();
});
jQuery('.popup-open-loyalty').click(function() {
if (jQuery(this).attr('id') === 'terms-popup') {
jQuery('#popup-loyalty-text').html(termsLoyalty);
} else {
jQuery('#popup-loyalty-text').html(privacyLoyalty);
}
jQuery('.popup-fade-loyalty').fadeIn();
return false;
});
jQuery('.popup-close-loyalty').click(function() {
jQuery(this).parents('.popup-fade-loyalty').fadeOut();
return false;
});
jQuery(document).keydown(function(e) {
if (e.keyCode === 27) { // Key Escape
e.stopPropagation();
jQuery('.popup-fade-loyalty').fadeOut();
}
});
jQuery('.popup-fade-loyalty').click(function(e) {
if (jQuery(e.target).closest('.popup-loyalty').length == 0) {
jQuery(this).fadeOut();
}
});
jQuery('#phoneLoyalty').keydown(function (e) {
let key = e.key;
return (key >= '0' && key <= '9') || key == '+' || key == '(' || key == ')'|| key == '-' ||
key == 'ArrowLeft' || key == 'ArrowRight' || key == 'Delete' || key == 'Backspace';
});
});

View file

@ -0,0 +1,6 @@
function inputLoyaltyCode() {
let couponInput = document.getElementById('coupon_code');
let couponCode = document.getElementById('input_loyalty_code');
couponInput.value = couponCode.innerText;
}

View file

@ -0,0 +1,36 @@
jQuery(function() {
if (jQuery('#woocommerce_integration-retailcrm_loyalty').is(':checked')) {
checkActiveCoupon();
}
jQuery('#woocommerce_integration-retailcrm_loyalty').change(function () {
if (this.checked) {
checkActiveCoupon();
}
})
function checkActiveCoupon()
{
jQuery.ajax({
url: AdminUrl.url + '/admin-ajax.php?action=get_status_coupon',
method: 'POST',
timeout: 0,
data: {ajax: 1},
dataType: 'json'
})
.done(function (response) {
if (response.coupon_status !== 'yes') {
var checkElement = jQuery('#woocommerce_integration-retailcrm_loyalty');
checkElement.parent().css('color', 'red');
checkElement.css('border-color', 'red');
checkElement.prop('checked', false);
if (!jQuery('#coupon_warning').length) {
checkElement.parent().parent().append(
"<p id='coupon_warning' style='color: red'>" + response.translate.coupon_warning + "</p>"
);
}
}
})
}
});

View file

@ -0,0 +1,335 @@
jQuery(function () {
function RetailcrmMetaFields()
{
jQuery('.order-meta-data-retailcrm').closest('tr').addClass('retailcrm-hidden')
jQuery('.customer-meta-data-retailcrm').closest('tr').addClass('retailcrm-hidden')
jQuery('.customer-meta-data-retailcrm').closest('tr').after('<tr class="retailcrm-insert-select"></tr>')
this.insertSelects = jQuery('.retailcrm-insert-select').get(0);
this.orderTextArea = jQuery('.order-meta-data-retailcrm').get(0);
this.customerTextArea = jQuery('.customer-meta-data-retailcrm').get(0);
this.indexOrder = 1;
this.indexCustomer = 1;
if (typeof this.insertSelects === 'undefined'
|| this.orderTextArea === 'undefined'
|| this.customerTextArea === 'undefined'
) {
return false;
}
jQuery(this.insertSelects).append(
`<label class="retailcrm-order-label">Custom fields for order</label><div class="retailcrm-order-selects"></div>` +
`<label class="retailcrm-customer-label">Custom fields for customer</label><div class="retailcrm-customer-selects"></div>` +
`<button class="add-new-select-retailcrm" id="add-new-select-order-retailcrm">Add new select for order</button>` +
`<button class="add-new-select-retailcrm" id="add-new-select-customer-retailcrm">Add new select for customer</button>`
);
let _this = this;
if (this.orderTextArea.value === '') {
this.orderTextArea.value = JSON.stringify({});
}
if (this.customerTextArea.value === '') {
this.customerTextArea.value = JSON.stringify({});
}
jQuery.ajax({
url: AdminUrl.url + '/admin-ajax.php?action=set_meta_fields',
method: 'POST',
timeout: 0,
data: {ajax: 1},
dataType: 'json'
})
.done(function (response) {
_this.dataFields = response;
jQuery(".retailcrm-order-label").text(response.translate.tr_lb_order);
jQuery(".retailcrm-customer-label").text(response.translate.tr_lb_customer);
jQuery(".add-new-select-retailcrm").text(response.translate.tr_btn);
jQuery(".add-new-select-retailcrm").text(response.translate.tr_btn);
if (jQuery.isEmptyObject(JSON.parse(_this.orderTextArea.value)) && jQuery.isEmptyObject(JSON.parse(_this.customerTextArea.value))) {
_this.createSelects(_this.dataFields);
}
if (!jQuery.isEmptyObject(JSON.parse(_this.orderTextArea.value))) {
let objectJson = JSON.parse(_this.orderTextArea.value);
_this.indexOrder = _this.restoreSelectsData(
objectJson,
_this.dataFields.order,
'order',
1
);
}
if (!jQuery.isEmptyObject(JSON.parse(_this.customerTextArea.value))) {
let objectJson = JSON.parse(_this.customerTextArea.value);
_this.indexCustomer = _this.restoreSelectsData(
objectJson,
_this.dataFields.customer,
'customer',
1
);
}
})
jQuery(this.insertSelects).on('change', '.retailcrm-meta-select', function() {
let selectsData = {};
let typeSelect = this.id.split('-')[0]
let idChangeSelect = this.id.split('-')[2]
if (this.id.includes('order')) {
selectsData = _this.getSelectsData(
_this.orderTextArea.value,
'order',
idChangeSelect,
typeSelect
);
} else {
selectsData = _this.getSelectsData(
_this.customerTextArea.value,
'customer',
idChangeSelect,
typeSelect
);
}
if (Object.keys(selectsData).length != 0) {
if (this.id.includes('order')) {
_this.orderTextArea.value = JSON.stringify(selectsData);
} else {
_this.customerTextArea.value = JSON.stringify(selectsData);
}
}
});
jQuery(this.insertSelects).on('click', '.add-new-select-retailcrm', function(event) {
event.preventDefault();
if (this.id.includes('order')) {
_this.addPairSelects(
'.retailcrm-order-selects',
'order',
_this.dataFields.order,
_this.indexOrder
);
_this.indexOrder++;
} else {
_this.addPairSelects(
'.retailcrm-customer-selects',
'customer',
_this.dataFields.customer,
_this.indexCustomer
);
_this.indexCustomer++;
}
});
jQuery(this.insertSelects).on('click', '.delete-select-retailcrm', function(event) {
event.preventDefault();
let objectJson = {}
let entity = this.id.split('-')[0];
let index = this.id.split('-')[1];
if (entity === 'order') {
objectJson = JSON.parse(_this.orderTextArea.value);
} else {
objectJson = JSON.parse(_this.customerTextArea.value);
}
let metaFiled = jQuery(`#metaFields-${entity}-${index}`);
let valueMetaField = metaFiled.val();
for (let i = 1; i <= index; i++) {
jQuery(`#select-pair-${entity}-${i}`).remove();
}
delete objectJson[valueMetaField];
if (entity === 'order') {
_this.indexOrder = _this.restoreSelectsData(
objectJson,
_this.dataFields.order,
'order',
1
);
_this.orderTextArea.value = JSON.stringify(objectJson);
} else {
_this.indexCustomer = _this.restoreSelectsData(
objectJson,
_this.dataFields.customer,
'customer',
1
);
_this.customerTextArea.value = JSON.stringify(objectJson);
}
jQuery(`#add-new-select-${entity}-retailcrm`).removeClass('retailcrm-hidden')
});
}
RetailcrmMetaFields.prototype.restoreSelectsData = function (objectJson, dataEntity, entity, index) {
for (let key in objectJson) {
let getMetaSelect = `#metaFields-${entity}-${index} option[value=${key}]`;
let getCustomSelect = `#customFields-${entity}-${index} option[value=${objectJson[key]}]`;
this.addPairSelects(`.retailcrm-${entity}-selects`, entity, dataEntity, index);
jQuery(getMetaSelect).prop('selected', true);
jQuery(getCustomSelect).prop('selected', true);
index++;
}
return index;
}
RetailcrmMetaFields.prototype.getSelectsData = function (textarea, entity, idChangeSelect, typeSelect) {
let _this = this;
let selectsData = {};
if (!jQuery.isEmptyObject(textarea)) {
let objectJson = JSON.parse(textarea);
let field = jQuery(`#${typeSelect}-${entity}-${idChangeSelect}`);
let selectedField = jQuery(`#${typeSelect}-${entity}-${idChangeSelect} option:selected`);
let fieldValue = field.val();
if (Object.values(objectJson).includes(fieldValue) || fieldValue in objectJson) {
_this.addWarning(field, selectedField.text());
// Search key in object by id selects
let counter = 0;
for (let key in objectJson) {
counter++;
if (counter == idChangeSelect) {
delete objectJson[key];
}
}
if (entity === 'order') {
_this.orderTextArea.value = JSON.stringify(objectJson);
} else {
_this.customerTextArea.value = JSON.stringify(objectJson);
}
}
}
for (let i = 1; i <= jQuery(`.retailcrm-${entity}-selects div`).length; i++) {
let metaFiled = jQuery(`#metaFields-${entity}-${i}`);
let customField = jQuery(`#customFields-${entity}-${i}`);
let valueMetaField = metaFiled.val();
let valueCustomField = customField.val();
if (valueMetaField === 'default_retailcrm' || valueCustomField === 'default_retailcrm') {
continue;
}
_this.deleteWarning(metaFiled);
_this.deleteWarning(customField);
selectsData[valueMetaField] = valueCustomField;
}
return selectsData;
}
RetailcrmMetaFields.prototype.createSelects = function (dataFields) {
let orderData = dataFields.order;
let customerData = dataFields.customer;
//Create selects for order data
this.buildElements('.retailcrm-order-selects', 'order', this.indexOrder);
this.fillSelects(orderData, 'order', this.indexOrder);
//Create selects for customer data
this.buildElements('.retailcrm-customer-selects', 'customer', this.indexCustomer);
this.fillSelects(customerData, 'customer', this.indexCustomer);
this.indexOrder++;
this.indexCustomer++;
}
RetailcrmMetaFields.prototype.buildElements = function (element, entity, index) {
jQuery(element).append(
`<div class="retailcrm-select-pair" id="select-pair-${entity}-${index}">` +
`<select class="retailcrm-meta-select" id="metaFields-${entity}-${index}"></select>` +
`<select class="retailcrm-meta-select" id="customFields-${entity}-${index}"></select>` +
`<button class="delete-select-retailcrm" id="${entity}-${index}"></button></div>`
);
jQuery(`#add-new-select-${entity}-retailcrm`).insertAfter(`#select-pair-${entity}-${index}`);
}
RetailcrmMetaFields.prototype.fillSelects = function (data, entity, index) {
let allCountCrmFields = Object.keys(data.custom).length + Object.keys(data.crmDefault).length;
if (Object.keys(data.meta).length - 1 <= index || allCountCrmFields - 1 <= index) {
if (entity === 'order') {
jQuery('#add-new-select-order-retailcrm').addClass('retailcrm-hidden')
} else {
jQuery('#add-new-select-customer-retailcrm').addClass('retailcrm-hidden')
}
}
// Set meta fields in select
jQuery.each(data.meta, function(key, value) {
jQuery(`#metaFields-${entity}-${index}`)
.append(jQuery('<option></option>')
.attr('value', key)
.text(value));
});
// Set custom fields in select
jQuery.each(data.custom, function(key, value) {
jQuery(`#customFields-${entity}-${index}`)
.append(jQuery('<option></option>')
.attr('value', key)
.text(value));
});
jQuery(`#customFields-${entity}-${index}`)
.append(jQuery(`<optgroup id=default-${entity}-${index}-crm-fields label = '${data.tr_default_crm_fields}'></optgroup>`));
jQuery.each(data.crmDefault, function(key, value) {
jQuery(`#default-${entity}-${index}-crm-fields`)
.append(jQuery('<option></option>')
.attr('value', key)
.text(value));
});
}
RetailcrmMetaFields.prototype.addPairSelects = function (element, entity, data, index) {
this.buildElements(element, entity, index);
this.fillSelects(data, entity, index);
}
RetailcrmMetaFields.prototype.addWarning = function (field, fieldValue) {
alert(fieldValue + ' already been selected');
field.prop('value', 'default_retailcrm');
field.addClass('red-selected-retailcrm');
}
RetailcrmMetaFields.prototype.deleteWarning = function (field) {
if (field.css('border-color') === "rgb(255, 0, 0)") {
field.removeClass('red-selected-retailcrm')
}
}
window.RetailcrmMetaFields = RetailcrmMetaFields;
if (typeof RetailcrmMetaFields !== 'undefined') {
new window.RetailcrmMetaFields();
}
});

View file

@ -0,0 +1,17 @@
jQuery(function () {
if (document.querySelector('#woocommerce_integration-retailcrm_bind_by_sku')) {
document.querySelector('#woocommerce_integration-retailcrm_bind_by_sku').onchange = function() {
let useXmlId = this.checked ? 'yes' : 'no';
document.querySelector('.submit').onmousedown = function() {
jQuery.ajax({
url: AdminUrl.url + '/admin-ajax.php?action=generate_icml',
method: 'POST',
timeout: 0,
data: {useXmlId: useXmlId},
dataType: 'json'
})
}
};
}
})

124
src/config/objects.xml Normal file
View file

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<options>
<fields>
<field id="id" group="customer">id</field>
<field id="first_name" group="customer">firstName</field>
<field id="last_name" group="customer">lastName</field>
<field id="patronymic" group="customer">patronymic</field>
<field id="email" group="customer">email</field>
<field id="birthday" group="customer">birthday</field>
<field id="phones" group="customer">phones</field>
<field id="manager" group="customer">manager</field>
<field id="commentary" group="customer">commentary</field>
<field id="external_id" group="customer">externalId</field>
<field id="cumulative_discount" group="customer">cumulativeDiscount</field>
<field id="personal_discount" group="customer">personalDiscount</field>
<field id="discount_card_number" group="customer">discountCardNumber</field>
<field id="id" group="customerCorporate">id</field>
<field id="external_id" group="customerCorporate">externalId</field>
<field id="nick_name" group="customerCorporate">nickName</field>
<field id="vip" group="customerCorporate">vip</field>
<field id="bad" group="customerCorporate">bad</field>
<field id="custom_fields" group="customerCorporate">customFields</field>
<field id="personal_discount" group="customerCorporate">personalDiscount</field>
<field id="discount_card_number" group="customerCorporate">discountCardNumber</field>
<field id="manager" group="customerCorporate">manager</field>
<field id="address" group="customerCorporate">address</field>
<field id="main_contact" group="customerCorporate">mainCustomerContact</field>
<field id="company.contragent.i_n_n" group="customerCorporate">companyInn</field>
<field id="main_company" group="customerCorporate">company</field>
<field id="address.index" group="customerAddress">index</field>
<field id="address.country" group="customerAddress">country</field>
<field id="address.region" group="customerAddress">region</field>
<field id="address.city" group="customerAddress">city</field>
<field id="address.street" group="customerAddress">street</field>
<field id="address.building" group="customerAddress">building</field>
<field id="address.house" group="customerAddress">house</field>
<field id="address.block" group="customerAddress">block</field>
<field id="address.flat" group="customerAddress">flat</field>
<field id="address.floor" group="customerAddress">floor</field>
<field id="address.intercom_code" group="customerAddress">intercomCode</field>
<field id="address.metro" group="customerAddress">metro</field>
<field id="address.notes" group="customerAddress">notes</field>
<field id="contragent.contragent_type" group="customerContragent">contragentType</field>
<field id="contragent.legal_name" group="customerContragent">legalName</field>
<field id="contragent.legal_address" group="customerContragent">legalAddress</field>
<field id="contragent.certificate_number" group="customerContragent">certificateNumber</field>
<field id="contragent.certificate_date" group="customerContragent">certificateDate</field>
<field id="contragent.bank" group="customerContragent">bank</field>
<field id="contragent.bank_address" group="customerContragent">bankAddress</field>
<field id="contragent.corr_account" group="customerContragent">corrAccount</field>
<field id="contragent.bank_account" group="customerContragent">bankAccount</field>
<field id="id" group="order">id</field>
<field id="created_at" group="order">createdAt</field>
<field id="order_type" group="order">orderType</field>
<field id="order_method" group="order">orderMethod</field>
<field id="site" group="order">site</field>
<field id="status" group="order">status</field>
<field id="customer" group="order">customer</field>
<field id="manager" group="order">manager</field>
<field id="first_name" group="order">firstName</field>
<field id="last_name" group="order">lastName</field>
<field id="patronymic" group="order">patronymic</field>
<field id="phone" group="order">phone</field>
<field id="additional_phone" group="order">additionalPhone</field>
<field id="email" group="order">email</field>
<field id="payment_type" group="order">paymentType</field>
<field id="payment_status" group="order">paymentStatus</field>
<field id="discount" group="order">discount</field>
<field id="discount_percent" group="order">discountPercent</field>
<field id="prepay_sum" group="order">prepaySum</field>
<field id="customer_comment" group="order">customerComment</field>
<field id="manager_comment" group="order">managerComment</field>
<field id="shipment_store" group="order">shipmentStore</field>
<field id="shipment_date" group="order">shipmentDate</field>
<field id="shipped" group="order">shipped</field>
<field id="contact" group="order">contact</field>
<field id="company" group="order">company</field>
<field id="payment" group="order">payment</field>
<field id="payments.amount" group="order">amount</field>
<field id="payments.paid_at" group="order">paidAt</field>
<field id="payments.comment" group="order">comment</field>
<field id="payments.type" group="order">type</field>
<field id="payments.status" group="order">status</field>
<field id="order_product.id" group="item">id</field>
<field id="order_product.initial_price" group="item">initialPrice</field>
<field id="order_product.discount_total" group="item">discountTotal</field>
<field id="order_product.discount_percent" group="item">discountPercent</field>
<field id="order_product.quantity" group="item">quantity</field>
<field id="order_product.status" group="item">status</field>
<field id="order_product.summ" group="item">summ</field>
<field id="order_product.price_type" group="item">priceType</field>
<field id="delivery_type" group="delivery">code</field>
<field id="delivery_service" group="delivery">service</field>
<field id="delivery_date" group="delivery">date</field>
<field id="delivery_time" group="delivery">time</field>
<field id="delivery_cost" group="delivery">cost</field>
<field id="delivery_net_cost" group="delivery">netCost</field>
<field id="delivery_address.country" group="orderAddress">country</field>
<field id="delivery_address.index" group="orderAddress">index</field>
<field id="delivery_address.region" group="orderAddress">region</field>
<field id="delivery_address.city" group="orderAddress">city</field>
<field id="delivery_address.street" group="orderAddress">street</field>
<field id="delivery_address.building" group="orderAddress">building</field>
<field id="delivery_address.house" group="orderAddress">house</field>
<field id="delivery_address.block" group="orderAddress">block</field>
<field id="delivery_address.flat" group="orderAddress">flat</field>
<field id="delivery_address.floor" group="orderAddress">floor</field>
<field id="delivery_address.intercom_code" group="orderAddress">intercomCode</field>
<field id="delivery_address.metro" group="orderAddress">metro</field>
<field id="delivery_address.notes" group="orderAddress">notes</field>
<field id="integration_delivery_data.status" group="integrationDelivery">status</field>
<field id="integration_delivery_data.track_number" group="integrationDelivery">trackNumber</field>
<field id="integration_delivery_data.courier" group="integrationDelivery">courier</field>
</fields>
</options>

View file

@ -0,0 +1,97 @@
<?php
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Abstract_Builder - Builds data for CRM.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
abstract class WC_Retailcrm_Abstract_Builder implements WC_Retailcrm_Builder_Interface
{
/** @var array|mixed $data */
protected $data;
/**
* @param array|mixed $data
*
* @return \WC_Retailcrm_Abstract_Builder
*/
public function setData($data)
{
$this->data = $data;
return $this;
}
/**
* @return array|mixed
*
* @codeCoverageIgnore
*/
public function getData()
{
return $this->data;
}
/**
* @return $this|\WC_Retailcrm_Builder_Interface
*/
public function reset()
{
$this->data = [];
return $this;
}
/**
* Returns key if it's present in data array (or object which implements ArrayAccess).
* Returns default value if key is not present in data, or data is not accessible as array.
*
* @param string $key
* @param mixed $default
*
* @return mixed
*/
protected function dataValue($key, $default = '')
{
return self::arrayValue($this->data, $key, $default);
}
/**
* Returns key from array if it's present in array
*
* @param array|\ArrayObject $data
* @param mixed $key
* @param string $default
*
* @return mixed|string
*/
protected static function arrayValue($data, $key, $default = '')
{
if (!is_array($data) && !($data instanceof ArrayAccess)) {
return $default;
}
if (array_key_exists($key, $data) && !empty($data[$key])) {
return $data[$key];
}
return $default;
}
/**
* @return \WC_Retailcrm_Builder_Interface
*/
abstract public function build();
/**
* Returns builder result. Should return null if WC_Retailcrm_Abstract_Builder::isBuilt() == false.
*
* @return mixed|null
*/
abstract public function getResult();
}

View file

@ -0,0 +1,145 @@
<?php
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Abstracts_Address - Builds data for addresses orders/customers.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
abstract class WC_Retailcrm_Abstracts_Address extends WC_Retailcrm_Abstracts_Data
{
/**
* Divider for order delivery address_1 and address_2
*/
const ADDRESS_LINE_DIVIDER = ' || ';
/**
* Returns shipping address from order.
*
* @param \WC_Order $order
*
* @return array
*/
protected function getOrderAddress($order)
{
if (!$order instanceof WC_Order) {
return [];
}
return [
'index' => $order->get_shipping_postcode(),
'city' => $order->get_shipping_city(),
'region' => $this->getRegion($order->get_shipping_country(), $order->get_shipping_state()),
'text' => $this->getText($order, 'order'),
];
}
/**
* Returns billing address from customer.
*
* @param \WC_Customer $customer
* @param \WC_Order $order
*
* @return array
*/
protected function getCustomerAddress($customer, $order)
{
if (!$customer instanceof WC_Customer) {
return [];
}
$customerBillingAddress = $customer->get_billing_address();
if ($order instanceof WC_Order && empty($customerBillingAddress)) {
return [
'index' => $order->get_billing_postcode(),
'countryIso' => $this->getCountryCode($order->get_billing_country()),
'region' => $this->getRegion($order->get_billing_country(), $order->get_billing_state()),
'city' => $order->get_billing_city(),
'text' => $this->getText($order),
];
} else {
return [
'index' => $customer->get_billing_postcode(),
'countryIso' => $this->getCountryCode($customer->get_billing_country()),
'region' => $this->getRegion($customer->get_billing_country(), $customer->get_billing_state()),
'city' => $customer->get_billing_city(),
'text' => $this->getText($customer),
];
}
}
/**
* Glue two addresses
*
* @param string $address1
* @param string $address2
*
* @return string
*/
protected function joinAddresses(string $address1 = '', string $address2 = '')
{
return implode(self::ADDRESS_LINE_DIVIDER, array_filter([$address1, $address2]));
}
/**
* Validate countryIso. Check if a given code represents a valid ISO 3166-1 alpha-2 code.
*
* @param $countryCode
*
* @return string
*/
private function getCountryCode($countryCode)
{
$countries = new WC_Countries();
return $countries->country_exists($countryCode) ? $countryCode : '';
}
/**
* Returns state name by it's code
*
* @param string $countryCode
* @param string $stateCode
*
* @return string
*/
protected function getRegion(string $countryCode, string $stateCode)
{
if (preg_match('/^[A-Z\-0-9]{0,5}$/', $stateCode) && !is_null($countryCode)) {
$countriesProvider = new WC_Countries();
$states = $countriesProvider->get_states($countryCode);
if (!empty($states) && array_key_exists($stateCode, $states)) {
return (string) $states[$stateCode];
}
}
return $stateCode;
}
/**
* Returns data for CRM field 'text'.
* If type entity equals 'order', get address for order and use shipping address,
* else get address for customer and use billing address.
*
* @return string
*/
protected function getText($wcEntity, $typeEntity = 'customer')
{
if ($typeEntity === 'order') {
return empty($wcEntity->get_shipping_address_2())
? $wcEntity->get_shipping_address_1()
: $this->joinAddresses($wcEntity->get_shipping_address_1(), $wcEntity->get_shipping_address_2());
} else {
return empty($wcEntity->get_billing_address_2())
? $wcEntity->get_billing_address_1()
: $this->joinAddresses($wcEntity->get_billing_address_1(), $wcEntity->get_billing_address_2());
}
}
}

View file

@ -0,0 +1,59 @@
<?php
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Abstracts_Data - Class manage different data.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
abstract class WC_Retailcrm_Abstracts_Data
{
/** @var array */
protected $data = [];
/**
* @param $data
*
* @return self
*/
abstract public function build($data);
/**
* @codeCoverageIgnore
*/
protected function setField($field, $value)
{
if (isset($this->data[$field]) && \gettype($value) !== \gettype($this->data[$field])) {
return false;
}
$this->data[$field] = $value;
return true;
}
/**
* @param $fields
*/
protected function setDataFields($fields)
{
if (!empty($fields)) {
foreach ($fields as $field => $value) {
$this->setField($field, $value);
}
}
}
/**
* @return array
*/
public function getData()
{
return $this->data;
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,18 @@
<?php
/**
* PHP version 5.3
* PHP version 7.0
*
* WC_Retailcrm_Exception_Curl class
* Class WC_Retailcrm_Exception_Curl.
*
* @category RetailCRM
* @package WC_Retailcrm_Exception_Curl
* @author RetailCRM <dev@retailcrm.ru>
* @license https://opensource.org/licenses/MIT MIT License
* @link http://retailcrm.ru/docs/Developers/ApiVersion4
* @link http://retailcrm.ru/docs/Developers/ApiVersion5
*/
class WC_Retailcrm_Exception_Curl extends \RuntimeException
{
}

View file

@ -1,16 +1,18 @@
<?php
/**
* PHP version 5.3
* PHP version 7.0
*
* WC_Retailcrm_Exception_Json class
* Class WC_Retailcrm_Exception_Json.
*
* @category RetailCRM
* @package WC_Retailcrm_Exception_Json
* @author RetailCRM <dev@retailcrm.ru>
* @license https://opensource.org/licenses/MIT MIT License
* @link http://retailcrm.ru/docs/Developers/ApiVersion5
*/
class WC_Retailcrm_Exception_Json extends \DomainException
{
}

View file

@ -0,0 +1,136 @@
<?php
if (!class_exists('WC_Retailcrm_Proxy')) :
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Proxy - RetailCRM Integration.
*
* @category Integration
* @package WC_Retailcrm_Proxy
* @author RetailCRM <dev@retailcrm.ru>
* @license https://opensource.org/licenses/MIT MIT License
* @link http://retailcrm.ru/docs/Developers/ApiVersion5
*/
class WC_Retailcrm_Proxy
{
protected $retailcrm;
protected $corporateEnabled;
public function __construct($api_url, $api_key, $corporateEnabled = false)
{
$this->corporateEnabled = $corporateEnabled;
if (!class_exists('WC_Retailcrm_Client_V5')) {
include_once(WC_Integration_Retailcrm::checkCustomFile('include/api/class-wc-retailcrm-client-v5.php'));
}
$this->retailcrm = new WC_Retailcrm_Client_V5($api_url, $api_key);
}
public function getCorporateEnabled(): ?bool
{
return $this->corporateEnabled;
}
/**
* Response will be omitted in logs for those methods
*
* @return string[]
*/
private function methodsWithoutFullLog(): array
{
$methodsList = [
'statusesList',
'paymentTypesList',
'deliveryTypesList',
'orderMethodsList',
'storesList',
];
foreach ($methodsList as $key => $method) {
$method = get_class($this->retailcrm) . '::' . $method;
$methodsList[$key] = $method;
}
return $methodsList;
}
public function __call($method, $arguments)
{
$response = null;
$called = sprintf('%s::%s', get_class($this->retailcrm), $method);
try {
$response = $this->getResponse($method, $arguments);
if (is_string($response)) {
WC_Retailcrm_Logger::info($method, $response, [], WC_Retailcrm_Logger::RESPONSE);
return $response;
}
if (!$response instanceof WC_Retailcrm_Response) {
WC_Retailcrm_Logger::error(
$method,
sprintf("[%s] null (no response whatsoever)", $called),
[],
WC_Retailcrm_Logger::RESPONSE
);
return null;
}
$this->logResponse($response, $method, $called);
} catch (WC_Retailcrm_Exception_Curl|WC_Retailcrm_Exception_Json|InvalidArgumentException $exception) {
WC_Retailcrm_Logger::exception($method, $exception);
}
return $response instanceof WC_Retailcrm_Response ? $response : new WC_Retailcrm_Response(900, '{}');
}
private function getResponse($method, $arguments)
{
WC_Retailcrm_Logger::info(
$method,
$arguments === [] ? '[no params]' : '[with params]',
['params' => $arguments],
WC_Retailcrm_Logger::REQUEST
);
return call_user_func_array(array($this->retailcrm, $method), $arguments);
}
private function logResponse(WC_Retailcrm_Response $response, $method, $called): void
{
if ($response->isSuccessful()) {
if (in_array($called, $this->methodsWithoutFullLog())) {
WC_Retailcrm_Logger::info(
$method,
'Ok',
['body' => 'request was successful, but response is omitted'],
WC_Retailcrm_Logger::RESPONSE
);
} else {
WC_Retailcrm_Logger::info(
$method,
'Ok',
['body' => json_decode($response->getRawResponse(), true)],
WC_Retailcrm_Logger::RESPONSE
);
}
} else {
WC_Retailcrm_Logger::error(
$method,
sprintf(
"Error: [HTTP-code %s] %s",
$response->getStatusCode(),
$response->getErrorString()
),
['response' => json_decode($response->getRawResponse(), true)],
WC_Retailcrm_Logger::RESPONSE
);
}
}
}
endif;

View file

@ -1,24 +1,24 @@
<?php
if (!class_exists('WC_Retailcrm_Exception_Curl')) {
include_once(WC_Integration_Retailcrm::checkCustomFile('include/api/class-wc-retailcrm-exception-curl.php'));
}
if (!class_exists('WC_Retailcrm_Response')) {
include_once(WC_Integration_Retailcrm::checkCustomFile('include/api/class-wc-retailcrm-response.php'));
}
/**
* PHP version 5.3
* PHP version 7.0
*
* Request class
* Class WC_Retailcrm_Request - Request class.
*
* @category Integration
* @package WC_Retailcrm_Request
* @author RetailCRM <dev@retailcrm.ru>
* @license https://opensource.org/licenses/MIT MIT License
* @link http://retailcrm.ru/docs/Developers/ApiVersion5
*/
if ( ! class_exists( 'WC_Retailcrm_Exception_Curl' ) ) {
include_once( __DIR__ . '/class-wc-retailcrm-exception-curl.php' );
}
if ( ! class_exists( 'WC_Retailcrm_Response' ) ) {
include_once( __DIR__ . '/class-wc-retailcrm-response.php' );
}
class WC_Retailcrm_Request
{
const METHOD_GET = 'GET';
@ -33,16 +33,9 @@ class WC_Retailcrm_Request
* @param string $url api url
* @param array $defaultParameters array of parameters
*
* @throws \InvalidArgumentException
*/
public function __construct($url, array $defaultParameters = array())
public function __construct($url, array $defaultParameters = [])
{
if (false === stripos($url, 'https://')) {
throw new \InvalidArgumentException(
'API schema requires HTTPS protocol'
);
}
$this->url = $url;
$this->defaultParameters = $defaultParameters;
}
@ -64,9 +57,9 @@ class WC_Retailcrm_Request
public function makeRequest(
$path,
$method,
array $parameters = array()
array $parameters = []
) {
$allowedMethods = array(self::METHOD_GET, self::METHOD_POST);
$allowedMethods = [self::METHOD_GET, self::METHOD_POST];
if (!in_array($method, $allowedMethods, false)) {
throw new \InvalidArgumentException(
@ -78,7 +71,16 @@ class WC_Retailcrm_Request
);
}
$parameters = array_merge($this->defaultParameters, $parameters);
$parameters = self::METHOD_GET === $method
? array_merge($this->defaultParameters, $parameters, [
'cms_source' => 'WordPress',
'cms_version' => function_exists('get_bloginfo') ? get_bloginfo('version') : '',
'woo_version' => WC()->version ?? '',
'php_version' => function_exists('phpversion') ? phpversion() : '',
'module_version' => WC_Integration_Retailcrm::MODULE_VERSION,
'ga_option_is_active' => getOptionByCode('ua') === WC_Retailcrm_Abstracts_Settings::YES,
])
: array_merge($this->defaultParameters, $parameters);
$url = $this->url . $path;

View file

@ -1,21 +1,20 @@
<?php
if (!class_exists('WC_Retailcrm_Exception_Json')) {
include_once(WC_Integration_Retailcrm::checkCustomFile('include/api/class-wc-retailcrm-exception-json.php'));
}
/**
* PHP version 5.3
* PHP version 7.0
*
* Response class
* Class WC_Retailcrm_Response - Response class.
*
* @category Integration
* @package WC_Retailcrm_Response
* @author RetailCRM <dev@retailcrm.ru>
* @license https://opensource.org/licenses/MIT MIT License
* @link http://retailcrm.ru/docs/Developers/ApiVersion5
*/
if ( ! class_exists( 'WC_Retailcrm_Exception_Json' ) ) {
include_once( __DIR__ . '/class-wc-retailcrm-exception-json.php' );
}
class WC_Retailcrm_Response implements \ArrayAccess
{
// HTTP response status code
@ -24,6 +23,9 @@ class WC_Retailcrm_Response implements \ArrayAccess
// response assoc array
protected $response;
// response raw data
protected $rawResponse;
/**
* ApiResponse constructor.
*
@ -35,6 +37,7 @@ class WC_Retailcrm_Response implements \ArrayAccess
public function __construct($statusCode, $responseBody = null)
{
$this->statusCode = (int) $statusCode;
$this->rawResponse = $responseBody;
if (!empty($responseBody)) {
$response = json_decode($responseBody, true);
@ -117,9 +120,8 @@ class WC_Retailcrm_Response implements \ArrayAccess
* @param mixed $value value
*
* @throws \BadMethodCallException
* @return void
*/
public function offsetSet($offset, $value)
public function offsetSet($offset, $value): void
{
throw new \BadMethodCallException('This activity not allowed');
}
@ -130,9 +132,8 @@ class WC_Retailcrm_Response implements \ArrayAccess
* @param mixed $offset offset
*
* @throws \BadMethodCallException
* @return void
*/
public function offsetUnset($offset)
public function offsetUnset($offset): void
{
throw new \BadMethodCallException('This call not allowed');
}
@ -142,9 +143,8 @@ class WC_Retailcrm_Response implements \ArrayAccess
*
* @param mixed $offset offset
*
* @return bool
*/
public function offsetExists($offset)
public function offsetExists($offset): bool
{
return isset($this->response[$offset]);
}
@ -153,10 +153,10 @@ class WC_Retailcrm_Response implements \ArrayAccess
* Get offset
*
* @param mixed $offset offset
*
* @return mixed
* @throws \InvalidArgumentException
*
* @return mixed
* TODO PHP < 8.0 не поддерживает тип mixed. Оператор | для перечисления типов также не поддерживается.
*/
public function offsetGet($offset)
{
@ -166,4 +166,40 @@ class WC_Retailcrm_Response implements \ArrayAccess
return $this->response[$offset];
}
/**
* Returns error string. If there's multiple errors present - they will be squashed into single string.
*
* @return string
*/
public function getErrorString()
{
if ($this->offsetExists('errorMsg')) {
return (string) $this->response['errorMsg'];
}
if (is_array($this->response['errors']) && $this->offsetExists('errors')) {
$errorMessage = '';
foreach ($this->response['errors'] as $error) {
$errorMessage .= $error . ' >';
}
if (strlen($errorMessage) > 2) {
return (string) substr($errorMessage, 0, strlen($errorMessage) - 2);
}
return $errorMessage;
}
return '';
}
/**
* @return mixed|null
*/
public function getRawResponse()
{
return $this->rawResponse;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,106 @@
<?php
if (!class_exists('WC_Retailcrm_Carts')) :
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Cart - Allows transfer data carts with CMS.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_Cart
{
protected $apiClient;
protected $dateFormat;
protected $settings;
public function __construct($apiClient, $settings)
{
$this->apiClient = $apiClient;
$this->settings = $settings;
$this->dateFormat = 'Y-m-d H:i:sP';
}
public function isCartExist($customerId, $site): bool
{
$getCart = $this->apiClient->cartGet($customerId, $site);
return !empty($getCart['cart']['externalId']);
}
public function processCart($customerId, $cartItems, $site, $isCartExist): bool
{
$isSuccessful = false;
try {
$crmCart = [
'customer' => ['externalId' => $customerId],
'clearAt' => null,
'updatedAt' => date($this->dateFormat),
'droppedAt' => date($this->dateFormat),
'link' => wc_get_cart_url(),
];
// If new cart, need set createdAt and externalId
if (!$isCartExist) {
$crmCart['createdAt'] = date($this->dateFormat);
$crmCart['externalId'] = $customerId . uniqid('_', true);
}
// If you delete one by one
if (empty($cartItems)) {
return $this->clearCart($customerId, $site, $isCartExist);
}
$useXmlId = isset($this->settings['bind_by_sku']) && $this->settings['bind_by_sku'] === WC_Retailcrm_Base::YES;
foreach ($cartItems as $item) {
$product = $item['data'];
$crmCart['items'][] = [
'offer' => $useXmlId ? ['xmlId' => $product->get_sku()] : ['externalId' => $product->get_id()],
'quantity' => $item['quantity'],
'createdAt' => $product->get_date_created()->date($this->dateFormat) ?? date($this->dateFormat),
'updatedAt' => $product->get_date_modified()->date($this->dateFormat) ?? date($this->dateFormat),
'price' => wc_get_price_including_tax($product),
];
}
$crmCart = apply_filters(
'retailcrm_process_cart',
WC_Retailcrm_Plugin::clearArray($crmCart),
$cartItems
);
$setResponse = $this->apiClient->cartSet($crmCart, $site);
$isSuccessful = $setResponse->isSuccessful() && !empty($setResponse['success']);
} catch (Throwable $exception) {
WC_Retailcrm_Logger::exception(__METHOD__, $exception);
}
return $isSuccessful;
}
public function clearCart($customerId, $site, $isCartExist): bool
{
$isSuccessful = false;
try {
if ($isCartExist) {
$crmCart = ['customer' => ['externalId' => $customerId], 'clearedAt' => date($this->dateFormat)];
$clearResponse = $this->apiClient->cartClear($crmCart, $site);
$isSuccessful = $clearResponse->isSuccessful() && !empty($clearResponse['success']);
}
} catch (Throwable $exception) {
WC_Retailcrm_Logger::exception(__METHOD__, $exception);
}
return $isSuccessful;
}
}
endif;

View file

@ -0,0 +1,759 @@
<?php
if (!class_exists('WC_Retailcrm_Customers')) :
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Customers - Allows transfer data customers with CMS.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_Customers
{
/** @var bool | WC_Retailcrm_Proxy | \WC_Retailcrm_Client_V5 */
protected $retailcrm;
/** @var array */
protected $retailcrm_settings = [];
/** @var WC_Retailcrm_Customer_Address */
protected $customer_address;
/** @var array */
private $customer = [];
/** @var array */
private $customerCorporate = [];
/** @var array */
private $customerCorporateCompany = [];
/** @var array */
private $customerCorporateAddress = [];
/**@var array */
private $customFields = [];
/**@var null */
public $isSubscribed = null;
/**
* WC_Retailcrm_Customers constructor.
*
* @param bool | WC_Retailcrm_Proxy $retailcrm
* @param array $retailcrm_settings
* @param WC_Retailcrm_Customer_Address $customer_address
*/
public function __construct($retailcrm, $retailcrm_settings, $customer_address)
{
$this->retailcrm = $retailcrm;
$this->retailcrm_settings = $retailcrm_settings;
$this->customer_address = $customer_address;
if (!empty($retailcrm_settings['customer-meta-data-retailcrm'])) {
$this->customFields = json_decode($retailcrm_settings['customer-meta-data-retailcrm'], true);
}
}
/**
* Return corporate customer
*
* @return array
*/
public function getCorporateCustomer()
{
return $this->customerCorporate;
}
/**
* Is corporate customers enabled in provided API
*
* @return bool
*/
public function isCorporateEnabled()
{
if (!$this->retailcrm instanceof WC_Retailcrm_Proxy) {
return false;
}
return $this->retailcrm->getCorporateEnabled();
}
/**
* Customer can registration on site, we need:
* 1. Check by email if the customer already exists in CRM - then update the customer details.
* 2. If the customer is not in CRM, then create a new customer.
*
* @param int $customerId
*
* @return void|null
* @throws Exception
*/
public function registerCustomer($customerId)
{
if (!$this->retailcrm instanceof WC_Retailcrm_Proxy) {
return null;
}
WC_Retailcrm_Logger::info(__METHOD__, 'Register WC_Customer ID: ' . $customerId);
$wcCustomer = new WC_Customer($customerId);
$email = $wcCustomer->get_billing_email();
if (empty($email)) {
$email = $wcCustomer->get_email();
}
if (empty($email)) {
WC_Retailcrm_Logger::error(
__METHOD__,
'Error: Customer email is empty, externalId: ' . $wcCustomer->get_id()
);
return null;
} else {
$wcCustomer->set_billing_email($email);
$wcCustomer->save();
}
$response = $this->retailcrm->customersList(['email' => $email]);
if ($response->isSuccessful() && !empty($response['customers'])) {
$customers = $response['customers'];
$customer = reset($customers);
if (isset($customer['id'])) {
$this->updateCustomerById($customerId, $customer['id']);
$builder = new WC_Retailcrm_WC_Customer_Builder();
$builder
->setWcCustomer($wcCustomer)
->setPhones(!empty($customer['phones']) ? $customer['phones'] : [])
->setAddress(!empty($customer['address']) ? $customer['address'] : false)
->build()
->getResult()
->save();
WC_Retailcrm_Logger::info(__METHOD__, 'Customer was edited, externalId: ' . $wcCustomer->get_id());
}
} else {
$this->createCustomer($customerId);
$message = $this->isSubscribed
? 'The client has agreed to receive promotional newsletter, email: '
: 'The client refused to receive promotional newsletters, email: ';
WC_Retailcrm_Logger::info(
__METHOD__,
sprintf('Customer was created, externalId: %s. %s', $wcCustomer->get_id(), $message . $email)
);
}
}
/**
* Create customer in CRM
*
* @param int | WC_Customer $customer
*
* @param \WC_Order|null $order
*
* @return mixed
* @throws \Exception
*/
public function createCustomer($customer, $order = null)
{
if (!$this->retailcrm instanceof WC_Retailcrm_Proxy) {
return null;
}
if (is_int($customer)) {
$customer = $this->wcCustomerGet($customer);
}
if (!$customer instanceof WC_Customer) {
WC_Retailcrm_Logger::error(__METHOD__, 'Customer not found');
return null;
}
if ($this->isCustomer($customer)) {
WC_Retailcrm_Logger::info(
__METHOD__,
'Process WC_Customer ' . $customer->get_id(),
['wc_customer' => WC_Retailcrm_Logger::formatWcObject($customer)]
);
$this->processCustomer($customer, $order);
$response = $this->retailcrm->customersCreate($this->customer);
if ((!empty($response) && $response->isSuccessful()) && isset($response['id'])) {
return $response['id'];
}
}
return null;
}
/**
* Update customer in CRM
*
* @param $customerId
*
* @return void|\WC_Customer
* @throws \Exception
*/
public function updateCustomer($customerId)
{
if (!$this->retailcrm instanceof WC_Retailcrm_Proxy) {
return;
}
$customer = $this->wcCustomerGet($customerId);
if ($this->isCustomer($customer)) {
WC_Retailcrm_Logger::info(
__METHOD__,
'Update WC_Customer ' . $customer->get_id(),
['wc_customer' => WC_Retailcrm_Logger::formatWcObject($customer)]
);
$this->processCustomer($customer);
$this->retailcrm->customersEdit($this->customer);
}
return $customer;
}
/**
* Update customer in CRM by ID
*
* @param int $customerId
* @param int|string $crmCustomerId
*
* @return void|\WC_Customer
* @throws \Exception
*/
public function updateCustomerById($customerId, $crmCustomerId)
{
if (!$this->retailcrm instanceof WC_Retailcrm_Proxy) {
return;
}
$customer = $this->wcCustomerGet($customerId);
if ($this->isCustomer($customer)) {
WC_Retailcrm_Logger::info(
__METHOD__,
'Update WC_Customer by CRM_Customer ID: ' . $crmCustomerId,
['wc_customer' => WC_Retailcrm_Logger::formatWcObject($customer)]
);
$this->processCustomer($customer);
$this->customer['id'] = $crmCustomerId;
$this->retailcrm->customersEdit($this->customer, 'id');
}
return $customer;
}
/**
* Create corporate customer in CRM
*
* @param int $crmCustomerId
* @param int | WC_Customer $customer
* @param \WC_Order $order
*
* @return mixed
* @throws \Exception
*/
public function createCorporateCustomerForOrder($crmCustomerId, $customer, $order)
{
if (!$this->retailcrm instanceof WC_Retailcrm_Proxy) {
return null;
}
if (is_int($customer)) {
$customer = $this->wcCustomerGet($customer);
}
if (!$customer instanceof WC_Customer) {
return null;
}
if ($this->isCustomer($customer)) {
$this->processCorporateCustomer($crmCustomerId, $customer, $order);
$response = $this->retailcrm->customersCorporateCreate($this->customerCorporate);
return $this->fillCorporateCustomer($response);
}
return null;
}
/**
* Create new address in corporate customer (if needed).
*
* @param int $corporateId
* @param \WC_Customer $customer
* @param \WC_Order|null $order
*
* @return bool
*/
public function fillCorporateAddress($corporateId, $customer, $order = null)
{
$found = false;
$builder = new WC_Retailcrm_Customer_Corporate_Address();
$newAddress = $builder
->setIsMain(false)
->build($customer, $order)
->getData();
$addresses = $this->retailcrm->customersCorporateAddresses(
$corporateId,
[],
null,
100,
'id'
);
if (!empty($addresses['addresses']) && $addresses->isSuccessful()) {
foreach ($addresses['addresses'] as $address) {
foreach ($newAddress as $field => $value) {
if (isset($address[$field]) && $address[$field] != $value) {
continue 2;
}
}
$found = true;
break;
}
} else {
$found = true;
}
if (!$found) {
$this->retailcrm->customersCorporateAddressesCreate(
$corporateId,
$newAddress,
'id',
$this->retailcrm->getSingleSiteForKey()
);
}
return $found;
}
/**
* Fills corporate customer with required data after customer was created or updated.
* Create or update response after sending customer must be passed.
*
* @param \WC_Retailcrm_Response $response
*
* @return string|int|null
*/
protected function fillCorporateCustomer($response)
{
if ((empty($response) || !$response->isSuccessful()) && !$response->offsetExists('id')) {
return null;
}
$customerId = $response['id'];
$response = $this->retailcrm->customersCorporateAddressesCreate(
$customerId,
$this->customerCorporateAddress,
'id',
$this->retailcrm->getSingleSiteForKey()
);
if ($response->isSuccessful() && $response->offsetExists('id')) {
$this->customerCorporateCompany['address'] = [
'id' => $response['id'],
];
$this->retailcrm->customersCorporateCompaniesCreate(
$customerId,
$this->customerCorporateCompany,
'id',
$this->retailcrm->getSingleSiteForKey()
);
}
return $customerId;
}
/**
* Process customer for upload
*
* @param WC_Customer $customer
*
* @return void
*/
public function processCustomerForUpload($customer)
{
WC_Retailcrm_Logger::info(
__METHOD__,
'Processing for upload WC_Customer ' . $customer->get_id(),
['wc_customer' => WC_Retailcrm_Logger::formatWcObject($customer)]
);
$this->processCustomer($customer);
}
/**
* Process customer
*
* @param WC_Customer $customer
* @param WC_Order|null $order
*
* @return void
* @throws \Exception
*/
protected function processCustomer($customer, $order = null)
{
$createdAt = $customer->get_date_created();
$firstName = $customer->get_first_name();
$lastName = $customer->get_last_name();
$billingPhone = $customer->get_billing_phone();
$email = strtolower($customer->get_billing_email());
if (empty($firstName) && empty($lastName) && $order instanceof WC_Order) {
$firstName = $order->get_billing_first_name();
$lastName = $order->get_billing_last_name();
if (empty($email)) {
$email = $order->get_billing_email();
}
if (empty($billingPhone)) {
$billingPhone = $order->get_billing_phone();
}
}
// If a customer has placed an order as a guest, then $customer->get_date_created() == null,
// then we take $order->get_date_created() order
$createdAt = empty($createdAt) ? $order->get_date_created() : $createdAt;
$customerData = [
'createdAt' => $createdAt->date('Y-m-d H:i:s'),
'firstName' => !empty($firstName) ? $firstName : $customer->get_username(),
'lastName' => $lastName,
'email' => $email,
'address' => $this->customer_address->build($customer, $order)->getData()
];
if ($customer->get_id() > 0) {
$customerData['externalId'] = $customer->get_id();
}
// The guest client is unsubscribed by default
if ($customer->get_id() === 0 && $customer->get_date_created() === null) {
$customerData['subscribed'] = false;
}
if ($this->isSubscribed !== null) {
$customerData['subscribed'] = $this->isSubscribed;
}
if (!empty($billingPhone)) {
$customerData['phones'][] = [
'number' => $billingPhone
];
}
// If the client is corporate, set the value isContact.
if ($this->isCorporateEnabled()) {
if ($order !== null) {
$company = $order->get_billing_company();
}
if (empty($company)) {
$company = $customer->get_billing_company();
}
if (!empty($company)) {
$customerData['isContact'] = true;
}
}
if (!empty($this->customFields)) {
foreach ($this->customFields as $metaKey => $customKey) {
$metaValue = $customer->get_meta($metaKey);
if (empty($metaValue)) {
continue;
}
if (strpos($customKey, 'default-crm-field') !== false) {
$crmField = explode('#', $customKey);
if (count($crmField) === 2 && isset($crmField[1])) {
if ($crmField[1] === 'phones') {
$customerData[$crmField[1]][] = ['number' => $metaValue];
} elseif ($crmField[1] === 'tags') {
$customerData['addTags'][] = $metaValue;
} else {
$customerData[$crmField[1]] = $metaValue;
}
} elseif (isset($crmField[1], $crmField[2])) {
// For customer delivery
$customerData[$crmField[1]][$crmField[2]] = $metaValue;
}
} else {
$customerData['customFields'][$customKey] = $metaValue;
}
}
}
$this->customer = apply_filters(
'retailcrm_process_customer',
WC_Retailcrm_Plugin::clearArray($customerData),
$customer
);
}
/**
* Process corporate customer
*
* @param int $crmCustomerId
* @param WC_Customer $customer
* @param \WC_Order $order
*
* @return void
*/
protected function processCorporateCustomer($crmCustomerId, $customer, $order)
{
$data_company = [
'isMain' => true,
'name' => $order->get_billing_company()
];
$data_customer = [
'nickName' => $order->get_billing_company(),
'customerContacts' => [
[
'isMain' => true,
'customer' => [
'id' => $crmCustomerId
]
]
]
];
$corpAddress = new WC_Retailcrm_Customer_Corporate_Address();
$billingAddress = $corpAddress
->setIsMain(true)
->build($customer, $order)
->getData();
if (!empty($billingAddress)) {
$data_company['contragent']['legalAddress'] = implode(
', ',
[
$billingAddress['index'],
$billingAddress['city'],
$billingAddress['region'],
$billingAddress['text']
]
);
}
$this->customerCorporateAddress = $billingAddress;
$this->customerCorporate = apply_filters(
'retailcrm_process_customer_corporate',
WC_Retailcrm_Plugin::clearArray($data_customer),
$customer
);
$this->customerCorporateCompany = apply_filters(
'retailcrm_process_customer_corporate_company',
WC_Retailcrm_Plugin::clearArray($data_company),
$customer
);
}
/**
* @param array $filter Search customer by fields.
*
* @return bool|array
*/
private function searchCustomer($filter)
{
if (isset($filter['externalId'])) {
$search = $this->retailcrm->customersGet($filter['externalId']);
} elseif (isset($filter['email'])) {
if (empty($filter['email'])) {
return false;
}
// If customer not corporate, we need unset this field.
if (empty($filter['isContact'])) {
unset($filter['isContact']);
}
$search = $this->retailcrm->customersList($filter);
}
if (!empty($search) && $search->isSuccessful()) {
$customer = false;
if (isset($search['customers'])) {
if (empty($search['customers'])) {
return false;
}
if (!empty($filter['email'])) {
foreach ($search['customers'] as $finding) {
if (isset($finding['email']) && $finding['email'] == $filter['email']) {
$customer = $finding;
}
}
} else {
$dataCustomers = $search['customers'];
$customer = reset($dataCustomers);
}
} else {
$customer = !empty($search['customer']) ? $search['customer'] : false;
}
return $customer;
}
return false;
}
/**
* Returns customer data by externalId or by email, returns false in case of failure
*
* @param string $customerExternalId Customer externalId.
* @param string $customerEmailOrPhone Customer email or phone.
* @param bool $isContact Customer is the contact person.
*
* @return array|bool
*/
public function findCustomerEmailOrId($customerExternalId, $customerEmailOrPhone, $isContact)
{
$customer = false;
if (!empty($customerExternalId)) {
$customer = $this->searchCustomer(['externalId' => $customerExternalId]);
}
if (!$customer && !empty($customerEmailOrPhone)) {
$customer = $this->searchCustomer(['email' => $customerEmailOrPhone, 'isContact' => $isContact]);
}
return $customer;
}
/**
* Search by provided filter, returns first found customer
*
* @param array $filter
* @param bool $returnGroup Return all customers for group filter instead of first
*
* @return bool|array
*/
public function searchCorporateCustomer($filter, $returnGroup = false)
{
$search = $this->retailcrm->customersCorporateList($filter);
if (!empty($search) && $search->isSuccessful()) {
if (isset($search['customersCorporate'])) {
if (empty($search['customersCorporate'])) {
return false;
}
if ($returnGroup) {
return $search['customersCorporate'];
} else {
$dataCorporateCustomers = $search['customersCorporate'];
$customer = reset($dataCorporateCustomers);
}
} else {
$customer = false;
}
return $customer;
}
return false;
}
/**
* @param WC_Order $order
*
* @return WC_Customer
* @throws Exception
*/
public function buildCustomerFromOrderData($order)
{
$new_customer = new WC_Customer();
foreach ($order->get_address('billing') as $prop => $value) {
if (method_exists($new_customer, 'set_billing_' . $prop)) {
$new_customer->{'set_billing_' . $prop}($value);
}
}
$new_customer->set_first_name($order->get_billing_first_name());
$new_customer->set_last_name($order->get_billing_last_name());
$new_customer->set_email($order->get_billing_email());
$new_customer->set_date_created($order->get_date_created());
WC_Retailcrm_Logger::info(
__METHOD__,
'Build new customer from order data',
['wc_customer' => WC_Retailcrm_Logger::formatWcObject($new_customer)]
);
return $new_customer;
}
/**
* @param int $customer_id
*
* @return WC_Customer
* @throws \Exception
*/
public function wcCustomerGet($customer_id)
{
return new WC_Customer($customer_id);
}
/**
* @return array
*/
public function getCustomer()
{
return $this->customer;
}
/**
* Returns true if provided WP_User or WC_Customer should be uploaded to CRM
*
* @param \WC_Customer|\WP_User $user
*
* @return bool
*/
public function isCustomer($user)
{
$clientRoles = wp_roles()->get_names();
$clientRoles = apply_filters('retailcrm_customer_roles', WC_Retailcrm_Plugin::clearArray($clientRoles));
if ($user instanceof WP_User) {
$userRole = !empty($user->roles[0]) ? $user->roles[0] : null;
} elseif ($user instanceof WC_Customer) {
$role = $user->get_role();
$userRole = !empty($role) ? $role : null;
} else {
return false;
}
return array_key_exists($userRole, $clientRoles);
}
}
endif;

View file

@ -0,0 +1,117 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Daemon_Collector - Integration with Daemon Collector.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_Daemon_Collector {
/** @var self $instance */
private static $instance;
/** @var array $options */
private $options;
/** @var string $code */
private $code = '';
/**
* @param array $options
*
* @return WC_Retailcrm_Daemon_Collector
*/
public static function getInstance($options = array())
{
if (self::$instance === null) {
self::$instance = new self($options);
}
return self::$instance;
}
/**
* WC_Retailcrm_Daemon_Collector constructor.
*
* @param array $options
*/
private function __construct($options = array())
{
$this->options = $options;
}
/**
* @return string
*/
public function initialize_daemon_collector() {
if (!$this->code) {
$this->buildHeader()
->buildParams()
->buildFooter();
}
return $this->code;
}
/**
* @return $this
*/
private function buildHeader() {
$header = <<<EOF
<script type="text/javascript">
(function(_,r,e,t,a,i,l){_['retailCRMObject']=a;_[a]=_[a]||function(){(_[a].q=_[a].q||[]).push(arguments)};_[a].l=1*new Date();l=r.getElementsByTagName(e)[0];i=r.createElement(e);i.async=!0;i.src=t;l.parentNode.insertBefore(i,l)})(window,document,'script','https://collector.retailcrm.pro/w.js','_rc');
EOF;
$this->code .= $header;
return $this;
}
/**
* @return $this
*/
private function buildParams() {
$params = array();
if (
function_exists('WC')
&& WC()->customer !== null
&& WC()->customer->get_id() > 0
) {
$params['customerId'] = WC()->customer->get_id();
}
$this->code .= apply_filters('retailcrm_daemon_collector', '') . sprintf(
"\t_rc('create', '%s', %s);\n",
$this->options['daemon_collector_key'],
json_encode((object) $params)
);
return $this;
}
/**
* @return $this
*/
private function buildFooter() {
$footer = <<<EOF
_rc('send', 'pageView');
</script>
EOF;
$this->code .= $footer;
return $this;
}
}

View file

@ -0,0 +1,135 @@
<?php
if (!class_exists('WC_Retailcrm_Google_Analytics')) {
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Google_Analytics - Integration with Google Analytics.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_Google_Analytics {
private static $instance;
private $options;
/**
* @param array $options
*
* @return WC_Retailcrm_Google_Analytics
*/
public static function getInstance($options = array())
{
if (self::$instance === null) {
self::$instance = new self($options);
}
return self::$instance;
}
/**
* WC_Retailcrm_Google_Analytics constructor.
*
* @param array $options
*/
private function __construct($options = array())
{
$this->options = $options;
}
/**
* @return string
*/
public function initialize_analytics() {
return apply_filters('retailcrm_initialize_analytics' ,"
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '" . $this->options['ua_code'] . "', 'auto');
function getRetailCrmCookie(name) {
var matches = document.cookie.match(new RegExp(
'(?:^|; )' + name + '=([^;]*)'
));
return matches ? decodeURIComponent(matches[1]) : '';
}
ga('set', 'dimension" . $this->options['ua_custom'] ."', getRetailCrmCookie('_ga'));
ga('send', 'pageview');
</script>
");
}
/**
* @return string
*/
public function send_analytics() {
$js = '';
if (!isset($_GET['key'])) {
return $js;
}
$order_id = wc_get_order_id_by_order_key($_GET['key']);
$order = wc_get_order($order_id);
if (is_object($order) === false) {
return $js;
}
foreach ($order->get_items() as $item) {
$uid = ($item['variation_id'] > 0) ? $item['variation_id'] : $item['product_id'];
$_product = wc_get_product($uid);
if ($_product) {
$order_item = array(
'id' => $uid,
'name' => $item['name'],
'price' => (float)$_product->get_price(),
'quantity' => $item['qty'],
);
$order_items[] = $order_item;
}
}
$url = parse_url(get_site_url());
$domain = $url['host'];
$js .= "
<script type=\"text/javascript\">
ga('require', 'ecommerce', 'ecommerce.js');
ga('ecommerce:addTransaction', {
'id':" . $order->get_id() . ",
'affiliation':'" . $domain . "',
'revenue':" . $order->get_total() . ",
'shipping':" . $order->get_total_tax() . ",
'tax':" . $order->get_shipping_total() . "
});
";
foreach ($order_items as $item) {
$js .= "
ga('ecommerce:addItem', {
'id':" . $order->get_id() . ",
'sku':" . $item['id'] . ",
'name': '" . $item['name'] . "',
'price': " . $item['price'] . ",
'quantity': " . $item['quantity'] . "
});
";
}
$js .= "ga('ecommerce:send');
</script>";
return apply_filters('retailcrm_send_analytics', $js);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,403 @@
<?php
if (!class_exists('WC_Retailcrm_Icml')) :
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Icml - Generate ICML file (catalog).
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_Icml
{
const OFFER_PROPERTIES = [
'name',
'productName',
'price',
'purchasePrice',
'vendor',
'picture',
'url',
'xmlId',
'productActivity'
];
protected $shop;
protected $file;
protected $tmpFile;
protected $settings;
protected $icmlWriter;
protected $unloadServices = false;
protected $activeLoyalty = false;
/**
* WC_Retailcrm_Icml constructor.
*
*/
public function __construct()
{
$this->shop = get_bloginfo('name');
$this->file = ABSPATH . 'simla.xml';
$this->tmpFile = sprintf('%s.tmp', $this->file);
$this->settings = get_option(WC_Retailcrm_Base::$option_key);
$this->icmlWriter = new WC_Retailcrm_Icml_Writer($this->tmpFile);
$this->unloadServices = (
isset($this->settings['icml_unload_services'])
&& $this->settings['icml_unload_services'] === WC_Retailcrm_Base::YES
);
if (isset($this->settings['loyalty']) && $this->settings['loyalty'] === WC_Retailcrm_Base::YES) {
$this->activeLoyalty = true;
}
}
public function changeBindBySku($useXmlId)
{
$this->settings['bind_by_sku'] = $useXmlId;
}
/**
* Generate ICML catalog.
*/
public function generate()
{
$this->icmlWriter->writeHead($this->shop);
$categories = $this->prepareCategories();
if (empty($categories)) {
WC_Retailcrm_Logger::error(__METHOD__, 'Can`t get categories!');
return;
}
$this->icmlWriter->writeCategories($categories);
$offers = $this->prepareOffers();
if (empty($offers)) {
WC_Retailcrm_Logger::error(__METHOD__, 'Can`t get offers!');
return;
}
$this->icmlWriter->writeOffers($offers);
$this->icmlWriter->writeEnd();
$this->icmlWriter->formatXml($this->tmpFile);
rename($this->tmpFile, $this->file);
WC_Retailcrm_Logger::info(__METHOD__, 'Catalog generated');
}
/**
* Prepare WC offers for write.
*
* @return void
*/
private function prepareOffers()
{
$productStatuses = $this->getProductStatuses();
if (!$productStatuses) {
$productStatuses = ['publish'];
}
$page = 1;
$offerAttributes = $this->getOfferAttributes();
do {
$products = wc_get_products(
[
'limit' => 1000,
'status' => $productStatuses,
'page' => $page,
'paginate' => true,
]
);
// Clearing the object cache after calling the function wc_get_products
wp_cache_flush();
if (empty($products)) {
WC_Retailcrm_Logger::error(__METHOD__, 'Can`t get products!');
return;
}
foreach ($products->products as $offer) {
$type = $offer->get_type();
if (strpos($type, 'variable') !== false || strpos($type, 'variation') !== false) {
foreach ($offer->get_children() as $childId) {
$childProduct = wc_get_product($childId);
if (!$childProduct) {
continue;
}
yield $this->getOffer($offerAttributes, $childProduct, $offer);
}
} else {
yield $this->getOffer($offerAttributes, $offer);
}
}
$page++;
} while ($page <= $products->max_num_pages);
}
/**
* Get WC offer attributes.
*
* @return array
*/
private function getOfferAttributes()
{
$offerAttributes = [];
$attributeTaxonomies = wc_get_attribute_taxonomies();
foreach ($attributeTaxonomies as $productAttribute) {
$attributeId = wc_attribute_taxonomy_name_by_id(intval($productAttribute->attribute_id));
$offerAttributes[$attributeId] = $productAttribute->attribute_label;
}
return $offerAttributes;
}
/**
* Get offer for ICML catalog
*
* @param array $productAttributes
* @param WC_Product $product
* @param bool | WC_Product_Variable $parent
*
* @return array
*/
private function getOffer(array $productAttributes, WC_Product $product, $parent = false)
{
$idImages = array_merge([$product->get_image_id()], $product->get_gallery_image_ids());
if ($parent !== false && empty(get_the_post_thumbnail_url($product->get_id()))) {
$idImages = array_merge([$parent->get_image_id()], $parent->get_gallery_image_ids());
}
$images = [];
foreach ($idImages as $id) {
$attachmentImageSrc = wp_get_attachment_image_src($id, 'full');
if (is_array($attachmentImageSrc) && isset($attachmentImageSrc[0])) {
$images[] = $attachmentImageSrc[0];
}
}
$termList = $parent !== false
? $parent->get_category_ids()
: $product->get_category_ids();
$attributes = $parent !== false
? get_post_meta($parent->get_id(), '_product_attributes')
: get_post_meta($product->get_id(), '_product_attributes');
// All attributes are in the first element of the array
$attributes = (isset($attributes[0])) ? $attributes[0] : $attributes;
$params = [];
if (!empty($attributes)) {
foreach ($attributes as $attributeName => $attribute) {
$attributeValue = $product->get_attribute($attributeName);
if ($attribute['is_visible'] == 1 && !empty($attributeValue)) {
$params[] = [
'code' => $attributeName,
'name' => $productAttributes[$attributeName],
'value' => $attributeValue
];
}
}
}
$dimensions = '';
if ($product->get_length() !== '') {
$dimensions = wc_get_dimension($product->get_length(), 'cm');
}
if ($product->get_width() !== '') {
$dimensions .= '/' . wc_get_dimension($product->get_width(), 'cm');
}
if ($product->get_height() !== '') {
$dimensions .= '/' . wc_get_dimension($product->get_height(), 'cm');
}
$weight = '';
if ($product->get_weight() !== '') {
$weight = wc_get_weight($product->get_weight(), 'kg');
}
if ($product->is_taxable()) {
$tax_rates = WC_Tax::get_rates($product->get_tax_class());
$tax = reset($tax_rates);
}
$productData = [
'id' => $product->get_id(),
'productId' => ($product->get_parent_id() > 0) ? $parent->get_id() : $product->get_id(),
'name' => $product->get_name(),
'productName' => ($product->get_parent_id() > 0) ? $parent->get_title() : $product->get_title(),
'price' => $this->activeLoyalty
? wc_get_price_including_tax($product, ["price" => $product->get_regular_price()])
: wc_get_price_including_tax($product)
,
'picture' => $images,
'url' => ($product->get_parent_id() > 0) ? $parent->get_permalink() : $product->get_permalink(),
'quantity' => $this->getQuantity($product),
'categoryId' => $termList,
'dimensions' => $dimensions,
'weight' => $weight,
'tax' => isset($tax) ? $tax['rate'] : 'none',
'type' => ($this->unloadServices && $product->is_virtual()) ? 'service' : 'product',
];
if ($product->get_sku() !== '') {
$params[] = ['code' => 'article', 'name' => 'Article', 'value' => $product->get_sku()];
if (isset($this->settings['bind_by_sku']) && $this->settings['bind_by_sku'] == WC_Retailcrm_Base::YES) {
$productData['xmlId'] = $product->get_sku();
}
}
if (isset($this->settings['product_description'])) {
$productDescription = $this->getOfferDescription($product);
if (empty($productDescription) && $parent instanceof WC_Product_Variable) {
$this->getOfferDescription($parent);
}
if ($productDescription != '') {
$params[] = ['code' => 'description', 'name' => 'Description', 'value' => $productDescription];
}
}
if (!empty($params)) {
$productData['params'] = $params;
}
$productData = apply_filters(
'retailcrm_process_offer',
WC_Retailcrm_Plugin::clearArray($productData),
$product
);
return $productData ?? [];
}
private function getQuantity($product)
{
if ($product->get_manage_stock()) {
$stockQuantity = $product->get_stock_quantity();
$quantity = $stockQuantity !== null && $stockQuantity !== 0 ? $stockQuantity : 0;
} else {
$quantity = $product->get_stock_status() === 'instock' ? 1 : 0;
}
return $quantity;
}
/**
* Get product statuses.
*
* @return array
*/
private function getProductStatuses()
{
$statuses = [];
foreach (get_post_statuses() as $key => $value) {
if (isset($this->settings['p_' . $key]) && $this->settings['p_' . $key] == WC_Retailcrm_Base::YES) {
$statuses[] = $key;
}
}
return $statuses;
}
/**
* Get offer description.
*
* @param WC_Product | WC_Product_Variable $product WC product.
*
* @return string
*/
private function getOfferDescription($product)
{
return $this->settings['product_description'] == 'full'
? $product->get_description()
: $product->get_short_description();
}
/**
* Prepare WC categories for write.
*
* @return array
*/
private function prepareCategories()
{
$categories = [];
$taxonomy = 'product_cat';
$orderby = 'parent';
$show_count = 0; // 1 for yes, 0 for no
$pad_counts = 0; // 1 for yes, 0 for no
$hierarchical = 1; // 1 for yes, 0 for no
$title = '';
$empty = 0;
$args = [
'taxonomy' => $taxonomy,
'orderby' => $orderby,
'show_count' => $show_count,
'pad_counts' => $pad_counts,
'hierarchical' => $hierarchical,
'title_li' => $title,
'hide_empty' => $empty
];
$wcTerms = get_categories($args);
foreach ($wcTerms as $term) {
$category = [
'id' => $term->term_id,
'parentId' => $term->parent,
'name' => $term->name
];
$thumbnailId = function_exists('get_term_meta')
? get_term_meta($term->term_id, 'thumbnail_id', true)
: get_woocommerce_term_meta($term->term_id, 'thumbnail_id', true);
$picture = wp_get_attachment_url($thumbnailId);
if ($picture) {
$category['picture'] = $picture;
}
$categories[] = $category;
}
return $categories;
}
}
endif;

View file

@ -0,0 +1,142 @@
<?php
if (!class_exists('WC_Retailcrm_Inventories')) :
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Inventories - Allows manage stocks.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_Inventories
{
/** @var WC_Retailcrm_Client_V5 */
protected $retailcrm;
/** @var array */
protected $crmSettings;
/** @var string */
protected $bindField = 'externalId';
/**
* WC_Retailcrm_Inventories constructor.
* @param bool $retailcrm
*/
public function __construct($retailcrm = false)
{
$this->crmSettings = get_option(WC_Retailcrm_Base::$option_key);
$this->retailcrm = $retailcrm;
if (!empty($this->crmSettings['bind_by_sku']) && $this->crmSettings['bind_by_sku'] === WC_Retailcrm_Base::YES) {
$this->bindField = 'xmlId';
}
}
/**
* Load stock from RetailCRM
*
* @return void
*/
protected function load_stocks()
{
if (!$this->retailcrm instanceof WC_Retailcrm_Proxy) {
return null;
}
$page = 1;
$availableStores = $this->crmSettings['stores_for_uploading'] ?? null;
$variationProducts = [];
do {
/** @var WC_Retailcrm_Response $response */
$response = $this->retailcrm->storeInventories(['details' => true], $page, 250);
if (empty($response['offers']) || !$response->isSuccessful()) {
return null;
}
$totalPageCount = $response['pagination']['totalPageCount'];
$page++;
foreach ($response['offers'] as $offer) {
$offerQuantity = $offer['quantity'];
if (!empty($availableStores) && count($offer['stores']) > 1) {
$offerQuantity = 0;
foreach ($offer['stores'] as $store) {
if (in_array($store['store'], $availableStores, true)) {
$offerQuantity += $store['quantity'];
}
}
}
if (isset($offer[$this->bindField])) {
$product = retailcrm_get_wc_product($offer[$this->bindField], $this->crmSettings);
if ($product instanceof WC_Product) {
if ($product->get_type() === 'external') {
continue;
}
if ($product->get_type() == 'variation' || $product->get_type() == 'variable') {
$parentId = $product->get_parent_id();
if (!empty($parentId)) {
if (isset($variationProducts[$parentId])) {
$variationProducts[$parentId] += $offerQuantity;
} else {
$variationProducts[$parentId] = $offerQuantity;
}
}
}
$product->set_manage_stock(true);
$product->set_stock_quantity($offerQuantity);
$product->save();
}
}
}
// Clearing the object cache after calling the function wc_get_products
wp_cache_flush();
} while ($page <= $totalPageCount);
if (!empty($variationProducts)) {
$chunks = array_chunk($variationProducts, 100, true);
foreach ($chunks as $chunk) {
foreach ($chunk as $id => $quantity) {
$variationProduct = wc_get_product($id);
if (is_object($variationProduct)) {
$variationProduct->set_manage_stock(true);
$variationProduct->set_stock_quantity($quantity);
$variationProduct->save();
}
}
wp_cache_flush();
}
}
}
/**
* Update stock quantity in WooCommerce
*
* @return void
*/
public function updateQuantity()
{
if ($this->crmSettings['sync'] === WC_Retailcrm_Base::YES) {
$this->load_stocks();
}
}
}
endif;

View file

@ -0,0 +1,583 @@
<?php
if (!class_exists('WC_Retailcrm_Loyalty')) :
if (!class_exists('WC_Retailcrm_Loyalty_Form')) {
include_once(WC_Integration_Retailcrm::checkCustomFile('include/components/class-wc-retailcrm-loyalty-form.php'));
}
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Loyalty - Allows transfer data carts with CMS.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_Loyalty
{
/** @var WC_Retailcrm_Client_V5 */
protected $apiClient;
protected $dateFormat;
protected $settings;
/** @var WC_Retailcrm_Loyalty_Form */
protected $loyaltyForm;
protected $validator;
public function __construct($apiClient, $settings)
{
$this->apiClient = $apiClient;
$this->settings = $settings;
$this->dateFormat = 'Y-m-d H:i:sP';
$this->loyaltyForm = new WC_Retailcrm_Loyalty_Form();
$this->validator = new WC_Retailcrm_Loyalty_Validator(
$this->apiClient,
isCorporateUserActivate($this->settings)
);
}
public function getForm(int $userId, $loyaltyTerms = '', $loyaltyPersonal = '')
{
$result = [];
$phone = '';
$wcCustomer = new WC_Customer($userId);
if ($wcCustomer instanceof WC_Customer) {
$phone = $wcCustomer->get_billing_phone();
}
try {
$response = $this->getLoyaltyAccounts($userId);
} catch (Throwable $exception) {
WC_Retailcrm_Logger::exception(
__METHOD__,
$exception,
'Exception get loyalty accounts: '
);
return $result;
}
$loyaltyAccount = $response['loyaltyAccounts'][0] ?? null;
if ($loyaltyAccount && (int) $loyaltyAccount['customer']['externalId'] === $userId) {
if ($loyaltyAccount['active'] === true) {
$result['form'] = $this->loyaltyForm->getInfoLoyalty($loyaltyAccount);
} else {
$result['form'] = $this->loyaltyForm->getActivationForm();
$result['loyaltyId'] = $loyaltyAccount['id'];
}
} else {
$result['form'] = $this->loyaltyForm->getRegistrationForm($phone, $loyaltyTerms, $loyaltyPersonal);
}
return $result;
}
public function registerCustomer(int $userId, string $phone, string $site): bool
{
$parameters = [
'phoneNumber' => $phone,
'customer' => [
'externalId' => $userId
]
];
try {
$wcCustomer = new WC_Customer($userId);
if ($wcCustomer instanceof WC_Customer) {
$currentPhone = $wcCustomer->get_billing_phone();
if (empty($currentPhone) && $phone !== '') {
$wcCustomer->set_billing_phone($phone);
$wcCustomer->save();
}
}
$response = $this->apiClient->createLoyaltyAccount($parameters, $site);
if (!$response->isSuccessful()) {
WC_Retailcrm_Logger::error(
__METHOD__,
'Error while registering in the loyalty program',
['response' => json_decode($response->getRawResponse(), true)]
);
}
return $response->isSuccessful();
} catch (Throwable $exception) {
WC_Retailcrm_Logger::exception(
__METHOD__,
$exception,
'Exception while registering in the loyalty program: '
);
return false;
}
}
public function activateLoyaltyCustomer(int $loyaltyId)
{
try {
$response = $this->apiClient->activateLoyaltyAccount($loyaltyId);
if (!$response->isSuccessful()) {
WC_Retailcrm_Logger::error(
__METHOD__,
'Error while registering in the loyalty program',
['response' => json_decode($response->getRawResponse(), true)]
);
}
return $response->isSuccessful();
} catch (Throwable $exception) {
WC_Retailcrm_Logger::exception(__METHOD__, $exception);
return false;
}
}
private function getDiscountLoyalty($cartItems, $site, $customerId)
{
$response = $this->calculateDiscountLoyalty($cartItems, $site, $customerId);
if ($response === 0) {
return 0;
}
$discount = 0;
//Checking if the loyalty discount is a percent discount
foreach ($response['order']['items'] as $item) {
if (!isset($item['discounts'])) {
continue;
}
foreach ($item['discounts'] as $discountItem) {
if ($discountItem['type'] === 'loyalty_level') {
$discount += $discountItem['amount'];
}
}
}
//If the discount has already been given, do not work with points deduction
if ($discount === 0) {
foreach ($response['calculations'] as $calculate) {
if ($calculate['privilegeType'] !== 'loyalty_level') {
continue;
}
$discount = $calculate['maxChargeBonuses'];
}
}
return $discount;
}
private function getLoyaltyAccounts(int $userId)
{
$response = $this->apiClient->customersGet($userId);
if (!isset($response['customer']['id'])) {
return [];
}
$filter['customerId'] = $response['customer']['id'];
$response = $this->apiClient->getLoyaltyAccountList($filter);
if (!$response->isSuccessful() || !$response->offsetExists('loyaltyAccounts')) {
return null;
}
return $response;
}
public function createLoyaltyCoupon($refreshCoupon = false)
{
global $woocommerce;
$site = $this->apiClient->getSingleSiteForKey();
$cartItems = $woocommerce->cart->get_cart();
$customerId = $woocommerce->customer ? $woocommerce->customer->get_id() : null;
$resultString = '';
if (!$customerId || !$cartItems) {
return null;
}
$couponsLp = [];
// Check exists used loyalty coupons
foreach ($woocommerce->cart->get_coupons() as $code => $coupon) {
if ($this->isLoyaltyCoupon($code)) {
$couponsLp[] = $code;
}
}
// if you need to refresh coupon that does not exist
if (count($couponsLp) === 0 && $refreshCoupon) {
return null;
}
//If one loyalty coupon is used, not generate a new one
if (count($couponsLp) === 1 && !$refreshCoupon) {
return null;
}
// if more than 1 loyalty coupon is used, delete all coupons
if (count($couponsLp) > 1 || $refreshCoupon) {
foreach ($couponsLp as $code) {
$woocommerce->cart->remove_coupon($code);
$coupon = new WC_Coupon($code);
$coupon->delete(true);
}
$woocommerce->cart->calculate_totals();
}
if (!$this->validator->checkAccount($customerId)) {
return null;
}
$lpDiscountSum = $this->getDiscountLoyalty($woocommerce->cart->get_cart(), $site, $customerId);
if ($lpDiscountSum === 0) {
return null;
}
//Check the existence of loyalty coupons and delete them
$coupons = $this->getCouponLoyalty($woocommerce->customer->get_email());
foreach ($coupons as $item) {
$coupon = new WC_Coupon($item['code']);
$coupon->delete(true);
}
//Generate new coupon
$coupon = new WC_Coupon();
$coupon->set_usage_limit(0);
$coupon->set_amount($lpDiscountSum);
$coupon->set_email_restrictions($woocommerce->customer->get_email());
$coupon->set_code('loyalty' . mt_rand());
$coupon->save();
if ($refreshCoupon) {
$woocommerce->cart->apply_coupon($coupon->get_code());
return $resultString;
}
//If a percentage discount, automatically apply a loyalty coupon
if ($this->validator->loyaltyAccount['level']['type'] === 'discount') {
$woocommerce->cart->apply_coupon($coupon->get_code());
return $resultString;
}
$resultString .= ' <div style="text-align: left; line-height: 3"><b>' . __('It is possible to write off', 'retailcrm') . ' ' . $lpDiscountSum . ' ' . __('bonuses', 'retailcrm') . '</b></div>';
return $resultString. '<div style="text-align: left;"><b>' . __('Use coupon:', 'retailcrm') . ' <u><i style="cursor: grab" id="input_loyalty_code" onclick="inputLoyaltyCode()">' . $coupon->get_code() . '</i></u></i></b></div>';
}
public function clearLoyaltyCoupon()
{
global $woocommerce;
foreach ($woocommerce->cart->get_coupons() as $code => $coupon) {
if ($this->isLoyaltyCoupon($code)) {
$woocommerce->cart->remove_coupon($code);
$coupon = new WC_Coupon($code);
$coupon->delete(true);
}
}
}
public function deleteLoyaltyCoupon($couponCode)
{
if ($this->isLoyaltyCoupon($couponCode)) {
$coupon = new WC_Coupon($couponCode);
$coupon->delete(true);
return true;
}
return false;
}
public function isLoyaltyCoupon($couponCode): bool
{
return preg_match('/^loyalty\d+$/m', $couponCode) === 1;
}
public function getCouponLoyalty($email)
{
global $wpdb;
return $wpdb->get_results(
$wpdb->prepare(
"SELECT posts.post_name code FROM {$wpdb->prefix}posts AS posts
LEFT JOIN {$wpdb->prefix}postmeta AS postmeta ON posts.ID = postmeta.post_id
WHERE posts.post_type = 'shop_coupon' AND posts.post_name LIKE 'loyalty%'
AND postmeta.meta_key = 'customer_email' AND postmeta.meta_value LIKE %s",
'%' . $email . '%'
), ARRAY_A
);
}
public function deleteLoyaltyCouponInOrder($wcOrder)
{
$discountLp = 0;
$coupons = $wcOrder->get_coupons();
foreach ($coupons as $coupon) {
$code = $coupon->get_code();
if ($this->isLoyaltyCoupon($code)) {
$discountLp = $coupon->get_discount();
$wcOrder->remove_coupon($code);
$objectCoupon = new WC_Coupon($code);
$objectCoupon->delete(true);
$wcOrder->recalculate_coupons();
break;
}
}
return $discountLp;
}
public function isValidOrder($wcCustomer, $wcOrder)
{
return !(!$wcCustomer || (isCorporateUserActivate($this->settings) && isCorporateOrder($wcCustomer, $wcOrder)));
}
public function isValidUser($wcCustomer)
{
if (empty($wcCustomer)) {
return false;
}
try {
$response = $this->getLoyaltyAccounts($wcCustomer->get_id());
} catch (Throwable $exception) {
WC_Retailcrm_Logger::exception(
__METHOD__,
$exception,
'Exception get loyalty accounts: '
);
return false;
}
return isset($response['loyaltyAccounts'][0]);
}
public function applyLoyaltyDiscount($wcOrder, $createdOrder, $discountLp = 0)
{
$isPercentDiscount = false;
$items = [];
// Verification of automatic creation of the percentage discount of the loyalty program
foreach ($createdOrder['items'] as $item) {
foreach ($item['discounts'] as $discount) {
if ($discount['type'] === 'loyalty_level') {
$isPercentDiscount = true;
$items = $createdOrder['items'];
break 2;
}
}
}
if (!$isPercentDiscount) {
$response = $this->apiClient->applyBonusToOrder($createdOrder['site'], ['externalId' => $createdOrder['externalId']], (float) $discountLp);
if (!$response instanceof WC_Retailcrm_Response || !$response->isSuccessful()) {
return $response->getErrorString();
}
$items = $response['order']['items'];
}
$this->calculateLoyaltyDiscount($wcOrder, $items);
}
public function calculateLoyaltyDiscount($wcOrder, $orderItems)
{
$wcItems = $wcOrder->get_items();
foreach ($orderItems as $item) {
$externalId = $item['externalIds'][0]['value'];
$externalId = preg_replace('/^\d+\_/m', '', $externalId);
if (isset($wcItems[(int) $externalId])) {
$discountLoyaltyTotal = 0;
foreach ($item['discounts'] as $discount) {
if (in_array($discount['type'], ['bonus_charge', 'loyalty_level'])) {
$discountLoyaltyTotal += $discount['amount'];
}
}
$wcItem = $wcItems[(int) $externalId];
$wcItem->set_total($wcItem->get_total() - $discountLoyaltyTotal);
$wcItem->calculate_taxes();
$wcItem->save();
}
}
$wcOrder->calculate_totals();
}
public function getCrmItemsInfo($orderExternalId)
{
$discountType = null;
$crmItems = [];
$response = $this->apiClient->ordersGet($orderExternalId);
if (!$response instanceof WC_Retailcrm_Response || !$response->isSuccessful() || !isset($response['order'])) {
WC_Retailcrm_Logger::error(
__METHOD__,
'Process order: Error when receiving an order from the CRM. Order Id: ' . $orderExternalId
);
return [];
}
foreach ($response['order']['items'] as $item) {
$externalId = $item['externalIds'][0]['value'];
$externalId = preg_replace('/^\d+\_/m', '', $externalId);
$crmItems[$externalId] = $item;
if (!$discountType) {
foreach ($item['discounts'] as $discount) {
if (in_array($discount['type'], ['bonus_charge', 'loyalty_level'])) {
$discountType = $discount['type'];
break;
}
}
}
}
return ['items' => $crmItems, 'discountType' => $discountType];
}
public function calculateDiscountLoyalty($cartItems, $site, $customerId, $bonuses = 0)
{
$order = [
'site' => $site,
'customer' => ['externalId' => $customerId],
'privilegeType' => 'loyalty_level'
];
$useXmlId = isset($this->settings['bind_by_sku']) && $this->settings['bind_by_sku'] === WC_Retailcrm_Base::YES;
foreach ($cartItems as $item) {
$product = $item['data'];
$productRegularPrice = wc_get_price_including_tax($product, ["price" => $product->get_regular_price()]);
$discount = $productRegularPrice - ($item['line_total'] / $item['quantity']);
$order['items'][] = [
'offer' => $useXmlId ? ['xmlId' => $product->get_sku()] : ['externalId' => $product->get_id()],
'quantity' => $item['quantity'],
'initialPrice' => $productRegularPrice,
'discountManualAmount' => $discount
];
}
$response = $this->apiClient->calculateDiscountLoyalty($site, $order, (float) $bonuses);
if (!$response->isSuccessful() || !isset($response['calculations'])) {
return 0;
}
return $response;
}
public function getCreditBonuses(): string
{
global $woocommerce;
$customerId = $woocommerce->customer ? $woocommerce->customer->get_id() : null;
$site = $this->apiClient->getSingleSiteForKey();
if (
!$customerId
|| !$woocommerce->cart
|| !$woocommerce->cart->get_cart()
|| !$site
|| !$this->validator->checkAccount($customerId)
) {
return '';
}
$loyaltyCoupon = null;
$coupons = $woocommerce->cart->get_applied_coupons();
foreach ($coupons as $coupon) {
if ($this->isLoyaltyCoupon($coupon)) {
$loyaltyCoupon = new WC_Coupon($coupon);
$woocommerce->cart->remove_coupon($coupon);
$woocommerce->cart->calculate_totals();
break;
}
}
if ($loyaltyCoupon) {
$chargeBonuses = $loyaltyCoupon->get_amount();
}
$cartItems = $woocommerce->cart->get_cart();
$response = $this->calculateDiscountLoyalty($cartItems, $site, $customerId, $chargeBonuses ?? 0);
if ($loyaltyCoupon) {
$coupon = new WC_Coupon();
$coupon->set_usage_limit(0);
$coupon->set_amount($loyaltyCoupon->get_amount());
$coupon->set_email_restrictions($loyaltyCoupon->get_email_restrictions());
$coupon->set_code($loyaltyCoupon->get_code());
$coupon->save();
$woocommerce->cart->apply_coupon($coupon->get_code());
$woocommerce->cart->calculate_totals();
}
if ($response === 0) {
return '';
}
$creditBonuses = $response['order']['bonusesCreditTotal'];
if ($creditBonuses) {
return $this->getHtmlCreditBonuses($creditBonuses);
}
return '';
}
private function getHtmlCreditBonuses($creditBonuses)
{
return '<b style="font-size: large">' . __("Points will be awarded upon completion of the order:", 'retailcrm') . ' <u style="color: green"><i>' . $creditBonuses . '</u></i></b>';
}
}
endif;

View file

@ -0,0 +1,786 @@
<?php
if (!class_exists('WC_Retailcrm_Orders')) :
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Orders - Allows transfer data orders with CMS.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_Orders
{
/** @var bool|WC_Retailcrm_Proxy|WC_Retailcrm_Client_V5 */
protected $retailcrm;
/** @var WC_Retailcrm_Loyalty|null */
protected $loyalty = null;
/** @var array */
protected $retailcrm_settings;
/** @var WC_Retailcrm_Order_Item */
protected $order_item;
/** @var WC_Retailcrm_Order_Address */
protected $order_address;
/** @var WC_Retailcrm_Order_Payment */
protected $order_payment;
/** @var WC_Retailcrm_Customers */
protected $customers;
/** @var WC_Retailcrm_Order */
protected $orders;
/** @var array */
private $ordersGetRequestCache = [];
/** @var array */
private $order = [];
/** @var bool */
private $cancelLoyalty = false;
/** @var string */
private $loyaltyDiscountType = '';
/** @var array */
private $payment = [];
/**@var array */
private $customFields = [];
public function __construct(
$retailcrm,
$retailcrm_settings,
$order_item,
$order_address,
$customers,
$orders,
$order_payment
) {
$this->retailcrm = $retailcrm;
$this->retailcrm_settings = $retailcrm_settings;
$this->order_item = $order_item;
$this->order_address = $order_address;
$this->customers = $customers;
$this->orders = $orders;
$this->order_payment = $order_payment;
if (isLoyaltyActivate($retailcrm_settings)) {
$this->loyalty = new WC_Retailcrm_Loyalty($retailcrm, $retailcrm_settings);
}
if (!empty($retailcrm_settings['order-meta-data-retailcrm'])) {
$this->customFields = json_decode($retailcrm_settings['order-meta-data-retailcrm'], true);
}
}
/**
* Create order. Returns wc_get_order data or error string.
*
* @param $orderId
*
* @return bool|WC_Order|string
* @throws \Exception
*/
public function orderCreate($orderId)
{
if (!$this->retailcrm instanceof WC_Retailcrm_Proxy) {
return null;
}
WC_Retailcrm_Logger::info(
__METHOD__,
'Start order creating ' . (is_int($orderId) ? $orderId : ''),
['wc_order' => WC_Retailcrm_Logger::formatWcObject($orderId)]
);
try {
$this->order_payment->resetData();
$wcOrder = wc_get_order($orderId);
if ($this->loyalty) {
$wcCustomer = null;
$privilegeType = 'none';
$discountLp = $this->loyalty->deleteLoyaltyCouponInOrder($wcOrder);
$dataOrder = $wcOrder->get_data();
if (isset($dataOrder['customer_id'])) {
$wcCustomer = new WC_Customer($dataOrder['customer_id']) ?? null;
}
if (!$this->loyalty->isValidOrder($wcCustomer, $wcOrder)) {
if ($discountLp > 0) {
WC_Retailcrm_Logger::info(
__METHOD__,
'The user does not meet the requirements for working with the loyalty program. Order Id: ' . $orderId
);
}
$discountLp = 0;
$privilegeType = 'none';
} elseif ($this->loyalty->isValidUser($wcCustomer)) {
$privilegeType = 'loyalty_level';
}
}
WC_Retailcrm_Logger::info(
__METHOD__,
'Create WC_Order ' . $wcOrder->get_id(),
['wc_order' => WC_Retailcrm_Logger::formatWcObject($wcOrder)]
);
$this->processOrder($wcOrder);
if (isset($privilegeType)) {
$this->order['privilegeType'] = $privilegeType;
}
$response = $this->retailcrm->ordersCreate($this->order);
// Allows you to verify order creation and perform additional actions
$response = apply_filters('retailcrm_order_create_after', $response, $wcOrder);
if (!$response instanceof WC_Retailcrm_Response || !$response->isSuccessful()) {
return $response->getErrorString();
}
if (isset($discountLp) && $discountLp > 0) {
$this->loyalty->applyLoyaltyDiscount($wcOrder, $response['order'], $discountLp);
}
} catch (Throwable $exception) {
WC_Retailcrm_Logger::exception(__METHOD__, $exception);
return null;
}
return $wcOrder;
}
/**
* Process order customer data
*
* @param \WC_Order $wcOrder
* @param bool $update
*
* @return bool Returns false if order cannot be processed
* @throws \Exception
*/
protected function processOrderCustomerInfo($wcOrder, $update = false)
{
$customerWasChanged = false;
$wpUser = $wcOrder->get_user();
if ($update) {
$response = $this->getCrmOrder($wcOrder->get_id());
if (!empty($response)) {
$customerWasChanged = self::isOrderCustomerWasChanged($wcOrder, $response);
}
}
if ($wpUser instanceof WP_User) {
if (!$this->customers->isCustomer($wpUser)) {
return false;
}
$wpUserId = (int) $wpUser->get('ID');
if (!$update || ($update && $customerWasChanged)) {
$this->fillOrderCreate($wpUserId, $wpUser->get('billing_email'), $wcOrder);
}
} else {
$wcCustomer = $this->customers->buildCustomerFromOrderData($wcOrder);
if (!$update || ($update && $customerWasChanged)) {
$this->fillOrderCreate(0, $wcCustomer->get_billing_email(), $wcOrder);
}
}
return true;
}
/**
* Fill order on create
*
* @param int $wcCustomerId
* @param string $wcCustomerEmail
* @param \WC_Order $wcOrder
*
* @throws \Exception
*/
protected function fillOrderCreate($wcCustomerId, $wcCustomerEmail, $wcOrder)
{
WC_Retailcrm_Logger::info(
__METHOD__,
sprintf(
'Fill order data: WC_Customer ID: %s email: %s WC_Order ID: %s',
$wcCustomerId,
$wcCustomerEmail,
$wcOrder->get_id()
)
);
$isContact = $this->retailcrm->getCorporateEnabled() && static::isCorporateOrder($wcOrder);
$foundCustomer = $this->customers->findCustomerEmailOrId(
$wcCustomerId,
strtolower($wcCustomerEmail),
$isContact
);
if (empty($foundCustomer)) {
$foundCustomerId = $this->customers->createCustomer($wcCustomerId, $wcOrder);
if (!empty($foundCustomerId)) {
$this->order['customer']['id'] = $foundCustomerId;
}
} else {
$this->order['customer']['id'] = $foundCustomer['id'];
$foundCustomerId = $foundCustomer['id'];
}
$this->order['contragent']['contragentType'] = 'individual';
if ($this->retailcrm->getCorporateEnabled() && static::isCorporateOrder($wcOrder)) {
unset($this->order['contragent']['contragentType']);
$crmCorporate = $this->customers->searchCorporateCustomer(array(
'contactIds' => array($foundCustomerId),
'companyName' => $wcOrder->get_billing_company()
));
if (empty($crmCorporate)) {
$crmCorporate = $this->customers->searchCorporateCustomer(array(
'companyName' => $wcOrder->get_billing_company()
));
}
if (empty($crmCorporate)) {
$corporateId = $this->customers->createCorporateCustomerForOrder(
$foundCustomerId,
$wcCustomerId,
$wcOrder
);
$this->order['customer']['id'] = $corporateId;
} else {
// Testing of this method occurs in customers tests.
// @codeCoverageIgnoreStart
$addressFound = $this->customers->fillCorporateAddress(
$crmCorporate['id'],
new WC_Customer($wcCustomerId),
$wcOrder
);
// If address not found create new address.
if (!$addressFound) {
WC_Retailcrm_Logger::info(
__METHOD__,
'Notification: Create new address for corporate customer ' . $this->order['customer']['id']
);
}
$this->order['customer']['id'] = $crmCorporate['id'];
// @codeCoverageIgnoreEnd
}
$companiesResponse = $this->retailcrm->customersCorporateCompanies(
$this->order['customer']['id'],
[],
null,
null,
'id'
);
if (!empty($companiesResponse) && $companiesResponse->isSuccessful()) {
foreach ($companiesResponse['companies'] as $company) {
if ($company['name'] == $wcOrder->get_billing_company()) {
$this->order['company'] = [
'id' => $company['id'],
'name' => $company['name']
];
break;
}
}
}
$this->order['contact']['id'] = $foundCustomerId;
}
}
/**
* Edit order in CRM
*
* @param int $orderId
*
* @return WC_Order $order | null
* @throws \Exception
*/
public function updateOrder($orderId, $statusTrash = false)
{
if (!$this->retailcrm instanceof WC_Retailcrm_Proxy) {
return null;
}
try {
$wcOrder = wc_get_order($orderId);
if ($wcOrder->get_status() === 'checkout-draft') {
return null;
}
WC_Retailcrm_Logger::info(
__METHOD__,
'Update WC_Order ' . $wcOrder->get_id(),
['wc_order' => WC_Retailcrm_Logger::formatWcObject($wcOrder)]
);
$needRecalculate = false;
$this->processOrder($wcOrder, true, $statusTrash);
if ($this->cancelLoyalty) {
$this->cancelLoyalty = false;
$this->order_item->cancelLoyalty = false;
$needRecalculate = true;
if ($this->loyaltyDiscountType === 'loyalty_level') {
$this->order['privilegeType'] = 'none';
}
if ($this->loyaltyDiscountType === 'bonus_charge') {
$responseCancelBonus = $this->retailcrm->cancelBonusOrder(['externalId' => $this->order['externalId']]);
if (!$responseCancelBonus instanceof WC_Retailcrm_Response || !$responseCancelBonus->isSuccessful()) {
WC_Retailcrm_Logger::error(__METHOD__, 'Error when canceling bonuses');
return null;
}
}
}
$response = $this->retailcrm->ordersEdit($this->order);
$response = apply_filters('retailcrm_order_update_after', $response, $wcOrder);
if ($response instanceof WC_Retailcrm_Response && $response->isSuccessful() && isset($response['order'])) {
$this->payment = $this->orderUpdatePaymentType($wcOrder);
if ($needRecalculate) {
$this->loyalty->calculateLoyaltyDiscount($wcOrder, $response['order']['items']);
}
}
} catch (Throwable $exception) {
WC_Retailcrm_Logger::exception(__METHOD__, $exception);
return null;
}
return $wcOrder;
}
/**
* Update order payment type
*
* @param WC_Order $order
*
* @return null | array $payment
*/
protected function orderUpdatePaymentType($order)
{
if (!isset($this->retailcrm_settings[$order->get_payment_method()])) {
return null;
}
$retailcrmOrder = $this->getCrmOrder($order->get_id());
if (empty($retailcrmOrder)) {
return null;
}
foreach ($retailcrmOrder['payments'] as $paymentData) {
$paymentId = explode('-', $paymentData['externalId']);
if ($paymentId[0] == $order->get_id()) {
$payment = $paymentData;
}
}
if (empty($payment)) {
return null;
}
if ($payment['type'] == $this->retailcrm_settings[$order->get_payment_method()] && $order->is_paid()) {
return $this->sendPayment($order, true, $payment['externalId']);
}
if ($payment['type'] != $this->retailcrm_settings[$order->get_payment_method()]) {
$response = $this->retailcrm->ordersPaymentDelete($payment['id']);
if (!empty($response) && $response->isSuccessful()) {
return $this->sendPayment($order);
}
}
return null;
}
/**
* process to combine order data
*
* @param WC_Order $order
* @param boolean $update
*
* @return void
* @throws \Exception
*/
protected function processOrder($order, $update = false, $statusTrash = false)
{
if (!$order instanceof WC_Order) {
return;
}
if ('auto-draft' === $order->get_status()) {
WC_Retailcrm_Logger::info(__METHOD__, 'Skip, order in auto-draft status');
return;
}
if ($update) {
$this->orders->is_new = false;
}
$orderData = $this->orders->build($order)->getData();
if ($order->get_items('shipping')) {
$shippings = $order->get_items('shipping');
$shipping = reset($shippings);
$shipping_code = explode(':', $shipping['method_id']);
if (isset($this->retailcrm_settings[$shipping['method_id']])) {
$shipping_method = $shipping['method_id'];
} elseif (isset($this->retailcrm_settings[$shipping_code[0]])) {
$shipping_method = $shipping_code[0];
} else {
$shipping_method = $shipping['method_id'] . ':' . $shipping['instance_id'];
}
if (!empty($shipping_method) && !empty($this->retailcrm_settings[$shipping_method])) {
$orderData['delivery']['code'] = $this->retailcrm_settings[$shipping_method];
$service = retailcrm_get_delivery_service($shipping['method_id'], $shipping['instance_id']);
if ($service) {
$orderData['delivery']['service'] = [
'name' => $service['title'],
'code' => $service['instance_id'],
'active' => true
];
}
}
if (isset($shipping['total'])) {
$orderData['delivery']['netCost'] = $shipping['total'];
$orderData['delivery']['cost'] = isset($shipping['total_tax'])
? $shipping['total'] + $shipping['total_tax']
: $shipping['total'];
if (wc_tax_enabled()) {
$shippingTaxClass = get_option('woocommerce_shipping_tax_class');
$rate = $shippingTaxClass == 'inherit'
? getOrderItemRate($order)
: getShippingRate();
if ($rate !== null) {
$orderData['delivery']['vatRate'] = $rate;
}
}
}
}
$orderData['delivery']['address'] = $this->order_address->build($order)->getData();
$orderItems = [];
$crmItems = [];
$wcItems = $order->get_items();
if ($this->loyalty && $update) {
$result = $this->loyalty->getCrmItemsInfo($order->get_id());
if ($result !== []) {
$crmItems = $result['items'];
if (
$statusTrash
|| (
$result['discountType'] !== null
&& in_array($order->get_status(), ['cancelled', 'refunded'])
)
) {
$this->cancelLoyalty = true;
$this->order_item->cancelLoyalty = true;
} else {
$this->cancelLoyalty = $this->order_item->isCancelLoyalty($wcItems, $crmItems);
}
$this->loyaltyDiscountType = $result['discountType'];
}
}
/** @var WC_Order_Item_Product $item */
foreach ($wcItems as $id => $item) {
WC_Retailcrm_Logger::info(
__METHOD__,
'Process WC_Order_Item_Product ' . $id,
['wc_order_item_product' => WC_Retailcrm_Logger::formatWcObject($item)]
);
$crmItem = $crmItems[$id] ?? null;
$orderItems[] = $this->order_item->build($item, $crmItem)->getData();
$this->order_item->resetData($this->cancelLoyalty);
}
unset($crmItems, $crmItem);
$orderData['items'] = $orderItems;
$orderData['discountManualAmount'] = 0;
$orderData['discountManualPercent'] = 0;
if (!$update && $order->get_total() > 0) {
$this->order_payment->isNew = true;
$orderData['payments'][] = $this->order_payment->build($order)->getData();
}
if (!empty($this->customFields)) {
foreach ($this->customFields as $metaKey => $customKey) {
$metaValue = $order->get_meta($metaKey);
if (empty($metaValue)) {
continue;
}
if (strpos($customKey, 'default-crm-field') !== false) {
$crmField = explode('#', $customKey);
if (count($crmField) === 2 && isset($crmField[1])) {
$orderData[$crmField[1]] = $metaValue;
} elseif (isset($crmField[1], $crmField[2], $crmField[3])) {
// For order delivery
$orderData[$crmField[1]][$crmField[2]][$crmField[3]] = $metaValue;
}
} else {
$orderData['customFields'][$customKey] = $metaValue;
}
}
}
$couponCustomField = $this->retailcrm_settings['woo_coupon_apply_field'];
if ($couponCustomField !== 'not-upload') {
$codeCoupons = [];
foreach ($order->get_coupons() as $coupon) {
if (!empty($coupon->get_code())) {
$codeCoupons[] = $coupon->get_code();
}
}
$orderData['customFields'][$couponCustomField] = implode('; ', $codeCoupons);
}
$this->order = WC_Retailcrm_Plugin::clearArray($orderData);
$this->processOrderCustomerInfo($order, $update);
$this->order = apply_filters(
'retailcrm_process_order',
WC_Retailcrm_Plugin::clearArray($this->order),
$order
);
}
public function processOrderForUpload($orderIds)
{
$ordersForUpload = [];
$errorOrders = [];
foreach ($orderIds as $orderId) {
try {
$this->order = [];
$this->processOrder(wc_get_order($orderId));
if ($this->order === []) {
throw new \RuntimeException(sprintf('Order %s is not uploaded', $orderId));
}
$ordersForUpload[] = $this->order;
} catch (Throwable $exception) {
$errorOrders[$orderId] = sprintf(
'Exception for Order [%s]: %s. Trace: %s',
$orderId, $exception->getMessage(), $exception->getTraceAsString()
);
}
}
return [$ordersForUpload, $errorOrders];
}
/**
* Send payment in CRM
*
* @param WC_Order $order
* @param boolean $update
* @param mixed $externalId
*
* @return array $payment
*/
protected function sendPayment($order, $update = false, $externalId = false)
{
$this->order_payment->isNew = !$update;
$payment = $this->order_payment->build($order, $externalId)->getData();
$integrationPayments = get_option('retailcrm_integration_payments');
if (is_array($integrationPayments)) {
$integrationPayments = array_flip($integrationPayments);
}
if ($update === true && isset($integrationPayments[$payment['type']])) {
return $payment;
}
if ($update === false) {
$this->retailcrm->ordersPaymentCreate($payment);
} else {
$this->retailcrm->ordersPaymentEdit($payment);
}
return $payment;
}
/**
* ordersGet wrapper with cache (in order to minimize request count).
*
* @param int|string $orderId
* @param bool $cached
*
* @return array
*/
protected function getCrmOrder($orderId, $cached = true)
{
if ($cached && isset($this->ordersGetRequestCache[$orderId])) {
return (array) $this->ordersGetRequestCache[$orderId];
}
$crmOrder = [];
$response = $this->retailcrm->ordersGet($orderId);
if (!empty($response) && $response->isSuccessful() && isset($response['order'])) {
$crmOrder = (array) $response['order'];
$this->ordersGetRequestCache[$orderId] = $crmOrder;
}
return $crmOrder;
}
/**
* @return array
*/
public function getOrder()
{
return $this->order;
}
/**
* @return array
*/
public function getPayment()
{
return $this->payment;
}
/**
* Returns true if provided order is for corporate customer
*
* @param WC_Order $order
*
* @return bool
*/
public static function isCorporateOrder($order)
{
$billingCompany = $order->get_billing_company();
return !empty($billingCompany);
}
/**
* Returns true if passed crm order is corporate
*
* @param array|\ArrayAccess $order
*
* @return bool
*/
public static function isCorporateCrmOrder($order)
{
return (is_array($order) || $order instanceof ArrayAccess)
&& isset($order['customer'])
&& isset($order['customer']['type'])
&& $order['customer']['type'] == 'customer_corporate';
}
/**
* Returns true if customer in order was changed. `true` will be returned if one of these four conditions is met:
*
* 1. If CMS order is corporate and retailCRM order is not corporate or vice versa, then customer obviously
* needs to be updated in retailCRM.
* 2. If billing company from CMS order is not the same as the one in the retailCRM order,
* then company needs to be updated.
* 3. If contact person or individual externalId is different from customer ID in the CMS order, then
* contact person or customer in retailCRM should be updated (even if customer id in the order is not set).
* 4. If contact person or individual email is not the same as the CMS order billing email, then
* contact person or customer in retailCRM should be updated.
*
* @param \WC_Order $wcOrder
* @param array|\ArrayAccess $crmOrder
*
* @return bool
*/
public static function isOrderCustomerWasChanged($wcOrder, $crmOrder)
{
if (!isset($crmOrder['customer'])) {
return false;
}
$customerWasChanged = self::isCorporateOrder($wcOrder) != self::isCorporateCrmOrder($crmOrder);
$synchronizableUserData = (self::isCorporateCrmOrder($crmOrder) && isset($crmOrder['contact']))
? $crmOrder['contact'] : $crmOrder['customer'];
if (!$customerWasChanged) {
if (self::isCorporateCrmOrder($crmOrder)) {
$currentCrmCompany = isset($crmOrder['company']) ? $crmOrder['company']['name'] : '';
if (!empty($currentCrmCompany) && $currentCrmCompany != $wcOrder->get_billing_company()) {
$customerWasChanged = true;
}
}
if (
isset($synchronizableUserData['externalId'])
&& $synchronizableUserData['externalId'] != $wcOrder->get_customer_id()
) {
$customerWasChanged = true;
} elseif (
isset($synchronizableUserData['email'])
&& $synchronizableUserData['email'] != $wcOrder->get_billing_email()
) {
$customerWasChanged = true;
}
}
return $customerWasChanged;
}
}
endif;

View file

@ -0,0 +1,206 @@
<?php
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Plugin - Internal plugin settings.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_Plugin
{
public $file;
public static $history_run = false;
private static $instance = null;
const MARKETPLACE_LOGO = 'https://s3.eu-central-1.amazonaws.com/retailcrm-billing/images/5b69ce4bda663-woocommercesvg2.svg';
const INTEGRATION_CODE = 'woocommerce';
public static function getInstance($file)
{
if (self::$instance === null) {
self::$instance = new self($file);
}
return self::$instance;
}
/**
* @param $file
*
* @codeCoverageIgnore
*/
private function __construct($file)
{
$this->file = $file;
add_filter('cron_schedules', [$this, 'filter_cron_schedules'], 10, 1);
}
public function filter_cron_schedules($schedules)
{
return array_merge(
$schedules,
[
'five_minutes' => [
'interval' => 300, // seconds
'display' => __('Every 5 minutes')
],
'three_hours' => [
'interval' => 10800, // seconds
'display' => __('Every 3 hours')
],
'fiveteen_minutes' => [
'interval' => 900, // seconds
'display' => __('Every 15 minutes')
],
'four_hours' => [
'interval' => 14400, //seconds
'display' => __('Every 4 hours')
]
],
apply_filters('retailcrm_add_cron_interval', $schedules)
);
}
public function register_activation_hook()
{
register_activation_hook($this->file, array($this, 'activate'));
}
public function register_deactivation_hook()
{
register_deactivation_hook($this->file, array($this, 'deactivate'));
}
/**
* @codeCoverageIgnore
*/
public function activate()
{
if (!class_exists('WC_Integration')) {
add_action('admin_notices', [new WC_Integration_Retailcrm(), 'woocommerce_missing_notice']);
return;
}
if (!class_exists('WC_Retailcrm_Logger')) {
require_once(WC_Integration_Retailcrm::checkCustomFile('include/components/class-wc-retailcrm-logger.php'));
}
if (!class_exists('WC_Retailcrm_Icml')) {
require_once(WC_Integration_Retailcrm::checkCustomFile('include/class-wc-retailcrm-icml.php'));
}
if (!class_exists('WC_Retailcrm_Icml_Writer')) {
require_once(WC_Integration_Retailcrm::checkCustomFile('include/icml/class-wc-retailcrm-icml-writer.php'));
}
if (!class_exists('WC_Retailcrm_Base')) {
require_once(WC_Integration_Retailcrm::checkCustomFile('include/class-wc-retailcrm-base.php'));
}
$retailcrm_icml = new WC_Retailcrm_Icml();
$retailcrm_icml->generate();
}
public function deactivate()
{
do_action('retailcrm_deactivate');
if (wp_next_scheduled('retailcrm_icml')) {
wp_clear_scheduled_hook('retailcrm_icml');
}
if (wp_next_scheduled('retailcrm_history')) {
wp_clear_scheduled_hook('retailcrm_history');
}
if (wp_next_scheduled('retailcrm_inventories')) {
wp_clear_scheduled_hook('retailcrm_inventories');
}
if (wp_next_scheduled('retailcrm_loyalty_upload_price')) {
wp_clear_scheduled_hook('retailcrm_loyalty_upload_price');
}
}
/**
* Edit configuration in CRM
*
* @param WC_Retailcrm_Proxy|WC_Retailcrm_Client_V5 $api_client
* @param string $client_id
* @param bool $active
*
* @return bool
*/
public static function integration_module($api_client, $client_id, $active = true)
{
if (!$api_client instanceof WC_Retailcrm_Proxy) {
return false;
}
$configuration = array(
'name' => 'WooCommerce',
'logo' => self::MARKETPLACE_LOGO,
'code' => self::INTEGRATION_CODE . '-' . $client_id,
'active' => $active,
);
$configuration['integrationCode'] = self::INTEGRATION_CODE;
$configuration['baseUrl'] = get_site_url();
$configuration['clientId'] = $client_id;
$configuration['accountUrl'] = get_site_url();
$response = $api_client->integrationModulesEdit($configuration);
return !empty($response) && $response->isSuccessful();
}
/**
* Unset empty fields
*
* @param array $arr input array
*
* @return array
*/
public static function clearArray(array $arr)
{
if (!is_array($arr)) {
return $arr;
}
$result = array();
foreach ($arr as $index => $node) {
$result[$index] = (is_array($node))
? self::clearArray($node)
: $node;
if (
$result[$index] === ''
|| $result[$index] === null
|| (is_array($result[$index]) && count($result[$index]) < 1)
) {
unset($result[$index]);
}
}
return $result;
}
/**
* Check running history
*
* @return boolean
*/
public static function history_running()
{
return self::$history_run;
}
}

View file

@ -0,0 +1,238 @@
<?php
if (!class_exists('WC_Retailcrm_Upload_Discount_Price')):
class WC_Retailcrm_Upload_Discount_Price
{
const DISCOUNT_TYPE_PRICE = 'woo-promotion-lp';
protected $activeLoyalty = false;
protected $settings;
protected $site;
/** @var bool|WC_Retailcrm_Proxy|WC_Retailcrm_Client_V5 */
protected $apiClient;
public function __construct($aplClient = false)
{
$this->settings = get_option(WC_Retailcrm_Base::$option_key);
$this->apiClient = $aplClient;
if (isset($this->settings['loyalty']) && $this->settings['loyalty'] === WC_Retailcrm_Base::YES) {
$this->activeLoyalty = true;
}
}
public function upload()
{
if (!$this->activeLoyalty) {
return;
}
$error = $this->uploadSettings();
if ($error !== '') {
WC_Retailcrm_Logger::error(__METHOD__, $error);
return;
}
$productStatuses = $this->getProductStatuses();
if (!$productStatuses) {
$productStatuses = ['publish'];
}
$page = 1;
$requestData = [];
do {
$products = wc_get_products(
[
'limit' => 1000,
'status' => $productStatuses,
'page' => $page,
'paginate' => true
]
);
/** WP version >= 6 */
if (function_exists('wp_cache_flush_runtime')) {
wp_cache_flush_runtime();
} else {
wp_cache_flush();
}
if (empty($products)) {
WC_Retailcrm_Logger::error(__METHOD__, 'Can`t get products!');
return;
}
try {
foreach ($products->products as $offer) {
$type = $offer->get_type();
if (strpos($type, 'variable') !== false || strpos($type, 'variation') !== false) {
foreach ($offer->get_children() as $childId) {
$childProduct = wc_get_product($childId);
if (!$childProduct) {
continue;
}
$sendOffer = $this->getOfferData($childProduct);
if ($sendOffer !== []) {
$requestData[] = $sendOffer;
}
}
} else {
$sendOffer = $this->getOfferData($offer);
if ($sendOffer !== []) {
$requestData[] = $sendOffer;
}
}
}
$chunks = array_chunk($requestData, 250);
foreach ($chunks as $chunk) {
$this->apiClient->storePricesUpload($chunk, $this->site);
time_nanosleep(0, 200000000);
}
unset($chunks);
} catch (\Throwable $exception) {
WC_Retailcrm_Logger::exception(__METHOD__, $exception);
return;
}
++$page;
} while ($page <= $products->max_num_pages);
}
private function getOfferData(WC_Product $product)
{
$currentPrice = wc_get_price_including_tax($product);
$defaultPrice = wc_get_price_including_tax($product, ["price" => $product->get_regular_price()]);
if ($currentPrice === $defaultPrice) {
return [];
}
return [
'externalId' => $product->get_id(),
'site' => $this->site,
'prices' => [
[
'code' => self::DISCOUNT_TYPE_PRICE,
'price' => $currentPrice
]
]
];
}
private function getProductStatuses()
{
$statuses = [];
foreach (get_post_statuses() as $key => $value) {
if (isset($this->settings['p_' . $key]) && $this->settings['p_' . $key] == WC_Retailcrm_Base::YES) {
$statuses[] = $key;
}
}
return $statuses;
}
private function uploadSettings()
{
if (!$this->apiClient instanceof WC_Retailcrm_Proxy
&& !$this->apiClient instanceof WC_Retailcrm_Client_V5
) {
return 'API client has not been initialized';
}
$this->site = $this->apiClient->getSingleSiteForKey();
if (empty($this->site)) {
return 'Error with CRM credentials: need an valid apiKey assigned to one certain site';
}
$response = $this->apiClient->getPriceTypes();
if (
!$response instanceof WC_Retailcrm_Response
|| !$response->offsetExists('priceTypes')
|| empty($response['priceTypes'])
) {
return 'Error getting price types';
}
$defaultPrice = null;
$discountPriceType = null;
foreach ($response['priceTypes'] as $priceType) {
if ($priceType['default'] === true) {
$defaultPrice = $priceType;
}
if ($priceType['code'] === self::DISCOUNT_TYPE_PRICE) {
$discountPriceType = $priceType;
}
}
if ($discountPriceType === null) {
$discountPriceType = [
'code' => self::DISCOUNT_TYPE_PRICE,
'name' => __('Woocommerce promotional price', 'retailcrm'),
'active' => true,
'description' => __('Promotional price type for Woocommerce store, generated automatically.
Necessary for correct synchronization work when loyalty program is enabled
(Do not delete. Do not deactivate)', 'retailcrm'),
'ordering' => 999,
'promo' => true
];
if (isset($defaultPrice['geo'])) {
$discountPriceType['geo'] = $defaultPrice['geo'];
}
if (isset($defaultPrice['groups'])) {
$discountPriceType['groups'] = $defaultPrice['groups'];
}
if (isset($defaultPrice['currency'])) {
$discountPriceType['currency'] = $defaultPrice['currency'];
}
if (isset($defaultPrice['filterExpression'])) {
$discountPriceType['filterExpression'] = $defaultPrice['filterExpression'];
}
$response = $this->apiClient->editPriceType($discountPriceType);
if (!$response instanceof WC_Retailcrm_Response || !$response['success']) {
return 'Error creating price type';
}
} elseif ($discountPriceType['active'] === false || $discountPriceType['promo'] === false) {
$discountPriceType['active'] = true;
$discountPriceType['promo'] = true;
$response = $this->apiClient->editPriceType($discountPriceType);
if (!$response instanceof WC_Retailcrm_Response || !$response['success']) {
return 'Error activate price type';
}
}
return '';
}
}
endif;

View file

@ -0,0 +1,305 @@
<?php
if (class_exists('WC_Retailcrm_Uploader') === false) {
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Uploader - Allows upload archival orders/customers in CRM.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_Uploader
{
const RETAILCRM_COUNT_OBJECT_UPLOAD = 50;
/**
* Api client RetailCRM
*
* @var WC_Retailcrm_Client_V5
*/
private $retailcrm;
/**
* Orders RetailCRM
*
* @var WC_Retailcrm_Orders
*/
private $orders;
/**
* Customers RetailCRM
*
* @var WC_Retailcrm_Customers
*/
private $customers;
/**
* WC_Retailcrm_Uploader constructor.
*
* @param WC_Retailcrm_Client_V5 $retailcrm Api client RetailCRM.
* @param WC_Retailcrm_Orders $orders Object order RetailCRM.
* @param WC_Retailcrm_Customers $customers Object customer RetailCRM.
*/
public function __construct($retailcrm, $orders, $customers)
{
$this->retailcrm = $retailcrm;
$this->orders = $orders;
$this->customers = $customers;
}
/**
* Uploads selected order in CRM
*
* @return void
* @throws Exception Invalid argument exception.
*/
public function uploadSelectedOrders()
{
$ids = $_GET['order_ids_retailcrm'];
WC_Retailcrm_Logger::info(__METHOD__, 'Selected order IDs: ' . json_encode($ids));
if (!empty($ids)) {
preg_match_all('/\d+/', $ids, $matches);
if (!empty($matches[0])) {
$this->uploadArchiveOrders(null, $matches[0]);
}
}
}
/**
* Uploads archive order in CRM
*
* @param int|null $page Number page uploads.
* @param array $ids Ids orders upload.
*
* @return void|null
* @throws Exception Invalid argument exception.
*/
public function uploadArchiveOrders(?int $page, array $ids = [])
{
if (!$this->retailcrm instanceof WC_Retailcrm_Proxy) {
return null;
}
$orderIds = [];
if (null !== $page) {
$orderIds = $this->getCmsOrders($page);
} elseif ([] !== $ids) {
$orderIds = $ids;
}
if ($orderIds === []) {
return null;
}
WC_Retailcrm_Logger::info(__METHOD__, 'Archive order IDs: ' . implode(', ', $ids));
[$ordersForUpload, $uploadErrors] = $this->orders->processOrderForUpload($orderIds);
try {
$response = $this->retailcrm->ordersUpload($ordersForUpload);
if (!$response->isSuccessful()) {
throw new RuntimeException(
sprintf(
'Failure to upload orders: %s. Status code: %s',
$response->getErrorString(),
$response->getStatusCode()
)
);
}
} catch (Exception $exception) {
WC_Retailcrm_Logger::error(
__METHOD__,
sprintf("Error while uploading orders: %s", $exception->getMessage())
);
return null;
}
/** WP version >= 6 */
if (function_exists('wp_cache_flush_runtime')) {
wp_cache_flush_runtime();
} else {
wp_cache_flush();
}
$this->logOrdersUploadErrors($uploadErrors);
}
/**
* Uploads archive customer in CRM
*
* @param integer $page Number page uploads.
*
* @return array
* @throws Exception Invalid argument exception.
*/
public function uploadArchiveCustomers($page)
{
if (!$this->retailcrm instanceof WC_Retailcrm_Proxy) {
return null;
}
$users = $this->getCmsUsers($page);
if ($users !== []) {
$dataCustomers = [];
foreach ($users as $user) {
if ($this->customers->isCustomer($user) === false) {
continue;
}
$customer = new WC_Customer($user->ID);
$this->customers->processCustomerForUpload($customer);
$dataCustomers[] = $this->customers->getCustomer();
}
$this->retailcrm->customersUpload($dataCustomers);
}
return $dataCustomers;
}
/**
* Return orders ids
*
* @param integer $page Number page uploads.
*
* @return mixed
*/
private function getCmsOrders($page)
{
return wc_get_orders(
[
'type' => wc_get_order_types('view-orders'),
'limit' => self::RETAILCRM_COUNT_OBJECT_UPLOAD,
'status' => array_keys(wc_get_order_statuses()),
'offset' => self::RETAILCRM_COUNT_OBJECT_UPLOAD * $page,
'return' => 'ids',
]
);
}
/**
* Return count orders
*
* @return integer
*/
public function getCountOrders()
{
global $wpdb;
if (useHpos()) {
// Use {$wpdb->prefix}, because wp_wc_orders not standard WP table
$result = $wpdb->get_results("SELECT COUNT(ID) as `count` FROM {$wpdb->prefix}wc_orders");
} else {
$result = $wpdb->get_results("SELECT COUNT(ID) as `count` FROM $wpdb->posts WHERE post_type = 'shop_order'");
}
return $result[0]->count ? (int) $result[0]->count : 0;
}
/**
* Return users ids
*
* @param integer $page Number page uploads.
*
* @return mixed
*/
private function getCmsUsers(int $page)
{
return get_users(
[
'number' => self::RETAILCRM_COUNT_OBJECT_UPLOAD,
'offset' => self::RETAILCRM_COUNT_OBJECT_UPLOAD * $page,
]
);
}
/**
* Return count users
*
* @return integer
*/
public function getCountUsers()
{
$userCount = count_users();
return $userCount['total_users'] ? (int) $userCount['total_users'] : 0;
}
/**
* Array keys must be orders ID's in WooCommerce, values must be strings (error messages).
*
* @param array $errors Id order - key and message error - value.
*
* @return void
*
* @codeCoverageIgnore
*/
private function logOrdersUploadErrors($errors)
{
if ($errors === []) {
return;
}
foreach ($errors as $orderId => $error) {
WC_Retailcrm_Logger::error(
__METHOD__,
sprintf("Error while uploading [%d] => %s", $orderId, $error)
);
}
}
public function uploadConsole($entity, $page = 0)
{
$ordersPages = (int) ceil($this->getCountOrders() / 50);
$customerPages = (int) ceil($this->getCountUsers() / 50);
try {
switch ($entity) {
case 'orders':
$this->archiveUpload('orders', $page, $ordersPages);
break;
case 'customers':
$this->archiveUpload('customers', $page, $customerPages);
break;
case 'full_upload':
$this->archiveUpload('customers', 0, $customerPages);
$this->archiveUpload('orders', 0, $ordersPages);
break;
default:
echo 'Unknown entity: ' . $entity;
}
} catch (Exception $exception) {
echo $exception->getMessage();
}
}
public function archiveUpload($entity, $page, $totalPages)
{
echo $entity . ' uploading started' . PHP_EOL;
do {
if ($entity === 'orders') {
$this->uploadArchiveOrders($page);
} elseif ($entity === 'customers') {
$this->uploadArchiveCustomers($page);
}
echo $page . ' page uploaded' . PHP_EOL;
$page++;
} while ($page <= $totalPages);
}
}
}

View file

@ -0,0 +1,311 @@
<?php
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Customer_Switcher - This component provides builder-like interface in order to make it easier to
* change customer & customer data in the order via retailCRM history.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_Customer_Switcher implements WC_Retailcrm_Builder_Interface
{
/**
* @var \WC_Retailcrm_Customer_Switcher_State $data
*/
private $data;
/**
* @var \WC_Retailcrm_Customer_Switcher_Result|null $result
*/
private $result;
/**
* WC_Retailcrm_Customer_Data_Replacer constructor.
*/
public function __construct()
{
$this->reset();
}
/**
* In fact, this will execute customer change in provided order.
* This will not produce any new entities.
*
* @return $this|\WC_Retailcrm_Builder_Interface
* @throws \WC_Data_Exception
*/
public function build()
{
$this->data->validate();
WC_Retailcrm_Logger::info(
__METHOD__,
'Build Customer state',
['customer_state' => $this->data]
);
$newCustomer = $this->data->getNewCustomer();
$newContact = $this->data->getNewContact();
$newCompany = $this->data->getNewCompanyName();
$companyAddress = $this->data->getCompanyAddress();
if (!empty($newCustomer)) {
WC_Retailcrm_Logger::info(
__METHOD__,
'Changing to individual customer for order ' . $this->data->getWcOrder()->get_id()
);
$this->processChangeToRegular($this->data->getWcOrder(), $newCustomer, false);
$this->data->getWcOrder()->set_billing_company('');
} else {
if (!empty($newContact)) {
WC_Retailcrm_Logger::info(
__METHOD__,
'Changing to contact person customer for order ' . $this->data->getWcOrder()->get_id()
);
$this->processChangeToRegular($this->data->getWcOrder(), $newContact, true);
}
if (!empty($newCompany)) {
WC_Retailcrm_Logger::info(
__METHOD__,
sprintf(
'Replacing old order id=`%d` company `%s` with new company `%s`',
$this->data->getWcOrder()->get_id(),
$this->data->getWcOrder()->get_billing_company(),
$newCompany
)
);
$this->processCompanyChange();
}
if (!empty($companyAddress)) {
$this->processCompanyAddress();
}
}
return $this;
}
/**
* Change order customer to regular one
*
* @param \WC_Order $wcOrder
* @param array $newCustomer
* @param bool $isContact
*
* @throws \WC_Data_Exception
*/
public function processChangeToRegular($wcOrder, $newCustomer, $isContact)
{
$wcCustomer = null;
WC_Retailcrm_Logger::info(
__METHOD__,
'Switching customer in order ' . $wcOrder->get_id(),
['crm_customer' => $newCustomer]
);
if (isset($newCustomer['externalId'])) {
$wcCustomer = new WC_Customer($newCustomer['externalId']);
if (!empty($wcCustomer)) {
$wcOrder->set_customer_id($wcCustomer->get_id());
WC_Retailcrm_Logger::info(
__METHOD__,
sprintf('Set customer to %s in order %s', $wcCustomer->get_id(), $wcOrder->get_id())
);
}
} else {
$wcOrder->set_customer_id(0);
WC_Retailcrm_Logger::info(
__METHOD__,
'Set customer to 0 (guest) in order ' . $wcOrder->get_id()
);
}
$fields = array(
'billing_first_name' => self::arrayValue($newCustomer, 'firstName'),
'billing_last_name' => self::arrayValue($newCustomer, 'lastName'),
'billing_email' => self::arrayValue($newCustomer, 'email')
);
foreach ($fields as $field => $value) {
$wcOrder->{'set_' . $field}($value);
}
$address = self::arrayValue($newCustomer, 'address', array());
if ($isContact) {
self::setShippingAddressToOrder($wcOrder, $address);
} else {
self::setBillingAddressToOrder($wcOrder, $address);
self::setShippingAddressToOrder($wcOrder, $address);
}
$wcOrder->set_billing_phone(self::singleCustomerPhone($newCustomer));
$this->result = new WC_Retailcrm_Customer_Switcher_Result($wcCustomer, $wcOrder);
}
/**
* Process company address.
*
* @throws \WC_Data_Exception
*/
protected function processCompanyAddress()
{
$wcOrder = $this->data->getWcOrder();
$companyAddress = $this->data->getCompanyAddress();
if (!empty($companyAddress)) {
self::setBillingAddressToOrder($wcOrder, $companyAddress);
}
if (empty($this->result)) {
$this->result = new WC_Retailcrm_Customer_Switcher_Result(null, $wcOrder);
}
}
/**
* This will update company field in order and create result if it's not set (happens when only company was changed).
*
* @throws \WC_Data_Exception
*/
public function processCompanyChange()
{
$this->data->getWcOrder()->set_billing_company($this->data->getNewCompanyName());
if (empty($this->result)) {
$this->result = new WC_Retailcrm_Customer_Switcher_Result(null, $this->data->getWcOrder());
}
}
/**
* @return $this|\WC_Retailcrm_Builder_Interface
*/
public function reset()
{
$this->data = new WC_Retailcrm_Customer_Switcher_State();
$this->result = null;
return $this;
}
/**
* Set initial state into component
*
* @param \WC_Retailcrm_Customer_Switcher_State $data
*
* @return $this|\WC_Retailcrm_Builder_Interface
*/
public function setData($data)
{
if (!($data instanceof WC_Retailcrm_Customer_Switcher_State)) {
throw new \InvalidArgumentException('Invalid data type');
}
$this->data = $data;
return $this;
}
/**
* @return \WC_Retailcrm_Customer_Switcher_Result|null
*/
public function getResult()
{
return $this->result;
}
/**
* @return \WC_Retailcrm_Customer_Switcher_State
*/
public function getData()
{
return $this->data;
}
/**
* Sets billing address properties in order
*
* @param \WC_Order $wcOrder
* @param array $address
*
* @throws \WC_Data_Exception
*/
private static function setBillingAddressToOrder($wcOrder, $address)
{
$wcOrder->set_billing_state(self::arrayValue($address, 'region', ''));
$wcOrder->set_billing_postcode(self::arrayValue($address, 'index', ''));
$wcOrder->set_billing_country(self::arrayValue($address, 'country', ''));
$wcOrder->set_billing_city(self::arrayValue($address, 'city', ''));
$wcOrder->set_billing_address_1(self::arrayValue($address, 'text', ''));
}
/**
* Sets shipping address properties in order
*
* @param \WC_Order $wcOrder
* @param array $address
*
* @throws \WC_Data_Exception
*/
private static function setShippingAddressToOrder($wcOrder, $address)
{
$wcOrder->set_shipping_state(self::arrayValue($address, 'region', ''));
$wcOrder->set_shipping_postcode(self::arrayValue($address, 'index', ''));
$wcOrder->set_shipping_country(self::arrayValue($address, 'country', ''));
$wcOrder->set_shipping_city(self::arrayValue($address, 'city', ''));
$wcOrder->set_shipping_address_1(self::arrayValue($address, 'text', ''));
}
/**
* @param array|\ArrayObject|\ArrayAccess $arr
* @param string $key
* @param string $def
*
* @return mixed|string
*/
private static function arrayValue($arr, $key, $def = '')
{
if (!is_array($arr) && !($arr instanceof ArrayObject) && !($arr instanceof ArrayAccess)) {
return $def;
}
if (!array_key_exists($key, $arr) && !empty($arr[$key])) {
return $def;
}
return isset($arr[$key]) ? $arr[$key] : $def;
}
/**
* Returns first phone from order data or null
*
* @param array $customerData
*
* @return string|null
*/
private static function singleCustomerPhone($customerData)
{
if (!array_key_exists('phones', $customerData)) {
return '';
}
if (empty($customerData['phones']) || !is_array($customerData['phones'])) {
return '';
}
$phones = $customerData['phones'];
$phone = reset($phones);
if (!isset($phone['number'])) {
return '';
}
return (string) $phone['number'];
}
}

View file

@ -0,0 +1,430 @@
<?php
/**
* PHP version 7.0
*
* Class WC_Retailcrm_History_Assembler - Assembles history records into list which closely resembles
* orders & customers list output from API.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_History_Assembler
{
/**
* Assembles orders list from history data
*
* @param array $orderHistory
*
* @return array
*/
public static function assemblyOrder($orderHistory)
{
$fields = self::getMappingValues();
$orders = [];
$orderHistory = self::filterHistory($orderHistory, 'order');
foreach ($orderHistory as $change) {
$change['order'] = self::removeEmpty($change['order']);
if (isset($change['order']['items']) && $change['order']['items']) {
$items = [];
foreach ($change['order']['items'] as $item) {
if (isset($change['created'])) {
$item['create'] = 1;
}
$items[$item['id']] = $item;
}
$change['order']['items'] = $items;
}
if (isset($change['order']['contragent']['contragentType']) && $change['order']['contragent']['contragentType']) {
$change['order']['contragentType'] = $change['order']['contragent']['contragentType'];
unset($change['order']['contragent']);
}
$orderMainInfo = WC_Retailcrm_Plugin::clearArray(
[
'id' => $change['order']['id'] ?? '',
'externalId' => $change['order']['externalId'] ?? '',
'managerId' => $change['order']['managerId'] ?? '',
'site' => $change['order']['site'] ?? '',
]
);
if (!empty($orders) && isset($orders[$change['order']['id']])) {
$orders[$change['order']['id']] = array_merge($orders[$change['order']['id']], $orderMainInfo);
} else {
$orders[$change['order']['id']] = !empty($change['created']) ? $change['order'] : $orderMainInfo;
}
if ($change['field'] === 'status') {
$orders[$change['order']['id']]['status'] = $change['order']['status'];
}
if (isset($change['item']) && $change['item']) {
if (isset($orders[$change['order']['id']]['items'][$change['item']['id']])) {
$orders[$change['order']['id']]['items'][$change['item']['id']] = array_merge($orders[$change['order']['id']]['items'][$change['item']['id']], $change['item']);
} else {
$orders[$change['order']['id']]['items'][$change['item']['id']] = $change['item'];
}
if ($change['oldValue'] === null && $change['field'] == 'order_product') {
$orders[$change['order']['id']]['items'][$change['item']['id']]['create'] = true;
}
if ($change['newValue'] === null && $change['field'] == 'order_product') {
$orders[$change['order']['id']]['items'][$change['item']['id']]['delete'] = true;
}
if (isset($fields['item'][$change['field']]) && $fields['item'][$change['field']]) {
$orders[$change['order']['id']]['items'][$change['item']['id']][$fields['item'][$change['field']]] = $change['newValue'];
}
}
if (isset($change['payment']) && $change['field'] == 'payments') {
if ($change['newValue'] !== null) {
$orders[$change['order']['id']]['payments'][] = self::newValue($change['payment']);
}
}
if ($change['field'] == 'payments.status' && $change['newValue'] !== null) {
$orders[$change['order']['id']]['payments']['id']['status'] = self::newValue($change['newValue']);
} else {
if (isset($fields['delivery'][$change['field']]) && $fields['delivery'][$change['field']] == 'service') {
$orders[$change['order']['id']]['delivery']['service']['code'] = self::newValue($change['newValue']);
} elseif (isset($fields['delivery'][$change['field']]) && $fields['delivery'][$change['field']]) {
$orders[$change['order']['id']]['delivery'][$fields['delivery'][$change['field']]] = self::newValue($change['newValue']);
} elseif (isset($fields['orderAddress'][$change['field']]) && $fields['orderAddress'][$change['field']]) {
$orders[$change['order']['id']]['delivery']['address'][$fields['orderAddress'][$change['field']]] = $change['newValue'];
} elseif (isset($fields['integrationDelivery'][$change['field']]) && $fields['integrationDelivery'][$change['field']]) {
$orders[$change['order']['id']]['delivery']['service'][$fields['integrationDelivery'][$change['field']]] = self::newValue($change['newValue']);
} elseif (isset($fields['customerContragent'][$change['field']]) && $fields['customerContragent'][$change['field']]) {
$orders[$change['order']['id']][$fields['customerContragent'][$change['field']]] = self::newValue($change['newValue']);
} elseif (strripos($change['field'], 'custom_') !== false) {
$orders[$change['order']['id']]['customFields'][str_replace('custom_', '', $change['field'])] = self::newValue($change['newValue']);
} elseif (isset($fields['order'][$change['field']]) && $fields['order'][$change['field']]) {
$orders[$change['order']['id']][$fields['order'][$change['field']]] = self::newValue($change['newValue']);
}
if (isset($change['created'])) {
$orders[$change['order']['id']]['create'] = 1;
}
if (isset($change['deleted'])) {
$orders[$change['order']['id']]['deleted'] = 1;
}
}
}
return $orders;
}
/**
* Assembles customers list from history changes
*
* @param array $customerHistory
*
* @return array
*/
public static function assemblyCustomer($customerHistory)
{
$customers = array();
$fields = self::getMappingValues(array('customer'));
$fieldsAddress = self::getMappingValues(array('customerAddress'));
$customerHistory = self::filterHistory($customerHistory, 'customer');
foreach ($customerHistory as $change) {
$change['customer'] = self::removeEmpty($change['customer']);
if (
isset($change['deleted'])
&& $change['deleted']
&& isset($customers[$change['customer']['id']])
) {
$customers[$change['customer']['id']]['deleted'] = true;
continue;
}
if ($change['field'] == 'id') {
$customers[$change['customer']['id']] = $change['customer'];
}
if (isset($customers[$change['customer']['id']])) {
$customers[$change['customer']['id']] = array_merge($customers[$change['customer']['id']], $change['customer']);
} else {
$customers[$change['customer']['id']] = $change['customer'];
}
if (
isset($fields['customer'][$change['field']])
&& $fields['customer'][$change['field']]
) {
$customers[
$change['customer']['id']
][
$fields['customer'][$change['field']]
] = self::newValue($change['newValue']);
}
if (
isset($fieldsAddress['customerAddress'][$change['field']])
&& $fieldsAddress['customerAddress'][$change['field']]
) {
if (!isset($customers[$change['customer']['id']]['address'])) {
$customers[$change['customer']['id']]['address'] = array();
}
$customers[
$change['customer']['id']
][
'address'
][
$fieldsAddress['customerAddress'][$change['field']]
] = self::newValue($change['newValue']);
}
if (strripos($change['field'], 'custom_') !== false) {
$customers[$change['customer']['id']]['customFields'][str_replace( 'custom_', '', $change['field'])] = self::newValue($change['newValue']);
}
// email_marketing_unsubscribed_at old value will be null and new value will be datetime in
// `Y-m-d H:i:s` format if customer was marked as unsubscribed in retailCRM
if (isset($change['customer']['id']) && $change['field'] == 'email_marketing_unsubscribed_at') {
if ($change['oldValue'] == null && is_string(self::newValue($change['newValue']))) {
$customers[$change['customer']['id']]['subscribed'] = false;
} elseif (is_string($change['oldValue']) && self::newValue($change['newValue']) == null) {
$customers[$change['customer']['id']]['subscribed'] = true;
}
}
}
return $customers;
}
/**
* Assembles corporate customers list from changes
*
* @param array $customerHistory
*
* @return array
*/
public static function assemblyCorporateCustomer($customerHistory)
{
$fields = self::getMappingValues(array('customerCorporate', 'customerAddress'));
$customersCorporate = array();
foreach ($customerHistory as $change) {
$change['customer'] = self::removeEmpty($change['customer']);
if (
isset($change['deleted'])
&& $change['deleted']
&& isset($customersCorporate[$change['customer']['id']])
) {
$customersCorporate[$change['customer']['id']]['deleted'] = true;
continue;
}
if (isset($customersCorporate[$change['customer']['id']])) {
if (
isset($customersCorporate[$change['customer']['id']]['deleted'])
&& $customersCorporate[$change['customer']['id']]['deleted']
) {
continue;
}
$customersCorporate[$change['customer']['id']] = array_merge(
$customersCorporate[$change['customer']['id']],
$change['customer']
);
} else {
$customersCorporate[$change['customer']['id']] = $change['customer'];
}
if (
isset($fields['customerCorporate'][$change['field']])
&& $fields['customerCorporate'][$change['field']]
) {
$customersCorporate[
$change['customer']['id']
][
$fields['customerCorporate'][$change['field']]
] = self::newValue($change['newValue']);
}
if (isset($fields['customerAddress'][$change['field']]) && $fields['customerAddress'][$change['field']]) {
if (empty($customersCorporate[$change['customer']['id']]['address'])) {
$customersCorporate[$change['customer']['id']]['address'] = array();
}
$customersCorporate[
$change['customer']['id']
][
'address'
][
$fields['customerAddress'][$change['field']]
] = self::newValue($change['newValue']);
}
if ($change['field'] == 'address') {
$customersCorporate[
$change['customer']['id']
]['address'] = array_merge($change['address'], self::newValue($change['newValue']));
}
}
foreach ($customersCorporate as $id => &$customer) {
if (empty($customer['id']) && !empty($id)) {
$customer['id'] = $id;
$customer['deleted'] = true;
}
}
return $customersCorporate;
}
/**
* Returns mapping data for retailCRM entities. Used to assembly entities from history.
*
* @param array $groupFilter
*
* @return array
*/
private static function getMappingValues($groupFilter = array())
{
$fields = array();
$mappingFile = realpath(WC_Integration_Retailcrm::checkCustomFile('config/objects.xml'));
if (file_exists($mappingFile)) {
$objects = simplexml_load_file($mappingFile);
foreach ($objects->fields->field as $object) {
if (empty($groupFilter) || in_array($object["group"], $groupFilter)) {
$fields[(string)$object["group"]][(string)$object["id"]] = (string)$object;
}
}
}
return $fields;
}
/**
* Value accessor
*
* @param array $value
*
* @return mixed
*/
private static function newValue($value)
{
if (isset($value['code'])) {
return $value['code'];
} else {
return $value;
}
}
/**
* Returns array without values which are considered empty
*
* @param array|\ArrayAccess $inputArray
*
* @return array
*/
private static function removeEmpty($inputArray)
{
$outputArray = array();
if (!empty($inputArray)) {
foreach ($inputArray as $key => $element) {
if (!empty($element) || $element === 0 || $element === '0') {
if (is_array($element)) {
$element = self::removeEmpty($element);
}
$outputArray[$key] = $element;
}
}
}
return $outputArray;
}
/**
* Filters out history by these terms:
* - Changes from current API key will be added only if CMS changes are more actual than history.
* - All other changes will be merged as usual.
* It fixes these problems:
* - Changes from current API key are merged when it's not needed.
* - Changes from CRM can overwrite more actual changes from CMS due to ignoring current API key changes.
*
* @param array $historyEntries Raw history from CRM
* @param string $recordType Entity field name, e.g. `customer` or `order`.
*
* @return array
*/
private static function filterHistory($historyEntries, $recordType)
{
$history = array();
$organizedHistory = array();
$notOurChanges = array();
foreach ($historyEntries as $entry) {
if (!isset($entry[$recordType]['externalId'])) {
if (
$entry['source'] == 'api'
&& isset($change['apiKey']['current'])
&& $entry['apiKey']['current'] == true
&& $entry['field'] != 'externalId'
) {
continue;
} else {
$history[] = $entry;
}
continue;
}
$externalId = $entry[$recordType]['externalId'];
$field = $entry['field'];
if (!isset($organizedHistory[$externalId])) {
$organizedHistory[$externalId] = array();
}
if (!isset($notOurChanges[$externalId])) {
$notOurChanges[$externalId] = array();
}
if (
$entry['source'] == 'api'
&& isset($entry['apiKey']['current'])
&& $entry['apiKey']['current'] == true
) {
if (isset($notOurChanges[$externalId][$field]) || $entry['field'] == 'externalId') {
$organizedHistory[$externalId][] = $entry;
} else {
continue;
}
} else {
$organizedHistory[$externalId][] = $entry;
$notOurChanges[$externalId][$field] = true;
}
}
unset($notOurChanges);
foreach ($organizedHistory as $historyChunk) {
$history = array_merge($history, $historyChunk);
}
return $history;
}
}

View file

@ -0,0 +1,212 @@
<?php
if (!class_exists('WC_Retailcrm_Logger') && class_exists('WC_Log_Levels')) :
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Logger - Allows display important debug information.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
* @codeCoverageIgnore
*/
class WC_Retailcrm_Logger
{
/** @var string */
private const HANDLE = 'retailcrm';
public const REQUEST = 'REQUEST';
public const RESPONSE = 'RESPONSE';
public CONST EXCEPTION = 'EXCEPTION';
/**
* @var \WC_Logger_Interface $instance
*/
private static $instance;
/**
* @var array $additionalHandlers
*/
private static $additionalHandlers;
/**
* @var string $logIdentifier
*/
private static $logIdentifier;
/**
* @var string $currentHook
*/
private static $currentHook;
/**
* @var float $startTime
*/
private static $startTime;
private function __construct() {}
private static function getInstance(): WC_Logger_Interface
{
if (!static::$instance instanceof WC_Logger) {
static::$instance = new WC_Logger(self::$additionalHandlers);
}
return static::$instance;
}
public static function setAdditionalHandlers(array $additionalHandlers): void
{
self::$additionalHandlers = $additionalHandlers;
}
public static function setHook(string $action, $id = null): void
{
static::$currentHook = $id === null ? $action : sprintf('%s-%d', $action, (int) $id);
}
private static function getIdentifier(): string
{
if (!is_string(static::$logIdentifier)) {
static::$logIdentifier = substr(wp_generate_uuid4(), 0, 8);
}
return static::$logIdentifier;
}
private static function getStartTime(): float
{
if (!is_float(static::$startTime)) {
static::$startTime = microtime(true);
}
return static::$startTime;
}
public static function exception(string $method, Throwable $exception, string $additionalMessage = ''): void
{
self::error(
$method,
sprintf(
'%s%s - Exception in file %s on line %s',
$additionalMessage,
$exception->getMessage(),
$exception->getFile(),
$exception->getLine()
),
['trace' => $exception->getTraceAsString()],
self::EXCEPTION
);
}
public static function error(string $method, string $message, array $context = [], $type = null): void
{
self::log($method, $message, $context, $type, WC_Log_Levels::ERROR);
}
public static function info(string $method, string $message, array $context = [], $type = null): void
{
self::log($method, $message, $context, $type, WC_Log_Levels::INFO);
}
private static function log(string $method, string $message, array $context = [], $type = null, $level = 'info'): void
{
$time = self::getStartTime();
$context['time'] = round((microtime(true) - $time), 3);
$context['source'] = self::HANDLE;
$message = sprintf(
'%s [%s] <%s> %s=> %s',
self::getIdentifier(),
self::$currentHook,
$method,
$type ? $type . ' ' : '',
$message
);
self::getInstance()->log($level, $message, $context);
}
/**
* Extracts information useful for logs from an object
*
* @param $object
* @return array
*/
public static function formatWcObject($object): array
{
if ($object instanceof WC_Order) {
return self::formatWcOrder($object);
}
if ($object instanceof WC_Customer) {
return self::formatWcCustomer($object);
}
if (is_object($object)) {
return method_exists($object, 'get_data') ? (array_filter($object->get_data())) : [$object];
}
return [$object];
}
public static function formatWcOrder(WC_Order $order) {
return [
'id' => $order->get_id(),
'status' => $order->get_status(),
'date_modified' => $order->get_date_modified(),
'total' => $order->get_total(),
'shipping' => [
'first_name' => $order->get_shipping_first_name(),
'last_name' => $order->get_shipping_last_name(),
'company' => $order->get_shipping_company(),
'address_1' => $order->get_shipping_address_1(),
'address_2' => $order->get_shipping_address_2(),
'city' => $order->get_shipping_city(),
'state' => $order->get_shipping_state(),
'postcode' => $order->get_shipping_postcode(),
'country' => $order->get_shipping_country(),
'phone' => method_exists($order, 'get_shipping_phone') ? $order->get_shipping_phone() : '',
],
'billing' => [
'phone' => $order->get_billing_phone()
],
'email' => $order->get_billing_email(),
'payment_method_title' => $order->get_payment_method_title(),
'date_paid' => $order->get_date_paid(),
];
}
public static function formatWcCustomer(WC_Customer $customer)
{
return [
'id' => $customer->get_id(),
'date_modified' => $customer->get_date_modified(),
'firstName' => $customer->get_first_name(),
'lastName' => $customer->get_last_name(),
'email' => $customer->get_email(),
'display_name' => $customer->get_display_name(),
'role' => $customer->get_role(),
'username' => $customer->get_username(),
'shipping' => [
'first_name' => $customer->get_shipping_first_name(),
'last_name' => $customer->get_shipping_last_name(),
'company' => $customer->get_shipping_company(),
'address_1' => $customer->get_shipping_address_1(),
'address_2' => $customer->get_shipping_address_2(),
'city' => $customer->get_shipping_city(),
'state' => $customer->get_shipping_state(),
'postcode' => $customer->get_shipping_postcode(),
'country' => $customer->get_shipping_country(),
'phone' => method_exists($customer, 'get_shipping_phone') ? $customer->get_shipping_phone() : '',
],
'billing' => [
'phone' => $customer->get_billing_phone()
],
];
}
}
endif;

View file

@ -0,0 +1,108 @@
<?php
if (!class_exists('WC_Retailcrm_Loyalty')) :
class WC_Retailcrm_Loyalty_Form
{
public function getRegistrationForm($phone = '', $loyaltyTerms = '', $loyaltyPersonal = '')
{
$htmlLoyaltyTerms = $loyaltyTerms !== ''
? sprintf(
'<p><input type="checkbox" name="terms" id="termsLoyalty" required>%s<a id="terms-popup" class="popup-open-loyalty" href="#">%s</a>.</p>',
__(' I agree with ', 'retailcrm'),
__('loyalty program terms', 'retailcrm')
)
: ''
;
$htmlLoyaltyPersonal = $loyaltyPersonal !== ''
? sprintf(
'<p><input type="checkbox" name="privacy" id="privacyLoyalty" required>%s<a id="privacy-popup" class="popup-open-loyalty" href="#">%s</a>.</p>',
__(' I agree with ', 'retailcrm'),
__('terms of personal data processing', 'retailcrm')
)
: ''
;
return sprintf(
'
<form id="loyaltyRegisterForm" method="post">
<p>%s</p>
%s
%s
<p><input type="text" name="phone" id="phoneLoyalty" placeholder="%s" value="%s" required></p>
<p><input type="submit" value="%s"></p>
</form>
<div class="popup-fade-loyalty">
<div class="popup-loyalty">
<a class="popup-close-loyalty" href="#">%s</a>
<br>
<div id="popup-loyalty-text"></div>
</div>
</div>
',
__('To register in the loyalty program, fill in the form:', 'retailcrm'),
$htmlLoyaltyTerms,
$htmlLoyaltyPersonal,
__('Phone', 'retailcrm'),
$phone,
__('Send', 'retailcrm'),
__('Close', 'retailcrm')
);
}
public function getActivationForm()
{
return sprintf('
<form id="loyaltyActivateForm" method="post">
<p><input type="checkbox" id="loyaltyActiveCheckbox" name="loyaltyCheckbox" required> %s</p>
<input type="submit" value="%s">
</form>',
__('Activate participation in the loyalty program', 'retailcrm'),
__('Send', 'retailcrm')
);
}
public function getInfoLoyalty(array $loyaltyAccount)
{
$data = [
'<b>' . __('Bonus account', 'retailcrm') . '</b>',
__('Participation ID: ', 'retailcrm') . $loyaltyAccount['id'],
__('Current level: ', 'retailcrm') . $loyaltyAccount['level']['name'],
__('Bonuses on the account: ', 'retailcrm') . $loyaltyAccount['amount'],
__('Bonus card number: ' , 'retailcrm') . ($loyaltyAccount['cardNumber'] ?? __('The card is not linked', 'retailcrm')),
__('Date of registration: ', 'retailcrm') . $loyaltyAccount['activatedAt'],
'<br>',
'<b>' . __('Current level rules', 'retailcrm') . '</b>',
__('Required amount of purchases to move to the next level: ', 'retailcrm') . $loyaltyAccount['nextLevelSum'] . ' ' . $loyaltyAccount['loyalty']['currency']
];
switch ($loyaltyAccount['level']['type']) {
case 'bonus_converting':
$data[] = sprintf(__('Ordinary products: accrual of 1 bonus for each %s %s', 'retailcrm'), $loyaltyAccount['level']['privilegeSize'], $loyaltyAccount['loyalty']['currency']);
$data[] = sprintf(__('Promotional products: accrual of 1 bonus for each %s %s', 'retailcrm'), $loyaltyAccount['level']['privilegeSizePromo'], $loyaltyAccount['loyalty']['currency']);
break;
case 'bonus_percent':
$data[] = sprintf(__('Ordinary products: bonus accrual in the amount of %s%% of the purchase amount', 'retailcrm'), $loyaltyAccount['level']['privilegeSize']);
$data[] = sprintf(__('Promotional products: bonus accrual in the amount of %s%% of the purchase amount', 'retailcrm'), $loyaltyAccount['level']['privilegeSizePromo']);
break;
case 'discount':
$data[] = sprintf(__('Ordinary products: %s%% discount', 'retailcrm'), $loyaltyAccount['level']['privilegeSize']);
$data[] = sprintf(__('Promotional products: %s%% discount', 'retailcrm'), $loyaltyAccount['level']['privilegeSizePromo']);
break;
}
$result = '';
foreach ($data as $line) {
$result .= "<p style='line-height: 1'>$line</p>";
}
return $result;
}
}
endif;

View file

@ -0,0 +1,45 @@
<?php
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Customer_Address - Builds a billing address for a customer.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_Customer_Address extends WC_Retailcrm_Abstracts_Address
{
/**
* @param WC_Customer $customer
* @param \WC_Order|null $order
*
* @return self
*/
public function build($customer, $order = null)
{
$address = $this->getCustomerAddress($customer, $order);
if (!empty($address)) {
$customerAddress = apply_filters(
'retailcrm_process_customer_address',
WC_Retailcrm_Plugin::clearArray($address),
$customer,
$order
);
$this->setDataFields($customerAddress);
} else {
WC_Retailcrm_Logger::error(
__METHOD__,
'Error: Customer address is empty',
['wc_customer' => WC_Retailcrm_Logger::formatWcObject($customer)]
);
}
return $this;
}
}

View file

@ -0,0 +1,63 @@
<?php
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Customer_Corporate_Address - Builds a billing address for a corporate customer.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_Customer_Corporate_Address extends WC_Retailcrm_Abstracts_Address
{
/** @var bool $isMain */
protected $isMain = true;
/**
* @param bool $isMain
*
* @return WC_Retailcrm_Customer_Corporate_Address
*/
public function setIsMain($isMain)
{
$this->isMain = $isMain;
return $this;
}
/**
* @param WC_Customer $customer
* @param \WC_Order|null $order
*
* @return self
*/
public function build($customer, $order = null)
{
$address = $this->getCustomerAddress($customer, $order);
if (!empty($address)) {
$address['isMain'] = $this->isMain;
$corporateCustomerAddress = apply_filters(
'retailcrm_process_customer_corporate_address',
WC_Retailcrm_Plugin::clearArray(array_merge(
$address,
['isMain' => $this->isMain]
)),
$customer
);
$this->setDataFields($corporateCustomerAddress);
} else {
WC_Retailcrm_Logger::error(
__METHOD__,
'Error: Corporate Customer address is empty.',
['wc_customer' => WC_Retailcrm_Logger::formatWcObject($customer)]
);
}
return $this;
}
}

View file

@ -0,0 +1,251 @@
<?php
/**
* PHP version 7.0
*
* Class WC_Retailcrm_WC_Customer_Builder - It converts retailCRM customer data (array) into WC_Customer.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_WC_Customer_Builder extends WC_Retailcrm_Abstract_Builder
{
/**
* @var \WC_Customer $customer
*/
private $customer;
/**
* WC_Retailcrm_WC_Customer_Builder constructor.
*/
public function __construct()
{
$this->reset();
}
/**
* @param string $firstName
*
* @return $this
*/
public function setFirstName($firstName)
{
$this->data['firstName'] = $firstName;
return $this;
}
/**
* @param string $lastName
*
* @return $this
*/
public function setLastName($lastName)
{
$this->data['lastName'] = $lastName;
return $this;
}
/**
* @param string $email
*
* @return $this
*/
public function setEmail($email)
{
$this->data['email'] = $email;
return $this;
}
/**
* @param string $externalId
*
* @return $this
*/
public function setExternalId($externalId)
{
$this->data['externalId'] = $externalId;
return $this;
}
/**
* @param array $phones
*
* @return $this
*/
public function setPhones($phones)
{
if (self::isPhonesArrayValid($phones)) {
$this->data['phones'] = $phones;
}
return $this;
}
/**
* @param array $address
*
* @return $this
*/
public function setAddress($address)
{
if (is_array($address)) {
$this->data['address'] = $address;
}
return $this;
}
/**
* @param \WC_Customer $customer
*
* @return $this
*/
public function setWcCustomer($customer)
{
if ($customer instanceof WC_Customer) {
$this->customer = $customer;
}
return $this;
}
/**
* Sets provided externalId and loads associated customer from DB (it it exists there).
* Returns true if everything went find; returns false if customer wasn't found.
*
* @param string $externalId
*
* @return bool
* @throws \Exception
*/
public function loadExternalId($externalId)
{
try {
$wcCustomer = new WC_Customer($externalId);
} catch (\Exception $exception) {
return false;
}
$this->setExternalId($externalId);
$this->setWcCustomer($wcCustomer);
return true;
}
public function reset()
{
parent::reset();
$this->customer = new WC_Customer();
return $this;
}
/**
* Fill WC_Customer fields with customer data from RetailCRM.
* If field is not present in retailCRM customer - it will remain unchanged.
*
* @return $this|\WC_Retailcrm_Builder_Interface
*/
public function build()
{
$this->checkBuilderValidity();
WC_Retailcrm_Logger::info(
__METHOD__,
'Building WC_Customer from data',
['customer_data' => $this->data]
);
$this->customer->set_first_name($this->dataValue('firstName', $this->customer->get_first_name()));
$this->customer->set_last_name($this->dataValue('lastName', $this->customer->get_last_name()));
$this->customer->set_billing_email($this->dataValue('email', $this->customer->get_billing_email()));
$phones = $this->dataValue('phones', []);
if ((is_array($phones) || $phones instanceof Countable) && count($phones) > 0) {
$phoneData = reset($phones);
if (is_array($phoneData) && isset($phoneData['number'])) {
$this->customer->set_billing_phone($phoneData['number']);
}
} elseif (is_string($phones) || is_numeric($phones)) {
$this->customer->set_billing_phone($phones);
}
$address = $this->dataValue('address');
if (!empty($address)) {
$this->customer->set_billing_state(self::arrayValue(
$address,
'region',
$this->customer->get_billing_state()
));
$this->customer->set_billing_postcode(self::arrayValue(
$address,
'index',
$this->customer->get_billing_postcode()
));
$this->customer->set_billing_country(self::arrayValue(
$address,
'country',
$this->customer->get_billing_country()
));
$this->customer->set_billing_city(self::arrayValue(
$address,
'city',
$this->customer->get_billing_city()
));
}
return $this;
}
/**
* @return mixed|\WC_Customer|null
*/
public function getResult()
{
return $this->customer;
}
/**
* Throws an exception if internal state is not ready for data building.
*
* @throws RuntimeException
*/
private function checkBuilderValidity()
{
if (empty($this->data)) {
throw new \RuntimeException('Empty data');
}
if (!is_array($this->data)) {
throw new \RuntimeException('Data must be an array');
}
}
/**
* Returns true if provided variable contains array with customer phones.
*
* @param mixed $phones
*
* @return bool
*/
private static function isPhonesArrayValid($phones)
{
if (!is_array($phones)) {
return false;
}
foreach ($phones as $phone) {
if (!is_array($phone) || count($phone) != 1 || !array_key_exists('number', $phone)) {
return false;
}
}
return true;
}
}

214
src/include/functions.php Normal file
View file

@ -0,0 +1,214 @@
<?php
if (! defined('ABSPATH')) {
exit; // Exit if accessed directly
}
// @codeCoverageIgnoreStart
function get_wc_shipping_methods_by_zones($enhanced = false)
{
$result = [];
$shippingZones = WC_Shipping_Zones::get_zones();
$defaultZone = WC_Shipping_Zones::get_zone_by();
$shippingZones[$defaultZone->get_id()] = [
$defaultZone->get_data(),
'zone_id' => $defaultZone->get_id(),
'formatted_zone_location' => $defaultZone->get_formatted_location(),
'shipping_methods' => $defaultZone->get_shipping_methods(false)
];
if ($shippingZones) {
foreach ($shippingZones as $code => $shippingZone) {
foreach ($shippingZone['shipping_methods'] as $key => $shipping_method) {
$shipping_methods = [
'id' => $shipping_method->id,
'instance_id' => $shipping_method->instance_id,
'title' => $shipping_method->title
];
if ($enhanced) {
$shipping_code = $shipping_method->id;
} else {
$shipping_code = $shipping_method->id . ':' . $shipping_method->instance_id;
}
if (!isset($result[$shipping_code])) {
$result[$shipping_code] = [
'name' => $shipping_method->method_title,
'enabled' => $shipping_method->enabled,
'description' => $shipping_method->method_description,
'title' => $shipping_method->title
];
}
if ($enhanced) {
$result[$shipping_method->id]['shipping_methods'][$shipping_method->id . ':' . $shipping_method->instance_id] = $shipping_methods;
unset($shipping_methods);
}
}
}
}
return $result;
}
// @codeCoverageIgnoreEnd
function get_wc_shipping_methods()
{
$wc_shipping = WC_Shipping::instance();
$shipping_methods = $wc_shipping->get_shipping_methods();
$result = [];
foreach ($shipping_methods as $code => $shipping) {
$result[$code] = [
'name' => $shipping->method_title,
'enabled' => $shipping->enabled,
'description' => $shipping->method_description,
'title' => $shipping->title ? $shipping->title : $shipping->method_title
];
}
return apply_filters('retailcrm_shipping_list', WC_Retailcrm_Plugin::clearArray($result));
}
function retailcrm_get_delivery_service($method_id, $instance_id)
{
$shippings_by_zone = get_wc_shipping_methods_by_zones(true);
$method = explode(':', $method_id);
$method_id = $method[0];
$shipping = $shippings_by_zone[$method_id] ?? [];
if ($shipping && isset($shipping['shipping_methods'][$method_id . ':' . $instance_id])) {
return $shipping['shipping_methods'][$method_id . ':' . $instance_id];
}
return false;
}
/**
* @param $id
* @param $settings
*
* @return false|WC_Product|null
*/
function retailcrm_get_wc_product($id, $settings)
{
if (
isset($settings['bind_by_sku'])
&& $settings['bind_by_sku'] == WC_Retailcrm_Base::YES
) {
$id = wc_get_product_id_by_sku($id);
}
return wc_get_product($id);
}
/**
* Returns true if either wordpress debug mode or module debugging is enabled
*
* @return bool
*/
function retailcrm_is_debug()
{
$options = get_option(WC_Retailcrm_Base::$option_key);
if (isset($options['debug_mode']) === true && $options['debug_mode'] === WC_Retailcrm_Base::YES) {
return true;
}
}
/**
* Returns true if current page equals wp-login
*
* @return bool
*/
function is_wplogin()
{
$ABSPATH_MY = str_replace(['\\','/'], DIRECTORY_SEPARATOR, ABSPATH);
return (
(in_array($ABSPATH_MY . 'wp-login.php', get_included_files())
|| in_array($ABSPATH_MY . 'wp-register.php', get_included_files()))
|| (isset($_GLOBALS['pagenow']) && $GLOBALS['pagenow'] === 'wp-login.php')
|| $_SERVER['PHP_SELF'] == '/wp-login.php'
);
}
/**
* If a tax class with a standart rate is selected, woocommerce_shipping_tax_class = ''
* If a tax class with a zero rate is selected, woocommerce_shipping_tax_class = zero-rate
* If a tax class with a reduced rate is selected, woocommerce_shipping_tax_class = reduced-rate
* If the tax is calculated based on the items in the cart, woocommerce_shipping_tax_class = inherit
*
* @return mixed
*/
function getShippingRate()
{
if (!isset(WC()->cart)) {
return null;
}
$shippingRates = WC_Tax::get_shipping_tax_rates();
// Only one tax can be selected for shipping
if (is_array($shippingRates)) {
$shippingRates = array_shift($shippingRates);
}
return $shippingRates['rate'] ?? $shippingRates;
}
/**
* Get order item rate.
*
* @return mixed
*/
function getOrderItemRate($wcOrder)
{
$orderItemTax = $wcOrder->get_taxes();
if (is_array($orderItemTax)) {
$orderItemTax = array_shift($orderItemTax);
}
return $orderItemTax instanceof WC_Order_Item_Tax ? $orderItemTax->get_rate_percent() : null;
}
function calculatePriceExcludingTax($priceIncludingTax, $rate)
{
return round($priceIncludingTax / (1 + $rate / 100), wc_get_price_decimals());
}
/**
* Checking the use of HPOS.
*
* @codeCoverageIgnore
*/
function useHpos()
{
return class_exists(Automattic\WooCommerce\Utilities\OrderUtil::class)
&& Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
}
function isLoyaltyActivate($settings)
{
return isset($settings['loyalty']) && $settings['loyalty'] === WC_Retailcrm_Base::YES;
}
function isCorporateUserActivate($settings)
{
return isset($settings['corporate_enabled']) && $settings['corporate_enabled'] === WC_Retailcrm_Base::YES;
}
function isCorporateOrder($wcCustomer, $wcOrder)
{
return !empty($wcCustomer->get_billing_company()) || !empty($wcOrder->get_billing_company());
}
function getOptionByCode($optionName)
{
return get_option(WC_Retailcrm_Base::$option_key)[$optionName] ?? null;
}

View file

@ -0,0 +1,254 @@
<?php
if (!class_exists('WC_Retailcrm_Icml_Writer')) :
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Icml - Generate ICML file (catalog).
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_Icml_Writer
{
private $writer;
public function __construct($tmpFile)
{
$this->writer = new \XMLWriter();
$this->writer->openUri($tmpFile);
}
/**
* Write HEAD in ICML catalog.
*
* @param string $shop
*
* @return void
*/
public function writeHead(string $shop)
{
$this->writer->startDocument('1.0', 'UTF-8');
$this->writer->startElement('yml_catalog'); // start <yml_catalog>
$this->writer->writeAttribute('date', date('Y-m-d H:i:s'));
$this->writer->startElement('shop'); // start <shop>
$this->writer->WriteElement('name', $shop);
}
/**
* Write categories in ICML catalog.
*
* @param array $categories
*
* @return void
*/
public function writeCategories(array $categories)
{
$this->writer->startElement('categories'); // start <categories>
$this->addCategories($categories);
$this->writer->endElement(); // end </categories>
}
/**
* Add category in ICML catalog.
*
* @param array $categories
*
* @return void
*/
private function addCategories(array $categories)
{
foreach ($categories as $category) {
if (!array_key_exists('name', $category) || !array_key_exists('id', $category)) {
continue;
}
$this->writer->startElement('category'); // start <category>
$this->writer->writeAttribute('id', $category['id']);
if (array_key_exists('parentId', $category) && 0 < $category['parentId']) {
$this->writer->writeAttribute('parentId', $category['parentId']);
}
$this->writer->writeElement('name', $category['name']);
if (array_key_exists('picture', $category) && $category['picture']) {
$this->writer->writeElement('picture', $category['picture']);
}
$this->writer->endElement(); // end </category>
}
}
/**
* Write offers in ICML catalog.
*
* @return void
*/
public function writeOffers($offers)
{
$this->writer->startElement('offers'); // start <offers>
$this->addOffers($offers);
$this->writer->endElement(); // end </offers>
}
/**
* Add offer in ICML catalog.
*
* @return void
*/
private function addOffers($offers)
{
foreach ($offers as $offer) {
if (!array_key_exists('id', $offer)) {
continue;
}
$this->writer->startElement('offer'); // start <offer>
if (!array_key_exists('productId', $offer) || empty($offer['productId'])) {
$offer['productId'] = $offer['id'];
}
$this->writer->writeAttribute('id', $offer['id']);
$this->writer->writeAttribute('productId', $offer['productId']);
$this->writer->writeAttribute('quantity', (int) $offer['quantity'] ?? 0);
$this->writer->writeAttribute('type', $offer['type']);
if (isset($offer['categoryId'])) {
if (is_array($offer['categoryId'])) {
foreach ($offer['categoryId'] as $categoryId) {
$this->writer->writeElement('categoryId', $categoryId);
}
} else {
$this->writer->writeElement('categoryId', $offer['$categoryId']);
}
}
if (!empty($offer['picture'])) {
foreach ($offer['picture'] as $urlImage) {
$this->writer->writeElement('picture', $urlImage);
}
}
if (empty($offer['name'])) {
$offer['name'] = __('Untitled', 'retailcrm');
}
if (empty($offer['productName'])) {
$offer['productName'] = $offer['name'];
}
unset($offer['id'], $offer['productId'], $offer['categoryId'], $offer['quantity'], $offer['picture']);
$this->writeOffersProperties($offer);
if (!empty($offer['params'])) {
$this->writeOffersParams($offer['params']);
}
if (!empty($offer['dimensions'])) {
$this->writer->writeElement('dimensions', $offer['dimensions']);
}
if (!empty($offer['weight'])) {
$this->writer->writeElement('weight', $offer['weight']);
}
if (!empty($offer['tax'])) {
$this->writer->writeElement('vatRate', $offer['tax']);
}
$this->writer->endElement(); // end </offer>
}
}
/**
* Set offer properties.
*
* @param array $offerProperties
*
* @return void
*/
private function writeOffersProperties(array $offerProperties)
{
foreach ($offerProperties as $key => $value) {
if (!in_array($key, WC_Retailcrm_Icml::OFFER_PROPERTIES)) {
continue;
}
if (is_array($value)) {
foreach ($value as $element) {
$this->writer->writeElement($key, $element);
}
} else {
$this->writer->writeElement($key, $value);
}
}
}
/**
* Set offer params.
*
* @param array $offerParams
*
* @return void
*/
private function writeOffersParams(array $offerParams)
{
foreach ($offerParams as $param) {
if (
empty($param['code'])
|| empty($param['name'])
|| empty($param['value'])
) {
continue;
}
$this->writer->startElement('param'); // start <param>
$this->writer->writeAttribute('code', $param['code']);
$this->writer->writeAttribute('name', $param['name']);
$this->writer->text($param['value']);
$this->writer->endElement(); // end </param>
}
}
/**
* Write end tags in ICML catalog.
*
* @return void
*/
public function writeEnd()
{
$this->writer->endElement(); // end </yml_catalog>
$this->writer->endElement(); // end </shop>
$this->writer->endDocument();
}
/**
* Save ICML catalog.
*
* @return void
*/
public function formatXml($tmpfile)
{
$dom = dom_import_simplexml(simplexml_load_file($tmpfile))->ownerDocument;
$dom->formatOutput = true;
$formatted = $dom->saveXML();
unset($dom, $this->writer);
file_put_contents($tmpfile, $formatted);
}
}
endif;

View file

@ -0,0 +1,55 @@
<?php
/**
* PHP version 7.0
*
* Interface WC_Retailcrm_Builder_Interface - Main interface for builders. All builders.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
interface WC_Retailcrm_Builder_Interface {
/**
* Sets data into builder
*
* @param array|mixed $data
*
* @return self
*/
public function setData($data);
/**
* Returns data present in the builder
*
* @return array|mixed
*/
public function getData();
/**
* This method should build result with data present in the builder.
* It should return builder instance in spite of actual building result.
* Any exception can be thrown in case of error. It should be processed accordingly.
*
* @return self
* @throws \Exception
*/
public function build();
/**
* This method should reset builder state.
* In other words, after calling reset() builder inner state should become identical to newly created builder's state.
*
* @return self
*/
public function reset();
/**
* Returns builder result. Can be anything (depends on builder).
*
* @return mixed|null
*/
public function getResult();
}

View file

@ -0,0 +1,85 @@
<?php
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Customer_Switcher_Result - Holds modified order and customer which was set in the order.
* If customer is null, then only order props was updated. Previous customer (if it was registered)
* will be detached from this order.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_Customer_Switcher_Result
{
/** @var \WC_Customer|null */
private $wcCustomer;
/** @var \WC_Order $wcOrder */
private $wcOrder;
/**
* WC_Retailcrm_Customer_Switcher_Result constructor.
*
* @param \WC_Customer|null $wcCustomer
* @param \WC_Order $wcOrder
*/
public function __construct($wcCustomer, $wcOrder)
{
$this->wcCustomer = $wcCustomer;
$this->wcOrder = $wcOrder;
if (
(!is_null($this->wcCustomer) && !($this->wcCustomer instanceof WC_Customer))
|| !($this->wcOrder instanceof WC_Order)
) {
throw new \InvalidArgumentException(sprintf('Incorrect data provided to %s', __CLASS__));
}
}
/**
* @return \WC_Customer|null
*/
public function getWcCustomer()
{
return $this->wcCustomer;
}
/**
* @return \WC_Order
*/
public function getWcOrder()
{
return $this->wcOrder;
}
/**
* Save customer (if exists) and order.
*
* @return $this
*/
public function save()
{
WC_Retailcrm_Logger::info(
__METHOD__,
'Saving WC_Customer and WC_Order',
[
'wc_customer' => WC_Retailcrm_Logger::formatWcObject($this->wcCustomer),
'wc_order' => WC_Retailcrm_Logger::formatWcObject($this->wcOrder),
]
);
if (!empty($this->wcCustomer) && $this->wcCustomer->get_id()) {
$this->wcCustomer->save();
}
if (!empty($this->wcOrder) && $this->wcOrder->get_id()) {
$this->wcOrder->save();
}
return $this;
}
}

View file

@ -0,0 +1,187 @@
<?php
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Customer_Switcher_State - Holds WC_Retailcrm_Customer_Switcher state.
* It exists only because we need to comply with builder interface.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_Customer_Switcher_State
{
/** @var \WC_Order $wcOrder */
private $wcOrder;
/** @var array */
private $newCustomer;
/** @var array */
private $newContact;
/** @var string $newCompanyName */
private $newCompanyName;
/** @var array $companyAddress */
private $companyAddress;
/**
* @return \WC_Order
*/
public function getWcOrder()
{
return $this->wcOrder;
}
/**
* @param \WC_Order $wcOrder
*
* @return WC_Retailcrm_Customer_Switcher_State
*/
public function setWcOrder($wcOrder)
{
$this->wcOrder = $wcOrder;
return $this;
}
/**
* @return array
*/
public function getNewCustomer()
{
return $this->newCustomer;
}
/**
* @param array $newCustomer
*
* @return WC_Retailcrm_Customer_Switcher_State
*/
public function setNewCustomer($newCustomer)
{
$this->newCustomer = $newCustomer;
return $this;
}
/**
* @return array
*/
public function getNewContact()
{
return $this->newContact;
}
/**
* @param array $newContact
*
* @return WC_Retailcrm_Customer_Switcher_State
*/
public function setNewContact($newContact)
{
$this->newContact = $newContact;
return $this;
}
/**
* @return string
*/
public function getNewCompanyName()
{
return $this->newCompanyName;
}
/**
* @param string $newCompanyName
*
* @return WC_Retailcrm_Customer_Switcher_State
*/
public function setNewCompanyName($newCompanyName)
{
$this->newCompanyName = $newCompanyName;
return $this;
}
/**
* @return array
*/
public function getCompanyAddress()
{
return $this->companyAddress;
}
/**
* @param array $companyAddress
*
* @return WC_Retailcrm_Customer_Switcher_State
*/
public function setCompanyAddress($companyAddress)
{
$this->companyAddress = $companyAddress;
return $this;
}
/**
* @param array $newCompany
*
* @return WC_Retailcrm_Customer_Switcher_State
*/
public function setNewCompany($newCompany)
{
if (isset($newCompany['name'])) {
$this->setNewCompanyName($newCompany['name']);
}
if (isset($newCompany['address']) && !empty($newCompany['address'])) {
$this->setCompanyAddress($newCompany['address']);
}
return $this;
}
/**
* Returns true if current state may be processable (e.g. when customer or related data was changed).
* It doesn't guarantee state validity.
*
* @return bool
*/
public function feasible()
{
return !(empty($this->newCustomer) && empty($this->newContact) && empty($this->newCompanyName));
}
/**
* Throws an exception if state is not valid
*
* @throws \InvalidArgumentException
* @return void
*/
public function validate()
{
if (empty($this->wcOrder)) {
throw new \InvalidArgumentException('Empty WC_Order.');
}
if (empty($this->newCustomer) && empty($this->newContact) && empty($this->newCompanyName)) {
throw new \InvalidArgumentException('New customer, new contact and new company is empty.');
}
if (!empty($this->newCustomer) && !empty($this->newContact)) {
WC_Retailcrm_Logger::info(
__METHOD__,
'State data - customer and contact',
[
'customer' => $this->getNewCustomer(),
'contact' => $this->getNewContact(),
]
);
throw new \InvalidArgumentException(
'Too much data in state - cannot determine which customer should be used.'
);
}
}
}

View file

@ -0,0 +1,39 @@
<?php
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Order_Address - Build address for CRM order.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_Order_Address extends WC_Retailcrm_Abstracts_Address
{
/**
* @param WC_Order $order
*
* @return self
*/
public function build($order)
{
$address = $this->getOrderAddress($order);
if (!empty($address)) {
$orderAddress = apply_filters(
'retailcrm_process_order_address',
WC_Retailcrm_Plugin::clearArray($address),
$order
);
$this->setDataFields($orderAddress);
} else {
WC_Retailcrm_Logger::error(__METHOD__, 'Error: Order address is empty');
}
return $this;
}
}

View file

@ -0,0 +1,238 @@
<?php
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Order_Item - Build items for CRM order.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_Order_Item extends WC_Retailcrm_Abstracts_Data
{
/**
* @var array order item
*/
protected $data = [
'offer' => [],
'productName' => '',
'initialPrice' => 0.00,
'quantity' => 0.00
];
/**
* @var array
*/
protected $settings = [];
/** @var bool */
public $cancelLoyalty = false;
/**
* WC_Retailcrm_Order_Item constructor.
*
* @param array $settings
*/
public function __construct($settings)
{
$this->settings = $settings;
}
/**
* @param WC_Order_Item_Product $item
*
* @return self
*/
public function build($item, $crmItem = null)
{
$decimalPlaces = wc_get_price_decimals();
// Calculate price and discount
$price = $this->calculatePrice($item, $decimalPlaces);
$discountPrice = $this->calculateDiscount($item, $price, $decimalPlaces, $crmItem);
$data['productName'] = $item['name'];
$data['initialPrice'] = $price;
$data['quantity'] = (double)$item['qty'];
$itemId = ($item['variation_id'] > 0) ? $item['variation_id'] : $item['product_id'];
$data['externalIds'] = [
[
'code' => 'woocomerce',
'value' => $itemId . '_' . $item->get_id(),
]
];
$this->setDataFields($data);
$this->setOffer($item);
$this->setField('discountManualAmount', $discountPrice);
return $this;
}
/**
* @param WC_Order_Item_Product $item
*
* @return void
*/
private function setOffer(WC_Order_Item_Product $item)
{
$uid = $item['variation_id'] > 0 ? $item['variation_id'] : $item['product_id'] ;
$offer = ['externalId' => $uid];
$product = $item->get_product();
if (
!empty($product)
&& isset($this->settings['bind_by_sku'])
&& $this->settings['bind_by_sku'] == WC_Retailcrm_Base::YES
) {
$offer['xmlId'] = $product->get_sku();
unset($offer['externalId']);
}
$this->setField('offer', $offer);
}
/**
* @param WC_Order_Item_Product $item
* @param int $decimalPlaces Price rounding from WC settings
*
* @return float
*/
private function calculatePrice(WC_Order_Item_Product $item, int $decimalPlaces)
{
if (isset($this->settings['loyalty']) && $this->settings['loyalty'] === WC_Retailcrm_Base::YES) {
$product = $item->get_product();
$price = wc_get_price_including_tax($product, ["price" => $product->get_regular_price()]);
} else {
$price = ($item['line_subtotal'] / $item->get_quantity()) + ($item['line_subtotal_tax'] / $item->get_quantity());
}
return round($price, $decimalPlaces);
}
/**
* @param WC_Order_Item_Product $item
* @param $price
* @param int $decimalPlaces Price rounding from WC settings
* @param array|null $crmItem Current trade position in CRM
* @return float|int
*/
private function calculateDiscount(
WC_Order_Item_Product $item,
$price,
int $decimalPlaces,
$crmItem = null
) {
if ($crmItem && isset($this->settings['loyalty']) && $this->settings['loyalty'] === WC_Retailcrm_Base::YES) {
$loyaltyDiscount = 0;
foreach ($crmItem['discounts'] as $discount) {
if (in_array($discount['type'], ['bonus_charge', 'loyalty_level'])) {
$loyaltyDiscount += $discount['amount'];
break;
}
}
/**
* The loyalty program discount is calculated within the CRM system. It must be deleted during transfer to avoid duplication.
*/
$productPrice = ($item->get_total() / $item->get_quantity()) + ($loyaltyDiscount / $crmItem['quantity']);
if ($this->cancelLoyalty) {
if ($item->get_total() + $loyaltyDiscount <= $item->get_subtotal()) {
$item->set_total($item->get_total() + $loyaltyDiscount);
$item->calculate_taxes();
$item->save();
}
$productPrice = $item->get_total() / $item->get_quantity();
}
} else {
$productPrice = $item->get_total() ? $item->get_total() / $item->get_quantity() : 0;
}
$productTax = $item->get_total_tax() ? $item->get_total_tax() / $item->get_quantity() : 0;
$itemPrice = $productPrice + $productTax;
return round($price - $itemPrice, $decimalPlaces);
}
/**
* Reset item data.
*/
public function resetData($cancelLoyalty)
{
$this->data = [
'offer' => [],
'productName' => '',
'initialPrice' => 0.00,
'quantity' => 0.00
];
$this->cancelLoyalty = $cancelLoyalty;
}
/**
* Checking whether the loyalty program discount needs to be canceled. (Changing the sales items in the order)
*
* @param array $wcItems
* @param array $crmItems
*
* @return bool
*/
public function isCancelLoyalty($wcItems, $crmItems): bool
{
/** If the number of sales items does not match */
if (count($wcItems) !== count($crmItems)) {
$this->cancelLoyalty = true;
return true;
}
foreach ($wcItems as $id => $item) {
$loyaltyDiscount = 0;
/** If a trading position has been added/deleted */
if (!isset($crmItems[$id])) {
$this->cancelLoyalty = true;
return true;
}
/** If the quantity of goods in a trade item does not match */
if ($item->get_quantity() !== $crmItems[$id]['quantity']) {
$this->cancelLoyalty = true;
return true;
}
foreach ($crmItems[$id]['discounts'] as $discount) {
if (in_array($discount['type'], ['bonus_charge', 'loyalty_level'])) {
$loyaltyDiscount = $discount['amount'];
break;
}
}
/**
*If the sum of the trade item including discounts and loyalty program discount exceeds the cost without discounts.
* (Occurs when recalculating an order, deleting/adding coupons)
*/
if (($item->get_total() + $loyaltyDiscount) > $item->get_subtotal()) {
$this->cancelLoyalty = true;
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,134 @@
<?php
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Order_Payment - Build payments for CRM order.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_Order_Payment extends WC_Retailcrm_Abstracts_Data
{
/** @var array */
protected $data = [
'type' => '',
'order' => [],
'externalId' => '',
];
/** @var bool */
public $isNew = true;
/**
* @var array
*/
protected $settings = [];
/**
* WC_Retailcrm_Order_Item constructor.
*
* @param array $settings
*/
public function __construct($settings)
{
$this->settings = $settings;
}
/**
* @param WC_Order $order
* @param mixed $externalId
*
* @return self
*/
public function build($order, $externalId = false)
{
$this->resetData();
$paymentData = [];
if (!$this->isNew) {
$paymentData['externalId'] = $externalId;
} else {
$paymentData['externalId'] = uniqid($order->get_id() . '-');
}
$paymentData['order'] = [
'externalId' => $order->get_id()
];
if ($order->is_paid()) {
if ($order->get_status() != 'completed' && $order->get_payment_method() == 'cod') {
WC_Retailcrm_Logger::info(
__METHOD__,
sprintf('Payment for order: %s. ' .
'Payment status cannot be changed as it is cash (or other payment method) on delivery. ' .
'The status will be changed when the order is in status completed.',
$order->get_id()
)
);
} else {
$paymentData['status'] = 'paid';
if (isset($this->settings[$order->get_payment_method()])) {
$paymentData['type'] = $this->settings[$order->get_payment_method()];
}
$paidAt = $order->get_date_paid();
if (!empty($paidAt)) {
$paymentData['paidAt'] = $paidAt->date('Y-m-d H:i:s');
}
}
}
if ($this->isNew) {
if (isset($this->settings[$order->get_payment_method()])) {
$paymentData['type'] = $this->settings[$order->get_payment_method()];
} else {
$paymentData = [];
}
}
$paymentData = apply_filters(
'retailcrm_process_payment',
WC_Retailcrm_Plugin::clearArray($paymentData),
$order
);
$this->setDataFields($paymentData);
return $this;
}
/**
* Returns false if payment doesn't have method
*
* @return array
*/
public function getData()
{
$data = parent::getData();
if (empty($data['type'])) {
return [];
}
// Need to clear the array from empty values
return array_filter($data);
}
public function resetData()
{
$this->data = [
'externalId' => '',
'type' => '',
'status' => '',
'paidAt' => '',
'order' => []
];
}
}

View file

@ -0,0 +1,103 @@
<?php
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Order - Build main data for CRM order.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
class WC_Retailcrm_Order extends WC_Retailcrm_Abstracts_Data
{
/** @var bool */
public $is_new = true;
protected $data = [
'externalId' => 0,
'status' => '',
'number' => '',
'createdAt' => '',
'firstName' => '',
'lastName' => '',
'email' => '',
'paymentType' => '',
'customerComment' => '',
'paymentStatus' => '',
'phone' => '',
'countryIso' => ''
];
/**
* @var array
*/
protected $settings = [];
/**
* WC_Retailcrm_Order constructor.
*
* @param array $settings
*/
public function __construct($settings)
{
$this->settings = $settings;
}
/**
* @param WC_Order $order
*
* @return self
*/
public function build($order)
{
$firstName = $order->get_shipping_first_name();
$lastName = $order->get_shipping_last_name();
if (empty($firstName) && empty($lastName)) {
$firstName = $order->get_billing_first_name();
$lastName = $order->get_billing_last_name();
}
$dateCreate = $order->get_date_created();
$data = [
'externalId' => $order->get_id(),
'createdAt' => !empty($dateCreate) ? $dateCreate->date('Y-m-d H:i:s') : date('Y-m-d H:i:s'),
'firstName' => $firstName,
'lastName' => $lastName,
'email' => strtolower($order->get_billing_email()),
'customerComment' => $order->get_customer_note(),
'phone' => $order->get_billing_phone(),
'countryIso' => $order->get_shipping_country()
];
if ($data['countryIso'] == '--' || empty($data['countryIso'])) {
$countries = new WC_Countries();
$data['countryIso'] = $countries->get_base_country();
}
$this->setDataFields($data);
$this->setNumber($order);
if (isset($this->settings[$order->get_status()]) && 'not-upload' !== $this->settings[$order->get_status()]) {
$this->setField('status', $this->settings[$order->get_status()]);
}
return $this;
}
/**
* @param WC_Order $order
*/
protected function setNumber($order)
{
if (isset($this->settings['update_number']) && $this->settings['update_number'] == WC_Retailcrm_Base::YES) {
$this->setField('number', $order->get_order_number());
} else {
unset($this->data['number']);
}
}
}

Some files were not shown because too many files have changed in this diff Show more