Перейти к основному содержимому

Остатки и заказы МоегоСклада в таблицу

Скрипт забирает из МоегоСклада остатки товаров и заказы покупателей через его 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. Адрес и подписка​

  1. На вкладке «Триггеры» добавьте «По вебхуку» с функцией onWebhook и скопируйте адрес вида https://app.coverse.team/api/hooks/scripts/whs_…. МойСклад не умеет передавать свои заголовки, поэтому подходит именно адрес с секретом.
  2. Вставьте функцию подписки, подставьте адрес в 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, как в примере из статьи.

Что дальше​