/simple-1c

Транслятор запросов и Linq-провайдер для 1С-Бухгалтерии

Primary LanguageC#MIT LicenseMIT

#Simple1C

This project is licensed under the terms of the MIT license.

Упрощаем интеграцию с 1С за счет двух основных функций:

  • умеем исполнять обычные запросы языка 1С (выбрать * из Справочник.Контрагенты) без участия самой 1С - напрямую через СУБД. Это может быть удобно при использовании механизма разделения данных чтобы выполнить запрос сразу на всех областях информационной базы или даже на нескольких информационных базах, если используется более одного сервера СУБД. Такая возможность становится особенно актуальной при большом числе различных организаций, работающих с одной типовой конфигурацией, например через технологию 1C-фреш.

  • если есть потребность заинтегрировать с 1С внешнее приложение, и это приложение написано на .NET, то одним из вариантов может быть использование 1С COM апи. Это апи основано на интерфейсе IDispatch и позднем связывании, поэтому любая опечатка в имени метода или свойства приводит к ошибке во время выполнения. Мы решаем эту проблему путем генерации оберточных классов для всех объектов конфигурации. Эти классы можно использовать как для сохранения новых данных, так и для выполнения запросов в стиле LINQ.

##NuGet

Для установки Simple1C пакета, выполните следующую команду в NuGet-консоли

PM> Install-Package Simple1C

##Simple1C.SQL

Преобразуем запрос в формате языка запросов 1С в чистый sql и исполняем его на реальной СУБД. Используем Irony для синтаксического анализа и построения AST.

На практике работает это следующим образом. Первым делом с помощью команды gen-sql-meta достаем через COM метаданные по объектам конфигурации и сохраняем их в отдельную схему simple1c в sql базе данных (пока поддерживается только PostgreSQL).

Generator.exe
	-cmd gen-sql-meta
	-connection-string <будет передан методу Connect объекта V83.COMConnector>
	-db-connection-string <для подключения к sql СУБД>

Эти метаданные нужны в первую очередь для установления соответствия между именами объектов и реквизитов конфигурации 1С и именами таблиц и колонок в БД. Затем для выполнения запроса используем команду run-sql следующим образом:

Generator.exe
	-cmd run-sql
	-connection-strings <на какой базе выполнять запрос, можно передать несколько через запятую>
	-query-file <имя файла с запросом>
	-result-connection-string <куда поместить результат>
	-dump-sql true <опционально, выводит фактически исполняемый sql-запрос для отладки>

Результат выполнения запроса помещается в таблицу в базе, указанной параметром result-connection-string. Пока поддерживается только MS SQL Server в качестве такой базы. Имя таблицы берется из имени файла с запросом. Основные возможности:

  • Поддерживается большая часть конструкций языка - все стандартные элементы sql (подзапросы, union, group by, order by и т.п.), операторы Значение и Ссылка, функции ПРЕДСТАВЛЕНИЕ, ДАТАВРЕМЯ, ГОД, КВАРТАЛ и другие.

  • Поддерживается синтаксис для доступа к вложенным реквизитам через точку

	select ДоговорКонтрагента.Наименование from Документ.ПоступлениеТоваровУслуг
	where Контрагент.ГоловнойКонтрагент.ИНН = "7710967300"

При трансляции в sql к такому запросу будут добавлены необходимые цепочки join-ов на справочники контрагентов и договоров.

  • Можно использовать как русский, так и аглийский варианты синтаксиса - select * from Справочник.Контрагенты эквиваленто выбрать * из Справочник.Контрагенты.

  • Если вы используете 1С-Фреш и у вас несколько sql баз, то можно передать их все в параметре connection-strings. В этом случае запрос будет выпоняться параллельно на всех базах, а все результаты будут объеденены в одной общей таблице. Можно потом, например, через SQL Server Management Studio применить group by к этой таблице и получить группировку данных между различными базами.

  • Чтобы понять, какие объекты и реквизиты можно использовать в запросе, можно посмотреть в сгенерированные командой gen-sql-meta метаданные в схеме simple1c, а именно в таблицу tableMappings. В ней есть имена объектов, имена соответствующих им таблиц и такое же соответствие для реквизитов и колонок в таблицах.

  • Оператор Ссылка можно использовать для ускорения выполнения запроса. Дело в том, что в 1С реквизит может не иметь фиксированного статического типа, а допускать сразу несколько типов. Основной пример - это реквизит субконто, который для разных счетов может ссылается на разные объекты конфигурации: Контрагент, Статья учета и т.п. Если в запросе есть обращение через точку к свойствам такого реквизита (doc.Субконто1.Наименование), то единственный вариант получить нужные данные - это заджойниться на все возможные таблицы, в которых есть свойство Наименование. Если в запросе имеется в виду какой-то конкретный тип субконто, то можно воспользоваться оператором Ссылка следующим образом:

	select * from Документ.ПоступлениеНаРасчетныйСчет where Субконто1 Ссылка Справочник.Контрагенты 

В этом примере будет сгенерирован только один джойн - с таблицей контрагентов, что существенно быстрее.

  • В схеме simple1c есть пара функций для работы с Guid-ами. to_guid преобразует байтовый массив, в котором 1С сохраняет ссылки между объектами, в стандартное строковое представление guid-а. date_from_guid получает дату из строкового представления guid-а. Так как 1С генерирует guid-ы на основе текущего времени, по этой дате можно судить о том, когда был создан объект с данным идентификатором. Эти функции можно использовать в любой части запроса, аналогично стандартным функциям языка запросов 1С.

  • Можно получить данные по остаткам и оборотам аналогично стандартному отчету ОСВ в 1С. Для этого доступны четыре таблицы. РегистрБухгалтерии.Хозрасчетный.Остатки - здесь лежат помесячные сальдо и обороты без субконто, РегистрБухгалтерии.Хозрасчетный.Субконто1, РегистрБухгалтерии.Хозрасчетный.Субконто2, и РегистрБухгалтерии.Хозрасчетный.Субконто3 - здесь тоже самое, только с субконто. Каждая транзакция по счету учитывается в остатках и в одной из таблиц Субконто, в зависимости от количества субконт по данному счету.

Вот статья на Хабре о том, как все это работает.

##Simple1C.COM

Несмотря на то, что еще в 2013-м году 1С опубликовал REST апи поверх протокола OData (раз, два), многим и сегодня по разным причинам приходится использовать для интеграции старые COM-интерфейсы. Мы упрощаем использование этих интерфейсов за счет автоматической генерации классов по метаданным 1С и реализации LINQ-провайдера. Используем C# как основной язык для автогенерации. Механику провайдера упрощаем за счет вот этой классной штуки. Аналогичные проекты: http://www.linq-demo.1csoftware.com, http://www.vanessa-sharp.ru/reference-linq.html. Основные возможности:

  • Автоматическая генерация классов объектной модели по 1С-конфигурации :
Generator.exe
	-cmd gen-cs-meta
	-connection-string File=C:\my-1c-db;Usr=Администратор;Pwd=
	-namespace-root MyNameSpace.ObjectModel1C
	-scan-items Документ.СписаниеСРасчетногоСчета
	-source-path C:\sources\AwesomeSolution\ObjectModel1CProject\AutoGenerated

Такая команда создаст класс СписаниеСРасчетногоСчета в папке Документы. Этот класс будет обладать всеми свойствами исходного 1С-документа СписаниеСРасчетногоСчета, включая все табличные части. По классу так же будет создано для каждого из объектов конфигурации, на которые эти свойства ссылаются (транзитивное замыкание). Основные возможности:

  • Для имен генерируемых классов используем исходные русскоязычные идентификаторы из 1С. За счет этого модель становится проще - нет необходимости придумывать англоязычные аналоги для весьма специфических терминов 1С, одни и те же вещи называются одинаково по всей кодовой базе.

  • Не навязываем механизм управления коннекциями. Здесь возможны различные варианты (ThreadLocal, Pool, пересоздание по таймауту, пересоздание при обрыве и т.п.), многое зависит от приложения и конкретных потребностей. Простейший пример чтобы начать работать:

	var connectorType = Type.GetTypeFromProgID("V83.COMConnector");
	dynamic connector = Activator.CreateInstance(connectorType);
	var globalContext = connector.Connect("File=C:\my-1c-db;Usr=Администратор;Pwd=");
	var dataContext = DataContextFactory.CreateCOM(globalContext, typeof(Контрагенты).Assembly);
	var контрагент = new Контрагенты
	{
		ИНН = "1234567890",
		Наименование = "test-counterparty",
		ЮридическоеФизическоеЛицо = ЮридическоеФизическоеЛицо.ЮридическоеЛицо
	};
	dataContext.Save(контрагент);
	var контрагент2 = dataContext.Select<Контрагенты>().Single(x => x.Код == counterparty.Код);
	Assert.That(контрагент2.ИНН, Is.EqualTo("1234567890"));
  • Документы и справочники Generator.exe превращает в классы, а перечисления - в enum-ы языка C#. В примере выше для создания контрагента мы используем enum ЮридическоеФизическоеЛицо.

  • Абстрагируем ссылки, про них в прикладном коде можно просто забыть - создаем экемпляры классов объектной модели, присваиваем их свойствам других классов, при маппинге на соответствующие COM-объекты ссылки между ними будут проставлены автоматически.

	var контрагент = new Контрагенты
	{
		ИНН = "1234567890",
		Наименование = "test-counterparty",
		ЮридическоеФизическоеЛицо = ЮридическоеФизическоеЛицо.ЮридическоеЛицо
	};
	dataContext.Save(контрагент);
	var организация = dataContext.Select<Организация>().Single();
	var договор = new ДоговорыКонтрагентов
	{
		ВидДоговора = ВидыДоговоровКонтрагентов.СПокупателем,
		Наименование = "test name",
		Владелец = контрагент,
		Организация = организация
	};
	dataContext.Save(договор);
  • Метод Save сохраняет все объекты, до которых может добраться по ссылкам. Пример выше можно упростить следующим образом
	dataContext.Save(new ДоговорыКонтрагентов
	{
		ВидДоговора = ВидыДоговоровКонтрагентов.СПокупателем,
		Наименование = "test name",
		Владелец = new Контрагенты
		{
			ИНН = "1234567890",
			Наименование = "test-counterparty",
			ЮридическоеФизическоеЛицо = ЮридическоеФизическоеЛицо.ЮридическоеЛицо
		},
		Организация = dataContext.Select<Организация>().Single()
	});
  • В запросах можно использовать стандартные Linq-операторы (Join, GroupBy пока не реализованы)
public decimal GetCurrencyRateToDate(string currencyCode, DateTime date)
{
	return dataContext.Select<КурсыВалют>()
		.Where(x => x.Валюта.Код == currencyCode)
		.Where(x => x.Период <= date)
		.OrderByDescending(x => x.Период)
		.First()
		.Курс;
}
  • Умеем обновлять отдельные строки табличных частей. Так, например, в таком примере
var поступлениеТоваровУслуг = new ПоступлениеТоваровУслуг
{
	Дата = new DateTime(2016, 6, 1),
	Контрагент = new Контрагенты
	{
		ИНН = "7711223344",
		Наименование = "ООО Тестовый контрагент",
	},
	Услуги = new List<ПоступлениеТоваровУслуг.ТабличнаяЧастьУслуги>
	{
		new ПоступлениеТоваровУслуг.ТабличнаяЧастьУслуги
		{
			Номенклатура = new Номенклатура
			{
				Наименование = "стрижка"
			},
			Количество = 10,
			Содержание = "стрижка с кудряшками"
		},
		new ПоступлениеТоваровУслуг.ТабличнаяЧастьУслуги
		{
			Номенклатура = new Номенклатура
			{
				Наименование = "стрижка усов"
			},
			Количество = 10,
			Содержание = "стрижка бороды"
		}                
	};
dataContext.Save(поступлениеТоваровУслуг);
var t = поступлениеТоваровУслуг.Услуги[0];
поступлениеТоваровУслуг.Услуги[0] = поступлениеТоваровУслуг.Услуги[1];
поступлениеТоваровУслуг.Услуги[1] = t;
dataContext.Save(поступлениеТоваровУслуг);

на втором вызове Save будет сгенерирован единственный вызов метода Сдвинуть, меняющий местами две строки.

  • Через метод DataContextFactory.CreateInMemory() можно получить inmemory-реализацию интерфейса IDataContext. Это здорово помогает при автоматизированном тестировании безнес-логики. Весь граф сервисных объектов поверх IDataContext можно создать целиком в inmemory-режиме, значительно ускорив этим прохождение тестов по сравнению с работой с настоящей 1С.