diff --git a/src/endpoints/index.ts b/src/endpoints/index.ts index 27fba00..fa19a7d 100644 --- a/src/endpoints/index.ts +++ b/src/endpoints/index.ts @@ -31,3 +31,4 @@ export * from "./production-task"; export * from "./assortment"; export * from "./purchase-order"; export * from "./production-stage"; +export * from "./inventory"; diff --git a/src/endpoints/inventory/endpoint.ts b/src/endpoints/inventory/endpoint.ts new file mode 100644 index 0000000..56afaaf --- /dev/null +++ b/src/endpoints/inventory/endpoint.ts @@ -0,0 +1,191 @@ +import { + Entity, + type BatchGetResult, + type GetFindResult, + type ListResponse, + type Subset, + MediaType, + type BatchDeleteResult, + type GetModelCreatableFields, + type GetModelUpdatableFields, + type UpdateMeta, +} from "../../types"; +import { BaseEndpoint } from "../base-endpoint"; +import type { + AllInventoryOptions, + CreateInventoryOptions, + InventoryModel, + FirstInventoryOptions, + GetInventoryOptions, + ListInventoryOptions, + UpdateInventoryOptions, +} from "./types"; +import { composeSearchParameters } from "../../api-client"; + +const ENDPOINT_URL = "/entity/inventory"; + +export class InventoryEndpoint extends BaseEndpoint { + /** + * Получить массив инвентаризаций. + * + * @param options - Опции для получения инвентаризаций {@linkcode ListInventoryOptions} + * @returns Объект с массивом инвентаризаций + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/documents/#dokumenty-inwentarizaciq-poluchit-inwentarizacii + */ + async list>( + options?: Subset, + ): Promise< + ListResponse, Entity.Inventory> + > { + const searchParameters = composeSearchParameters(options ?? {}); + + const response = await this.client.get(ENDPOINT_URL, { + searchParameters, + }); + return response.json(); + } + + /** + * Получить все инвентаризации. + * + * @param options - Опции для получения всех инвентаризаций {@linkcode AllInventoryOptions} + * @returns Объект с массивом инвентаризаций + */ + async all>( + options?: Subset, + ): Promise< + BatchGetResult, Entity.Inventory> + > { + return this.client.batchGet( + async (limit, offset) => + this.list({ + ...options, + pagination: { limit, offset }, + }), + Boolean(options?.expand), + ); + } + + /** + * Получить первую инвентаризацию. + * + * @param options - Опции для получения первой инвентаризации {@linkcode FirstInventoryOptions} + * @returns Объект с первой инвентаризацией + */ + async first>( + options?: Subset, + ): Promise< + ListResponse, Entity.Inventory> + > { + return this.list({ ...options, pagination: { limit: 1 } }); + } + + /** + * Получить инвентаризацию по id. + * + * @param id - id инвентаризации + * @param options - Опции для получения инвентаризации {@linkcode GetInventoryOptions} + * @returns Объект с инвентаризацией {@linkcode InventoryModel} + */ + async get>( + id: string, + options?: Subset, + ): Promise> { + const searchParameters = composeSearchParameters(options ?? {}); + + const response = await this.client.get(`${ENDPOINT_URL}/${id}`, { + searchParameters, + }); + + return response.json(); + } + + /** + * Создать инвентаризацию. + * + * @param data - данные для создания инвентаризации + * @param options - Опции для создания инвентаризации {@linkcode CreateInventoryOptions} + * @returns Объект с созданной инвентаризацией {@linkcode InventoryModel} + */ + async create>( + data: GetModelCreatableFields, + options?: Subset, + ): Promise> { + const searchParameters = composeSearchParameters(options ?? {}); + + const response = await this.client.post(ENDPOINT_URL, { + body: data, + searchParameters, + }); + + return response.json(); + } + + /** + * Обновить инвентаризацию. + * + * @param id - id инвентаризации + * @param data - данные для обновления инвентаризации + * @param options - Опции для обновления инвентаризации {@linkcode UpdateInventoryOptions} + * @returns Объект с обновленной инвентаризацией {@linkcode InventoryModel} + */ + async update>( + id: string, + data: GetModelUpdatableFields, + options?: Subset, + ): Promise> { + const searchParameters = composeSearchParameters(options ?? {}); + + const response = await this.client.put(`${ENDPOINT_URL}/${id}`, { + body: data, + searchParameters, + }); + + return response.json(); + } + + /** + * Массово создать или обновить инвентаризации. + * + * @param data - массив данных для создания или обновления инвентаризаций + * @param options - Опции для создания инвентаризаций {@linkcode CreateInventoryOptions} + * @returns Массив с созданными или обновленными инвентаризациями {@linkcode InventoryModel} + */ + async upsert>( + data: ( + | GetModelCreatableFields + | (GetModelUpdatableFields & UpdateMeta) + )[], + options?: Subset, + ): Promise[]> { + const searchParameters = composeSearchParameters(options ?? {}); + + const response = await this.client.post(ENDPOINT_URL, { + body: data, + searchParameters, + }); + + return response.json(); + } + + /** + * Массово удалить инвентаризации. + * + * @param ids - массив id инвентаризаций + * @returns Массив с результатами удаления инвентаризаций + */ + async batchDelete(ids: string[]): Promise { + const response = await this.client.post(`${ENDPOINT_URL}/delete`, { + body: ids.map((id) => ({ + meta: { + href: this.client.buildUrl(`${ENDPOINT_URL}/${id}`), + type: Entity.Inventory, + mediaType: MediaType.Json, + }, + })), + }); + + return response.json(); + } +} diff --git a/src/endpoints/inventory/index.ts b/src/endpoints/inventory/index.ts new file mode 100644 index 0000000..5764dd5 --- /dev/null +++ b/src/endpoints/inventory/index.ts @@ -0,0 +1,2 @@ +export { InventoryEndpoint } from "./endpoint"; +export * from "./types"; diff --git a/src/endpoints/inventory/types.ts b/src/endpoints/inventory/types.ts new file mode 100644 index 0000000..a34e5e0 --- /dev/null +++ b/src/endpoints/inventory/types.ts @@ -0,0 +1,205 @@ +import { + type AssortmentEntity, + type AssortmentModel, + type Attribute, + type BooleanFilter, + type DateTime, + type DateTimeFilter, + type Entity, + type ExpandOptions, + type FilterOptions, + type IdFilter, + type Idable, + type ListMeta, + type Meta, + type Model, + type NumberFilter, + type OrderOptions, + type PaginationOptions, + type StringFilter, +} from "../../types"; +import type { GroupModel } from "../group"; +import type { OrganizationModel } from "../organization"; +import type { EmployeeModel } from "../employee"; +import type { EnterModel } from "../enter"; + +export interface InventoryPosition + extends Idable, + Meta { + /** ID учетной записи */ + readonly accountId: string; + + /** Метаданные товара/услуги/серии/модификации, которую представляет собой позиция */ + assortment: Meta; + + /** Расчетный остаток */ + calculatedQuantity: number; + + /** Разница между расчетным остатком и фактическим */ + readonly correctionAmount: number; + + /** Избыток/недостача */ + readonly correctionSum: number; + + /** + * Упаковка Товара. + * @see https://dev.moysklad.ru/doc/api/remap/1.2/dictionaries/#suschnosti-towar-towary-atributy-wlozhennyh-suschnostej-upakowki-towara + */ + pack?: unknown; // TODO: add pack type + + /** Цена товара/услуги в копейках */ + price: number; + + /** Количество товаров/услуг данного вида в позиции */ + quantity: number; +} + +export interface InventoryPositionModel extends Model { + object: InventoryPosition; + expandable: { + assortment: AssortmentModel; + }; +} + +export interface Inventory extends Idable, Meta { + /** ID учетной записи */ + readonly accountId: string; + + /** Коллекция метаданных доп. полей */ + attributes?: Attribute[]; + + /** Код */ + code?: string; + + /** Дата создания */ + readonly created: DateTime; + + /** Момент последнего удаления */ + readonly deleted?: DateTime; + + /** Комментарий */ + description?: string; + + /** Внешний код */ + externalCode: string; + + /** Метаданные массива Файлов */ + files: unknown[]; // TODO: add files type + + /** Отдел сотрудника */ + group: Meta; + + /** Дата документа */ + moment: DateTime; + + /** Наименование */ + name: string; + + /** Метаданные юрлица */ + organization: Meta; + + /** Владелец (Сотрудник) */ + owner?: Meta; + + /** Метаданные позиций Инвентаризации */ + positions: ListMeta; + + /** Напечатан ли документ */ + readonly printed: boolean; + + /** Опубликован ли документ */ + readonly published: boolean; + + /** Общий доступ */ + shared: boolean; + + /** Метаданные статуса Инвентаризации */ + state?: Meta; + + /** Метаданные склада */ + store: Meta; // TODO: add store expand + + /** Сумма Инвентаризации в копейках */ + readonly sum: number; + + /** ID синхронизации */ + syncId?: string; + + /** Момент последнего обновления */ + readonly updated: DateTime; + + /** Связанные оприходования */ + enters?: Meta[]; + + /** Связанные списания */ + losses?: Meta[]; // TODO: add loss expand +} + +export interface InventoryModel extends Model { + object: Inventory; + expandable: { + group: GroupModel; + organization: OrganizationModel; + owner: EmployeeModel; + enters: EnterModel; + positions: InventoryPositionModel; + }; + filters: { + id: IdFilter; + accountId: IdFilter; + code: StringFilter; + created: DateTimeFilter; + deleted: DateTimeFilter; + description: StringFilter; + externalCode: StringFilter; + group: IdFilter; + moment: DateTimeFilter; + name: StringFilter; + organization: IdFilter; + owner: IdFilter; + printed: BooleanFilter; + published: BooleanFilter; + shared: BooleanFilter; + state: IdFilter; + store: IdFilter; + sum: NumberFilter; + syncId: IdFilter; + updated: DateTimeFilter; + isDeleted: BooleanFilter; + }; + orderableFields: + | "id" + | "syncId" + | "updated" + | "name" + | "description" + | "externalCode" + | "moment" + | "sum" + | "created"; + requiredCreateFields: "organization" | "store"; +} + +export interface ListInventoryOptions { + pagination?: PaginationOptions; + expand?: ExpandOptions; + order?: OrderOptions; + search?: string; + filter?: FilterOptions; + namedfilter?: string; +} + +export interface CreateInventoryOptions { + expand?: ExpandOptions; +} + +export interface UpdateInventoryOptions { + expand?: ExpandOptions; +} + +export interface GetInventoryOptions { + expand?: ExpandOptions; +} + +export type FirstInventoryOptions = Omit; +export type AllInventoryOptions = Omit; diff --git a/src/moysklad.ts b/src/moysklad.ts index 0f33161..acb881d 100644 --- a/src/moysklad.ts +++ b/src/moysklad.ts @@ -22,6 +22,7 @@ import { AssortmentEndpoint, PurchaseOrderEndpoint, ProductionStageEndpoint, + InventoryEndpoint, } from "./endpoints"; /** @@ -185,6 +186,13 @@ export class Moysklad { */ public productionStage: ProductionStageEndpoint; + /** + * Инвентаризации + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/documents/#dokumenty-inwentarizaciq + */ + public inventory: InventoryEndpoint; + constructor(options: ApiClientOptions) { this.client = new ApiClient(options); this.bonusTransaction = new BonusTransactionEndpoint(this.client); @@ -210,6 +218,7 @@ export class Moysklad { this.assortment = new AssortmentEndpoint(this.client); this.purchaseOrder = new PurchaseOrderEndpoint(this.client); this.productionStage = new ProductionStageEndpoint(this.client); + this.inventory = new InventoryEndpoint(this.client); } } diff --git a/src/types/entity.ts b/src/types/entity.ts index 5614d1c..5b58ab7 100644 --- a/src/types/entity.ts +++ b/src/types/entity.ts @@ -81,6 +81,9 @@ export enum Entity { InvoicePosition = "invoiceposition", EnterPosition = "enterposition", SupplyPosition = "supplyposition", + Inventory = "inventory", + InventoryPosition = "inventoryposition", + Loss = "loss", } export type AssortmentEntity =