Остатки и заказы МоегоСклада в таблицу
Скрипт забирает из МоегоСклада остатки товаров и заказы покупателей через его JSON API. По расписанию — остатки и все изменённые заказы, через вебхук — новый заказ сразу после создания.
Что понадобится:
- таблица Coverse на тарифе «Бизнес» или выше;
- пользователь МоегоСклада с правами на просмотр товаров, остатков и заказов покупателей;
- для вебхуков — права администратора и платный тариф МоегоСклада;
- 15 минут.
Доступ к API
Скрипт входит в API МоегоСклада логином и паролем пользователя. Лучше завести для обмена отдельного пользователя с правами только на чтение.
На вкладке «Секреты» скрипта добавьте:
MS_LOGIN— логин видаобмен@компания;MS_PASSWORD— пароль.
МойСклад принимает только ответы в сжатии gzip и на запрос с другим списком сжатий отвечает кодом 415 без текста ошибки. Функция ms в примерах ниже передаёт заголовок Accept-Encoding: gzip сама.
Функция ms отправляет логин и пароль только на адреса API МоегоСклада. Это важно для вебхука: ссылки на заказ приходят в теле запроса, и чужой запрос мог бы подставить свой адрес.
Остатки товаров
Скрипт раз в час переписывает лист «Остатки» свежим отчётом.
const MS = "https://api.moysklad.ru/api/remap/1.2/";
function stock() {
const rows = [];
for (let offset = 0; offset < 10000; offset += 1000) {
const data = ms("report/stock/all", { limit: 1000, offset: offset });
for (const item of data.rows) {
rows.push([
asText(item.code),
asText(item.article),
asText(item.name),
item.stock,
item.reserve,
item.quantity,
(Number(item.salePrice) || 0) / 100,
]);
}
if (data.rows.length < 1000) break;
}
const sheet = Coverse.table.getSheetByName("Остатки") || Coverse.table.insertSheet("Остатки");
sheet.getRange("A1:G").clear();
sheet.getRange(1, 1, 1, 7).setValues([["Код", "Артикул", "Товар", "Остаток", "Резерв", "Доступно", "Цена продажи"]]);
if (rows.length > 0) sheet.getRange(2, 1, rows.length, 7).setValues(rows);
console.log("Товаров:", rows.length);
}
function ms(path, query) {
const login = Secrets.get("MS_LOGIN");
const password = Secrets.get("MS_PASSWORD");
if (!login || !password) throw new Error("Добавьте секреты MS_LOGIN и MS_PASSWORD");
const url = path.indexOf("https://") === 0 ? path : MS + path;
if (url.indexOf(MS) !== 0) throw new Error("Адрес не из API МоегоСклада: " + url.slice(0, 100));
const response = Http.get(url, {
headers: {
Authorization: "Basic " + Utilities.base64Encode(login + ":" + password),
"Accept-Encoding": "gzip",
},
query: query,
});
if (!response.ok) throw new Error("МойСклад ответил с кодом " + response.status + ": " + response.text().slice(0, 300));
return response.json();
}
function asText(value) {
const text = String(value === undefined || value === null ? "" : value);
return text.charAt(0) === "=" ? "'" + text : text;
}
stock— остаток на складах,reserve— резерв,quantity— доступно с учётом резерва и ожидания.- Цены в МоемСкладе хранятся в копейках, поэтому скрипт делит их на 100.
- Скрипт очищает только столбцы A:G листа «Остатки». Свои формулы держите правее или на другом листе.
- Отчёт по остаткам дорогой для лимитов МоегоСклада, поэтому не запускайте его чаще раза в 15 минут.
- Скрипт загружает до 10 000 товаров. Если их больше, увеличьте предел в цикле, но помните о 30 секундах на запуск.
Заказы покупателей
Функция orders забирает заказы, изменённые после прошлой выгрузки, и обновляет их строки на листе «Заказы МойСклад». Добавьте её в тот же скрипт.
function orders() {
const since = Properties.script.get("msUpdated")
|| Utilities.formatDate(Utilities.addDays(Utilities.today(), -7), "yyyy-MM-dd HH:mm:ss", "Europe/Moscow");
const rows = [];
let newest = since;
for (let offset = 0; offset < 1000; offset += 100) {
const data = ms("entity/customerorder", {
filter: "updated>=" + since,
order: "updated,asc",
expand: "agent,state",
limit: 100,
offset: offset,
});
for (const order of data.rows) {
rows.push(orderRow(order));
if (order.updated > newest) newest = order.updated;
}
if (data.rows.length < 100) break;
}
if (rows.length === 0) {
console.log("Изменений нет");
return;
}
saveOrders(rows);
Coverse.table.flush();
Properties.script.set("msUpdated", newest.slice(0, 19));
console.log("Заказов обновлено:", rows.length);
}
function orderRow(order) {
return [
order.id,
asText(order.name),
Utilities.toDate(String(order.moment).slice(0, 19)) || "",
asText(order.agent && order.agent.name),
asText(order.state && order.state.name),
(Number(order.sum) || 0) / 100,
];
}
function saveOrders(rows) {
const sheet = Coverse.table.getSheetByName("Заказы МойСклад") || Coverse.table.insertSheet("Заказы МойСклад");
if (sheet.getLastRow() === 0) sheet.appendRow(["ID", "Номер", "Дата", "Контрагент", "Статус", "Сумма"]);
const keys = sheet.getRange(1, 1, sheet.getLastRow(), 1).getValues();
const index = new Map(keys.map((row, i) => [String(row[0]), i + 1]));
const added = new Map();
for (const row of rows) {
const at = index.get(String(row[0]));
if (at === undefined) {
added.set(String(row[0]), row);
} else {
sheet.getRange(at, 1, 1, 6).setValues([row]);
}
}
if (added.size > 0) sheet.appendRows([...added.values()]);
}
Что важно в коде:
- Фильтр
updated>=берёт заказы, изменённые начиная с прошлой отметки. Время в фильтре московское, в формате2026-10-19 12:30:00. В ответах МойСклад добавляет миллисекунды, поэтому скрипт отрезает их перед разбором даты. expand=agent,stateподставляет в ответ названия контрагента и статуса. МойСклад раскрывает связанные объекты, только когдаlimitне больше 100, поэтому заказы загружаются по 100.Coverse.table.flush()отправляет строки в таблицу до того, как скрипт сдвинет отметку в «Памяти». Если запись не удастся, следующий запуск загрузит эти заказы снова.- Первая выгрузка берёт заказы, изменённые за последние 7 дней.
Расписание
На вкладке «Триггеры» добавьте два триггера «По расписанию»: функция stock раз в час и функция orders каждые 15 или 30 минут.
Новые заказы сразу: вебхук МоегоСклада
МойСклад умеет сам сообщать о новых заказах. В уведомлении приходит не заказ, а ссылка на него, поэтому скрипт получает уведомление и сам запрашивает заказ.
1. Функция для вебхука
Добавьте в тот же скрипт:
function onWebhook(event) {
if (!event || !event.json || !event.json.events) return;
const rows = [];
const events = event.json.events.filter((item) => item.meta && item.meta.type === "customerorder");
for (const item of events.slice(0, 40)) {
rows.push(orderRow(ms(item.meta.href, { expand: "agent,state" })));
}
if (events.length > 40) console.warn("Заказов в уведомлении больше 40, остальные загрузит функция orders по расписанию");
if (rows.length > 0) saveOrders(rows);
}
2. Адрес и подписка
- На вкладке «Триггеры» добавьте «По вебхуку» с функцией
onWebhookи скопируйте адрес видаhttps://app.coverse.team/api/hooks/scripts/whs_…. МойСклад не умеет передавать свои заголовки, поэтому подходит именно адрес с секретом. - Вставьте функцию подписки, подставьте адрес в
HOOK_URLи один раз запуститеsubscribe. Подписку создаёт администратор МоегоСклада, поэтому на время запуска в секретах должен быть его логин и пароль.
const HOOK_URL = "https://app.coverse.team/api/hooks/scripts/whs_…";
function subscribe() {
const response = Http.post(MS + "entity/webhook", {
url: HOOK_URL,
action: "CREATE",
entityType: "customerorder",
}, {
headers: {
Authorization: "Basic " + Utilities.base64Encode(Secrets.get("MS_LOGIN") + ":" + Secrets.get("MS_PASSWORD")),
"Accept-Encoding": "gzip",
},
});
console.log("МойСклад ответил", response.status, response.text().slice(0, 300));
}
Чтобы получать и изменения заказов, создайте вторую подписку с action: "UPDATE".
За один запуск скрипт может сделать 50 внешних запросов, поэтому из одного уведомления он загружает до 40 заказов, остальные подхватит функция orders по расписанию. Держите оба способа включёнными.
МойСклад ждёт ответ 200 за полторы секунды. Coverse отвечает сразу, а скрипт выполняется после ответа. Если адрес несколько дней подряд отвечает ошибками, МойСклад выключит вебхук и сообщит владельцу аккаунта на почту.
Лимиты МоегоСклада
- Запросы с логином и паролем МойСклад считает дороже, чем запросы приложений: на один аккаунт пользователя приходится около 15 запросов за 3 секунды, и это число МойСклад планирует снижать.
- Отчёт по остаткам стоит как несколько обычных запросов.
- После сотен ошибочных запросов в минуту МойСклад временно закрывает пользователю доступ к API. Если скрипт падает на каждом запуске, не ставьте ему частое расписание, пока не исправите ошибку.
Актуальные лимиты — в разделе «Ограничения» документации МоегоСклада.
Если что-то не работает
| Симптом | Причина | Что сделать |
|---|---|---|
| код 401 | неверный логин или пароль | проверьте секреты MS_LOGIN и MS_PASSWORD |
| код 403 | у пользователя нет прав на объект или вебхуки недоступны на тарифе | проверьте права пользователя и тариф МоегоСклада |
| код 415 без текста | МойСклад не принял список сжатий в запросе | передавайте Accept-Encoding: gzip, как функция ms из примера |
| «Адрес не из API МоегоСклада» | в уведомление подставлена чужая ссылка | проверьте, кто знает адрес вебхука; при необходимости перевыпустите его в карточке триггера |
| код 429 | превышен лимит запросов | запускайте реже, разнесите stock и orders по времени |
| в столбце «Контрагент» пусто | связанный объект не раскрылся | проверьте expand и что limit не больше 100 |
| вебхук не приходит | подписку создал не администратор или тариф бесплатный | создайте подписку от имени администратора |
Частые вопросы
Можно ли выгружать остатки МоегоСклада в таблицу?
Да. Скрипт по расписанию загружает отчёт по остаткам через JSON API МоегоСклада и переписывает лист «Остатки».
Как получать новые заказы МоегоСклада сразу?
Создайте вебхук МоегоСклада на адрес триггера скрипта: МойСклад сообщит о заказе, а скрипт загрузит его из API.
Почему МойСклад отвечает кодом 415?
МойСклад принимает только ответы в сжатии gzip. Передавайте заголовок Accept-Encoding: gzip, как в примере из статьи.