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

source: web/include/doc.nulltype.php @ 07ea77e

Last change on this file since 07ea77e was 07ea77e, checked in by BlackLight <blacklight@…>, 2 years ago

Добавлено управление привилегиями для печатных форм

  • Property mode set to 100644
File size: 119.8 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 = array();
958        if (isset($this->PDFForms)) {
959            if (is_array($this->PDFForms)) {
960                foreach ($this->PDFForms as $form) {
961                    $ret[] = array('name' => 'int:' . $form['name'], 'desc' => $form['desc'], 'mime' => '');
962                }
963            }
964        }
965        $dir = $CONFIG['site']['location'] . '/include/doc/printforms/' . $this->typename . '/';
966        if (is_dir($dir)) {
967            $dh = opendir($dir);
968            if ($dh) {
969                while (($file = readdir($dh)) !== false) {
970                    if (preg_match('/.php$/', $file)) {
971                        $cn = explode('.', $file);
972                        $class_name = '\\doc\\printforms\\' . $this->typename . '\\' . $cn[0];
973                        $class = new $class_name;
974                        $nm = $class->getName();
975                        $mime = $class->getMimeType();
976                        if (\acl::testAccess("doc.{$this->typename}.{$cn[0]}", $aclFlag)) {
977                            $ret[] = array('name' => 'ext:' . $cn[0], 'desc' => $nm, 'mime' => $mime);
978                        }
979                    }
980                }
981                closedir($dh);
982            }
983        }
984        usort($ret, array(get_class(), 'sortDescriptionCallback'));
985        return $ret;
986    }
987
988    /// Получить список доступных печатных форм c CSV экспортом
989    /// @return Массив со списком печатных форм
990    public function getCSVPrintFormList() {
991        $ret = $this->getPrintFormList();
992        if ($this->sklad_editor_enable) {
993            $ret[] = array('name' => 'csv:export', 'desc' => 'Экспорт в CSV', 'mime' => 'text/csv');
994        }
995        return $ret;
996    }
997
998    /// Проверить, существует ли печатная форма с заданным названием
999    /// @return true, если существует, false в ином случае
1000    protected function isPrintFormExists($form_name) {
1001        $forms = $this->getCSVPrintFormList();
1002        $found = false;
1003        foreach ($forms as $form) {
1004            if ($form['name'] == $form_name) {
1005                $found = true;
1006                break;
1007            }
1008        }
1009        return $found;
1010    }
1011
1012    /// Получить mime тип формы
1013    /// @return тип, если форма существует, false в ином случае
1014    protected function getPrintFormMime($form_name) {
1015        $forms = $this->getCSVPrintFormList();
1016        $found = false;
1017        foreach ($forms as $form) {
1018            if ($form['name'] == $form_name) {
1019                $found = $form['mime'];
1020                break;
1021            }
1022        }
1023        return $found;
1024    }
1025
1026    /// Получить отображаемое наименование формы
1027    /// @return Название формы, если существует, null в ином случае
1028    protected function getPrintFormViewName($form_name) {
1029        $forms = $this->getPrintFormList();
1030        foreach ($forms as $form) {
1031            if ($form['name'] == $form_name) {
1032                return $form['desc'];
1033            }
1034        }
1035        return null;
1036    }
1037
1038    /**
1039     * Сформировать печатную форму
1040     * @param string $form_name Имя печатной формы
1041     * @param bool $to_str      Вернуть ли данные в виде строки
1042     * @return string Если $to_str == true - возвращает сформированный документ, false в ином случае
1043     * @throws AccessException
1044     * @throws NotFoundException
1045     */
1046    protected function makePrintForm($form_name, $to_str = false) {
1047        $aclFlag = \acl::GET_PRINTDRAFT;
1048        if ($this->doc_data['ok']) {
1049            $aclFlag = \acl::GET_PRINTFORM;
1050        }
1051        list(,$form_acl) = explode(':', $form_name);
1052        \acl::accessGuard("doc.{$this->typename}.$form_acl", $aclFlag);
1053        \acl::accessGuard([ 'firm.global', "firm.{$this->doc_data['firm_id']}" ], $aclFlag);
1054        return $this->makePrintFormNoACLTest($form_name, $to_str);
1055    }
1056
1057    /**
1058     * Сформировать печатную форму, не проверяя привилегии
1059     * @param string $form_name Имя печатной формы
1060     * @param bool $to_str Вернуть ли данные в виде строки
1061     * @return string Если $to_str == true - возвращает сформированный документ, false в ином случае
1062     * @throws NotFoundException
1063     */
1064    public function makePrintFormNoACLTest($form_name, $to_str = false) {
1065        if (!$this->isPrintFormExists($form_name)) {
1066            throw new \NotFoundException('Печатная форма ' . html_out($form_name) . ' не зарегистрирована');
1067        }
1068        $f_param = explode(':', $form_name);
1069        if ($f_param[0] == 'int') {
1070            $method = '';
1071            foreach ($this->PDFForms as $form) {
1072                if ($form['name'] == $f_param[1]) {
1073                    $method = $form['method'];
1074                }
1075            }
1076            return $this->$method($to_str);
1077        } elseif ($f_param[0] == 'ext') {
1078            $class_name = '\\doc\\printforms\\' . $this->typename . '\\' . $f_param[1];
1079            $print_obj = new $class_name;
1080            $print_obj->setDocument($this);
1081            $print_obj->initForm();
1082            $print_obj->make();
1083            return $print_obj->outData($to_str);
1084        } elseif ($f_param[0] == 'csv') {
1085            return $this->CSVExport($to_str);
1086        } else {
1087            throw new \NotFoundException('Неверный тип печатной формы');
1088        }
1089    }
1090
1091    /// Отправка документа по факсу
1092    /// @param $form_name   Имя печатной формы
1093    final function sendFax($form_name = '') {
1094        global $tmpl, $db;
1095        $tmpl->ajax = 1;
1096        try {
1097            if ($form_name == '') {
1098                $agent = new \models\agent($this->doc_data['agent']);
1099                $ret_data = array(
1100                    'response' => 'item_list',
1101                    'faxnum' => $agent->getFaxNum(),
1102                    'content' => $this->getPrintFormList()
1103                );
1104                $tmpl->setContent(json_encode($ret_data, JSON_UNESCAPED_UNICODE));
1105            } else {
1106                $faxnum = request('faxnum');
1107                if ($faxnum == '') {
1108                    throw new \Exception('Номер факса не указан');
1109                }
1110                if (!preg_match('/^\+\d{8,15}$/', $faxnum)) {
1111                    throw new \Exception("Номер факса $faxnum указан в недопустимом формате");
1112                }
1113                include_once('sendfax.php');
1114                $data = $this->makePrintForm($form_name, true);
1115                $fs = new FaxSender();
1116                $fs->setFileBuf($data);
1117                $fs->setFaxNumber($faxnum);
1118
1119                $res = $db->query("SELECT `worker_email` FROM `users_worker_info` WHERE `user_id`='{$_SESSION['uid']}'");
1120                if ($res->num_rows) {
1121                    list($email) = $res->fetch_row();
1122                    $fs->setNotifyMail($email);
1123                }
1124                $res = $fs->send();
1125                $tmpl->setContent("{'response': 'send'}");
1126                doc_log("Send FAX", $faxnum, 'doc', $this->id);
1127            }
1128        } catch (Exception $e) {
1129            $tmpl->setContent("{response: 'err', text: '" . $e->getMessage() . "'}");
1130        }
1131    }
1132   
1133    /** Отправка документа по факсу на указанный номер
1134     *
1135     * @param $form_name Имя формы отправляемого документа
1136     * @param $faxnum Номер факса получателя
1137     */
1138    final function sendFaxTo($form_name, $faxnum) {
1139        global $db;
1140        if ($faxnum == '') {
1141            throw new \Exception('Номер факса не указан');
1142        }
1143        if (!preg_match('/^\+\d{8,15}$/', $faxnum)) {
1144            throw new \Exception("Номер факса $faxnum указан в недопустимом формате");
1145        }
1146        include_once('sendfax.php');
1147        $data = $this->makePrintFormNoACLTest($form_name, true);
1148        $fs = new \FaxSender();
1149        $fs->setFileBuf($data);
1150        $fs->setFaxNumber($faxnum);
1151
1152        $res = $db->query("SELECT `worker_email` FROM `users_worker_info` WHERE `user_id`='{$_SESSION['uid']}'");
1153        if ($res->num_rows) {
1154            list($email) = $res->fetch_row();
1155            $fs->setNotifyMail($email);
1156        }
1157        $res = $fs->send();       
1158        doc_log("Send FAX", $faxnum, 'doc', $this->id);
1159        return true;
1160    }
1161   
1162    function getExtensionFromMIME($mime) {
1163        switch ($mime) {
1164            case 'text/csv':
1165                return '.csv';
1166            case 'application/vnd.ms-excel':
1167                return '.xls';
1168            case 'application/vnd.oasis.opendocument.spreadsheet':
1169                return '.ods';
1170            case 'application/pdf':
1171            default:
1172                return '.pdf';
1173        }
1174    }
1175
1176    /// Отправка документа по электронной почте
1177    /// @param $form_name   Имя печатной формы
1178    final function sendEMail($form_name = '') {
1179        global $tmpl, $db;
1180        $tmpl->ajax = 1;
1181        try {
1182            if ($form_name == '') {
1183                $agent = new \models\agent($this->doc_data['agent']);
1184                $ret_data = array(
1185                    'response' => 'item_list',
1186                    'email' => $agent->getEmail(),
1187                    'content' => $this->getCSVPrintFormList()
1188                );
1189                $tmpl->setContent(json_encode($ret_data, JSON_UNESCAPED_UNICODE));
1190            } else {
1191                $email = request('email');
1192                $comment = request('comment');
1193                if ($email == '') {
1194                    throw new \Exception('Адрес электронной почты не указан!');
1195                } else {
1196                    $data = $this->makePrintForm($form_name, true);
1197                    $mime = $this->getPrintFormMime($form_name);
1198                    $extension = $this->getExtensionFromMIME($mime);
1199
1200                    $fname = $this->typename . '_' . str_replace(":", "_", $form_name) . $extension;
1201                    $viewname = $this->getPrintFormViewName($form_name) . ' (' . $this->viewname . ')';
1202                    $this->sendDocByEMail($email, $comment, $viewname, $data, $fname);
1203                    $tmpl->setContent("{'response': 'send'}");
1204                    doc_log("Send email", $email, 'doc', $this->id);
1205                }
1206            }
1207        } catch (Exception $e) {
1208            $tmpl->setContent("{'response':'err','text':'" . $e->getMessage() . "'}");
1209        }
1210    }
1211   
1212    /** Отправка документа по электронной почте
1213     *
1214     * @param $form_name Имя печатной формы
1215     * @param $email Адрес электронной почты
1216     * @param string $text Текст сообщения электронной почты
1217     */
1218    final function sendEmailTo($form_name, $email, $text='') {
1219        if ($email == '') {
1220            throw new \Exception('Адрес электронной почты не указан!');
1221        }
1222        $data = $this->makePrintFormNoACLTest($form_name, true);
1223        $mime = $this->getPrintFormMime($form_name);
1224        $extension = $this->getExtensionFromMIME($mime);
1225
1226        $fname = $this->typename . '_' . str_replace(":", "_", $form_name) . $extension;
1227        $viewname = $this->getPrintFormViewName($form_name) . ' (' . $this->viewname . ')';
1228        $this->sendDocByEMail($email, $text, $viewname, $data, $fname);
1229        doc_log("Send email", $email, 'doc', $this->id);
1230        return true;
1231    }
1232
1233    /// Печать документа
1234    /// @param $form_name   Имя печатной формы
1235    /// @param $user_print  Если истина - документ запрошен из пользовательского раздела
1236    function printForm($form_name = '') {
1237        global $tmpl;
1238        $tmpl->ajax = 1;
1239        if ($form_name == '') {
1240            $ret_data = array(
1241                'response' => 'item_list',
1242                'content' => $this->getCSVPrintFormList()
1243            );
1244            $tmpl->setContent(json_encode($ret_data, JSON_UNESCAPED_UNICODE));
1245        } else {
1246            $this->makePrintForm($form_name);
1247            $this->sentZEvent('print');
1248            doc_log("PRINT", $form_name, 'doc', $this->id);
1249        }
1250    }
1251
1252    /// Печать документа посетителем сайта / не сотрудником
1253    /// @param $form_name   Имя печатной формы
1254    /// @param $user_print  Если истина - документ запрошен из пользовательского раздела
1255    function printFormFromCabinet($form_name) {
1256        global $tmpl;
1257        $tmpl->ajax = 1;
1258        if ($form_name == '') {
1259            throw new \NotFoundException('Печатная форма не выбрана');
1260        } else {
1261            $this->makePrintFormNoACLTest($form_name);
1262            $this->sentZEvent('userprint');
1263            doc_log("USERPRINT", $form_name, 'doc', $this->id);
1264        }
1265    }
1266
1267    /// Выполнить удаление документа. Если есть зависимости - удаление не производится.
1268    function delExec() {
1269        global $db;
1270        if ($this->doc_data['ok']) {
1271            throw new \Exception("Нельзя удалить проведённый документ");
1272        }
1273        $res = $db->query("SELECT `id`, `mark_del` FROM `doc_list` WHERE `p_doc`='{$this->id}'");
1274        if ($res->num_rows) {
1275            throw new \Exception("Нельзя удалить документ с неудалёнными потомками");
1276        }
1277        $db->query("DELETE FROM `doc_list_pos` WHERE `doc`='{$this->id}'");
1278        $db->query("DELETE FROM `doc_dopdata` WHERE `doc`='{$this->id}'");
1279        $db->query("DELETE FROM `doc_list` WHERE `id`='{$this->id}'");
1280    }
1281
1282    /// Сделать документ потомком указанного документа и вернуть резутьтат в json формате
1283    function connectJson($p_doc) {
1284        try {
1285            \acl::accessGuard('doc.' . $this->typename, \acl::UPDATE);
1286            if ($this->doc_data['firm_id'] > 0) {
1287                \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::UPDATE);
1288            }
1289            $this->subordinate($p_doc);
1290            return " { \"response\": \"connect_ok\" }";
1291        } catch (Exception $e) {
1292            return " { \"response\": \"error\", \"message\": \"" . $e->getMessage() . "\" }";
1293        }
1294    }
1295
1296    /// отправка документа по электронной почте
1297    function sendDocByEMail($email, $comment, $docname, $data, $filename, $body = '') {
1298        global $CONFIG, $db;
1299        $pref = \pref::getInstance();
1300        $res_autor = $db->query("SELECT `worker_real_name`, `worker_phone`, `worker_email` FROM `users_worker_info`
1301            WHERE `user_id`='" . $this->doc_data['user'] . "'");
1302        $doc_autor = $res_autor->fetch_assoc();
1303        $agent = new \models\agent($this->doc_data['agent']);
1304
1305        $email_message = new \email_message();
1306        $email_message->default_charset = "UTF-8";
1307        if ($agent->fullname) {
1308            $email_message->SetEncodedEmailHeader("To", $email, $agent->fullname);
1309        } else if ($agent->name) {
1310            $email_message->SetEncodedEmailHeader("To", $email, $agent->name);
1311        } else {
1312            $email_message->SetEncodedEmailHeader("To", $email, $email);
1313        }
1314
1315        $email_message->SetEncodedHeader("Subject", "{$pref->site_display_name} - $docname ({$pref->site_name})");
1316
1317        if (!@$doc_autor['worker_email']) {
1318            $email_message->SetEncodedEmailHeader("From", $pref->site_email, "Почтовый робот {$pref->site_name}");
1319            $email_message->SetHeader("Sender", $pref->site_email);
1320            $text_message = "Здравствуйте, {$agent->fullname}!\n"
1321                . "Во вложении находится заказанный Вами документ ($docname) от {$pref->site_display_name} ({$pref->site_name})\n\n"
1322                . "$comment\n\n"
1323                . "Сообщение сгенерировано автоматически, отвечать на него не нужно!\n"
1324                . "Для переписки используйте адрес, указанный в контактной информации на сайте http://{$pref->site_name}!";
1325        } else {
1326            $email_message->SetEncodedEmailHeader("From", $doc_autor['worker_email'], $doc_autor['worker_real_name']);
1327            $email_message->SetHeader("Sender", $doc_autor['worker_email']);
1328            $text_message = "Здравствуйте, {$agent->fullname}!\n"
1329                . "Во вложении находится заказанный Вами документ ($docname) от {$pref->site_name}\n\n$comment\n\n"
1330                . "Ответственный сотрудник: {$doc_autor['worker_real_name']}\n"
1331                . "Контактный телефон: {$doc_autor['worker_phone']}\n"
1332                . "Электронная почта (e-mail): {$doc_autor['worker_email']}\n"
1333                . "Отправитель: {$_SESSION['name']}";
1334        }
1335        if ($body) {
1336            $email_message->AddQuotedPrintableTextPart($body);
1337        } else {
1338            $email_message->AddQuotedPrintableTextPart($text_message);
1339        }
1340
1341        $text_attachment = array(
1342            "Data" => $data,
1343            "Name" => $filename,
1344            "Content-Type" => "automatic/name",
1345            "Disposition" => "attachment"
1346        );
1347        $email_message->AddFilePart($text_attachment);
1348
1349        $error = $email_message->Send();
1350
1351        if (strcmp($error, "")) {
1352            throw new \Exception($error);
1353        } else {
1354            return 0;
1355        }
1356    }
1357
1358    /// Обработка отправки запроса на отмену документа
1359    protected function sendPetition() {
1360        global $db;
1361        $ret = array('object' => 'send_petition', 'response' => 'success');
1362        try {
1363            $text = request('text');
1364            $pref = pref::getInstance();
1365            if (mb_strlen($text) < 8) {
1366                throw new Exception('Сообщение слишком короткое! Опишите причину подробнее!');
1367            }
1368            $res = $db->query("SELECT `users`.`reg_email`, `users_worker_info`.`worker_email` FROM `users`
1369                LEFT JOIN `users_worker_info` ON `users_worker_info`.`user_id`=`users`.`id`
1370                WHERE `id`='{$_SESSION['uid']}'");
1371            $user_info = $res->fetch_array();
1372            if ($user_info['worker_email'] != '') {
1373                $from = $user_info['worker_email'];
1374            } else if ($user_info['reg_email'] != '') {
1375                $from = $user_info['reg_email'];
1376            } else {
1377                $from = \cfg::get('site', 'doc_adm_email');
1378            }
1379
1380            $proto = @$_SERVER['HTTPS'] ? 'https' : 'http';
1381            $ip = getenv("REMOTE_ADDR");
1382            $date = date("Y-m-d H:i:s", $this->doc_data['date']);
1383            $txt = "Здравствуйте!\nПользователь {$_SESSION['name']} просит Вас отменить проводку документа *{$this->viewname}* с ID: {$this->id},"
1384                . " {$this->doc_data['altnum']}{$this->doc_data['subtype']} от {$date} на сумму {$this->doc_data['sum']}."
1385                . " Клиент {$this->doc_data['agent_name']}.\n{$proto}://{$_SERVER["HTTP_HOST"]}/doc.php?mode=body&doc={$this->id} \n"
1386                . "Цель отмены: $text.\n"
1387                . "IP: $ip\n"
1388                . "Пожалуйста, дайте ответ на это письмо на $from, как в случае отмены документа, так и об отказе отмены!";
1389
1390            if (\cfg::get('site', 'doc_adm_email')) {
1391                mailto(\cfg::get('site', 'doc_adm_email'), 'Запрос на отмену проведения документа', $txt, $from);
1392            }
1393
1394            if (\cfg::get('site', 'doc_adm_jid') && \cfg::get('xmpp', 'host')) {
1395                require_once(\cfg::getroot('location') . '/common/XMPPHP/XMPP.php');
1396                $xmppclient = new \XMPPHP\XMPP(\cfg::get('xmpp', 'host'), \cfg::get('xmpp', 'port'), \cfg::get('xmpp', 'login'), \cfg::get('xmpp', 'pass')
1397                    , 'MultiMag r' . MULTIMAG_REV);
1398                $xmppclient->connect();
1399                $xmppclient->processUntil('session_start');
1400                $xmppclient->presence();
1401                $xmppclient->message(\cfg::get('site', 'doc_adm_jid'), $txt);
1402                $xmppclient->disconnect();
1403            }
1404            $ret['message'] = "Сообщение было отправлено уполномоченному лицу! Ответ о снятии проводки придёт вам на e-mail!";
1405        } catch (\XMPPHP\Exception $e) {
1406            writeLogException($e);
1407            $ret = array('object' => 'send_petition', 'response' => 'error',
1408                'errormessage' => "Невозможно отправить сообщение по XMPP: " . $e->getMessage()
1409            );
1410        } catch (\Exception $e) {
1411            $ret = array('object' => 'send_petition', 'response' => 'error', 'errormessage' => $e->getMessage());
1412        }
1413        return json_encode($ret, JSON_UNESCAPED_UNICODE);
1414    }
1415
1416    function service() {
1417        global $tmpl;
1418        $tmpl->ajax = 1;
1419        $opt = request('opt');
1420        $pos = rcvint('pos');
1421        $this->_service($opt, $pos);
1422    }
1423
1424    /// Служебные опции
1425    function _service($opt, $pos) {
1426        global $tmpl;
1427        $tmpl->ajax = 1;
1428
1429        if ($this->sklad_editor_enable) {
1430            include_once('doc.poseditor.php');
1431            $poseditor = new DocPosEditor($this);
1432            $poseditor->setAllowNegativeCounts($this->allow_neg_cnt);
1433        }
1434
1435        $peopt = request('peopt'); // Опции редактора списка товаров
1436
1437        \acl::accessGuard('doc.' . $this->typename, \acl::VIEW);
1438        if ($this->doc_data['firm_id'] > 0) {
1439            \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::VIEW);
1440        }
1441
1442        switch ($opt) {
1443            case 'link_info':
1444                $ret = $this->getLinkInfo();
1445                $tmpl->setContent(json_encode($ret, JSON_UNESCAPED_UNICODE));
1446                return 1;
1447            case 'petition':
1448                $tmpl->setContent($this->sendPetition());
1449                return 1;
1450            case 'getheader':
1451                $ret = array(
1452                    'response' => 'success',
1453                    'object' => 'getheader',
1454                    'content' => $this->getDocumentHeader(),
1455                );
1456                $tmpl->setContent(json_encode($ret, JSON_UNESCAPED_UNICODE));
1457                return 1;
1458        }
1459
1460        /// Операции, для которых нужен доступ только на чтение
1461        switch ($peopt) {
1462            case 'jget':    // Json-вариант списка товаров
1463                // TODO: пересчет цены перенести внутрь poseditor
1464                $this->recalcSum();
1465                $doc_content = $poseditor->GetAllContent();
1466                $tmpl->addContent($doc_content);
1467                return 1;
1468            case 'jgetgroups':
1469                $doc_content = $poseditor->getGroupList();
1470                $tmpl->addContent($doc_content);
1471                return 1;
1472            case 'jgpi':        // Получение данных наименования
1473                $pos = rcvint('pos');
1474                $tmpl->addContent($poseditor->GetPosInfo($pos));
1475                return 1;
1476            case 'jsklad':      // Получение номенклатуры выбранной группы
1477                $group_id = rcvint('group_id');
1478                $str = "{ response: 'sklad_list', group: '$group_id',  content: [" . $poseditor->GetSkladList($group_id) . "] }";
1479                $tmpl->setContent($str);
1480                return 1;
1481            case 'jsklads':     // Поиск по подстроке по складу
1482                $s = request('s');
1483                $str = "{ response: 'sklad_list', content: " . $poseditor->SearchSkladList($s) . " }";
1484                $tmpl->setContent($str);
1485                return 1;
1486        }
1487
1488        /// TODO: Это тоже переделать!
1489        if ($this->doc_data['ok']) {
1490            throw new \Exception("Операция не допускается для проведённого документа!");
1491        }
1492        switch ($opt) {
1493            case 'jdeldoc':     // Пометка на удаление
1494                $tmpl->setContent($this->serviceDelDoc());
1495                return 1;
1496            case 'jundeldoc':   // Снять пометку на удаление
1497                $tmpl->setContent($this->serviceUnDelDoc());
1498                return 1;
1499            case 'merge':       // Загрузка номенклатурной таблицы
1500                $ret = $this->mergeDocList($poseditor);
1501                $tmpl->setContent(json_encode($ret, JSON_UNESCAPED_UNICODE));
1502                return 1;
1503        }
1504        if ($this->doc_data['mark_del']) {
1505            throw new \Exception("Операция не допускается для документа, отмеченного для удаления!");
1506        }
1507
1508        /// Операции, изменяющие документ       
1509        switch ($peopt) {
1510            case 'jadd':        // Json вариант добавления позиции
1511                \acl::accessGuard('doc.' . $this->typename, \acl::UPDATE);
1512                if ($this->doc_data['firm_id'] > 0) {
1513                    \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::UPDATE);
1514                }
1515                $pe_pos = rcvint('pe_pos');
1516                $tmpl->setContent($poseditor->AddPos($pe_pos));
1517                break;
1518            case 'jdel':        // Json вариант удаления строки
1519                \acl::accessGuard('doc.' . $this->typename, \acl::UPDATE);
1520                if ($this->doc_data['firm_id'] > 0) {
1521                    \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::UPDATE);
1522                }
1523                $line_id = rcvint('line_id');
1524                $tmpl->setContent($poseditor->Removeline($line_id));
1525                break;
1526            case 'jup':     // Json вариант обновления
1527                \acl::accessGuard('doc.' . $this->typename, \acl::UPDATE);
1528                if ($this->doc_data['firm_id'] > 0) {
1529                    \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::UPDATE);
1530                }
1531                $line_id = rcvint('line_id');
1532                $value = request('value');
1533                $type = request('type');
1534                // TODO: пересчет цены перенести внутрь poseditor
1535                $tmpl->setContent($poseditor->UpdateLine($line_id, $type, $value));
1536                break;
1537            case 'jsn':         // Серийные номера
1538                \acl::accessGuard('doc.' . $this->typename, \acl::UPDATE);
1539                if ($this->doc_data['firm_id'] > 0) {
1540                    \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::UPDATE);
1541                }
1542                $action = request('a');
1543                $line_id = request('line');
1544                $data = request('data');
1545                $tmpl->setContent($poseditor->SerialNum($action, $line_id, $data));
1546                break;
1547            case 'jrc':         // Сброс цен
1548                \acl::accessGuard('doc.' . $this->typename, \acl::UPDATE);
1549                if ($this->doc_data['firm_id'] > 0) {
1550                    \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::UPDATE);
1551                }
1552                $poseditor->resetPrices();
1553                break;
1554            case 'jorder':      // Сортировка наименований
1555                \acl::accessGuard('doc.' . $this->typename, \acl::UPDATE);
1556                if ($this->doc_data['firm_id'] > 0) {
1557                    \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::UPDATE);
1558                }
1559                $by = request('by');
1560                $poseditor->reOrder($by);
1561                break;
1562            default:
1563                return 0;
1564        }
1565        return 1;
1566    }
1567
1568    /// Получить многомерный массив с данными заголовка документа
1569    public function getDocumentHeader() {
1570        global $db, $CONFIG;
1571        $ret = array();
1572
1573        if ($this->doc_type == 0) {
1574            throw new Exception("Невозможно получить заголовок документа неизвестного типа");
1575        } else {
1576            // Динамические: баланс, бонусы, список договоров агента           
1577            $ret['id'] = $this->id;
1578            $ret['viewname'] = $this->viewname;
1579            $ret['type'] = $this->doc_type;
1580            $ret['typename'] = $this->typename;
1581            $ret['altnum'] = $this->doc_data['altnum'];
1582            $ret['subtype'] = $this->doc_data['subtype'];
1583            $ret['mark_del'] = $this->doc_data['mark_del'];
1584            $ret['firm_id'] = $this->doc_data['firm_id'];           
1585            $ret['comment'] = $this->doc_data['comment'];
1586            $ret['created'] = $this->doc_data['created'];
1587            $ret['ok'] = $this->doc_data['ok'];
1588            $ret['p_doc'] = $this->doc_data['p_doc'];
1589           
1590            $fields = explode(' ', $this->header_fields);
1591            $ret['header_fields'] = $fields;
1592           
1593            foreach ($fields as $f) {
1594                switch ($f) {
1595                    case 'agent': 
1596                        $ret['agent_id'] = $this->doc_data['agent'];
1597                        $ret['contract_id'] = $this->doc_data['contract'];
1598                        break;
1599                    case 'sklad':
1600                        $ret['store_id'] = $this->doc_data['sklad'];
1601                        break;
1602                    case 'kassa':
1603                        $ret['cash_id'] = $this->doc_data['kassa'];
1604                        break;
1605                    case 'bank':
1606                        $ret['bank_id'] = $this->doc_data['bank'];
1607                        break;
1608                    case 'cena':
1609                        $ret['price_id'] = $this->dop_data['cena'];
1610                        break;
1611                    case 'sum': 
1612                        $ret['sum'] = $this->doc_data['sum'];
1613                        break;
1614                }
1615            }
1616
1617            if (isset($CONFIG['site']['default_firm'])) {
1618                $ret['default_firm_id'] = $CONFIG['site']['default_firm'];
1619            }
1620            $ret['dop_buttons'] = $this->getDopButtons();
1621            $firm_ldo = new \Models\LDO\firmnames();
1622            $ret['firm_names'] = $firm_ldo->getData();
1623           
1624
1625            if ($this->doc_data['date']) {
1626                $ret['date'] = date("Y-m-d H:i:s", $this->doc_data['date']);
1627            } else {
1628                $ret['date'] = date("Y-m-d H:i:s");
1629            }
1630
1631            if (in_array('agent', $ret['header_fields'])) {
1632                $contract_list = array();
1633                $res = $db->query("SELECT `doc_list`.`id`, `doc_list`.`altnum`, `doc_list`.`subtype`, `doc_dopdata`.`value` AS `name`, `doc_list`.`date`
1634                    FROM `doc_list`
1635                    LEFT JOIN `doc_dopdata` ON `doc_dopdata`.`doc`=`doc_list`.`id` AND `doc_dopdata`.`param`='name'
1636                    WHERE `agent`='{$this->doc_data['agent']}' AND `type`='14' AND `firm_id`='{$this->doc_data['firm_id']}'");
1637                while ($line = $res->fetch_assoc()) {
1638                    $line['date'] = date("Y-m-d H:i:s", $line['date']);
1639                    $contract_list[] = $line;
1640                }
1641                $ret['agent_info'] = array(
1642                    'name' => $this->doc_data['agent_name'],
1643                    'balance' => agentCalcDebt($this->doc_data['agent']),
1644                    'bonus' => docCalcBonus($this->doc_data['agent']),
1645                    'contract_list' => $contract_list,
1646                    'dishonest' => $this->doc_data['agent_dishonest'],
1647                );
1648            }           
1649            $ret['ext_fields'] = $this->getExtControls();
1650            $ret = array_merge($this->dop_data, $this->text_data, $ret);
1651        }
1652        return $ret;
1653    }
1654   
1655    /// обновить заголовок документа данными из массива
1656    public function updateDocumentHeader($data) {
1657        $doc_data = array();
1658        $dop_data = array();
1659        if(isset($data['altnum'])) {
1660            $doc_data['altnum'] = $data['altnum'];
1661        }
1662        if(isset($data['subtype'])) {
1663            $doc_data['subtype'] = $data['subtype'];
1664        }
1665        if(isset($data['firm_id'])) {
1666            $doc_data['firm_id'] = $data['firm_id'];
1667        }
1668        if(isset($data['comment'])) {
1669            $doc_data['comment'] = $data['comment'];
1670        }
1671       
1672        $fields = explode(' ', $this->header_fields);
1673        foreach ($fields as $f) {
1674            switch ($f) {
1675                case 'agent': 
1676                    if (isset($data['agent_id'])) {
1677                        $doc_data['agent'] = $data['agent_id'];
1678                    }
1679                    if (isset($data['contract_id'])) {
1680                        $doc_data['contract'] = $data['contract_id'];
1681                    }
1682                    break;
1683                case 'sklad':
1684                    if (isset($data['store_id'])) {
1685                        $doc_data['sklad'] = $data['store_id'];
1686                    }
1687                    break;
1688                case 'kassa':
1689                    if (isset($data['cash_id'])) {
1690                        $doc_data['kassa'] = $data['cash_id'];
1691                    }
1692                    break;
1693                case 'bank':
1694                    if (isset($data['bank_id'])) {
1695                        $doc_data['bank'] = $data['bank_id'];
1696                    }
1697                    break;
1698                case 'cena':
1699                    if (isset($data['price_id'])) {
1700                        $dop_data['cena'] = $data['price_id'];
1701                    }
1702                    break;
1703                case 'sum':
1704                    if (isset($data['sum'])) {
1705                        $doc_data['sum'] = $data['sum'];
1706                    }
1707                    break;
1708            }
1709        }
1710        foreach($this->def_dop_data as $name => $value) {
1711            if (isset($data[$name])) {
1712                $dop_data[$name] = $data[$name];
1713            }
1714        }
1715        $extcontrols = $this->getExtControls();
1716        foreach ($extcontrols as $ex_name => $ex_data) {
1717            switch($ex_data['type']) {
1718                case 'text':
1719                case 'select':
1720                case 'status':
1721                    if (isset($data[$ex_name])) {
1722                        $dop_data[$ex_name] = $data[$ex_name];
1723                    }
1724                    break;
1725                case 'checkbox':
1726                    if (isset($data[$ex_name])) {
1727                        $dop_data[$ex_name] = $data[$ex_name]?1:0;
1728                    }
1729                    break;
1730            }
1731        }
1732        if(count($doc_data)>0) {
1733            $this->setDocDataA($doc_data);
1734        }
1735        if(count($dop_data)>0) {
1736            //throw new Exception(json_encode($dop_data));
1737            $this->setDopDataA($dop_data);
1738        }
1739    }
1740
1741    /// Слияние табличной части двух документов
1742    protected function mergeDocList($poseditor) {
1743        global $db;
1744        $from_doc = rcvint('from_doc');
1745        $clear = rcvint('clear');
1746        $no_sum = rcvint('no_sum');
1747
1748        try {
1749            if ($from_doc == 0) {
1750                throw new Exception("Документ не задан");
1751            }
1752            $db->startTransaction();
1753
1754            $res = $db->query("SELECT `id` FROM `doc_list` WHERE `id`=$from_doc");
1755            if (!$res->num_rows) {
1756                throw new Exception("Документ не найден");
1757            }
1758
1759            if ($clear) {
1760                $db->query("DELETE FROM `doc_list_pos` WHERE `doc`='{$this->id}'");
1761            }
1762
1763            $res = $db->query("SELECT `doc`, `tovar`, SUM(`cnt`) AS `cnt`, `gtd`, `comm`, `cost`, `page` FROM `doc_list_pos`"
1764                . "WHERE `doc`=$from_doc AND `page`=0 GROUP BY `tovar`");
1765            while ($line = $res->fetch_assoc()) {
1766                if (!$no_sum) {
1767                    $poseditor->simpleIncrementPos($line['tovar'], $line['cost'], $line['cnt'], $line['comm']);
1768                } else {
1769                    $poseditor->simpleRewritePos($line['tovar'], $line['cost'], $line['cnt'], $line['comm']);
1770                }
1771            }
1772            doc_log("REWRITE", "", 'doc', $this->id);
1773            $db->commit();
1774            $ret = array('response' => 'merge_ok');
1775        } catch (Exception $e) {
1776            $ret = array('response' => 'err', 'text' => $e->getMessage());
1777        }
1778        return $ret;
1779    }
1780
1781    /// Получить информацию о связях документа
1782    protected function getLinkInfo() {
1783        global $db;
1784        $childs = array();
1785        $parent = null;
1786        if ($this->doc_data['p_doc']) {
1787            $res = $db->query("SELECT `doc_list`.`id`, `doc_types`.`name`, `doc_list`.`altnum`, `doc_list`.`subtype`, `doc_list`.`date`,
1788                                    `doc_list`.`ok`, `doc_list`.`sum` FROM `doc_list`
1789                                    LEFT JOIN `doc_types` ON `doc_types`.`id`=`doc_list`.`type`
1790                                    WHERE `doc_list`.`id`='{$this->doc_data['p_doc']}'");
1791            $parent = $res->fetch_assoc();
1792            $parent['vdate'] = date("d.m.Y", $parent['date']);
1793        }
1794        $res = $db->query("SELECT `doc_list`.`id`, `doc_types`.`name`, `doc_list`.`altnum`, `doc_list`.`subtype`, `doc_list`.`date`,
1795                                `doc_list`.`ok`, `doc_list`.`sum` FROM `doc_list`
1796                                LEFT JOIN `doc_types` ON `doc_types`.`id`=`doc_list`.`type`
1797                                WHERE `doc_list`.`p_doc`='{$this->id}'");
1798
1799        while ($line = $res->fetch_assoc()) {
1800            $line['vdate'] = date("d.m.Y", $line['date']);
1801            $childs[] = $line;
1802        }
1803        $ret = array('response' => 'link_info', 'parent' => $parent, 'childs' => $childs);
1804        return $ret;
1805    }
1806
1807    protected function drawLHeadformStart() {
1808        $this->drawHeadformStart('j');
1809    }
1810
1811    /// Отобразить заголовок шапки документа
1812    protected function drawHeadformStart($alt = '') {
1813        global $tmpl, $CONFIG, $db;
1814        $pref = \pref::getInstance();
1815        if ($this->doc_data['date'])
1816            $dt = date("Y-m-d H:i:s", $this->doc_data['date']);
1817        else
1818            $dt = date("Y-m-d H:i:s");
1819        $tmpl->addContent("<form method='post' action='' id='doc_head_form'>
1820                <input type='hidden' name='mode' value='{$alt}heads'>
1821                <input type='hidden' name='type' value='" . $this->doc_type . "'>");
1822        if (isset($this->doc_data['id']))
1823            $tmpl->addContent("<input type='hidden' name='doc' value='" . $this->doc_data['id'] . "'>");
1824        if (@$this->doc_data['mark_del'])
1825            $tmpl->addContent("<h3>Документ помечен на удаление!</h3>");
1826        $tmpl->addContent("
1827                <table id='doc_head_main'>
1828                <tr><td class='altnum'>А. номер</td><td class='subtype'>Подтип</td><td class='datetime'>Дата и время</td><tr>
1829                <tr class='inputs'>
1830                <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>
1831                <td class='subtype'><input type='text' name='subtype' value='" . $this->doc_data['subtype'] . "' id='sudata'></td>
1832                <td class='datetime'><input type='text' name='datetime' value='$dt' id='datetime'></td>
1833                </tr>
1834                </table>
1835                Организация:<br><select name='firm' id='firm_id'>");
1836        $res = $db->query("SELECT `id`, `firm_name` FROM `doc_vars` ORDER BY `firm_name`");
1837        if (!$this->doc_data['firm_id'])
1838            $this->doc_data['firm_id'] = $pref->site_default_firm;
1839        while ($nx = $res->fetch_row()) {
1840            if ($this->doc_data['firm_id'] == $nx[0])
1841                $s = ' selected';
1842            else
1843                $s = '';
1844            $tmpl->addContent("<option value='$nx[0]' $s>$nx[1] / $nx[0]</option>");
1845        }
1846        $tmpl->addContent("</select><br>");
1847    }
1848
1849    protected function drawLHeadformEnd() {
1850        global $tmpl;
1851        $tmpl->addContent("<br>Комментарий:<br><textarea name='comment'>" . html_out($this->doc_data['comment']) . "</textarea></form>");
1852    }
1853
1854    protected function drawHeadformEnd() {
1855        global $tmpl;
1856        $tmpl->addContent(@"<br>Комментарий:<br><textarea name='comment'>" . html_out($this->doc_data['comment']) . "</textarea><br><input type=submit value='Записать'></form>");
1857    }
1858
1859    /// Сформировать поля выбора агента
1860    protected function drawAgentField() {
1861        global $tmpl, $db;
1862        $balance = agentCalcDebt($this->doc_data['agent']);
1863        $bonus = docCalcBonus($this->doc_data['agent']);
1864        $col = '';
1865        if ($balance > 0)
1866            $col = "color: #f00; font-weight: bold;";
1867        if ($balance < 0)
1868            $col = "color: #f08; font-weight: bold;";
1869
1870        $res = $db->query("SELECT `doc_list`.`id`, `doc_dopdata`.`value`
1871                FROM `doc_list`
1872                LEFT JOIN `doc_dopdata` ON `doc_dopdata`.`doc`=`doc_list`.`id` AND `doc_dopdata`.`param`='name'
1873                WHERE `agent`='{$this->doc_data['agent']}' AND `type`='14' AND `firm_id`='{$this->doc_data['firm_id']}'");
1874        $contr_content = '';
1875        while ($nxt = $res->fetch_row()) {
1876            $selected = ($this->doc_data['contract'] == $nxt[0]) ? 'selected' : '';
1877            $contr_content.="<option value='$nxt[0]' $selected>N$nxt[0]: $nxt[1]</option>";
1878        }
1879        if ($contr_content)
1880            $contr_content = "Договор:<br><select name='contract'>$contr_content</select>";
1881
1882        if ($this->doc_data['agent_dishonest'])
1883            $ag = "<span style='color: #f00; font-weight:bold;'>Был выбран недобросовестный агент!</span>";
1884        else
1885            $ag = '';
1886        $tmpl->addContent("
1887                <div>
1888                <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>
1889                Агент:
1890                <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>
1891                <a href='/docs.php?l=agent&mode=srv&opt=ep' target='_blank'><img src='/img/i_add.png'></a>
1892                </div>
1893                <input type='hidden' name='agent' id='agent_id' value='{$this->doc_data['agent']}'>
1894                <input type='text' id='agent_nm'  style='width: 100%;' value='" . html_out($this->doc_data['agent_name']) . "'>
1895                $ag
1896                <div id='agent_contract'>$contr_content</div>
1897                <br>
1898
1899                <script type=\"text/javascript\">
1900                $(document).ready(function(){
1901                        $(\"#agent_nm\").autocomplete(\"/docs.php\", {
1902                                delay:300,
1903                                minChars:1,
1904                                matchSubset:1,
1905                                autoFill:false,
1906                                selectFirst:true,
1907                                matchContains:1,
1908                                cacheLength:10,
1909                                maxItemsToShow:15,
1910                                formatSelectedItem: function(li) {
1911                                        if(li.querySelector('em').dataset.name)
1912                                                li.selectValue = li.querySelector('em').dataset.name;
1913                                        return li;
1914                                },
1915                                formatItem:agliFormat,
1916                                onItemSelect:agselectItem,
1917                                extraParams:{'l':'agent','mode':'srv','opt':'ac'}
1918                        });
1919                });
1920
1921                function agliFormat (row, i, num) {
1922                        var result =
1923                                row[0] +
1924                                \"<em class='qnt' data-name = '\"+row[4]+\"'> тел . \" + row[2] + \"</em>\";
1925                        return result;
1926                }
1927
1928                function agselectItem(li) {
1929                        if( li == null ) var sValue = \"Ничего не выбрано!\";
1930                        if( !!li.extra ) var sValue = li.extra[0];
1931                        else var sValue = li.selectValue;
1932                        document.getElementById('agent_id').value=sValue;
1933                        document.getElementById('ag_edit_link').href='/docs.php?l=agent&mode=srv&opt=ep&pos='+sValue;
1934                        var firm_id_elem = document.getElementById('firm_id');
1935                        var firm_id = 0;
1936                        if(firm_id_elem) {
1937                            firm_id = firm_id_elem.value;
1938                        }
1939                        UpdateContractInfo('{$this->id}',firm_id,sValue);
1940                       
1941                        ");
1942        if (!$this->id)
1943            $tmpl->addContent("
1944                        var plat_id=document.getElementById('plat_id');
1945                        if(plat_id)     plat_id.value=li.extra[0];
1946                        var plat=document.getElementById('plat');
1947                        if(plat)        plat.value=li.selectValue;
1948                        var gruzop_id=document.getElementById('gruzop_id');
1949                        if(gruzop_id)   gruzop_id.value=li.extra[0];
1950                        var gruzop=document.getElementById('gruzop');
1951                        if(gruzop)      gruzop.value=li.selectValue;");
1952        $tmpl->addContent("
1953                }
1954                </script>");
1955    }
1956
1957    protected function drawSkladField() {
1958        global $tmpl, $db;
1959        $tmpl->addContent("Склад:<br>
1960                <select name='sklad'>");
1961        $res = $db->query("SELECT `id`,`name` FROM `doc_sklady` ORDER BY `id`");
1962
1963        while ($nxt = $res->fetch_row()) {
1964            if ($nxt[0] == $this->doc_data['sklad'])
1965                $tmpl->addContent("<option value='$nxt[0]' selected>" . html_out($nxt[1]) . "</option>");
1966            else
1967                $tmpl->addContent("<option value='$nxt[0]'>" . html_out($nxt[1]) . "</option>");
1968        }
1969        $tmpl->addContent("</select><br>");
1970    }
1971
1972    protected function drawBankField() {
1973        global $tmpl, $CONFIG, $db;
1974        if ($this->doc_data['firm_id'])
1975            $sql_add = "AND ( `firm_id`='0' OR `num`='{$this->doc_data['bank']}' OR `firm_id`='{$this->doc_data['firm_id']}' )";
1976        else
1977            $sql_add = '';
1978        if ($this->doc_data['bank'])
1979            $bank = $this->doc_data['bank'];
1980        else {
1981            $pref = \pref::getInstance();
1982            $bank = $pref->getSitePref('default_bank_id');
1983        }
1984        $tmpl->addContent("Банк:<br><select name='bank'>");
1985        $res = $db->query("SELECT `num`, `name`, `rs` FROM `doc_kassa` WHERE `ids`='bank' $sql_add  ORDER BY `num`");
1986        while ($nxt = $res->fetch_row()) {
1987            if ($nxt[0] == $bank)
1988                $tmpl->addContent("<option value='$nxt[0]' selected>" . html_out($nxt[1] . ' / ' . $nxt[2]) . "</option>");
1989            else
1990                $tmpl->addContent("<option value='$nxt[0]'>" . html_out($nxt[1] . ' / ' . $nxt[2]) . "</option>");
1991        }
1992        $tmpl->addContent("</select><br>");
1993    }
1994
1995    protected function drawKassaField() {
1996        global $tmpl, $db, $CONFIG;
1997        if ($this->doc_data['kassa']) {
1998            $kassa = $this->doc_data['kassa'];
1999        } else {
2000            $pref = \pref::getInstance();
2001            $kassa = $pref->getSitePref('default_cash_id');
2002        }
2003        settype($kassa, 'int');
2004        $tmpl->addContent("Касса:<br><select name='kassa'>");
2005        $res = $db->query("SELECT `num`, `name` FROM `doc_kassa` WHERE `ids`='kassa' AND
2006                    (`firm_id`='0' OR `firm_id` IS NULL OR `firm_id`='{$this->doc_data['firm_id']}' OR `num`='$kassa') ORDER BY `num`");
2007
2008        if ($kassa == 0) {
2009            $tmpl->addContent("<option value='0'>--не выбрана--</option>");
2010        }
2011        while ($nxt = $res->fetch_row()) {
2012            if ($nxt[0] == $kassa) {
2013                $tmpl->addContent("<option value='$nxt[0]' selected>" . html_out($nxt[1]) . "</option>");
2014            } else {
2015                $tmpl->addContent("<option value='$nxt[0]'>" . html_out($nxt[1]) . "</option>");
2016            }
2017        }
2018        $tmpl->addContent("</select><br>");
2019    }
2020
2021    protected function drawSumField() {
2022        global $tmpl;
2023        $tmpl->addContent("Сумма:<br>
2024                <input type='text' name='sum' value='{$this->doc_data['sum']}'><img src='/img/i_+-.png'><br>");
2025    }
2026
2027    protected function drawPriceField() {
2028        global $tmpl, $db;
2029        $tmpl->addContent("Цена:<a onclick='ResetCost(\"{$this->id}\"); return false;' id='reset_cost'><img src='/img/i_reload.png'></a><br>
2030                <select name='cena'>");
2031        $s = '';
2032        if ($this->dop_data['cena'] == 0)
2033            $s = ' selected';
2034        $tmpl->addContent("<option value='0'{$s}>--авто--</option>");
2035        $res = $db->query("SELECT `id`,`name` FROM `doc_cost` ORDER BY `name`");
2036        while ($nxt = $res->fetch_row()) {
2037            if ($this->dop_data['cena'] == $nxt[0])
2038                $s = 'selected';
2039            else
2040                $s = '';
2041            $tmpl->addContent("<option value='$nxt[0]' $s>" . html_out($nxt[1]) . "</option>");
2042        }
2043
2044        if ($this->doc_data['nds'])
2045            $tmpl->addContent("<label><input type='radio' name='nds' value='0' disabled>Выделять НДС</label>&nbsp;&nbsp;
2046                        <label><input type='radio' name='nds' value='1' checked>Включать НДС</label><br>");
2047        else
2048            $tmpl->addContent("<label><input type='radio' name='nds' value='0' checked>Выделять НДС</label>&nbsp;&nbsp;
2049                        <label><input type='radio' name='nds' value='1'>Включать НДС</label><br>");
2050        $tmpl->addContent("<br>");
2051    }
2052
2053    // ====== Получение данных, связанных с документом =============================
2054    protected function get_docdata() {
2055        if (isset($this->doc_data)) {
2056            return;
2057        }
2058        global $db;
2059        if ($this->id) {
2060            $this->loadFromDb($this->id);
2061        } else {
2062            if (method_exists($this, 'initDefDopData')) {
2063                $this->initDefDopData();
2064            }
2065            $this->dop_data = $this->def_dop_data;
2066            $pref = \pref::getInstance();
2067
2068            $this->doc_data = array('id' => 0, 'type' => '', 'agent' => $pref->getSitePref('default_agent_id'), 'comment' => '', 'date' => time(), 'ok' => 0,
2069                'sklad' => $pref->getSitePref('default_store_id'), 'user' => 0, 'altnum' => 0, 'subtype' => '', 'sum' => 0, 'nds' => 1, 'p_doc' => 0, 'mark_del' => 0,
2070                'kassa' => 0, 'bank' => 0, 'firm_id' => 0, 'contract' => 0, 'created' => 0, 'agent_name' => '', 'agent_fullname' => '', 'agent_dishonest' => 0, 'agent_comment' => '');
2071
2072            if (!$this->doc_data['agent']) {
2073                $this->doc_data['agent'] = 1;
2074            }
2075            $agent_data = $db->selectRow('doc_agent', $this->doc_data['agent']);
2076            if (is_array($agent_data)) {
2077                $this->doc_data['agent_name'] = $agent_data['name'];
2078            }
2079
2080            if (!$this->doc_data['sklad']) {
2081                $this->doc_data['sklad'] = 1;
2082            }
2083        }
2084    }
2085
2086    /// Проверка уникальности альтернативного порядкового номера документа
2087    public function isAltNumUnique() {
2088        global $db;
2089        $start_date = strtotime(date("Y-01-01 00:00:00", $this->doc_data['date']));
2090        $end_date = strtotime(date("Y-12-31 23:59:59", $this->doc_data['date']));
2091        $subtype_sql = $db->real_escape_string($this->doc_data['subtype']);
2092        $res = $db->query("SELECT `altnum` FROM `doc_list`"
2093            . " WHERE `type`='{$this->doc_type}' AND `altnum`='{$this->doc_data['altnum']}' AND `subtype`='$subtype_sql'"
2094            . " AND `id`!='{$this->id}' AND `date`>='$start_date' AND `date`<='$end_date' AND `firm_id`='{$this->doc_data['firm_id']}'");
2095        return $res->num_rows ? false : true;
2096    }
2097
2098    /// Получение альтернативного порядкового номера документа
2099    public function getNextAltNum($doc_type, $subtype, $date, $firm_id) {
2100        global $CONFIG, $db;
2101        if (!$doc_type) {
2102            $doc_type = $this->doc_type;
2103        }
2104        $start_date = strtotime(date("Y-01-01 00:00:00", strtotime($date)));
2105        $end_date = strtotime(date("Y-12-31 23:59:59", strtotime($date)));
2106        $res = $db->query("SELECT `altnum` FROM `doc_list` WHERE `type`='$doc_type' AND `subtype`='$subtype'"
2107            . " AND `id`!='{$this->id}' AND `date`>='$start_date' AND `date`<='$end_date' AND `firm_id`='$firm_id'"
2108            . " ORDER BY `altnum` ASC");
2109        $newnum = 0;
2110        while ($nxt = $res->fetch_row()) {
2111            if (($nxt[0] - 1 > $newnum) && @$CONFIG['doc']['use_persist_altnum'])
2112                break;
2113            $newnum = $nxt[0];
2114        }
2115        $newnum++;
2116        return $newnum;
2117    }
2118
2119    /// Кнопки меню - провети / отменить
2120    protected function getDopButtons() {
2121        global $tmpl;
2122        $ret = '';
2123        if ($this->id) {
2124            $ret.="<a href='/doc.php?mode=log&amp;doc={$this->id}' title='История изменений документа'><img src='img/i_log.png' alt='История'></a>";
2125            $ret.="<span id='provodki'>";
2126            if ($this->doc_data['ok']) {
2127                $ret .= $this->getCancelButtons();
2128            } else {
2129                $ret .= $this->getApplyButtons();
2130            }
2131
2132            $ret .= "</span>
2133                <img src='/img/i_separator.png' alt=''>
2134                <a href='#' onclick=\"return PrintMenu(event, '{$this->id}')\" title='Печать'>
2135                    <img src='img/i_print.png' alt='Печать'></a>
2136                <a href='#' onclick=\"return FaxMenu(event, '{$this->id}')\" title='Отправить по факсу'>
2137                    <img src='img/i_fax.png' alt='Факс'></a>
2138                <a href='#' onclick=\"return MailMenu(event, '{$this->id}')\" title='Отправить по email'>
2139                    <img src='img/i_mailsend.png' alt='email'></a>
2140                <img src='/img/i_separator.png' alt=''>
2141                <a href='#' onclick=\"DocConnect('{$this->id}', '{$this->doc_data['p_doc']}'); return false;\" title='Связать документ'>
2142                    <img src='img/i_conn.png' alt='Связать'></a>
2143                <a href='#' onclick=\"return ShowContextMenu(event, '/doc.php?mode=morphto&amp;doc={$this->id}')\"
2144                    title='Создать связанный документ'><img src='img/i_to_new.png' alt='Связь'></a>";
2145            if ($this->sklad_editor_enable) {
2146                $ret .= " <a href='#' onclick=\"return addNomMenu(event, '{$this->id}', '{$this->doc_data['p_doc']}');\" title='Обновить номенклатурную таблицу'><img src='img/i_addnom.png' alt='Обновить номенклатурную таблицу'></a>";
2147            }
2148            $ret.="<img src='/img/i_separator.png' alt=''>";
2149        }
2150
2151        if (method_exists($this, 'getAdditionalButtonsHTML')) {
2152            $ret .= $this->getAdditionalButtonsHTML();
2153        }
2154        return $ret;
2155    }
2156
2157    protected function getApplyButtons() {
2158        if ($this->doc_data['mark_del']) {
2159            return "<a href='#' title='Отменить удаление' onclick='unMarkDelDoc({$this->id}); return false;'><img src='img/i_trash_undo.png' alt='отменить удаление'></a>";
2160        } else {
2161            return "<a href='#' title='Пометить на удаление' onclick='MarkDelDoc({$this->id}); return false;'><img src='img/i_trash.png' alt='Пометить на удаление'></a>" .
2162                "<a href='#' title='Провести документ' onclick='ApplyDoc({$this->id}); return false;'><img src='img/i_ok.png' alt='Провести'></a>";
2163        }
2164        //<a href='?mode=ehead&amp;doc={$this->doc}' title='Правка заголовка'><img src='img/i_docedit.png' alt='Правка'></a>
2165    }
2166
2167    protected function getCancelButtons() {
2168        return "<a title='Отменить проводку' onclick='CancelDoc({$this->id}); return false;'><img src='img/i_revert.png' alt='Отменить' /></a>";
2169    }
2170
2171    /// Вычисление, можно ли отменить кассовый документ
2172    protected function checkKassMinus() {
2173        global $db;
2174        $sum = $i = 0;
2175        $res = $db->query("SELECT `doc_list`.`id`, `doc_list`.`type`, `doc_list`.`sum`, `doc_list`.`kassa` FROM `doc_list`
2176                WHERE  `doc_list`.`ok`>'0' AND ( `doc_list`.`type`='6' OR `doc_list`.`type`='7' OR `doc_list`.`type`='9')
2177                ORDER BY `doc_list`.`date`");
2178        while ($nxt = $res->fetch_row()) {
2179            if ($nxt[3] == $this->doc_data['kassa']) {
2180                if ($nxt[1] == 6)
2181                    $sum += $nxt[2];
2182                else if ($nxt[1] == 7 || $nxt[1] == 9)
2183                    $sum -= $nxt[2];
2184            }
2185            else if ($nxt[1] == 9) {
2186                $rr = $db->query("SELECT `value` FROM `doc_dopdata` WHERE `doc`='$nxt[0]' AND `param`='v_kassu'");
2187                if (!$rr->num_rows)
2188                    throw new AutoLoggedException('Касса назначения не найдена в документе ' . $this->id);
2189                $data = $rr->fetch_row();
2190                if ($data[0] == $this->doc_data['kassa'])
2191                    $sum+=$nxt[2];
2192            }
2193
2194            $sum = sprintf("%01.2f", $sum);
2195            if ($sum < 0)
2196                break;
2197            $i++;
2198        }
2199        $res->free();
2200        return $sum;
2201    }
2202
2203    /// Показать историю изменений документа
2204    public function showLog() {
2205        global $tmpl;
2206        \acl::accessGuard('doc.' . $this->typename, \acl::VIEW);
2207        if ($this->doc_data['firm_id'] > 0) {
2208            \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::VIEW);
2209        }
2210        $tmpl->setTitle($this->viewname . ' N' . $this->id);
2211        doc_menu($this->getDopButtons());
2212        $tmpl->addContent("<h1>{$this->viewname} N{$this->id} - история документа</h1>");
2213
2214        $logview = new \LogView();
2215        $logview->setObject('doc');
2216        $logview->setObjectId($this->id);
2217        $logview->showLog();
2218    }
2219
2220    /// Получить список номенклатуры
2221    function getDocumentNomenclature($options = '') {
2222        global $CONFIG, $db;
2223        $opts = array();
2224        $e_options = explode(',', $options);
2225        foreach ($e_options as $opt) {
2226            $opts[$opt] = 1;
2227        }
2228        $fields_sql = $join_sql = '';
2229        if (isset($opts['country'])) {
2230            $fields_sql .= ", `class_country`.`name` AS `country_name`, `class_country`.`number_code` AS `country_code`";
2231            $join_sql .= " LEFT JOIN `class_country` ON `class_country`.`id`=`doc_base`.`country`";
2232        }
2233        if (isset($opts['comment'])) {
2234            $fields_sql .= ", `doc_list_pos`.`comm` AS `comment`";
2235        }
2236        if (isset($opts['base_desc'])) {
2237            $fields_sql .= ", `doc_base`.`desc` AS `base_desc`";
2238        }
2239        if (isset($opts['vat'])) {
2240            $fields_sql .= ", `doc_base`.`nds` AS `vat`";
2241        }
2242        if (isset($opts['base_price'])) {
2243            $fields_sql .= ", `doc_base`.`cost` AS `base_price`";
2244        }
2245        if (isset($opts['bulkcnt'])) {
2246            $fields_sql .= ", `doc_base`.`bulkcnt`";
2247        }
2248        if (isset($opts['dest_place'])) {
2249            $to_sklad = (int) $this->dop_data['na_sklad'];
2250            $fields_sql .= ", `pt_d`.`mesto` AS `dest_place`";
2251            $join_sql .= " LEFT JOIN `doc_base_cnt` AS `pt_d` ON `pt_d`.`id`=`doc_list_pos`.`tovar` AND `pt_d`.`sklad`='{$to_sklad}'";
2252        }
2253        if (isset($opts['bigpack'])) {
2254            // ID параметра большой упаковки
2255            $res = $db->query("SELECT `id` FROM `doc_base_params` WHERE `codename`='bigpack_cnt'");
2256            if (!$res->num_rows) {
2257                $db->query("INSERT INTO `doc_base_params` (`name`, `codename`, `type`, `hidden`)"
2258                    . " VALUES ('Кол-во в большой упаковке', 'bigpack_cnt', 'int', 0)");
2259                throw new \Exception("Параметр *bigpack_cnt - кол-во в большой упаковке* не найден. Параметр создан.");
2260            }
2261            list($p_bp_id) = $res->fetch_row();
2262            $fields_sql .= ", `bp_t`.`value` AS `bigpack_cnt`";
2263            $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'";
2264        }
2265        if (isset($opts['rto'])) {
2266            $fields_sql .= ", `doc_base_dop`.`transit`, `doc_base_dop`.`reserve`, `doc_base_dop`.`offer`";
2267            $join_sql .= " LEFT JOIN `doc_base_dop` ON `doc_base_dop`.`id`=`doc_list_pos`.`tovar`";
2268        }
2269        $list = array();
2270        $res = $db->query("SELECT
2271                `doc_list_pos`.`tovar` AS `pos_id`, `doc_list_pos`.`cnt`, `doc_list_pos`.`cost` AS `price`,
2272                `doc_base`.`vc`, `doc_base`.`name`, `doc_base`.`proizv` AS `vendor`, `doc_base`.`mass`, `doc_base`.`mult`,
2273                `doc_group`.`printname` AS `group_printname`, `doc_group`.`id` AS `group_id`,
2274                `doc_base_cnt`.`mesto` AS `place`, `doc_base_cnt`.`cnt` AS `base_cnt`,
2275                `class_unit`.`rus_name1` AS `unit_name`, `class_unit`.`number_code` AS `unit_code`
2276                $fields_sql
2277            FROM `doc_list_pos`
2278            INNER JOIN `doc_base` ON `doc_list_pos`.`tovar`=`doc_base`.`id`
2279            LEFT JOIN `doc_group` ON `doc_group`.`id`=`doc_base`.`group`
2280            LEFT JOIN `doc_base_cnt` ON `doc_base_cnt`.`id`=`doc_list_pos`.`tovar` AND `doc_base_cnt`.`sklad`='{$this->doc_data['sklad']}'
2281            LEFT JOIN `class_unit` ON `doc_base`.`unit`=`class_unit`.`id`
2282            $join_sql
2283            WHERE `doc_list_pos`.`doc`='{$this->id}'
2284            ORDER BY `doc_list_pos`.`id`");
2285
2286        while ($line = $res->fetch_assoc()) {
2287            if ($line['group_printname']) {
2288                $line['name'] = $line['group_printname'] . ' ' . $line['name'];
2289            }
2290            if (!@$CONFIG['doc']['no_print_vendor'] && $line['vendor']) {
2291                $line['name'] .= ' / ' . $line['vendor'];
2292            }
2293            $line['code'] = $line['pos_id'];
2294            if ($line['vc']) {
2295                $line['code'] .= ' / ' . $line['vc'];
2296            }
2297            $line['sum'] = $line['price'] * $line['cnt'];
2298
2299            if (isset($opts['vat'])) {
2300                if($this->firm_vars['param_nds']) {
2301                    if($line['vat']===null) {
2302                        $line['vat'] = 0;
2303                    }                       
2304                    $ndsp = $line['vat'];
2305                    $vat = $ndsp / 100;
2306                }
2307                else {
2308                    $ndsp = $vat = 0;
2309                }
2310
2311                /*
2312                  $line['price_wo_vat'] = round($line['price'] / (1 + ($line['vat_p'] / 100)), 2);
2313                  $line['sum_wo_vat'] = $line['price_wo_vat'] * $line['cnt'];
2314                  $line['vat_s'] = ($line['price'] * $line['cnt']) - $line['sum_wo_vat']; */
2315                $pos = $this->calcVAT($line['price'], $line['cnt'], $vat);
2316                $line['price_wo_vat'] = $pos['price'];
2317                $line['sum_wo_vat'] = round($pos['sum_wo_vat'], 2);
2318                $line['vat_p'] = $ndsp;
2319                $line['vat_s'] = round($pos['vat_s'], 2);
2320                $line['sum'] = round($pos['sum'], 2);
2321               
2322            }
2323
2324
2325            $list[] = $line;
2326        }
2327        $res->free();
2328        return $list;
2329    }
2330
2331    /// Получить список номенклатуры документа с НДС и НТД
2332    public function getDocumentNomenclatureWVATandNums() {
2333        global $CONFIG, $db;
2334
2335        $list = array();
2336        $res = $db->query("SELECT `doc_group`.`printname` AS `group_printname`, `doc_base`.`name`, `doc_base`.`proizv` AS `vendor`, `doc_list_pos`.`cnt`,
2337            `doc_list_pos`.`cost`, `doc_list_pos`.`gtd`, `class_country`.`name` AS `country_name`, `doc_base_dop`.`ntd`,
2338            `class_unit`.`rus_name1` AS `unit_name`, `doc_list_pos`.`tovar` AS `pos_id`, `class_unit`.`number_code` AS `unit_code`,
2339            `class_country`.`number_code` AS `country_code`, `doc_base`.`vc`, `doc_base`.`mass`, `doc_base`.`nds` AS `vat`, `doc_list_pos`.`comm`,
2340            `doc_list_pos`.`id` AS `line_id`
2341        FROM `doc_list_pos`
2342        LEFT JOIN `doc_base` ON `doc_base`.`id`=`doc_list_pos`.`tovar`
2343        LEFT JOIN `doc_base_dop` ON `doc_base_dop`.`id`=`doc_list_pos`.`tovar`
2344        LEFT JOIN `doc_group` ON `doc_group`.`id`=`doc_base`.`group`
2345        LEFT JOIN `class_unit` ON `doc_base`.`unit`=`class_unit`.`id`
2346        LEFT JOIN `class_country` ON `class_country`.`id`=`doc_base`.`country`
2347        WHERE `doc_list_pos`.`doc`='{$this->id}'
2348        ORDER BY `doc_list_pos`.`id`");
2349
2350        while ($nxt = $res->fetch_assoc()) {
2351            if($this->firm_vars['param_nds']) {
2352                if($nxt['vat']===null) {
2353                    $nxt['vat'] = 0;
2354                }                       
2355                $ndsp = $nxt['vat'];
2356                $vat = $ndsp / 100;
2357            }
2358            else {
2359                $ndsp = $vat = 0;
2360            }
2361
2362            if (!$nxt['country_code']) {
2363                //throw new \Exception("Не возможно формирование списка номенклатуры без указания страны происхождения товара");
2364                $nxt['country_code'] = 0;
2365            }
2366
2367            $pos_name = $nxt['name'];
2368            if ($nxt['group_printname']) {
2369                $pos_name = $nxt['group_printname'] . ' ' . $pos_name;
2370            }
2371            if (!@$CONFIG['doc']['no_print_vendor'] && $nxt['vendor']) {
2372                $pos_name .= ' / ' . $nxt['vendor'];
2373            }
2374            $pos_code = $nxt['pos_id'];
2375            if ($nxt['vc']) {
2376                $pos_code .= ' / ' . $nxt['vc'];
2377            }
2378
2379            if (@$CONFIG['poseditor']['true_gtd']) {
2380                $gtd_array = array();
2381                $gres = $db->query("SELECT `doc_list`.`type`, `doc_list_pos`.`gtd`, `doc_list_pos`.`cnt`, `doc_list`.`id` FROM `doc_list_pos`
2382                    INNER JOIN `doc_list` ON `doc_list`.`id`=`doc_list_pos`.`doc`
2383                    WHERE `doc_list_pos`.`tovar`='{$nxt['pos_id']}' AND `doc_list`.`firm_id`='{$this->doc_data['firm_id']}' AND `doc_list`.`type`<='2'
2384                    AND `doc_list`.`date`<'{$this->doc_data['date']}' AND `doc_list`.`ok`>'0'
2385                    ORDER BY `doc_list`.`date`");
2386                while ($line = $gres->fetch_assoc()) {
2387                    if ($line['type'] == 1) { // Поступление
2388                        $gtd_array[] = array('num' => $line['gtd'], 'cnt' => $line['cnt']);
2389                    } else {
2390                        $cnt = $line['cnt'];
2391                        while ($cnt > 0) {
2392                            if (count($gtd_array) == 0) {
2393                                if (\cfg::get('poseditor', 'true_gtd') != 'easy') {
2394                                    throw new \Exception("Не найдены поступления для $cnt единиц товара {$nxt['name']} (для реализации N{$line['id']} в прошлом). Товар был оприходован на другую организацию?");
2395                                } else {
2396                                    $gtd_array[] = array('num' => $line['gtd'], 'cnt' => $cnt);
2397                                }
2398                            }
2399                            if ($gtd_array[0]['cnt'] == $cnt) {
2400                                array_shift($gtd_array);
2401                                $cnt = 0;
2402                            } elseif ($gtd_array[0]['cnt'] > $cnt) {
2403                                $gtd_array[0]['cnt'] -= $cnt;
2404                                $cnt = 0;
2405                            } else {
2406                                $cnt -= $gtd_array[0]['cnt'];
2407                                array_shift($gtd_array);
2408                            }
2409                        }
2410                    }
2411                }
2412
2413                $unigtd = array();
2414                $need_cnt = $nxt['cnt'];
2415                while ($need_cnt > 0 && count($gtd_array) > 0) {
2416                    $gtd_num = $gtd_array[0]['num'];
2417                    $gtd_cnt = $gtd_array[0]['cnt'];
2418                    if ($gtd_cnt >= $need_cnt) {
2419                        if (isset($unigtd[$gtd_num])) {
2420                            $unigtd[$gtd_num] += $need_cnt;
2421                        } else {
2422                            $unigtd[$gtd_num] = $need_cnt;
2423                        }
2424                        $need_cnt = 0;
2425                    } else {
2426                        if (isset($unigtd[$gtd_num])) {
2427                            $unigtd[$gtd_num] += $gtd_cnt;
2428                        } else {
2429                            $unigtd[$gtd_num] = $gtd_cnt;
2430                        }
2431                        $need_cnt -= $gtd_cnt;
2432                        array_shift($gtd_array);
2433                    }
2434                }
2435                if ($need_cnt > 0) {
2436                    if (\cfg::get('poseditor', 'true_gtd') != 'easy') {
2437                        throw new Exception("Не найдены поступления для $need_cnt единиц товара {$pos_name}. Товар был оприходован на другую организацию?");
2438                    } else {
2439                        $unigtd['   --   '] = $need_cnt;
2440                    }
2441                }
2442                foreach ($unigtd as $gtd => $cnt) {
2443                    $pos = $this->calcVAT($nxt['cost'], $cnt, $vat);
2444                    $list[] = array(
2445                        'line_id' => $nxt['line_id'],
2446                        'pos_id' => $nxt['pos_id'],
2447                        'code' => $pos_code,
2448                        'name' => $pos_name,
2449                        'unit_code' => $nxt['unit_code'],
2450                        'unit_name' => $nxt['unit_name'],
2451                        'cnt' => $cnt,
2452                        'price' => $pos['price'],
2453                        'orig_price' => $nxt['cost'],
2454                        'sum_wo_vat' => round($pos['sum_wo_vat'], 2),
2455                        'excise' => 'без акциза',
2456                        'vat_p' => $ndsp,
2457                        'vat_s' => round($pos['vat_s'], 2),
2458                        'sum' => round($pos['sum'], 2),
2459                        'country_code' => $nxt['country_code'],
2460                        'country_name' => $nxt['country_name'],
2461                        'gtd' => $gtd,
2462                        'mass' => $nxt['mass'],
2463                        'comm' => $nxt['comm'],
2464                    );
2465                }
2466            } else {
2467                $pos = $this->calcVAT($nxt['cost'], $nxt['cnt'], $vat);
2468                $list[] = array(
2469                    'line_id' => $nxt['line_id'],
2470                    'pos_id' => $nxt['pos_id'],
2471                    'code' => $pos_code,
2472                    'name' => $pos_name,
2473                    'unit_code' => $nxt['unit_code'],
2474                    'unit_name' => $nxt['unit_name'],
2475                    'cnt' => $nxt['cnt'],
2476                    'price' => $pos['price'],
2477                    'orig_price' => $nxt['cost'],
2478                    'sum_wo_vat' => round($pos['sum_wo_vat'], 2),
2479                    'excise' => 'без акциза',
2480                    'vat_p' => $ndsp,
2481                    'vat_s' => round($pos['vat_s'], 2),
2482                    'sum' => round($pos['sum'], 2),
2483                    'country_code' => $nxt['country_code'],
2484                    'country_name' => $nxt['country_name'],
2485                    'gtd' => $nxt['ntd'],
2486                    'mass' => $nxt['mass'],
2487                    'comm' => $nxt['comm'],
2488                );
2489            }
2490        }
2491        return $list;
2492    }
2493
2494    /// Расчет НДС для строки документа
2495    /// @param $doc_price Цена единицы товара в документе
2496    /// @param $count Количество товара
2497    /// @param $vat Ставка НДС
2498    protected function calcVAT($doc_price, $count, $vat) {
2499        global $CONFIG;
2500        if (isset($CONFIG['poseditor']['vat_scheme'])) {
2501            $scheme = $CONFIG['poseditor']['vat_scheme'];
2502        } else {
2503            $scheme = 'correct';
2504        }
2505        if ($this->doc_data['nds']) {   // НДС включен
2506            $pos['sum'] = $doc_price * $count;
2507            if ($scheme == '1c') {
2508                $pos['sum_wo_vat'] = round($pos['sum'] / (1 + $vat), 2);
2509                $pos['vat_s'] = $pos['sum'] - $pos['sum_wo_vat'];
2510                $pos['price'] = round($pos['sum_wo_vat'] / $count, 2);
2511            } else {
2512                $pos['price'] = round($doc_price / (1 + $vat), 2);
2513                $pos['sum_wo_vat'] = round($pos['price'] * $count, 2);
2514                $pos['vat_s'] = round($doc_price * $count, 2) - $pos['sum_wo_vat'];
2515            }
2516        } else {
2517            $pos['price'] = $pos['price_w_vat'] = $doc_price;
2518            $pos['sum_wo_vat'] = round($pos['price'] * $count, 2);
2519            $pos['vat_s'] = round($pos['sum_wo_vat'] * $vat, 2);
2520            $pos['sum'] = $pos['sum_wo_vat'] + $pos['vat_s'];
2521        }
2522        return $pos;
2523    }
2524
2525    /// Установить пометку на удаление у документа
2526    protected function serviceDelDoc() {
2527        global $db;
2528        try {
2529            \acl::accessGuard('doc.' . $this->typename, \acl::DELETE);
2530            if ($this->doc_data['firm_id'] > 0) {
2531                \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::DELETE);
2532            }
2533            $tim = time();
2534
2535            $res = $db->query("SELECT `id` FROM `doc_list` WHERE `p_doc`='{$this->id}' AND `mark_del`='0'");
2536            if ($res->num_rows) {
2537                throw new Exception("Есть подчинённые не удалённые документы. Удаление невозможно.");
2538            }
2539            $db->update('doc_list', $this->id, 'mark_del', $tim);
2540            doc_log("MARKDELETE", '', "doc", $this->id);
2541            $this->doc_data['mark_del'] = $tim;
2542            $json = ' { "response": "1", "message": "Пометка на удаление установлена!", "buttons": "' . $this->getApplyButtons() . '", '
2543                . '"statusblock": "Документ помечен на удаление" }';
2544            return $json;
2545        } catch (Exception $e) {
2546            return "{response: 0, message: '" . $e->getMessage() . "'}";
2547        }
2548    }
2549
2550    /// Снять пометку на удаление у документа
2551    protected function serviceUnDelDoc() {
2552        global $db;
2553        try {
2554            \acl::accessGuard('doc.' . $this->typename, \acl::DELETE);
2555            if ($this->doc_data['firm_id'] > 0) {
2556                \acl::accessGuard([ 'firm.global', 'firm.' . $this->doc_data['firm_id']], \acl::DELETE);
2557            }
2558            $db->update('doc_list', $this->id, 'mark_del', 0);
2559            doc_log("UNMARKDELETE", '', "doc", $this->id);
2560            $json = ' { "response": "1", "message": "Пометка на удаление снята!", "buttons": "' . $this->getApplyButtons() . '", '
2561                . '"statusblock": "Документ не будет удалён" }';
2562            return $json;
2563        } catch (Exception $e) {
2564            return "{response: 0, message: '" . $e->getMessage() . "'}";
2565        }
2566    }
2567
2568    /// Экспорт табличной части документа в CSV
2569    function CSVExport($to_str = 0) {
2570        global $tmpl;
2571        $header = "PosNum;ID;VC;Name;Vendor;Cnt;Price;Sum;Comment\r\n";
2572        if (!$to_str) {
2573            $tmpl->ajax = 1;
2574            header("Content-type: 'application/octet-stream'");
2575            header("Content-Disposition: 'attachment'; filename=predlojenie.csv;");
2576            echo $header;
2577        } else {
2578            $str_out = $header;
2579        }
2580        $nomenclature = $this->getDocumentNomenclature('base_desc');
2581
2582        $i = 0;
2583        foreach ($nomenclature as $line) {
2584            $i++;
2585            $str_line = "$i;{$line['pos_id']};\"{$line['vc']}\";\"{$line['name']}\";\"{$line['vendor']}\";{$line['cnt']};{$line['price']};{$line['sum']}\r\n";
2586            if (!$to_str) {
2587                echo $str_line;
2588            } else {
2589                $str_out.=$str_line;
2590            }
2591        }
2592        if ($to_str) {
2593            return $str_out;
2594        }
2595    }
2596
2597    /// @brief Создание другого документа на основе текущего
2598    /// Метод необходимо переопределить у потомков
2599    /// @param $target_type Тип создаваемого документа
2600    /// @return Всегда false
2601        /// Формирование другого документа на основании текущего
2602    function morphTo($target) {
2603        global $tmpl, $db;
2604        $morphs = $this->getMorphList();
2605       
2606        if ($target == '') {
2607            $tmpl->ajax = 1;           
2608            $base_link = "window.location='/doc.php?mode=morphto&amp;doc={$this->id}&amp;tt=";
2609            foreach($morphs as $line) {
2610                $acl_obj = 'doc.'.$line['document'];
2611                if(\acl::testAccess($acl_obj, \acl::CREATE)) {
2612                    $tmpl->addContent("<div onclick=\"{$base_link}{$line['name']}'\">{$line['viewname']}</div>");
2613                }
2614            }
2615        } else {
2616            $morphs = $this->getMorphList();
2617            $info = null;
2618            foreach($morphs as $m_info) {
2619                if($m_info['name']===$target) {
2620                    $info = $m_info;
2621                    break;
2622                }
2623            }
2624            if(!$info) {
2625                throw new \Exception("Неверный код целевого документа.");
2626            }
2627           
2628            \acl::accessGuard('doc.'.$morphs[$target]['document'], \acl::CREATE);
2629            $method = 'morphTo_'.$info['name'];
2630            if(!method_exists($this, $method)) {
2631                throw new \NotFoundException("Метод морфинга не определён.");
2632            } 
2633            $db->startTransaction();
2634            $new_doc = $this->$method($target);
2635            $new_doc_id = $new_doc->getId();
2636            $db->commit();
2637            redirect("/doc.php?mode=body&doc=$new_doc_id");
2638        }
2639    }
2640
2641    /**
2642     * Проверка для приходных/расходных кассовых ордеров
2643     * и средств из/в банк при проведении документа
2644     * @throws Exception При отсутствии
2645     */
2646    protected function checkIfTypeForDocumentExists() {
2647        $allowedTypes = [
2648            4 => 'credit_type',
2649            5 => 'rasxodi',
2650            6 => 'credit_type',
2651            7 => 'rasxodi',
2652        ];
2653        if (!isset($allowedTypes[$this->doc_type])) {
2654            throw new \Exception('Для данного типа документа проверка не разрешена');
2655        }
2656        if (cfg::get('doc', 'restrict_dc_nulltype', true) && isset($this->dop_data[$allowedTypes[$this->doc_type]]) && $this->dop_data[$allowedTypes[$this->doc_type]] == 0) {
2657            $type = $this->doc_type % 2 === 1 ? 'расхода' : 'дохода';
2658            throw new \Exception("Не задан вид $type у проводимого документа.");
2659        }
2660    }
2661
2662}
Note: See TracBrowser for help on using the repository browser.