Jamil-Abdullayev b8246597d4 Initial commit: orgsteklo WordPress theme
Custom WooCommerce theme for orgsteklo.ru including:
- Product catalog with category/subcategory hierarchy
- Custom checkout with delivery calculation
- Price calculator
- Admin settings panel
- Search functionality
- User account pages
2026-03-05 00:48:06 +04:00

2408 lines
96 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
//files css & js
add_action('wp_enqueue_scripts', 'true_styles_and_scripts_frontend');
function true_styles_and_scripts_frontend() {
wp_enqueue_style('swiper_css', 'https://cdn.jsdelivr.net/npm/swiper@8/swiper-bundle.min.css');
wp_enqueue_style('bootstrap_css', 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css', array('swiper_css'));
wp_enqueue_style('fancybox_css', get_stylesheet_directory_uri() . '/assets/css/fancybox.css', array('bootstrap_css'));
wp_enqueue_style('style_css', get_stylesheet_directory_uri() . '/assets/css/style.css', array('fancybox_css'));
wp_enqueue_script('jquery_js', 'https://code.jquery.com/jquery-3.7.1.js', null, true);
wp_enqueue_script('cookie_js', 'https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js', array('jquery_js'), null, true);
wp_enqueue_script('swiper_js', 'https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js', array('cookie_js'), null, true);
wp_enqueue_script('inputmask_js', 'https://cdnjs.cloudflare.com/ajax/libs/jquery.inputmask/5.0.6/jquery.inputmask.min.js', array('swiper_js'), null, true);
wp_enqueue_script('bootstrap_js', 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js', array('inputmask_js'), null, true);
wp_enqueue_script('fancybox_js', get_stylesheet_directory_uri() . '/assets/js/fancybox.min.js', array('bootstrap_js'), '1.0.0', true);
wp_enqueue_script('main_js', get_stylesheet_directory_uri() . '/assets/js/main.js', array('fancybox_js'), '1.4.0', true);
// Передаём данные пользователя для автозаполнения на checkout
if ( function_exists('is_checkout') && is_checkout() && is_user_logged_in() ) {
$uid = get_current_user_id();
$u = wp_get_current_user();
wp_localize_script( 'main_js', 'orgstekloCheckoutUser', array(
'isLoggedIn' => true,
'fio' => $u->display_name,
'phone' => get_user_meta( $uid, 'billing_phone', true ),
'email' => $u->user_email,
'company_name' => get_user_meta( $uid, 'company_name', true ),
'inn' => get_user_meta( $uid, 'inn', true ),
'kpp' => get_user_meta( $uid, 'kpp', true ),
'legal_address' => get_user_meta( $uid, 'legal_address', true ),
'actual_address' => get_user_meta( $uid, 'actual_address', true ),
'account_type' => get_user_meta( $uid, 'account_type', true ),
));
} else {
wp_localize_script( 'main_js', 'orgstekloCheckoutUser', array(
'isLoggedIn' => false,
));
}
}
function custom_theme_setup() {
add_theme_support('post-thumbnails');
}
add_action('after_setup_theme', 'custom_theme_setup');
//woocommerce
add_action( 'after_setup_theme', 'woocommerce_support' );
function woocommerce_support() {
add_theme_support( 'woocommerce' );
}
## Определение шаблона для страницы Магазина shop.php
add_filter( 'woocommerce_template_loader_files','qfurs_add_shop_template_file', 10, 1 );
function qfurs_add_shop_template_file($default_file){
if( is_shop()){
$default_file[] = WC()->template_path() .'shop.php';
}
return $default_file;
}
function custom_product_category_template($template) {
if ( is_product_category() || ( is_search() && isset( $_GET['post_type'] ) && $_GET['post_type'] === 'product' ) ) {
$new_template = locate_template(array('woocommerce/shop.php'));
if (!empty($new_template)) {
return $new_template;
}
}
return $template;
}
add_filter('template_include', 'custom_product_category_template', 99);
//login
add_action('wp_ajax_nopriv_login_by_phone', 'handle_login_by_phone');
function handle_login_by_phone() {
$phone = isset($_POST['phone']) ? sanitize_text_field($_POST['phone']) : '';
$password = isset($_POST['password']) ? $_POST['password'] : '';
if (!$phone || !$password) {
wp_send_json_error('Поля не заполнены');
}
// Найти пользователя по мета-данным billing_phone
$users = get_users([
'meta_key' => 'billing_phone',
'meta_value' => $phone,
'number' => 1,
'fields' => ['ID']
]);
if (empty($users)) {
wp_send_json_error('Пользователь не найден');
}
$user_id = $users[0]->ID;
$user = get_user_by('id', $user_id);
if (wp_check_password($password, $user->user_pass, $user_id)) {
wp_set_current_user($user_id);
wp_set_auth_cookie($user_id, true);
wp_send_json_success();
} else {
wp_send_json_error('Неверный пароль');
}
}
//recovey
function custom_password_reset_handler() {
$email = sanitize_email($_POST['user_email']);
if (!email_exists($email)) {
wp_send_json_error(['message' => 'User not found.']);
}
$user = get_user_by('email', $email);
$new_password = wp_generate_password(12, true);
wp_set_password($new_password, $user->ID);
$subject = 'Ваш новый пароль';
$message = "Здравствуйте!\n\nВаш новый пароль: $new_password\n\nВы можете войти здесь: " . wp_login_url();
$sent = wp_mail($email, $subject, $message);
if ($sent) {
wp_send_json_success();
} else {
wp_send_json_error(['message' => 'Не удалось отправить письмо.']);
}
}
add_action('wp_ajax_custom_password_reset', 'custom_password_reset_handler');
add_action('wp_ajax_nopriv_custom_password_reset', 'custom_password_reset_handler');
//search
function custom_search_by_attribute( $search, $wp_query ) {
global $wpdb;
if ( ! empty( $wp_query->query_vars['s'] ) ) {
$attribute_search = $wpdb->esc_like( $wp_query->query_vars['s'] );
$attribute_search = '%' . $attribute_search . '%';
$args = array(
'posts_per_page' => -1,
'post_type' => 'product',
'meta_query' => array(
'relation' => 'OR',
array(
'key' => '_sku',
'value' => $attribute_search,
'compare' => 'LIKE'
),
array(
'key' => '_product_attributes',
'value' => $attribute_search,
'compare' => 'LIKE'
),
),
);
$products = get_posts( $args );
if ( $products ) {
$product_ids = wp_list_pluck( $products, 'ID' );
$search .= " OR {$wpdb->posts}.ID IN (" . implode( ',', $product_ids ) . ")";
}
}
return $search;
}
add_filter( 'posts_search', 'custom_search_by_attribute', 10, 2 );
// Переопределение текста статусов наличия
add_action('admin_head', 'hide_default_stock_status');
function hide_default_stock_status() {
$screen = get_current_screen();
if ($screen->post_type === 'product') {
echo '<style>
.stock_status_field { display: none !important; }
</style>';
}
}
add_action('woocommerce_product_options_stock_status', 'custom_stock_status_dropdown');
function custom_stock_status_dropdown() {
woocommerce_wp_select([
'id' => '_custom_stock_status',
'label' => 'Статус наличия (ручной)',
'options' => [
'in_stock' => 'В наличии',
'low_stock' => 'Осталось мало',
'on_order' => 'Под заказ',
]
]);
}
add_action('woocommerce_process_product_meta', 'save_custom_stock_status');
function save_custom_stock_status($post_id) {
if (isset($_POST['_custom_stock_status'])) {
update_post_meta($post_id, '_custom_stock_status', sanitize_text_field($_POST['_custom_stock_status']));
}
}
// ========================================
// ГАЛОЧКИ "ПОПУЛЯРНЫЙ" И "РЕКОМЕНДУЕМЫЙ"
// ========================================
/**
* Добавляем галочки в редактирование товара (вкладка "Основные")
*/
add_action( 'woocommerce_product_options_general_product_data', 'orgsteklo_add_popular_recommended_fields' );
function orgsteklo_add_popular_recommended_fields() {
global $post;
echo '<div class="options_group">';
// Галочка "Популярный товар"
woocommerce_wp_checkbox( array(
'id' => '_is_popular_product',
'label' => 'Популярный товар',
'description' => 'Добавляет метку "Популярный" на карточку товара',
) );
// Галочка "Рекомендуемый товар"
woocommerce_wp_checkbox( array(
'id' => '_is_recommended_product',
'label' => 'Рекомендуемый товар',
'description' => 'Товар будет показываться в блоке "Рекомендуемые товары" у других товаров этой категории',
) );
echo '</div>';
// Характеристики для превью карточки товара
echo '<div class="options_group">';
woocommerce_wp_text_input( array(
'id' => '_preview_char_1',
'label' => 'Характеристика 1',
'description' => 'Первая строка характеристики в карточке товара (каталог, рекомендации)',
'desc_tip' => true,
'placeholder' => 'Например: Коэффициент пропускания света 92%',
) );
woocommerce_wp_text_input( array(
'id' => '_preview_char_2',
'label' => 'Характеристика 2',
'description' => 'Вторая строка характеристики в карточке товара (каталог, рекомендации)',
'desc_tip' => true,
'placeholder' => 'Например: Устойчив к УФ-излучению',
) );
echo '</div>';
}
/**
* Сохраняем галочки и управляем тегом "Популярный"
*/
add_action( 'woocommerce_process_product_meta', 'orgsteklo_save_popular_recommended_fields' );
function orgsteklo_save_popular_recommended_fields( $post_id ) {
// Сохраняем "Популярный"
$is_popular = isset( $_POST['_is_popular_product'] ) ? 'yes' : 'no';
update_post_meta( $post_id, '_is_popular_product', $is_popular );
// Управляем тегом "Популярный"
$popular_tag = get_term_by( 'name', 'Популярный', 'product_tag' );
if ( $is_popular === 'yes' ) {
// Добавляем тег если его нет
if ( ! $popular_tag ) {
// Создаём тег если не существует
$new_tag = wp_insert_term( 'Популярный', 'product_tag' );
if ( ! is_wp_error( $new_tag ) ) {
wp_set_object_terms( $post_id, $new_tag['term_id'], 'product_tag', true );
}
} else {
wp_set_object_terms( $post_id, $popular_tag->term_id, 'product_tag', true );
}
} else {
// Удаляем тег
if ( $popular_tag ) {
wp_remove_object_terms( $post_id, $popular_tag->term_id, 'product_tag' );
}
}
// Сохраняем "Рекомендуемый"
$is_recommended = isset( $_POST['_is_recommended_product'] ) ? 'yes' : 'no';
update_post_meta( $post_id, '_is_recommended_product', $is_recommended );
// Сохраняем характеристики превью
if ( isset( $_POST['_preview_char_1'] ) ) {
update_post_meta( $post_id, '_preview_char_1', sanitize_text_field( $_POST['_preview_char_1'] ) );
}
if ( isset( $_POST['_preview_char_2'] ) ) {
update_post_meta( $post_id, '_preview_char_2', sanitize_text_field( $_POST['_preview_char_2'] ) );
}
}
add_filter('woocommerce_get_availability_text', 'custom_availability_text', 10, 2);
function custom_availability_text($text, $product) {
$status = get_post_meta($product->get_id(), '_custom_stock_status', true);
switch ($status) {
case 'in_stock':
return 'В наличии';
case 'low_stock':
return 'Осталось мало';
case 'on_order':
return 'Под заказ';
default:
return $text;
}
}
add_action('wp_ajax_send_sms_code', 'send_sms_code');
add_action('wp_ajax_nopriv_send_sms_code', 'send_sms_code');
function send_sms_code() {
$phone = sanitize_text_field($_POST['phone']);
if (empty($phone)) {
wp_send_json_error(['message' => 'Телефон не указан']);
}
$code = rand(1000, 9999);
set_transient('sms_verification_' . $phone, $code, 5 * MINUTE_IN_SECONDS);
$api_id = '2DA8FFB5-365F-4A68-B369-14E40F5E81C4';
$msg = "Код подтверждения: $code";
$response = wp_remote_post('https://sms.ru/sms/send', [
'body' => [
'api_id' => $api_id,
'to' => $phone,
'msg' => $msg,
'json' => 1
]
]);
if (is_wp_error($response)) {
wp_send_json_error(['message' => 'Ошибка отправки SMS']);
}
// ⚠️ в проде не стоит возвращать код, здесь — только для тестов
wp_send_json_success(['message' => 'Код отправлен', 'code' => $code]);
}
add_action('wp_ajax_check_email_exists', 'check_email_exists');
add_action('wp_ajax_nopriv_check_email_exists', 'check_email_exists');
function check_email_exists() {
$email = sanitize_email($_POST['email']);
if (!is_email($email)) {
wp_send_json_error(['message' => 'Неверный формат email']);
}
if (email_exists($email)) {
wp_send_json_error(['message' => 'Email уже зарегистрирован']);
}
wp_send_json_success(['message' => 'Email доступен']);
}
add_action('wp_ajax_custom_register_user', 'custom_register_user');
add_action('wp_ajax_nopriv_custom_register_user', 'custom_register_user');
function custom_register_user() {
$email = sanitize_email($_POST['email']);
$phone = sanitize_text_field($_POST['phone']);
$password = $_POST['password'];
$type = sanitize_text_field($_POST['type']);
if (email_exists($email)) {
wp_send_json_error(['message' => 'Email уже существует']);
}
$code = rand(1000, 9999);
set_transient('sms_verification_' . $phone, $code, 5 * MINUTE_IN_SECONDS);
// Отправка SMS через SMS.ru
$api_id = '2DA8FFB5-365F-4A68-B369-14E40F5E81C4';
$msg = "Код подтверждения: $code";
$response = wp_remote_post('https://sms.ru/sms/send', [
'body' => [
'api_id' => $api_id,
'to' => $phone,
'msg' => $msg,
'json' => 1
]
]);
wp_send_json_success(['code' => $code]);
}
add_action('wp_ajax_register_user', 'register_user');
add_action('wp_ajax_nopriv_register_user', 'register_user');
function register_user() {
$email = sanitize_email($_POST['email']);
$phone = sanitize_text_field($_POST['phone']);
$password = $_POST['password'];
$type = sanitize_text_field($_POST['registration_type']);
$entered_code = sanitize_text_field($_POST['code']);
$saved_code = get_transient('sms_verification_' . $phone);
if (!$saved_code || $saved_code != $entered_code) {
wp_send_json_error(['message' => 'Неверный или просроченный код']);
}
if (email_exists($email)) {
wp_send_json_error(['message' => 'Email уже зарегистрирован']);
}
$user_id = wp_create_user($email, $password, $email);
if (is_wp_error($user_id)) {
wp_send_json_error(['message' => 'Ошибка регистрации']);
}
$user = new WP_User($user_id);
$user->set_role('customer');
// Сохраняем телефон в billing_phone (WooCommerce)
update_user_meta($user_id, 'billing_phone', $phone);
// Сохраняем тип аккаунта
update_user_meta($user_id, 'account_type', $type);
delete_transient('sms_verification_' . $phone);
wp_set_current_user($user_id);
wp_set_auth_cookie($user_id);
wp_send_json_success(['message' => 'Пользователь зарегистрирован']);
}
add_action('wp_ajax_resend_sms_code', 'resend_sms_code');
add_action('wp_ajax_nopriv_resend_sms_code', 'resend_sms_code');
function resend_sms_code() {
$phone = sanitize_text_field($_POST['phone']);
$code = rand(1000, 9999);
set_transient('sms_verification_' . $phone, $code, 5 * MINUTE_IN_SECONDS);
$api_id = '2DA8FFB5-365F-4A68-B369-14E40F5E81C4';
$msg = "Код подтверждения: $code";
$response = wp_remote_post('https://sms.ru/sms/send', [
'body' => [
'api_id' => $api_id,
'to' => $phone,
'msg' => $msg,
'json' => 1
]
]);
wp_send_json_success(['code' => $code]);
}
add_action('show_user_profile', 'add_account_type_field_admin');
add_action('edit_user_profile', 'add_account_type_field_admin');
function add_account_type_field_admin($user) {
$account_type = get_user_meta($user->ID, 'account_type', true);
?>
<h3>Дополнительная информация</h3>
<table class="form-table">
<tr>
<th><label for="account_type">Тип аккаунта</label></th>
<td>
<select name="account_type" id="account_type">
<option value="Физическое лицо" <?php selected($account_type, 'Физическое лицо'); ?>>Физическое лицо</option>
<option value="Юридическое лицо или ИП" <?php selected($account_type, 'Юридическое лицо или ИП'); ?>>Юридическое лицо или ИП</option>
</select>
</td>
</tr>
</table>
<?php
}
add_action('personal_options_update', 'save_account_type_admin');
add_action('edit_user_profile_update', 'save_account_type_admin');
function save_account_type_admin($user_id) {
if (current_user_can('edit_user', $user_id)) {
update_user_meta($user_id, 'account_type', sanitize_text_field($_POST['account_type']));
}
}
add_action('wp_ajax_delete_my_account', 'delete_my_account');
function delete_my_account() {
if (is_user_logged_in()) {
require_once ABSPATH . 'wp-admin/includes/user.php';
wp_delete_user(get_current_user_id());
}
wp_die();
}
// Сохранение данных пользователя
add_action('wp_ajax_save_user_profile_data', function () {
$user_id = get_current_user_id();
if (!$user_id) {
wp_send_json_error(['message' => 'Не авторизован']);
}
$name = sanitize_text_field($_POST['name']);
$phone = sanitize_text_field($_POST['phone']);
$email = sanitize_email($_POST['email']);
// Обновим имя
wp_update_user([
'ID' => $user_id,
'display_name' => $name
]);
// Обновим номер телефона в billing_phone (WooCommerce)
update_user_meta($user_id, 'billing_phone', $phone);
wp_send_json_success();
});
add_action('wp_ajax_save_user_profile_data2', function () {
$user_id = get_current_user_id();
if (!$user_id) {
wp_send_json_error(['message' => 'Не авторизован']);
}
// Получаем данные
$name = sanitize_text_field($_POST['name']);
$phone = sanitize_text_field($_POST['phone']);
$email = sanitize_email($_POST['email']);
$company_name = sanitize_text_field($_POST['company_name']);
$inn = sanitize_text_field($_POST['inn']);
$kpp = sanitize_text_field($_POST['kpp']);
$legal_address = sanitize_text_field($_POST['legal_address']);
$actual_address = sanitize_text_field($_POST['actual_address']);
// Обновляем пользователя
$user_update = wp_update_user([
'ID' => $user_id,
'display_name' => $name,
'user_email' => $email,
]);
if (is_wp_error($user_update)) {
wp_send_json_error(['message' => $user_update->get_error_message()]);
}
update_user_meta($user_id, 'billing_phone', $phone);
update_user_meta($user_id, 'company_name', $company_name);
update_user_meta($user_id, 'inn', $inn);
update_user_meta($user_id, 'kpp', $kpp);
update_user_meta($user_id, 'legal_address', $legal_address);
update_user_meta($user_id, 'actual_address', $actual_address);
wp_send_json_success(['message' => 'Данные успешно сохранены']);
});
add_action('wp_ajax_send_email_verification_link', function () {
$user_id = get_current_user_id();
if (!$user_id) {
wp_send_json_error(['message' => 'Вы не авторизованы']);
}
$user = wp_get_current_user();
$email = $user->user_email;
$token = bin2hex(random_bytes(16));
update_user_meta($user_id, 'email_verification_token', $token);
$link = add_query_arg([
'verify_email' => 1,
'user_id' => $user_id,
'token' => $token
], site_url());
wp_mail($email, 'Подтверждение Email', "Подтвердите email, перейдя по ссылке: $link");
wp_send_json_success(['message' => 'Письмо отправлено']);
});
add_action('init', function () {
if (
isset($_GET['verify_email']) &&
$_GET['verify_email'] == 1 &&
isset($_GET['user_id']) &&
isset($_GET['token'])
) {
$user_id = intval($_GET['user_id']);
$token = sanitize_text_field($_GET['token']);
$saved_token = get_user_meta($user_id, 'email_verification_token', true);
if ($token === $saved_token) {
update_user_meta($user_id, 'email_verified', 1);
delete_user_meta($user_id, 'email_verification_token');
wp_redirect(home_url('/?email_verified=1'));
exit;
} else {
wp_die('Неверная или просроченная ссылка.');
}
}
});
add_filter('manage_users_columns', function ($columns) {
$columns['email_verified'] = 'Email подтвержден';
return $columns;
});
add_filter('manage_users_custom_column', function ($value, $column_name, $user_id) {
if ($column_name === 'email_verified') {
return get_user_meta($user_id, 'email_verified', true) === '1' ? '✅ Да' : '❌ Нет';
}
return $value;
}, 10, 3);
add_action('wp_ajax_check_user_password', 'check_user_password_callback');
function check_user_password_callback() {
if (!is_user_logged_in()) {
wp_send_json_error();
}
$user = wp_get_current_user();
$password = $_POST['password'];
if (wp_check_password($password, $user->data->user_pass, $user->ID)) {
wp_send_json_success();
} else {
wp_send_json_error();
}
}
add_action('wp_ajax_change_user_password', 'change_user_password_callback');
function change_user_password_callback() {
if (!is_user_logged_in()) {
wp_send_json_error(['message' => 'Вы не авторизованы']);
}
$user = wp_get_current_user();
$old_password = $_POST['old_password'];
$new_password = $_POST['new_password'];
if (!wp_check_password($old_password, $user->data->user_pass, $user->ID)) {
wp_send_json_error(['message' => 'Старый пароль неверен']);
}
wp_set_password($new_password, $user->ID);
wp_send_json_success();
}
function find_variation_id_by_attribute($product_id, $attribute_name, $attribute_value) {
$product = wc_get_product($product_id);
if (!$product || !$product->is_type('variable')) {
return false;
}
$available_variations = $product->get_available_variations();
foreach ($available_variations as $variation) {
$attributes = $variation['attributes'];
if (
isset($attributes['attribute_' . $attribute_name]) &&
$attributes['attribute_' . $attribute_name] === $attribute_value
) {
return $variation['variation_id'];
}
}
return false;
}
add_action('wp_ajax_add_variation_by_attribute', 'add_variation_by_attribute');
add_action('wp_ajax_nopriv_add_variation_by_attribute', 'add_variation_by_attribute');
function add_variation_by_attribute() {
try {
$product_id = isset($_POST['product_id']) ? absint($_POST['product_id']) : 0;
$attribute_name = isset($_POST['attribute_name']) ? wc_clean($_POST['attribute_name']) : '';
$attribute_value = isset($_POST['attribute_value']) ? sanitize_text_field(urldecode($_POST['attribute_value'])) : '';
$quantity = isset($_POST['quantity']) ? absint($_POST['quantity']) : 1;
$custom_price = isset($_POST['custom_price']) ? floatval($_POST['custom_price']) : 0;
if (!$product_id || !$attribute_name || !$attribute_value) {
throw new Exception('Не указаны обязательные параметры');
}
$variation_id = find_variation_id_by_attribute($product_id, $attribute_name, $attribute_value);
if (!$variation_id) {
throw new Exception('Вариация с указанными параметрами не найдена');
}
$variation_product = wc_get_product($variation_id);
if (!$variation_product) {
throw new Exception('Вариация не найдена');
}
if (!$variation_product->is_purchasable()) {
throw new Exception('Эта вариация недоступна для покупки');
}
if (!$variation_product->is_in_stock()) {
throw new Exception('Эта вариация нет в наличии');
}
// Автоматически соберём все атрибуты вариации
$variation_attributes = $variation_product->get_attributes();
$attributes = array();
foreach ($variation_attributes as $key => $value) {
$attributes['attribute_' . $key] = $value;
}
// Передаём кастомную цену из карточки товара
$cart_item_data = array();
if ($custom_price > 0) {
$cart_item_data['custom_display_price'] = $custom_price;
}
$cart_item_key = WC()->cart->add_to_cart(
$product_id,
$quantity,
$variation_id,
$attributes,
$cart_item_data
);
if (!$cart_item_key) {
throw new Exception('Не удалось добавить товар в корзину');
}
$data = array(
'success' => true,
'added' => true,
'message' => 'Товар успешно добавлен в корзину',
'cart_item_count' => WC()->cart->get_cart_contents_count(),
'fragments' => apply_filters('woocommerce_add_to_cart_fragments', array()),
'cart_hash' => WC()->cart->get_cart_hash()
);
wp_send_json($data);
} catch (Exception $e) {
wp_send_json_error($e->getMessage());
}
}
// Сохраняем кастомную цену в сессии корзины
add_filter('woocommerce_add_cart_item_data', 'save_custom_price_to_cart_item', 10, 3);
function save_custom_price_to_cart_item($cart_item_data, $product_id, $variation_id) {
if (isset($cart_item_data['custom_display_price'])) {
$cart_item_data['custom_display_price'] = floatval($cart_item_data['custom_display_price']);
}
return $cart_item_data;
}
// Восстанавливаем кастомную цену из сессии
add_filter('woocommerce_get_cart_item_from_session', 'restore_custom_price_from_session', 10, 3);
function restore_custom_price_from_session($cart_item, $values, $key) {
if (isset($values['custom_display_price'])) {
$cart_item['custom_display_price'] = $values['custom_display_price'];
}
return $cart_item;
}
// Применяем кастомную цену при расчете корзины
add_action('woocommerce_before_calculate_totals', 'apply_custom_display_price', 20, 1);
function apply_custom_display_price($cart) {
if (is_admin() && !defined('DOING_AJAX')) {
return;
}
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
if (isset($cart_item['custom_display_price']) && $cart_item['custom_display_price'] > 0) {
$cart_item['data']->set_price($cart_item['custom_display_price']);
}
}
}
add_action('wp_ajax_get_cart_contents', 'custom_get_cart_contents');
add_action('wp_ajax_nopriv_get_cart_contents', 'custom_get_cart_contents');
function custom_get_cart_contents() {
$items = [];
$regular_total = 0;
$sale_total = 0;
foreach (WC()->cart->get_cart() as $cart_item) {
$product = $cart_item['data'];
$variation_id = $cart_item['variation_id'];
$quantity = (int) $cart_item['quantity'];
// Use same logic as cart.php - get_regular_price and get_sale_price
$regular_price = $product->get_regular_price();
$sale_price = $product->get_sale_price();
if ( $sale_price === '' ) {
$sale_price = $regular_price;
}
$line_regular = $regular_price * $quantity;
$line_sale = $sale_price * $quantity;
$regular_total += $line_regular;
$sale_total += $line_sale;
$line_subtotal = number_format( $line_sale, 0, '', ' ' ) . ' ₽';
$old_price = '';
if ( $line_regular > $line_sale ) {
$old_price = number_format( $line_regular, 0, '', ' ' ) . ' ₽';
}
// Определяем единицу измерения
$unit = 'ШТ';
if ( isset( $cart_item['orgsteklo_calculator'] ) ) {
$unit = 'ЛИСТ';
}
// Получаем название товара без вариаций
$product_name = $product->get_name();
if ( $product->is_type('variation') ) {
$parent_product = wc_get_product( $cart_item['product_id'] );
if ( $parent_product ) {
$product_name = $parent_product->get_name();
}
}
$items[] = [
'id' => (int) $cart_item['product_id'],
'variation_id' => (int) $variation_id,
'name' => $product_name,
'sku' => $product->get_sku(),
'price' => $line_subtotal,
'oldPrice' => $old_price,
'quantity' => $quantity,
'unit' => $unit,
'image' => get_the_post_thumbnail_url($cart_item['product_id'], 'thumbnail'),
'url' => get_permalink($cart_item['product_id']),
];
}
$count = WC()->cart->get_cart_contents_count();
// Применяем скидку от суммы заказа (порог проверяется по сумме СО скидками на товары)
$cart_discount = orgsteklo_calculate_cart_discount( $sale_total );
$final_total = $sale_total - $cart_discount['amount'];
$old_total = '';
if ($regular_total > $final_total) {
$old_total = number_format($regular_total, 0, '', ' ') . ' ₽';
}
wp_send_json([
'items' => $items,
'total' => number_format($final_total, 0, '', ' ') . ' ₽',
'oldTotal' => $old_total,
'count' => (int) $count
]);
}
add_action('wp_ajax_remove_from_cart', 'custom_remove_from_cart');
add_action('wp_ajax_nopriv_remove_from_cart', 'custom_remove_from_cart');
function custom_remove_from_cart() {
$product_id = absint($_POST['product_id']);
foreach (WC()->cart->get_cart() as $cart_item_key => $cart_item) {
if ($cart_item['product_id'] == $product_id) {
WC()->cart->remove_cart_item($cart_item_key);
break;
}
}
wp_send_json(['removed' => true, 'total' => WC()->cart->get_total()]);
}
add_action('wp_ajax_update_cart_quantity', 'custom_update_cart_quantity');
add_action('wp_ajax_nopriv_update_cart_quantity', 'custom_update_cart_quantity');
function custom_update_cart_quantity() {
$product_id = absint($_POST['product_id']);
$quantity = isset($_POST['quantity']) ? absint($_POST['quantity']) : 1;
// Найдем ключ товара в корзине
$cart = WC()->cart->get_cart();
$updated = false;
foreach ($cart as $cart_item_key => $cart_item) {
if ($cart_item['product_id'] == $product_id) {
// Обновим количество
WC()->cart->set_quantity($cart_item_key, $quantity, true);
$updated = true;
break;
}
}
wp_send_json([
'updated' => $updated
]);
}
add_action('template_redirect', function() {
// Не обрабатываем обновление количества, если это запрос на удаление выбранных товаров
if (isset($_POST['delete_selected']) && $_POST['delete_selected'] == '1') {
return;
}
if (isset($_POST['update_checkout_cart']) && !empty($_POST['cart'])) {
foreach ($_POST['cart'] as $cart_item_key => $values) {
if (isset($values['qty'])) {
WC()->cart->set_quantity($cart_item_key, (int) $values['qty'], true);
}
}
WC()->cart->calculate_totals(); // Пересчет корзины
wp_safe_redirect(wc_get_cart_url()); // Обновляем страницу
exit;
}
});
add_action('template_redirect', function() {
if ( isset($_POST['delete_selected']) && $_POST['delete_selected'] == '1' && !empty($_POST['cart_product_keys']) && is_array($_POST['cart_product_keys']) ) {
foreach ($_POST['cart_product_keys'] as $cart_item_key) {
WC()->cart->remove_cart_item($cart_item_key);
}
wc_clear_notices(); // Чистим сообщения об ошибках/успехе
// Редирект на страницу корзины для обновления состояния
wp_safe_redirect(wc_get_cart_url());
exit;
}
});
add_filter( 'woocommerce_checkout_fields', 'custom_checkout_fields' );
function custom_checkout_fields( $fields ) {
// Billing fields
$fields['billing'] = array(
'billing_country' => array(
'type' => 'hidden',
'default' => 'RU',
'required' => false,
'priority' => 1,
),
'billing_email' => array(
'label' => __('Email', 'woocommerce'),
'required' => true,
'class' => array('form-row-wide'),
'priority' => 10,
),
'billing_phone' => array(
'label' => __('Номер телефона', 'woocommerce'),
'required' => true,
'class' => array('form-row-wide'),
'priority' => 20,
),
'billing_first_name' => array(
'label' => __('ФИО', 'woocommerce'),
'required' => true,
'class' => array('form-row-wide'),
'priority' => 30,
),
);
// Shipping fields — необязательные, т.к. при самовывозе адрес не нужен
$fields['shipping'] = array(
'shipping_address_1' => array(
'label' => __('Адрес доставки', 'woocommerce'),
'required' => false,
'class' => array('form-row-wide'),
'priority' => 10,
'placeholder' => __('Улица, дом, квартира', 'woocommerce'),
),
);
return $fields;
}
/**
* Принудительно устанавливаем страну RU для всех клиентов,
* чтобы WooCommerce всегда показывал способы доставки.
*/
add_action( 'woocommerce_before_checkout_form', 'orgsteklo_set_default_country', 5 );
function orgsteklo_set_default_country() {
if ( WC()->customer && ! WC()->customer->get_billing_country() ) {
WC()->customer->set_billing_country( 'RU' );
WC()->customer->set_shipping_country( 'RU' );
}
}
add_action( 'woocommerce_checkout_create_order', 'save_custom_shipping_field_to_order', 20, 2 );
function save_custom_shipping_field_to_order( $order, $data ) {
if ( isset( $_POST['shipping_address_1'] ) ) {
$order->set_shipping_address_1( sanitize_text_field( $_POST['shipping_address_1'] ) );
}
}
// Сохраняем доп. поля заказа (тип лица, ИНН, КПП, адреса юр. лица)
add_action( 'woocommerce_checkout_create_order', 'orgsteklo_save_custom_billing_fields', 30, 2 );
function orgsteklo_save_custom_billing_fields( $order, $data ) {
$fields = array(
'billing_person_type' => '_billing_person_type',
'billing_company_name_val' => '_billing_company_name',
'billing_inn_val' => '_billing_inn',
'billing_kpp_val' => '_billing_kpp',
'billing_legal_address_val'=> '_billing_legal_address',
'billing_actual_address_val'=> '_billing_actual_address',
'shipping_km_mkad' => '_shipping_km_mkad',
);
foreach ( $fields as $post_key => $meta_key ) {
if ( isset( $_POST[ $post_key ] ) && $_POST[ $post_key ] !== '' ) {
$order->update_meta_data( $meta_key, sanitize_text_field( $_POST[ $post_key ] ) );
}
}
}
add_action('wp_ajax_load_products', 'custom_ajax_load_products');
add_action('wp_ajax_nopriv_load_products', 'custom_ajax_load_products');
function custom_ajax_load_products() {
$paged = isset($_POST['page']) ? intval($_POST['page']) : 1;
$posts_per_page = isset($_POST['per_page']) ? intval($_POST['per_page']) : 12;
$args = array(
'post_type' => 'product',
'post_status' => array( 'publish', 'draft' ),
'paged' => $paged,
'posts_per_page' => $posts_per_page,
);
$query = new WP_Query($args);
$total = $query->found_posts;
$total_pages = $query->max_num_pages;
if ($query->have_posts()) {
ob_start();
echo '<div class="catalog_page-products__grid">';
while ($query->have_posts()) {
$query->the_post();
wc_get_template_part('content', 'product');
}
echo '</div>';
// === ПАГИНАЦИЯ ===
echo '<div class="catalog_page-products__bottom">';
// Показ количества
echo '<div class="catalog_page-count">';
echo '<p>Количество товаров на странице:</p>';
// (Можно оставить HTML как есть — JS сам обновляет активную кнопку)
echo '</div>';
echo '<div class="catalog_page-pagination">';
echo '<div class="catalog_page-pagination__row">';
// Prev
if ($paged > 1) {
echo '<a href="#" class="catalog_page-pagination-item prev" data-page="' . ($paged - 1) . '"><svg>...</svg></a>';
}
// Номера страниц
for ($i = 1; $i <= $total_pages; $i++) {
$active = $i == $paged ? ' active' : '';
echo '<a href="#" class="catalog_page-pagination-item' . $active . '" data-page="' . $i . '">' . $i . '</a>';
}
// Next
if ($paged < $total_pages) {
echo '<a href="#" class="catalog_page-pagination-item next" data-page="' . ($paged + 1) . '"><svg>...</svg></a>';
}
echo '</div>';
// Показ "00 товаров из 000"
$from = ($paged - 1) * $posts_per_page + 1;
$to = min($paged * $posts_per_page, $total);
echo '<p>' . $from . '' . $to . ' товаров <span>из ' . $total . '</span></p>';
echo '</div></div>';
wp_reset_postdata();
echo ob_get_clean();
} else {
echo '<p>Нет товаров.</p>';
}
wp_die();
}
// Добавляем поле "Длина" в вариации
add_action( 'woocommerce_variation_options_pricing', 'add_custom_length_field_to_variations', 10, 3 );
function add_custom_length_field_to_variations( $loop, $variation_data, $variation ) {
woocommerce_wp_text_input( array(
'id' => 'variation_length[' . $loop . ']',
'class' => 'short',
'label' => __('Длина', 'woocommerce') . ' (мм)',
'value' => get_post_meta( $variation->ID, '_variation_length', true )
) );
}
// Добавляем поле "Ширина" в вариации
add_action( 'woocommerce_variation_options_pricing', 'add_custom_width_field_to_variations', 10, 3 );
function add_custom_width_field_to_variations( $loop, $variation_data, $variation ) {
woocommerce_wp_text_input( array(
'id' => 'variation_width[' . $loop . ']',
'class' => 'short',
'label' => __('Ширина', 'woocommerce') . ' (мм)',
'value' => get_post_meta( $variation->ID, '_variation_width', true )
) );
}
// Сохраняем значение "Длина"
add_action( 'woocommerce_save_product_variation', 'save_custom_length_field_variations', 10, 2 );
function save_custom_length_field_variations( $variation_id, $i ) {
if ( isset( $_POST['variation_length'][$i] ) ) {
update_post_meta( $variation_id, '_variation_length', sanitize_text_field( $_POST['variation_length'][$i] ) );
}
}
// Сохраняем значение "Ширина"
add_action( 'woocommerce_save_product_variation', 'save_custom_width_field_variations', 10, 2 );
function save_custom_width_field_variations( $variation_id, $i ) {
if ( isset( $_POST['variation_width'][$i] ) ) {
update_post_meta( $variation_id, '_variation_width', sanitize_text_field( $_POST['variation_width'][$i] ) );
}
}
// Добавляем мета-данные в объект вариации
add_filter( 'woocommerce_available_variation', 'add_length_to_available_variations', 10, 3 );
function add_length_to_available_variations( $variation, $product, $variation_obj ) {
// Сначала проверяем кастомное поле
$length = get_post_meta( $variation['variation_id'], '_variation_length', true );
// Если кастомное пустое, берём стандартное поле WooCommerce
if ( empty( $length ) && $variation_obj ) {
$length = $variation_obj->get_length();
}
if ( $length ) {
$variation['length'] = $length;
}
return $variation;
}
// Добавляем ширину в объект вариации
add_filter( 'woocommerce_available_variation', 'add_width_to_available_variations', 10, 3 );
function add_width_to_available_variations( $variation, $product, $variation_obj ) {
// Сначала проверяем кастомное поле
$width = get_post_meta( $variation['variation_id'], '_variation_width', true );
// Если кастомное пустое, берём стандартное поле WooCommerce
if ( empty( $width ) && $variation_obj ) {
$width = $variation_obj->get_width();
}
if ( $width ) {
$variation['width'] = $width;
}
return $variation;
}
// Добавляем вес в данные вариации для отображения в шаблоне
add_filter( 'woocommerce_available_variation', 'add_weight_to_available_variations', 20 );
function add_weight_to_available_variations( $variation ) {
$variation_obj = wc_get_product( $variation['variation_id'] );
if ( $variation_obj ) {
$weight_raw = $variation_obj->get_weight();
if ( $weight_raw !== '' ) {
$weight_num = (float) wc_format_decimal( $weight_raw );
$variation['weight_formatted'] = number_format( $weight_num, 3, '.', '' ) . ' кг';
} else {
$variation['weight_formatted'] = '';
}
}
return $variation;
}
// Добавляем точные цены (как в корзине) в данные вариации
add_filter( 'woocommerce_available_variation', 'add_raw_prices_to_variations', 25 );
function add_raw_prices_to_variations( $variation ) {
$variation_obj = wc_get_product( $variation['variation_id'] );
if ( $variation_obj ) {
// Точные цены как их использует корзина, округлённые до целых рублей
$variation['raw_price'] = round( (float) $variation_obj->get_price() );
$variation['raw_regular_price'] = round( (float) $variation_obj->get_regular_price() );
$sale_price = $variation_obj->get_sale_price();
$variation['raw_sale_price'] = $sale_price !== '' ? round( (float) $sale_price ) : null;
}
return $variation;
}
// Показываем дополнительные поля в админке на странице редактирования пользователя
add_action('show_user_profile', 'add_custom_user_fields_admin');
add_action('edit_user_profile', 'add_custom_user_fields_admin');
function add_custom_user_fields_admin($user) {
?>
<h2>Юридическая информация</h2>
<table class="form-table">
<tr>
<th><label for="company_name">Наименование организации</label></th>
<td>
<input type="text" name="company_name" id="company_name" value="<?php echo esc_attr(get_user_meta($user->ID, 'company_name', true)); ?>" class="regular-text" />
</td>
</tr>
<tr>
<th><label for="inn">ИНН</label></th>
<td>
<input type="text" name="inn" id="inn" value="<?php echo esc_attr(get_user_meta($user->ID, 'inn', true)); ?>" class="regular-text" />
</td>
</tr>
<tr>
<th><label for="kpp">КПП</label></th>
<td>
<input type="text" name="kpp" id="kpp" value="<?php echo esc_attr(get_user_meta($user->ID, 'kpp', true)); ?>" class="regular-text" />
</td>
</tr>
<tr>
<th><label for="legal_address">Юридический адрес</label></th>
<td>
<input type="text" name="legal_address" id="legal_address" value="<?php echo esc_attr(get_user_meta($user->ID, 'legal_address', true)); ?>" class="regular-text" />
</td>
</tr>
<tr>
<th><label for="actual_address">Фактический адрес</label></th>
<td>
<input type="text" name="actual_address" id="actual_address" value="<?php echo esc_attr(get_user_meta($user->ID, 'actual_address', true)); ?>" class="regular-text" />
</td>
</tr>
</table>
<?php
}
// Сохраняем поля при обновлении профиля
add_action('personal_options_update', 'save_custom_user_fields_admin');
add_action('edit_user_profile_update', 'save_custom_user_fields_admin');
function save_custom_user_fields_admin($user_id) {
if (!current_user_can('edit_user', $user_id)) {
return false;
}
update_user_meta($user_id, 'company_name', sanitize_text_field($_POST['company_name']));
update_user_meta($user_id, 'inn', sanitize_text_field($_POST['inn']));
update_user_meta($user_id, 'kpp', sanitize_text_field($_POST['kpp']));
update_user_meta($user_id, 'legal_address', sanitize_text_field($_POST['legal_address']));
update_user_meta($user_id, 'actual_address', sanitize_text_field($_POST['actual_address']));
}
// ========================================
// ФУНКЦИИ ДЛЯ РАБОТЫ СО СКИДКАМИ И МЕТКАМИ
// ========================================
/**
* Извлекает процент скидки из названия метки (тега)
* Например: "-10%" => 10, "-5%" => 5, "10%" => 10, "10 %" => 10
* @return int|false процент скидки или false если не найден
*/
function get_discount_from_tag_name( $tag_name ) {
// Убираем пробелы по краям
$tag_name = trim( $tag_name );
// Ищем число с опциональным минусом и символом процента (с пробелом или без)
// Поддерживает: "-10%", "10%", "-10 %", "10 %", "- 10%", и т.д.
if ( preg_match( '/^-?\s*(\d+)\s*%/', $tag_name, $matches ) ) {
return absint( $matches[1] );
}
return false;
}
/**
* Получает процент скидки для товара из его меток (тегов)
* @param int $product_id ID товара
* @return int|false процент скидки или false
*/
function get_product_discount_percent( $product_id ) {
$tags = get_the_terms( $product_id, 'product_tag' );
if ( ! $tags || is_wp_error( $tags ) ) {
return false;
}
foreach ( $tags as $tag ) {
$discount = get_discount_from_tag_name( $tag->name );
if ( $discount !== false ) {
return $discount;
}
}
return false;
}
/**
* Рассчитывает цену со скидкой на основе процента
* @param float $price исходная цена
* @param int $discount_percent процент скидки
* @return float цена со скидкой
*/
function calculate_discounted_price( $price, $discount_percent ) {
// Округляем до целых рублей для консистентности между PHP и JS
return round( $price * ( 100 - $discount_percent ) / 100 );
}
/**
* Рассчитывает процент скидки между двумя ценами
* @param float $regular_price обычная цена
* @param float $sale_price акционная цена
* @return int процент скидки (округленный)
*/
function calculate_discount_percentage( $regular_price, $sale_price ) {
if ( $regular_price <= 0 || $sale_price <= 0 || $sale_price >= $regular_price ) {
return 0;
}
return round( ( ( $regular_price - $sale_price ) / $regular_price ) * 100 );
}
/**
* Получает отсортированные метки (теги) товара
* Сначала идут метки статуса (без дефиса и процента), затем метки со скидками (с "-" и "%")
* @param int $product_id ID товара
* @return array массив объектов WP_Term, отсортированных
*/
function get_sorted_product_tags( $product_id ) {
$tags = get_the_terms( $product_id, 'product_tag' );
if ( ! $tags || is_wp_error( $tags ) ) {
return [];
}
$status_tags = [];
$discount_tags = [];
foreach ( $tags as $tag ) {
// Если метка содержит "-" и "%", это скидка
if ( get_discount_from_tag_name( $tag->name ) !== false ) {
$discount_tags[] = $tag;
} else {
$status_tags[] = $tag;
}
}
// Возвращаем: сначала статусные, потом скидки
return array_merge( $status_tags, $discount_tags );
}
/**
* Проверяет, является ли метка скидкой (содержит "-" и "%")
* @param string $tag_name название метки
* @return bool
*/
function is_discount_tag( $tag_name ) {
return get_discount_from_tag_name( $tag_name ) !== false;
}
// ========================================
// АВТОМАТИЧЕСКИЙ РАСЧЁТ СКИДОК ПО МЕТКАМ
// ========================================
/**
* Автоматически рассчитывает sale price для простого товара на основе метки со скидкой
* Хук срабатывает при получении цены со скидкой
*/
add_filter( 'woocommerce_product_get_sale_price', 'auto_calculate_sale_price_from_tag', 10, 2 );
add_filter( 'woocommerce_product_get_price', 'auto_apply_discount_to_price', 10, 2 );
function auto_calculate_sale_price_from_tag( $sale_price, $product ) {
// ПРИОРИТЕТ: Если вручную установлена sale price - используем её
// Читаем напрямую из мета, чтобы избежать рекурсии
$manual_sale_price = get_post_meta( $product->get_id(), '_sale_price', true );
if ( $manual_sale_price !== '' && $manual_sale_price !== null && $manual_sale_price !== false ) {
return $manual_sale_price; // Вручную установленная цена имеет приоритет
}
// Получаем процент скидки из меток товара
$discount_percent = get_product_discount_percent( $product->get_id() );
if ( $discount_percent === false ) {
return $sale_price; // Нет метки со скидкой - возвращаем как есть
}
// Если скидка есть, рассчитываем sale price от regular price
$regular_price = $product->get_regular_price();
if ( empty( $regular_price ) || $regular_price <= 0 ) {
return $sale_price; // Нет обычной цены - не можем рассчитать
}
// Рассчитываем цену со скидкой
$calculated_sale = calculate_discounted_price( $regular_price, $discount_percent );
return $calculated_sale;
}
function auto_apply_discount_to_price( $price, $product ) {
// Применяем скидку к финальной цене
$sale_price = $product->get_sale_price();
if ( $sale_price !== '' && $sale_price !== null ) {
return $sale_price;
}
return $price;
}
/**
* Автоматически рассчитывает sale price для вариаций на основе метки родителя или самой вариации
*/
add_filter( 'woocommerce_product_variation_get_sale_price', 'auto_calculate_variation_sale_price_from_tag', 10, 2 );
add_filter( 'woocommerce_product_variation_get_price', 'auto_apply_variation_discount_to_price', 10, 2 );
function auto_calculate_variation_sale_price_from_tag( $sale_price, $variation ) {
// ПРИОРИТЕТ: Если вручную установлена sale price на вариации - используем её
$manual_sale_price = get_post_meta( $variation->get_id(), '_sale_price', true );
if ( $manual_sale_price !== '' && $manual_sale_price !== null && $manual_sale_price !== false ) {
return $manual_sale_price; // Вручную установленная цена имеет приоритет
}
// Сначала проверяем метку на самой вариации (если есть)
$discount_percent = get_product_discount_percent( $variation->get_id() );
// Если на вариации нет метки, проверяем родительский товар
if ( $discount_percent === false ) {
$parent_id = $variation->get_parent_id();
if ( $parent_id ) {
$discount_percent = get_product_discount_percent( $parent_id );
}
}
if ( $discount_percent === false ) {
return $sale_price; // Нет метки со скидкой
}
// Рассчитываем sale price от regular price вариации
$regular_price = $variation->get_regular_price();
if ( empty( $regular_price ) || $regular_price <= 0 ) {
return $sale_price;
}
$calculated_sale = calculate_discounted_price( $regular_price, $discount_percent );
return $calculated_sale;
}
function auto_apply_variation_discount_to_price( $price, $variation ) {
$sale_price = $variation->get_sale_price();
if ( $sale_price !== '' && $sale_price !== null ) {
return $sale_price;
}
return $price;
}
/**
* Помечаем товар как "on sale" если есть метка со скидкой
*/
add_filter( 'woocommerce_product_is_on_sale', 'mark_product_on_sale_if_has_discount_tag', 10, 2 );
function mark_product_on_sale_if_has_discount_tag( $on_sale, $product ) {
// Проверяем есть ли метка со скидкой
$discount_percent = get_product_discount_percent( $product->get_id() );
if ( $discount_percent !== false ) {
return true; // Есть метка со скидкой = товар на распродаже
}
// Для вариативных товаров проверяем метку на родителе
if ( $product->is_type( 'variation' ) ) {
$parent_id = $product->get_parent_id();
if ( $parent_id ) {
$parent_discount = get_product_discount_percent( $parent_id );
if ( $parent_discount !== false ) {
return true;
}
}
}
return $on_sale;
}
// Глобальная сортировка вариантов по ЧИСЛАМ: по УБЫВАНИЮ (lexicographic: [x1,x2,…] desc)
add_filter('woocommerce_dropdown_variation_attribute_options_args', function ($args) {
if (empty($args['options']) || empty($args['product']) || empty($args['attribute'])) {
return $args;
}
// <= поменяйте на 'asc' если нужно по возрастанию
$direction = 'desc'; // 'desc' или 'asc'
$attribute = $args['attribute'];
$options = (array) $args['options'];
// Берём видимую метку для опции
$get_label_for_option = function($opt) use ($attribute) {
static $cache = [];
$key = $attribute . '|' . (string)$opt;
if (isset($cache[$key])) return $cache[$key];
if (taxonomy_exists($attribute)) {
$field = is_numeric($opt) ? 'id' : 'slug';
$term = get_term_by($field, $opt, $attribute);
if ($term && !is_wp_error($term)) {
return $cache[$key] = (string)$term->name;
}
}
// fallback (кастомный атрибут или слаг)
$label = (string)$opt;
$label = str_replace(['-', '_'], ' ', $label);
$label = preg_replace('~\s+~u', ' ', $label);
return $cache[$key] = trim($label);
};
// Извлекаем ВСЕ числа из строки (поддержка "1,5" как 1.5)
$extract_numbers = function(string $label): array {
$normalized = str_replace(',', '.', $label);
preg_match_all('/\d+(?:\.\d+)?/u', $normalized, $m);
return array_map('floatval', $m[0] ?? []);
};
// Подготовка данных
$rows = [];
foreach ($options as $i => $opt) {
$label = ($opt === '') ? '' : $get_label_for_option($opt);
$nums = ($opt === '') ? [] : $extract_numbers($label);
$rows[] = [
'value' => $opt,
'label' => $label,
'nums' => $nums,
'i' => $i, // исходный индекс для стабильности
];
}
// Оставим пустую опцию первой
$firstEmpty = null;
$data = [];
foreach ($rows as $r) {
if ($r['value'] === '' && $firstEmpty === null) {
$firstEmpty = $r;
continue;
}
$data[] = $r;
}
// Сортировка по кортежам чисел (лексикографически), направление: asc/desc
usort($data, function($a, $b) use ($direction) {
$na = $a['nums']; $nb = $b['nums'];
$aHas = !empty($na); $bHas = !empty($nb);
if ($aHas && !$bHas) return -1; // числовые — выше текстовых
if (!$aHas && $bHas) return 1;
if (!$aHas && !$bHas) {
$c = strnatcasecmp($a['label'], $b['label']);
return ($c !== 0) ? $c : ($a['i'] <=> $b['i']);
}
$len = max(count($na), count($nb));
for ($k = 0; $k < $len; $k++) {
$ai = $na[$k] ?? null;
$bi = $nb[$k] ?? null;
if ($ai === null && $bi === null) break;
if ($ai === null) return 1; // короче — НИЖЕ (напр. [50] после [50,10])
if ($bi === null) return -1;
if ($ai != $bi) {
// КОМБИНИРОВАННАЯ СОРТИРОВКА:
// Первое число: от меньшего к большему (ASC)
// Второе и последующие: от большего к меньшему (DESC)
if ($k === 0) {
// Первое число: ASC (5 → 6 → 10)
return ($ai < $bi) ? -1 : 1; // меньше — выше
} else {
// Второе+ число: DESC (при одинаковом первом: 7 → 6 → 4)
return ($ai < $bi) ? 1 : -1; // больше — выше
}
}
}
// Полное равенство чисел — по метке, затем по исходному индексу
$c = strnatcasecmp($a['label'], $b['label']);
return ($c !== 0) ? $c : ($a['i'] <=> $b['i']);
});
$ordered = array_map(fn($r) => $r['value'], $data);
if ($firstEmpty !== null) array_unshift($ordered, '');
$args['options'] = $ordered;
return $args;
}, 20);
// АВТОМАТИЧЕСКАЯ СОРТИРОВКА ТЕРМИНОВ АТРИБУТОВ ПО ЧИСЛАМ (ОТ БОЛЬШЕГО К МЕНЬШЕМУ)
// Применяется ВЕЗДЕ: в админке, на фронте, в вариациях
// Решает проблему: после импорта термины в правильном порядке БЕЗ ручного вмешательства
add_filter( 'get_terms', 'auto_sort_attribute_terms_by_numbers', 10, 4 );
function auto_sort_attribute_terms_by_numbers( $terms, $taxonomies, $args, $term_query ) {
// Применяем только к таксономиям атрибутов WooCommerce
if ( empty( $terms ) || is_wp_error( $terms ) ) {
return $terms;
}
// Проверяем, что это атрибут товара
$is_product_attribute = false;
foreach ( (array) $taxonomies as $taxonomy ) {
if ( strpos( $taxonomy, 'pa_' ) === 0 ) {
$is_product_attribute = true;
break;
}
}
if ( ! $is_product_attribute ) {
return $terms;
}
// Направление сортировки (как в вариациях)
$direction = 'desc'; // от большего к меньшему
// Функция извлечения чисел (ТОЧНО как в вариациях)
$extract_numbers = function( $label ) {
$normalized = str_replace( ',', '.', $label );
preg_match_all( '/\d+(?:\.\d+)?/u', $normalized, $m );
return array_map( 'floatval', $m[0] ?? [] );
};
// Подготовка данных
$data = [];
foreach ( $terms as $i => $term ) {
// Пропускаем, если термин не объект (может быть строка в некоторых случаях)
if ( ! is_object( $term ) || ! isset( $term->name ) ) {
continue;
}
$label = $term->name;
$nums = $extract_numbers( $label );
$data[] = [
'term' => $term,
'label' => $label,
'nums' => $nums,
'i' => $i,
];
}
// Сортировка по кортежам чисел (КОМБИНИРОВАННАЯ: первое число ASC, второе+ DESC)
usort( $data, function( $a, $b ) {
$na = $a['nums'];
$nb = $b['nums'];
$aHas = ! empty( $na );
$bHas = ! empty( $nb );
if ( $aHas && ! $bHas ) return -1;
if ( ! $aHas && $bHas ) return 1;
if ( ! $aHas && ! $bHas ) {
$c = strnatcasecmp( $a['label'], $b['label'] );
return ( $c !== 0 ) ? $c : ( $a['i'] <=> $b['i'] );
}
$len = max( count( $na ), count( $nb ) );
for ( $k = 0; $k < $len; $k++ ) {
$ai = $na[$k] ?? null;
$bi = $nb[$k] ?? null;
if ( $ai === null && $bi === null ) break;
if ( $ai === null ) return 1;
if ( $bi === null ) return -1;
if ( $ai != $bi ) {
// КОМБИНИРОВАННАЯ СОРТИРОВКА:
// Первое число: от меньшего к большему (ASC)
// Второе и последующие: от большего к меньшему (DESC)
if ($k === 0) {
return ($ai < $bi) ? -1 : 1; // меньше — выше
} else {
return ($ai < $bi) ? 1 : -1; // больше — выше
}
}
}
$c = strnatcasecmp( $a['label'], $b['label'] );
return ( $c !== 0 ) ? $c : ( $a['i'] <=> $b['i'] );
} );
// Возвращаем отсортированные термины
return array_map( function( $item ) {
return $item['term'];
}, $data );
}
// СОХРАНЕНИЕ ПОЛЬЗОВАТЕЛЬСКОГО ПОРЯДКА АТРИБУТОВ ПРИ ОБНОВЛЕНИИ ТОВАРА
// Решает проблему: порядок самих атрибутов (не терминов!) сбрасывается
add_action( 'woocommerce_process_product_meta', 'preserve_attribute_order_on_save', 20 );
function preserve_attribute_order_on_save( $product_id ) {
// Получаем текущие атрибуты из POST (если они были отправлены)
if ( ! isset( $_POST['attribute_names'] ) || ! is_array( $_POST['attribute_names'] ) ) {
return;
}
// Получаем существующие атрибуты товара
$product_attributes = get_post_meta( $product_id, '_product_attributes', true );
if ( ! is_array( $product_attributes ) ) {
return;
}
// Обновляем позиции атрибутов согласно порядку из формы
$position = 0;
foreach ( $_POST['attribute_names'] as $attribute_name ) {
if ( isset( $product_attributes[ $attribute_name ] ) ) {
$product_attributes[ $attribute_name ]['position'] = $position;
$position++;
}
}
// Сохраняем обновлённые атрибуты напрямую в meta (без вызова save)
update_post_meta( $product_id, '_product_attributes', $product_attributes );
}
// ПЕРЕИМЕНОВАНИЕ АТРИБУТА "ВЕС" В "ВЕС 1 ШТ" ДЛЯ ВАРИАТИВНЫХ ТОВАРОВ
// Применяется только на странице товара с вариациями
add_filter( 'woocommerce_attribute_label', 'rename_weight_attribute_for_variations', 10, 3 );
function rename_weight_attribute_for_variations( $label, $name, $product ) {
// Проверяем что это атрибут "Вес"
// Проверяем и slug (pa_ves) и оригинальное название (Вес)
if (
$name === 'pa_ves' ||
$name === 'Вес' ||
$label === 'Вес' ||
strpos( $name, 'ves' ) !== false
) {
// Если есть product и это вариативный товар
if ( $product && is_object( $product ) && $product->is_type( 'variable' ) ) {
return 'Вес 1 шт';
}
// Если мы на странице вариативного товара (даже если $product не передан)
if ( is_product() ) {
global $post;
if ( $post ) {
$current_product = wc_get_product( $post->ID );
if ( $current_product && $current_product->is_type( 'variable' ) ) {
return 'Вес 1 шт';
}
}
}
}
return $label;
}
// Убираем дублирование "Вес 1 шт" у клея и труб в корзине
add_filter( 'woocommerce_get_item_data', 'remove_duplicate_weight_from_cart', 10, 2 );
function remove_duplicate_weight_from_cart( $item_data, $cart_item ) {
// Проверяем что это вариативный товар
if ( ! isset( $cart_item['variation_id'] ) || empty( $cart_item['variation_id'] ) ) {
return $item_data;
}
// Получаем product_id родительского товара
$product_id = $cart_item['product_id'];
$product_template = get_field('product_template', $product_id);
// Если это клей или трубы, убираем атрибут веса из отображения
if ( in_array( $product_template, array( 'Клей', 'Трубы' ) ) ) {
// Фильтруем массив, убирая элементы с ключом содержащим "вес" или "Вес"
$item_data = array_filter( $item_data, function( $data ) {
$key = isset( $data['key'] ) ? $data['key'] : ( isset( $data['name'] ) ? $data['name'] : '' );
// Убираем если ключ содержит "вес" в любом регистре
return stripos( $key, 'вес' ) === false;
});
}
return $item_data;
}
// Подключаем админ-панель для калькулятора цен на оргстекло
require_once get_template_directory() . '/inc/price-calculator-admin.php';
// Подключаем функции расчета цен на оргстекло
require_once get_template_directory() . '/inc/price-calculator-functions.php';
/**
* Создание атрибута "Толщина листа" для вариаций без калькулятора
*/
add_action( 'after_switch_theme', 'orgsteklo_create_thickness_attribute' );
function orgsteklo_create_thickness_attribute() {
// Проверяем, существует ли уже атрибут
$attribute_taxonomy = 'pa_tolschina_lista';
if ( ! taxonomy_exists( $attribute_taxonomy ) ) {
// Создаем атрибут через WordPress
$attribute_data = array(
'slug' => 'tolschina_lista',
'name' => __( 'Толщина листа', 'woocommerce' ),
'type' => 'select',
'order_by' => 'menu_order',
'has_archives' => false,
);
// Используем функцию WooCommerce для создания атрибута
if ( function_exists( 'wc_create_attribute' ) ) {
$attribute_id = wc_create_attribute( $attribute_data );
if ( ! is_wp_error( $attribute_id ) ) {
// Регистрируем таксономию
register_taxonomy(
$attribute_taxonomy,
'product',
array(
'labels' => array(
'name' => __( 'Толщина листа', 'woocommerce' ),
),
'hierarchical' => false,
'show_ui' => false,
'query_var' => true,
'rewrite' => false,
)
);
}
}
}
}
// Регистрируем таксономию атрибута при каждой загрузке
add_action( 'init', 'orgsteklo_register_thickness_taxonomy', 5 );
function orgsteklo_register_thickness_taxonomy() {
if ( taxonomy_exists( 'pa_tolschina_lista' ) ) {
return;
}
register_taxonomy(
'pa_tolschina_lista',
'product',
array(
'labels' => array(
'name' => __( 'Толщина листа', 'woocommerce' ),
'singular_name' => __( 'Толщина листа', 'woocommerce' ),
),
'hierarchical' => false,
'show_ui' => false,
'query_var' => true,
'rewrite' => false,
'public' => true,
)
);
}
/**
* Убираем вариации из названий товаров в корзине и оформлении заказа
*/
add_filter( 'woocommerce_cart_item_name', 'orgsteklo_remove_variation_from_cart_item_name', 10, 3 );
add_filter( 'woocommerce_order_item_name', 'orgsteklo_remove_variation_from_order_item_name', 10, 2 );
function orgsteklo_remove_variation_from_cart_item_name( $product_name, $cart_item, $cart_item_key ) {
$_product = $cart_item['data'];
if ( $_product && $_product->is_type('variation') ) {
$parent_product = wc_get_product( $cart_item['product_id'] );
if ( $parent_product ) {
$product_name = $parent_product->get_name();
}
}
return $product_name;
}
function orgsteklo_remove_variation_from_order_item_name( $product_name, $item ) {
$product = $item->get_product();
if ( $product && $product->is_type('variation') ) {
$parent_product = wc_get_product( $product->get_parent_id() );
if ( $parent_product ) {
$product_name = $parent_product->get_name();
}
}
return $product_name;
}
/**
* Страница настроек скидок от суммы заказа
*/
add_action( 'admin_menu', 'orgsteklo_add_cart_discount_page', 99 );
function orgsteklo_add_cart_discount_page() {
add_submenu_page(
'woocommerce',
'Скидки от суммы заказа',
'Скидки от суммы',
'manage_woocommerce',
'orgsteklo-cart-discounts',
'orgsteklo_cart_discount_settings_page'
);
}
function orgsteklo_cart_discount_settings_page() {
// Сохранение настроек
if ( isset( $_POST['orgsteklo_discount_save'] ) && check_admin_referer( 'orgsteklo_discount_settings' ) ) {
$discounts = array();
if ( isset( $_POST['discount_amount'] ) && is_array( $_POST['discount_amount'] ) ) {
foreach ( $_POST['discount_amount'] as $index => $amount ) {
$amount = floatval( $amount );
$percent = floatval( $_POST['discount_percent'][$index] ?? 0 );
if ( $amount > 0 && $percent > 0 ) {
$discounts[$amount] = $percent;
}
}
}
// Сортируем по возрастанию суммы
ksort( $discounts );
update_option( 'orgsteklo_cart_discounts', $discounts );
echo '<div class="notice notice-success"><p>Настройки сохранены!</p></div>';
}
$discounts = get_option( 'orgsteklo_cart_discounts', array(
10000 => 3,
50000 => 5,
100000 => 7,
200000 => 10,
) );
?>
<div class="wrap">
<h1>Настройки скидок от суммы заказа</h1>
<p>Настройте автоматические скидки в зависимости от суммы корзины. Скидка применяется автоматически при достижении указанной суммы.</p>
<form method="post" action="">
<?php wp_nonce_field( 'orgsteklo_discount_settings' ); ?>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>От суммы (₽)</th>
<th>Скидка (%)</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="discount-rows">
<?php foreach ( $discounts as $amount => $percent ) : ?>
<tr>
<td><input type="number" name="discount_amount[]" value="<?php echo esc_attr( $amount ); ?>" min="0" step="1" style="width: 150px;"></td>
<td><input type="number" name="discount_percent[]" value="<?php echo esc_attr( $percent ); ?>" min="0" max="100" step="0.1" style="width: 100px;"></td>
<td><button type="button" class="button delete-discount-row">Удалить</button></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p>
<button type="button" id="add-discount-row" class="button">Добавить уровень скидки</button>
</p>
<p class="submit">
<input type="submit" name="orgsteklo_discount_save" class="button button-primary" value="Сохранить настройки">
</p>
</form>
</div>
<script>
jQuery(document).ready(function($) {
$('#add-discount-row').on('click', function() {
var row = '<tr>' +
'<td><input type="number" name="discount_amount[]" value="" min="0" step="1" style="width: 150px;"></td>' +
'<td><input type="number" name="discount_percent[]" value="" min="0" max="100" step="0.1" style="width: 100px;"></td>' +
'<td><button type="button" class="button delete-discount-row">Удалить</button></td>' +
'</tr>';
$('#discount-rows').append(row);
});
$(document).on('click', '.delete-discount-row', function() {
$(this).closest('tr').remove();
});
});
</script>
<style>
.wp-list-table input[type="number"] {
padding: 5px;
}
</style>
<?php
}
/**
* Настройки страницы оформления заказа (тексты, адрес самовывоза, карта и т.д.)
*/
add_action( 'admin_menu', 'orgsteklo_add_checkout_settings_page', 99 );
function orgsteklo_add_checkout_settings_page() {
add_submenu_page(
'woocommerce',
'Настройки страницы доставки',
'Настройки доставки',
'manage_woocommerce',
'orgsteklo-checkout-settings',
'orgsteklo_checkout_settings_page'
);
}
function orgsteklo_checkout_settings_defaults() {
return array(
'comment_hint' => 'В этом поле вы можете указать график работы склада, необходимость заказа пропуска, а также другие комментарии к заказу',
'file_hint' => '',
'payment_description' => '*Ваш заказ, информация о желаемом способе доставки и Ваши контакты будут направлены менеджеру компании "ОРГСТЕКЛО" для дальнейшей обработки заказа и выставления счета на оплату. Счет на оплату будет направлен Вам на указанный e-mail.',
'pickup_address' => 'Московская обл., г. Реутов, ул. Победы, д. 1',
'pickup_hours' => 'Часы работы: с 10:00 до 20:00',
'pickup_description' => '(заезд грузового транспорта только со стороны шоссе Энтузиастов по проспекту Мира)',
'pickup_map_link' => '',
'pickup_map_iframe' => 'https://yandex.ru/map-widget/v1/?um=constructor%3A292408e4d7b13e325054d9d40cfd751bedc6dcb36775466b6e852ed33b3d2d1f&source=constructor',
'samovyvoz_label' => 'Московская обл., г. Реутов, ул. Победы, д. 1',
'moscow_label' => 'Стоимость: {cost}',
'moscow_area_label' => 'Стоимость: {cost} + {km_rate} ₽ за каждый километр за МКАД',
'moscow_area_km_rate' => '30',
'region_label' => 'Транспортной компанией',
'pochta_label' => 'Стоимость зависит от веса и габаритов заказа',
'moscow_area_warning' => '*Если Вы введете недостоверные данные и не доплатите за доставку по области, продавец оставляет за собой право не доставлять товар.',
'region_description' => 'Стоимость и условия доставки в другой регион рассчитываются индивидуально, после оформления заказа с вами свяжется менеджер для уточнения деталей доставки.',
'pochta_description' => 'Стоимость и условия доставки в другой регион Почтой России рассчитываются индивидуально, после оформления заказа с вами свяжется менеджер для уточнения деталей доставки.',
'online_payment_description' => '',
);
}
function orgsteklo_get_checkout_setting( $key ) {
$settings = get_option( 'orgsteklo_checkout_settings', array() );
$defaults = orgsteklo_checkout_settings_defaults();
return isset( $settings[ $key ] ) && $settings[ $key ] !== '' ? $settings[ $key ] : ( isset( $defaults[ $key ] ) ? $defaults[ $key ] : '' );
}
function orgsteklo_checkout_settings_page() {
if ( isset( $_POST['orgsteklo_checkout_save'] ) && check_admin_referer( 'orgsteklo_checkout_settings_nonce' ) ) {
$fields = array_keys( orgsteklo_checkout_settings_defaults() );
$settings = array();
foreach ( $fields as $field ) {
$settings[ $field ] = isset( $_POST[ $field ] ) ? wp_kses_post( wp_unslash( $_POST[ $field ] ) ) : '';
}
update_option( 'orgsteklo_checkout_settings', $settings );
echo '<div class="notice notice-success"><p>Настройки сохранены!</p></div>';
}
$defaults = orgsteklo_checkout_settings_defaults();
$settings = get_option( 'orgsteklo_checkout_settings', array() );
$get = function( $key ) use ( $settings, $defaults ) {
return isset( $settings[ $key ] ) ? $settings[ $key ] : $defaults[ $key ];
};
?>
<div class="wrap">
<h1>Настройки страницы доставки</h1>
<form method="post" action="">
<?php wp_nonce_field( 'orgsteklo_checkout_settings_nonce' ); ?>
<h2>Комментарий к заказу</h2>
<table class="form-table">
<tr>
<th><label for="comment_hint">Подсказка к комментарию</label></th>
<td><textarea name="comment_hint" id="comment_hint" rows="3" class="large-text"><?php echo esc_textarea( $get('comment_hint') ); ?></textarea></td>
</tr>
<tr>
<th><label for="file_hint">Подсказка к прикреплению файла</label></th>
<td><textarea name="file_hint" id="file_hint" rows="2" class="large-text"><?php echo esc_textarea( $get('file_hint') ); ?></textarea></td>
</tr>
</table>
<h2>Способ оплаты</h2>
<table class="form-table">
<tr>
<th><label for="payment_description">Описание способа оплаты «Сформировать счёт»</label></th>
<td><textarea name="payment_description" id="payment_description" rows="4" class="large-text"><?php echo esc_textarea( $get('payment_description') ); ?></textarea></td>
</tr>
<tr>
<th><label for="online_payment_description">Описание способа оплаты «Оплата онлайн»</label></th>
<td><textarea name="online_payment_description" id="online_payment_description" rows="4" class="large-text"><?php echo esc_textarea( $get('online_payment_description') ); ?></textarea></td>
</tr>
</table>
<h2>Самовывоз</h2>
<table class="form-table">
<tr>
<th><label for="pickup_address">Адрес самовывоза</label></th>
<td><input type="text" name="pickup_address" id="pickup_address" value="<?php echo esc_attr( $get('pickup_address') ); ?>" class="large-text"></td>
</tr>
<tr>
<th><label for="pickup_hours">Часы работы</label></th>
<td><input type="text" name="pickup_hours" id="pickup_hours" value="<?php echo esc_attr( $get('pickup_hours') ); ?>" class="large-text"></td>
</tr>
<tr>
<th><label for="pickup_description">Дополнительное описание</label></th>
<td><textarea name="pickup_description" id="pickup_description" rows="2" class="large-text"><?php echo esc_textarea( $get('pickup_description') ); ?></textarea></td>
</tr>
<tr>
<th><label for="pickup_map_link">Ссылка «Показать на карте»</label></th>
<td><input type="url" name="pickup_map_link" id="pickup_map_link" value="<?php echo esc_attr( $get('pickup_map_link') ); ?>" class="large-text" placeholder="https://yandex.ru/maps/..."></td>
</tr>
<tr>
<th><label for="pickup_map_iframe">URL iframe Яндекс Карты</label></th>
<td><input type="text" name="pickup_map_iframe" id="pickup_map_iframe" value="<?php echo esc_attr( $get('pickup_map_iframe') ); ?>" class="large-text" placeholder="https://yandex.ru/map-widget/v1/..."></td>
</tr>
</table>
<h2>Описания способов доставки (серый текст под названием)</h2>
<p class="description">Для «Доставка по Москве» и «Доставка по МО» используйте <code>{cost}</code> — подставится стоимость из WooCommerce. Для МО также доступен <code>{km_rate}</code> — стоимость за 1 км.</p>
<table class="form-table">
<tr>
<th><label for="samovyvoz_label">Самовывоз</label></th>
<td><input type="text" name="samovyvoz_label" id="samovyvoz_label" value="<?php echo esc_attr( $get('samovyvoz_label') ); ?>" class="large-text"></td>
</tr>
<tr>
<th><label for="moscow_label">Доставка по Москве</label></th>
<td><input type="text" name="moscow_label" id="moscow_label" value="<?php echo esc_attr( $get('moscow_label') ); ?>" class="large-text"></td>
</tr>
<tr>
<th><label for="moscow_area_label">Доставка по Московской области</label></th>
<td><input type="text" name="moscow_area_label" id="moscow_area_label" value="<?php echo esc_attr( $get('moscow_area_label') ); ?>" class="large-text">
<p class="description">Используйте <code>{km_rate}</code> для подстановки стоимости за км.</p></td>
</tr>
<tr>
<th><label for="moscow_area_km_rate">Стоимость за 1 км от МКАД (₽)</label></th>
<td><input type="number" name="moscow_area_km_rate" id="moscow_area_km_rate" value="<?php echo esc_attr( $get('moscow_area_km_rate') ); ?>" class="small-text" min="0" step="1"></td>
</tr>
<tr>
<th><label for="region_label">В другой регион</label></th>
<td><input type="text" name="region_label" id="region_label" value="<?php echo esc_attr( $get('region_label') ); ?>" class="large-text"></td>
</tr>
<tr>
<th><label for="pochta_label">В другой регион Почтой России</label></th>
<td><input type="text" name="pochta_label" id="pochta_label" value="<?php echo esc_attr( $get('pochta_label') ); ?>" class="large-text"></td>
</tr>
</table>
<h2>Тексты-комментарии к адресным формам доставки</h2>
<table class="form-table">
<tr>
<th><label for="moscow_area_warning">Доставка по Московской области</label></th>
<td><textarea name="moscow_area_warning" id="moscow_area_warning" rows="3" class="large-text"><?php echo esc_textarea( $get('moscow_area_warning') ); ?></textarea></td>
</tr>
<tr>
<th><label for="region_description">Доставка в другой регион</label></th>
<td><textarea name="region_description" id="region_description" rows="3" class="large-text"><?php echo esc_textarea( $get('region_description') ); ?></textarea></td>
</tr>
<tr>
<th><label for="pochta_description">Доставка Почтой России</label></th>
<td><textarea name="pochta_description" id="pochta_description" rows="3" class="large-text"><?php echo esc_textarea( $get('pochta_description') ); ?></textarea></td>
</tr>
</table>
<p class="submit">
<input type="submit" name="orgsteklo_checkout_save" class="button-primary" value="Сохранить настройки">
</p>
</form>
</div>
<?php
}
/**
* Скидка от суммы заказа
* Updated: <?php echo date('d.m.Y'); ?> - Проверено и активировано
*
* Применяет скидку на основе общей суммы корзины:
* - От 10 000 ₽ - скидка 3%
* - От 50 000 ₽ - скидка 5%
* - От 100 000 ₽ - скидка 7%
* - От 200 000 ₽ - скидка 10%
*
* Настройки можно изменить в админке: WooCommerce → Скидки от суммы
*/
/**
* Вспомогательная функция для расчета скидки от суммы заказа
* Возвращает массив: ['percent' => %, 'amount' => сумма скидки]
*/
function orgsteklo_calculate_cart_discount( $cart_total ) {
$discount_tiers = get_option( 'orgsteklo_cart_discounts', array(
10000 => 3,
50000 => 5,
100000 => 7,
200000 => 10,
) );
if ( empty( $discount_tiers ) ) {
return array( 'percent' => 0, 'amount' => 0 );
}
$discount_percent = 0;
krsort( $discount_tiers );
foreach ( $discount_tiers as $threshold => $percent ) {
if ( $cart_total >= $threshold && $percent > $discount_percent ) {
$discount_percent = $percent;
break;
}
}
$discount_amount = 0;
if ( $discount_percent > 0 ) {
$discount_amount = round( ( $cart_total * $discount_percent ) / 100 );
}
return array( 'percent' => $discount_percent, 'amount' => $discount_amount );
}
add_action( 'woocommerce_cart_calculate_fees', 'orgsteklo_cart_discount_based_on_total', 10 );
function orgsteklo_cart_discount_based_on_total() {
if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
return;
}
// Получаем настройки скидок из админки
$discount_tiers = get_option( 'orgsteklo_cart_discounts', array(
10000 => 3,
50000 => 5,
100000 => 7,
200000 => 10,
) );
if ( empty( $discount_tiers ) ) {
return; // Если настройки пусты, не применяем скидку
}
// Считаем сумму СО скидками на товары для проверки порога и применения скидки
$sale_total = 0;
foreach ( WC()->cart->get_cart() as $item ) {
$product = $item['data'];
$qty = $item['quantity'];
$sale_price = $product->get_sale_price();
if ( $sale_price === '' ) {
$sale_price = $product->get_regular_price();
}
$sale_total += $sale_price * $qty;
}
$discount_percent = 0;
// Определяем скидку по сумме СО скидками на товары
krsort( $discount_tiers );
foreach ( $discount_tiers as $threshold => $percent ) {
if ( $sale_total >= $threshold && $percent > $discount_percent ) {
$discount_percent = $percent;
break;
}
}
// Применяем скидку
if ( $discount_percent > 0 ) {
$discount_amount = round( ( $sale_total * $discount_percent ) / 100 );
WC()->cart->add_fee( 'Скидка от суммы заказа (' . $discount_percent . '%)', -$discount_amount );
}
}
/**
* Включаем черновики (draft) в поиск и каталог товаров
*/
add_action( 'pre_get_posts', 'orgsteklo_include_drafts_in_product_queries' );
function orgsteklo_include_drafts_in_product_queries( $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
// Поиск по товарам
if ( $query->is_search() && isset( $query->query_vars['post_type'] ) && $query->query_vars['post_type'] === 'product' ) {
$query->set( 'post_status', array( 'publish', 'draft' ) );
}
// Каталог / категории товаров
if ( $query->is_post_type_archive( 'product' ) || $query->is_tax( 'product_cat' ) ) {
$query->set( 'post_status', array( 'publish', 'draft' ) );
}
}
/**
* Переключатель количества товаров на странице (?per_page=24)
*/
add_filter( 'loop_shop_per_page', 'orgsteklo_products_per_page', 20 );
function orgsteklo_products_per_page( $cols ) {
if ( isset( $_GET['per_page'] ) ) {
$val = sanitize_text_field( $_GET['per_page'] );
if ( $val === 'all' ) {
return 9999;
}
$num = intval( $val );
if ( in_array( $num, array( 12, 24, 48 ), true ) ) {
return $num;
}
}
return 12;
}
/**
* Делаем черновики видимыми в каталоге/поиске,
* чтобы is_visible() не скрывала их в шаблоне content-product.php
*/
add_filter( 'woocommerce_product_is_visible', 'orgsteklo_draft_products_visible', 10, 2 );
function orgsteklo_draft_products_visible( $visible, $product_id ) {
if ( ! $visible && get_post_status( $product_id ) === 'draft' ) {
return true;
}
return $visible;
}
/**
* AJAX обработчик для добавления простых товаров в корзину
*/
add_action( 'wp_ajax_woocommerce_ajax_add_to_cart', 'woocommerce_ajax_add_to_cart' );
add_action( 'wp_ajax_nopriv_woocommerce_ajax_add_to_cart', 'woocommerce_ajax_add_to_cart' );
function woocommerce_ajax_add_to_cart() {
$product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0;
$product_id = apply_filters( 'woocommerce_add_to_cart_product_id', $product_id );
$quantity = empty( $_POST['quantity'] ) ? 1 : wc_stock_amount( $_POST['quantity'] );
$variation_id = isset( $_POST['variation_id'] ) ? absint( $_POST['variation_id'] ) : 0;
// Проверяем что product_id передан
if ( ! $product_id ) {
wp_send_json( array(
'error' => true,
'message' => 'ID товара не указан'
) );
wp_die();
}
// Получаем товар
$product = wc_get_product( $product_id );
if ( ! $product ) {
wp_send_json( array(
'error' => true,
'message' => 'Товар не найден'
) );
wp_die();
}
// Проверяем можно ли купить товар
if ( ! $product->is_purchasable() ) {
wp_send_json( array(
'error' => true,
'message' => 'Товар нельзя купить'
) );
wp_die();
}
// Проверяем наличие
if ( ! $product->is_in_stock() ) {
wp_send_json( array(
'error' => true,
'message' => 'Товар не в наличии'
) );
wp_die();
}
$passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity );
if ( ! $passed_validation ) {
wp_send_json( array(
'error' => true,
'message' => 'Ошибка валидации товара'
) );
wp_die();
}
// Добавляем в корзину
$cart_item_key = WC()->cart->add_to_cart( $product_id, $quantity, $variation_id );
if ( $cart_item_key ) {
do_action( 'woocommerce_ajax_added_to_cart', $product_id );
if ( 'yes' === get_option( 'woocommerce_cart_redirect_after_add' ) ) {
wc_add_to_cart_message( array( $product_id => $quantity ), true );
}
WC_AJAX::get_refreshed_fragments();
} else {
wp_send_json( array(
'error' => true,
'message' => 'Не удалось добавить товар в корзину',
'product_url' => apply_filters( 'woocommerce_cart_redirect_after_error', get_permalink( $product_id ), $product_id )
) );
}
wp_die();
}