<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="http://blogs.technet.com/utility/FeedStylesheets/rss.xsl" media="screen"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:wfw="http://wellformedweb.org/CommentAPI/"><channel><title>Investigating Dynamics AX : Разработка</title><link>http://blogs.technet.com/denisfed/archive/tags/_2004300437044004300431043E0442043A043004_/default.aspx</link><description>Tags: Разработка</description><dc:language>ru</dc:language><generator>CommunityServer 2.1 SP1 (Build: 61025.2)</generator><item><title>Новые поля в складских проводках</title><link>http://blogs.technet.com/denisfed/archive/2007/05/23/1029577.aspx</link><pubDate>Wed, 23 May 2007 16:57:00 GMT</pubDate><guid isPermaLink="false">d5e57398-b9ef-4490-9955-07cbb4e4a80d:1029577</guid><dc:creator>denisfed</dc:creator><slash:comments>0</slash:comments><comments>http://blogs.technet.com/denisfed/comments/1029577.aspx</comments><wfw:commentRss>http://blogs.technet.com/denisfed/commentrss.aspx?PostID=1029577</wfw:commentRss><description>&lt;H1&gt;&lt;/H1&gt;
&lt;P mce_keep="true"&gt;&amp;nbsp;&lt;/P&gt;
&lt;H1&gt;Общие вопросы&lt;/H1&gt;
&lt;P&gt;При внедрении DAX часто возникает примерно следующая задача: Клиент говорит что-нибудь типа "Мне нужно добавить в заказы (складские журналы, закупки, производственные заказы и т.п.) новое поле - направление продаж (код продавца, номер&amp;nbsp;автомобиля для отгрузки, идентификатор кредитной линии клиента и тп). Кроме того - мне нужно уметь строить отчеты (обычные или OLAP) по складским списаниям (приходам) в разрезе этого нового поля". 
&lt;P&gt;Первое что приходит в голову - это просто добавить новое поле в строки заказов и потом переделать некоторые складские отчеты таким образом, чтобы они строились по соединению (join) таблицы складских проводок с исходным документом. Проблема в том, что в реальности постоянно возникают ситуации, при которых&amp;nbsp;клиент, задним числом вспоминает что неплохо бы это поле добавить не только в заказы (например), но и в складские журналы списания или переноса. В этой ситуации внедренцу приходится либо плодить кучу отчетов - по одному для каждого вида складских документов, либо строить какой-то хитромудрый отчет, который собирает данные из множества таблиц. Очевидно, что оба подхода имеют свои минусы и почти не имеют плюсов. 
&lt;P&gt;Второй, более продвинутый вариант - это добавление нового поля либо в таблицу складских проводок (inventTrans) либо в таблицу складской аналитики (inventDim). Надо понимать, что с точки зрения нагрузки на систему, вариант добавления в таблицу inventDim новых полей значительно более затратный. Это связано с тем, что с ростом&amp;nbsp;числа&amp;nbsp;записей в таблице&amp;nbsp;inventDim, растет и таблица inventSum (запасы в наличии). Поскольку&amp;nbsp;модуль логистики&amp;nbsp;ПОСТОЯННО использует таблицу inventSum для получения текущего складского остатка, критически важно для производительности, чтобы эта таблица не разросталась. Поэтому, добавлять новые поля в складскую аналитику имеет смысл только при выполнении следующих условий: 
&lt;P&gt;1. Имеет смысл сальдирование по этой аналитике. Грубо говоря - количественный или денежный остаток&amp;nbsp;в разрезе этой&amp;nbsp;аналитики имеет экономический смысл. 
&lt;P&gt;2. Предполагается, что система должна проверять неотрицательность остатка в разрезе этой складской аналитики. 
&lt;P&gt;Поэтому - реально добавлять новые поля в складскую аналитику приходится достаточно нечасто. Например - ни код продавца, ни номер автомобиля явно не проходят по обоим условиям. Мне приходилось добавлять, например, складскую аналитику "Материально ответственное лицо" или "Вид продукции" (своя, комисионная, давальческая) и некоторые другие. Причем добавлять ее приходилось в первую очередь даже не для отчетности, а для контроля - чтобы нечаянно не списать с аналитики "МОЛ" или "Вид продукции" больше номенклатуры, чем на нее изначально было оприходовано. 
&lt;P&gt;Для всех остальных случаев - правильнее добавлять новое поле в inventTrans. Причем я для себя выработал следующий приблизительный критерий, позволяющий принимать решение - добавлять новое поле в inventTrans или попытаться обойтись хитрым запросом (с джойном inventTrans и исходного документа): Добавлять новое поле в inventTrans следует если выполняется одно из следующих условий: 
&lt;P&gt;1. Новый аттрибут есть более чем в одном типе складского документа 
&lt;P&gt;2. Новый аттрибут будет заполняться для более чем 25% складских проводок данного вида (то есть - либо приходов, либо расходов) 
&lt;P&gt;3. Новый аттрибут будет использоваться для вычисления дополнительных количеств в таблице остатков в наличии (Об этом подходе я напишу в другой раз. Основная идея состоит в том, чтобы в остатках в наличии иметь не только поле, допустим, "Зарезервировано", но&amp;nbsp;еще и&amp;nbsp;дополнительное поле "Зарезервировано в журналах перемещения".) 
&lt;P&gt;Наивный подход к реализации заполнения нового поля в складских проводках состоит в том, чтобы переопределить методы insert()&amp;nbsp;или update() таблиц inventTrans и, допустим, SalesLine таким образом, чтобы при обеспечить синхронизацию этого нового поля между двумя таблицами. Надо сказать, что во первых этот метод приводит к серьезному возрастанию нагрузки не систему во время обновления, во вторых - очень сильно не вяжется с идеологией логистического модуля DAX. 
&lt;P&gt;Цель данной заметки состоит как раз в том, чтобы дать приблизительное описание той инфраструктуры которая обеспечивает интерфейс между исходными логистическими документами (заказами, закупками, складскими журналами, производственными заказами и т.п.) и стандартными механизмами логистических операций DAX, а затем дать простой пример того как можно обеспечить копирование аттрибутов из исходных документов в складские проводки. 
&lt;H1&gt;Немного о программной инфраструктуре логистики&lt;/H1&gt;
&lt;P&gt;На мой взгляд - инфраструктура логистического модуля, это очень наглядный пример мощи объектно ориентированного подхода. При проектировании классов этого модуля разработчиками было принято очень простое решение: 
&lt;P&gt;1. Классы отвечающие за выполнение типовых логистических операций - резервирования, комплектации, регистрации, физической и финансовой разноски ничего не знают о том, на основании какого исходного документа выполняются эти операции. 
&lt;P&gt;2. Вся логика, связанная со специфическими операциями по конкретному складскому документу инкапуслирована в иерархию классов (inventMov_*).&amp;nbsp; При этом каждому виду складского документа соответствует ОДИН конкретный класс в этой иерархии. 
&lt;P&gt;3. При выполнении логистических операций классы InventUpd* либо запрашивают у классов InventMov* нужную им информацию (например код номенклатуры или заказанную дату), либо вызывают методы классов inventMov, чтобы те выполнили некторые операции, специфичные для данного складского документа. 
&lt;P&gt;Рассмотрим несколько конкретных примеров: 
&lt;P&gt;В иерархии классов inventMovement&amp;nbsp;имеется метод mustBeAutoReserved. Этот метод вызывается классов inventUpd_expected, отвечающим за создание и обновление складских проводок в статусе "Заказано"/"В заказе" для того чтобы определить - не следует ли автоматически резервировать данную проводку списания. В классе inventMovement этот метод возвращает значение false. В классе inventMov_sales (отвечающим за модуль заказов) - возвращает значение в зависимости от режима авторезервирования в шапке заказа, а в классе InventMov_QuarantineOrder (отвечающем за карантинный заказ) этот метод всегда возвращает значение true, чтобы по карантинному заказу у нас товар автоматически резервировался на карантинном складе. 
&lt;P&gt;Метод inventMovement.updateLedgerFinancial() используется для создания проводок в ГК по приходным операциям. В большинстве случаев, эта операция выполняется методом самого базового класса, который вытаскивает из дочернего класса (относящегося к данному виду приходного документа) данные о счете и коррсчете проводки (через методы accountBalanceSheet() и accountOperations()), финансовой аналитике (через метод Dimension()) и разноске в ГК (методы postingBalanceSheet() и postingOperations()). Для закупок, этот метод был переопределен в классе inventMov_purch(), поскольку по закупкам разноска по коррсчету делается совсем другим классом (vendVoucher), который также создает запись в проводках по поставщику (vendTrans). Поэтому - в методе inventMov_purch.updateLedgerFinancial() создется только вторая половина проводки - приход на инвентарный счет (10.x или 41.x). 
&lt;P&gt;Метод inventMov.addRemainFinancialUnit() обновляет недопоставленное количество в исходном складском документе. В базовом классе inventMovement этот метод определен так, чтобы он ничего не делал если для данного типа складского документа обновление недопоставленного количества не требуется и чтобы он выдавал сообщение об ошибке если таковое обновление требуется. В классах inventMov_purch,inventMov_sales, inventMov_prodLine этот метод переопределен таким образом, чтобы он обновлял соответствующие количества в строке закупки, заказа и производственной спецификации. 
&lt;P&gt;Ну и так далее... 
&lt;P&gt;При создании экземпляра класса inventMovement используется constructor controlled inheritance: Для создания экземпляра класса используется метод inventMovement::construct(common table). При этом, логика внутри метода contruct (точнее даже метода constructNoThrow, который вызывается из construct), на основании информации о типе таблицы со строкой исходного складского документа, создает нужный экземпляр конкретного наследника класса inventMovement. При этом, переданный экземпляр строки исходного складского документа храниться в переменной buffer, доступной из класса inventMovement и его наследников. Кроме того - существует еще один метод создания экземпляра класса inventMovement. В таблице inventTrans существует метод inventMovement, который находит нужную строку таблицы с исходным документом, а затем через inventMovement::construct() создает и возвращает соответствующий экземпляр объекта класса inventMovement. 
&lt;H1&gt;Конкретный пример.&lt;/H1&gt;
&lt;P&gt;Заказчик – крупная торговая организация. Им для работы требуется четко контролировать складские резервы в двух дополнительных разрезах: 
&lt;P&gt;1. Менеджер по продажам (сейл), к которому относится резерв 
&lt;P&gt;2. Срок жизни резерва. Резервы, которые кто-то создал и потом в течении N-дней не продал – должны автоматически удаляться системой. 
&lt;P&gt;Если транслировать эту задачу в более приземленные термины, то нужно: 
&lt;P&gt;1. Добавить в таблицу складских проводок (inventTrans) поле "Сейл" и копировать туда поле "Ответственный продавец" из шапки заказа (возможно – и из некого дополнительного поля, которое мы добавим в шапку складского журнала – для резервов по складским журналам). 
&lt;P&gt;2. Добавить в таблицу складских проводок поле "Дата автоматического снятия резерва". При резервировании – в это поле должна заносится текущая дата + 5 дней. Желательно сделать механизм расчета автоматической даты снятия расширяемым, поскольку велика вероятность того, что в дальнейшем метод расчета срока жизни резерва будет зависеть от типа исходного документа, номенклатуры, клиента под которого ставиться резерв и тп. 
&lt;P&gt;3. Разработать процедуру удаления просроченных резервов. Здесь я эту тему рассматривать не буду - оставлю для самостоятельного изучения :) 
&lt;P&gt;Для начала попытаемся решить задачу с ответственным продавцом. Для этого: 
&lt;P&gt;· В классе inventMovement создадим метод salesResponsible(), возвращающий значение типа emplId. В базовом классе этот метод будет возвращать пустую строку. 
&lt;P&gt;· В классе inventMov_sales (связанном со строкой заказа) переопределяем этот метод таким образом, чтобы он возвращал значение salesResponsible из шапки соответствующего заказа. 
&lt;P&gt;· Добавляем поле salesResponsible в таблицу inventTrans 
&lt;P&gt;· Изменяем метод inventMovement.initInventTransFromBuffer() таким образом, чтобы он инициализировал новое поле значением, полученным из метода inventMovement.salesResponsible(). 
&lt;P&gt;Первичное тестирование данная доработка пройдет. Если при создании заказа заполнить в шапке поле "Ответственный продавец", то при создании строк оно попадет в складские проводки. Но вот если попробовать изменить это поле у уже созданного заказа, то в складских проводках так и останется старое значение. Почему это происходит ? Давайте попробуем изменить в шапке заказа поле "Дата заказа" и протрассировать метод inventUpd_estimated.updateNow(), который где-то в своих недрах должен изменить значение поля dateExpected таблицы складских проводок. При трассировке довольно быстро натыкаешься на код метода updateFieldsChange(), который вызывает метод инициализации полей складской проводки (inventMovement.initInventTransFromBuffer()) в том случае, если метод inventMovement.mustUpdateInventTransFields() вернул true. Если заглянуть в этот метод, то можно обнаружить следующий код: 
&lt;P&gt;return (this.transDate() != _movement_orig.transDate() || 
&lt;P&gt;this.shippingDateRequested()!= _movement_orig.shippingDateRequested()|| 
&lt;P&gt;this.transSchedTime() != _movement_orig.transSchedTime() || 
&lt;P&gt;this.transItemBOMId() != _movement_orig.transItemBOMId() || 
&lt;P&gt;this.transItemRouteId() != _movement_orig.transItemRouteId() || 
&lt;P&gt;this.transIdReturn() != _movement_orig.transIdReturn() || 
&lt;P&gt;this.projCategoryId() != _movement_orig.projCategoryId() || 
&lt;P&gt;this.custVendAc() != _movement_orig.custVendAc() || 
&lt;P&gt;this.assetId() != _movement_orig.assetId() || 
&lt;P&gt;this.inventRefTransId() != _movement_orig.inventRefTransId()) || 
&lt;P&gt;this.probabilityId() != _movement_orig.probabilityId(). 
&lt;P&gt;Попросту говоря – система создает на основании старой (не измененной) копии строки исходного документа (доставаемой через buffer.Orig()) экземпляр класса InventMovement() и сравнивает значение некоторых методов, значения которых в дальнейшем попадают в складские проводки. Значит – для того чтобы добиться правильного поведения системы нам нужно: 
&lt;P&gt;1. Добавить в этот метод сравнение значений, возвращаемых методом salesResponsible() 
&lt;P&gt;2. Для того чтобы логика сравнения отработала нам придется добавить в СТРОКИ заказа копию поля salesResponsible. (Ведь метод пляшет от сравнения СТРОКИ ЗАКАЗА до и после обновления, а не от значения шапки заказа.). Нам придется переопределить метод обновления шапки заказа (salesTableType.update()) таким образом, чтобы при изменении ответственного продавца в шапке заказа, новое значение поля копировалось бы и в строки заказа. При этом – обновление строки заказа у нас будет вызывать обращение к inventUpd_estimated.updateNow(), порождая таким образом обновление информации и в складских проводках. 
&lt;P&gt;Теперь попробуем разобраться с датой автоматического снятия резерва. 
&lt;P&gt;· Для начала добавим в таблицу складских проводок новое поле dateExpired 
&lt;P&gt;· Поскольку хочется сделать механизм расчета даты автоматического снятия максимально гибким, создадим метод inventMovement.dateExpired(). Этот метод получает в качестве параметра дату создания резерва, а возвращает рассчитанную на ее основе дату снятия резерва (на первых порах – просто дату+5 дней). 
&lt;P&gt;· Подправим метод inventUpd_Reservation.updateReserveMore() (этот метод собственно и резервирует складские проводки), таким образом, чтобы в поле inventTrans,dateExpired записывалось значение, полученное из нового метода inventMovement.dateExpired(). 
&lt;P&gt;· В методе inventUpdReservation.updateReserveLess() (он снимает резервы) вставляем очистку поля dateExpired – чтобы дата снятия резерва не была заполнена для проводок в статусе "Заказанно" 
&lt;P&gt;· Наконец – для того чтобы дата снятия резерва не стояла у уже отгруженных проводок – вставляем очистку даты резервирования в методе inventMovement.initInventTransPhysical(), который вызывается для заполнения полей inventTrans при физической разноске складских проводок. (Кстати – inventMovement.initInventTransFinancial выполняет аналогичную ситуацию при финансовой разноске). Поле salesResponsible я бы не стал зачищать в этом методе, поскольку достаточно удобно, когда это поле заполнено в проводках с любым статусом – а не только в проводках резервирования. 
&lt;P&gt;Если после выполнения этих модификаций поэкспериментировать с резервированием, выясняется что в целом все работает, однако есть один тонкий момент: Как известно, DAX умеет схлопывать записи в inventTrans в рамках одного номера лота, если КЛЮЧЕВЫЕ поля этих записей совпадают. В том случае, если галка "Автоматическое добавление" в параметрах модуля управления запасами установлена – это происходит автоматически – при любом обновлении inventTrans. Если галка не установлена, аналогичного эффекта можно добиться через пункт меню "Суммирование" в форме складских проводок. Так вот – если мы нарезервировали по одному и тому же лоту в разное время, с разными датами автоматического снятия резерва, система при схлопывании проводок может схлопнуть проводки с разными датами снятия резерва, подставив в результирующую проводку первую попавшуюся из нескольких дат. Для того чтобы предотвратить подобный эффект, нужно изменить метод inventTrans.setSumAmount(), так чтобы он дополнительно проверял совпадение даты резервирования и ответственного продавца в двух проводках. 
&lt;P&gt;NB. Кстати – если у вас на проекте есть проблемы с ростом inventTrans, посмотрите – включена ли галка "Автоматическое добавление". Если не включена – попробуйте оценить выигрыш от ее включения. Напишите джобик, который пробежится по существующим складским проводкам и проверит их на совпадение полей, проверяемых в методе inventTrans.setSumAmount. На моей практике – установка этой галки однажды позволила уменьшить число записей в inventTrans по заказам почти в два раза. Хотя с другой стороны – включение этой галки заведомо замедляет обновление inventTrans. 
&lt;P mce_keep="true"&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;Подводя общий итог рассмотренному примеру, можно дать следующую рекомендацию: 
&lt;OL&gt;
&lt;LI&gt;&amp;nbsp;Если аттрибут копируется в складскую проводку из исходного документа, то необходимо дополнить метод inventMovement.initInventTrans* инициализацией нового поля из вновь созданного метода класса InventMovement.&lt;/LI&gt;
&lt;LI&gt;Если аттрибут тем или иным образом рассчитывается в процессе выполнения логистической операции, следует поместить инициализацию этого аттрибута в методы соответствующего класса inventUpd_*. &lt;/LI&gt;&lt;/OL&gt;
&lt;P&gt;P.S. Рассмотренный пример (по крайней мере&amp;nbsp;- с датой автоматического снятия резеров) хорошо работает только при отсутствии резервирования в заказанных. По логике вещей - автоматическое снятие резерва должно работать только для товара, уже находящегося на складе. Для товара в пути эта логика в принципе не работает, поскольку время обработки закупки поставщиком и транспортировки закупленного товара до его прибытия на склад достаточно непредсказуемо и, обычно, значительно больше типичного времени жизни резерва на складе. Поэтому, в случае использования резервирования в заказанных, надо во первых заблокировать удаление просроченных резервов в заказанных, а во вторых - подправить метод inventUpdate.updateDimReserveChange(), таким образом, чтобы при приходе товара, у складской проводки резервирования повторно инициализировалась дата автоматического снятия резерва. (Попросту говоря - этот метод при вызывается при физическом приходе товара, ищет по двум разным алгоритмам подходящую проводку в статусе "Зарезервировано в заказанных" и переводит ее в статус "Физически зарезервировано").&lt;/P&gt;
&lt;P&gt;&lt;EM&gt;Update: Когда готовил контрольный пример для статьи, забыл об одном интересном нюансе. Для того чтобы снятие резервов работало интуитивно понятно для пользователя, первыми внутри данного лота должны сниматься резервы с наиболее ранней датой автоматического снятия. Для того чтобы система&lt;/EM&gt; &lt;EM&gt;действовала именно таким образом, необходимо в методе inventUpd_reservation.updateReserveLess поставить сортировку по дате автоматического снятия в те несколько запросов, которые отбирают строки inventTrans для перевода в статус "Заказанно".&lt;/EM&gt;&lt;/P&gt;&lt;img src="http://blogs.technet.com/aggbug.aspx?PostID=1029577" width="1" height="1"&gt;</description><category domain="http://blogs.technet.com/denisfed/archive/tags/_1B043E04330438044104420438043A043004_/default.aspx">Логистика</category><category domain="http://blogs.technet.com/denisfed/archive/tags/Dynamics+AX/default.aspx">Dynamics AX</category><category domain="http://blogs.technet.com/denisfed/archive/tags/_2004300437044004300431043E0442043A043004_/default.aspx">Разработка</category></item><item><title>Перемещаем товар программно</title><link>http://blogs.technet.com/denisfed/archive/2007/03/27/710328.aspx</link><pubDate>Tue, 27 Mar 2007 13:12:27 GMT</pubDate><guid isPermaLink="false">d5e57398-b9ef-4490-9955-07cbb4e4a80d:710328</guid><dc:creator>denisfed</dc:creator><slash:comments>1</slash:comments><comments>http://blogs.technet.com/denisfed/comments/710328.aspx</comments><wfw:commentRss>http://blogs.technet.com/denisfed/commentrss.aspx?PostID=710328</wfw:commentRss><description>&lt;p&gt;На проектах время от времени возникает ситуация, при которой заказчик хочет приделать какой-то свой собственный интерфейс к складским перемещениям. Классический пример - для автоматизации транспортной компании нужно сделать какой-то модуль, который бы отслеживал перемещение транспорта и накапливал бы затраты на перемещение, а также&amp;nbsp; попутно переносил бы в логистическом модуле товар между складами, каждый из которых соответствует путевым точкам или маршрутам от точки до точки. Обычно, в таком случае, в таком случае разработчики приписывают к данному модулю механизм, который создает и разносит журналы переноса. Этот механизм получается достаточно тяжеловесным и неудобным. Кроме того - есть шансы что при неаккуратной настройке журналов переноса этот механизм интеграции с логистикой сломается и будет выдавать непонятные пользователю сообщения (скажем - об отсутствии имени складского журнала или номерной серии для нумерации журналов).&lt;/p&gt; &lt;p&gt;Существует гораздо более изящный и удобный механизм, позволяющий перемещать товар между складскими аналитиками. Этот механизм&amp;nbsp;- использование функции InventUpd_Financial::updateVirtuelTransfer(). Вот список ее параметров:&lt;/p&gt; &lt;p&gt;&amp;nbsp;&lt;/p&gt; &lt;p&gt;static void updateVirtuelTransfer(&lt;br&gt;ItemId _itemId,&lt;br&gt;InventDimId _fromInventDimId,&lt;br&gt;InventDimId _toInventDimId,&lt;br&gt;TransDate _transDate,&lt;br&gt;InventQty _qty, // Positive&lt;br&gt;NoYes _mustBeQuarantineControlled&lt;br&gt;)  &lt;p&gt;Думаю - по названию параметров их смысл и так понятен - номенклатура, складская аналитика-источник, складская-аналитика приемник, дата, количество (положительное) и признак использования карантинного склада (я бы его всегда выключал). Для того чтобы это все работало - достаточно настроить&amp;nbsp;в параметрах модуля управления запасами номерную серию "Операция журнала". (В третьей версии этот параметр, кажется, назывался "Документ ГК по журналу").&lt;/p&gt;&lt;img src="http://blogs.technet.com/aggbug.aspx?PostID=710328" width="1" height="1"&gt;</description><category domain="http://blogs.technet.com/denisfed/archive/tags/_1B043E04330438044104420438043A043004_/default.aspx">Логистика</category><category domain="http://blogs.technet.com/denisfed/archive/tags/Dynamics+AX/default.aspx">Dynamics AX</category><category domain="http://blogs.technet.com/denisfed/archive/tags/_2004300437044004300431043E0442043A043004_/default.aspx">Разработка</category></item></channel></rss>