Каждая компания хочет, чтобы её товары стояли на полках всех магазинов — чистенькие, с нормальным сроком годности, этикеткой к покупателю, на проходном месте и на удобной высоте. Бери и неси на кассу. Но жизнь, как говорится, вносит свои коррективы. Поэтому мерчандайзер регулярно объезжает магазины, чтобы проверить, как всё расставлено на самом деле.
У нашего заказчика есть продукция, которую он производит и поставляет в розничные магазины по всей России. Мерчандайзеры — его глаза и руки. Не хватало только удобного технологического инструмента, который поможет контролировать представленность товара, а случись что — оперативно сообщать о сбоях, чтобы как можно быстрее всё исправить.
Мы сделали решение на базе мобильного приложения и портала, которые оцифровали работу мерчандайзера. Ниже опишем подробности, поделимся примерами кода и техническими идеями, которые помогли упростить работу пользователей и снизили риски недобросовестного отношения и мошенничества. Мы решили рассказать всё это, потому что самим интересно посмотреть со стороны, как оцифровывается процесс с бумаги — это словно построить самолет на кульмане или в 3D-виде.
Было: нет времени объяснять, кидай в чатик
Мерчандайзеры нашего клиента ежедневно проверяют по несколько магазинов. Не редко случались и накладки, когда в одном магазине побывали несколько человек, а в другой никто не заглянул.
Сухой отчет о посещенных магазинах ранее сотрудник забивал в программу на смартфоне. А всю дополнительную информацию — фотоотчет с результатами работы на точке — скидывал супервайзеру в Viber. Поток снимков превращался в спам.
Бывает, что выкладка в магазине отличается от договоренностей — не подвезли товар, часть оказалась бракованной или по другой причине. Эта информация тоже уходила в Viber, там и оседала безо всякого учета и структурирования. У осмотров не было единой технологии — одно пересчитал, про другое забыл, качество раскладки и долю на полке вообще никто не оценивал.
Из чатиков мерчандайзеру сыпались дополнительные задания: «Проверьте, сколько стоит майонез конкурента в разных сетях» или «Как там POS-материалы к 8 марта, лежат?». Информация от руководства шла долго и терялась среди других сообщений. Непонятно, получил ли сотрудник твоё указание, а если получил, то когда исполнит.
Поставим себя на место менеджера. Мы хотим знать, где за день побывал мерчендайзер и с каким результатом. Покупатели и конкуренты быстро могут поменять его красивую выкладку, вот почему важно качество осмотров и регулярность визитов в каждый магазин.
Стало: Объединяем все каналы информации и получаем мобильное приложение для мерчендайзеров и портал для руководителей и супервайзеров
Для мерчандайзеров мы сделали мобильное приложение на Android, куда регулярно подгружается план работы на день. Через приложение работник сообщает о начале и окончании работы, заполняет информацию о наличии товара в каждом магазине, а если что-то выложено не так — сигнализирует о сбое в доставке или маркетинговой акции. Например, в «Ленте» не проводится акция в честь Нового года, хотя должна идти полным ходом. Повод разобраться.
Вся информация поступает на портал для руководителей: супервайзеров и территориальных менеджеров. Разграничение прав доступа происходит по сетевому логину и паролю.
На портале планируется маршрут мерчандайзера, а потом можно легко проверить его результаты. План проверок автоматически формируется на портале на основании информации о режиме работы магазинов и выходных работников. В серверной части мы получаем, храним и обрабатываем информацию о проверках магазинов. Там же создаем каждое посещение и привязываем к нему фотографии, сообщения об инцидентах и другие подробности.
Мы синхронизируемся с сервисом заказчика, который хранит информацию обо всех магазинах, категориях товаров и брендах, которыми торгует наш клиент. Потом сервер отдает эту информацию на портал супервайзерам и в мобильное приложение мерчандайзерам.
Мерчендайзер и супервайзер сообщают об инцидентах через приложение, приложив подтверждающую фотографию. На портале эту информацию получают и проверяют все заинтересованные участники. Отчеты отображаются в виде таблиц. Данные можно сортировать, анализировать и решать — повторить проверку через неделю или исправить выкладку завтра первым делом. Так руководители могут не теряя времени разбираться с причинами и устранить проблему. Стало проще формировать отчеты.
Взаимодействия портала с учетными системами и мобильным приложением:
Техническая реализация
Теперь расскажем подробнее, как мы реализовали «слежку» за мерчандайзером (что он действительно проверяет магазин), программно обеспечили качественные фотоотчеты, отслеживаем, что сотрудник проверил все, и пересылаем информацию о посещении на сервер.
1. Стой там, иди сюда: как мы проверяем, где находится работник
В начале рабочего дня мерчендайзер запускает приложение и получает список магазинов на сегодня.
Чтобы начать проверку, он должен находиться в 400 метрах от магазина или ближе. Координаты торговых точек подгружаются в приложение с портала, а приложение использует комбинированный подход — положение пользователя определяется через GPS и WiFi беспроводных сетей.
Во многих местах в нашем приложении мы используем rxJava, в том числе и для получения координат. Чтобы определить местоположение, мы используем метод
observeSingleLocation(LocationRequest locationRequest)
.
public Observable<Location> observeSingleLocation(
LocationRequest locationRequest) {
return locationService.lastKnownLocation()
.filter(this::isFreshLocation)
.first()
.onErrorResumeNext(throwable -> {
if (throwable instanceof NoSuchElementException) {
return checkSettingsAndGetUpdates(locationRequest);
}
return Observable.error(throwable);
});
}
Здесь мы обращаемся к locationService, который возвращает нам последние доступные координаты устройства. Полученные координаты мы «проверяем на свежесть», и если они не проходят (а свежими мы считаем координаты, полученные не более 2-х минут назад), мы вызываем метод checkSettingsAndGetUpdates(locationRequest)
, в котором проверяем возможность получения координат с указанными требованиями. Если их можно получить — подписываемся на получение первых доступных координат с помощью:
locationService.locationUpdates(locationRequest).take(1)
.
private Observable<Location>
checkSettingsAndGetUpdates(LocationRequest locationRequest) {
return checkSettingsObservable(locationRequest)
.concatMap(locationSettingsResult ->
locationService.locationUpdates(locationRequest).take(1)
);
}
private Observable<LocationSettingsResult>
checkSettingsObservable(LocationRequest locationRequest) {
LocationSettingsRequest settingsRequest
= new LocationSettingsRequest.Builder()
.addLocationRequest(locationRequest)
.setAlwaysShow(true)
.build();
return locationService.checkLocationSettings(settingsRequest)
.flatMap(locationSettingsResult -> {
if (locationSettingsResult.getStatus().getStatusCode() == SUCCESS)
{
return Observable.just(locationSettingsResult);
}
return Observable.error(
new GeoException(locationSettingsResult.getStatus()));
});
}
Когда мерчандайзер входит в магазин, то сообщает об этом в приложении, нажав соответствующую кнопку.
У каждого магазина уже есть список товаров, которые нужно проверить, и инциденты, которые тут уже были.
Итак, работник начал проверку ассортимента. В мобильном приложении он выбирает бренд и получает список всех SKU (идентификаторов товарной позиции), которые поставляются в эту торговую точку. О каждом товаре нужно заполнить подробную информацию и можно внести рацпредложение, как улучшить ситуацию.
Каждую выкладку надо сфотографировать. Качество фотографий мы проверяем сразу и делаем это системно, чтобы потом не было досадных неожиданностей, когда снова нужно идти в магазин и все перефотографировать. Байты, которые являются фотографией, мы сохраняем в файловую систему, получая снимок JPG формата качества 80 % от оригинала. Этот файл потом отправляется на сервер. 80 % — это хороший показатель, при котором видно продукты на полках. Его мы задаём через API камеры. Фотографии весят 1-2 Мб в зависимости от качества камеры. Если будет не хватать места на смартфоне, уменьшим показатель до 70 %.
Углубимся в детали. При открытии экрана инициализируем вьюшку-камеру с параметрами:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_photo);
...
binding.cameraView.setAutoPreview(true);
binding.cameraView.setCallback(this);
binding.cameraView.setPictureQuality(80);
binding.cameraView.setFlashMode(CameraView.FLASH_MODE_ON);
binding.cameraView.setForceLegacyCamera(true);
binding.takePictureBtn.setOnClickListener(
v -> getPresenter().onTakePicture(binding.cameraView));
binding.cameraView.setOnTouchListener(
new PinchToZoomTouchListener(binding.cameraView));
}
После того, как фото сделано, камера передает нам массив байтов, который мы сохраняем в файл.
private void onPhotoTaken(final byte[] bytes) {
saveFileSubscription = Observable.fromCallable(
() -> fileCommand.saveFile(getView().getContext().getFilesDir(), bytes))
.subscribe(new Subscriber<String>() {
@Override
public void onCompleted() {
saveFileSubscription = null;
}
@Override
public void onError(Throwable e) {
if (!TextUtils.isEmpty(e.getLocalizedMessage())) {
if (e instanceof IOException
&& e.getLocalizedMessage().contains("ENOSPC")) {
getStateModel().errorMessage.set(
R.string.photo_no_space_left_error);
}
} else {
getStateModel().errorMessage.set(
e.getLocalizedMessage());
}
saveFileSubscription = null;
}
@Override
public void onNext(String photoPath) {
//Сохраняем фотографию в модель состояния экрана и передаем её в активность, чтобы отобразить её там
Uri savedPhotoPath = Uri.fromFile(new File(photoPath));
getStateModel().savedPhotoPaths.add(savedPhotoPath);
getStateModel().previewPhoto = new UriPhoto(savedPhotoPath);
sendStateModel();
}
});
}
Фотографии сделаны. О тонкостях их отправки на сервер подробнее расскажем далее, а пока вернемся к осмотру.
Если выкладка нарушена, мерчандайзер сообщает об этом, создав инцидент — тип проблемы, описание и приложив подтверждающие фотографии. Здесь же можно посмотреть «историю вопроса»: какие инциденты уже замечены в этом магазине и их судьбу.
2. Полный отчет: без результатов не уйдешь
Итак, мерчандайзер проверил все товары. Но нельзя просто взять и завершить посещение, если ты вписал только половину брендов. Приложение позволит закончить работу в магазине только после внесения данных по всем товарным категориям. Так мы автоматически проверим полноту загруженной информации. В конце работы мерчендайзер увидит сводные результаты посещения, магазин перейдет в раздел «Посещенные», а данные с результатами посещения улетят на сервер (портал).
Фотографии посещения тяжелые, а качество мобильного интернета может хромать. Мерчендайзер может настроить в приложении отправку изображений только по сети WiFi и завершить отчет сразу у выхода, а фотографии дослать в течение дня.
Отправка данных на сервер состоит из пяти ступеней. Когда мерчандайзер завершает проверку торговой точки, в первую очередь из приложения отправляется ядро — ключевая информация о посещении. Сервер подтверждает доставку, и вслед улетают фотографии посещения. Информация о посещении появляется на портале и готова к просмотру.
Последовательность отправки информации о посещении на сервер выглядит вот так:
Фотография привязывается к посещению на сервере, поэтому и уходит второй ступенью. Если базовой информации нет, мы не знаем, к чему относятся эти снимки. Все фотографии отправляем по одной. Конечно, проще и быстрее отправить их вместе, но тогда рискуем перегрузить оперативную память приложения. Если работник не хочет тратить мобильный интернет, может отправить их позже.
Потом на сервер уносятся сообщение об инциденте, подтверждающие фотографии и комментарии.
Чтобы все данные обязательно улетели, мы реализовали периодическую отправку данных на сервер через JobService.
В нашем Application классе задаем, чтобы задача по отправке данных на сервер выполнялась каждые 60 минут на случай, если что-то недоотправилось.
@Override
public void onCreate() {
super.onCreate();
...
SyncJobSchedulerManager syncJobSchedulerManager
= new SyncJobSchedulerManager(this);
syncJobSchedulerManager.setSendDataToServerPeriodic(
TimeUnit.MINUTES.toMillis(60));
...
}
public void setSendDataToServerPeriodic(long millis) {
JobScheduler jS = (JobScheduler) context.getSystemService(
Context.JOB_SCHEDULER_SERVICE);
jS.schedule(JobSchedulerService.getSendDataJobPeriodic(
context.getPackageName(), millis));
}
В результате каждый час у нас запускается служба, которая отправляет данные о выполненных посещениях на сервер в заднем фоне.
public class JobSchedulerService extends JobService {
public static JobInfo getSendDataJobPeriodic(String package, long millis) {
JobInfo.Builder builder = new JobInfo.Builder(SEND_DATA_JOB_PERIODIC_ID, new ComponentName(package, JobSchedulerService.class.getName()));
builder.setPeriodic(millis);
return builder.build();
}
...
@Override
public boolean onStartJob(JobParameters params) {
if (params.getJobId() == SEND_DATA_JOB_PERIODIC_ID && isNetworkAvailable()) {
startService(
WorkService.getServiceSendAllIntent(getApplicationContext()));
}
return false;
}
Теперь наверняка в конце дня вся информация о посещении и результатах проверок отобразится на портале.
3. Хотелось бы знать точно: как мы замеряем время проверки
Мы фиксируем, сколько времени ушло на проверку каждой торговой точки. Если работник провёл полдня в магазине на три ряда, что-то явно пошло не так :)
Самый простой вариант фиксации — отслеживать местоположение человека постоянно. Мы сразу от него отказались, поскольку это быстро посадит батарею. Можно было организовать работу в офлайн-режиме: пусть пользователь заранее подгружает список всех торговых точек и работает по нему весь день. От этого способа отказались, поскольку любой хитрец может перевести время на телефоне вручную.
Второй вариант: учитывать внутренний счетчик наносекунд с запуска смартфона. Такая возможность есть во всех смартфонах на базе Android. Когда клиент обращается к серверу с сообщением о начале проверки, мы спрашиваем сервер, сколько сейчас времени, а у смартфона — сколько времени он был включен. Затем сверяем оба значения и вычисляем без использования интернета, сколько времени прошло c момента последней синхронизации. Так можно понять, сколько времени прошло на самом деле. Но есть важное условие — счетчик должен быть точным и надежным.
Для этого можно использовать статистический метод SystemClock.elapsedRealtimeNanos();. Более подробно читайте о нём на официальном сайте Android.
Мы остановились на варианте учёта посещения по времени сервера. Когда мерчандайзер начинает и заканчивает работу, приложение посылает запрос на сервер о текущем времени. На сервере фиксируется время начала и окончания посещения. Так вычисляется интервал, сколько мерчандайзер провёл в магазине.
Чтобы избежать жульничества, мы исключаем возможность для пользователя и мобильного приложения указать время. Сервер непогрешим и всегда сообщит нам точную информацию. Если же приложению не удалось получить время от сервера (например, нет интернета), то посещение не начнется.
Итого
Решение заменило привычные способы отчета полевых сотрудников. Работа мерчандайзера стала более прозрачной и предсказуемой. Руководителям теперь проще контролировать дистрибуцию и оперативно узнать о сбоях в поставке товара. Мы заменили несколько разрозненных инструментов одним.
Дальше продолжим работу над мониторингом инцидентов и дисциплины, чтобы эти показатели влияли на доходы сотрудников. В планах также добавить анкетирование сотрудников и инструмент для мониторинга конкурентов. И, наконец, думаем научить искусственный интеллект распознать по фотографии, какую долю полки занимает наша продукция и какие цены у конкурента.
Автор: eastbanctech