1
doc.nulltype.php in web/include – MultiMag

source: web/include/doc.nulltype.php @ 1c9737e

Last change on this file since 1c9737e was 1c9737e, checked in by BlackLight <blacklight@…>, 2 years ago
  • Добавлена поддержка импорта платёжных поручений из формата банк-клиента 1.03
  • Экспорт CSV вынесен во внешние печатные формы
  • Заявка на комплектующие вынесена во внешние печатные формы
  • Рефакторинг: вынес получение списка модулей в ядро
  • Property mode set to 100644
File size: 116.9 KB
Line 
1<?php
2//      MultiMag v0.2 - Complex sales system
3//
4//      Copyright (C) 2005-2018, BlackLight, TND Team, http://tndproject.org
5//
6//      This program is free software: you can redistribute it and/or modify
7//      it under the terms of the GNU Affero General Public License as
8//      published by the Free Software Foundation, either version 3 of the
9//      License, or (at your option) any later version.
10//
11//      This program is distributed in the hope that it will be useful,
12//      but WITHOUT ANY WARRANTY; without even the implied warranty of
13//      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14//      GNU Affero General Public License for more details.
15//
16//      You should have received a copy of the GNU Affero General Public License
17//      along with this program.  If not, see <http://www.gnu.org/licenses/>.
18//
19
20include_once($CONFIG['site']['location'] . "/include/doc.core.php");
21
22/// Базовый класс для всех документов системы. Содержит основные методы для работы с документами.
23class doc_Nulltype extends \document {
24
25    protected $doc_type;   ///< ID типа документа       
26    protected $sklad_editor_enable;  ///< Разрешить отображение редактора склада
27    // Значение следующих полей: +1 - увеличивает, -1 - уменьшает, 0 - не влияет
28    // Документы перемещений должны иметь 0 в соответствующих полях !
29    protected $bank_modify;   ///< Изменяет ли общие средства в банке
30    protected $kassa_modify;  ///< Изменяет ли общие средства в кассе
31    protected $header_fields;  ///< Поля заголовка документа, доступные через форму редактирования
32    protected $doc_data;   ///< Основные данные документа
33    protected $dop_data;   ///< Дополнительные данные документа
34    protected $firm_vars;   ///< Информация с данными о фирме
35    protected $child_docs = array();        ///< Информация о документах-потомках
36    protected $allow_neg_cnt;   ///< Разрешить отрицательное количество товара
37
38    public function __construct($doc = 0) {
39        $this->id = (int) $doc;
40        $this->doc_type = 0;
41        $this->typename = '';
42        $this->viewname = 'Неопределенный документ';
43        $this->sklad_editor_enable = false;
44        $this->bank_modify = 0;
45        $this->kassa_modify = 0;
46        $this->header_fields = '';
47        $this->get_docdata();
48    }
49
50    public function isSkladEditorEnable() {
51        return $this->sklad_editor_enable;
52    }
53   
54    public function getFirmVarsA() {
55        return $this->firm_vars;
56    }
57
58    /// Шаблон метода для инициализации дополнительных данных документа
59    protected function initDefDopData() {
60       
61    }
62   
63    public function getExtControls() {
64        return null;
65    }
66
67    /// Зафиксировать цену документа, если она установлена в *авто*. Выполняется при проведении некоторых типов документов.
68    protected function fixPrice() {
69        if (!$this->dop_data['cena']) {
70            $pc = PriceCalc::getInstance();
71            $pc->setFirmId($this->doc_data['firm_id']);
72            $pc->setOrderSum($this->doc_data['sum']);
73            $pc->setAgentId($this->doc_data['agent']);
74            $pc->setUserId($this->doc_data['user']);
75            if (isset($this->dop_data['ishop'])) {
76                $pc->setFromSiteFlag($this->dop_data['ishop']);
77            }
78            $price_id = $pc->getCurrentPriceID();
79            $this->setDopData('cena', $price_id);
80        }
81    }
82
83    /// Создать документ с заданными данными
84    public function create($doc_data, $from = 0) {
85        global $db;
86        \acl::accessGuard('doc.' . $this->typename, \acl::CREATE);
87        \acl::accessGuard([ 'firm.global', 'firm.' . $doc_data['firm_id']], \acl::CREATE);
88        $date = time();
89        $doc_data['altnum'] = $this->getNextAltNum($this->doc_type, $doc_data['subtype'], date("Y-m-d", $doc_data['date']), $doc_data['firm_id']);
90        $doc_data['created'] = date("Y-m-d H:i:s");
91        $res = $db->query("SHOW COLUMNS FROM `doc_list`");
92        $col_array = array();
93        while ($nxt = $res->fetch_row()) {
94            $col_array[$nxt[0]] = $nxt[0];
95        }
96        // Эти поля копировать не нужно
97        unset($col_array['id'], $col_array['date'], $col_array['type'], $col_array['user'], $col_array['ok']);
98
99        $data = array_intersect_key($doc_data, $col_array);
100        $data['date'] = $date;
101        $data['type'] = $this->doc_type;
102        $data['user'] = $_SESSION['uid'];
103
104        $this->id = $db->insertA('doc_list', $data);
105        if ($from) {
106            $data['from'] = $from;
107        }
108        $this->writeLogArray("CREATE", $data);
109        unset($this->doc_data);
110        unset($this->dop_data);
111        $this->get_docdata();
112        return $this->id;
113    }
114
115    /** Получить ID корневого документа в дереве иерархии
116     *
117     * @param bool $no_exception    Не бросать исключение при ошибке иерархии
118     * @return int                  ID корневого документа
119     * @throws Exception            Бросает исключение при обнаружении петли с участием текущего документа
120     */
121    public function getRootDocumentId($no_exception = false) {
122        global $db;
123        if ($this->doc_data['p_doc'] == 0) {
124            return $this->id;
125        }
126
127        $docmem = array();
128        $doc = $this->doc_data['p_doc'];
129
130        while ($doc) {
131            if (in_array($doc, $docmem)) {
132                if ($no_exception) {
133                    return -1;
134                } else {
135                    throw new Exception('Обнаружена петля в иерархии документов!');
136                }
137            }
138            $res = $db->query("SELECT `p_doc` FROM `doc_list` WHERE `id`='$doc' AND `p_doc`>'0' AND `p_doc` IS NOT NULL");
139            if (!$res->num_rows) {
140                return $doc;
141            }
142            list($pdoc) = $res->fetch_row();
143            if (!$pdoc) {
144                return $doc;
145            }
146            $docmem[] = $doc;
147            $doc = $pdoc;
148        }
149        return $doc;
150    }
151
152    public function getSubtreeDocuments($doc) {
153        global $db;
154        settype($doc, 'int');
155        $ret = array();
156        $sql = "SELECT `doc_list`.`id`, `doc_list`.`ok`, `doc_list`.`date`, `doc_list`.`altnum`, `doc_list`.`subtype`, `doc_list`.`sum`, `doc_types`.`name`,
157            `doc_list`.`type`, `doc_list`.`firm_id`
158            , `doc_agent`.`name` AS `agent_name`
159        FROM `doc_list`
160        LEFT JOIN `doc_agent` ON `doc_list`.`agent`=`doc_agent`.`id`
161        LEFT JOIN `doc_types` ON `doc_types`.`id`=`doc_list`.`type`
162        WHERE `doc_list`.`p_doc`='$doc'
163        ORDER by `doc_list`.`date` DESC";
164        $res = $db->query($sql);
165        $i = 1;
166        while ($line = $res->fetch_assoc()) {
167            $line['date'] = date("Y.m.d H:i:s", $line['date']);
168            $line['childs'] = $this->getSubtreeDocuments($line['id']);
169            $ret[] = $line;
170        }
171        return $ret;
172    }
173
174    protected function getDocumentSubtreeElementHTML($item, $last = true) {
175        $ret = '';
176
177        $ok_status = $item['ok'] ? 'Проведённый' : 'Непроведённый';
178        $r = ($last) ? " IsLast" : '';
179        $ret .= "<li class='Node ExpandLeaf $r'><div class='Expand'></div><div class='Content'>";
180        if (!\acl::testAccess([ 'firm.global', 'firm.' . $item['firm_id']], \acl::VIEW) && $item['firm_id'] > 0) {
181            if ($item['id'] == $this->id) {
182                $ret .= "<b>";
183            }
184            $ret .= "Неизвестный документ N {$item['altnum']}{$item['subtype']} от {$item['date']}."
185                . " Агент: {$item['agent_name']}";
186            if ($item['id'] == $this->id) {
187                $ret .= "</b>";
188            }
189        } else {
190            if ($item['id'] == $this->id) {
191                $ret .= "<b>";
192            }
193            $ret .= "<a href='doc.php?mode=body&doc={$item['id']}'>$ok_status {$item['name']}</a> N {$item['altnum']}{$item['subtype']} от {$item['date']}."
194                . " Агент: {$item['agent_name']}, на сумму {$item['sum']}";
195            if ($item['id'] == $this->id) {
196                $ret .= "</b>";
197            }
198        }
199        $ret .= "</li>";
200        return $ret;
201    }
202
203    protected function getDocumentSubtreeHTML($tree) {
204        $ret = '';
205        $cnt = count($tree);
206        foreach ($tree as $i => $item) {
207            $ret .= $this->getDocumentSubtreeElementHTML($item, $i >= $cnt);
208            $ret .= "<ul class='Container'>";
209            $ret .= $this->getDocumentSubtreeHTML($item['childs']);
210            $ret .= "</ul></div></li>";
211        }
212        return $ret;
213    }
214
215    public function viewDocumentTree() {
216        global $tmpl;
217        \acl::accessGuard('doc.' . $this->typename, \acl::VIEW);
218        if ($this->doc_data['firm_id'] > 0) {
219            \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::VIEW);
220        }
221        $root_doc_id = $this->getRootDocumentId();
222        $tmpl->addContent("<h1>Структура для {$this->id} с $root_doc_id </h1>");
223        $root_doc = \document::getInstanceFromDb($root_doc_id);
224        $item = $root_doc->getDocDataA();
225        $item['name'] = $root_doc->getViewName();
226        $tree = $this->getSubtreeDocuments($root_doc_id);
227        $tmpl->addContent("<ul class='Container'>");
228        $tmpl->addContent($this->getDocumentSubtreeElementHTML($item));
229        $tmpl->addContent("<ul class='Container'>");
230        $tmpl->addContent($this->getDocumentSubtreeHTML($tree));
231        $tmpl->addContent("</ul>");
232        $tmpl->addContent("</ul>");
233    }
234
235    /// Создать документ на основе данных другого документа
236    public function createFrom($doc_obj) {
237        $doc_data = $doc_obj->doc_data;
238        $doc_data['p_doc'] = $doc_obj->id;
239        $this->create($doc_data, $doc_obj->id);
240
241        return $this->id;
242    }
243
244    /// Создать документ с товарными остатками на основе другого документа
245    public function createFromP($doc_obj) {
246        global $db;
247        $doc_data = $doc_obj->doc_data;
248        $doc_data['p_doc'] = $doc_obj->id;
249        $this->create($doc_data, $doc_obj->id);
250        if ($this->sklad_editor_enable) {
251            $res = $db->query("SELECT `tovar`, `cnt`, `cost`, `page`, `comm` FROM `doc_list_pos` WHERE `doc`='{$doc_obj->id}' ORDER BY `doc_list_pos`.`id`");
252            while ($line = $res->fetch_assoc()) {
253                $line['doc'] = $this->id;
254                unset($line['id']);
255                $db->insertA('doc_list_pos', $line);
256            }
257        }
258        return $this->id;
259    }
260
261    /// Создать несвязанный документ с товарными остатками из другого документа
262    public function createParent($doc_obj) {
263        global $db;
264        $doc_data = $doc_obj->doc_data;
265        $doc_data['p_doc'] = 0;
266        $this->create($doc_data);
267        if ($this->sklad_editor_enable) {
268            $res = $db->query("SELECT `tovar`, `cnt`, `cost`, `page`, `comm` FROM `doc_list_pos` WHERE `doc`='{$doc_obj->id}' ORDER BY `doc_list_pos`.`id`");
269            while ($line = $res->fetch_assoc()) {
270                $line['doc'] = $this->id;
271                unset($line['id']);
272                $db->insertA('doc_list_pos', $line);
273            }
274        }
275        unset($this->doc_data);
276        $this->get_docdata();
277        return $this->id;
278    }
279
280    /// Создать документ с товарными остатками на основе другого документа
281    /// В новый документ войдут только те наименования, которых нет в других подчинённых документах
282    public function createFromPDiff($doc_obj) {
283        global $db;
284        $doc_data = $doc_obj->doc_data;
285        $doc_data['p_doc'] = $doc_obj->id;
286        if ($this->sklad_editor_enable) {
287            $res = $db->query("SELECT `id` FROM `doc_list` WHERE `p_doc`='{$doc_obj->id}' AND `type`='{$this->doc_type}'");
288            $child_count = $res->num_rows;
289        }
290        $this->create($doc_data);
291        if ($this->sklad_editor_enable) {
292            if ($child_count < 1) {
293                $res = $db->query("SELECT `tovar`, `cnt`, `cost`, `page`, `comm` FROM `doc_list_pos` WHERE `doc`='{$doc_obj->id}' ORDER BY `doc_list_pos`.`id`");
294                while ($line = $res->fetch_assoc()) {
295                    $line['doc'] = $this->id;
296                    unset($line['id']);
297                    $db->insertA('doc_list_pos', $line);
298                }
299            } else {
300                $res = $db->query("SELECT `a`.`tovar`, `a`.`cnt`, `a`.`comm`, `a`.`cost`,
301                                ( SELECT SUM(`b`.`cnt`) FROM `doc_list_pos` AS `b`
302                                INNER JOIN `doc_list` ON `b`.`doc`=`doc_list`.`id` AND `doc_list`.`p_doc`='{$doc_obj->id}' AND `doc_list`.`mark_del`='0'
303                                WHERE `b`.`tovar`=`a`.`tovar` ) AS `doc_cnt`, `a`.`page`
304                                FROM `doc_list_pos` AS `a`
305                                WHERE `a`.`doc`='{$doc_obj->id}'
306                                ORDER BY `a`.`id`");
307                while ($line = $res->fetch_assoc()) {
308                    if ($line['doc_cnt'] < $line['cnt']) {
309                        $line['cnt']-=$line['doc_cnt'];
310                        unset($line['doc_cnt']);
311                        $line['doc'] = $this->id;
312                        unset($line['id']);
313                        $db->insertA('doc_list_pos', $line);
314                    }
315                }
316            }
317            $this->recalcSum();
318        }
319        return $this->id;
320    }
321
322    /// Пересчитать и вернуть сумму документа, исходя из товаров в нём. Работает только для документов, в которых могут быть товары.
323    /// Для безтоварных документов просто вернёт сумму.
324    /// TODO: функция устарела. Перейти на использование DocPosEditor::updateDocSum()
325    public function recalcSum() {
326        global $db;
327        if (!$this->id)
328            return 0;
329        if (!$this->sklad_editor_enable)
330            return $this->doc_data['sum'];
331        $old_sum = $this->doc_data['sum'];
332        $sum = 0;
333        $res = $db->query("SELECT `cnt`, `cost` FROM `doc_list_pos` WHERE `doc`='{$this->id}' AND `page`='0'");
334        while ($nxt = $res->fetch_row())
335            $sum+=$nxt[0] * $nxt[1];
336        $res->free();
337        if (round($sum, 2) != round($old_sum, 2))
338            $this->setDocData('sum', $sum);
339        return $sum;
340    }
341
342    /// Получить объект документа заявки для текущей цепочки документов
343    /// @return Объект doc_Zayavka, или false если не найден. Может быть текущим документом.
344    public function getZDoc() {
345        global $db;
346        if ($this->doc_type == 3) {
347            return $this;
348        }
349        $pdoc = $this->doc_data['p_doc'];
350        while ($pdoc) {
351            $res = $db->query("SELECT `id`, `type`, `p_doc` FROM `doc_list` WHERE `id`='$pdoc'");
352            if (!$res->num_rows) {
353                throw new Exception("Документ не найден");
354            }
355            list($doc_id, $pdoc_type, $pdoc_id) = $res->fetch_row();
356            if ($pdoc_type == 3) {
357                return new doc_Zayavka($doc_id);
358            }
359            $pdoc = $pdoc_id;
360        }
361        return false;
362    }
363
364    /// Послать в связанный заказ событие с заданным типом.
365    /// Полное название события будет doc:{$docname}:{$event_type}
366    /// @param event_type Название события
367    /// TODO: зависимость от дочернего класса выглядит некорректной
368    public function sentZEvent($event_type) {
369        global $db;
370        $event_name = "doc:{$this->typename}:$event_type";
371        $zdoc = $this->getZDoc();
372        if ($zdoc) {
373            return $zdoc->dispatchZEvent($event_name, $this);
374        }
375        return false;
376    }
377
378    /// Отправить оповещение по всем доступным каналам связи с клиентом
379    function sendNotify($text) {
380        return
381            $this->sendEmailNotify($text) ||
382            $this->sendSMSNotify($text) ||
383            $this->sendXMPPNotify($text);
384    }
385
386    /// Отправить SMS с заданным текстом заказчику на первый из подходящих номеров
387    /// @param text текст отправляемого сообщения
388    function sendSMSNotify($text) {
389        global $CONFIG, $db;
390        if (!isset($CONFIG['doc']['notify_sms'])) {
391            return false;
392        }
393        if (!$CONFIG['doc']['notify_sms']) {
394            return false;
395        }
396        if (isset($this->dop_data['buyer_phone'])) {
397            if (preg_match('/^\+79\d{9}$/', $this->dop_data['buyer_phone'])) {
398                $smsphone = $this->dop_data['buyer_phone'];
399            }
400        }
401        if ($this->doc_data['agent'] > 1 && !$smsphone) {
402            $agent = new \models\agent($this->doc_data['agent']);
403            $smsphone = $agent->getSMSPhone();
404        }
405        if (preg_match('/^\+79\d{9}$/', $smsphone)) {
406            require_once('include/sendsms.php');
407            $sender = new SMSSender();
408            $sender->setNumber($smsphone);
409            $sender->setContent($text);
410            $sender->send();
411            if (@$CONFIG['doc']['notify_debug']) {
412                $this->writeLogArray("NOTIFY SMS", ['number' => $smsphone, 'text' => $text]);
413            }
414            return true;
415        }
416        return false;
417    }
418
419    /// Отправить email с заданным текстом заказчику на все доступные адреса
420    /// @param text текст отправляемого сообщения
421    function sendEmailNotify($text, $subject = null) {
422        global $CONFIG, $db;
423        $pref = \pref::getInstance();
424        if (!isset($CONFIG['doc']['notify_email'])) {
425            return false;
426        }
427        if (!$CONFIG['doc']['notify_email']) {
428            return false;
429        }
430        $emails = array();
431        if (isset($this->dop_data['buyer_email'])) {
432            if ($this->dop_data['buyer_email']) {
433                $emails[$this->dop_data['buyer_email']] = $this->dop_data['buyer_email'];
434            }
435        }
436        if ($this->doc_data['agent'] > 1) {
437            $agent = new \models\agent($this->doc_data['agent']);
438            $contacts = $agent->contacts;
439            foreach ($contacts as $line) {
440                if ($line['type'] == 'email') {
441                    $emails[$line['value']] = $line['value'];
442                }
443            }
444        }
445        if (count($emails) > 0) {
446            foreach ($emails as $email) {
447                $user_msg = "Уважаемый клиент!\n" . $text;
448                if (!$subject) {
449                    $subject = "Документ N {$this->id} на {$pref->site_name}";
450                }
451                mailto($email, $subject, $user_msg);
452                if (@$CONFIG['doc']['notify_debug']) {
453                    $this->writeLogArray("NOTIFY Email", ['email' => $email, 'text' => $user_msg]);
454                }
455            }
456            return true;
457        }
458        return false;
459    }
460
461    /// Отправить сообщение по XMPP с заданным текстом заказчику на все доступные адреса
462    /// @param text текст отправляемого сообщения
463    function sendXMPPNotify($text) {
464        global $CONFIG, $db;
465        if (!isset($CONFIG['doc']['notify_xmpp'])) {
466            return false;
467        }
468        if (!$CONFIG['doc']['notify_xmpp']) {
469            return false;
470        }
471        $addresses = array();
472        if ($this->doc_data['agent'] > 1) {
473            $agent = new \models\agent($this->doc_data['agent']);
474            $contacts = $agent->contacts;
475            foreach ($contacts as $line) {
476                if ($line['type'] == 'jid' || $line['type'] == 'xmpp') {
477                    $addresses[$line['value']] = $line['value'];
478                }
479            }
480        }
481        if (count($addresses) > 0) {
482            require_once($CONFIG['location'] . '/common/XMPPHP/XMPP.php');
483            $xmppclient = new \XMPPHP\XMPP($CONFIG['xmpp']['host'], $CONFIG['xmpp']['port'], $CONFIG['xmpp']['login'], $CONFIG['xmpp']['pass'], 'MultiMag r' . MULTIMAG_REV);
484            $xmppclient->connect();
485            $xmppclient->processUntil('session_start');
486            $xmppclient->presence();
487            foreach ($addresses as $addr) {
488                $user_msg = $text;
489                $xmppclient->message($addr, $user_msg);
490                if (@$CONFIG['doc']['notify_debug']) {
491                    $this->writeLogArray("NOTIFY xmpp", ['jid' => $addr, 'text' => $user_msg]);
492                }
493            }
494            $xmppclient->disconnect();
495            return true;
496        }
497        return false;
498    }
499
500    /// отобразить заголовок документа
501    public function head() {
502        global $tmpl;
503        if ($this->doc_type == 0)
504            throw new Exception("Невозможно создать документ без типа!");
505        else {
506            $tmpl->setTitle($this->viewname . ' N' . $this->id);
507            if ($this->typename)
508                $object = 'doc_' . $this->typename;
509            else
510                $object = 'doc';
511            \acl::accessGuard('doc.' . $this->typename, \acl::VIEW);
512            if ($this->doc_data['firm_id'] > 0) {
513                \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::VIEW);
514            }
515            doc_menu($this->getDopButtons());
516            $this->drawHeadformStart();
517            $fields = explode(' ', $this->header_fields);
518            foreach ($fields as $f) {
519                switch ($f) {
520                    case 'agent': $this->DrawAgentField();
521                        break;
522                    case 'sklad': $this->DrawSkladField();
523                        break;
524                    case 'kassa': $this->drawKassaField();
525                        break;
526                    case 'bank': $this->drawBankField();
527                        break;
528                    case 'cena': $this->drawPriceField();
529                        break;
530                    case 'sum': $this->drawSumField();
531                        break;
532                    case 'separator': $tmpl->addContent("<hr>");
533                        break;
534                }
535            }
536            if (method_exists($this, 'DopHead'))
537                $this->DopHead();
538
539            $this->DrawHeadformEnd();
540        }
541    }
542
543    protected function try_head_save() {
544        $write_doc_data = array(
545            'date' => @strtotime(request('datetime')),
546            'firm_id' => rcvint('firm'),
547            'comment' => request('comment'),
548            'altnum' => rcvint('altnum'),
549            'subtype' => request('subtype'),
550        );
551        $write_dop_data = array();
552        if (!$write_doc_data['altnum']) {
553            $write_doc_data['altnum'] = $this->getNextAltNum($this->doc_type, $write_doc_data['subtype']
554                , date("Y-m-d", $write_doc_data['date']), $write_doc_data['firm_id']);
555        }
556        if (!$this->id) {
557            $write_doc_data['user'] = intval($_SESSION['uid']);
558            $write_doc_data['type'] = $this->doc_type;
559        } elseif (@$this->doc_data['ok']) {
560            throw new \Exception("Операция не допускается для проведённого документа!");
561        } else if (@$this->doc_data['mark_del']) {
562            throw new \Exception("Операция не допускается для документа, отмеченного для удаления!");
563        }
564        $fields = explode(' ', $this->header_fields);
565        foreach ($fields as $f) {
566            switch ($f) {
567                case 'cena':
568                case 'price':
569                    $write_dop_data['cena'] = rcvint('cena');
570                    $write_doc_data['nds'] = rcvint('nds');
571                    break;
572                case 'agent':
573                    $write_doc_data['agent'] = rcvint('agent');
574                    if (!$write_doc_data['agent']) {
575                        $pref = \pref::getInstance();
576                        $write_doc_data['agent'] = $pref->getSitePref('default_agent_id');
577                    }
578                    $write_doc_data['contract'] = rcvint('contract');
579                    break;
580                case 'separator':
581                    break;
582                case 'sum':
583                    $write_doc_data['sum'] = rcvrounded('sum');
584                    break;
585                default:
586                    $write_doc_data[$f] = rcvint($f);
587                    break;
588            }
589        }
590        if ($this->id) {
591            \acl::accessGuard('doc.' . $this->typename, \acl::UPDATE);
592            \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::UPDATE);
593        } else {
594            \acl::accessGuard('doc.' . $this->typename, \acl::CREATE);
595            if ($this->doc_data['firm_id'] > 0) {
596                \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::CREATE);
597            }
598        }
599        $this->setDocDataA($write_doc_data);
600        if (count($write_dop_data)) {
601            $this->setDopDataA($write_dop_data);
602        }
603        if (method_exists($this, 'DopSave')) {
604            $this->DopSave();
605        }
606    }
607
608    /// Применить изменения редактирования заголовка
609    public function head_submit() {
610        $this->try_head_save();
611        redirect("/doc.php?mode=body&doc={$this->id}");
612        return $this->id;
613    }
614
615    /// Сохранение заголовка документа и возврат результата в json формате
616    public function json_head_submit() {
617        global $tmpl;
618        $tmpl->ajax = 1;
619        try {
620            $this->try_head_save();
621            if ($this->doc_data['agent']) {
622                $b = agentCalcDebt($this->doc_data['agent']);
623            } else {
624                $b = 0;
625            }
626            $json_content = json_encode(['response' => 'ok', 'agent_balance' => $b], JSON_UNESCAPED_UNICODE);
627            $tmpl->setContent($json_content);
628        } catch (mysqli_sql_exception $e) {
629            $id = writeLogException($e);
630            $ret_data = array('response' => 'err',
631                'text' => "Ошибка в базе данных! Порядковый номер ошибки: $id. Сообщение об ошибке занесено в журнал.");
632            $tmpl->setContent(json_encode($ret_data, JSON_UNESCAPED_UNICODE));
633        } catch (Exception $e) {
634            $json_content = json_encode(['response' => 'err', 'text' => $e->getMessage()], JSON_UNESCAPED_UNICODE);
635            $tmpl->setContent($json_content);
636        }
637    }
638
639    /// Редактирование тела докумнета
640    public function body() {
641        global $tmpl, $db;
642        \acl::accessGuard('doc.' . $this->typename, \acl::VIEW);
643        if ($this->doc_data['firm_id'] > 0) {
644            \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::VIEW);
645        }
646        $this->extendedViewAclCheck();
647        $tmpl->setTitle($this->viewname . ' N' . $this->id);
648        $dt = date("Y-m-d H:i:s", $this->doc_data['date']);
649        doc_menu($this->getDopButtons());
650        $tmpl->addContent("<div id='doc_container'>
651                <div id='doc_left_block' class='doc_head'>");
652        $tmpl->addContent("<h1>{$this->viewname} N{$this->id}</h1>");
653
654        $this->drawLHeadformStart();
655        $fields = explode(' ', $this->header_fields);
656        foreach ($fields as $f) {
657            switch ($f) {
658                case 'agent': $this->DrawAgentField();
659                    break;
660                case 'sklad': $this->DrawSkladField();
661                    break;
662                case 'kassa': $this->drawKassaField();
663                    break;
664                case 'bank': $this->drawBankField();
665                    break;
666                case 'cena': $this->drawPriceField();
667                    break;
668                case 'sum': $this->drawSumField();
669                    break;
670                case 'separator': $tmpl->addContent("<hr>");
671                    break;
672            }
673        }
674        if (method_exists($this, 'DopHead'))
675            $this->DopHead();
676
677        $this->DrawLHeadformEnd();
678       
679       
680        if($info = $this->getParentInfo()) {
681            $str = "<b>Относится к:</b><br>";
682            if($info['ok']) {
683                $str.='Проведённый';
684            }
685            else {
686                $str.='Непроведённый';
687            }
688            $str .= " <a href='?mode=body&amp;doc={$info['id']}'>{$info['viewname']} N{$info['altnum']}{$info['subtype']}</a> от {$info['date']}";
689            $tmpl->addContent($str);
690        }
691
692        $infol = $this->getSubordinatesInfo();
693        if($infol && count($infol)>0) {
694            $tmpl->addContent("<br><b>Зависящие документы:</b><br>");
695            foreach($infol as $info) {
696                if($info['ok']) {
697                    $str='Проведённый';
698                }
699                else {
700                    $str='Непроведённый';
701                }
702                $str .= " <a href='?mode=body&amp;doc={$info['id']}'>{$info['viewname']} N{$info['altnum']}{$info['subtype']}</a> от {$info['date']}<br>";
703                $tmpl->addContent($str); 
704            }
705        }
706           
707        $tmpl->addContent("<br><b>Дата создания:</b>: {$this->doc_data['created']}<br>");
708        if ($this->doc_data['ok']) {
709            $tmpl->addContent("<b>Дата проведения:</b> " . date("Y-m-d H:i:s", $this->doc_data['ok']) . "<br>");
710        }
711        $tmpl->addContent("</div>
712                <script type=\"text/javascript\">
713                addEventListener('load',DocHeadInit,false);
714                //newDynamicDocHeader('doc_left_block', '{$this->id}');
715                </script>");
716        $tmpl->addContent("<div id='doc_main_block'>");
717        $tmpl->addContent("<img src='/img/i_leftarrow.png' onclick='DocLeftToggle()' id='doc_left_arrow'><br>");
718
719        if (method_exists($this, 'DopBody'))
720            $this->DopBody();
721
722        if ($this->sklad_editor_enable) {
723            include_once('doc.poseditor.php');
724            $poseditor = new DocPosEditor($this);
725            $poseditor->cost_id = $this->dop_data['cena'];
726            $poseditor->sklad_id = $this->doc_data['sklad'];
727            $poseditor->SetEditable($this->doc_data['ok'] ? 0 : 1);
728            $tmpl->addContent($poseditor->Show());
729        }
730
731        $tmpl->addContent("<div id='statusblock'></div><br><br></div></div>");
732    }
733
734   
735   
736    /// Выполнение дополнительных проверок доступа для просмотра документа
737    public function extendedViewAclCheck() {
738        return true;
739    }
740
741    /// Выполнение дополнительных проверок доступа для проведения документа
742    public function extendedApplyAclCheck() {
743        return true;
744    }
745
746    /// Выполнение дополнительных проверок доступа для отмены документа
747    public function extendedCancelAclCheck() {
748        return true;
749    }
750   
751    /// Провести документ
752    public function apply($silent = false) {
753        global $db;
754        if ($this->doc_data['mark_del']) {
755            throw new \Exception("Документ помечен на удаление!");
756        }
757        $this->docApply($silent);
758        if(!$silent) {
759            doc_log("APPLY", '', 'doc', $this->id);
760        }
761        $db->query("UPDATE `doc_list` SET `err_flag`='0' WHERE `id`='{$this->id}'");
762    }
763
764    /// Провести документ и вернуть JSON результат
765    public function applyJson() {
766        global $db;
767        try {
768            $d_start = date_day(time());
769            $d_end = $d_start + 60 * 60 * 24 - 1;
770            if (!\acl::testAccess('doc.' . $this->typename, \acl::APPLY)) {
771                if (!\acl::testAccess('doc.' . $this->typename, \acl::TODAY_APPLY)) {
772                    throw new AccessException('Не достаточно привилегий для проведения документа');
773                } elseif ($this->doc_data['date'] < $d_start || $this->doc_data['date'] > $d_end) {
774                    throw new AccessException('Не достаточно привилегий для проведения документа произвольной датой');
775                }
776            }
777            $this->extendedApplyAclCheck();
778            if ($this->doc_data['mark_del']) {
779                throw new Exception("Документ помечен на удаление!");
780            }
781
782            $res = $db->query("SELECT `recalc_active` FROM `variables`");
783            if ($res->num_rows) {
784                list($lock) = $res->fetch_row();
785            } else {
786                $lock = 0;
787            }
788            if ($lock) {
789                throw new Exception("Идёт обслуживание базы данных. Проведение невозможно!");
790            }
791
792            $db->startTransaction();
793            $this->DocApply(0);
794            $db->query("UPDATE `doc_list` SET `err_flag`='0' WHERE `id`='{$this->id}'");
795            doc_log("APPLY", '', 'doc', $this->id);
796            $db->commit();           
797        } catch (mysqli_sql_exception $e) {
798            $db->rollback();
799            writeLogException($e);
800           
801            $data = array(
802                'response' => 0,
803                'message' => $e->getMessage(),
804            );
805            $json = json_encode($data, JSON_UNESCAPED_UNICODE);
806            return $json;
807        } catch (Exception $e) {
808            $db->rollback();
809            writeLogException($e);           
810            $data = array(
811                'response' => 0,
812                'message' => $e->getMessage(),
813            );
814            $json = json_encode($data, JSON_UNESCAPED_UNICODE);
815            return $json;
816        }
817       
818        $data = array(
819            'response' => 1,
820            'message' => "Документ успешно проведён!",
821            'buttons' => $this->getCancelButtons(),
822            'sklad_view' => 'hide',
823            'statusblock' => 'Дата проведения: ' . date("Y-m-d H:i:s"),
824            'poslist' => 'refresh',
825        );
826        $json = json_encode($data, JSON_UNESCAPED_UNICODE);
827        return $json;
828    }
829
830    /// Отменить проведение документа
831    public function cancel() {
832        $this->docCancel();
833        doc_log("CANCEL", '', 'doc', $this->id);
834    }
835   
836    public function cancelJson() {
837        global $db;
838        $tim = time();
839        $dd = date_day($tim);
840        if ($this->typename) {
841            $object = 'doc_' . $this->typename;
842        } else {
843            $object = 'doc';
844        }
845
846        try {
847            if (!\acl::testAccess('doc.' . $this->typename, \acl::CANCEL)) {
848                if ((!\acl::testAccess('doc.' . $this->typename, \acl::TODAY_CANCEL)) || ($dd > $this->doc_data['date'])) {
849                    throw new \AccessException();
850                }
851            }
852            $this->extendedCancelAclCheck();
853
854            $res = $db->query("SELECT `recalc_active` FROM `variables`");
855            if ($res->num_rows) {
856                list($lock) = $res->fetch_row();
857            } else {
858                $lock = 0;
859            }
860            if ($lock) {
861                throw new \Exception("Идёт обслуживание базы данных. Проведение невозможно!");
862            }
863
864            $db->startTransaction();
865            $this->get_docdata();
866            $this->DocCancel();
867            $db->query("UPDATE `doc_list` SET `err_flag`='0' WHERE `id`='{$this->id}'");
868        } catch (mysqli_sql_exception $e) {
869            $db->rollback();
870            writeLogException($e);
871            $json = " { \"response\": \"0\", \"message\": \"" . $e->getMessage() . "\" }";
872            return $json;
873        } catch (AccessException $e) {
874            $db->rollback();
875            doc_log("CANCEL-DENIED", $e->getMessage(), 'doc', $this->id);
876            $json = " { \"response\": \"0\", \"message\": \"Недостаточно привилегий для выполнения операции!<br>" . $e->getMessage() . "<br>Вы можете <a href='#' onclick=\"return petitionMenu(event, '{$this->id}')\">попросить руководителя</a> выполнить отмену этого документа.\" }";
877            return $json;
878        } catch (Exception $e) {
879            $db->rollback();
880            $msg = '';
881            if (\acl::testAccess('doc.' . $this->typename, \acl::CANCEL_FORCE)) {
882                $msg = "<br>Вы можете <a href='/doc.php?mode=forcecancel&amp;doc={$this->id}'>принудительно снять проведение</a>.";
883            }
884            $json = " { \"response\": \"0\", \"message\": \"" . $e->getMessage() . $msg . "\" }";
885            return $json;
886        }
887
888        $db->commit();
889        doc_log("CANCEL", '', 'doc', $this->id);
890        $json = ' { "response": "1", "message": "Документ успешно отменен!", "buttons": "' . $this->getApplyButtons() . '", "sklad_view": "show", "statusblock": "Документ отменён", "poslist": "refresh" }';
891        return $json;
892    }
893
894    /// Провести документ
895    /// @param silent Не менять отметку проведения
896    public function docApply($silent = 0) {
897        global $db;
898        if ($silent) {
899            return;
900        }
901        if ($this->doc_data['ok']) {
902            throw new Exception('Документ уже проведён!');
903        }
904        $ok_time = time();
905        $db->update('doc_list', $this->id, 'ok', $ok_time);
906        $this->doc_data['ok'] = $ok_time;
907        $this->sentZEvent('apply');
908    }
909
910    /// отменить проведение документа
911    public function docCancel() {
912        global $db;
913        if (!$this->doc_data['ok']) {
914            throw new \Exception('Документ не проведён!');
915        }
916        $db->update('doc_list', $this->id, 'ok', 0);
917        $this->doc_data['ok'] = 0;
918        $this->sentZEvent('cancel');
919    }
920
921    /// Отменить проведение, не обращая внимание на структуру подчинённости
922    function forceCancel() {
923        global $tmpl, $db;
924
925        \acl::accessGuard('doc.' . $this->typename, \acl::CANCEL_FORCE);
926        if ($this->doc_data['firm_id'] > 0) {
927            \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::CANCEL_FORCE);
928        }
929        $opt = request('opt');
930        if ($opt == '') {
931            $tmpl->addContent("<h2>Внимание! Опасная операция!</h2>Отмена производится простым снятием отметки проведения, без проверки зависимостией, учета структуры подчинённости и изменения значений счётчиков. Вы приниматете на себя все последствия данного действия. Вы точно хотите это сделать?<br>
932                        <center>
933                        <a href='/docj_new.php' style='color: #0b0'>Нет</a> |
934                        <a href='/doc.php?mode=forcecancel&amp;opt=yes&amp;doc={$this->id}' style='color: #f00'>Да</a>
935                        </center>");
936        } else {
937            doc_log("FORCE CANCEL", '', 'doc', $this->id);
938            $db->query("UPDATE `doc_list` SET `ok`='0', `err_flag`='1' WHERE `id`='{$this->id}'");
939            $db->query("UPDATE `variables` SET `corrupted`='1'");
940            $tmpl->msg("Всё, сделано.", "err", "Снятие отметки проведения");
941        }
942    }
943
944    /// Callback функция для сортировки (например, печатных форм)
945    static function sortDescriptionCallback($a, $b) {
946        return strcmp($a["desc"], $b["desc"]);
947    }
948
949    /// Получить список доступных печатных форм
950    /// @return Массив со списком печатных форм
951    protected function getPrintFormList() {
952        global $CONFIG;
953        $aclFlag = \acl::GET_PRINTDRAFT;
954        if ($this->doc_data['ok']) {
955            $aclFlag = \acl::GET_PRINTFORM;
956        }
957        $ret = [];
958        $modules = \getModuleListInDir('include/doc/printforms', 'site');
959
960        foreach ($modules as $module) {
961            $class_name = '\\doc\\printforms\\' . $this->typename . '\\' . $module;
962            $class = new $class_name;
963            $nm = $class->getName();
964            $mime = $class->getMimeType();
965            if (\acl::testAccess("doc.{$this->typename}.{$module}", $aclFlag)) {
966                $ret[] = array('name' => 'ext:' . $module, 'desc' => $nm, 'mime' => $mime);
967            }
968        }
969
970        usort($ret, array(get_class(), 'sortDescriptionCallback'));
971        return $ret;
972    }
973
974    /// Проверить, существует ли печатная форма с заданным названием
975    /// @return true, если существует, false в ином случае
976    protected function isPrintFormExists($form_name) {
977        $forms = $this->getPrintFormList();
978        $found = false;
979        foreach ($forms as $form) {
980            if ($form['name'] == $form_name) {
981                $found = true;
982                break;
983            }
984        }
985        return $found;
986    }
987
988    /// Получить mime тип формы
989    /// @return тип, если форма существует, false в ином случае
990    protected function getPrintFormMime($form_name) {
991        $forms = $this->getPrintFormList();
992        $found = false;
993        foreach ($forms as $form) {
994            if ($form['name'] == $form_name) {
995                $found = $form['mime'];
996                break;
997            }
998        }
999        return $found;
1000    }
1001
1002    /// Получить отображаемое наименование формы
1003    /// @return Название формы, если существует, null в ином случае
1004    protected function getPrintFormViewName($form_name) {
1005        $forms = $this->getPrintFormList();
1006        foreach ($forms as $form) {
1007            if ($form['name'] == $form_name) {
1008                return $form['desc'];
1009            }
1010        }
1011        return null;
1012    }
1013
1014    /**
1015     * Сформировать печатную форму
1016     * @param string $form_name Имя печатной формы
1017     * @param bool $to_str      Вернуть ли данные в виде строки
1018     * @return string Если $to_str == true - возвращает сформированный документ, false в ином случае
1019     * @throws AccessException
1020     * @throws NotFoundException
1021     */
1022    protected function makePrintForm($form_name, $to_str = false) {
1023        $aclFlag = \acl::GET_PRINTDRAFT;
1024        if ($this->doc_data['ok']) {
1025            $aclFlag = \acl::GET_PRINTFORM;
1026        }
1027        list(,$form_acl) = explode(':', $form_name);
1028        \acl::accessGuard("doc.{$this->typename}.$form_acl", $aclFlag);
1029        \acl::accessGuard([ 'firm.global', "firm.{$this->doc_data['firm_id']}" ], $aclFlag);
1030        return $this->makePrintFormNoACLTest($form_name, $to_str);
1031    }
1032
1033    /**
1034     * Сформировать печатную форму, не проверяя привилегии
1035     * @param string $form_name Имя печатной формы
1036     * @param bool $to_str Вернуть ли данные в виде строки
1037     * @return string Если $to_str == true - возвращает сформированный документ, false в ином случае
1038     * @throws NotFoundException
1039     */
1040    public function makePrintFormNoACLTest($form_name, $to_str = false) {
1041        if (!$this->isPrintFormExists($form_name)) {
1042            throw new \NotFoundException('Печатная форма ' . html_out($form_name) . ' не зарегистрирована');
1043        }
1044        $f_param = explode(':', $form_name);
1045        if ($f_param[0] == 'ext') {
1046            $class_name = '\\doc\\printforms\\' . $this->typename . '\\' . $f_param[1];
1047            $print_obj = new $class_name;
1048            $print_obj->setDocument($this);
1049            $print_obj->initForm();
1050            $print_obj->make();
1051            return $print_obj->outData($to_str);
1052        } else {
1053            throw new \NotFoundException('Неверный тип печатной формы');
1054        }
1055    }
1056
1057    /// Отправка документа по факсу
1058    /// @param $form_name   Имя печатной формы
1059    final function sendFax($form_name = '') {
1060        global $tmpl, $db;
1061        $tmpl->ajax = 1;
1062        try {
1063            if ($form_name == '') {
1064                $agent = new \models\agent($this->doc_data['agent']);
1065                $ret_data = array(
1066                    'response' => 'item_list',
1067                    'faxnum' => $agent->getFaxNum(),
1068                    'content' => $this->getPrintFormList()
1069                );
1070                $tmpl->setContent(json_encode($ret_data, JSON_UNESCAPED_UNICODE));
1071            } else {
1072                $faxnum = request('faxnum');
1073                if ($faxnum == '') {
1074                    throw new \Exception('Номер факса не указан');
1075                }
1076                if (!preg_match('/^\+\d{8,15}$/', $faxnum)) {
1077                    throw new \Exception("Номер факса $faxnum указан в недопустимом формате");
1078                }
1079                include_once('sendfax.php');
1080                $data = $this->makePrintForm($form_name, true);
1081                $fs = new FaxSender();
1082                $fs->setFileBuf($data);
1083                $fs->setFaxNumber($faxnum);
1084
1085                $res = $db->query("SELECT `worker_email` FROM `users_worker_info` WHERE `user_id`='{$_SESSION['uid']}'");
1086                if ($res->num_rows) {
1087                    list($email) = $res->fetch_row();
1088                    $fs->setNotifyMail($email);
1089                }
1090                $res = $fs->send();
1091                $tmpl->setContent("{'response': 'send'}");
1092                doc_log("Send FAX", $faxnum, 'doc', $this->id);
1093            }
1094        } catch (Exception $e) {
1095            $tmpl->setContent("{response: 'err', text: '" . $e->getMessage() . "'}");
1096        }
1097    }
1098
1099    /** Отправка документа по факсу на указанный номер
1100     *
1101     * @param $form_name Имя формы отправляемого документа
1102     * @param $faxnum Номер факса получателя
1103     * @return bool
1104     * @throws NotFoundException
1105     */
1106    final function sendFaxTo($form_name, $faxnum) {
1107        global $db;
1108        if ($faxnum == '') {
1109            throw new \Exception('Номер факса не указан');
1110        }
1111        if (!preg_match('/^\+\d{8,15}$/', $faxnum)) {
1112            throw new \Exception("Номер факса $faxnum указан в недопустимом формате");
1113        }
1114        include_once('sendfax.php');
1115        $data = $this->makePrintFormNoACLTest($form_name, true);
1116        $fs = new \FaxSender();
1117        $fs->setFileBuf($data);
1118        $fs->setFaxNumber($faxnum);
1119
1120        $res = $db->query("SELECT `worker_email` FROM `users_worker_info` WHERE `user_id`='{$_SESSION['uid']}'");
1121        if ($res->num_rows) {
1122            list($email) = $res->fetch_row();
1123            $fs->setNotifyMail($email);
1124        }
1125        $res = $fs->send();       
1126        doc_log("Send FAX", $faxnum, 'doc', $this->id);
1127        return true;
1128    }
1129   
1130    function getExtensionFromMIME($mime) {
1131        switch ($mime) {
1132            case 'text/csv':
1133                return '.csv';
1134            case 'application/vnd.ms-excel':
1135                return '.xls';
1136            case 'application/vnd.oasis.opendocument.spreadsheet':
1137                return '.ods';
1138            case 'application/pdf':
1139            default:
1140                return '.pdf';
1141        }
1142    }
1143
1144    /// Отправка документа по электронной почте
1145    /// @param $form_name   Имя печатной формы
1146    final function sendEMail($form_name = '') {
1147        global $tmpl, $db;
1148        $tmpl->ajax = 1;
1149        try {
1150            if ($form_name == '') {
1151                $agent = new \models\agent($this->doc_data['agent']);
1152                $ret_data = array(
1153                    'response' => 'item_list',
1154                    'email' => $agent->getEmail(),
1155                    'content' => $this->getPrintFormList()
1156                );
1157                $tmpl->setContent(json_encode($ret_data, JSON_UNESCAPED_UNICODE));
1158            } else {
1159                $email = request('email');
1160                $comment = request('comment');
1161                if ($email == '') {
1162                    throw new \Exception('Адрес электронной почты не указан!');
1163                } else {
1164                    $data = $this->makePrintForm($form_name, true);
1165                    $mime = $this->getPrintFormMime($form_name);
1166                    $extension = $this->getExtensionFromMIME($mime);
1167
1168                    $fname = $this->typename . '_' . str_replace(":", "_", $form_name) . $extension;
1169                    $viewname = $this->getPrintFormViewName($form_name) . ' (' . $this->viewname . ')';
1170                    $this->sendDocByEMail($email, $comment, $viewname, $data, $fname);
1171                    $tmpl->setContent("{'response': 'send'}");
1172                    doc_log("Send email", $email, 'doc', $this->id);
1173                }
1174            }
1175        } catch (Exception $e) {
1176            $tmpl->setContent("{'response':'err','text':'" . $e->getMessage() . "'}");
1177        }
1178    }
1179
1180    /** Отправка документа по электронной почте
1181     *
1182     * @param $form_name Имя печатной формы
1183     * @param $email Адрес электронной почты
1184     * @param string $text Текст сообщения электронной почты
1185     * @return true
1186     * @throws Exception
1187     */
1188    final function sendEmailTo($form_name, $email, $text='') {
1189        if ($email == '') {
1190            throw new \Exception('Адрес электронной почты не указан!');
1191        }
1192        $data = $this->makePrintFormNoACLTest($form_name, true);
1193        $mime = $this->getPrintFormMime($form_name);
1194        $extension = $this->getExtensionFromMIME($mime);
1195
1196        $fname = $this->typename . '_' . str_replace(":", "_", $form_name) . $extension;
1197        $viewname = $this->getPrintFormViewName($form_name) . ' (' . $this->viewname . ')';
1198        $this->sendDocByEMail($email, $text, $viewname, $data, $fname);
1199        doc_log("Send email", $email, 'doc', $this->id);
1200        return true;
1201    }
1202
1203    /// Печать документа
1204    /// @param $form_name   Имя печатной формы
1205    function printForm($form_name = '') {
1206        global $tmpl;
1207        $tmpl->ajax = 1;
1208        if ($form_name == '') {
1209            $ret_data = array(
1210                'response' => 'item_list',
1211                'content' => $this->getPrintFormList()
1212            );
1213            $tmpl->setContent(json_encode($ret_data, JSON_UNESCAPED_UNICODE));
1214        } else {
1215            $this->makePrintForm($form_name);
1216            $this->sentZEvent('print');
1217            doc_log("PRINT", $form_name, 'doc', $this->id);
1218        }
1219    }
1220
1221    /// Печать документа посетителем сайта / не сотрудником
1222    /// @param $form_name   Имя печатной формы
1223    /// @param $user_print  Если истина - документ запрошен из пользовательского раздела
1224    function printFormFromCabinet($form_name) {
1225        global $tmpl;
1226        $tmpl->ajax = 1;
1227        if ($form_name == '') {
1228            throw new \NotFoundException('Печатная форма не выбрана');
1229        } else {
1230            $this->makePrintFormNoACLTest($form_name);
1231            $this->sentZEvent('userprint');
1232            doc_log("USERPRINT", $form_name, 'doc', $this->id);
1233        }
1234    }
1235
1236    /// Выполнить удаление документа. Если есть зависимости - удаление не производится.
1237    function delExec() {
1238        global $db;
1239        if ($this->doc_data['ok']) {
1240            throw new \Exception("Нельзя удалить проведённый документ");
1241        }
1242        $res = $db->query("SELECT `id`, `mark_del` FROM `doc_list` WHERE `p_doc`='{$this->id}'");
1243        if ($res->num_rows) {
1244            throw new \Exception("Нельзя удалить документ с неудалёнными потомками");
1245        }
1246        $db->query("DELETE FROM `doc_list_pos` WHERE `doc`='{$this->id}'");
1247        $db->query("DELETE FROM `doc_dopdata` WHERE `doc`='{$this->id}'");
1248        $db->query("DELETE FROM `doc_list` WHERE `id`='{$this->id}'");
1249    }
1250
1251    /// Сделать документ потомком указанного документа и вернуть резутьтат в json формате
1252    function connectJson($p_doc) {
1253        try {
1254            \acl::accessGuard('doc.' . $this->typename, \acl::UPDATE);
1255            if ($this->doc_data['firm_id'] > 0) {
1256                \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::UPDATE);
1257            }
1258            $this->subordinate($p_doc);
1259            return " { \"response\": \"connect_ok\" }";
1260        } catch (Exception $e) {
1261            return " { \"response\": \"error\", \"message\": \"" . $e->getMessage() . "\" }";
1262        }
1263    }
1264
1265    /// отправка документа по электронной почте
1266    function sendDocByEMail($email, $comment, $docname, $data, $filename, $body = '') {
1267        global $CONFIG, $db;
1268        $pref = \pref::getInstance();
1269        $res_autor = $db->query("SELECT `worker_real_name`, `worker_phone`, `worker_email` FROM `users_worker_info`
1270            WHERE `user_id`='" . $this->doc_data['user'] . "'");
1271        $doc_autor = $res_autor->fetch_assoc();
1272        $agent = new \models\agent($this->doc_data['agent']);
1273
1274        $email_message = new \email_message();
1275        $email_message->default_charset = "UTF-8";
1276        if ($agent->fullname) {
1277            $email_message->SetEncodedEmailHeader("To", $email, $agent->fullname);
1278        } else if ($agent->name) {
1279            $email_message->SetEncodedEmailHeader("To", $email, $agent->name);
1280        } else {
1281            $email_message->SetEncodedEmailHeader("To", $email, $email);
1282        }
1283
1284        $email_message->SetEncodedHeader("Subject", "{$pref->site_display_name} - $docname ({$pref->site_name})");
1285
1286        if (!@$doc_autor['worker_email']) {
1287            $email_message->SetEncodedEmailHeader("From", $pref->site_email, "Почтовый робот {$pref->site_name}");
1288            $email_message->SetHeader("Sender", $pref->site_email);
1289            $text_message = "Здравствуйте, {$agent->fullname}!\n"
1290                . "Во вложении находится заказанный Вами документ ($docname) от {$pref->site_display_name} ({$pref->site_name})\n\n"
1291                . "$comment\n\n"
1292                . "Сообщение сгенерировано автоматически, отвечать на него не нужно!\n"
1293                . "Для переписки используйте адрес, указанный в контактной информации на сайте http://{$pref->site_name}!";
1294        } else {
1295            $email_message->SetEncodedEmailHeader("From", $doc_autor['worker_email'], $doc_autor['worker_real_name']);
1296            $email_message->SetHeader("Sender", $doc_autor['worker_email']);
1297            $text_message = "Здравствуйте, {$agent->fullname}!\n"
1298                . "Во вложении находится заказанный Вами документ ($docname) от {$pref->site_name}\n\n$comment\n\n"
1299                . "Ответственный сотрудник: {$doc_autor['worker_real_name']}\n"
1300                . "Контактный телефон: {$doc_autor['worker_phone']}\n"
1301                . "Электронная почта (e-mail): {$doc_autor['worker_email']}\n"
1302                . "Отправитель: {$_SESSION['name']}";
1303        }
1304        if ($body) {
1305            $email_message->AddQuotedPrintableTextPart($body);
1306        } else {
1307            $email_message->AddQuotedPrintableTextPart($text_message);
1308        }
1309
1310        $text_attachment = array(
1311            "Data" => $data,
1312            "Name" => $filename,
1313            "Content-Type" => "automatic/name",
1314            "Disposition" => "attachment"
1315        );
1316        $email_message->AddFilePart($text_attachment);
1317
1318        $error = $email_message->Send();
1319
1320        if (strcmp($error, "")) {
1321            throw new \Exception($error);
1322        } else {
1323            return 0;
1324        }
1325    }
1326
1327    /// Обработка отправки запроса на отмену документа
1328    protected function sendPetition() {
1329        global $db;
1330        $ret = array('object' => 'send_petition', 'response' => 'success');
1331        try {
1332            $text = request('text');
1333            $pref = pref::getInstance();
1334            if (mb_strlen($text) < 8) {
1335                throw new Exception('Сообщение слишком короткое! Опишите причину подробнее!');
1336            }
1337            $res = $db->query("SELECT `users`.`reg_email`, `users_worker_info`.`worker_email` FROM `users`
1338                LEFT JOIN `users_worker_info` ON `users_worker_info`.`user_id`=`users`.`id`
1339                WHERE `id`='{$_SESSION['uid']}'");
1340            $user_info = $res->fetch_array();
1341            if ($user_info['worker_email'] != '') {
1342                $from = $user_info['worker_email'];
1343            } else if ($user_info['reg_email'] != '') {
1344                $from = $user_info['reg_email'];
1345            } else {
1346                $from = \cfg::get('site', 'doc_adm_email');
1347            }
1348
1349            $proto = @$_SERVER['HTTPS'] ? 'https' : 'http';
1350            $ip = getenv("REMOTE_ADDR");
1351            $date = date("Y-m-d H:i:s", $this->doc_data['date']);
1352            $txt = "Здравствуйте!\nПользователь {$_SESSION['name']} просит Вас отменить проводку документа *{$this->viewname}* с ID: {$this->id},"
1353                . " {$this->doc_data['altnum']}{$this->doc_data['subtype']} от {$date} на сумму {$this->doc_data['sum']}."
1354                . " Клиент {$this->doc_data['agent_name']}.\n{$proto}://{$_SERVER["HTTP_HOST"]}/doc.php?mode=body&doc={$this->id} \n"
1355                . "Цель отмены: $text.\n"
1356                . "IP: $ip\n"
1357                . "Пожалуйста, дайте ответ на это письмо на $from, как в случае отмены документа, так и об отказе отмены!";
1358
1359            if (\cfg::get('site', 'doc_adm_email')) {
1360                mailto(\cfg::get('site', 'doc_adm_email'), 'Запрос на отмену проведения документа', $txt, $from);
1361            }
1362
1363            if (\cfg::get('site', 'doc_adm_jid') && \cfg::get('xmpp', 'host')) {
1364                require_once(\cfg::getroot('location') . '/common/XMPPHP/XMPP.php');
1365                $xmppclient = new \XMPPHP\XMPP(\cfg::get('xmpp', 'host'), \cfg::get('xmpp', 'port'), \cfg::get('xmpp', 'login'), \cfg::get('xmpp', 'pass')
1366                    , 'MultiMag r' . MULTIMAG_REV);
1367                $xmppclient->connect();
1368                $xmppclient->processUntil('session_start');
1369                $xmppclient->presence();
1370                $xmppclient->message(\cfg::get('site', 'doc_adm_jid'), $txt);
1371                $xmppclient->disconnect();
1372            }
1373            $ret['message'] = "Сообщение было отправлено уполномоченному лицу! Ответ о снятии проводки придёт вам на e-mail!";
1374        } catch (\XMPPHP\Exception $e) {
1375            writeLogException($e);
1376            $ret = array('object' => 'send_petition', 'response' => 'error',
1377                'errormessage' => "Невозможно отправить сообщение по XMPP: " . $e->getMessage()
1378            );
1379        } catch (\Exception $e) {
1380            $ret = array('object' => 'send_petition', 'response' => 'error', 'errormessage' => $e->getMessage());
1381        }
1382        return json_encode($ret, JSON_UNESCAPED_UNICODE);
1383    }
1384
1385    function service() {
1386        global $tmpl;
1387        $tmpl->ajax = 1;
1388        $opt = request('opt');
1389        $pos = rcvint('pos');
1390        $this->_service($opt, $pos);
1391    }
1392
1393    /// Служебные опции
1394    function _service($opt, $pos) {
1395        global $tmpl;
1396        $tmpl->ajax = 1;
1397
1398        if ($this->sklad_editor_enable) {
1399            include_once('doc.poseditor.php');
1400            $poseditor = new DocPosEditor($this);
1401            $poseditor->setAllowNegativeCounts($this->allow_neg_cnt);
1402        }
1403
1404        $peopt = request('peopt'); // Опции редактора списка товаров
1405
1406        \acl::accessGuard('doc.' . $this->typename, \acl::VIEW);
1407        if ($this->doc_data['firm_id'] > 0) {
1408            \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::VIEW);
1409        }
1410
1411        switch ($opt) {
1412            case 'link_info':
1413                $ret = $this->getLinkInfo();
1414                $tmpl->setContent(json_encode($ret, JSON_UNESCAPED_UNICODE));
1415                return 1;
1416            case 'petition':
1417                $tmpl->setContent($this->sendPetition());
1418                return 1;
1419            case 'getheader':
1420                $ret = array(
1421                    'response' => 'success',
1422                    'object' => 'getheader',
1423                    'content' => $this->getDocumentHeader(),
1424                );
1425                $tmpl->setContent(json_encode($ret, JSON_UNESCAPED_UNICODE));
1426                return 1;
1427        }
1428
1429        /// Операции, для которых нужен доступ только на чтение
1430        switch ($peopt) {
1431            case 'jget':    // Json-вариант списка товаров
1432                // TODO: пересчет цены перенести внутрь poseditor
1433                $this->recalcSum();
1434                $doc_content = $poseditor->GetAllContent();
1435                $tmpl->addContent($doc_content);
1436                return 1;
1437            case 'jgetgroups':
1438                $doc_content = $poseditor->getGroupList();
1439                $tmpl->addContent($doc_content);
1440                return 1;
1441            case 'jgpi':        // Получение данных наименования
1442                $pos = rcvint('pos');
1443                $tmpl->addContent($poseditor->GetPosInfo($pos));
1444                return 1;
1445            case 'jsklad':      // Получение номенклатуры выбранной группы
1446                $group_id = rcvint('group_id');
1447                $str = "{ response: 'sklad_list', group: '$group_id',  content: [" . $poseditor->GetSkladList($group_id) . "] }";
1448                $tmpl->setContent($str);
1449                return 1;
1450            case 'jsklads':     // Поиск по подстроке по складу
1451                $s = request('s');
1452                $str = "{ response: 'sklad_list', content: " . $poseditor->SearchSkladList($s) . " }";
1453                $tmpl->setContent($str);
1454                return 1;
1455        }
1456
1457        /// TODO: Это тоже переделать!
1458        if ($this->doc_data['ok']) {
1459            throw new \Exception("Операция не допускается для проведённого документа!");
1460        }
1461        switch ($opt) {
1462            case 'jdeldoc':     // Пометка на удаление
1463                $tmpl->setContent($this->serviceDelDoc());
1464                return 1;
1465            case 'jundeldoc':   // Снять пометку на удаление
1466                $tmpl->setContent($this->serviceUnDelDoc());
1467                return 1;
1468            case 'merge':       // Загрузка номенклатурной таблицы
1469                $ret = $this->mergeDocList($poseditor);
1470                $tmpl->setContent(json_encode($ret, JSON_UNESCAPED_UNICODE));
1471                return 1;
1472        }
1473        if ($this->doc_data['mark_del']) {
1474            throw new \Exception("Операция не допускается для документа, отмеченного для удаления!");
1475        }
1476
1477        /// Операции, изменяющие документ       
1478        switch ($peopt) {
1479            case 'jadd':        // Json вариант добавления позиции
1480                \acl::accessGuard('doc.' . $this->typename, \acl::UPDATE);
1481                if ($this->doc_data['firm_id'] > 0) {
1482                    \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::UPDATE);
1483                }
1484                $pe_pos = rcvint('pe_pos');
1485                $tmpl->setContent($poseditor->AddPos($pe_pos));
1486                break;
1487            case 'jdel':        // Json вариант удаления строки
1488                \acl::accessGuard('doc.' . $this->typename, \acl::UPDATE);
1489                if ($this->doc_data['firm_id'] > 0) {
1490                    \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::UPDATE);
1491                }
1492                $line_id = rcvint('line_id');
1493                $tmpl->setContent($poseditor->Removeline($line_id));
1494                break;
1495            case 'jup':     // Json вариант обновления
1496                \acl::accessGuard('doc.' . $this->typename, \acl::UPDATE);
1497                if ($this->doc_data['firm_id'] > 0) {
1498                    \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::UPDATE);
1499                }
1500                $line_id = rcvint('line_id');
1501                $value = request('value');
1502                $type = request('type');
1503                // TODO: пересчет цены перенести внутрь poseditor
1504                $tmpl->setContent($poseditor->UpdateLine($line_id, $type, $value));
1505                break;
1506            case 'jsn':         // Серийные номера
1507                \acl::accessGuard('doc.' . $this->typename, \acl::UPDATE);
1508                if ($this->doc_data['firm_id'] > 0) {
1509                    \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::UPDATE);
1510                }
1511                $action = request('a');
1512                $line_id = request('line');
1513                $data = request('data');
1514                $tmpl->setContent($poseditor->SerialNum($action, $line_id, $data));
1515                break;
1516            case 'jrc':         // Сброс цен
1517                \acl::accessGuard('doc.' . $this->typename, \acl::UPDATE);
1518                if ($this->doc_data['firm_id'] > 0) {
1519                    \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::UPDATE);
1520                }
1521                $poseditor->resetPrices();
1522                break;
1523            case 'jorder':      // Сортировка наименований
1524                \acl::accessGuard('doc.' . $this->typename, \acl::UPDATE);
1525                if ($this->doc_data['firm_id'] > 0) {
1526                    \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::UPDATE);
1527                }
1528                $by = request('by');
1529                $poseditor->reOrder($by);
1530                break;
1531            default:
1532                return 0;
1533        }
1534        return 1;
1535    }
1536
1537    /// Получить многомерный массив с данными заголовка документа
1538    public function getDocumentHeader() {
1539        global $db, $CONFIG;
1540        $ret = array();
1541
1542        if ($this->doc_type == 0) {
1543            throw new Exception("Невозможно получить заголовок документа неизвестного типа");
1544        } else {
1545            // Динамические: баланс, бонусы, список договоров агента           
1546            $ret['id'] = $this->id;
1547            $ret['viewname'] = $this->viewname;
1548            $ret['type'] = $this->doc_type;
1549            $ret['typename'] = $this->typename;
1550            $ret['altnum'] = $this->doc_data['altnum'];
1551            $ret['subtype'] = $this->doc_data['subtype'];
1552            $ret['mark_del'] = $this->doc_data['mark_del'];
1553            $ret['firm_id'] = $this->doc_data['firm_id'];           
1554            $ret['comment'] = $this->doc_data['comment'];
1555            $ret['created'] = $this->doc_data['created'];
1556            $ret['ok'] = $this->doc_data['ok'];
1557            $ret['p_doc'] = $this->doc_data['p_doc'];
1558           
1559            $fields = explode(' ', $this->header_fields);
1560            $ret['header_fields'] = $fields;
1561           
1562            foreach ($fields as $f) {
1563                switch ($f) {
1564                    case 'agent': 
1565                        $ret['agent_id'] = $this->doc_data['agent'];
1566                        $ret['contract_id'] = $this->doc_data['contract'];
1567                        break;
1568                    case 'sklad':
1569                        $ret['store_id'] = $this->doc_data['sklad'];
1570                        break;
1571                    case 'kassa':
1572                        $ret['cash_id'] = $this->doc_data['kassa'];
1573                        break;
1574                    case 'bank':
1575                        $ret['bank_id'] = $this->doc_data['bank'];
1576                        break;
1577                    case 'cena':
1578                        $ret['price_id'] = $this->dop_data['cena'];
1579                        break;
1580                    case 'sum': 
1581                        $ret['sum'] = $this->doc_data['sum'];
1582                        break;
1583                }
1584            }
1585
1586            if (isset($CONFIG['site']['default_firm'])) {
1587                $ret['default_firm_id'] = $CONFIG['site']['default_firm'];
1588            }
1589            $ret['dop_buttons'] = $this->getDopButtons();
1590            $firm_ldo = new \Models\LDO\firmnames();
1591            $ret['firm_names'] = $firm_ldo->getData();
1592           
1593
1594            if ($this->doc_data['date']) {
1595                $ret['date'] = date("Y-m-d H:i:s", $this->doc_data['date']);
1596            } else {
1597                $ret['date'] = date("Y-m-d H:i:s");
1598            }
1599
1600            if (in_array('agent', $ret['header_fields'])) {
1601                $contract_list = array();
1602                $res = $db->query("SELECT `doc_list`.`id`, `doc_list`.`altnum`, `doc_list`.`subtype`, `doc_dopdata`.`value` AS `name`, `doc_list`.`date`
1603                    FROM `doc_list`
1604                    LEFT JOIN `doc_dopdata` ON `doc_dopdata`.`doc`=`doc_list`.`id` AND `doc_dopdata`.`param`='name'
1605                    WHERE `agent`='{$this->doc_data['agent']}' AND `type`='14' AND `firm_id`='{$this->doc_data['firm_id']}'");
1606                while ($line = $res->fetch_assoc()) {
1607                    $line['date'] = date("Y-m-d H:i:s", $line['date']);
1608                    $contract_list[] = $line;
1609                }
1610                $ret['agent_info'] = array(
1611                    'name' => $this->doc_data['agent_name'],
1612                    'balance' => agentCalcDebt($this->doc_data['agent']),
1613                    'bonus' => docCalcBonus($this->doc_data['agent']),
1614                    'contract_list' => $contract_list,
1615                    'dishonest' => $this->doc_data['agent_dishonest'],
1616                );
1617            }           
1618            $ret['ext_fields'] = $this->getExtControls();
1619            $ret = array_merge($this->dop_data, $this->text_data, $ret);
1620        }
1621        return $ret;
1622    }
1623   
1624    /// обновить заголовок документа данными из массива
1625    public function updateDocumentHeader($data) {
1626        $doc_data = array();
1627        $dop_data = array();
1628        if(isset($data['altnum'])) {
1629            $doc_data['altnum'] = $data['altnum'];
1630        }
1631        if(isset($data['subtype'])) {
1632            $doc_data['subtype'] = $data['subtype'];
1633        }
1634        if(isset($data['firm_id'])) {
1635            $doc_data['firm_id'] = $data['firm_id'];
1636        }
1637        if(isset($data['comment'])) {
1638            $doc_data['comment'] = $data['comment'];
1639        }
1640       
1641        $fields = explode(' ', $this->header_fields);
1642        foreach ($fields as $f) {
1643            switch ($f) {
1644                case 'agent': 
1645                    if (isset($data['agent_id'])) {
1646                        $doc_data['agent'] = $data['agent_id'];
1647                    }
1648                    if (isset($data['contract_id'])) {
1649                        $doc_data['contract'] = $data['contract_id'];
1650                    }
1651                    break;
1652                case 'sklad':
1653                    if (isset($data['store_id'])) {
1654                        $doc_data['sklad'] = $data['store_id'];
1655                    }
1656                    break;
1657                case 'kassa':
1658                    if (isset($data['cash_id'])) {
1659                        $doc_data['kassa'] = $data['cash_id'];
1660                    }
1661                    break;
1662                case 'bank':
1663                    if (isset($data['bank_id'])) {
1664                        $doc_data['bank'] = $data['bank_id'];
1665                    }
1666                    break;
1667                case 'cena':
1668                    if (isset($data['price_id'])) {
1669                        $dop_data['cena'] = $data['price_id'];
1670                    }
1671                    break;
1672                case 'sum':
1673                    if (isset($data['sum'])) {
1674                        $doc_data['sum'] = $data['sum'];
1675                    }
1676                    break;
1677            }
1678        }
1679        foreach($this->def_dop_data as $name => $value) {
1680            if (isset($data[$name])) {
1681                $dop_data[$name] = $data[$name];
1682            }
1683        }
1684        $extcontrols = $this->getExtControls();
1685        foreach ($extcontrols as $ex_name => $ex_data) {
1686            switch($ex_data['type']) {
1687                case 'text':
1688                case 'select':
1689                case 'status':
1690                    if (isset($data[$ex_name])) {
1691                        $dop_data[$ex_name] = $data[$ex_name];
1692                    }
1693                    break;
1694                case 'checkbox':
1695                    if (isset($data[$ex_name])) {
1696                        $dop_data[$ex_name] = $data[$ex_name]?1:0;
1697                    }
1698                    break;
1699            }
1700        }
1701        if(count($doc_data)>0) {
1702            $this->setDocDataA($doc_data);
1703        }
1704        if(count($dop_data)>0) {
1705            //throw new Exception(json_encode($dop_data));
1706            $this->setDopDataA($dop_data);
1707        }
1708    }
1709
1710    /// Слияние табличной части двух документов
1711    protected function mergeDocList($poseditor) {
1712        global $db;
1713        $from_doc = rcvint('from_doc');
1714        $clear = rcvint('clear');
1715        $no_sum = rcvint('no_sum');
1716
1717        try {
1718            if ($from_doc == 0) {
1719                throw new Exception("Документ не задан");
1720            }
1721            $db->startTransaction();
1722
1723            $res = $db->query("SELECT `id` FROM `doc_list` WHERE `id`=$from_doc");
1724            if (!$res->num_rows) {
1725                throw new Exception("Документ не найден");
1726            }
1727
1728            if ($clear) {
1729                $db->query("DELETE FROM `doc_list_pos` WHERE `doc`='{$this->id}'");
1730            }
1731
1732            $res = $db->query("SELECT `doc`, `tovar`, SUM(`cnt`) AS `cnt`, `gtd`, `comm`, `cost`, `page` FROM `doc_list_pos`"
1733                . "WHERE `doc`=$from_doc AND `page`=0 GROUP BY `tovar`");
1734            while ($line = $res->fetch_assoc()) {
1735                if (!$no_sum) {
1736                    $poseditor->simpleIncrementPos($line['tovar'], $line['cost'], $line['cnt'], $line['comm']);
1737                } else {
1738                    $poseditor->simpleRewritePos($line['tovar'], $line['cost'], $line['cnt'], $line['comm']);
1739                }
1740            }
1741            doc_log("REWRITE", "", 'doc', $this->id);
1742            $db->commit();
1743            $ret = array('response' => 'merge_ok');
1744        } catch (Exception $e) {
1745            $ret = array('response' => 'err', 'text' => $e->getMessage());
1746        }
1747        return $ret;
1748    }
1749
1750    /// Получить информацию о связях документа
1751    protected function getLinkInfo() {
1752        global $db;
1753        $childs = array();
1754        $parent = null;
1755        if ($this->doc_data['p_doc']) {
1756            $res = $db->query("SELECT `doc_list`.`id`, `doc_types`.`name`, `doc_list`.`altnum`, `doc_list`.`subtype`, `doc_list`.`date`,
1757                                    `doc_list`.`ok`, `doc_list`.`sum` FROM `doc_list`
1758                                    LEFT JOIN `doc_types` ON `doc_types`.`id`=`doc_list`.`type`
1759                                    WHERE `doc_list`.`id`='{$this->doc_data['p_doc']}'");
1760            $parent = $res->fetch_assoc();
1761            $parent['vdate'] = date("d.m.Y", $parent['date']);
1762        }
1763        $res = $db->query("SELECT `doc_list`.`id`, `doc_types`.`name`, `doc_list`.`altnum`, `doc_list`.`subtype`, `doc_list`.`date`,
1764                                `doc_list`.`ok`, `doc_list`.`sum` FROM `doc_list`
1765                                LEFT JOIN `doc_types` ON `doc_types`.`id`=`doc_list`.`type`
1766                                WHERE `doc_list`.`p_doc`='{$this->id}'");
1767
1768        while ($line = $res->fetch_assoc()) {
1769            $line['vdate'] = date("d.m.Y", $line['date']);
1770            $childs[] = $line;
1771        }
1772        $ret = array('response' => 'link_info', 'parent' => $parent, 'childs' => $childs);
1773        return $ret;
1774    }
1775
1776    protected function drawLHeadformStart() {
1777        $this->drawHeadformStart('j');
1778    }
1779
1780    /// Отобразить заголовок шапки документа
1781    protected function drawHeadformStart($alt = '') {
1782        global $tmpl, $CONFIG, $db;
1783        $pref = \pref::getInstance();
1784        if ($this->doc_data['date'])
1785            $dt = date("Y-m-d H:i:s", $this->doc_data['date']);
1786        else
1787            $dt = date("Y-m-d H:i:s");
1788        $tmpl->addContent("<form method='post' action='' id='doc_head_form'>
1789                <input type='hidden' name='mode' value='{$alt}heads'>
1790                <input type='hidden' name='type' value='" . $this->doc_type . "'>");
1791        if (isset($this->doc_data['id']))
1792            $tmpl->addContent("<input type='hidden' name='doc' value='" . $this->doc_data['id'] . "'>");
1793        if (@$this->doc_data['mark_del'])
1794            $tmpl->addContent("<h3>Документ помечен на удаление!</h3>");
1795        $tmpl->addContent("
1796                <table id='doc_head_main'>
1797                <tr><td class='altnum'>А. номер</td><td class='subtype'>Подтип</td><td class='datetime'>Дата и время</td><tr>
1798                <tr class='inputs'>
1799                <td class='altnum'><input type='text' name='altnum' value='" . $this->doc_data['altnum'] . "' id='anum'><a href='#' onclick=\"return GetValue('/doc.php?mode=incnum&type=" . $this->doc_type . "&amp;doc=" . $this->id . "', 'anum', 'sudata', 'datetime', 'firm_id')\"><img border=0 src='/img/i_add.png' alt='Новый номер'></a></td>
1800                <td class='subtype'><input type='text' name='subtype' value='" . $this->doc_data['subtype'] . "' id='sudata'></td>
1801                <td class='datetime'><input type='text' name='datetime' value='$dt' id='datetime'></td>
1802                </tr>
1803                </table>
1804                Организация:<br><select name='firm' id='firm_id'>");
1805        $res = $db->query("SELECT `id`, `firm_name` FROM `doc_vars` ORDER BY `firm_name`");
1806        if (!$this->doc_data['firm_id'])
1807            $this->doc_data['firm_id'] = $pref->site_default_firm;
1808        while ($nx = $res->fetch_row()) {
1809            if ($this->doc_data['firm_id'] == $nx[0])
1810                $s = ' selected';
1811            else
1812                $s = '';
1813            $tmpl->addContent("<option value='$nx[0]' $s>$nx[1] / $nx[0]</option>");
1814        }
1815        $tmpl->addContent("</select><br>");
1816    }
1817
1818    protected function drawLHeadformEnd() {
1819        global $tmpl;
1820        $tmpl->addContent("<br>Комментарий:<br><textarea name='comment'>" . html_out($this->doc_data['comment']) . "</textarea></form>");
1821    }
1822
1823    protected function drawHeadformEnd() {
1824        global $tmpl;
1825        $tmpl->addContent(@"<br>Комментарий:<br><textarea name='comment'>" . html_out($this->doc_data['comment']) . "</textarea><br><input type=submit value='Записать'></form>");
1826    }
1827
1828    /// Сформировать поля выбора агента
1829    protected function drawAgentField() {
1830        global $tmpl, $db;
1831        $balance = agentCalcDebt($this->doc_data['agent']);
1832        $bonus = docCalcBonus($this->doc_data['agent']);
1833        $col = '';
1834        if ($balance > 0)
1835            $col = "color: #f00; font-weight: bold;";
1836        if ($balance < 0)
1837            $col = "color: #f08; font-weight: bold;";
1838
1839        $res = $db->query("SELECT `doc_list`.`id`, `doc_dopdata`.`value`
1840                FROM `doc_list`
1841                LEFT JOIN `doc_dopdata` ON `doc_dopdata`.`doc`=`doc_list`.`id` AND `doc_dopdata`.`param`='name'
1842                WHERE `agent`='{$this->doc_data['agent']}' AND `type`='14' AND `firm_id`='{$this->doc_data['firm_id']}'");
1843        $contr_content = '';
1844        while ($nxt = $res->fetch_row()) {
1845            $selected = ($this->doc_data['contract'] == $nxt[0]) ? 'selected' : '';
1846            $contr_content.="<option value='$nxt[0]' $selected>N$nxt[0]: $nxt[1]</option>";
1847        }
1848        if ($contr_content)
1849            $contr_content = "Договор:<br><select name='contract'>$contr_content</select>";
1850
1851        if ($this->doc_data['agent_dishonest'])
1852            $ag = "<span style='color: #f00; font-weight:bold;'>Был выбран недобросовестный агент!</span>";
1853        else
1854            $ag = '';
1855        $tmpl->addContent("
1856                <div>
1857                <div style='float: right; $col' id='agent_balance_info' onclick=\"ShowPopupWin('/docs.php?l=inf&mode=srv&opt=dolgi&agent={$this->doc_data['agent']}'); return false;\">$balance / $bonus</div>
1858                Агент:
1859                <a href='/docs.php?l=agent&mode=srv&opt=ep&pos={$this->doc_data['agent']}' id='ag_edit_link' target='_blank'><img src='/img/i_edit.png'></a>
1860                <a href='/docs.php?l=agent&mode=srv&opt=ep' target='_blank'><img src='/img/i_add.png'></a>
1861                </div>
1862                <input type='hidden' name='agent' id='agent_id' value='{$this->doc_data['agent']}'>
1863                <input type='text' id='agent_nm'  style='width: 100%;' value='" . html_out($this->doc_data['agent_name']) . "'>
1864                $ag
1865                <div id='agent_contract'>$contr_content</div>
1866                <br>
1867
1868                <script type=\"text/javascript\">
1869                $(document).ready(function(){
1870                        $(\"#agent_nm\").autocomplete(\"/docs.php\", {
1871                                delay:300,
1872                                minChars:1,
1873                                matchSubset:1,
1874                                autoFill:false,
1875                                selectFirst:true,
1876                                matchContains:1,
1877                                cacheLength:10,
1878                                maxItemsToShow:15,
1879                                formatSelectedItem: function(li) {
1880                                        if(li.querySelector('em').dataset.name)
1881                                                li.selectValue = li.querySelector('em').dataset.name;
1882                                        return li;
1883                                },
1884                                formatItem:agliFormat,
1885                                onItemSelect:agselectItem,
1886                                extraParams:{'l':'agent','mode':'srv','opt':'ac'}
1887                        });
1888                });
1889
1890                function agliFormat (row, i, num) {
1891                        var result =
1892                                row[0] +
1893                                \"<em class='qnt' data-name = '\"+row[4]+\"'> тел . \" + row[2] + \"</em>\";
1894                        return result;
1895                }
1896
1897                function agselectItem(li) {
1898                        if( li == null ) var sValue = \"Ничего не выбрано!\";
1899                        if( !!li.extra ) var sValue = li.extra[0];
1900                        else var sValue = li.selectValue;
1901                        document.getElementById('agent_id').value=sValue;
1902                        document.getElementById('ag_edit_link').href='/docs.php?l=agent&mode=srv&opt=ep&pos='+sValue;
1903                        var firm_id_elem = document.getElementById('firm_id');
1904                        var firm_id = 0;
1905                        if(firm_id_elem) {
1906                            firm_id = firm_id_elem.value;
1907                        }
1908                        UpdateContractInfo('{$this->id}',firm_id,sValue);
1909                       
1910                        ");
1911        if (!$this->id)
1912            $tmpl->addContent("
1913                        var plat_id=document.getElementById('plat_id');
1914                        if(plat_id)     plat_id.value=li.extra[0];
1915                        var plat=document.getElementById('plat');
1916                        if(plat)        plat.value=li.selectValue;
1917                        var gruzop_id=document.getElementById('gruzop_id');
1918                        if(gruzop_id)   gruzop_id.value=li.extra[0];
1919                        var gruzop=document.getElementById('gruzop');
1920                        if(gruzop)      gruzop.value=li.selectValue;");
1921        $tmpl->addContent("
1922                }
1923                </script>");
1924    }
1925
1926    protected function drawSkladField() {
1927        global $tmpl, $db;
1928        $tmpl->addContent("Склад:<br>
1929                <select name='sklad'>");
1930        $res = $db->query("SELECT `id`,`name` FROM `doc_sklady` ORDER BY `id`");
1931
1932        while ($nxt = $res->fetch_row()) {
1933            if ($nxt[0] == $this->doc_data['sklad'])
1934                $tmpl->addContent("<option value='$nxt[0]' selected>" . html_out($nxt[1]) . "</option>");
1935            else
1936                $tmpl->addContent("<option value='$nxt[0]'>" . html_out($nxt[1]) . "</option>");
1937        }
1938        $tmpl->addContent("</select><br>");
1939    }
1940
1941    protected function drawBankField() {
1942        global $tmpl, $CONFIG, $db;
1943        if ($this->doc_data['firm_id'])
1944            $sql_add = "AND ( `firm_id`='0' OR `num`='{$this->doc_data['bank']}' OR `firm_id`='{$this->doc_data['firm_id']}' )";
1945        else
1946            $sql_add = '';
1947        if ($this->doc_data['bank'])
1948            $bank = $this->doc_data['bank'];
1949        else {
1950            $pref = \pref::getInstance();
1951            $bank = $pref->getSitePref('default_bank_id');
1952        }
1953        $tmpl->addContent("Банк:<br><select name='bank'>");
1954        $res = $db->query("SELECT `num`, `name`, `rs` FROM `doc_kassa` WHERE `ids`='bank' $sql_add  ORDER BY `num`");
1955        while ($nxt = $res->fetch_row()) {
1956            if ($nxt[0] == $bank)
1957                $tmpl->addContent("<option value='$nxt[0]' selected>" . html_out($nxt[1] . ' / ' . $nxt[2]) . "</option>");
1958            else
1959                $tmpl->addContent("<option value='$nxt[0]'>" . html_out($nxt[1] . ' / ' . $nxt[2]) . "</option>");
1960        }
1961        $tmpl->addContent("</select><br>");
1962    }
1963
1964    protected function drawKassaField() {
1965        global $tmpl, $db;
1966        if ($this->doc_data['kassa']) {
1967            $kassa = $this->doc_data['kassa'];
1968        } else {
1969            $pref = \pref::getInstance();
1970            $kassa = $pref->getSitePref('default_cash_id');
1971        }
1972        settype($kassa, 'int');
1973        $tmpl->addContent("Касса:<br><select name='kassa'>");
1974        $res = $db->query("SELECT `num`, `name` FROM `doc_kassa` WHERE `ids`='kassa' AND
1975                    (`firm_id`='0' OR `firm_id` IS NULL OR `firm_id`='{$this->doc_data['firm_id']}' OR `num`='$kassa') ORDER BY `num`");
1976
1977        if ($kassa == 0) {
1978            $tmpl->addContent("<option value='0'>--не выбрана--</option>");
1979        }
1980        while ($nxt = $res->fetch_row()) {
1981            if ($nxt[0] == $kassa) {
1982                $tmpl->addContent("<option value='$nxt[0]' selected>" . html_out($nxt[1]) . "</option>");
1983            } else {
1984                $tmpl->addContent("<option value='$nxt[0]'>" . html_out($nxt[1]) . "</option>");
1985            }
1986        }
1987        $tmpl->addContent("</select><br>");
1988    }
1989
1990    protected function drawSumField() {
1991        global $tmpl;
1992        $tmpl->addContent("Сумма:<br>
1993                <input type='text' name='sum' value='{$this->doc_data['sum']}'><img src='/img/i_+-.png'><br>");
1994    }
1995
1996    protected function drawPriceField() {
1997        global $tmpl, $db;
1998        $tmpl->addContent("Цена:<a onclick='ResetCost(\"{$this->id}\"); return false;' id='reset_cost'><img src='/img/i_reload.png'></a><br>
1999                <select name='cena'>");
2000        $s = '';
2001        if ($this->dop_data['cena'] == 0)
2002            $s = ' selected';
2003        $tmpl->addContent("<option value='0'{$s}>--авто--</option>");
2004        $res = $db->query("SELECT `id`,`name` FROM `doc_cost` ORDER BY `name`");
2005        while ($nxt = $res->fetch_row()) {
2006            if ($this->dop_data['cena'] == $nxt[0])
2007                $s = 'selected';
2008            else
2009                $s = '';
2010            $tmpl->addContent("<option value='$nxt[0]' $s>" . html_out($nxt[1]) . "</option>");
2011        }
2012
2013        if ($this->doc_data['nds'])
2014            $tmpl->addContent("<label><input type='radio' name='nds' value='0' disabled>Выделять НДС</label>&nbsp;&nbsp;
2015                        <label><input type='radio' name='nds' value='1' checked>Включать НДС</label><br>");
2016        else
2017            $tmpl->addContent("<label><input type='radio' name='nds' value='0' checked>Выделять НДС</label>&nbsp;&nbsp;
2018                        <label><input type='radio' name='nds' value='1'>Включать НДС</label><br>");
2019        $tmpl->addContent("<br>");
2020    }
2021
2022    // ====== Получение данных, связанных с документом =============================
2023    protected function get_docdata() {
2024        if (isset($this->doc_data)) {
2025            return;
2026        }
2027        global $db;
2028        if ($this->id) {
2029            $this->loadFromDb($this->id);
2030        } else {
2031            if (method_exists($this, 'initDefDopData')) {
2032                $this->initDefDopData();
2033            }
2034            $this->dop_data = $this->def_dop_data;
2035            $pref = \pref::getInstance();
2036
2037            $this->doc_data = array('id' => 0, 'type' => '', 'agent' => $pref->getSitePref('default_agent_id'), 'comment' => '', 'date' => time(), 'ok' => 0,
2038                'sklad' => $pref->getSitePref('default_store_id'), 'user' => 0, 'altnum' => 0, 'subtype' => '', 'sum' => 0, 'nds' => 1, 'p_doc' => 0, 'mark_del' => 0,
2039                'kassa' => 0, 'bank' => 0, 'firm_id' => 0, 'contract' => 0, 'created' => 0, 'agent_name' => '', 'agent_fullname' => '', 'agent_dishonest' => 0, 'agent_comment' => '');
2040
2041            if (!$this->doc_data['agent']) {
2042                $this->doc_data['agent'] = 1;
2043            }
2044            $agent_data = $db->selectRow('doc_agent', $this->doc_data['agent']);
2045            if (is_array($agent_data)) {
2046                $this->doc_data['agent_name'] = $agent_data['name'];
2047            }
2048
2049            if (!$this->doc_data['sklad']) {
2050                $this->doc_data['sklad'] = 1;
2051            }
2052        }
2053    }
2054
2055    /// Проверка уникальности альтернативного порядкового номера документа
2056    public function isAltNumUnique() {
2057        global $db;
2058        $start_date = strtotime(date("Y-01-01 00:00:00", $this->doc_data['date']));
2059        $end_date = strtotime(date("Y-12-31 23:59:59", $this->doc_data['date']));
2060        $subtype_sql = $db->real_escape_string($this->doc_data['subtype']);
2061        $res = $db->query("SELECT `altnum` FROM `doc_list`"
2062            . " WHERE `type`='{$this->doc_type}' AND `altnum`='{$this->doc_data['altnum']}' AND `subtype`='$subtype_sql'"
2063            . " AND `id`!='{$this->id}' AND `date`>='$start_date' AND `date`<='$end_date' AND `firm_id`='{$this->doc_data['firm_id']}'");
2064        return $res->num_rows ? false : true;
2065    }
2066
2067    /// Получение альтернативного порядкового номера документа
2068    public function getNextAltNum($doc_type, $subtype, $date, $firm_id) {
2069        global $CONFIG, $db;
2070        if (!$doc_type) {
2071            $doc_type = $this->doc_type;
2072        }
2073        $start_date = strtotime(date("Y-01-01 00:00:00", strtotime($date)));
2074        $end_date = strtotime(date("Y-12-31 23:59:59", strtotime($date)));
2075        $res = $db->query("SELECT `altnum` FROM `doc_list` WHERE `type`='$doc_type' AND `subtype`='$subtype'"
2076            . " AND `id`!='{$this->id}' AND `date`>='$start_date' AND `date`<='$end_date' AND `firm_id`='$firm_id'"
2077            . " ORDER BY `altnum` ASC");
2078        $newnum = 0;
2079        while ($nxt = $res->fetch_row()) {
2080            if (($nxt[0] - 1 > $newnum) && @$CONFIG['doc']['use_persist_altnum'])
2081                break;
2082            $newnum = $nxt[0];
2083        }
2084        $newnum++;
2085        return $newnum;
2086    }
2087
2088    /// Кнопки меню - провети / отменить
2089    protected function getDopButtons() {
2090        global $tmpl;
2091        $ret = '';
2092        if ($this->id) {
2093            $ret.="<a href='/doc.php?mode=log&amp;doc={$this->id}' title='История изменений документа'><img src='img/i_log.png' alt='История'></a>";
2094            $ret.="<span id='provodki'>";
2095            if ($this->doc_data['ok']) {
2096                $ret .= $this->getCancelButtons();
2097            } else {
2098                $ret .= $this->getApplyButtons();
2099            }
2100
2101            $ret .= "</span>
2102                <img src='/img/i_separator.png' alt=''>
2103                <a href='#' onclick=\"return PrintMenu(event, '{$this->id}')\" title='Печать'>
2104                    <img src='img/i_print.png' alt='Печать'></a>
2105                <a href='#' onclick=\"return FaxMenu(event, '{$this->id}')\" title='Отправить по факсу'>
2106                    <img src='img/i_fax.png' alt='Факс'></a>
2107                <a href='#' onclick=\"return MailMenu(event, '{$this->id}')\" title='Отправить по email'>
2108                    <img src='img/i_mailsend.png' alt='email'></a>
2109                <img src='/img/i_separator.png' alt=''>
2110                <a href='#' onclick=\"DocConnect('{$this->id}', '{$this->doc_data['p_doc']}'); return false;\" title='Связать документ'>
2111                    <img src='img/i_conn.png' alt='Связать'></a>
2112                <a href='#' onclick=\"return ShowContextMenu(event, '/doc.php?mode=morphto&amp;doc={$this->id}')\"
2113                    title='Создать связанный документ'><img src='img/i_to_new.png' alt='Связь'></a>";
2114            if ($this->sklad_editor_enable) {
2115                $ret .= " <a href='#' onclick=\"return addNomMenu(event, '{$this->id}', '{$this->doc_data['p_doc']}');\" title='Обновить номенклатурную таблицу'><img src='img/i_addnom.png' alt='Обновить номенклатурную таблицу'></a>";
2116            }
2117            $ret.="<img src='/img/i_separator.png' alt=''>";
2118        }
2119
2120        if (method_exists($this, 'getAdditionalButtonsHTML')) {
2121            $ret .= $this->getAdditionalButtonsHTML();
2122        }
2123        return $ret;
2124    }
2125
2126    protected function getApplyButtons() {
2127        if ($this->doc_data['mark_del']) {
2128            return "<a href='#' title='Отменить удаление' onclick='unMarkDelDoc({$this->id}); return false;'><img src='img/i_trash_undo.png' alt='отменить удаление'></a>";
2129        } else {
2130            return "<a href='#' title='Пометить на удаление' onclick='MarkDelDoc({$this->id}); return false;'><img src='img/i_trash.png' alt='Пометить на удаление'></a>" .
2131                "<a href='#' title='Провести документ' onclick='ApplyDoc({$this->id}); return false;'><img src='img/i_ok.png' alt='Провести'></a>";
2132        }
2133        //<a href='?mode=ehead&amp;doc={$this->doc}' title='Правка заголовка'><img src='img/i_docedit.png' alt='Правка'></a>
2134    }
2135
2136    protected function getCancelButtons() {
2137        return "<a title='Отменить проводку' onclick='CancelDoc({$this->id}); return false;'><img src='img/i_revert.png' alt='Отменить' /></a>";
2138    }
2139
2140    /// Вычисление, можно ли отменить кассовый документ
2141    protected function checkKassMinus() {
2142        global $db;
2143        $sum = $i = 0;
2144        $res = $db->query("SELECT `doc_list`.`id`, `doc_list`.`type`, `doc_list`.`sum`, `doc_list`.`kassa` FROM `doc_list`
2145                WHERE  `doc_list`.`ok`>'0' AND ( `doc_list`.`type`='6' OR `doc_list`.`type`='7' OR `doc_list`.`type`='9')
2146                ORDER BY `doc_list`.`date`");
2147        while ($nxt = $res->fetch_row()) {
2148            if ($nxt[3] == $this->doc_data['kassa']) {
2149                if ($nxt[1] == 6)
2150                    $sum += $nxt[2];
2151                else if ($nxt[1] == 7 || $nxt[1] == 9)
2152                    $sum -= $nxt[2];
2153            }
2154            else if ($nxt[1] == 9) {
2155                $rr = $db->query("SELECT `value` FROM `doc_dopdata` WHERE `doc`='$nxt[0]' AND `param`='v_kassu'");
2156                if (!$rr->num_rows)
2157                    throw new AutoLoggedException('Касса назначения не найдена в документе ' . $this->id);
2158                $data = $rr->fetch_row();
2159                if ($data[0] == $this->doc_data['kassa'])
2160                    $sum+=$nxt[2];
2161            }
2162
2163            $sum = sprintf("%01.2f", $sum);
2164            if ($sum < 0)
2165                break;
2166            $i++;
2167        }
2168        $res->free();
2169        return $sum;
2170    }
2171
2172    /// Показать историю изменений документа
2173    public function showLog() {
2174        global $tmpl;
2175        \acl::accessGuard('doc.' . $this->typename, \acl::VIEW);
2176        if ($this->doc_data['firm_id'] > 0) {
2177            \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::VIEW);
2178        }
2179        $tmpl->setTitle($this->viewname . ' N' . $this->id);
2180        doc_menu($this->getDopButtons());
2181        $tmpl->addContent("<h1>{$this->viewname} N{$this->id} - история документа</h1>");
2182
2183        $logview = new \LogView();
2184        $logview->setObject('doc');
2185        $logview->setObjectId($this->id);
2186        $logview->showLog();
2187    }
2188
2189    /// Получить список номенклатуры
2190    function getDocumentNomenclature($options = '') {
2191        global $CONFIG, $db;
2192        $opts = array();
2193        $e_options = explode(',', $options);
2194        foreach ($e_options as $opt) {
2195            $opts[$opt] = 1;
2196        }
2197        $fields_sql = $join_sql = '';
2198        if (isset($opts['country'])) {
2199            $fields_sql .= ", `class_country`.`name` AS `country_name`, `class_country`.`number_code` AS `country_code`";
2200            $join_sql .= " LEFT JOIN `class_country` ON `class_country`.`id`=`doc_base`.`country`";
2201        }
2202        if (isset($opts['comment'])) {
2203            $fields_sql .= ", `doc_list_pos`.`comm` AS `comment`";
2204        }
2205        if (isset($opts['base_desc'])) {
2206            $fields_sql .= ", `doc_base`.`desc` AS `base_desc`";
2207        }
2208        if (isset($opts['vat'])) {
2209            $fields_sql .= ", `doc_base`.`nds` AS `vat`";
2210        }
2211        if (isset($opts['base_price'])) {
2212            $fields_sql .= ", `doc_base`.`cost` AS `base_price`";
2213        }
2214        if (isset($opts['bulkcnt'])) {
2215            $fields_sql .= ", `doc_base`.`bulkcnt`";
2216        }
2217        if (isset($opts['dest_place'])) {
2218            $to_sklad = (int) $this->dop_data['na_sklad'];
2219            $fields_sql .= ", `pt_d`.`mesto` AS `dest_place`";
2220            $join_sql .= " LEFT JOIN `doc_base_cnt` AS `pt_d` ON `pt_d`.`id`=`doc_list_pos`.`tovar` AND `pt_d`.`sklad`='{$to_sklad}'";
2221        }
2222        if (isset($opts['bigpack'])) {
2223            // ID параметра большой упаковки
2224            $res = $db->query("SELECT `id` FROM `doc_base_params` WHERE `codename`='bigpack_cnt'");
2225            if (!$res->num_rows) {
2226                $db->query("INSERT INTO `doc_base_params` (`name`, `codename`, `type`, `hidden`)"
2227                    . " VALUES ('Кол-во в большой упаковке', 'bigpack_cnt', 'int', 0)");
2228                throw new \Exception("Параметр *bigpack_cnt - кол-во в большой упаковке* не найден. Параметр создан.");
2229            }
2230            list($p_bp_id) = $res->fetch_row();
2231            $fields_sql .= ", `bp_t`.`value` AS `bigpack_cnt`";
2232            $join_sql .= " LEFT JOIN `doc_base_values` AS `bp_t` ON `bp_t`.`id`=`doc_base`.`id` AND `bp_t`.`param_id`='$p_bp_id'";
2233        }
2234        if (isset($opts['rto'])) {
2235            $fields_sql .= ", `doc_base_dop`.`transit`, `doc_base_dop`.`reserve`, `doc_base_dop`.`offer`";
2236            $join_sql .= " LEFT JOIN `doc_base_dop` ON `doc_base_dop`.`id`=`doc_list_pos`.`tovar`";
2237        }
2238        $list = array();
2239        $res = $db->query("SELECT
2240                `doc_list_pos`.`tovar` AS `pos_id`, `doc_list_pos`.`cnt`, `doc_list_pos`.`cost` AS `price`,
2241                `doc_base`.`vc`, `doc_base`.`name`, `doc_base`.`proizv` AS `vendor`, `doc_base`.`mass`, `doc_base`.`mult`,
2242                `doc_group`.`printname` AS `group_printname`, `doc_group`.`id` AS `group_id`,
2243                `doc_base_cnt`.`mesto` AS `place`, `doc_base_cnt`.`cnt` AS `base_cnt`,
2244                `class_unit`.`rus_name1` AS `unit_name`, `class_unit`.`number_code` AS `unit_code`
2245                $fields_sql
2246            FROM `doc_list_pos`
2247            INNER JOIN `doc_base` ON `doc_list_pos`.`tovar`=`doc_base`.`id`
2248            LEFT JOIN `doc_group` ON `doc_group`.`id`=`doc_base`.`group`
2249            LEFT JOIN `doc_base_cnt` ON `doc_base_cnt`.`id`=`doc_list_pos`.`tovar` AND `doc_base_cnt`.`sklad`='{$this->doc_data['sklad']}'
2250            LEFT JOIN `class_unit` ON `doc_base`.`unit`=`class_unit`.`id`
2251            $join_sql
2252            WHERE `doc_list_pos`.`doc`='{$this->id}'
2253            ORDER BY `doc_list_pos`.`id`");
2254
2255        while ($line = $res->fetch_assoc()) {
2256            if ($line['group_printname']) {
2257                $line['name'] = $line['group_printname'] . ' ' . $line['name'];
2258            }
2259            if (!@$CONFIG['doc']['no_print_vendor'] && $line['vendor']) {
2260                $line['name'] .= ' / ' . $line['vendor'];
2261            }
2262            $line['code'] = $line['pos_id'];
2263            if ($line['vc']) {
2264                $line['code'] .= ' / ' . $line['vc'];
2265            }
2266            $line['sum'] = $line['price'] * $line['cnt'];
2267
2268            if (isset($opts['vat'])) {
2269                if($this->firm_vars['param_nds']) {
2270                    if($line['vat']===null) {
2271                        $line['vat'] = 0;
2272                    }                       
2273                    $ndsp = $line['vat'];
2274                    $vat = $ndsp / 100;
2275                }
2276                else {
2277                    $ndsp = $vat = 0;
2278                }
2279
2280                $pos = $this->calcVAT($line['price'], $line['cnt'], $vat);
2281                $line['price_wo_vat'] = $pos['price'];
2282                $line['sum_wo_vat'] = round($pos['sum_wo_vat'], 2);
2283                $line['vat_p'] = $ndsp;
2284                $line['vat_s'] = round($pos['vat_s'], 2);
2285                $line['sum'] = round($pos['sum'], 2);
2286               
2287            }
2288
2289
2290            $list[] = $line;
2291        }
2292        $res->free();
2293        return $list;
2294    }
2295
2296    /// Получить список номенклатуры документа с НДС и НТД
2297    public function getDocumentNomenclatureWVATandNums() {
2298        global $CONFIG, $db;
2299
2300        $list = array();
2301        $res = $db->query("SELECT `doc_group`.`printname` AS `group_printname`, `doc_base`.`name`, `doc_base`.`proizv` AS `vendor`, `doc_list_pos`.`cnt`,
2302            `doc_list_pos`.`cost`, `doc_list_pos`.`gtd`, `class_country`.`name` AS `country_name`, `doc_base_dop`.`ntd`,
2303            `class_unit`.`rus_name1` AS `unit_name`, `doc_list_pos`.`tovar` AS `pos_id`, `class_unit`.`number_code` AS `unit_code`,
2304            `class_country`.`number_code` AS `country_code`, `doc_base`.`vc`, `doc_base`.`mass`, `doc_base`.`nds` AS `vat`, `doc_list_pos`.`comm`,
2305            `doc_list_pos`.`id` AS `line_id`
2306        FROM `doc_list_pos`
2307        LEFT JOIN `doc_base` ON `doc_base`.`id`=`doc_list_pos`.`tovar`
2308        LEFT JOIN `doc_base_dop` ON `doc_base_dop`.`id`=`doc_list_pos`.`tovar`
2309        LEFT JOIN `doc_group` ON `doc_group`.`id`=`doc_base`.`group`
2310        LEFT JOIN `class_unit` ON `doc_base`.`unit`=`class_unit`.`id`
2311        LEFT JOIN `class_country` ON `class_country`.`id`=`doc_base`.`country`
2312        WHERE `doc_list_pos`.`doc`='{$this->id}'
2313        ORDER BY `doc_list_pos`.`id`");
2314
2315        while ($nxt = $res->fetch_assoc()) {
2316            if($this->firm_vars['param_nds']) {
2317                if($nxt['vat']===null) {
2318                    $nxt['vat'] = 0;
2319                }                       
2320                $ndsp = $nxt['vat'];
2321                $vat = $ndsp / 100;
2322            }
2323            else {
2324                $ndsp = $vat = 0;
2325            }
2326
2327            if (!$nxt['country_code']) {
2328                //throw new \Exception("Не возможно формирование списка номенклатуры без указания страны происхождения товара");
2329                $nxt['country_code'] = 0;
2330            }
2331
2332            $pos_name = $nxt['name'];
2333            if ($nxt['group_printname']) {
2334                $pos_name = $nxt['group_printname'] . ' ' . $pos_name;
2335            }
2336            if (!@$CONFIG['doc']['no_print_vendor'] && $nxt['vendor']) {
2337                $pos_name .= ' / ' . $nxt['vendor'];
2338            }
2339            $pos_code = $nxt['pos_id'];
2340            if ($nxt['vc']) {
2341                $pos_code .= ' / ' . $nxt['vc'];
2342            }
2343
2344            if (@$CONFIG['poseditor']['true_gtd']) {
2345                $gtd_array = array();
2346                $gres = $db->query("SELECT `doc_list`.`type`, `doc_list_pos`.`gtd`, `doc_list_pos`.`cnt`, `doc_list`.`id` FROM `doc_list_pos`
2347                    INNER JOIN `doc_list` ON `doc_list`.`id`=`doc_list_pos`.`doc`
2348                    WHERE `doc_list_pos`.`tovar`='{$nxt['pos_id']}' AND `doc_list`.`firm_id`='{$this->doc_data['firm_id']}' AND `doc_list`.`type`<='2'
2349                    AND `doc_list`.`date`<'{$this->doc_data['date']}' AND `doc_list`.`ok`>'0'
2350                    ORDER BY `doc_list`.`date`");
2351                while ($line = $gres->fetch_assoc()) {
2352                    if ($line['type'] == 1) { // Поступление
2353                        $gtd_array[] = array('num' => $line['gtd'], 'cnt' => $line['cnt']);
2354                    } else {
2355                        $cnt = $line['cnt'];
2356                        while ($cnt > 0) {
2357                            if (count($gtd_array) == 0) {
2358                                if (\cfg::get('poseditor', 'true_gtd') != 'easy') {
2359                                    throw new \Exception("Не найдены поступления для $cnt единиц товара {$nxt['name']} (для реализации N{$line['id']} в прошлом). Товар был оприходован на другую организацию?");
2360                                } else {
2361                                    $gtd_array[] = array('num' => $line['gtd'], 'cnt' => $cnt);
2362                                }
2363                            }
2364                            if ($gtd_array[0]['cnt'] == $cnt) {
2365                                array_shift($gtd_array);
2366                                $cnt = 0;
2367                            } elseif ($gtd_array[0]['cnt'] > $cnt) {
2368                                $gtd_array[0]['cnt'] -= $cnt;
2369                                $cnt = 0;
2370                            } else {
2371                                $cnt -= $gtd_array[0]['cnt'];
2372                                array_shift($gtd_array);
2373                            }
2374                        }
2375                    }
2376                }
2377
2378                $unigtd = array();
2379                $need_cnt = $nxt['cnt'];
2380                while ($need_cnt > 0 && count($gtd_array) > 0) {
2381                    $gtd_num = $gtd_array[0]['num'];
2382                    $gtd_cnt = $gtd_array[0]['cnt'];
2383                    if ($gtd_cnt >= $need_cnt) {
2384                        if (isset($unigtd[$gtd_num])) {
2385                            $unigtd[$gtd_num] += $need_cnt;
2386                        } else {
2387                            $unigtd[$gtd_num] = $need_cnt;
2388                        }
2389                        $need_cnt = 0;
2390                    } else {
2391                        if (isset($unigtd[$gtd_num])) {
2392                            $unigtd[$gtd_num] += $gtd_cnt;
2393                        } else {
2394                            $unigtd[$gtd_num] = $gtd_cnt;
2395                        }
2396                        $need_cnt -= $gtd_cnt;
2397                        array_shift($gtd_array);
2398                    }
2399                }
2400                if ($need_cnt > 0) {
2401                    if (\cfg::get('poseditor', 'true_gtd') != 'easy') {
2402                        throw new Exception("Не найдены поступления для $need_cnt единиц товара {$pos_name}. Товар был оприходован на другую организацию?");
2403                    } else {
2404                        $unigtd['   --   '] = $need_cnt;
2405                    }
2406                }
2407                foreach ($unigtd as $gtd => $cnt) {
2408                    $pos = $this->calcVAT($nxt['cost'], $cnt, $vat);
2409                    $list[] = array(
2410                        'line_id' => $nxt['line_id'],
2411                        'pos_id' => $nxt['pos_id'],
2412                        'code' => $pos_code,
2413                        'name' => $pos_name,
2414                        'unit_code' => $nxt['unit_code'],
2415                        'unit_name' => $nxt['unit_name'],
2416                        'cnt' => $cnt,
2417                        'price' => $pos['price'],
2418                        'orig_price' => $nxt['cost'],
2419                        'sum_wo_vat' => round($pos['sum_wo_vat'], 2),
2420                        'excise' => 'без акциза',
2421                        'vat_p' => $ndsp,
2422                        'vat_s' => round($pos['vat_s'], 2),
2423                        'sum' => round($pos['sum'], 2),
2424                        'country_code' => $nxt['country_code'],
2425                        'country_name' => $nxt['country_name'],
2426                        'gtd' => $gtd,
2427                        'mass' => $nxt['mass'],
2428                        'comm' => $nxt['comm'],
2429                    );
2430                }
2431            } else {
2432                $pos = $this->calcVAT($nxt['cost'], $nxt['cnt'], $vat);
2433                $list[] = array(
2434                    'line_id' => $nxt['line_id'],
2435                    'pos_id' => $nxt['pos_id'],
2436                    'code' => $pos_code,
2437                    'name' => $pos_name,
2438                    'unit_code' => $nxt['unit_code'],
2439                    'unit_name' => $nxt['unit_name'],
2440                    'cnt' => $nxt['cnt'],
2441                    'price' => $pos['price'],
2442                    'orig_price' => $nxt['cost'],
2443                    'sum_wo_vat' => round($pos['sum_wo_vat'], 2),
2444                    'excise' => 'без акциза',
2445                    'vat_p' => $ndsp,
2446                    'vat_s' => round($pos['vat_s'], 2),
2447                    'sum' => round($pos['sum'], 2),
2448                    'country_code' => $nxt['country_code'],
2449                    'country_name' => $nxt['country_name'],
2450                    'gtd' => $nxt['ntd'],
2451                    'mass' => $nxt['mass'],
2452                    'comm' => $nxt['comm'],
2453                );
2454            }
2455        }
2456        return $list;
2457    }
2458
2459    /// Расчет НДС для строки документа
2460    /// @param $doc_price Цена единицы товара в документе
2461    /// @param $count Количество товара
2462    /// @param $vat Ставка НДС
2463    protected function calcVAT($doc_price, $count, $vat) {
2464        global $CONFIG;
2465        if (isset($CONFIG['poseditor']['vat_scheme'])) {
2466            $scheme = $CONFIG['poseditor']['vat_scheme'];
2467        } else {
2468            $scheme = 'correct';
2469        }
2470        if ($this->doc_data['nds']) {   // НДС включен
2471            $pos['sum'] = $doc_price * $count;
2472            if ($scheme == '1c') {
2473                $pos['sum_wo_vat'] = round($pos['sum'] / (1 + $vat), 2);
2474                $pos['vat_s'] = $pos['sum'] - $pos['sum_wo_vat'];
2475                $pos['price'] = round($pos['sum_wo_vat'] / $count, 2);
2476            } else {
2477                $pos['price'] = round($doc_price / (1 + $vat), 2);
2478                $pos['sum_wo_vat'] = round($pos['price'] * $count, 2);
2479                $pos['vat_s'] = round($doc_price * $count, 2) - $pos['sum_wo_vat'];
2480            }
2481        } else {
2482            $pos['price'] = $pos['price_w_vat'] = $doc_price;
2483            $pos['sum_wo_vat'] = round($pos['price'] * $count, 2);
2484            $pos['vat_s'] = round($pos['sum_wo_vat'] * $vat, 2);
2485            $pos['sum'] = $pos['sum_wo_vat'] + $pos['vat_s'];
2486        }
2487        return $pos;
2488    }
2489
2490    /// Установить пометку на удаление у документа
2491    protected function serviceDelDoc() {
2492        global $db;
2493        try {
2494            \acl::accessGuard('doc.' . $this->typename, \acl::DELETE);
2495            if ($this->doc_data['firm_id'] > 0) {
2496                \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::DELETE);
2497            }
2498            $tim = time();
2499
2500            $res = $db->query("SELECT `id` FROM `doc_list` WHERE `p_doc`='{$this->id}' AND `mark_del`='0'");
2501            if ($res->num_rows) {
2502                throw new Exception("Есть подчинённые не удалённые документы. Удаление невозможно.");
2503            }
2504            $db->update('doc_list', $this->id, 'mark_del', $tim);
2505            doc_log("MARKDELETE", '', "doc", $this->id);
2506            $this->doc_data['mark_del'] = $tim;
2507            $json = ' { "response": "1", "message": "Пометка на удаление установлена!", "buttons": "' . $this->getApplyButtons() . '", '
2508                . '"statusblock": "Документ помечен на удаление" }';
2509            return $json;
2510        } catch (Exception $e) {
2511            return "{response: 0, message: '" . $e->getMessage() . "'}";
2512        }
2513    }
2514
2515    /// Снять пометку на удаление у документа
2516    protected function serviceUnDelDoc() {
2517        global $db;
2518        try {
2519            \acl::accessGuard('doc.' . $this->typename, \acl::DELETE);
2520            if ($this->doc_data['firm_id'] > 0) {
2521                \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::DELETE);
2522            }
2523            $db->update('doc_list', $this->id, 'mark_del', 0);
2524            doc_log("UNMARKDELETE", '', "doc", $this->id);
2525            $json = ' { "response": "1", "message": "Пометка на удаление снята!", "buttons": "' . $this->getApplyButtons() . '", '
2526                . '"statusblock": "Документ не будет удалён" }';
2527            return $json;
2528        } catch (Exception $e) {
2529            return "{response: 0, message: '" . $e->getMessage() . "'}";
2530        }
2531    }
2532
2533
2534    /// @brief Создание другого документа на основе текущего
2535    /// Метод необходимо переопределить у потомков
2536    /// @param $target_type Тип создаваемого документа
2537    /// @return Всегда false
2538        /// Формирование другого документа на основании текущего
2539    function morphTo($target) {
2540        global $tmpl, $db;
2541        $morphs = $this->getMorphList();
2542       
2543        if ($target == '') {
2544            $tmpl->ajax = 1;           
2545            $base_link = "window.location='/doc.php?mode=morphto&amp;doc={$this->id}&amp;tt=";
2546            foreach($morphs as $line) {
2547                $acl_obj = 'doc.'.$line['document'];
2548                if(\acl::testAccess($acl_obj, \acl::CREATE)) {
2549                    $tmpl->addContent("<div onclick=\"{$base_link}{$line['name']}'\">{$line['viewname']}</div>");
2550                }
2551            }
2552        } else {
2553            $morphs = $this->getMorphList();
2554            $info = null;
2555            foreach($morphs as $m_info) {
2556                if($m_info['name']===$target) {
2557                    $info = $m_info;
2558                    break;
2559                }
2560            }
2561            if(!$info) {
2562                throw new \Exception("Неверный код целевого документа.");
2563            }
2564           
2565            \acl::accessGuard('doc.'.$morphs[$target]['document'], \acl::CREATE);
2566            $method = 'morphTo_'.$info['name'];
2567            if(!method_exists($this, $method)) {
2568                throw new \NotFoundException("Метод морфинга не определён.");
2569            } 
2570            $db->startTransaction();
2571            $new_doc = $this->$method($target);
2572            $new_doc_id = $new_doc->getId();
2573            $db->commit();
2574            redirect("/doc.php?mode=body&doc=$new_doc_id");
2575        }
2576    }
2577
2578    /**
2579     * Проверка для приходных/расходных кассовых ордеров
2580     * и средств из/в банк при проведении документа
2581     * @throws Exception При отсутствии
2582     */
2583    protected function checkIfTypeForDocumentExists() {
2584        $allowedTypes = [
2585            4 => 'credit_type',
2586            5 => 'rasxodi',
2587            6 => 'credit_type',
2588            7 => 'rasxodi',
2589        ];
2590        if (!isset($allowedTypes[$this->doc_type])) {
2591            throw new \Exception('Для данного типа документа проверка не разрешена');
2592        }
2593        if (cfg::get('doc', 'restrict_dc_nulltype', true) && isset($this->dop_data[$allowedTypes[$this->doc_type]]) && $this->dop_data[$allowedTypes[$this->doc_type]] == 0) {
2594            $type = $this->doc_type % 2 === 1 ? 'расхода' : 'дохода';
2595            throw new \Exception("Не задан вид $type у проводимого документа.");
2596        }
2597    }
2598
2599}
Note: See TracBrowser for help on using the repository browser.