Шаг 0. Зачем? Почему так?
Нужно было сделать:
1. работало быстро(не замороживало контекст пользователя, быстрый отклик)
2. было открыто для переиспользования в других инхаус поделках(фронт, бэк)
3. добавить фичи в которые штатная диаграмма не может(закраска на пересечении, выделить регламентированные фрагменты)
4. выглядело нормально
Технологический стэк:
1. 1с 8.3.8(3 сторона, источник событий)
2. ms sql(бд)
3. python 36(бэк)
4. js(фронт)
5. active x(фронт)
6. chromiumembedded(в статье особо не фигурирует, но он есть)(фронт)
Общая схемы работы:
бэкенд отвечает за данные, обрабатывает события об изменениях, инапп статистика, выполняет запросы в бд к view (with no lock)(ORM Model в этот раз), отдает актуальные данные(json, XML(XDTO), да тот самый который 1С понимает полностью и может после прогона через фабрику принять за свой объект(по этой части будет сказано ниже более подробно), можно отдавать например диаграмму Ганта целиком, что и было сделано)
1С изменяет данные(штатно)(условно ,проводит документы), триггерит в бэкенд, дергает методы фронт( обновить)
фронт( вм(то что исполняет код) chromium ) запрашивает из бэкенд данные, отрисовывает форму, обрабатывает события пользователя( отркыть, перетащить, удалить), триггерит в 1с(не обработка внешнего события!) при необходимости(сменить форму, изменить данные)
active x(фронт) - позволяет chromium(chromiumembedded) работать в IE(те еще раз, в ИЕ(тот что в 1с) работает chrome(chromiumembedded), с devtools и отладкой из клиентского сеанса 1с и прочими своими функциями, и проблемами)
Шаг 1. Пишем backend.
Структура:
start.py - страртер
common\common.py - общие функции
common\external_record_log.py - основной модуль
common\ones_xml_types.py - модуль формирования xml(xdto)
common\orm\base.py - орм база(класс)
common\orm\fields.py - орм описание полей
common\orm\models.py - орм описание таблиц
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
from common import external_record_log
import sys
from common import common
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
"""Handle requests in a separate thread."""
if __name__ == '__main__':
parser = common.createParser()
namespace = parser.parse_args(sys.argv[1:])
erl = external_record_log.ExternalRecordLog()
server = ThreadedHTTPServer((namespace.ip, int(namespace.port)), external_record_log.ExternalRecordLog_Handler)
print('Starting External Record Log '+str(namespace.ip)+':'+str(namespace.port)+' (use <Ctrl-C> to stop)')
server.serve_forever()
В данном модуле происходит парсинг входных параметров, стартует поток HTTP сервер, вешается хендлер(то что будет обрабатывать запросы)
from common.orm import models as orm
#region xml_header
def xml_header_start():
return \
"""
<GanttChart xmlns="http://v8.1c.ru/8.2/data/chart" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="GanttChart">
"""
def xml_header_end_start(date, now, time_acceptions, time_begin, time_end):
fmt_start = '%Y-%m-%dT'+time_begin+':00'
fmt_end = '%Y-%m-%dT'+time_end+':59'
return \
"""
<drawEmpty>true</drawEmpty>
<timeScale>
<!--
<level>
<measure>Day</measure>
<interval>1</interval>
<show>false</show>
<line width="1" gap="false">
<style xmlns="http://v8.1c.ru/8.1/data/ui" xsi:type="ChartLineType">Dotted</style>
</line>
<scaleColor>#C0C0C0</scaleColor>
<dayFormatRule>MonthDayWeekDay</dayFormatRule>
<format/>
<labels>
<ticks>0</ticks>
</labels>
<backColor>auto</backColor>
<textColor>auto</textColor>
<showPereodicalLabels>true</showPereodicalLabels>
</level>
-->
<level>
<measure>Hour</measure>
<interval>1</interval>
<show>true</show>
<line width="0" gap="true">
<style xmlns="http://v8.1c.ru/8.1/data/ui" xsi:type="ChartLineType">Dotted</style>
</line>
<scaleColor>#C0C0C0</scaleColor>
<dayFormatRule>MonthDayWeekDay</dayFormatRule>
<format>
<item xmlns="http://v8.1c.ru/8.1/data/core">
<lang>ru</lang>
<content>ДФ=Ч</content>
</item>
<item xmlns="http://v8.1c.ru/8.1/data/core">
<lang>#</lang>
<content>ДФ=Ч</content>
</item>
</format>
<labels>
<ticks>5</ticks>
</labels>
<backColor>auto</backColor>
<textColor>auto</textColor>
<showPereodicalLabels>true</showPereodicalLabels>
</level>
<level>
<measure>Minute</measure>
<interval>""" + str(time_acceptions) + """</interval>
<show>true</show>
<line width="0" gap="true">
<style xmlns="http://v8.1c.ru/8.1/data/ui" xsi:type="ChartLineType">Dotted</style>
</line>
<scaleColor>#C0C0C0</scaleColor>
<dayFormatRule>MonthDayWeekDay</dayFormatRule>
<format>
<item xmlns="http://v8.1c.ru/8.1/data/core">
<lang>ru</lang>
<content>ДФ=мм</content>
</item>
<item xmlns="http://v8.1c.ru/8.1/data/core">
<lang>#</lang>
<content>ДФ=мм</content>
</item>
</format>
<labels>
<ticks>5</ticks>
</labels>
<backColor>#C0D0D0</backColor>
<textColor>auto</textColor>
<showPereodicalLabels>true</showPereodicalLabels>
</level>
<level>
<measure>Day</measure>
<interval>1</interval>
<show>true</show>
<line width="1" gap="false">
<style xmlns="http://v8.1c.ru/8.1/data/ui" xsi:type="ChartLineType">Solid</style>
</line>
<scaleColor>#C0D0D0</scaleColor>
<dayFormatRule>MonthDayWeekDay</dayFormatRule>
<format/>
<labels>
<label>
<key>""" + now.strftime('%Y-%m-%dT%H:%M:00') + """</key>
<text/>
<lineColor xmlns:d6p1="http://v8.1c.ru/8.1/data/ui/colors/web">d6p1:Red</lineColor>
<textColor>auto</textColor>
</label>
<ticks>0</ticks>
</labels>
<backColor>auto</backColor>
<textColor>auto</textColor>
<showPereodicalLabels>false</showPereodicalLabels>
</level>
<transparent>false</transparent>
<backColor xmlns:d3p1="http://v8.1c.ru/8.1/data/ui/style">d3p1:FieldBackColor</backColor>
<textColor xmlns:d3p1="http://v8.1c.ru/8.1/data/ui/style">d3p1:FormTextColor</textColor>
<currentLevel>0</currentLevel>
</timeScale>
<keepScaleVariant>Auto</keepScaleVariant>
<fixedVariantMeasure>Month</fixedVariantMeasure>
<fixedVariantInterval>1</fixedVariantInterval>
<autoFullInterval>false</autoFullInterval>
<fullIntervalBegin>""" + date.strftime(fmt_start) + """</fullIntervalBegin>
<fullIntervalEnd>""" + date.strftime(fmt_end) + """</fullIntervalEnd>
<visualBegin>""" + date.strftime(fmt_start) + """</visualBegin>
<intervalDrawType>ThreeDimensional</intervalDrawType>
<noneVariantChars>4</noneVariantChars>
<noneVariantMeasure>Hour</noneVariantMeasure>
<verticalStretch>None</verticalStretch>
<verticalScrollEnable>true</verticalScrollEnable>
<showValueText>Right</showValueText>
<extTitle/>
<outboundColor>#FFFFFF</outboundColor>
<backIntervals>
<collection>
<ticks>0</ticks>
</collection>
<ticks>0</ticks>
</backIntervals>"""
def xml_header_end_end():
return """
<linksColor>#000080</linksColor>
<linksLine width="1" gap="false">
<style xmlns="http://v8.1c.ru/8.1/data/ui" xsi:type="ChartLineType">Dashed</style>
</linksLine>
</GanttChart>
"""
def xml_header_end_link(curBeginKey, curEndKey):
return """
<link>
<curBeginKey>"""+str(curBeginKey)+"""</curBeginKey>
<curEndKey>"""+str(curEndKey)+"""</curEndKey>
<beginKey>"""+str(curBeginKey)+"""</beginKey>
<endKey>"""+str(curEndKey)+"""</endKey>
<color>auto</color>
<linkType>EndBegin</linkType>
</link>"""
#xml_chart
def xml_chart():
return \
"""
<chart>
<seriesCurId>1</seriesCurId>
<pointsCurId>0</pointsCurId>
<isSeriesDesign>true</isSeriesDesign>
<realSeriesCount>0</realSeriesCount>
<realExSeriesData>
<id>1</id>
<color>#991919</color>
<line width="2" gap="false">
<style xmlns="http://v8.1c.ru/8.1/data/ui" xsi:type="ChartLineType">Solid</style>
</line>
<marker>Rhomb</marker>
<text/>
<strIsChanged>false</strIsChanged>
<isExpand>false</isExpand>
<isIndicator>false</isIndicator>
<colorPriority>false</colorPriority>
</realExSeriesData>
<isPointsDesign>true</isPointsDesign>
<realPointCount>0</realPointCount>
<curSeries>-1</curSeries>
<curPoint>0</curPoint>
<chartType>Column3D</chartType>
<circleLabelType>None</circleLabelType>
<labelsDelimiter>, </labelsDelimiter>
<labelsLocation>Edge</labelsLocation>
<lbFormat/>
<lbpFormat/>
<labelsColor xmlns:d3p1="http://v8.1c.ru/8.1/data/ui/style">d3p1:FormTextColor</labelsColor>
<labelsFont kind="AutoFont"/>
<transparentLabelsBkg>true</transparentLabelsBkg>
<labelsBkgColor>auto</labelsBkgColor>
<labelsBorder width="1">
<style xmlns="http://v8.1c.ru/8.1/data/ui" xsi:type="ControlBorderType">Single</style>
</labelsBorder>
<labelsBorderColor>auto</labelsBorderColor>
<circleExpandMode>None</circleExpandMode>
<chart3Dcrd>SouthWest</chart3Dcrd>
<title/>
<isShowTitle>false</isShowTitle>
<isShowLegend>false</isShowLegend>
<ttlBorder width="0">
<style xmlns="http://v8.1c.ru/8.1/data/ui" xsi:type="ControlBorderType">WithoutBorder</style>
</ttlBorder>
<ttlBorderColor xmlns:d3p1="http://v8.1c.ru/8.1/data/ui/style">d3p1:BorderColor</ttlBorderColor>
<lgBorder width="0">
<style xmlns="http://v8.1c.ru/8.1/data/ui" xsi:type="ControlBorderType">WithoutBorder</style>
</lgBorder>
<lgBorderColor xmlns:d3p1="http://v8.1c.ru/8.1/data/ui/style">d3p1:BorderColor</lgBorderColor>
<chBorder width="1">
<style xmlns="http://v8.1c.ru/8.1/data/ui" xsi:type="ControlBorderType">Single</style>
</chBorder>
<chBorderColor xmlns:d3p1="http://v8.1c.ru/8.1/data/ui/style">d3p1:BorderColor</chBorderColor>
<transparent>false</transparent>
<bkgColor xmlns:d3p1="http://v8.1c.ru/8.1/data/ui/style">d3p1:FieldBackColor</bkgColor>
<isTrnspTtl>false</isTrnspTtl>
<ttlColor xmlns:d3p1="http://v8.1c.ru/8.1/data/ui/style">d3p1:FieldBackColor</ttlColor>
<isTrnspLeg>false</isTrnspLeg>
<legColor xmlns:d3p1="http://v8.1c.ru/8.1/data/ui/style">d3p1:FieldBackColor</legColor>
<isTrnspCh>false</isTrnspCh>
<chColor xmlns:d3p1="http://v8.1c.ru/8.1/data/ui/style">d3p1:FieldBackColor</chColor>
<ttlTxtColor xmlns:d3p1="http://v8.1c.ru/8.1/data/ui/style">d3p1:FormTextColor</ttlTxtColor>
<legTxtColor xmlns:d3p1="http://v8.1c.ru/8.1/data/ui/style">d3p1:FormTextColor</legTxtColor>
<chTxtColor xmlns:d3p1="http://v8.1c.ru/8.1/data/ui/style">d3p1:FormTextColor</chTxtColor>
<ttlFont xmlns:style="http://v8.1c.ru/8.1/data/ui/style" ref="style:TextFont" kind="StyleItem"/>
<legFont xmlns:style="http://v8.1c.ru/8.1/data/ui/style" ref="style:TextFont" kind="StyleItem"/>
<chFont xmlns:style="http://v8.1c.ru/8.1/data/ui/style" ref="style:TextFont" kind="StyleItem"/>
<isShowScale>true</isShowScale>
<isShowScaleVL>true</isShowScaleVL>
<isShowSeriesScale>true</isShowSeriesScale>
<isShowPointsScale>true</isShowPointsScale>
<isShowValuesScale>true</isShowValuesScale>
<vsFormat/>
<xLabelsOrientation>Auto</xLabelsOrientation>
<scaleLine width="1" gap="false">
<style xmlns="http://v8.1c.ru/8.1/data/ui" xsi:type="ChartLineType">Solid</style>
</scaleLine>
<scaleColor>#A9A9A9</scaleColor>
<isAutoSeriesName>true</isAutoSeriesName>
<isAutoPointName>true</isAutoPointName>
<maxMode>NotDefined</maxMode>
<maxSeries>4</maxSeries>
<maxSeriesPrc>30</maxSeriesPrc>
<spaceMode>Half</spaceMode>
<baseVal>0</baseVal>
<isOutline>false</isOutline>
<realPiePoint>0</realPiePoint>
<realStockSeries>0</realStockSeries>
<isLight>true</isLight>
<isGradient>false</isGradient>
<isTransposition>false</isTransposition>
<hideBaseVal>false</hideBaseVal>
<dataTable>false</dataTable>
<dtVerLines>true</dtVerLines>
<dtHorLines>true</dtHorLines>
<dtHAlign>Right</dtHAlign>
<dtFormat/>
<dtKeys>true</dtKeys>
<paletteKind>Palette32</paletteKind>
<animation>Auto</animation>
<rebuildTime>0</rebuildTime>
<isTransposed>false</isTransposed>
<autoTransposition>false</autoTransposition>
<legendScrollEnable>true</legendScrollEnable>
<surfaceColor>#A90000</surfaceColor>
<radarScaleType>Circle</radarScaleType>
<gaugeValuesPresentation>Needle</gaugeValuesPresentation>
<gaugeQualityBands useTextStr="false" useTooltipStr="false"/>
<beginGaugeAngle>0</beginGaugeAngle>
<endGaugeAngle>180</endGaugeAngle>
<gaugeThickness>5</gaugeThickness>
<gaugeLabelsLocation>InsideScale</gaugeLabelsLocation>
<gaugeLabelsArcDirection>false</gaugeLabelsArcDirection>
<gaugeBushThickness>4</gaugeBushThickness>
<gaugeBushColor>#A9A9A9</gaugeBushColor>
<autoMaxValue>true</autoMaxValue>
<userMaxValue>0</userMaxValue>
<autoMinValue>true</autoMinValue>
<userMinValue>0</userMinValue>
<elementsIsInit>true</elementsIsInit>
<titleIsInit>true</titleIsInit>
<legendIsInit>true</legendIsInit>
<chartIsInit>true</chartIsInit>
<elementsChart>
<left>0</left>
<right>0</right>
<top>0</top>
<bottom>0</bottom>
</elementsChart>
<elementsLegend>
<left>0.9857029388403491</left>
<right>0</right>
<top>0.0407725321888412</top>
<bottom>0</bottom>
</elementsLegend>
<elementsTitle>
<left>0.8328025477707002</left>
<right>0</right>
<top>0</top>
<bottom>0.9594017094017091</bottom>
</elementsTitle>
<borderColor xmlns:d3p1="http://v8.1c.ru/8.1/data/ui/style">d3p1:BorderColor</borderColor>
<border width="1">
<style xmlns="http://v8.1c.ru/8.1/data/ui" xsi:type="ControlBorderType">Single</style>
</border>
<dataSourceDescription/>
<isDataSourceMode>false</isDataSourceMode>
<isRandomizedNewValues>true</isRandomizedNewValues>
<splineStrain>95</splineStrain>
<translucencePercent>0</translucencePercent>
<funnelNeckHeightPercent>10</funnelNeckHeightPercent>
<funnelNeckWidthPercent>10</funnelNeckWidthPercent>
<funnelGapSumPercent>3</funnelGapSumPercent>
<multiStageLinkLine width="1" gap="false">
<style xmlns="http://v8.1c.ru/8.1/data/ui" xsi:type="ChartLineType">Solid</style>
</multiStageLinkLine>
<multiStageLinkColor>#000000</multiStageLinkColor>
</chart>
"""
#xml_points
def xml_points_start_header_start():
return \
"""
<points>
<testMode>false</testMode>
"""
def xml_points_start_header_end():
return \
"""
<autoText>true</autoText>
<useValuesReverseBehavior>false</useValuesReverseBehavior>
</points>
"""
#xml_points_value
def xml_points_value_empty():
return \
"""
<value>
<itemKey>0</itemKey>
<key>0</key>
<parentKey>0</parentKey>
<leftKey>0</leftKey>
<rightKey>0</rightKey>
<extKey>0</extKey>
<title/>
<cacheKey>0</cacheKey>
<font kind="AutoFont"/>
<picture/>
</value>
"""
def xml_points_value_0(extKey):
return \
"""
<value>
<itemKey>0</itemKey>
<key>0</key>
<parentKey>0</parentKey>
<leftKey>1</leftKey>
<rightKey>0</rightKey>
<extKey>"""+str(extKey)+"""</extKey>
<title/>
<cacheKey>0</cacheKey>
<font kind="AutoFont"/>
<picture/>
</value>
"""
def xml_points_value(itemKey, rightKey, ref, content):
return \
"""
<value>
<itemKey>"""+str(itemKey)+"""</itemKey>
<key>"""+str(itemKey)+"""</key>
<parentKey>0</parentKey>
<leftKey>0</leftKey>
<rightKey>"""+str(rightKey)+"""</rightKey>
<extKey>0</extKey>
<valueKey xmlns:d4p1="http://v8.1c.ru/8.1/data/enterprise/current-config" xsi:type="d4p1:CatalogRef.Сотрудники">"""+ref+"""</valueKey>
<title>
<item xmlns="http://v8.1c.ru/8.1/data/core">
<lang>#</lang>
<content>"""+content+"""</content>
</item>
</title>
<cacheKey>"""+str(itemKey)+"""</cacheKey>
<font kind="AutoFont"/>
<picture/>
</value>
"""
def xml_points_content_cache_item(mainColor='#000000', secondColor='#000000', backColor='auto', textColor='auto'):
return \
"""
<contentCacheItem>
<mainColor>"""+mainColor+"""</mainColor>
<secondColor>"""+secondColor+"""</secondColor>
<backColor>"""+backColor+"""</backColor>
<textColor>"""+textColor+"""</textColor>
</contentCacheItem>
"""
#xml_series
def xml_series_empty():
return \
"""
<series>
<testMode>false</testMode>
<value>
<itemKey>0</itemKey>
<key>0</key>
<parentKey>0</parentKey>
<leftKey>0</leftKey>
<rightKey>0</rightKey>
<extKey>0</extKey>
<title/>
<cacheKey>0</cacheKey>
</value>
<contentCacheItem>
<mainColor>#000000</mainColor>
<secondColor>#000000</secondColor>
<hatchBetweenIntervalsColor>#000000</hatchBetweenIntervalsColor>
</contentCacheItem>
<autoText>true</autoText>
<useValuesReverseBehavior>false</useValuesReverseBehavior>
</series>
"""
def xml_series(date):
return \
"""
<series>
<testMode>false</testMode>
<value>
<itemKey>0</itemKey>
<key>0</key>
<parentKey>0</parentKey>
<leftKey>1</leftKey>
<rightKey>0</rightKey>
<extKey>1</extKey>
<title/>
<cacheKey>0</cacheKey>
</value>
<value>
<itemKey>1</itemKey>
<key>1</key>
<parentKey>0</parentKey>
<leftKey>0</leftKey>
<rightKey>0</rightKey>
<extKey>0</extKey>
<valueKey xsi:type="xs:dateTime">"""+date.strftime('%Y-%m-%dT00:00:00')+"""</valueKey>
<title/>
<cacheKey>1</cacheKey>
</value>
<contentCacheItem>
<mainColor>#000000</mainColor>
<secondColor>#000000</secondColor>
<hatchBetweenIntervalsColor>#000000</hatchBetweenIntervalsColor>
</contentCacheItem>
<contentCacheItem>
<mainColor>#CC0000</mainColor>
<secondColor>#009966</secondColor>
<hatchBetweenIntervalsColor>#000000</hatchBetweenIntervalsColor>
</contentCacheItem>
<autoText>true</autoText>
<useValuesReverseBehavior>false</useValuesReverseBehavior>
</series>
"""
#xml_main_value
def xml_main_interval(intervalKey, data, itemKey, color, content):
is_receipt = 'false'
is_repair = 'false'
is_other = 'false'
kind_work = ''
var_a = 'ЗонаПриема'
if data['kind_work_ref']=='F5C17810-4BE3-4689-88D0-5F7BA50DEBA2':
is_repair = 'true'
kind_work = 'Ремонт'
var_a = 'ЗонаРемонта'
elif data['kind_work_ref']=='407179CF-91FF-4C9B-A789-A4FDF5E7DB94':
is_receipt = 'true'
kind_work = 'Прием'
else:
is_other = 'true'
kind_work = 'Выдача'
return \
"""
<interval>
<itemKey>"""+str(itemKey)+"""</itemKey>
<key>"""+str(itemKey)+"""</key>
<begin>"""+data['r_begin'].strftime('%Y-%m-%dT%H:%M:%S')+"""</begin>
<end>"""+data['r_end'].strftime('%Y-%m-%dT%H:%M:%S')+"""</end>
<text>
<item xmlns="http://v8.1c.ru/8.1/data/core">
<lang>#</lang>
<content>"""+str(content)+"""</content>
</item>
</text>
<details xmlns:d3p1="http://v8.1c.ru/8.1/data/core" xsi:type="d3p1:Structure">
<d3p1:Property name="Сотрудник">
<d3p1:Value xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config" xsi:type="d5p1:CatalogRef.Сотрудники">"""+data['m_ref_ones']+"""</d3p1:Value>
</d3p1:Property>
<d3p1:Property name="Регистратор">
<d3p1:Value xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config" xsi:type="d5p1:DocumentRef.ЗаписьВЖурналЗаписи">"""+data['r_ref_ones']+"""</d3p1:Value>
</d3p1:Property>
<d3p1:Property name="ПериодНачало">
<d3p1:Value xsi:type="xs:dateTime">"""+data['r_begin'].strftime('%Y-%m-%dT%H:%M:%S')+"""</d3p1:Value>
</d3p1:Property>
<d3p1:Property name="ПериодОкончание">
<d3p1:Value xsi:type="xs:dateTime">"""+data['r_end'].strftime('%Y-%m-%dT%H:%M:%S')+"""</d3p1:Value>
</d3p1:Property>
<d3p1:Property name="ЭтоПрием">
<d3p1:Value xsi:type="xs:boolean">"""+is_receipt+"""</d3p1:Value>
</d3p1:Property>
<d3p1:Property name="ЭтоРемонт">
<d3p1:Value xsi:type="xs:boolean">"""+is_repair+"""</d3p1:Value>
</d3p1:Property>
<d3p1:Property name="ЭтоВыдача">
<d3p1:Value xsi:type="xs:boolean">"""+is_other+"""</d3p1:Value>
</d3p1:Property>
<d3p1:Property name="ВидРаботы">
<d3p1:Value xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config" xsi:type="d5p1:EnumRef.ЖурналЗаписиВидРаботы">"""+kind_work+"""</d3p1:Value>
</d3p1:Property>
<d3p1:Property name="НомерСтроки">
<d3p1:Value xsi:type="xs:decimal">"""+str(data['num_str'])+"""</d3p1:Value>
</d3p1:Property>
<d3p1:Property name="РежимыРаботыСотрудников">
<d3p1:Value xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config" xsi:type="d5p1:EnumRef.РежимыРаботыСотрудниковЖЭЗ">"""+var_a+"""</d3p1:Value>
</d3p1:Property>
</details>
<color>"""+color+"""</color>
<intervalKey>"""+str(intervalKey)+"""</intervalKey>
</interval>
"""
def xml_main_value(itemKey, r_ref):
editFlag = 'false'
if not r_ref is None:
editFlag = 'true'
return \
"""
<value>
<itemKey>"""+str(itemKey)+"""</itemKey>
<key>"""+str(itemKey)+"""</key>
<text/>
<editFlag>"""+editFlag+"""</editFlag>
<backColor>auto</backColor>
<textColor>auto</textColor>
<mainColor>auto</mainColor>
<secondColor>auto</secondColor>
</value>
"""
собирает строку xml с подстановкой параметров. важные момент здесь конструкции вида:
...
<details xmlns:d3p1="http://v8.1c.ru/8.1/data/core" xsi:type="d3p1:Structure">
<d3p1:Property name="Сотрудник">
<d3p1:Value xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config" xsi:type="d5p1:CatalogRef.Сотрудники">"""+data['m_ref_ones']+"""</d3p1:Value>
</d3p1:Property>
...
именно так потом, когда строка придет в 1с и прогонить через фабрику, 1с поймет что это ее объект(типа структура с полем сотрудник и тд...). Больше ничего интересного тут нет. Для тех кто не понял откуда брать текст: в 1с дергаете сериализатор xdto(тем и публикаций куча), сохраняете в xml, разбираетесь со структурой. В моем случае был затык с магическими числами при установке интревалов. База числа 4294967297. Об этом ниже будет. Про то что здесь xml собирается текстом и именно так, и то что так (на конкатенацию) подставляются параметры, давайте не будем.
В качестве ORM используется ORM(peewee) http://docs.peewee-orm.com/en/latest/
import peewee
import peewee_mssql
import pdb
import time
import threading
import sys
from common import common
from common.orm import fields
from datetime import datetime
parser = common.createParser()
namespace = parser.parse_args(sys.argv[1:])
sql_host = namespace.sql_host
sql_base = namespace.sql_base
sql_user = namespace.sql_user
sql_pass = namespace.sql_pass
class OnesBase(OnesCore):
ref = fields.LinkField()
ref_db = fields.LinkFieldDB()
ref_ones = fields.LinkFieldOneS()
def __bool__(self):
return self.ref and True or False
@classmethod
def warm_up(cls, query=None):
super(OnesBase, cls).warm_up(query = cls.select().order_by(cls.ref).limit(1000))
далее куски кода переопределения методов пиви и прочий трэш
class OnesEnum(OnesBase):
order = peewee.IntegerField(db_column = "Порядок")
values = []
def __str__(self):
if isinstance(self.name, str):
return self.name
return self.order or self.__class__.__name__ + ": has NO name"
class OnesRef(OnesBase):
mark = fields.BoolField(db_column = 'ПометкаУдаления', inverted = False)
code = peewee.CharField(db_column = 'Код', max_length = 10)
name = peewee.CharField(db_column = 'Наименование', max_length = 100)
def __str__(self):
return self.name or self.ref or self.__class__.__name__ + ": has NO name"
class OnesBP(OnesBase):
date = fields.DateField()
mark = fields.BoolField(db_column = 'ПометкаУдаления', inverted = False)
number = peewee.CharField(db_column = 'Номер', max_length = 10)
completed = fields.BoolField(db_column='Завершен')
started = fields.BoolField(db_column='Стартован')
def __str__(self):
return self.name or self.ref or self.__class__.__name__ + ": has NO name"
class OnesPointRouteBP(OnesBase):
order = peewee.IntegerField(db_column = "Порядок")
def __str__(self):
return self.name or self.ref or self.__class__.__name__ + ": has NO name"
class OnesTask(OnesBase):
mark = fields.BoolField(db_column = 'ПометкаУдаления', inverted = False)
name = peewee.CharField(db_column = 'Наименование', max_length = 100)
date = fields.DateField()
completed = fields.BoolField(db_column = 'Выполнена')
def __str__(self):
return self.name or self.ref or self.__class__.__name__ + ": has NO name"
class OnesDoc(OnesBase):
mark = fields.BoolField(db_column = 'ПометкаУдаления', inverted = False)
number = peewee.CharField(db_column = 'Номер', max_length = 10)
date = fields.DateField()
posted = fields.BoolField(db_column = 'Проведен')
def __str__(self):
return self.table + ' №' + self.number + ' от ' + self.date.strftime("%d.%m.%Y")
class OnesTable(OnesBase):
key = peewee.BlobField(db_column = '_KeyField')
order = peewee.IntegerField(db_column = "НомерСтроки")
class Meta:
primary_key = peewee.CompositeKey('ref', 'key')
передача параметров со стартера для установки соединения с базой, и описание базовых классов (документ, справочник и тд)
from datetime import datetime
import peewee, base64, binascii
import decimal
nullrefs = (
b'00000000000000000000000000000000',
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
'NO REF!',
)
nulldate = datetime(2599, 1, 1, 0, 0)
class LinkField(peewee.BlobField):
def __init__(self, *args, **kwargs):
if not kwargs.get('primary_key'):
kwargs['primary_key'] = True
kwargs['db_column'] = 'Ссылка'
super().__init__(*args, **kwargs)
def python_value(self, value):
if value in nullrefs or value is None:
return None
return base64.b16encode(value).decode()
def db_value(self, value):
if value in nullrefs or value is None:
return None
try:
return base64.b16decode(value.encode())
except binascii.Error as e:
return value
class LinkFieldDB(peewee.CharField):
def __init__(self, *args, **kwargs):
kwargs['db_column'] = '_ссылка_бд'
super().__init__(*args, **kwargs)
def python_value(self, value):
if value in nullrefs or value is None:
return None
return value
def db_value(self, value):
if value in nullrefs or value is None:
return None
return value
class LinkFieldOneS(peewee.CharField):
def __init__(self, *args, **kwargs):
kwargs['db_column'] = '_ссылка_'
super().__init__(*args, **kwargs)
def python_value(self, value):
if value in nullrefs or value is None:
return None
return value
def db_value(self, value):
if value in nullrefs or value is None:
return None
return value
class ForeignKey(LinkField, peewee.ForeignKeyField):
def __init__(self, *args, **kwargs):
peewee.ForeignKeyField.__init__(self, *args, **kwargs)
class DateField(peewee.DateTimeField):
def __init__(self, *args, **kwargs):
if not kwargs.get('db_column'):
kwargs['db_column'] = 'Дата'
super().__init__(*args, **kwargs)
def python_value(self, value):
if value is None:
return nulldate
if isinstance(value, str):
value = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
return value.replace(year = value.year - 2000)
def db_value(self, value):
return value.replace(year = value.year + 2000)
def timestamp(self):
print(self._data)
return 'AAA'
class BoolField(peewee.BooleanField):
def __init__(self, *args, **kwargs):
if kwargs.get('inverted') is not None:
self.inverted = (kwargs.get('inverted') is True)
del kwargs['inverted']
super().__init__(*args, **kwargs)
def python_value(self, value):
if getattr(self, 'inverted', False):
return True if value == b'\x00' else False
else:
return True if value == b'\x01' else False
def db_value(self, value):
if getattr(self, 'inverted', False):
return 0x00 if value else 0x01
else:
return 0x01 if value else 0x00
class ENameKey(peewee.CharField):
def __init__(self, values, *args, **kwargs):
kwargs['db_column'] = "ПредставлениеЗначения"
kwargs['max_length'] = 32
super().__init__(*args, **kwargs)
self.values = values
class ENameKeyTarget(ENameKey):
pass
class RefField(peewee.UUIDField):
def __init__(self, *args, **kwargs):
super().__init__()
self.default = None
self.null = True
def db_value(self, value):
return value.upper()
преобразование типов ms sql, описание логической связи(например ссылка)
import peewee
if __name__ == '__main__':
from base import OnesRef, OnesDoc, OnesEnum, OnesTable, OnesCore, OnesTask
from fields import ForeignKey, BoolField, ENameKey, DateField, ENameKeyTarget
else:
from .base import OnesRef, OnesDoc, OnesEnum, OnesTable, OnesCore, OnesTask
from .fields import ForeignKey, BoolField, ENameKey, DateField, ENameKeyTarget
import common.orm.base as base
from common import common
from datetime import datetime
# region Перечисления
class Direction(OnesEnum):
name = ENameKeyTarget([
"Продажа",
"Сервис",
"Запчасти",
"Прочее",
"Кредит"
])
class Meta:
db_table = 'e_ВидДеятельности'
...
# region Справочники
class Org(OnesRef):
class Meta:
db_table = 'r_Организации'
class CarsBrand(OnesRef):
class Meta:
db_table = 'r_АвтомобильныеБренды'
class Brand(OnesRef):
abrand = ForeignKey(CarsBrand, db_column='АвтомобильныйБренд', related_name='brands', null = True)
class Meta:
db_table = 'r_Бренды'
class Dep(OnesRef):
area = ForeignKey(Area, db_column='Площадка', null = True)
brand = ForeignKey(Brand, db_column='Бренд', null = True)
org = ForeignKey(Org, db_column='Организация')
direction = ForeignKey(Direction, db_column='НаправлениеДеятельности', null = True)
class Meta:
db_table = 'r_ПодразделенияКомпании'
class Workshop(OnesRef):
org = ForeignKey(Org, db_column='Организация')
dep = ForeignKey(Dep, db_column='Подразделение')
class Meta:
db_table = 'r_Цеха'
...
# region Документы
class Event(OnesDoc):
author = ForeignKey(User, db_column='Автор', related_name='events_author')
manager = ForeignKey(User, db_column='Менеджер', related_name='events_manager')
freelanceSituation = BoolField(db_column='ВнештатнаяСитуация')
client = ForeignKey(ClientsCRM, db_column='Клиент', related_name='events_client')
clientName = peewee.CharField(db_column='ИмяКлиента')
phone = peewee.CharField(db_column='Телефон')
dep = ForeignKey(Dep, db_column='Подразделение', related_name='events_dep')
kind = ForeignKey(EventKinds, db_column='ТипСобытия', related_name='events')
read = BoolField(db_column='Просмотрено')
# task = ForeignKey(CRM_TasksService, db_column='Задача', related_name='events')
class Meta:
db_table = 'd_CRM_Событие'
def save(self, force_insert = False, recursive = True):
peewee.Model.save(self)
class OrderRepair(OnesDoc):
car = ForeignKey(Cars, db_column='Автомобиль', related_name='carOrderOutfit')
recommen = peewee.CharField(db_column='Рекомендации')
dep = ForeignKey(Dep, db_column='ПодразделениеКомпании', related_name='Orders')
class Meta:
db_table = 'd_ЗаявкаНаРемонт'
class OrderOutfit(OnesDoc):
car = ForeignKey(Cars, db_column='Автомобиль', related_name='carOrderRepair')
recommen = peewee.CharField(db_column='Рекомендации')
dep = ForeignKey(Dep, db_column='ПодразделениеКомпании', related_name='Queries')
class Meta:
db_table = 'd_ЗаказНаряд'
class RecordToLogRecord_Periods(OnesTable):
ref = ForeignKey(RecordToLogRecord, db_column='Ссылка')
kindWork = ForeignKey(LogRecordKindWork, db_column='ЖурналЗаписиВидРаботы')
periodStart = DateField(db_column="ПериодНачало")
periodEnd = DateField(db_column="ПериодОкончание")
employee = ForeignKey(Employee, db_column='Сотрудник')
num_str = peewee.DecimalField(db_column="НомерСтроки")
class Meta:
db_table = 'd_ЗаписьВЖурналЗаписи_ПериодыРемонта'
...
# region Задачи
class CRM_TasksService(OnesTask):
buisnessProcess = ForeignKey(CRM_BuisnessProcessService, db_column='БизнесПроцесс')
pointRoute = ForeignKey(CRM_BuisnessProcessService_PointRoute, db_column='ТочкаМаршрута')
noteByTask = peewee.CharField(db_column='ЗаметкаПоЗадаче', max_length=256)
client = ForeignKey(ClientsCRM, db_column='Клиент')
contragent = ForeignKey(Contragent, db_column='Контрагент')
user = ForeignKey(User, db_column='Пользователь')
dep = ForeignKey(Dep, db_column='ПодразделениеКомпании')
employee = ForeignKey(Employee, db_column='Сотрудник')
kindContact = ForeignKey(CRM_TypeEvent, db_column='ВидКонтакта')
document = ForeignKey(Event, db_column='Документ', related_name='tasks')
dateEnd = DateField(db_column='ДатаОкончания')
dateNotify = DateField(db_column='ВремяНапоминания')
position = ForeignKey(Positions, db_column='Должность')
isNotActual = BoolField(db_column='НеСостоялась')
completed = BoolField(db_column='Выполнена')
class Meta:
db_table = 't_CRM_ЗадачиСервиса'
# endregion
# region План видов характеристик
class RightAndSetting(OnesRef):
class Meta:
db_table = 'c_ПраваИНастройки'
# endregion
# region Регистры сведений
class CarsCharacteristics(OnesCore):
period = DateField(db_column="Период")
car = ForeignKey(Cars, db_column='Автомобиль', related_name='car_char')
kindValue = ForeignKey(AdditionalCarInformation, db_column='ВидЗначения')
valueType= peewee.BlobField(db_column='Значение_типПоля')
valueStr = peewee.CharField(db_column='Значение_строка', max_length=20)
valueInt = peewee.IntegerField(db_column='Значение_число')
valueRef = ForeignKey(OnesRef, db_column='Значение_ссылка', related_name='valRef_char')
class Meta:
# db_table = '_InfoRg2828'
db_table = 's_Автомобили'
primary_key = False
def save(self, force_insert = False, recursive = True):
if not self.period:
self.period = datetime.now().date()
peewee.Model.save(self)
@staticmethod
def slice(**kwargs):
res = (CarsCharacteristics
.select(
CarsCharacteristics.kindValue.alias('kindValue'),
CarsCharacteristics.car.alias('car'),
peewee.fn.MAX(CarsCharacteristics.period).alias('MAXPERIOD_')
)
.join(Cars, on=(CarsCharacteristics.car == Cars.ref))
.group_by(
CarsCharacteristics.kindValue,
CarsCharacteristics.car
)
.alias("subquery")
)
if kwargs.get('period') is not None:
res = res.where(CarsCharacteristics.period <= (kwargs.get('period')))
if kwargs.get('car') is not None:
res = res.where(CarsCharacteristics.car == (kwargs.get('car')))
if kwargs.get('vin') is not None:
res = res.where(Cars.vin.contains(kwargs.get('vin')) or Cars.vin2.contains(kwargs.get('vin')))
res1 = (CarsCharacteristics
.select(
CarsCharacteristics.kindValue,
CarsCharacteristics.valueStr,
CarsCharacteristics.valueRef,
CarsCharacteristics.valueInt,
AdditionalCarInformation.name,
Cars.name.alias("carName"),
Cars.ref,
Cars.vin,
Cars.vin2
)
.join(AdditionalCarInformation)
.join(Cars, on=(CarsCharacteristics.car == Cars.ref))
.join(res, on=(
(CarsCharacteristics.kindValue == res.c.kindValue) &
(CarsCharacteristics.period == res.c.MAXPERIOD_) &
(CarsCharacteristics.car == res.c.car)))
.order_by(Cars.name.asc()))
if kwargs.get('contragent') is not None:
res1 = res1.where(CarsCharacteristics.valueRef == (kwargs.get('contragent')))
res1 = res1.where(AdditionalCarInformation.name == 'Хозяин')
if kwargs.get('number') is not None:
res1 = res1.where(CarsCharacteristics.valueStr.contains(kwargs.get('number')))
res1 = res1.where(AdditionalCarInformation.name == 'ГосНомер')
if kwargs.get('notActualCars') is not None:
if len(kwargs.get('notActualCars'))>0:
res1 = res1.where(CarsCharacteristics.car.not_in((kwargs.get('notActualCars'))))
return res1
... ну так далее
тут думаю без меня всё и так понятно. описывается связь логики с физическим хранением. Могу добавить лишь то что в 1с есть регламентное задание которое создает view(create view ... with nolock ... и так далее, отсюда и имена таблиц s_, r_, e_ и прочие) этим же самым решается проблема внутреннего именование таблиц, вида db_table = '_InfoRg2828 -> 'db_table = 's_Автомобили
# coding:utf8
from socketserver import ThreadingMixIn
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
import urllib.request
from urllib.parse import urlparse, parse_qs
import socket
import threading
from datetime import datetime
from datetime import timedelta
from common import common
from common.orm import models as orm
import peewee
from playhouse.shortcuts import case
import base64
import sys
import pymssql
import uuid
import time
import _thread
class ExternalRecordLog_Exception(Exception):
pass
class ExternalRecordLog_Handler(BaseHTTPRequestHandler):
callback = None
def log_message(self, format, *args):
return
def smart_response(self, code, message, headers = []):
self.send_response(code)
for h, v in headers:
self.send_header(h, v)
self.end_headers()
if (code != 200):
print(message)
message = "<br><br><br><center><h2>" + message + "</h2>"
return self.wfile.write(message.encode())
def do_GET(self):
path = urlparse(self.path).path
qs = urlparse(self.path).query
qs = parse_qs(qs)
res = self.callback(path, qs, self)
class ExternalRecordLog():
server = None
parser = common.createParser()
namespace = parser.parse_args(sys.argv[1:])
server_host = namespace.ip
server_port = int(namespace.port)
handler = ExternalRecordLog_Handler
cache = {}
cache_content = {}
cache_dep = {}
empl_dev = {}
work_time = {}
upd_event_list = []
req_count_all = 0
req_count_hit = 0
req_count_fault = 0
req_count_upd = 0
def __init__(self, caller = None):
self.handler.callback = self.callback
_thread.start_new_thread(self.upd_loop, ())
стартер создает сохдает как мы помним объект ExternalRecordLog. Вот так он инициализируется. ExternalRecordLog_Handler - обработчик запросов http, do_GET - функция обработки(туда попадет когда пакет прилетит), выражение res = self.callback(path, qs, self) - выполнить логику, вернуть ответ.
def callback(self, path, qs, handler):
while path and path[0] == '/':
path = path[1:]
d_ref = None
date = None
r_ref = None
is_garant = None
fn = None
if qs.get('d_ref'):
d_ref = qs.get('d_ref')[0].upper()
if qs.get('date'):
date = qs.get('date')[0].upper()
date = datetime.strptime(date, "%d.%m.%Y %H:%M:%S")
date = date.replace(hour=0, minute=0, second=0, microsecond=0)
if qs.get('r_ref'):
r_ref = qs.get('r_ref')[0].upper()
if qs.get('is_garant'):
is_garant = qs.get('is_garant')[0]
is_garant = is_garant=='Да'
if qs.get('fn'):
fn = qs.get('fn')[0].upper()
self.req_count_all += 1
try:
res = eval('self.%s(d_ref, date, r_ref, is_garant)' % path)
except KeyError as e:
return handler.smart_response(500, "Не задано значение параметра: %s" % e)
except ValueError as e:
return handler.smart_response(500, "Ошибка в значении параметра: %s" % e)
except ExternalRecordLog_Exception as e:
return handler.smart_response(500, "%s" % e)
except Exception as e:
return handler.smart_response(500, "Неожиданная ошибка: %s" % e)
if not res:
res = []
else:
empl_dev = self.empl_dev.get(d_ref)
if fn=='XML_ONES':
res = res_to_xml(res, date, self.cache_content, r_ref, is_garant, empl_dev)
else:
if path=='get':
dep_work_time = self.work_time[d_ref][date]
list_time_reserved = self.cache_dep.get(d_ref)['list_time_reserved']
res = res_to_vis_js(res, date, self.cache_content, dep_work_time, list_time_reserved, r_ref, is_garant, empl_dev)
res = json.dumps(res, default=common.json_serial)
try:
handler.smart_response(200, res, [
#("Content-type", "application/json"),
("Access-Control-Allow-Origin", "*"),
("Access-Control-Expose-Headers", "Access-Control-Allow-Origin"),
("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"),
])
except socket.error as e:
pass
return
else:
handler.smart_response(401, "Unauthorized call: %s from %s" % (path, client_address))
переход в обработку запроса, разбираются параметры запроса, его путь, и да срабатывает евал (по роутинг в курсе), (про кваргс тоже). Собственно отсюда и дергаются методы типа localhost\get?dep='asda'&date='2018.02.03' - что перерастает в self.get(dep, date), то что вернет метод (данные без оформления) упаковывается в json(для vis js), или xml( для 1с)
def get(self, d_ref, date=None, r_ref=None, is_garant=None):
if self.cache_dep.get(d_ref) is None:
time_acceptions = get_time_acceptions(d_ref)
time_begin = get_time_begin(d_ref)
time_end = get_time_end(d_ref)
list_time_reserved = get_setting_record_log(d_ref, 'B9A50C920CD0772B473E192A276B0B8E')
self.cache_dep[d_ref] = {'time_end' : time_end, 'time_acceptions' : time_acceptions, 'time_begin' : time_begin, 'list_time_reserved' : list_time_reserved}
time_acceptions = self.cache_dep.get(d_ref)['time_acceptions']
time_begin = self.cache_dep.get(d_ref)['time_begin']
time_end = self.cache_dep.get(d_ref)['time_end']
list_time_reserved = self.cache_dep.get(d_ref)['list_time_reserved']
if self.empl_dev.get(d_ref) is None:
self.empl_dev[d_ref] = get_empl_dev(d_ref, date)
if self.work_time.get(d_ref) is None:
self.work_time[d_ref] = {}
if not date is None:
dep_work_time = self.work_time[d_ref]
if dep_work_time.get(date) is None:
dep_work_time = get_dep_work_time(d_ref, date)
self.work_time[d_ref].update({date : dep_work_time})
if self.cache.get(d_ref) is None:
res = build_record_log(d_ref, date)
self.req_count_fault += 1
res['time_acceptions'] = time_acceptions
res['time_begin'] = time_begin
res['time_end'] = time_end
build_cache_content(res, self.cache_content)
self.cache[d_ref] = {}
self.cache[d_ref][date] = res
else:
cache_dep = self.cache.get(d_ref)
if cache_dep.get(date) is None:
res = build_record_log(d_ref, date)
self.req_count_fault += 1
res['time_acceptions'] = time_acceptions
res['time_begin'] = time_begin
res['time_end'] = time_end
build_cache_content(res, self.cache_content)
self.cache[d_ref][date] = res
else:
res = cache_dep.get(date)
self.req_count_hit += 1
return res
Если коротко - если данных нет в кэше(типа хэш таблица) делает запрос в бд , добавляет результат в кэш, далее будет отслеживать ее при помощи триггеров 3ей стороны.
def upd_loop(self):
t_loop = 0.150
while True:
for x in self.upd_event_list:
d_ref = x['d_ref']
if self.cache.get(d_ref) is None:
self.upd_event_list.remove(x)
continue
date = x['date']
self.cache[d_ref][date] = None
self.get(d_ref, date)
self.upd_event_list.remove(x)
tsdate = int(date.timestamp())
jsonurl = urllib.request.urlopen('http://dev.ahost.ru/api__unsafe2_send_upd_records?d_ref='+str(d_ref)+'&date='+str(tsdate))
time.sleep(t_loop)
фоновое задание если в терминах 1с, крутится с тайм аутом. Читает массив для обновления данных(туда данные добавляются с триггеров), ну и выполняет обновление(дергает get), триггерит в инхаус вэб апп, которая в конечном счете придет в get и получит обновленные данные.
def clear(self, d_ref, date, r_ref=None, is_garant=None):
self.cache = {}
self.cache_content = {}
self.cache = {}
self.cache_content = {}
self.cache_dep = {}
self.empl_dev = {}
self.work_time = {}
self.self.upd_event_list = []
return S_OK
def view(self, d_ref, date, r_ref=None, is_garant=None):
res = {}
for x in self.cache:
res[x] = []
for y in self.cache.get(x):
res[x].append(y)
return json.dumps(res, default=common.json_serial)
def req_view(self, d_ref, date, r_ref=None, is_garant=None):
res = {'req_count_all':self.req_count_all, 'req_count_hit':self.req_count_hit, 'req_count_fault':self.req_count_fault, 'req_count_upd':self.req_count_upd }
return json.dumps(res, default=common.json_serial)
прочие функции
используются для управления и мониторингом приложения, выводит сколько обращений было, сколько попало в кэш, сколько нет, сколько всего запросов было и тд, очищает поля класса(сбрасывает кэш)
def add_to_upd_event_list(self, d_ref, date):
self.upd_event_list.append({ 'd_ref':d_ref, 'date':date })
def upd(self, d_ref, date, r_ref=None, is_garant=None):
self.req_count_upd += 1
self.add_to_upd_event_list(d_ref, date)
return S_OK
def upd_from_order(self, d_ref=None, date=None, r_ref=None, is_garant=None):
self.req_count_upd += 1
if r_ref is None:
return S_OK
self.empl_dev[d_ref] = None
res = (orm.RecordToLogRecord
.select(
orm.RecordToLogRecord.ref.alias('r_ref'),
orm.RecordToLogRecord.dep.alias('d_ref'),
orm.RecordToLogRecord.ref_ones.alias('r_ref_ones'),
orm.RecordToLogRecord.orderOutfit.alias('z_ref'),
orm.RecordToLogRecord.orderRepair.alias('q_ref'),
orm.RecordToLogRecord_Periods.periodStart.alias('r_begin'),
orm.RecordToLogRecord_Periods.periodEnd.alias('r_end'),
)
.join(orm.RecordToLogRecord_Periods, on=(
(orm.RecordToLogRecord_Periods.ref == orm.RecordToLogRecord.ref)
))
.where(orm.RecordToLogRecord.orderOutfit == r_ref))
list_res = list(res.dicts())
for x in list_res:
d_ref = x['d_ref']
date = x['r_begin']
date = date.replace(hour=0, minute=0, second=0, microsecond=0)
self.add_to_upd_event_list(d_ref, date)
return S_OK
def upd_from_invoice(self, d_ref=None, date=None, r_ref=None, is_garant=None):
self.req_count_upd += 1
if r_ref is None:
return S_OK
res = (orm.RecordToLogRecord
.select(
orm.RecordToLogRecord.ref.alias('r_ref'),
orm.RecordToLogRecord.dep.alias('d_ref'),
orm.RecordToLogRecord.ref_ones.alias('r_ref_ones'),
orm.RecordToLogRecord.orderOutfit.alias('z_ref'),
orm.RecordToLogRecord.orderRepair.alias('q_ref'),
orm.RecordToLogRecord_Periods.periodStart.alias('r_begin'),
orm.RecordToLogRecord_Periods.periodEnd.alias('r_end'),
)
.join(orm.RecordToLogRecord_Periods, on=(
(orm.RecordToLogRecord_Periods.ref == orm.RecordToLogRecord.ref)
))
.where(orm.RecordToLogRecord.orderRepair == r_ref))
list_res = list(res.dicts())
for x in list_res:
d_ref = x['d_ref']
date = x['r_begin']
date = date.replace(hour=0, minute=0, second=0, microsecond=0)
self.add_to_upd_event_list(d_ref, date)
return S_OK
def upd_from_worksheet(self, d_ref, date=None, r_ref=None, is_garant=None):
self.req_count_upd += 1
if not self.work_time.get(d_ref) is None:
work_time = self.work_time.get(d_ref)
del self.work_time[d_ref]
if not self.cache.get(d_ref) is None:
self.cache[d_ref] = None
return S_OK
обработка триггеров 3-й стороны(1с)
ну тут всё понятно и так прилетал тригер, добавили структуру параметров для обновления в upd_event_list через add_to_upd_event_list. Обратите внимание на конструкцию вида:
res = (orm.RecordToLogRecord
.select(
orm.RecordToLogRecord.ref.alias('r_ref'),
orm.RecordToLogRecord.dep.alias('d_ref'),
orm.RecordToLogRecord.ref_ones.alias('r_ref_ones'),
orm.RecordToLogRecord.orderOutfit.alias('z_ref'),
orm.RecordToLogRecord.orderRepair.alias('q_ref'),
orm.RecordToLogRecord_Periods.periodStart.alias('r_begin'),
orm.RecordToLogRecord_Periods.periodEnd.alias('r_end'),
)
.join(orm.RecordToLogRecord_Periods, on=(
(orm.RecordToLogRecord_Periods.ref == orm.RecordToLogRecord.ref)
))
.where(orm.RecordToLogRecord.orderRepair == r_ref))
list_res = list(res.dicts())
Не напоминает запрос в 1с? а потом РезультатЗапроса.Выгрузить()? Доп логика запроса вынесена из 1с, это когда нужно доп запрос сделать, чтобы например найти по заказ наряду ссылку на другой объект.
def build_record_log(d_ref, date):
dep = orm.Dep.get(orm.Dep.ref == d_ref)
pos = get_positions()
res = (orm.RecordToLogRecord
.select(
orm.Employee.ref.alias('m_ref'),
orm.Employee.name.alias('m_name'),
orm.Employee.phone.alias('m_phone'),
orm.Employee.ref_ones.alias('m_ref_ones'),
orm.RecordToLogRecord.mark.alias('r_mark'),
orm.RecordToLogRecord.posted.alias('r_post'),
orm.RecordToLogRecord.ref.alias('r_ref'),
orm.RecordToLogRecord.ref_ones.alias('r_ref_ones'),
orm.RecordToLogRecord.customer.alias('c_ref'),
orm.ClientsCRM.name.alias('c_name'),
orm.RecordToLogRecord.phone.alias('c_phone'),
orm.RecordToLogRecord.orderOutfit.alias('z_ref'),
orm.RecordToLogRecord.orderRepair.alias('q_ref'),
orm.RecordToLogRecord.notCome,
orm.RecordToLogRecord.reason,
orm.RecordToLogRecord.phone,
orm.RecordToLogRecord.carStr,
orm.RecordToLogRecord.carNumber,
orm.Contragent.name.alias('k_name'),
orm.LogRecordKindWork.ref_ones.alias('kind_work_ref'),
orm.Cars.name.alias('car_name'),
orm.KindRepair.name.alias('kind_repair_name'),
orm.User.name.alias('user_name'),
case(None, (
(orm.OrderOutfit.posted == True, 'closed'),
(orm.RecordToLogRecord.orderOutfit != '', 'order'),
(orm.RecordToLogRecord.orderRepair != '', 'query'),
), 'record'
).alias('type'),
case(None, (
(orm.KindRepairCategory.name =='Гарантийный', True),
), False
).alias('is_guarantee'),
local = False
)
.join(orm.RecordToLogRecord_Periods, on=(
(orm.RecordToLogRecord_Periods.ref == orm.RecordToLogRecord.ref)
))
.join(orm.LogRecordKindWork, on=(
(orm.LogRecordKindWork.ref == orm.RecordToLogRecord_Periods.kindWork)
))
.join(orm.ClientsCRM, join_type=peewee.JOIN.LEFT_OUTER, on=(
(orm.ClientsCRM.ref == orm.RecordToLogRecord.customer)
))
.join(orm.OrderOutfit, join_type=peewee.JOIN.LEFT_OUTER, on=(
(orm.OrderOutfit.ref == orm.RecordToLogRecord.orderOutfit)
))
.join(orm.KindRepair, join_type=peewee.JOIN.LEFT_OUTER, on=(
(orm.KindRepair.ref == orm.RecordToLogRecord.kindRepair)
))
.join(orm.KindRepairCategory, join_type=peewee.JOIN.LEFT_OUTER, on=(
(orm.KindRepairCategory.ref == orm.KindRepair.category)
))
.join(orm.Contragent, join_type=peewee.JOIN.LEFT_OUTER, on=(
(orm.Contragent.ref == orm.RecordToLogRecord.contragent)
))
.join(orm.Cars, join_type=peewee.JOIN.LEFT_OUTER, on=(
(orm.Cars.ref == orm.RecordToLogRecord.car)
))
.join(orm.User, join_type=peewee.JOIN.LEFT_OUTER, on=(
(orm.User.ref == orm.RecordToLogRecord.author)
))
)
res = (res
.select(
case(None, (
(orm.RecordToLogRecord.ref >> None, orm.WorksheetKIA.date),
), orm.RecordToLogRecord_Periods.periodStart
).alias('r_begin'),
case(None, (
(orm.RecordToLogRecord.ref >> None, orm.WorksheetKIA.date),
), orm.RecordToLogRecord_Periods.periodEnd
).alias('r_end'),
orm.Positions.name.alias('pos_name'),
orm.RecordToLogRecord_Periods.num_str,
case(None, (
(orm.Positions.name == 'Мастер-приемщик', 999),
), 0
).alias('order1'),
)
.join(orm.WorksheetKIA, join_type=peewee.JOIN.FULL, on=(
(orm.WorksheetKIA.employee == orm.RecordToLogRecord_Periods.employee) &
(orm.RecordToLogRecord_Periods.periodStart >= date) &
(orm.RecordToLogRecord_Periods.periodEnd < date + timedelta(days=1)) &
(orm.RecordToLogRecord.mark == False) &
(orm.RecordToLogRecord.posted == True) &
(orm.RecordToLogRecord.notCome == False) &
(orm.RecordToLogRecord.dep == dep)
)
)
.join(orm.JobMark, on=(
(orm.JobMark.ref == orm.WorksheetKIA.mark)
)
)
.join(orm.Employee, on=(
(orm.Employee.ref == orm.WorksheetKIA.employee)
)
)
.join(orm.Positions, on=(
(orm.Employee.position == orm.Positions.ref)
)
)
.slice(
orm.Staff_Work_Schedule.employee,
on = (
(orm.WorksheetKIA.employee == orm.Staff_Work_Schedule.employee)
)
)
.join(orm.KindsShedule, on=(
(orm.KindsShedule.ref == orm.Staff_Work_Schedule.kindShedule)
)
)
.where(orm.WorksheetKIA.dep == dep)
.where(orm.WorksheetKIA.date == date)
.where(orm.JobMark.name << orm.JobMark.getJobMarkWorkTime())
.where(orm.Positions.name << pos)
.where(orm.Staff_Work_Schedule.dep == base64.b16decode(d_ref.encode()))
)
data = {}
count = 0
data['date_upd'] = datetime.now()
data['empl'] = {}
list_res = list(res.dicts())
for x in list_res:
if x['pos_name']=='Сервис-менеджер':
x['order1'] = 1000
list_res = sorted(list_res, key=lambda e: e['m_name'])
list_res = sorted(list_res, key=lambda e: e['pos_name'])
list_res = sorted(list_res, key=lambda e: e['order1'])
for x in list_res:
empls = data['empl']
empl = empls.get(x['m_ref'])
if empl is None:
empl = {'m_ref' : x['m_ref'], 'm_name' : x['m_name'], 'm_phone' : x['m_phone'], 'm_ref_ones' : x['m_ref_ones'], 'periods':[], 'pos_name' : x['pos_name']}
empls[x['m_ref']] = empl
empl['periods'].append(x)
for i in empl['periods']:
if i.get('r_begin', 0).year > 4000:
i['r_begin'] = i['r_begin'].replace(year = i['r_begin'].year - 2000)
i['r_end'] = i['r_end'].replace(year = i['r_end'].year - 2000)
return data
выполняет запрос к бд, возвращает данные без оформления, если коротко то это бизнес логика(1с)
далее идут служебные функция вида получить доп данные запросом(например время приемки, время начала приемки ну и тд, выполняются по аналогии как выше)
def res_to_xml(res, date, cache_content, r_ref, is_garant, empl_dev):
time_acceptions = res['time_acceptions']
time_begin = res['time_begin']
time_end = res['time_end']
links = []
import common.ones_xml_types
xml_res = ''
xml_res += common.ones_xml_types.xml_header_start()
xml_res += common.ones_xml_types.xml_chart()
xml_res += common.ones_xml_types.xml_points_start_header_start()
if len(res['empl'])==0:
xml_res += common.ones_xml_types.xml_points_value_empty()
xml_res += common.ones_xml_types.xml_points_content_cache_item()
xml_res += common.ones_xml_types.xml_points_start_header_end()
xml_res += common.ones_xml_types.xml_series_empty()
else:
xml_res += common.ones_xml_types.xml_points_value_0(len(res['empl']))
itemKey = 0
for x in res['empl']:
itemKey +=1
rightKey = itemKey+1
if itemKey== len(res['empl']):
rightKey = 0
empl = res['empl'].get(x)
str_empl_dev = ''
if not empl_dev is None:
for y in empl_dev:
if y['employee']==x:
str_empl_dev = ' ('+str(round(y['count'], 1))+' ч)'
xml_res += common.ones_xml_types.xml_points_value(itemKey, rightKey, empl['m_ref_ones'], (empl['m_name']+' , '+empl['pos_name'])[0:30]+str_empl_dev)
xml_res += common.ones_xml_types.xml_points_content_cache_item()
for x in res['empl']:
empl = res['empl'].get(x)
backColor = 'auto'
if res['empl'].get(x)['pos_name'] == 'Мастер-приемщик':
backColor = '#dcdcdc'
xml_res += common.ones_xml_types.xml_points_content_cache_item('#000000', '#000000', backColor)
xml_res += common.ones_xml_types.xml_points_start_header_end()
xml_res += common.ones_xml_types.xml_series(date)
intervals_gant = []
intervalKey = 0
_var_a = 4294967297
itemKey = 0
for x in res['empl']:
itemKey = itemKey+1
magic = itemKey*_var_a
if itemKey!=1:
magic = magic-itemKey+1
intervals_gant.append(magic)
for i in res['empl'].get(x)['periods']:
if i['r_ref_ones'] is None:
continue
intervalKey +=1
if is_garant==True:
color = '#faebd7'
if i['is_guarantee']==1:
color = '#a6caf0'
else:
color = '#ffa500'
if i['type']=='order':
color = '#007700'
elif i['type']=='closed':
color = '#dcdcdc'
elif i['type']=='query':
color = '#c6c600'
elif i['type']=='record' and not i['z_ref'] is None:
color = '#007700'
if i['is_guarantee']==1 and (not i['z_ref'] is None or not i['q_ref'] is None):
color = '#ffff00'
if i['type']=='record' and i['z_ref'] is None and i['q_ref'] is None and res['empl'].get(x)['pos_name']=='Мастер-приемщик':
var_b = False
for var_x in res['empl']:
if res['empl'].get(var_x)['pos_name']=='Мастер-приемщик':
continue
for var_i in res['empl'].get(var_x)['periods']:
if i['r_ref_ones']==var_i['r_ref_ones']:
var_b = True
break
if var_b!=True:
color = '#0064ff'
content = cache_content.get(i['r_ref']).get(x)
if r_ref==i['r_ref']:
color = '#8CC88C'
links.append(intervalKey)
xml_res += common.ones_xml_types.xml_main_interval(intervalKey, i, magic, color, content)
itemKey = 0
for x in intervals_gant:
xml_res += common.ones_xml_types.xml_main_value(x, r_ref)
xml_res += common.ones_xml_types.xml_header_end_start(date, datetime.now(), time_acceptions, time_begin, time_end)
links.sort()
if not r_ref is None:
x = 0
list_len = -(len(links)-1)
while x>list_len:
end = links[-x]
begin = links[-(x+1)]
if x==len(links)-1:
end = links[0]
else:
pass
xml_res += common.ones_xml_types.xml_header_end_link(begin, end)
x -= 1
xml_res += common.ones_xml_types.xml_header_end_end()
return xml_res
преобразует результат запроса в xml строку для 1с. Обратите внимание на _var_a = 4294967297 это базовое число с которого начинается индекс=0 для вставки интервалов.
def res_to_vis_js ... ()
_time_begin = '2018-01-01 time_begin:00'.replace('time_begin', data['time_end'])
_time_end = '2018-01-02 time_end:00'.replace('time_end', data['time_begin'])
data['timeline'] = {}
data['timeline']['hiddenDates'] = [{ 'start': _time_begin, 'end': _time_end, 'repeat': 'daily' }]
timeline = data['timeline']
timeline['groups'] = []
timeline['items'] = []
timeline['items_unselected'] = []
timeline['items_garant'] = []
timeline['links'] = {}
time_acceptions = data['time_acceptions']
time_accept_step = (int)(time_acceptions/TIME_STEP)
empls = data['empl']
val = 0
for x in empls:
val += 1
empl = empls.get(x)
className = 'openwheel'
style = "background-color: white;"
if empl['pos_name']=='Мастер-приемщик':
style = "color: white; background-color: grey;"
content = (empl['m_name']+', '+str(empl['pos_name']))[0:30],
d = {'content':str(content[0]), 'id':empl['m_ref'], 'value':val, 'className': className, 'style': style}
timeline['groups'].append(d)
work_time_empl = dep_work_time.get(x)
wt_start_accept = None
_wt_start_accept = []
if not work_time_empl is None:
_wt_start_accept = []
for wt_l in work_time_empl['lunch_time']:
wt_l_date_end = wt_l['period_date_time']+timedelta(minutes=TIME_STEP)
style = 'color: red; background-color: #CCCCCC;'
d3 = {'start' : wt_l['period_date_time'].strftime("%Y-%m-%dT%H:%M:00.000"),
'end' : wt_l_date_end.strftime("%Y-%m-%dT%H:%M:00.000"),
'group' : x,
'content' : '',
'title' : '',
'id' : str(uuid.uuid4()),
'type' : 'background',
'style' : style}
timeline['items'].append(d3)
timeline['items_unselected'].append(d3)
timeline['items_garant'].append(d3)
for wt_n in work_time_empl['not_working_time']:
wt_l_date_end = wt_n['period_date_time']+timedelta(minutes=TIME_STEP)
style = 'color: red; background-color: #CCCCCC;'
d2 = {'start' : wt_n['period_date_time'].strftime("%Y-%m-%dT%H:%M:00.000"),
'end' : wt_l_date_end.strftime("%Y-%m-%dT%H:%M:00.000"),
'group' : x,
'content' : '',
'title' : '',
'id' : str(uuid.uuid4()),
'type' : 'background',
'style' : style}
timeline['items'].append(d2)
timeline['items_unselected'].append(d2)
timeline['items_garant'].append(d2)
for v in list_time_reserved:
if empl['pos_name']!='Мастер-приемщик':
break
wt_start_accept = []
last_date_max = None
for wt_w in work_time_empl['working_time']:
if wt_w['period_date_time'].hour<v['accept_start'].hour:
continue
if wt_w['period_date_time'].hour>v['accept_end'].hour:
continue
if last_date_max is None:
last_date_max = wt_w['period_date_time']+timedelta(minutes=time_acceptions)
wt_start_accept.append(wt_w['period_date_time'])
if not wt_w['period_date_time'] in _wt_start_accept:
_wt_start_accept.append(wt_w['period_date_time'])
if last_date_max==wt_w['period_date_time']:
wt_start_accept.append(wt_w['period_date_time'])
if not wt_w['period_date_time'] in _wt_start_accept:
_wt_start_accept.append(wt_w['period_date_time'])
last_date_max = wt_w['period_date_time']+timedelta(minutes=time_acceptions)
else:
for __x in work_time_empl['lunch_time']:
if __x['period_date_time']==last_date_max:
last_date_max = work_time_empl['lunch_time'][len(work_time_empl['lunch_time'])-1]['period_date_time']+timedelta(minutes=TIME_STEP)
break
rule = {'every' : 1, 'from':1}
if v['accept_proc'] is None:
continue
elif v['accept_proc']==0:
rule = {'every' : 0, 'from':0}
elif v['accept_proc']==100:
rule = {'every' : 1, 'from':1}
elif v['accept_proc']==50:
rule = {'every' : 1, 'from':2}
elif v['accept_proc']==33:
rule = {'every' : 2, 'from':3}
elif v['accept_proc']==66:
rule = {'every' : 1, 'from':3}
elif v['accept_proc']==25:
rule = {'every' : 1, 'from':4}
count = 1
for _x in wt_start_accept:
if count==rule['from']:
count = 1
continue
if count<=rule['every']:
style = 'color: red; background-color: #CCFFFF;'
d4 = {'start' : _x.strftime("%Y-%m-%dT%H:%M:00.000"),
'end' : (_x+timedelta(minutes=time_acceptions)).strftime("%Y-%m-%dT%H:%M:00.000"),
'group' : x,
'content' : '',
'title' : '',
'id' : str(uuid.uuid4()),
'type' : 'background',
'style' : style}
timeline['items'].append(d4)
timeline['items_unselected'].append(d4)
timeline['items_garant'].append(d4)
count += 1
wt_accept = []
for y in empl['periods']:
if y['r_ref'] is None:
continue
if timeline['links'].get(y['r_ref']) is None:
timeline['links'][y['r_ref']] = []
timeline['links'][y['r_ref']].append(y['r_ref']+'_'+str(y['num_str']))
class_name = get_class_name_from_record(y, empl, is_garant, data)
content = get_content_from_record(y)
if r_ref is None:
d = {'start' : y['r_begin'].strftime("%Y-%m-%dT%H:%M:00.000"),
'end' : y['r_end'].strftime("%Y-%m-%dT%H:%M:00.000"),
'group' : x,
'className' : class_name,
'content' : str(y['c_name']),
'title': content,
'id': y['r_ref']+'_'+str(y['num_str'])}
else:
d = {'start' : y['r_begin'].strftime("%Y-%m-%dT%H:%M:00.000"),
'end' : y['r_end'].strftime("%Y-%m-%dT%H:%M:00.000"),
'group' : x,
'className' : 'unselected',
'content' : '',
'title': '',
'editable' : False,
'id': y['r_ref']+'_'+str(y['num_str'])}
if r_ref==y['r_ref']:
d = {'start' : y['r_begin'].strftime("%Y-%m-%dT%H:%M:00.000"),
'end' : y['r_end'].strftime("%Y-%m-%dT%H:%M:00.000"),
'group' : x,
'className' : 'select_ref',
'content' : str(y['c_name']),
'title': '',
'editable' : True,
'id': y['r_ref']+'_'+str(y['num_str'])}
timeline['items'].append(d)
wt_accept.append({'r_begin' : y['r_begin'], 'r_end' : y['r_end']})
d1 = {'start' : y['r_begin'].strftime("%Y-%m-%dT%H:%M:00.000"),
'end' : y['r_end'].strftime("%Y-%m-%dT%H:%M:00.000"),
'group' : x,
'className' : 'unselected',
'content' : '',
'title': content,
'id': y['r_ref']+'_'+str(y['num_str'])}
timeline['items_unselected'].append(d1)
class_name = get_class_name_from_record(y, empl, True, data)
d2 = {'start' : y['r_begin'].strftime("%Y-%m-%dT%H:%M:00.000"),
'end' : y['r_end'].strftime("%Y-%m-%dT%H:%M:00.000"),
'group' : x,
'className' : 'unselected',
'content' : str(y['c_name']),
'title': content,
'id': y['r_ref']+'_'+str(y['num_str'])}
timeline['items_garant'].append(d2)
if empl['pos_name']=='Мастер-приемщик':
if not _wt_start_accept is None:
for _y in wt_accept:
for _x in _wt_start_accept:
_x_end = _x+timedelta(minutes=time_acceptions)
if _x.hour == _y['r_begin'].hour and _x_end.hour == _y['r_end'].hour:
if _x.minute >= _y['r_begin'].minute and _x_end.minute <= _y['r_end'].minute:
_wt_start_accept.remove(_x)
for _x in _wt_start_accept:
d5 = {'start' : _x.strftime("%Y-%m-%dT%H:%M:00.000"),
'end' : (_x+timedelta(minutes=time_acceptions)).strftime("%Y-%m-%dT%H:%M:00.000"),
'group' : x,
'content' : '<BR>',
'title' : _x.strftime("%H:%M")+'-'+(_x+timedelta(minutes=time_acceptions)).strftime("%H:%M"),
'className' : 'expected',
'id' : str(uuid.uuid4()),
#'type' : 'background',
#'style' : style
}
timeline['items'].append(d5)
#timeline['items_unselected'].append(d5)
#timeline['items_garant'].append(d5)
return data
Преобразует результат запроса для загрузки данных в vis js на фронте.
запускается всё это дело строкой вида: python36 start.py -port 2032 -ip 192.168.777.888 -sql_host localhost -sql_base my_db -sql_user my_sql_user -sql_pass my_sql_user_password
Функция ExRecordLog() Экспорт
Таймаут = 3;
Прокси = Новый ИнтернетПрокси(Ложь);
ExternalLogRecord = ПараметрыСеанса.ExternalLogRecord;
HTTPСоединение = Новый HTTPСоединение(ExternalLogRecord.Сервер, ExternalLogRecord.Порт,,,Прокси,Таймаут);
Возврат HTTPСоединение;
КонецФункции
Процедура _ОтправитьДанныеОбИзменениях_ExRecordLog(d_ref, date) Экспорт
ТелоЗапроса = "d_ref="+d_ref+"&date="+date;
HTTPСоединение = ExRecordLog();
Запрос = Новый HTTPЗапрос("/upd?"+ТелоЗапроса);
ТемаЛога = "ex_record_log_upd";
СтрокаЛога = "ТехДанные: "+ТелоЗапроса+Символы.ПС;
РаботаСВебСервисом.ЗаписатьЛог(ТемаЛога, СтрокаЛога);
Попытка
Результат = HTTPСоединение.Получить(Запрос);
Исключение
ТекстОшибки = "ОШИБКА. отправки запроса ex_record_log_upd ОПИСАНИЕ ОШИБКИ:" + ОписаниеОшибки();
СтрокаЛога = "ТехДанные: "+ТелоЗапроса+Символы.ПС;
КонецПопытки;
КонецПроцедуры
Подписка на событие(триггер), который вызывает обновление данных в кэше. Есть вариант асинхронного выполнения get запросов (winapi) приводить здесь не буду.
Шаг 2. Пишем загрузку XDTO.
Для загрузки XML(XDTO) и преобразования в диаграмму ганта используется следующий код:
Прокси = Новый ИнтернетПрокси(Ложь);
ExternalLogRecord = ПараметрыСеанса.ExternalLogRecord;
HTTPСоединение = Новый HTTPСоединение(ExternalLogRecord.Сервер, ExternalLogRecord.Порт,,,Прокси);
r_ref = РаботаСВебСервисом.ПолучитьНавиСсылку_(ссылка на документ);
d_ref = РаботаСВебСервисом.ПолучитьНавиСсылку_(ссылка на подразделение);
Результат = Неопределено;
Адрес = "/get?date="+Строка(НачалоДня(ПланировщикДатаНач))+"&d_ref="+d_ref+"&r_ref="+r_ref+"&is_garant="+Строка(доп параметр)+"&fn=XML_ONES";
HTTPЗапрос = Новый HTTPЗапрос(Адрес);
Попытка
HTTPОтвет = HTTPСоединение.Получить(HTTPЗапрос);
Исключение
Сообщить("Внешний журнал записи не доступен");
Возврат;
КонецПопытки;
Если HTTPОтвет.КодСостояния=200 Тогда
Результат = HTTPОтвет.ПолучитьТелоКакСтроку();
КонецЕсли;
НовыйСериализаторXDTO = Новый СериализаторXDTO(ФабрикаXDTO);
НовоеЧтениеXML = Новый ЧтениеXML;
НовоеЧтениеXML.УстановитьСтроку(Результат);
ДиаграммаГанта.Очистить();
ДиаграммаГанта = НовыйСериализаторXDTO.ПрочитатьXML(НовоеЧтениеXML);
ДиаграммаГанта.Обновление = Истина;
НовоеЧтениеXML.Закрыть();
НовоеЧтениеXML = Неопределено;
Данный вариант пришлось отложить от использования, потому что нужно было установить при определенных условиях фон ячейки диаграммы Ганта на пересечении(время - сотрудник). Для решения данного вопроса было принято решение уйти от штатной диаграммы, и реализовать ее в front end части web app.
Шаг 3. Пишем front end.
За основу реализации диаграммы была взята либа vis.js http://visjs.org. Обзор делать здесь не буду, либа очень крутая!
В качестве фронта используется html страница отдаваемая nginx по гет запросу.
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style>
body, html {
font-family: arial, sans-serif;
font-size: 8pt;
margin: 0;
}
#visualization {
box-sizing: border-box;
width: 100%;
}
.vis-item.is_guarantee {
background-color: #a6caf0;
}
.vis-item.not_is_guarantee {
background-color: #faebd7;
}
.vis-item.empty {
background-color: #ffa500;
}
.vis-item.order {
background-color: #007700;
}
.vis-item.closed {
background-color: #dcdcdc;
}
.vis-item.query {
background-color: #c6c600;
}
.vis-item.record_having_zn {
background-color: #007700;
}
.vis-item.not_is_guarantee_not_having_zn {
background-color: #ffff00;
}
.vis-item.not_assigned_empl {
background-color: #0064ff;
}
.vis-item.unselected {
background-color: #333333;
}
.vis-item.select_ref {
background-color: #CCCC00;
}
.vis-item.expected {
background-color: transparent;
border-style: dashed;
z-index: 1;
}
.vis-item.new_ref {
background-color: #CC0033;
}
.vis-item.is_delivery {
background-color: #990099;
}
.progress-wrapper {
background: white;
width: 100%;
height: 18px;
text-align: center;
position: relative;
overflow: hidden;
}
.progress {
height: 100%;
width: 60%;
position: absolute;
left: 0px;
top: 0px;
background: #63ed63;
}
.progress-label {
position: absolute;
z-index: 1;
}
</style>
<script src="./libs/jquery.min.js"></script>
<script src="./libs/vis.js"></script>
<link href="./libs/vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css">
<body false;">
<body>
<div id="loading">loading...</div>
<div id="visualization"></div>
<script type="text/javascript">
window.r_ref = '';
var items;
var items_unselected;
var items_garant;
var items_test;
var _items;
var items_test;
var groups;
var timeline;
var container;
var is_garant;
var properties_select;
var links;
is_garant = false;
container = document.getElementById('visualization');
timeline = new vis.Timeline(container, [], {});
function update() {
var editable;
editable = false;
if (window.editable == '1') {
editable = true
}
$.ajax({
url: 'http://'+window.external_log_record+'/get?d_ref='+window.d_ref+'&date='+window.date+'&r_ref='+window.r_ref,
success: function (data) {
document.getElementById('loading').style.display = 'none';
data_json = JSON.parse(data)
groups = new vis.DataSet(data_json.timeline.groups);
_items = data_json.timeline.items;
items = new vis.DataSet(_items);
items_test = new vis.DataSet(_items);
items_unselected = new vis.DataSet(data_json.timeline.items_unselected);
items_garant = new vis.DataSet(data_json.timeline.items_garant);
hiddenDates = data_json.timeline.hiddenDates;
links = data_json.timeline.links;
var options = {
orientation: 'top',
editable: editable,
groupEditable: false,
start: window.date.toDateFromDatetime(),
end: window.date.toDateFromDatetime().setMilliseconds(23 * 60 * 60 * 1000),
min: window.date.toDateFromDatetime(),
max: window.date.toDateFromDatetime().setMilliseconds(23 * 60 * 60 * 1000),
zoomMin: 1000 * 60 * 60 * 4,
zoomMax: 1000 * 60 * 60 * 24 * 31 * 1,
type: 'range',
multiselect: false,
hiddenDates: hiddenDates,
showMajorLabels: false,
margin: { item: -1, axis: -1 },
tooltip: {followMouse: true},
//autoResize: true,
//timeAxis: {scale: 'minute', step: 20},
//tooltip: {followMouse: true, overflowMethod: 'cap'},
};
timeline.setItems(items);
timeline.setOptions(options);
timeline.setGroups(groups);
timeline.on('select', function (properties) {
setTimeout(setSelection, 500, properties);
});
timeline.on('doubleClick', function (properties) {
logEvent('doubleClick', properties);
});
items.on('*', function (event, properties) {
item = timeline.itemsData._data[properties.items];
logEvent(event, item);
});
},
error: function (err) {
console.log('Error', err);
if (err.status === 0) {
alert('Ошибка загрузки данных');
}
else {
alert('Ошибка загрузки данных');
}
}
});
}
function setSelection(properties){
logEvent('select', properties);
if (window.r_ref!=''){
return;
}
var _items = properties.items;
if (_items.length==0){
timeline.setItems(items);
return;
}
var now = new Date().getTime();
timeline.setItems(items_unselected);
while(new Date().getTime() < now + 250){
/* do nothing */
}
var sels = [];
var i_id = _items[0].slice(0,32);
sels = links[i_id];
timeline.setSelection(sels, {
focus: true
});
}
async function update_client(){
if (is_garant == '1') {
timeline.setItems(items_garant)}
else{
timeline.setItems(items)
}
}
function stringifyObject(object) {
if (!object) return;
var replacer = function (key, value) {
if (value && value.tagName) {
return "DOM Element";
} else {
return value;
}
}
return JSON.stringify(object, replacer)
}
async function logEvent(event, properties) {
window.postComMessage('event=' + JSON.stringify(event) + ', ' +
'properties=' + stringifyObject(properties))
}
String.prototype.toDateFromDatetime = function () {
var parts = this.split(/[. :]/);
return new Date(parts[2], parts[1] - 1, parts[0], parts[3], parts[4], parts[5]);
}
function add_test_item() {
items_test.add(var_item);
timeline.setItems(items_test);
}
function del_test_item() {
timeline.setItems(items);
items_test = new vis.DataSet(_items);
}
</script>
</body>
</html>
Получаем данные с backend, формируем диаграмму, подписываемся на события(удаление, изменение, добавление, перенос и др), отправляем события 3 стороне(1C).
Естественно штатный механизм поля html не смог отобразить страницу, хотя в Opera (chromium) всё работало нормально.
Шаг 4. Встраиваем chromium (chromiumembedded ) в 1С.
Есть такая штука(chromiumembedded ) https://bitbucket.org/chromiumembedded/cef . Обзор здесь делать не буду.
И так ядро(для выполнения кода фронта) нашли. Вспоминаем что, штатный браузер 1С (поле html) это обертка над MS IE(active x web browser) , ну раз active-x, значит нужно обернуть cef в active-x и собственно всё. Далее немного погуглив находится готовое решение https://www.webkitx.com это для cef3, и https://github.com/mobileFX/WebKitX для тех кто всё сам. Делать свой врапер cef3 особого желания не было, взял для cef3.
Чтение доки нам говорит что он более чем на 100 процентов подходит нам и умеет генерировать события из js, делать калбэки, делать евал в jsк коде, имеет встроенную поддержку devtools!. ок, берем.
Для запуска active x в управляемых формах есть только один ход:
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
...
AxControl =
"<HTML>
|<style> html, body { overflow: hidden;} </style>
|<style> * {margin: 0; padding: 0;} </style>
|<OBJECT id=""AxControl""
|classid=""clsid:DFF43867-EAC5-4170-96B0-265A91723BCA"" width=""100%"" height=""100%""
|</OBJECT>
|</HTML>";
КонецПроцедуры
где AxControl - реквизит формы
&НаКлиенте
Функция ЭлементВК()
Возврат Элементы.AxControl.Документ.getElementById("AxControl");
КонецФункции
&НаКлиенте
Процедура AxControlДокументСформирован(Элемент)
ЭлементВК = ЭлементВК();
ДобавитьОбработчик ЭлементВК.contentDocument.OnBrowserReady, OnBrowserReady;
КонецПроцедуры
&НаКлиенте
Процедура OnBrowserReady()
ЭлементВК = ЭлементВК();
ЭлементВК.SetPreference("browser.enable_spellchecking", "false");
ЭлементВК.ASYNC_EVENTS = True;
ЭлементВК.ExecCommandSetFocus = True;
ЭлементВК.FormatUsingInternalSelectionApi = True;
ЭлементВК.DownloadScripts = True;
//ЭлементВК.Open("file:/// Адрес туда где хостится html");
ЭлементВК.Open(Адрес туда где хостится html);
ДобавитьОбработчик ЭлементВК.contentDocument.OnLoadEnd, OnLoadEnd;
КонецПроцедуры
&НаКлиенте
Процедура OnLoadEnd()
ЭлементВК = ЭлементВК();
ЭлементВК.Events = 1;
ЭлементВК.FireOnEventForAllEvents = False;
ДобавитьОбработчик ЭлементВК.contentDocument.OnComPostMessage, OnComPostMessage;
ЭлементВКЗагружен = Истина;
вызвать функцию обновления в 1C, типа "Обновить Диаграмму VIS JS()"
КонецПроцедуры
Инициализация webkitax
&НаКлиенте
Процедура OnComPostMessage(Params)
Если Сред(Params, 1, 18)="event=""doubleClick" Тогда
ЭлементВК = ЭлементВК();
УдалитьОбработчик ЭлементВК.contentDocument.OnComPostMessage, OnComPostMessage;
СтрокаДжейсон = СтрЗаменить(Params, "event=""doubleClick"", properties=", "");
Данные = JSON.лПрочитатьJSON(СтрокаДжейсон);
Ссылка = Данные["item"];
Ссылка = Лев(Ссылка, 32);
Логика обработки
ДобавитьОбработчик ЭлементВК.contentDocument.OnComPostMessage, OnComPostMessage;
ИначеЕсли Сред(Params, 1, 13)="event=""select" Тогда
СтрокаДжейсон = СтрЗаменить(Params, "event=""select"", properties=", "");
Данные = JSON.лПрочитатьJSON(СтрокаДжейсон);
Если Не Данные["items"].Количество()=1 Тогда
Возврат;
КонецЕсли;
r_ref = ВРег(РаботаСВебСервисом.ПолучитьНавиСсылку_(Обработка.ЗаписьЖЗ));
ТекЯчейка = Данные["items"][0];
Ссылка = Лев(ТекЯчейка, 32);
Если Не Ссылка=r_ref Тогда
Возврат;
КонецЕсли;
Логика обработки
ИначеЕсли Сред(Params, 1, 13)="event=""update" Тогда
СтрокаДжейсон = СтрЗаменить(Params, "event=""update"", properties=", "");
Данные = JSON.лПрочитатьJSON(СтрокаДжейсон);
Логика обработки
Если Сотрудник=СтрПериодРемонта.Сотрудник Тогда
ПодключитьОбработчикОжидания("ОтложенноеОкончаниеРедактированияИнтревала", 0.1, Истина);
Иначе
ПодключитьОбработчикОжидания("ПеренестиЯчейку", 0.1, Истина);
КонецЕсли;
ИначеЕсли Сред(Params, 1, 13)="event=""remove" Тогда
ПодключитьОбработчикОжидания("УдалитьЯчейкуВК", 0.1, Истина);
ИначеЕсли Сред(Params, 1, 10)="event=""add" Тогда
СтрокаДжейсон = СтрЗаменить(Params, "event=""add"", properties=", "");
Данные = JSON.лПрочитатьJSON(СтрокаДжейсон);
Если Данные["title"]="is_test" Тогда
Возврат;
КонецЕсли;
Логика обработки
Начало = Данные["start"]+60*60*5;
Конец = Данные["end"]+60*60*5;
Логика обработки
ПодключитьОбработчикОжидания("ДобавитьЯчейкуВК", 0.1, Истина);
КонецЕсли;
КонецПроцедуры
Обработка событий front end в 1С
&НаКлиенте
Процедура Отладка(Команда)
ЭлементВК = ЭлементВК();
ЭлементВК.showdevTools();
КонецПроцедуры
ЭлементВК = ЭлементВК();
Если ЭлементВК=Неопределено Тогда
Возврат;
КонецЕсли;
Если Не ЭлементВКЗагружен=Истина Тогда
Возврат;
КонецЕсли;
ExternalLogRecord = ПараметрыСеанса.ExternalLogRecord;
d_ref = ссылка на подразделение
date = дата
r_ref = ссылка на документ
Порт = СтрЗаменить(Строка(ExternalLogRecord.Порт), Символы.НПП, "");
Порт = СтрЗаменить(Порт, " ", "");
Сервер = Строка(ExternalLogRecord.Сервер);
external_log_record = Сервер+":"+Порт;
ЭлементВК.Eval("window.external_log_record = '"+external_log_record+"'");
ЭлементВК.Eval("window.d_ref = '"+d_ref+"'");
ЭлементВК.Eval("window.date = '"+date+"'");
ЭлементВК.Eval("window.editable = '1'");
ЭлементВК.Eval("window.r_ref = '"+r_ref+"'");
ЭлементВК.CallByName("update");
Тест на пустой форме, тест событий, dev tools
https://yadi.sk/d/NiXGSN0H3aMvxJ